From fa419c7c992bc8eb804a83a9691f3b240de33a81 Mon Sep 17 00:00:00 2001 From: Alasdair Allan Date: Fri, 14 Aug 2026 16:33:31 +0100 Subject: [PATCH 1/4] Guard the throw payload at the boundary it crosses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `throw(v)` narrows `v` into the `Exn` payload, and since this issue's static half the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist. `throw(0 - 5)` under `effects(>)` ran to completion and returned -5 through the `@Nat` payload; the refined spelling did the same, and a `@Byte` payload of 200 reached a `{ @Byte | @Byte.0 < 10 }` slot. The consequence is worse than a wrong answer. A handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke: a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a postcondition violation at run time, on a postcondition `vera verify` had proved. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s (#1203): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined first, as at every other narrowing site — the §2.6.5 predicate guard, which traps through `$vera.contract_fail` naming the predicate that failed. The three arms mirror the verifier's own obligation triple one for one, so the payload obligation is `guarded` at all three and its Tier-3 leg is counted rather than disclosed; the refined arm's claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it. `CellNames` therefore carries the type expression its producer already held, rather than parsing a predicate back out of a mangled family name. The predicate lowering is injected into the translation context, because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. A lifted-closure context is left without the emitter deliberately: it carries no effect-op cells, so a `throw` there is not a boundary this could guard (it does not compile at all today), and the absent emitter fails closed at a loud skip rather than emitting a payload the verifier records as guarded. Three findings from the adversarial round are folded in, each a place the `guarded` PROMISE was wider than the guard. The mirror `_refined_boundary_codegen_guardable` answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud E618 — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen; it now bails on a refinement base and the obligation discloses `tier3_unguarded`. The verifier's `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since #1203, so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed E504/E506 for a boundary that traps, and `State.put(v)` had been doing the same since #1203; both now take the bare arm's rule on the same key, so the two spellings of one operation record identical statuses. And two diagnostic rationales (E504, E531) listed the `throw` payload among the sites with no runtime guard, which is false once the guard lands and contradicts the spec sentences this commit amends. An unrefined payload's WAT is byte-identical to before — a differential over all 278 pre-existing corpus programs, compiled and verified on both trees, moves nothing in emitted WAT or in the obligation and diagnostic streams. New run-level conformance program `ch07_exn_payload_guard` carries the legal controls; the violating twins trap, which no conformance level can express, so those are asserted in `tests/test_exn_throw_payload_1268.py` alongside the proved-`ensures` differential. The #1269 width test's adjacency assertion is rewritten to resolve the operand the `throw` consumes, since the guard now sits between the literal and the instruction. Closes #1268 Co-Authored-By: Claude --- AGENTS.md | 8 +- CHANGELOG.md | 1 + CLAUDE.md | 8 +- FAQ.md | 4 +- KNOWN_ISSUES.md | 3 +- README.md | 2 +- ROADMAP.md | 2 +- SKILL.md | 6 +- TESTING.md | 36 +- docs/SKILL.md | 6 +- docs/index.html | 2 +- docs/index.md | 2 +- docs/llms-full.txt | 18 +- docs/llms.txt | 2 +- spec/02-types.md | 4 +- spec/06-contracts.md | 2 +- spec/11-compilation.md | 4 +- tests/conformance/ch07_exn_payload_guard.vera | 59 +++ tests/conformance/manifest.json | 24 +- ..._closure_boundary_widths_1255_1256_1269.py | 22 +- tests/test_exn_throw_payload_1268.py | 457 ++++++++++++++++-- vera/README.md | 4 +- vera/cli.py | 8 +- vera/codegen/closures.py | 8 + vera/codegen/contracts.py | 60 ++- vera/codegen/functions.py | 13 + vera/verifier.py | 138 ++++-- vera/wasm/calls.py | 27 +- vera/wasm/calls_handlers.py | 122 ++++- vera/wasm/context.py | 40 ++ vera/wasm/helpers.py | 11 + 31 files changed, 952 insertions(+), 151 deletions(-) create mode 100644 tests/conformance/ch07_exn_payload_guard.vera diff --git a/AGENTS.md b/AGENTS.md index 7e8890af..237b02c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Read `SKILL.md` for the full language reference. It covers syntax, slot referenc ### Conformance programs as reference -The conformance suite in `tests/conformance/` contains 241 small, self-contained programs — often one per language feature — that serve as minimal working examples (most are fully self-contained; the cross-module programs of Chapters 7–9 import companion `_lib`/module fixtures). Each positive program must pass its declared verification level (see `manifest.json` for mappings: `parse`, `check`, `verify`, or `run`); the thirty-seven negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) instead must *fail* with the E-code in their `expected_error` field, at the stage their `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses. When you need to see how a specific construct works (e.g. effect handlers, match expressions, closures), check the corresponding conformance program before reading the spec. +The conformance suite in `tests/conformance/` contains 242 small, self-contained programs — often one per language feature — that serve as minimal working examples (most are fully self-contained; the cross-module programs of Chapters 7–9 import companion `_lib`/module fixtures). Each positive program must pass its declared verification level (see `manifest.json` for mappings: `parse`, `check`, `verify`, or `run`); the thirty-seven negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) instead must *fail* with the E-code in their `expected_error` field, at the stage their `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses. When you need to see how a specific construct works (e.g. effect handlers, match expressions, closures), check the corresponding conformance program before reading the spec. ### Workflow @@ -187,9 +187,9 @@ Each stage is a module with a single public API function (`parse_file`, `transfo pytest tests/ -v # Run all tests (see TESTING.md) pytest tests/test_conformance.py -v # Conformance suite only mypy vera/ # Type-check the compiler -python scripts/check_conformance.py # All 241 conformance programs hold (positives pass; negatives fail with their E-code) +python scripts/check_conformance.py # All 242 conformance programs hold (positives pass; negatives fail with their E-code) python scripts/check_examples.py # All 42 examples must pass -python scripts/check_corpus_canonical.py # All 290 corpus programs in canonical form +python scripts/check_corpus_canonical.py # All 291 corpus programs in canonical form ``` Test helpers follow a pattern: `_check_ok(source)` / `_check_err(source, match)` / `_verify_ok(source)` / `_verify_err(source, match)`. See existing tests for examples. @@ -198,7 +198,7 @@ When implementing a new language feature, write the conformance program *first* ### Invariants -- All 241 conformance programs in `tests/conformance/` must hold at their declared level — positive entries pass, and the negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) must *fail* with their `expected_error` E-code, at the stage `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses (`ch08_module_prelude_adt_contention_rejected` → E621), which also asserts the program type-checks cleanly first +- All 242 conformance programs in `tests/conformance/` must hold at their declared level — positive entries pass, and the negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) must *fail* with their `expected_error` E-code, at the stage `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses (`ch08_module_prelude_adt_contention_rejected` → E621), which also asserts the program type-checks cleanly first - All 42 examples in `examples/` must pass `vera check` and `vera verify` - `mypy vera/` must be clean - `pytest tests/ -v` must pass diff --git a/CHANGELOG.md b/CHANGELOG.md index c151d375..322a7157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Two imports supplying one bare name are refused, in every namespace** ([#1304](https://github.com/aallan/vera/issues/1304)). Spec §8.5 ordered a local declaration against an import (§8.5.2) and gave the module-qualified form for reaching what a clash hides (§8.5.3), but defined no order between two *imports* that both supply one name. Neither did the implementation, and the gap was reachable: a module importing two dependencies that each export `forall fn gen` — one returning `@Int`, one `@Bool` — bound its bare call to whichever supplier a set of module paths happened to yield first, so one unchanged file was `vera check`-green on one run and `[E121] body has type Bool` on the next. Measured at the branch point across eight consecutive runs and eight hash seeds: accepted on seeds 0, 2 and 3, rejected on 1, 4, 5, 6 and 7, with the winner tracking module-name hash order rather than which import is written first. Codegen's E608 rail caught the *entry-visible* pair before it could matter there; the flap lived in the shapes the rail only reached at compile, from inside a module the entry program merely imports. Spec §8.5.2.2 now states the rule — a program **MUST NOT** leave a namespace with two imports supplying one bare function name — and the checker enforces it as **E155**, a check-phase code for a scope question that had been enforced by a codegen rail at the wrong layer. Refusing is what removes the flap rather than merely labelling it: with no pick to make, there is no iteration order left to expose, which a deterministic first-wins order would not have achieved (it would make the resolved declaration implicit in import sequence, §0.2.2, and let a library *adding* an export silently rebind a downstream bare call). The refusal is **definition-gated**, matching the rail it generalises: it fires because the import pair exists, not because a body names it, so an entry program importing two suppliers and never calling either is refused exactly as E608 already refused it, and rewriting a bare call in module-qualified form does not lift it. Two shapes clear it, both exercised through to their runtime value: a **local declaration** of the name (§8.5.2 — every bare call is then the local one, and each import stays reachable through `dep::name(...)`), or a **selective import** narrowing the other module's list. The ambiguity predicate is the one `namespace_fn_names` already derived for #1281 and #1299, now exposed per namespace as well as unioned, so the layer that refuses early and the layer that backstops it cannot disagree about which shape is ambiguous; the E608 condition keeps its cell, driven through a door that bypasses the checker. An ambiguous name is not injected into the type environment at all — reporting the clash while binding one supplier would leave the follow-on diagnostics keyed to whichever module the injection loop reached first, which is the nondeterminism the refusal exists to remove — so a bare call to it misses with an ordinary `E200` instead — an E-coded diagnostic emitted at **warning** severity, which the `--json` envelope reports in `warnings` rather than `diagnostics` and which does not fail the check on its own (measured: a program whose only diagnostic is `E200` reports `ok: true` and exits 0). The W-series is the separate `W001`/`W002` code namespace, and this is not one of them. **The data namespaces flapped the same way and are folded in.** Spec §8.5.4 gives constructor names the same shadowing rules as function names, which a function-only refusal would have made false: two modules each exporting a `public data Shape` with different constructor field types type-checked on some hash seeds and reported `[E213]` on others (accepted on seeds 2, 8, 9, 10 and 11; rejected on 0, 1, 3, 4, 5, 6 and 7), and the accepting seeds were the worse half — `check` **and** `verify` both passed, and the program died at `run` with an `E609` located at line 0 of the entry file, naming two modules the entry never imported. Data types are now **E156** and constructors **E157**, one code per declaration namespace exactly as codegen splits E608/E609/E610, and reported independently because they come apart: two modules exporting differently-named types that share a constructor name clash on the constructor alone. Their remedy differs from the function one and says so — E609/E610 refuse two modules' same-named data declarations by DECLARATION, consulting neither visibility nor the importer's filter nor local shadowing (the relaxation E608 received in [#1281](https://github.com/aallan/vera/issues/1281) has no data-side twin), so narrowing an import or shadowing the name locally leaves the program `E609` at compile. Both were measured against the fixture and both fail — as does marking one declaration `private` — so the two diagnostics prescribe renaming, and a cell pins that measurement so the fix text cannot drift into offering remedies that do not work. That rail over-breadth is now tracked as [#1317](https://github.com/aallan/vera/issues/1317). **A name the built-in registry already owns is not a clash** — the injection loops are `setdefault` over a `TypeEnv` the built-ins populate first, so a dependency exporting its own `option_map` never wins the bare name (measured as `E201` against the *prelude's* two-argument signature). The first cut of this refusal did not pass the built-in snapshot and reported two such dependencies as a clash, which was a new rejection rather than an earlier one; `namespace_fn_names`' claim that its ambiguity half is identical with or without the prelude argument was wrong for the same reason and is corrected, with the codegen call ordering it depends on now pinned by a cell. +- **A `throw` payload is runtime-guarded, not only obligated** ([#1268](https://github.com/aallan/vera/issues/1268)). `throw(v)` narrows `v` into the `Exn` payload, and since the static half of this issue the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist and an unverified `vera compile`/`run` delivered the violating value anyway. `throw(0 - 5)` under `effects(>)` ran to completion and returned **-5** through the `@Nat` payload; the refined spelling (`type Pos = { @Int | @Int.0 > 0 }`) did the same. Worse than a wrong answer: a handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke — a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a **postcondition violation at run time on a postcondition `vera verify` had proved**. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s ([#1203](https://github.com/aallan/vera/issues/1203)): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined FIRST, as at every other narrowing site — the §2.6.5 predicate guard for a refined payload, which traps through `$vera.contract_fail` naming the predicate that failed (`Refinement violation in throw(@Pos) / payload: @Int.0 > 0 failed`). The three arms mirror the verifier's own obligation triple one-for-one, so the obligation stream and the emitted guards stay in lock-step: the payload obligation is now `guarded` at all three arms and its Tier-3 leg is counted in `tier3_runtime` rather than disclosed as `tier3_unguarded`, and the refined arm's `guarded` claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. That mirror needed one repair to be true: it answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud `E618` because the outer predicate alone would silently drop the inner membership — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen. It now bails on a refinement base, and the obligation discloses `tier3_unguarded` while `E618` still refuses. The same audit found the **qualified spelling recording something different from the bare one**: the `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since [#1203](https://github.com/aallan/vera/issues/1203), so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed `E504`/`E506` for a boundary that traps, and `State.put(v)` had been doing the same since #1203. Both now take the bare arm's rule on the same key (`op.parent_effect`), so the two spellings of one operation record identical statuses; a user-declared effect's operation and `IO.sleep`'s `@Nat` formal stay the honest [#754](https://github.com/aallan/vera/issues/754) unguarded class. Two diagnostic rationales (`E504`, `E531`) that listed the `throw` payload among the unguarded sites — false once the guard landed, and contradicting the spec sentences this change amends — no longer do. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it, so `CellNames` now carries the type expression its producer already held rather than parsing one back out of a mangled family name. The predicate lowering itself is injected into the translation context (`set_refinement_guard_emitter`), because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. An unrefined payload's WAT is byte-identical to before: a differential over all 278 pre-existing corpus programs — every `examples/` and `tests/conformance/` program, compiled and verified on both trees — moves nothing, in emitted WAT or in the obligation and diagnostic streams. - **`json_parse` accepts one domain, and both runtimes accept it** ([#1306](https://github.com/aallan/vera/issues/1306)). The reference host parsed with `json.loads`, whose default `parse_constant` admits `NaN`, `Infinity` and `-Infinity`; the browser gated with `JSON.parse`, which refuses them as RFC 8259 requires. So the two hosts disagreed about *which call* rejects a non-finite value: the browser at the parse with a handled `Err`, the reference host at `json_stringify` — and there as a raw Python traceback rather than a Vera error ([#1302](https://github.com/aallan/vera/issues/1302) below). Spec §9.7.1 now states the accepted domain instead of leaving each host to inherit its parser's: RFC 8259-valid text that decodes to finite numbers and strings of Unicode scalar values, everything else `Err` at the parse with the same message on every runtime. **A non-finite number has two entry routes and the domain closes both.** The constants are one; the other is a syntactically valid number that overflows — `1e999`, `-1e999`, `[1e999]`, `{"a":1e309}` — which both host parsers accept, decoding to an infinite `JNumber` that then died at `json_stringify`, the same divergent-refusal-point defect one syntax over. RFC 8259 §6 sets no limit on a number's range and says an implementation may set one; Vera's is the finite `Float64` values, which is exactly what `json_stringify` can write back. **The refusal covers the integer spelling too**, and that half was reference-host-only: `json.loads` returns a Python `int` for a digit string with no fraction and no exponent, so `1` followed by 309 zeros never met a float range check — and then had to become an f64 at the WASM boundary, where `float()` raises. It died with `int too large to convert to float` where `JSON.parse`, which has no int/float split and sees an `Infinity` either way, returned the shared sentence. The integer bound is the double **rounding** boundary (`2**1024 - 2**970`) rather than `sys.float_info.max`, and compared in integer arithmetic: an integer *larger* than the largest finite double still rounds to it and both hosts accept it, so the obvious bound would have traded this divergence for its mirror image, and a bound implemented as `float(value)` would be the very overflow it is looking for. Underflow is not the same question and is not refused: `1e-999` decodes to `0`, finite and in the domain, pinned as a control beside `1e308` and the largest representable double so a refusal cannot generalise from "unrepresentable magnitude" to "large". Both value-level exclusions — overflow and lone surrogate — are found by ONE document-order walk returning the sentence itself, so "whichever comes first names the refusal" is the rule rather than a precedence table the two hosts could implement differently. The domain is the parse-side counterpart of the canonical output form [#1293](https://github.com/aallan/vera/issues/1293) pinned — a non-finite number has no JSON representation in either direction — and with no entry route through `json_parse`, the output-side refusal is now reachable only from a `JNumber` a program *constructed* from `nan()` or `infinity()`. The reference host's `parse_constant` hook **records rather than raises**, and the refusal is decided after the parse completes. Raising on sight would have made it answer a different question from the browser's: Python's scanner calls the hook the moment it sees the token, so `[Infinity_x]` — malformed for a reason that has nothing to do with the constant — would have reported the non-finite sentence natively while the browser reported a syntax error. Recording and continuing asks what the browser asks, by substituting `0` for each bare constant and re-parsing: *would this text be valid JSON if the constants were admitted?* Only then is the constant the whole story, and only then do both hosts say the same sentence; text malformed for any other reason keeps its host-native syntax message, as every syntax error always has. The browser's scan also only considers a token where a *value* may begin — the start of the text, or after `[`, `,` or `:`. Without that it found `NaN` at offset 1 of `-NaN`, substituted, re-parsed `-0` successfully and reported the shared sentence, where the reference host's parser never reaches the token at all and gives a syntax error. `-NaN`, `[-NaN]`, `+Infinity`, `infinity`, `nan`, `NaNx` and `-Infinityx` are all pinned as host-native on both hosts. The parity battery pins all four probe inputs from the issue's table plus the container and multi-constant shapes, compares the whole `Err` message across hosts rather than which arm was taken, and runs beside controls the refusal must not disturb — `"NaN"` as an ordinary string value among them. diff --git a/CLAUDE.md b/CLAUDE.md index 6c6ce211..dde844be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,10 +59,10 @@ VERA_EAGER_GC=1 vera run file.vera # Force GC on every alloc (see ENVIRONMENT.m VERA_DEBUG_HOST_ERRORS=1 vera run file.vera # Re-raise a host callback's own exception (see ENVIRONMENT.md, debug knob for host-binding bugs) mypy vera/ # Type-check the compiler itself -python scripts/check_conformance.py # Verify all 241 conformance programs (positives pass their level; negatives fail with their expected_error E-code) +python scripts/check_conformance.py # Verify all 242 conformance programs (positives pass their level; negatives fail with their expected_error E-code) python scripts/check_examples.py # Verify all 42 examples parse + check + verify python scripts/check_examples_run.py # Run every runnable example trap-free under the native runtime; the rest carry a documented skip property, and an example that is neither is an error -python scripts/check_corpus_canonical.py # Verify all 290 corpus programs are in canonical form (vera fmt) +python scripts/check_corpus_canonical.py # Verify all 291 corpus programs are in canonical form (vera fmt) python scripts/check_examples_readme.py # Verify vera run commands in examples/README.md python scripts/check_spec_examples.py # Verify spec code blocks parse python scripts/check_readme_examples.py # Verify README code blocks parse @@ -94,7 +94,7 @@ See [`TOOLCHAIN.md`](TOOLCHAIN.md) for the CLI cookbook — driving the toolchai - `vera/` — Reference compiler: grammar, parser, AST, transformer, type checker, verifier, codegen, CLI - `examples/` — 42 example Vera programs (all must pass `vera check` and `vera verify`) - `tests/` — Test suite (unit tests + conformance suite) -- `tests/conformance/` — 241 conformance programs validating every language feature against the spec +- `tests/conformance/` — 242 conformance programs validating every language feature against the spec - `scripts/` — CI and validation scripts ## Writing Vera code @@ -131,7 +131,7 @@ Before changing code — **adding or removing** — write the test that proves y ## What not to break - Pre-commit hooks run mypy + pytest + conformance suite + example validation on every commit -- All 241 conformance programs in `tests/conformance/` must hold at their declared level — positive entries pass, and the negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) must *fail* with their `expected_error` E-code, at the stage `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses (`ch08_module_prelude_adt_contention_rejected` → E621), which also asserts the program type-checks cleanly first +- All 242 conformance programs in `tests/conformance/` must hold at their declared level — positive entries pass, and the negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) must *fail* with their `expected_error` E-code, at the stage `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses (`ch08_module_prelude_adt_contention_rejected` → E621), which also asserts the program type-checks cleanly first - All 42 examples in `examples/` must pass `vera check` and `vera verify` - Version must stay in sync across `pyproject.toml`, `vera/__init__.py`, `docs/index.html`, `README.md`, and `uv.lock` (gated by `scripts/check_version_sync.py`); CHANGELOG.md must also carry a matching `## [X.Y.Z]` section - All tests must pass: `pytest tests/ -v` diff --git a/FAQ.md b/FAQ.md index cb6f8aae..b4b42e73 100644 --- a/FAQ.md +++ b/FAQ.md @@ -236,7 +236,7 @@ None of this is Vera-specific, but it validates the design choices. The thesis i This is a real concern. LLMs are trained on trillions of tokens of Python, TypeScript, and JavaScript. A MojoBench study (NAACL 2025) found that even fine-tuned models achieved only 30–35% improvement over base models on Mojo code generation, illustrating the cold-start problem for new languages. -Vera's approach has three parts. First, the agent-facing documentation (SKILL.md) is designed to be dropped into a model's context window, so the model works from the language specification rather than training data recall. Second, Vera's syntax is deliberately simple and regular — fewer constructs, each with exactly one canonical form — which reduces the surface area a model needs to learn. Third, the conformance test suite (241 programs covering every language feature) gives models concrete examples to learn from and conform to. Simon Willison's December 2025 JustHTML write-up illustrates the same point in practice: an LLM-assisted implementation, guided by the html5lib conformance suite, conformed to the HTML parsing spec by running against its tests — a comprehensive test suite is a strong scaffold for a model implementing to a specification. +Vera's approach has three parts. First, the agent-facing documentation (SKILL.md) is designed to be dropped into a model's context window, so the model works from the language specification rather than training data recall. Second, Vera's syntax is deliberately simple and regular — fewer constructs, each with exactly one canonical form — which reduces the surface area a model needs to learn. Third, the conformance test suite (242 programs covering every language feature) gives models concrete examples to learn from and conform to. Simon Willison's December 2025 JustHTML write-up illustrates the same point in practice: an LLM-assisted implementation, guided by the html5lib conformance suite, conformed to the HTML parsing spec by running against its tests — a comprehensive test suite is a strong scaffold for a model implementing to a specification. ## How does Vera compare to Dafny / Lean / Koka / F*? @@ -279,7 +279,7 @@ The reference compiler is under active development. The current release includes - A seven-stage pipeline: parse, transform, resolve, typecheck, verify, compile, execute - A 14-chapter formal specification -- 11,600 tests, including a 241-program conformance suite +- 11,623 tests, including a 242-program conformance suite - 42 working example programs - 164 built-in functions covering strings, arrays, math, parsing, and data types - Four built-in abilities (Eq, Ord, Hash, Show) with constrained generics and ADT auto-derivation diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 45ce79f8..7deeba31 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -23,7 +23,6 @@ Defects in shipped compiler, runtime, or tooling behaviour — this table matche | Spec Chapter 10 is a second, hand-maintained copy of `vera/grammar.lark`, and the rule-name alignment gate `scripts/check_grammar_alignment.py` compares only rule-name headers — three classes of drift pass it unseen. **Terminals, both directions**: the header pattern requires a lowercase lead, so a fabricated terminal added to §10.2 leaves the gate green, and no declared-versus-referenced audit exists in either direction. **Rule references**: restoring a removed ambiguity to a production's right-hand side is invisible, because only headers are compared. **Production bodies**: the class most grammar edits actually fall into. Two instances are live in the chapter on `main` today, both body-level and both this issue's to close: typed holes (`"?" -> hole_expr`, in `grammar.lark` since 2026-03-30) appear nowhere in `primary_expr`, and §10.2's `BLOCK_COMMENT: /\{-[\s\S]*?-\}/` is non-nesting, contradicting both §1.3 ("They nest") and the implementation, which parses `{- a {- b -} c -}` clean. Never a wrong answer from a program — the defect is that the published grammar misdescribes the one the parser has. Fix direction: extend the gate side-aware (terminal audit both ways, reference-set comparison), or fold Chapter 10 toward the DESIGN Grammar row's actual promise of a single-sourced shared grammar rather than a hand-maintained copy held honest by ever-wider cross-checks; the two live instances are fixable independently of which direction wins. | [#1290](https://github.com/aallan/vera/issues/1290) | | `.github/workflows/release.yml`'s `Tag and create GitHub Release` step fails with `HTTP 422: Validation Failed — body is too long (maximum is 125000 characters)` when the CHANGELOG section `scripts/release.py notes` extracts into `RELEASE_NOTES.md` exceeds GitHub's release-body limit. It fired on v0.1.10, whose notes extract to roughly 148,700 bytes — the #1213 burndown's 44-issue section, some 23,000 characters past the limit — and it fired at the worst point in the pipeline: **after** PyPI had accepted the immutable archives and **after** the tag was created, leaving the release half-cut with no repeatable path back. The v0.1.10 GitHub Release was completed by hand, mirroring the step exactly: the run's artifacts downloaded and hash-verified three ways, then `gh release create --verify-tag --latest` with the wheel, sdist and SHA256SUMS, and a generated body — the section's bold bullet lead-ins as a headline index plus a link to the canonical section at the tag. Rare, since it needs a release this large, but rarity is not the mitigating factor here; the landing point is. Fix direction: make the step total — before `gh release create`, regenerate oversized notes into that index form so the release always carries a body that fits, with the full notes staying in `CHANGELOG.md`, which is already the release notes of record. | [#1288](https://github.com/aallan/vera/issues/1288) | | `_infer_vera_type` (monomorphization's Vera-level type namer) still reads only `then_branch` of an `IfExpr` and only `arms[0]` of a `MatchExpr` — the #1276 join fix landed in its WAT-side siblings but not here. Confirmed by inspection; latent: a wrong answer needs an instantiation discovered through a divergent-first-arm conditional, and no check-green program reaching that shape has been constructed (the checker's upstream unification constrains the same joins). The fix is the mechanical #1276 mirror — take the first arm that yields a type — landing with the failing program that proves it, per the test-first rule. | [#1286](https://github.com/aallan/vera/issues/1286) | -| A `throw` payload is statically obligated but not runtime-guarded: `throw(v)` narrows `v` into the `Exn` payload and the narrowing now carries the same obligation every other binding site does — a provably-violating value is a loud E503/E505 and an undischargeable one is a disclosed `tier3_unguarded` E504/E506/E531 — but codegen emits no predicate or sign guard on the payload (`throw` lowers straight to `throw $exn_` with the value on the stack), so an unverified `vera compile`/`run` still passes a violating value through. Same disclosed-unguarded class as the user-effect operation argument in #754 below, and the guard wants the same metadata; fix direction: emit the guard at the `throw` op-call site, reusing the #1203 `put`-store guard emission and the §2.6.5 boundary predicate lowering. | [#1268](https://github.com/aallan/vera/issues/1268) | | `ch05_closure_nat_return` (a run-level conformance program in the pre-commit + CI gate) trapped **once** in a full `check_conformance.py` run (`unreachable` in `main` — the sentinel `assert` or a GC shadow-stack guard) and has not reproduced in ~960 attempts across isolated, parallel, eager-GC, and hash-seed-swept executions; the emitted WAT is deterministic and correct. Suspected rare runtime/GC/wasmtime interaction, tracked so a future intermittent CI red resolves here instead of starting fresh. | [#996](https://github.com/aallan/vera/issues/996) | ## Limitations @@ -36,7 +35,7 @@ Things Vera cannot do yet, as distinct from defects in what it claims to do. | An ADT `decreases` measure is runtime-ranked only when its type's reachable field structure is fully concrete: a parameterized measure (`List`), or a concrete type whose recursion rides through a parameterized field, gets no runtime guard (never a wrong one — the registered generic layout does not describe concrete construction), staying statically checked and Tier-3-disclosed. Per-instantiation `$dec_size_` helpers (the `$eq_` pattern) close it. | [#1177](https://github.com/aallan/vera/issues/1177) | | Tier 2 verification (Z3 guided by `assert`/lemma hints) is specified in §6.3.2 but not implemented, so contracts that need hints fall to Tier 3 runtime checks. Per-monomorphization verification (#732) has since landed; full Tier 2 (hint-guided) stays on the Milestone 4 horizon. | [#427](https://github.com/aallan/vera/issues/427) | | `data invariant(...)` clauses (spec §2.6, §6.2.3) are not implemented — every documented form fails with E130 because the slot environment for the invariant predicate isn't wired up. Refinement types (`{ @T \| predicate }`, §2.6) are the working alternative until this lands. | [#686](https://github.com/aallan/vera/issues/686) | -| **Statically**, every narrowing *binding site* is obligated: a provably-negative value is an E503 error, and an untranslatable narrowing at an unguarded site is an E504 warning — so a program whose narrowings are all statically PROVEN stores no negative `@Nat` at these sites. `vera verify` exits 0 on the E504 warnings too, so verify-clean is the weaker property: a disclosed narrowing is one the static proof did not reach, and at an unguarded site nothing else does either. **At runtime** (an unverified `vera compile`/`run`), the guard covers every concrete *direct* binding site (a nested constructor sub-pattern is the exception, #765 below), generic function-formal calls, and the string/markup builtin `@Nat` parameters (`string_repeat`, `string_pad_start`/`_end`, `string_from_char_code`, `md_has_heading`), but three statically-obligated sites stay unguarded and store a negative silently: the effect-operation argument — user-declared effects, and the built-in `Exn` `throw` payload (#1268), the built-in `State` `put`/`resume` boundaries being obligated and codegen-guarded — (codegen's `_effect_ops` carries only the dispatch target), the generic-instantiated constructor field (constructor layouts carry no per-field `@Nat` mono metadata), and the `nat_to_int`/`nat_to_string` conversion builtins (special-cased before the `_fn_nat_params` guard loop) — no runtime guard is emitted at those three sites; the unguarded residual is disclosed statically as an E504 warning instead. (The static obligation now also covers the function **return** position — an `@Int` value narrowing into a `@Nat` return is an E503 obligation backed by a codegen return guard, #758 — and the value-position tuple/constructor component, which is both statically obligated and runtime-guarded at the function boundary.) #820 threaded the per-component target-type table into codegen (the metadata these narrowing guards need); the generic-field and effect-op sites are now unblocked for wiring, though the narrowing direction is not yet guarded here. | [#754](https://github.com/aallan/vera/issues/754), [#757](https://github.com/aallan/vera/issues/757) | +| **Statically**, every narrowing *binding site* is obligated: a provably-negative value is an E503 error, and an untranslatable narrowing at an unguarded site is an E504 warning — so a program whose narrowings are all statically PROVEN stores no negative `@Nat` at these sites. `vera verify` exits 0 on the E504 warnings too, so verify-clean is the weaker property: a disclosed narrowing is one the static proof did not reach, and at an unguarded site nothing else does either. **At runtime** (an unverified `vera compile`/`run`), the guard covers every concrete *direct* binding site (a nested constructor sub-pattern is the exception, #765 below), generic function-formal calls, and the string/markup builtin `@Nat` parameters (`string_repeat`, `string_pad_start`/`_end`, `string_from_char_code`, `md_has_heading`), but three statically-obligated sites stay unguarded and store a negative silently: the effect-operation argument of a USER-declared effect — the built-in `State` `put`/`resume` boundaries and the built-in `Exn` `throw` payload are obligated and codegen-guarded, the latter with the §2.6.5 predicate guard as well as the sign pair ([#1268](https://github.com/aallan/vera/issues/1268)) — (codegen's `_effect_ops` carries only the dispatch target for the rest), the generic-instantiated constructor field (constructor layouts carry no per-field `@Nat` mono metadata), and the `nat_to_int`/`nat_to_string` conversion builtins (special-cased before the `_fn_nat_params` guard loop) — no runtime guard is emitted at those three sites; the unguarded residual is disclosed statically as an E504 warning instead. (The static obligation now also covers the function **return** position — an `@Int` value narrowing into a `@Nat` return is an E503 obligation backed by a codegen return guard, #758 — and the value-position tuple/constructor component, which is both statically obligated and runtime-guarded at the function boundary.) #820 threaded the per-component target-type table into codegen (the metadata these narrowing guards need); the generic-field and effect-op sites are now unblocked for wiring, though the narrowing direction is not yet guarded here. | [#754](https://github.com/aallan/vera/issues/754), [#757](https://github.com/aallan/vera/issues/757) | | A refinement / `@Nat` narrowing bound in a **nested** constructor sub-pattern (`Some(Some(@PosInt))` on `Option>`) is statically obligated — a `vera verify`-clean program is sound; a bad nested refined narrowing is `E505`, a bad nested `@Nat` narrowing is `E503` (#763) — but the runtime guard covers only the `@Nat` nested bind (verified by run probes: a negative payload traps in both direct and closure positions) — the *refined* nested bind is not guarded, so an unverified compile doesn't trap on a violating nested refined payload; the verifier discloses that residual as `tier3_unguarded`/E506 | [#765](https://github.com/aallan/vera/issues/765) | | A refinement whose base carries a **non-plain type argument** (`{ @Array<{ @Int \| ... }> \| array_length(...) > 0 }`, an `Array` base) gets **no runtime guard at any boundary** — `_refinement_guard_parts` cannot spell the binder slot for such a base, so named-function and closure parameters and returns alike leave the predicate unchecked at run time. The verifier discloses this honestly (`tier3_unguarded`, E506, excluded from runtime-checked totals — `_refined_boundary_codegen_guardable` mirrors the codegen bail, KEEP IN SYNC); the guard itself is the open work. Same disclosure family as #754 / #757 / #765. | [#1036](https://github.com/aallan/vera/issues/1036) | | Contracts that depend on a `handle[...]` expression's value fall to Tier 3 — the verifier does not model the `handle[...]` expression's value or handler-clause state, so an `ensures` over a handle result is runtime-checked rather than statically proved. (Primitive-op and binding obligations *inside* the handle body walk at enclosing-scope precision and can prove Tier-1; a clause body's slot-dependent obligations are fresh-scope Tier-3, while a manifest violation there — `5 / 0` — stays a loud compile error.) | [#439](https://github.com/aallan/vera/issues/439) | diff --git a/README.md b/README.md index 15a5d958..d65c8137 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ cp /path/to/vera/SKILL.md ~/.claude/skills/vera-language/SKILL.md ## Project status -Vera is in **active development** at v0.1.11: 2,000+ commits, 209 releases, 11,600 tests, 95% Python code coverage, 241 conformance programs, 42 examples, and a 14-chapter specification. Known bugs and limitations are tracked in **[KNOWN_ISSUES.md](KNOWN_ISSUES.md)**. See **[HISTORY.md](HISTORY.md)** for how the compiler was built. +Vera is in **active development** at v0.1.11: 2,000+ commits, 209 releases, 11,623 tests, 95% Python code coverage, 242 conformance programs, 42 examples, and a 14-chapter specification. Known bugs and limitations are tracked in **[KNOWN_ISSUES.md](KNOWN_ISSUES.md)**. See **[HISTORY.md](HISTORY.md)** for how the compiler was built. The reference compiler — parser, AST, type checker, contract verifier (Z3), WASM code generator, module system, browser runtime, and runtime contract insertion — is working. The language specification is in draft across [14 chapters](spec/). diff --git a/ROADMAP.md b/ROADMAP.md index c3df6a0f..703d275d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,7 +12,7 @@ Ordering derives from the design principles ([DESIGN.md](DESIGN.md)): verificati ## Where we are -11,600 tests, 241 conformance programs, 42 examples, 14 spec chapters. [KNOWN_ISSUES.md](KNOWN_ISSUES.md) tracks the open bugs — burndown material rather than stage work — plus the *limitations* the stages below retire. +11,623 tests, 242 conformance programs, 42 examples, 14 spec chapters. [KNOWN_ISSUES.md](KNOWN_ISSUES.md) tracks the open bugs — burndown material rather than stage work — plus the *limitations* the stages below retire. ## Stage 19 — The verification completeness sprint diff --git a/SKILL.md b/SKILL.md index ef409117..850b216b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -442,7 +442,7 @@ array, useful when processing diagnostics programmatically. - `Unit` — singleton type, value is `()`. Zero-size and **declaration-only**: a `@Unit` parameter (function or handler-clause op) is legal but reading it (`@Unit.0`) is a checker error (E182) — write the literal `()` instead — and a `let` of a zero-size type (`let @Unit = put(5);`) is a checker error (E183) — call the expression as a statement (`put(5);`). Applies to anything with no runtime representation, including `Future`. - `Never` — bottom type (used for non-terminating expressions like `throw`) -**`Int` and `Nat` are interchangeable in both directions.** `@Nat <: @Int` is a formal subtyping rule at the *type* level (use a `@Nat` anywhere `@Int` is expected, no `nat_to_int` call), and `@Int <: @Nat` is permitted by the type checker with a verifier-discharged obligation (`@Int.0 >= 0`). This means `array_length` (declared `@Int`) flows freely into `@Nat` positions without explicit conversion — the verifier proves non-negativity from context or falls back to a runtime check. Both directions carry a *value*-level obligation, because `@Nat` is a u64 and `@Int` an i64: narrowing requires `>= 0` (`E503`/`E504`), and widening requires `<= i64.MAX` (`E530`; or an `E531` warning at the generic-instantiated `@Int`-field component site code generation cannot guard) — a `@Nat` above i64.MAX bit-reinterprets to a negative `@Int`. Runtime guards and verifier obligations correspond at every closure depth (nested closure returns included); the documented residual runs in one direction only. Obligated-but-not-guarded: three narrowing sites are statically obligated yet carry no runtime guard — the effect-operation argument and the generic-instantiated constructor field ([#754](https://github.com/aallan/vera/issues/754)/[#757](https://github.com/aallan/vera/issues/757)), and the `nat_to_int`/`nat_to_string` conversion builtins — where an `E504` warning discloses the unguarded residual rather than claiming a check the runtime never performs. **Do not** insert `nat_to_int` defensively; `@Nat` already flows to `@Int`. Keep a value that may be negative as `@Int`, or use `int_to_nat` (which returns `Option`) when an explicit narrowing must handle the failure case. See spec §2.2.1 for the formal rule. +**`Int` and `Nat` are interchangeable in both directions.** `@Nat <: @Int` is a formal subtyping rule at the *type* level (use a `@Nat` anywhere `@Int` is expected, no `nat_to_int` call), and `@Int <: @Nat` is permitted by the type checker with a verifier-discharged obligation (`@Int.0 >= 0`). This means `array_length` (declared `@Int`) flows freely into `@Nat` positions without explicit conversion — the verifier proves non-negativity from context or falls back to a runtime check. Both directions carry a *value*-level obligation, because `@Nat` is a u64 and `@Int` an i64: narrowing requires `>= 0` (`E503`/`E504`), and widening requires `<= i64.MAX` (`E530`; or an `E531` warning at the generic-instantiated `@Int`-field component site code generation cannot guard) — a `@Nat` above i64.MAX bit-reinterprets to a negative `@Int`. Runtime guards and verifier obligations correspond at every closure depth (nested closure returns included); the documented residual runs in one direction only. Obligated-but-not-guarded: three narrowing sites are statically obligated yet carry no runtime guard — a user-declared effect operation's argument and the generic-instantiated constructor field ([#754](https://github.com/aallan/vera/issues/754)/[#757](https://github.com/aallan/vera/issues/757)), and the `nat_to_int`/`nat_to_string` conversion builtins — where an `E504` warning discloses the unguarded residual rather than claiming a check the runtime never performs. The built-in effects' operation arguments are guarded: the `State` write boundaries, and the `Exn` `throw` payload, which also takes the refinement-predicate guard ([#1268](https://github.com/aallan/vera/issues/1268)). **Do not** insert `nat_to_int` defensively; `@Nat` already flows to `@Int`. Keep a value that may be negative as `@Int`, or use `int_to_nat` (which returns `Option`) when an explicit narrowing must handle the failure case. See spec §2.2.1 for the formal rule. ### Composite types @@ -2461,7 +2461,7 @@ public fn main(@Unit -> @Unit) ## Conformance Suite -The `tests/conformance/` directory contains 241 small programs — most self-contained, with the Chapter 8 module-system programs and a few cross-module Chapter 7 and 9 programs importing companion `_lib.vera` / `_mid.vera` modules — that validate every language feature against the spec — often one program per feature, though some features (slot references, match, contracts) span several. These are the best minimal working examples of Vera syntax and semantics. +The `tests/conformance/` directory contains 242 small programs — most self-contained, with the Chapter 8 module-system programs and a few cross-module Chapter 7 and 9 programs importing companion `_lib.vera` / `_mid.vera` modules — that validate every language feature against the spec — often one program per feature, though some features (slot references, match, contracts) span several. These are the best minimal working examples of Vera syntax and semantics. Each program is organized by spec chapter (`ch01_int_literals.vera`, `ch04_match_basic.vera`, `ch07_state_handler.vera`, etc.) and the `manifest.json` file maps features to programs. When you need to see how a specific construct works, check the conformance program before reading the spec. @@ -2502,7 +2502,7 @@ Current reference-implementation bugs that an agent writing Vera code is likely |---|---|---|---| | Rare conformance-gate flake | `ch05_closure_nat_return` trapped once in a full conformance run and never again (~960 clean attempts) — suspected runtime/GC timing interaction, not a compiler defect. | If CI reds on this program with `Reached unreachable` in `main`, re-run and report on the issue with wasmtime version + load conditions — do not chase the compiler. | [#996](https://github.com/aallan/vera/issues/996) | -When a Vera program type-checks cleanly, compiles without errors, and then produces a runtime trap you can't explain, runtime trap diagnostics are now Vera-native end-to-end: each trap carries a `kind` label (`divide_by_zero` / `out_of_bounds` / `stack_exhausted` / `unreachable` / `overflow` / `contract_violation` / `host_error` / `unknown`), a per-kind `Fix:` paragraph naming the canonical remediation, and a source backtrace pointing at the offending Vera function and line — not just `wasm trap: `. Tail-recursive iteration runs in constant WASM stack space for both non-allocating ([#517](https://github.com/aallan/vera/issues/517), v0.0.126) and allocating ([#549](https://github.com/aallan/vera/issues/549), v0.0.154) tail calls — the latter prepends a `$gc_sp` restore before each `return_call` to keep the shadow stack bounded across iterations. +When a Vera program type-checks cleanly, compiles without errors, and then produces a runtime trap you can't explain, runtime trap diagnostics are now Vera-native end-to-end: each trap carries a `kind` label (`divide_by_zero` / `out_of_bounds` / `stack_exhausted` / `unreachable` / `overflow` / `contract_violation` / `host_error` / `unknown`), a per-kind `Fix:` paragraph naming the canonical remediation (omitted for `contract_violation` and `host_error`, whose descriptions already carry the specific instruction, and for `unknown`, where there is nothing general to suggest), and a source backtrace pointing at the offending Vera function and line — not just `wasm trap: `. Tail-recursive iteration runs in constant WASM stack space for both non-allocating ([#517](https://github.com/aallan/vera/issues/517), v0.0.126) and allocating ([#549](https://github.com/aallan/vera/issues/549), v0.0.154) tail calls — the latter prepends a `$gc_sp` restore before each `return_call` to keep the shadow stack bounded across iterations. ## Specification Reference diff --git a/TESTING.md b/TESTING.md index d01305b2..b87f8849 100644 --- a/TESTING.md +++ b/TESTING.md @@ -6,9 +6,9 @@ This is the single source of truth for Vera's testing infrastructure, coverage d | Metric | Value | |--------|-------| -| **Tests** | 11,600 across 173 files (~155,000 lines of test code; 11,403 passed + 26 stress, 171 skipped) | +| **Tests** | 11,623 across 173 files (~155,000 lines of test code; 11,426 passed + 26 stress, 171 skipped) | | **Compiler code coverage** | 95% Python, 87% JavaScript (CI minimum: 80%) | -| **Conformance programs** | 241 programs across 9 spec chapters, validating every language feature | +| **Conformance programs** | 242 programs across 9 spec chapters, validating every language feature | | **Example programs** | 42, all validated through `vera check` + `vera verify` | | **Spec code blocks** | 189 parseable blocks from 14 spec chapters: 92 parse, 86 type-check, 85 verify (the rest carry inline `vera:skip` annotations, #538) | | **README code blocks** | 4 Vera blocks (4 validated, 0 annotated) | @@ -43,7 +43,7 @@ pytest tests/test_runtime_traps.py::TestHostErrorDebugKnob1302 -v mypy vera/ # strict mode # Validation scripts -python scripts/check_conformance.py # conformance suite (241 programs, see manifest.json) +python scripts/check_conformance.py # conformance suite (242 programs, see manifest.json) python scripts/check_examples.py # 42 example programs python scripts/check_spec_examples.py # spec code blocks python scripts/check_readme_examples.py # README code blocks @@ -72,13 +72,13 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_naming_env_provenance_1208.py` | 45 | 2,097 | The other half of the #1208 contract: every consumer is handed the ENVIRONMENT the checker rendered under, not just the same renderer. Four provenance seams, each pinned by the adversarial probe that exhibited it — an IMPORTED callee's contract rendered in its DEFINING module's alias namespace (a violated precondition that vanished, and its mirror, a correct call spuriously rejected, both under a `Cnt` that names different bodies on the two sides), an imported GENERIC monomorphized and verified in that same namespace (a lying postcondition that proved clean, plus a verifier↔codegen clone differential over the recounted slot references — the desync is invisible to a unit test on either side), a `forall` variable shadowing a same-named module alias wherever a generic signature renders (the mono clone, a body `let`, the verifier's collapsed premises, and the exported uninstantiated template), and the tester's `SmtContext` holding the narrowed scope its own names were keyed in — the last one behavioural, since a generator handed the wrong scope collapses two parameters onto one variable and returns NO inputs at all. Three of the seams are also crossed against a SECOND component rather than checked for internal consistency, because a wrong-but-consistent scope is invisible from inside one: the verifier's declared parameter names against `slots.slot_table`'s, its `where`-helper scope against `slots.fn_scopes`' accumulation, and the monomorphizer's post-substitution names against what the consumers rebuild on the clone — each independent on the axis under test (which variables the two sides narrow by) but sharing `fn_slot_scope`/`slot_name` below it, so the hand-derived literal rendering beside every comparison is what a shared-renderer defect cannot satisfy. An imported generic nested under a non-generic function is pinned too — both its discovery-time recount and its verification-time clone must run in the DEFINING module's namespace. Three more seams arrived from the PR #1224 review, each with the false Tier-1 or miscompile that exhibited it: an UNPINNED callee (an imported generic's own `where`-helper, which the origin registry never pins) rendering in the module under verification rather than the entry program, whose absence let a violated precondition discharge as true and trap at run time; a callee's refined-RETURN predicate translated in the callee's namespace alongside its `requires`/`ensures`, pinned by provenance because today's bare-headed binder masks it behaviourally; and codegen's declaration-index space keyed PER NAMESPACE, without which a module's stamp turned the main file's forward alias reference into a backward one and a check-clean, verify-clean program read the wrong parameter through valid WASM. Two seams from the #1213 burndown close the same shape from the other side: the prelude's own aliases are injected only into the reserved `Vera` namespace, so no name a program can spell resolves on the codegen side alone (#1221 — the differential compares the checker's and codegen's partition of one signature, with the emitted WAT beside it), and an imported ADT is ordered at the index its OWN module gave it rather than the built-in floor (#1227), each with the control that differs by exactly the namespace under test. Controls carried alongside: the same programs with the alias renamed or the shadowing declaration removed, and the runtime oracle that shows the new E500/E501 agrees with the emitted code rather than merely reporting more | | `test_callee_contract_scope_1220_1225_1226.py` | 40 | 1,433 | A callee's contract is READ in the callee's own module. Three burndown defects, each asserted against the runtime oracle wherever the two directions of a wrong namespace (an obligation that vanishes, one that fires for no reason) look identical from inside the verifier: an E501's `Precondition:` line quoted from the file that DECLARED the clause (#1220 — in the misattribution direction too, both files carrying a plausible `requires` on the same line number, plus the imported-generic `where`-helper whose clause sits past the end of a short importer and used to quote nothing at all); a bare-name call inside an IMPORTED callee's contract resolved through the CALLEE's module registry (#1225 — the false Tier-1 whose run traps and its mirror spurious E501, through the `requires` and the `ensures` path, against a no-collision control); and the refined-RETURN binder derived through the naming layer, so a refinement over a PARAMETERISED base pushes the key its own predicate looks itself up under (#1226 — single-module and cross-module, the second proving the derivation happens INSIDE the callee scope); plus the PR #1239 review round — a module's pinned registry holds what its OWN file imports (a DEPTH-2 chain, the shape the single-level corpus could not exhibit: requires and ensures directions, bare-vs-qualified tier agreement, a name outside the middle module's import filter still missing per §8.5.1, and the mirror gate that filling the registry does not re-export), and every part of a diagnostic follows the declaring module (location, file name and excerpt, including a clause past a shorter importer's end, with a multi-line clause quoted whole), and the characterization of the binder-reference walk's one documented exception — a CLOSURE inside a predicate owns the first `@T.n` in traversal order, whose consequence is a Tier-3 demotion rather than a fact assumed about the wrong term; and the mini-review round — an obligation carries the FILE its line number belongs to, so the documented `(file, line, column)` join between the two `--json` arrays holds for a module-located obligation, with the entry-file control and warm==cold parity, and a multi-line clause is quoted with its `--` comments blanked (a `--` inside a string literal surviving, which a naive split would corrupt) | | `test_alias_application_refinement_base_1237.py` | 11 | 398 | A parameterised alias APPLICATION substitutes its arguments in the verifier's own resolver (#1237). `type Box = T;` applied as `@Box` resolved to `AdtType('T')` — the alias's binder leaking as an ADT name — so a refinement over it failed the modelled-primitive gate, the refined-return fact was dropped, and a valid program was rejected with a spurious E501 while `vera run` returned the right answer. Both halves of the fix are asserted separately: the alias body registers its own parameters as type variables (`substitute` maps type variables, so an ADT-named binder is unsubstitutable however the application side is written) and the application substitutes. Plus depth (an argument that is itself an application, and an alias whose body applies another alias), the bounded direction (a consumer wanting `>= 100` where the refinement grants `>= 18` is still rejected, and the runtime agrees), and the gate that must NOT move — an unmodelled `@Byte` base resolves correctly and still degrades to a Tier-3 runtime check, with a consumer of its predicate still refused | -| `test_exn_throw_payload_1268.py` | 20 | 453 | `throw`'s payload is obligated like every other narrowing site (#1268). `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with ZERO obligations while `vera run` returned -5 out of the `@Nat` payload — check-green, verify-green, silently wrong — because `throw` is a bare call with no function-registry entry, so the argument loop never saw it and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. All three arms are asserted (the `@Nat` refutation, the refined refutation over a modelled base, and the `@Nat`->`@Int` widening obligation appearing where none existed), plus the #1251(b) concrete gate reaching the payload for free (`throw(200)` into an `Exn<{ @Byte \| @Byte.0 < 10 }>` names the value; the satisfying twin proves at Tier 1), the user-effect contrast that localized the bug (a declared op's argument was loud for the same value — both are loud now, at the same site name), and the refined-alias payload spelling. The unguarded disclosure is checked against codegen rather than asserted: an undischargeable payload must land on the `tier3_unguarded` E504 leg (excluded from the totals), and a run confirms the payload really is unchecked — the two together go red whichever side moves without the other. A dischargeable twin proves the site is not merely always-loud, and the six `Exn` conformance programs are verified whole as canaries so obligating a position that had none names itself instead of arriving as one line of a corpus sweep | +| `test_exn_throw_payload_1268.py` | 32 | 848 | `throw`'s payload is obligated AND runtime-guarded like every other narrowing site (#1268). `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with ZERO obligations while `vera run` returned -5 out of the `@Nat` payload — check-green, verify-green, silently wrong — because `throw` is a bare call with no function-registry entry, so the argument loop never saw it and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. All three arms are asserted (the `@Nat` refutation, the refined refutation over a modelled base, and the `@Nat`->`@Int` widening obligation appearing where none existed), plus the #1251(b) concrete gate reaching the payload for free (`throw(200)` into an `Exn<{ @Byte \| @Byte.0 < 10 }>` names the value; the satisfying twin proves at Tier 1), the user-effect contrast that localized the bug (a declared op's argument was loud for the same value — both are loud now, at the same site name), and the refined-alias payload spelling. The GUARD is checked against codegen rather than asserted: an undischargeable payload must land on the runtime-guarded `tier3` leg, counted in the totals, and a run must confirm the payload really is stopped — the two together go red whichever side moves without the other (delete the emission and the runs go red; flip the flag back and the statuses do). Both arms are run at the boundary: a `@Nat` payload traps on -5 and delivers 5, a refined `{ @Int \| @Int.0 > 0 }` payload traps through the `$vera.contract_fail` channel on BOTH -5 and 0 — the value that clears the base's `>= 0` and violates the predicate, so a sign guard standing in for the predicate guard fails here — and delivers 7. The type gate carries its own over-refusal control: an `Exn` payload has no invariant to violate, so a negative one is a correct program and must still return -5. Both REPRESENTATIONS are covered — a scalar payload in one local, and a `@String`-based one whose (ptr, len) pair has to be saved in two, checked over the ptr and put back in the right order (the satisfying twin is what shows the order). The **soundness differential** is the point of the guard rather than a property of it: the clause parameter's type is what the verifier hands every downstream consumer, so a consumer discharging `ensures(@Bool.result)` at Tier 1 from its `@Nat` parameter alone is asserted PROVED and then run — pre-fix the run reported a postcondition violation on that proved postcondition, with nothing else in the path (the argument is already `@Nat`-typed, so no call-site narrowing guard fires). A dischargeable twin proves the site is not merely always-loud, and the six `Exn` conformance programs are verified whole as canaries so obligating a position that had none names itself instead of arriving as one line of a corpus sweep. The adversarial round adds the three places the `guarded` PROMISE and the emitted guard could disagree in a direction no value oracle can see, because the program either never runs or runs identically either way: a refinement OVER a refinement is asserted `tier3_unguarded` AND `E618`-refused at compile in one cell (the mirror claimed a runtime check for a program that cannot be compiled at all — either half alone reads as consistent); the bare and qualified spellings of one operation are asserted to record IDENTICAL statuses as a differential rather than two literals, on both arms, with the run confirming which value is the true one (`Exn.throw` was disclosed unguarded while codegen delegated it to the guard-emitting dispatcher, and `State.put` had been since #1203); and `E504`'s rationale is read from a real diagnostic — reached through the site that IS still unguarded, a user-declared effect's operation argument — to pin that it no longer lists the `throw` payload among the sites with no runtime guard | | `test_refinement_binder_convergence_1208.py` | 16 | 459 | Codegen's refinement boundary guard and `vera/naming.py` derive the predicate binder ONCE (#1208). A per-shape differential over a direct refinement, an alias hop, `@Nat`- and `@Byte`-based refinements (both range-conjoining), a composite base whose binder is a RESOLVED argument list, and two non-refinements that must be `None` on both sides — plus the property the convergence exists to hold, that the guard's binder equals the key a predicate's own `@Base.n` resolves to. A per-shape differential is green either way while two copies agree — which is exactly how a duplicated derivation drifts unnoticed — so the load-bearing assertion is a MUTATION: perturb `naming.refinement_binder_parts` and codegen's guard must report the perturbed binder. Codegen's two layered WASM decisions are pinned alongside — the loud E618 for a nested refinement base, reported ONCE per declaration however many call sites consult the derivation and however many clones a generic is instantiated into (two genuinely distinct sites still report twice), and no guard at all for an erased one (parametrized over `@Unit` and `Future`, the corner that erases identically but is not spelled `Unit`) — plus runtime traps proving BOTH the `@Byte` and the `@Nat` range conjunctions reach the emitted check, each pinned by trap `kind` as well as by the conjunct its message names. The mutation perturbs the predicate as well as the binder name: the two travel by different routes, and the range conjunction lives on the predicate. The once-per-site dedup is keyed on a resolved location, so a cross-module pin holds up the premise that a location carries its owning file: two imported library modules of identical shape, declaring their nested refinement at the same line and column, must produce two diagnostics attributed to two files — each quoting its own module's declaration, which is also what catches an attribution pointing past the importer's last line | | `test_checker_effects.py` | 90 | 1,455 | Effect declarations, abilities, effect subtyping, async effect, handler typing (#420 split), and the #1149 built-in-effect redeclaration gate (E152: divergent and faithful `effect IO`, codegen-only `Exn`, a registry-parametrised sweep, and a differential pinning the gate's name set to what `vera effects --json` publishes) | | `test_state_exn_registration.py` | 30 | 1296 | #1210 — State/Exn host-import registration covers the whole handler, not just its body. Four shapes, one per sub-expression position the walk used to miss (a nested handler in a clause body, in the state-init expression, in a clause's `with` update, and an `Exn` handler in a clause body), each check-green and verify-clean and therefore required to COMPILE — pre-fix every one died at whole-module WAT compilation with `unknown func` / `unknown tag`; plus the `i32_pair` cell (`handle[State]` in a `pure` function) that the walk skipped in silence, now the same loud E607 the declared-effect gate emits. The **registration-completeness differential** is the cross-component invariant itself: over every `examples/` + `tests/conformance/` program that compiles, every `state_*` / `exn_*` symbol the emitted WAT REFERENCES must have a matching import or tag DECLARATION — a desync between the registration pass and the lowering pass is invisible to a unit test on either. Round two adds the Exn twin of the silent skip (`handle[Exn]` in a `pure` function — the walk called the shared tag registration and discarded the verdict, so it compiled where the declared-row spelling was a clean E612) and the four CONTRACT positions, which are lowered code: a handler in a `requires`, an `ensures`, an `assert`, or a `decreases` measure. The differential gained a **validation leg** — every HANDLER-BEARING module is handed to `wasmtime.Module` through the exceptions-enabled engine `execute()` uses, because a symbol declared at the WRONG TYPE passes the name comparison while being invalid WASM, and 10 of the 30 handler-bearing modules fail to load with `wasm_exceptions` off — a supported wasmtime configuration, though the current runner defaults it on. The conformance suite's deliberate negatives are filtered out of the sweep: they never reach codegen through `vera check`. Carries floors on programs swept, modules validated, summed symbol references and globally distinct symbols, plus three can-go-red tests: the State and Exn extractions each stripped of their declaration lines, and a planted retyped import that only the validation leg catches. Round five adds the three positions no corpus program contained — a destructuring `let`'s value (which also disarmed the E612 gate), a module call's ARGUMENTS, and a signature refinement predicate reached through the alias table — and a cross-module shape test for the module-call leg. Round seven adds the boundary-guard routes that enumeration missed (a tuple parameter's components, a tuple return's, and a closure's refined formal and return) and the co-extensiveness half those shapes cannot show: a refined tuple behind a CLOSURE formal must declare nothing, since the closure path emits no component guards, and the nested-refinement (E618) and erased-base bails must stay silent registrars too. Plus the cycle guard on the closure signature leg, asserted in both directions — the walk terminates, and with the guard neutered the same walk blows the recursion limit | | `test_closure_lift_boundaries_1234_1235_1245.py` | 18 | 757 | Closure lifting at refinement boundaries — three burndown defects of one seam. **#1245**: `_lift_pending_closures` ran BEFORE `_compile_postconditions`, so a closure created while lowering a refined-RETURN guard, a tuple return's component guards, or an `ensures(...)` predicate was registered and never lifted — the table stayed empty, its `call_indirect` was orphaned, and the #1185 propagation dropped the function and every caller: a check-green, verify-clean program compiling to ZERO exports. The param-position twin (lowered before the lift, so it always worked) is carried as the control that makes it an ORDERING defect, the `ensures`-clause twin shows the same bug with no refinement in sight, and a violating return asserts the lifted guard ENFORCES rather than merely existing. **#1234**: the lift worklist fed itself — a refinement whose predicate holds a closure refined by a type whose chain leads back to it (`type SelfRef = { @Int \| ... fn(@SelfRef -> @Int) ... }`, and equally a mutual `A -> B -> A` or a three-type cycle) had each lift's own boundary guard queue an `AnonFn` for ever, and `vera compile` never returned. All three cycle lengths are asserted on a daemon thread with a wall-clock budget, so a regression fails fast instead of hanging the suite, and each on the [E602] naming the closure it refused (a guard that never fired cannot produce it). Two controls carry the other half — the guard is keyed on the lift CHAIN, not on everything already lifted, so `fn f(@R, @R -> @Int)` and a diamond, which each legitimately lift one predicate's closure twice, must still run; mutation-measured, they are the only two tests a seen-set spelling reddens. **#1235**: a `Tuple` formal crossing into a closure was unguarded where the named path traps — both spellings of the same boundary are run against each other, violating and passing | | `test_byte_literal_joins_1212.py` | 25 | 759 | #1212 — a `@Byte` literal inside a value-position join lowers at the i32 Byte width. `@Byte` is i32 (spec §11) while an int literal defaults to `i64.const`, and the #865 / #1092 coercions each tested for a TOP-LEVEL `IntLit` — which the checker's bidirectional coercion is equally happy to type inside an `if` or `match` branch. Ten write boundaries are parametrized with the literal in a branch (`let`, handler state-init, clause-dispatched `put`, bare `put`, get-clause `resume` — verbatim the form the E602 clause-lowerability skip message recommends — a clause `with` update, a `@Byte` call argument, a generic constructor field at `Box`, a lifted closure's own RETURN whose named twin had been coerced since #865 while the closure path had no such step, and a HETEROGENEOUS join at a `@Byte` return, where the arm the result-type decider reads is already i32 and a sibling is a bare literal — arm ORDER decided which way that one failed, so both orders and both paths are pinned). The module docstring states what that list is and is not: measured coverage, since the checker's single Byte coercion makes the true enumeration "every position propagating a Byte expectation", which nothing enumerates in one place, each a check-green program that failed WASM validation with `type mismatch: expected i32, found i64` before the fix. Every case carries a VALUE oracle (200, distinguishable from every other constant in its fixture) rather than merely asserting the module runs, and a separate test drives the OTHER branch so a fix that marked only the arm the result type is read off would still fail. The controls are the load-bearing half: a plain `@Int` join must stay i64 — pinned on 5,000,000,000, which an i32 store cannot represent, so a spreading mark is a wrong VALUE and not just a validation failure — a Byte join with no literal arm must be untouched, and a Byte-RETURNING literal join must keep its own #865 return coercion. The constructor-field case runs through the real pipeline (checker artifacts threaded), because the #1092 width keys on the checker-recorded target type | -| `test_closure_boundary_widths_1255_1256_1269.py` | 52 | 949 | Widths and pointer-ness at a closure or effect boundary — three burndown defects of one seam, each a boundary answering "what is this declared type" from something other than that type. **#1255**: GC pointer-ness was read off the SYNTACTIC head, so `type SmallByte = { @Byte \| ... }` was rooted on the shadow stack at the closure parameter, return and capture and at the two named-function twins. The oracle is a DIFFERENTIAL against the `@Byte` spelling of the same program rather than an absolute push count — these bodies must allocate or no prologue is emitted at all, so they legitimately root their own intermediates — with the base spelling's own count PINNED beside it, because equality alone also holds when both spellings root the scalar, which is the pre-fix state and what a mutation deleting the exclusion outright would produce. A genuine pointer at each of the four boundaries is the control (rooting nothing anywhere satisfies the differential too), and every shape runs under `VERA_EAGER_GC=1` — a collection at each `$alloc`, where removing a load-bearing push reads back as a wrong value rather than as a passing test. The heap-layout invariant the defect was inert behind is executable here: a module with no string pool at all — the exposure the issue named — still starts its heap above the inline scalar range, and shrinking the two constants that create that margin fires the build guard. **#1256**: the `apply_fn` `call_indirect` signature took each parameter's width from the ARGUMENT, so a `@Byte` formal fed a literal registered two incompatible `$closure_sig` types and trapped; asserted by run AND by the emitted signature list, since a value oracle alone would also pass if both sides converged on the wrong shared width. The join spelling, the refined formal, the function-type-alias arm of the formal recovery, a directly-called named twin and an i64 control (pinned above 2^32, which an i32 parameter cannot carry) surround it. **#1269**: `throw`'s payload was not a `@Byte` write boundary, so `throw(5)` into `Exn<{ @Byte \| @Byte.0 < 10 }>` put an `i64.const` under an i32 tag and failed WASM validation at load. Both halves of the width agreement are pinned — a fix that widened the TAG would also run, and would put a Byte cell at eight bytes everywhere else — across the bare, aliased, refined, branch-literal, qualified-`Exn.throw` and thrown-inside-the-handled-body spellings, the last two reaching registration paths the others do not | +| `test_closure_boundary_widths_1255_1256_1269.py` | 52 | 969 | Widths and pointer-ness at a closure or effect boundary — three burndown defects of one seam, each a boundary answering "what is this declared type" from something other than that type. **#1255**: GC pointer-ness was read off the SYNTACTIC head, so `type SmallByte = { @Byte \| ... }` was rooted on the shadow stack at the closure parameter, return and capture and at the two named-function twins. The oracle is a DIFFERENTIAL against the `@Byte` spelling of the same program rather than an absolute push count — these bodies must allocate or no prologue is emitted at all, so they legitimately root their own intermediates — with the base spelling's own count PINNED beside it, because equality alone also holds when both spellings root the scalar, which is the pre-fix state and what a mutation deleting the exclusion outright would produce. A genuine pointer at each of the four boundaries is the control (rooting nothing anywhere satisfies the differential too), and every shape runs under `VERA_EAGER_GC=1` — a collection at each `$alloc`, where removing a load-bearing push reads back as a wrong value rather than as a passing test. The heap-layout invariant the defect was inert behind is executable here: a module with no string pool at all — the exposure the issue named — still starts its heap above the inline scalar range, and shrinking the two constants that create that margin fires the build guard. **#1256**: the `apply_fn` `call_indirect` signature took each parameter's width from the ARGUMENT, so a `@Byte` formal fed a literal registered two incompatible `$closure_sig` types and trapped; asserted by run AND by the emitted signature list, since a value oracle alone would also pass if both sides converged on the wrong shared width. The join spelling, the refined formal, the function-type-alias arm of the formal recovery, a directly-called named twin and an i64 control (pinned above 2^32, which an i32 parameter cannot carry) surround it. **#1269**: `throw`'s payload was not a `@Byte` write boundary, so `throw(5)` into `Exn<{ @Byte \| @Byte.0 < 10 }>` put an `i64.const` under an i32 tag and failed WASM validation at load. Both halves of the width agreement are pinned — a fix that widened the TAG would also run, and would put a Byte cell at eight bytes everywhere else — across the bare, aliased, refined, branch-literal, qualified-`Exn.throw` and thrown-inside-the-handled-body spellings, the last two reaching registration paths the others do not | | `test_nested_handler_clause_ops.py` | 27 | 971 | #1211 — a handler clause body's bare `get`/`put` belongs to the handler's DECLARATION scope, not to the body it refines. Eight nested shapes, each asserted on all three components (checker accepts, verifier discharges clean, compiled program returns the checker-derived value): `put` in a put clause and in a get clause, a bare `get` in a `with` state-update expression, depth-3 nesting proving the IMMEDIATELY enclosing handler wins, the qualified `State.put` spelling, a nested handle expression inside a clause body (its registries must be restored to the declaration's, not the intervening handler's), and the two op-result-type mirrors — a bare `get(())` in match-scrutinee (`_effect_op_result_wt`) and array-element (`_effect_op_result_vera`) position, both of which emitted invalid WASM for a check-green program before the alignment. Every oracle is derived from the checker's story, never from what codegen emits, and a meta-test asserts each shape still SEPARATES enclosing-cell from inner-cell routing (the pre-fix value is recorded per case) so none can go vacuous. Round two adds the two dispositions of an EMPTY enclosing handler stack — the declared effect row (the only route that reads the restored `_effect_ops`, which every handler-enclosed case bypasses) and the outermost handler in a `pure` function (E122 at check) — the enclosing handler's own clause running on the outward-routed op (a transforming `with` one level out: 300100, where the intrinsic reading gives 300050), `IO.print` inside a clause body, the #1233 same-family refusals (nested handler, `with` expression, declared row) with their different-family control, and the outward-re-entry depth cap (below it, at it with a WAT-size bound, and past it as a loud E602) | | `test_handler_op_ownership_1284.py` | 15 | 419 | #1284 — whose declaration a bare `get`/`put` call site denotes. The checker resolves user-fn-first (pinned directly: an over-applied `get` under a `handle[State]` reports the USER signature's arity), and codegen used to answer that question twice more and differently — the declared-effect row withheld the op when a function owned the name, the handler expression overwrote unconditionally. Four shapes from check-green source, each asserted on the CHECKER's value and on the dispatch target in the emitted WAT: a handled body returning the cell instead of the function's answer (silently 5 for 4), a `@Bool`-returning user `get` whose module WASM validation rejected, same-family nesting refused outright with a spurious `[E602]` naming a State operation the source never contained, and different-family nesting emitting the enclosing cell's getter at the wrong width. A parametrized differential runs all five shapes (the four plus a user `put`) as one table with each case's pre-fix behaviour recorded, so a case that stops distinguishing the two answers is visible rather than vacuous; the controls — an unshadowed handler and an unshadowed declared row, both of which must still reach the intrinsics — are what a fix that simply stopped installing the ops would fail, and `new(State)` under a shadowed op name pins that the #1285 family registry composes with this | | `test_new_state_family_1285.py` | 9 | 323 | #1285 — which cell `new(State)` reads under a multi-`State` effect row. `old()` has been family-keyed since #1205/#1209 while `new()` read the name-keyed op registry, so the two sides of one `ensures` clause read different cells: `effects(, State>)` with `ensures(new(State) == …)` was check-green and verify-green, put `state_get_Int`'s i64 into the Bool comparison's `i32.eq`, and died at load. Three multi-row cases — the width-mismatched shape that could not load, an `Int`/`Nat` pair that loaded and answered about the wrong cell, and `old()` beside `new()` of one family, whose unchanged-cell claim the runtime refuted on a contract the verifier had discharged — plus the single-`State` and alias-spelled controls the whole existing corpus exercises. Each cell is seeded from a caller's handler at a value the other cell is not holding, so a wrong-cell read cannot coincide with the right answer, and a deliberately false postcondition asserts the Tier 3 runtime check really traps, without which every "the program runs" assertion here would prove nothing | @@ -104,7 +104,7 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_checker_errors.py` | 73 | 1,196 | Error codes, resolution-coverage diagnostics, contracts, error accumulation (#420 split); cyclic type aliases incl. #1059 self-reference through a type argument (`Future`, mutual `Future`/`Future`, `Array`) rejected E132 | | `test_checker_builtins_collections.py` | 97 | 848 | Map / Set / Decimal / Json / Html / Http / Inference built-in type-checking (#420 split) | | `test_checker_builtins_strings.py` | 122 | 945 | String / numeric / type-conversion / float-predicate / string-search / markdown / regex built-in type-checking, removed-legacy-name regression (#420 split) | -| `test_obligations.py` | 766 | 1,741 | Reified proof obligations + warm `VerificationSession` (#222 Phase A): full-corpus differential oracle (warm session == cold `verify()` on diagnostics, summary, and obligation stream, plus warm-twice determinism, across all 42 examples and every verify/run-level conformance program), summary↔obligation tier-bookkeeping consistency (including the #967 `total == tier1_verified + tier3_runtime` leg, plus a focused self-consistency pin on the three call-demotion examples), the #1242 stream partition — over a corpus widened to every conformance program that type-checks, at any level, `len(obligations) == total + violated + tier3_unguarded` and every status is one of the documented five, with the vocabulary read from the `ObligationStatus` Literal so a sixth member fails rather than vanishing from the counts — per-kind unit tests (requires / ensures / decreases / nat_sub / call_pre statuses, counterexamples, error codes), content-key stability + same-text-two-sites span disambiguation, session solver reuse, type-error short-circuit, ADT-registry resync between programs; plus the Phase B incremental suite — identical-source full replay, callee-body-edit replays callers while callee-contract-edit invalidates them, span-shift and ADT-edit conservative invalidation, cross-program isolation, timeout-status never cached (monkeypatched solver), FIFO eviction bound; plus the #727 dedup pin — a violating call in a let RHS records exactly one E501 diagnostic and one call_pre obligation; plus the #1208 call-site rendering pin — a PARAMETERISED callee slot substitutes into the E501 message and its fix instead of falling back to the generic wording | +| `test_obligations.py` | 769 | 1,741 | Reified proof obligations + warm `VerificationSession` (#222 Phase A): full-corpus differential oracle (warm session == cold `verify()` on diagnostics, summary, and obligation stream, plus warm-twice determinism, across all 42 examples and every verify/run-level conformance program), summary↔obligation tier-bookkeeping consistency (including the #967 `total == tier1_verified + tier3_runtime` leg, plus a focused self-consistency pin on the three call-demotion examples), the #1242 stream partition — over a corpus widened to every conformance program that type-checks, at any level, `len(obligations) == total + violated + tier3_unguarded` and every status is one of the documented five, with the vocabulary read from the `ObligationStatus` Literal so a sixth member fails rather than vanishing from the counts — per-kind unit tests (requires / ensures / decreases / nat_sub / call_pre statuses, counterexamples, error codes), content-key stability + same-text-two-sites span disambiguation, session solver reuse, type-error short-circuit, ADT-registry resync between programs; plus the Phase B incremental suite — identical-source full replay, callee-body-edit replays callers while callee-contract-edit invalidates them, span-shift and ADT-edit conservative invalidation, cross-program isolation, timeout-status never cached (monkeypatched solver), FIFO eviction bound; plus the #727 dedup pin — a violating call in a let RHS records exactly one E501 diagnostic and one call_pre obligation; plus the #1208 call-site rendering pin — a PARAMETERISED callee slot substitutes into the E501 message and its fix instead of falling back to the generic wording | | `test_verifier_contracts.py` | 95 | 898 | Z3 verification over the example corpus, trivial/ensures/if-else/let/multi-clause contracts, counterexamples, tier classification, arithmetic, verification summaries, Diverge effect, edge cases, string-length + string-predicate verification (#839 split) | | `test_verifier_nat_obligations.py` | 82 | 1,743 | **`@Nat` subtraction underflow obligation** (#520 — Path-A discharge via requires/path-conditions/path-aware Z3 refutation, pure-literal exclusion, Int-Int and Nat-Int exemptions) and **`@Nat` binding-site narrowing obligation** (#552/#747/#749 — Tier-1 `value >= 0` at let/call-arg/effect-op-arg/ctor-field/match-bind/destructure narrowing — a concrete site classifies `tier3_runtime` (codegen-guarded) while the effect-op argument and generic-instantiated constructor field classify `E504` (obligated but unguarded, #754/#757) whose rationale names its actual cause — an untranslatable value — rather than the untranslatable-or-timeout conflation #1251 removed, walker-recursion pins, `_narrows_into_nat` verifier/codegen soundness parity; PR #972 clone-instantiated side-table substitution — a `Some(@T)` bind in an `Option`-instantiated clone is no narrowing, genuine clone-path narrowings still obligated); #1201 — a builtin `Tuple` parameter's match-bound components carry their declared component facts (a valid ensures over one proves instead of falsely violating) and an `Int` component bound as `@Nat` fires one loud `E503` per component, both mutation-caught (#839 split) | | `test_verifier_primitive_ops.py` | 39 | 662 | **Primitive-operation safety obligations** (#680) — division/modulo by-zero `E526` and array-index-bounds `E527`, the in-bounds/out-of-bounds two-check with float-exemption, honest Tier-3 for opaque lengths, off-by-one and lower-bound pins, De Bruijn-correct fix hints (#839 split) | @@ -122,7 +122,7 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_soundness_392.py` | 36 | 584 | #392 audit batches 1–2 — verifier soundness/completeness fixes: signed div/mod truncate toward zero (#799), body `assert(P)` carries a Tier-1 obligation (#800), divisions in contract predicates carry a `div_zero` obligation (#801), and the #804 assume-half of #800's `assert` rule — a prior `assert`/`assume` discharges later obligations (including a later call's precondition) + the postcondition at Tier 1, removing false E501/E503/E500/E505 | | `test_int_overflow.py` | 6 | 143 | #798 — `@Int`/`@Nat` arithmetic-overflow obligations (part of the #392 `smt.py` soundness audit): `+`/`-`/`*` on `@Int`/`@Nat` now emit an `int_overflow` obligation (the analog of `nat_sub`/`div_zero`) rather than modelling the operands as Z3's unbounded integers, so a `ensures(@Int.result > @Int.0)` over `@Int.0 + 1` no longer proves a contract the i64/u64 runtime violates under two's-complement wraparound. Unbounded operands leave the obligation undischarged (Tier-3, runtime-guarded); operand bounds that prove the result stays in range discharge it at Tier 1 | | `test_int_overflow_codegen.py` | 62 | 718 | #798 Stage 3 — runtime overflow-trap codegen: the codegen emits a guard at *exactly* the `@Int`/`@Nat` `+`/`-`/`*` sites the verifier obligates, so `vera run`/`vera compile` programs trap on overflow instead of silently wrapping at the i64/u64 boundary. #808 wired the guard to the `vera.overflow_trap` host import, so the trap now classifies `kind="overflow"` (carrying the overflow Fix paragraph) rather than the generic `unreachable`; `TestOverflowTrapKind808` pins that, with controls proving the #520 `nat_sub` underflow and #813 `@Nat`→`@Int` widen guards still classify `unreachable` | -| `test_int_overflow_differential.py` | 256 | 398 | #798 Stage 3 verifier↔codegen classification differential (cross-component soundness rule): the codegen overflow guard must fire at exactly the sites the verifier obligates *and* classify each site's operand type (`@Int` i64 vs `@Nat` u64) identically — else a Tier-1-clean program traps spuriously or a wrapping op slips through unguarded. Over a corpus exercising all five operand combos plus the literal-left ambiguity (a naive codegen mis-classifies it as `@Nat`), asserts the verifier's per-site gated classification equals the codegen's site for site, both sides driven by the same `ast.span_key` | +| `test_int_overflow_differential.py` | 257 | 398 | #798 Stage 3 verifier↔codegen classification differential (cross-component soundness rule): the codegen overflow guard must fire at exactly the sites the verifier obligates *and* classify each site's operand type (`@Int` i64 vs `@Nat` u64) identically — else a Tier-1-clean program traps spuriously or a wrapping op slips through unguarded. Over a corpus exercising all five operand combos plus the literal-left ambiguity (a naive codegen mis-classifies it as `@Nat`), asserts the verifier's per-site gated classification equals the codegen's site for site, both sides driven by the same `ast.span_key` | | `test_nat_int_widening.py` | 36 | 602 | #813 — `@Nat -> @Int` widening coercion obligation (dual of #552 `nat_bind`, part of the #392 soundness audit): a `@Nat` in (i64.MAX, u64.MAX] reinterprets when widened (u64.MAX → -1), so a `nat_to_int_coerce` obligation that the value is `<= i64.MAX` now fires at the return position — provably-in-range → Tier-1, provably-out-of-range (`@Nat.0 >= 2**63`) → loud E530, unbounded → honest Tier-3 (runtime-guarded), with an `@Int -> @Int` control that must not fire; the unguarded generic-`@Int`-field case also has its `E531` rationale read for WHAT IT SAYS — a value bounded on neither side, not the untranslatable-or-timeout conflation #1251 removed. The #813 follow-up adds the explicit `nat_to_int` built-in and heterogeneous `if`/`match` arms with a non-negative-literal alternative; #820 adds the heterogeneous-`@Int`-slot arm, closure argument, and closure return/capture obligations (each per-arm / per-site, with `@Int`-arm and `@Nat`-formal controls that must not fire) | | `test_int_widening_codegen.py` | 52 | 535 | #813 Stage 3 — runtime `@Nat -> @Int` widening-trap codegen: the codegen emits a guard at *exactly* the `@Nat -> @Int` coercion sites the verifier obligates (return, `let`, call argument, and — since #820 — array element, tuple construction/destructure, heterogeneous `if`/`match` arm, closure argument/return), so `vera run`/`vera compile` programs trap when a `@Nat` above i64.MAX would reinterpret to a negative `@Int` instead of silently returning the wrong value. The trap is a bare `unreachable` (shares `_emit_negative_i64_guard` with the #552 nat-bind guard), classified `kind="unreachable"` today (a dedicated widening trap kind is a follow-up) | | `test_int_widening_differential.py` | 26 | 320 | #813 verifier↔codegen behavioural differential (cross-component soundness rule): at every `@Nat -> @Int` coercion site the verifier's `nat_to_int_coerce` classification must AGREE with the runtime — a `tier3` (codegen-guarded) site MUST trap on a `@Nat` above i64.MAX (return / `let` / call-arg / constructor field / ADT sub-pattern / match-bind, and the #820 array-element / tuple-component / heterogeneous-arm / closure argument-return sites), while a `tier3_unguarded` (E531) site must NOT trap (the generic-instantiated `@Int`-field coercion codegen cannot guard). Runs BOTH sides on one corpus so the "runtime-guarded" claim is checked against the actual trap — catching a verifier deferral codegen never guards (unsound silent -1) or a spurious trap | @@ -191,7 +191,7 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_string_length_soundness.py` | 15 | 278 | #802 — string_length code-point vs UTF-8 byte soundness: a non-literal `string_length` defers to Tier 3 (the issue's `"é"` probe no longer proves `== 1` at Tier 1), a string-literal length is modeled at its exact UTF-8 byte count (`== 2` for `"é"`), and the boolean predicates `string_contains` / `string_starts_with` / `string_ends_with` stay Tier 1 (sound under UTF-8 self-synchronization), while a predicate over an astral (> U+2FFFF) or lone-surrogate literal defers to Tier 3 (z3.StringVal cannot model those code points) | | `test_errors.py` | 62 | 657 | Error code registry, diagnostic formatting, serialisation, SourceLocation, and error display sync — the canonical `E001` diagnostic must match each of its mirrors: `README.md`, `docs/index.html`, `spec/00-introduction.md`, `AGENTS.md`'s example `--json` block, and the hardcoded example in `scripts/build_site.py` that generates `docs/index.md` (#829; `AGENTS.md`'s ellipsis-truncated description/rationale are prefix-compared, its `error_code`/`spec_ref`/`fix` exactly) | | `test_eq_contract_874.py` | 13 | 430 | `eq`/`compare` ability ops in contract position: codegen canonicalization + verifier Tier-1 discharge/counterexample, where-fn contracts, compare Ordering-sort materialization, shadowing guard (#874) | -| `test_formatter.py` | 536 | 3,638 | Comment extraction, interior comment positioning, expression/declaration formatting, match arm block bodies, §1.8 rule 2 in value position (a `let`-bound `match`/`if` expands exactly as one in statement position, and a comment above an arm inside a statement's value stays on that arm), blank-line preservation (§1.8 rule 13 — gaps between statements, before a block result and around a comment, collapsed to one and never invented), idempotency, parenthesization, spec rules, ability declarations | +| `test_formatter.py` | 537 | 3,638 | Comment extraction, interior comment positioning, expression/declaration formatting, match arm block bodies, §1.8 rule 2 in value position (a `let`-bound `match`/`if` expands exactly as one in statement position, and a comment above an arm inside a statement's value stays on that arm), blank-line preservation (§1.8 rule 13 — gaps between statements, before a block result and around a comment, collapsed to one and never invented), idempotency, parenthesization, spec rules, ability declarations | | `test_cli.py` | 273 | 4,612 | CLI commands (check, verify, compile, run, serve, test, fmt, version, quiet), subprocess integration, JSON error paths (including the `verify --json` `obligations` array and its summary-reproducibility pin, #967, and the #1242 partition pin — the array is emitted unfiltered, a refuted obligation is counted by no summary field, and it still joins its E500 on the location key), runtime traps, arg validation, multi-file resolution, IO exit codes, --explain-slots (including the #1208 naming pins — an alias in type-argument position is tabled resolved, and a `forall` variable shadowing a module alias keeps the two parameter stacks apart — and the #1217 `where`-helper tables: the helper prints indented under its parent, appears in the JSON qualified as `parent.helper`, and inherits the enclosing `forall` variables so the shadowing holds inside it too), `builtins`/`effects`/`errors` introspection dispatch, and a USAGE-completeness guard (every dispatched `cmd_` handler has a help row) | | `test_introspect.py` | 39 | 221 | `vera builtins/effects/errors --json` registry introspection (#539): the `{schema, items}` envelope, count-equals-registry differential per registry, error-phase derivation, effect/ability `kind` tagging, the parameterised `Exn` effect, and best-effort `since` attribution with full-coverage guards | | `test_resolver.py` | 20 | 602 | Module resolution, path lookup, parse caching, circular import detection, the E011/E012/E013 diagnostic contract, internal-error isolation (a compiler bug is not masked as E013), and the transitive-closure return of `resolve_imports` (#890 — a diamond yields each reachable module once, direct imports tagged `direct`, the transitive one not) | @@ -209,7 +209,7 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_markdown.py` | 94 | 610 | Markdown parser: block/inline parsing, rendering, round-trips, edge cases | | `test_lsp.py` | 146 | 2470 | LSP transport + coordinate layer (#222 Phase C) and language features (#222 Phase D): parametrized code-point↔UTF-16 goldens incl. astral-plane fixtures and surrogate-pair snapping, Span (1-based, exclusive-end) and SourceLocation (0-based col) → LSP Range conversions, point→token-range widening, DocumentStore open/change/close + index invalidation, an in-process handler-drive test, and one stdio end-to-end round-trip against the real `vera lsp` subprocess (initialize → didOpen → shutdown → exit) pinning serverInfo + textDocumentSync capabilities; plus the Phase D feature suite — parse-error single-diagnostic path, type-error verification short-circuit, tier=3 in E520 diagnostic data, per-function tier Hint synthesis (and its suppression for functions with violated obligations), smallest-enclosing-span hover, De Bruijn slot goto (most-recent-parameter jump, out-of-range None, off-slot None, and the #1208 keying pins: a parameterised reference resolves, an alias-spelled parameter is reachable from a canonically-spelled reference, and a `forall` variable shadowing a module alias lands on the right parameter), and typed-hole completion (inside/after hole, away-from-hole None); plus the Phase E speculativeEdit suite — identical-text all-unchanged, breaking edit surfaces newly_undischarged (violated nat_sub) with canonical state untouched, strengthening edit surfaces newly_discharged, parse/type errors report ok:false, deleted functions report removed, proof_delta purity; plus the Phase F1 proposeEdit suite — the apply gate (clean and strengthening edits apply, breaking and non-compiling edits refuse), force overriding both gates with the delta still reported, wiring against a structural fake server (apply round-trip with exact full-document replacement range, refuse touches no canonical state, unopened-URI clamp sentinel), and full-document-range goldens (trailing-newline virtual line, UTF-16 end column); plus the Phase F2 strengthenContract suite — splice goldens (first-clause-only replacement with byte-identical remainder, ensures variant, unknown-fn None), the call-site audit pin (tightened precondition refused with newly_undischarged call_pre items, canonical state untouched), provable-ensures strengthening applies, and the three splice-target refusal paths (no analysis, unparseable document, unknown function); plus the Phase F3 addEffect suite — transitive-caller closure goldens (diamond in declaration order, leaf, unknown-fn None, recursion appears once), handler bounding (#725: a caller discharging the effect around its only call site drops out of the closure and is not rewritten, while a second unhandled path, a call in a handler clause, a handler for another effect, a handler naming a different instance of the same effect (`handle[State]` against a `State` propagation, which the checker does not discharge — end-to-end that caller must still be rewritten and the candidate must still apply, with the matching `State` propagation against the same fixture as the positive control that this handler key does prune something), and a bare `where`-helper call all keep it in, as does a refinement type argument at either depth (`Exn<{ @Int \| p }>` and `Exn>` render as their bare base type but discharge nothing of `Exn` — also pinned end-to-end), while an unparameterised `handle[IO]` does bound an `IO` propagation, a whitespace-spelled `State< Int >` request still bounds, and nesting bounds in either order (a matching handler inside a foreign one, a foreign one inside a matching one) — plus a key-level pin that `handle[Mod.IO]` keeps its module, and the effect-less query pinned handler-unaware; plus the two boundary pins the review added — a call in the handler's STATE INITIALISER keeps its edge, since the initialiser is evaluated in the enclosing scope before the handler is installed (pinned beside the E125 the checker raises there against a `pure` caller, with the identical call in the handler body clean as the contrast), and an alias-spelled handler does not bound a `State` propagation though the checker discharges it (`handle[State]` with `type MyAlias = Int` — the spelling comparison's under-prune, #1292, with the alias-spelled request as the control that does prune)), effect-row rewrite goldens (pure to singleton set, source-preserving append, already-present None, base-name identity blocking State next to State), diamond propagation applying one multi-site candidate with the bystander untouched, mixed append/replace rows with already-satisfied callers skipped, the fully-satisfied no-op shape, and the two refusal paths; plus the #728 instruction-contract suite — the LSP message carries description, rationale, and the Fix: paragraph (also pinning single E501 emission at the LSP surface), and a bare diagnostic maps to the description alone | | `test_browser.py` | 413 | 5,094 | Browser parity: Python/wasmtime vs Node.js/JS-runtime output equivalence across IO, State, contracts, Markdown, Regex, and the examples the browser target can execute (two explicit lists in the file, not the whole `examples/` directory — interactive stdin, file IO, `DB` and the non-standalone `modules` example are excluded with their reasons recorded); plus the #349 `runtime.mjs` coverage battery — per-value-type and per-key-type Map variants, per-element-type Set variants, cold `Decimal` branches (exact-zero sign, negative-shift division, `decRoundPlaces` special cases, non-finite storage), `readJson`/`json_stringify` across every Json ADT tag, the Regex/Json `Result.Err` arms, and nested-Markdown walks. Two operations carry a canonical form the specification states rather than merely agreeing across the hosts, so their batteries assert more than equality: `json_stringify` (spec §9.7.1) pins the expected string on every Json ADT tag and on the number-rendering boundaries, checks three-pass idempotence, checks that a non-finite `JNumber` fails on both hosts *and* prints nothing, pins the object key orders an ordinary JS object cannot carry (array-index keys, which ES enumeration hoists to the front in ascending numeric order, and a `__proto__` key, whose assignment writes a prototype instead of a field) on both a parsed and a program-built object, and checks the reference host's own number rendering differentially against a real `JSON.stringify` over doubles drawn from raw bit patterns; `md_render` (§9.7.3) pins the expected render, re-renders it to prove the fixed point, runs the round-trip property over a corpus carrying the container and multi-line shapes a flat corpus misses, and renders `MdBlock` values the test builds directly, since several renderer rules — a container's child separator, an empty container, a code span wider than one backtick — are unreachable through `md_parse`. plus the #1306/#1308 accept-domain battery (`TestBrowserJsonAcceptDomainParity1306_1308`), which runs one `.wasm` under both runtimes and compares the WHOLE stdout — arm taken and `Err` message together — over the JavaScript constants, over numbers that overflow to an infinity in both their exponent and integer spellings, and over lone surrogates at every position a string can occupy, with the expected sentences imported from `vera/wasm/json_serde.py` so `runtime.mjs`'s hand-copied duplicates are held against the originals, beside acceptance controls (matched pairs, finite boundary values, underflow to `0`, `"NaN"` as a string value), a ten-case host-native-message battery pinning that neither the substitute-and-re-parse probe nor the value-start constraint hijacks an unrelated syntax error (`-NaN` is the case needing both rules — the substitution alone turns it into `-0` and manufactures a refusal the reference host never makes), a precedence case fixing that a non-finite constant outranks a lone surrogate on both hosts though each reaches that answer by a different route, and a document-order case fixing that overflow and lone surrogate are resolved by one walk rather than by two per-host precedence rules that would agree on every single-violation document; `md_parse` itself is not yet at parity and its remaining divergence classes are a tracked bug ([#1301](https://github.com/aallan/vera/issues/1301)); the suite pins the shapes the two implementations agree on | -| `test_conformance.py` | 1205 | 144 | Parametrized conformance suite: parse, check, verify, run, format idempotency across 241 programs; a negative entry fails at the stage `expected_error_stage` names (`check`, the default, or `compile` — which also asserts the program type-checks cleanly first) | +| `test_conformance.py` | 1210 | 144 | Parametrized conformance suite: parse, check, verify, run, format idempotency across 242 programs; a negative entry fails at the stage `expected_error_stage` names (`check`, the default, or `compile` — which also asserts the program type-checks cleanly first) | | `test_prelude.py` | 29 | 585 | Prelude injection: Option/Result/array operation detection, combinator shadowing, type aliases, the reserved namespace every injected alias declaration lives in — checked against the checker's own E154 regex rather than a second spelling of the rule, since an alias the prelude injects outside it is one codegen resolves and the checker leaves opaque (#1184/#1221) — end-to-end compilation | | `test_checker_apply_fn.py` | 18 | 455 | #854 — `apply_fn` as a checker special form: zero-warning pins (API + CLI `--json` + closures.vera), E201 arity / E202 type / non-function-first-arg errors, E122/E125 effect-row enforcement for applied fn values, E151 redefinition rejection, variadic two-param application, prelude combinator regression pins | | `test_prelude_diagnostics.py` | 8 | 271 | #851 — prelude combinator skip-warnings: unreferenced-prelude E602/E604 suppression (zero-warning minimal compile, API + CLI `--json`), `` origin attribution for referenced-but-skipped combinators (text + `to_dict`), transitive reference scan, and user-fn warning locations pinned unchanged | @@ -231,11 +231,11 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_doc_builtin_shadowing.py` | 8 | 107 | `check_doc_builtin_shadowing.py` gate ([#819](https://github.com/aallan/vera/issues/819)): reject-set membership (opaque built-ins in, overridable combinators out), top-level + `where`-block `fn ` definitions flagged, non-built-in / overridable / prose-mention ignored, and the shipped docs are currently clean | | `test_runtime_traps.py` | 98 | 3,255 | Runtime trap categorisation (#516 Stage 1), out-of-bounds host-read bounds check (#1145), stdout/stderr-on-trap preservation (#522), `IO.print` live tee (#543), and trap source backtrace (#516 Stage 2): `_classify_trap` per-`kind` mapping (`divide_by_zero`/`out_of_bounds`/`stack_exhausted`/`unreachable`/`overflow`/`contract_violation`/`unknown`), plus `host_error` from `_classify_host_error` on `execute()`'s non-`Trap` branch, `WasmTrapError` shape + `RuntimeError` substitutability, end-to-end `cmd_run` text + JSON envelopes including `trap_kind`, captured `stdout`, captured `stderr`, JSON-mode "no stderr leak" invariant, cross-stream code-order regression using merged `redirect_stdout`/`redirect_stderr`, the v0.0.123 tee suite (live streaming, write-count + order preservation, JSON-mode tee suppression, trap preservation invariant under tee, per-write flush count, default-execute silence), and the v0.0.124 source-mapping suite — `_resolve_trap_frames` unit tests covering user-fn / built-in / built-in-prefix / monomorphized base-name fallback / unknown-name / no-frames-attribute / leaf-first ordering preservation; end-to-end `cmd_run` text-mode + JSON-mode backtrace including the **leaf-first** ordering invariant; contract-violation backtrace in both text and JSON modes; direct `execute()` `WasmTrapError.frames` attachment; **suppression marker** for collapsed leading runtime-helper frames (mocked `vera.codegen.execute` with synthetic `is_builtin=True` leaf frames so the collapse logic is testable deterministically); source-map population for top-level fns + lifted closures (with span-value assertion against the closure literal's exact line range); and the no-builtin-leakage regression that pins built-in helpers (`alloc` / `gc_collect` / `contract_fail`) NOT being registered in `fn_source_map`; plus the v0.0.125 Stage 3 suite (`#547`) — text-mode `Fix:` block surfacing with position-ordering invariant (Fix appears after the source backtrace), text-mode block suppression for `contract_violation` (no empty header noise), JSON-mode `fix` field always-present (schema stability) including the empty-string case, `_TRAP_FIX_PARAGRAPHS` table-completeness assertion (every kind in the taxonomy has a Fix paragraph entry), and the column-wrap invariant (~76 chars max per line, two-space indent under the `Fix:` heading); plus the UTF-8 hardening suite **`TestHostPrintInvalidUtf8589`** (`#589` / `#592`) — after `#592` centralised the `errors="replace"` invariant into the single `vera.runtime.text.safe_utf8_decode` helper — reached only through a shared `_slice_and_decode` helper (`vera/runtime/heap.py`) that the three WASM-memory string readers (`_read_wasm_string` and `_read_string_export` there, and `vera/wasm/markdown.py::_read_string`) delegate to, with the `host_print` / `host_stderr` / `host_contract_fail` host imports and the String-return extractor in `execute()` routing through those readers rather than decoding inline: one helper unit test pinning the invariant once (invalid bytes → U+FFFD, valid + empty pass through), three wire-real end-to-end tests that drive the **production** readers (`_read_wasm_string` / markdown `_read_string` behind a synthetic-WAT `probe` host import; `_read_string_export` against a real exported memory, also covering its out-of-bounds → `None` pointer-fallback) over a region seeded with invalid UTF-8 — so a strict-decode regression surfaces as a `UnicodeDecodeError` escaping wasmtime's trampoline, and the host imports / extractor are transitively covered — and one synthetic-WAT end-to-end test that imports `vera.print` and calls it with raw invalid UTF-8 bytes to pin the wasmtime-trampoline fact independently (a Python `UnicodeDecodeError` inside a host import escapes as a "python exception" cause iff the host decode is strict); the six pre-`#592` structural source-grep assertions were retired by the centralisation; plus the Ctrl-C-during-host-import suite **`TestHostSleepKeyboardInterrupt`** ([#595](https://github.com/aallan/vera/issues/595) / [#599](https://github.com/aallan/vera/issues/599)) — after the v0.0.160 relocation to a single `except KeyboardInterrupt` handler in `execute()` (enabled by `wasmtime>=45.0.0`'s `except BaseException` trampoline fix): one structural assertion that the four per-host-import `raise _VeraExit(130)` guards are gone and the centralized handler maps to `exit_code=130`, plus four end-to-end tests that compile real Vera programs calling `IO.sleep(...)`, `IO.read_char(())`, a mocked fused `await` ([#841](https://github.com/aallan/vera/issues/841) — `Future.result()` patched to interrupt), and a live in-flight fused `await` (no mocking — `_thread.interrupt_main()` fired only once the server confirms the request arrived, handler then released so the executor teardown has a real worker to wait out; post-[#848](https://github.com/aallan/vera/issues/848) the progress print precedes the `async(...)`, so program order makes its stdout assertion deterministic), raise `KeyboardInterrupt` from inside the blocking call, and assert the program exits with `ExecuteResult.exit_code == 130` (pre-interrupt stdout preserved) instead of a raw Python traceback escaping wasmtime's trampoline; plus the host-callback surface suite **`TestHostCallbackErrorSurface1302`** / **`TestClassifyHostError1302`** ([#1302](https://github.com/aallan/vera/issues/1302)) — `execute()` classified on the exception's TYPE NAME (`Trap` / `WasmtimeError`), so a host import raising an ordinary Python exception skipped the conversion and escaped as a 63-line interpreter traceback with the captured streams dropped, and in `--json` mode with no envelope emitted at all. The conversion is now keyed on the BOUNDARY (everything escaping the guest invocation), and the suite drives a real `json_stringify(JNumber(nan()))` program through all three surfaces: `execute()` raising a `WasmTrapError` of `kind="host_error"` carrying the host's sentence, the pre-failure stdout and the original exception as `__cause__`; text-mode `cmd_run` asserted on the ABSENCE of `Traceback` / `File "` / `wasmtime` and a sub-ten-line diagnostic, since asserting only that the sentence appears would still pass on the pre-fix output where it was the traceback's last line; and JSON-mode `cmd_run` producing a parseable envelope with `trap_kind`, an always-present empty `fix`, `frames`, and the captured `stdout`. **`TestHostErrorDebugKnob1302`** covers the escape hatch the conversion needs — `VERA_DEBUG_HOST_ERRORS` (ENVIRONMENT.md) re-raises the original exception so a binding bug stays diagnosable — as a deliberate pair, one test proving the knob does something and one proving its absence is what produces the one-liner, since neither alone distinguishes a working knob from unconditional behaviour, plus the truthiness table shared with `VERA_EAGER_GC` and an end-to-end `cmd_run` case | | `test_serve.py` | 8 | 189 | #305 `vera serve` driver end-to-end: GET/POST echo round-trips (method/path/headers/body cross the host↔guest boundary via `build_request_adt` / `decode_response_adt`), handler status propagation, runtime contract violation → 500 with `trap_kind` JSON, `State` isolation across requests (instance-per-request pinned), and clean `make_server` validation errors (missing / wrong-signature `handle`), and an eager-GC round-trip pinning the Request builder's shadow-rooting; all on ephemeral ports | -| `test_wasi_target.py` | 273 | 2,142 | #237 WASI Preview 2 target (spec chapter 13): component emission validated live against the real wasmtime host — parse (`Component(engine, wat)`), instantiate (`Linker.add_wasip2()` + `WasiConfig`), and execute (stdout/stderr capture, env, argv incl. a 500-arg GC-pressure stress and a >64 KiB arena-cap trap, preopen file round-trips + errno mapping, stdin incl. UTF-8 multibyte, clocks, random bounds, exit, contract-violation text on WASI stderr, overflow); the family gate (clean diagnostic naming unsupported families, never a silent fallback); the core-emission pin (default `--target wasm` WAT untouched); `cmd_compile`/`cmd_run --target wasi-p2` CLI integration (binary component artifact, `--wat` component text, JSON envelopes, trap-kind classification through the component boundary, exit-code 0/1 degradation, `--fn` rejection); the `execute_wasi_p2` host runner (env passthrough, argv, stderr capture, String-main `wasi:cli/run` fallback); the **dual-target conformance differential** (all 172 run-level conformance programs driven under both targets, byte-identical stdout/stderr required — 120 are dual-tested and 52 skip *loudly* rather than passing silently: 45 whose compiled WAT imports a host family outside `IO`/`Random` (`state`, `map`, `json`, `set`, `decimal`, `html`, `md`, `regex`, `db`), 6 with no public zero-argument `main`, and 1 calling a nondeterministic op. The excluded set is defined by those three properties rather than by a filename list, so it stays accurate as programs are added); a stock-`wasmtime`-CLI smoke test (skips when the CLI is not installed); and the Stage-D **server world** (`world="server"`): incoming-handler emission pins (adapter lift, 32-slot dispatch table, @0.2.0 version pin, no `wasi:cli/run`), #305 handler validation + server family-gate diagnostics (rejected IO ops, non-String map instantiations, unsupported families), the cli-world pin (default emission carries no server machinery), Request/Response layout tripwires, and a stock-`wasmtime serve` smoke battery (host-vs-served differential over a method/path/header/body matrix incl. duplicate-header later-wins, in-guest map-op order parity, `IO.print` console routing, trap→500 with symbolized backtrace + violation text, graceful 500s for forbidden headers and out-of-range status, a 1 MiB GC-stress echo, and an eager-GC shadow-push mutation validation; skips when the CLI is not installed) | +| `test_wasi_target.py` | 274 | 2,142 | #237 WASI Preview 2 target (spec chapter 13): component emission validated live against the real wasmtime host — parse (`Component(engine, wat)`), instantiate (`Linker.add_wasip2()` + `WasiConfig`), and execute (stdout/stderr capture, env, argv incl. a 500-arg GC-pressure stress and a >64 KiB arena-cap trap, preopen file round-trips + errno mapping, stdin incl. UTF-8 multibyte, clocks, random bounds, exit, contract-violation text on WASI stderr, overflow); the family gate (clean diagnostic naming unsupported families, never a silent fallback); the core-emission pin (default `--target wasm` WAT untouched); `cmd_compile`/`cmd_run --target wasi-p2` CLI integration (binary component artifact, `--wat` component text, JSON envelopes, trap-kind classification through the component boundary, exit-code 0/1 degradation, `--fn` rejection); the `execute_wasi_p2` host runner (env passthrough, argv, stderr capture, String-main `wasi:cli/run` fallback); the **dual-target conformance differential** (all 173 run-level conformance programs driven under both targets, byte-identical stdout/stderr required — 121 are dual-tested and 52 skip *loudly* rather than passing silently: 45 whose compiled WAT imports a host family outside `IO`/`Random` (`state`, `map`, `json`, `set`, `decimal`, `html`, `md`, `regex`, `db`), 6 with no public zero-argument `main`, and 1 calling a nondeterministic op. The excluded set is defined by those three properties rather than by a filename list, so it stays accurate as programs are added); a stock-`wasmtime`-CLI smoke test (skips when the CLI is not installed); and the Stage-D **server world** (`world="server"`): incoming-handler emission pins (adapter lift, 32-slot dispatch table, @0.2.0 version pin, no `wasi:cli/run`), #305 handler validation + server family-gate diagnostics (rejected IO ops, non-String map instantiations, unsupported families), the cli-world pin (default emission carries no server machinery), Request/Response layout tripwires, and a stock-`wasmtime serve` smoke battery (host-vs-served differential over a method/path/header/body matrix incl. duplicate-header later-wins, in-guest map-op order parity, `IO.print` console routing, trap→500 with symbolized backtrace + violation text, graceful 500s for forbidden headers and out-of-range status, a 1 MiB GC-stress echo, and an eager-GC shadow-push mutation validation; skips when the CLI is not installed) | ## Conformance Suite -The conformance suite is a collection of 241 small, focused programs in `tests/conformance/` that systematically validate every language feature against the spec. Most programs are self-contained; the module-focused Chapter 8 cases use `import` statements where needed, and `ch07_cross_module_contracts.vera` still depends on `ch07_cross_module_contracts_lib.vera`. Each program tests one feature or a small group of related features. +The conformance suite is a collection of 242 small, focused programs in `tests/conformance/` that systematically validate every language feature against the spec. Most programs are self-contained; the module-focused Chapter 8 cases use `import` statements where needed, and `ch07_cross_module_contracts.vera` still depends on `ch07_cross_module_contracts_lib.vera`. Each program tests one feature or a small group of related features. Simon Willison [argues](https://simonwillison.net/tags/conformance-suites/) that conformance suites are a "huge unlock" for language projects — they transform development from trust-based to verification-based. The conformance suite serves as the definitive specification artifact that any implementation (or agent) can validate against. @@ -262,7 +262,7 @@ Each conformance program declares the deepest pipeline stage it must pass: | `parse` | Source text is syntactically valid | 0 | | `check` | Parses and type-checks cleanly | 49 | | `verify` | Type-checks and all contracts verified by Z3 | 20 | -| `run` | Compiles to WASM and executes correctly | 172 | +| `run` | Compiles to WASM and executes correctly | 173 | Almost all programs are at the `run` level — they compile and execute, producing correct results. Forty-nine programs (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch03_typed_holes`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_cross_module_contracts_lib`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch07_bare_effect_op_rejected`, `ch08_ambiguous_import_adt_lib_bool`, `ch08_ambiguous_import_adt_lib_int`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_lib_bool`, `ch08_ambiguous_import_lib_int`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_cross_module_generic_lib`, `ch08_module_generic_diamond_base`, `ch08_module_prelude_adt_contention_rejected`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_transitive_module_import_base`, `ch08_visibility_private`, `ch08_xmod_widen_lib`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_eq_non_derivable_rejected`, `ch09_http`, `ch09_inference`, `ch09_ord_adt_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`) are at the `check` level. Thirty-six of them — `ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch07_bare_effect_op_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, and `ch02_alias_cycle_rejected` — are **negative tests** that assert a specific diagnostic (E206, E135, E183, E201, E127, E153, E153, E153, E130, E130, E174, E182, E217, E156, E156, E155, E155, E011, E154, E154, E154, E154, E154, E154, E150, E152, E151, E242, E243, E207, E208, E208, E209, E128, E336, and E132 respectively) via the manifest's `expected_error` field. One more — `ch08_module_prelude_adt_contention_rejected` — is a negative at the `compile` stage rather than at `check`: it carries `expected_error_stage: "compile"` beside `expected_error: E621`, so the harness asserts it type-checks CLEANLY and is then refused by `vera compile` with that code, which is the property a codegen-phase diagnostic exists for. `ch09_http` and `ch09_inference` are environment-gated (network / API key). Twenty programs (`ch03_slot_let_chains`, `ch03_slot_noncommutative`, `ch04_nested_option_ctor`, `ch04_primitive_obligations`, `ch05_apply_fn_typing`, `ch06_adt_sort_disambiguation`, `ch07_cross_module_contracts`, `ch07_invisible_import_op_name_lib`, `ch07_io_read_char`, `ch07_io_sleep`, `ch07_random_effect`, `ch08_state_alias_module_table_lib`, `ch08_module_generic_diamond_mid1`, `ch08_module_generic_diamond_mid2`, `ch08_state_alias_per_module_lib`, `ch08_transitive_module_import_mid`, `ch09_http_server`, `ch09_invisible_import_ability_op_lib`, `ch09_math_builtins`, `ch09_nested_helper_family_op_name_lib`) are at the `verify` level, using Z3-provable contracts — a library module is pinned at the deepest level it reaches, so the two per-module alias-table libraries are verified rather than only checked. @@ -408,7 +408,7 @@ tests/conformance/ ├── ch01_int_literals.vera # Chapter 1: Integer literals ├── ch01_float_literals.vera # Chapter 1: Float64 literals ├── ch01_string_escapes.vera # Chapter 1: String escape sequences -├── ... # 241 programs total, organized by spec chapter +├── ... # 242 programs total, organized by spec chapter ├── ch07_state_handler.vera # Chapter 7: State effect handler ├── ch07_exn_handler.vera # Chapter 7: Exn effect handler ├── ch09_numeric_builtins.vera # Chapter 9: Numeric built-in functions @@ -442,7 +442,7 @@ The manifest is the machine-readable feature inventory — agents can query it t ### Running the conformance suite ```bash -# Via pytest (parametrized — 1,195 tests: five stages × 239 entries) +# Via pytest (parametrized — 1,210 tests: five stages × 242 entries) pytest tests/test_conformance.py -v # Via standalone script (used in CI and pre-commit) @@ -971,9 +971,9 @@ Twenty-nine scripts in `scripts/` validate cross-cutting concerns beyond unit te | Script | What it validates | |--------|-------------------| -| `check_conformance.py` | All 241 conformance entries hold at their declared level (parse/check/verify/run) — positives pass; the negatives fail at the stage their `expected_error_stage` names (`check` by default, or `compile`, which also asserts the program type-checks cleanly) with their `expected_error` E-code | +| `check_conformance.py` | All 242 conformance entries hold at their declared level (parse/check/verify/run) — positives pass; the negatives fail at the stage their `expected_error_stage` names (`check` by default, or `compile`, which also asserts the program type-checks cleanly) with their `expected_error` E-code | | `check_examples.py` | All 42 `.vera` examples pass `vera check` + `vera verify` | -| `check_corpus_canonical.py` | All 290 corpus programs (recursive over `examples/` + `tests/conformance/`) are in canonical form under `vera fmt` | +| `check_corpus_canonical.py` | All 291 corpus programs (recursive over `examples/` + `tests/conformance/`) are in canonical form under `vera fmt` | | `check_examples_readme.py` | Every `vera run` command in examples/README.md references an existing file and exported function | | `check_spec_examples.py` | 189 parseable code blocks from spec chapters: parse, type-check, and verify | | `check_readme_examples.py` | All Vera code blocks in README.md parse correctly | @@ -1080,9 +1080,9 @@ The repository configures 36 hooks across two stages: 34 run at the commit stage | `ruff check .` | Lint Python with ruff (default `F` + `E` rules) | | `mypy vera/` | Type-check compiler in strict mode | | `pytest tests/ -q` | Run full test suite | -| `check_conformance.py` | All 241 conformance entries hold at their declared level — positives pass; negatives fail at the stage their `expected_error_stage` names (`check` or `compile`) with their `expected_error` E-code | +| `check_conformance.py` | All 242 conformance entries hold at their declared level — positives pass; negatives fail at the stage their `expected_error_stage` names (`check` or `compile`) with their `expected_error` E-code | | `check_examples.py` | All 42 examples pass `vera check` + `vera verify` | -| `check_corpus_canonical.py` | All 290 `examples/` + `tests/conformance/` programs (recursive) are in canonical form (`vera fmt`) | +| `check_corpus_canonical.py` | All 291 `examples/` + `tests/conformance/` programs (recursive) are in canonical form (`vera fmt`) | | `check_examples_readme.py` | `vera run` commands in `examples/README.md` reference existing files and exported functions | | `check_readme_examples.py` | README code blocks parse correctly | | `check_examples_doc.py` | EXAMPLES.md code blocks parse correctly | diff --git a/docs/SKILL.md b/docs/SKILL.md index 08f3458f..c11ce0c0 100644 --- a/docs/SKILL.md +++ b/docs/SKILL.md @@ -440,7 +440,7 @@ array, useful when processing diagnostics programmatically. - `Unit` — singleton type, value is `()`. Zero-size and **declaration-only**: a `@Unit` parameter (function or handler-clause op) is legal but reading it (`@Unit.0`) is a checker error (E182) — write the literal `()` instead — and a `let` of a zero-size type (`let @Unit = put(5);`) is a checker error (E183) — call the expression as a statement (`put(5);`). Applies to anything with no runtime representation, including `Future`. - `Never` — bottom type (used for non-terminating expressions like `throw`) -**`Int` and `Nat` are interchangeable in both directions.** `@Nat <: @Int` is a formal subtyping rule at the *type* level (use a `@Nat` anywhere `@Int` is expected, no `nat_to_int` call), and `@Int <: @Nat` is permitted by the type checker with a verifier-discharged obligation (`@Int.0 >= 0`). This means `array_length` (declared `@Int`) flows freely into `@Nat` positions without explicit conversion — the verifier proves non-negativity from context or falls back to a runtime check. Both directions carry a *value*-level obligation, because `@Nat` is a u64 and `@Int` an i64: narrowing requires `>= 0` (`E503`/`E504`), and widening requires `<= i64.MAX` (`E530`; or an `E531` warning at the generic-instantiated `@Int`-field component site code generation cannot guard) — a `@Nat` above i64.MAX bit-reinterprets to a negative `@Int`. Runtime guards and verifier obligations correspond at every closure depth (nested closure returns included); the documented residual runs in one direction only. Obligated-but-not-guarded: three narrowing sites are statically obligated yet carry no runtime guard — the effect-operation argument and the generic-instantiated constructor field ([#754](https://github.com/aallan/vera/issues/754)/[#757](https://github.com/aallan/vera/issues/757)), and the `nat_to_int`/`nat_to_string` conversion builtins — where an `E504` warning discloses the unguarded residual rather than claiming a check the runtime never performs. **Do not** insert `nat_to_int` defensively; `@Nat` already flows to `@Int`. Keep a value that may be negative as `@Int`, or use `int_to_nat` (which returns `Option`) when an explicit narrowing must handle the failure case. See spec §2.2.1 for the formal rule. +**`Int` and `Nat` are interchangeable in both directions.** `@Nat <: @Int` is a formal subtyping rule at the *type* level (use a `@Nat` anywhere `@Int` is expected, no `nat_to_int` call), and `@Int <: @Nat` is permitted by the type checker with a verifier-discharged obligation (`@Int.0 >= 0`). This means `array_length` (declared `@Int`) flows freely into `@Nat` positions without explicit conversion — the verifier proves non-negativity from context or falls back to a runtime check. Both directions carry a *value*-level obligation, because `@Nat` is a u64 and `@Int` an i64: narrowing requires `>= 0` (`E503`/`E504`), and widening requires `<= i64.MAX` (`E530`; or an `E531` warning at the generic-instantiated `@Int`-field component site code generation cannot guard) — a `@Nat` above i64.MAX bit-reinterprets to a negative `@Int`. Runtime guards and verifier obligations correspond at every closure depth (nested closure returns included); the documented residual runs in one direction only. Obligated-but-not-guarded: three narrowing sites are statically obligated yet carry no runtime guard — a user-declared effect operation's argument and the generic-instantiated constructor field ([#754](https://github.com/aallan/vera/issues/754)/[#757](https://github.com/aallan/vera/issues/757)), and the `nat_to_int`/`nat_to_string` conversion builtins — where an `E504` warning discloses the unguarded residual rather than claiming a check the runtime never performs. The built-in effects' operation arguments are guarded: the `State` write boundaries, and the `Exn` `throw` payload, which also takes the refinement-predicate guard ([#1268](https://github.com/aallan/vera/issues/1268)). **Do not** insert `nat_to_int` defensively; `@Nat` already flows to `@Int`. Keep a value that may be negative as `@Int`, or use `int_to_nat` (which returns `Option`) when an explicit narrowing must handle the failure case. See spec §2.2.1 for the formal rule. ### Composite types @@ -2402,7 +2402,7 @@ public fn main(@Unit -> @Unit) ## Conformance Suite -The `tests/conformance/` directory contains 241 small programs — most self-contained, with the Chapter 8 module-system programs and a few cross-module Chapter 7 and 9 programs importing companion `_lib.vera` / `_mid.vera` modules — that validate every language feature against the spec — often one program per feature, though some features (slot references, match, contracts) span several. These are the best minimal working examples of Vera syntax and semantics. +The `tests/conformance/` directory contains 242 small programs — most self-contained, with the Chapter 8 module-system programs and a few cross-module Chapter 7 and 9 programs importing companion `_lib.vera` / `_mid.vera` modules — that validate every language feature against the spec — often one program per feature, though some features (slot references, match, contracts) span several. These are the best minimal working examples of Vera syntax and semantics. Each program is organized by spec chapter (`ch01_int_literals.vera`, `ch04_match_basic.vera`, `ch07_state_handler.vera`, etc.) and the `manifest.json` file maps features to programs. When you need to see how a specific construct works, check the conformance program before reading the spec. @@ -2443,7 +2443,7 @@ Current reference-implementation bugs that an agent writing Vera code is likely |---|---|---|---| | Rare conformance-gate flake | `ch05_closure_nat_return` trapped once in a full conformance run and never again (~960 clean attempts) — suspected runtime/GC timing interaction, not a compiler defect. | If CI reds on this program with `Reached unreachable` in `main`, re-run and report on the issue with wasmtime version + load conditions — do not chase the compiler. | [#996](https://github.com/aallan/vera/issues/996) | -When a Vera program type-checks cleanly, compiles without errors, and then produces a runtime trap you can't explain, runtime trap diagnostics are now Vera-native end-to-end: each trap carries a `kind` label (`divide_by_zero` / `out_of_bounds` / `stack_exhausted` / `unreachable` / `overflow` / `contract_violation` / `host_error` / `unknown`), a per-kind `Fix:` paragraph naming the canonical remediation, and a source backtrace pointing at the offending Vera function and line — not just `wasm trap: `. Tail-recursive iteration runs in constant WASM stack space for both non-allocating ([#517](https://github.com/aallan/vera/issues/517), v0.0.126) and allocating ([#549](https://github.com/aallan/vera/issues/549), v0.0.154) tail calls — the latter prepends a `$gc_sp` restore before each `return_call` to keep the shadow stack bounded across iterations. +When a Vera program type-checks cleanly, compiles without errors, and then produces a runtime trap you can't explain, runtime trap diagnostics are now Vera-native end-to-end: each trap carries a `kind` label (`divide_by_zero` / `out_of_bounds` / `stack_exhausted` / `unreachable` / `overflow` / `contract_violation` / `host_error` / `unknown`), a per-kind `Fix:` paragraph naming the canonical remediation (omitted for `contract_violation` and `host_error`, whose descriptions already carry the specific instruction, and for `unknown`, where there is nothing general to suggest), and a source backtrace pointing at the offending Vera function and line — not just `wasm trap: `. Tail-recursive iteration runs in constant WASM stack space for both non-allocating ([#517](https://github.com/aallan/vera/issues/517), v0.0.126) and allocating ([#549](https://github.com/aallan/vera/issues/549), v0.0.154) tail calls — the latter prepends a `$gc_sp` restore before each `return_call` to keep the shadow stack bounded across iterations. ## Specification Reference diff --git a/docs/index.html b/docs/index.html index 4ed15a2b..fd324e1f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -699,7 +699,7 @@

This page is also a machine-readable specificati Vera is under active development

- A complete compiler with 164 built-in functions, ten algebraic effects (IO, Http, HttpServer, State, Exceptions, Async, Inference, DB, Random, Diverge), contract-driven testing via Z3, a language server with agent-facing proof deltas, and a 14-chapter specification. A 241-program conformance suite and 42 worked examples are validated against the spec on every pull request. All of it is developed openly on GitHub and released under the MIT licence. + A complete compiler with 164 built-in functions, ten algebraic effects (IO, Http, HttpServer, State, Exceptions, Async, Inference, DB, Random, Diverge), contract-driven testing via Z3, a language server with agent-facing proof deltas, and a 14-chapter specification. A 242-program conformance suite and 42 worked examples are validated against the spec on every pull request. All of it is developed openly on GitHub and released under the MIT licence.

diff --git a/docs/index.md b/docs/index.md index b1763be5..49454d5b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -281,7 +281,7 @@ For other models: point them at [`SKILL.md`](https://veralang.dev/SKILL.md) via ## Status -Vera is under [active development](https://raw.githubusercontent.com/aallan/vera/main/ROADMAP.md). A complete compiler with 164 built-in functions, ten algebraic effects (IO, Http, HttpServer, State, Exceptions, Async, Inference, DB, Random, Diverge), contract-driven testing via [Z3](https://www.microsoft.com/en-us/research/project/z3-3/), and a 14-chapter specification. A 241-program conformance suite and 42 worked examples are validated against the spec on every pull request. All of it is developed openly on [GitHub](https://github.com/aallan/vera) and released under the MIT licence. +Vera is under [active development](https://raw.githubusercontent.com/aallan/vera/main/ROADMAP.md). A complete compiler with 164 built-in functions, ten algebraic effects (IO, Http, HttpServer, State, Exceptions, Async, Inference, DB, Random, Diverge), contract-driven testing via [Z3](https://www.microsoft.com/en-us/research/project/z3-3/), and a 14-chapter specification. A 242-program conformance suite and 42 worked examples are validated against the spec on every pull request. All of it is developed openly on [GitHub](https://github.com/aallan/vera) and released under the MIT licence. ## Links diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 8dcc8ce8..2c6b265c 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -446,7 +446,7 @@ array, useful when processing diagnostics programmatically. - `Unit` — singleton type, value is `()`. Zero-size and **declaration-only**: a `@Unit` parameter (function or handler-clause op) is legal but reading it (`@Unit.0`) is a checker error (E182) — write the literal `()` instead — and a `let` of a zero-size type (`let @Unit = put(5);`) is a checker error (E183) — call the expression as a statement (`put(5);`). Applies to anything with no runtime representation, including `Future`. - `Never` — bottom type (used for non-terminating expressions like `throw`) -**`Int` and `Nat` are interchangeable in both directions.** `@Nat <: @Int` is a formal subtyping rule at the *type* level (use a `@Nat` anywhere `@Int` is expected, no `nat_to_int` call), and `@Int <: @Nat` is permitted by the type checker with a verifier-discharged obligation (`@Int.0 >= 0`). This means `array_length` (declared `@Int`) flows freely into `@Nat` positions without explicit conversion — the verifier proves non-negativity from context or falls back to a runtime check. Both directions carry a *value*-level obligation, because `@Nat` is a u64 and `@Int` an i64: narrowing requires `>= 0` (`E503`/`E504`), and widening requires `<= i64.MAX` (`E530`; or an `E531` warning at the generic-instantiated `@Int`-field component site code generation cannot guard) — a `@Nat` above i64.MAX bit-reinterprets to a negative `@Int`. Runtime guards and verifier obligations correspond at every closure depth (nested closure returns included); the documented residual runs in one direction only. Obligated-but-not-guarded: three narrowing sites are statically obligated yet carry no runtime guard — the effect-operation argument and the generic-instantiated constructor field ([#754](https://github.com/aallan/vera/issues/754)/[#757](https://github.com/aallan/vera/issues/757)), and the `nat_to_int`/`nat_to_string` conversion builtins — where an `E504` warning discloses the unguarded residual rather than claiming a check the runtime never performs. **Do not** insert `nat_to_int` defensively; `@Nat` already flows to `@Int`. Keep a value that may be negative as `@Int`, or use `int_to_nat` (which returns `Option`) when an explicit narrowing must handle the failure case. See spec §2.2.1 for the formal rule. +**`Int` and `Nat` are interchangeable in both directions.** `@Nat <: @Int` is a formal subtyping rule at the *type* level (use a `@Nat` anywhere `@Int` is expected, no `nat_to_int` call), and `@Int <: @Nat` is permitted by the type checker with a verifier-discharged obligation (`@Int.0 >= 0`). This means `array_length` (declared `@Int`) flows freely into `@Nat` positions without explicit conversion — the verifier proves non-negativity from context or falls back to a runtime check. Both directions carry a *value*-level obligation, because `@Nat` is a u64 and `@Int` an i64: narrowing requires `>= 0` (`E503`/`E504`), and widening requires `<= i64.MAX` (`E530`; or an `E531` warning at the generic-instantiated `@Int`-field component site code generation cannot guard) — a `@Nat` above i64.MAX bit-reinterprets to a negative `@Int`. Runtime guards and verifier obligations correspond at every closure depth (nested closure returns included); the documented residual runs in one direction only. Obligated-but-not-guarded: three narrowing sites are statically obligated yet carry no runtime guard — a user-declared effect operation's argument and the generic-instantiated constructor field ([#754](https://github.com/aallan/vera/issues/754)/[#757](https://github.com/aallan/vera/issues/757)), and the `nat_to_int`/`nat_to_string` conversion builtins — where an `E504` warning discloses the unguarded residual rather than claiming a check the runtime never performs. The built-in effects' operation arguments are guarded: the `State` write boundaries, and the `Exn` `throw` payload, which also takes the refinement-predicate guard ([#1268](https://github.com/aallan/vera/issues/1268)). **Do not** insert `nat_to_int` defensively; `@Nat` already flows to `@Int`. Keep a value that may be negative as `@Int`, or use `int_to_nat` (which returns `Option`) when an explicit narrowing must handle the failure case. See spec §2.2.1 for the formal rule. ### Composite types @@ -2408,7 +2408,7 @@ public fn main(@Unit -> @Unit) ## Conformance Suite -The `tests/conformance/` directory contains 241 small programs — most self-contained, with the Chapter 8 module-system programs and a few cross-module Chapter 7 and 9 programs importing companion `_lib.vera` / `_mid.vera` modules — that validate every language feature against the spec — often one program per feature, though some features (slot references, match, contracts) span several. These are the best minimal working examples of Vera syntax and semantics. +The `tests/conformance/` directory contains 242 small programs — most self-contained, with the Chapter 8 module-system programs and a few cross-module Chapter 7 and 9 programs importing companion `_lib.vera` / `_mid.vera` modules — that validate every language feature against the spec — often one program per feature, though some features (slot references, match, contracts) span several. These are the best minimal working examples of Vera syntax and semantics. Each program is organized by spec chapter (`ch01_int_literals.vera`, `ch04_match_basic.vera`, `ch07_state_handler.vera`, etc.) and the `manifest.json` file maps features to programs. When you need to see how a specific construct works, check the conformance program before reading the spec. @@ -2449,7 +2449,7 @@ Current reference-implementation bugs that an agent writing Vera code is likely |---|---|---|---| | Rare conformance-gate flake | `ch05_closure_nat_return` trapped once in a full conformance run and never again (~960 clean attempts) — suspected runtime/GC timing interaction, not a compiler defect. | If CI reds on this program with `Reached unreachable` in `main`, re-run and report on the issue with wasmtime version + load conditions — do not chase the compiler. | [#996](https://github.com/aallan/vera/issues/996) | -When a Vera program type-checks cleanly, compiles without errors, and then produces a runtime trap you can't explain, runtime trap diagnostics are now Vera-native end-to-end: each trap carries a `kind` label (`divide_by_zero` / `out_of_bounds` / `stack_exhausted` / `unreachable` / `overflow` / `contract_violation` / `host_error` / `unknown`), a per-kind `Fix:` paragraph naming the canonical remediation, and a source backtrace pointing at the offending Vera function and line — not just `wasm trap: `. Tail-recursive iteration runs in constant WASM stack space for both non-allocating ([#517](https://github.com/aallan/vera/issues/517), v0.0.126) and allocating ([#549](https://github.com/aallan/vera/issues/549), v0.0.154) tail calls — the latter prepends a `$gc_sp` restore before each `return_call` to keep the shadow stack bounded across iterations. +When a Vera program type-checks cleanly, compiles without errors, and then produces a runtime trap you can't explain, runtime trap diagnostics are now Vera-native end-to-end: each trap carries a `kind` label (`divide_by_zero` / `out_of_bounds` / `stack_exhausted` / `unreachable` / `overflow` / `contract_violation` / `host_error` / `unknown`), a per-kind `Fix:` paragraph naming the canonical remediation (omitted for `contract_violation` and `host_error`, whose descriptions already carry the specific instruction, and for `unknown`, where there is nothing general to suggest), and a source backtrace pointing at the offending Vera function and line — not just `wasm trap: `. Tail-recursive iteration runs in constant WASM stack space for both non-allocating ([#517](https://github.com/aallan/vera/issues/517), v0.0.126) and allocating ([#549](https://github.com/aallan/vera/issues/549), v0.0.154) tail calls — the latter prepends a `$gc_sp` restore before each `return_call` to keep the shadow stack bounded across iterations. ## Specification Reference @@ -2487,7 +2487,7 @@ Read `SKILL.md` for the full language reference. It covers syntax, slot referenc ### Conformance programs as reference -The conformance suite in `tests/conformance/` contains 241 small, self-contained programs — often one per language feature — that serve as minimal working examples (most are fully self-contained; the cross-module programs of Chapters 7–9 import companion `_lib`/module fixtures). Each positive program must pass its declared verification level (see `manifest.json` for mappings: `parse`, `check`, `verify`, or `run`); the thirty-seven negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) instead must *fail* with the E-code in their `expected_error` field, at the stage their `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses. When you need to see how a specific construct works (e.g. effect handlers, match expressions, closures), check the corresponding conformance program before reading the spec. +The conformance suite in `tests/conformance/` contains 242 small, self-contained programs — often one per language feature — that serve as minimal working examples (most are fully self-contained; the cross-module programs of Chapters 7–9 import companion `_lib`/module fixtures). Each positive program must pass its declared verification level (see `manifest.json` for mappings: `parse`, `check`, `verify`, or `run`); the thirty-seven negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) instead must *fail* with the E-code in their `expected_error` field, at the stage their `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses. When you need to see how a specific construct works (e.g. effect handlers, match expressions, closures), check the corresponding conformance program before reading the spec. ### Workflow @@ -2666,9 +2666,9 @@ Each stage is a module with a single public API function (`parse_file`, `transfo pytest tests/ -v # Run all tests (see TESTING.md) pytest tests/test_conformance.py -v # Conformance suite only mypy vera/ # Type-check the compiler -python scripts/check_conformance.py # All 241 conformance programs hold (positives pass; negatives fail with their E-code) +python scripts/check_conformance.py # All 242 conformance programs hold (positives pass; negatives fail with their E-code) python scripts/check_examples.py # All 42 examples must pass -python scripts/check_corpus_canonical.py # All 290 corpus programs in canonical form +python scripts/check_corpus_canonical.py # All 291 corpus programs in canonical form ``` Test helpers follow a pattern: `_check_ok(source)` / `_check_err(source, match)` / `_verify_ok(source)` / `_verify_err(source, match)`. See existing tests for examples. @@ -2677,7 +2677,7 @@ When implementing a new language feature, write the conformance program *first* ### Invariants -- All 241 conformance programs in `tests/conformance/` must hold at their declared level — positive entries pass, and the negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) must *fail* with their `expected_error` E-code, at the stage `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses (`ch08_module_prelude_adt_contention_rejected` → E621), which also asserts the program type-checks cleanly first +- All 242 conformance programs in `tests/conformance/` must hold at their declared level — positive entries pass, and the negative fixtures (`ch02_generic_over_unit_rejected`, `ch02_map_unit_value_rejected`, `ch04_let_unit_rejected`, `ch05_apply_fn_arity`, `ch05_decreases_float_rejected`, `ch05_reserved_fn_name_rejected`, `ch05_reserved_keyword_fn_rejected`, `ch05_reserved_resume_fn_rejected`, `ch05_where_helper_outer_slot_rejected`, `ch07_handler_state_body_scope_rejected`, `ch07_old_outside_ensures_rejected`, `ch07_state_unit_op_param_read_rejected`, `ch08_ambiguous_import_adt_rejected`, `ch08_ambiguous_import_adt_swapped_rejected`, `ch08_ambiguous_import_rejected`, `ch08_ambiguous_import_swapped_rejected`, `ch08_circular_import`, `ch08_reserved_vera_prefix_rejected`, `ch08_reserved_vera_prefix_reference_rejected`, `ch08_reserved_vera_prefix_binder_rejected`, `ch08_reserved_vera_prefix_effect_rejected`, `ch08_reserved_vera_prefix_ability_rejected`, `ch08_reserved_vera_prefix_constructor_rejected`, `ch08_visibility_private`, `ch09_builtin_effect_redefinition_rejected`, `ch09_builtin_redefinition`, `ch09_ord_adt_rejected`, `ch09_eq_non_derivable_rejected`, `ch09_sql_injection_rejected`, `ch09_sql_placeholder_mismatch_rejected`, `ch09_sql_placeholder_let_mismatch_rejected`, `ch09_sql_numbered_placeholder_rejected`, `ch07_bare_effect_op_rejected`, `ch06_quantifier_array_domain_rejected`, `ch07_handler_state_type_mismatch_rejected`, `ch02_alias_cycle_rejected`, `ch08_module_prelude_adt_contention_rejected`) must *fail* with their `expected_error` E-code, at the stage `expected_error_stage` names — `check` by default, or `compile` for a diagnostic the checker accepts and codegen refuses (`ch08_module_prelude_adt_contention_rejected` → E621), which also asserts the program type-checks cleanly first - All 42 examples in `examples/` must pass `vera check` and `vera verify` - `mypy vera/` must be clean - `pytest tests/ -v` must pass @@ -3222,7 +3222,7 @@ None of this is Vera-specific, but it validates the design choices. The thesis i This is a real concern. LLMs are trained on trillions of tokens of Python, TypeScript, and JavaScript. A MojoBench study (NAACL 2025) found that even fine-tuned models achieved only 30–35% improvement over base models on Mojo code generation, illustrating the cold-start problem for new languages. -Vera's approach has three parts. First, the agent-facing documentation (SKILL.md) is designed to be dropped into a model's context window, so the model works from the language specification rather than training data recall. Second, Vera's syntax is deliberately simple and regular — fewer constructs, each with exactly one canonical form — which reduces the surface area a model needs to learn. Third, the conformance test suite (241 programs covering every language feature) gives models concrete examples to learn from and conform to. Simon Willison's December 2025 JustHTML write-up illustrates the same point in practice: an LLM-assisted implementation, guided by the html5lib conformance suite, conformed to the HTML parsing spec by running against its tests — a comprehensive test suite is a strong scaffold for a model implementing to a specification. +Vera's approach has three parts. First, the agent-facing documentation (SKILL.md) is designed to be dropped into a model's context window, so the model works from the language specification rather than training data recall. Second, Vera's syntax is deliberately simple and regular — fewer constructs, each with exactly one canonical form — which reduces the surface area a model needs to learn. Third, the conformance test suite (242 programs covering every language feature) gives models concrete examples to learn from and conform to. Simon Willison's December 2025 JustHTML write-up illustrates the same point in practice: an LLM-assisted implementation, guided by the html5lib conformance suite, conformed to the HTML parsing spec by running against its tests — a comprehensive test suite is a strong scaffold for a model implementing to a specification. ## How does Vera compare to Dafny / Lean / Koka / F*? @@ -3265,7 +3265,7 @@ The reference compiler is under active development. The current release includes - A seven-stage pipeline: parse, transform, resolve, typecheck, verify, compile, execute - A 14-chapter formal specification -- 11,600 tests, including a 241-program conformance suite +- 11,623 tests, including a 242-program conformance suite - 42 working example programs - 164 built-in functions covering strings, arrays, math, parsing, and data types - Four built-in abilities (Eq, Ord, Hash, Show) with constrained generics and ADT auto-derivation diff --git a/docs/llms.txt b/docs/llms.txt index 0bdc4197..2447d760 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -56,4 +56,4 @@ Current version: 0.1.11. The reference compiler is written in Python. Install th - [TESTING.md](https://raw.githubusercontent.com/aallan/vera/main/TESTING.md): Test suite architecture, coverage data, and test conventions. - [KNOWN_ISSUES.md](https://raw.githubusercontent.com/aallan/vera/main/KNOWN_ISSUES.md): Known bugs and limitations. - [CONTRIBUTING.md](https://raw.githubusercontent.com/aallan/vera/main/CONTRIBUTING.md): Contribution guidelines. -- [Conformance Suite](https://github.com/aallan/vera/tree/main/tests/conformance): 241 programs validating every language feature against the spec. +- [Conformance Suite](https://github.com/aallan/vera/tree/main/tests/conformance): 242 programs validating every language feature against the spec. diff --git a/spec/02-types.md b/spec/02-types.md index 6e288942..ad3ac3cd 100644 --- a/spec/02-types.md +++ b/spec/02-types.md @@ -300,7 +300,9 @@ An obligation drops to Tier 3 — reported as an `E506` warning rather than sile ### 2.6.5 Runtime Guards -A refinement predicate is also guarded at **runtime**: the compiler emits a predicate check at every function boundary — a refined parameter is checked at entry and a refined return at exit — that traps (via the contract-failure channel) if the value violates the predicate. So even a program compiled *without* `vera verify` rejects a refinement-violating value rather than silently accepting it; for example, calling `clamp_percent(@Int)` whose body returns a value outside `0..100` traps with a refinement-violation diagnostic. This holds at a `public`/FFI entry point too, where an untrusted caller cannot bypass the callee's entry guard. A call argument is covered by that guard, so the boundary checks compose to cover every narrowing whose result is consumed across a boundary; a purely internal narrowing (a `let`, match bind, destructure, constructor field, ADT sub-pattern bind, or effect-operation argument that never crosses a boundary) is Tier-3-static-only — surfaced as an `E506` warning, not silently accepted. +A refinement predicate is also guarded at **runtime**: the compiler emits a predicate check at every function boundary — a refined parameter is checked at entry and a refined return at exit — that traps (via the contract-failure channel) if the value violates the predicate. So even a program compiled *without* `vera verify` rejects a refinement-violating value rather than silently accepting it; for example, calling `clamp_percent(@Int)` whose body returns a value outside `0..100` traps with a refinement-violation diagnostic. This holds at a `public`/FFI entry point too, where an untrusted caller cannot bypass the callee's entry guard. A call argument is covered by that guard, so the boundary checks compose to cover every narrowing whose result is consumed across a boundary; a purely internal narrowing (a `let`, match bind, destructure, constructor field, ADT sub-pattern bind, or a *user-declared* effect operation's argument that never crosses a boundary) is Tier-3-static-only — surfaced as an `E506` warning, not silently accepted. + +The built-in `Exn` effect's `throw` payload is guarded, not internal. `throw(v)` narrows `v` into the `Exn` payload and the value leaves the throwing function, but it crosses no *function* boundary on the way, so none of the composing checks above reaches it: the compiler emits the predicate check at the `throw` itself, and a violating payload traps there rather than arriving in a handler clause that has already assumed the predicate. This covers refinements over **a base the verifier does not model** too — `{ @Array | array_length(@Array.0) > 0 }`, and equally `{ @Byte | @Byte.0 < 10 }`. The predicate translator does not substitute such a binder, so a *symbolic* narrowing is Tier 3 *statically* (§2.6.4, cause 2) even where the predicate is itself in the decidable fragment; codegen compiles it directly to WebAssembly regardless, so an empty array passed into a `@NonEmptyArray` parameter — or a `@Byte` computed at run time and passed into a `@SmallByte` one — traps at run time. A literal argument does not reach the guard: it is decided statically (§2.6.4, cause 2), so `small(200)` is rejected before it can run. diff --git a/spec/06-contracts.md b/spec/06-contracts.md index 0ca79ae4..99e92402 100644 --- a/spec/06-contracts.md +++ b/spec/06-contracts.md @@ -266,7 +266,7 @@ To discharge an operation obligation, the programmer encodes the constraint in a **Division and modulo** are Tier-1-decidable — the divisor is a concrete integer term — so an unguarded `a / b` whose Tier-1-translatable divisor admits a zero counterexample is a compile error (E526); a divisor beyond the translatable fragment (a fresh-scope slot in a closure or handler clause, an opaque effect result, a solver timeout) degrades to a runtime-guarded Tier-3 obligation instead, while a manifest zero divisor is E526 in any position. (Float division is exempt: `f64.div` by zero yields inf/NaN, not a trap.) **Array indexing** depends on `array_length`, which the SMT layer models as an *uninterpreted* function (§6.3.2), so bounds reasoning is in general beyond Tier 1. The verifier therefore tiers the obligation honestly: it proves the bound at **Tier 1** when a literal length, refinement, precondition, or path condition pins the length; reports a compile error (**E527**) when the index provably exceeds a statically-known length (e.g. `[1, 2, 3][5]`); and otherwise — a dynamic, opaque length — degrades to a runtime-guarded **Tier 3** obligation (counted in `vera verify --json`, never a silent pass). An index inside a closure body, quantifier predicate, or handler-clause body is walked under a fresh (empty) slot scope; one that depends on a captured length or fresh slot is reported as a runtime-guarded **Tier 3** obligation — beyond the fresh scope's decidable fragment, with the codegen bounds-check backing it — while a literal-only shape still classifies exactly (a manifest out-of-bounds literal is a loud E527) — while an index in a quantifier *domain* or a handler *body* (enclosing-scope positions) is tiered at full precision like any direct-position index; lifting the fresh-scope sites to a Tier-1 proof is the Tier 2 work in [#427](https://github.com/aallan/vera/issues/427). Indexing applies to `Array` only; indexing a `String` is a type error (E161). -The `@Nat` obligations (E502 / E503) carry the most nuance, spanning many binding sites. The verifier emits an E502 obligation `lhs >= rhs` at every `@Nat - @Nat` subtraction site (see [#520](https://github.com/aallan/vera/issues/520)), and an E503 obligation `value >= 0` where an `@Int` value narrows into a `@Nat` **binding** slot — `let`, call-argument, effect-operation-argument, constructor-field, top-level match-bind, and literal-tuple-destructure sites (see [#552](https://github.com/aallan/vera/issues/552)), plus the generic-instantiation, ADT sub-pattern, non-literal-destructure, and cross-module imported-constructor sites (see [#747](https://github.com/aallan/vera/issues/747)), and the function **return** position — an `@Int` value (including an `if`/`match` tail) narrowing into a `@Nat` return is obligated `result >= 0` under the body's path conditions, the dual of the [#813](https://github.com/aallan/vera/issues/813) `@Nat -> @Int` widen-return obligation (see [#758](https://github.com/aallan/vera/issues/758)). The codegen mirrors the subtraction obligation and the `@Nat` binding and return sites with runtime guards — every concrete site (`let`, destructure, match-bind, sub-pattern, concrete constructor field, concrete call-argument) plus **generic function-formal calls**, which guard on the monomorphised callee (the mangled instance `pick$Nat` carries concrete `@Nat` flags). Two sites stay unguarded, both still obligated statically, so a Tier-3 narrowing the solver cannot discharge at either surfaces an E504 warning: the **effect-operation argument**, whose runtime guard is deferred (see [#754](https://github.com/aallan/vera/issues/754)), and the **generic-instantiated constructor field**, since constructor layouts carry no per-field `@Nat` metadata to monomorphise. Division, modulo, and array indexing now follow the same auto-synthesis pattern ([#680](https://github.com/aallan/vera/issues/680)); lifting dynamic or closure-captured array bounds from a runtime-guarded Tier 3 to a Tier-1 proof is part of the Tier 2 verification work in [#427](https://github.com/aallan/vera/issues/427). +The `@Nat` obligations (E502 / E503) carry the most nuance, spanning many binding sites. The verifier emits an E502 obligation `lhs >= rhs` at every `@Nat - @Nat` subtraction site (see [#520](https://github.com/aallan/vera/issues/520)), and an E503 obligation `value >= 0` where an `@Int` value narrows into a `@Nat` **binding** slot — `let`, call-argument, effect-operation-argument, constructor-field, top-level match-bind, and literal-tuple-destructure sites (see [#552](https://github.com/aallan/vera/issues/552)), plus the generic-instantiation, ADT sub-pattern, non-literal-destructure, and cross-module imported-constructor sites (see [#747](https://github.com/aallan/vera/issues/747)), and the function **return** position — an `@Int` value (including an `if`/`match` tail) narrowing into a `@Nat` return is obligated `result >= 0` under the body's path conditions, the dual of the [#813](https://github.com/aallan/vera/issues/813) `@Nat -> @Int` widen-return obligation (see [#758](https://github.com/aallan/vera/issues/758)). The codegen mirrors the subtraction obligation and the `@Nat` binding and return sites with runtime guards — every concrete site (`let`, destructure, match-bind, sub-pattern, concrete constructor field, concrete call-argument) plus **generic function-formal calls**, which guard on the monomorphised callee (the mangled instance `pick$Nat` carries concrete `@Nat` flags). Two sites stay unguarded, both still obligated statically, so a Tier-3 narrowing the solver cannot discharge at either surfaces an E504 warning: a **user-declared effect operation's argument**, whose runtime guard is deferred (see [#754](https://github.com/aallan/vera/issues/754)), and the **generic-instantiated constructor field**, since constructor layouts carry no per-field `@Nat` metadata to monomorphise. The built-in effects' operation arguments are guarded at their op-call sites: the `State` write boundaries (see [#1203](https://github.com/aallan/vera/issues/1203)) and the `Exn` `throw` payload, which also carries the §2.6.5 refinement-predicate guard (see [#1268](https://github.com/aallan/vera/issues/1268)). Division, modulo, and array indexing now follow the same auto-synthesis pattern ([#680](https://github.com/aallan/vera/issues/680)); lifting dynamic or closure-captured array bounds from a runtime-guarded Tier 3 to a Tier-1 proof is part of the Tier 2 verification work in [#427](https://github.com/aallan/vera/issues/427). **Integer overflow** ([#798](https://github.com/aallan/vera/issues/798)). `@Int` is a signed 64-bit machine integer and `@Nat` an unsigned one; `+` / `-` / `*` wrap at the i64 / u64 boundary. Like `@Nat` underflow and signed-division `MIN / -1`, an overflowing operation is a *partial* operation that **traps** at runtime rather than silently wrapping, so each `@Int` / `@Nat` `+` / `-` / `*` carries an obligation that the result stays in range. It is classified at the operands' **common (coerced) type** — `@Int` if either operand is `@Int` (since `@Nat <: @Int`), else `@Nat` — not one operand's self-type (a non-negative literal is `@Nat`, but `5 + @Int.0` is an i64 add) nor the possibly-narrowed result type (an `@Int.0 + 1` stored into a `@Nat` slot is still an i64 add). A two-check mirrors array indexing: the result provably in range → **Tier 1**; provably out of range (e.g. a literal `u64.MAX + 1`, or `@Int.0 + 1` under `requires(@Int.0 == i64.MAX)`) → a compile error (**E528**); otherwise — dynamic operands — a runtime-guarded **Tier 3** trap. `@Nat` subtraction is excluded — it is the underflow obligation (E502) above, never a high-overflow. diff --git a/spec/11-compilation.md b/spec/11-compilation.md index d46f0673..3b9eb432 100644 --- a/spec/11-compilation.md +++ b/spec/11-compilation.md @@ -69,7 +69,7 @@ The trap is classified as `kind="unreachable"` rather than a dedicated `kind="un Arithmetic **overflow** of `@Int`/`@Nat` addition, subtraction, and multiplication (`+`/`-`/`*`) is handled the same way ([#798](https://github.com/aallan/vera/issues/798)). Because `Int` and `Nat` share the `i64` representation, these operations wrap under two's-complement arithmetic, so each such site carries a Tier-1 proof obligation that the `result` stays in range — classified at the operands' **common (coerced) arithmetic type** (`@Int` if either operand is `@Int`, else `@Nat`; `@Nat` subtraction is excluded, being the underflow obligation above). The range checked is the **signed** 64-bit range for `@Int` and the **unsigned** 64-bit range for `@Nat` — the shared `i64` interpreted with the operation's signedness. The verifier discharges it three ways: result provably in range → **Tier 1**; provably out of range → a compile error (**E528**) raised *before* codegen; otherwise — dynamic operands — **Tier 3**. As with `@Nat` subtraction, the codegen is type-driven, not tier-driven: it emits the guarded op at every such site regardless of the verifier's tier (a Tier-1 discharge means the guard provably never fires, but it is still emitted). The overflow trap is classified `kind="overflow"`: [#808](https://github.com/aallan/vera/issues/808) wired the guard to a `vera.overflow_trap` host import (mirroring `vera.contract_fail`) that it calls immediately before its `unreachable`, so a dynamic overflow surfaces the dedicated overflow diagnostic and its Fix paragraph rather than the generic `unreachable` kind. (`@Nat` subtraction underflow keeps the bare-`unreachable` net described above — its dedicated kind is tracked separately.) The obligation stays sound because the function traps on overflow before it can return a wrapped value. -Authors lift Tier-3 functions back to Tier 1 by adding `requires lhs >= rhs` clauses. Subtraction sites that do not produce a `Nat`-typed result (e.g., `@Int - @Int`) carry no *underflow* obligation — they may produce negative values, which is well-defined for `Int` (they remain subject to the overflow obligation described above). `@Byte` arithmetic is not currently permitted by the type checker (`Byte` is excluded from `NUMERIC_TYPES` in `vera/types.py`), so the underflow obligation needs no `Byte` extension today; allowing `@Byte` arithmetic with both underflow and overflow guards is tracked speculatively as [#564](https://github.com/aallan/vera/issues/564). The verifier checks the `@Nat >= 0` invariant at subtraction sites (in any position, including a function's return expression) and at binding sites where an `@Int` value narrows into a `@Nat` slot — `let` bindings, call arguments, constructor fields, top-level match binds, and literal-tuple destructures, plus the pure-literal `let @Nat = 0 - 1` case the subtraction obligation defers ([#552](https://github.com/aallan/vera/issues/552)). [#747](https://github.com/aallan/vera/issues/747) extended the narrowing obligation to the projection and instantiation sites — ADT sub-pattern binds (`match opt { Some(@Nat) -> }` on `Option`), non-literal tuple destructures, generic constructor / effect-operation / function formals instantiated to `@Nat`, and imported ADT constructors — so every narrowing **binding site** is now statically obligated. [#758](https://github.com/aallan/vera/issues/758) extended the obligation to the function **return** position: a bare `@Int`→`@Nat` narrowing at a return slot (including an `if`/`match` tail) is obligated `result >= 0` under the body's path conditions — the dual of #813's `@Nat -> @Int` widen-return — and codegen emits the mirroring return guard so an unverified compile traps rather than returning a negative through the `@Nat` slot. The Tier-3 runtime guard backs every binding and return site except two — the effect-operation argument, and the generic-instantiated constructor field (which erases to i64 with no per-field guard) — and a tripped guard reports a generic trap rather than the `requires(... >= 0)` fix. The effect-op guard and the dedicated trap kind are tracked as [#754](https://github.com/aallan/vera/issues/754), the generic constructor field as [#757](https://github.com/aallan/vera/issues/757) (see §11.17). +Authors lift Tier-3 functions back to Tier 1 by adding `requires lhs >= rhs` clauses. Subtraction sites that do not produce a `Nat`-typed result (e.g., `@Int - @Int`) carry no *underflow* obligation — they may produce negative values, which is well-defined for `Int` (they remain subject to the overflow obligation described above). `@Byte` arithmetic is not currently permitted by the type checker (`Byte` is excluded from `NUMERIC_TYPES` in `vera/types.py`), so the underflow obligation needs no `Byte` extension today; allowing `@Byte` arithmetic with both underflow and overflow guards is tracked speculatively as [#564](https://github.com/aallan/vera/issues/564). The verifier checks the `@Nat >= 0` invariant at subtraction sites (in any position, including a function's return expression) and at binding sites where an `@Int` value narrows into a `@Nat` slot — `let` bindings, call arguments, constructor fields, top-level match binds, and literal-tuple destructures, plus the pure-literal `let @Nat = 0 - 1` case the subtraction obligation defers ([#552](https://github.com/aallan/vera/issues/552)). [#747](https://github.com/aallan/vera/issues/747) extended the narrowing obligation to the projection and instantiation sites — ADT sub-pattern binds (`match opt { Some(@Nat) -> }` on `Option`), non-literal tuple destructures, generic constructor / effect-operation / function formals instantiated to `@Nat`, and imported ADT constructors — so every narrowing **binding site** is now statically obligated. [#758](https://github.com/aallan/vera/issues/758) extended the obligation to the function **return** position: a bare `@Int`→`@Nat` narrowing at a return slot (including an `if`/`match` tail) is obligated `result >= 0` under the body's path conditions — the dual of #813's `@Nat -> @Int` widen-return — and codegen emits the mirroring return guard so an unverified compile traps rather than returning a negative through the `@Nat` slot. The Tier-3 runtime guard backs every binding and return site except two — a user-declared effect operation's argument, and the generic-instantiated constructor field (which erases to i64 with no per-field guard) — and a tripped guard reports a generic trap rather than the `requires(... >= 0)` fix. The built-in effects' operation arguments are guarded at their op-call sites: the `State` write boundaries ([#1203](https://github.com/aallan/vera/issues/1203)) and the `Exn` `throw` payload ([#1268](https://github.com/aallan/vera/issues/1268)). The remaining effect-op guard and the dedicated trap kind are tracked as [#754](https://github.com/aallan/vera/issues/754), the generic constructor field as [#757](https://github.com/aallan/vera/issues/757) (see §11.17). The **`@Nat`→`@Int` widening** direction carries the dual obligation ([#813](https://github.com/aallan/vera/issues/813)). Because `Nat` (u64) and `Int` (i64) share the `i64` representation, widening a `@Nat` whose value exceeds `i64.MAX` bit-reinterprets it to a *negative* `@Int` (`u64.MAX` → `-1`), so each `@Nat`→`@Int` coercion site carries a Tier-1 proof obligation that the value is `<= i64.MAX` (`nat_to_int_coerce`). The verifier discharges it the same three ways: provably in range → **Tier 1**; provably out of range → a compile error (**E530**) before codegen; otherwise — dynamic value — **Tier 3**. Code generation emits the matching runtime guard (the same `unreachable` net, tripping when the widened i64 reads as negative) at the sites where the source `@Nat` is statically known: the return, `let`, call-argument, concrete `@Int` constructor-field, `@Nat`-field ADT sub-pattern extraction, match-binding, array-literal element, tuple construction/destructure component, heterogeneous `if`/`match` arm (a genuine `@Int`-slot alternative makes the join `@Int`, so the `@Nat` arm is guarded per-arm), and closure argument, return, and capture positions (a captured `@Nat` widening into an `@Int` closure body shares the body-return guard) ([#820](https://github.com/aallan/vera/issues/820) threaded the checker's per-component target-type table into code generation so the erased tuple/array layouts and heterogeneous arms recover their `@Int` target; the closure positions recover it from the closure's own function type). The table-recovered component guards fire for **imported module bodies** too ([#987](https://github.com/aallan/vera/issues/987)): the checker collects each resolved module's own span-keyed target-type table (`CheckArtifacts.module_artifacts`) and threads it into code generation, so an imported function's array-element and tuple-construction widening is runtime-guarded through the import door — including transitively-reached and shadowed (`mod$…`) module bodies — exactly as the library's own `vera verify` reports the site Tier-3. Each imported body is resolved against *its* module's table, not the importer's, so a coincidental cross-file span cannot mis-key a guard (a module with no threaded table falls back to no component guard rather than risking a wrong target). Imported **generic** function bodies are guarded the same way ([#998](https://github.com/aallan/vera/issues/998)): each monomorphized clone of an imported `forall` function carries its origin module, and the clone — including the shadowed `mod$…` clones and the per-clone hoisted `where`-helpers — is compiled against that module's own table (monomorphization preserves node spans, so the template's spans key the clone's body correctly), so a concrete `Array` / `Tuple` widening inside a generic import traps at every instantiation exactly as the library's own `vera verify` classifies it Tier-3. One *component* coercion site code generation still cannot guard remains: a generic-instantiated `@Int` field (e.g. `Some(@Nat.0)` into `Option`, erased to i64 with no per-field mono metadata) — the widening there is disclosed as an unguarded **E531** warning rather than claiming a runtime check it never emits (tracked with its narrowing dual, [#757](https://github.com/aallan/vera/issues/757)). Unlike the narrowing guard (a no-op on a valid `@Nat`, so safe to apply to every `@Nat` target), the widen guard fires **only** when the source is provably `@Nat` — never a genuine `@Int`, which may be legitimately negative. @@ -623,4 +623,4 @@ Whether the prelude is compiling its own declaration of that name depends on whi ## 11.17 Limitations -The Tier-3 runtime guard for the `@Nat >= 0` narrowing invariant covers every concrete binding site, the function **return** position ([#758](https://github.com/aallan/vera/issues/758)), and generic function-formal calls (guarded on the monomorphised callee). Two binding sites stay unguarded — the effect-operation argument (guard deferred) and the generic-instantiated constructor field (constructor layouts carry no per-field `@Nat` mono metadata) — though both are still obligated statically (a negative is an E503/E504 at compile time). A tripped guard also reports a generic trap rather than the `requires(... >= 0)` fix. The effect-op-argument guard and the dedicated trap kind are tracked as [#754](https://github.com/aallan/vera/issues/754). +The Tier-3 runtime guard for the `@Nat >= 0` narrowing invariant covers every concrete binding site, the function **return** position ([#758](https://github.com/aallan/vera/issues/758)), and generic function-formal calls (guarded on the monomorphised callee). Two binding sites stay unguarded — a user-declared effect operation's argument (guard deferred) and the generic-instantiated constructor field (constructor layouts carry no per-field `@Nat` mono metadata) — though both are still obligated statically (a negative is an E503/E504 at compile time). The built-in effects' operation arguments are guarded at their op-call sites: the `State` write boundaries ([#1203](https://github.com/aallan/vera/issues/1203)) and the `Exn` `throw` payload, which takes the §2.6.5 refinement-predicate guard beside the sign pair ([#1268](https://github.com/aallan/vera/issues/1268)). A tripped sign guard reports a generic trap rather than the `requires(... >= 0)` fix. The remaining effect-op-argument guard and the dedicated trap kind are tracked as [#754](https://github.com/aallan/vera/issues/754). diff --git a/tests/conformance/ch07_exn_payload_guard.vera b/tests/conformance/ch07_exn_payload_guard.vera new file mode 100644 index 00000000..0ebbdb9d --- /dev/null +++ b/tests/conformance/ch07_exn_payload_guard.vera @@ -0,0 +1,59 @@ +-- Conformance: the Exn payload is a guarded write boundary (Chapter 2, +-- Section 2.6.5; Chapter 7, Section 7.4). Tests: `throw`'s payload takes +-- the boundary's runtime guards at the throw itself — the refinement +-- predicate for a refined payload, the `>= 0` sign check for a `@Nat` one +-- (#1268) — so a legal payload passes them and reaches the handler clause +-- that binds it at the declared type. Both throws are proved at Tier 1 +-- from their preconditions, which is exactly the case §2.6.5 calls defense +-- in depth: the guards are emitted and never reached. Their violating +-- twins trap, which no conformance level can express, so those live in +-- tests/test_exn_throw_payload_1268.py. +type Pos = { @Int | @Int.0 > 0 }; + +private fn refined_thrower(@Int -> @Int) + requires(@Int.0 > 0) + ensures(true) + effects(>) +{ + throw(@Int.0) +} + +private fn nat_thrower(@Int -> @Int) + requires(@Int.0 >= 0) + ensures(true) + effects(>) +{ + throw(@Int.0) +} + +private fn refined_caught(@Unit -> @Int) + requires(true) + ensures(@Int.result == 7) + effects(pure) +{ + handle[Exn] { + throw(@Pos) -> { @Pos.0 } + } in { + refined_thrower(7) + } +} + +private fn nat_caught(@Unit -> @Int) + requires(true) + ensures(@Int.result == 5) + effects(pure) +{ + handle[Exn] { + throw(@Nat) -> { nat_to_int(@Nat.0) } + } in { + nat_thrower(5) + } +} + +public fn main(@Unit -> @Int) + requires(true) + ensures(@Int.result == 0) + effects(pure) +{ + refined_caught(()) - 7 + (nat_caught(()) - 5) +} diff --git a/tests/conformance/manifest.json b/tests/conformance/manifest.json index bc07a935..5989833d 100644 --- a/tests/conformance/manifest.json +++ b/tests/conformance/manifest.json @@ -1202,7 +1202,7 @@ "id": "ch07_state_clause_transform", "file": "ch07_state_clause_transform.vera", "chapter": 7, - "title": "State clause bodies execute \u2014 intrinsic-hybrid semantics (#976)", + "title": "State clause bodies execute — intrinsic-hybrid semantics (#976)", "level": "run", "spec_ref": "Section 7.5.2", "features": [ @@ -1261,9 +1261,9 @@ "id": "ch07_exn_string", "file": "ch07_exn_string.vera", "chapter": 7, - "title": "Exn effect handler \u2014 String payload", + "title": "Exn effect handler — String payload", "level": "run", - "spec_ref": "Section 7.4 Exn handler \u2014 String payload", + "spec_ref": "Section 7.4 Exn handler — String payload", "features": [ "exn_handler", "string_exception" @@ -1696,7 +1696,7 @@ "id": "ch08_transitive_module_import_base", "file": "ch08_transitive_module_import_base.vera", "chapter": 8, - "title": "Transitive import chain \u2014 base module", + "title": "Transitive import chain — base module", "level": "check", "spec_ref": "Section 8.6.4", "features": [ @@ -1708,7 +1708,7 @@ "id": "ch08_transitive_module_import_mid", "file": "ch08_transitive_module_import_mid.vera", "chapter": 8, - "title": "Transitive import chain \u2014 middle module", + "title": "Transitive import chain — middle module", "level": "verify", "spec_ref": "Section 8.6.4", "features": [ @@ -3460,5 +3460,19 @@ "array", "json_keys" ] + }, + { + "id": "ch07_exn_payload_guard", + "file": "ch07_exn_payload_guard.vera", + "chapter": 7, + "title": "The Exn payload is a runtime-guarded write boundary (#1268)", + "level": "run", + "spec_ref": "Section 2.6.5", + "features": [ + "exn_effect", + "refinement_type", + "nat_narrowing", + "runtime_guard" + ] } ] diff --git a/tests/test_closure_boundary_widths_1255_1256_1269.py b/tests/test_closure_boundary_widths_1255_1256_1269.py index e780c895..4861d7ef 100644 --- a/tests/test_closure_boundary_widths_1255_1256_1269.py +++ b/tests/test_closure_boundary_widths_1255_1256_1269.py @@ -757,11 +757,31 @@ def test_the_tag_and_the_thrown_value_agree_on_i32(self) -> None: Pinning both halves is what makes this a WIDTH-agreement test rather than "it runs" — a fix that widened the TAG to i64 would also run, and would put a Byte cell at eight bytes everywhere else. + + The value half no longer reads as adjacency. #1268 made the payload + a guarded write boundary as well as a sized one, so a REFINED payload + routes through a guard local between the literal and the `throw`; + asserting `i32.const 5\\n throw` would then be a test of where the + guard is, not of how wide the value is. What the width claim needs + is that the value the `throw` consumes is i32 by whichever route it + arrives — so the pushed operand is resolved: a literal directly, or + the guard local, whose DECLARED width is the thing checked. """ wat = _compile_ok(_THROW_REFINED_BYTE).wat tags = re.findall(r"\(tag \$exn_\S+ \(param ([^)]*)\)\)", wat) assert tags == ["i32"], tags - assert "i32.const 5\n throw $exn_" in wat, _fn_body(wat, "boom") + body = _fn_body(wat, "boom") + assert "i32.const 5" in body, body + assert "i64.const 5" not in body, body + lines = [ln.strip() for ln in body.strip().splitlines()] + throw_at = next(i for i, ln in enumerate(lines) + if ln.startswith("throw $exn_")) + pushed = lines[throw_at - 1] + if pushed.startswith("local.get "): + idx = pushed.split()[1] + assert f"(local $l{idx} i32)" in body, body + else: + assert pushed == "i32.const 5", body # ===================================================================== diff --git a/tests/test_exn_throw_payload_1268.py b/tests/test_exn_throw_payload_1268.py index 86d33b6c..c845c2dc 100644 --- a/tests/test_exn_throw_payload_1268.py +++ b/tests/test_exn_throw_payload_1268.py @@ -1,4 +1,5 @@ -"""``throw``'s payload is obligated like every other narrowing site (#1268). +"""``throw``'s payload is obligated AND guarded like every other narrowing +site (#1268). Every position a value narrows into a typed slot carries a proof obligation — ``let``, call argument, constructor field, effect-operation argument, handler @@ -15,18 +16,33 @@ table-driven fallback for it — keyed on ``expr.name == "put"``, so ``throw``, the only other bare built-in op taking an argument, stayed outside it. -Codegen emits no guard on the payload — ``throw`` lowers straight to a WASM -``throw $exn_`` with the argument on the stack — so the obligation is -unguarded (``guarded=False``) at all three arms, and its Tier-3 leg discloses -rather than claiming a runtime check it never gets. That claim is checked -against codegen here rather than asserted: the disclosure's own status is -pinned, and a run confirms the value really does pass through unchecked. +Codegen emitted no guard on the payload either — ``throw`` lowered straight to +a WASM ``throw $exn_`` with the argument on the stack — so a program +that never ran ``vera verify`` (and one whose ``E503`` its author ignored) +still delivered the violating value. It now takes the write boundary's +guards, at the op-call site beside ``put``'s: the ``@Int`` -> ``@Nat`` sign +guard, the ``@Nat`` -> ``@Int`` widening guard, and — refined FIRST, as +everywhere else — the §2.6.5 predicate guard for a refined payload, which +traps through ``$vera.contract_fail`` naming the predicate. So the obligation +is ``guarded`` at all three arms and its Tier-3 leg is counted, not disclosed. + +That claim is checked against codegen rather than asserted: each arm's status +is pinned AND a run confirms the value really is stopped, with a satisfying +twin beside it so "guarded" cannot be satisfied by a site that always traps. """ from __future__ import annotations import pytest -from tests.codegen_helpers import _run +from vera.codegen import execute +from vera.codegen.api import WasmTrapError +from tests.codegen_helpers import ( + _compile, + _compile_ok, + _run, + _run_refine_trap, + _run_trap, +) from tests.verifier_helpers import _verify, _verify_err @@ -213,17 +229,16 @@ def test_the_refined_alias_spelling_is_obligated_too(self) -> None: ] -class TestTheThrowPayloadDisclosureIsHonestAboutTheGuard: - """The verifier's `guarded=False` is checked against what codegen emits. +class TestTheThrowPayloadGuardIsRealAndItsDisclosureIsHonest: + """The verifier's `guarded=True` is checked against what codegen emits. - A unit assertion that the status is `tier3_unguarded` says what the - verifier believes; it cannot say whether the belief is true. The pair - here does: the status pins the flag, and the run pins codegen's side of - it. Were codegen to gain a payload guard, the run would trap and this - class would go red rather than leaving the verifier quietly pessimistic; - were the flag flipped without that codegen work, the status assertion - would go red rather than leaving a claimed runtime check that is not - emitted. + A unit assertion that the status is `tier3` says what the verifier + believes; it cannot say whether the belief is true. The pair here does: + the status pins the flag, and the run pins codegen's side of it. Delete + the guard emission and the run assertions go red rather than leaving a + claimed runtime check that is not emitted; flip the flag back without + removing the guard and the status assertions go red rather than leaving + the verifier quietly pessimistic about a boundary it does have. """ _UNTRANSLATABLE = """ @@ -270,23 +285,164 @@ class would go red rather than leaving the verifier quietly pessimistic; } """ - def test_an_undischargeable_payload_discloses_unguarded(self) -> None: + _REFINED_SYMBOLIC = """ +type Pos = { @Int | @Int.0 > 0 }; + +private fn boom(@Int -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(@Int.0) +} + +public fn main(@Int -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@Pos) -> { @Pos.0 } + } in { + boom(@Int.0) + } +} +""" + + _REFINED_OPAQUE = """ +type Pos = { @Int | @Int.0 > 0 }; + +private fn boom(@Float64 -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(float_to_int(@Float64.0)) +} + +public fn main(@Float64 -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@Pos) -> { @Pos.0 } + } in { + boom(@Float64.0) + } +} +""" + + _UNREFINED_INT = """ +private fn boom(@Int -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(@Int.0) +} + +public fn main(@Int -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@Int) -> { @Int.0 } + } in { + boom(@Int.0) + } +} +""" + + def test_an_undischargeable_payload_discloses_guarded(self) -> None: """`array_length` over a non-literal is deliberately untranslatable - (#802), so the obligation reaches Tier 3 — and must land on the - UNGUARDED leg (E504, excluded from the totals), not the - runtime-guarded one.""" + (#802), so the obligation reaches Tier 3 — and lands on the + runtime-GUARDED leg, counted in the totals, because the guard the leg + promises is emitted at the throw.""" result = _verify(self._UNTRANSLATABLE) binds = [o for o in result.obligations if o.kind == "nat_bind"] - assert [o.status for o in binds] == ["tier3_unguarded"], binds - assert [o.error_code for o in binds] == ["E504"], binds - assert result.summary.tier3_runtime == 0, result.summary + assert [o.status for o in binds] == ["tier3"], binds + assert result.summary.tier3_runtime >= 1, result.summary + + def test_the_payload_really_is_checked_at_run_time(self) -> None: + """...because codegen emits the sign guard: -5 no longer comes back + out of the `@Nat` payload, it traps. This is the measurement the + `guarded=True` above rests on.""" + _run_trap(self._SYMBOLIC, "main", [-5]) + + def test_a_satisfying_payload_still_runs(self) -> None: + """The over-refusal control for the run above. A guard that trapped + unconditionally would satisfy the trap assertion and break every + correct `throw`; the same program at a non-negative payload must + deliver it.""" + assert _run(self._SYMBOLIC, "main", [5]) == 5 + + def test_a_refined_payload_traps_naming_its_predicate(self) -> None: + """The refined arm's run: the §2.6.5 predicate guard, not the sign + guard — `0` clears the `@Int` base but violates `> 0`, so a `>= 0` + check would let it through. `_run_refine_trap` pins the + `$vera.contract_fail` channel rather than any trap.""" + _run_refine_trap(self._REFINED_SYMBOLIC, "main", [0]) + _run_refine_trap(self._REFINED_SYMBOLIC, "main", [-5]) + + def test_a_satisfying_refined_payload_still_runs(self) -> None: + """The refined arm's over-refusal control.""" + assert _run(self._REFINED_SYMBOLIC, "main", [7]) == 7 + + def test_an_unrefined_int_payload_keeps_its_negative(self) -> None: + """The TYPE gate is load-bearing: an `Exn` payload has no + non-negativity invariant to violate, so a negative one is a correct + program and must run. Guarding on the wrong type — the sign guard + applied to the `@Int` arm — turns this into a trap. + """ + assert _run(self._UNREFINED_INT, "main", [-5]) == -5 - def test_the_payload_really_is_unchecked_at_run_time(self) -> None: - """...because codegen emits no guard: -5 comes back out of the `@Nat` - payload. This is the measurement the `guarded=False` above rests on, - and the reason the refutation in the first class has to be an ERROR - rather than a deferral to a runtime check.""" - assert _run(self._SYMBOLIC, "main", [-5]) == -5 + _REFINED_STRING = """ +type Short = { @String | string_length(@String.0) < 5 }; + +private fn boom(@Int -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(to_string(@Int.0)) +} + +public fn main(@Int -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@Short) -> { string_length(@Short.0) } + } in { + boom(@Int.0) + } +} +""" + + def test_a_pair_payload_is_guarded_over_its_pointer(self) -> None: + """The other REPRESENTATION: a `@String`-based payload is (ptr, len) + in two locals, not one scalar, so the guard has to save both halves + and check over the ptr — the shape the lifted closure's `i32_pair` + return guard uses. A scalar-only guard emits an ill-typed body here + rather than a wrong answer, so this is the branch that would fail + loudly; the satisfying twin beside it is what shows the pair is put + back on the stack in the right order. + """ + _run_refine_trap(self._REFINED_STRING, "main", [12345678]) + assert _run(self._REFINED_STRING, "main", [1]) == 1 + + def test_a_refined_payload_discloses_guarded(self) -> None: + """The refined arm's status twin of the `@Nat` one above: an opaque + payload — `float_to_int`, which the verifier models only as an opaque + result — records `refine_bind` at the runtime-GUARDED Tier 3, counted + in the totals, because the predicate guard is emitted.""" + result = _verify(self._REFINED_OPAQUE) + binds = [o for o in result.obligations if o.kind == "refine_bind"] + assert [o.status for o in binds] == ["tier3"], binds + assert result.summary.tier3_runtime >= 1, result.summary def test_a_symbolic_payload_the_contract_bounds_proves(self) -> None: """The obligation is dischargeable, not merely loud: a `requires` @@ -320,6 +476,245 @@ def test_a_symbolic_payload_the_contract_bounds_proves(self) -> None: ], [d.description[:90] for d in result.diagnostics] +class TestTheGuardedClaimIsNeverWiderThanTheGuard: + """The three places the claim and the guard could disagree. + + A `guarded` Tier-3 is a PROMISE — "this will be checked at run time" — + and the obligation stream is the only place a reader can see it. Each + cell here is a shape where the promise and the emitted code came apart in + a direction a value oracle cannot see, because the program either never + runs or runs identically either way. + """ + + _NESTED = """ +type Pos = { @Int | @Int.0 > 0 }; + +type Tiny = { @Pos | @Pos.0 < 10 }; + +private fn boom(@Float64 -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(float_to_int(@Float64.0)) +} + +public fn main(@Float64 -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@Tiny) -> { @Tiny.0 } + } in { + boom(@Float64.0) + } +} +""" + + def _payload(self, base: str, spelling: str) -> str: + """A `throw` into `Exn` written bare or qualified. + + The payload is `float_to_int`'s opaque result, so the obligation + reaches Tier 3 and its GUARDEDNESS is what the status reports — + a refutation or a proof would hide the flag being compared. + """ + prelude = ("type Pos = { @Int | @Int.0 > 0 };\n\n" + if base == "Pos" else "") + body = "@Pos.0" if base == "Pos" else "nat_to_int(@Nat.0)" + return f""" +{prelude}private fn boom(@Float64 -> @Int) + requires(true) + ensures(true) + effects(>) +{{ + {spelling}(float_to_int(@Float64.0)) +}} + +public fn main(@Float64 -> @Int) + requires(true) + ensures(true) + effects(pure) +{{ + handle[Exn<{base}>] {{ + throw(@{base}) -> {{ {body} }} + }} in {{ + boom(@Float64.0) + }} +}} +""" + + def test_a_nested_refinement_payload_promises_nothing(self) -> None: + """A refinement OVER a refinement has no guard and cannot have one. + + `_refinement_guard_parts` refuses the shape outright — the outer + predicate alone would silently drop the inner membership — and says + so with a loud E618 at compile. The verifier's mirror answered + `True` for it, so `vera verify` exited 0 recording a Tier-3 that + "will be checked at run time" for a program that cannot be compiled + at all: a promise about a run that can never happen. Both halves are + pinned together, because either alone reads as consistent. + """ + result = _verify(self._NESTED) + binds = [o for o in result.obligations if o.kind == "refine_bind"] + assert [o.status for o in binds] == ["tier3_unguarded"], binds + assert result.summary.tier3_runtime == 1, result.summary + compiled = _compile(self._NESTED) + errors = [d for d in compiled.diagnostics if d.severity == "error"] + assert [d.error_code for d in errors] == ["E618"], [ + (d.error_code, d.description[:80]) for d in errors + ] + + @pytest.mark.parametrize( + ("base", "kind"), [("Nat", "nat_bind"), ("Pos", "refine_bind")], + ) + def test_the_two_spellings_record_the_same_thing( + self, base: str, kind: str, + ) -> None: + """`Exn.throw(v)` is `throw(v)`, so it must obligate identically. + + Codegen's qualified arm SYNTHESIZES a bare node and delegates to the + dispatcher that emits the guards, so both spellings are guarded — but + the verifier's `QualifiedCall` arm hardcoded `guarded=False` behind a + comment stale since [#1203], and disclosed E504/E506 for a boundary + that traps. Asserted as a differential rather than two literals: the + two spellings are ONE boundary, so the statuses must agree whatever + that agreed value is, and the run confirms which one is true. + """ + def of(result: object) -> list[tuple[str, str]]: + return [(o.kind, o.status) for o in result.obligations + if o.kind == kind] + + bare = _verify(self._payload(base, "throw")) + qualified = _verify(self._payload(base, "Exn.throw")) + assert of(bare) == of(qualified), (of(bare), of(qualified)) + assert of(bare) == [(kind, "tier3")], of(bare) + assert (bare.summary.tier3_runtime + == qualified.summary.tier3_runtime), ( + bare.summary, qualified.summary) + # ...and the guard both now claim is really emitted on both paths. + _run_trap(self._payload(base, "throw"), "main", [-5.0]) + _run_trap(self._payload(base, "Exn.throw"), "main", [-5.0]) + + def test_the_unguarded_disclosure_no_longer_names_the_throw_payload( + self, + ) -> None: + """E504's rationale listed the `Exn` `throw` payload among the sites + with no runtime guard. That is false since the guard landed, and it + contradicts the spec sentences this change amended — a reader taking + the diagnostic at its word would add a defensive check the compiler + already emits. Reached through the site that IS still unguarded, a + user-declared effect's operation argument, so the sentence is read + from a real diagnostic rather than from the source. + """ + result = _verify(""" +effect Counter { + op bump(Nat -> Unit); +} + +public fn main(@Unit -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Counter] { + bump(@Nat) -> { resume(()) } + } in { + Counter.bump(array_length(string_lines("a\\nb"))); + 0 + } +} +""") + binds = [o for o in result.obligations if o.kind == "nat_bind"] + assert [o.status for o in binds] == ["tier3_unguarded"], binds + rationales = [d.rationale for d in result.diagnostics + if d.error_code == "E504"] + assert len(rationales) == 1, [d.error_code for d in result.diagnostics] + assert "throw` payload ARE" in rationales[0], rationales[0] + assert "or an Exn `throw` payload)" not in rationales[0], rationales[0] + + +class TestAProvedContractSurvivesTheThrowPayload: + """The soundness differential: verify says PROVED, so run must agree. + + The clause parameter `@Nat` is not a claim the handler makes, it is one + the verifier hands every DOWNSTREAM consumer: `is_nonneg` discharges + `ensures(@Bool.result)` at Tier 1 purely from its parameter's type, with + no `requires` doing the work. So the payload reaching it decides whether + a Tier-1 proof is worth anything. Pre-fix it was not — the run reported + ``Postcondition violation in is_nonneg`` on a postcondition `vera verify` + had just proved, which is the signature the repo's verifier-probing rule + names. Everything between the throw and the consumer is deliberately + guard-free: the argument `@Nat.0` is already `@Nat`-typed, so no call-site + narrowing guard fires, and no `nat_to_int` coercion sits in the path. + """ + + _SRC = """ +private fn boom(@Unit -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(0 - 5) +} + +private fn is_nonneg(@Nat -> @Bool) + requires(true) + ensures(@Bool.result) + effects(pure) +{ + @Nat.0 >= 0 +} + +public fn main(@Unit -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@Nat) -> { if is_nonneg(@Nat.0) then { 1 } else { 0 } } + } in { + boom(()) + } +} +""" + + def test_the_consumers_postcondition_is_proved(self) -> None: + """Half one: `is_nonneg`'s `ensures` really is a Tier-1 proof, and it + rests on the parameter's `@Nat` invariant alone. Without this the + run below proves nothing — a postcondition the verifier had left at + Tier 3 would be entitled to fail.""" + result = _verify(self._SRC) + proved = [o for o in result.obligations + if o.kind == "ensures" and o.status == "verified" + and o.fn_name == "is_nonneg"] + assert [o.expr_text for o in proved] == ["@Bool.result"], [ + (o.fn_name, o.kind, o.status, o.expr_text) + for o in result.obligations + ] + + def test_the_proof_is_not_violated_at_run(self) -> None: + """Half two: the run. The program is refuted statically (E503 at the + throw), but `vera run` does not verify, so this is the unverified + path §2.6.5 calls defense in depth — it must stop at the boundary + that was violated, not carry -5 into a function that proved it could + not receive one. + + The assertion is on WHICH failure, not merely that one occurred: a + contract violation reaches the host through the same + ``WasmTrapError`` channel a guard trap does, so `_run_trap` alone is + satisfied by the pre-fix behaviour and would be green either way. + The pre-fix RED is a `Postcondition violation in is_nonneg` — the + proved postcondition failing — so that is what must be absent. + """ + result = _compile_ok(self._SRC) + with pytest.raises(WasmTrapError) as caught: + execute(result) + message = str(caught.value) + assert "Postcondition violation" not in message, message + assert "is_nonneg" not in message, message + + class TestABareOpArgumentIsObligatedByStructureNotByName: """The fallback is keyed on "this resolves to an effect operation". diff --git a/vera/README.md b/vera/README.md index 84e150d7..e2ee3374 100644 --- a/vera/README.md +++ b/vera/README.md @@ -152,7 +152,7 @@ execute(compile_result, ...) # → run WASM via wasmtime | `tester.py` | 1,285 | Test | Z3-guided input generation (parameter types resolved through `naming.py`; a TIER-3 target whose input constraints do not all translate is skipped naming the blocker rather than trialled, while a Tier-1-proved function is reported verified and never trialled at all), WASM execution, tier classification | `test()` | | `formatter.py` | 2,036 | Format | Canonical code formatter | `format_source()` | | `errors.py` | 813 | All | Diagnostic class, error hierarchy, error code registry | `Diagnostic`, `VeraError`, `ERROR_CODES` | -| `skip.py` | 241 | All | Codegen-internal control-flow exceptions behind structured skip diagnostics (#626) | `CodegenSkip`, `CodegenInvariantError` | +| `skip.py` | 242 | All | Codegen-internal control-flow exceptions behind structured skip diagnostics (#626) | `CodegenSkip`, `CodegenInvariantError` | | `introspect.py` | 127 | All | Payloads for `vera builtins` / `effects` / `errors --json` | `builtins_payload()`, `effects_payload()`, `errors_payload()` | | `envflags.py` | 35 | All | One truthiness rule for the `VERA_*` diagnostic flags catalogued in ENVIRONMENT.md; a leaf module (imports `os` only) so any layer can read a flag without a cycle | `flag_enabled()` | | `_since.py` | 376 | All | Best-effort `since` version attribution for built-ins, effects, abilities | | @@ -753,7 +753,7 @@ The `ERROR_CODES` dict in `errors.py` maps every code to a short description (16 ## Test Suite -Testing spans a **pytest suite** of 11,600 tests across 173 files — compiler-internals unit tests plus a **conformance suite** (241 programs in `tests/conformance/` validating every language feature against the spec) and **example programs** (42 end-to-end demos). The conformance suite is the definitive specification artifact — most programs target a single feature, though some (slot references, match, contracts) span several, and each serves as a minimal working example. +Testing spans a **pytest suite** of 11,623 tests across 173 files — compiler-internals unit tests plus a **conformance suite** (242 programs in `tests/conformance/` validating every language feature against the spec) and **example programs** (42 end-to-end demos). The conformance suite is the definitive specification artifact — most programs target a single feature, though some (slot references, match, contracts) span several, and each serves as a minimal working example. See **[TESTING.md](../TESTING.md)** for the comprehensive testing reference -- test file table, conformance suite details, compiler code coverage, language feature coverage, helper conventions, validation scripts, CI pipeline, and guidelines for adding tests. diff --git a/vera/cli.py b/vera/cli.py index 96968839..cf08bb5d 100644 --- a/vera/cli.py +++ b/vera/cli.py @@ -1328,9 +1328,11 @@ def cmd_run( ) # #516 Stage 3 (#547) — append the per-kind Fix paragraph # after the backtrace. Empty string for kinds that don't - # admit a generic suggestion (`contract_violation`, where - # the description already explains what failed; `unknown`, - # where by definition we don't know what to suggest), so + # admit a generic suggestion (`contract_violation` and + # `host_error`, where the description already explains what + # failed; `unknown`, where by definition we don't know what + # to suggest — the three empty entries in + # `_TRAP_FIX_PARAGRAPHS`), so # we suppress the block entirely in those cases — printing # an empty "Fix:" header would just be noise. # diff --git a/vera/codegen/closures.py b/vera/codegen/closures.py index dc908fe2..e3cea168 100644 --- a/vera/codegen/closures.py +++ b/vera/codegen/closures.py @@ -390,6 +390,14 @@ def _compile_lifted_closure( # int-literal → i32.const coercion inside closure bodies too. ctx.set_fn_byte_params(self._fn_byte_params) ctx.set_alias_env(self._alias_env) + # No `set_refinement_guard_emitter` here (#1268), deliberately: this + # context is built with no `effect_op_cells`, so a `throw` in a + # closure body reaches no cell and is not a write boundary the guard + # could key on — it does not compile at all today (`call target + # 'throw' not registered in this module`, a closure skip). Threading + # the op registries in is what would make the boundary real, and the + # emitter's absence then fails CLOSED at a loud skip rather than + # emitting an unguarded payload the verifier records as guarded. # #814/#774: a qualified call inside a closure body must resolve the # same way it does in a top-level body — to the module's function # (`mod$…` for a shadowed fn) and, for a shadowed imported generic, to diff --git a/vera/codegen/contracts.py b/vera/codegen/contracts.py index 59439dee..1c395343 100644 --- a/vera/codegen/contracts.py +++ b/vera/codegen/contracts.py @@ -85,12 +85,16 @@ def _refinement_guard_parts( # @Pos.0 < 10 }` where `Pos = { @Int | @Int.0 > 0 }`): the # outer guard would compile only the outer predicate and # silently DROP the inner `> 0` membership — a soundness - # hole that wrongly accepts `f(-1)`. The verifier already - # records such a narrowing as a Tier-3 E506 (its - # `_base_slot_name` returns None for a non-primitive base), - # so reject it loudly here at codegen (the "reject before - # codegen" choice) with a clean E618 — a non-zero-exit - # diagnostic, not a partial guard. Returns None after + # hole that wrongly accepts `f(-1)`. The verifier records + # such a narrowing Tier-3 and, since #1268, + # **UNGUARDED** — `_refined_boundary_codegen_guardable` bails + # on a refinement base for exactly this reason. It used to + # claim `guarded`, so `vera verify` exited 0 promising a + # runtime check for a program `vera compile` then refuses: + # a promise about a run that can never happen. Reject it + # loudly here at codegen (the "reject before codegen" + # choice) with a clean E618 — a non-zero-exit diagnostic, + # not a partial guard. Returns None after # recording the error so the helper stays total; the # recorded error fails the compile. This IS reachable. # `base` IS the inner `RefinementType` on this branch; the @@ -203,6 +207,50 @@ def _emit_refinement_check( "end", ] + def _emit_boundary_refinement_guard( + self, + ctx: WasmContext, + te: ast.TypeExpr, + value_local: int, + message_head: str, + env: WasmSlotEnv, + ) -> list[str] | None: + """The two guard halves — classify then lower — as ONE call, for a + boundary that reaches this layer from inside expression translation + (#1268). + + Every other §2.6.5 guard site is CodeGenerator code that already + holds both halves and calls them in sequence. A ``throw`` payload is + not: it is discovered mid-expression by :class:`WasmContext`, which + owns the representation decisions (which local, at what width) but + none of the lowering machinery — the string pool the trap message + interns into, the ``$vera.contract_fail`` import flag, the E617/E618 + diagnostics. So the context is handed this bound pair via + ``set_refinement_guard_emitter`` and supplies only the local. + + Returns ``None`` for an unrefined *te* and for the two refined shapes + :meth:`_refinement_guard_parts` emits no guard for — an erased + ``@Unit`` base, and a nested refinement (which it also reports as a + loud E618). The verifier's ``_refined_boundary_codegen_guardable`` + mirrors exactly that set, so a caller recording the obligation + ``guarded`` is making a claim this method keeps. The nested-refinement + half of that mirror is #1268's: it answered ``True`` there, so a + program `vera compile` refuses outright (E618) verified clean while + recording a Tier-3 runtime check that could never run. + + *message_head* is everything before the predicate in the trap text, + so the message reads in the same shape as every other boundary's + (``Refinement violation in \\n : failed``). + """ + parts = self._refinement_guard_parts(te) + if parts is None: + return None + predicate, base_name = parts + message = f"{message_head}: {ast.format_expr(predicate)} failed" + return self._emit_refinement_check( + ctx, predicate, base_name, value_local, message, env, + ) + def _resolve_type_alias(self, te: ast.TypeExpr) -> ast.TypeExpr: """Walk a ``type Foo = Bar`` alias chain to the underlying TypeExpr, applying each *generic* alias's type-argument substitution (cycle- diff --git a/vera/codegen/functions.py b/vera/codegen/functions.py index 06dbcd35..925bb30f 100644 --- a/vera/codegen/functions.py +++ b/vera/codegen/functions.py @@ -6,6 +6,7 @@ from __future__ import annotations +import functools from typing import TYPE_CHECKING, cast from vera import ast @@ -415,9 +416,13 @@ def _compile_fn( # width: `throw(5)` into `Exn<{ @Byte | … }>` put an # `i64.const` under an i32 tag and the module failed # WASM validation (#1269). + # The payload's TYPE EXPRESSION rides along (#1268): + # the throw call site guards a refined payload by + # lowering its predicate, which neither name carries. exn_cell = CellNames( family=self._family_name_te(eff.type_args[0]), base=self._family_base_te(eff.type_args[0]), + type_expr=eff.type_args[0], ) effect_ops["throw"] = ( f"$exn_{mangle_type_name(exn_cell.family)}", @@ -514,6 +519,14 @@ def _compile_fn( # return types. One value, so the alias bodies and their parameter # lists cannot be handed over half-updated (#1184 / #1208). ctx.set_alias_env(self._alias_env) + # #1268: the §2.6.5 predicate lowering, bound to THIS context, for + # the boundaries the context discovers mid-expression (a `throw` + # payload). Installed here rather than passed per call so the two + # halves of a guard — representation and lowering — cannot be paired + # with different contexts. + ctx.set_refinement_guard_emitter( + functools.partial(self._emit_boundary_refinement_guard, ctx), + ) ctx.set_closure_id_start(self._next_closure_id) ctx.set_closure_sigs(self._closure_sigs) # #814 §8.5.3: module-qualified call target table, so a ``m::f`` call diff --git a/vera/verifier.py b/vera/verifier.py index 83fa83fd..2af6f841 100644 --- a/vera/verifier.py +++ b/vera/verifier.py @@ -4389,20 +4389,29 @@ def _obligate_binding_triple( site: str, nat_guarded: bool, widen_guarded: bool, + refined_guarded: bool = False, ) -> None: """The refined-first / @Nat / widen binding-obligation triple for a single value flowing into a typed slot (#1203 — shared by the - handler state-init, state-update, get-resume, and State-put sites; - the older call-argument and constructor-field copies carry - site-specific side-table subtleties and stay inline). The refined - arm is ALWAYS unguarded: no handler boundary emits a - refined-predicate guard, so it discloses E506 honestly.""" + handler state-init, state-update, get-resume, State-put, and + `throw`-payload sites; the older call-argument and constructor-field + copies carry site-specific side-table subtleties and stay inline). + + *refined_guarded* defaults to False because most of those sites emit + sign guards only — a handler write boundary lowers no refinement + predicate, so its refined arm discloses E506 honestly. The `throw` + payload is the exception (#1268): codegen lowers the predicate there, + so that site passes True and the arm records a guarded Tier-3. The + flag is only ever an upper bound — `_check_refined_binding_obligation` + intersects it with `_refined_boundary_codegen_guardable`, so a shape + codegen emits no guard for stays unguarded whatever a caller claims. + """ refined = self._refined_binding_target(value, formal) if (refined is not None and self._narrows_into_refined(value, refined)): self._check_refined_binding_obligation( decl, value, refined, smt, slot_env, assumptions, - site=site, guarded=False, + site=site, guarded=refined_guarded, ) elif (self._nat_binding_target(value, formal) and self._narrows_into_nat(value)): @@ -4850,18 +4859,19 @@ def _walk_for_nat_binding_obligations( # keys the guard off the dispatch target's cell type), which # is why the test is the op's PARENT EFFECT and not its name # — a user effect's `put` is no more guarded than its - # `emit`. Everything else is the #754 unguarded class and - # discloses E504/E531: a user-effect op of any name (its - # handler does not compile today, E602), and `throw`, which - # lowers straight to `throw $exn_` with the payload - # on the stack and no guard anywhere on that path (measured - # by run, #1268). The refined branch is ALWAYS unguarded — - # no handler or throw boundary emits a refined-predicate - # guard (only sign-bit pairs) — so it discloses E506 - # honestly. A `resume` value is obligated from the - # HandleExpr arm instead, where the clause's effect identity - # is known; `resume` resolves to no operation here, so the - # two cannot both fire. + # `emit`. `Exn`'s `throw` joined `State` at the guarded end + # in #1268: its payload now takes the same sign guards at the + # op-call site plus the §2.6.5 predicate guard for a REFINED + # payload, so it is the one op whose refined arm is guarded + # too (`refined_guarded` below). Everything else is still + # the #754 unguarded class and discloses E504/E506/E531: a + # user-effect op of any name, whose dispatch carries only a + # target (`_effect_ops`) and so reaches no cell a guard could + # be keyed on. A `resume` value is obligated from the + # HandleExpr arm + # instead, where the clause's effect identity is known; + # `resume` resolves to no operation here, so the two cannot + # both fire. op = None for eff_name in reversed(self._walk_handled_effects): info = self.env.lookup_effect(eff_name) @@ -4871,14 +4881,20 @@ def _walk_for_nat_binding_obligations( if op is None: op = self.env.lookup_effect_op(expr.name) if op is not None: - op_guarded = op.parent_effect == "State" - op_site = ("State-op argument" if op_guarded + op_guarded = op.parent_effect in ("State", "Exn") + op_site = ("State-op argument" + if op.parent_effect == "State" else "effect-operation argument") for arg in expr.args: self._obligate_binding_triple( decl, arg, None, smt, slot_env, assumptions, site=op_site, nat_guarded=op_guarded, widen_guarded=op_guarded, + # #1268: only the `throw` payload boundary lowers + # a refinement predicate. The State write + # boundaries emit sign guards alone, so their + # refined arm stays honestly unguarded. + refined_guarded=op.parent_effect == "Exn", ) if param_types is not None: # A generic function whose `TypeVar` formal is fixed to @Nat @@ -5044,6 +5060,27 @@ def _walk_for_nat_binding_obligations( op = self.env.lookup_effect_op(expr.name, qualifier=expr.qualifier) param_types = getattr(op, "param_types", None) if param_types is not None: + # Guardedness is the BARE arm's rule, on the same key (#1268). + # The QUALIFIED spelling is the same operation at the same + # boundary — codegen's `State.put` / `Exn.throw` arms + # synthesize a bare node and delegate to the very dispatcher + # that emits the guards — so the two spellings must record + # identical statuses or the obligation stream says the + # boundary moved when only the syntax did. This arm still + # said "codegen does NOT yet guard effect-op arguments", + # stale since #1203 gave `State`'s write boundaries their + # guards and false again since #1268 gave `throw`'s payload + # the sign pair AND the predicate guard: `Exn.throw(v)` was + # guarded at run time and disclosed E504/E506 as if it were + # not. The qualifier NAMES the effect, so `op.parent_effect` + # is exactly the bare arm's resolved answer with no + # innermost-handler search needed. Everything else stays the + # #754 unguarded class (`IO.sleep`'s `@Nat` formal, a + # user-declared effect's op) and discloses honestly. + op_effect = getattr(op, "parent_effect", None) + op_guarded = op_effect in ("State", "Exn") + op_site = ("State-op argument" if op_effect == "State" + else "effect-operation argument") # A concretely-@Nat formal obligates directly (#552); a # generic (TypeVar) formal — `E.wait` instantiated as # `E` — is resolved via the checker's recorded @@ -5058,18 +5095,18 @@ def _walk_for_nat_binding_obligations( and self._narrows_into_refined(arg, refined_target)): self._check_refined_binding_obligation( decl, arg, refined_target, smt, slot_env, - assumptions, site="effect-operation argument", - guarded=False, + assumptions, site=op_site, + # Only the `throw` payload boundary lowers a + # refinement predicate (#1268); the State write + # boundaries emit sign guards alone, so their + # refined arm stays honestly unguarded. + guarded=op_effect == "Exn", ) elif (self._nat_binding_target(arg, formal) and self._narrows_into_nat(arg)): self._check_nat_binding_obligation( decl, arg, smt, slot_env, assumptions, - site="effect-operation argument", - # codegen does NOT yet guard effect-op arguments - # (#754), so an untranslatable narrowing here is - # unguarded regardless of formal concreteness. - guarded=False, + site=op_site, guarded=op_guarded, ) for arg in expr.args: self._walk_for_nat_binding_obligations( @@ -6421,12 +6458,13 @@ def _report_int_widen_unguarded( "Codegen runtime-guards every concrete @Int " "coercion site (return, let, call-argument, constructor field, " "tuple component, array element, heterogeneous arm, closure " - "argument/return/capture) but not this one — an " - "effect-operation argument (a user-declared effect's " - "operation, or an Exn `throw` payload), a tuple-destructure " - "component, or a generic-instantiated @Int field with no " - "per-field mono metadata — so here the widening is neither " - "statically proven nor runtime-checked." + "argument/return/capture) but not this one — a " + "USER-declared effect operation's argument (the built-in " + "`State` write boundaries and the `Exn` `throw` payload ARE " + "guarded), a tuple-destructure component, or a " + "generic-instantiated @Int field with no per-field mono " + "metadata — so here the widening is neither statically " + "proven nor runtime-checked." ), spec_ref='Chapter 11, Section 11.2.1 "Nat as i64"', error_code="E531", @@ -7756,11 +7794,11 @@ def _report_nat_binding_unguarded( "Codegen runtime-guards " "the concrete @Nat binding sites (let, destructure, match, " "sub-pattern, concrete field) and all call-arguments (generic " - "ones on the monomorphised callee) but not this one — an " - "effect-operation argument (a user-declared effect's " - "operation, or an Exn `throw` payload), or a " - "generic-instantiated constructor field with no per-field " - "mono metadata — so here " + "ones on the monomorphised callee) but not this one — a " + "USER-declared effect operation's argument (the built-in " + "`State` write boundaries and the `Exn` `throw` payload ARE " + "guarded), or a generic-instantiated constructor field with " + "no per-field mono metadata — so here " "the narrowing is neither statically proven nor " "runtime-checked." ), @@ -9096,11 +9134,13 @@ def _refined_boundary_codegen_guardable(ty: Type) -> bool: conditions (``vera/codegen/contracts.py``); KEEP IN SYNC (#1036). Codegen bails (emits NO guard) when (a) the base is the erased - ``@Unit`` (no local to check — the ``_is_unit_refinement`` case), or - (b) the base carries a NON-PLAIN type argument — a nested refinement - or fn type, e.g. ``Array<{ @Int | ... }>`` — whose binder slot name - cannot be spelt. A ``guarded=True`` Tier-3 for either was an - unfulfilled runtime-guard promise: an empty array flowed through a + ``@Unit`` (no local to check — the ``_is_unit_refinement`` case), + (b) the base is itself a REFINEMENT, which codegen refuses outright + rather than emitting a partial guard, or (c) the base carries a + NON-PLAIN type argument — a nested refinement or fn type, e.g. + ``Array<{ @Int | ... }>`` — whose binder slot name cannot be spelt. + A ``guarded=True`` Tier-3 for any of them was an unfulfilled + runtime-guard promise: an empty array flowed through a NonEmpty-refined closure boundary silently while the obligation stream claimed a runtime check (PR #1034 adversarial review). Plain named args (``Array``, nested ``Array>`` via the @@ -9112,6 +9152,18 @@ def _refined_boundary_codegen_guardable(ty: Type) -> bool: # erases exactly like bare `@Unit` (#841), so neither has a # local for the guard to check (PR #1034 full review). return False + if isinstance(ty.base, RefinedType): + # Refinement OVER a refinement (`type Tiny = { @Pos | @Pos.0 < 10 }` + # where `Pos = { @Int | @Int.0 > 0 }`). `_refinement_guard_parts` + # answers None for this shape — it records a loud E618 and emits + # nothing, because the outer guard alone would silently drop the + # inner membership predicate. This mirror claimed True anyway, so + # `vera verify` exited 0 recording a Tier-3 that "will be checked + # at run time" for a program `vera compile` then REFUSES: a + # promise about a runtime that cannot be reached at all. The + # honest answer is unguarded (the obligation discloses E506) and + # E618 still refuses at compile. + return False if isinstance(ty.base, AdtType): return all( isinstance(arg, (PrimitiveType, AdtType)) diff --git a/vera/wasm/calls.py b/vera/wasm/calls.py index 45dbd29b..78263610 100644 --- a/vera/wasm/calls.py +++ b/vera/wasm/calls.py @@ -537,8 +537,9 @@ def _translate_call( # not — while an int literal defaults to `i64.const`, so # `throw(5)` into `Exn<{ @Byte | … }>` emitted a module WASM # validation rejects at load. Same marking, same derivation, - # different op; the #1268 narrowing GUARD on this payload is a - # separate obligation and is deliberately NOT added here. + # different op — and, since #1268, the same narrowing GUARDS + # below: the payload is a write boundary in the full sense, not + # only in its width. is_exn_throw = ( call.name == "throw" and len(call.args) == 1 and cell is not None @@ -578,7 +579,27 @@ def _translate_call( if arg_instrs is None: return None instructions.extend(arg_instrs) - if is_state_put: + # The write boundary's guards. `throw` joined `put` here in + # #1268: its payload narrows into the `Exn` slot exactly as + # `put`'s argument narrows into the cell, but it crossed no + # function boundary, so none of §2.6.5's composing guards covered + # it — `throw(0 - 5)` into an `Exn` ran to completion and + # handed `-5` to a clause that had assumed non-negativity, and a + # `@Nat`-typed consumer's Tier-1-PROVED `ensures` then failed at + # run time. The three arms mirror the verifier's + # `_obligate_binding_triple` one-for-one, refined FIRST for the + # same reason it is: the refinement's own predicate carries the + # base's implicit range (`_refinement_guard_parts` conjoins it), + # so the sign guards would be redundant under it, and running + # them instead of it would check `>= 0` where the boundary + # invariant is `> 0`. + refined_payload: ast.TypeExpr | None = None + if is_exn_throw and cell is not None: + refined_payload = self._refined_exn_payload_type(cell, call) + if refined_payload is not None: + instructions = self._emit_exn_payload_refine_guard( + instructions, refined_payload, cell, call, env) + if refined_payload is None and (is_state_put or is_exn_throw): if base == "Nat" and self._narrows_into_nat(call.args[0]): instructions = self._emit_nat_bind_guard(instructions) elif base == "Int" and self._result_is_nat(call.args[0]): diff --git a/vera/wasm/calls_handlers.py b/vera/wasm/calls_handlers.py index bfc0a5b5..32fd47bb 100644 --- a/vera/wasm/calls_handlers.py +++ b/vera/wasm/calls_handlers.py @@ -7,11 +7,11 @@ from __future__ import annotations -from typing import ClassVar +from typing import Callable, ClassVar from dataclasses import fields, is_dataclass -from vera import ast +from vera import ast, naming from vera.monomorphize import mangle_type_name from vera.slots import effect_op_result_names, type_expr_slot_name from vera.skip import STATE_CLAUSE_INLINE_DEPTH_CAP, CodegenSkip @@ -67,6 +67,10 @@ class CallsHandlersMixin: _pushed_cell_families: list[str] _addressable_from: int _clause_inline_depth: int + _refinement_guard_emitter: ( + Callable[[ast.TypeExpr, int, str, WasmSlotEnv], list[str] | None] + | None + ) # ----------------------------------------------------------------- # Ability operation dispatch: show and hash (§9.8) @@ -2215,6 +2219,113 @@ def total_count(node: object) -> int: return (self._tail_resume_arg(body) is not None and total_count(body) == 1) + def _refined_exn_payload_type( + self, cell: CellNames, call: ast.FnCall, + ) -> ast.TypeExpr | None: + """This ``throw``'s payload type when it is a REFINEMENT, else None + (#1268). + + The refined-first branch selector at the throw write boundary, asked + BEFORE a guard local is allocated so an unrefined payload's WAT stays + byte-identical to the pre-#1268 output. It is the same + :mod:`vera.naming` derivation :meth:`_refinement_guard_parts` + resolves through — the naming layer answers "is this a refinement, + and over what binder", and codegen layers its representation + decisions on top — so this cannot select an arm the emitter then + disagrees with. + + A ``throw`` cell with no payload type expression fails CLOSED. Both + producers (the declared-effect row in ``codegen/functions.py`` and + the ``handle[Exn]`` body below) thread it; a producer that forgot + to would otherwise emit no guard silently while the verifier went on + recording one — the exact false-``guarded`` claim this issue was. + """ + if cell.type_expr is None: + raise CodegenSkip( # pragma: no cover — defensive + call, + f"Exn<{cell.family}> payload carries no type expression, so " + "its refinement predicate cannot be guarded at the throw", + ) + if naming.refinement_binder_parts( + cell.type_expr, self._alias_env) is None: + return None + return cell.type_expr + + def _emit_exn_payload_refine_guard( + self, value: list[str], payload_te: ast.TypeExpr, cell: CellNames, + call: ast.FnCall, env: WasmSlotEnv, + ) -> list[str]: + """Wrap *value* with the §2.6.5 predicate guard for a refined + ``Exn`` payload (#1268) — the refined twin of the sign guards + ``_emit_nat_bind_guard`` / ``_emit_int_widen_guard`` give the + unrefined payload at the same call site. + + ``throw(v)`` narrows *v* into the payload slot exactly as a call + argument narrows into a refined formal, but the payload crosses no + function boundary, so none of §2.6.5's composing boundary guards + covers it: pre-fix, ``throw(0 - 5)`` into an ``Exn<{ @Int | @Int.0 > + 0 }>`` ran to completion and handed ``-5`` to a clause that had + assumed the predicate. This is that boundary's own guard — save the + value, test the predicate over it, push it back — so it traps through + the same ``$vera.contract_fail`` channel a refined parameter does. + + Emitted UNGATED for every refined payload, matching the closure + return guard rather than the sign guards' narrowing test: a value + already typed at the refinement satisfies its own predicate, so the + guard costs a dead check at worst, while a missing one is a false + ``guarded`` claim in the obligation stream. The verifier's mirror is + ``_refined_boundary_codegen_guardable``, which downgrades exactly the + shapes the emitter answers ``None`` for (an erased ``@Unit`` base, a + nested refinement), so obligation and guard stay in lock-step. + + Called only with the *payload_te* :meth:`_refined_exn_payload_type` + returned, which is the same expression ``cell.type_expr`` holds. + """ + emitter = self._refinement_guard_emitter + if emitter is None: + raise CodegenSkip( # pragma: no cover — defensive + call, + "no refinement-guard emitter is installed on this " + f"translation context, so the refined Exn<{cell.family}> " + "payload cannot be guarded at the throw", + ) + # The payload's SOURCE spelling, not `cell.family`: a refined + # family renders its own predicate (#1218), so naming the cell that + # way printed the predicate twice in one two-line message, once as + # the "type" and again as the thing that failed. + head = ( + f"Refinement violation in " + f"throw({ast.format_type_expr(payload_te)})\n" + " payload" + ) + if self._is_pair_type_name(cell.base): + # A `String`-based payload is (ptr, len) in two CONSECUTIVE + # locals, checked over the ptr — the same shape the lifted + # closure's i32_pair return guard uses. + ptr_local = self.alloc_local("i32") + len_local = self.alloc_local("i32") + guard = emitter(payload_te, ptr_local, head, env) + if guard is None: + return value + return [ + *value, + f"local.set {len_local}", + f"local.set {ptr_local}", + *guard, + f"local.get {ptr_local}", + f"local.get {len_local}", + ] + value_local = self.alloc_local(self._type_name_to_wasm(cell.base)) + guard = emitter(payload_te, value_local, head, env) + if guard is None: + return value + return [ + *value, + f"local.set {value_local}", + *guard, + f"local.get {value_local}", + ] + def _translate_handle_exn( self, expr: ast.HandleExpr, env: WasmSlotEnv, ) -> list[str] | None: @@ -2324,7 +2435,12 @@ def _translate_handle_exn( self._effect_ops = {**saved_ops, "throw": (tag_name, False)} self._effect_op_cells = { **saved_cells, - "throw": CellNames(family=family, base=family_base), + # `type_arg` rides along for the same reason the two names do + # (#1268): a `throw` in this body guards a refined payload by + # lowering the predicate, which only the type expression carries. + "throw": CellNames( + family=family, base=family_base, type_expr=type_arg, + ), } # Compile body diff --git a/vera/wasm/context.py b/vera/wasm/context.py index 47278e53..2355ba26 100644 --- a/vera/wasm/context.py +++ b/vera/wasm/context.py @@ -428,6 +428,26 @@ def __init__( # swapped independently and fall out of step (the #1184 mispairing). # Seeded empty; codegen calls `set_alias_env` before translation. self._alias_env: AliasEnv = EMPTY_ALIAS_ENV + # #1268: lower a refinement predicate to a boundary guard over a + # value already in a local. Injected by codegen via + # `set_refinement_guard_emitter`, because the two halves of a §2.6.5 + # guard live on opposite sides of this seam: the REPRESENTATION half + # — which local, at what width, in what order relative to the value + # on the stack — is this context's, while lowering it needs the string + # pool's trap message, the `$vera.contract_fail` import flag and the + # E617/E618 diagnostics, all of which are the generator's. The same + # injection shape as `set_adt_eq_derivable`. `None` until installed: + # a context translating a `throw` without it FAILS CLOSED (a loud + # skip), never silently unguarded — see + # `_emit_exn_payload_refine_guard`. A lifted-closure context is + # deliberately left at `None`: it carries no `effect_op_cells`, so no + # `throw` there is a write boundary this could guard (it does not + # compile at all today), and the closed failure is what a future + # thread-through would meet rather than a silently unguarded payload. + self._refinement_guard_emitter: ( + Callable[[ast.TypeExpr, int, str, WasmSlotEnv], list[str] | None] + | None + ) = None # Closure signature registry: sig_key -> (type_name, param/result WAT) self._closure_sigs: dict[str, str] = {} # Flags for resource requirements detected during translation @@ -629,6 +649,26 @@ def set_alias_env(self, env: AliasEnv) -> None: """ self._alias_env = env + def set_refinement_guard_emitter( + self, + emitter: Callable[ + [ast.TypeExpr, int, str, WasmSlotEnv], list[str] | None + ], + ) -> None: + """Install the §2.6.5 refinement-predicate guard lowering (#1268). + + *emitter* takes ``(type_expr, value_local, message, env)`` and returns + the WAT that traps via ``$vera.contract_fail`` when the value in + *value_local* violates *type_expr*'s predicate — or ``None`` when the + type is unrefined, or refined over a base codegen emits no guard for + (an erased ``@Unit``, a nested refinement). Codegen binds it to + ``CodeGenerator._emit_boundary_refinement_guard`` for THIS context, so + the trap message interns into the shared string pool and the + contract-fail import flag is raised on the generator that assembles + the module. + """ + self._refinement_guard_emitter = emitter + def set_closure_id_start(self, start: int) -> None: """Set the starting closure ID for this context.""" self._next_closure_id = start diff --git a/vera/wasm/helpers.py b/vera/wasm/helpers.py index a924269d..1e81b372 100644 --- a/vera/wasm/helpers.py +++ b/vera/wasm/helpers.py @@ -98,10 +98,21 @@ class CellNames: the family — the seam #1233's round-5 review found re-mangling an already-mangled name at — and it is gone: one canonical family is threaded to both consumers. + + *type_expr* is the cell type as it was WRITTEN, carried for the one + question neither name can answer: a refined cell's PREDICATE (#1268). + ``family`` renders it and ``base`` strips it, but the #1268 payload guard + has to LOWER it, so the guard reads the type expression its producer + already held rather than parsing a predicate back out of a mangled family + — the second-derivation trap #1218/#1233 closed everywhere else. Excluded + from equality (``compare=False``): a cell's identity is its family, and a + ``TypeExpr`` carries source spans, so comparing it would make two cells of + one family differ by where each was written. """ family: str base: str + type_expr: ast.TypeExpr | None = field(default=None, compare=False) # ===================================================================== From 059e7158b500cdb09a604c177bb13b8fb053dbff Mon Sep 17 00:00:00 2001 From: Alasdair Allan Date: Fri, 14 Aug 2026 18:34:25 +0100 Subject: [PATCH 2/4] Close the review's third finding, and refute its second MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The qualified effect-op arm obligated two of its three arms. PR #1325's review found `_walk_for_nat_binding_obligations`' `QualifiedCall` branch hand-written as a refined-then-`@Nat` chain with no `@Nat` -> `@Int` widening case at all, so `State.put(@Nat.0)` and `Exn.throw(@Nat.0)` into an `@Int` cell recorded NO obligation whatever — while codegen emitted the widening guard on both spellings, the qualified forms synthesizing a bare node and delegating to the dispatcher that emits it. A guard the obligation stream never mentions is the mirror image of the claim without a guard this issue started from: `verify --json` is the only place a reader sees either. Measured as a differential before the fix — bare `nat_to_int_coerce tier3` against qualified `[]`, for both ops — with the guard confirmed present in the emitted WAT of all four. The arm now routes through the shared `_obligate_binding_triple`, so the three arms cannot drift apart again by omission, and the two spellings of one operation record identical statuses. The TESTING.md pytest-invocation comment kept #1318's `1,190` while the union updated 238 to 239 entries; collection says 1,195, which is what the oracle-gated row three hundred lines above already said. The review's rooting finding is REFUTED, and the evidence is recorded in the report rather than in code: its premise is that the pair payload pointer "exists only in `ptr_local`", which the emitted WAT contradicts — a parameter payload is shadow-pushed by the GC prologue before any body code, and a freshly allocated one is pushed at its own `$alloc` site, with `$gc_sp` lowered only in the function epilogue, after the throw. The collector is mark-sweep and never moves objects, so a second root of the same pointer value would add nothing: rooting decides liveness, and liveness is already established by the producer's push. Co-Authored-By: Claude --- CHANGELOG.md | 2 +- TESTING.md | 2 +- tests/test_exn_throw_payload_1268.py | 108 +++++++++++++++++++++++++++ vera/verifier.py | 53 +++++++------ 4 files changed, 139 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 322a7157..39848d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Two imports supplying one bare name are refused, in every namespace** ([#1304](https://github.com/aallan/vera/issues/1304)). Spec §8.5 ordered a local declaration against an import (§8.5.2) and gave the module-qualified form for reaching what a clash hides (§8.5.3), but defined no order between two *imports* that both supply one name. Neither did the implementation, and the gap was reachable: a module importing two dependencies that each export `forall fn gen` — one returning `@Int`, one `@Bool` — bound its bare call to whichever supplier a set of module paths happened to yield first, so one unchanged file was `vera check`-green on one run and `[E121] body has type Bool` on the next. Measured at the branch point across eight consecutive runs and eight hash seeds: accepted on seeds 0, 2 and 3, rejected on 1, 4, 5, 6 and 7, with the winner tracking module-name hash order rather than which import is written first. Codegen's E608 rail caught the *entry-visible* pair before it could matter there; the flap lived in the shapes the rail only reached at compile, from inside a module the entry program merely imports. Spec §8.5.2.2 now states the rule — a program **MUST NOT** leave a namespace with two imports supplying one bare function name — and the checker enforces it as **E155**, a check-phase code for a scope question that had been enforced by a codegen rail at the wrong layer. Refusing is what removes the flap rather than merely labelling it: with no pick to make, there is no iteration order left to expose, which a deterministic first-wins order would not have achieved (it would make the resolved declaration implicit in import sequence, §0.2.2, and let a library *adding* an export silently rebind a downstream bare call). The refusal is **definition-gated**, matching the rail it generalises: it fires because the import pair exists, not because a body names it, so an entry program importing two suppliers and never calling either is refused exactly as E608 already refused it, and rewriting a bare call in module-qualified form does not lift it. Two shapes clear it, both exercised through to their runtime value: a **local declaration** of the name (§8.5.2 — every bare call is then the local one, and each import stays reachable through `dep::name(...)`), or a **selective import** narrowing the other module's list. The ambiguity predicate is the one `namespace_fn_names` already derived for #1281 and #1299, now exposed per namespace as well as unioned, so the layer that refuses early and the layer that backstops it cannot disagree about which shape is ambiguous; the E608 condition keeps its cell, driven through a door that bypasses the checker. An ambiguous name is not injected into the type environment at all — reporting the clash while binding one supplier would leave the follow-on diagnostics keyed to whichever module the injection loop reached first, which is the nondeterminism the refusal exists to remove — so a bare call to it misses with an ordinary `E200` instead — an E-coded diagnostic emitted at **warning** severity, which the `--json` envelope reports in `warnings` rather than `diagnostics` and which does not fail the check on its own (measured: a program whose only diagnostic is `E200` reports `ok: true` and exits 0). The W-series is the separate `W001`/`W002` code namespace, and this is not one of them. **The data namespaces flapped the same way and are folded in.** Spec §8.5.4 gives constructor names the same shadowing rules as function names, which a function-only refusal would have made false: two modules each exporting a `public data Shape` with different constructor field types type-checked on some hash seeds and reported `[E213]` on others (accepted on seeds 2, 8, 9, 10 and 11; rejected on 0, 1, 3, 4, 5, 6 and 7), and the accepting seeds were the worse half — `check` **and** `verify` both passed, and the program died at `run` with an `E609` located at line 0 of the entry file, naming two modules the entry never imported. Data types are now **E156** and constructors **E157**, one code per declaration namespace exactly as codegen splits E608/E609/E610, and reported independently because they come apart: two modules exporting differently-named types that share a constructor name clash on the constructor alone. Their remedy differs from the function one and says so — E609/E610 refuse two modules' same-named data declarations by DECLARATION, consulting neither visibility nor the importer's filter nor local shadowing (the relaxation E608 received in [#1281](https://github.com/aallan/vera/issues/1281) has no data-side twin), so narrowing an import or shadowing the name locally leaves the program `E609` at compile. Both were measured against the fixture and both fail — as does marking one declaration `private` — so the two diagnostics prescribe renaming, and a cell pins that measurement so the fix text cannot drift into offering remedies that do not work. That rail over-breadth is now tracked as [#1317](https://github.com/aallan/vera/issues/1317). **A name the built-in registry already owns is not a clash** — the injection loops are `setdefault` over a `TypeEnv` the built-ins populate first, so a dependency exporting its own `option_map` never wins the bare name (measured as `E201` against the *prelude's* two-argument signature). The first cut of this refusal did not pass the built-in snapshot and reported two such dependencies as a clash, which was a new rejection rather than an earlier one; `namespace_fn_names`' claim that its ambiguity half is identical with or without the prelude argument was wrong for the same reason and is corrected, with the codegen call ordering it depends on now pinned by a cell. -- **A `throw` payload is runtime-guarded, not only obligated** ([#1268](https://github.com/aallan/vera/issues/1268)). `throw(v)` narrows `v` into the `Exn` payload, and since the static half of this issue the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist and an unverified `vera compile`/`run` delivered the violating value anyway. `throw(0 - 5)` under `effects(>)` ran to completion and returned **-5** through the `@Nat` payload; the refined spelling (`type Pos = { @Int | @Int.0 > 0 }`) did the same. Worse than a wrong answer: a handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke — a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a **postcondition violation at run time on a postcondition `vera verify` had proved**. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s ([#1203](https://github.com/aallan/vera/issues/1203)): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined FIRST, as at every other narrowing site — the §2.6.5 predicate guard for a refined payload, which traps through `$vera.contract_fail` naming the predicate that failed (`Refinement violation in throw(@Pos) / payload: @Int.0 > 0 failed`). The three arms mirror the verifier's own obligation triple one-for-one, so the obligation stream and the emitted guards stay in lock-step: the payload obligation is now `guarded` at all three arms and its Tier-3 leg is counted in `tier3_runtime` rather than disclosed as `tier3_unguarded`, and the refined arm's `guarded` claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. That mirror needed one repair to be true: it answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud `E618` because the outer predicate alone would silently drop the inner membership — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen. It now bails on a refinement base, and the obligation discloses `tier3_unguarded` while `E618` still refuses. The same audit found the **qualified spelling recording something different from the bare one**: the `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since [#1203](https://github.com/aallan/vera/issues/1203), so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed `E504`/`E506` for a boundary that traps, and `State.put(v)` had been doing the same since #1203. Both now take the bare arm's rule on the same key (`op.parent_effect`), so the two spellings of one operation record identical statuses; a user-declared effect's operation and `IO.sleep`'s `@Nat` formal stay the honest [#754](https://github.com/aallan/vera/issues/754) unguarded class. Two diagnostic rationales (`E504`, `E531`) that listed the `throw` payload among the unguarded sites — false once the guard landed, and contradicting the spec sentences this change amends — no longer do. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it, so `CellNames` now carries the type expression its producer already held rather than parsing one back out of a mangled family name. The predicate lowering itself is injected into the translation context (`set_refinement_guard_emitter`), because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. An unrefined payload's WAT is byte-identical to before: a differential over all 278 pre-existing corpus programs — every `examples/` and `tests/conformance/` program, compiled and verified on both trees — moves nothing, in emitted WAT or in the obligation and diagnostic streams. +- **A `throw` payload is runtime-guarded, not only obligated** ([#1268](https://github.com/aallan/vera/issues/1268)). `throw(v)` narrows `v` into the `Exn` payload, and since the static half of this issue the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist and an unverified `vera compile`/`run` delivered the violating value anyway. `throw(0 - 5)` under `effects(>)` ran to completion and returned **-5** through the `@Nat` payload; the refined spelling (`type Pos = { @Int | @Int.0 > 0 }`) did the same. Worse than a wrong answer: a handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke — a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a **postcondition violation at run time on a postcondition `vera verify` had proved**. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s ([#1203](https://github.com/aallan/vera/issues/1203)): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined FIRST, as at every other narrowing site — the §2.6.5 predicate guard for a refined payload, which traps through `$vera.contract_fail` naming the predicate that failed (`Refinement violation in throw(@Pos) / payload: @Int.0 > 0 failed`). The three arms mirror the verifier's own obligation triple one-for-one, so the obligation stream and the emitted guards stay in lock-step: the payload obligation is now `guarded` at all three arms and its Tier-3 leg is counted in `tier3_runtime` rather than disclosed as `tier3_unguarded`, and the refined arm's `guarded` claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. That mirror needed one repair to be true: it answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud `E618` because the outer predicate alone would silently drop the inner membership — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen. It now bails on a refinement base, and the obligation discloses `tier3_unguarded` while `E618` still refuses. The same audit found the **qualified spelling recording something different from the bare one**: the `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since [#1203](https://github.com/aallan/vera/issues/1203), so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed `E504`/`E506` for a boundary that traps, and `State.put(v)` had been doing the same since #1203. Both now take the bare arm's rule on the same key (`op.parent_effect`), so the two spellings of one operation record identical statuses. The review of that fix found the arm had been hand-written as a refined-then-`@Nat` chain with **no widening branch at all**, so `State.put(@Nat.0)` / `Exn.throw(@Nat.0)` into an `@Int` cell recorded no obligation whatever while codegen emitted the `@Nat` -> `@Int` widening guard on both spellings — a guard the obligation stream never mentioned, the mirror image of the claim-without-a-guard this issue started from. It now routes through the shared `_obligate_binding_triple`, so the three arms cannot drift apart again by omission; a user-declared effect's operation and `IO.sleep`'s `@Nat` formal stay the honest [#754](https://github.com/aallan/vera/issues/754) unguarded class. Two diagnostic rationales (`E504`, `E531`) that listed the `throw` payload among the unguarded sites — false once the guard landed, and contradicting the spec sentences this change amends — no longer do. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it, so `CellNames` now carries the type expression its producer already held rather than parsing one back out of a mangled family name. The predicate lowering itself is injected into the translation context (`set_refinement_guard_emitter`), because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. An unrefined payload's WAT is byte-identical to before: a differential over all 278 pre-existing corpus programs — every `examples/` and `tests/conformance/` program, compiled and verified on both trees — moves nothing, in emitted WAT or in the obligation and diagnostic streams. - **`json_parse` accepts one domain, and both runtimes accept it** ([#1306](https://github.com/aallan/vera/issues/1306)). The reference host parsed with `json.loads`, whose default `parse_constant` admits `NaN`, `Infinity` and `-Infinity`; the browser gated with `JSON.parse`, which refuses them as RFC 8259 requires. So the two hosts disagreed about *which call* rejects a non-finite value: the browser at the parse with a handled `Err`, the reference host at `json_stringify` — and there as a raw Python traceback rather than a Vera error ([#1302](https://github.com/aallan/vera/issues/1302) below). Spec §9.7.1 now states the accepted domain instead of leaving each host to inherit its parser's: RFC 8259-valid text that decodes to finite numbers and strings of Unicode scalar values, everything else `Err` at the parse with the same message on every runtime. **A non-finite number has two entry routes and the domain closes both.** The constants are one; the other is a syntactically valid number that overflows — `1e999`, `-1e999`, `[1e999]`, `{"a":1e309}` — which both host parsers accept, decoding to an infinite `JNumber` that then died at `json_stringify`, the same divergent-refusal-point defect one syntax over. RFC 8259 §6 sets no limit on a number's range and says an implementation may set one; Vera's is the finite `Float64` values, which is exactly what `json_stringify` can write back. **The refusal covers the integer spelling too**, and that half was reference-host-only: `json.loads` returns a Python `int` for a digit string with no fraction and no exponent, so `1` followed by 309 zeros never met a float range check — and then had to become an f64 at the WASM boundary, where `float()` raises. It died with `int too large to convert to float` where `JSON.parse`, which has no int/float split and sees an `Infinity` either way, returned the shared sentence. The integer bound is the double **rounding** boundary (`2**1024 - 2**970`) rather than `sys.float_info.max`, and compared in integer arithmetic: an integer *larger* than the largest finite double still rounds to it and both hosts accept it, so the obvious bound would have traded this divergence for its mirror image, and a bound implemented as `float(value)` would be the very overflow it is looking for. Underflow is not the same question and is not refused: `1e-999` decodes to `0`, finite and in the domain, pinned as a control beside `1e308` and the largest representable double so a refusal cannot generalise from "unrepresentable magnitude" to "large". Both value-level exclusions — overflow and lone surrogate — are found by ONE document-order walk returning the sentence itself, so "whichever comes first names the refusal" is the rule rather than a precedence table the two hosts could implement differently. The domain is the parse-side counterpart of the canonical output form [#1293](https://github.com/aallan/vera/issues/1293) pinned — a non-finite number has no JSON representation in either direction — and with no entry route through `json_parse`, the output-side refusal is now reachable only from a `JNumber` a program *constructed* from `nan()` or `infinity()`. The reference host's `parse_constant` hook **records rather than raises**, and the refusal is decided after the parse completes. Raising on sight would have made it answer a different question from the browser's: Python's scanner calls the hook the moment it sees the token, so `[Infinity_x]` — malformed for a reason that has nothing to do with the constant — would have reported the non-finite sentence natively while the browser reported a syntax error. Recording and continuing asks what the browser asks, by substituting `0` for each bare constant and re-parsing: *would this text be valid JSON if the constants were admitted?* Only then is the constant the whole story, and only then do both hosts say the same sentence; text malformed for any other reason keeps its host-native syntax message, as every syntax error always has. The browser's scan also only considers a token where a *value* may begin — the start of the text, or after `[`, `,` or `:`. Without that it found `NaN` at offset 1 of `-NaN`, substituted, re-parsed `-0` successfully and reported the shared sentence, where the reference host's parser never reaches the token at all and gives a syntax error. `-NaN`, `[-NaN]`, `+Infinity`, `infinity`, `nan`, `NaNx` and `-Infinityx` are all pinned as host-native on both hosts. The parity battery pins all four probe inputs from the issue's table plus the container and multi-constant shapes, compares the whole `Err` message across hosts rather than which arm was taken, and runs beside controls the refusal must not disturb — `"NaN"` as an ordinary string value among them. diff --git a/TESTING.md b/TESTING.md index b87f8849..9aff98d5 100644 --- a/TESTING.md +++ b/TESTING.md @@ -72,7 +72,7 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_naming_env_provenance_1208.py` | 45 | 2,097 | The other half of the #1208 contract: every consumer is handed the ENVIRONMENT the checker rendered under, not just the same renderer. Four provenance seams, each pinned by the adversarial probe that exhibited it — an IMPORTED callee's contract rendered in its DEFINING module's alias namespace (a violated precondition that vanished, and its mirror, a correct call spuriously rejected, both under a `Cnt` that names different bodies on the two sides), an imported GENERIC monomorphized and verified in that same namespace (a lying postcondition that proved clean, plus a verifier↔codegen clone differential over the recounted slot references — the desync is invisible to a unit test on either side), a `forall` variable shadowing a same-named module alias wherever a generic signature renders (the mono clone, a body `let`, the verifier's collapsed premises, and the exported uninstantiated template), and the tester's `SmtContext` holding the narrowed scope its own names were keyed in — the last one behavioural, since a generator handed the wrong scope collapses two parameters onto one variable and returns NO inputs at all. Three of the seams are also crossed against a SECOND component rather than checked for internal consistency, because a wrong-but-consistent scope is invisible from inside one: the verifier's declared parameter names against `slots.slot_table`'s, its `where`-helper scope against `slots.fn_scopes`' accumulation, and the monomorphizer's post-substitution names against what the consumers rebuild on the clone — each independent on the axis under test (which variables the two sides narrow by) but sharing `fn_slot_scope`/`slot_name` below it, so the hand-derived literal rendering beside every comparison is what a shared-renderer defect cannot satisfy. An imported generic nested under a non-generic function is pinned too — both its discovery-time recount and its verification-time clone must run in the DEFINING module's namespace. Three more seams arrived from the PR #1224 review, each with the false Tier-1 or miscompile that exhibited it: an UNPINNED callee (an imported generic's own `where`-helper, which the origin registry never pins) rendering in the module under verification rather than the entry program, whose absence let a violated precondition discharge as true and trap at run time; a callee's refined-RETURN predicate translated in the callee's namespace alongside its `requires`/`ensures`, pinned by provenance because today's bare-headed binder masks it behaviourally; and codegen's declaration-index space keyed PER NAMESPACE, without which a module's stamp turned the main file's forward alias reference into a backward one and a check-clean, verify-clean program read the wrong parameter through valid WASM. Two seams from the #1213 burndown close the same shape from the other side: the prelude's own aliases are injected only into the reserved `Vera` namespace, so no name a program can spell resolves on the codegen side alone (#1221 — the differential compares the checker's and codegen's partition of one signature, with the emitted WAT beside it), and an imported ADT is ordered at the index its OWN module gave it rather than the built-in floor (#1227), each with the control that differs by exactly the namespace under test. Controls carried alongside: the same programs with the alias renamed or the shadowing declaration removed, and the runtime oracle that shows the new E500/E501 agrees with the emitted code rather than merely reporting more | | `test_callee_contract_scope_1220_1225_1226.py` | 40 | 1,433 | A callee's contract is READ in the callee's own module. Three burndown defects, each asserted against the runtime oracle wherever the two directions of a wrong namespace (an obligation that vanishes, one that fires for no reason) look identical from inside the verifier: an E501's `Precondition:` line quoted from the file that DECLARED the clause (#1220 — in the misattribution direction too, both files carrying a plausible `requires` on the same line number, plus the imported-generic `where`-helper whose clause sits past the end of a short importer and used to quote nothing at all); a bare-name call inside an IMPORTED callee's contract resolved through the CALLEE's module registry (#1225 — the false Tier-1 whose run traps and its mirror spurious E501, through the `requires` and the `ensures` path, against a no-collision control); and the refined-RETURN binder derived through the naming layer, so a refinement over a PARAMETERISED base pushes the key its own predicate looks itself up under (#1226 — single-module and cross-module, the second proving the derivation happens INSIDE the callee scope); plus the PR #1239 review round — a module's pinned registry holds what its OWN file imports (a DEPTH-2 chain, the shape the single-level corpus could not exhibit: requires and ensures directions, bare-vs-qualified tier agreement, a name outside the middle module's import filter still missing per §8.5.1, and the mirror gate that filling the registry does not re-export), and every part of a diagnostic follows the declaring module (location, file name and excerpt, including a clause past a shorter importer's end, with a multi-line clause quoted whole), and the characterization of the binder-reference walk's one documented exception — a CLOSURE inside a predicate owns the first `@T.n` in traversal order, whose consequence is a Tier-3 demotion rather than a fact assumed about the wrong term; and the mini-review round — an obligation carries the FILE its line number belongs to, so the documented `(file, line, column)` join between the two `--json` arrays holds for a module-located obligation, with the entry-file control and warm==cold parity, and a multi-line clause is quoted with its `--` comments blanked (a `--` inside a string literal surviving, which a naive split would corrupt) | | `test_alias_application_refinement_base_1237.py` | 11 | 398 | A parameterised alias APPLICATION substitutes its arguments in the verifier's own resolver (#1237). `type Box = T;` applied as `@Box` resolved to `AdtType('T')` — the alias's binder leaking as an ADT name — so a refinement over it failed the modelled-primitive gate, the refined-return fact was dropped, and a valid program was rejected with a spurious E501 while `vera run` returned the right answer. Both halves of the fix are asserted separately: the alias body registers its own parameters as type variables (`substitute` maps type variables, so an ADT-named binder is unsubstitutable however the application side is written) and the application substitutes. Plus depth (an argument that is itself an application, and an alias whose body applies another alias), the bounded direction (a consumer wanting `>= 100` where the refinement grants `>= 18` is still rejected, and the runtime agrees), and the gate that must NOT move — an unmodelled `@Byte` base resolves correctly and still degrades to a Tier-3 runtime check, with a consumer of its predicate still refused | -| `test_exn_throw_payload_1268.py` | 32 | 848 | `throw`'s payload is obligated AND runtime-guarded like every other narrowing site (#1268). `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with ZERO obligations while `vera run` returned -5 out of the `@Nat` payload — check-green, verify-green, silently wrong — because `throw` is a bare call with no function-registry entry, so the argument loop never saw it and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. All three arms are asserted (the `@Nat` refutation, the refined refutation over a modelled base, and the `@Nat`->`@Int` widening obligation appearing where none existed), plus the #1251(b) concrete gate reaching the payload for free (`throw(200)` into an `Exn<{ @Byte \| @Byte.0 < 10 }>` names the value; the satisfying twin proves at Tier 1), the user-effect contrast that localized the bug (a declared op's argument was loud for the same value — both are loud now, at the same site name), and the refined-alias payload spelling. The GUARD is checked against codegen rather than asserted: an undischargeable payload must land on the runtime-guarded `tier3` leg, counted in the totals, and a run must confirm the payload really is stopped — the two together go red whichever side moves without the other (delete the emission and the runs go red; flip the flag back and the statuses do). Both arms are run at the boundary: a `@Nat` payload traps on -5 and delivers 5, a refined `{ @Int \| @Int.0 > 0 }` payload traps through the `$vera.contract_fail` channel on BOTH -5 and 0 — the value that clears the base's `>= 0` and violates the predicate, so a sign guard standing in for the predicate guard fails here — and delivers 7. The type gate carries its own over-refusal control: an `Exn` payload has no invariant to violate, so a negative one is a correct program and must still return -5. Both REPRESENTATIONS are covered — a scalar payload in one local, and a `@String`-based one whose (ptr, len) pair has to be saved in two, checked over the ptr and put back in the right order (the satisfying twin is what shows the order). The **soundness differential** is the point of the guard rather than a property of it: the clause parameter's type is what the verifier hands every downstream consumer, so a consumer discharging `ensures(@Bool.result)` at Tier 1 from its `@Nat` parameter alone is asserted PROVED and then run — pre-fix the run reported a postcondition violation on that proved postcondition, with nothing else in the path (the argument is already `@Nat`-typed, so no call-site narrowing guard fires). A dischargeable twin proves the site is not merely always-loud, and the six `Exn` conformance programs are verified whole as canaries so obligating a position that had none names itself instead of arriving as one line of a corpus sweep. The adversarial round adds the three places the `guarded` PROMISE and the emitted guard could disagree in a direction no value oracle can see, because the program either never runs or runs identically either way: a refinement OVER a refinement is asserted `tier3_unguarded` AND `E618`-refused at compile in one cell (the mirror claimed a runtime check for a program that cannot be compiled at all — either half alone reads as consistent); the bare and qualified spellings of one operation are asserted to record IDENTICAL statuses as a differential rather than two literals, on both arms, with the run confirming which value is the true one (`Exn.throw` was disclosed unguarded while codegen delegated it to the guard-emitting dispatcher, and `State.put` had been since #1203); and `E504`'s rationale is read from a real diagnostic — reached through the site that IS still unguarded, a user-declared effect's operation argument — to pin that it no longer lists the `throw` payload among the sites with no runtime guard | +| `test_exn_throw_payload_1268.py` | 38 | 956 | `throw`'s payload is obligated AND runtime-guarded like every other narrowing site (#1268). `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with ZERO obligations while `vera run` returned -5 out of the `@Nat` payload — check-green, verify-green, silently wrong — because `throw` is a bare call with no function-registry entry, so the argument loop never saw it and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. All three arms are asserted (the `@Nat` refutation, the refined refutation over a modelled base, and the `@Nat`->`@Int` widening obligation appearing where none existed), plus the #1251(b) concrete gate reaching the payload for free (`throw(200)` into an `Exn<{ @Byte \| @Byte.0 < 10 }>` names the value; the satisfying twin proves at Tier 1), the user-effect contrast that localized the bug (a declared op's argument was loud for the same value — both are loud now, at the same site name), and the refined-alias payload spelling. The GUARD is checked against codegen rather than asserted: an undischargeable payload must land on the runtime-guarded `tier3` leg, counted in the totals, and a run must confirm the payload really is stopped — the two together go red whichever side moves without the other (delete the emission and the runs go red; flip the flag back and the statuses do). Both arms are run at the boundary: a `@Nat` payload traps on -5 and delivers 5, a refined `{ @Int \| @Int.0 > 0 }` payload traps through the `$vera.contract_fail` channel on BOTH -5 and 0 — the value that clears the base's `>= 0` and violates the predicate, so a sign guard standing in for the predicate guard fails here — and delivers 7. The type gate carries its own over-refusal control: an `Exn` payload has no invariant to violate, so a negative one is a correct program and must still return -5. Both REPRESENTATIONS are covered — a scalar payload in one local, and a `@String`-based one whose (ptr, len) pair has to be saved in two, checked over the ptr and put back in the right order (the satisfying twin is what shows the order). The **soundness differential** is the point of the guard rather than a property of it: the clause parameter's type is what the verifier hands every downstream consumer, so a consumer discharging `ensures(@Bool.result)` at Tier 1 from its `@Nat` parameter alone is asserted PROVED and then run — pre-fix the run reported a postcondition violation on that proved postcondition, with nothing else in the path (the argument is already `@Nat`-typed, so no call-site narrowing guard fires). A dischargeable twin proves the site is not merely always-loud, and the six `Exn` conformance programs are verified whole as canaries so obligating a position that had none names itself instead of arriving as one line of a corpus sweep. The adversarial round adds the three places the `guarded` PROMISE and the emitted guard could disagree in a direction no value oracle can see, because the program either never runs or runs identically either way: a refinement OVER a refinement is asserted `tier3_unguarded` AND `E618`-refused at compile in one cell (the mirror claimed a runtime check for a program that cannot be compiled at all — either half alone reads as consistent); the bare and qualified spellings of one operation are asserted to record IDENTICAL statuses as a differential rather than two literals, on both arms, with the run confirming which value is the true one (`Exn.throw` was disclosed unguarded while codegen delegated it to the guard-emitting dispatcher, and `State.put` had been since #1203); and `E504`'s rationale is read from a real diagnostic — reached through the site that IS still unguarded, a user-declared effect's operation argument — to pin that it no longer lists the `throw` payload among the sites with no runtime guard | | `test_refinement_binder_convergence_1208.py` | 16 | 459 | Codegen's refinement boundary guard and `vera/naming.py` derive the predicate binder ONCE (#1208). A per-shape differential over a direct refinement, an alias hop, `@Nat`- and `@Byte`-based refinements (both range-conjoining), a composite base whose binder is a RESOLVED argument list, and two non-refinements that must be `None` on both sides — plus the property the convergence exists to hold, that the guard's binder equals the key a predicate's own `@Base.n` resolves to. A per-shape differential is green either way while two copies agree — which is exactly how a duplicated derivation drifts unnoticed — so the load-bearing assertion is a MUTATION: perturb `naming.refinement_binder_parts` and codegen's guard must report the perturbed binder. Codegen's two layered WASM decisions are pinned alongside — the loud E618 for a nested refinement base, reported ONCE per declaration however many call sites consult the derivation and however many clones a generic is instantiated into (two genuinely distinct sites still report twice), and no guard at all for an erased one (parametrized over `@Unit` and `Future`, the corner that erases identically but is not spelled `Unit`) — plus runtime traps proving BOTH the `@Byte` and the `@Nat` range conjunctions reach the emitted check, each pinned by trap `kind` as well as by the conjunct its message names. The mutation perturbs the predicate as well as the binder name: the two travel by different routes, and the range conjunction lives on the predicate. The once-per-site dedup is keyed on a resolved location, so a cross-module pin holds up the premise that a location carries its owning file: two imported library modules of identical shape, declaring their nested refinement at the same line and column, must produce two diagnostics attributed to two files — each quoting its own module's declaration, which is also what catches an attribution pointing past the importer's last line | | `test_checker_effects.py` | 90 | 1,455 | Effect declarations, abilities, effect subtyping, async effect, handler typing (#420 split), and the #1149 built-in-effect redeclaration gate (E152: divergent and faithful `effect IO`, codegen-only `Exn`, a registry-parametrised sweep, and a differential pinning the gate's name set to what `vera effects --json` publishes) | | `test_state_exn_registration.py` | 30 | 1296 | #1210 — State/Exn host-import registration covers the whole handler, not just its body. Four shapes, one per sub-expression position the walk used to miss (a nested handler in a clause body, in the state-init expression, in a clause's `with` update, and an `Exn` handler in a clause body), each check-green and verify-clean and therefore required to COMPILE — pre-fix every one died at whole-module WAT compilation with `unknown func` / `unknown tag`; plus the `i32_pair` cell (`handle[State]` in a `pure` function) that the walk skipped in silence, now the same loud E607 the declared-effect gate emits. The **registration-completeness differential** is the cross-component invariant itself: over every `examples/` + `tests/conformance/` program that compiles, every `state_*` / `exn_*` symbol the emitted WAT REFERENCES must have a matching import or tag DECLARATION — a desync between the registration pass and the lowering pass is invisible to a unit test on either. Round two adds the Exn twin of the silent skip (`handle[Exn]` in a `pure` function — the walk called the shared tag registration and discarded the verdict, so it compiled where the declared-row spelling was a clean E612) and the four CONTRACT positions, which are lowered code: a handler in a `requires`, an `ensures`, an `assert`, or a `decreases` measure. The differential gained a **validation leg** — every HANDLER-BEARING module is handed to `wasmtime.Module` through the exceptions-enabled engine `execute()` uses, because a symbol declared at the WRONG TYPE passes the name comparison while being invalid WASM, and 10 of the 30 handler-bearing modules fail to load with `wasm_exceptions` off — a supported wasmtime configuration, though the current runner defaults it on. The conformance suite's deliberate negatives are filtered out of the sweep: they never reach codegen through `vera check`. Carries floors on programs swept, modules validated, summed symbol references and globally distinct symbols, plus three can-go-red tests: the State and Exn extractions each stripped of their declaration lines, and a planted retyped import that only the validation leg catches. Round five adds the three positions no corpus program contained — a destructuring `let`'s value (which also disarmed the E612 gate), a module call's ARGUMENTS, and a signature refinement predicate reached through the alias table — and a cross-module shape test for the module-call leg. Round seven adds the boundary-guard routes that enumeration missed (a tuple parameter's components, a tuple return's, and a closure's refined formal and return) and the co-extensiveness half those shapes cannot show: a refined tuple behind a CLOSURE formal must declare nothing, since the closure path emits no component guards, and the nested-refinement (E618) and erased-base bails must stay silent registrars too. Plus the cycle guard on the closure signature leg, asserted in both directions — the walk terminates, and with the guard neutered the same walk blows the recursion limit | diff --git a/tests/test_exn_throw_payload_1268.py b/tests/test_exn_throw_payload_1268.py index c845c2dc..3a34b6fd 100644 --- a/tests/test_exn_throw_payload_1268.py +++ b/tests/test_exn_throw_payload_1268.py @@ -32,6 +32,8 @@ """ from __future__ import annotations +import re + import pytest from vera.codegen import execute @@ -42,6 +44,7 @@ _run, _run_refine_trap, _run_trap, + wat_fn_body, ) from tests.verifier_helpers import _verify, _verify_err @@ -634,6 +637,111 @@ def test_the_unguarded_disclosure_no_longer_names_the_throw_payload( assert "or an Exn `throw` payload)" not in rationales[0], rationales[0] +class TestTheQualifiedArmObligatesAllThreeArms: + """The qualified arm records the WIDENING too, not just two of three. + + PR #1325 review. The `QualifiedCall` arm was hand-written as a + refined-then-@Nat chain, and simply had no `@Nat` -> `@Int` widening + branch — so `State.put(@Nat.0)` / `Exn.throw(@Nat.0)` into an `@Int` + cell recorded NO obligation at all, while codegen emitted the widening + guard on both spellings (the qualified forms synthesize a bare node and + delegate to the dispatcher that emits it). A guard the obligation + stream never mentions is the same disease as a guard it claims and does + not emit: `verify --json` is the only place a reader can see either. + + Asserted as a differential over the two spellings of each op rather + than as literals, because the two spellings ARE one boundary. + """ + + def _widen(self, op: str, spelling: str) -> str: + """A `@Nat` argument widening into an `@Int` cell / payload.""" + if op == "throw": + return f""" +private fn boom(@Nat -> @Int) + requires(true) + ensures(true) + effects(>) +{{ + {spelling}(@Nat.0) +}} + +public fn main(@Nat -> @Int) + requires(true) + ensures(true) + effects(pure) +{{ + handle[Exn] {{ + throw(@Int) -> {{ @Int.0 }} + }} in {{ + boom(@Nat.0) + }} +}} +""" + return f""" +private fn store(@Nat -> @Unit) + requires(true) + ensures(true) + effects(>) +{{ + {spelling}(@Nat.0) +}} + +public fn main(@Nat -> @Int) + requires(true) + ensures(true) + effects(pure) +{{ + handle[State](@Int = 1) {{ + get(@Unit) -> {{ resume(@Int.0) }}, + put(@Int) -> {{ resume(()) }} + }} in {{ + store(@Nat.0); + 0 + }} +}} +""" + + @pytest.mark.parametrize( + ("op", "bare", "qualified"), + [("throw", "throw", "Exn.throw"), ("put", "put", "State.put")], + ) + def test_both_spellings_record_the_widening( + self, op: str, bare: str, qualified: str, + ) -> None: + """One `nat_to_int_coerce` on each side, at the same status.""" + def coerce(src: str) -> list[tuple[str, str]]: + return [(o.kind, o.status) for o in _verify(src).obligations + if o.kind == "nat_to_int_coerce"] + + b = coerce(self._widen(op, bare)) + q = coerce(self._widen(op, qualified)) + assert b == q, (b, q) + assert b == [("nat_to_int_coerce", "tier3")], b + + @pytest.mark.parametrize( + ("op", "spelling"), + [("throw", "throw"), ("throw", "Exn.throw"), + ("put", "put"), ("put", "State.put")], + ) + def test_the_guard_the_obligation_promises_is_emitted( + self, op: str, spelling: str, + ) -> None: + """...and codegen really does emit it, on BOTH spellings. + + The obligation above says `tier3` — runtime-guarded — so this is the + half that makes that a fact rather than a claim. Without it the + differential is satisfied by two sides agreeing on a promise neither + keeps. + """ + result = _compile_ok(self._widen(op, spelling)) + body = wat_fn_body(result.wat, "boom" if op == "throw" else "store") + guard = re.compile( + r"local\.tee \d+\s+i64\.const 0\s+i64\.lt_s\s+if\s+unreachable\s+end", + re.S, + ) + assert guard.search(body), body + + class TestAProvedContractSurvivesTheThrowPayload: """The soundness differential: verify says PROVED, so run must agree. diff --git a/vera/verifier.py b/vera/verifier.py index 2af6f841..fe5609e9 100644 --- a/vera/verifier.py +++ b/vera/verifier.py @@ -5081,33 +5081,38 @@ def _walk_for_nat_binding_obligations( op_guarded = op_effect in ("State", "Exn") op_site = ("State-op argument" if op_effect == "State" else "effect-operation argument") - # A concretely-@Nat formal obligates directly (#552); a - # generic (TypeVar) formal — `E.wait` instantiated as + # The SHARED triple, not a local copy of two of its three + # arms. A concretely-@Nat formal obligates directly (#552); + # a generic (TypeVar) formal — `E.wait` instantiated as # `E` — is resolved via the checker's recorded # instantiated target (`_nat_binding_target`, #747), as for - # generic constructor fields. + # generic constructor fields; and a refined formal obligates + # its predicate refined-FIRST (#746), the side-table + # recovering a generic formal instantiated to a RefinedType. + # + # The @Nat -> @Int WIDENING arm is the reason this is a + # delegation rather than an inline chain (PR #1325 review). + # The hand-written version carried refined + nat and simply + # omitted widen, so `State.put(@Nat.0)` / `Exn.throw(@Nat.0)` + # into an `@Int` cell recorded NO obligation at all while + # codegen emitted the widening guard on both spellings (the + # qualified forms synthesize a bare node and delegate to the + # dispatcher that emits it) — a guard the obligation stream + # never mentioned, which is the same obligation-versus-guard + # gap one boundary over that this issue's fix round closed. + # Routing through `_obligate_binding_triple` means the three + # arms cannot drift apart again by omission. for arg, formal in zip(expr.args, param_types): - # #746: a refined effect-op formal obligates the argument - # against its predicate (refined-first); the side-table also - # recovers a generic formal instantiated to a RefinedType. - refined_target = self._refined_binding_target(arg, formal) - if (refined_target is not None - and self._narrows_into_refined(arg, refined_target)): - self._check_refined_binding_obligation( - decl, arg, refined_target, smt, slot_env, - assumptions, site=op_site, - # Only the `throw` payload boundary lowers a - # refinement predicate (#1268); the State write - # boundaries emit sign guards alone, so their - # refined arm stays honestly unguarded. - guarded=op_effect == "Exn", - ) - elif (self._nat_binding_target(arg, formal) - and self._narrows_into_nat(arg)): - self._check_nat_binding_obligation( - decl, arg, smt, slot_env, assumptions, - site=op_site, guarded=op_guarded, - ) + self._obligate_binding_triple( + decl, arg, formal, smt, slot_env, assumptions, + site=op_site, + nat_guarded=op_guarded, widen_guarded=op_guarded, + # Only the `throw` payload boundary lowers a + # refinement predicate (#1268); the State write + # boundaries emit sign guards alone, so their + # refined arm stays honestly unguarded. + refined_guarded=op_effect == "Exn", + ) for arg in expr.args: self._walk_for_nat_binding_obligations( decl, arg, smt, slot_env, assumptions, From 189a25339eae991e8776635f72bb397e0cedc83d Mon Sep 17 00:00:00 2001 From: Alasdair Allan Date: Fri, 14 Aug 2026 19:30:17 +0100 Subject: [PATCH 3/4] A refinement over @Int no longer disables the widening guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review's fourth finding is refuted as stated and real underneath it. Its claim was a verifier-versus-codegen desync: that the elif chain in `_obligate_binding_triple` records only `refine_bind` for a refined-over- `@Int` formal fed an intrinsically-`@Nat` value, while the emitted payload path carries both the refinement guard and the widening guard. The second half is false — measured on `Exn<{ @Int | true }>` fed a `@Nat`, codegen emits the refinement guard and NO sign guard, because the payload lowering mirrors the verifier's chain deliberately: the sign arms are gated on the refined arm not having fired. Obligation and guard agree, one for one. What the measurement did find is worse in one respect: both sides agreed to skip a check the UNREFINED spelling performs. A refinement predicate does not imply fit-in-i64 — `{ @Int | true }` is satisfied by the negative a `@Nat` above i64.MAX reinterprets to, exactly as #820 says `< 100` is — so adding a refinement WEAKENED the boundary. `Exn` fed u64.MAX trapped on the widening guard; `Exn<{ @Int | true }>` fed the same value returned -1. The #820 intersection now holds at these boundaries too: the widening obligation rides alongside the refined one in the shared triple rather than being skipped by the chain, and codegen emits the widening guard beside the predicate guard. Not a double-record — the arms below stay `elif`, so a value reaching the refined branch reaches neither of them, and the two obligations are different kinds describing different facts about one value. The State siblings inherit it through the same helper, where the widening guard was already being emitted, so their disclosure stops understating what the module does. Both spellings now trap at u64.MAX and both deliver an in-range value untouched. Co-Authored-By: Claude --- CHANGELOG.md | 2 +- FAQ.md | 2 +- README.md | 2 +- ROADMAP.md | 2 +- TESTING.md | 4 +- docs/llms-full.txt | 2 +- tests/test_exn_throw_payload_1268.py | 93 ++++++++++++++++++++++++++++ vera/README.md | 2 +- vera/verifier.py | 19 ++++++ vera/wasm/calls.py | 11 ++++ 10 files changed, 131 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39848d47..cad0aa12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Two imports supplying one bare name are refused, in every namespace** ([#1304](https://github.com/aallan/vera/issues/1304)). Spec §8.5 ordered a local declaration against an import (§8.5.2) and gave the module-qualified form for reaching what a clash hides (§8.5.3), but defined no order between two *imports* that both supply one name. Neither did the implementation, and the gap was reachable: a module importing two dependencies that each export `forall fn gen` — one returning `@Int`, one `@Bool` — bound its bare call to whichever supplier a set of module paths happened to yield first, so one unchanged file was `vera check`-green on one run and `[E121] body has type Bool` on the next. Measured at the branch point across eight consecutive runs and eight hash seeds: accepted on seeds 0, 2 and 3, rejected on 1, 4, 5, 6 and 7, with the winner tracking module-name hash order rather than which import is written first. Codegen's E608 rail caught the *entry-visible* pair before it could matter there; the flap lived in the shapes the rail only reached at compile, from inside a module the entry program merely imports. Spec §8.5.2.2 now states the rule — a program **MUST NOT** leave a namespace with two imports supplying one bare function name — and the checker enforces it as **E155**, a check-phase code for a scope question that had been enforced by a codegen rail at the wrong layer. Refusing is what removes the flap rather than merely labelling it: with no pick to make, there is no iteration order left to expose, which a deterministic first-wins order would not have achieved (it would make the resolved declaration implicit in import sequence, §0.2.2, and let a library *adding* an export silently rebind a downstream bare call). The refusal is **definition-gated**, matching the rail it generalises: it fires because the import pair exists, not because a body names it, so an entry program importing two suppliers and never calling either is refused exactly as E608 already refused it, and rewriting a bare call in module-qualified form does not lift it. Two shapes clear it, both exercised through to their runtime value: a **local declaration** of the name (§8.5.2 — every bare call is then the local one, and each import stays reachable through `dep::name(...)`), or a **selective import** narrowing the other module's list. The ambiguity predicate is the one `namespace_fn_names` already derived for #1281 and #1299, now exposed per namespace as well as unioned, so the layer that refuses early and the layer that backstops it cannot disagree about which shape is ambiguous; the E608 condition keeps its cell, driven through a door that bypasses the checker. An ambiguous name is not injected into the type environment at all — reporting the clash while binding one supplier would leave the follow-on diagnostics keyed to whichever module the injection loop reached first, which is the nondeterminism the refusal exists to remove — so a bare call to it misses with an ordinary `E200` instead — an E-coded diagnostic emitted at **warning** severity, which the `--json` envelope reports in `warnings` rather than `diagnostics` and which does not fail the check on its own (measured: a program whose only diagnostic is `E200` reports `ok: true` and exits 0). The W-series is the separate `W001`/`W002` code namespace, and this is not one of them. **The data namespaces flapped the same way and are folded in.** Spec §8.5.4 gives constructor names the same shadowing rules as function names, which a function-only refusal would have made false: two modules each exporting a `public data Shape` with different constructor field types type-checked on some hash seeds and reported `[E213]` on others (accepted on seeds 2, 8, 9, 10 and 11; rejected on 0, 1, 3, 4, 5, 6 and 7), and the accepting seeds were the worse half — `check` **and** `verify` both passed, and the program died at `run` with an `E609` located at line 0 of the entry file, naming two modules the entry never imported. Data types are now **E156** and constructors **E157**, one code per declaration namespace exactly as codegen splits E608/E609/E610, and reported independently because they come apart: two modules exporting differently-named types that share a constructor name clash on the constructor alone. Their remedy differs from the function one and says so — E609/E610 refuse two modules' same-named data declarations by DECLARATION, consulting neither visibility nor the importer's filter nor local shadowing (the relaxation E608 received in [#1281](https://github.com/aallan/vera/issues/1281) has no data-side twin), so narrowing an import or shadowing the name locally leaves the program `E609` at compile. Both were measured against the fixture and both fail — as does marking one declaration `private` — so the two diagnostics prescribe renaming, and a cell pins that measurement so the fix text cannot drift into offering remedies that do not work. That rail over-breadth is now tracked as [#1317](https://github.com/aallan/vera/issues/1317). **A name the built-in registry already owns is not a clash** — the injection loops are `setdefault` over a `TypeEnv` the built-ins populate first, so a dependency exporting its own `option_map` never wins the bare name (measured as `E201` against the *prelude's* two-argument signature). The first cut of this refusal did not pass the built-in snapshot and reported two such dependencies as a clash, which was a new rejection rather than an earlier one; `namespace_fn_names`' claim that its ambiguity half is identical with or without the prelude argument was wrong for the same reason and is corrected, with the codegen call ordering it depends on now pinned by a cell. -- **A `throw` payload is runtime-guarded, not only obligated** ([#1268](https://github.com/aallan/vera/issues/1268)). `throw(v)` narrows `v` into the `Exn` payload, and since the static half of this issue the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist and an unverified `vera compile`/`run` delivered the violating value anyway. `throw(0 - 5)` under `effects(>)` ran to completion and returned **-5** through the `@Nat` payload; the refined spelling (`type Pos = { @Int | @Int.0 > 0 }`) did the same. Worse than a wrong answer: a handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke — a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a **postcondition violation at run time on a postcondition `vera verify` had proved**. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s ([#1203](https://github.com/aallan/vera/issues/1203)): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined FIRST, as at every other narrowing site — the §2.6.5 predicate guard for a refined payload, which traps through `$vera.contract_fail` naming the predicate that failed (`Refinement violation in throw(@Pos) / payload: @Int.0 > 0 failed`). The three arms mirror the verifier's own obligation triple one-for-one, so the obligation stream and the emitted guards stay in lock-step: the payload obligation is now `guarded` at all three arms and its Tier-3 leg is counted in `tier3_runtime` rather than disclosed as `tier3_unguarded`, and the refined arm's `guarded` claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. That mirror needed one repair to be true: it answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud `E618` because the outer predicate alone would silently drop the inner membership — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen. It now bails on a refinement base, and the obligation discloses `tier3_unguarded` while `E618` still refuses. The same audit found the **qualified spelling recording something different from the bare one**: the `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since [#1203](https://github.com/aallan/vera/issues/1203), so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed `E504`/`E506` for a boundary that traps, and `State.put(v)` had been doing the same since #1203. Both now take the bare arm's rule on the same key (`op.parent_effect`), so the two spellings of one operation record identical statuses. The review of that fix found the arm had been hand-written as a refined-then-`@Nat` chain with **no widening branch at all**, so `State.put(@Nat.0)` / `Exn.throw(@Nat.0)` into an `@Int` cell recorded no obligation whatever while codegen emitted the `@Nat` -> `@Int` widening guard on both spellings — a guard the obligation stream never mentioned, the mirror image of the claim-without-a-guard this issue started from. It now routes through the shared `_obligate_binding_triple`, so the three arms cannot drift apart again by omission; a user-declared effect's operation and `IO.sleep`'s `@Nat` formal stay the honest [#754](https://github.com/aallan/vera/issues/754) unguarded class. Two diagnostic rationales (`E504`, `E531`) that listed the `throw` payload among the unguarded sites — false once the guard landed, and contradicting the spec sentences this change amends — no longer do. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it, so `CellNames` now carries the type expression its producer already held rather than parsing one back out of a mangled family name. The predicate lowering itself is injected into the translation context (`set_refinement_guard_emitter`), because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. An unrefined payload's WAT is byte-identical to before: a differential over all 278 pre-existing corpus programs — every `examples/` and `tests/conformance/` program, compiled and verified on both trees — moves nothing, in emitted WAT or in the obligation and diagnostic streams. +- **A `throw` payload is runtime-guarded, not only obligated** ([#1268](https://github.com/aallan/vera/issues/1268)). `throw(v)` narrows `v` into the `Exn` payload, and since the static half of this issue the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist and an unverified `vera compile`/`run` delivered the violating value anyway. `throw(0 - 5)` under `effects(>)` ran to completion and returned **-5** through the `@Nat` payload; the refined spelling (`type Pos = { @Int | @Int.0 > 0 }`) did the same. Worse than a wrong answer: a handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke — a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a **postcondition violation at run time on a postcondition `vera verify` had proved**. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s ([#1203](https://github.com/aallan/vera/issues/1203)): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined FIRST, as at every other narrowing site — the §2.6.5 predicate guard for a refined payload, which traps through `$vera.contract_fail` naming the predicate that failed (`Refinement violation in throw(@Pos) / payload: @Int.0 > 0 failed`). The three arms mirror the verifier's own obligation triple one-for-one, so the obligation stream and the emitted guards stay in lock-step: the payload obligation is now `guarded` at all three arms and its Tier-3 leg is counted in `tier3_runtime` rather than disclosed as `tier3_unguarded`, and the refined arm's `guarded` claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. That mirror needed one repair to be true: it answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud `E618` because the outer predicate alone would silently drop the inner membership — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen. It now bails on a refinement base, and the obligation discloses `tier3_unguarded` while `E618` still refuses. The same audit found the **qualified spelling recording something different from the bare one**: the `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since [#1203](https://github.com/aallan/vera/issues/1203), so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed `E504`/`E506` for a boundary that traps, and `State.put(v)` had been doing the same since #1203. Both now take the bare arm's rule on the same key (`op.parent_effect`), so the two spellings of one operation record identical statuses. The review of that fix found the arm had been hand-written as a refined-then-`@Nat` chain with **no widening branch at all**, so `State.put(@Nat.0)` / `Exn.throw(@Nat.0)` into an `@Int` cell recorded no obligation whatever while codegen emitted the `@Nat` -> `@Int` widening guard on both spellings — a guard the obligation stream never mentioned, the mirror image of the claim-without-a-guard this issue started from. It now routes through the shared `_obligate_binding_triple`, so the three arms cannot drift apart again by omission. The triple itself then turned out to be missing the [#820](https://github.com/aallan/vera/issues/820) INTERSECTION at these boundaries: its three arms are an `elif` chain, so a refinement OVER `@Int` claimed the value and the widening check never ran — and codegen mirrored that exactly, so both sides agreed to skip a check the UNREFINED spelling performs. A refinement predicate does not imply fit-in-i64, and `{ @Int | true }` is satisfied by the negative a `@Nat` above i64.MAX reinterprets to, so adding a refinement WEAKENED the boundary: `Exn` fed u64.MAX trapped on the widening guard while `Exn<{ @Int | true }>` fed the same value returned **-1**. The widening obligation and its guard now ride beside the refined pair rather than being replaced by it, and the two spellings trap alike; a user-declared effect's operation and `IO.sleep`'s `@Nat` formal stay the honest [#754](https://github.com/aallan/vera/issues/754) unguarded class. Two diagnostic rationales (`E504`, `E531`) that listed the `throw` payload among the unguarded sites — false once the guard landed, and contradicting the spec sentences this change amends — no longer do. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it, so `CellNames` now carries the type expression its producer already held rather than parsing one back out of a mangled family name. The predicate lowering itself is injected into the translation context (`set_refinement_guard_emitter`), because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. An unrefined payload's WAT is byte-identical to before: a differential over all 278 pre-existing corpus programs — every `examples/` and `tests/conformance/` program, compiled and verified on both trees — moves nothing, in emitted WAT or in the obligation and diagnostic streams. - **`json_parse` accepts one domain, and both runtimes accept it** ([#1306](https://github.com/aallan/vera/issues/1306)). The reference host parsed with `json.loads`, whose default `parse_constant` admits `NaN`, `Infinity` and `-Infinity`; the browser gated with `JSON.parse`, which refuses them as RFC 8259 requires. So the two hosts disagreed about *which call* rejects a non-finite value: the browser at the parse with a handled `Err`, the reference host at `json_stringify` — and there as a raw Python traceback rather than a Vera error ([#1302](https://github.com/aallan/vera/issues/1302) below). Spec §9.7.1 now states the accepted domain instead of leaving each host to inherit its parser's: RFC 8259-valid text that decodes to finite numbers and strings of Unicode scalar values, everything else `Err` at the parse with the same message on every runtime. **A non-finite number has two entry routes and the domain closes both.** The constants are one; the other is a syntactically valid number that overflows — `1e999`, `-1e999`, `[1e999]`, `{"a":1e309}` — which both host parsers accept, decoding to an infinite `JNumber` that then died at `json_stringify`, the same divergent-refusal-point defect one syntax over. RFC 8259 §6 sets no limit on a number's range and says an implementation may set one; Vera's is the finite `Float64` values, which is exactly what `json_stringify` can write back. **The refusal covers the integer spelling too**, and that half was reference-host-only: `json.loads` returns a Python `int` for a digit string with no fraction and no exponent, so `1` followed by 309 zeros never met a float range check — and then had to become an f64 at the WASM boundary, where `float()` raises. It died with `int too large to convert to float` where `JSON.parse`, which has no int/float split and sees an `Infinity` either way, returned the shared sentence. The integer bound is the double **rounding** boundary (`2**1024 - 2**970`) rather than `sys.float_info.max`, and compared in integer arithmetic: an integer *larger* than the largest finite double still rounds to it and both hosts accept it, so the obvious bound would have traded this divergence for its mirror image, and a bound implemented as `float(value)` would be the very overflow it is looking for. Underflow is not the same question and is not refused: `1e-999` decodes to `0`, finite and in the domain, pinned as a control beside `1e308` and the largest representable double so a refusal cannot generalise from "unrepresentable magnitude" to "large". Both value-level exclusions — overflow and lone surrogate — are found by ONE document-order walk returning the sentence itself, so "whichever comes first names the refusal" is the rule rather than a precedence table the two hosts could implement differently. The domain is the parse-side counterpart of the canonical output form [#1293](https://github.com/aallan/vera/issues/1293) pinned — a non-finite number has no JSON representation in either direction — and with no entry route through `json_parse`, the output-side refusal is now reachable only from a `JNumber` a program *constructed* from `nan()` or `infinity()`. The reference host's `parse_constant` hook **records rather than raises**, and the refusal is decided after the parse completes. Raising on sight would have made it answer a different question from the browser's: Python's scanner calls the hook the moment it sees the token, so `[Infinity_x]` — malformed for a reason that has nothing to do with the constant — would have reported the non-finite sentence natively while the browser reported a syntax error. Recording and continuing asks what the browser asks, by substituting `0` for each bare constant and re-parsing: *would this text be valid JSON if the constants were admitted?* Only then is the constant the whole story, and only then do both hosts say the same sentence; text malformed for any other reason keeps its host-native syntax message, as every syntax error always has. The browser's scan also only considers a token where a *value* may begin — the start of the text, or after `[`, `,` or `:`. Without that it found `NaN` at offset 1 of `-NaN`, substituted, re-parsed `-0` successfully and reported the shared sentence, where the reference host's parser never reaches the token at all and gives a syntax error. `-NaN`, `[-NaN]`, `+Infinity`, `infinity`, `nan`, `NaNx` and `-Infinityx` are all pinned as host-native on both hosts. The parity battery pins all four probe inputs from the issue's table plus the container and multi-constant shapes, compares the whole `Err` message across hosts rather than which arm was taken, and runs beside controls the refusal must not disturb — `"NaN"` as an ordinary string value among them. diff --git a/FAQ.md b/FAQ.md index b4b42e73..3e990049 100644 --- a/FAQ.md +++ b/FAQ.md @@ -279,7 +279,7 @@ The reference compiler is under active development. The current release includes - A seven-stage pipeline: parse, transform, resolve, typecheck, verify, compile, execute - A 14-chapter formal specification -- 11,623 tests, including a 242-program conformance suite +- 11,633 tests, including a 242-program conformance suite - 42 working example programs - 164 built-in functions covering strings, arrays, math, parsing, and data types - Four built-in abilities (Eq, Ord, Hash, Show) with constrained generics and ADT auto-derivation diff --git a/README.md b/README.md index d65c8137..fdb31928 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ cp /path/to/vera/SKILL.md ~/.claude/skills/vera-language/SKILL.md ## Project status -Vera is in **active development** at v0.1.11: 2,000+ commits, 209 releases, 11,623 tests, 95% Python code coverage, 242 conformance programs, 42 examples, and a 14-chapter specification. Known bugs and limitations are tracked in **[KNOWN_ISSUES.md](KNOWN_ISSUES.md)**. See **[HISTORY.md](HISTORY.md)** for how the compiler was built. +Vera is in **active development** at v0.1.11: 2,000+ commits, 209 releases, 11,633 tests, 95% Python code coverage, 242 conformance programs, 42 examples, and a 14-chapter specification. Known bugs and limitations are tracked in **[KNOWN_ISSUES.md](KNOWN_ISSUES.md)**. See **[HISTORY.md](HISTORY.md)** for how the compiler was built. The reference compiler — parser, AST, type checker, contract verifier (Z3), WASM code generator, module system, browser runtime, and runtime contract insertion — is working. The language specification is in draft across [14 chapters](spec/). diff --git a/ROADMAP.md b/ROADMAP.md index 703d275d..b31aa397 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,7 +12,7 @@ Ordering derives from the design principles ([DESIGN.md](DESIGN.md)): verificati ## Where we are -11,623 tests, 242 conformance programs, 42 examples, 14 spec chapters. [KNOWN_ISSUES.md](KNOWN_ISSUES.md) tracks the open bugs — burndown material rather than stage work — plus the *limitations* the stages below retire. +11,633 tests, 242 conformance programs, 42 examples, 14 spec chapters. [KNOWN_ISSUES.md](KNOWN_ISSUES.md) tracks the open bugs — burndown material rather than stage work — plus the *limitations* the stages below retire. ## Stage 19 — The verification completeness sprint diff --git a/TESTING.md b/TESTING.md index 9aff98d5..ccb398fa 100644 --- a/TESTING.md +++ b/TESTING.md @@ -6,7 +6,7 @@ This is the single source of truth for Vera's testing infrastructure, coverage d | Metric | Value | |--------|-------| -| **Tests** | 11,623 across 173 files (~155,000 lines of test code; 11,426 passed + 26 stress, 171 skipped) | +| **Tests** | 11,633 across 173 files (~155,000 lines of test code; 11,436 passed + 26 stress, 171 skipped) | | **Compiler code coverage** | 95% Python, 87% JavaScript (CI minimum: 80%) | | **Conformance programs** | 242 programs across 9 spec chapters, validating every language feature | | **Example programs** | 42, all validated through `vera check` + `vera verify` | @@ -72,7 +72,7 @@ python scripts/check_wheel_availability.py # pre-flight: every runtime | `test_naming_env_provenance_1208.py` | 45 | 2,097 | The other half of the #1208 contract: every consumer is handed the ENVIRONMENT the checker rendered under, not just the same renderer. Four provenance seams, each pinned by the adversarial probe that exhibited it — an IMPORTED callee's contract rendered in its DEFINING module's alias namespace (a violated precondition that vanished, and its mirror, a correct call spuriously rejected, both under a `Cnt` that names different bodies on the two sides), an imported GENERIC monomorphized and verified in that same namespace (a lying postcondition that proved clean, plus a verifier↔codegen clone differential over the recounted slot references — the desync is invisible to a unit test on either side), a `forall` variable shadowing a same-named module alias wherever a generic signature renders (the mono clone, a body `let`, the verifier's collapsed premises, and the exported uninstantiated template), and the tester's `SmtContext` holding the narrowed scope its own names were keyed in — the last one behavioural, since a generator handed the wrong scope collapses two parameters onto one variable and returns NO inputs at all. Three of the seams are also crossed against a SECOND component rather than checked for internal consistency, because a wrong-but-consistent scope is invisible from inside one: the verifier's declared parameter names against `slots.slot_table`'s, its `where`-helper scope against `slots.fn_scopes`' accumulation, and the monomorphizer's post-substitution names against what the consumers rebuild on the clone — each independent on the axis under test (which variables the two sides narrow by) but sharing `fn_slot_scope`/`slot_name` below it, so the hand-derived literal rendering beside every comparison is what a shared-renderer defect cannot satisfy. An imported generic nested under a non-generic function is pinned too — both its discovery-time recount and its verification-time clone must run in the DEFINING module's namespace. Three more seams arrived from the PR #1224 review, each with the false Tier-1 or miscompile that exhibited it: an UNPINNED callee (an imported generic's own `where`-helper, which the origin registry never pins) rendering in the module under verification rather than the entry program, whose absence let a violated precondition discharge as true and trap at run time; a callee's refined-RETURN predicate translated in the callee's namespace alongside its `requires`/`ensures`, pinned by provenance because today's bare-headed binder masks it behaviourally; and codegen's declaration-index space keyed PER NAMESPACE, without which a module's stamp turned the main file's forward alias reference into a backward one and a check-clean, verify-clean program read the wrong parameter through valid WASM. Two seams from the #1213 burndown close the same shape from the other side: the prelude's own aliases are injected only into the reserved `Vera` namespace, so no name a program can spell resolves on the codegen side alone (#1221 — the differential compares the checker's and codegen's partition of one signature, with the emitted WAT beside it), and an imported ADT is ordered at the index its OWN module gave it rather than the built-in floor (#1227), each with the control that differs by exactly the namespace under test. Controls carried alongside: the same programs with the alias renamed or the shadowing declaration removed, and the runtime oracle that shows the new E500/E501 agrees with the emitted code rather than merely reporting more | | `test_callee_contract_scope_1220_1225_1226.py` | 40 | 1,433 | A callee's contract is READ in the callee's own module. Three burndown defects, each asserted against the runtime oracle wherever the two directions of a wrong namespace (an obligation that vanishes, one that fires for no reason) look identical from inside the verifier: an E501's `Precondition:` line quoted from the file that DECLARED the clause (#1220 — in the misattribution direction too, both files carrying a plausible `requires` on the same line number, plus the imported-generic `where`-helper whose clause sits past the end of a short importer and used to quote nothing at all); a bare-name call inside an IMPORTED callee's contract resolved through the CALLEE's module registry (#1225 — the false Tier-1 whose run traps and its mirror spurious E501, through the `requires` and the `ensures` path, against a no-collision control); and the refined-RETURN binder derived through the naming layer, so a refinement over a PARAMETERISED base pushes the key its own predicate looks itself up under (#1226 — single-module and cross-module, the second proving the derivation happens INSIDE the callee scope); plus the PR #1239 review round — a module's pinned registry holds what its OWN file imports (a DEPTH-2 chain, the shape the single-level corpus could not exhibit: requires and ensures directions, bare-vs-qualified tier agreement, a name outside the middle module's import filter still missing per §8.5.1, and the mirror gate that filling the registry does not re-export), and every part of a diagnostic follows the declaring module (location, file name and excerpt, including a clause past a shorter importer's end, with a multi-line clause quoted whole), and the characterization of the binder-reference walk's one documented exception — a CLOSURE inside a predicate owns the first `@T.n` in traversal order, whose consequence is a Tier-3 demotion rather than a fact assumed about the wrong term; and the mini-review round — an obligation carries the FILE its line number belongs to, so the documented `(file, line, column)` join between the two `--json` arrays holds for a module-located obligation, with the entry-file control and warm==cold parity, and a multi-line clause is quoted with its `--` comments blanked (a `--` inside a string literal surviving, which a naive split would corrupt) | | `test_alias_application_refinement_base_1237.py` | 11 | 398 | A parameterised alias APPLICATION substitutes its arguments in the verifier's own resolver (#1237). `type Box = T;` applied as `@Box` resolved to `AdtType('T')` — the alias's binder leaking as an ADT name — so a refinement over it failed the modelled-primitive gate, the refined-return fact was dropped, and a valid program was rejected with a spurious E501 while `vera run` returned the right answer. Both halves of the fix are asserted separately: the alias body registers its own parameters as type variables (`substitute` maps type variables, so an ADT-named binder is unsubstitutable however the application side is written) and the application substitutes. Plus depth (an argument that is itself an application, and an alias whose body applies another alias), the bounded direction (a consumer wanting `>= 100` where the refinement grants `>= 18` is still rejected, and the runtime agrees), and the gate that must NOT move — an unmodelled `@Byte` base resolves correctly and still degrades to a Tier-3 runtime check, with a consumer of its predicate still refused | -| `test_exn_throw_payload_1268.py` | 38 | 956 | `throw`'s payload is obligated AND runtime-guarded like every other narrowing site (#1268). `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with ZERO obligations while `vera run` returned -5 out of the `@Nat` payload — check-green, verify-green, silently wrong — because `throw` is a bare call with no function-registry entry, so the argument loop never saw it and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. All three arms are asserted (the `@Nat` refutation, the refined refutation over a modelled base, and the `@Nat`->`@Int` widening obligation appearing where none existed), plus the #1251(b) concrete gate reaching the payload for free (`throw(200)` into an `Exn<{ @Byte \| @Byte.0 < 10 }>` names the value; the satisfying twin proves at Tier 1), the user-effect contrast that localized the bug (a declared op's argument was loud for the same value — both are loud now, at the same site name), and the refined-alias payload spelling. The GUARD is checked against codegen rather than asserted: an undischargeable payload must land on the runtime-guarded `tier3` leg, counted in the totals, and a run must confirm the payload really is stopped — the two together go red whichever side moves without the other (delete the emission and the runs go red; flip the flag back and the statuses do). Both arms are run at the boundary: a `@Nat` payload traps on -5 and delivers 5, a refined `{ @Int \| @Int.0 > 0 }` payload traps through the `$vera.contract_fail` channel on BOTH -5 and 0 — the value that clears the base's `>= 0` and violates the predicate, so a sign guard standing in for the predicate guard fails here — and delivers 7. The type gate carries its own over-refusal control: an `Exn` payload has no invariant to violate, so a negative one is a correct program and must still return -5. Both REPRESENTATIONS are covered — a scalar payload in one local, and a `@String`-based one whose (ptr, len) pair has to be saved in two, checked over the ptr and put back in the right order (the satisfying twin is what shows the order). The **soundness differential** is the point of the guard rather than a property of it: the clause parameter's type is what the verifier hands every downstream consumer, so a consumer discharging `ensures(@Bool.result)` at Tier 1 from its `@Nat` parameter alone is asserted PROVED and then run — pre-fix the run reported a postcondition violation on that proved postcondition, with nothing else in the path (the argument is already `@Nat`-typed, so no call-site narrowing guard fires). A dischargeable twin proves the site is not merely always-loud, and the six `Exn` conformance programs are verified whole as canaries so obligating a position that had none names itself instead of arriving as one line of a corpus sweep. The adversarial round adds the three places the `guarded` PROMISE and the emitted guard could disagree in a direction no value oracle can see, because the program either never runs or runs identically either way: a refinement OVER a refinement is asserted `tier3_unguarded` AND `E618`-refused at compile in one cell (the mirror claimed a runtime check for a program that cannot be compiled at all — either half alone reads as consistent); the bare and qualified spellings of one operation are asserted to record IDENTICAL statuses as a differential rather than two literals, on both arms, with the run confirming which value is the true one (`Exn.throw` was disclosed unguarded while codegen delegated it to the guard-emitting dispatcher, and `State.put` had been since #1203); and `E504`'s rationale is read from a real diagnostic — reached through the site that IS still unguarded, a user-declared effect's operation argument — to pin that it no longer lists the `throw` payload among the sites with no runtime guard | +| `test_exn_throw_payload_1268.py` | 42 | 1049 | `throw`'s payload is obligated AND runtime-guarded like every other narrowing site (#1268). `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with ZERO obligations while `vera run` returned -5 out of the `@Nat` payload — check-green, verify-green, silently wrong — because `throw` is a bare call with no function-registry entry, so the argument loop never saw it and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. All three arms are asserted (the `@Nat` refutation, the refined refutation over a modelled base, and the `@Nat`->`@Int` widening obligation appearing where none existed), plus the #1251(b) concrete gate reaching the payload for free (`throw(200)` into an `Exn<{ @Byte \| @Byte.0 < 10 }>` names the value; the satisfying twin proves at Tier 1), the user-effect contrast that localized the bug (a declared op's argument was loud for the same value — both are loud now, at the same site name), and the refined-alias payload spelling. The GUARD is checked against codegen rather than asserted: an undischargeable payload must land on the runtime-guarded `tier3` leg, counted in the totals, and a run must confirm the payload really is stopped — the two together go red whichever side moves without the other (delete the emission and the runs go red; flip the flag back and the statuses do). Both arms are run at the boundary: a `@Nat` payload traps on -5 and delivers 5, a refined `{ @Int \| @Int.0 > 0 }` payload traps through the `$vera.contract_fail` channel on BOTH -5 and 0 — the value that clears the base's `>= 0` and violates the predicate, so a sign guard standing in for the predicate guard fails here — and delivers 7. The type gate carries its own over-refusal control: an `Exn` payload has no invariant to violate, so a negative one is a correct program and must still return -5. Both REPRESENTATIONS are covered — a scalar payload in one local, and a `@String`-based one whose (ptr, len) pair has to be saved in two, checked over the ptr and put back in the right order (the satisfying twin is what shows the order). The **soundness differential** is the point of the guard rather than a property of it: the clause parameter's type is what the verifier hands every downstream consumer, so a consumer discharging `ensures(@Bool.result)` at Tier 1 from its `@Nat` parameter alone is asserted PROVED and then run — pre-fix the run reported a postcondition violation on that proved postcondition, with nothing else in the path (the argument is already `@Nat`-typed, so no call-site narrowing guard fires). A dischargeable twin proves the site is not merely always-loud, and the six `Exn` conformance programs are verified whole as canaries so obligating a position that had none names itself instead of arriving as one line of a corpus sweep. The adversarial round adds the three places the `guarded` PROMISE and the emitted guard could disagree in a direction no value oracle can see, because the program either never runs or runs identically either way: a refinement OVER a refinement is asserted `tier3_unguarded` AND `E618`-refused at compile in one cell (the mirror claimed a runtime check for a program that cannot be compiled at all — either half alone reads as consistent); the bare and qualified spellings of one operation are asserted to record IDENTICAL statuses as a differential rather than two literals, on both arms, with the run confirming which value is the true one (`Exn.throw` was disclosed unguarded while codegen delegated it to the guard-emitting dispatcher, and `State.put` had been since #1203); the #820 INTERSECTION at this boundary — a refinement over `@Int` keeps the widening obligation AND its guard beside the predicate's, pinned as a differential against the unrefined spelling (both must trap at u64.MAX, where the refined one used to return -1) with an in-range control so the guard is not simply always-on; and `E504`'s rationale is read from a real diagnostic — reached through the site that IS still unguarded, a user-declared effect's operation argument — to pin that it no longer lists the `throw` payload among the sites with no runtime guard | | `test_refinement_binder_convergence_1208.py` | 16 | 459 | Codegen's refinement boundary guard and `vera/naming.py` derive the predicate binder ONCE (#1208). A per-shape differential over a direct refinement, an alias hop, `@Nat`- and `@Byte`-based refinements (both range-conjoining), a composite base whose binder is a RESOLVED argument list, and two non-refinements that must be `None` on both sides — plus the property the convergence exists to hold, that the guard's binder equals the key a predicate's own `@Base.n` resolves to. A per-shape differential is green either way while two copies agree — which is exactly how a duplicated derivation drifts unnoticed — so the load-bearing assertion is a MUTATION: perturb `naming.refinement_binder_parts` and codegen's guard must report the perturbed binder. Codegen's two layered WASM decisions are pinned alongside — the loud E618 for a nested refinement base, reported ONCE per declaration however many call sites consult the derivation and however many clones a generic is instantiated into (two genuinely distinct sites still report twice), and no guard at all for an erased one (parametrized over `@Unit` and `Future`, the corner that erases identically but is not spelled `Unit`) — plus runtime traps proving BOTH the `@Byte` and the `@Nat` range conjunctions reach the emitted check, each pinned by trap `kind` as well as by the conjunct its message names. The mutation perturbs the predicate as well as the binder name: the two travel by different routes, and the range conjunction lives on the predicate. The once-per-site dedup is keyed on a resolved location, so a cross-module pin holds up the premise that a location carries its owning file: two imported library modules of identical shape, declaring their nested refinement at the same line and column, must produce two diagnostics attributed to two files — each quoting its own module's declaration, which is also what catches an attribution pointing past the importer's last line | | `test_checker_effects.py` | 90 | 1,455 | Effect declarations, abilities, effect subtyping, async effect, handler typing (#420 split), and the #1149 built-in-effect redeclaration gate (E152: divergent and faithful `effect IO`, codegen-only `Exn`, a registry-parametrised sweep, and a differential pinning the gate's name set to what `vera effects --json` publishes) | | `test_state_exn_registration.py` | 30 | 1296 | #1210 — State/Exn host-import registration covers the whole handler, not just its body. Four shapes, one per sub-expression position the walk used to miss (a nested handler in a clause body, in the state-init expression, in a clause's `with` update, and an `Exn` handler in a clause body), each check-green and verify-clean and therefore required to COMPILE — pre-fix every one died at whole-module WAT compilation with `unknown func` / `unknown tag`; plus the `i32_pair` cell (`handle[State]` in a `pure` function) that the walk skipped in silence, now the same loud E607 the declared-effect gate emits. The **registration-completeness differential** is the cross-component invariant itself: over every `examples/` + `tests/conformance/` program that compiles, every `state_*` / `exn_*` symbol the emitted WAT REFERENCES must have a matching import or tag DECLARATION — a desync between the registration pass and the lowering pass is invisible to a unit test on either. Round two adds the Exn twin of the silent skip (`handle[Exn]` in a `pure` function — the walk called the shared tag registration and discarded the verdict, so it compiled where the declared-row spelling was a clean E612) and the four CONTRACT positions, which are lowered code: a handler in a `requires`, an `ensures`, an `assert`, or a `decreases` measure. The differential gained a **validation leg** — every HANDLER-BEARING module is handed to `wasmtime.Module` through the exceptions-enabled engine `execute()` uses, because a symbol declared at the WRONG TYPE passes the name comparison while being invalid WASM, and 10 of the 30 handler-bearing modules fail to load with `wasm_exceptions` off — a supported wasmtime configuration, though the current runner defaults it on. The conformance suite's deliberate negatives are filtered out of the sweep: they never reach codegen through `vera check`. Carries floors on programs swept, modules validated, summed symbol references and globally distinct symbols, plus three can-go-red tests: the State and Exn extractions each stripped of their declaration lines, and a planted retyped import that only the validation leg catches. Round five adds the three positions no corpus program contained — a destructuring `let`'s value (which also disarmed the E612 gate), a module call's ARGUMENTS, and a signature refinement predicate reached through the alias table — and a cross-module shape test for the module-call leg. Round seven adds the boundary-guard routes that enumeration missed (a tuple parameter's components, a tuple return's, and a closure's refined formal and return) and the co-extensiveness half those shapes cannot show: a refined tuple behind a CLOSURE formal must declare nothing, since the closure path emits no component guards, and the nested-refinement (E618) and erased-base bails must stay silent registrars too. Plus the cycle guard on the closure signature leg, asserted in both directions — the walk terminates, and with the guard neutered the same walk blows the recursion limit | diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 2c6b265c..a440350b 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -3265,7 +3265,7 @@ The reference compiler is under active development. The current release includes - A seven-stage pipeline: parse, transform, resolve, typecheck, verify, compile, execute - A 14-chapter formal specification -- 11,623 tests, including a 242-program conformance suite +- 11,633 tests, including a 242-program conformance suite - 42 working example programs - 164 built-in functions covering strings, arrays, math, parsing, and data types - Four built-in abilities (Eq, Ord, Hash, Show) with constrained generics and ADT auto-derivation diff --git a/tests/test_exn_throw_payload_1268.py b/tests/test_exn_throw_payload_1268.py index 3a34b6fd..657a5430 100644 --- a/tests/test_exn_throw_payload_1268.py +++ b/tests/test_exn_throw_payload_1268.py @@ -742,6 +742,99 @@ def test_the_guard_the_obligation_promises_is_emitted( assert guard.search(body), body +class TestARefinementDoesNotDisableTheWideningGuard: + """A refinement OVER `@Int` rides BESIDE the widening obligation. + + The #820 intersection, which the shared triple was missing at this + boundary (PR #1325 review). The three arms are an `elif` chain, so a + refined formal claimed the value and the widening check never ran — and + codegen mirrored it exactly, which is why this was not a + verifier-versus-codegen desync but something worse in one respect: both + sides agreed to skip a check the UNREFINED spelling performs. + + A refinement predicate does not imply fit-in-i64. `@Nat` is u64 and + `@Int` is i64, so a `@Nat` above i64.MAX reinterprets to a negative + `@Int` — and `{ @Int | true }` is satisfied by that negative, as `< 100` + would be. Adding a refinement therefore WEAKENED the boundary: measured + before the fix, `Exn` fed u64.MAX trapped on the widening guard + while `Exn<{ @Int | true }>` fed the same value returned -1. + """ + + _REFINED = """ +type AnyInt = { @Int | true }; + +private fn boom(@Nat -> @Int) + requires(true) + ensures(true) + effects(>) +{ + throw(@Nat.0) +} + +public fn main(@Nat -> @Int) + requires(true) + ensures(true) + effects(pure) +{ + handle[Exn] { + throw(@AnyInt) -> { @AnyInt.0 } + } in { + boom(@Nat.0) + } +} +""" + + #: The same program with the refinement removed — the spelling whose + #: protection the refined one has to match. + _BARE = _REFINED.replace( + "type AnyInt = { @Int | true };\n\n", "").replace("AnyInt", "Int") + + #: u64.MAX in an i64 slot reads back as -1. + _U64_MAX = 18446744073709551615 + + def test_both_obligations_are_recorded(self) -> None: + """`refine_bind` AND `nat_to_int_coerce`, not one or the other. + + Different kinds describing different facts about one value, so this + is a pair rather than a double-record: the predicate is about which + inhabitants are legal, the coercion about whether the value survives + the u64-to-i64 reinterpretation at all. + """ + kinds = [o.kind for o in _verify(self._REFINED).obligations + if o.kind in ("refine_bind", "nat_to_int_coerce")] + assert kinds == ["refine_bind", "nat_to_int_coerce"], kinds + + def test_both_guards_are_emitted(self) -> None: + """...and codegen emits both, so neither obligation is a promise + the module does not keep.""" + body = wat_fn_body(_compile_ok(self._REFINED).wat, "boom") + assert "contract_fail" in body, body + assert re.search( + r"i64\.const 0\s+i64\.lt_s\s+if\s+unreachable", body, re.S, + ), body + + def test_the_refinement_does_not_weaken_the_boundary(self) -> None: + """The behavioural pin, as a DIFFERENTIAL against the bare spelling. + + Asserting "the refined one traps" alone would be satisfied by a + boundary that traps on everything; asserting it agrees with the bare + spelling is the property that was broken — refined returned -1 where + bare trapped. + """ + with pytest.raises(WasmTrapError): + execute(_compile_ok(self._BARE), fn_name="main", + args=[self._U64_MAX]) + with pytest.raises(WasmTrapError): + execute(_compile_ok(self._REFINED), fn_name="main", + args=[self._U64_MAX]) + + def test_a_value_that_fits_still_passes(self) -> None: + """The over-refusal control: the guard is dead for an in-range value, + on both spellings.""" + assert _run(self._REFINED, "main", [5]) == 5 + assert _run(self._BARE, "main", [5]) == 5 + + class TestAProvedContractSurvivesTheThrowPayload: """The soundness differential: verify says PROVED, so run must agree. diff --git a/vera/README.md b/vera/README.md index e2ee3374..90b1f754 100644 --- a/vera/README.md +++ b/vera/README.md @@ -753,7 +753,7 @@ The `ERROR_CODES` dict in `errors.py` maps every code to a short description (16 ## Test Suite -Testing spans a **pytest suite** of 11,623 tests across 173 files — compiler-internals unit tests plus a **conformance suite** (242 programs in `tests/conformance/` validating every language feature against the spec) and **example programs** (42 end-to-end demos). The conformance suite is the definitive specification artifact — most programs target a single feature, though some (slot references, match, contracts) span several, and each serves as a minimal working example. +Testing spans a **pytest suite** of 11,633 tests across 173 files — compiler-internals unit tests plus a **conformance suite** (242 programs in `tests/conformance/` validating every language feature against the spec) and **example programs** (42 end-to-end demos). The conformance suite is the definitive specification artifact — most programs target a single feature, though some (slot references, match, contracts) span several, and each serves as a minimal working example. See **[TESTING.md](../TESTING.md)** for the comprehensive testing reference -- test file table, conformance suite details, compiler code coverage, language feature coverage, helper conventions, validation scripts, CI pipeline, and guidelines for adding tests. diff --git a/vera/verifier.py b/vera/verifier.py index fe5609e9..9e072ec6 100644 --- a/vera/verifier.py +++ b/vera/verifier.py @@ -4413,6 +4413,25 @@ def _obligate_binding_triple( decl, value, refined, smt, slot_env, assumptions, site=site, guarded=refined_guarded, ) + # #820 INTERSECTION, at this boundary too (PR #1325 review). A + # refinement OVER @Int does not imply fit-in-i64 — `< 100` is + # SATISFIED by a reinterpreted negative — so the widen obligation + # is not subsumed by the refined one and rides ALONGSIDE it + # rather than being skipped by the chain below. Measured before + # the fix: `Exn` fed a @Nat of u64.MAX TRAPPED on the widen + # guard, while `Exn<{ @Int | true }>` fed the same value returned + # -1 — adding a refinement silently disabled the protection the + # bare spelling had. Not a double-record: the arms below are + # `elif`, so a value that reaches this branch reaches neither, + # and the two obligations are different kinds (`refine_bind` and + # `nat_to_int_coerce`) describing different facts about one + # value. + if (self._int_widening_target(value, formal) + and self._result_is_nat(value)): + self._check_int_widening_obligation( + decl, value, smt, slot_env, list(assumptions), + site=site, guarded=widen_guarded, + ) elif (self._nat_binding_target(value, formal) and self._narrows_into_nat(value)): self._check_nat_binding_obligation( diff --git a/vera/wasm/calls.py b/vera/wasm/calls.py index 78263610..256314fb 100644 --- a/vera/wasm/calls.py +++ b/vera/wasm/calls.py @@ -599,6 +599,17 @@ def _translate_call( if refined_payload is not None: instructions = self._emit_exn_payload_refine_guard( instructions, refined_payload, cell, call, env) + # #820 INTERSECTION (PR #1325 review): the predicate does + # not imply fit-in-i64, so a refinement OVER `@Int` keeps + # the widening guard BESIDE its predicate guard rather + # than replacing it. Without this, adding a refinement + # weakened the boundary: `Exn` fed a @Nat of u64.MAX + # trapped, `Exn<{ @Int | true }>` returned -1. The + # verifier's `_obligate_binding_triple` records the pair + # in the same shape, so obligation and guard still match + # one-for-one. + if base == "Int" and self._result_is_nat(call.args[0]): + instructions = self._emit_int_widen_guard(instructions) if refined_payload is None and (is_state_put or is_exn_throw): if base == "Nat" and self._narrows_into_nat(call.args[0]): instructions = self._emit_nat_bind_guard(instructions) From 48cb45f61bff9e01b95c4539e05e768d3d27923e Mon Sep 17 00:00:00 2001 From: Alasdair Allan Date: Fri, 14 Aug 2026 21:05:34 +0100 Subject: [PATCH 4/4] Align the two trap-fix comments and the #754 row with the shipped guards The final review pass's two Minors, both verified: the JSON-mode comment on _TRAP_FIX_PARAGRAPHS still named two empty-Fix kinds where the corrected human-mode comment names three; and the #754 row's "sign pair" phrase both omitted the independent widening guard and over-claimed the pair on the refined payload path, which carries the predicate guard (whose lowered check includes the base's range) plus the widening guard. Skip-changelog: comment-only cli.py edit; the KNOWN_ISSUES row wording rides the PR's existing entries Co-Authored-By: Claude --- KNOWN_ISSUES.md | 2 +- vera/cli.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 7deeba31..0a2c59b7 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -35,7 +35,7 @@ Things Vera cannot do yet, as distinct from defects in what it claims to do. | An ADT `decreases` measure is runtime-ranked only when its type's reachable field structure is fully concrete: a parameterized measure (`List`), or a concrete type whose recursion rides through a parameterized field, gets no runtime guard (never a wrong one — the registered generic layout does not describe concrete construction), staying statically checked and Tier-3-disclosed. Per-instantiation `$dec_size_` helpers (the `$eq_` pattern) close it. | [#1177](https://github.com/aallan/vera/issues/1177) | | Tier 2 verification (Z3 guided by `assert`/lemma hints) is specified in §6.3.2 but not implemented, so contracts that need hints fall to Tier 3 runtime checks. Per-monomorphization verification (#732) has since landed; full Tier 2 (hint-guided) stays on the Milestone 4 horizon. | [#427](https://github.com/aallan/vera/issues/427) | | `data invariant(...)` clauses (spec §2.6, §6.2.3) are not implemented — every documented form fails with E130 because the slot environment for the invariant predicate isn't wired up. Refinement types (`{ @T \| predicate }`, §2.6) are the working alternative until this lands. | [#686](https://github.com/aallan/vera/issues/686) | -| **Statically**, every narrowing *binding site* is obligated: a provably-negative value is an E503 error, and an untranslatable narrowing at an unguarded site is an E504 warning — so a program whose narrowings are all statically PROVEN stores no negative `@Nat` at these sites. `vera verify` exits 0 on the E504 warnings too, so verify-clean is the weaker property: a disclosed narrowing is one the static proof did not reach, and at an unguarded site nothing else does either. **At runtime** (an unverified `vera compile`/`run`), the guard covers every concrete *direct* binding site (a nested constructor sub-pattern is the exception, #765 below), generic function-formal calls, and the string/markup builtin `@Nat` parameters (`string_repeat`, `string_pad_start`/`_end`, `string_from_char_code`, `md_has_heading`), but three statically-obligated sites stay unguarded and store a negative silently: the effect-operation argument of a USER-declared effect — the built-in `State` `put`/`resume` boundaries and the built-in `Exn` `throw` payload are obligated and codegen-guarded, the latter with the §2.6.5 predicate guard as well as the sign pair ([#1268](https://github.com/aallan/vera/issues/1268)) — (codegen's `_effect_ops` carries only the dispatch target for the rest), the generic-instantiated constructor field (constructor layouts carry no per-field `@Nat` mono metadata), and the `nat_to_int`/`nat_to_string` conversion builtins (special-cased before the `_fn_nat_params` guard loop) — no runtime guard is emitted at those three sites; the unguarded residual is disclosed statically as an E504 warning instead. (The static obligation now also covers the function **return** position — an `@Int` value narrowing into a `@Nat` return is an E503 obligation backed by a codegen return guard, #758 — and the value-position tuple/constructor component, which is both statically obligated and runtime-guarded at the function boundary.) #820 threaded the per-component target-type table into codegen (the metadata these narrowing guards need); the generic-field and effect-op sites are now unblocked for wiring, though the narrowing direction is not yet guarded here. | [#754](https://github.com/aallan/vera/issues/754), [#757](https://github.com/aallan/vera/issues/757) | +| **Statically**, every narrowing *binding site* is obligated: a provably-negative value is an E503 error, and an untranslatable narrowing at an unguarded site is an E504 warning — so a program whose narrowings are all statically PROVEN stores no negative `@Nat` at these sites. `vera verify` exits 0 on the E504 warnings too, so verify-clean is the weaker property: a disclosed narrowing is one the static proof did not reach, and at an unguarded site nothing else does either. **At runtime** (an unverified `vera compile`/`run`), the guard covers every concrete *direct* binding site (a nested constructor sub-pattern is the exception, #765 below), generic function-formal calls, and the string/markup builtin `@Nat` parameters (`string_repeat`, `string_pad_start`/`_end`, `string_from_char_code`, `md_has_heading`), but three statically-obligated sites stay unguarded and store a negative silently: the effect-operation argument of a USER-declared effect — the built-in `State` `put`/`resume` boundaries and the built-in `Exn` `throw` payload are obligated and codegen-guarded, the latter with the #1203 sign/widening pair on unrefined payloads and, on a refined payload, the §2.6.5 predicate guard (its lowered check includes the base's range) plus the independent `@Nat`-to-`@Int` widening guard ([#1268](https://github.com/aallan/vera/issues/1268)) — (codegen's `_effect_ops` carries only the dispatch target for the rest), the generic-instantiated constructor field (constructor layouts carry no per-field `@Nat` mono metadata), and the `nat_to_int`/`nat_to_string` conversion builtins (special-cased before the `_fn_nat_params` guard loop) — no runtime guard is emitted at those three sites; the unguarded residual is disclosed statically as an E504 warning instead. (The static obligation now also covers the function **return** position — an `@Int` value narrowing into a `@Nat` return is an E503 obligation backed by a codegen return guard, #758 — and the value-position tuple/constructor component, which is both statically obligated and runtime-guarded at the function boundary.) #820 threaded the per-component target-type table into codegen (the metadata these narrowing guards need); the generic-field and effect-op sites are now unblocked for wiring, though the narrowing direction is not yet guarded here. | [#754](https://github.com/aallan/vera/issues/754), [#757](https://github.com/aallan/vera/issues/757) | | A refinement / `@Nat` narrowing bound in a **nested** constructor sub-pattern (`Some(Some(@PosInt))` on `Option>`) is statically obligated — a `vera verify`-clean program is sound; a bad nested refined narrowing is `E505`, a bad nested `@Nat` narrowing is `E503` (#763) — but the runtime guard covers only the `@Nat` nested bind (verified by run probes: a negative payload traps in both direct and closure positions) — the *refined* nested bind is not guarded, so an unverified compile doesn't trap on a violating nested refined payload; the verifier discloses that residual as `tier3_unguarded`/E506 | [#765](https://github.com/aallan/vera/issues/765) | | A refinement whose base carries a **non-plain type argument** (`{ @Array<{ @Int \| ... }> \| array_length(...) > 0 }`, an `Array` base) gets **no runtime guard at any boundary** — `_refinement_guard_parts` cannot spell the binder slot for such a base, so named-function and closure parameters and returns alike leave the predicate unchecked at run time. The verifier discloses this honestly (`tier3_unguarded`, E506, excluded from runtime-checked totals — `_refined_boundary_codegen_guardable` mirrors the codegen bail, KEEP IN SYNC); the guard itself is the open work. Same disclosure family as #754 / #757 / #765. | [#1036](https://github.com/aallan/vera/issues/1036) | | Contracts that depend on a `handle[...]` expression's value fall to Tier 3 — the verifier does not model the `handle[...]` expression's value or handler-clause state, so an `ensures` over a handle result is runtime-checked rather than statically proved. (Primitive-op and binding obligations *inside* the handle body walk at enclosing-scope precision and can prove Tier-1; a clause body's slot-dependent obligations are fresh-scope Tier-3, while a manifest violation there — `5 / 0` — stays a loud compile error.) | [#439](https://github.com/aallan/vera/issues/439) | diff --git a/vera/cli.py b/vera/cli.py index cf08bb5d..12f63f26 100644 --- a/vera/cli.py +++ b/vera/cli.py @@ -1230,7 +1230,9 @@ def cmd_run( # Always present for shape stability (same reasoning # as `trap_kind` and `frames`); empty string for the # kinds that don't admit a generic suggestion - # (`contract_violation`, `unknown`). Mirrors the + # (`contract_violation` and `host_error`, whose + # descriptions already carry the remediation; + # `unknown`). Mirrors the # `fix` field on compile-time `Diagnostic` objects. "fix": exc.fix, }