Fix concurrent handler isolation for #642 (capture, modules, streams, depth) - #643
Conversation
…andlers (#642) Red evidence for issue #642 blocker: `wait for request ... with timeout` evaluates the timeout expression while holding the shared request-receiver mutex, so one handler's awaiting timeout expression parks every concurrent sibling. Fails now with: request stalled ~1.8s behind the sibling's timeout-expression evaluation (assert < 1s). Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…shared receiver (#642) The timeout clause of `wait for request` is an arbitrary WFL expression; evaluating it under the receiver mutex shared by all concurrent handlers let one handler's awaiting timeout expression (user action that sleeps, does I/O, ...) stall every sibling's dequeue. Evaluate it first, then lock. Green for tests/concurrent_timeout_eval_lock_test.rs (red at 3cce244). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
#642) Red evidence for issue #642 blocker: the io_capture stack is a single thread-local shared by every concurrent handler, so a handler that awaits mid-capture leaks sibling output into its buffer and loses its own lines to a sibling's buffer. Fails now with: - /a captured only "ALPHA-1" (its second line landed in /b's buffer) - /cap captured "CHILD-1 NOISE-FROM-SIBLING CHILD-2" Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…handlers (#642) The io_capture stack is thread-local, so interleaved concurrent handlers shared one capture stack: sibling output landed in whichever buffer was innermost, and LIFO guard pops mismatched when handlers finished out of push order. Now: - the capture stack is part of the per-handler RunState swap: each handler's own stack is installed for the duration of each poll, with the ambient stack parked and restored around it; - a fresh handler inherits a clone of the ambient stack, so uncaptured handler output still reaches an enclosing execute-file capture; - CaptureGuard removes its buffer by Rc identity instead of popping blindly, so out-of-order completion and guards dropped while their handler's stack is parked cannot remove another capture's buffer. Green for tests/concurrent_execute_capture_test.rs (red at 3efd592). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
… path base (#642) Red evidence for issue #642 blocker: current_source_file and loading_stack are interpreter-global, so a handler parked mid-include makes a sibling's include of the same module fail with a false 'Circular dependency', and makes a sibling's relative include resolve against the parked handler's module directory. Both /b requests currently get 500. Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…nt handlers (#642) current_source_file and loading_stack move into the per-handler RunState swap, so each concurrent handler resolves relative module paths against its own context and cycle/import-depth checks see only its own in-flight loads (plus inherited enclosing loads, which remain true cycles). ModuleLoadGuard now restores the source file only when its module is still the installed context and removes its stack entry by value, so a guard dropped while its handler's context is parked cannot clobber the ambient context. Green for tests/concurrent_module_loading_test.rs (red at bc8657f). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…esponse stream (#642) Red evidence for issue #642 blocker: stream verbs act on the global stream map with no ownership check, so a handler holding another handler's stream handle (via a shared global) can inject bytes into the sibling's body, flush it, or close it mid-response. Fails now with 'write:allowed flush:allowed close:allowed'. Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…se (#642) Stream verbs previously acted on the global stream map with no ownership check, so a handler holding a sibling's stream handle (via a shared global) could inject bytes into the sibling's response, flush it, or close it mid-response — and because StreamWriteStatement clones the sender out of the map before awaiting, a sibling's close did not even stop an in-flight write. write/flush/close now require the handle to be in the current handler's open-stream list; a live stream owned by another handler reports ownership, a missing one keeps the closed-stream error, and re-closing one's own already-closed stream stays a no-op. With ownership enforced, write and close on the same stream can no longer race across handlers (a handler is sequential within itself). Green for tests/concurrent_stream_ownership_test.rs (red at c262e44). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…t live depth (#642) Red evidence for issue #642 follow-up: a handler under a concurrent loop nested in user actions seeds call_depth from base_call_depth (run entry) instead of the live depth at loop entry, so enclosing frames go uncounted and the depth ceiling no longer bounds the native stack (an early variant of this test aborted with a real stack overflow on a 2MB thread). Fails now with 'not-limited' vs expected 'depth-limited'. Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
… futures under their own state (#642) Handlers under a nested `main loop concurrently:` seeded call_depth from base_call_depth (run entry, usually 0), discarding the live enclosing action frames — so the depth ceiling stopped bounding the native stack (reproducibly overflowing a 2MB thread in debug builds). Handlers now seed from the live depth at loop entry, mirroring execute-file's child seeding. IsolatedHandler::drop now takes the handler future and drops it with the handler's own run state swapped in: RAII guards inside a future dropped mid-suspend (call-depth, capture, module-load) previously unwound against whatever ambient state was installed at teardown, corrupting the enclosing context's accounting. Green for tests/concurrent_recursion_depth_test.rs (red at f604d46). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…tinuations (#642) Red evidence for issue #642 follow-up: the bare-marker guard in parse_write_to_statement enumerates continuation tokens by hand and is missing KeywordStarts, KeywordEnds, and Colon relative to the binary continuation parser, so 'write line starts with ...' silently mis-parses into the streaming branch and 'write line : to ...' parse-fails, while the identical programs with any other variable name still work. 4 new tests fail now. Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…write line|chunk (#642) The bare-marker guard now includes KeywordStarts, KeywordEnds, and Colon, matching the operator arms of parse_binary_continuation_inner, so classic programs like 'write line starts with "/" to f' and 'write line : to f' keep their pre-streaming file-write meaning instead of mis-parsing into the streaming branch. Comment now ties the set to the binary parser as the authoritative list. Green for the 4 tests added in 294aaac. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…acy side effects (#642) Red evidence for issue #642 follow-up: pre-streaming these forms were TWO statements (bare 'flush' expression statement + the operand expression statement). The current fallback evaluates only Variable("flush") and discards the operand, so its side effects (a call) no longer fire. 3 new tests fail now with only FLUSH_RAN in the output. Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…cy statement (#642) Pre-streaming, these exact forms were two statements: the bare 'flush' expression statement followed by the operand expression statement. The fallback now executes both in the original order when the bare 'flush' binding exists (exact form only — merged forms' fallback already spans the operand), and the analyzer/typechecker validate the operand the same way, keeping all three passes aligned with the runtime. Green for the 3 tests added in 6fe8e86. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
… shadowed in checker scope (#642) Red evidence for issue #642 follow-ups: - RepeatUntilLoop's typechecker arm infers the condition BEFORE the body (runtime runs body first, same scope, no fixed point), so a body statement retyping a binding (e.g. start streaming response ... as out) is invisible to the condition — a guaranteed runtime type error passes the checker. - WebSocketHandlerStatement's typechecker arm pushes a scope but never defines the binding symbol, so the handler body is checked against an outer same-named symbol's concrete type — flagging runtime-valid programs (Cannot index into Number). Base: 62a1b30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…g in checker scope (#642) - RepeatUntilLoop now checks the body first under a backedge fixed point and then the condition against the POST-body type state (a new check_loop_body_fixed_point_post_body keeps the body's final state installed — repeat-until always runs the body before the condition, so the header-state restore used by while would hide body retypings). - WebSocketHandlerStatement recreates its binding symbol (Type::Unknown) inside the pushed checker scope, shadowing an outer same-named symbol the way runtime define_direct does, so handler bodies are no longer checked against the outer symbol's concrete type. Green for the 3 tests added in c16d7ba. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…rship (#642) - web-servers.md: the concurrently isolation bullet now enumerates what is handler-local (count-loop state, live recursion depth, execute-file capture, module loading) and notes timeout expressions are evaluated before contending for the next request; new Ownership note for streaming responses. - Dev Diary entry mapping each #642 item to its Red/Green commits and recording the deferred try/finally alias-scope decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe PR isolates concurrent handler execution state, enforces response-stream ownership, moves timeout evaluation outside receiver locking, preserves legacy IO behavior, aligns typechecking with runtime semantics, and adds documentation and regression tests. ChangesLifecycle and concurrency
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Client
participant RequestHandler
participant RunState
participant ResponseStream
Client->>RequestHandler: send concurrent request
RequestHandler->>RunState: install handler-local context
RequestHandler->>ResponseStream: write, flush, or close
ResponseStream-->>RequestHandler: allow owner or return ownership error
RequestHandler-->>Client: return response
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR addresses remaining correctness and backward-compatibility issues related to issue #642, focusing on isolating concurrent handler execution under main loop concurrently: and restoring pre-streaming parsing/typechecking behaviors where concurrency/streaming changes regressed legacy programs.
Changes:
- Hardened concurrent-handler isolation in the interpreter (handler-local capture/module-load context, explicit handler-future drop ordering, response-stream ownership checks, timeout evaluation ordering, recursion depth seeding).
- Fixed parser/typechecker/analyzer alignment for legacy-compatible forms (
write line|chunk …, exactflush (…)/flush call …,repeat untilruntime order, WebSocket handler binding shadowing). - Added targeted Red→Green regressions plus updated docs and a dev diary entry documenting the fixes.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/write_web_postfix_test.rs | Adds regression coverage for classic `write line |
| tests/typechecker_websocket_binding_scope_test.rs | Regression to ensure WebSocket handler binding shadows outer symbols in typechecking. |
| tests/typechecker_repeat_until_backedge_test.rs | Regression for repeat until body-first typechecking with correct backedge behavior. |
| tests/flush_action_backcompat_test.rs | Regression ensuring exact legacy flush (…) / flush call … still evaluates operand side effects. |
| tests/concurrent_timeout_eval_lock_test.rs | Regression ensuring timeout expressions don’t stall sibling handlers by holding the receiver lock. |
| tests/concurrent_stream_ownership_test.rs | Regression ensuring response streams are handler-owned (no sibling write/flush/close). |
| tests/concurrent_recursion_depth_test.rs | Regression ensuring handler recursion budget includes enclosing action frames. |
| tests/concurrent_module_loading_test.rs | Regression ensuring handler-local module loading context (no false cycles / wrong relative base). |
| tests/concurrent_execute_capture_test.rs | Regression ensuring output capture is handler-local (no cross-wired capture buffers). |
| src/typechecker/mod.rs | Implements repeat until body-first fixed-point checking; restores legacy flush operand checks; fixes WS handler binding recreation. |
| src/parser/stmt/io.rs | Extends classic-continuation guard tokens for `write line |
| src/interpreter/mod.rs | Adds per-handler run-state swapping for capture/module-load context, explicit handler drop ordering, response-stream ownership enforcement, timeout eval ordering, live-depth seeding. |
| src/interpreter/io_capture.rs | Makes capture guards identity-based; adds stack swap/snapshot for per-handler capture isolation. |
| src/analyzer/mod.rs | Analyzes legacy flush operand expression for exact forms to preserve name validation. |
| Docs/04-advanced-features/web-servers.md | Documents concurrency isolation details and response-stream ownership semantics. |
| Dev diary/2026-07-25-issue-642-remaining-blockers.md | Records Red→Green evidence and rationale for the #642 follow-up fixes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// interpreter's own list under serial execution). Stream verbs | ||
| /// (`write`/`flush`/`close`) require ownership, so a handler holding a | ||
| /// sibling's stream handle (e.g. through a shared global) cannot inject | ||
| /// into, flush, or truncate the sibling's response (#642). | ||
| fn owns_response_stream(&self, handle_id: &str) -> bool { |
| // Re-closing an already-closed own stream stays a | ||
| // silent no-op (the live-elsewhere case errors). | ||
| if !self.owns_response_stream(&id) | ||
| && self.server_response_streams.borrow().contains_key(&id) | ||
| { |
There was a problem hiding this comment.
Addressed by documenting the actual guarantee rather than adding post-close ownership tracking (627f679 for the user docs, 7553a6a for the owns_response_stream/close-site comments — this thread and the two sibling comments were all the same nuance).
Rationale for not tracking previously-owned/closed ids: the enforced invariant is that a live response can't be injected into, flushed, or truncated by a non-owner — once the stream is closed there is no response left to protect, so a stale-handle close is a harmless idempotent no-op for any holder. Per-handler closed-id tracking would add unbounded per-handler state and would break the serial loop's existing idempotent re-close across iterations (the per-iteration drain clears the open list, so iteration N+1's re-close of an iteration-N stream would start erroring). write/flush on a closed stream still fail with the closed-stream error.
Generated by Claude Code
| **Ownership:** a response stream belongs to the handler that started it. Under | ||
| `main loop concurrently:`, `write`, `flush`, and `close` on a stream handle that | ||
| another handler owns (for example one shared through a global variable) fail | ||
| with a catchable error instead of injecting into — or truncating — the owner's | ||
| response. Re-`close`-ing a stream you own that is already closed stays a silent | ||
| no-op. |
#642) Copilot review on PR #643: ownership is enforced for LIVE streams; once a stream is closed there is no response left to protect — write/flush keep the closed-stream error and close is an idempotent no-op for any handler holding the stale handle. Say exactly that instead of overstating the close guarantee. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0163817603
ℹ️ 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".
| let timeout_duration = if let Some(timeout_expr) = timeout { | ||
| let timeout_val = self | ||
| .evaluate_expression(timeout_expr, Rc::clone(&env)) | ||
| .await?; |
There was a problem hiding this comment.
Avoid eagerly evaluating every handler timeout
When main loop concurrently begins with wait ... with timeout call f, all 256 prefilled handler futures can reach this evaluation before any request is dequeued, so an idle server now invokes f once per handler slot. This bypasses the first-wait invariant documented in src/analyzer/mod.rs:1241-1260 to prevent speculative pre-request side effects; for example, a timeout action that increments state or performs external I/O fires roughly 256 times instead of once before the first request. Avoid holding the receiver lock across a slow evaluation without allowing every queued handler to evaluate the timeout eagerly.
AGENTS.md reference: AGENTS.md:L145-L148
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged, but keeping the current behavior deliberately — the two goals are in genuine tension. Any scheme that serializes timeout-expression evaluation (the old accidental behavior, or a separate eval mutex) recreates the P1 this fixes: one handler's awaiting expression stops every sibling from dequeuing, which was a merge-blocker in #642. The eval must therefore happen outside any shared lock, and each of the N handler iterations evaluates its own operand when it starts — the burst is inherent to N concurrent iterations, not to the lock reorder (each iteration always evaluated it eventually; only the timing/parallelism changed).
The first-wait analyzer invariant still holds for its stated purpose (no user statements run per-slot before a request); the timeout operand is part of the wait itself. Documented in 7553a6a: web-servers.md now states each slot evaluates its own timeout expression at iteration start and that timeout expressions should stay side-effect free. If eval-once-per-loop-entry caching semantics are preferred for the operand, that's a language-semantics decision for the maintainer — happy to implement it as a follow-up if wanted.
Generated by Claude Code
| // same backedge fixed point as `while`/`repeat while`, but | ||
| // keeping the post-body state) so body retypings are visible | ||
| // to the condition (#642). | ||
| self.check_loop_body_fixed_point_post_body(body); |
There was a problem hiding this comment.
Exclude break-only states from repeat conditions
When a repeat until body retypes a binding and then executes break, the runtime exits at the break without evaluating the condition, but this unconditional post-body state makes the checker validate the condition against that retyped binding. For example, a body that starts a response stream as out and then breaks is runtime-valid even if the condition compares the original numeric out, yet it now produces a type error; such errors are fatal while loading modules. Track only condition-reaching body paths for condition inference while retaining break-path state for code after the loop.
AGENTS.md reference: AGENTS.md:L19-L22
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed — load module does treat type errors as fatal, so this was a real compat hazard, not just imprecision. As of 7553a6a (red test in c6e719c): when the body subtree contains an early exit (break/exit/return, including inside if/try/nested blocks), the condition — and post-loop state — is checked against the join of the stabilized header and the post-body state, so a retype-then-break body typechecks cleanly again. Straight-line bodies keep the precise post-body check (the original #642 soundness fix). The scan is deliberately conservative: a nested loop's break also softens, which can only under-report, never falsely reject.
Generated by Claude Code
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/concurrent_module_loading_test.rs (1)
22-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the duplicated server harness into
tests/common. All five new concurrency tests copy the samestart_server_thread/wait_for_server/shutdowntrio, and each already declaresmod commonforfree_tcp_port— so the shared module is the obvious home. Parameterizing stack size and optional source file covers every current variant.
tests/concurrent_module_loading_test.rs#L22-L65: movestart_server_thread(with itsOption<PathBuf>source-file parameter),wait_for_server,shutdown, andwfl_pathintotests/common.tests/concurrent_recursion_depth_test.rs#L26-L73: drop the local copies and call the shared helper, passing the 16 MiB stack size (keep the stack-size rationale comment at the call site).tests/concurrent_execute_capture_test.rs#L26-L62: drop the local copies in favor of the shared helpers; keepwrite_childlocal.tests/concurrent_stream_ownership_test.rs#L23-L59: drop the local copies in favor of the shared helpers.tests/concurrent_timeout_eval_lock_test.rs#L26-L68: drop the local copies in favor of the shared helpers; the doc comments on readiness/shutdown can move with them.As per coding guidelines, "Place Rust unit and integration tests under
tests/, using feature-oriented names such as*_test.rs" — a sharedcommonmodule keeps the feature-named test files focused on their scenario.🤖 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_module_loading_test.rs` around lines 22 - 65, Move the duplicated start_server_thread, wait_for_server, shutdown, and wfl_path helpers into tests/common, preserving optional source-file handling and parameterizing stack size as needed. In tests/concurrent_module_loading_test.rs:22-65, relocate the helpers; in tests/concurrent_recursion_depth_test.rs:26-73, tests/concurrent_execute_capture_test.rs:26-62, tests/concurrent_stream_ownership_test.rs:23-59, and tests/concurrent_timeout_eval_lock_test.rs:26-68, remove local copies and use the shared helpers, retaining the recursion stack-size rationale, write_child, and applicable readiness/shutdown documentation at their respective sites.Source: Coding guidelines
🤖 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.
Nitpick comments:
In `@tests/concurrent_module_loading_test.rs`:
- Around line 22-65: Move the duplicated start_server_thread, wait_for_server,
shutdown, and wfl_path helpers into tests/common, preserving optional
source-file handling and parameterizing stack size as needed. In
tests/concurrent_module_loading_test.rs:22-65, relocate the helpers; in
tests/concurrent_recursion_depth_test.rs:26-73,
tests/concurrent_execute_capture_test.rs:26-62,
tests/concurrent_stream_ownership_test.rs:23-59, and
tests/concurrent_timeout_eval_lock_test.rs:26-68, remove local copies and use
the shared helpers, retaining the recursion stack-size rationale, write_child,
and applicable readiness/shutdown documentation at their respective sites.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 21209f93-f2fd-4135-8f03-5ad1de32b480
📒 Files selected for processing (16)
Dev diary/2026-07-25-issue-642-remaining-blockers.mdDocs/04-advanced-features/web-servers.mdsrc/analyzer/mod.rssrc/interpreter/io_capture.rssrc/interpreter/mod.rssrc/parser/stmt/io.rssrc/typechecker/mod.rstests/concurrent_execute_capture_test.rstests/concurrent_module_loading_test.rstests/concurrent_recursion_depth_test.rstests/concurrent_stream_ownership_test.rstests/concurrent_timeout_eval_lock_test.rstests/flush_action_backcompat_test.rstests/typechecker_repeat_until_backedge_test.rstests/typechecker_websocket_binding_scope_test.rstests/write_web_postfix_test.rs
| assert!( | ||
| typecheck(code).is_ok(), | ||
| "a well-typed repeat-until loop must not be rejected: {:?}", | ||
| typecheck(code) | ||
| ); |
…n types (#642) Codex review on PR #643: runtime exits at break without evaluating the condition, and type errors are fatal inside load module, so checking the condition strictly against post-body state falsely rejects a runtime- valid retype-then-break body. Also dedupes a double typecheck() call in the happy-path assertion (Copilot nit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…w feedback (#642) PR #643 review feedback: - Codex: a body that can break/exit/return skips the repeat-until condition at runtime, and type errors are fatal inside load module — check the condition (and post-loop state) against the join of header and post-body state when the body subtree contains an early exit, keeping the precise post-body check for straight-line bodies. Green for the red test in c6e719c. - Copilot: owns_response_stream / close-site comments now state the live-stream nuance (close on an already-closed stream is an idempotent no-op for any holder) instead of overstating the close guarantee. - Codex: document that each concurrent handler slot evaluates its own timeout expression at iteration start, so timeout expressions should stay side-effect free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
| /// Whether executing `body` can leave its enclosing loop without reaching | ||
| /// the loop's own condition: a `break`, `exit`, or `return` anywhere in | ||
| /// the statement subtree. Deliberately conservative — a `break` inside a | ||
| /// NESTED loop only exits that inner loop, but treating it as early exit | ||
| /// merely softens condition checking back to the joined state, it never | ||
| /// rejects a valid program. |
There was a problem hiding this comment.
Fixed in eb02330 (red test in 0b3c6c3). The walker now stops descending for break at nested loop boundaries — a nested loop's body is scanned only for exit loop/return, which the runtime re-raises through every enclosing loop's control-flow dispatch (ControlFlow::Exit/Return), while break is absorbed by the nearest loop. Two new regressions pin both directions: a nested-loop break keeps the precise post-body condition check (the retyped binding is reported again), and a nested exit loop still softens.
Generated by Claude Code
…condition (#642) Copilot review on PR #643: a break inside a nested loop only exits that inner loop, so the outer repeat-until body falls through to its condition and the precise post-body check must apply — over-broad softening hides real type errors. exit loop propagates through every enclosing loop and must keep softening (companion guard test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
…n escape any depth (#642) Copilot review on PR #643: the early-exit walker now distinguishes break (absorbed by the nearest loop — nested loops no longer soften the outer condition) from exit loop/return (re-raised by every loop's control-flow dispatch, so they count from any nesting depth). Restores the precise post-body condition check for bodies whose only break is in a nested loop. Green for the red test in 0b3c6c3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
Summary
Completes the remaining work from issue #642 (re-review of the merged #641 streaming/concurrency head): four release-blockers for concurrent web serving, the follow-up correctness/compatibility items, and the PR-review feedback rounds. Every behavioral change landed as a Red test-only commit that is an ancestor of its Green fix (testing.md §6.2 path 1); pairs are listed under Test evidence below.
Key Changes
Runtime isolation (concurrent handler safety)
src/interpreter/io_capture.rs,src/interpreter/mod.rs) — theexecute file ... and read outputcapture stack joins the per-handlerRunStateswap; handlers inherit a clone of the ambient stack so output still reaches an enclosing capture;CaptureGuardremoves its buffer byRcidentity instead of a blind pop. Regression:tests/concurrent_execute_capture_test.rs.src/interpreter/mod.rs) —current_source_fileandloading_stackjoin theRunStateswap, so relativeinclude/load modulepaths resolve against the handler's own context and a sibling's in-flight load is never misreported as a cycle;ModuleLoadGuardrestores/removes by identity. Regression:tests/concurrent_module_loading_test.rs.src/interpreter/mod.rs) —write/flush/closeon a live stream require it to be in the current handler's open list, so a handler holding a sibling's handle (e.g. via a shared global) cannot inject into, flush, or truncate the sibling's response. A closed stream keeps the closed-stream error for write/flush;closeon a stale handle stays an idempotent no-op (nothing live left to protect). Regression:tests/concurrent_stream_ownership_test.rs.src/interpreter/mod.rs) —wait for request ... with timeout <expr>evaluates the expression before locking the shared receiver, so one handler's awaiting expression no longer stalls every sibling's dequeue. Each concurrent slot evaluates its own operand at iteration start (documented). Regression:tests/concurrent_timeout_eval_lock_test.rs.src/interpreter/mod.rs) — handlers seedcall_depthfrom the live depth at loop entry (mirroringexecute filechild seeding), so enclosing action frames stay counted; an early test variant reproduced a real native stack overflow.IsolatedHandler::dropnow drops the handler future with the handler's own run state installed, so RAII guards in a future dropped mid-suspend unwind against the right context. Regression:tests/concurrent_recursion_depth_test.rs.Parser/typechecker/analyzer compatibility
write line|chunkcontinuations (src/parser/stmt/io.rs) — the bare-marker guard now includesstarts/ends/:(the arms it was missing relative toparse_binary_continuation_inner), sowrite line starts with "/" to fandwrite line : to fkeep their pre-streaming file-write meaning. Regression:tests/write_web_postfix_test.rs.flush (…)/flush call …(src/interpreter/mod.rs,src/analyzer/mod.rs,src/typechecker/mod.rs) — these were two legacy statements (bareflush+ the operand); the fallback now evaluates both in order when the bareflushbinding exists, and all three passes validate the operand. Regression:tests/flush_action_backcompat_test.rs.repeat untilruntime-order typechecking (src/typechecker/mod.rs) — body checked first under a backedge fixed point, then the condition against post-body state; bodies containing an early exit (break/exit/return) soften the condition to the header⊔post-body join, since runtime skips the condition on those paths and type errors are fatal inload module. Regressions:tests/typechecker_repeat_until_backedge_test.rs.src/typechecker/mod.rs) — the binding symbol is recreated (Type::Unknown) in the handler's checker scope, shadowing an outer same-named symbol the way runtimedefine_directdoes. Regression:tests/typechecker_websocket_binding_scope_test.rs.Docs & process
Docs/04-advanced-features/web-servers.md: whatconcurrentlyisolates per handler; response-stream ownership semantics (including exact close-after-close behavior); per-slot timeout-expression evaluation guidance.Dev diary/2026-07-25-issue-642-remaining-blockers.md: full Red→Green map and rationale.finally; analyzer/typechecker deliberately resolve the outer binding there, and each direction is locked in by existing tests — aligning either way is a breaking decision (details in the Dev Diary).Test evidence
62a1b30):01ee863→4556a6591ffee1→952e270822778d→cfd34403ba71e7→1f71935bd183ad→0320d24dff4f3b→c4f74bccd7bef4→daca8c7f2e28d9→59a71cdc6e719c→7553a6acargo fmt --all -- --check;cargo clippy --all-targets --all-features -- -D warnings;cargo test --workspace(all green);./scripts/run_integration_tests.sh— TestPrograms walk 111 passed / 0 failed / 24 documented skips (backward-compat gate §11.6);./scripts/run_web_tests.sh— 3/3;python scripts/validate_docs_examples.py --force— 18/18.tests/commonhelper would emit dead-code warnings in the other suites that includecommon).https://claude.ai/code/session_01DPs45HiGQ4RQzDdihoti4K
<img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review">Summary by CodeRabbit
flushbehavior and improved classic write syntax compatibility.