Skip to content

Add streaming response support: client, server, and concurrent handlers - #641

Merged
logbie merged 132 commits into
mainfrom
claude/wfl-runtime-streaming-c3pvf9
Jul 25, 2026
Merged

Add streaming response support: client, server, and concurrent handlers#641
logbie merged 132 commits into
mainfrom
claude/wfl-runtime-streaming-c3pvf9

Conversation

@logbie

@logbie logbie commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

This change implements the complete streaming response feature set requested downstream: outbound response streaming with incremental reads, streamed server responses, and concurrent request handlers. It ships items 1–4 of the five-capability request and the lifecycle guarantees (item 5) for both client and server streaming.

Key Changes

1. Outbound Response Streaming (Client-Side)

  • New AST statement: HttpStreamStatement for open url ... and stream response as
  • New statements: WaitForNextChunkStatement and WaitForNextLineStatement to pull body incrementally
  • New IoClient methods:
    • open_http_stream(): Opens a streaming request, returns status/headers immediately without buffering the body
    • stream_pull(), next_chunk(), next_line(): Pull body chunks/lines with per-chunk timeout and response-byte ceiling enforcement
    • take_stream() / put_stream(): Manage live stream handles across async boundaries
  • New HttpStreamHandle struct: Wraps a pinned futures stream, buffered bytes, and read tracking
  • Handle object: Exposes status, ok, headers, and internal _stream id to WFL code

2. Streamed Server Responses (Server-Side)

  • New AST statements:
    • StartStreamingResponseStatement: start streaming response to [with status/content type/headers] as
    • StreamWriteStatement: write line|chunk to (frames or raw bytes)
    • FlushStreamStatement: flush (advisory)
  • New HandlerReply enum: Replaces WflHttpResponse as the per-request oneshot payload
    • Buffered(WflHttpResponse): Existing fully-buffered response
    • Streaming { status, content_type, headers, body: mpsc::Receiver<...> }: Head sent immediately, body fed chunk-by-chunk over a bounded channel
  • Bounded channel (RESPONSE_STREAM_BUFFER = 64): Provides backpressure so a slow client slows the handler's writes rather than queuing unbounded chunks
  • New Interpreter fields:
    • server_response_streams: Map of open response streams by handle id
    • next_response_stream_id: Counter for handle generation
  • Warp integration: Transport task reads from the bounded channel and streams chunks to the client; dropping the sender (handler end, close, or error) closes the body

3. Concurrent Request Handlers

  • New AST field: MainLoop.concurrent: bool (default false)
  • Parser: main loop concurrently: sets concurrent = true; plain main loop stays serial (byte-compatible)
  • Interpreter: Concurrent iterations run cooperatively on a single thread — a FuturesUnordered of !Send, &self-borrowing handler futures (catch_unwind-wrapped), not tokio::spawn — up to CONCURRENT_HANDLER_LIMIT = 256. Per-handler run state (count-loop counters, call stack/depth, open response streams and pending requests) is swapped in and out of the shared interpreter per poll, so concurrent handlers don't clobber each other's execution bookkeeping across await points.
    • Interleave at await points — cooperative, I/O-bound concurrency, not parallelism. CPU-bound work with no await still holds the single thread until it yields.
    • Isolation boundary: each iteration runs in its own child scope, so per-request local variables (a store inside the handler) do not clobber another request's. Top-level (global) bindings and any collections shared through them remain shared by design — mutations to parent globals or shared objects are visible across handlers.
    • Transport already bounds the request queue; this bounds concurrent handler execution.
  • Admission control: Requests beyond the limit wait for handlers to free up; transport sheds 503 if its queue also fills

4. Lifecycle & Backpressure (Items 5, Client + Server)

  • Outbound streaming:
    • Head phase (connect + headers) bounded by finite deadline + cooperative cancellation
    • Each body read bounded per-chunk in stream_pull()
    • Response-byte ceiling enforced on running total
    • Dropping the handle (EOF, error, close, or teardown) cancels the upstream request
    • All errors catchable via try/catch
  • Server streaming:
    • Bounded channel gives backpressure: slow client slows handler writes
    • Dropping the sender (handler end, close, or caught error) closes the body and finalizes the response
    • Per-handler isolation via concurrent scopes

5. Outbound-stream lifecycle P1s (shipped)

Both P1 outbound-stream lifecycle items from the re-review are implemented, each with a real-socket Red→Green regression (mock upstream via tokio::TcpListener + the interpreter):

  • Absolute deadline bounds an active read. run_http_with_budget composes the operation deadline as min(run/budget remaining, idle, remaining absolute stream deadline) instead of discarding the stream's shorter bound; the absolute clock starts at request initiation (covers connect/header). tests/outbound_stream_deadline_test.rs: a head-then-stall upstream now fails wait for next chunk at ~1s under outbound_stream_max_seconds = 1 (was ~10s).
  • Outbound handles are handler-owned. Tracked per-handler in RunState.open_http_streams; dropped from IoClient.stream_handles (cancelling the upstream) on every handler exit — IsolatedHandler::drop for the concurrent loop and the serial-loop/program cleanup sites. tests/outbound_stream_ownership_test.rs.
  • Disconnect cancels a blocked upstream read. The upstream wait for next line|chunk is select!ed against the downstream response stream's Sender::closed(), so a browser disconnect cancels a blocked proxy read promptly (dropping the upstream) rather than waiting out the deadline. tests/outbound_stream_disconnect_test.rs.

https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX


``<img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review">``

Summary by CodeRabbit

  • New Features
    • Added opt-in main loop concurrently: for interleaved request handling (plain main loop: stays serial).
    • Added HTTP client streaming: open url ... and stream response as ..., plus wait for next line/chunk ....
    • Added HTTP server response streaming: start streaming response, write line|chunk, flush, and streaming close.
  • Bug Fixes
    • Handlers that dequeue a request and exit without responding now trigger immediate HTTP 500.
    • Streamed responses auto-close on handler exit; operations after close/end-of-stream raise catchable errors while the server continues.
  • Documentation
    • Updated concurrency + streaming guides with new examples and design/status notes.
  • Tests
    • Added/expanded parser and integration tests for concurrency and streaming lifecycles.

claude added 4 commits July 22, 2026 15:12
…xt chunk|line)

Add generic outbound response streaming to the WFL client so a large or
progressively-emitted upstream body can be consumed without buffering:

  open url at "<url>" [with method .. and headers .. and body ..]
      and stream response as upstream
  wait for next line  from upstream as line   // Text, or nothing at EOF
  wait for next chunk from upstream as chunk   // Binary, or nothing at EOF
  close upstream                               // cancels the upstream request

`stream response as` returns as soon as the status/headers arrive, without
buffering the body, binding an object with status/ok/headers and an internal
stream id. The body is parked in a new IoClient stream-handle table and pulled
incrementally; each read takes the handle out of the map so a slow read never
blocks other streams.

Lifecycle (client side): the head phase and every per-chunk read go through the
existing run_http_with_budget select, so connect/read timeouts, the
response-byte ceiling (enforced on the running total, not just Content-Length),
and cooperative cancellation all apply. Mid-stream network errors are catchable
RuntimeErrors; dropping the handle (EOF, error, close, teardown) cancels the
upstream; reading a closed/drained handle is a predictable error. A final
unterminated line is delivered before a single clean `nothing` at EOF.

- AST: HttpStreamStatement, WaitForNextChunkStatement, WaitForNextLineStatement
- Parser: `stream response as` clause (io.rs); `wait for next chunk|line from`
  (processes.rs); no new lexer tokens (contextual identifiers)
- Interpreter: IoClient open_http_stream/next_chunk/next_line/close_stream;
  three statement arms; `close` extended to accept a streaming-response object
- Analyzer/typechecker/transpiler: variable-binding registration; explicit
  unsupported-in-JS-transpilation arm
- Docs: interoperability.md "Streaming a response incrementally" + validated
  example; Dev Diary entry
- Tests: tests/http_stream_test.rs (parser + offline runtime, incl. EOF and
  closed-stream error)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
…g roadmap

Capture the turnkey design for server-side response streaming and the overall
response-streaming roadmap, so the next increment is unambiguous:

- Chosen surface: `start streaming response to <req> with status .. and content
  type .. as <out>`, `write line|chunk <expr> to <out>`, `flush <out>`,
  `close <out>` — consistent with the shipped client streaming and dispatched
  like the existing `send websocket message` leading-identifier statements.
- Mechanism: a HandlerReply enum (Buffered | Streaming{status, content_type,
  headers, body: mpsc::Receiver}) over the existing per-request oneshot; the
  warp route's final closure converts to a Body-typed reply (Body::from for the
  buffered/504 arms, Body::wrap_stream(unfold(rx)) for the streaming arm) while
  the recover helper stays Vec<u8> (warp unifies via Either) — so only the final
  handler closure changes, not the five reply helpers.
- Lifecycle: bounded mpsc for backpressure; a closed receiver (browser
  disconnect) makes `write` fail catchably, which the handler uses to close the
  upstream; `close`/drop ends the body; writes after close fail predictably.

Also records status of all five requested capabilities: items 1-2 and the
client half of item 5 shipped; item 3 designed; item 4 deferred to the locked
concurrency-phase-plan (Phase 1 `main loop concurrently:`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
…e|chunk / flush / close)

Add server-side response streaming so a WFL handler can send status/headers
immediately and produce the body progressively, without buffering:

  start streaming response to req with status 200 and content type "application/x-ndjson" as out
  write line json_text to out    // frames a line (newline appended)
  write chunk raw_bytes to out    // raw bytes/text, verbatim
  flush out                       // advisory
  close out                       // ends the response body

Combined with the client-side `stream response as upstream` + `wait for next
line`, a handler can proxy a slow upstream to the browser line-by-line.

Mechanism:
- The per-request oneshot now carries a HandlerReply enum
  (Buffered(WflHttpResponse) | Streaming{status, content_type, headers,
  body: mpsc::Receiver}). `respond` sends Buffered; `start streaming response`
  sends Streaming with a bounded body channel's receiver.
- The warp route's final closure returns a Body-typed reply. warp's recover
  unifies reply types via Either, so only that closure changed: it wraps the
  buffered/504 arms with Body::from and builds the streaming arm with
  Body::wrap_stream(unfold(rx)) (no new dependency); the five Vec<u8> reply
  helpers are untouched.
- Backpressure: the body channel is bounded (64) so a slow client backpressures
  `write`. Disconnect: hyper drops the body -> receiver dropped -> the next
  `write` fails with a catchable error, propagating the disconnect to the
  handler (which can close any upstream it proxies). `close`/handler-exit drops
  the sender, ending the response.
- `start` is a keyword; `streaming`/`flush`/`line`/`chunk` are identifiers.
  `close` now also closes a server response stream (`_server_stream`).

- AST: StartStreamingResponseStatement, StreamWriteStatement, FlushStreamStatement
- Parser: parse_start_streaming_response/parse_flush_stream (web.rs), write
  line|chunk branch (io.rs), KeywordStart/flush dispatch (parser/mod.rs)
- Interpreter: server_response_streams map, three exec arms, close extension
- Analyzer/typechecker/transpiler arms
- Docs: web-servers.md "Streaming a response" (+ upstream-proxy example);
  response-streaming-design.md status; validated example; Dev Diary
- Tests: tests/http_server_streaming_test.rs (parser + end-to-end streaming
  server read back with reqwest); existing web-server suites still green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
Add opt-in cooperative concurrent request handling so a slow handler (e.g. one
proxying a slow upstream stream) no longer blocks other requests:

  main loop concurrently:
      wait for request comes in on server as req
      ... slow handler ...
      respond to req with "..."
  end loop

Plain `main loop` stays strictly serial and byte-compatible; `concurrently` is
the only opt-in and nothing changes silently. This is Phase 1 of the
maintainer-locked concurrency-phase-plan.md (locked marker, no Rc->Arc/Send
rewrite of the interpreter core) and lands at that plan's STOP/review gate.

Mechanism:
- AST: MainLoop gains `concurrent: bool`. `concurrently` is a contextual
  identifier (only special right after `main loop`), so it stays usable as a
  variable name elsewhere.
- execute_concurrent_main_loop keeps up to CONCURRENT_HANDLER_LIMIT (256) body
  iterations in flight via a FuturesUnordered of !Send, non-'static,
  &self-borrowing handler futures — cooperative concurrency on the single
  interpreter thread (no spawn_local, no Send/Arc across the core). Each
  iteration runs in a fresh isolated child scope. Cap >= 1 keeps the set
  non-empty, avoiding the Ready(None) busy-spin trap.
- Containment: each handler future is AssertUnwindSafe(...).catch_unwind(); a
  panicking or erroring handler is contained and its request is answered 500 by
  the existing ResponseCompletion drop guard while siblings keep running.
- 503/504/500 are inherited from the existing transport layer (bounded queue,
  response deadline, drop guard); `wait for request` releases the receiver lock
  before the handler runs, so iterations hand off requests one at a time then
  handle them concurrently. No RefCell held across await (crate-wide
  await_holding_refcell_ref deny enforces this).

- Docs: web-servers.md "Concurrent request handling" (concurrent != parallel,
  yield cliff, 503/504/500) + validated example; concurrency-phase-plan.md
  tracker updated to Phase-1-done/awaiting-review; Dev Diary.
- Tests: tests/concurrent_main_loop_test.rs — concurrently parses vs serial;
  slow handler does not block fast sibling; serial DOES block (no silent
  upgrade); handler-error containment keeps the server serving.

Also removes two .ast.txt parse-dump artifacts accidentally added in the two
prior streaming commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
Copilot AI review requested due to automatic review settings July 22, 2026 16:20
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The interpreter adds outbound response streaming, server-side streamed responses, and opt-in cooperative concurrency for HTTP request handlers. New WFL syntax is parsed, analyzed, type-checked, documented, demonstrated, and covered by parser and runtime tests.

Changes

HTTP streaming and concurrent handlers

Layer / File(s) Summary
Language contracts and static handling
src/parser/..., src/analyzer/..., src/typechecker/mod.rs, src/transpiler/javascript.rs
Adds AST variants, parsing, analysis, typechecking, static-use tracking, and transpiler handling for streaming statements and main loop concurrently:.
Outbound response streaming
src/interpreter/mod.rs, tests/http_stream_test.rs, Docs/04-advanced-features/interoperability.md
Opens HTTP responses without buffering, reads lines or chunks incrementally, returns nothing at EOF, and closes streaming handles.
Server response streaming
src/interpreter/mod.rs, tests/http_server_streaming_test.rs, Docs/04-advanced-features/web-servers.md, Docs/development/response-streaming-design.md
Supports bounded streamed replies with line or chunk writes, flushing, closing, headers, backpressure, disconnect errors, and handler-exit cleanup.
Concurrent main-loop execution
src/interpreter/mod.rs, tests/concurrent_main_loop_test.rs, Docs/development/concurrency-phase-plan.md, Docs/04-advanced-features/web-servers.md
Adds bounded cooperative handler execution with isolated scopes, poll-local run-state isolation, contained failures, immediate fallback responses, and serial behavior by default.
Documentation examples and testing governance
Dev diary/*, TestPrograms/docs_examples/*, testing.md, AGENTS.md, CLAUDE.md
Documents shipped behavior, registers examples, and defines repository testing-policy requirements.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WFL
  participant IoClient
  participant Upstream
  WFL->>IoClient: open url and stream response
  IoClient->>Upstream: send HTTP request
  Upstream-->>IoClient: return status and headers
  IoClient-->>WFL: bind streaming handle
  WFL->>IoClient: wait for next line or chunk
  IoClient-->>WFL: return body data or nothing
Loading
sequenceDiagram
  participant Handler
  participant StreamChannel
  participant WarpTransport
  participant HTTPClient
  Handler->>WarpTransport: start streaming response
  WarpTransport->>StreamChannel: receive bounded body channel
  Handler->>StreamChannel: write line or chunk
  StreamChannel-->>HTTPClient: deliver body data
  Handler->>StreamChannel: close response
  StreamChannel-->>HTTPClient: end response body
Loading

Possibly related PRs

  • WebFirstLanguage/wfl#262: The transpiler now rejects concurrent main loops and streaming statements, directly connecting this change to the JavaScript transpiler infrastructure.
  • WebFirstLanguage/wfl#556: Both changes modify request-handling semantics used by concurrent handlers and request lifecycle tracking.
  • WebFirstLanguage/wfl#609: The streaming and concurrent interpreter paths reuse shared response/body size and deadline enforcement.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main additions: client streaming, server streaming, and concurrent handlers.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wfl-runtime-streaming-c3pvf9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed61ba5ac7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +3756 to +3758
futs.push(
std::panic::AssertUnwindSafe(self.execute_block(body, scope)).catch_unwind(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Isolate interpreter loop state per concurrent handler

When main loop concurrently: runs multiple execute_block futures on the same Interpreter, the child environments are isolated but interpreter-level state is not. For example, a handler that awaits inside a count loop can be resumed while another handler has overwritten self.current_count/self.in_count_loop, and count lookup reads those shared fields before the environment binding, so concurrent requests can return each other's loop counters or leave stale loop state behind.

Useful? React with 👍 / 👎.

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +8830 to +8832
self.server_response_streams
.borrow_mut()
.insert(handle_id.clone(), tx);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop response streams when the handler finishes

After start streaming response, the only sender that can close the HTTP body is stored in server_response_streams, but there is no per-handler cleanup when the iteration returns or errors. In a long-running main loop/main loop concurrently: handler that writes a few chunks and then falls off the end (or errors after the stream starts) without an explicit close, the sender remains in this map, so hyper never observes EOF and the client hangs while the handle leaks.

Useful? React with 👍 / 👎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

🐛 1 issue in files not directly in the diff

🐛 Streamed server responses never finish unless the handler explicitly closes them (src/interpreter/mod.rs:8830-8832)

The sender for a streamed server response is stored in a long-lived interpreter table (server_response_streams insert at src/interpreter/mod.rs:8830-8832) that is never cleared when the request handler finishes, so a streamed response whose handler does not explicitly close it stays open forever and its body never completes.
Impact: a client waiting on such a response hangs indefinitely (the chunked body is never terminated), and the table grows by one entry per streamed request, leaking memory over the life of the server.

Why the documented auto-close never happens

The WFL handle object bound by start streaming response ... as out only stores the text id _server_stream (src/interpreter/mod.rs:8834); the actual mpsc::Sender<Vec<u8>> lives in the interpreter-wide server_response_streams map keyed by that id. The transport turns the matching receiver into the response body via unfold, which only ends (yields None) once every sender is dropped.

The map entry is removed in only two places: an explicit close out (src/interpreter/mod.rs:5341) and the write-after-disconnect path (src/interpreter/mod.rs:8887). Nothing ties the sender's lifetime to the handler's scope or main-loop iteration. When a handler ends (normally, via a caught error, or via a break) without calling close out, the sender remains in the map, the receiver never sees None, and the client's body never terminates. Across many requests the map also accumulates dead senders.

This directly contradicts the shipped documentation, which promises The stream is closed automatically when the handler ends on any path. (Docs/04-advanced-features/web-servers.md:502-503) and the design doc's "Close-on-exit" guarantee.

Open in Devin Review

Comment on lines +502 to +503
stop producing (and `close` any upstream you are proxying). The stream is closed
automatically when the handler ends on any path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Documentation promises automatic close of streamed responses that the runtime does not perform

The web servers guide states that a streamed server response is closed automatically when the handler ends on any path (Docs/04-advanced-features/web-servers.md:502-503), but the runtime only ends the response when the code explicitly calls close out or the client disconnects, so the prose describes behavior the implementation does not have.
Impact: readers relying on the documented guarantee will ship handlers that omit close, causing responses that never finish for their clients.

Rule basis

CLAUDE.md's "Docs Must Be Honest" policy forbids describing runtime behavior that does not exist. The streamed-response sender is held in the interpreter-wide server_response_streams map with no scope-bound drop (src/interpreter/mod.rs:8830-8832), and is removed only by an explicit close (src/interpreter/mod.rs:5341) or on a write-after-disconnect. There is no code path that closes the stream when a handler ends, so this doc claim is inaccurate.

Prompt for agents
The sentence 'The stream is closed automatically when the handler ends on any path.' at Docs/04-advanced-features/web-servers.md:502-503 does not match the implementation, which only ends a streamed response on an explicit `close out` or a client disconnect. Either implement scope-bound auto-close (see the related runtime finding) or reword this to state that the handler must call `close out` to finalize the response body, so the docs remain honest about current behavior.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end HTTP streaming support to WFL: client-side streaming response handles with incremental reads, server-side streamed responses (chunk/line writes), and an opt-in main loop concurrently: execution mode so slow handlers don’t block other requests.

Changes:

  • Introduces new AST statements + parser/analyzer/typechecker/interpreter support for outbound HTTP response streaming and incremental next chunk|line pulls.
  • Adds streamed server responses via start streaming response + write line|chunk + flush, implemented with a bounded channel for backpressure.
  • Implements main loop concurrently: using cooperative single-thread concurrency, and adds tests + docs + validated doc examples.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/http_stream_test.rs Adds parser + runtime tests for outbound streaming handles and incremental reads.
tests/http_server_streaming_test.rs Adds parser + runtime tests for streamed server responses (write line/chunk, flush, close).
tests/concurrent_main_loop_test.rs Adds parser + runtime tests verifying concurrent vs serial main loop behavior.
TestPrograms/docs_examples/web_servers/streaming_response.wfl New docs example snippet for streaming server responses.
TestPrograms/docs_examples/web_servers/concurrent_main_loop.wfl New docs example snippet for main loop concurrently:.
TestPrograms/docs_examples/interoperability/streaming_response.wfl New docs example snippet for outbound streaming reads.
TestPrograms/docs_examples/_meta/manifest.json Registers new doc snippets in the validation manifest.
src/typechecker/mod.rs Adds typechecking hooks for new streaming statements and concurrent main loop binding behavior.
src/transpiler/javascript.rs Explicitly rejects streaming HTTP statements in JS transpilation; updates async detection.
src/parser/stmt/web.rs Implements parsing for start streaming response ... as <out> and flush <out>.
src/parser/stmt/processes.rs Extends wait for ... parsing to support `wait for next chunk
src/parser/stmt/io.rs Adds stream response as <name> and `write line
src/parser/stmt/control_flow.rs Parses main loop concurrently: and stores the flag on the AST.
src/parser/mod.rs Dispatches new start ... and flush ... statements at the top-level statement parser.
src/parser/ast.rs Adds new statement variants and the MainLoop.concurrent flag with documentation.
src/interpreter/mod.rs Implements outbound stream handle parking/pulling, server streaming replies, and concurrent main loop execution.
src/analyzer/mod.rs Adds analyzer support for new statements and bound variables.
Docs/development/response-streaming-design.md Adds/updates design/status tracking doc for the streaming feature set.
Docs/development/concurrency-phase-plan.md Updates the concurrency phase tracker to reflect Phase 1 landing and review checkpoint.
Docs/04-advanced-features/web-servers.md Documents concurrent handlers and streamed server responses with examples.
Docs/04-advanced-features/interoperability.md Documents outbound streaming response reads and handle semantics.
Dev diary/2026-07-22-server-response-streaming.md Dev diary entry describing server streaming design and implementation.
Dev diary/2026-07-22-outbound-response-streaming.md Dev diary entry describing outbound streaming design and implementation.
Dev diary/2026-07-22-concurrent-request-handlers.md Dev diary entry describing the concurrent main loop design and containment behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/concurrent_main_loop_test.rs Outdated
Comment on lines +78 to +79
let port = 8241;
let _server = start_server_thread(server_code(port, true));
Comment on lines +57 to +74
fn server_code(port: u16, concurrently: bool) -> String {
let marker = if concurrently { " concurrently" } else { "" };
format!(
r#"
listen on port {port} as srv
main loop{marker}:
wait for request comes in on srv as req with timeout 20000
store p as req["path"]
check if p is equal to "/slow":
wait for 500 milliseconds
respond to req with "slow"
otherwise:
respond to req with "fast"
end check
end loop
"#
)
}
Comment thread tests/concurrent_main_loop_test.rs Outdated
Comment on lines +106 to +108
let slow_resp = slow.await.unwrap();
assert_eq!(slow_resp.text().await.unwrap(), "slow");
}
Comment on lines +498 to +503
**Lifecycle & backpressure:** the body channel is bounded, so a slow client
slows your `write` calls (backpressure) instead of buffering without bound. If
the client disconnects, hyper drops the response body and your next `write` to
that stream fails with a catchable error — use `try`/`when` to detect it and
stop producing (and `close` any upstream you are proxying). The stream is closed
automatically when the handler ends on any path.
Comment on lines +155 to +160
The same limits as buffered requests apply: the running total of body bytes is
held under `web_server_max_response_size`, each read is bounded by the request's
timeout, and cooperative cancellation interrupts a read waiting on the peer. A
mid-stream network error surfaces as a catchable error from the `wait for next
...` statement, and every stream is closed when the handle is dropped on any
exit path.
Comment on lines +143 to +150
// The server survived the caught error and keeps serving.
let ok = client
.get(format!("http://127.0.0.1:{port}/ok"))
.send()
.await
.expect("follow-up request failed");
assert_eq!(ok.text().await.unwrap(), "ok");
}
Comment thread tests/concurrent_main_loop_test.rs Outdated
Comment on lines +182 to +183
let _ = slow.await;
}
Comment thread src/interpreter/mod.rs Outdated
Comment on lines +8714 to +8729
let status_code = match status {
Some(expr) => {
let v = self.evaluate_expression(expr, Rc::clone(&env)).await?;
match &v {
Value::Number(n) => *n as u16,
_ => {
return Err(RuntimeError::new(
format!("Expected number for status, got {}", v.type_name()),
*line,
*column,
));
}
}
}
None => 200,
};
Comment on lines +2073 to 2078
| Statement::HttpStreamStatement { .. }
| Statement::WaitForNextChunkStatement { .. }
| Statement::WaitForNextLineStatement { .. }
| Statement::StartStreamingResponseStatement { .. }
| Statement::StreamWriteStatement { .. }
| Statement::WaitForProcessStatement { .. }
Comment thread src/typechecker/mod.rs
Comment on lines +922 to +941
if let Some(body) = body {
let body_type = self.infer_expression_type(body);
if !matches!(
body_type,
Type::Text
| Type::Number
| Type::Boolean
| Type::Unknown
| Type::Any
| Type::Error
) {
self.type_error(
"HTTP request body must be text".to_string(),
Some(Type::Text),
Some(body_type),
*_line,
*_column,
);
}
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (1)
src/parser/stmt/web.rs (1)

429-464: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Content-type clause spelling is inconsistent between respond and start streaming response.

start streaming response ... and content type "x" (two words) works here because the parser matches Token::KeywordContent directly. respond ... and content_type "x" only works with the underscored single-word form — content always lexes as a keyword, so parse_respond_statement's identifier-only branch can never see a two-word content type. Based on learnings, the two-word form was previously confirmed to fail for respond with Unexpected token in expression: KeywordAnd. Consider documenting this difference explicitly (or unifying the accepted spelling across both statements) so users don't carry the content_type-only assumption from respond into the new streaming statement, or vice versa.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/stmt/web.rs` around lines 429 - 464, Unify content-type clause
parsing between parse_respond_statement and the start streaming response parser
so both accept content type and content_type spellings. Update the respond
parser to recognize the Token::KeywordContent followed by the type marker, while
preserving existing underscored parsing and expression handling. Ensure both
statement forms produce the same content_type result for equivalent input.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Docs/development/concurrency-phase-plan.md`:
- Around line 53-68: Update the Phase 1 concurrency documentation around the
“slow handler does not block other requests” example to state that this applies
when the handler yields at an await point; explicitly note that CPU-bound
handlers can still stall the interpreter thread.

In `@Docs/development/response-streaming-design.md`:
- Line 18: Escape the pipe character in the table cell containing “wait for next
chunk|line” so Markdown treats it as literal content rather than a column
delimiter. Update only that table entry and preserve the existing text and
shipped status.

In `@src/interpreter/mod.rs`:
- Around line 3737-3759: Update execute_concurrent_main_loop and the per-handler
execution path so current_count, in_count_loop, call_stack, and call_depth are
stored in a handler-local execution context rather than shared Interpreter
fields. Create and pass one context per spawned execute_block invocation, ensure
nested calls and count loops read/write that context, and preserve restoration
through normal returns and panic unwinds without sibling state leaking.

In `@src/parser/stmt/io.rs`:
- Around line 796-822: Update the streaming-marker handling near the
`next_token` check so exact `line` or `chunk` identifiers are intercepted only
when the following token is not `Token::KeywordTo`; otherwise preserve the
existing `WriteToStatement` path for variables named `line` or `chunk`. Add
regression coverage for `write line to out` and `write chunk to out` using plain
variables and verify both parse successfully.

In `@src/parser/stmt/processes.rs`:
- Around line 399-432: Restrict the bare-`next` branch in the statement parser
to cases where the following token is `chunk` or `line`; otherwise do not
consume or error on it, and let the existing fallback duration/expression
handling process the identifier. Preserve the dedicated handling for `next
chunk` and `next line` while allowing variables named `next`, including `wait
for next milliseconds`.

In `@src/typechecker/mod.rs`:
- Around line 972-1002: Update the streamed-response branches in the typechecker
to perform the same concrete operand validation as RespondStatement: require a
numeric status, text content type, map headers, and valid stream handles;
enforce Text|Number|Boolean for StreamWriteStatement line payloads and
Text|Binary for chunk payloads. Replace the inference-only checks around
StartStreamingResponseStatement, StreamWriteStatement, and FlushStreamStatement
while preserving existing symbol initialization.

In `@TestPrograms/docs_examples/web_servers/concurrent_main_loop.wfl`:
- Around line 3-5: Update the documentation comment describing concurrent
request handling to state that slow handlers yield to sibling handlers only at
await points, while CPU-bound work still blocks the single interpreter thread;
retain the distinction between concurrent and serial main loops without
overclaiming isolation behavior.

In `@tests/concurrent_main_loop_test.rs`:
- Around line 111-150: In tests/concurrent_main_loop_test.rs lines 111-150, add
a deterministic handler-panic scenario that exercises the concurrent loop’s
catch_unwind path, asserting the panicking request receives HTTP 500 and a
subsequent request still receives a successful response; retain the existing
runtime-error coverage as appropriate. In
Docs/development/concurrency-phase-plan.md lines 57-58, describe only
runtime-error containment as tested unless the new panic-path test is added.
- Around line 79-108: Replace the fixed 80 ms dispatch delay with a test-only
barrier or signal emitted when the /slow handler begins, then await that signal
before sending /fast in tests/concurrent_main_loop_test.rs lines 79-108 and
before timing /fast in lines 155-183. Preserve the existing assertions while
ensuring both tests measure ordering only after /slow has entered its handler.
- Around line 160-182: Update the concurrent request test to assert that the
spawned slow request completes successfully instead of discarding its result at
the final slow.await. Preserve the existing timing and fast-response assertions
while propagating or explicitly failing on both task panics and request errors.

---

Nitpick comments:
In `@src/parser/stmt/web.rs`:
- Around line 429-464: Unify content-type clause parsing between
parse_respond_statement and the start streaming response parser so both accept
content type and content_type spellings. Update the respond parser to recognize
the Token::KeywordContent followed by the type marker, while preserving existing
underscored parsing and expression handling. Ensure both statement forms produce
the same content_type result for equivalent input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9cdec2a-875d-47bd-9466-efabcf094a6e

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8e0cc and ed61ba5.

📒 Files selected for processing (24)
  • Dev diary/2026-07-22-concurrent-request-handlers.md
  • Dev diary/2026-07-22-outbound-response-streaming.md
  • Dev diary/2026-07-22-server-response-streaming.md
  • Docs/04-advanced-features/interoperability.md
  • Docs/04-advanced-features/web-servers.md
  • Docs/development/concurrency-phase-plan.md
  • Docs/development/response-streaming-design.md
  • TestPrograms/docs_examples/_meta/manifest.json
  • TestPrograms/docs_examples/interoperability/streaming_response.wfl
  • TestPrograms/docs_examples/web_servers/concurrent_main_loop.wfl
  • TestPrograms/docs_examples/web_servers/streaming_response.wfl
  • src/analyzer/mod.rs
  • src/interpreter/mod.rs
  • src/parser/ast.rs
  • src/parser/mod.rs
  • src/parser/stmt/control_flow.rs
  • src/parser/stmt/io.rs
  • src/parser/stmt/processes.rs
  • src/parser/stmt/web.rs
  • src/transpiler/javascript.rs
  • src/typechecker/mod.rs
  • tests/concurrent_main_loop_test.rs
  • tests/http_server_streaming_test.rs
  • tests/http_stream_test.rs

Comment thread Docs/development/concurrency-phase-plan.md Outdated
Comment thread Docs/development/response-streaming-design.md Outdated
Comment thread src/interpreter/mod.rs
Comment thread src/parser/stmt/io.rs Outdated
Comment thread src/parser/stmt/processes.rs
Comment thread src/typechecker/mod.rs Outdated
Comment thread TestPrograms/docs_examples/web_servers/concurrent_main_loop.wfl Outdated
Comment thread tests/concurrent_main_loop_test.rs Outdated
Comment on lines +79 to +108
let _server = start_server_thread(server_code(port, true));
tokio::time::sleep(Duration::from_millis(300)).await;

let client = reqwest::Client::new();

// Kick off the slow request first and give it a moment to be dequeued.
let slow_url = format!("http://127.0.0.1:{port}/slow");
let slow =
tokio::spawn(async move { reqwest::Client::new().get(&slow_url).send().await.unwrap() });
tokio::time::sleep(Duration::from_millis(80)).await;

// The fast request must complete promptly even though /slow is mid-handler.
let t0 = Instant::now();
let fast = client
.get(format!("http://127.0.0.1:{port}/fast"))
.send()
.await
.expect("fast request failed");
let fast_elapsed = t0.elapsed();
let fast_body = fast.text().await.unwrap();

assert_eq!(fast_body, "fast");
assert!(
fast_elapsed < Duration::from_millis(300),
"fast request was blocked behind the slow handler ({fast_elapsed:?})"
);

let slow_resp = slow.await.unwrap();
assert_eq!(slow_resp.text().await.unwrap(), "slow");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Replace sleep-based dispatch ordering with a barrier. The 80 ms delay does not guarantee that /slow has been dequeued. A serial implementation can process /fast first and falsely pass the concurrent test or fail the serial test depending on scheduling.

  • tests/concurrent_main_loop_test.rs#L79-L108: wait for a test-only signal emitted after the slow request enters its handler before sending /fast.
  • tests/concurrent_main_loop_test.rs#L155-L183: use the same signal before timing /fast, so the serial blocking assertion measures the intended ordering.
📍 Affects 1 file
  • tests/concurrent_main_loop_test.rs#L79-L108 (this comment)
  • tests/concurrent_main_loop_test.rs#L155-L183
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/concurrent_main_loop_test.rs` around lines 79 - 108, Replace the fixed
80 ms dispatch delay with a test-only barrier or signal emitted when the /slow
handler begins, then await that signal before sending /fast in
tests/concurrent_main_loop_test.rs lines 79-108 and before timing /fast in lines
155-183. Preserve the existing assertions while ensuring both tests measure
ordering only after /slow has entered its handler.

Comment thread tests/concurrent_main_loop_test.rs
Comment thread tests/concurrent_main_loop_test.rs Outdated

logbie commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Deep review: request changes

Reviewed commit: ed61ba5ac70d50a8663590952d26d9c1aeff1d88

The happy-path APIs are promising: generic NDJSON-friendly response streaming exists, basic line/chunk reads work, and slow/fast handlers can interleave. However, the lifecycle and concurrency guarantees at the core of this change are not yet satisfied. I do not think this is safe to merge in its current form.

Goal assessment

Goal Result Assessment
Outbound response streaming Partial Headers are exposed without buffering the entire body, but handles are not request-owned and absolute deadlines/cancellation are missing.
Incremental reads Partial Chunk/line/EOF happy paths work; normal loop-style reuse of as line fails.
Server streaming Partial Generic chunk/line framing works, but handler exit does not close the body and post-head timeouts are absent.
Concurrent handlers Fail Interleaving works, but handlers share unsafe interpreter state and can mutate parent bindings.
Lifecycle guarantees Fail Leaks, delayed cancellation, missing total deadlines, byte-unbounded buffering, and incomplete error containment remain.

Merge blockers

  1. Critical — concurrent handlers share interpreter state that assumes one linear execution stack.

    Every concurrent iteration calls execute_block on the same interpreter (src/interpreter/mod.rs:3731-3790). Shared fields include current_count, call_stack, call_depth, block-overload state, and module/source tracking (1101-1160). Concurrent futures can overwrite/restore each other's count state, pop another handler's action frame when completions occur out of order, combine independent recursion depth, and corrupt LIFO-style guards.

    There is also a live RefCell hazard: close server holds web_servers.borrow_mut() across an await (9088-9097), while sibling request handling borrows the same RefCell. The module disables the supposed safeguard with #![allow(clippy::await_holding_refcell_ref)].

  2. Critical — handler scopes are not actually isolated.

    Environment::new_child_env explicitly sets isolated: false (src/interpreter/environment.rs:42-51). Assignment and store walk into parent environments and mutate existing bindings (237-268, 345-403); lists/maps/objects remain shared. Two handlers can read a global, await, and overwrite each other with stale values. This contradicts the documented claim that request variables never clobber one another.

  3. Critical — pre-response errors and panics hang until the request timeout instead of returning 500.

    wait for request parks the response sender in pending_responses (src/interpreter/mod.rs:8377-8400), but the fallback ResponseCompletion guard is created only after execution reaches respond or start streaming response. A failure before then is only logged by the scheduler (3775-3785). The client waits for the default 300-second 504, and enough such requests can exhaust admission capacity. The existing containment test responds successfully first, so it does not cover this path or an actual panic.

  4. Critical — browser disconnects and request timeouts do not cancel handlers or upstream requests.

    WflHttpRequest carries no handler cancellation token (src/interpreter/mod.rs:106-121). When the transport times out and drops its receiver (7775-7799), the handler keeps sleeping, reading upstream, or waiting on a downstream write. A disconnect is observed only if a later write finds the body receiver gone. Abandoned handlers can therefore occupy all 256 slots after every browser has disconnected.

  5. Critical — client and server streams are not scope-owned and leak on exit paths.

    Server response senders live in the interpreter-wide server_response_streams map (1151-1154, inserted at 8830-8832). They are removed only by explicit close or a later failed write. Normal return, runtime error, panic, break, and cancellation do not close them, so browser bodies never reach EOF and the map grows.

    Outbound handles have the same problem in IoClient.stream_handles: dropping a handler environment does not drop its parked upstream body. This contradicts the documented close-on-every-exit guarantee.

  6. High — the requested timeout model is incomplete.

    Server timeout coverage ends once the response head arrives. Body transport uses bare rx.recv().await (7857-7863) and producer writes can await channel capacity indefinitely. A slow connected browser can stall a handler forever.

    Outbound pulls receive a fresh full timeout on every read; the handle stores no creation time or absolute deadline. A trickling upstream can therefore run forever. The implementation needs distinct finite connect/header, per-read idle, and absolute-total deadlines, with cleanup and catchable timeout errors.

  7. High — backpressure is message-count bounded, not byte bounded.

    RESPONSE_STREAM_BUFFER = 64 limits queued messages, but every message can contain an arbitrary-sized Vec<u8>. One huge write—or 64 huge writes—can consume effectively unbounded memory. A byte-weighted permit/budget and strict maximum chunk/body size are needed.

  8. High — ordinary incremental-read loops fail on the second iteration.

    The analyzer permits replacement, but the interpreter calls Environment::define for each wait for next line/chunk ... as name result (src/interpreter/mod.rs:6735-6774). Reusing as line inside one loop consumes the second upstream value and then errors because line already exists. The tests avoid this by using line1, line2, etc.

  9. High — JavaScript transpilation silently loses concurrency.

    Statement::MainLoop { body, .. } ignores the concurrent flag and emits the ordinary serial loop (src/transpiler/javascript.rs:354-363). Until equivalent semantics exist, this should be an explicit transpilation error rather than a silent behavior change.

  10. CI is non-green.

    Run WFL Programs (ubuntu-latest) fails on all three new documentation programs; the concurrency example times out. Their manifest entries specify skip_execution: true, but this workflow does not consult the manifest and requires its own skip marker. The Windows program job was canceled. Build/Test/Clippy, formatting, fuzz compilation, database tests, and Ubuntu integration are green.

Additional gaps

  • Invalid status values can wrap during as u16, and invalid statuses later silently become 200.
  • Invalid headers/content types are validated after the handler believes the response started, outside its catchable error path.
  • flush only yields; it does not acknowledge drain or detect disconnect.
  • Server start does not yield/await head acceptance, so subsequent CPU work or immediately-ready writes can delay transport polling.
  • A final line ending in a lone \r loses that byte.
  • The immediate-header test sends headers and body together and closes immediately; it does not prove behavior with a delayed/stalled body.

Required adversarial test gate

Before merge, I recommend covering:

  • Pre-response runtime error and actual panic → immediate 500; sibling still succeeds.
  • Browser disconnect and 504 → handler/upstream canceled and slot freed.
  • Handler return/error/panic after starting a response → EOF and handle removal.
  • Two concurrent count loops and two awaiting action calls → no shared-state corruption.
  • Repeated as line in one loop through clean EOF.
  • Stalled reads, trickling beyond the total deadline, slow consumers, and byte-ceiling violations.
  • Invalid metadata, write/flush after close, and mid-stream network failure.
  • close server under load without a RefCell panic or hot retry loop.

The parser/API work and generic framing are a solid foundation, but the ownership, cancellation, deadline, and execution-isolation model needs another pass before this can provide the guarantees described in the PR.

… honesty, test hygiene

Automated review (CodeRabbit/Copilot/Devin/Codex) on PR #641 surfaced several
issues; this fixes the clear ones:

Backward-compatibility regressions (parser):
- `write <var> to <file>` with a variable literally named `line`/`chunk` was
  wrongly intercepted as a stream write and failed to parse. Only treat
  `line`/`chunk` as the stream marker when it is NOT immediately followed by
  `to`. Regression test added.
- `wait for next <unit>` (e.g. `wait for next milliseconds`) with a variable
  named `next` was wrongly intercepted as `wait for next chunk|line`. Only take
  the bare-`next` path when `chunk`/`line` actually follows. Regression test
  added.

Correctness / consistency:
- `start streaming response ... with status <n>`: require a whole number in
  100..=599 instead of silently wrapping via `as u16`.
- `FlushStreamStatement` is now classified async in the transpiler's
  `stmt_is_async` (it yields), consistent with the other streaming statements.

Docs honesty (CLAUDE.md "Docs Must Be Honest"):
- Removed the false claim that a streamed server response is "closed
  automatically when the handler ends"; the handler must `close out` (documented
  with a finally: recommendation). Client-side wording likewise corrected: a
  stream is released on EOF/error/explicit close/program exit, not on handle
  drop.
- Concurrent example + phase plan: state the cooperative limitation (a CPU-bound
  handler with no await still holds the thread), and separate tested
  runtime-error containment from by-construction catch_unwind panic containment.
- Escaped the `chunk|line` pipe in a Markdown table (MD056).

Test hygiene (concurrent_main_loop_test):
- Moved off ports that collided with another test (8241 -> 8341..8343).
- Each server now exposes `/shutdown` (close server + break); tests send it and
  join the server thread deterministically.
- Assert the slow request actually completed instead of discarding its result.

Also flags in concurrency-phase-plan.md a known gap for maintainer review: the
concurrent loop isolates the environment but not interpreter-level run-state
(count/call-stack), which needs a per-handler execution context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
Copilot AI review requested due to automatic review settings July 22, 2026 16:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

Comment thread src/interpreter/mod.rs
Comment on lines +3731 to +3737
/// Execute a `main loop concurrently:` body. Keeps up to
/// `CONCURRENT_HANDLER_LIMIT` iterations of `body` in flight at once, each in
/// its own isolated child scope, driven cooperatively on this single thread
/// (no threads, no `Send`/`Arc` across the interpreter core). A handler that
/// errors or panics is contained — its request is resolved with 500 by the
/// response-completion drop guard — and its siblings keep running.
async fn execute_concurrent_main_loop(
The integration runner executes every TestPrograms/*.wfl, but the three new
docs-examples need a live upstream / HTTP clients (or run a server loop forever),
so they fail/timeout when run standalone. They are validated statically via the
docs-examples manifest (layers 1-4). Add the runner's first-line `CI-SKIP:`
directive to each so they are skipped by run_integration_tests.sh while still
being parse/analyze/lint-validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
Copilot AI review requested due to automatic review settings July 22, 2026 17:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (5)

src/typechecker/mod.rs:940

  • This error message/expected-type pair is inconsistent with the allowed types: the check explicitly accepts Number and Boolean, but the diagnostic says "must be text" and reports Text as the expected type. Align the message (and expected type) with the actual accepted set.
                        self.type_error(
                            "HTTP request body must be text".to_string(),
                            Some(Type::Text),
                            Some(body_type),
                            *_line,
                            *_column,
                        );
                    }

src/interpreter/mod.rs:3758

  • execute_concurrent_main_loop runs multiple execute_block futures concurrently against the same Interpreter instance. The interpreter has shared mutable run-state (current_count, in_count_loop, call_stack, call_depth, etc.), so concurrent handlers can overwrite each other’s state if they yield at an await inside a count loop or action call. This can produce incorrect count values, wrong error stacks, or mis-enforced call-depth limits.
    /// Execute a `main loop concurrently:` body. Keeps up to
    /// `CONCURRENT_HANDLER_LIMIT` iterations of `body` in flight at once, each in
    /// its own isolated child scope, driven cooperatively on this single thread
    /// (no threads, no `Send`/`Arc` across the interpreter core). A handler that
    /// errors or panics is contained — its request is resolved with 500 by the
    /// response-completion drop guard — and its siblings keep running.
    async fn execute_concurrent_main_loop(
        &self,
        body: &[Statement],
        env: &Rc<RefCell<Environment>>,
    ) -> Result<(Value, ControlFlow), RuntimeError> {
        use futures_util::FutureExt;
        use futures_util::stream::{FuturesUnordered, StreamExt};

        let cap = CONCURRENT_HANDLER_LIMIT.max(1);
        let mut futs = FuturesUnordered::new();
        let mut last_value = Value::Null;

        loop {
            self.check_time()?;

            // Refill to the concurrency cap. Each iteration gets a fresh isolated
            // scope so concurrent requests never clobber each other's variables.
            while futs.len() < cap {
                let scope = Environment::new_child_env(env);
                futs.push(
                    std::panic::AssertUnwindSafe(self.execute_block(body, scope)).catch_unwind(),
                );

tests/concurrent_main_loop_test.rs:97

  • Using a fixed TCP port in tests can cause flaky CI failures when the port is already in use on the runner. Prefer binding an ephemeral port (127.0.0.1:0) and using the assigned port for the server/client.
    let port = 8341;

tests/concurrent_main_loop_test.rs:135

  • Using a fixed TCP port in tests can cause flaky CI failures when the port is already in use on the runner. Prefer binding an ephemeral port (127.0.0.1:0) and using the assigned port for the server/client.
    let port = 8342;

tests/concurrent_main_loop_test.rs:183

  • Using a fixed TCP port in tests can cause flaky CI failures when the port is already in use on the runner. Prefer binding an ephemeral port (127.0.0.1:0) and using the assigned port for the server/client.
    let port = 8343;

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +6706 to +6715
// Internal id used by `wait for next chunk|line` and
// `close`. Underscore-prefixed to signal "not for
// direct program use", mirroring request objects.
stream_map.insert("_stream".to_string(), Value::Text(handle_id.into()));

let value = Value::Object(Rc::new(RefCell::new(stream_map)));
match env.borrow_mut().define(variable_name, value) {
Ok(_) => Ok((Value::Null, ControlFlow::None)),
Err(msg) => Err(RuntimeError::new(msg, *line, *column)),
}
Comment thread src/interpreter/mod.rs Outdated
Comment on lines +8848 to +8855
let mut stream_map = HashMap::new();
stream_map.insert("_server_stream".to_string(), Value::Text(handle_id.into()));
stream_map.insert("status".to_string(), Value::Number(status_code as f64));
let value = Value::Object(Rc::new(RefCell::new(stream_map)));
match env.borrow_mut().define(variable_name, value) {
Ok(_) => Ok((Value::Null, ControlFlow::None)),
Err(msg) => Err(RuntimeError::new(msg, *line, *column)),
}
Comment thread src/interpreter/mod.rs
Comment on lines +140 to +142
/// line|chunk`. A bounded channel gives backpressure: a slow client slows the
/// handler's writes. Dropping the sender (handler end, `close`, or a caught
/// error) closes the body stream and finalizes the response.
Comment thread tests/http_server_streaming_test.rs Outdated

#[tokio::test]
async fn test_streamed_response_lines_and_headers() {
let port = 8231;
Comment thread tests/http_server_streaming_test.rs Outdated

#[tokio::test]
async fn test_streamed_response_write_chunk_verbatim() {
let port = 8232;
Comment thread tests/http_server_streaming_test.rs Outdated
let body = response.text().await.expect("Failed to read body");
assert_eq!(body, "alpha\nbeta\ngamma\n");

let _ = server_handle.join();
Comment on lines +87 to +93
async fn shutdown(port: u16, server: std::thread::JoinHandle<()>) {
let _ = reqwest::Client::new()
.get(format!("http://127.0.0.1:{port}/shutdown"))
.send()
.await;
let _ = tokio::task::spawn_blocking(move || server.join()).await;
}
Comment thread tests/http_server_streaming_test.rs Outdated
// write chunk does not append newlines.
assert_eq!(body, "onetwo");

let _ = server_handle.join();

logbie commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review at ed89b65: still request changes

I re-reviewed the full current implementation and the two-commit delta from ed61ba5 to ed89b65, including the updated tests, docs, unresolved review findings, and current Actions run.

What this revision fixed

  • Streaming status codes are now validated as whole numbers from 100–599 before the response is committed.
  • The parser no longer steals legacy write line to out, write chunk to out, or wait for next milliseconds forms.
  • Concurrent-loop tests now exercise their slow response and shut their servers down.
  • The three non-executable documentation programs have explicit CI skip markers.
  • The user guide now honestly says close out is mandatory.
  • CI #1714 is fully green: formatting, debug/release builds, workspace tests, Clippy, Linux/Windows integration, WFL programs, database tests, and fuzz-target compilation all passed.

Those are useful corrections. However, the revision does not change the core concurrency, ownership, cancellation, deadline, or backpressure implementation, so the main blockers from the first review remain.

Remaining merge blockers

  1. P1 — concurrent handlers still share execution state designed for one linear stack.

    Every handler future still executes against the same &Interpreter at src/interpreter/mod.rs:3754-3758. Shared state includes current_count, in_count_loop, current_block_overload_dups, call_stack, call_depth, and module/source tracking at 1101-1160.

    Count loops hold and manually restore shared count state across execute_block(...).await; action calls push a shared frame before awaiting their body and pop afterward. Out-of-order completion can therefore read another request's count, pop another handler's frame, falsely exhaust recursion depth, or restore stale state. The updated phase plan now explicitly acknowledges this gap, but acknowledgment does not make the runtime safe.

  2. P1 — the fresh handler environment is not isolated from parent mutation.

    The concurrent loop calls Environment::new_child_env, whose isolated flag is still false at environment.rs:42-51. store and assignment walk upward and modify parent bindings; maps/lists/objects remain shallow-shared. A read → await → write sequence can lose another request's update.

    This still conflicts with the user guide's promise that concurrent request variables never clobber one another.

  3. P1 — errors or panics before respond still do not produce an immediate 500.

    Dequeue parks the sender in pending_responses at mod.rs:8377-8400, but ResponseCompletion is not created until execution enters respond or start streaming response. The scheduler's error/panic branches only log and refill the handler set at 3775-3785.

    A failure between dequeue and response therefore leaves the client and its admission slot waiting for the 300-second 504. Enough ordinary error-path requests can force unrelated traffic into 503 for that interval. The revised test still sends boom-ok successfully before triggering a second-response error, so it does not cover a pre-response failure or real panic.

  4. P1 — disconnect/504 still does not cancel the handler or upstream request.

    WflHttpRequest still has no request cancellation token (mod.rs:106-121). Abandonment is checked once during dequeue. After a browser disconnect or transport 504 drops the response receiver, the handler can continue sleeping or waiting on an upstream read indefinitely.

    The downstream streaming path notices disconnect only on the next tx.send. If the handler is blocked in wait for next line, no cancellation signal is selected and the upstream request remains active. This can occupy all 256 handler slots after the clients are gone.

  5. P1 — streams still are not handler-owned and do not close on every exit path.

    Server body senders remain in the global server_response_streams map, inserted at mod.rs:8839-8846. They are removed only by explicit close or a later failed write. Normal return, error, panic, break, cancellation, and handler-future drop do not remove them, leaving the browser waiting for EOF and leaking the entry.

    There is an additional deterministic leak: the head and sender are committed before env.define(variable_name, value). If the output name already exists, binding fails after the response starts and the now-unreachable stream cannot be closed by WFL code.

    Outbound handles have the same ownership problem in IoClient.stream_handles. The head is opened and parked before its WFL variable is bound, so a binding collision leaks that upstream too. Handler scope exit does not clean parked handles.

  6. P1 — connect/read/idle/total timeout guarantees are still incomplete.

    On the server side, the request deadline stops after the streaming head. The body uses bare rx.recv().await at mod.rs:7857-7863, and a producer can wait forever on a full channel.

    On the outbound side, the head and every stream_pull get a fresh timeout. HttpStreamHandle has no opened-at/absolute deadline, so an upstream that trickles a chunk just before each read timeout can run forever. This is a per-operation timeout, not the required absolute total stream lifetime.

  7. P1 — server buffering is count-bounded, not byte-bounded.

    RESPONSE_STREAM_BUFFER = 64 limits message count, but each message is an arbitrary-sized Vec<u8>. Streaming writes clone unrestricted text/binary data, do not call check_response_bytes, and do not track queued or cumulative bytes. One huge chunk—or 64 huge chunks—can consume effectively unbounded memory and streamed output bypasses web_server_max_response_size.

  8. P1 — normal incremental-read loops still fail on their second iteration.

    The runtime still uses Environment::define for every line/chunk and EOF result at mod.rs:6735-6774. Reusing as line in the same loop consumes the next upstream item and then errors because line already exists. The analyzer models this as replaceable and the runtime tests still avoid the problem with line1 through line4.

  9. P1 — JavaScript transpilation still silently drops concurrently.

    Statement::MainLoop { body, .. } ignores the concurrency flag and emits the same serial while (true) loop. The new delta only adds FlushStreamStatement to async classification. Until concurrent semantics are implemented in JavaScript, main loop concurrently: should fail transpilation explicitly.

Secondary gaps

  • Content type and custom headers are still validated only by the transport after start reports success, so malformed metadata is not catchable at the WFL start statement.
  • flush remains a scheduler yield rather than a drain/transport acknowledgment.
  • Server head delivery has no transport handshake; the handler may enqueue many immediately-ready writes or continue CPU work before the transport task is polled.
  • The advertised immediate-head test still sends the head and complete body together, so it cannot detect accidental body buffering.
  • close server still holds web_servers.borrow_mut() across a 50 ms await, while src/interpreter/mod.rs overrides the crate's await_holding_refcell_ref deny with allow.
  • The design/status document still says lifecycle and close-on-exit are shipped, contradicting both the implementation and the newly honest user guide.

Required test gate remains

Before merge, I would still require adversarial coverage for:

  • pre-response error and real panic → immediate 500; sibling succeeds;
  • disconnect/504 → handler and upstream canceled; execution slot freed;
  • handler return/error/panic after start streaming response → browser EOF and handle removal;
  • concurrent count loops and awaiting action calls;
  • repeated as line through EOF;
  • stalled and trickling-beyond-total upstreams;
  • slow consumers and byte-budget violations;
  • invalid metadata, write/flush after close, and mid-stream network failure;
  • structural accept-loop failure without a hot retry loop.

Verdict: request changes. The revision fixed CI and several edge cases, but the implementation still does not provide the concurrency and lifecycle guarantees this PR is intended to ship.

…file

Add the binding Logbie Testing Policy v1.0 as root `testing.md` (required by the
policy's §4), together with the WFL project testing profile: supported
platform/runtime tuples, one command per test layer + a run-all-presubmit block,
critical user/operator journeys, risk triggers (esp. §11.3 concurrency/streaming/
lifecycle for this repo's async work), coverage targets, CI gating, owners,
justified N/A layers, and tracked conformance gaps.

Wire it into the agent guides: CLAUDE.md and AGENTS.md now list `testing.md` in
the governance table and summarize the non-negotiables — Red→Green TDD evidence,
risk classification (async/streaming/lifecycle = R3), real-boundary tests with
meaningful/negative assertions, no manufactured-green, and PR evidence (§15).

Risk class: R0 (documentation/policy; no executable behavior changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
Copilot AI review requested due to automatic review settings July 22, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (4)

src/interpreter/mod.rs:8843

  • If binding variable_name fails, the streaming response has already been started and tx is stored in server_response_streams, leaving an unreachable open stream (client hangs + leaked sender). On define failure, ensure the sender is removed/dropped so the body closes and resources are released.
                let handle_id = {
                    let n = self.next_response_stream_id.get();
                    self.next_response_stream_id.set(n + 1);
                    format!("respstream{n}")
                };

src/interpreter/mod.rs:142

  • The doc comment says dropping the streaming-body sender happens on “handler end”, but the sender is stored in server_response_streams and will not be dropped when the handler finishes unless WFL code explicitly closes the stream (or you remove it on error). This is misleading about lifecycle behavior.
/// handler's writes. Dropping the sender (handler end, `close`, or a caught
/// error) closes the body stream and finalizes the response.

src/typechecker/mod.rs:936

  • The body type check allows Text | Number | Boolean, but the error message/expected type currently say “must be text” (and “expected Text”). This is misleading for valid inputs (numbers/bools).
                            "HTTP request body must be text".to_string(),
                            Some(Type::Text),
                            Some(body_type),

src/interpreter/mod.rs:3756

  • main loop concurrently: runs multiple execute_block(...) futures concurrently on the same Interpreter. Several pieces of execution state are stored on the shared interpreter (e.g. current_count/in_count_loop, call_stack, call_depth) and are mutated across .await points, so concurrent handlers can overwrite each other’s run-state and produce incorrect behavior (wrong count, corrupted call stacks, etc.).
            // Refill to the concurrency cap. Each iteration gets a fresh isolated
            // scope so concurrent requests never clobber each other's variables.
            while futs.len() < cap {
                let scope = Environment::new_child_env(env);
                futs.push(

Comment thread src/typechecker/mod.rs Outdated
Comment thread src/typechecker/mod.rs
Comment on lines +972 to +996
Statement::StartStreamingResponseStatement {
request,
status,
content_type,
headers,
variable_name,
..
} => {
let _ = self.infer_expression_type(request);
if let Some(status) = status {
let _ = self.infer_expression_type(status);
}
if let Some(content_type) = content_type {
let _ = self.infer_expression_type(content_type);
}
if let Some(headers) = headers {
let _ = self.infer_expression_type(headers);
}
if !variable_name.is_empty()
&& let Some(symbol) = self.analyzer.get_symbol_mut(variable_name)
{
symbol.symbol_type =
Some(Type::Map(Box::new(Type::Text), Box::new(Type::Unknown)));
}
}
Comment on lines +107 to +111
#[tokio::test]
async fn test_streamed_response_lines_and_headers() {
let port = 8231;
let server_code = format!(
r#"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Docs/development/concurrency-phase-plan.md (1)

49-51: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize the stale Phase 1 TODO checklist.

The checklist at Lines 244-256 still marks the parser surface, runtime path, isolation, cap, timeout, and panic handling as unchecked, contradicting this tracker’s “Done” status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Docs/development/concurrency-phase-plan.md` around lines 49 - 51, Update the
Phase 1 TODO checklist in the concurrency phase plan so the parser surface,
runtime path, isolation, cap, timeout, and panic-handling items match the
completed statuses in the Phase 1 tracker. Change only the stale unchecked
markers; preserve the existing checklist wording and structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 15-21: Make the testing-policy adoption status consistent across
all governance references: update AGENTS.md lines 15-21 and 125-128, CLAUDE.md
lines 17-23 and 163-167, and testing.md lines 137-143 to state that the policy
is locally adopted and effective, including a consistent effective date;
alternatively, explicitly document why it remains proposed across every site.
Preserve the existing testing requirements while aligning the policy wording,
status, and section titles.

In `@Docs/development/concurrency-phase-plan.md`:
- Around line 59-61: Qualify the transport-layer status mapping in the
concurrency phase plan so it does not claim unconditional 500 containment.
Specifically revise the `ResponseCompletion` drop statement to acknowledge that
handler failures before response startup may instead leave clients waiting until
the default timeout, or fix that failure path and add a regression test covering
it.
- Around line 66-71: Add a dedicated Rust-level regression test at the handler
boundary that injects a panic in one handler, verifies the server remains alive,
and confirms sibling handlers continue operating. Keep the existing
runtime-error test and validate containment through the actual catch_unwind
path, rather than relying only on the panic = "unwind" configuration.
- Around line 56-58: Qualify the concurrent-handler isolation wording to state
that handlers use separate child environments, while parent bindings,
shallow-shared collections, and interpreter-level run state remain shared or
unsafe. Apply this wording in Docs/development/concurrency-phase-plan.md lines
56-58 and replace “own isolated scope” with the same qualified description in
TestPrograms/docs_examples/web_servers/concurrent_main_loop.wfl lines 4-8.

In `@testing.md`:
- Around line 39-49: Expand the “Run all presubmit checks” documentation to
define a complete, reproducible presubmit gate: include documented policy,
secret/dependency, coverage, and risk-triggered checks, plus exact blocking CI
job names and their release mapping. Use one checked-in wrapper/workflow or
explicitly enumerate every required command and platform equivalent, updating
the related CI section as well.
- Around line 94-100: Update the “Coverage & budgets” policy and the related
release-candidate gate section so mandatory coverage and release gates are
either implemented or documented as compliant exceptions. For each remaining
gap, include a ticket ID, owner, deadline, and time-limited Section 17
exception; do not leave “known gap” or target-only language without those
details.
- Around line 15-21: Update the “Supported configuration tuples” table in
testing.md to match the existing CI coverage: remove the macOS row, or document
it only if a corresponding macOS release job or command is added. Preserve the
Linux and Windows entries and their current pipeline status.

---

Outside diff comments:
In `@Docs/development/concurrency-phase-plan.md`:
- Around line 49-51: Update the Phase 1 TODO checklist in the concurrency phase
plan so the parser surface, runtime path, isolation, cap, timeout, and
panic-handling items match the completed statuses in the Phase 1 tracker. Change
only the stale unchecked markers; preserve the existing checklist wording and
structure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a20a7843-0e2d-42fb-aafb-38d08562cbf4

📥 Commits

Reviewing files that changed from the base of the PR and between ed61ba5 and d2d88c4.

📒 Files selected for processing (17)
  • AGENTS.md
  • CLAUDE.md
  • Docs/04-advanced-features/interoperability.md
  • Docs/04-advanced-features/web-servers.md
  • Docs/development/concurrency-phase-plan.md
  • Docs/development/response-streaming-design.md
  • TestPrograms/docs_examples/interoperability/streaming_response.wfl
  • TestPrograms/docs_examples/web_servers/concurrent_main_loop.wfl
  • TestPrograms/docs_examples/web_servers/streaming_response.wfl
  • src/interpreter/mod.rs
  • src/parser/stmt/io.rs
  • src/parser/stmt/processes.rs
  • src/transpiler/javascript.rs
  • testing.md
  • tests/concurrent_main_loop_test.rs
  • tests/http_server_streaming_test.rs
  • tests/http_stream_test.rs
🚧 Files skipped from review as they are similar to previous changes (11)
  • TestPrograms/docs_examples/web_servers/streaming_response.wfl
  • TestPrograms/docs_examples/interoperability/streaming_response.wfl
  • Docs/04-advanced-features/interoperability.md
  • Docs/04-advanced-features/web-servers.md
  • tests/concurrent_main_loop_test.rs
  • Docs/development/response-streaming-design.md
  • src/transpiler/javascript.rs
  • tests/http_stream_test.rs
  • src/parser/stmt/io.rs
  • tests/http_server_streaming_test.rs
  • src/interpreter/mod.rs

Comment thread AGENTS.md
Comment on lines +15 to +21
| `testing.md` | **Binding Logbie Testing Policy + WFL testing profile** — Red→Green TDD evidence, required test layers, risk classes, and merge/release gates (see **Testing Guidelines** below) |

**Agent implications (already in force via governance):**

- **AI is first-class** — use coding agents freely; same quality bar as hand-written work (tests, docs, compatibility, reviewability).
- **Backward compatibility is sacred** — never break existing WFL programs without the documented deprecation path.
- **TDD mandatory** — failing tests first (`tests/`, `TestPrograms/`).
- **TDD mandatory** — failing tests first (`tests/`, `TestPrograms/`), governed by the binding **Logbie Testing Policy** in root `testing.md`: auditable **Red→Green** evidence for every behavioral change, coverage at the lowest useful layer plus every affected higher layer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Make the testing-policy adoption status consistent across all governance files.

The agent guides describe testing.md as binding, while its embedded policy remains “Proposed” and effective only “Upon adoption.”

  • AGENTS.md#L15-L21: state the local adoption status and effective date consistently.
  • AGENTS.md#L125-L128: align the “Binding policy” wording with that status.
  • CLAUDE.md#L17-L23: align the governance reference with the adopted status.
  • CLAUDE.md#L163-L167: align the section title and introduction.
  • testing.md#L137-L143: mark the policy adopted locally, or explicitly document why it remains proposed.
📍 Affects 3 files
  • AGENTS.md#L15-L21 (this comment)
  • AGENTS.md#L125-L128
  • CLAUDE.md#L17-L23
  • CLAUDE.md#L163-L167
  • testing.md#L137-L143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 15 - 21, Make the testing-policy adoption status
consistent across all governance references: update AGENTS.md lines 15-21 and
125-128, CLAUDE.md lines 17-23 and 163-167, and testing.md lines 137-143 to
state that the policy is locally adopted and effective, including a consistent
effective date; alternatively, explicitly document why it remains proposed
across every site. Preserve the existing testing requirements while aligning the
policy wording, status, and section titles.

Comment thread Docs/development/concurrency-phase-plan.md Outdated
Comment thread Docs/development/concurrency-phase-plan.md Outdated
Comment on lines +66 to +71
> **Containment:** *runtime-error* containment is tested (an erroring handler
> does not kill the server). *Panic* containment is by construction via
> `catch_unwind` on each handler future but is **not** covered by a dedicated
> test — a deterministic WFL-level Rust panic is not readily expressible from
> the language, so the guarantee rests on the wrapper + the `panic = "unwind"`
> gate, not a regression test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Add a dedicated panic-containment regression test.

Concurrency and lifecycle changes are R3, so relying only on catch_unwind and panic = "unwind" leaves server survival and sibling containment unverified. Add a Rust-level injected-panic test at the handler boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Docs/development/concurrency-phase-plan.md` around lines 66 - 71, Add a
dedicated Rust-level regression test at the handler boundary that injects a
panic in one handler, verifies the server remains alive, and confirms sibling
handlers continue operating. Keep the existing runtime-error test and validate
containment through the actual catch_unwind path, rather than relying only on
the panic = "unwind" configuration.

Source: Coding guidelines

Comment thread testing.md Outdated
Comment thread testing.md
Comment thread testing.md
Comment on lines +94 to +100
### Coverage & budgets

- Baseline: the policy's defaults (≥80% line / 70% branch overall; ≥90/85 on
changed code) are the target. **Known gap:** the repo does not yet run an
automated coverage gate in CI; establishing one is a tracked conformance item
(§19). Until then, changed behavior MUST still ship behavior/boundary/negative
tests at the lowest useful layer plus every affected higher layer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Do not leave mandatory gates as unticketed “known gaps.”

The embedded policy requires coverage thresholds and a release-candidate gate, but the profile downgrades coverage to a target and acknowledges no release-candidate gate without per-gap ticket IDs, owners, deadlines, or a time-limited Section 17 exception. Add the gates or record compliant exceptions before calling the policy adopted.

Also applies to: 125-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing.md` around lines 94 - 100, Update the “Coverage & budgets” policy and
the related release-candidate gate section so mandatory coverage and release
gates are either implemented or documented as compliant exceptions. For each
remaining gap, include a ticket ID, owner, deadline, and time-limited Section 17
exception; do not leave “known gap” or target-only language without those
details.

…d-collision leak)

Address PR #641 review (P1 #8 + Copilot bind-collision leak).

`wait for next chunk|line as <name>` and the `open ... stream response as <name>`
/ `start streaming response as <name>` object binds used `Environment::define`,
which errors if the name already exists in scope. Re-reading into the same
variable in one scope (or a recycled `main loop` env) therefore failed on the
second read — the normal streaming loop pattern. Switch all streaming bindings to
`define_or_replace`, matching `wait for request ... as req`.

This also removes the bind-collision leak Copilot flagged: `start streaming
response` committed the response head + stored the body sender before binding the
handle; a binding failure left an unreachable open stream (client hang + leaked
sender). With define_or_replace the bind cannot fail after the head is sent.

Risk class: R2 (streaming boundary behavior). Red→Green evidence: added
`test_next_line_reusing_same_variable_in_one_scope` (observed failing with
"Variable 'line' has already been defined" before the fix; passing after) plus
`test_next_line_count_loop_reusing_same_variable`. Full http_stream_test suite
green (12/12); clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX
Copilot AI review requested due to automatic review settings July 22, 2026 18:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (7)

src/typechecker/mod.rs:937

  • The type error message for HttpStreamStatement body is inconsistent with the accepted types (Text/Number/Boolean). As written, it claims only text is allowed, and the diagnostic also reports Expected Text, which can confuse users when the runtime accepts numbers/bools by stringifying them.
                        self.type_error(
                            "HTTP request body must be text".to_string(),
                            Some(Type::Text),
                            Some(body_type),
                            *_line,

src/interpreter/mod.rs:3756

  • main loop concurrently: runs multiple handler executions against the same Interpreter instance. The interpreter has per-execution mutable state (e.g. current_count/in_count_loop, call_stack, call_depth) that is mutated and then awaited on inside loops/calls; concurrent handlers can interleave and overwrite each other’s state, producing incorrect count behavior and corrupted call-stack/depth tracking.
            // Refill to the concurrency cap. Each iteration gets a fresh isolated
            // scope so concurrent requests never clobber each other's variables.
            while futs.len() < cap {
                let scope = Environment::new_child_env(env);
                futs.push(

tests/http_server_streaming_test.rs:109

  • This test uses a hard-coded port. Rust tests run in parallel by default, so fixed ports can collide with other tests or processes and make CI flaky. Prefer asking the OS for an ephemeral free port.
    let port = 8231;

tests/http_server_streaming_test.rs:150

  • This test uses a hard-coded port. Rust tests run in parallel by default, so fixed ports can collide with other tests or processes and make CI flaky. Prefer asking the OS for an ephemeral free port.
    let port = 8232;

tests/concurrent_main_loop_test.rs:97

  • This test uses a hard-coded port. Rust tests run in parallel by default, so fixed ports can collide with other tests or processes and make CI flaky. Prefer asking the OS for an ephemeral free port.
    let port = 8341;

tests/concurrent_main_loop_test.rs:135

  • This test uses a hard-coded port. Rust tests run in parallel by default, so fixed ports can collide with other tests or processes and make CI flaky. Prefer asking the OS for an ephemeral free port.
    let port = 8342;

tests/concurrent_main_loop_test.rs:183

  • This test uses a hard-coded port. Rust tests run in parallel by default, so fixed ports can collide with other tests or processes and make CI flaky. Prefer asking the OS for an ephemeral free port.
    let port = 8343;

Comment thread src/parser/mod.rs Outdated
Comment on lines +617 to +620
// `flush <out>` — a bare-identifier target merges into the token.
Token::Identifier(id) if id == "flush" || id.starts_with("flush ") => {
self.parse_flush_stream()
}
Comment thread src/interpreter/mod.rs
Comment on lines +3673 to +3694
let value = self.evaluate_expression(source, Rc::clone(env)).await?;
match &value {
Value::Object(obj) => match obj.borrow().get("_stream") {
Some(Value::Text(id)) => Ok(id.to_string()),
_ => Err(RuntimeError::new(
"Expected a streaming response handle (from `stream response as ...`), \
but this value has no open stream"
.to_string(),
line,
column,
)),
},
Value::Text(id) => Ok(id.to_string()),
_ => Err(RuntimeError::new(
format!(
"Expected a streaming response handle, got {}",
value.type_name()
),
line,
column,
)),
}
Comment thread src/interpreter/mod.rs
Comment on lines +3707 to +3728
let value = self.evaluate_expression(target, Rc::clone(env)).await?;
match &value {
Value::Object(obj) => match obj.borrow().get("_server_stream") {
Some(Value::Text(id)) => Ok(id.to_string()),
_ => Err(RuntimeError::new(
"Expected a server response stream (from `start streaming response as ...`), \
but this value has no open stream"
.to_string(),
line,
column,
)),
},
Value::Text(id) => Ok(id.to_string()),
_ => Err(RuntimeError::new(
format!(
"Expected a server response stream handle, got {}",
value.type_name()
),
line,
column,
)),
}
Comment thread src/parser/stmt/io.rs Outdated
Comment on lines +839 to +858
} else {
let left = Expression::Variable(rest, marker_line, marker_column);
match self.cursor.peek().map(|t| &t.token) {
// `<field> of <object>`, e.g. `write line body of msg to out`.
Some(Token::KeywordOf) => {
self.bump_sync(); // Consume "of"
let object = self.parse_primary_expression()?;
Expression::FunctionCall {
function: Box::new(left),
arguments: vec![crate::parser::ast::Argument {
name: None,
value: object,
}],
line: marker_line,
column: marker_column,
}
}
// A bare variable value: the next token starts `to ...`.
_ => left,
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 80 out of 84 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 83 out of 87 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (3)

TestPrograms/subprocess_comprehensive.wfl:29

  • Same portability issue here: spawn command "cargo --version" requires Cargo to be installed. Use the wfl_bin fixture command selected earlier and pass --version via with arguments.
    TestPrograms/subprocess_comprehensive.wfl:78
  • Capturing process output should also avoid assuming cargo exists. Using the repo’s wfl binary with --version keeps this test cross-platform and independent of a Rust toolchain.
    TestPrograms/subprocess_comprehensive.wfl:98
  • These concurrent-process spawns also assume Cargo is installed. Use the wfl_bin fixture command instead so the TestProgram remains runnable anywhere the integration runner’s release binary exists.

// Test 1: Simple Command Execution
display "Test 1: Execute Command"
wait for execute command "echo Hello from subprocess" as cmd_result
wait for execute command "cargo --version" as cmd_result
Ensures existing programs using file writes, custom flush actions, and display folds keep their original behavior. This prevents the new web streaming syntax from incorrectly taking over older code patterns during parsing and JavaScript transpilation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 86 out of 90 changed files in this pull request and generated 2 comments.

Comment on lines +638 to +655
Statement::StreamWriteStatement {
target,
fallback_content: Some(fallback_content),
..
} => {
// The parser preserves both readings of ambiguous classic syntax
// such as `write line note to "f.txt"`. JavaScript has no response
// stream implementation, but it can still emit the pre-streaming
// file-write reading exactly as it did before.
let content_expr = self.transpile_expression(fallback_content)?;
let file_expr = self.transpile_expression(target)?;
Ok(format!(
"{}WFL.file.write({}.path, {});\n",
self.indent(),
file_expr,
content_expr
))
}
Comment thread tests/transpiler_test.rs
let source = "store line note as \"hello\"\nwrite line note to \"f.txt\"";
let js = transpile_wfl(source)
.expect("an ambiguous write with a classic file fallback must transpile");
assert_contains(&js, "WFL.file.write(\"f.txt\".path, line_note);");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants