Skip to content

release: v0.0.173 — @Nat narrowing at projection & instantiation sites (#747, #749)#756

Merged
aallan merged 27 commits into
mainfrom
feat/747-nat-projection-narrowing
Jun 18, 2026
Merged

release: v0.0.173 — @Nat narrowing at projection & instantiation sites (#747, #749)#756
aallan merged 27 commits into
mainfrom
feat/747-nat-projection-narrowing

Conversation

@aallan

@aallan aallan commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Generalizes the @Nat >= 0 binding-site narrowing obligation (#552) to every projection and instantiation site, and folds in the #749 review-debt. Cuts release v0.0.173.

After #552, the verifier obligated @Nat narrowings only where it could read the target type directly (let, call args, effect-op args, concrete ctor fields, top-level match binds, literal-tuple destructures). This PR closes the residual six shapes plus the runtime-guard gap.

Static obligations — all six #747 shapes

The enabler: _synth_expr now records each expression's semantic and instantiated-target type in span-keyed side-tables, threaded into the verifier, so the deferred sites resolve their @Nat target with one lookup.

  • ADT sub-pattern bindsmatch opt { Some(@Nat) -> } on Option<Int>
  • Non-literal tuple destructureslet Tuple<@Nat, @Nat> = f(), via new Z3 tuple-datatype support in the SMT layer so components are projectable
  • Generic constructor fields instantiated to @NatSome(@Int.0) building Option<Nat>
  • Generic effect-operation and function formals fixed to @Nat at the call site
  • Imported ADT constructors with @Nat fields — a _module_constructors registry harvested in _register_modules

Every narrowing shape now verifies statically, so a vera verify-clean program is sound.

Runtime guards

_emit_nat_bind_guard now also fires at the tuple-destructure, top-level match-bind, ADT-sub-pattern, concrete-constructor-field, and call-argument sites (five of six), so an unverified compile traps on a negative @Nat rather than storing it silently. Projection sites guard the @Nat target slot; value-flow sites guard a concrete @Nat formal/field when _narrows_into_nat(arg). @Nat is an i64 scalar, so each guard is GC-neutral. Builtin @Nat parameters (string_repeat, string_pad_start/_end, string_from_char_code, md_has_heading) are guarded too — folded in from #757; string_slice is exempt, since it clamps its indices to [0, len] and a negative argument can't underflow.

#749 fold-in

Review-debt test pins: IndexExpr / InterpolatedString walker-recursion _verify_err pins, a _fresh_slot_var nat-alias unit test, and a _narrows_into_nat verifier/codegen parity test over a value-expression corpus.

#757 fold-in (builtin @Nat-param guards)

Round 14 folded in the builtin half of #757: _emit_nat_bind_guard now also fires on the builtin translators that consume a narrowing @Nat argument unsafely (above). #757 stays open, re-scoped to its remaining half — the generic-instantiated constructor field (Wrap(@Int.0) building a Box<Nat>), which needs per-ctor nat_fields monomorphisation metadata that constructor layouts don't yet carry. Statically obligated either way, so verified programs stay sound.

Deferred to #754 (transparent)

Two low-value residuals, neither a correctness gap (every shape is obligated statically and Tier-3-flagged):

Follow-ups filed

  • #754 — the two deferred residuals above
  • #755 — the checker types Int <op> Nat as Nat (e.g. @Int.0 - 2 mis-types as @Nat), the spurious-narrowing source surfaced by this work
  • #757 — the remaining generic-instantiated constructor-field runtime guard (the builtin half is folded into this PR; re-scoped, stays open)

Release

v0.0.172 → v0.0.173 across the six version sites + CHANGELOG cut + HISTORY row. Bundles the already-accumulated ruff-gate (#733) and pytest-9.1.0 (#742) [Unreleased] entries.

Closes #747
Closes #749

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes v0.0.173

  • New Features
    • Broader non-negative @Nat runtime narrowing guards, with runtime traps for additional binding, destructuring, constructor-field, and generic/cross-module call cases.
    • Exposed typecheck_with_artifacts plus verification artifacts for richer integration.
  • Bug Fixes
    • More negative @Nat situations now trap instead of being stored; clarified the Int <op> Nat type-check regression.
  • Improvements
    • Enforced ruff as a lint gate in pre-commit and CI; updated pytest to 9.1.0.
  • Documentation / Known Issues
    • Updated vera lsp support: unsupported on Python 3.16+; refined limitations/spec wording.
  • Tests
    • Expanded verifier and codegen coverage for the new guard/obligation behaviour.

aallan and others added 8 commits June 17, 2026 15:57
…747 step 1)

Infrastructure for #747: record per-expression semantic Type (synthesised result + the instantiated 'expected' target each value was checked against) in span-keyed side-tables during type-check (typecheck_with_artifacts), and thread them into ContractVerifier via verify() + the LSP VerificationSession.  Adds _resolved_type_of / _target_type_of helpers.  No behaviour change yet — the @nat narrowing walker consumes the tables in the following steps.  Validation confirmed the 'expected' table already carries the instantiated @nat target at generic construction/call sites (Some(@Int.0) building Option<Nat> records the argument's target as Nat), so the deferred sites resolve with a single lookup rather than re-deriving substitutions.

Co-Authored-By: Claude <[email protected]>
…747 step 2)

Generalises the @nat >= 0 binding-site obligation to the three generic
instantiation shapes #552 deferred: generic constructor fields
(`Some(@Int.0)` building `Option<Nat>`), generic effect-op formals
(`E<Nat>.wait(@Int.0)`), and generic function formals fixed to @nat by a
sibling argument (`pick(@Nat.0, @Int.0)`).

The verifier's `_nat_binding_target()` keeps the concrete-formal check
(#552, table-independent) and, when the formal is a TypeVar, consults the
checker's recorded *instantiated target* for the argument
(`_target_type_of`, step 1).  Two checker changes make every argument
carry that target: `_check_op_call` resolves the effect type mapping
before synthesising args (so op args are checked against their
instantiated formal), and the generic-function path records the
post-inference formal as each arg's target (the initial synth uses
`expected=None` for generic calls, and `Int <: Nat` skips the re-synth).

Tests: the deferred `_not_obligated_yet` pins become obligated +
discharged pairs, plus a generic-function-formal pair; `_verify` now
mirrors the CLI verify path (collects and threads the side-tables).
`ch09_async`'s `async_helper` gains `requires(@Int.0 >= 0)`: `async`
infers `Future<Nat>` from the Nat-typed `@Int.0 * 2`, a real narrowing now
caught (the checker `Int * Nat = Nat` interaction is tracked as a
follow-up).

`verify()` now collects the side-tables itself when a caller omits them,
so a bare `verify(program, source)` matches the table-threading CLI / LSP
paths and the warm/cold differential oracle stays consistent.  TESTING.md
/ ROADMAP.md test counts synced (4,414 -> 4,418).

Co-Authored-By: Claude <[email protected]>
…site 1)

Generalises the @nat >= 0 obligation to ADT sub-pattern bindings —
`match opt { Some(@Nat.0) -> }` on `Option<Int>` binds the @int payload as
@nat.  A literal-constructor scrutinee (`match Some(@Int.0) { ... }`) binds
the constructor's own AST argument, obligated directly; an opaque scrutinee
binds an uninterpreted Z3 field accessor, obligated as a term (new
`_check_nat_binding_obligation_term`).  Field types come from
`_instantiated_field_types`, mirroring the checker's `_check_ctor_pattern`
(declared field types substituted against the scrutinee's resolved ADT type
args).  Only a genuine narrowing fires — an already-@nat field is skipped
(its accessor carries no >= 0 fact, so a spurious obligation would fail).

Site 2 (non-literal tuple destructure) stays deferred: the SMT layer models
a tuple as a scalar (its Z3 sort is `Int`, not a projectable datatype), so
a per-component `>= 0` obligation cannot be expressed.  Closing it needs SMT
tuple-datatype support — a separate prerequisite.

Co-Authored-By: Claude <[email protected]>
…747 site 2)

Closes the last deferred projection site: `let Tuple<@nat, @nat> = f()`
where `f` returns `Tuple<Int, Int>` now obligates each @int component
`>= 0`.

The blocker was the SMT layer modelling a tuple as a scalar `Int` (not
projectable), so a per-component obligation could not be expressed.
`smt.py` now synthesises a single-constructor Z3 datatype on demand for
each concrete `Tuple<...>` instance (`_get_or_create_tuple_sort`) — one
field per component — so the components project through accessors.

The verifier's `_obligate_destructure_narrowings` reads the RHS's
resolved tuple type from the checker side-table and obligates only a
*genuine* narrowing — a component whose source type is not already @nat —
exactly the ADT-sub-pattern guard, since the projected accessor term
carries no `>= 0` fact and an already-@nat source would fail the proof
spuriously.  A source the SMT layer cannot project into components (e.g.
an `if`-expression over tuples, which it does not model as a datatype)
surfaces one unguarded Tier-3 obligation + E504 rather than dropping the
narrowing silently — the non-let untranslatable-value honesty path.

The former deferral tripwire becomes a trio: a function-call source
obligated and failing (E503), an already-@nat source not obligated (the
soundness guard), and the if-expr source surfaced as Tier-3 E504.  All 90
conformance programs and 35 examples still pass.

Also refreshes the walker docstring and the generic-instantiation
site comments, which still read "deferred to #747" after the earlier
target-table commits made those sites fire — they now describe the
implemented behaviour.

Co-Authored-By: Claude <[email protected]>
…site 4)

The last #747 narrowing site: a constructor imported from another module
with an @nat field — concrete (`WrapN(Nat)`) or a generic field
instantiated to @nat (`Wrap(@Int.0)` building `Box<Nat>`) — now obligates
its @int argument `>= 0`.

The verifier harvested only imported *functions* into its env, so
`_lookup_constructor_info` returned None for an imported ctor and the
ConstructorCall walker's `if ci is not None` guard skipped the obligation
silently.  `_register_modules` now also harvests each resolved module's
data constructors into a flat `_module_constructors` fallback registry,
consulted last by `_lookup_constructor_info` (after the builtin and local
data-type lookups).  The obligation falls on the local @int argument, so
no imported-ADT SMT sort is needed — only the field types; over-harvesting
is inert because the fallback is reached only for a name already resolved
as a constructor.

A concrete @nat field obligates directly; a generic field's instantiated
@nat target still comes from the checker side-table (`_nat_binding_target`),
exactly as the local generic-field site.  Tests cover the concrete
obligated + discharged pair and the generic obligated case across a
two-module fixture.

Co-Authored-By: Claude <[email protected]>
#552 emitted the runtime `value >= 0` guard only at `let @nat = <Int>`.
#747 extends `_emit_nat_bind_guard` to five more @nat binding sites so an
unverified compile traps on a negative @nat rather than silently storing
it:

  * tuple destructure       — guard each @nat component load
  * top-level match bind     — `match <Int> { @nat -> }`
  * ADT sub-pattern bind     — `match opt { Some(@nat) -> }` on Option<Int>
  * concrete ctor field      — `WrapN(@Int.0)` where `WrapN(Nat)`
  * call argument            — `f(@Int.0)` into a concrete @nat formal

Projection sites (destructure, match/sub-pattern) guard on the @nat
*target* slot — the bound value is a synthesized `i64.load`, sound because
the guard only ever traps on a genuinely negative i64, never on a valid
@nat in [0, 2^63).  Value-flow sites (ctor field, call arg) guard the
argument expression when the formal/field is a concrete @nat and
`_narrows_into_nat(arg)` holds; an @int target is exempt.  @nat is an i64
scalar, never a heap pointer, so each guard (tee/check/get, no alloc) is
GC-neutral at every site.

Plumbing: `ConstructorLayout` gains `nat_fields`; a `_fn_nat_params`
registry (per-parameter concrete-@nat flags) is threaded codegen -> context
alongside `_fn_ret_type_exprs`.  Generic formals/fields fixed to @nat at
the call site erase to i64 in codegen, so they stay statically-only
(verifier-obligated).

Two residuals stay a follow-up: the effect-op-argument runtime guard
(`_effect_ops` carries no formal types — the rarest site, statically
obligated and E504-flagged when Tier-3) and the dedicated @Nat-guard trap
kind (#749 item 4 — needs the contract-fail host channel wired to a
mid-expression guard).  The guards trap correctly meanwhile, localized by
the #516 source frame.

Co-Authored-By: Claude <[email protected]>
…lization

Closes the #749 review follow-up test debt (items 1-3) and refreshes the
docs now that #747's static obligations cover every narrowing shape:

* IndexExpr and InterpolatedString container-recursion pins — a narrowing
  nested in `arr[takes_nat(@Int.0)]` or `"\(takes_nat(@Int.0))"` is
  obligated, pinning the two walker branches a regression could drop.
* `_fresh_slot_var` nat-alias unit test — a `type Count = Nat` slot
  resolves to a real Z3 nat var (same sort as a direct @nat), while an
  unknown ADT type yields None.
* `_narrows_into_nat` verifier/codegen parity test — a value-expression
  corpus run through both hand-mirrored predicates, asserting they agree
  so a builtin added to one and not the other can't silently desync the
  static obligation from the runtime guard.

`ch04_nat_binding.vera` gains a generic-constructor-field narrowing
(`Some(@Int.0)` building `Option<Nat>`, discharged via requires) and an
already-@nat sub-pattern bind (no spurious obligation).

Docs: the #747 KNOWN_ISSUES / spec / README rows are rewritten — every
narrowing shape is now statically obligated, leaving only the
effect-operation-argument runtime guard and the dedicated trap-kind
message as a residual, tracked as #754.  Also files the residual (#754)
and the `Int <op> Nat`-types-as-`Nat` checker imprecision (#755) surfaced
by this work, and folds in the #753 pygls/Python-3.16 row (KNOWN_ISSUES).

Co-Authored-By: Claude <[email protected]>
#747, #749)

Bumps 0.0.172 -> 0.0.173 and cuts the CHANGELOG.  The release bundles the
already-accumulated [Unreleased] entries (the ruff lint gate #733 and the
pytest 9.1.0 bump #742) with the #747 @Nat-narrowing generalization and
the #749 review-debt fold-in shipped across this branch.

Version bumped across vera/__init__.py, pyproject.toml, README, both docs
landing pages, and uv.lock; HISTORY gains the v0.0.173 row and its
active-days count ticks to 77; the regenerated llms.txt / llms-full.txt
pick up the new version.

Co-Authored-By: Claude <[email protected]>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aallan, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 27 minutes and 32 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0fe755b7-09bf-47b1-9cb0-f90869c06997

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb53ac and 3cd106d.

⛔ Files ignored due to path filters (7)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • tests/conformance/ch04_nat_binding.vera is excluded by !**/*.vera
  • tests/conformance/ch09_async.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (34)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • spec/06-contracts.md
  • spec/09-standard-library.md
  • spec/11-compilation.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/README.md
  • vera/__init__.py
  • vera/checker/__init__.py
  • vera/checker/calls.py
  • vera/checker/core.py
  • vera/checker/expressions.py
  • vera/cli.py
  • vera/codegen/api.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/modules.py
  • vera/codegen/registration.py
  • vera/obligations/session.py
  • vera/smt.py
  • vera/verifier.py
  • vera/wasm/calls.py
  • vera/wasm/calls_markup.py
  • vera/wasm/calls_strings.py
  • vera/wasm/context.py
  • vera/wasm/data.py
  • vera/wasm/operators.py
📝 Walkthrough

Walkthrough

Releases v0.0.173 of the Vera compiler by extending the @Nat >= 0 narrowing obligation to ADT sub-patterns, non-literal tuple destructures, generic constructor/function/effect-operation formals instantiated to @Nat, and cross-module imported constructors. Checker-produced span-keyed semantic type side-tables are threaded from typecheck_with_artifacts through the verify pipeline and into ContractVerifier, which uses them to classify generic instantiation targets. Runtime i64.lt_s/unreachable guards are emitted at six codegen binding sites and four builtin string/markup operations. SMT support for concrete Tuple sorts is added.

Changes

@Nat narrowing obligation and runtime-guard generalisation

Layer / File(s) Summary
Checker artifact schema and side-table capture
vera/checker/core.py, vera/checker/__init__.py, vera/checker/expressions.py
CheckArtifacts gains expr_semantic_types and expr_target_types span-keyed dicts; TypeChecker declares nullable instance attributes; typecheck_with_artifacts initialises and returns both; _synth_expr derives span key once and gates each write on result/expected presence; both symbols re-exported.
Call-site instantiation-aware target type recording
vera/checker/calls.py
Generic calls populate expr_target_types post-inference for each argument span whose instantiated formal is TypeVar-free. Effect-operation calls resolve the effect-context type mapping before argument synthesis, providing instantiated expected types per argument index.
Verify pipeline artifact propagation
vera/cli.py, vera/obligations/session.py, vera/verifier.py
cmd_verify, VerificationSession.verify_source, and verify() switch to typecheck_with_artifacts; all three paths forward expr_semantic_types/expr_target_types to ContractVerifier; bare verify() lazily populates tables when omitted.
ContractVerifier artifact intake, helpers, and cross-module constructor lookup
vera/verifier.py
Imports contains_typevar and substitute; stores span-keyed registries; adds _nat_binding_target(arg, formal), _resolved_type_of, _target_type_of; cross-module registration harvests public imported ADT constructors into _module_constructors; _lookup_constructor_info consults that fallback.
SMT Tuple sort synthesis
vera/smt.py
_get_or_create_adt_sort routes missing Tuple entries to new _get_or_create_tuple_sort, which builds a single-constructor Z3 Datatype with Tuple_0/Tuple_1/… field accessors, caches by canonical key.
Instantiation-aware nat-binding obligation walker and guarded tier accounting
vera/verifier.py
Walker uses _nat_binding_target for calls, constructor-fields, and effect-operations (last forced guarded=False); PIPE desugaring handled; non-literal tuple destructure dispatches to _obligate_destructure_narrowings; match arm path-condition push/pop fixed; ConstructorPattern sub-patterns handled via _obligate_subpattern_narrowings; _fresh_pattern_env added; _check_nat_binding_obligation gains guarded: bool; _record_nat_bind_tier3 splits tier3_runtime from E504 unguarded; _is_nat_typed prefers span side-table; _has_nat_origin updated for generic call instantiation.
Codegen @Nat metadata discovery: ConstructorLayout.nat_fields and _fn_nat_params
vera/codegen/api.py, vera/codegen/registration.py, vera/codegen/core.py
ConstructorLayout gains nat_fields: tuple[bool, ...]; CodeGenerator._fn_nat_params initialised; _type_resolves_to_nat(TypeExpr) introduced with alias/generic unwrapping and cycle guard; _register_fn and _compute_constructor_layout populate flags; MdHeading annotated nat_fields=(True, False).
Wasm context wiring and cross-module @Nat param propagation
vera/wasm/context.py, vera/codegen/functions.py, vera/codegen/closures.py, vera/codegen/modules.py
WasmContext gains _fn_nat_params field and set_fn_nat_params setter; _compile_fn and _compile_lifted_closure propagate flags before body compilation; _register_modules harvests _fn_nat_params from imported CodeGenerator instances using setdefault.
Runtime @Nat bind guard emission across six binding sites
vera/wasm/data.py, vera/wasm/calls.py, vera/wasm/context.py, vera/wasm/operators.py
i64.lt_s/unreachable guards emitted at: constructor-field storage, let-destructure component loads, match BindingPattern, constructor-pattern field extraction, regular call arguments indexed via _fn_nat_params, and let binding (alias-aware via _resolve_base_type_name). _emit_nat_bind_guard docstring enumerates all sites.
Runtime @Nat guards for builtin string and markup operations
vera/wasm/calls_markup.py, vera/wasm/calls_strings.py
_translate_md_has_heading guards heading level_arg before host call; _translate_from_char_code, _translate_string_repeat, and _translate_pad wrap narrowing instruction sequences with guards when _narrows_into_nat is true, preventing negative @Int values in i32 contexts.
Verifier test refactoring and obligation coverage expansion
tests/test_verifier.py
_verify harness threads typecheck_with_artifacts side-tables; "not obligated yet" tests replaced by obligated/discharged pairs; new coverage for ADT sub-patterns, generic formals, non-literal destructures, pipe-call narrowing, IndexExpr/interpolated-string walker regressions, _fresh_slot_var and _narrows_into_nat parity tests; cross-module imported constructor-field tests; tier totals updated to tier3_runtime == 28, total == 284, tier3_unguarded == 0.
Codegen runtime guard WAT-inspection and trap test suite
tests/test_codegen.py
New TestNatBindingRuntimeGuard747 suite with _body/_assert_guarded WAT helpers; structural guard-emission tests across all six binding forms plus aliases, built-in MdHeading, and generic-instantiated calls; negative @Int assertion; runtime trap tests; cross-module _boxes_module and _nat_fn_module synthetic resolved-module tests.
v0.0.173 version bump and aligned documentation
pyproject.toml, vera/__init__.py, CHANGELOG.md, HISTORY.md, KNOWN_ISSUES.md, README.md, ROADMAP.md, TESTING.md, vera/README.md, spec/06-contracts.md, spec/09-standard-library.md, spec/11-compilation.md
Version incremented to 0.0.173; CHANGELOG/HISTORY record expanded narrowing coverage; KNOWN_ISSUES documents new regression (#755), deferred effect-operation guard (#754), and LSP Python 3.16+ issue (#753); spec sections enumerate E502/E503/E504 sites and guard coverage; aggregate metrics updated; spec §9 Async example updated to @Nat parameters.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • #747 — This PR directly implements the six binding-site generalisation objectives from #747: ADT sub-patterns, non-literal destructures, generic constructor/effect-operation/function formals, cross-module imported constructors, and the Tier-3 runtime guard extension to six additional codegen binding sites.
  • #749 — The PR folds in the review follow-ups from #749: IndexExpr/interpolated-string walker regression pins, _fresh_slot_var alias unit test, and _narrows_into_nat verifier/codegen differential test. The dedicated guard trap kind remains deferred.
  • #754 — Documents and references the deferred follow-ups: the residual effect-operation-argument runtime guard gap and the dedicated trap kind for @Nat guard tripping, both linked throughout the v0.0.173 changes.
  • #758 — Clarifies the limitation that narrowing in function return position and value-position tuple/constructor components remains statically uncovered, with specification and documentation updates.

Possibly related PRs

  • aallan/vera#748: Direct predecessor — introduced the @Nat binding-site obligation walker and the initial let-site runtime guard that this PR generalises to six additional codegen binding sites and four builtin string/markup operations.

Suggested labels

compiler, tests, spec, docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: releasing v0.0.173 with @Nat narrowing generalisation to projection and instantiation sites, explicitly referencing issues #747 and #749.
Linked Issues check ✅ Passed The PR fully implements both linked issues: #747 generalises @Nat binding-site obligations to six additional narrowing shapes (ADT sub-pattern, non-literal destructure, generic constructor fields, imported constructors, generic effect-op formals, generic function formals) with runtime guards at five sites; #749 fold-in adds test pins for IndexExpr/InterpolatedString, _fresh_slot_var alias unit test, and _narrows_into_nat verifier/codegen parity test.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked objectives: v0.0.173 release bump, @Nat narrowing obligation/runtime-guard coverage, ruff-gate and pytest updates mentioned in objectives, Z3 tuple-datatype SMT support enabling destructure projection, module constructor harvesting for cross-module verification, and comprehensive test coverage for narrowing shapes and edge cases.
Docstring Coverage ✅ Passed Docstring coverage is 95.16% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/747-nat-projection-narrowing

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.22059% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.63%. Comparing base (7fb53ac) to head (3cd106d).

Files with missing lines Patch % Lines
vera/verifier.py 92.72% 12 Missing ⚠️
vera/smt.py 93.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #756      +/-   ##
==========================================
+ Coverage   91.60%   91.63%   +0.02%     
==========================================
  Files          70       70              
  Lines       25218    25459     +241     
  Branches      321      321              
==========================================
+ Hits        23101    23329     +228     
- Misses       2109     2122      +13     
  Partials        8        8              
Flag Coverage Δ
javascript 65.33% <ø> (ø)
python 94.82% <95.22%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
vera/verifier.py (1)

2063-2072: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep Tier-3 classification in sync with the new runtime guards.

This still treats every non-let site as unguarded E504, but the current guard plumbing covers call arguments, constructor fields, tuple destructures, match bindings, and ADT sub-pattern extraction; only effect-operation arguments are deferred. Otherwise runtime-checked obligations are reported as “not runtime-guarded” and excluded from the summary.

Proposed fix
-        if site == "let binding":
+        runtime_guarded_sites = {
+            "let binding",
+            "call argument",
+            "constructor field",
+            "tuple destructure",
+            "match binding",
+            "ADT sub-pattern bind",
+        }
+        if site in runtime_guarded_sites:
             self.summary.tier3_runtime += 1
             self._record_obligation(decl.name, "nat_bind", value_node, status)
         else:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/verifier.py` around lines 2063 - 2072, The current code treats all
non-let-binding sites as tier3_unguarded E504, but the guard plumbing now covers
additional runtime-guarded sites: call arguments, constructor fields, tuple
destructures, match bindings, and ADT sub-pattern extraction. Update the
conditional logic in the else block to explicitly check for these
runtime-guarded sites and increment tier3_runtime with a tier3_runtime status
(similar to the let binding case), rather than treating them as unguarded E504.
Only effect-operation arguments should remain as the unguarded E504 case. You
will need to modify the condition structure to check for specific site values
corresponding to call arguments, constructor fields, tuple destructures, match
bindings, and ADT sub-pattern extraction before falling back to the unguarded
behavior.
vera/codegen/registration.py (1)

168-172: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Built-in MdHeading is missing nat_fields metadata, so its Nat field is unguarded at runtime.

You added nat_fields generation for user ADTs (Line 255 onward), but built-ins are pre-seeded in _register_builtin_adts. MdHeading has a concrete Nat field at Line 170 and currently carries no nat_fields, so constructor-site runtime narrowing guard logic won’t trigger there.

Suggested fix
         self._adt_layouts["MdBlock"] = {
             ...
             "MdHeading": ConstructorLayout(
                 tag=1,
                 field_offsets=((8, "i64"), (16, "i32_pair")),
                 total_size=24,
+                nat_fields=(True, False),
             ),
             ...
         }

Also applies to: 255-279

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/codegen/registration.py` around lines 168 - 172, The MdHeading built-in
type defined in the _register_builtin_adts function is missing the nat_fields
metadata parameter in its ConstructorLayout definition. The MdHeading type has a
Nat field (i32_pair at offset 16) that requires runtime narrowing guard logic,
but without nat_fields metadata this guard won't be applied. Add the nat_fields
parameter to the MdHeading ConstructorLayout to specify which fields contain Nat
values, similar to how nat_fields generation is handled for user ADTs starting
at line 255. This ensures that Nat fields are properly guarded at runtime for
built-in types just as they are for user-defined types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/11-compilation.md`:
- Line 56: The paragraph at the specified location mentions that the
effect-operation argument guard gap and the use of generic trap reporting
instead of a dedicated trap kind are deferred to issue `#754`, but section 11.17
(the limitations summary section) currently concludes with "No open
limitations." You need to update section 11.17 to either add a list of these
residual limitations (specifically the effect-operation binding site guard gap
and the generic trap reporting limitation) or modify the closing statement to
acknowledge that there are still open limitations tracked in `#754` rather than
claiming no limitations remain.

In `@TESTING.md`:
- Line 62: In the TESTING.md file on line 62, within the test_verifier.py row,
locate the `#749` section that mentions "IndexExpr/InterpolatedString
container-recursion pins" and replace the term "container-recursion" with
"walker-recursion" to accurately reflect how PR `#749` describes these pins. This
correction ensures the coverage note accurately represents the recursion type
being tested rather than using misleading terminology.

In `@tests/test_verifier.py`:
- Around line 2418-2436: The test test_imported_ctor_generic_field_nat_obligated
currently only verifies the violated case where a generic constructor field
instantiation fails the obligation. Add a companion test method that checks the
discharged/satisfied case by using a requires clause that actually satisfies the
obligation (such as requires(`@Int.0` >= 0) instead of requires(true)), and verify
that the obligations list contains zero violated entries in this case. This
ensures regression coverage for imported generic-field instantiation so it is
not always treated as violated.

In `@vera/codegen/registration.py`:
- Around line 39-46: The code in the `_fn_nat_params` assignment only recognizes
direct `NamedType("Nat")` instances but does not resolve type aliases or
refinements that ultimately resolve to `Nat`. For each parameter in
`decl.params`, resolve the parameter type through any aliases or refinements
first before checking if it equals `Nat`, then use that resolved type in the
tuple comprehension instead of just checking the raw parameter directly. Apply
the same fix to the similar code handling `nat_fields` around line 269.

In `@vera/verifier.py`:
- Around line 1951-1953: When scrutinee_z3 is None in the conditional block
where lit_args is also None, the current code silently returns and loses the
narrowing obligation for `@Nat` field bindings. Instead of returning early, record
a Tier-3 nat-bind outcome at the match or sub-pattern site to ensure the
runtime-guarded narrowing is captured in diagnostics and the verification
summary. This ensures that unprojectable sub-pattern narrowings are properly
tracked rather than being dropped.
- Around line 533-535: The constructor harvesting loop (iterating through
adt.constructors.items() and adding to _module_constructors with setdefault) is
collecting all constructors from all modules without filtering for visibility or
import status, causing the wrong constructor variant to be used when multiple
modules define constructors with the same name. Apply the same
public/import-name filtering logic that is used for functions when adding
constructors in this loop, or alternatively change the dictionary key structure
from a bare ctor_name to include the resolved module and declaration context so
that lookups can correctly disambiguate between constructors with identical
names from different modules. Look for how function filtering is implemented
elsewhere in the verifier to apply the same pattern to constructor filtering.
- Around line 2032-2035: The `_record_nat_bind_tier3()` method expects the
caller to have already incremented `summary.total` by one before the call, as it
subtracts one for unguarded sites. The direct call to `_record_nat_bind_tier3()`
in the tuple destructure case (when sort is None or idx is None) is missing this
prerequisite increment. Add an increment to `summary.total` by one immediately
before calling `_record_nat_bind_tier3()` to ensure the method's internal logic
correctly nets out the obligation count instead of corrupting the summary.
- Around line 1687-1694: The call to `_obligate_subpattern_narrowings()` in the
constructor pattern arm is being executed before the pattern condition
(pat_cond) is added to the SMT solver's path conditions, which allows Z3 to
consider invalid cases and produce false E503 failures. Ensure that the
`_obligate_subpattern_narrowings()` call is wrapped within the same
constructor-pattern guard condition that is used to protect the arm body proof,
so that pat_cond is already present in smt._path_conditions when the sub-pattern
field obligations are proven.

---

Outside diff comments:
In `@vera/codegen/registration.py`:
- Around line 168-172: The MdHeading built-in type defined in the
_register_builtin_adts function is missing the nat_fields metadata parameter in
its ConstructorLayout definition. The MdHeading type has a Nat field (i32_pair
at offset 16) that requires runtime narrowing guard logic, but without
nat_fields metadata this guard won't be applied. Add the nat_fields parameter to
the MdHeading ConstructorLayout to specify which fields contain Nat values,
similar to how nat_fields generation is handled for user ADTs starting at line
255. This ensures that Nat fields are properly guarded at runtime for built-in
types just as they are for user-defined types.

In `@vera/verifier.py`:
- Around line 2063-2072: The current code treats all non-let-binding sites as
tier3_unguarded E504, but the guard plumbing now covers additional
runtime-guarded sites: call arguments, constructor fields, tuple destructures,
match bindings, and ADT sub-pattern extraction. Update the conditional logic in
the else block to explicitly check for these runtime-guarded sites and increment
tier3_runtime with a tier3_runtime status (similar to the let binding case),
rather than treating them as unguarded E504. Only effect-operation arguments
should remain as the unguarded E504 case. You will need to modify the condition
structure to check for specific site values corresponding to call arguments,
constructor fields, tuple destructures, match bindings, and ADT sub-pattern
extraction before falling back to the unguarded behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 983b4f39-8877-459e-9070-5766cc6feee0

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb53ac and b781a09.

⛔ Files ignored due to path filters (7)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • tests/conformance/ch04_nat_binding.vera is excluded by !**/*.vera
  • tests/conformance/ch09_async.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (29)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • spec/11-compilation.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/README.md
  • vera/__init__.py
  • vera/checker/__init__.py
  • vera/checker/calls.py
  • vera/checker/core.py
  • vera/checker/expressions.py
  • vera/cli.py
  • vera/codegen/api.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/registration.py
  • vera/obligations/session.py
  • vera/smt.py
  • vera/verifier.py
  • vera/wasm/calls.py
  • vera/wasm/context.py
  • vera/wasm/data.py
  • vera/wasm/operators.py

Comment thread spec/11-compilation.md Outdated
Comment thread TESTING.md Outdated
Comment thread tests/test_verifier.py
Comment thread vera/codegen/registration.py
Comment thread vera/verifier.py Outdated
Comment thread vera/verifier.py Outdated
Comment thread vera/verifier.py Outdated
Comment thread vera/verifier.py
The multi-agent /review-pr (PR #756) found no critical code bugs; this
applies the actionable findings.

Comment accuracy — the Step-5 codegen guards post-dated the Step-3 verifier
docstrings, leaving stale claims the comment-analyzer caught:
- `_walk_for_nat_binding_obligations`, `_check_nat_binding_obligation_term`,
  and `_obligate_destructure_narrowings` no longer say "codegen guards only
  the let site" / "no runtime guard yet" — codegen now guards five sites, so
  the E504 is a static-unverifiability signal, not a no-guard claim.
- `_emit_nat_bind_guard` cites the live tracker #754 (was "#749 item 4").
- `_module_constructors` bare-name first-writer-wins keying and
  `ConstructorLayout.nat_fields`'s parallel-length invariant are documented.
- `_obligate_subpattern_narrowings` notes nested sub-patterns aren't
  recursed (tracked as #754).

Tests:
- the cross-module imported-ctor runtime guard (#747 site 4) was emitted but
  untested — add a guard-presence + negative-traps pair.
- add negative-traps run-tests for the tuple-destructure and ADT sub-pattern
  guards (previously only call-arg had one), proving guard *semantics*, not
  just emission.
- the `_narrows_into_nat` parity test now asserts the soundness-relevant
  implication (codegen guards must be a superset of verifier obligations)
  rather than equality, and gains FnCall corpus entries — which surfaced
  that codegen over-guards `string_length` (a sound, harmless over-guard).

Two further residuals tracked as #754/#755 comments (nested sub-pattern
narrowing; the `Int <op> Nat`-typed-as-`Nat` destructure interaction).

Co-Authored-By: Claude <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
vera/verifier.py (2)

97-108: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Populate missing checker side-tables independently.

Line 97 only collects artifacts when both tables are omitted. If a caller supplies expr_types but leaves expr_target_types=None (or vice versa), the missing table becomes {} in ContractVerifier, silently disabling either projection/source-type obligations or generic-instantiation @Nat targets.

Proposed fix
-    if expr_types is None and expr_target_types is None:
+    if expr_types is None or expr_target_types is None:
         # `#747`: when the caller didn't supply the checker's semantic-type
         # side-tables, collect them here so a bare verify() matches the CLI
         # (cmd_verify) and LSP (VerificationSession) paths — both of which
         # thread them — keeping the warm/cold differential oracle and any
         # external caller consistent.  Lazy import avoids a module cycle.
         from vera.checker import typecheck_with_artifacts
         _diags, _arts = typecheck_with_artifacts(
             program, source, file=file, resolved_modules=resolved_modules,
         )
-        expr_types = _arts.expr_semantic_types
-        expr_target_types = _arts.expr_target_types
+        if expr_types is None:
+            expr_types = _arts.expr_semantic_types
+        if expr_target_types is None:
+            expr_target_types = _arts.expr_target_types
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/verifier.py` around lines 97 - 108, The condition at line 97 only
collects artifacts when both expr_types and expr_target_types are None, but this
causes issues when a caller supplies only one of these tables and leaves the
other as None, resulting in the missing table being silently set to an empty
dictionary in ContractVerifier. Change the condition to check if either
expr_types is None OR expr_target_types is None, then after calling
typecheck_with_artifacts, conditionally populate only the missing tables: if
expr_types is None, set it to _arts.expr_semantic_types, and if
expr_target_types is None, set it to _arts.expr_target_types.

2076-2084: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stop saying guarded projection sites are unguarded.

The new comments at Lines 1458-1460 and 2029-2030 say codegen still guards projection/destructure sites at runtime, but this warning still tells users the narrowing “is not runtime-guarded” and “neither statically proven nor runtime-checked”. Split the message by site, or use wording that describes the static E504 gap without contradicting the runtime guard coverage.

Also applies to: 2108-2120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/verifier.py` around lines 2076 - 2084, The docstring and warning message
for the Tier-3 nat_bind outcome handling contains contradictory information.
While comments at lines 1458-1460 and 2029-2030 indicate that codegen guards
projection/destructure sites at runtime, the current message in this section
states the narrowing "is not runtime-guarded" and "neither statically proven nor
runtime-checked", which contradicts the runtime guard coverage. Revise the
warning message to either split the explanation by site type (distinguishing
guarded let sites from unguarded non-let sites with appropriate messaging for
each) or reword it to accurately describe the static E504 gap without falsely
claiming these sites lack runtime guards when they actually do have them for
certain site types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@vera/verifier.py`:
- Around line 97-108: The condition at line 97 only collects artifacts when both
expr_types and expr_target_types are None, but this causes issues when a caller
supplies only one of these tables and leaves the other as None, resulting in the
missing table being silently set to an empty dictionary in ContractVerifier.
Change the condition to check if either expr_types is None OR expr_target_types
is None, then after calling typecheck_with_artifacts, conditionally populate
only the missing tables: if expr_types is None, set it to
_arts.expr_semantic_types, and if expr_target_types is None, set it to
_arts.expr_target_types.
- Around line 2076-2084: The docstring and warning message for the Tier-3
nat_bind outcome handling contains contradictory information. While comments at
lines 1458-1460 and 2029-2030 indicate that codegen guards
projection/destructure sites at runtime, the current message in this section
states the narrowing "is not runtime-guarded" and "neither statically proven nor
runtime-checked", which contradicts the runtime guard coverage. Revise the
warning message to either split the explanation by site type (distinguishing
guarded let sites from unguarded non-let sites with appropriate messaging for
each) or reword it to accurately describe the static E504 gap without falsely
claiming these sites lack runtime guards when they actually do have them for
certain site types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 241f30c9-84ba-4e13-9fd6-e7b51f9af7dd

📥 Commits

Reviewing files that changed from the base of the PR and between b781a09 and a2a9c9a.

📒 Files selected for processing (7)
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/codegen/api.py
  • vera/verifier.py
  • vera/wasm/operators.py

Eight findings from the initial CodeRabbit review, all valid:

- **`summary.total` accounting** — `_obligate_destructure_narrowings`'s
  Tier-3 branch called `_record_nat_bind_tier3` (which nets out a non-let
  site by subtracting one) without the matching `+1`, corrupting the
  obligation count.  The same `+1` is applied to the new sub-pattern Tier-3
  path below.
- **imported-ctor harvest** now filters to PUBLIC data types + the
  import-name filter (mirroring the function harvest), so a private or
  unimported same-named constructor can't shadow the resolved one with the
  wrong field types.
- **match-arm sub-pattern obligation** is now proven under the arm's
  discriminant condition (`is-<Ctor>(scrutinee)`) rather than before it, so
  Z3 can't witness a negative accessor in a branch that never reads it (a
  potential false E503).
- an **opaque, untranslatable sub-pattern scrutinee** now records a Tier-3
  obligation + E504 instead of silently dropping the narrowing.
- **runtime-guard metadata** (`_fn_nat_params`, `ConstructorLayout.nat_fields`)
  now resolves `@Nat` through `type X = Nat` aliases and refinement bases, so
  an alias/refinement-typed @nat formal or field is guarded too.

Docs/tests: §11.17 now lists the #754 residual (was "No open limitations");
TESTING.md says "walker-recursion"; added the discharged companion for the
imported generic-constructor narrowing.

Co-Authored-By: Claude <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ROADMAP.md (1)

23-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim the stale #747 status text.

This row still says the guard backs only the let site and that imported-constructor lookup is still needed. That is now out of date; the only remaining runtime gap is the effect-operation argument, tracked in #754. Please rewrite the row to reflect the current state.

♻️ Suggested fix
-| [`#747`](https://github.com/aallan/vera/issues/747) | `@Nat` narrowing at projection binding sites | `let`/argument/effect-operation-argument/constructor-field/match-bind/literal-destructure narrowing is now obligation-checked (`#552`); ADT sub-pattern binds, non-literal destructures, generic constructor fields, effect-operation formals, and function formals instantiated to `@Nat`, and imported ADT constructors still need scrutinee/call-site type inference or imported-module constructor lookup to obligate, and the Tier-3 runtime guard backs only the let site. |
+| [`#747`](https://github.com/aallan/vera/issues/747) | `@Nat` narrowing at projection binding sites | `let`/argument/effect-operation-argument/constructor-field/match-bind/literal-destructure narrowing is now obligation-checked (`#552`); ADT sub-pattern binds, non-literal destructures, generic constructor fields, effect-operation formals, and function formals instantiated to `@Nat`, and imported ADT constructors are now covered. The only remaining runtime gap is the effect-operation argument, tracked in `#754`. |

Based on the PR summary and the updated spec text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ROADMAP.md` at line 23, Update the row for issue `#747` in ROADMAP.md to remove
stale information. The current text incorrectly states that the Tier-3 runtime
guard backs only the `let` site and that imported-constructor lookup is still
needed. Replace this outdated content to accurately reflect that these have been
addressed, and specify that the only remaining runtime gap is now the
effect-operation argument, which is tracked in issue `#754`. Ensure the updated
text aligns with the current PR summary and the latest spec documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_verifier.py`:
- Around line 2465-2467: The test is currently only asserting that no `nat_bind`
obligations are violated, but it does not verify that a `nat_bind` obligation
with verified status actually exists. Add an additional assertion that
explicitly checks for the presence of at least one obligation where the kind is
"nat_bind" and the status is "verified". This should be done alongside the
existing assertion that checks for no violated nat_bind obligations to ensure
the regression test properly validates that the discharged path both emits and
verifies the nat_bind obligation as intended.

In `@vera/verifier.py`:
- Around line 2014-2016: The calls to _record_nat_bind_tier3() for ADT
sub-pattern bind and tuple-destructure sites (found in the methods handling
these patterns around the referenced lines) incorrectly treat these
codegen-guarded projection sites as unguarded, causing false E504 errors. Modify
these _record_nat_bind_tier3() calls to skip recording effect-operation
arguments through this path, or add a parameter to indicate these sites are
codegen-guarded rather than unguarded. Ensure that effect-operation arguments
are not downgraded to tier3_unguarded at these guarded projection sites, keeping
them on the unguarded path until the deferred runtime guard implementation is
complete.
- Around line 537-549: The condition at line 547 that checks if ctor_name is in
name_filter does not account for selective imports of data type names
themselves. When a data type name like List is imported via selective import,
its constructors should automatically be available. Modify the condition from
checking only if ctor_name is in name_filter to also check if adt_name (the
parent data type) is in name_filter, so that importing a data type declaration
via its name makes all of its constructors available in _module_constructors
through the setdefault call.

---

Outside diff comments:
In `@ROADMAP.md`:
- Line 23: Update the row for issue `#747` in ROADMAP.md to remove stale
information. The current text incorrectly states that the Tier-3 runtime guard
backs only the `let` site and that imported-constructor lookup is still needed.
Replace this outdated content to accurately reflect that these have been
addressed, and specify that the only remaining runtime gap is now the
effect-operation argument, which is tracked in issue `#754`. Ensure the updated
text aligns with the current PR summary and the latest spec documentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4ca36987-27b0-45a0-a72b-1a92f373618b

📥 Commits

Reviewing files that changed from the base of the PR and between a2a9c9a and 675310a.

📒 Files selected for processing (6)
  • ROADMAP.md
  • TESTING.md
  • spec/11-compilation.md
  • tests/test_verifier.py
  • vera/codegen/registration.py
  • vera/verifier.py

Comment thread tests/test_verifier.py Outdated
Comment thread vera/verifier.py
Comment thread vera/verifier.py Outdated
aallan and others added 2 commits June 17, 2026 21:41
The `compute` async example narrowed an `@Int` into `async`'s generic
formal instantiated to `@Nat` — a narrowing #747 newly obligates and #755
spuriously induces (the checker types `@Int.n * k` as `@Nat`).  Switching
its parameters to `@Nat` keeps the async/await demonstration intact while
sidestepping the #755 mis-typing, so `check_spec_examples` (CI lint, not
run in pre-commit) passes.

Co-Authored-By: Claude <[email protected]>
Three findings from CodeRabbit's re-review of the round-1 fixes:

- tier3 accounting — `_record_nat_bind_tier3` treated every non-`let` site
  as unguarded (E504), but #747's codegen guards the tuple-destructure,
  top-level match-bind, and ADT-sub-pattern sites too. A
  `_GUARDED_NAT_BIND_SITES` set now routes those to `tier3_runtime` (a
  guarded runtime check) instead of a false E504; only the effect-op
  argument and generic value-flow sites stay unguarded. The
  if-expr-destructure test asserts the guarded outcome.
- imported-ctor filter — the round-1 public/import-name filter was too
  strict: `import m(List)` names the data type, not its `Cons`/`Nil`
  ctors, so they were skipped. A ctor is now harvested when the import
  names it OR its data type (or on a wildcard import).
- the imported generic-ctor discharged test now asserts the `nat_bind`
  obligation is present and `verified`, not merely "not violated".

Also deletes the now-completed #747 ROADMAP row (the residual is tracked
in #754 / KNOWN_ISSUES; the HISTORY row landed with the release).

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Outside-diff (ROADMAP.md): fixed in e207a7d — the stale #747 row is deleted entirely (this PR completes #747; the HISTORY row landed with the release and the effect-op-arg / trap-kind residual is tracked in #754).

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Confirmed — deleting the row entirely is the right call given #747 is closed by this PR. The residual scope (effect-op-arg guard + dedicated trap kind) is cleanly scoped to #754. Thanks for the pointer to the commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vera/verifier.py`:
- Around line 126-136: The _record_nat_bind_tier3() function currently
classifies all Tier-3 narrowings for "call argument" and "constructor field"
sites the same way, treating both concrete and generic-instantiated forms as
unguarded and reporting them as E504. However, concrete `@Nat` function formals
and constructor fields are actually runtime-guarded by codegen and should be
distinguished from the generic-instantiated value-flow cases that remain
unguarded. Modify _record_nat_bind_tier3() to accept and pass a runtime_guarded
parameter that should be True for concrete forms and False for
generic-instantiated forms and effect-operation arguments, ensuring the correct
distinction is preserved when reporting narrowing issues.
- Around line 2090-2099: The current code records only one Tier-3 obligation for
unprojectable tuple component narrowing, but when a tuple contains multiple `@Nat`
components, each should be counted separately to match the behavior of the
literal and projectable paths. Modify the code to iterate through the components
in the narrowing constraints and record or count a Tier-3 obligation for each
`@Int` -> `@Nat` narrowing found, rather than recording just once. This ensures that
let Tuple<`@Nat`, `@Nat`> = opaque_tuple() correctly counts both Nat components as
guarded runtime obligations instead of undercounting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4b5b7628-cce7-4e30-9cd3-d60c87366bcf

📥 Commits

Reviewing files that changed from the base of the PR and between 8c84025 and e207a7d.

📒 Files selected for processing (4)
  • ROADMAP.md
  • TESTING.md
  • tests/test_verifier.py
  • vera/verifier.py
💤 Files with no reviewable changes (1)
  • ROADMAP.md

Comment thread vera/verifier.py Outdated
Comment thread vera/verifier.py Outdated
aallan and others added 2 commits June 17, 2026 22:08
…(ruff S112)

The accessor guard in `_obligate_destructure_narrowings` was unreachable
(`i` is filtered into `narrowing` against `source_args`, whose length
matches the tuple sort's fields) and inconsistent with the unguarded
sub-pattern accessor.  Removing it clears the lone `ruff --select S` (S112)
violation, which CI's lint job runs beyond the default rules — latent until
the §9.5.4 spec fix let the lint job reach the security check.

Co-Authored-By: Claude <[email protected]>
…lassification

CodeRabbit (PR #756) flagged two issues in _record_nat_bind_tier3:

1. Classifying guarded-vs-unguarded by the coarse `site` string reported
   every Tier-3 "call argument" / "constructor field" narrowing as an
   unguarded E504 -- including the *concrete* @nat forms that codegen DOES
   runtime-guard (wasm/calls.py, wasm/data.py). Only the generic-instantiated
   value-flow cases (whose @nat erases to i64) and effect-op arguments
   (unguarded, #754) should warn.

2. The untranslatable tuple-destructure fallback recorded a single Tier-3
   obligation even when the destructure binds multiple @nat components, so
   `let Tuple<@nat, @nat> = opaque()` undercounted.

Fix: _record_nat_bind_tier3 now takes an explicit `guarded: bool` the caller
supplies from the formal/field type (_is_nat_type), replacing the
_GUARDED_NAT_BIND_SITES site-string set. Call-arg and ctor-field pass
guarded=self._is_nat_type(formal/field_ty) -- concrete guarded, generic not;
effect-op-arg passes guarded=False; the let/destructure/match/sub-pattern
sites keep the guarded default. The destructure fallback records one Tier-3
per @nat component, matching the projectable path.

Updates the E504 warning/rationale text (references #754 for the residual
unguarded sites, lists the actual guarded site set), spec chapter 6 and
TESTING.md to match, and the three affected tests:
- test_if_expr_destructure_tier3_runtime: 2 per-component obligations
- test_non_let_tier3_narrowing_warns_unguarded: rewritten onto the effect-op
  argument, the genuinely-unguarded site (the old nat_to_int(array_length())
  call-arg is now codegen-guarded by this PR's Step 5)
- test_overall_tier_counts: 256/28/284, 0 tier3_unguarded (string_utilities'
  concrete call-arg narrowings are now correctly codegen-guarded tier3_runtime)

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 13: The arithmetic in the ruff findings summary is incorrect. Locate the
text "~176 findings had accumulated silently" in the CHANGELOG entry and change
the approximate total from "~176" to "~175" to accurately reflect the sum of the
134 auto-fixed findings and 41 hand-triaged findings that are explicitly
mentioned in the same entry.

In `@HISTORY.md`:
- Line 3: The roll-up footer line in HISTORY.md is out of sync with the header
statement. The header indicates 77 active development days, but the footer still
reports 76 active days and 173 tagged releases. Update the footer line to change
the active development days count from 76 to 77 to match the header.
Additionally, verify that the tagged releases count of 173 in this footer
matches the releases figure in README.md to ensure the two files remain
synchronized per coding guidelines.

In `@TESTING.md`:
- Line 62: In the test_verifier.py row within the `@Nat` binding-site narrowing
obligation section, the phrase describing which narrowings surface the E504
unguarded warning currently mentions both effect-op-arg and generic-instantiated
narrowings. Since `#747` now covers generic-instantiated `@Nat` formals, remove the
reference to generic-instantiated narrowings from the residual warning note,
leaving only the effect-op-arg scope that is tracked in the `#754` PR objective.

In `@tests/test_codegen.py`:
- Around line 16533-16547: The `_body` method uses plain substring matching with
`wat.find(f"(func ${fn}")` which can incorrectly match similarly-prefixed WAT
symbols (e.g., matching `$gcall_helper` when searching for `$gcall`). Replace
the substring find calls with a boundary-safe regex pattern that matches the
function name with word boundaries to ensure you only extract the correct
function body. Use regex to search for the opening function definition pattern
and closing boundary in a way that prevents false matches on similarly-named
symbols.

In `@tests/test_verifier.py`:
- Around line 2431-2445: The test method
test_imported_ctor_concrete_nat_field_discharged is not pinning down that the
nat_bind obligation is actually emitted and verified. Currently it only asserts
the absence of violated nat_bind obligations and error diagnostics, which means
the test would pass even if no nat_bind obligations were emitted at all. Add an
assertion to verify that at least one nat_bind obligation with status equal to
"verified" exists in result.obligations, similar to how the generic discharged
companion test handles this, to ensure the test catches real compiler behavior
rather than just the absence of failures.

In `@vera/codegen/registration.py`:
- Around line 277-303: The _type_resolves_to_nat method resolves alias bodies by
name only without applying type argument substitution, so generic aliases like
type Id<T> = T used as Id<Nat> resolve to T instead of Nat. When retrieving the
alias from self._type_aliases.get(te.name) and before assigning it to te for the
next loop iteration, check if te (the NamedType) has type arguments, and if so,
apply those type argument substitutions to the alias body before using it. This
ensures that generic type aliases with concrete type arguments are properly
resolved to their instantiated types rather than their unsubstituted template
forms.

In `@vera/verifier.py`:
- Around line 97-108: The condition checking whether to recompute artifacts uses
AND operator to verify that both expr_types and expr_target_types are None, but
it should use OR to check if either one is missing. Change the condition from
expr_types is None and expr_target_types is None to expr_types is None or
expr_target_types is None. Additionally, after calling typecheck_with_artifacts,
selectively assign only the missing tables by adding conditional checks: assign
expr_types from _arts.expr_semantic_types only if expr_types is None, and assign
expr_target_types from _arts.expr_target_types only if expr_target_types is
None. This ensures that partially-provided tables are completed consistently and
`#747` narrowing checks fire correctly.

In `@vera/wasm/data.py`:
- Around line 218-225: The runtime guard detection at lines 224, 454, and 569
uses a simple string comparison `type_name == "Nat"` which fails to detect Nat
aliases and refinement-wrapped Nat types, leading to missed guards. Replace the
direct `type_name == "Nat"` checks with an alias-aware Nat detection method that
properly resolves aliases and refinement types, similar to how constructor and
call metadata paths already handle Nat detection. Apply this change consistently
across all three guard emission sites where `_emit_nat_bind_guard` is called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 14128e2a-75e0-4372-ae40-2ec30573a60b

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb53ac and 8c0bb75.

⛔ Files ignored due to path filters (7)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • tests/conformance/ch04_nat_binding.vera is excluded by !**/*.vera
  • tests/conformance/ch09_async.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (31)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • spec/06-contracts.md
  • spec/09-standard-library.md
  • spec/11-compilation.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/README.md
  • vera/__init__.py
  • vera/checker/__init__.py
  • vera/checker/calls.py
  • vera/checker/core.py
  • vera/checker/expressions.py
  • vera/cli.py
  • vera/codegen/api.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/registration.py
  • vera/obligations/session.py
  • vera/smt.py
  • vera/verifier.py
  • vera/wasm/calls.py
  • vera/wasm/context.py
  • vera/wasm/data.py
  • vera/wasm/operators.py

Comment thread CHANGELOG.md Outdated
Comment thread HISTORY.md
Comment thread TESTING.md Outdated
Comment thread tests/test_codegen.py
Comment thread tests/test_verifier.py
Comment thread vera/codegen/registration.py
Comment thread vera/verifier.py Outdated
Comment thread vera/wasm/data.py
…e-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]>
@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
vera/verifier.py (1)

1705-1749: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shadow pattern bindings when the scrutinee is not translatable.

If smt.translate_expr(expr.scrutinee) returns None, arm_env stays as the outer slot_env, but Vera pattern bindings are still in scope. Obligations inside the arm can then read a stale outer @T.0 or miss the new binding entirely. Use fresh pattern slots in this fallback path, matching the LetDestruct stale-binding guard.

Proposed direction
             scrutinee_z3 = smt.translate_expr(expr.scrutinee, slot_env)
             for arm in expr.arms:
                 arm_env = slot_env
                 pat_cond = None
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
                     if bound is not None:
                         arm_env = bound
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                else:
+                    arm_env = self._bind_fresh_pattern_slots(
+                        smt, arm.pattern, slot_env,
+                    )
def _bind_fresh_pattern_slots(
    self,
    smt: SmtContext,
    pattern: ast.Pattern,
    env: SlotEnv,
) -> SlotEnv:
    if isinstance(pattern, ast.BindingPattern):
        slot_name = smt._type_expr_to_slot_name(pattern.type_expr)
        slot_val = self._fresh_slot_var(smt, pattern.type_expr)
        if slot_name is not None and slot_val is not None:
            return env.push(slot_name, slot_val)
        return env

    if isinstance(pattern, ast.ConstructorPattern):
        cur = env
        for sub_pattern in pattern.sub_patterns:
            cur = self._bind_fresh_pattern_slots(smt, sub_pattern, cur)
        return cur

    return env
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vera/verifier.py` around lines 1705 - 1749, When the scrutinee cannot be
translated to Z3 (scrutinee_z3 is None), pattern bindings in match arms are not
properly shadowed, causing stale or missing bindings in downstream obligations.
Add the proposed _bind_fresh_pattern_slots method to the Verifier class to
recursively create fresh slot variables for BindingPattern and
ConstructorPattern cases. In the match expression handling where arm_env is
initialized to slot_env, check if scrutinee_z3 is None and call
_bind_fresh_pattern_slots to update arm_env with fresh pattern slots instead of
leaving it as the outer slot_env, ensuring pattern bindings are properly
shadowed similar to how LetDestruct handles this case.
ROADMAP.md (1)

73-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim the #749 row to the remaining work.

The row still lists the IndexExpr / InterpolatedString pins and the _fresh_slot_var / _narrows_into_nat tests as future work, but the PR summary says those items were folded in and the residual trap-kind work is tracked in #754. Please delete the completed items or drop the row if nothing remains.
As per coding guidelines, completed items should be deleted from ROADMAP.md once they land, with the residual tracked separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ROADMAP.md` at line 73, In the ROADMAP.md file, locate the `#749` row and
remove the completed items from it: the container IndexExpr/InterpolatedString
test pins, the _fresh_slot_var alias unit-test, and the _narrows_into_nat
differential test. Either trim the row to only include the dedicated trap-kind
work if it warrants separate tracking, or delete the entire `#749` row if the
remaining work is already captured in the related `#754` issue, following the
guideline that completed roadmap items should be removed once they land with
residual work tracked separately.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ROADMAP.md`:
- Line 11: There is a discrepancy between the test count reported in ROADMAP.md
(line 11 shows 4,443 tests) and the test count in README.md's project-status
section (which shows 4,436 tests). Verify the actual current test count from the
live test suite, then update both ROADMAP.md and README.md to reflect the same
correct count, ensuring consistency across all documentation references to test
totals.

In `@spec/06-contracts.md`:
- Line 248: The final sentence describing which cases surface E504 warnings is
ambiguous about unguarded narrowing sites. The sentence currently mixes
effect-operation arguments and generic formals/fields in a way that suggests the
latter only applies when `@Nat` erases to i64, but both are actually separate
residual classes that surface E504. Revise the sentence to explicitly call out
two distinct unguarded classes: effect-operation arguments as one site-kind
residual, and generic-instantiated formals/fields as a separate residual, making
clear both surface E504 warnings independently rather than treating them as
conditionally related.

In `@spec/11-compilation.md`:
- Line 596: The section summary in §11.17 currently states that only the
effect-operation argument is an uncovered binding site for the `@Nat` >= 0
narrowing invariant, but the detailed coverage in §11.2.1 identifies an
additional residual: generic-instantiated `@Nat` formals and fields on the E504
path are also left uncovered. Update the summary in §11.17 to reflect both
residuals tracked in issue `#754` rather than just the effect-operation argument
site-kind gap, ensuring the summary accurately represents all coverage gaps
documented in the detailed sections.

In `@tests/test_codegen.py`:
- Around line 16552-16571: The guard-shape assertions in
test_param_destructure_nat_components_guarded and
test_subpattern_nat_bind_guarded are incomplete. Currently each test only
asserts the presence of "i64.lt_s" in the compiled WAT output, but this can miss
regressions where the comparison is emitted without the trap edge. Strengthen
each assertion by adding an additional check for "unreachable" in the same WAT
body, ensuring both the comparison instruction and the unreachable trap
instruction are present together, which verifies the complete guard
implementation.
- Around line 16572-16579: The test_toplevel_match_nat_bind_guarded test only
verifies the structural WAT output by checking for the presence of the i64.lt_s
instruction, but does not test runtime trapping behavior. Add a runtime
negative-input test case that calls the gmatch function with an invalid input (a
negative integer that violates the `@Nat` guard constraint) and verifies that the
function traps or raises an exception at runtime. This ensures that the
narrowing and guard semantics are actually enforced during execution, not just
present in the compiled code structure.

In `@vera/codegen/registration.py`:
- Around line 271-275: The nat_fields tuple in manually registered concrete-Nat
built-ins like MdHeading is not being populated and remains as an empty default
tuple, which prevents the proper validation guards from being applied during
constructor emission. Identify where manual built-in layouts are being
registered (look for explicit ConstructorLayout instantiations outside of
_compute_constructor_layout), and for those layouts that have concrete Nat
fields (such as MdHeading with its first Nat field), populate the nat_fields
parameter with a tuple containing the indices of the Nat-type fields instead of
leaving it empty, ensuring the i64.lt_s/unreachable guard is properly generated
to prevent invalid negative values from being stored as Nat.

In `@vera/README.md`:
- Line 690: In the README.md file within the "Verification gaps that downgrade
silently" table row, the issue references `#754` and `#730` do not exist in the
aallan/vera repository. Remove these non-existent issue references or replace
them with verified tracking issue numbers if they exist. Verify that `#555`
remains correctly referenced as it is documented in KNOWN_ISSUES.md and tracks
the generic forall body verification gaps mentioned in the row description.

In `@vera/verifier.py`:
- Around line 1468-1481: The comments at the projection site handling (around
the ADT sub-pattern bind and non-literal tuple destructure discussion) describe
outdated behavior where non-let Tier-3 paths were not runtime-backed. Update
these comments to reflect that the current implementation records guarded tuple
destructure, match-bind, and ADT sub-pattern fallbacks as tier3_runtime by
passing guarded=True. Clarify that these paths are now treated as
runtime-backed/guarded in the current accounting scheme, rather than as
unguarded paths that become E504 errors. Apply the same updates to the related
comments also mentioned in the affected line ranges to maintain consistency
across all similar documentation.

---

Outside diff comments:
In `@ROADMAP.md`:
- Line 73: In the ROADMAP.md file, locate the `#749` row and remove the completed
items from it: the container IndexExpr/InterpolatedString test pins, the
_fresh_slot_var alias unit-test, and the _narrows_into_nat differential test.
Either trim the row to only include the dedicated trap-kind work if it warrants
separate tracking, or delete the entire `#749` row if the remaining work is
already captured in the related `#754` issue, following the guideline that
completed roadmap items should be removed once they land with residual work
tracked separately.

In `@vera/verifier.py`:
- Around line 1705-1749: When the scrutinee cannot be translated to Z3
(scrutinee_z3 is None), pattern bindings in match arms are not properly
shadowed, causing stale or missing bindings in downstream obligations. Add the
proposed _bind_fresh_pattern_slots method to the Verifier class to recursively
create fresh slot variables for BindingPattern and ConstructorPattern cases. In
the match expression handling where arm_env is initialized to slot_env, check if
scrutinee_z3 is None and call _bind_fresh_pattern_slots to update arm_env with
fresh pattern slots instead of leaving it as the outer slot_env, ensuring
pattern bindings are properly shadowed similar to how LetDestruct handles this
case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 352abdc0-57b2-4965-ae7a-82b878d105c0

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb53ac and f5a328f.

⛔ Files ignored due to path filters (7)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • tests/conformance/ch04_nat_binding.vera is excluded by !**/*.vera
  • tests/conformance/ch09_async.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (31)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • spec/06-contracts.md
  • spec/09-standard-library.md
  • spec/11-compilation.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/README.md
  • vera/__init__.py
  • vera/checker/__init__.py
  • vera/checker/calls.py
  • vera/checker/core.py
  • vera/checker/expressions.py
  • vera/cli.py
  • vera/codegen/api.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/registration.py
  • vera/obligations/session.py
  • vera/smt.py
  • vera/verifier.py
  • vera/wasm/calls.py
  • vera/wasm/context.py
  • vera/wasm/data.py
  • vera/wasm/operators.py

Comment thread ROADMAP.md Outdated
Comment thread spec/06-contracts.md Outdated
Comment thread spec/11-compilation.md Outdated
Comment thread tests/test_codegen.py
Comment thread tests/test_codegen.py Outdated
Comment thread vera/codegen/registration.py
Comment thread vera/README.md Outdated
Comment thread vera/verifier.py Outdated
…ng edit)

The round-15 docstring-accuracy fix to test_array_nat_element_subtraction_obligated
added one line after check_doc_counts had already been run, so the committed
TESTING.md line count lagged by one and the CI doc-counts gate failed. Count only.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

…ng guard (review of #756)

Independent multi-agent review of this PR surfaced that the "every narrowing
shape is statically obligated, so verified programs are sound" wording is
overstated: a `vera verify`-clean program can still store a negative `@Nat` at
two narrowing *positions* the obligation walker never reaches —

  - a function return slot:  fn f(@int -> @nat) ensures(true) { @Int.0 }
                             -> verifies clean, f(0-5) returns -5
  - a @nat component built in value position:
                             fn g(@int -> @tuple<Nat,Nat>) { Tuple(@Int.0, 5) }
                             -> verifies clean, no obligation on the component

Both confirmed firsthand and both pre-existing (present on v0.0.172) — not a
regression in #747/#756 — but this PR is where the universal-soundness wording
landed. Filed #758 for the obligation gap; this commit makes the docs honest:

- Scope the six overstated claims to "every narrowing BINDING SITE", with an
  explicit carve-out pointing at #758: CHANGELOG.md, KNOWN_ISSUES.md,
  vera/README.md, spec/06-contracts.md, spec/11-compilation.md (x2). The narrow
  claim that the two deferred *runtime-guard* residuals (#754, #757) are
  statically obligated is true and is retained.
- KNOWN_ISSUES: the "builtin @nat parameters" guard claim was incomplete —
  nat_to_int/nat_to_string take @nat, are E503-obligated, but bypass the
  _fn_nat_params guard loop (nat_to_int(0-9) returns -9 unguarded). Scoped the
  guarded set to the string/markup builtins and listed the conversion builtins
  among the unguarded-but-obligated residuals.

Also closes a test gap the review found: md_has_heading's @nat `level` guard
(round-14 #757 fold-in) was emitted but untested — its @MdBlock/@Bool signature
kept it out of the @String-returning parametrized trap test. Added a dedicated
negative-level trap test.

No compiler logic changed — docs + one test. Doc counts synced (4459).

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Independent multi-agent review (post-CR)

Ran a 5-agent review (code / tests / comments / silent-failures / type-design) over the full origin/main...HEAD diff. The core machinery was independently confirmed sound — every guarded=True classification is backed by a real codegen trap, every unguarded site emits a loud E504, and every side-table/lookup miss fails closed. Zero new soundness regressions. Findings addressed in ef8b439:

1. Overstated soundness wording (the headline). Three agents + a firsthand repro showed the "every narrowing shape is statically obligated, so verified programs are sound" claim is too strong — two narrowing positions are not obligated, so a vera verify-clean program can store a negative @Nat:

  • function return slot — fn f(@Int -> @Nat) requires(true) ensures(true) { @Int.0 } verifies clean; f(0 - 5) returns -5.
  • a @Nat component built in value positionfn g(@Int -> @Tuple<Nat, Nat>) { Tuple(@Int.0, 5) } verifies clean with no obligation on the component.

Both are pre-existing (reproduced on v0.0.172, base 7fb53ac) — not a regression in this PR — but this PR is where the universal-soundness wording landed. Filed #758 for the obligation gap and scoped the six claims (CHANGELOG, KNOWN_ISSUES, vera/README.md, spec/06, spec/11 ×2) to "every narrowing binding site", each pointing at #758. The narrow, true claim — that the two deferred runtime-guard residuals (#754, #757) are statically obligated — is retained.

2. Incomplete builtin-guard inventory. KNOWN_ISSUES.md claimed the runtime guard covers "builtin @Nat parameters", but nat_to_int/nat_to_string take @Nat, are E503-obligated, yet bypass the _fn_nat_params guard loop (nat_to_int(0 - 9) returns -9 unguarded). Scoped the guarded set to the string/markup builtins and listed the conversion builtins among the unguarded-but-statically-obligated residuals.

3. Untested guard. md_has_heading's @Nat level guard (round-14 #757 fold-in) was emitted but untested — its @MdBlock/@Bool signature kept it out of the @String-returning parametrized trap test. Added a dedicated negative-level trap test.

No compiler logic changed in this commit — docs + one test; doc counts synced.

@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

The multi-agent review of #756 surfaced four small, non-correctness follow-ups.
Filed as two issues and tracked in their natural homes:

- #759 (refactor) -> ROADMAP Tier 2 (single source of truth): single-source the
  checker/verifier span-key constructor, enforce the nat_fields/field_offsets
  length invariant via a ConstructorLayout.__post_init__ assertion, and mirror
  the expected= side-table write into _check_ability_op_call.
- #760 (testing) -> KNOWN_ISSUES Test coverage gaps: pin the #757
  generic-ctor-field runtime deferral with an assertion, symmetric with #754.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@KNOWN_ISSUES.md`:
- Line 23: The final two sentences of this KNOWN_ISSUES row conflate static
compile-time obligations with runtime-unguarded sites, making it unclear which
failures occur where. Reorganize the text to clearly separate two distinct
issues: first, identify which specific sites have static obligations that
produce E503/E504 compile errors, then separately describe which different sites
remain runtime-unguarded in unverified compiles and would trip a generic
unreachable rather than the proper requires check. This separation will prevent
overstating what fails at compile time versus what still traps at runtime.

In `@spec/06-contracts.md`:
- Line 248: The paragraph currently presents the generic function-formal calls
guard as universally protective, but this needs qualification. Add a caveat to
the sentence discussing "generic function-formal calls, which guard on the
monomorphised callee" to acknowledge that while concrete guarded call-arguments
work reliably, generic-instantiated `@Nat` formals can still erase to i64 and
surface E504 errors when the expression is untranslatable, similar to the caveat
already noted for generic-instantiated constructor fields staying unguarded.

In `@spec/11-compilation.md`:
- Line 56: The paragraph describing the Tier-3 runtime guard coverage is
incomplete. Locate the sentence that currently states "The Tier-3 runtime guard
backs every binding site except the effect-operation argument" and expand it to
also exclude generic-instantiated constructor fields from having runtime guards,
since the verifier/codegen path leaves these unguarded. Update the sentence to
mention both the effect-operation argument and constructor-field residuals as
sites where the runtime guard does not apply, ensuring consistency with section
11.17 which already reflects this split.

In `@TESTING.md`:
- Line 62: The current description in the test_verifier.py row conflates the
guarded cases with all generic function-formal call-arg handling, making it
appear that all such cases are protected by the monomorphised callee guard.
Revise the text to explicitly separate the concrete guarded cases from the
residual path where untranslatable generic-instantiated formals and fields can
still erase to i64 and surface E504 warnings. Keep the unguarded residual path
explicit in the documentation rather than collapsing it into the guarded
description, ensuring readers understand that generic instantiation does not
universally prevent E504 emissions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c3babd1-2450-4f1d-8aa2-6d34bf1b4ddd

📥 Commits

Reviewing files that changed from the base of the PR and between fbd68dc and ef8b439.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/06-contracts.md
  • spec/11-compilation.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/README.md
  • vera/verifier.py
  • vera/wasm/calls_strings.py

Comment thread KNOWN_ISSUES.md Outdated
Comment thread spec/06-contracts.md
Comment thread spec/11-compilation.md Outdated
Comment thread TESTING.md
Two of four CodeRabbit doc findings were valid:
- KNOWN_ISSUES: separated the static-obligation axis (E503/E504 at binding
  sites) from the runtime-unguarded axis (which sites store a negative
  silently) — the row had interleaved them.
- spec/11 §11.2: the "every binding site except the effect-operation argument"
  sentence omitted the generic-instantiated constructor field (also unguarded,
  #757); named both, consistent with §11.17, and fixed the ref (#757, not #754,
  for the ctor field).

The other two findings (spec/06, TESTING.md — "generic function-formal calls
can surface E504 when untranslatable") were declined as empirically false.
Generic function-formal call-args are guarded=True (verifier.py:1541, guarded on
the monomorphised callee pick$Nat), so an untranslatable arg falls to
tier3_runtime with 0 E504; only the effect-op arg and the generic-ctor-field
(guarded=False) reach E504. Verified: pick(@Nat.0, array_length(...)) -> 0 E504,
vs the same arg in Some(...) -> 1 E504.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@aallan aallan merged commit 25e4492 into main Jun 18, 2026
28 checks passed
@aallan aallan deleted the feat/747-nat-projection-narrowing branch June 18, 2026 13:50
aallan added a commit that referenced this pull request Jun 22, 2026
…ee (PR #778)

CodeRabbit outside-diff Critical (verifier.py match-arm walker): when a
`match` scrutinee is untranslatable (an effect operation, `Source.next(())`),
translate_expr returns None, so the arm kept the outer slot env. A binding
pattern still introduces a slot, so a primitive op in the arm discharged
against a STALE same-name outer: `match Source.next(()) { Some(@int) -> 1 /
@Int.0 }` under `requires(@Int.0 != 0)` verified clean while the matched field
can be 0 at runtime — a silent false-discharge, the worst #680 failure class.

Fix: when the scrutinee is opaque, shadow the arm's pattern slots via
_fresh_pattern_env (the nat-binding walker's existing helper, CR #756),
extended with a `track` flag so the fresh vars are recorded in
self._opaque_shadows — a bare fresh @int carries no `!= 0` invariant, so
without the shadow tag the division would flip from false-discharge to a
false E526. The op now falls to Tier-3 (runtime-guarded), mirroring the
untranslatable-let shadow.

Test (test-first, RED->GREEN proven against the pre-fix status):
test_opaque_match_scrutinee_shadows_arm_bindings (verified -> tier3).

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant