Skip to content

throw's payload is never narrowed: a violating value flows into Exn<T> with no obligation and no runtime guard #1268

Description

@aallan

Found during the #1213 burndown (PR C5's scope-boundary check). throw's argument position carries no narrowing obligation at all:

effects(<Exn<Nat>>) ... throw(0 - 5)

verifies clean — 4/4 Tier 1, ZERO refine_bind obligations — and vera run returns 0 SILENTLY, the negative value having been narrowed into the @Nat payload with no static obligation and no runtime guard. The refined spelling (type Pos = { @Int | @Int.0 > 0 }, effects(<Exn<Pos>>), throw(0 - 5)) is identical: clean verification, silent run.

The contrast that localizes it: a USER effect's op argument in the same position is correctly obligated — op emit(Pos -> Unit) + Log.emit(0 - 5) → loud E505. So the shared binding-obligation machinery covers effect-op arguments generally; throw specifically bypasses it. Not refinement-specific, not Byte-specific.

Pre-existing (verified against the pre-C5 base). A silent verification hole — the class the burndown's completion bar treats as must-fix: check-green, verify-green (falsely), silently wrong at run.

Fix direction: throw's payload flows through the same binding-obligation family as every other narrowing site (the shared helper the C5 gate lives in covers let/call-arg/ctor-field/tuple/effect-op-arg/match/array/handler-init already — the question is why the throw lowering path skips it, likely because throw's Never return shape takes a different translation leg). The proving test is the E505 twin of the user-effect contrast above, plus the runtime-guard leg for the Tier-3 case.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions