Skip to content

Land the checkThrows library surface; repair the guard's constant propagation (EH-16) - #154

Merged
biuld merged 1 commit into
masterfrom
slice/eh-library
Oct 18, 2026
Merged

biuld merged 1 commit into
masterfrom
slice/eh-library

Conversation

@biuld

@biuld biuld commented Oct 18, 2026

Copy link
Copy Markdown
Owner

What lands

Slice 5 of the exception-handling adoption (row EH-16), spanning both repositories:

Compiler side:

  • The psrs:runtime-exception CC publication is now evidenced through the real frontend: publication keeps the callable operand + Boolean result unchanged (the provider has no host ABI; plan_import plans no import), checked() accepts the published external, three malformed shapes rejected with module attribution.
  • A real MIR-optimizer bug found and fixed: mir/opt/constants.rs built its block-parameter predecessor lattice from normal-edges-only reachability — the third normal-CFG-only analysis (slice 4 fixed two). For a join shared by a protected path and a catch path, the join's parameter merged to the normal path's constant, was materialized away, and the removal stripped the handler's true argument from its jump — every checkThrows call observed false even on the catch path. Fixed via reachable_blocks_with_unwind over unwind_successors.
  • stdlib.lock.json pin updated to the stdlib change 728a82f9 (fingerprint fnv1a64-v1:5dfa6f2ce6b3092c — the hasher was verified by recomputing the old value 7d96f1c44d8d52cc on the pre-change checkout).

Stdlib side (independent repo, merged to its master as 728a82f9 before this PR): Test/Assert.checkThrows implemented over Target.perform + the checked psrs:runtime-exception#checkThrows binding (10-line diff, surface unchanged, assert''s storage-trap protocol untouched); adaptation record docs/assert-throws.md with the pinned upstream declaration and target justification; upstream-lock.json refreshed.

EH-16 is recorded Blocked — honestly, and here is why

TCOMutRec compiles under the enabled profile but its first assertThrows traps (exit 134): \_ -> ntcoN 100000 returns normally at depth 100000 — the backend's mandated closure-tail reuse (mark_tail → ReturnCallRef, CF-07's verified row) runs arity-mismatched mutual recursion in constant stack, so the budget never fires. The corpus's own comment names the distinction: upstream's TCO pass loopifies arity-matched named mutual-recursion groups and leaves arity-mismatched closure-mediated cycles growing the stack; the same program requires both behaviors (tcoN reuse, ntcoN exhaust). Reproducing it needs a recursion-classification/loopification topic of its own (cross-function loopification + declining physical tail reuse through closure-mediated cycles) — a normative decision, not a slice repair. The record names the owning decision; nothing was faked or weakened.

What IS verified end to end (real frontend, pinned engine, all executed): depth-100000 genuinely-non-tail recursion under the guard exhausts the budget and the catch delivers true (exit 0); a returning argument reports false; the corpus source's four depth-100 assertEqual halves log Done and exit 0; the full TCOMutRec is rejected under the disabled profile with the channel's diagnostic.

Validation

cargo fmt --all --check ✅ · cargo clippy --workspace --all-targets -- -D warnings ✅ · cargo test -p psrs-backend 588/0 (coordinator); full workspace suite run by the slice ✅. Stdlib npm test: 12 passed / 1 pre-existing environmental failure (record-unsafe needs pinned upstream checkouts absent on this host — unrelated). D-04 untouched; profile default unchanged; L6/M7 board deliberately NOT run (that is EH-17's evidence, and its number depends on the tail-call decision above).

…pagation (EH-16)

The library surface of the exception-handling adoption: the checked
psrs:runtime-exception#checkThrows contract publishes through CC and the
stdlib's adapted Test.Assert implements checkThrows over it, making the
guard real end to end. The corpus case's own execution is blocked by the
backend's constant-stack tail-call treatment, diagnosed with evidence
below; row EH-16 is recorded Blocked, not Verified.

- stdlib.lock.json pins the psrs-stdlib check-throws branch commit
  728a82f94af6e7c54ba10124af54644e8c364d19 (fingerprint
  fnv1a64-v1:5dfa6f2ce6b3092c): lib/Test/Assert.purs implements
  checkThrows :: forall a. (Unit -> a) -> Effect Boolean as
  Target.perform \_ -> checkThrowsImpl fn over the checked foreign, with
  its adaptation record (docs/assert-throws.md in the stdlib repo) and
  refreshed vendored_sha256; exports and assertImpl's storage-trap
  protocol unchanged.
- Publication evidence: bindings/runtime/call/tests.rs
  canonical_exception_call_publishes_the_checked_source_signature_without_a_host_abi
  — canonical_signature keeps the callable operand and Boolean result
  unchanged (the provider has no host ABI), checked accepts the published
  external, and malformed shapes are rejected with the binding's module.
- mir/opt/constants.rs + mir/opt/cfg.rs: the scalar-propagation lattice
  collected block-parameter predecessors over normal-edges-only
  reachability, the third such analysis after slice 4's two; a join shared
  by a protected path and a catch path saw only the normal predecessor's
  constants, so every checkThrows join materialized false and the
  handler's true argument was stripped from its jump — the guard observed
  false even when its catch path fired. Predecessor collection now uses
  reachable_blocks_with_unwind; psrs-backend 587 passed unchanged.
- psrs-driver/src/program: the trusted-prelude compile path takes an
  explicit TargetCapabilities (default profile for every existing
  caller; tests::exception_library selects the enabled profile until the
  EH-17 flip); tests/mod.rs factors run_artifact_with_wasmtime so the
  corpus evidence runs the pinned engine limit.
- tests/exception_library.rs, executed under PSRS_REQUIRE_WASMTIME=1 and
  -W max-wasm-stack=524288: the guard is interpreted and a genuinely
  non-tail depth-100000 recursion exhausts the budget inside the guarded
  call, catching true (200 then true, exit 0); a returning argument
  observes false (1009 then false); the corpus source's four depth-100
  assertEqual halves log Done and exit 0 under the enabled profile; the
  full TCOMutRec is rejected under the disabled profile with the
  channel's diagnostic.
- checklist row EH-16: Blocked with the evidence record, the observed
  TCOMutRec trap (stdout empty, exit 134, 1.25 s) and the diagnosis: the
  corpus's tco/ntco distinction is upstream's tail-call classification;
  our mandated closure-tail reuse makes both constant-stack, so the
  budget never fires for the ntco halves while the tco halves require
  that same reuse.

Validation: cargo fmt --all --check; cargo clippy --workspace
--all-targets -- -D warnings; PSRS_STDLIB_ROOT=… PSRS_REQUIRE_WASMTIME=1
cargo test --workspace — 2194 passed, 0 failed, 5 ignored (the
scoreboard suites, out of scope).
@biuld
biuld merged commit f929525 into master Oct 18, 2026
1 check passed
@biuld
biuld deleted the slice/eh-library branch October 18, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant