[Plan D Task 111b] Sync ABI threading — terminal_out as trailing param#90
Conversation
Second of 4 PRs implementing Task 111 (TLS → caller-owned terminal
channel). Brian-authorized 4-PR breakdown 2026-05-03.
**ABI change.** Every Sync user fn signature gains a trailing
`terminal_out: *mut TerminalResult` parameter:
Sync ABI: (closure_ptr, user_params..., terminal_out) -> ret_ty
The pointer is threaded end-to-end through Sync → Sync calls, into
the main shim's stack-allocated `TerminalResult`, and forwarded into
every `sigil_run_loop` call site that originates from a Sync user fn.
TLS dual-write (added in 111a) remains in place at runtime; pointer-
side becomes authoritative for Sync ABI propagation. 111c will thread
terminal_out through the Cps + synth fn ABIs; 111d switches handle-
exit reads to the caller-owned pointer and removes the TLS path.
**Codegen sites updated.**
- Sync ABI signature emit (`UserFnAbi::Sync` arm + Sync shim emit):
+1 trailing `pointer_ty` AbiParam in both the declaration-time
signature and the body-emit signature.
- Lowerer struct: new `terminal_out_param: Option<Value>` field.
`Some(value)` for Sync user fn body emit (block-param-bound at fn
entry). `None` for Cps body emit, synth-arm-fn emit, synth-cont
emit, post-arm-k synth fn emit, chained-let-bind synth-cont emit
(8 sites total) — those contexts pass null until 111c.
- Main shim allocates a 16-byte stack slot (matching the runtime's
`repr(C) { value: u64, tag: u64 }` layout) and passes its pointer
to user_main as the trailing Sync ABI arg.
- `lower_call`'s Sync direct-call branch threads `self.terminal_out_-
param` (or null) as the trailing arg.
- `lower_call`'s Cps interop wrapper forwards `self.terminal_out_-
param` (or null) into `sigil_run_loop`'s `out` slot.
- `Expr::ClosureRecord` callee branch (IIFE / lambda-as-value): looks
up callee ABI; threads terminal_out for Sync callees, keeps the
3-arg shape for Cps callees (111c plumbs Cps).
- Indirect call (`call_indirect` via closure record's `code_ptr`):
fn-typed values resolve to Sync ABI callees (hoisted user fn or
Sync shim), so the indirect signature gets +1 pointer_ty AbiParam
and the call args get +1 trailing pointer.
- Per-Cps-fn Sync shim body emit: reads the trailing block param as
`terminal_out_v` and forwards it into `sigil_run_loop` instead of
null.
- Five `sigil_run_loop` call sites inside Lowerer methods (handle
body's nested run_loop, pre-Phase-4g return-arm dispatch, perform-
side run_loop drive, branched k-call dispatch, Slice B fallback)
switch from `null_terminal_out` to `self.terminal_out_param`-or-null.
**Verification.**
- `cargo build --workspace` — clean.
- `cargo test --workspace` — 274 passed; 3 failed (pre-existing perf
timing tests fib_perf / fib_cps_perf / tree_example whose debug-
build wall-clock under parallel test-runner contention exceeds the
floor; all three fail identically on pre-111b state, verified via
`git stash` + re-run).
- `cargo clippy --workspace --all-targets` — clean.
- `cargo fmt --all -- --check` — clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — Plan D Task 111bReviewed @ HEAD (6143fd7). Net assessment: structurally sound, ABI threading is symmetric across all changed call sites, alignment/size for the main-shim slot is correct. Six concrete issues, ordered by severity. 1. Silent ABI fallback at
|
Addresses Brian's R1 review on PR #90 (codegen.rs:18483 silent fallback, debug_assert gaps, no semantic test, comment duplication, main shim slot ownership note). One commit covering all 6 issues. **Issue 1 (medium) — silent ABI fallback at `Expr::ClosureRecord`.** `unwrap_or(UserFnAbi::Sync)` replaced with `match`+`unreachable!()`, mirroring the discipline at the direct-Sync branch (line 17613) and the `user_fn_refs.get(code_fn_name)` lookup two lines below. A future refactor that diverges `user_fn_refs` and `user_fns` keys now trips a panic instead of silently emitting a Sync-ABI call against a Cps callee (stack-corrupting ABI mismatch). **Issue 2 (low) — implicit length precondition.** `debug_assert!` added at the Sync shim body emit (block_params.len() >= 2) and at the Sync user fn body emit (block_params.len() == f.params.len() + 2). A future signature-emit refactor that drops the trailing terminal_out trips here instead of producing OOB indexing later in the fn. **Issue 3 (medium) — TLS-vs-pointer agreement debug_assert.** `sigil_run_loop` now reads `*out` back after writing at both the DONE and DISCHARGED terminal sites and asserts the read-back value matches `LAST_TERMINAL_TAG` / `LAST_TERMINAL_VALUE` TLS. Tautological under single-threaded sequential execution today (both written from the same `(tag, v)` locals) but documents the load-bearing dual-write invariant during the 111a→111d transition. Catches any future reordering, aliasing, or write-failure that desyncs the two channels before 111d removes TLS. **Issue 4 (low) — indirect-call signature is unconditionally Sync.** `debug_assert!` added in `lower_closure_record` immediately before the code_ptr store. Confirms the resolved `code_fn_ref` came from either `sync_shim_refs` or a Sync-ABI entry in `user_fns`. A future change that stores a raw Cps-ABI func_addr directly in a closure record now trips here instead of silently mis-typing every indirect call site (whose signature builder unconditionally assumes Sync). **Issue 5 (low) — comment duplication.** New `Lowerer::terminal_out_or_null` helper method centralizes the `unwrap_or_else` pattern + rationale at one site; the 10 call sites collapse to single-line invocations. When 111c lands and every Lowerer construction sets `terminal_out_param: Some(...)`, the helper's `unwrap_or_else` branch becomes dead code and the helper either collapses to `unwrap()` or the field type relaxes from `Option<Value>` to `Value`. **Issue 6 (informational) — main shim slot ownership.** Comment added at the main shim's `terminal_slot` site documenting "slot is write-only from main()'s perspective". A future maintainer adding a read-back path (e.g., to forward terminal up to the C ABI exit code) MUST initialize the slot first or it picks up stack garbage on the pre-first-terminal codepath. **Verification.** - `cargo build --workspace` — clean. - `cargo clippy --workspace --all-targets` — clean. - `cargo fmt --all -- --check` — clean. - `cargo test --workspace` — 274 passed; 3 failed (pre-existing perf timing tests under parallel contention; identical to PR #90's initial state and to pre-111b state, verified prior session). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
R1 review fixes pushed in
Net diff: +149 / -71 across Verification: |
…sole channel (#92) * [Plan D Task 111d] TLS removal — caller-owned TerminalResult slot is sole channel Final of 4 PRs implementing Task 111. Brian-authorized 4-PR breakdown 2026-05-03. Closes the cross-fn discharge propagation gap that the prior multi-return / out-pointer / per-fn-stack-slot attempts on PR #50 could not address (see `[DEVIATION Task 111]`); the option-(C) "thread `*mut TerminalResult` through every fn ABI" architectural slice is now complete. **Channel transition.** 111a–c ran TLS and the caller-owned `TerminalResult` pointer in dual-write. 111d switches handle-exit reads from TLS to the pointer and removes the TLS path entirely: - `LAST_TERMINAL_TAG` / `LAST_TERMINAL_VALUE` thread-local statics removed from `runtime/src/handlers.rs`. - 4 FFI helpers removed: `sigil_last_terminal_tag`, `sigil_reset_last_terminal_tag`, `sigil_last_terminal_value`, `sigil_reset_last_terminal_value`. 4 corresponding `module.declare_function` entries in codegen + the `PerFnRefsCtx` / `PerFnRefs` / `Lowerer` ref fields all removed; the destructure clauses at every Lowerer construction site are correspondingly cleaned up. - TLS dual-write at `sigil_run_loop`'s DONE + DISCHARGED terminals removed. The PR #90 R1 issue 3 TLS-vs-pointer agreement debug_asserts removed (no TLS to compare against). - The slot becomes the sole terminal channel; codegen always passes a non-null pointer (main shim + Sync/Cps/synth fn ABI threading guarantee). Null is still tolerated at run_loop for runtime tests that drive the trampoline directly without observing the terminal. **Codegen sites switched to load/store on the slot.** - 5 handle-exit tag / value queries (`Expr::Handle`'s outer return-arm / no-return-arm / k-pair-call branches): replace `call sigil_last_terminal_tag/value` with `load i64 [terminal_out_param + {0,8}]`. The `discharged_const` iconst widens from I32 to I64 to match the slot field width. - 2 handle-entry resets (was `call sigil_reset_last_terminal_*`) become two `store i64` at offsets 0 and 8: `(value=0, tag=DONE)` before body lowering. Same role as the old TLS resets — keeps no-perform handles seeing a clean DONE state, and prevents a prior handle's terminal from leaking into the next handle in the same fn. - 2 new `i32` constants `TERMINAL_RESULT_VALUE_OFF = 0` / `TERMINAL_RESULT_TAG_OFF = 8` mirror the runtime's `TerminalResult` `#[repr(C)]` layout. **Field type relaxed.** `Lowerer.terminal_out_param: Option<Value>` collapses to `Value` now that all 9 construction sites populate `Some(...)`. The `terminal_out_or_null` helper (which existed only to cover the dead `None` branch) is gone; call sites read `self.terminal_out_param` directly. 13 call sites updated. **End-to-end test added.** `task_111d_terminal_channel_propagation_- through_nested_sync_calls` pins the new pointer-side path: a 3-deep Sync user-fn call chain (`a → b → c`) where `c` performs an effect whose handler discharges. The (value=17, tag=DISCHARGED) written by `sigil_run_loop` at `c`'s perform-site terminal must propagate through `b`'s and `a`'s returns into the handle-exit's load from the SAME caller-owned slot, route through the discharge_block, and surface `17` to stdout. Closes the test coverage gap Brian flagged in PR #91 R1 issue 4. **Runtime / codegen comment refresh.** `TerminalResult` docstring, `sigil_perform`'s "outer codegen logic" reference, and the `sigil_run_loop` chain-routing note all update to reflect the post-111d state. The transitional 111a/b/c notes are gone. **Verification.** - `cargo build --workspace --release` — clean. (Required for `libsigil_runtime.a` to be rebuilt; the compiler's `link.rs` prefers the release lib over debug.) - `cargo clippy --workspace --all-targets` — clean. - `cargo fmt --all -- --check` — clean. - `bash scripts/check-no-interior-pointers.sh` — OK. - `cargo test --workspace` — 275 passed (incl. new e2e); 3 failed (pre-existing perf timing tests fib_perf / fib_cps_perf / tree_example under parallel test-runner contention). **Closure.** `[DEVIATION Task 111]` (deferred 2026-04-30) is now fully closed. Plan B' Stage-6.8-followup carryover #1 (TLS → caller- owned terminal channel) closes alongside. Plan D Task 119 closeout audit's Task 111 line-item is unblocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [Plan D Task 111d] PR #92 R1 review fixes — issues 1 (nested-handle leak), 2 (doc rot), quality nit Addresses Brian's R1 review on PR #92. **Issue 1 (medium, real correctness bug) — nested-handle slot leak.** Confirmed via repro: when an outer handle's body contains an inner handle whose op-arm DISCHARGES, the inner's `sigil_run_loop` writes `(value, DISCHARGED)` to the fn-wide slot. The inner handle's exit loads tag/value and routes through its discharge_block correctly, but the slot RETAINS the inner's terminal state. The outer body continues evaluating post-inner-handle expressions synchronously (no further `sigil_run_loop` writes). When the outer handle's exit-tag query loads from the slot, it reads the inner's leftover DISCHARGED tag and incorrectly: 1. Skips the outer's return arm, AND 2. Loads the inner's leftover value as the handle's overall. Pre-111d this leak existed identically in TLS form — the inner's TLS write to `LAST_TERMINAL_TAG` clobbered the outer's expected DONE state — but no test exercised the composition. Confirmed pre-existing by repro on 111c (also outputs `99`); the slot is load-bearing post-111d so the leak was newly the only source of truth. Fix: snapshot/restore at every handle entry/exit. At handle entry, load the slot's pre-handle `(value, tag)` into local Cranelift Values (`snap_value_v`, `snap_tag_v`). At every exit path (return- arm and no-return-arm merge-blocks), restore the snapshot to the slot before yielding the handle's overall. Pinned by new e2e `task_111d_nested_handle_inner_discharge_does_not_leak_to_outer` (invariant: `1090\n` post-fix; `99\n` pre-fix). **Issue 2 (doc rot) — TLS/FFI references scattered through the tree.** Brian flagged 13 hits across 5 files where stale references to the now-removed `LAST_TERMINAL_*` thread-locals + `sigil_last_- terminal_*` / `sigil_reset_last_terminal_*` FFI helpers persisted in docstrings (most prominently `sigil_run_loop`'s contract docstring which still described the dual-write transitional state). Updated all to reflect the post-111d reality (caller-owned slot is sole terminal channel) while preserving the historical "previously TLS" provenance: - `runtime/src/lib.rs:39-46` — module FFI list now notes the four TLS helpers were removed by 111d. - `runtime/src/handlers.rs:1693-1701` — `sigil_run_loop`'s contract docstring rewritten: "slot is the sole terminal channel post-111d"; null tolerance scoped to runtime unit tests. - `compiler/src/codegen.rs:7189-7192, 10930-10934, 14704-14708, 15375-15379, 16528-16532, 16729-16732, 16976-16983` — 7 codegen comments updated. - `abi/src/effect.rs:43-48` — `NEXT_STEP_TAG_DISCHARGED` doc updated. - `compiler/tests/e2e.rs:1001, 4600, 9550` — 3 test docstrings updated. **Quality nit — `!out.is_null()` annotation.** Brian asked for a SAFETY/NOTE line at the null check explaining "unreachable from generated code post-111d; only runtime unit tests pass null." The DONE branch already had a brief comment pointing at the DISCHARGED bypass; expanded both to be self-contained and explicit about the unreachability. **Verification.** - `cargo build --workspace --release` — clean. - `cargo clippy --workspace --all-targets` — clean. - `cargo fmt --all -- --check` — clean. - `bash scripts/check-no-interior-pointers.sh` — OK. - `cargo test --workspace` — 276 passed (incl. new nested-handle e2e); 3 failed (pre-existing perf timing tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Second of 4 PRs implementing Task 111 (TLS → caller-owned terminal channel). Brian-authorized 4-PR breakdown 2026-05-03.
ABI change. Every Sync user fn signature gains a trailing
terminal_out: *mut TerminalResultparameter:The pointer is threaded end-to-end through Sync → Sync calls, into the main shim's stack-allocated
TerminalResult, and forwarded into everysigil_run_loopcall site that originates from a Sync user fn. TLS dual-write (added in 111a) remains in place at runtime; pointer-side becomes authoritative for Sync ABI propagation. 111c will thread terminal_out through the Cps + synth fn ABIs; 111d switches handle-exit reads to the caller-owned pointer and removes the TLS path.Codegen sites updated
UserFnAbi::Syncarm + Sync shim emit): +1 trailingpointer_tyAbiParam in both the declaration-time signature and the body-emit signature.terminal_out_param: Option<Value>field.Some(value)for Sync user fn body emit (block-param-bound at fn entry).Nonefor Cps body emit, synth-arm-fn emit, synth-cont emit, post-arm-k synth fn emit, chained-let-bind synth-cont emit (8 sites total) — those contexts pass null until 111c.repr(C) { value: u64, tag: u64 }layout) and passes its pointer to user_main as the trailing Sync ABI arg.lower_call's Sync direct-call branch: threadsself.terminal_out_param(or null) as the trailing arg.lower_call's Cps interop wrapper: forwardsself.terminal_out_param(or null) intosigil_run_loop'soutslot.Expr::ClosureRecordcallee branch (IIFE / lambda-as-value): looks up callee ABI; threads terminal_out for Sync callees, keeps the 3-arg shape for Cps callees (111c plumbs Cps).call_indirectvia closure record'scode_ptr): fn-typed values resolve to Sync ABI callees (hoisted user fn or Sync shim), so the indirect signature gets +1 pointer_ty AbiParam and the call args get +1 trailing pointer.terminal_out_vand forwards it intosigil_run_loopinstead of null.sigil_run_loopcall sites inside Lowerer methods (handle body's nested run_loop, pre-Phase-4g return-arm dispatch, perform-side run_loop drive, branched k-call dispatch, Slice B fallback): switch fromnull_terminal_outtoself.terminal_out_param-or-null.Test plan
cargo build --workspace— clean.cargo test --workspace— 274 passed; 3 failed (pre-existing perf timing testsfib_perf/fib_cps_perf/tree_examplewhose debug-build wall-clock under parallel test-runner contention exceeds the floor; all three fail identically on pre-111b state, verified viagit stash+ re-run).cargo clippy --workspace --all-targets— clean.cargo fmt --all -- --check— clean.🤖 Generated with Claude Code