Scope
This tracks the remaining findings from a re-review of #641 at exact head b25aed57ea50697c596796446d1f47466668773d (69 files, +10,978 / -303).
The exact-head gate is genuinely green: CI run 30098113534 passed Rust tests/formatting/all-features Clippy, the documented integration scripts on Linux and Windows, Windows docs validation (18/18), and Windows web tests (3/3). Most earlier review findings are closed. Green CI does not cover the cases below, though, and I would not merge that exact head until the P1 items are fixed.
1. P1 — request-local failures can still stop the concurrent server
execute_concurrent_main_loop sends every non-Cancelled handler error and every panic through one global 256-consecutive-failure breaker (src/interpreter/mod.rs:4402-4428). That includes request-local upstream/network failures, request-dependent response errors, and an ordinary wait for request ... with timeout expiry (:9040-9058). After enough consecutive instances, the entire loop stops even though the listener and unrelated handlers are healthy.
There is also a concrete disconnect path that still reaches this breaker:
- A handler dequeues a request and parks its sender in
pending_responses.
- A sibling
wait for request globally prunes closed senders (:9166-9178).
- The owning handler later reaches
respond (:9236-9268) or start streaming response (:9489-9518).
- The pending entry is missing, so both paths return
ErrorKind::General, not Cancelled.
- That expected client disconnect is counted as a structural failure.
The new burst test is false-green for this race:
- it fires only 270 clients with concurrency 40 (
tests/concurrent_disconnect_paths_burst_test.rs:24-26), leaving roughly the final batch on the correctly classified direct-send path;
- it waits only three seconds before
/ping (:104-106);
- the General-failure backoff (
src/interpreter/mod.rs:4444-4455) takes about 11.5 seconds to consume 256 failures, so /ping can be handled and reset the counter before the buggy results are drained;
- its
fired counter is incremented but never asserted.
Acceptance criteria:
2. P1 — the outbound hard-lifetime reaper has an ownership race and leaks timer tasks
The new reaper removes a handle once at its deadline (src/interpreter/mod.rs:2018-2035). A body read removes that handle from the shared map for the duration of the await (:2044-2053), then put_stream blindly reinserts it (:2056-2062; called by next_chunk/next_line).
If expiry lands while a read owns the handle, the one-shot removal is a no-op. A ready chunk can win the read/timeout race, and the read can reinsert the expired handle after the reaper has exited. With no subsequent read, the live upstream can outlast the documented real-time hard cap.
Related lifecycle problems:
- every open stream spawns a timer that remains parked until the cap even after EOF, error, or explicit close, so rapid open/close cycles accumulate roughly request-rate × cap sleeping tasks;
- when the reaper does remove a parked handle, the next read reports “unknown/already closed” (General) rather than the promised typed timeout;
- arbitrary
u64 config values reach Instant::now() + Duration::from_secs(secs) (:1943-1946), which can panic for extreme values.
Acceptance criteria:
3. P1 — ambiguous write line|chunk checking is still unsound
The aligned analyzer reports the ambiguous lead only when neither candidate exists (src/analyzer/mod.rs:3760-3769). The typechecker then selects a concrete branch, but unresolved variables infer as Unknown without another diagnostic (src/typechecker/mod.rs:3076-3140). That lets the selected runtime branch contain an undefined name.
Concrete text-target regression:
store value as "x"
write line value to "/tmp/out"
The stream lead value exists, so analysis passes. The text target makes runtime select the classic branch and evaluate the missing variable line value. Before this PR, that classic file write was rejected statically. The inverse occurs with a concrete ResponseStream when only line value exists. PropertyAccess is also absent from the aligned analyzer walker, so write line missing.field to "/tmp/out" can evade the “neither lead exists” check.
There are two further branch-selection holes:
- for a gradual target,
src/typechecker/mod.rs:1083-1112 accepts when either speculative branch is valid, although runtime may select the invalid branch;
- analyzer loop scopes are discarded, and the typechecker only marks a streaming-response binding when a surviving symbol already exists (
:1045-1050). Inside the canonical main loop, out can therefore remain Unknown; a valid file fallback can mask an invalid payload on the actual stream branch.
Example of the latter:
main loop:
wait for request comes in on s as req
start streaming response to req with status 200 as out
store items as [1 and 2]
store line items as "legacy"
write line items to out
end loop
At runtime out is definitely a response stream and the list payload is invalid, but the gradual/file branch can make this pass static checking.
Acceptance criteria:
4. P1 — merged operands do not compose like normal WFL expressions
parse_write_value_from_lead delegates postfix handling to parse_trailing_postfix (src/parser/stmt/io.rs:22-29). That helper handles brackets and dot/method access, but omits two existing WFL indexing forms handled by the ordinary primary parser: direct integer indexing (src/parser/expr/primary.rs:1301-1341) and at indexing (:1414-1424).
Previously valid classic code now parse-fails:
store line values as ["first" and "second"]
write line values at 0 to "/tmp/out"
write line values 0 to ... is broken similarly.
The same limited helper is used for merged content type and headers operands (src/parser/stmt/web.rs:449-500). The documented <e> clauses therefore reject ordinary call/operator continuations such as:
start streaming response to req with status 200 and content type mime_type of path as out
Acceptance criteria:
5. P1 — flush compatibility preserves only one old behavior
Before this PR, a merged bare name such as flush cache was an ordinary expression statement. The new runtime fallback (src/interpreter/mod.rs:9879-9903) preserves a Function or an Overloaded value containing a zero-argument overload, then otherwise falls through to stream flushing.
This changes previously legal behavior:
store flush cache as 1
flush cache
The base branch evaluates that variable and completes. This head instead tries to flush an undefined cache. An overloaded flush cache action with no zero-argument overload likewise changed from ordinary bare-expression evaluation/no-op to a stream error.
Acceptance criteria:
R3 test-strength follow-ups
These are not substitutes for the correctness fixes above, but they are needed to make the Green evidence meaningful:
Verification note
I could not run new ad-hoc Rust regressions locally because this review environment has no cargo. The existing exact-head Actions run is fully green, but it does not contain these cases. Per the repository’s R3 policy, each behavior change above should land as a Red test-only commit before its Green fix.
Scope
This tracks the remaining findings from a re-review of #641 at exact head
b25aed57ea50697c596796446d1f47466668773d(69 files, +10,978 / -303).The exact-head gate is genuinely green: CI run 30098113534 passed Rust tests/formatting/all-features Clippy, the documented integration scripts on Linux and Windows, Windows docs validation (18/18), and Windows web tests (3/3). Most earlier review findings are closed. Green CI does not cover the cases below, though, and I would not merge that exact head until the P1 items are fixed.
1. P1 — request-local failures can still stop the concurrent server
execute_concurrent_main_loopsends every non-Cancelledhandler error and every panic through one global 256-consecutive-failure breaker (src/interpreter/mod.rs:4402-4428). That includes request-local upstream/network failures, request-dependent response errors, and an ordinarywait for request ... with timeoutexpiry (:9040-9058). After enough consecutive instances, the entire loop stops even though the listener and unrelated handlers are healthy.There is also a concrete disconnect path that still reaches this breaker:
pending_responses.wait for requestglobally prunes closed senders (:9166-9178).respond(:9236-9268) orstart streaming response(:9489-9518).ErrorKind::General, notCancelled.The new burst test is false-green for this race:
tests/concurrent_disconnect_paths_burst_test.rs:24-26), leaving roughly the final batch on the correctly classified direct-send path;/ping(:104-106);src/interpreter/mod.rs:4444-4455) takes about 11.5 seconds to consume 256 failures, so/pingcan be handled and reset the counter before the buggy results are drained;firedcounter is incremented but never asserted.Acceptance criteria:
wait for requesttimeouts must not terminate an otherwise healthy server./ping(or make the breaker threshold injectable and small in tests).2. P1 — the outbound hard-lifetime reaper has an ownership race and leaks timer tasks
The new reaper removes a handle once at its deadline (
src/interpreter/mod.rs:2018-2035). A body read removes that handle from the shared map for the duration of the await (:2044-2053), thenput_streamblindly reinserts it (:2056-2062; called bynext_chunk/next_line).If expiry lands while a read owns the handle, the one-shot removal is a no-op. A ready chunk can win the read/timeout race, and the read can reinsert the expired handle after the reaper has exited. With no subsequent read, the live upstream can outlast the documented real-time hard cap.
Related lifecycle problems:
u64config values reachInstant::now() + Duration::from_secs(secs)(:1943-1946), which can panic for extreme values.Acceptance criteria:
Timeout.outbound_stream_open_expiry_testcurrently covers only an unread handle that stays in the map.3. P1 — ambiguous
write line|chunkchecking is still unsoundThe aligned analyzer reports the ambiguous lead only when neither candidate exists (
src/analyzer/mod.rs:3760-3769). The typechecker then selects a concrete branch, but unresolved variables infer asUnknownwithout another diagnostic (src/typechecker/mod.rs:3076-3140). That lets the selected runtime branch contain an undefined name.Concrete text-target regression:
The stream lead
valueexists, so analysis passes. The text target makes runtime select the classic branch and evaluate the missing variableline value. Before this PR, that classic file write was rejected statically. The inverse occurs with a concreteResponseStreamwhen onlyline valueexists.PropertyAccessis also absent from the aligned analyzer walker, sowrite line missing.field to "/tmp/out"can evade the “neither lead exists” check.There are two further branch-selection holes:
src/typechecker/mod.rs:1083-1112accepts when either speculative branch is valid, although runtime may select the invalid branch;:1045-1050). Inside the canonicalmain loop,outcan therefore remainUnknown; a valid file fallback can mask an invalid payload on the actual stream branch.Example of the latter:
At runtime
outis definitely a response stream and the list payload is invalid, but the gradual/file branch can make this pass static checking.Acceptance criteria:
ResponseStreambinding while typechecking main-loop and action bodies.4. P1 — merged operands do not compose like normal WFL expressions
parse_write_value_from_leaddelegates postfix handling toparse_trailing_postfix(src/parser/stmt/io.rs:22-29). That helper handles brackets and dot/method access, but omits two existing WFL indexing forms handled by the ordinary primary parser: direct integer indexing (src/parser/expr/primary.rs:1301-1341) andatindexing (:1414-1424).Previously valid classic code now parse-fails:
write line values 0 to ...is broken similarly.The same limited helper is used for merged
content typeandheadersoperands (src/parser/stmt/web.rs:449-500). The documented<e>clauses therefore reject ordinary call/operator continuations such as:Acceptance criteria:
[],.field,.method(),at, direct-integer indexing,of, and operator continuations acrosswrite,content type, andheaders.5. P1 —
flushcompatibility preserves only one old behaviorBefore this PR, a merged bare name such as
flush cachewas an ordinary expression statement. The new runtime fallback (src/interpreter/mod.rs:9879-9903) preserves aFunctionor anOverloadedvalue containing a zero-argument overload, then otherwise falls through to stream flushing.This changes previously legal behavior:
The base branch evaluates that variable and completes. This head instead tries to flush an undefined
cache. An overloadedflush cacheaction with no zero-argument overload likewise changed from ordinary bare-expression evaluation/no-op to a stream error.Acceptance criteria:
R3 test-strength follow-ups
These are not substitutes for the correctness fixes above, but they are needed to make the Green evidence meaningful:
response_stream_backpressure_testmust assert the expected timeout/error and a reasonable lower timing bound; it currently discardsinterpret()and passes on an unrelated immediate exit.outbound_stream_open_expiry_testmust assert successful setup/the expected terminal reason and a lower bound; it also currently ignoresinterpret().dropped_interpret_server_cleanup_teststarts a streaming response, so it proves body closure but has already consumed the pending sender.Verification note
I could not run new ad-hoc Rust regressions locally because this review environment has no
cargo. The existing exact-head Actions run is fully green, but it does not contain these cases. Per the repository’s R3 policy, each behavior change above should land as a Red test-only commit before its Green fix.