Skip to content

Commit f5a328f

Browse files
aallanClaude
andcommitted
fix: address CodeRabbit full-review findings (alias-aware guards, side-table, test pins, doc counts)
CodeRabbit's full review of PR #756 surfaced 8 findings; this commit addresses all (7 fixes + 1 evidence-based reword). Soundness (Major): - registration.py `_type_resolves_to_nat`: apply alias type-argument substitution so a generic alias instantiated to Nat (`type Id<T> = T` used as `Id<Nat>`) resolves to Nat rather than the bare type-param `T`, closing a missed runtime guard for generic-alias Nat ctor fields / formals. - wasm/data.py + wasm/context.py: the four bind-site runtime guards (let, top-level match, ADT sub-pattern, tuple-destructure component) gated on `type_name == "Nat"` now resolve through `_resolve_base_type_name`, so a `type Age = Nat` alias / refined-Nat target is guarded too (was silently skipped on a verification-less compile). Provable no-op for non-aliases. - verifier.py `verify()`: recompute the checker side-tables when EITHER is missing (was `and`), so a caller supplying only one table no longer leaves the other empty and silently under-fires the #747 generic-instantiation checks. Tests: - test_codegen.py `_body`: boundary-safe regex so `$gcall` does not slice `$gcall_helper`'s body. Add two alias-guard regression pins (Nat-alias let-bind, generic-alias ctor field). - test_verifier.py: pin the concrete imported-ctor discharged test to `status == ["verified"]` (was satisfiable by no obligation emitted at all). Docs: - CHANGELOG: ruff total ~176 -> ~175 (134 auto-fixed + 41 hand-triaged). - HISTORY footer: 173 -> 174 tagged releases, 76 -> 77 active days (sync with the intro header and the README status line). - TESTING: reworded the Nat E504-residual note. Kept generic-instantiated — empirically it DOES reach E504 (a generic formal erases to i64, so codegen cannot guard it), so removing it as suggested would make the doc inaccurate; fixed only the muddled shared parenthetical. Synced test/line counts. - ROADMAP: test count 4441 -> 4443. Co-Authored-By: Claude <[email protected]>
1 parent 8c0bb75 commit f5a328f

10 files changed

Lines changed: 81 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1010

1111
### Added
1212

13-
- **ruff is now an enforced lint gate** ([#733](https://github.com/aallan/vera/issues/733)). `ruff check` (default `F`/`E` rules) runs in pre-commit (before mypy) and the CI lint job — ruff shipped in the `[dev]` extras but nothing ran it, so ~176 findings had accumulated silently. All cleared: 134 auto-fixed (unused imports, placeholder-less f-strings, redefinitions) and 41 hand-triaged (the three dead `skip_langs` sets, unused locals, ambiguous names, `ResolvedModule` string-annotation hoists, semicolon splits, a `type()` comparison). Bundles the ruff `0.15.16``0.15.17` bump ([#741](https://github.com/aallan/vera/pull/741)).
13+
- **ruff is now an enforced lint gate** ([#733](https://github.com/aallan/vera/issues/733)). `ruff check` (default `F`/`E` rules) runs in pre-commit (before mypy) and the CI lint job — ruff shipped in the `[dev]` extras but nothing ran it, so ~175 findings had accumulated silently. All cleared: 134 auto-fixed (unused imports, placeholder-less f-strings, redefinitions) and 41 hand-triaged (the three dead `skip_langs` sets, unused locals, ambiguous names, `ResolvedModule` string-annotation hoists, semicolon splits, a `type()` comparison). Bundles the ruff `0.15.16``0.15.17` bump ([#741](https://github.com/aallan/vera/pull/741)).
1414
- **The `@Nat >= 0` narrowing invariant is now obligated at every binding site** ([#747](https://github.com/aallan/vera/issues/747)). #552 covered `let` / call-argument / effect-operation-argument / concrete-constructor-field / match-bind / literal-tuple-destructure narrowing; #747 extends the **static** obligation to the projection and instantiation sites — ADT sub-pattern binds (`match opt { Some(@Nat) -> }` on `Option<Int>`), non-literal tuple destructures (via new Z3 tuple-datatype support in the SMT layer), generic constructor / effect-operation / function formals instantiated to `@Nat`, and imported ADT constructors with `@Nat` fields — by threading the checker's instantiated expression types into the verifier, so every narrowing shape now verifies statically. The Tier-3 **runtime** guard also extends to the tuple-destructure, top-level match-bind, ADT-sub-pattern, concrete-constructor-field, and call-argument sites, so an unverified compile traps on a negative `@Nat` rather than storing it silently (the effect-operation-argument runtime guard and a dedicated guard trap kind are tracked as [#754](https://github.com/aallan/vera/issues/754)). Folds in the [#749](https://github.com/aallan/vera/issues/749) review-debt test pins (IndexExpr / InterpolatedString walker recursion, the `_fresh_slot_var` nat-alias path, and a `_narrows_into_nat` verifier/codegen parity test).
1515

1616
### Changed

HISTORY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,4 @@ Proof obligations became first-class records with a warm Z3 session, and the LSP
399399
| Spec chapters | 7 | 10 | 11 | 12 | 13 | 13 | 13 | 13 |
400400
| Code coverage |||| 90% | 91% | 96% | 96% | 95% |
401401

402-
Total: **1,400+ commits, 173 tagged releases, 76 active development days.**
402+
Total: **1,400+ commits, 174 tagged releases, 77 active development days.**

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Priority lives in this file and nowhere else — issues carry kind and area labe
88

99
## Where we are
1010

11-
4,441 tests, 90 conformance programs, 35 examples, 13 spec chapters.
11+
4,443 tests, 90 conformance programs, 35 examples, 13 spec chapters.
1212

1313
## The roadmap
1414

TESTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This is the single source of truth for Vera's testing infrastructure, coverage d
66

77
| Metric | Value |
88
|--------|-------|
9-
| **Tests** | 4,441 across 36 files (~57,700 lines of test code; 4,410 passed + 16 stress, 15 skipped) |
9+
| **Tests** | 4,443 across 36 files (~57,700 lines of test code; 4,412 passed + 16 stress, 15 skipped) |
1010
| **Compiler code coverage** | 95% Python, 61% JavaScript — 91% combined (CI minimum: 80%) |
1111
| **Conformance programs** | 90 programs across 9 spec chapters, validating every language feature |
1212
| **Example programs** | 35, all validated through `vera check` + `vera verify` |
@@ -59,8 +59,8 @@ python scripts/fix_allowlists.py --fix # auto-fix stale allowlists
5959
| `test_ast.py` | 128 | 1,122 | AST transformation, node structure, serialisation, string escape sequences, ability declarations |
6060
| `test_checker.py` | 521 | 5,939 | Type synthesis, slot resolution, effects, effect subtyping, contracts, exhaustiveness, cross-module typing, visibility, error codes, string built-ins, generic rejection, IO operation types, Markdown types, Regex types, abilities, Map collection, Set collection, Decimal type, Json type, Html type, Http effect, Inference effect, removed legacy name regression |
6161
| `test_obligations.py` | 268 | 851 | 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 35 examples and every verify/run-level conformance program), summary↔obligation tier-bookkeeping consistency, 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 |
62-
| `test_verifier.py` | 195 | 3,169 | Z3 verification, counterexamples, tier classification, call-site preconditions, branch-aware preconditions, pipe operator, cross-module contracts, match/ADT verification, decreases verification, mutual recursion, refined Bool/String/Float64 param sorts, **@Nat subtraction underflow obligation** (#520 — Path-A obligation discharge via requires/path-conditions/path-aware Z3 refutation, pure-literal exclusion, Int-Int and Nat-Int → Int exemptions), **@Nat binding-site narrowing obligation** (#552 — Tier-1 `value >= 0` at let/call-arg/effect-op-arg/ctor-field/match-bind/literal-destructure narrowing, #520 double-emit disjointness, E503 counterexample-witness pin; #747 — generic-instantiation, ADT sub-pattern, non-literal tuple-destructure, and cross-module imported-ctor narrowing, with concrete-site Tier-3 narrowings (incl. the if-expr-source SMT gap, recorded per-component) classified as codegen-guarded `tier3_runtime` and only effect-op-arg / generic-instantiated narrowings (whose `@Nat` codegen erases to i64) surfacing the `E504` unguarded warning; #749 — IndexExpr/InterpolatedString walker-recursion pins, `_fresh_slot_var` nat-alias unit test, `_narrows_into_nat` verifier/codegen soundness parity) |
63-
| `test_codegen.py` | 1,167 | 20,268 | WASM compilation, arithmetic, Float64, Byte, arrays (incl. compound element types), ADTs, match (incl. nested patterns), generics, State\<T\>, Exn\<E\> handlers, control flow, strings, string escape sequences, IO (read\_line, read\_file, write\_file, args, exit, get\_env, sleep, time, stderr), bounds checking, quantifiers, assert/assume, refinement type aliases, pipe operator, string built-ins, built-in shadowing, parse\_nat Result, GC, Markdown host bindings, Regex host bindings, Map collection, Set collection, Decimal type, Json type, Html type, Http effect, Inference effect, Random effect, example round-trips, GC shadow stack overflow, **WASM tail-call optimization** (#517 — `return_call` emission for tail-position calls, 50K- and 1M-iteration stress, structural assertions on `return_call`/plain `call` boundary, **GC-aware TCO for allocating fns (#549 — `$gc_sp` restore before each `return_call`)**, postcondition-fallback regression (still reverts to plain `call`), analyzer unit tests covering Block-trailing / IfExpr-both-branches / MatchExpr-arm-bodies / let-value-NOT-marked / call-args-NOT-marked / ExprStmt-statement-NOT-marked / IfExpr-condition-NOT-marked / MatchExpr-scrutinee-NOT-marked), **@Nat narrowing runtime guards** (#552 let site; #747 extends to tuple-destructure / top-level match-bind / ADT sub-pattern / concrete ctor-field / call-arg sites — `i64.lt_s; unreachable` net, @Int targets exempt) |
62+
| `test_verifier.py` | 195 | 3,173 | Z3 verification, counterexamples, tier classification, call-site preconditions, branch-aware preconditions, pipe operator, cross-module contracts, match/ADT verification, decreases verification, mutual recursion, refined Bool/String/Float64 param sorts, **@Nat subtraction underflow obligation** (#520 — Path-A obligation discharge via requires/path-conditions/path-aware Z3 refutation, pure-literal exclusion, Int-Int and Nat-Int → Int exemptions), **@Nat binding-site narrowing obligation** (#552 — Tier-1 `value >= 0` at let/call-arg/effect-op-arg/ctor-field/match-bind/literal-destructure narrowing, #520 double-emit disjointness, E503 counterexample-witness pin; #747 — generic-instantiation, ADT sub-pattern, non-literal tuple-destructure, and cross-module imported-ctor narrowing, with concrete-site Tier-3 narrowings (incl. the if-expr-source SMT gap, recorded per-component) classified as codegen-guarded `tier3_runtime`, and the two genuinely-unguarded residuals — effect-op-arg narrowings (no codegen guard yet, #754) and generic-instantiated field/argument narrowings (whose `@Nat` erases to i64) — surfacing the `E504` unguarded warning; #749 — IndexExpr/InterpolatedString walker-recursion pins, `_fresh_slot_var` nat-alias unit test, `_narrows_into_nat` verifier/codegen soundness parity) |
63+
| `test_codegen.py` | 1,169 | 20,296 | WASM compilation, arithmetic, Float64, Byte, arrays (incl. compound element types), ADTs, match (incl. nested patterns), generics, State\<T\>, Exn\<E\> handlers, control flow, strings, string escape sequences, IO (read\_line, read\_file, write\_file, args, exit, get\_env, sleep, time, stderr), bounds checking, quantifiers, assert/assume, refinement type aliases, pipe operator, string built-ins, built-in shadowing, parse\_nat Result, GC, Markdown host bindings, Regex host bindings, Map collection, Set collection, Decimal type, Json type, Html type, Http effect, Inference effect, Random effect, example round-trips, GC shadow stack overflow, **WASM tail-call optimization** (#517 — `return_call` emission for tail-position calls, 50K- and 1M-iteration stress, structural assertions on `return_call`/plain `call` boundary, **GC-aware TCO for allocating fns (#549 — `$gc_sp` restore before each `return_call`)**, postcondition-fallback regression (still reverts to plain `call`), analyzer unit tests covering Block-trailing / IfExpr-both-branches / MatchExpr-arm-bodies / let-value-NOT-marked / call-args-NOT-marked / ExprStmt-statement-NOT-marked / IfExpr-condition-NOT-marked / MatchExpr-scrutinee-NOT-marked), **@Nat narrowing runtime guards** (#552 let site; #747 extends to tuple-destructure / top-level match-bind / ADT sub-pattern / concrete ctor-field / call-arg sites — `i64.lt_s; unreachable` net, @Int targets exempt) |
6464
| `test_codegen_contracts.py` | 32 | 570 | Runtime pre/postconditions, contract fail messages, old/new state postconditions |
6565
| `test_codegen_monomorphize.py` | 71 | 1,320 | Generic instantiation, type inference, monomorphization edge cases, ability constraint satisfaction (Eq/Ord/Hash/Show), operation rewriting (eq/compare), show/hash dispatch, ADT auto-derivation, array operations (slice/map/filter/fold) |
6666
| `test_codegen_closures.py` | 50 | 1,618 | Closure lifting, captured variables, higher-order functions, iterative-builder shadow-stack regressions (#570), closure return-value shadow-push balance for both i32-pair and i32-ADT branches across array_map and array_mapi, plus VERA_EAGER_GC injection self-test (#593), IndexExpr-of-FnCall element-type inference (#614), non-contiguous capture and walker-order miscompiles (#615) |

tests/test_codegen.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16541,8 +16541,11 @@ def _body(wat: str, fn: str) -> str:
1654116541
arithmetic / ctor / match bodies — the negative-traps tests below
1654216542
pin the guard's runtime *semantics* independently.
1654316543
"""
16544-
idx = wat.find(f"(func ${fn}")
16545-
assert idx >= 0, f"{fn} not found in WAT"
16544+
# Boundary-safe so `$gcall` does not match `$gcall_helper` — a plain
16545+
# substring `find` would slice the wrong body (CR #756).
16546+
m = re.search(rf"\(func \${re.escape(fn)}(?![A-Za-z0-9_$.])", wat)
16547+
assert m is not None, f"{fn} not found in WAT"
16548+
idx = m.start()
1654616549
end = wat.find("\n (func ", idx + 1)
1654716550
return wat[idx:end if end >= 0 else len(wat)]
1654816551

@@ -16597,6 +16600,31 @@ def test_concrete_nat_call_arg_guarded(self) -> None:
1659716600
""")
1659816601
assert "i64.lt_s" in self._body(result.wat, "gcall")
1659916602

16603+
def test_nat_alias_let_bind_guarded(self) -> None:
16604+
"""A `type Age = Nat` alias target is guarded at the let-bind site —
16605+
`_resolve_base_type_name` resolves the alias so the runtime guard is
16606+
not skipped by the bare `type_name == "Nat"` check (CR #756)."""
16607+
result = _compile_ok("""
16608+
type Age = Nat;
16609+
public fn galias(@Int -> @Age)
16610+
requires(true) ensures(true) effects(pure)
16611+
{ let @Age = @Int.0; @Age.0 }
16612+
""")
16613+
assert "i64.lt_s" in self._body(result.wat, "galias")
16614+
16615+
def test_generic_nat_alias_ctor_field_guarded(self) -> None:
16616+
"""A generic alias instantiated to @Nat (`type Id<T> = T` used as
16617+
`Id<Nat>`) resolves to Nat via type-argument substitution, so the
16618+
constructor-field narrowing is still guarded (CR #756)."""
16619+
result = _compile_ok("""
16620+
type Id<T> = T;
16621+
public data GBox { GWrap(Id<Nat>) }
16622+
public fn ggen(@Int -> @GBox)
16623+
requires(true) ensures(true) effects(pure)
16624+
{ GWrap(@Int.0) }
16625+
""")
16626+
assert "i64.lt_s" in self._body(result.wat, "ggen")
16627+
1660016628
def test_int_ctor_field_emits_no_guard(self) -> None:
1660116629
"""A concrete @Int constructor field is not a narrowing target —
1660216630
no guard, mirroring the @Int-field/@Int-formal exemption."""

tests/test_verifier.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,8 +2440,12 @@ def test_imported_ctor_concrete_nat_field_discharged(self) -> None:
24402440
effects(pure)
24412441
{ WrapN(@Int.0) }
24422442
""", [mod])
2443-
assert not [o for o in result.obligations
2444-
if o.kind == "nat_bind" and o.status == "violated"]
2443+
# Pin that the obligation actually fired and verified — not merely the
2444+
# absence of a violation (which a no-obligation regression would also
2445+
# satisfy), mirroring the generic discharged companion (CR #756).
2446+
statuses = [o.status for o in result.obligations
2447+
if o.kind == "nat_bind"]
2448+
assert statuses == ["verified"], statuses
24452449
assert [d for d in result.diagnostics if d.severity == "error"] == []
24462450

24472451
def test_imported_ctor_generic_field_nat_obligated(self) -> None:

vera/codegen/registration.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from vera import ast
1010
from vera.codegen.api import ConstructorLayout, _align_up, _wasm_type_align, _wasm_type_size
11+
from vera.wasm.inference import substitute_type_vars
1112

1213

1314
class RegistrationMixin:
@@ -276,15 +277,18 @@ def _compute_constructor_layout(
276277

277278
def _type_resolves_to_nat(self, te: ast.TypeExpr) -> bool:
278279
"""True if *te* is ``@Nat`` directly, through a ``type X = Nat``
279-
alias, or as the base of a refinement (``{ @Nat | p }``) — used for
280-
the #747 runtime-guard metadata so an alias/refinement-typed @Nat
281-
formal or field is still guarded (CR #756), mirroring the verifier's
282-
alias-aware ``_is_nat_type``.
280+
alias, the base of a refinement (``{ @Nat | p }``), or a *generic*
281+
alias instantiated to @Nat (``type Id<T> = T`` used as ``Id<Nat>``)
282+
— used for the #747 runtime-guard metadata so an alias/refinement-
283+
typed @Nat formal or field is still guarded (CR #756), mirroring the
284+
verifier's alias-aware ``_is_nat_type``.
283285
284286
Alias resolution uses ``self._type_aliases``, populated in
285287
declaration order during ``_register_all``; a `@Nat` alias declared
286288
*after* the function/data that uses it is not yet visible here and
287-
falls back to the verifier's static obligation.
289+
falls back to the verifier's static obligation. Generic alias
290+
arguments are bound into the body via ``substitute_type_vars`` so
291+
``Id<Nat>`` resolves to ``Nat`` rather than the bare type-param ``T``.
288292
"""
289293
seen: set[str] = set()
290294
while True:
@@ -297,6 +301,12 @@ def _type_resolves_to_nat(self, te: ast.TypeExpr) -> bool:
297301
alias = self._type_aliases.get(te.name)
298302
if alias is not None and te.name not in seen:
299303
seen.add(te.name)
304+
params = self._type_alias_params.get(te.name)
305+
if (params and te.type_args
306+
and len(params) == len(te.type_args)):
307+
alias = substitute_type_vars(
308+
alias, dict(zip(params, te.type_args)),
309+
)
300310
te = alias
301311
continue
302312
return False

vera/verifier.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,23 @@ def verify(
9494
contract verification (C7d). Imported function preconditions are
9595
checked at call sites; postconditions are assumed.
9696
"""
97-
if expr_types is None and expr_target_types is None:
97+
if expr_types is None or expr_target_types is None:
9898
# #747: when the caller didn't supply the checker's semantic-type
9999
# side-tables, collect them here so a bare verify() matches the CLI
100100
# (cmd_verify) and LSP (VerificationSession) paths — both of which
101101
# thread them — keeping the warm/cold differential oracle and any
102-
# external caller consistent. Lazy import avoids a module cycle.
102+
# external caller consistent. A caller that supplies only *one*
103+
# table still gets the other filled — an empty target table would
104+
# silently under-fire the #747 generic-instantiation checks (CR
105+
# #756). Lazy import avoids a module cycle.
103106
from vera.checker import typecheck_with_artifacts
104107
_diags, _arts = typecheck_with_artifacts(
105108
program, source, file=file, resolved_modules=resolved_modules,
106109
)
107-
expr_types = _arts.expr_semantic_types
108-
expr_target_types = _arts.expr_target_types
110+
if expr_types is None:
111+
expr_types = _arts.expr_semantic_types
112+
if expr_target_types is None:
113+
expr_target_types = _arts.expr_target_types
109114
verifier = ContractVerifier(
110115
source=source, file=file, timeout_ms=timeout_ms,
111116
resolved_modules=resolved_modules,

vera/wasm/context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,10 @@ def translate_block(
519519
# statically (Tier 3), or when codegen runs without
520520
# `vera verify`. The guard never trips on a provably-@Nat
521521
# value, mirroring the #520 subtraction guard's
522-
# belt-and-suspenders role.
523-
if (type_name == "Nat"
522+
# belt-and-suspenders role. Alias-aware (`type Age = Nat`)
523+
# via `_resolve_base_type_name` so an alias/refined @Nat let
524+
# target is guarded too (CR #756).
525+
if (self._resolve_base_type_name(type_name) == "Nat"
524526
and self._narrows_into_nat(stmt.value)):
525527
instructions.extend(
526528
self._emit_nat_bind_guard(val_instrs))

0 commit comments

Comments
 (0)