Add Rust installation script - #4
Merged
Merged
Conversation
Co-Authored-By: bsbyrd@logbie.com <bsbyrd@logbie.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
⚙️ Control Options:
|
Co-Authored-By: bsbyrd@logbie.com <bsbyrd@logbie.com>
Co-Authored-By: bsbyrd@logbie.com <bsbyrd@logbie.com>
…rror handling Co-Authored-By: bsbyrd@logbie.com <bsbyrd@logbie.com>
Co-Authored-By: bsbyrd@logbie.com <bsbyrd@logbie.com>
devin-ai-integration Bot
added a commit
that referenced
this pull request
Apr 19, 2025
…nce cycle Co-Authored-By: bsbyrd@logbie.com <bsbyrd@logbie.com>
logbie
pushed a commit
that referenced
this pull request
Jul 12, 2026
Round-3 maintainer review (web findings): - (#4) Cap the WebSocket transport itself: apply max_ws_message_bytes via warp Ws::max_message_size/max_frame_size before on_upgrade, so a fragmented text frame or ignored binary frame cannot allocate up to Tungstenite's defaults on the receive side. The queued-byte permit remains the global layer. - (#6) Fail closed when the Connect event cannot be admitted: unregister the connection and close the socket instead of leaving a live socket whose connect handler (app init/auth) never ran but which could still emit Message events. - (#11) Reserve the WS queued-byte permit on the *borrowed* payload length (ws_message_byte_len) before materializing the String, on both send and broadcast, so an oversized runtime value is never fully cloned first; an oversized broadcast is rejected before it is materialized once. - (#14) Drop cleanup for WflWebServer and WflWebSocketServer: abort the accept task (and signal WS close) so a dropped/replaced/setup-errored server can't orphan a bound listener; `close server` .take()s the handle so no double-abort. - (#5) Carry the global admission RequestGuard *inside* the queued WflHttpRequest (no longer Clone), so a queued body counts against the pending ceiling until the interpreter dequeues/drops it — a timed-out route future no longer un-accounts a still-queued body. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A
logbie
added a commit
that referenced
this pull request
Jul 12, 2026
…609) * feat: add shared ExecutionBudget consolidating runtime resource caps Replace a dozen isolated, per-subsystem resource ceilings (and several that were simply unbounded) with one coherent ExecutionBudget object that travels with a run through parsing, evaluation, pattern matching, web handling, and module loading. The budget owns every dimension: - Deadline and cooperative cancellation (was Interpreter.max_duration/op_count) - Interpreter operation ceiling (new, opt-in; default unlimited) - Recursion and import depth (recursion was unguarded in release) - execute-file depth (was MAX_EXECUTE_FILE_DEPTH) - Pattern transitions and active states (was MAX_STEPS; states now bounded) - Source, request-body, and response byte caps (source/response were unbounded) - Pending HTTP requests (was web_server_request_queue_bound) - WebSocket queue and connection limits (were unbounded channels/registry) Design: - src/exec/budget.rs: ExecutionBudget is Send+Sync (atomics only), so an Arc clones into the multi-threaded web transport without any Rc/RefCell crossing a thread boundary. The interpreter core stays !Send. BudgetLimits maps the existing .wflcfg keys plus nine new budget keys; BudgetExceeded keeps the historic timeout wording/ErrorKind so existing handling matches. - The interpreter runs on a dedicated 1 GiB-stack thread so max_call_depth (default 1000) turns runaway recursion into a clean, catchable error instead of a native stack overflow (an 8 MiB stack overflows near depth 40). Backward compatibility: the three pre-existing knobs keep their defaults exactly; new ceilings default generously; max_operations is off by default. All 515 lib tests, the new budget suite, and 107 TestPrograms pass. Docs: configuration-reference (new keys + Execution budget section), web-servers (response/WebSocket limits), and a Dev Diary entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: address PR review — budget spans execute-file, nested source caps, catchable-error stack Follow-up fixes from automated PR review on #609: - Share the ExecutionBudget with the `execute file` child interpreter (clone the parent Arc) so the deadline, operation ceiling, and cancellation span the whole run instead of resetting per executed file. - Enforce max_source_size on nested sources (load module / include / execute file) via file metadata *before* reading; the CLI top-level check now also reads metadata first, so oversized sources are refused without allocating. - budget_error only force-clears the call stack for the terminal deadline; a catchable ResourceLimit (e.g. the recursion ceiling) leaves the stack for call_function to unwind, so the depth counter stays correct after a caught recursion error under try/when. - Pattern VM checks the active-state ceiling after each expansion (fail fast) rather than only once the next generation is fully built. - Doc/comment accuracy: PatternVM::new no longer references the removed MAX_STEPS; the VM error-map comment no longer implies deadline sampling; the CLI comment says "same config" not "same budget instance"; and the duplicate "Execution budget (resource limits)" doc heading was renamed for a unique anchor. New regression tests: nested_execute_file_source_is_size_checked and execute_file_shares_the_parent_operation_budget. Full lib suite, budget suite, execute-file suite, and 107 TestPrograms pass; fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(test): use forward-slash paths in execute-file budget tests (Windows CI) WFL treats backslash as an escape character in string literals, so embedding a Windows path (from Path::display()) into an `execute file at "..."` statement broke parsing on windows-latest. Normalize embedded paths to forward slashes, which the runtime accepts on every platform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): one budget per run + bounded source loader for every entry point Addresses maintainer P1-4, P1-5 and related Copilot findings. - One ExecutionBudget per run: main.rs builds a single budget from the timeout-capped run_config up front and reuses it for the pre-parse source check AND the interpreter (new Interpreter::with_config_and_budget + Interpreter::budget() cancellation handle). Its deadline clock now covers lexing/parsing/analysis/interpretation, and execute-file already shares it. - Bounded source loader: read at most max_source_size+1 bytes (CLI, load module, include, execute file, and now the REPL), so an oversized source is refused without allocating it — even when metadata is unavailable, stale, or reports 0 (special files). Replaces the metadata-only enforce_source_size. - u64->usize source length converted safely (overflow = too large). - charge_operation(false) (main-loop exemption) no longer increments the operation counter, so exempt work can't trigger a post-loop Operations breach; cancellation is still honored every call. - Large interpreter stack is no longer a hard prerequisite: --help/--version run on the normal stack, and a failed large-stack reservation (tight RLIMIT_AS / 32-bit) falls back to the default stack instead of refusing to start. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1-1): dedicated RAII recursion-depth counter; don't corrupt state on catch Addresses the maintainer's P1-1 (recursion guard bypassable after a caught limit) and Copilot's count-loop-state finding. - Enforcement recursion depth now lives in a dedicated `call_depth: Cell<usize>` incremented/decremented by an RAII CallDepthGuard in call_function, separate from the diagnostic `call_stack` (which may be force-cleared). The guard restores depth on every unwind, so a caught ResourceLimit can never leave the depth under-counted and pile onto still-live native frames. - budget_error no longer mutates any interpreter state. Every budget breach is catchable by a general try/when, so the call stack, count-loop flags, and recursion depth must unwind naturally; force-clearing them (the historic timeout behavior) corrupted an enclosing count loop and under-counted depth. - interpret() resets in_count_loop/current_count/call_depth up front so an uncaught terminal breach can't leak stale state into a reused interpreter (REPL). Regression test catching_a_recursion_limit_leaves_a_consistent_interpreter: a count loop survives 3 caught recursion errors, `count` stays readable, and re-recursing stays bounded (no stack overflow). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1-2): meter pattern transitions per-instruction on one shared budget Addresses the maintainer's P1-2 and the pattern error-swallowing findings. - Transitions are now charged per *instruction* inside step()'s dispatch loop (and per negative-lookahead iteration), so an epsilon-Jump cycle or a lookaround instruction chain is bounded — not just the outer NFA waves. - ONE shared meter (ExecutionBudget::pattern_steps, reset once per top-level op) is charged by nested lookaround/lookbehind VMs too, so their work counts against the same budget instead of resetting to zero. execute/find/find_all no longer reset; CompiledPattern's top-level methods reset once. - Budget breaches PROPAGATE instead of collapsing into a non-match: the VM's find/find_all return Result, CompiledPattern's *_with_budget return Result, and the interpreter's matches/find operators plus the stdlib pattern builtins (pattern_matches/find/find_all/split) surface a catchable ResourceLimit error. - A thread-local "current budget" (set for the run in interpret()) lets the stdlib pattern builtins — whose native signature has no budget parameter — honour the run's configured max_pattern_steps/max_pattern_states. - max_pattern_steps default raised to 5_000_000 to suit per-instruction granularity (was a per-wave 100_000); ordinary patterns clear it easily. - Active-state fan-out is checked after each expansion, including in the negative-lookahead loop. Tests: pattern_step_limit_is_enforced_and_propagated / _is_catchable / patterns_run_normally_under_default_budget; budget unit tests for the shared meter and the current-budget scope guard. 107 TestPrograms + full lib pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P2): register budget keys with ConfigChecker; document real MSRV - ConfigChecker::new now knows every ExecutionBudget key (plus the previously unregistered web_server_request_queue_bound), so `--configCheck` accepts them and `--configFix` no longer strips them — which for max_operations would have silently reverted a configured ceiling back to unlimited. Round-trip test test_budget_keys_are_known_and_survive_fix covers all new keys. - Record the real MSRV: the codebase uses `let`-chains (stable in Rust 1.88), so Cargo.toml gains rust-version = "1.88" and CLAUDE.md's stale "1.75+" is corrected, resolving the reviewer's MSRV-vs-let-chains concern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1-3): stream HTTP body, global request guard + timeout, reliable WS close Addresses maintainer P1-3 and the remaining P2 web/REPL findings. - Streaming body limit: the request-body ceiling is enforced while the body streams in (warp::body::stream + read_body_capped), so a chunked body with no Content-Length is bounded too — an oversized body gets 413 without being fully buffered. The Content-Length fast-path 413 stays for well-behaved clients. - Global in-flight cap: admission now uses the shared ExecutionBudget RequestGuard, so the accepted-request cap is enforced across all listeners (not per-server). The guard is acquired before the body is read and held until the handler responds, the client disconnects, or the request times out. - Per-request timeout: a dequeued request that is never answered is shed with 504 after web_server_response_timeout_seconds (default 300, 0 disables), freeing its slot instead of pinning it forever. - Response cap checks the borrowed Text/Binary length before duplicating into content_bytes (bounds peak allocation), then re-checks materialized fallbacks. - WebSocket close reliability: the writer sends a best-effort close frame on any exit — explicit Close, empty channel (close server dropped the sender), or a Full queue that couldn't carry Close — so `close server` always terminates the socket. Outbound send/broadcast now log on a full/closed queue (matching docs). - REPL uses a no-deadline budget so later commands aren't rejected by a session-start timeout (each other ceiling still applies; Ctrl-C interrupts). - New web_server_response_timeout_seconds config key (registered with ConfigChecker); docs updated. Full lib + web + 107 TestPrograms pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * docs: record deep-review round (P1-1..P1-5 + P2) in Dev Diary Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): per-match pattern meter with deadline sampling; keep VM API Round-2 maintainer review (pattern findings): - P1 (mod.rs:295): the pattern transition counter no longer lives on the shared run budget. Two matches that share one Arc<ExecutionBudget> (e.g. concurrent web handlers) previously reset each other's meter, letting one grant the other unbounded extra quota. Introduce a per-top-level-match PatternMeter that owns its own step/state counters and is cloned only into nested lookaround/lookbehind VMs; a second match under the same run budget gets an independent meter. - P1 (budget.rs:519): the meter now samples the wall-clock deadline on the transition stride (with the interpreter's main-loop exemption, tracked via a new deadline_exempt flag synced by set_in_main_loop) and propagates it as PatternError::Timeout -> ErrorKind::Timeout, so a single synchronous match cannot run past timeout_seconds and reports the historic timeout error. - P2 (vm.rs:267): active-state slots are reserved/released across the current, next, and nested frontiers via one RAII StateReservation, so the ceiling counts all simultaneously-live states (including under nesting), not just one output vector. - P2 (vm.rs:203): restore the public PatternVM API — find -> Option and find_all -> Vec compatible wrappers, with separately-named fallible try_find/try_find_all entry points; direct top-level ops reset their own meter while private no-reset runners feed nested lookarounds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): bound HTTP request lifetime; prune abandoned requests Round-2 maintainer review (inbound HTTP findings): - P1 (interpreter:5751): establish one deadline at admission and apply its remaining time to the *body read* as well as the response wait. Previously only the response wait was bounded, so an unauthenticated client could open max_pending_requests chunked uploads and trickle bytes under the size cap forever, pinning every global in-flight slot (503 for everyone). A slow body now sheds with 408; the handler wait still sheds with 504, sharing the same deadline. - P1 (interpreter:5836): on timeout the dropped oneshot closes its sender, and the interpreter now (a) skips a dequeued request whose sender is already closed (client gave up) instead of running a zombie handler, and (b) prunes closed entries from pending_responses before registering a new one, so repeated timeouts can't accumulate dead map entries or handler work. - P2 (interpreter:6429): reject composite/opaque response bodies (lists, objects, …) with a clear error instead of materializing an unbounded `{:?}` string past the response cap; take the response sender into an RAII ResponseCompletion guard up front so every fallible step in `respond` resolves the request (500 on early error) rather than leaving the client to hang until timeout. - P2 (interpreter:203): keep read_body_capped's max+1 bound exact — copy at most the remaining allowance plus one sentinel byte from a chunk and return TooLarge without appending the rest, so one large Buf::chunk() can't grow the buffer to max+chunk_len. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): bound WebSocket queued bytes; cancellable close handshake Round-2 maintainer review (WebSocket findings): - P1 (interpreter:373): bound WebSocket memory by BYTES, not only frame count. Add a budget-derived per-message size cap (web_socket_max_message_size, default 1 MiB) enforced on inbound and outbound text frames, plus a global queued-byte ceiling (web_socket_max_queued_bytes, default 16 MiB) reserved per queued frame via an RAII WsBytePermit that releases on send/consume/shed. Previously the event and per-connection outbound queues could each hold ws_queue_bound arbitrarily-large messages, permitting OOM-scale buffering. - P2 (interpreter:354): a close frame alone did not guarantee the reader task (blocked in ws_rx.next()) or its WsConnectionGuard terminated when the peer ignored the close handshake. Give each server a watch-based cancellation channel; the reader now select!s on it so `close server` wakes every connection, with a bounded (250 ms) close-handshake timeout before the writer is force-aborted. Adds config keys web_socket_max_message_size and web_socket_max_queued_bytes (both positive-integer validated; mapped into BudgetLimits). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): share recursion depth across execute-file boundary Round-2 maintainer review (interpreter:7232): a child interpreter spawned by `execute file` started with call_depth = 0 (and interpret() reset it again), so a parent already near max_call_depth could run a child that consumed another full allowance — and nested execute files multiplied the native stack toward 1 GiB-thread overflow before any guard fired. The child now seeds its base recursion depth from the parent's live depth (base_call_depth), and interpret() resets to that base rather than 0, so the combined WFL call depth across nested execute-file runs is bounded by max_call_depth. Regression test execute_file_shares_the_parent_recursion_depth pins that a child cannot get a fresh depth allowance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P2): config checker validates budget keys against loader ranges Round-2 maintainer review (checker.rs:560): the budget keys were registered as generic Integer, validated with parse::<i64>() and no per-key range, so max_operations = -1 or max_call_depth = 0 passed check/fix even though the loader rejects them (leaving max_operations unlimited), and valid u64 values above i64::MAX were wrongly rejected and could be "fixed" to 0/unlimited. Integer keys are now validated as non-negative u64 (the loader's usize/u64 domain), and each budget/web key enforces the loader's exact minimum via integer_min_for_key (max_operations and web_server_response_timeout_seconds allow 0; every other ceiling requires >= 1). Also add the Execution Budget category to get_settings_by_category so the config wizard no longer omits every setting in it, and register the two new web_socket_max_message_size / web_socket_max_queued_bytes keys. New test test_budget_keys_enforce_loader_ranges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1/P2): REPL enforces source cap pre-lex; per-command budget + Ctrl-C Round-2 maintainer review (REPL findings): - P1 (repl.rs:149): enforce the source-size ceiling immediately after appending a line to the input buffer, before it is cloned/lexed/parsed — an oversized incomplete paste was previously retained and re-cloned/re-tokenized on every following line, defeating the pre-lexing cap. Also load the applicable `.wflcfg` (via load_config_with_global) instead of WflConfig::default(), so a configured smaller max_source_size / timeout is honored. - P2 (repl.rs:49): give each command a fresh budget (new wall-clock deadline, cleared counters) via reset_command_budget while preserving the session environment, instead of disabling the deadline for the whole session — so the first runaway command still times out. Wire Ctrl-C during execution to budget.cancel() (cooperative cancellation), racing the command future against tokio::signal::ctrl_c; Ctrl-C while awaiting input is still handled by rustyline. Adds Interpreter::set_budget/config accessors and a REPL budget-reset test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): thread the budget through the front end Round-2 maintainer review (main.rs:828): starting the budget's clock did not make lexing, parsing, analysis, or type checking consult it — none received the budget, so `--analyze` and the dump modes could finish without checking, and a slow/oversized parse could not be cancelled mid-phase; the budget only measured elapsed time for later interpretation. main.rs now installs the run budget as the current-thread budget for the ENTIRE run (source read through interpretation), so every mode consults one budget. The parser polls it once per top-level statement, the type checker once per top-level statement, and the analyzer at its phase boundary — each honoring the deadline and cooperative cancellation and aborting cleanly on breach. The source read is already bounded (read_source_bounded), so lexing/parsing are bounded by max_source_size. New test analyze_mode_consults_the_budget_in_the_ front_end proves --analyze (which never interprets) surfaces a budget breach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * docs: document round-2 budget changes (WS bytes, request deadline, front end) - Configuration reference: add web_socket_max_message_size and web_socket_max_queued_bytes (summary + detailed), fix the stale max_pattern_steps default (5_000_000). - Web servers guide: document the single admission deadline (408 body timeout / 504 handler timeout), abandoned-request pruning, WebSocket per-message and global queued-byte bounds, and the deterministic close-server handshake. - Dev Diary: record the second deep-review round (P1×8 + P2×7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: address round-2 bot review (deadline exemption, yield, checker) - Nested `execute file` now preserves the parent's `deadline_exempt`: the child shares the budget and its interpret() clears the shared flag, which would make a parent still inside its own `main loop` enforce the wall-clock deadline on later pattern matches and time out spuriously. Save/restore around the child. - Add a throttled cooperative `tokio::task::yield_now()` in _execute_statement (outside a `main loop`) so a tight CPU-bound count/while/repeat loop returns control to the runtime — letting the REPL's Ctrl-C → budget.cancel() actually be delivered by the select! (the per-command deadline was already the safety net; this makes manual interruption work). - Config checker: enforce the loader's minimums for timeout_seconds and web_server_max_body_size (both >= 1); extend the range test. - Fix the stale budget_error doc that claimed the deadline force-releases the call stack — the helper mutates no state (every breach unwinds naturally). Skipped two nitpicks with reason: the lookbehind run_execute/run_find_all double-scan is bounded by the generous step ceiling and collapsing it to find_at_position(0) is not semantically equivalent under greedy/non-overlapping matching; the frontier-loop extraction is a pure refactor left to avoid churn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): main-loop exemption as a shared depth counter + RAII guard Round-3 maintainer review (#1,#2,#3,#13): the `main loop` deadline exemption was a bool split between the interpreter (`in_main_loop`) and the budget (`deadline_exempt`), which leaked on caught errors, didn't nest, wasn't inherited across `execute file`, and was snapshotted by the pattern meter. Replace it with one shared `main_loop_depth: AtomicUsize` on the budget plus an RAII `MainLoopGuard` (enter_main_loop): - (#2) the guard restores the depth on EVERY exit — normal, early return, a caught error unwinding through `?`, and nested loops — so the exemption is never leaked or cleared while an outer loop is active. - (#3) a child `execute file` shares the budget, so the parent's active main-loop exemption covers the child and the nested front end; the parser/type-checker/analyzer checkpoints now charge with `!is_deadline_exempt()`, so `execute file` from a server's main-loop handler no longer times out in the nested parse. Removes the brittle save/restore. - (#13) PatternMeter reads the exemption LIVE per sampled stride (no `enforce_deadline` snapshot), so reusing one VM across a main-loop boundary is correct. Test reuses one meter and toggles the budget. - (#1) `_execute_statement` yields on a dedicated per-statement scheduling counter that advances even inside a `main loop` (whose op counter is exempt), so a CPU-only main loop still yields and lets the REPL Ctrl-C fire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): harden WebSocket + HTTP lifecycle and peak allocation Round-3 maintainer review (web findings): - (#4) Cap the WebSocket transport itself: apply max_ws_message_bytes via warp Ws::max_message_size/max_frame_size before on_upgrade, so a fragmented text frame or ignored binary frame cannot allocate up to Tungstenite's defaults on the receive side. The queued-byte permit remains the global layer. - (#6) Fail closed when the Connect event cannot be admitted: unregister the connection and close the socket instead of leaving a live socket whose connect handler (app init/auth) never ran but which could still emit Message events. - (#11) Reserve the WS queued-byte permit on the *borrowed* payload length (ws_message_byte_len) before materializing the String, on both send and broadcast, so an oversized runtime value is never fully cloned first; an oversized broadcast is rejected before it is materialized once. - (#14) Drop cleanup for WflWebServer and WflWebSocketServer: abort the accept task (and signal WS close) so a dropped/replaced/setup-errored server can't orphan a bound listener; `close server` .take()s the handle so no double-abort. - (#5) Carry the global admission RequestGuard *inside* the queued WflHttpRequest (no longer Clone), so a queued body counts against the pending ceiling until the interpreter dequeues/drops it — a timed-out route future no longer un-accounts a still-queued body. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix(P1): scope budget across REPL/front end; fatal type-check budget breach Round-3 maintainer review (scoping + front-end findings): - (#9) The REPL now installs the per-command budget as the current-thread budget for the WHOLE pipeline (lex/parse/analyze/type-check/interpret), not just interpretation, so the front-end checkpoints actually see it (they saw None before, since interpret() installed the guard only for its own phase). - (#10) A shared-budget breach during type checking is propagated on a distinct fatal channel (TypeChecker::take_budget_error); the CLI consults it and stops, instead of printing it as a non-fatal type-check "warning" and continuing. - (#8) More front-end checkpoints: Analyzer::analyze (backs the type checker and every load/include/execute-file) checks the budget at its phase boundary, and parse_statement checks recursively (covering deeply nested block bodies the top-level parse loop never revisits) while advancing a token so the parser's no-progress invariant holds. All checkpoints are exemption-aware. - (P2 repl:95) Check the prospective buffer length with checked_add BEFORE push_str, so a single oversized pasted line is never copied into the buffer. - (P2 docs) Fix the detailed max_pattern_steps default (5000000). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * docs: record round-3 review (main-loop depth, WS/HTTP lifecycle, scoping) Dev Diary: document the third deep-review round — the main-loop-depth + RAII redesign, WebSocket/HTTP lifecycle hardening, budget scoping across the REPL and front end, fatal type-check budget breaches, and the two deferred items (task-local budget and pattern text-once) with rationale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: poll the execution budget inside recursive front-end traversal The round-3 front-end checkpoints only covered phase entry / top-level statements: `Analyzer::analyze` polled the budget once, and `TypeChecker::check_types` polled once per top-level statement, but both then recurse (`analyze_statement`, `check_statement_types`) into every nested block, loop, `try`, action, and container-method body without re-checking. A single deeply nested top-level statement could run the analyzer or type checker to completion without honoring the run's deadline / cancellation / operation budget — unlike the parser, whose checkpoint already lives inside the recursively-called `parse_statement`. Move the poll into the recursive methods, mirroring the parser: - Analyzer: `analyze_statement` polls at its top; a new `budget_exhausted` flag (reset per run) records the breach once and short-circuits the rest of the traversal so a large nested body yields one error, not one per statement. The phase-boundary poll in `analyze` is retained. - Type checker: `check_statement_types` polls at its top, guarded by the existing `budget_error` channel (reset per run) as the record-once latch. The top-level loop no longer charges separately — it stops iterating once a breach is recorded, including one surfaced deep in a nested body. Both polls stay exemption-aware. `max_operations` defaults to unlimited, so ordinary programs are unaffected; the extra per-statement charging only applies when a user opts into an operation cap. Add two library-level regression tests that parse a program whose single top-level `count` loop holds 64 nested statements before installing a budget (so the parser cannot consume the cap), then run under a tiny operation cap: one proves the analyzer trips from inside the nested body, the other uses `with_analyzer` to prove the type checker trips from the recursive `check_statement_types` poll onto the `budget_error` channel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: make a type-check budget breach a fatal typed result, not a side channel A shared-budget breach recorded during type checking was an optional side channel (`take_budget_error`): the `include from` caller printed the type errors as non-fatal warnings and then executed the included program, so a deadline / cancellation / resource breach hit while checking an included file was silently followed by running it. And when `TypeChecker::new()`'s internal `Analyzer::analyze` failed, the breach was rendered as ordinary `TypeError`s while the budget channel stayed empty — so even a caller that checked the side channel saw `None`. Make the distinction impossible to miss at the type level: `check_types` now returns `Result<(), TypeCheckError>` where `TypeCheckError::Budget` (fatal — stop the run) is separate from `TypeCheckError::Types` (ordinary diagnostics). The analyzer records its breach on a typed `budget_error` latch (`take_budget_error`), which `check_types` propagates as the fatal variant, closing the analysis-phase gap. Every caller now distinguishes the two: - `include from`: a budget breach is fatal (return the catchable resource/timeout error) instead of a printed warning; ordinary type diagnostics stay non-fatal warnings as before. - `load module`, main CLI, REPL: budget breach aborts the run; type diagnostics keep their existing handling. - LSP (report-only) renders either via `into_diagnostics()`. Tests: analysis-phase and type-check-phase breaches now assert the fatal `TypeCheckError::Budget` variant; existing type-checker/LSP call sites updated to the typed result. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * perf: collect pattern input once, checkpoint before preprocessing `PatternVM::step` re-ran `text.chars().collect()` on every transition, so a single match against a large runtime input performed O(text) allocation and copying per step. Pattern input is a runtime value that is NOT bounded by `max_source_size` (it can be an unbounded file read, HTTP/client data, or a constructed string), and `max_pattern_steps` bounds the transition count, not the O(text) work done before each charge — so a ~1 MiB input could incur enormous repeated conversions between sampled deadline checks. Materialize the input into `Vec<char>` exactly once at the top of each runner (`run_execute` / `run_find` / `run_find_all`) and thread the `&[char]` slice through `execute_at_position`, `find_at_position`, and `step`. Charge the budget once *before* that collection so an already-expired deadline or cancelled run aborts without paying to materialize a large input. Capture extraction and lookbehind slicing now reuse the same slice, and the position loops bound on `chars.len()` (character length) instead of `text.len()` (byte length), which also removes redundant no-op start positions on multibyte input. Tests: matching by character index across multibyte input; and a cancelled run aborting before materializing a 1 MiB input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: hold the HTTP admission slot until the response completes Moving the in-flight admission guard into the queued `WflHttpRequest` fixed timed-out bodies that were still queued, but once the interpreter dequeued a request, `WaitForRequestStatement` moved only `response_sender` into `pending_responses` and dropped the local request (and its guard) when the statement returned. So the global admission gate reopened at dequeue: a handler could dequeue requests in a loop without responding, immediately readmitting new work while every route task and pending sender stayed unfinished until its timeout. Carry the admission slot with the parked response for its whole post-dequeue life. `pending_responses` now stores a `PendingResponse { sender, admission }`, and `respond` moves the slot into the `ResponseCompletion` guard, so it is released only when the response actually completes — on success, on the `Drop` 500 fallback, on disconnect/timeout pruning (a dropped pruned entry releases it), or on shutdown (the map drops). The slot now spans the full request lifetime, dequeue through response completion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: guarantee a WebSocket Disconnect for every admitted Connect The connect event is fail-closed, so every live connection has run (or will run) its `on websocket connect` handler, which may initialize per-connection application state. The disconnect event — the only place that state is cleaned up — used a lossy `try_send`, so a momentarily-full event queue could drop it, leaking application state and leaving a queued Connect with no matching Disconnect. Deliver the disconnect with a blocking `send().await` instead: it resolves as the interpreter drains the bounded event queue, and returns `Err` only when the server has shut down (its receiver dropped), where no disconnect handler remains to run anyway. Message events keep their lossy `try_send` (dropping a message under overload is acceptable backpressure); only the lifecycle pair is made guaranteed. Under normal load the send completes immediately, so there is no behavior change except under the full-queue race this fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: scope the run budget task-locally so interleaved runs cannot cross-contaminate `Interpreter` is re-exported from the crate root and is `!Send`, so a library embedder can legally `join!` or `spawn_local` two interpreter futures on one thread. The current budget was installed in a thread-local held across every `.await` (the interpreter run and the REPL command), so when two runs interleaved on a thread the second overwrote the first's current budget and restored stale state — the runs cross-contaminated. Install the run budget in a `tokio::task_local!` instead, via a new `ExecutionBudget::scope(budget, future)` that wraps the run. Task-local state is per-future, so each interleaved run keeps seeing its own budget across awaits. `interpret` is now a thin wrapper that scopes the budget around the run body (`interpret_inner`); an `execute file` child nests its own scope. The REPL command is likewise wrapped in `scope`. `ExecutionBudget::enter` (thread-local) is retained only as a synchronous fallback for the CLI front-end (lex/parse/analyze/type-check), which runs to completion without awaiting on a single-future runtime and therefore cannot interleave. `current()` consults the task-local scope first and this fallback only when no async scope is active — and since every async run establishes a scope that shadows it, the fallback can never cross-contaminate a run. Test: two runs with distinct budgets interleaved via `join!` on a current-thread runtime each keep observing only their own budget across an `.await` — the property a thread-local violates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: budget-checkpoint the lexer and recursive expression parsing/analysis The front-end checkpoints only polled the budget at statement boundaries and during recursive *statement* traversal, so one huge expression — a million-element list, a long flat operator chain — was a single statement that lexed, parsed, analyzed, and type-checked to completion after the deadline. The lexer had no budget call at all, so `wfl --lex` processed the whole (source-size-capped) input past its deadline and a same-task Ctrl-C could not interrupt lexing. Add strided/recursive checkpoints across the front end: - Lexer: every 4096 tokens, consult the run budget and stop tokenizing cooperatively on a deadline/cancellation/operation breach (it returns `Vec`, so it yields the tokens gathered so far; the truncated stream then trips the parser's checkpoint). - Parser: a strided checkpoint at every primary-expression parse (the universal operand leaf — list elements, operator-chain terms, call arguments), so a single giant expression is interruptible, returning the breach as a `ParseError`. - Analyzer / type checker: poll the budget at the top of `analyze_expression` and `infer_expression_type`, latched via `budget_error`, so a huge expression tree inside one statement is interruptible during analysis and type checking. All polls are exemption-aware and no-ops when no run budget is installed, so normal programs (default `max_operations` unlimited) are unaffected. Test: a single 6000-element list literal trips the parser checkpoint under a tiny operation cap, where the old parser ran to completion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: expose the large interpreter stack as a public helper for embedders The interpreter's async recursion is stack-heavy (~1 MiB per WFL call), so its `max_call_depth` only turns runaway recursion into a clean, catchable error on a stack large enough to reach the limit. The 1 GiB stack was set up privately in the CLI's `main`, so a library embedder driving `Interpreter::interpret` directly on an ordinary 8 MiB thread with the default `max_call_depth` (1000) could crash the host process with a native stack overflow that no depth limit can catch. Extract the large-stack thread setup into a public `wfl::run_with_interpreter_stack(work)` helper (and a `INTERPRETER_STACK_SIZE` constant), documented as the entry point embedders should wrap their runtime + `interpret` call in — the same one the CLI now uses (so the logic is shared, not duplicated). It returns `Err` only if the large stack cannot be reserved, so a caller can fall back or lower `max_call_depth`. The `Interpreter` type gains a `# Stack safety` doc section pointing embedders at it. Test: recursion deep enough to overflow the default test-thread stack completes when driven through the helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * docs: Dev Diary entry for the fifth review round (maintainer P1s) Document the seven fixes landed for the maintainer's fourth-pass review: typed fatal type-check result, pattern collect-once, HTTP admission lifetime, guaranteed WebSocket disconnect, task-local budget scoping, lexer/expression front-end checkpoints, and the public large-stack embedder helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: address issue #611 — lexer typed outcome, HTTP admission release, and three residuals The fifth review round's own remediation left two P1 merge blockers plus three correctness/API residuals (issue #611). Fix all five, each with a regression test. P1 — lexer must not turn a budget breach into a successful partial stream: The strided lexer checkpoint `break`'d on a breach and returned the prefix, so `wfl --lex` dumped it and exited 0, and a prefix ending at a statement boundary could parse/execute. The loop is now a private `lex_positions_core` taking a checkpoint closure: `lex_wfl_with_positions` supplies a no-op (never truncates — LSP/tooling/tests unaffected) and the new `lex_wfl_with_positions_checked` returns `Result<_, BudgetExceeded>`. Every production execution caller (CLI run and `--lex`, nested execute-file/include/load-module, REPL) propagates it. At each stride the deadline and cancellation are checked directly (not via `charge_operation`'s sampling, which nested in the 4096-token stride could postpone them by millions of tokens); the operation ceiling is charged separately. P1 — a timed-out pending HTTP request can no longer wedge admission: The guard was parked in the interpreter's pending map and pruned only when a later dequeued request was inserted; once the cap filled with unanswered requests, no new request could be admitted to trigger the prune, pinning every slot forever. The guard now rides with the warp transport future (which outlives the dequeue while awaiting the response), so the slot is held through handling and released when that future ends — respond, response timeout, or client disconnect — independently of any future admission. The bounded request mpsc still caps queued bodies. P2 — analyzer entry-time breach preserves the typed channel: the phase-boundary checkpoint now stores `budget_error` before returning, so `TypeChecker::new()` sees a cancelled/exhausted entry breach as fatal `TypeCheckError::Budget`, not misclassified `Types`. P2 — Unicode lookbehind stays in character indices: the full-slice test compared `MatchResult::end` (chars) against `text_slice.len()` (bytes), inverting positive/negative lookbehind on multibyte input; it now compares the slice's character length. The public `CompiledPattern::find` doc example that sliced `&text[m.start..m.end]` with char offsets is fixed to extract by chars. P2 — the default public interpreter path is stack-safe: `Interpreter::new()` now caps recursion at the conservative `DEFAULT_EMBED_CALL_DEPTH` (12) instead of the config default (1000), which only stays catchable on the CLI's 1 GiB stack; `with_config`/`with_config_and_budget` still honor the configured depth so the CLI reaches 1000. A deep program on an ordinary stack now returns a catchable call-depth error instead of aborting. Tests: typed lexer outcomes (cancel/deadline/operations), no-truncation, and `--lex` exit; a live-server admission-reopens-after-timeout regression; a cancelled-budget analyzer entry test; positive/negative Unicode lookbehind over a 2-byte char; and an ordinary-8-MiB-stack embedding test. Dev Diary updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A * fix: consult the run budget at the lexer boundary, not only on a full stride The strided lexer checkpoint fired only at token 4096, 8192, … so a source shorter than one stride never consulted the budget: an already cancelled or already expired budget lexed to a successful `Ok`. This was most visible in `--lex`, which has no later parser/analyzer checkpoint and so could dump a short source and exit 0 under an already-breached run budget. `lex_positions_core` now invokes the checkpoint once before the first token (catching an already-breached budget for any input length) and once after the last (catching a breach during the final partial stride), keeping the strided checks for long inputs. The non-budgeted `lex_wfl_with_positions` still supplies a no-op checkpoint, so it is unaffected. Tests: `lex_wfl_with_positions_checked("display 1\n")` under an already-cancelled budget and under an already-expired deadline both abort with the typed breach at the entry checkpoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y498gapRYjXfGSciauq49A --------- Co-authored-by: Claude <noreply@anthropic.com>
logbie
pushed a commit
that referenced
this pull request
Jul 24, 2026
…ll_ref deny (P1 #4) Brad's re-review: `close server` held `web_servers.borrow_mut()` across a 50ms await, and the whole module suppressed `clippy::await_holding_refcell_ref` with a crate-level allow — so a sibling handler touching the map during that yield could panic, and the lint could not catch future regressions. - `close server` (HTTP and WebSocket): remove the entry into a local and drop the map borrow BEFORE the graceful-shutdown await. - Parent-method call: clone the parent `Rc`/type out of the container instance so no instance/parent RefCell borrow spans the awaited method call. - Two `open file for reading` paths: capture the `env.define` result and drop the env borrow before the `close_file` await. - Flip the module attribute from `allow` to `#![deny(clippy::await_holding_refcell_ref)]` so borrow-across-await is now a hard error for this module going forward. clippy --all-targets clean; streaming/concurrent tests green.
logbie
added a commit
that referenced
this pull request
Jul 25, 2026
…rs (#641) * feat: outbound HTTP response streaming (stream response / wait for next 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 * docs: lock design for streamed server responses (item 3) and streaming 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 * feat: streamed server responses (start streaming response / write line|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 * feat: concurrent request handlers (main loop concurrently:) 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 * fix: address PR #641 review — backward-compat, status validation, doc 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 * ci: skip non-executable streaming docs examples in run_integration_tests 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 * docs: adopt Logbie Testing Policy (root testing.md) + WFL testing profile 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 * fix: streaming bindings use define_or_replace (loop re-reads + no bind-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 * fix: address PR #641 review — transpile-fail, streaming type checks, response byte budget Contained hardening from maintainer review: - transpiler: `main loop concurrently:` now returns a TranspileError instead of silently emitting a serial `while(true)` loop (no faithful serial translation exists). Plain `main loop` still transpiles. - typechecker: enforce clause types on `start streaming response` (status: Number, content type: Text, headers: Map<Text, Any>) and clarify the HTTP-body message (numbers/booleans are accepted and converted). - interpreter: `write line|chunk` on a server response stream now enforces the `max_response_bytes` ceiling, so a stream cannot bypass the buffered-response budget. Byte total is tracked per open stream. - parser: `flush` only dispatches as a flush statement when it has an operand. - interpreter: stream handles must be the handle object (`_stream` / `_server_stream`); a bare Text id is no longer accepted. Tests: write-after-close does not reach the client; concurrent main loop fails to transpile. * fix: isolate per-handler run-state under `main loop concurrently:` (P1 #1) The concurrent loop isolated each handler's environment but interpreter run-state (current_count/in_count_loop, call_depth, call_stack, and the block overload-dup set) still lived on the shared `Interpreter`. Two concurrent handlers interleaving on the single thread could overwrite each other's count-loop/recursion/call-stack bookkeeping across an await — e.g. one count loop reading a `count` set by another handler. Fix without touching the single-threaded Rc core: each handler carries its own `RunState`, and an `IsolatedHandler` poll wrapper swaps that state into the interpreter only for the duration of each poll, swapping it back out the instant poll returns (ready or pending). The run-state fields become poll-local; a suspended handler's state is parked in its own `RunState` where no sibling can touch it. Serial execution is untouched. Red→Green: tests/concurrent_main_loop_test.rs:: test_concurrent_handlers_do_not_share_count_loop_state — two handlers count over disjoint ranges (1..5 vs 100..104), yielding mid-loop. Red (isolation bypassed): /a returned 100-101-102-103-104- (the other handler's range). Green: /a returns 1-2-3-4-5-. Also from review: - Fix stale doc comments on resolve_stream_handle / resolve_server_stream_handle (they require the handle object; a bare Text id is rejected — doc now matches). - Docs: web-servers.md disconnect note uses try/catch (consistent with examples). * fix: auto-close server response streams on handler exit (spec item 5; Devin/Copilot) A streamed server response (`start streaming response`) parked its body-channel sender in the long-lived `server_response_streams` table. The sender was dropped only by an explicit `close out` or the write-after-disconnect path, so a handler that ended without `close out` (normal return, caught error, break) left the sender in the table forever: the client's chunked body never terminated (client hangs) and the table leaked one dead entry per streamed request. This contradicted the shipped docs/design and the streaming spec's item-5 lifecycle guarantee ("all streams close on every exit path"). Fix: each handler tracks the respstream* ids it opens in per-handler run-state (`open_response_streams`, part of the RunState swapped per poll), and closes them when it ends on ANY path: - concurrent handlers: IsolatedHandler's Drop (covers return/error/panic/cancel); - serial main loop: drain after each iteration (normal and error paths); - top level: drain at program exit. close_response_streams is idempotent, so explicit `close out` (still preferred, finalizes promptly) and auto-close compose safely. Red→Green: tests/http_server_streaming_test.rs:: test_stream_auto_closes_when_handler_ends_without_close — handler omits `close out`; client reads body under a 5s timeout. Red (drain disabled): body never finishes (Elapsed). Green: reads "hello\n". Also from review: - typechecker: reword the HTTP-body type error to list accepted types (text, number, boolean) instead of the self-contradictory "must be text (…)". - docs: web-servers.md, response-streaming-design.md, and the server-streaming dev diary now describe the shipped close-on-exit behavior; new dev diary entry. * fix: 500 immediately when a handler ends without responding (P1 #3) The ResponseCompletion drop guard only arms once a handler reaches a respond/start-streaming statement. A handler that dequeues a request (`wait for request comes in`) and then ends before responding — runtime error, break, or a plain return — left the sender parked in pending_responses, so the client waited out the request timeout instead of getting a prompt 500. Fix: each handler tracks the request ids it dequeued but hasn't answered in per-handler run-state (`open_pending_requests`, part of the RunState swapped per poll). `respond`/`start streaming response` remove the id from the map and disarm tracking; on handler exit (any path) `fail_unanswered_requests` 500s any id still in pending_responses. Fully synchronous (try_lock + oneshot send) so it runs from IsolatedHandler's Drop; also wired into the serial main loop's per-iteration drain and program exit. Idempotent — a responded request is gone from the map, so the sweep skips it. Red→Green: tests/concurrent_main_loop_test.rs:: test_handler_that_never_responds_gets_immediate_500 — /drop dequeues and returns without responding. Red (sweep disabled): the request hangs past the 120s test timeout. Green: 500 arrives in <1s and the server keeps serving. Also from review: - The response-byte-ceiling and disconnect paths untrack the stream id from open_response_streams so a handler that catches the error keeps no stale ids. - typechecker: HTTP/response header type hints widened to map[text, any]. * fix: `write line/chunk <var> to <file>` keeps the classic file write (back-compat) `write line <value> to <out>` shares a surface with the classic file write `write <content> to <file>`, and WFL identifiers can be space-separated, so the lexer merges `line payload` into one token. The parser unconditionally split it into a `line` marker + value, silently reinterpreting a pre-existing file write of a variable named `line payload` as a stream write — a backward-compat break (flagged by Copilot). The two readings can't be told apart at parse time (both the NDJSON stream write and the file write use a bare variable), so disambiguate on the runtime target type: - AST/parser: StreamWriteStatement gains `fallback_content` — for the ambiguous merged form it records both the stream value (`payload`) and the classic file-write content (`line payload`); unambiguous forms set None. - interpreter: evaluate the target first; if it is a server response stream, do the stream write, else if a fallback exists do the classic file write, else error. - analyzer: defer definedness for the ambiguous form (the live reading, and thus which variable must exist, is only known at runtime); count both candidate variables as used so neither is falsely reported unused. Red→Green: tests/write_line_backcompat_test.rs — `store line note as "…"` / `write line note to "<file>"`. Red (analyzer analyzing the stream value): rejects with `Variable 'note' is not defined`. Green: analysis accepts it and the file receives the variable's value, not the token `note`. Existing streaming tests (`write line "alpha" to out`, bare `write line to out`) still pass. Docs: web-servers.md notes the target-type disambiguation. * docs: reconcile concurrency/testing status with shipped reality (review) Address doc-consistency review comments: - concurrency-phase-plan.md: qualify per-request isolation (environment + now run-state; global bindings/shared collections stay shared by design); state 500 containment precisely (transport ResponseCompletion mid-respond, plus the interpreter's immediate 500 when a handler ends without responding); sync the stale PR-1b TODO checklist to the as-shipped state ([x] done, [~] transport-provided, and the two remaining known gaps — request-ID logging and a dedicated panic-containment test — called out explicitly). - testing.md: add an adoption note so the verbatim "Status: Proposed / Effective: Upon adoption" policy block reads consistently with this repo's header and CLAUDE.md/AGENTS.md (adopted, binding, effective 2026-07-22); correct the supported-tuples table — CI has no macOS runner (only ubuntu + windows), so macOS is best-effort/not-gated, not "release smoke only". Docs-only; no behavior change. * fix: parse `content type <var>` in start-streaming; tighten review tests/docs - parser: `start streaming response ... and content type <var>` where <var> is a bare identifier merges into `type <var>`; split the marker so the variable is bound correctly (previously bound `type <var>` as one name). Regression test. - tests: `write chunk` fallback parser coverage (ambiguous + literal forms); transpiler test asserts the specific unsupported-transpilation message rather than any error; write-line backcompat test uses an isolated TempDir per run. - docs: web-servers.md no longer over-specifies chunked transfer-encoding (HTTP/1.1 only) — describes the observable behavior (no Content-Length, streamed incrementally) with the HTTP/2 note. * docs: qualify concurrent-handler isolation wording in example (review) Replace 'own isolated scope' with the precise contract: per-iteration child scope + per-handler run-state isolation (so per-request `store` variables don't clobber), while global bindings and collections shared through them stay shared by design. Matches the concurrency-phase-plan wording. * test: wait for server readiness instead of a fixed sleep (fix flaky CI) CI "Build, Test, Clippy" failed on concurrent_main_loop_test with `Connection refused`: the tests slept a fixed 300ms after starting the server thread, but on a loaded CI runner binding the port can take longer, so the client connected before the server was ready. Per the testing policy a flaky required test is a failing test. Replace the fixed sleep with `wait_for_server(port)` — a bounded readiness probe that TCP-connects until the server accepts (a bare connect that drops immediately delivers no HTTP request to the handler). Applied to both concurrent_main_loop_test and http_server_streaming_test. Deterministic and faster (returns as soon as the port is bound). * fix: mark streaming-statement vars used in analyzer; tidy body type hint & docs - analyzer: `mark_used_variables` now covers HttpStreamStatement, WaitForNextChunk/LineStatement, and StartStreamingResponseStatement — so variables referenced only in those (URL/body/headers, stream source, request/status/content-type) are no longer falsely reported unused (Copilot). Regression test in static_analyzer. - typechecker: the HTTP-body type error passes no single "expected" type (the accepted set is Text|Number|Boolean), so the expected-vs-actual diagnostic isn't misleadingly rendered as "expected Text" (Copilot). - docs: web-servers.md clarifies that only the ambiguous bare-identifier `write line/chunk` form falls back to a file write; literal/number/boolean forms are stream-only and error on a non-stream target (Copilot). * test+docs: broaden streaming unused-var test; qualify phase-plan claims (review) - static_analyzer test: cover every new streaming arm (HttpStream url/method/body, WaitForNextChunk + WaitForNextLine source, StartStreamingResponse status/content type) and metadata operands, with a negative assertion that a genuinely-unused variable is still the only one flagged (CodeRabbit). - concurrency-phase-plan.md: narrow the "tested" 503/504/500 claim to the immediate-500 case that actually has a dedicated test (503/504 are transport-provided, untested at Phase 1); mark the eval-core RefCell-across-await audit as OPEN/partial — the clippy backstop covers await-holding borrows but the shutdown/signal lifecycle paths are not yet separately audited; qualify the 1c tracker status to match the open checklist items (CodeRabbit). * docs: align write-chunk value types, note write-line `with` limit, drop dead var - web-servers.md: `write chunk <value>` also accepts numbers/booleans (written as their text form), matching the interpreter; add a note that `write line/chunk` don't yet accept in-statement `with`-concatenation on a bare-variable value (workaround: build the value with `store` first). - interoperability.md: remove the unused `store done as no` leftover from the incremental-read example. Doc-only; matches shipped behavior. (Copilot) * fix: drop RefCell borrows before await; re-enable await_holding_refcell_ref deny (P1 #4) Brad's re-review: `close server` held `web_servers.borrow_mut()` across a 50ms await, and the whole module suppressed `clippy::await_holding_refcell_ref` with a crate-level allow — so a sibling handler touching the map during that yield could panic, and the lint could not catch future regressions. - `close server` (HTTP and WebSocket): remove the entry into a local and drop the map borrow BEFORE the graceful-shutdown await. - Parent-method call: clone the parent `Rc`/type out of the container instance so no instance/parent RefCell borrow spans the awaited method call. - Two `open file for reading` paths: capture the `env.define` result and drop the env borrow before the `close_file` await. - Flip the module attribute from `allow` to `#![deny(clippy::await_holding_refcell_ref)]` so borrow-across-await is now a hard error for this module going forward. clippy --all-targets clean; streaming/concurrent tests green. * fix: budget-check server write size before materializing bytes (P1 #6) Brad's re-review: `write line|chunk` cloned the entire text/binary value into a new `Vec` and only then checked it against `web_server_max_response_size` — so a huge write allocated/copied far past the limit just to be rejected. Now compute the outgoing byte length from the value in place (`str::len` / `[u8]::len`, plus the optional newline; numbers/booleans render to a short string), reserve the response-byte budget under the map borrow, and only materialize the bytes once the write is known to fit. Over-budget writes are refused before any large allocation; the stream is still dropped/untracked on breach as before. * fix: finalize top-level streams/requests on every exit path (P1 #5) Brad's re-review: "all streams close on every exit path" was false outside ordinary completed loop iterations. - Cleanup now runs AFTER the conventional `main` action (it ran before), so a stream/request opened by `main` is finalized instead of leaking. - The mid-run per-statement timeout early-return now drains open streams and 500s unanswered requests before returning (previously bypassed cleanup). - On interpreter reuse (REPL), the reset now actually CLOSES the prior run's open streams and 500s its unanswered requests (draining the tracking) instead of merely clearing the id lists — clearing alone stranded the still-open sender/receiver in the maps, hanging the client and leaking the entry. Streaming/concurrent tests green. * fix: only dispatch `start` to streaming when `streaming` follows (back-compat) Brad/Copilot: statement dispatch routed every `Token::KeywordStart` to `parse_start_streaming_response()`, which errors unless the next token is `streaming`. That would hijack any other statement-initial use of the `start` keyword (e.g. a `start of text` pattern anchor). Guard the arm so it only fires when `streaming` actually follows; otherwise `start` falls through to its normal handling. `start streaming response ...` still parses. * fix: back off and cap consecutive failures in concurrent main loop (P1 #3) Brad's re-review: the concurrent loop refilled and re-polled with no backoff/classification, so a deterministic pre-await error (bad expression) or `wait for request` on a server closed without the loop breaking would hot-spin the CPU and the log forever — and the loop is deadline-exempt. Track consecutive handler failures (reset on any successful iteration). Between failures, back off with a small capped sleep so instant failures yield instead of spinning; once failures cross a structural-failure threshold (256 with no progress), terminate the loop rather than refilling forever. Incidental handler errors interleaved with successful requests never trip the guard. * feat: absolute total deadline for outbound streaming responses (P1 #2) Brad's re-review: each outbound `stream_pull` got a fresh idle timer, so an upstream that trickles a byte before every idle timeout could run forever inside the deadline-exempt main loop. Add an absolute total-lifetime cap distinct from the per-read idle timeout: - new config `outbound_stream_max_seconds` (default 300; `0` disables), with a config-reference entry; - HttpStreamHandle records a `total_deadline` at open time; - `stream_pull` refuses a read once the total deadline passes AND bounds each read's idle timeout by the time remaining to the total, so no single read waits past the cap and a trickling upstream cannot outlive it. config/http_stream tests green; clippy clean. * ci: gate docs-validation and web tests; make scripts executable (P1 gating) Brad's re-review: testing.md claims CI runs web tests and docs validation, but ci.yml invoked neither, and run_web_tests.sh / validate_docs_examples.py were committed non-executable (mode 100644) — so green CI didn't supply the R3 web/streaming boundary evidence the policy now mandates. - Mark both scripts executable (git +x). - Integration Tests job now runs `validate_docs_examples.py --ci` and `run_web_tests.sh` on Linux (after the release binary is built). The Rust streaming/concurrent integration tests already run via `cargo test --test '*'`; these add the doc-example and real-web-server gates. * test: surface server-thread interpret errors and re-raise join panics Copilot: the streaming/concurrent server threads ignored `interpret()`'s result and the tests ignored `join()`, so a server-side interpreter error or panic was dropped and the test could still pass. - Server threads now `panic!` on an unexpected interpreter error. - `join_server` (streaming) and `shutdown` (concurrent) `resume_unwind` the thread's panic payload (JoinHandle::join's error is Box<dyn Any>, no Debug), so a server-side failure fails the test loudly with its original message. All streaming/concurrent tests still pass (confirming clean shutdown on Ok). * fix: tighten ambiguous write line/chunk analysis Analyze the unambiguous of-object subexpression of a merged `write line|chunk <ident> to <target>`, and report an undefined variable only when NEITHER candidate name resolves (stream value <ident> vs classic file-write variable `line <ident>`), so a genuine typo is still caught without breaking either valid reading. Addresses Copilot review comment on src/analyzer/mod.rs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix: require wait-for-request first in concurrent loop; enforce text header keys Three review-driven hardening changes on the streaming branch: - Analyzer: a `main loop concurrently:` body must begin with `wait for request`. Concurrent handler slots start from the top of the body, so any statement before the first `wait for request` runs once per slot before a request is dequeued. Reject that at analysis time with an actionable error instead of running setup speculatively. Serial `main loop` is unaffected. (New surface; every example/test already complies.) - Typechecker: HTTP header maps must have text keys. A single is_valid_header_map_type helper (used by outbound HTTP, streaming responses, and `respond ... and headers`) rejects a map with a concrete non-text key while accepting text-keyed and loosely-typed maps, matching the "header names" contract in the error message. - CI: run docs-example validation and the web-server suite on Windows too (run_web_tests.ps1), and pass --force so docs validation ignores the committed cache and always re-validates. Red->Green evidence: test_concurrent_main_loop_requires_wait_for_request_first and test_header_map_type_requires_text_keys, both confirmed failing without the change and passing with it. Docs + Dev Diary updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * docs: restore report_undefined_name doc comment to its function The analyze_loop_body extraction inserted the new function between report_undefined_name's doc comment and its definition, orphaning the doc above the wrong function. Move it back so each function documents itself. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(ci): give Start-Process distinct stdout/stderr targets in web test runner Enabling the Windows web-server test gate surfaced a pre-existing bug in run_web_tests.ps1: all three Start-Process calls passed "NUL" for both -RedirectStandardOutput and -RedirectStandardError, which PowerShell 7 rejects ("RedirectStandardOutput and RedirectStandardError are same"), failing the job before any test ran. Redirect the discarded server output to two distinct temp files instead (the TLS case uses its already-cleaned temp dir). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(ci): address WFL server on 127.0.0.1 (not localhost) in Windows web tests The Windows web-test gate got past the redirect-collision fix and revealed the real cause of the timeouts: WFL binds 127.0.0.1 (IPv4 only) by default, but the runner addressed the server as http://localhost. On Windows, localhost resolves to IPv6 ::1 first, so Invoke-WebRequest never connected and every readiness probe/request timed out. The Linux .sh runner uses curl, which resolves localhost->127.0.0.1, so it never hit this. Address the server by 127.0.0.1 everywhere (the redirect Location mirrors the Host header, so its expected value changes to match; the self-signed cert CN is irrelevant under -SkipCertificateCheck). Also per maintainer review, to make the gate trustworthy/debuggable: - Dump the server's captured stdout/stderr on every failure path (Show-ServerLogs) so a real server error is not hidden behind a bare TIMEOUT. - Kill AND WaitForExit before removing the TLS temp dir (Stop-ServerProcess) to avoid a Windows cleanup race on the cert/log file handles. - Wrap the route-test requests so a request failure fails that test (and dumps logs) instead of throwing out of the script and skipping the summary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(ci): deterministic flush + monotonic deadline + exit code in web tests Addresses the maintainer's remaining Windows-harness recommendations (the localhost->127.0.0.1 address-family fix and Kill/WaitForExit landed in 59d4809): - simple_web_test.wfl: add `close server test_server` after the one-shot respond. `respond` hands the reply to the async transport, so reaching EOF can tear the runtime down before the socket flushes; `close server` has a short grace that drains the pending response first, making single-shot delivery deterministic. - run_web_tests.ps1: replace the fixed retry-count readiness loops with a Stopwatch wall-clock deadline. The old '20 tries' loop could run ~50s for a '10s' wait (each failed 2s request stacked on the 500ms sleep); the deadline caps total wait at the advertised timeout. - run_web_tests.ps1: Show-ServerLogs now also reports the child process exit code (or that it was still running), so a failure shows the decisive server-side evidence, not just its stdout/stderr. Verified simple_web_test.wfl still parses and analyzes cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix: robust redirect Location extraction; exact streaming dispatch; header value types Three review-driven fixes: - run_web_tests.ps1 (Windows CI): the 127.0.0.1 fix got 2/3 web tests passing; the TLS redirect assertion still failed with an empty Location because pwsh 7 throws on -MaximumRedirection 0 and $_.Exception.Response is an HttpResponseMessage whose Headers has no string indexer (so ["Location"] is $null). Add Get-LocationHeader that reads the strongly-typed HttpResponseHeaders.Location (Uri) for that shape and the case-insensitive string indexer otherwise. - parser (Copilot): restrict the `start streaming response` dispatch to the EXACT Identifier("streaming") token instead of starts_with("streaming "). `response` is a keyword so `streaming` never merges; the prefix form would hijack an unrelated `start streaming <ident> ...` into a confusing parse error. - typechecker (Copilot): is_valid_header_map_type now also validates the map VALUE type. The interpreter accepts only text/number/bool header values (stringified) and rejects the rest, so Map<Text, Binary> (etc.) is flagged; Unknown/Any/Error keys and values still pass to avoid false positives. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix: parse with-continuation in write line/chunk; harden Windows web runner Backward-compat (maintainer review): the merged `write line|chunk <ident>` form expected `to` immediately after the identifier value, so a `with`/operator continuation was rejected — breaking `write line payload with "!" to out` AND the pre-existing classic file write `write line payload with "!" to file`. Expose parse_binary_continuation (the operator loop of parse_binary_expression) and use it to absorb the continuation onto the stream reading, then mirror it onto the classic file-write fallback by swapping the leftmost leaf operand (replace_leftmost_leaf). Both readings now carry the full expression. Windows web runner hardening (maintainer review): - Get-LocationHeader matches the HttpResponseMessage shape by type NAME, not the [System.Net.Http.HttpResponseMessage] type literal, which a clean PowerShell 5.1 process could fail to resolve before the fallback runs. - Stop-ServerProcess checks WaitForExit(5000)'s result and only reports "terminated" when the process actually exited (else warns), instead of always claiming success while a live process races temp-file cleanup. Tests: write_line_backcompat_test gains a parse test (continuation mirrored to both readings) and a runtime test (classic file write preserves the concatenation). Full parser suite + clippy green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * docs: write line/chunk now supports with-concatenation directly The continuation-parsing fix makes the write value a full expression, so `write line prefix with json to out` works directly in the statement. Update the web-servers guide, which still said with-concatenation was 'not yet supported' and told users to build the value first — a now-stale claim that contradicts the parser and its tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix: parse ambiguous write line/chunk readings independently; analyze continuation Fixes a back-compat regression the leftmost-leaf approach introduced, plus the coupled analyzer gaps (maintainer review). Parser (P1 back-compat): the two readings of the merged `write line|chunk <ident> ...` form are now parsed INDEPENDENTLY from the same tokens via cursor checkpoint/rewind, instead of deriving the classic file-write AST from the stream AST by swapping the leftmost leaf. A continuation desugars differently per leading operand — a builtin name becomes an ActionCall, `is between` duplicates the left operand, `starts/ends with` and pattern ops build calls — so leaf-swapping silently dropped or mangled the continuation. Regression: `store line length as "kept" / write line length with "!" to <file>` now writes "kept!" (was "kept"). Analyzer: the ambiguous arm now analyzes the shared continuation (every sub-expression except the ambiguous leading operand) so an undefined variable there is caught, and reports the leading operand undefined only when NEITHER reading resolves via a leftmost-leaf lead name — fixing both a false negative (undefined RHS slipping through) and a false positive (valid classic program defining `line <name>` rejected because the split stream name is undefined). Docs: the web-servers note no longer claims the value is a "full expression"; it states that `with`/operators work but postfix (indexing/property) on the leading identifier is not parsed there. Tests: write_line_backcompat_test adds the builtin-named regression, an undefined-in-continuation case, and a valid-classic-with-continuation case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(typechecker): accept stream handles in close, not just File Maintainer review: stream handles (server response streams from `start streaming response as <out>` and outbound streams from `... stream response as <upstream>`) bind as map-shaped objects, but CloseFileStatement accepted only a `File` object, so a valid `close out` / `close upstream` produced a spurious "Expected a File object" diagnostic. Add is_closeable_type (File/Custom, the map-shaped stream handle, or a statically-unresolved Unknown/Any/Error) and use it; a concrete scalar like `close 5` is still rejected. Tests (new stream_handle_type_test): closing a server response stream and an outbound stream both type-check clean; closing a number still errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(typechecker): only File custom type is closeable; drop misleading expected hint Two Copilot review points on the close type-contract fix: - is_closeable_type treated any Type::Custom(_) as closeable, so `close db` (Custom("Database")) / `close req` would wrongly pass. Restrict to Custom("File"); stream handles are Map-shaped (still accepted). - The error says "file or stream handle" but passed Some(File) as the expected type, mis-rendering expected-vs-found. Pass None instead. Test: closing a database handle is now rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(ci): guard Kill() and warn before racing TLS temp cleanup Maintainer review (Windows-gate completeness): Kill() in Stop-ServerProcess was outside the try, so a throwing Kill() (access denied / concurrent exit) went unhandled; wrap it. And the TLS finally deleted the temp dir with -ErrorAction SilentlyContinue even when the server had not actually exited, hiding a failed cleanup — now re-check HasExited and warn that cleanup may be incomplete before the Remove-Item, rather than silently proceeding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(typechecker): distinct stream-handle types so close rejects ordinary maps Copilot review: is_closeable_type accepted any Map, so `close some_map` passed even though the runtime can only close file/stream handles. Give stream handles distinct static types instead of Map<Text,_>: the outbound handle is Custom("HttpStream") and the server response stream is Custom("ResponseStream"). `close` now accepts only File|HttpStream|ResponseStream (plus Unknown/Any/Error); an ordinary map, another custom type, or a scalar is rejected. IndexAccess on the stream-handle types returns Any so reading their fields (status/ok/headers, including nested header lookups) still type-checks; MemberAccess on Custom already returned Unknown. Tests: close of both handle kinds passes; closing an ordinary map, a database handle, and a number are rejected; direct and nested field indexing still checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * style: rustfmt the stream-handle index-access match arm Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(analyzer): stop rejecting valid classic writes with desugared values Fatal back-compat regression from the previous analyzer walk (maintainer review): stream_write_lead_name/analyze_stream_write_continuation assumed the ambiguous lead was always the leftmost leaf/function callee and that every call argument or binary right branch was shared/unambiguous. Desugared values break that — `starts/ends with` makes the lead a call ARGUMENT, `is between` DUPLICATES the lead, and pattern/of/builtin-with bury it — so valid classic file writes like `write line path starts with "/" to <file>` (only `line path` defined) were wrongly reported undefined. Analyze only the shapes where the lead is provably the single leftmost bare variable — a bare Variable, or `<var> with <continuation>` (Concatenation with a Variable left); for those, still catch an undefined shared RHS and flag the lead only when NEITHER reading resolves. Every other (desugared) shape defers entirely to runtime, so no valid program is rejected. Tests: valid classic `starts with` / `is between` / `matches pattern` (and a `write chunk` case) now analyze cleanly, while the undefined-RHS and both-leads-undefined cases still error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(typechecker): support dot access on stream handles; require text index key Follow-ups to the distinct stream-handle types (maintainer review): - The canonical docs use dot access (upstream.status, upstream.headers[...]), but PropertyAccess only handled containers/maps/gradual types, so the new Custom("HttpStream")/Custom("ResponseStream") handle emitted a false "cannot access property" diagnostic. Add a stream-handle arm returning Unknown (runtime-known field type), mirroring the index-access and member-access paths. - The stream IndexAccess arm returned Any without checking the key; a numeric key (resp[5]) wrongly passed even though runtime object indexing requires a text field name (Map<Text,_> rejected it before the nominal-type change). Require a text-compatible key. Tests: dot access (incl nested header lookup) type-checks; a numeric stream-handle key is rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(ci): surface TLS temp-dir cleanup failure instead of hiding it Maintainer review (Windows cleanup completeness): the TLS finally deleted the temp dir with -ErrorAction SilentlyContinue even when the server was still alive, racing its open cert/log handles and hiding a failed cleanup (a leaked dir or a lingering fixed-port child). Now give a brief extra grace if the process is still running after Kill()+WaitForExit, then attempt Remove-Item with -ErrorAction Stop and report a failure via a WARN rather than swallowing it silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(parser): do not require the unused classic reading to parse (write line/chunk) Maintainer review: the ambiguous merged form parses BOTH the stream reading and the classic file-write reading (via cursor rewind), but the classic reading is only USED at runtime when the target is a file. Requiring it to parse could reject a valid stream-only value whose grammar the classic reading can't consume. Make the fallback parse fallible (.ok()) and always resume right after the stream value, so the statement parses on its stream reading alone when the classic reading doesn't; the fallback is simply omitted (runtime then has no file reading to fall back to, which is correct — that reading was not valid anyway). Existing tests still exercise the fallback-parses path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(parser): consume a connective before 'as' in start streaming response Copilot review: the streaming-response clause loop broke on an unrecognized token after and/with WITHOUT consuming the connective, so `... with status 200 and as out` failed at expect_token(as) reporting the leftover `and` instead of parsing. Consume a connective that directly precedes `as` (the end-of-clauses join) so the `as <name>` binding parses cleanly, and correct the comment to describe the actual behavior for a connective before any other unrecognized token. Test: `start streaming response to req with status 200 and as out` parses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): bound an active outbound read by the absolute stream deadline run_http_with_budget derived its timeout purely from the run/budget duration and DISCARDED the caller's configured_timeout, which already encodes the stream's idle timeout AND its remaining absolute-total deadline (min(idle, remaining) from stream_pull). So with timeout_seconds=10 and outbound_stream_max_seconds=1, a head-then-stall upstream let `wait for next chunk` wait ~10s instead of ~1s. Compose the operation deadline as MIN(configured_timeout, run/budget duration), reporting the stream Timeout or the budget Deadline depending on which bound fired; because configured_timeout is always finite, the read is now bounded even when the budget has no run-wide deadline. Also start the absolute total-lifetime clock at request initiation (before send()), not after the head arrives, so connect/header time counts toward the documented total, and bound the head phase by the remaining-to-total as well. Red->Green: new real-socket test outbound_stream_deadline_test — a mock upstream sends the head then stalls; the read now fails at ~1s (was ~10s). Existing outbound HTTP budget/cancellation/stream tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * test: build WflConfig with struct-update syntax (clippy field_reassign_with_default) CI's clippy --all-targets flagged the P1-B deadline test's `let mut config = WflConfig::default(); config.x = ...` (field_reassign_with_default, denied by -D warnings). Use struct-update syntax instead. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): make outbound streams handler-owned, closed on every exit path Maintainer review: outbound httpstream* handles lived only in the interpreter- wide IoClient.stream_handles map; RunState/IsolatedHandler::drop tracked and closed downstream response streams and pending requests but NOT upstream handles. A handler that opened `... stream response as up` and then ended (normal, error, panic, cancellation, loop exit) without closing leaked the upstream connection until the whole interpreter tore down. Track outbound handle ids per-handler in RunState.open_http_streams (swapped per-poll like the other run state). Add on open; untrack on EOF/error/explicit close; and on every handler exit — IsolatedHandler::drop for the concurrent loop, and close_open_http_streams() at the serial-loop/program-exit cleanup sites — drop any still-open handles from stream_handles (via a synchronous try_lock), which cancels their in-flight upstream requests. Idempotent and best-effort. Red->Green real-socket test (outbound_stream_ownership_test): a program opens an outbound stream, reads one chunk, and ends WITHOUT close while the interpreter is still alive; the mock upstream observes its client disconnect only because handler-exit cleanup cancelled the upstream (confirmed Red by disabling the cleanup). Existing concurrent/server-streaming isolation tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): cancel a blocked upstream read when the downstream client disconnects Maintainer review: a proxy handler blocked in `wait for next line/chunk` on the upstream received no signal when the browser disconnected — it was only noticed at the next downstream write, or (now) at the absolute stream deadline. Give the read a proactive disconnect signal: the downstream response stream's mpsc Sender `closed()` resolves when hyper drops the client's body Receiver. Clone the senders of the handler's open response streams (no RefCell borrow held across the await) and `select!` the upstream read against "any downstream disconnected". On disconnect, dropping the read future cancels the upstream; we also close the handle and return a catchable Cancelled error so the handler unwinds and its handler-owned cleanup runs. Red->Green real-socket test (outbound_stream_disconnect_test): mock upstream sends one chunk then stalls; a WFL concurrent proxy relays it; the client reads the first chunk and disconnects while the handler is blocked on the stalled upstream read; the mock observes its own connection close within the window only because the blocked read was cancelled (confirmed Red by disabling the select). Full lib + HTTP/concurrent/streaming suites and clippy --all-targets stay green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * docs: outbound-stream deadline/ownership/disconnect now accurate (P1 shipped) Update the streaming docs and add a Dev Diary entry for the two P1 lifecycle fixes: reads are bounded by min(idle, remaining absolute deadline) started at stream open; a stream is released on handler/program exit on any path (outbound handles are handler-owned); and a downstream disconnect cancels a blocked upstream read promptly rather than waiting out the deadline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * test(P1): failing burst-disconnect regression for the concurrent loop A burst of >256 downstream disconnects currently trips the concurrent main loop's global consecutive-failure breaker (each disconnect is miscounted as a handler failure), tearing the loop down so an unrelated /ping is refused. Red: /ping is refused after 256 disconnects because the loop broke. This is the R3 negative/availability test that the fix must turn green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): treat a client disconnect as cancellation, not a handler failure A downstream disconnect that cancels a proxy handler's blocked upstream read was surfaced as a generic budget error and fed into the concurrent main loop's single global consecutive-failure breaker: every disconnect incremented the counter and backed off, and 256 disconnects with no interleaved success broke the whole loop, turning an ordinary client hang-up into a denial of service. The disconnect branch now returns a distinct HttpClientError::Disconnected mapped to a new ErrorKind::Cancelled (catchable like any error). The concurrent loop recognizes a Cancelled handler outcome as a normal, expected cancellation: it releases the handler (its owned upstream/response streams are already closed on unwind) without touching the failure counter or backing off. Internal budget-cancellation of an outbound request keeps its ResourceLimit kind, so only a real downstream disconnect is exempted. Turns tests/concurrent_disconnect_burst_test.rs green: a burst of 270 disconnects no longer tears the loop down; an unrelated /ping is still served (was refused; ~13s -> ~0.9s). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * style: cargo fmt (expand HttpClientError::Timeout, wrap assert) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * test(P1): failing coverage for property-then-index parsing A bracket index right after a .property/.method access split into a separate list-literal statement, so `store ct as obj.headers["k"]` silently dropped the key and bound `ct` to the whole headers map. Red evidence: - property_index_access_test: AST asserts one IndexAccess over the PropertyAccess (currently splits); a runtime map lookup yields the indexed value "BBB" (currently returns the whole map -> INDEX_WRONG). - stream_handle_type_test: the previously typecheck-only dot test now also asserts the parse structure (it was a false green: both halves of the split still type-check). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): compose a bracket index after a .property/.method access A bracket index immediately after an identifier property access (`obj.headers["content-type"]`) or method call (`obj.get()[0]`) was dropped: the primary-expression dispatch returned the PropertyAccess/ MethodCall early, before the postfix loop could consume the `[...]`, so the bracket was re-parsed as a standalone list-literal statement and the lookup silently vanished (`ct` bound to the whole map). Route both the property-access and method-call fast paths through a new parse_trailing_bracket_index helper that folds any chained `[...]` index accesses onto the base (`grid.rows[0][1]`). The shared postfix loop and the static-member `.` arm are untouched, so a trailing `.member` keeps its pre-existing behavior. Turns tests/property_index_access_test.rs and the strengthened dot test in tests/stream_handle_type_test.rs green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * test(P1): failing coverage for span-mismatched write fallback `write line min with a: 1 and b: 2 to <target>`: the stream reading consumes the builtin call `min` with named args, but the classic file-write reading of `line min` can only parse `line min with a` (a shorter span, stopping at the `:`). Retaining that partial parse as the fallback corrupts a file write. Red evidence: - AST: the span-mismatched fallback must be dropped (currently retained as a partial Concatenation). - runtime: with `line min`/`a` defined, the old fallback writes "CORRUPT..." to the file; the fix must make it a clean error instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * test(P1): failing coverage for absolute stream lifetime on buffered reads outbound_stream_max_seconds must be a true absolute lifetime, but next_line/next_chunk serve locally-buffered bytes before consulting the deadline. Red: a buffered `wait for next line` taken ~1.5s after opening (past the 1s absolute lifetime) is still served instead of failing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): drop a span-mismatched classic write fallback The ambiguous `write line|chunk <ident> ... to <target>` form parses two independent readings of the same continuation: the stream value and the classic file-write fallback. The fallback was kept whenever it merely parsed, even if it consumed a DIFFERENT span than the stream reading — so `write line min with a: 1 and b: 2 to <file>` retained a partial `line min with a` fallback (the classic reading stops at the `:` that the builtin-call stream reading consumes as a named arg), corrupting the file write. Keep the fallback only when it consumed exactly to the stream reading's end checkpoint; otherwise the two readings disagree and there is no valid classic interpretation, so the non-stream target is a clean error. Turns tests/write_line_backcompat_test.rs (two new cases) green while the matching-span back-compat cases still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): enforce the absolute stream lifetime on buffered reads next_line/next_chunk served locally-buffered bytes before consulting the handle's absolute deadline, so outbound_stream_max_seconds bounded only network reads: a proxy that pulled a multi-line chunk could keep draining the buffer past the stream's absolute lifetime. Add check_stream_deadline and call it before serving buffered bytes in next_chunk and before serving a buffered line in next_line's loop; on expiry the handle is dropped (cancelling the upstream). An empty-buffer read already expired via stream_pull's identical check. Turns tests/outbound_stream_absolute_lifetime_test.rs green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * test(P1): failing coverage for outbound cleanup on a dropped interpret() A dropped/cancelled interpret() future does not run the handler-exit or program cleanup sites, so an outbound stream handle parked (opened, not mid-read) in IoClient.stream_handles leaks the upstream until the interpreter itself is dropped. Red: with the interpreter kept alive after the future is dropped, the mock upstream is never disconnected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LxXAm5E4Vy39kRLr8kRnX * fix(P1): close outbound handles when the interpret() future is dropped Handler-exit and program-cleanup sites close outbound stream handles on normal control-flow exits, but a dropped/cancelled interpret() future runs none of them, leaking a parked (opened, not mid-read) upstream until the interpreter itself is dropped. Add an RAII OutboundStreamCleanup guard held for the whole interpret_inner body: it shares open_http_streams and the IoClient via Rc, so its Drop closes the tracked handles (cancelling their upstreams) even as the future unwinds and the interpreter stays alive. On a normal run the exit sites drain t…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add Rust Installation Script
This PR adds a shell script (
install_rust.sh) to help users set up Rust for WFL development.Features
Testing
Link to Devin run: https://app.devin.ai/sessions/96c9c249e4594d79bc1e12512ba848d7
Requested by: bsbyrd@logbie.com