feat: @Nat binding-site narrowing obligations (#552)#748
Conversation
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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughImplements issue Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**docs/sitemap.xmlis excluded by!docs/**tests/conformance/ch04_nat_binding.verais excluded by!**/*.vera
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdKNOWN_ISSUES.mdROADMAP.mdSKILL.mdTESTING.mdscripts/check_doc_counts.pyspec/11-compilation.mdtests/conformance/manifest.jsontests/test_codegen.pytests/test_verifier.pyvera/README.mdvera/errors.pyvera/obligations/core.pyvera/smt.pyvera/verifier.pyvera/wasm/context.pyvera/wasm/operators.py
…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]>
…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]>
Review summaryThis PR went through two review passes — the Fixed
Regression tests: +6 (wrapped/nested subtraction caught and trapped, ADT sub-pattern pinned to no obligation, E503 + Deferred (tracked)
Declined
ScopeThe PR statically obligates narrowing at |
There was a problem hiding this comment.
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 winCheck
QualifiedCallformal types before only recursing into arguments.This branch misses narrowing at qualified operation calls. For example, an effect operation with a
Natparameter such asIO.sleep(@Int.0)is represented as aQualifiedCall, 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.sleeptakesNat -> 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 liftInclude imported ADT constructors in constructor-field narrowing checks.
_lookup_constructor_info()only searchesself.env, but_register_modules()registers imported modules in isolation and injects only public functions. A public imported constructor with a concreteNatfield can therefore accept anIntargument 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 winDo not record unguarded non-let failures as Tier-3 runtime obligations.
val is Noneand solver timeout recordnat_bindas Tier-3 for every site, but the PR’s runtime guard only backsletbindings. 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 usingtier3_runtimehere.🤖 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 winBroaden the
#747row to match the limitations table.
KNOWN_ISSUES.mdstill lists generic constructor fields instantiated to@Natas uncovered, but this row only mentions ADT sub-pattern binds and non-literal tuple destructures. Please keep the two docs aligned so#747does not read as narrower than the current gap. Based on the supplied#552scope notes andKNOWN_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
⛔ Files ignored due to path filters (3)
docs/SKILL.mdis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**
📒 Files selected for processing (12)
AGENTS.mdFAQ.mdKNOWN_ISSUES.mdROADMAP.mdSKILL.mdTESTING.mdscripts/build_site.pytests/conformance/manifest.jsontests/test_codegen.pytests/test_verifier.pyvera/verifier.pyvera/wasm/operators.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]>
|
@coderabbitai Triaged the four outside-diff findings against current code — two fixed in d939737, two declined with reasons: 1. 2. ROADMAP 3. Imported ADT constructors (verifier.py) — confirmed, deferred to #747. 4. Non- |
|
Tip For best results, initiate chat on the files or code changes.
1 & 2 — fixed. The 3 — deferred correctly. 4 — decline accepted. The false-positive argument is sound: No further concerns on this pass. The PR is in good shape. |
There was a problem hiding this comment.
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 winRegister user-defined effect operation signatures before checking effect-op arguments.
Line 1369 now depends on
lookup_effect_op()exposingparam_types, but_register_effect()stores every userEffectDeclwithoperations={}. A user effect such aseffect E { op wait(Nat -> Unit); }followed byE.wait(@Int.0)would therefore bypass the new E503 narrowing check. Mirror_register_ability()here and populateOpInfoentries 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 winUpdate 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
📒 Files selected for processing (6)
CHANGELOG.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdtests/test_verifier.pyvera/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]>
|
@coderabbitai Both outside-diff findings fixed in ee5108d:
TESTING.md:199 stale count — fixed. |
|
Tip For best results, initiate chat on the files or code changes.
TESTING.md No further concerns. The PR is in good shape. |
There was a problem hiding this comment.
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 liftDo not keep walking with the old slot environment after non-literal destructures.
For
let Tuple<Int> =@tuple<Int>.0;, the following@Int.0refers to the destructured field, but this branch only pushes slots for literalTuple(...)sources. If an outer@Int.0is constrained non-negative, a latertakes_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
📒 Files selected for processing (5)
CHANGELOG.mdROADMAP.mdTESTING.mdtests/test_verifier.pyvera/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]>
|
@coderabbitai The non-literal-destructure stale-binding finding (LetDestruct branch) is fixed in c0e0732. The branch now rebinds every destructured slot in Verified: a non-literal destructure ( |
|
Tip For best results, initiate chat on the files or code changes.
The The |
…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]>
Post-CodeRabbit
|
Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
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 winFreshen untranslatable
letbindings before walking later statements.When
translate_expr(stmt.value, cur_env)returnsNone,cur_envis not updated, so a later same-type slot reference can resolve to a stale outer binding and falsely discharge E503. For example, afterlet@int= E.next(()), a latertakes_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 liftInstantiate generic function formals before deciding there is no
@Natnarrowing.This branch only recognises concretely resolved
Natformals. A generic function whoseTypeVaris fixed toNatby another argument or the surrounding context can still accept an@Intvalue without E503 and without a call-site runtime guard, e.g. collection/option-style APIs whereT = Nat. Either instantiateparam_typesfor the call before_is_nat_type(formal), or explicitly add generic function formals to the same#747deferral/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
⛔ Files ignored due to path filters (1)
docs/llms-full.txtis excluded by!docs/**
📒 Files selected for processing (9)
CHANGELOG.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdtests/test_codegen.pytests/test_verifier.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
) 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]>
|
@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 Generic function formals (verifier.py:1355) — deferred to #747. A generic function whose |
|
Tip For best results, initiate chat on the files or code changes.
Let-statement stale binding — the Generic function formals → No further concerns. The PR is in good shape. |
Summary
Generalises the #520
@Nat - @Natsubtraction obligation to the binding sites where a value that can be negative narrows into a@Natslot — statically obligatinglet, call-argument, constructor-field, top-level match-bind, and literal-tuple-destructure narrowing (Tier-1,E503), with a runtime guard at theletsite. This narrows the soundness hole where a negative@Intcould be stored silently in a@Nat; the residual (projection sites, and runtime guards beyond theletsite) is tracked as #747.What's covered
Verifier (Tier-1 static obligation,
E503) — a new_walk_for_nat_binding_obligationswalk emits avalue >= 0proof obligation, discharged from preconditions and path conditions exactly like #520, at:letbindingslet @Nat = @Int.0f(@Int.0)with an@NatformalMkBox(@Int.0)with a concrete@Natfieldmatch @Int.0 { @Nat -> ... }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@Natbut valued negative, and #520 deliberately defers it here. This keeps #552 disjoint from #520's@Nat - @Nat(with@Natprovenance) so the two obligations never co-fire on one site.Codegen (Tier-3 runtime guard) — a new
_emit_nat_bind_guardtraps on a negative value at the canonicalletsite, for programs compiled withoutvera 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_completionfilled the now-unconstrained slots with arbitrary defaults (e.g.@Int.0 = 0for the goal@Int.0 >= 0) instead of the violating assignment. Extracting the model before the pop sharpensE502,E503, and call-site precondition diagnostics alike.Scope and deferrals
The issue's documented core — narrowing into a
@Natlet binding or argument — is covered and extended (constructor fields, match binds, literal destructures, and the0 - 1carve-out). Two narrowing shapes from the broader "all binding sites" generalisation are deferred:match opt { Some(@Nat.0) -> }onOption<Int>) and non-literal tuple destructures — the bound value is a field projection whose source type the verifier does not retain (slot_envmaps slot → Z3 var, not → VeraType), so obligating them soundly needs scrutinee-type inference. Tracked as Generalize @Nat narrowing obligation to projection binding sites (ADT sub-pattern, non-literal destructure) #747.letsite — the verifier statically obligates all five sites (Tier-1), but the codegen runtime guard (the belt-and-suspenders for programs compiled withoutvera verify) is emitted only at theletsite, so a Tier-3 (untranslatable-value) narrowing at a call-argument / constructor-field / match-bind / destructure site is caught byvera verifybut not by an unverified compile. Folded into Generalize @Nat narrowing obligation to projection binding sites (ADT sub-pattern, non-literal destructure) #747.{ @Int | p }) — refinement predicates are verified nowhere today (parsed and stripped to the base type); a separate epic, Implement refinement-type predicate verification #746.Tests
TestNatBindingObligation552(17 verifier cases): all six sites, three Nat subtraction silently underflows to negative i64 — refinement-type soundness hole #520 double-emit pins, the0 - 1carve-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-@NatWAT assertions.tests/conformance/ch04_nat_binding.vera(levelrun): 16 Tier-1 obligations across let / call-arg / path-condition / destructure.json.vera+1 T1;string_utilities.vera+3 T3 wherenat_to_int(array_length(...))narrowsarray_length's@Intresult intonat_to_int's@Natparam).Closes #552
🤖 Generated with Claude Code
Summary by CodeRabbit
@Int→@Natnon-negativity (>= 0) obligations at binding-site narrowing points (includinglet, call/effect arguments, constructor fields, match binds, and tuple destructures).let-site narrowing now emits a runtime guard that traps on negative values.@Natbinding-site behaviour.