Skip to content

feat: @Nat binding-site narrowing obligations (#552)#748

Merged
aallan merged 11 commits into
mainfrom
feat/552-nat-binding-sites
Jun 16, 2026
Merged

feat: @Nat binding-site narrowing obligations (#552)#748
aallan merged 11 commits into
mainfrom
feat/552-nat-binding-sites

Conversation

@aallan

@aallan aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Generalises the #520 @Nat - @Nat subtraction obligation to the binding sites where a value that can be negative narrows into a @Nat slot — statically obligating let, call-argument, constructor-field, top-level match-bind, and literal-tuple-destructure narrowing (Tier-1, E503), with a runtime guard at the let site. This narrows the soundness hole where a negative @Int could be stored silently in a @Nat; the residual (projection sites, and runtime guards beyond the let site) is tracked as #747.

What's covered

Verifier (Tier-1 static obligation, E503) — a new _walk_for_nat_binding_obligations walk emits a value >= 0 proof obligation, discharged from preconditions and path conditions exactly like #520, at:

Site Example
let bindings let @Nat = @Int.0
call arguments f(@Int.0) with an @Nat formal
constructor fields MkBox(@Int.0) with a concrete @Nat field
top-level match binds match @Int.0 { @Nat -> ... }
literal-tuple destructure let Tuple<@Nat, @Nat> = Tuple(@Int.0, 5)

The trigger (_narrows_into_nat) fires when the value is not already statically @Nat, or is a pure-literal subtraction (let @Nat = 0 - 1) — the latter is typed @Nat but valued negative, and #520 deliberately defers it here. This keeps #552 disjoint from #520's @Nat - @Nat (with @Nat provenance) so the two obligations never co-fire on one site.

Codegen (Tier-3 runtime guard) — a new _emit_nat_bind_guard traps on a negative value at the canonical let site, for programs compiled without vera verify (mirrors the #520 subtraction guard).

Also fixed

A pre-existing bug in SmtContext.check_valid: it extracted the Z3 model after popping the assertion scope, so counterexamples described the base context — model_completion filled the now-unconstrained slots with arbitrary defaults (e.g. @Int.0 = 0 for the goal @Int.0 >= 0) instead of the violating assignment. Extracting the model before the pop sharpens E502, E503, and call-site precondition diagnostics alike.

Scope and deferrals

The issue's documented core — narrowing into a @Nat let binding or argument — is covered and extended (constructor fields, match binds, literal destructures, and the 0 - 1 carve-out). Two narrowing shapes from the broader "all binding sites" generalisation are deferred:

Tests

  • TestNatBindingObligation552 (17 verifier cases): all six sites, three Nat subtraction silently underflows to negative i64 — refinement-type soundness hole #520 double-emit pins, the 0 - 1 carve-out, the non-negative-literal no-fire.
  • TestNatBindingRuntimeGuard552 (4 codegen cases): runtime trap on a negative narrowing, normal return on a non-negative, guard-present / no-guard-for-already-@Nat WAT assertions.
  • tests/conformance/ch04_nat_binding.vera (level run): 16 Tier-1 obligations across let / call-arg / path-condition / destructure.
  • Example T1/T3 counts shift 255/25/280 → 256/28/284 (json.vera +1 T1; string_utilities.vera +3 T3 where nat_to_int(array_length(...)) narrows array_length's @Int result into nat_to_int's @Nat param).

Closes #552

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added @Int@Nat non-negativity (>= 0) obligations at binding-site narrowing points (including let, call/effect arguments, constructor fields, match binds, and tuple destructures).
    • When a build can’t statically verify narrowing, let-site narrowing now emits a runtime guard that traps on negative values.
  • Bug Fixes
    • Improved verifier counterexamples so diagnostics map to the actual violating assignment (including E503), and clarified related “unverified” reporting (E504).
  • Documentation
    • Updated conformance-suite size and refreshed spec/limitations and status notes to 90 programs.
  • Tests
    • Expanded verifier and codegen test coverage for the new @Nat binding-site behaviour.

aallan and others added 2 commits June 16, 2026 11:06
Generalise #520's @Nat-@Nat subtraction obligation to every binding
site where a value that can be negative narrows into a @nat slot.

Verifier (vera/verifier.py): new `_walk_for_nat_binding_obligations`
emits a Tier-1 `value >= 0` obligation (E503, ObligationKind
"nat_bind") at:
  - let bindings              `let @nat = <Int>`
  - call arguments            `f(<Int>)` with an @nat formal
  - constructor fields        `MkBox(<Int>)` with a concrete @nat field
  - top-level match binds       `match <Int> { @nat -> ... }`
  - literal-tuple destructure   `let Tuple<@nat,...> = Tuple(<Int>,...)`
discharged from preconditions and path conditions exactly like #520.

The `_narrows_into_nat` predicate fires when the value is not already
statically @nat OR is a pure-literal subtraction (`0 - 1`) — the latter
is typed @nat but valued negative, and #520 deliberately defers it
here.  This keeps #552 disjoint from #520's `@Nat - @Nat` (with @nat
origin) so the two obligations never co-fire on one site.

Codegen (vera/wasm): new `_emit_nat_bind_guard` emits a runtime
`value >= 0` trap at the canonical `let @nat = <narrowing>` site,
mirroring the #520 subtraction guard for programs compiled without
`vera verify`.  The verifier provides full static coverage at the
other binding sites.

Projection sites whose source type the verifier cannot resolve
statically (ADT sub-pattern binds, non-literal destructures) are
deferred to a follow-up (#747) rather than risk a false E503.

Also fixes a pre-existing bug in SmtContext.check_valid: it popped the
assertion scope before extracting the Z3 model, so counterexamples
described the base context (`@Int.0 = 0` for the goal `@Int.0 >= 0`)
instead of the violating assignment.  Extract the model before the
pop — sharpens E502/E503/call-site precondition diagnostics alike.

Tests: TestNatBindingObligation552 (17 verifier cases — all six sites,
three #520 double-emit pins, the `0 - 1` carve-out) +
TestNatBindingRuntimeGuard552 (4 codegen cases).  Example T1/T3 counts
shift 255/25/280 -> 256/28/284.

Co-Authored-By: Claude <[email protected]>
- tests/conformance/ch04_nat_binding.vera: let / call-arg / path-condition
  / tuple-destructure narrowing, all Tier-1 discharged (16 obligations);
  manifest entry at level "run".  Conformance suite 89 -> 90.
- spec/11 §11.2.1, vera/README, KNOWN_ISSUES: the @nat invariant is now
  obligated at let / call-arg / ctor-field / match-bind / literal-destructure
  binding sites (#552); the projection residual (ADT sub-pattern binds,
  non-literal destructures) is tracked as #747.
- ROADMAP: drop the #552 Tier-0 row; add #747 (projection residual) and
  #746 (refinement-predicate verification epic).
- CHANGELOG [Unreleased]: the #552 binding-site obligation + the
  check_valid counterexample fix.
- check_doc_counts.py: accept "A"/"An" before the conformance count (the
  article depends on how the number reads: "an 89-" but "a 90-").
- Test / conformance count bumps across TESTING.md / ROADMAP / docs.

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

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95082% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.61%. Comparing base (9eb700e) to head (ea5b116).

Files with missing lines Patch % Lines
vera/wasm/operators.py 85.71% 3 Missing ⚠️
vera/verifier.py 99.07% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #748      +/-   ##
==========================================
+ Coverage   91.48%   91.61%   +0.12%     
==========================================
  Files          70       70              
  Lines       24986    25226     +240     
  Branches      321      321              
==========================================
+ Hits        22859    23111     +252     
+ Misses       2119     2107      -12     
  Partials        8        8              
Flag Coverage Δ
javascript 65.33% <ø> (ø)
python 94.84% <97.95%> (+0.10%) ⬆️

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 commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements issue #552: @Int@Nat binding-site narrowing obligations are now checked by the verifier at let bindings, call arguments, constructor fields, effect operations, match binds, and literal tuple destructures, with WASM codegen emitting a runtime i64.lt_s/unreachable guard for unchecked builds. A new nat_bind obligation kind and error code E503 are introduced. A separate fix corrects Z3 counterexample extraction to occur before popping the solver stack. Conformance suite count advances from 89 to 90.

Changes

@Nat Binding-Site Narrowing Obligation (#552)

Layer / File(s) Summary
nat_bind obligation kind and E503 error code
vera/obligations/core.py, vera/errors.py
ObligationKind gains "nat_bind" and ObligationStatus gains "tier3_unguarded" to distinguish runtime-guarded vs unguarded untranslatable sites; ERROR_CODES gains "E503" (@Nat binding-site narrowing may be negative) and "E504" (@Nat narrowing unverified and not runtime-guarded), establishing contracts consumed by the new verifier phase and diagnostic emitter.
Z3 counterexample extraction before solver pop
vera/smt.py
SmtContext.check_valid now extracts the Z3 model immediately on sat whilst assertions are still live on the solver stack, before calling solver.pop(), fixing misleading counterexample values in E502/E503 diagnostics where prior model extraction after popping yielded unconstrained bindings.
Effect operation metadata resolution
vera/verifier.py
_register_effect now resolves declared effect operations into an operations map containing parameter types and return types, instead of storing an empty operations dict, enabling effect-operation argument narrowing checks.
Verifier narrowing-obligation walker and discharge
vera/verifier.py
Adds verifier Phase 5.7 with _walk_for_nat_binding_obligations (AST traversal emitting value >= 0 obligations with path-condition threading at let/arg/field/match-bind/literal-tuple/effect-operation sites), _lookup_constructor_info (resolves constructor field types from built-ins or user ADTs), _check_nat_binding_obligation (SMT discharge, tier accounting, E503/E504 on violation), _fresh_slot_var (invalidates stale destructuring bindings), _report_nat_binding (E503 emitter), and helper predicates _narrows_into_nat and _has_underflow_leaf to distinguish genuine narrowing from pure-literal @Nat subtraction and avoid overlap with #520.
WASM codegen runtime guard
vera/wasm/operators.py, vera/wasm/context.py
OperatorsMixin adds _emit_nat_bind_guard (emits i64.lt_s + unreachable for negative i64), _narrows_into_nat, and _has_underflow_leaf (recursively detects subtraction-tree underflow risk in pure-literal patterns); WasmContext.translate_block wraps let-binding instructions with the guard when the target is Nat and narrowing is required.
Verifier tests for #552
tests/test_verifier.py
Adds TestNatBindingObligation552 covering obligation failure/discharge/disjointness vs #520, regression patterns with wrapped pure-literal subtraction, narrowing at let/call-argument/constructor-field/match-bind/tuple-destructure/effect-operation sites, environment threading across tuple destructuring, and ADT sub-pattern behaviour pinning; updates TestSummary.test_overall_tier_counts to track tier3_unguarded obligations and adjust expected totals.
Codegen tests for #552
tests/test_codegen.py
Adds TestNatBindingRuntimeGuard552 with class-level fixtures _GUARDED_LET and _SAFE_LET, covering runtime trap on negative narrowing, value pass-through on non-negative, WAT guard opcode presence/absence inspection, untranslatable-value narrowing guard emission, non-narrowing passthrough without guard, and wrapped pure-literal subtraction trap behaviour.
Conformance test registration
tests/conformance/manifest.json
Registers ch04_nat_binding in the conformance manifest with chapter 4 metadata, run execution level, and nat_binding/binding_narrowing feature tags alongside required discharge-related features.
Specification, known-issues, and changelog documentation
spec/11-compilation.md, KNOWN_ISSUES.md, vera/README.md, CHANGELOG.md
Refines the @Nat >= 0 binding-site checking description in the spec and narrows the remaining-gap scope to ADT sub-pattern binds and unresolvable tuple destructures in known-issues and limitations tables; adds CHANGELOG entries documenting expanded @Nat >= 0 obligation checking at binding sites (with runtime guard trapping for pure-literal 0 - 1 on non-verified builds) and the Z3 counterexample extraction fix.
Roadmap and metrics updates
ROADMAP.md
Updates "Where we are" metrics to reflect 90 conformance programs and adjusted test counts; replaces the #552 roadmap entry with #747 (projected-source narrowing at projection binding sites) and #746 (refinement-type predicate verification); adds new Tier 3 item #749 for @Nat-narrowing review follow-ups including additional test pins and dedicated guard trap kind.
Conformance suite count updates (89→90)
scripts/check_doc_counts.py, scripts/build_site.py, AGENTS.md, CLAUDE.md, FAQ.md, SKILL.md, TESTING.md
Broadens check_doc_counts.py regex to accept both A and An articles when parsing conformance-suite counts from docs/index.html; updates landing-page template placeholders; advances all conformance-suite references from 89 to 90 programmes; refreshes test/conformance metrics and parametrised test counts (445→450 tests, 78→79 run levels) in TESTING.md Overview, test-files table, conformance-suite coverage, and validation/pre-commit-hooks table rows.

Sequence Diagram(s)

sequenceDiagram
    participant verifier as _verify_fn
    participant walker as _walk_for_nat_binding_obligations
    participant smt as SmtContext.check_valid
    participant z3 as Z3 Solver
    participant codegen as WasmContext.translate_block

    rect rgba(70, 130, 180, 0.5)
        note over verifier,walker: Verifier phase (static discharge)
        verifier->>walker: walk body, threading path_conditions
        walker->>smt: check value >= 0 per binding site
        smt->>z3: push + check()
        z3-->>smt: unsat → obligation proved (Tier 1)
        z3-->>smt: sat → extract model (before pop)
        smt->>z3: pop()
        smt-->>walker: SmtResult(violated, counterexample)
        walker-->>verifier: emit E503 diagnostic
    end

    rect rgba(180, 100, 60, 0.5)
        note over codegen: Codegen phase (runtime guard for unchecked builds)
        codegen->>codegen: _narrows_into_nat(stmt.value)?
        codegen->>codegen: emit value instructions
        codegen->>codegen: _emit_nat_bind_guard → i64.lt_s + unreachable
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #747 — Projected-source narrowing obligation checking at projection binding sites (ADT sub-pattern binds, non-literal tuple destructures, generic constructor fields instantiated to @Nat, imported ADT constructors with concrete @Nat fields) is identified as remaining uncovered narrowing patterns and tracked as follow-up work to extend the same narrowing-obligation mechanism.

  • #749@Nat-narrowing review follow-ups from this implementation, requesting additional test pins for _fresh_slot_var and _narrows_into_nat coverage, plus a dedicated guard trap kind for runtime @Nat-binding guards introduced in this PR.

Possibly related PRs

  • aallan/vera#716: The main PR builds on the proof-obligation recording infrastructure extended in the retrieved PR by adding the nat_bind obligation kind to vera/obligations/core.py and implementing nat_bind discharge/reporting with E503 counterexamples in vera/verifier.py.

  • aallan/vera#423: Both PRs touch scripts/check_doc_counts.py to keep conformance-count documentation synchronised; the main PR adjusts the extraction regex to accept both article forms, whilst the retrieved PR extends the doc-count validation infrastructure.

Suggested labels

compiler, tests, spec, ci, docs

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: generalizing @Nat >= 0 invariant checking to all binding sites (issue #552), with clear, concise phrasing.
Linked Issues check ✅ Passed All objectives from #552 are met: obligations emit at six binding sites (let, call args, effect-op args, constructor fields, match binds, literal tuple destructuring), runtime guards preserve Tier-3 fallback, recursive _has_underflow_leaf detects wrapped/nested narrowing, effect-operation typing is resolved via _register_effect, fresh slot rebinding prevents spurious discharge, diagnostics extract Z3 model before pop, E504 warnings surface unguarded Tier-3 sites, and E503 errors report accurate counterexamples.
Out of Scope Changes check ✅ Passed All changes are squarely within #552 scope: binding-site obligation mechanics, runtime guard emission, effect-op formal typing, fresh-slot rebinding, Z3 model extraction timing, and E504 tier-3 honesty. Documentation updates and test counts reflect the conformance suite size increase and new obligation counts. No unrelated refactoring or feature additions detected.
Description check ✅ Passed Documentation comprehensively describes the implementation across all layers: verifier phase 5.7 obligation walking, codegen guard emission, effect-op registration fixes, destructure rebinding semantics, Tier-3 warning surfacing, diagnostic Z3 timing correction, and scope deferrals. Limitations and follow-ups are clearly articulated in KNOWN_ISSUES, ROADMAP, and code comments.
Docstring Coverage ✅ Passed Docstring coverage is 84.75% 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/552-nat-binding-sites

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

@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: 10

🤖 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 `@SKILL.md`:
- Line 2276: Remove the claim that there is "one program per feature" in the
sentence about the conformance test directory, as this is factually incorrect.
Since several features (such as slot references, match expressions, and
contracts) map to multiple conformance programs, reword the sentence to
accurately describe that the 90 programs validate language features without
implying a strict 1:1 mapping between features and programs.

In `@spec/11-compilation.md`:
- Line 56: The ADT pattern matching syntax documentation uses an incorrect form
`Some(`@Nat.0`)` when the correct pattern syntax is `Some(`@Nat`)` without the
index; the dotted form `.0` should only appear when referencing the bound value
in the match arm body. Fix this in two locations: in spec/11-compilation.md
(line 56), replace `Some(`@Nat.0`)` with `Some(`@Nat`)` in the pattern example; and
in KNOWN_ISSUES.md (line 22), apply the same correction to the limitation row.
Both changes use the same substitution to align the documentation with actual
Vera pattern syntax.

In `@TESTING.md`:
- Around line 9-11: The overview section in TESTING.md shows 4,390 total tests,
but this conflicts with the actual count of 3,630 from the per-file breakdown
listed below it, a difference of 760 tests. Run the scripts/check_doc_counts.py
script to determine the authoritative test count and then update the test count
figure in the overview (the "4,390 across 36 files" entry) to match the correct
total from the per-file breakdown, ensuring the documentation stays synchronized
with the actual test suite.

In `@tests/test_verifier.py`:
- Around line 764-775: The test function test_let_narrow_unguarded_fails and
similar narrowing-failure test functions currently only assert the error message
text ("may be negative") but do not explicitly verify the error code. Modify
each test function to include an explicit assertion that error_code == "E503" in
addition to the message text check, ensuring the diagnostic error code is
validated and the test will fail if the error code changes while keeping similar
wording. This applies to all similar test functions that check narrowing-related
failures.

In `@vera/README.md`:
- Line 690: In the verification gaps table on line 690 of vera/README.md, revise
the description of `@Nat` narrowing through ADT sub-pattern binds and non-literal
tuple destructures. Change the language from describing it as "unchecked" to
describing it as a "static-verification gap" instead. This is more accurate
because these bindings still receive Tier-3 runtime guards during codegen in
vera/wasm/context.py and vera/wasm/operators.py; the issue is that the verifier
cannot discharge them statically, not that they lack runtime protection
entirely.

In `@vera/verifier.py`:
- Around line 1479-1491: The binding pattern arm check in the match statement at
line 1489 (_check_nat_binding_obligation call) does not account for earlier arms
that may have already excluded certain cases from reaching this binding arm. To
fix this, collect the patterns from all previous arms processed before the
current binding pattern arm, create a condition representing the negation of the
disjunction of those patterns (Not(Or(previous_patterns))), and include this
condition in the assumptions when calling _check_nat_binding_obligation.
Additionally, apply the same negation condition to the arm body walk for
binding-pattern arms to ensure the arm is checked under the actual reachability
conditions determined by the ordered match semantics.
- Around line 1341-1355: The constructor field type checking in the
`_is_nat_type` method call only recognizes concretely resolved Nat types and
misses generic constructor fields (type variables) that are instantiated as Nat
at the call site, allowing Int arguments to bypass E503 obligations. To fix
this, either resolve the constructor field type (field_ty) through the call-site
type instantiation/context before checking if it is Nat in the _is_nat_type
condition, or alternatively emit a conservative Tier-3 nat_bind obligation by
calling _check_nat_binding_obligation even for generic fields to ensure a
runtime guard backs the obligation for common patterns like Option<Nat> and
Result<Nat, E>. Apply whichever approach your design prefers around the existing
_lookup_constructor_info and field_types access in the constructor call handling
block.
- Around line 1821-1823: The current check in the return statement only detects
top-level BinaryExpr with SUB operation, missing nested subtractions like (0 -
1) + 0 or if-else branches containing unproven subtraction. Create a new
recursive helper method (e.g., _contains_unproven_subtraction) that recursively
checks if a value or any of its sub-expressions contains a subtraction without
Nat origin by examining child nodes of BinaryExpr, conditional expressions, and
other composite nodes. Replace the current three-line check with a condition
that uses _is_nat_typed(value) and calls this new recursive predicate to
properly detect unproven subtraction at any nesting level.
- Around line 1506-1507: The fallback return statement at the end of the Nat
binding walker function skips recursion into expression containers like
ast.ArrayLit, causing nested narrowings to be missed. Before the default return
statement, add explicit branches to handle expression container types that can
hold child expressions. For each container type (such as ast.ArrayLit and
similar expression containers), recursively walk their child expressions using
the appropriate binding walker methods before falling through to the default
case.
- Around line 1421-1438: After the literal tuple destructure check completes in
the ast.LetDestruct branch, the destructured bindings are never added to
cur_env, causing subsequent narrowing obligations to refer to stale outer
bindings instead of the newly destructured values. Following the
_check_nat_binding_obligation call for each matching nat type binding, push the
destructured bindings into cur_env so that later code referencing those slots
uses the correct values from the destructure rather than the original outer
scope bindings.
🪄 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: c59d309f-6ef0-4a26-a95f-f74280149ada

📥 Commits

Reviewing files that changed from the base of the PR and between 9eb700e and ffeab1f.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/**
  • docs/sitemap.xml is excluded by !docs/**
  • tests/conformance/ch04_nat_binding.vera is excluded by !**/*.vera
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • scripts/check_doc_counts.py
  • spec/11-compilation.md
  • tests/conformance/manifest.json
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/README.md
  • vera/errors.py
  • vera/obligations/core.py
  • vera/smt.py
  • vera/verifier.py
  • vera/wasm/context.py
  • vera/wasm/operators.py

Comment thread SKILL.md Outdated
Comment thread spec/11-compilation.md
Comment thread TESTING.md Outdated
Comment thread tests/test_verifier.py
Comment thread vera/README.md
Comment thread vera/verifier.py
Comment thread vera/verifier.py
Comment thread vera/verifier.py
Comment thread vera/verifier.py
Comment thread vera/verifier.py Outdated
…es (#552)

Addresses the /review-pr findings on PR #748.

Critical (soundness): `_narrows_into_nat` only un-shadowed a *top-level*
`BinaryExpr.SUB`, so a pure-literal subtraction wrapped in a block /
if-branch / match arm, or nested in arithmetic — `let @nat = { 0 - 1 }`,
`if c then 5 else 0 - 1`, `(0 - 1) + (0 - 1)` — escaped both the
obligation and the runtime guard and stored a negative.  Replace the
syntactic check with a recursive `_has_underflow_leaf` (verifier +
codegen mirror) that descends block tails, if-branches, match arms, and
arithmetic operands to the value-producing leaf.

Accuracy:
- `_walk_for_nat_binding_obligations` docstring no longer claims a
  "single condition" gate (it is `_narrows_into_nat`, two shapes).
- Three verifier comments claimed projection sites "fall to the codegen
  runtime guard"; corrected — they are unchecked and tracked as #747
  (codegen guards only the let site).
- E503 spec_ref and the conformance manifest cited Section 4.5 / 4.4;
  "Let Bindings" is Section 4.7.
- Codegen `_narrows_into_nat` docstring documents the harmless dead
  guard on `let @nat = <user-fn returning @nat>` (codegen sees the i64
  return type, not the @nat Vera type).

Honesty / scope:
- KNOWN_ISSUES #747 records that the Tier-3 runtime guard backs only the
  let site, so a Tier-3 narrowing at call-arg / ctor-field / match /
  destructure is caught by `vera verify` but not an unverified compile.
- PR summary reframed from "closing" to "narrowing" the hole.
- "an 90-program" -> "a 90-program" (build_site template, FAQ, regen).

Tests (+5): wrapped/nested-subtraction caught (verifier) and trapped
(codegen); ADT sub-pattern bind pinned to no obligation (forces #747 to
update consciously); caught narrowing asserts E503 + nat_bind kind;
`@Nat - @Nat` call argument pinned to nat_sub-only (cross-site #520/#552
disjointness).

Co-Authored-By: Claude <[email protected]>
aallan and others added 2 commits June 16, 2026 12:23
…552)

Addresses the CodeRabbit review on PR #748.

- vera/verifier.py LetDestruct branch now threads the destructured slots
  into `cur_env`, so a later statement's narrowing obligation translates
  against the destructured value rather than a stale outer binding of the
  same slot name.  Components are translated in the outer env before any
  push, so an element referring to an outer slot of the same type is not
  shadowed.  +1 regression test (destructure of two `0 - 5` components →
  the following `takes_nat(@Int.0)` correctly fails E503).
- #747 (KNOWN_ISSUES + issue body) now also tracks generic constructor
  fields instantiated to @nat (`Some(@Int.0)` building `Option<Nat>` —
  the field is a TypeVar, so recognising it needs the call-site
  instantiation) and records that the Tier-3 runtime guard backs only
  the `let` site.
- SKILL.md / AGENTS.md: soften the "one program per feature" claim (some
  features span several conformance programs).

Declined (with reasons in the PR replies): the match-arm-reachability
refinement (the binding-arm obligation is already sound and
conservative); the README "unchecked" wording (projection sites get no
codegen guard, so "unchecked" is accurate); the TESTING.md per-file sum
(check_doc_counts validates total and per-file counts independently from
pytest --collect-only).

Co-Authored-By: Claude <[email protected]>
Addresses CodeRabbit on PR #748: `_walk_for_nat_binding_obligations`
fell through on expression containers (`ArrayLit`, `IndexExpr`,
`InterpolatedString`), so a narrowing nested inside one — e.g.
`[takes_nat(@Int.0)]` — was never visited and got no obligation.  Add
recursion branches for the three containers.  +1 test.  (The #520
subtraction walker has the same pre-existing container gap; aligning it
is out of #552's scope.)

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

aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Review summary

This PR went through two review passes — the pr-review-toolkit agents (code review, test analysis, silent-failure hunting, comment accuracy) and CodeRabbit. They overlapped but each caught things the other missed. Findings and resolutions:

Fixed

  • Soundness — wrapped pure-literal subtraction. _narrows_into_nat only un-shadowed a top-level BinaryExpr.SUB, so a pure-literal subtraction wrapped in a block / if-branch / match arm, or nested in arithmetic — let @Nat = { 0 - 1 }, if c then 5 else 0 - 1, (0 - 1) + (0 - 1) — escaped both the obligation and the runtime guard and stored a negative @Nat. Replaced the syntactic check with a recursive _has_underflow_leaf (verifier + codegen mirror) that descends to the value-producing leaf. (Found by the review agents.)
  • Correctness — stale cur_env after destructure. The LetDestruct branch never threaded the destructured slots into cur_env, so a later statement's obligation translated against a stale outer binding of the same slot name. Now threads them, translating each component in the outer env first so a same-type element is not shadowed by an earlier push. (Found by CodeRabbit.)
  • Completeness — expression containers. The walker fell through on ArrayLit / IndexExpr / InterpolatedString, so a narrowing nested inside one ([takes_nat(@Int.0)]) was never visited. Added recursion branches for the three containers. (Found by CodeRabbit.)
  • Counterexamples. SmtContext.check_valid extracted the Z3 model after popping the assertion scope, so counterexamples reported model-completed defaults (@Int.0 = 0 for the goal @Int.0 >= 0) instead of the violating assignment. Now extracts before the pop — fixes E502 / E503 / call-site diagnostics alike.
  • Accuracy. Corrected three verifier comments that claimed projection sites "fall to the codegen runtime guard" (the guard is let-only); fixed the E503 spec_ref and conformance manifest (§4.5 / §4.4 → §4.7 "Let Bindings"); documented the harmless dead guard on let @Nat = <user-fn returning @Nat>; softened the "one program per feature" claim; an 90-programa 90-program.

Regression tests: +6 (wrapped/nested subtraction caught and trapped, ADT sub-pattern pinned to no obligation, E503 + nat_bind assertion, cross-site @Nat - @Nat call-argument disjointness, destructure cur_env threading).

Deferred (tracked)

Declined

  • Match-arm reachability (check the binding-arm obligation under the negation of previous arms): the obligation is already checked under preconditions + outer path conditions, which is sound and conservative; excluding all negatives via preceding finite literal arms is impractical, so no false positive arises.
  • README "unchecked" wording: the projection sites that row refers to get no codegen guard (the guard is let-only; a negative reaches the @Nat slot at both tiers — confirmed by repro), so "unchecked" is accurate.
  • TESTING.md per-file sum: check_doc_counts.py validates the overview total and each per-file count independently against pytest --collect-only; the curated table need not sum to the total.

Scope

The PR statically obligates narrowing at let, call-argument, (concrete) constructor-field, top-level match-bind, and literal-tuple-destructure sites, with a runtime guard at the let site. The residual above is honestly out of scope and tracked in #747 / #746. Given the deferred projection sites, I've left the close-on-merge decision for #552 to the maintainer.

@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: 1

Caution

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

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

1366-1371: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check QualifiedCall formal types before only recursing into arguments.

This branch misses narrowing at qualified operation calls. For example, an effect operation with a Nat parameter such as IO.sleep(@Int.0) is represented as a QualifiedCall, but the walker never resolves the operation signature, so no E503 obligation is emitted and there is no call-site runtime guard.

As per coding guidelines, qualified operation calls are part of Vera syntax and IO.sleep takes Nat -> Unit.

🤖 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 1366 - 1371, The handling of QualifiedCall
expressions in _walk_for_nat_binding_obligations is incomplete. Before recursing
into the arguments, the code must first resolve the operation's signature and
check whether the formal parameter types require narrowing (such as a Nat
parameter). Apply the same narrowing and obligation checking logic to the formal
types of the qualified operation before processing the arguments, so that E503
obligations are properly emitted for operations like IO.sleep that expect
specific parameter types.

Source: Coding guidelines


1269-1283: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Include imported ADT constructors in constructor-field narrowing checks.

_lookup_constructor_info() only searches self.env, but _register_modules() registers imported modules in isolation and injects only public functions. A public imported constructor with a concrete Nat field can therefore accept an Int argument without an E503 obligation.

As per coding guidelines, imported ADT constructors are available at call sites, so verifier constructor lookup must include the resolved-module constructor registries too.

🤖 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 1269 - 1283, The `_lookup_constructor_info()`
method currently only searches `self.env` for constructors, but imported ADT
constructors are registered in resolved-module registries that are not being
checked. Extend the method to also search through the constructor registries
from resolved/imported modules after checking `self.env.lookup_constructor()`
and `self.env.data_types`, ensuring that imported constructors with concrete
fields are included in the lookup for proper type-checking and E503 obligation
validation.

Source: Coding guidelines


1622-1643: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not record unguarded non-let failures as Tier-3 runtime obligations.

val is None and solver timeout record nat_bind as Tier-3 for every site, but the PR’s runtime guard only backs let bindings. For call arguments, constructor fields, and match binds, this can leave an unproved narrowing marked as runtime-covered while unchecked compilation still has no guard. Either emit a diagnostic/warning for unguarded non-let sites or add guards before using tier3_runtime here.

🤖 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 1622 - 1643, The code in the nat_bind checking
block (both in the `val is None` case and the solver timeout case) records
obligations as tier3_runtime for all binding sites, but runtime guards only
exist for `let` bindings. Before recording as tier3_runtime in both locations
where self.summary.tier3_runtime is incremented, add a check to determine if
this is a `let` binding. For non-let sites (call arguments, constructor fields,
match binds), either emit a diagnostic/warning instead of recording as
tier3_runtime, or skip the tier3_runtime recording altogether and handle
unguarded non-let failures differently to avoid marking unproved narrowings as
runtime-covered when no guard actually exists at compilation time.
ROADMAP.md (1)

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

Broaden the #747 row to match the limitations table.

KNOWN_ISSUES.md still lists generic constructor fields instantiated to @Nat as uncovered, but this row only mentions ADT sub-pattern binds and non-literal tuple destructures. Please keep the two docs aligned so #747 does not read as narrower than the current gap. Based on the supplied #552 scope notes and KNOWN_ISSUES.md.

🤖 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` around lines 23 - 24, The `#747` row in ROADMAP.md describes the
narrowing limitations too narrowly compared to what is documented in
KNOWN_ISSUES.md. Expand the `#747` row description to include generic constructor
fields instantiated to `@Nat` as an additional uncovered case alongside the
currently mentioned ADT sub-pattern binds and non-literal tuple destructures,
ensuring both documents align on the actual scope of the limitation.
🤖 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 1032-1051: The test_adt_subpattern_bind_not_obligated_yet method
currently only asserts the absence of nat_bind obligations in the result, but
does not verify that the overall verification succeeds with no errors. Add an
explicit assertion to check that the result object contains no errors (examine
the result object structure to determine the appropriate attribute or method to
check for errors). This strengthens the test to fully pin the current successful
verification behavior, ensuring that if the verifier fails through a different
diagnostic path in the future, the test will catch it rather than passing
silently.

---

Outside diff comments:
In `@ROADMAP.md`:
- Around line 23-24: The `#747` row in ROADMAP.md describes the narrowing
limitations too narrowly compared to what is documented in KNOWN_ISSUES.md.
Expand the `#747` row description to include generic constructor fields
instantiated to `@Nat` as an additional uncovered case alongside the currently
mentioned ADT sub-pattern binds and non-literal tuple destructures, ensuring
both documents align on the actual scope of the limitation.

In `@vera/verifier.py`:
- Around line 1366-1371: The handling of QualifiedCall expressions in
_walk_for_nat_binding_obligations is incomplete. Before recursing into the
arguments, the code must first resolve the operation's signature and check
whether the formal parameter types require narrowing (such as a Nat parameter).
Apply the same narrowing and obligation checking logic to the formal types of
the qualified operation before processing the arguments, so that E503
obligations are properly emitted for operations like IO.sleep that expect
specific parameter types.
- Around line 1269-1283: The `_lookup_constructor_info()` method currently only
searches `self.env` for constructors, but imported ADT constructors are
registered in resolved-module registries that are not being checked. Extend the
method to also search through the constructor registries from resolved/imported
modules after checking `self.env.lookup_constructor()` and
`self.env.data_types`, ensuring that imported constructors with concrete fields
are included in the lookup for proper type-checking and E503 obligation
validation.
- Around line 1622-1643: The code in the nat_bind checking block (both in the
`val is None` case and the solver timeout case) records obligations as
tier3_runtime for all binding sites, but runtime guards only exist for `let`
bindings. Before recording as tier3_runtime in both locations where
self.summary.tier3_runtime is incremented, add a check to determine if this is a
`let` binding. For non-let sites (call arguments, constructor fields, match
binds), either emit a diagnostic/warning instead of recording as tier3_runtime,
or skip the tier3_runtime recording altogether and handle unguarded non-let
failures differently to avoid marking unproved narrowings as runtime-covered
when no guard actually exists at compilation time.
🪄 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: 91e8a32d-0188-4c9e-ae63-42f985865e53

📥 Commits

Reviewing files that changed from the base of the PR and between ffeab1f and 599f112.

⛔ Files ignored due to path filters (3)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (12)
  • AGENTS.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • scripts/build_site.py
  • tests/conformance/manifest.json
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/verifier.py
  • vera/wasm/operators.py

Comment thread tests/test_verifier.py
CodeRabbit review round on PR #748:

- verifier: the QualifiedCall branch of the nat-binding walker now
  resolves the effect-op signature via lookup_effect_op and obligates
  @nat formal-parameter narrowings, so IO.sleep(@Int.0) reports E503
  (was silently accepted). Mirrors the FnCall/ModuleCall call-arg check.
- test: the ADT sub-pattern test now also asserts no error diagnostics
  (pins successful verification); add test_effect_op_argument_narrowing_caught.
- docs: ROADMAP/KNOWN_ISSUES/CHANGELOG/TESTING gain the effect-op-arg
  site and broaden the #747 residual to generic + imported constructor
  fields and the let-only runtime-guard scope.

Declined/deferred to #747: imported-ADT-constructor lookup (needs
cross-module ADT injection + SMT registration) and non-let Tier-3
runtime guards.

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

aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Triaged the four outside-diff findings against current code — two fixed in d939737, two declined with reasons:

1. QualifiedCall formal types (verifier.py) — fixed. Confirmed the gap: IO.sleep(@Int.0) verified clean because the QualifiedCall branch only recursed into arguments. It now resolves the operation signature via lookup_effect_op(name, qualifier) and emits a value >= 0 obligation for each @Nat formal before recursing, so IO.sleep(@Int.0) now reports E503. Added test_effect_op_argument_narrowing_caught; coverage docs gained the effect-op-arg site.

2. ROADMAP #747 row — fixed. Re-synced with KNOWN_ISSUES: the row now lists generic-instantiated and imported constructor fields plus the let-only runtime-guard scope.

3. Imported ADT constructors (verifier.py) — confirmed, deferred to #747. _register_modules harvests only public functions, so _lookup_constructor_info cannot see imported constructors. Closing this needs cross-module ADT injection + SMT registration (a broader cross-module-verification change) and is the same constructor-field residual class as the generic-field gap, so it is documented in #747 (KNOWN_ISSUES + ROADMAP + the issue body) rather than fixed here.

4. Non-let Tier-3 recording (verifier.py) — declining. The Tier-3 status reflects that the value is outside Z3's decidable fragment (consistent with #520), and the let-only guard scope is documented in #747, the KNOWN_ISSUES/ROADMAP rows, and the _check_nat_binding_obligation docstring. A per-site warning would false-alarm on safe-but-untranslatable patterns like nat_to_int(array_length(...)) (always ≥ 0 yet unprovable); the proper fix — per-site runtime guards — is tracked in #747.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

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

@aallan: All four dispositions look correct.

1 & 2 — fixed. The QualifiedCall gap was a real soundness hole (effect-op arguments with @Nat formals received no obligation), and the fix — resolving via lookup_effect_op before recursing — is the right shape. ROADMAP row re-sync is clean.

3 — deferred correctly. _register_modules harvesting only public functions is a fundamental cross-module visibility boundary; injecting imported ADT constructors into the SMT context is the same class of work as the generic-field gap and belongs in #747 rather than as a late addition here.

4 — decline accepted. The false-positive argument is sound: nat_to_int(array_length(...)) is always ≥ 0 but is SMT-opaque, so a per-site Tier-3 warning at call-argument / constructor-field / match-bind sites would flag safe patterns as unverified. Silent Tier-3 recording (consistent with the #520 precedent) is the right behaviour until per-site runtime guards land in #747.

No further concerns on this pass. The PR is in good shape.

@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: 1

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)

327-334: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register user-defined effect operation signatures before checking effect-op arguments.

Line 1369 now depends on lookup_effect_op() exposing param_types, but _register_effect() stores every user EffectDecl with operations={}. A user effect such as effect E { op wait(Nat -> Unit); } followed by E.wait(@Int.0) would therefore bypass the new E503 narrowing check. Mirror _register_ability() here and populate OpInfo entries for each declared operation.

Proposed fix
 def _register_effect(self, decl: ast.EffectDecl) -> None:
     """Register an effect declaration."""
     from vera.environment import EffectInfo, OpInfo
+    saved_params = dict(self.env.type_params)
+    if decl.type_params:
+        for tv in decl.type_params:
+            self.env.type_params[tv] = TypeVar(tv)
+    ops: dict[str, OpInfo] = {}
+    try:
+        for op in decl.operations:
+            param_types = tuple(
+                self._resolve_type(p) for p in op.param_types
+            )
+            ret_type = self._resolve_type(op.return_type)
+            ops[op.name] = OpInfo(op.name, param_types, ret_type, decl.name)
+    finally:
+        self.env.type_params = saved_params
     self.env.effects[decl.name] = EffectInfo(
         name=decl.name,
         type_params=decl.type_params,
-        operations={},
+        operations=ops,
     )

Also applies to: 1367-1374

🤖 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 327 - 334, The _register_effect() method
currently initializes operations as an empty dictionary, which means
user-defined effect operations are not registered with their OpInfo entries.
This causes lookup_effect_op() to be unable to access param_types for
user-defined operations, allowing E503 narrowing checks to be bypassed. Refactor
_register_effect() to mirror the pattern used in _register_ability() by
iterating through the operations declared in the effect declaration
(decl.operations) and populating the operations dictionary with OpInfo entries
for each operation, including their param_types information.
TESTING.md (1)

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

Update the conformance-suite count here too.

This still says 445 tests, but the updated table above reports 450. Please mirror that change so the file stays internally consistent. Based on learnings, every literal occurrence of a stale count should be updated in the same file.

🤖 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 `@TESTING.md` at line 199, The pytest parametrized test count in the heading is
stale and does not match the updated conformance-suite table. Update the test
count from 445 to 450 in the line "# Via pytest (parametrized — 445 tests)" to
maintain consistency with the table reported above. Verify that all other
literal test count references in the file are also synchronized with the current
conformance-suite numbers.

Source: Learnings

🤖 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 1139-1153: The test_effect_op_argument_narrowing_caught method
only covers the error case where IO.sleep receives a negative `@Int` value. Add a
companion test method (such as test_effect_op_argument_narrowing_discharged)
that verifies the positive case where the same narrowing operation succeeds
cleanly. This companion test should use the same function structure and IO.sleep
call with `@Int.0`, but add a requires guard that ensures the value is
non-negative (e.g., requires(`@Int.0` >= 0)) so that verification passes, ensuring
regressions are caught if the narrowing binding site becomes overly
conservative.

---

Outside diff comments:
In `@TESTING.md`:
- Line 199: The pytest parametrized test count in the heading is stale and does
not match the updated conformance-suite table. Update the test count from 445 to
450 in the line "# Via pytest (parametrized — 445 tests)" to maintain
consistency with the table reported above. Verify that all other literal test
count references in the file are also synchronized with the current
conformance-suite numbers.

In `@vera/verifier.py`:
- Around line 327-334: The _register_effect() method currently initializes
operations as an empty dictionary, which means user-defined effect operations
are not registered with their OpInfo entries. This causes lookup_effect_op() to
be unable to access param_types for user-defined operations, allowing E503
narrowing checks to be bypassed. Refactor _register_effect() to mirror the
pattern used in _register_ability() by iterating through the operations declared
in the effect declaration (decl.operations) and populating the operations
dictionary with OpInfo entries for each operation, including their param_types
information.
🪄 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: 89bc52d1-a20b-4ef0-b32d-0e9fbcea73cb

📥 Commits

Reviewing files that changed from the base of the PR and between 599f112 and d939737.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_verifier.py
  • vera/verifier.py

Comment thread tests/test_verifier.py
CodeRabbit review round 3 on PR #748:

- verifier: _register_effect now populates an OpInfo per declared
  operation (mirroring _register_ability), so lookup_effect_op exposes
  param_types for user-declared effects. Round 2's QualifiedCall check
  silently no-op'd for them (operations were stored empty); E.wait(@Int.0)
  on a user effect now reports E503, and the guarded form verifies clean.
- test: add test_effect_op_argument_narrowing_discharged (guarded
  IO.sleep verifies clean) and test_user_effect_op_argument_narrowing_caught.
- docs: TESTING.md conformance-count comment 445 -> 450; CHANGELOG notes
  user-declared effects.

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

aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Both outside-diff findings fixed in ee5108d:

_register_effect empty operations (verifier.py:327) — fixed (valid, Major). Confirmed: user-declared effects stored operations={}, so lookup_effect_op returned None and round 2's E503 check silently no-op'd for them — exactly the case it most needed to cover. (Built-in IO is registered separately in the base TypeEnv, which is why the round-2 IO.sleep test still passed.) _register_effect now populates an OpInfo per declared operation, mirroring _register_ability (type-param scoping included). Verified: effect E { op wait(Nat -> Unit); } … E.wait(@Int.0) now reports E503, and the guarded form (requires(@Int.0 >= 0)) verifies clean. Added test_user_effect_op_argument_narrowing_caught.

TESTING.md:199 stale count — fixed. 445450 to match the test_conformance.py table row; the live parametrized count is 450 (pytest --collect-only). doc-counts validates table cells, not this prose comment, which is why it slipped through.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

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

@aallan: Both fixes look correct.

_register_effect / OpInfo population was the critical one — the built-in IO registration masking the gap in the IO.sleep round-2 test is exactly the kind of coverage blind spot that's easy to miss. Mirroring _register_ability's OpInfo-per-operation pattern (with type-param scoping) is the right shape, and the E.wait(@Int.0) → E503 / guarded-form-clean verification confirms the fix is end-to-end correct.

TESTING.md 445450 is clean; the explanation of why check_doc_counts.py didn't catch it (validates table cells, not the prose comment) is a useful note to have on record.

No further concerns. The PR is in good shape.

@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

Caution

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

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

1457-1488: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Do not keep walking with the old slot environment after non-literal destructures.

For let Tuple<Int> = @tuple<Int>.0;, the following @Int.0 refers to the destructured field, but this branch only pushes slots for literal Tuple(...) sources. If an outer @Int.0 is constrained non-negative, a later takes_nat(@Int.0) can verify against that stale outer value while codegen passes the destructured field, allowing a negative value through with no guard. Push conservative bindings/accessor terms for all destructures, or stop checking subsequent statements under the stale environment.

🤖 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 1457 - 1488, The code only pushes destructured
slot bindings to cur_env when the destructure source is a literal constructor
call (when isinstance(stmt.value, ast.ConstructorCall) and stmt.value.name ==
stmt.constructor is true), leaving stale outer bindings in cur_env for
non-literal sources like field accesses. This allows verification against an old
constrained value while codegen uses the destructured field. After the
conditional block that handles literal sources, add logic to handle non-literal
destructures by pushing conservative bindings or accessor terms for all
destructured slots in stmt.type_bindings, or alternatively prevent subsequent
statement checking from using the stale cur_env by resetting it or using a
separate environment for the non-literal case.
🤖 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 1168-1185: Add a companion test method after
test_user_effect_op_argument_narrowing_caught that verifies the passing path for
user-declared effect operation argument narrowing. Create a new test using
_verify_ok (instead of _verify_err) with the same effect E definition and
function f structure, but add requires(`@Int.0` >= 0) to the function to satisfy
the non-negativity constraint for the E.wait(Nat) parameter. This ensures that
when the narrowing requirement is properly discharged, the code verification
succeeds, catching any regressions that might make the compiler
over-conservative at user-declared effect-op sites.

In `@vera/verifier.py`:
- Around line 343-348: Generic effect-op formals are not being instantiated
before the _is_nat_type check, allowing invalid operations on concrete effect
instances. When processing operations in the op registration logic (around
verifier.py lines 343-348 and 1389-1394), resolve the operation signatures
(param_types and return_type) through the enclosing function's effect-row
instantiation to replace any TypeVar formals with their concrete types before
testing _is_nat_type on those formals. This ensures that when a generic effect
like E<T> with operation wait(T -> Unit) is instantiated as E<Nat>, the formal T
is resolved to Nat before the `@Nat` type check occurs.

---

Outside diff comments:
In `@vera/verifier.py`:
- Around line 1457-1488: The code only pushes destructured slot bindings to
cur_env when the destructure source is a literal constructor call (when
isinstance(stmt.value, ast.ConstructorCall) and stmt.value.name ==
stmt.constructor is true), leaving stale outer bindings in cur_env for
non-literal sources like field accesses. This allows verification against an old
constrained value while codegen uses the destructured field. After the
conditional block that handles literal sources, add logic to handle non-literal
destructures by pushing conservative bindings or accessor terms for all
destructured slots in stmt.type_bindings, or alternatively prevent subsequent
statement checking from using the stale cur_env by resetting it or using a
separate environment for the non-literal 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: 5348e541-6936-459a-a550-6d5f69470b19

📥 Commits

Reviewing files that changed from the base of the PR and between d939737 and ee5108d.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_verifier.py
  • vera/verifier.py

Comment thread tests/test_verifier.py
Comment thread vera/verifier.py
…ormals (#552)

CodeRabbit review round 4 on PR #748:

- verifier: a non-literal destructure source (field access, call, if-expr)
  now rebinds each destructured slot to a fresh slot var (_fresh_slot_var),
  invalidating the stale outer binding so a later obligation no longer
  discharges against the old, more-constrained value. Literal sources keep
  their precise components.
- verifier: generic (TypeVar) effect-op formals are skipped like generic
  constructor fields (only concretely-@nat formals obligate); documented as
  deferred to #747 (KNOWN_ISSUES / ROADMAP / CHANGELOG aligned + a code
  comment at the QualifiedCall check).
- test: add the non-literal-destructure stale-binding regression and the
  user-effect discharged companion.

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

aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai The non-literal-destructure stale-binding finding (LetDestruct branch) is fixed in c0e0732. The branch now rebinds every destructured slot in cur_env, not just literal-source slots: a literal component keeps its precise translated value (still translated in the outer env first to avoid self-shadowing), while a non-literal source — field access, call, if-expression — or any component the SMT layer can't translate falls back to a fresh slot var (_fresh_slot_var, carrying the slot's type invariant). A later obligation therefore translates against the fresh unknown, never the stale outer binding.

Verified: a non-literal destructure (let Tuple<@Int,@Int> = if … then { Tuple(0-1,0-1) } else { … }) followed by takes_nat(@Int.0) now reports E503 instead of wrongly discharging against the @Int param's requires(@Int.0 >= 0). Added test_destructure_non_literal_source_invalidates_stale_binding.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

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

@aallan: Fix looks correct and complete.

The _fresh_slot_var fallback for non-literal / SMT-opaque sources is exactly the right shape: it carries the slot's type invariant (so a @Nat-typed slot still gets value >= 0 as a known fact) without asserting any specific value, which is the sound conservative position — the obligation translates against an unconstrained unknown and cannot be spuriously discharged. Translating the literal component in the outer env before the push correctly avoids self-shadowing when a slot name reappears on the right-hand side.

The test_destructure_non_literal_source_invalidates_stale_binding test pins the previously broken case (obligation wrongly discharged by the outer requires(@Int.0 >= 0)) — good regression anchor.

…ins)

Post-CodeRabbit comprehensive review (4 specialist agents) on PR #748:

- verifier: a non-`let` Tier-3 narrowing (untranslatable value, no runtime
  guard off the let site) now surfaces an E504 warning + `tier3_unguarded`
  status instead of being silently counted as runtime-checked; the
  self-contradictory docstring is fixed (silent-failure-hunter C1).
- verifier: `_fresh_slot_var` dispatches on the *resolved* type, so an
  alias-of-scalar destructure slot (`type Count = Nat`) is invalidated
  (silent-failure-hunter I1).
- comments: Site-3 ctor-field "degrades to runtime guard" -> "deferred to
  #747" (no guard for ctor fields); effect-op-arg added to 3 five-site
  enumerations (comment-analyzer).
- tests: E503 counterexample-witness assertion (pins the model-before-pop
  fix, pr-test C1); 3 discharge tests assert status=="verified" (I2);
  codegen guard pinned for an untranslatable narrowing (I3); an E504 unit
  pin; 3 #747-deferral tripwires.
- docs: CHANGELOG E504 note; TESTING/ROADMAP counts;
  test_overall_tier_counts 256/25/281.

Remaining low-value suggestions (container IndexExpr/InterpolatedString
pins, alias unit-test, verifier/codegen `_narrows_into_nat` differential
test, dedicated @Nat-guard trap kind) noted for follow-up.

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

aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Post-CodeRabbit /review-pr round (4 specialist agents)

After the 4 CodeRabbit rounds landed green, I ran a fresh independent review (code-reviewer, silent-failure-hunter, pr-test-analyzer, comment-analyzer) over the full #552 diff. It surfaced real items the automated rounds missed — concentrated in test-pin strength, a stale comment, and a re-raise of the non-let Tier-3 honesty issue. No new soundness bug in the implemented behavior (two agents independently confirmed the obligations, #520 disjointness, the round-4 stale-binding fix, and the check_valid model-before-pop fix are correct).

Fixed in 912e55e:

  • Non-let Tier-3 overstatement (silent-failure-hunter, Critical) — a narrowing the solver can't discharge at a non-let site has no runtime guard, but was silently counted as a "runtime check (Tier 3)". It now records a new tier3_unguarded status and surfaces an E504 warning ("could not be verified statically and is not runtime-guarded — add requires(... >= 0)…"), excluded from the discharged totals. The self-contradictory docstring ("relies on vera verify") is fixed. (string_utilities's 3 nat_to_int(array_length(...)) narrowings now warn honestly, consistent with its existing 3 E522 warnings.)
  • Model-before-pop fix was unpinned (pr-test-analyzer, Critical) — a revert degraded the E503 counterexample from @Int.0 = -1 to a non-witnessing 0, yet every test passed. Added a counterexample-witness assertion.
  • Stale Site-3 comment (comment-analyzer) — generic @Nat ctor fields "degrade to the runtime guard" was wrong (no guard for ctor fields) → "deferred to Generalize @Nat narrowing obligation to projection binding sites (ADT sub-pattern, non-literal destructure) #747". Added the missing effect-op-arg to 3 five-site enumerations.
  • Vacuous discharged tests (pr-test-analyzer) — 3 structurally-distinct discharge tests now assert the obligation actually verified, not merely "no error".
  • Codegen guard untested on its raison d'être (pr-test-analyzer) — pinned the let-site guard for an untranslatable (Tier-3) narrowing.
  • _fresh_slot_var alias-of-scalar (silent-failure-hunter) — now dispatches on the resolved type, so a type Count = Nat destructure slot is invalidated.
  • Generalize @Nat narrowing obligation to projection binding sites (ADT sub-pattern, non-literal destructure) #747-deferral tripwires — added pins for generic effect-op formal, generic ctor field, and non-literal @Nat destructure, so a future change can't silently flip a deferral.

Deferred as low-value follow-ups, tracked in #749 (ROADMAP Tier 3): container IndexExpr/InterpolatedString pins (the array-literal is pinned), an alias unit-test (the code fix is validated), a verifier/codegen _narrows_into_nat differential test, and a dedicated @Nat-guard trap kind.

Validated: mypy clean, 1602 verifier/codegen/obligations tests pass, examples (35) + conformance (90) green, doc-counts consistent (4,407).

@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

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)

1459-1463: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Freshen untranslatable let bindings before walking later statements.

When translate_expr(stmt.value, cur_env) returns None, cur_env is not updated, so a later same-type slot reference can resolve to a stale outer binding and falsely discharge E503. For example, after let @int = E.next(()), a later takes_nat(@Int.0) can prove the outer parameter instead of the effect result. Mirror the destructure path and push a fresh slot variable for untranslatable let RHSs.

Proposed fix
                     val = smt.translate_expr(stmt.value, cur_env)
+                    if val is None:
+                        val = self._fresh_slot_var(smt, stmt.type_expr)
                     if val is not None:
                         type_name = smt._type_expr_to_slot_name(stmt.type_expr)
                         if type_name is not None:
                             cur_env = cur_env.push(type_name, val)
🤖 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 1459 - 1463, When translate_expr returns None
for a let statement's value (untranslatable expression), the current code does
not update cur_env, allowing later slot references to incorrectly resolve to
stale outer bindings. Handle the None case by still extracting the type_name
using _type_expr_to_slot_name on stmt.type_expr and pushing a fresh slot
variable to cur_env instead of the translated val. This ensures that even
untranslatable let bindings create new environment entries, preventing false
resolution to outer scope bindings that could incorrectly discharge E503 errors.

1355-1356: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Instantiate generic function formals before deciding there is no @Nat narrowing.

This branch only recognises concretely resolved Nat formals. A generic function whose TypeVar is fixed to Nat by another argument or the surrounding context can still accept an @Int value without E503 and without a call-site runtime guard, e.g. collection/option-style APIs where T = Nat. Either instantiate param_types for the call before _is_nat_type(formal), or explicitly add generic function formals to the same #747 deferral/test coverage as generic constructor fields and effect-op formals.

🤖 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 1355 - 1356, The check for Nat type narrowing
in the loop iterating through expr.args and param_types does not instantiate
generic TypeVars that may be fixed to Nat by the call context or surrounding
type information. Before the loop that zips expr.args with param_types,
instantiate param_types to resolve any generic TypeVars based on the actual call
context, ensuring _is_nat_type(formal) can correctly identify Nat types in
generic functions (e.g., where T=Nat in collection-style APIs). Alternatively,
if full instantiation is not feasible, defer the narrowing check for generic
function formals to match the existing deferral/test coverage applied to generic
constructor fields and effect-op formals.
🤖 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 1314-1330: The test_user_effect_op_argument_narrowing_discharged
test currently uses _verify_ok(...) which only checks for the absence of errors,
but does not verify that the nat_bind obligation was actually discharged. This
can mask regressions in user effect-op formal resolution. Replace the
_verify_ok(...) call with a stronger assertion pattern that verifies the
nat_bind status is ["verified"], following the same pattern demonstrated in the
tests around line 1291-1293. This ensures the test explicitly confirms the
narrowing logic is working as intended, not just that the code happens to
compile.

In `@vera/verifier.py`:
- Around line 1466-1468: The comment at lines 1466-1468 incorrectly states that
a non-literal source "degrades to the runtime guard". Update this comment to
accurately reflect that non-literal destructures are not runtime-guarded at
codegen time—only the canonical `let `@Nat` = ...` site has runtime guard
protection. The implementation below only freshens slots for non-literal
sources. Clarify that this represents gap `#747` and is not protected at codegen
time, so future changes do not make incorrect assumptions about runtime guard
coverage.

---

Outside diff comments:
In `@vera/verifier.py`:
- Around line 1459-1463: When translate_expr returns None for a let statement's
value (untranslatable expression), the current code does not update cur_env,
allowing later slot references to incorrectly resolve to stale outer bindings.
Handle the None case by still extracting the type_name using
_type_expr_to_slot_name on stmt.type_expr and pushing a fresh slot variable to
cur_env instead of the translated val. This ensures that even untranslatable let
bindings create new environment entries, preventing false resolution to outer
scope bindings that could incorrectly discharge E503 errors.
- Around line 1355-1356: The check for Nat type narrowing in the loop iterating
through expr.args and param_types does not instantiate generic TypeVars that may
be fixed to Nat by the call context or surrounding type information. Before the
loop that zips expr.args with param_types, instantiate param_types to resolve
any generic TypeVars based on the actual call context, ensuring
_is_nat_type(formal) can correctly identify Nat types in generic functions
(e.g., where T=Nat in collection-style APIs). Alternatively, if full
instantiation is not feasible, defer the narrowing check for generic function
formals to match the existing deferral/test coverage applied to generic
constructor fields and effect-op formals.
🪄 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: 54c3ca6f-d5ec-4db8-a2ab-692416a9286a

📥 Commits

Reviewing files that changed from the base of the PR and between ee5108d and 51c1d1a.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (9)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • tests/test_verifier.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread tests/test_verifier.py
Comment thread vera/verifier.py Outdated
)

CodeRabbit review round on PR #748 (51c1d1a review):

- verifier: a non-translatable `let` RHS (e.g. `let @int = E.next(())`)
  now rebinds the slot to a fresh slot var, mirroring the destructure
  path — the let-statement analogue of the round-4 stale-binding
  soundness fix. A later narrowing on that slot no longer falsely
  discharges against a stale outer binding. Added
  test_let_non_translatable_source_invalidates_stale_binding.
- verifier: generic function formals fixed to @nat by context are skipped
  like generic ctor/effect-op formals; documented as deferred to #747
  (KNOWN_ISSUES / ROADMAP / issue body + a code comment).
- test: the user-effect discharged test asserts the nat_bind obligation
  verified (not merely no-error).
- comment: the non-literal destructure path is left unchecked (no runtime
  guard off the let site), not "degrades to the runtime guard".

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

aallan commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Both outside-diff findings addressed in ea5b116:

Let-statement stale binding (verifier.py:1459) — fixed (valid, Major). Confirmed the same soundness hole as the round-4 destructure case, now in the let-statement path: an untranslatable RHS (let @Int = E.next(())) left cur_env unchanged, so a later takes_nat(@Int.0) could falsely discharge against the @Int param's requires(@Int.0 >= 0). Applied your fix — the RHS falls back to _fresh_slot_var when translate_expr returns None, mirroring the destructure path. Verified: let @Int = array_length(string_lines("ab")); takes_nat(@Int.0) now reports E503 instead of discharging. Added test_let_non_translatable_source_invalidates_stale_binding.

Generic function formals (verifier.py:1355) — deferred to #747. A generic function whose TypeVar formal is fixed to Nat by context is skipped by _is_nat_type exactly like generic constructor fields and effect-op formals — the same call-site-instantiation gap, which you rated Heavy lift. Added it to the #747 residual (KNOWN_ISSUES / ROADMAP / issue body) and a code comment at the call-arg branch, per your suggestion.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

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

@aallan: Both dispositions look correct.

Let-statement stale binding — the _fresh_slot_var fallback on an untranslatable RHS is the direct symmetric counterpart of the round-4 destructure fix: a None from translate_expr now invalidates the slot rather than leaving cur_env pointing at the stale outer binding. The concrete regression (array_length(string_lines(...)) → E503 instead of spurious discharge) and test_let_non_translatable_source_invalidates_stale_binding are the right pins.

Generic function formals → #747 — skipping on TypeVar is consistent with the generic constructor field and effect-op formal deferrals already in the issue; all three cases share the same call-site-instantiation gap and the same fix class. The code comment at the call-arg branch makes the deferral explicit for future readers.

No further concerns. The PR is in good shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generalize @Nat invariant check to all binding sites (broader generalisation of #520)

1 participant