feat(746): static verification of refinement-type predicates (v0.0.174)#763
Conversation
Add the verifier helpers #746 builds on, with no behaviour change (unused until Step 2): - _refined_parts(ty): (base, predicate) of a user RefinedType, or None. @nat (a PrimitiveType, not a RefinedType) is deliberately excluded so the nat_bind and refine_bind paths stay disjoint (one obligation per site, never both). - _base_slot_name(base): the slot type-name the predicate's binder uses (the base primitive's name, e.g. the @Int.0 binder -> "Int"); None for a non-primitive base (predicate left untranslatable -> Tier 3, never silent). - _translate_refined_predicate(smt, refined_ty, value_term): translate the predicate to Z3 with the binder substituted by value_term, against a fresh SlotEnv keyed on the base name. Pinned the binder convention empirically: the predicate's SlotRef carries the BASE name ("Int"), not the alias ("PosInt"), so a wrong push-key would leave the binder unconstrained and silently "verify". Unit tests prove the substitution (v=5 -> True, v=-1 -> False) and that a builtin-call predicate translates. Co-Authored-By: Claude <[email protected]>
Generalises the proven @nat binding-discharge machinery (#552/#747) from the baked-in `>= 0` to an arbitrary translated refinement predicate, closing the Tier-0 silent failure where `type PosInt = { @int | @Int.0 > 0 }` parsed but its predicate was never checked. Implementation (verifier.py + smt reuse): - `_is_refined_type` / extended `_translate_refined_predicate` (folds the base-@nat intrinsic `>= 0` into the obligation so a refinement over @nat discharges `>= 0 && P`, never silently dropping P). - `_narrows_into_refined` — R3 exemption via predicate-AST equality (NOT types_equal, which ignores predicates), so an already-`@PosInt` source raises no obligation while `@Percentage` into `@PosInt` still obligates. - `_check_refined_binding_obligation` + `_record_refined_bind_tier3` mirror the @nat discharge: verified -> Tier 1; counterexample -> E505; untranslatable value/predicate/non-primitive base or timeout -> E506 Tier-3 warning, excluded from totals, never a silent pass (R7; refined narrowings have no codegen runtime guard yet). - Param-assume: a refined param's predicate is assumed into the body (the matched half of the call-site discharge, R1). - Wired refined-first at every binding site (let / call-arg / ctor-field / effect-op-arg / tuple-destructure / match-bind) and at refined return positions. Bare @nat stays on its own path (#758 untouched). Diagnostics: E505 (predicate may be violated) renders the predicate source + counterexample; E506 (unverified, not runtime-guarded). New refine_bind ObligationKind. Two existing tests updated for the now-correct behaviour: the example tier-count golden (refinement_types.vera gains 2 Tier-1 + 1 tier3_unguarded) and a tester coverage test whose old ensures now verifies via param-assume (swapped for one that still fails, preserving the error-path coverage). Refs #746. Co-Authored-By: Claude <[email protected]>
Tests: - tests/test_verifier.py: TestRefinementPredicateVerification — discharge / E505 violation / E506 Tier-3 trio across call-arg, let, ctor-field, return, match-bind, tuple sites; plus the soundness-risk pins: R3 already-refined source -> zero obligations (and the distinct/stronger-refinement variants), R5 violating-return-is-caught, R7 non-primitive-base -> E506-not-silent, R8 multi-slot/string_length predicates, R9 bare-@Nat-stays-nat_bind, and the refinement-over-@nat `>= 0 && P` cases. Plus a translation-primitive unit test for the base-@nat conjoin. - tests/test_obligations.py: refine_bind kind enumerated + E505 violation carries a counterexample, both summary-consistent. - tests/test_tester_coverage.py / test_verifier.py golden already updated in the implementation commit. Conformance: ch02_refinement_types.vera header documents that the predicates are now statically discharged (param-assume, call-site, and return). Docs: - spec/02-types.md §2.6.4 "Predicate Verification" — when/where predicates are discharged + the deferred runtime-guard limitation. - vera/types.py RefinedType docstring no longer says "not verified in C3". - Limitation documented across the gated tiers (KNOWN_ISSUES.md, vera/README.md, SKILL.md, spec) referencing the codegen-guard follow-up #762. - examples/refinement_types.vera comments explain the Tier-1 discharges and the array-base Tier-3 boundary (#762). - TESTING.md / ROADMAP.md test counts refreshed. Refs #746, #762. Co-Authored-By: Claude <[email protected]>
Bumps the version across all six sites (pyproject.toml, vera/__init__.py, docs/index.html badge, README.md, uv.lock) and cuts the CHANGELOG. - CHANGELOG: promote [Unreleased] -> [0.0.174] (bundling the #759/#760 guard-hardening already there) and add the #746 refinement-predicate Added entry; recreate an empty [Unreleased]; add the [0.0.174] compare link. - HISTORY.md: new Stage 14 row for v0.0.174 (#746). - README.md: status line v0.0.174 + refreshed test/release counts. - docs/ site assets regenerated (llms.txt, llms-full.txt, index.md, docs/SKILL.md) to pick up the #746 limitation row. Refs #746, #762. Co-Authored-By: Claude <[email protected]>
|
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:
📝 WalkthroughWalkthroughGeneralises the ChangesRefinement-predicate static verification and runtime guards
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #763 +/- ##
==========================================
+ Coverage 91.63% 91.67% +0.04%
==========================================
Files 70 70
Lines 25463 26021 +558
Branches 321 321
==========================================
+ Hits 23332 23855 +523
- Misses 2123 2158 +35
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:
|
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 (3)
vera/verifier.py (3)
1560-1568:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRecover refined targets from the checker side-table, not only concrete formals.
These refined-first checks miss targets that are only known after desugaring or generic instantiation: piped calls still only use
_nat_binding_target, andTypeVarformals/fields instantiated asPosIntnever enter the refined path. Add a refined analogue of_nat_binding_target(arg, formal)and use it for pipe, call, constructor, and effect-op arguments soT = PosIntandleft |> use()get E505/E506 instead of being skipped.Suggested shape
+ def _refined_binding_target( + self, arg: ast.Expr, formal: Type | None + ) -> Type | None: + if formal is not None and self._is_refined_type(formal): + return formal + if formal is not None and not contains_typevar(formal): + return None + target = self._target_type_of(arg) + return target if target is not None and self._is_refined_type(target) else None + - if (formal is not None - and self._is_refined_type(formal) - and self._narrows_into_refined(arg, formal)): + refined_target = self._refined_binding_target(arg, formal) + if (refined_target is not None + and self._narrows_into_refined(arg, refined_target)): self._check_refined_binding_obligation( - decl, arg, formal, smt, slot_env, assumptions, + decl, arg, refined_target, smt, slot_env, assumptions, site="call argument", )Also applies to: 1599-1605, 1635-1641, 1672-1678
🤖 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 1560 - 1568, The `_nat_binding_target(arg, None)` calls in the pipe/call/constructor/effect-op argument checking blocks miss refined targets from desugaring and generic instantiation (e.g., TypeVar formals instantiated as PosInt). Create a new refined analogue method (such as `_nat_binding_target_refined`) that recovers refined targets from the checker side-table, then replace or supplement the `_nat_binding_target` checks at all four locations (in the pipe call block around line 1560, and the similar blocks at lines 1599-1605, 1635-1641, and 1672-1678) with calls to this new method to ensure E505/E506 errors are properly raised for refined instantiations.
657-684:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDo not let generic functions bypass refined return obligations.
The
decl.forall_varsbranch returns before the new refined-return check runs, so a generic function with a concrete refined return can silently produce an invalid inhabitant when its contracts are trivial, e.g.forall<T> fn bad(@t->@Posint) { 0 }. Please make the generic path still emit arefine_bindresult for refined return types — verified when translatable, otherwise E506 — instead of returning before Line 984.Also applies to: 973-984
🤖 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 657 - 684, The early return statement in the decl.forall_vars branch prevents refined return type verification from being performed on generic functions. Add refined return type checking within the generic function handling block before the return statement, ensuring that refine_bind results are emitted for any refined return types in the function signature. When a refined return type is present, record it as verified if it is translatable to Z3, otherwise record it with error code E506 to prevent generic functions with concrete refined returns from silently bypassing verification.
1797-1802:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftAdd refined projection obligations for tuple and ADT pattern binds.
Literal tuple destructures and top-level match binds are covered, but the projection paths still call Nat-only helpers.
let Tuple<@Posint> = make_tuple(...)andmatch opt { Some(@Posint) -> ... }can therefore bind anIntfield into a refined slot without any E505/E506; add a term-based refined checker analogous to_check_nat_binding_obligation_termand call it from the non-literal destructure and constructor sub-pattern walkers.Also applies to: 1905-1912
🤖 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 1797 - 1802, The method _obligate_destructure_narrowings and related code around lines 1905-1912 currently only call Nat-only helpers for handling projection paths in non-literal destructures, missing refined type projections for tuple and ADT pattern binds. Create a new term-based refined checker method analogous to _check_nat_binding_obligation_term that handles refined projections generally (not just Nat), then update _obligate_destructure_narrowings and the constructor sub-pattern walkers to call this new method from the non-literal destructure and constructor sub-pattern walkers to ensure refined type obligations are generated for cases like let Tuple<`@PosInt`> = make_tuple(...) and match opt { Some(`@PosInt`) -> ... }.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@spec/02-types.md`:
- Around line 253-255: The example using array_length in the refinement type
contradicts the specification because array_length is already listed as allowed
in the decidable fragment §2.6.1. Replace the array_length example with a
genuinely undecidable refinement predicate that actually falls outside the
decidable fragment, or alternatively rephrase the limitation description to
clarify that this is an unsupported lowering case rather than claiming the
refinement falls outside the decidable fragment. Ensure the example and
explanation are consistent with what is defined as permitted in §2.6.1.
In `@tests/test_verifier.py`:
- Around line 3661-3678: The test currently only asserts that at least one E506
warning exists but does not pin the expected count or verify there are no
errors. Tighten the assertion by replacing the generic check `assert warns,
"expected an E506 Tier-3 warning"` with a more specific check that verifies
exactly one E506 warning exists (using `len(warns) == 1`). Additionally, add an
assertion to verify that there are zero error diagnostics in the result object
to catch unexpected verifier errors that might otherwise be masked by the loose
assertion.
---
Outside diff comments:
In `@vera/verifier.py`:
- Around line 1560-1568: The `_nat_binding_target(arg, None)` calls in the
pipe/call/constructor/effect-op argument checking blocks miss refined targets
from desugaring and generic instantiation (e.g., TypeVar formals instantiated as
PosInt). Create a new refined analogue method (such as
`_nat_binding_target_refined`) that recovers refined targets from the checker
side-table, then replace or supplement the `_nat_binding_target` checks at all
four locations (in the pipe call block around line 1560, and the similar blocks
at lines 1599-1605, 1635-1641, and 1672-1678) with calls to this new method to
ensure E505/E506 errors are properly raised for refined instantiations.
- Around line 657-684: The early return statement in the decl.forall_vars branch
prevents refined return type verification from being performed on generic
functions. Add refined return type checking within the generic function handling
block before the return statement, ensuring that refine_bind results are emitted
for any refined return types in the function signature. When a refined return
type is present, record it as verified if it is translatable to Z3, otherwise
record it with error code E506 to prevent generic functions with concrete
refined returns from silently bypassing verification.
- Around line 1797-1802: The method _obligate_destructure_narrowings and related
code around lines 1905-1912 currently only call Nat-only helpers for handling
projection paths in non-literal destructures, missing refined type projections
for tuple and ADT pattern binds. Create a new term-based refined checker method
analogous to _check_nat_binding_obligation_term that handles refined projections
generally (not just Nat), then update _obligate_destructure_narrowings and the
constructor sub-pattern walkers to call this new method from the non-literal
destructure and constructor sub-pattern walkers to ensure refined type
obligations are generated for cases like let Tuple<`@PosInt`> = make_tuple(...)
and match opt { Some(`@PosInt`) -> ... }.
🪄 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: da20dd76-ee2f-4d08-bcdd-1df036ad6c91
⛔ Files ignored due to path filters (8)
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/**examples/refinement_types.verais excluded by!**/*.veratests/conformance/ch02_refinement_types.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (18)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlspec/02-types.mdtests/test_obligations.pytests/test_tester_coverage.pytests/test_verifier.pyvera/README.mdvera/__init__.pyvera/errors.pyvera/obligations/core.pyvera/types.pyvera/verifier.py
From the /review-pr pass (code-reviewer, silent-failure-hunter, pr-test-analyzer, type-design-analyzer, comment-analyzer) — no correctness/soundness defects found; these are accuracy + coverage improvements. Documentation accuracy: - Correct the Array-base Tier-3 reason in spec §2.6.4 and the example header: the cause is the non-primitive BASE (the predicate translator only encodes primitive bases), not "array_length is uninterpreted" (array_length IS translatable; the base check short-circuits before the predicate is reached). - Fix the match-binding pattern syntax in spec §2.6.4 (`@PosInt`, not `@PosInt.0` — a binding pattern carries no slot index). - Widen the `tier3_unguarded` ObligationStatus docstring: it is now shared by nat_bind (E504) and refine_bind (E506). Test coverage (new verify-level tests for previously-untested binding sites — each fires a refine_bind obligation, so a broken gate there would have shipped green): - effect-operation argument, match binding, and tuple-destructure components (discharge + violation each); let-binding discharge direction. - refine_bind Tier-3 bookkeeping: a non-primitive-base narrowing is excluded from summary.total and not counted as tier3_runtime (the total -= 1 path _assert_summary_consistent does not otherwise exercise). Cosmetic: hoist PrimitiveType to the module-level import; pass list(assumptions) at the return-position discharge to match the binding-site convention. Test counts refreshed (4489). Refs #746. Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ROADMAP.md (1)
23-24:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the completed
#746item from Tier 0.This work now belongs in
HISTORY.md, not the forward-looking roadmap. Keeping it here makes the roadmap stale.As per coding guidelines, completed items get deleted from
ROADMAP.mdand noted inHISTORY.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, Remove the completed item `#746` (Refinement-type predicate verification) from the ROADMAP.md file since it belongs in the completed items history, not the forward-looking roadmap. Delete the entire row containing the `#746` issue and its description about user refinement predicates from ROADMAP.md, then add this completed item with its details to HISTORY.md to maintain an accurate record of finished work.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@ROADMAP.md`:
- Around line 23-24: Remove the completed item `#746` (Refinement-type predicate
verification) from the ROADMAP.md file since it belongs in the completed items
history, not the forward-looking roadmap. Delete the entire row containing the
`#746` issue and its description about user refinement predicates from ROADMAP.md,
then add this completed item with its details to HISTORY.md to maintain an
accurate record of finished work.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b560e3d6-38d1-4320-904b-1fa314a949a5
⛔ Files ignored due to path filters (1)
examples/refinement_types.verais excluded by!**/*.vera
📒 Files selected for processing (8)
README.mdROADMAP.mdTESTING.mdspec/02-types.mdtests/test_obligations.pytests/test_verifier.pyvera/obligations/core.pyvera/verifier.py
… (CR) Addresses CodeRabbit's review (commit aaabc3b). The refined path now matches the @nat path's #552/#747 site coverage everywhere it can be done soundly. Coverage gaps closed (each fired no obligation before, so a violating value was silently accepted): - Piped arguments into a refined formal (`(0 - 5) |> use()`): the pipe branch only ran `_nat_binding_target`. Added a refined check via the new `_refined_binding_target(arg, formal)`, which also recovers a generic formal instantiated to a RefinedType (`T = PosInt`) from the checker side-table — wired at the pipe, call-argument, constructor-field, and effect-op sites. - ADT sub-pattern binds (`match opt { Some(@Posint) -> }`) and non-literal tuple-destructure components (`let Tuple<@Posint> = f()`): the projection walkers called Nat-only helpers. Added a term-based `_check_refined_binding_obligation_term` (mirroring the @nat term checker) and a type-level `_refined_field_narrows` gate, wired into both walkers (refined-first, R9-disjoint; untranslatable/unprojectable -> E506 Tier-3, never a silent pass). Deferred (documented): a concrete refined return on a *generic* function (`forall<T> fn f(@t -> @Posint)`) is not obligated — the verifier's early return for generics skips all static checks (#555). Noted on the #555 row. Inline findings: - spec §2.6.4: separate the two Tier-3 causes — a predicate outside the decidable fragment vs a non-primitive base the translator does not lower (array_length IS in the fragment; the base is the gate), so the array example no longer reads as a contradiction of §2.6.1. - test_non_primitive_base_is_tier3_e506: assert zero error diagnostics and pin the E506 count to 1 (avoid a false pass if an unexpected error appears). Tests: pipe / ADT-sub-pattern / non-literal-destructure discharge-or-exempt + violation. Counts refreshed (4492); site assets regenerated. Refs #746, #555. Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
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)
3391-3441:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not treat
Byterefinements as invariant-free primitive refinements.Line 3417 says only
@Natcontributes an intrinsic base invariant, butByteis also range-constrained. Since the verifier declaration path has noBytebranch and falls through todeclare_int, a predicate like{@byte|@Byte.0< 256 }is checked over an unconstrained integer, producing false E505s for valid byte values. Either inject0 <= value <= 255forByteand declare byte slots with the same bounds, or make unsupported primitive bases returnNoneso they surface as E506 instead of using the wrong SMT semantics.As per coding guidelines,
Byteis an unsigned 8-bit integer with values0–255.🤖 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 3391 - 3441, The `_translate_refined_predicate` method currently only injects an intrinsic invariant for `@Nat` (>= 0), but `@Byte` is also a range-constrained primitive that needs similar treatment since it has bounds 0-255. After the existing check for `base == NAT` that returns `z3.And(value_term >= 0, translated)`, add a similar check for `Byte` that returns `z3.And(z3.And(value_term >= 0, value_term <= 255), translated)` to ensure byte refinement predicates are checked against the correct SMT semantics with the proper bounds constraint.Source: Coding guidelines
998-1039:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPropagate refined return predicates to caller-side SMT assumptions.
Line 1022 records the callee’s refined return predicate as verified, but caller translation still receives only an unconstrained fresh base return value:
vera/smt.py::_translate_call_with_infostripsRefinedTypetobase_retand only assumes explicitEnsuresclauses. A caller usingmake_pos()as a@NonNegargument, or proving anensuresfrom a@PosIntresult, can therefore get a false E505/E500 unless the callee duplicated the refinement as an explicitensures. Add the refined return predicate as a modular postcondition on the fresh call result, or synthesise an equivalent call-site assumption when translating calls returningRefinedType.🤖 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 998 - 1039, The verified refined return predicate from lines 998-1039 is only recorded locally and not propagated to callers, so when `_translate_call_with_info` in smt.py translates a call to this function, it strips the RefinedType constraint and leaves the fresh return value unconstrained. After the refined return predicate is verified and recorded by `_record_obligation` around line 1022, you need to ensure this constraint is also added to the caller-side SMT assumptions when a function returning a RefinedType is called. Either synthesize a call-site assumption that encodes the verified refined predicate on the fresh call result, or add it as a modular postcondition that the SMT translation layer can use when processing calls to functions with verified refined return types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@vera/verifier.py`:
- Around line 3391-3441: The `_translate_refined_predicate` method currently
only injects an intrinsic invariant for `@Nat` (>= 0), but `@Byte` is also a
range-constrained primitive that needs similar treatment since it has bounds
0-255. After the existing check for `base == NAT` that returns
`z3.And(value_term >= 0, translated)`, add a similar check for `Byte` that
returns `z3.And(z3.And(value_term >= 0, value_term <= 255), translated)` to
ensure byte refinement predicates are checked against the correct SMT semantics
with the proper bounds constraint.
- Around line 998-1039: The verified refined return predicate from lines
998-1039 is only recorded locally and not propagated to callers, so when
`_translate_call_with_info` in smt.py translates a call to this function, it
strips the RefinedType constraint and leaves the fresh return value
unconstrained. After the refined return predicate is verified and recorded by
`_record_obligation` around line 1022, you need to ensure this constraint is
also added to the caller-side SMT assumptions when a function returning a
RefinedType is called. Either synthesize a call-site assumption that encodes the
verified refined predicate on the fresh call result, or add it as a modular
postcondition that the SMT translation layer can use when processing calls to
functions with verified refined return types.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7afd1e16-503c-44e0-83cc-a9d36aacfdde
📒 Files selected for processing (7)
KNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdspec/02-types.mdtests/test_verifier.pyvera/verifier.py
Addresses the user's pushback on deferring #762: the runtime guard does NOT need new infrastructure. Vera already compiles boolean expressions to WASM (translate_expr — how requires/ensures become runtime checks) and already has the $vera.contract_fail host channel. A refinement predicate is structurally a contract on the value, so the guard reuses both directly. Implementation (codegen): - `_refinement_guard_parts` / `_emit_refinement_check` (contracts mixin): resolve an alias chain to the underlying RefinementType, bind the predicate's `@<base>.0` binder to the value's local in a fresh slot env, translate the predicate, and trap via $vera.contract_fail when false. - Refined parameters are guarded at function entry (after the explicit preconditions); refined returns at function exit (alongside postconditions, incl. the String i32_pair return by saving both halves around the check). Coverage (verified end-to-end via `vera run`): - Refined params over every primitive base (Int/Nat/Bool/Float64/String) trap on a violating value, pass on a valid one. This covers the public/FFI boundary — an untrusted caller can't bypass the callee's entry check. - Refined scalar + String returns trap on a violating result. - Call arguments are guarded transitively by the callee's entry guard (no separate call-site guard needed). - Generic functions: the guard fires on the monomorphised instance, so a concrete refined return on a `forall<T>` fn is runtime-guarded even though its static obligation is skipped (#555). Scope: refinements over a NON-PRIMITIVE base (e.g. `{ @array<Int> | ... }`) stay Tier-3 statically (the predicate translator does not lower a collection binder) and are not refinement-runtime-guarded — the underlying operations' checks (e.g. array bounds) are the safety net. #762 is narrowed to exactly that residual. Docs updated to reflect runtime guards are implemented (spec §2.6.5, KNOWN_ISSUES, vera/README, SKILL, CHANGELOG, example); the E506 reporter and the #555 note corrected accordingly. 8 codegen runtime-guard tests added. Refs #746. Narrows #762. Co-Authored-By: Claude <[email protected]>
… refined returns Addresses the user's direction to close #762 in this PR (not defer/narrow) plus the latest CodeRabbit findings. Runtime guards now cover EVERY refinement base, not just primitives (closes #762): - The guard compiles the predicate to WASM and resolves the binder by its canonical slot name (`Array<Int>`, not a bare `Array`) — the bug that made the array predicate silently fail to compile. Threading the function's slot env (rather than a bare one) gives a pair value its `(ptr, len)` context. - Verified end-to-end: `head([])` into a `@NonEmptyArray` parameter, and a `@NonEmptyArray` return, both trap with a refinement-violation diagnostic (Z3 can't decide `array_length`, but codegen can). - Accounting reconciled: a Tier-3 refinement narrowing is now recorded as a runtime-checked `tier3` (informational E506 "checked at run time"), not `tier3_unguarded`/excluded — every refinement narrowing is guarded, directly at the boundary or transitively. Example tier counts 258/29/287/0. Refined returns propagate to callers (CodeRabbit): `_translate_call_with_info` now assumes a refined return's predicate on the fresh call result, so a caller can rely on a verified refined return (`let @Posint = mk(...); ...` now proves `@PosInt.0 > 0`). Other CodeRabbit findings: - ROADMAP.md: removed the completed #746 row (it's in HISTORY). - @byte intrinsic bound: NOT changed — `@Byte`'s 0-255 isn't modeled at declaration (no `declare_byte`; not masked in codegen), so conjoining it only into the refinement predicate would false-positive a `@Byte` source and mismatch the codegen guard. Proper enforcement is a separate `@Byte`-semantics task; `{@byte|P}` (no occurrences) checks P consistently in both tiers. CI lint fix: refreshed the two stale `check_spec_examples.py` allowlist line numbers (the §2.6.4/2.6.5 additions shifted the `forall sort`/`swap` blocks). Docs updated to reflect #762 closed (spec §2.6.5, CHANGELOG, example; #762 rows removed from KNOWN_ISSUES/README/SKILL). 2 array runtime-guard tests added. Closes #762. Refs #746. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Thanks — addressed the three outside-diff findings:
|
|
@coderabbitai Addressed the latest outside-diff findings in commit 5a2706f:
|
|
Tip For best results, initiate chat on the files or code changes.
No further concerns on this PR from my side. |
|
Tip For best results, initiate chat on the files or code changes.
|
…unctions Addresses the last of the six outside-diff CodeRabbit findings (verifier.py 657-684): a generic function's early return skipped ALL static checks, so a concrete refined return like `forall<T> fn bad(@t -> @Posint) { 0 }` was only runtime-guarded (on its monomorphised instance), never caught at verify time. A concrete refined-return obligation is independent of the type parameter, so `_check_generic_refined_return` sets up a minimal SMT context (TypeVar params fall back to declare_int), translates the body, and discharges the predicate — `{ 0 }` is now an E505, `{ 5 }` verifies. The rest of a generic body's obligations still fall to Tier 3 (the broader #555/#732 per-monomorphization work); the #555 note is updated accordingly. Refs #746, #555. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Consolidated status on all six outside-diff findings (latest head
|
|
Tip For best results, initiate chat on the files or code changes.
No further concerns. This is a solid closure of all six items. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 13: The CHANGELOG.md entry contains two inaccuracies in the Tier-3
explanation section. First, correct the explanation that states Tier-3 warnings
are triggered by undecidable predicates like array_length - the actual reason
for Tier-3 is the non-primitive base type, not predicate decidability (the
array_length example is fine, but the reasoning is wrong). Second, clarify that
the runtime guard applies at every function boundary (refined parameters at
entry, refined returns at exit), not only at public/FFI boundaries - remove or
revise the parenthetical comment that suggests the guard only covers public/FFI
entry points.
In `@spec/02-types.md`:
- Around line 255-261: The Runtime Guards section currently states that the
compiler emits predicate checks only at public/FFI boundaries, but the actual
implementation emits guards at every function entry/exit. Revise the
documentation to clarify that refined parameters are checked at entry and
refined returns are checked at exit for all functions (not just public/FFI
boundaries), and that this broader scope provides more comprehensive coverage of
refinement violations. Update the relevant sentences in the paragraph to
accurately reflect that the guard applies universally at function boundaries
rather than limiting the statement to only public/FFI contexts.
In `@vera/codegen/contracts.py`:
- Around line 80-83: When translate_expr returns None for the predicate at the
guard creation point (where cond is None after calling
ctx.translate_expr(predicate, guard_env)), the code currently silently returns
None without any safeguard. Replace the silent return with a diagnostic emission
that indicates the refinement guard could not be lowered, then either skip
processing the function or raise an exception to fail closed. Apply this same
pattern to all other locations where refinement guards are created and
translate_expr is called (including the parameter guards handling), ensuring no
refinement boundary is left unguarded at runtime due to a failed predicate
translation.
- Around line 47-54: The current generic type formatting logic in the code that
builds arg_names for base.type_args only extracts the immediate type name
(ta.name) without recursively handling nested generics. This causes
Array<Option<Int>> to be formatted as Array<Option>, breaking predicate
resolution. Modify the logic that appends to arg_names to recursively format
nested type arguments: when processing each type argument ta, check if it has
its own type_args and recursively format them instead of just appending ta.name,
so the full nested structure like Option<Int> is preserved in the final name
string.
In `@vera/verifier.py`:
- Around line 2265-2282: The `_record_refined_bind_tier3()` method currently
increments `tier3_runtime` for all refined narrowing fallbacks, but this
conflates guarded sites (refined parameters at entry, refined returns at exit)
with unguarded sites (let bindings, destructures, match patterns, ADT fields,
effect arguments). To fix this, either add a `guarded` parameter to
`_record_refined_bind_tier3()` (mirroring the pattern used in
`_record_nat_bind_tier3()`) and only increment `tier3_runtime` when `guarded` is
True, or ensure codegen emits runtime guards for all local refinement sites.
Update all call sites to pass the appropriate `guarded` flag value based on
whether the refinement site has a corresponding codegen guard.
🪄 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: 96c91e48-6f3e-487e-b3c6-e02c747c7cde
⛔ Files ignored due to path filters (2)
docs/llms-full.txtis excluded by!docs/**examples/refinement_types.verais excluded by!**/*.vera
📒 Files selected for processing (14)
CHANGELOG.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdscripts/check_spec_examples.pyspec/02-types.mdtests/test_codegen.pytests/test_obligations.pytests/test_verifier.pyvera/codegen/contracts.pyvera/codegen/functions.pyvera/smt.pyvera/verifier.py
|
@coderabbitai Re: the ROADMAP.md 23-24 outside-diff finding (review of cb33f4f) — Remove the completed |
|
@coderabbitai Re: the two vera/verifier.py outside-diff findings (review of 9282bab):
|
|
Tip For best results, initiate chat on the files or code changes.
|
|
Tip For best results, initiate chat on the files or code changes.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_verifier.py`:
- Around line 3936-3959: The "bad" case assertion is too loose, checking only
that at least one E505 exists without verifying the exact count or catching
spurious additional errors. In the
test_generic_concrete_refined_return_discharged method, replace the assertion
`assert errs, "expected E505 on the generic concrete refined return"` with a
tighter assertion that verifies exactly one E505 error exists and no other
error-level diagnostics are present. This should follow the pattern used in
test_non_primitive_base_is_tier3_e506, where the test asserts a specific count
of errors (e.g., `assert len([d for d in bad.diagnostics if d.error_code ==
"E505"]) == 1` and `assert [d for d in bad.diagnostics if d.severity == "error"]
== [<the E505>]`).
In `@vera/verifier.py`:
- Around line 2316-2341: The issue is that check_valid is being called with an
empty premise list, which means parameter predicates and requires clauses are
not being considered when validating the return type. To fix this, after setting
up the parameter variables in the loop over decl.params, collect the assumptions
from these parameters (their refined predicates) and any requires clauses from
the function declaration. Before calling smt.check_valid on the goal, pass these
collected assumptions as the second argument instead of the empty list, ensuring
the validity check properly seeds with the function's assumptions.
🪄 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: 7aa5ad43-0f55-424a-bbe8-6647ff55c7ea
📒 Files selected for processing (6)
KNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdtests/test_verifier.pyvera/verifier.py
Two were real bugs I introduced: - **Generic refined-return false positive** (verifier.py): `_check_generic_refined_return` discharged with an EMPTY premise, so a return justified by a refined param — `forall<T> fn keep(@Posint, @t -> @Posint) { @PosInt.0 }` — was a false E505. Now seeds the refined-param predicates and translatable `requires` clauses into the assumptions and passes them to `check_valid`. Verified, with a regression test. - **Tier-3 accounting conflated guarded/unguarded sites** (verifier.py): `_record_refined_bind_tier3` counted ALL Tier-3 refinement fallbacks as `tier3_runtime`, but codegen only guards param/return boundaries. Added a `guarded` flag (mirroring `_record_nat_bind_tier3`): a return narrowing and a call argument (caught by the callee's entry guard) are guarded -> `tier3` (informational E506); an internal narrowing — let / ctor-field / effect-op-arg / match-bind / destructure / ADT-sub-pattern — is unguarded -> `tier3_unguarded`, excluded from totals, so the verifier no longer overstates runtime safety. Regression test added. Doc-wording fixes: - CHANGELOG + spec §2.6.5: the Tier-3 reason is the non-primitive base (not `array_length` being "undecidable"), and the runtime guard applies at every function boundary, not only `public`/FFI entry points. - Hardened the generic-return "bad" test (pin E505 count, no other errors). Declined (with reasons in the PR replies): - Nested generic slot names: `_refinement_guard_parts` and `_translate_slot_ref` use the SAME one-level canonicalization, so they match — a nested `Array<Option<Int>>` guard resolves and fires (verified end-to-end), so the premise doesn't hold. - Fail-closed on un-lowerable guards: refinement predicates are constrained to the §2.6.1 decidable fragment (whose only slot is the binder, and whose builtins all lower to WASM), so `_emit_refinement_check` returns None only for a predicate that wouldn't type-check — unreachable for valid programs. Refs #746. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai full review All seven inline threads from the previous round are confirmed above (5 fixes, 2 declines accepted). Requesting a fresh top-level review of the current head |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
spec/02-types.md (1)
259-259:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReword the Tier-3 rationale.
array_length(...) > 0is still in the decidable fragment; the E506 here comes from the non-primitive@Arraybinder not being lowered, not fromarray_lengthbeing undecidable.🤖 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 `@spec/02-types.md` at line 259, The explanation around line 259 incorrectly attributes the Tier 3 classification to Z3 being unable to decide array_length. Reword the rationale to clarify that array_length(...) > 0 is actually in the decidable fragment and remains decidable; the Tier 3 classification instead comes from the non-primitive `@Array` binder not being lowered, which causes error E506, not from array_length being undecidable. Update the text to accurately reflect that the limitation is in the lowering of the `@Array` binder, not in the decidability of the comparison predicate itself.
🤖 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_codegen.py`:
- Around line 5407-5424: The current test has a false positive because head([])
traps both due to the NonEmptyArray guard and due to the out-of-bounds indexing
when accessing `@NonEmptyArray.0`[0]. Modify the body of the head function to
perform an operation that would return normally for an empty array (such as
returning a default value without accessing the array, or checking the array
length), so that the test specifically demonstrates the refinement guard is
firing rather than just the indexing failing. This ensures the test proves the
guard is actually working as intended.
In `@vera/codegen/contracts.py`:
- Around line 228-234: The comment block explaining pair and array return guard
behavior is outdated and contradicts the current implementation. Update the
comment in the section starting with "# Pair returns (String/Array) don't
support general ensures checks" to correctly reflect that array refined returns
ARE now guarded by _refinement_guard_parts() when they have refinements, rather
than stating they get no guard. Remove or correct the statement about array
bases being non-primitive and getting no guard, as this no longer matches the
actual behavior demonstrated by test_array_return_guard_traps_on_empty.
In `@vera/codegen/functions.py`:
- Around line 198-217: The current code compiles preconditions first via
_compile_preconditions and then adds refinement parameter guards via the for
loop that processes value_local and param_te pairs. However, refinement guards
must execute before preconditions since a requires expression may depend on the
refined parameter invariant. Reorder the logic to collect refinement parameter
guards first (move the for loop before the _compile_preconditions call), then
compile the preconditions, and combine both sets of instructions with the
refinement guards appearing first in pre_instrs.
In `@vera/errors.py`:
- Line 504: The E506 error message currently states "not runtime-guarded" which
is inaccurate because E506 is raised for both guarded and unguarded Tier-3
refinement cases. Modify the error message string for E506 to remove the "not
runtime-guarded" claim and instead use guard-neutral wording that only describes
the core issue of unverified refinement narrowing, without making assumptions
about the presence or absence of runtime guards.
In `@vera/obligations/core.py`:
- Around line 68-73: The comment for tier3_unguarded contains outdated semantics
regarding E506 warnings for refinement narrowing. The current comment implies
that refine_bind refinement narrowing warnings only occur at sites with no
runtime guard, but the current refinement flow now emits E506 warnings for both
runtime-checked and non-runtime-checked Tier-3 sites. Update the comment to
accurately reflect that E506 can be emitted for refinement narrowing regardless
of whether a runtime guard is present, removing the outdated qualifier about "no
runtime guard yet".
In `@vera/verifier.py`:
- Around line 2351-2354: The Float64 parameter handling in the generic
refined-return path is incorrectly declaring float types as Int-sort variables
instead of Float64/Real-sort variables. In the conditional block starting with
`elif self._is_float64_type(param_ty):` around line 2351-2352, replace the
`smt.declare_int(z3_name)` call with the appropriate SMT float or real sort
declaration that matches the non-generic setup. This ensures that float values
like 0.5 are properly included in the refinement proof quantification.
---
Duplicate comments:
In `@spec/02-types.md`:
- Line 259: The explanation around line 259 incorrectly attributes the Tier 3
classification to Z3 being unable to decide array_length. Reword the rationale
to clarify that array_length(...) > 0 is actually in the decidable fragment and
remains decidable; the Tier 3 classification instead comes from the
non-primitive `@Array` binder not being lowered, which causes error E506, not from
array_length being undecidable. Update the text to accurately reflect that the
limitation is in the lowering of the `@Array` binder, not in the decidability of
the comparison predicate itself.
🪄 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: 4a8fac17-2988-48c5-90f6-155ce8e13636
⛔ Files ignored due to path filters (7)
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/**examples/refinement_types.verais excluded by!**/*.veratests/conformance/ch02_refinement_types.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (21)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/02-types.mdtests/test_codegen.pytests/test_obligations.pytests/test_tester_coverage.pytests/test_verifier.pyvera/__init__.pyvera/codegen/contracts.pyvera/codegen/functions.pyvera/errors.pyvera/obligations/core.pyvera/smt.pyvera/types.pyvera/verifier.py
…d-return-from-match, generic hook, refinement-over-tuple
Full audit of all 18 outside-diff findings across the PR (the user flagged a
missed one). Verified each of the round's findings against current code; fixed
the valid ones, skipped the rest with reasons.
Fixed:
- smt.py refined-return binder (outside-diff, missed last round): `_translate_
call` assumed a callee's refined return under `ret_type.base.name`, so an
ALIAS-base refinement (`{ @Age | @Age.0 >= 18 }`, `type Age = Nat`) lost its
predicate (`@Age.0` won't resolve against `Nat` → z3_pred None). Now uses the
predicate binder via a SHARED `ast.predicate_binder_name` (the verifier's
static method now delegates to it, so verifier/codegen/SMT can't drift).
Confirmed: `needs_adult(mk_adult(...))` was E501, now clean.
- Refined return FROM a match arm (`Some(@Posint) -> @PosInt.0` returned as
@Posint) false-E505'd on BOTH the normal and generic paths — the arm
source-fact was a transient path condition, gone before the refined-return
goal is checked. `_translate_match` now also adds a global
`arm-matched => source-fact` implication, so the goal sees it. This is the
real fix behind the generic-path finding; the generic fast path now also
installs the `_subpattern_fact_hook` (so its match translation contributes
the implication). Soundness pinned: `Option<Int>` payload as @Posint still E505.
- contracts.py refinement-OVER-tuple: `{ @tuple<PosInt, Int> | true }` carries
no top-level Tuple shape, so its components crossed unguarded behind the
refinement. New `_resolve_tuple_type` unwraps a refinement to the tuple, used
by the emit helper (component now does both its predicate AND recursion), the
guardable-components predicate, and the param collection. Recurses through
`Tuple<Pair, Int>`. Confirmed: `use_pair(Tuple(-5,3))` now traps.
Skipped (with reason):
- pyproject/CHANGELOG "[0.0.174] missing": stale — the section and its compare
link are both present (CR reviewed a pre-changelog commit).
- verifier.py 1939-2003 "destructure facts → first-pass SMT (false-E501)": the
predicted false-E501 does NOT reproduce. `_translate_block` returns None at a
`LetDestruct`, truncating SMT block translation, so a call after a destructure
isn't modelled at all (neither false-E501 nor checked statically; the callee's
own runtime `requires` still guards it). That's a pre-existing, refinement-
independent gap in SMT block translation, orthogonal to this PR — flagged for
a separate issue rather than expanded here.
Full suite 4513 passed; conformance 90; examples 35; mypy/ruff/site-assets/
doc-counts clean.
Co-Authored-By: Claude <[email protected]>
|
@coderabbitai resume |
|
@coderabbitai Addressing the two outside-diff findings from the latest review:
|
|
Tip For best results, initiate chat on the files or code changes.
Would you like me to file a tracking issue for the |
|
Tip For best results, initiate chat on the files or code changes.
Destructure-facts gap ( Would you like me to file a tracking issue for the |
|
✅ Action performedReviews resumed. Full review finished. |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
vera/verifier.py (1)
865-866:⚠️ Potential issue | 🟡 MinorBlock
LetDestructsource facts do not reach SMT body translation.
vera/smt.pyline 1246–1267 (_translate_block) returnsNoneforLetDestructstatements without injecting source facts, whereas match arms receive refined sub-pattern facts via the_subpattern_fact_hook(lines 1273–1287) during translation. The verifier's narrowing walk (lines 1837–2003) does seedblock_assumptionswith_term_source_fact(lines 1997–2003), but this happens only in the second pass, after the initial body translation. A block likelet Tuple<@Posint,@int> =@Box.0; needs_positive(@PosInt.0)will perform body translation without the destructured@PosIntbinding's invariant (> 0) in scope, potentially triggering false E501/E505 on the call toneeds_positiveduring the first SMT pass.Fix: Either have
_translate_blockemit the equivalent source facts before translating the block's final expression (mirroring the match-arm pattern), or extend the narrowing walk to inject those facts intosmtbefore the initial body translation at line 866.Also applies to: 1939–2003
🤖 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 865 - 866, The LetDestruct block processing in _translate_block (vera/smt.py lines 1246-1267) does not inject destructured pattern source facts before translating the block's body expression, unlike match arms which use _subpattern_fact_hook to provide refined sub-pattern facts. To fix this, modify _translate_block to extract and emit source facts from LetDestruct pattern bindings (similar to how _subpattern_fact_hook injects facts for match arms at lines 1273-1287) before calling smt.translate_expr on the block body at line 866 in verifier.py. This ensures invariants from destructured values (like `@PosInt` from Tuple destructuring) are available in the SMT context during the initial body translation pass, preventing false E501/E505 errors on dependent function calls.vera/codegen/contracts.py (1)
45-49:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReuse the substituting alias resolver for refinement guard decisions.
Both alias walks still replace a
NamedTypewith_type_aliases[name]without applying use-sitetype_args. Fortype Id<T> = T; type Small = {@id<Nat> |@id<Nat>.0 < 10 },base_noderemains the unresolved alias body instead ofNat, so the runtime guard omits the implicit>= 0membership check and can accept-1at a public boundary. Use_resolve_type_alias()here too so guard extraction shares the substituting alias path already added below.Suggested fix
- node: ast.TypeExpr = te - seen: set[str] = set() - while (isinstance(node, ast.NamedType) - and node.name in self._type_aliases - and node.name not in seen): - seen.add(node.name) - node = self._type_aliases[node.name] + node = self._resolve_type_alias(te) @@ - base_node: ast.TypeExpr = base - bseen: set[str] = set() - while (isinstance(base_node, ast.NamedType) - and base_node.name in self._type_aliases - and base_node.name not in bseen): - bseen.add(base_node.name) - base_node = self._type_aliases[base_node.name] + base_node = self._resolve_type_alias(base)Also applies to: 77-83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/codegen/contracts.py` around lines 45 - 49, The while loop that walks through type aliases is directly replacing the node with _type_aliases[node.name] without applying use-site type_args substitution, causing refinement guards to work with unresolved alias bodies instead of properly substituted types. Replace the current alias resolution logic in this while loop with a call to the _resolve_type_alias() method, which already handles proper type argument substitution. Apply the same fix to the similar alias resolution code at lines 77-83 to ensure consistent behavior and proper guard extraction across all alias resolution paths.
🤖 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 `@TESTING.md`:
- Around line 61-63: The markdown table in TESTING.md has unescaped literal pipe
characters (|) within the table cell content in the coverage descriptions for
test_obligations.py, test_verifier.py, and test_codegen.py rows, which causes
the markdown parser to treat them as cell delimiters and break the table
structure. Fix this by escaping each literal pipe character in the cell content
with a backslash (change | to \|), or alternatively move the long coverage
descriptions outside the table into separate paragraphs or a different structure
to keep the table properly formatted.
In `@tests/test_codegen.py`:
- Around line 5605-5619: The test method
test_nested_refinement_over_tuple_guard_traps currently only validates the
violating case where a negative value fails the refinement guard on the nested
Pair type. To ensure component recursion does not over-trap valid values, add a
happy-path assertion by creating an additional test case or check that passes a
valid nested tuple value (such as Tuple with a positive integer in the inner
Tuple) to the entry function and verifies it executes successfully without
trapping, ensuring the guard logic correctly handles valid nested refinement
paths.
In `@tests/test_verifier.py`:
- Around line 3963-3979: The test test_generic_refined_return_from_match_arm
covers the positive case with Option<PosInt>, but a matching negative case with
Option<Int> is needed to ensure soundness and prevent generic-specific bugs from
laundering unrefined payloads. Add a new test method that mirrors
test_generic_refined_return_from_match_arm but uses Option<Int> instead of
Option<PosInt> in the function signature, parameter matching, and return type to
verify that the generic refined-return fast path properly rejects unrefined
payloads and produces appropriate error diagnostics.
---
Outside diff comments:
In `@vera/codegen/contracts.py`:
- Around line 45-49: The while loop that walks through type aliases is directly
replacing the node with _type_aliases[node.name] without applying use-site
type_args substitution, causing refinement guards to work with unresolved alias
bodies instead of properly substituted types. Replace the current alias
resolution logic in this while loop with a call to the _resolve_type_alias()
method, which already handles proper type argument substitution. Apply the same
fix to the similar alias resolution code at lines 77-83 to ensure consistent
behavior and proper guard extraction across all alias resolution paths.
In `@vera/verifier.py`:
- Around line 865-866: The LetDestruct block processing in _translate_block
(vera/smt.py lines 1246-1267) does not inject destructured pattern source facts
before translating the block's body expression, unlike match arms which use
_subpattern_fact_hook to provide refined sub-pattern facts. To fix this, modify
_translate_block to extract and emit source facts from LetDestruct pattern
bindings (similar to how _subpattern_fact_hook injects facts for match arms at
lines 1273-1287) before calling smt.translate_expr on the block body at line 866
in verifier.py. This ensures invariants from destructured values (like `@PosInt`
from Tuple destructuring) are available in the SMT context during the initial
body translation pass, preventing false E501/E505 errors on dependent function
calls.
🪄 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: d2016bb4-4863-4025-be49-bbf7a62164b9
📒 Files selected for processing (11)
CHANGELOG.mdREADME.mdROADMAP.mdTESTING.mdtests/test_codegen.pytests/test_verifier.pyvera/ast.pyvera/codegen/contracts.pyvera/codegen/functions.pyvera/smt.pyvera/verifier.py
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
vera/codegen/contracts.py (1)
45-49:⚠️ Potential issue | 🟠 Major | ⚡ Quick winApply generic alias substitution in the guard-decision walks.
These two alias walks still use the raw alias body, unlike
_resolve_type_alias(). Fortype Id<T> = T; type Small = {@id<Nat> |@id<Nat>.0 < 10 },base_nodebecomesTrather thanNat, so the implicit>= 0membership guard is omitted and-1can satisfy the boundary check.Suggested fix
- node: ast.TypeExpr = te - seen: set[str] = set() - while (isinstance(node, ast.NamedType) - and node.name in self._type_aliases - and node.name not in seen): - seen.add(node.name) - node = self._type_aliases[node.name] + node: ast.TypeExpr = self._resolve_type_alias(te) @@ - base_node: ast.TypeExpr = base - bseen: set[str] = set() - while (isinstance(base_node, ast.NamedType) - and base_node.name in self._type_aliases - and base_node.name not in bseen): - bseen.add(base_node.name) - base_node = self._type_aliases[base_node.name] + base_node: ast.TypeExpr = self._resolve_type_alias(base)Also applies to: 77-83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/codegen/contracts.py` around lines 45 - 49, Apply generic alias substitution in the two guard-decision walk functions at the specified locations, similar to how _resolve_type_alias() handles type aliases. When processing generic type aliases like Id<T>, ensure that type parameters are properly substituted so that base_node correctly resolves to the concrete type (e.g., Nat instead of T). This will ensure that implicit membership guards like the >= 0 check are properly included and prevent invalid values like -1 from satisfying boundary checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@vera/codegen/functions.py`:
- Around line 245-265: The guard emission order for refined-tuple parameters
needs to be reordered to ensure component refinements are established before
top-level predicates execute. In the code section around the refined-tuple
parameter handling in functions.py, locate where top-level predicate guards are
emitted before _emit_component_refinement_guards is called. Reorder the emission
sequence so that _emit_component_refinement_guards executes first to establish
the component refinements, followed by the top-level refined guards, and ensure
both guard emissions occur before any requires(...) call. This ensures that
predicates like P in patterns such as { `@Tuple`<NonZero, Int> | P } have access
to properly established component refinements when they execute.
In `@vera/verifier.py`:
- Around line 1726-1732: At tuple construction and destructure sites (around
lines 1726-1732, 1948-1952, and 2885-2888), before extracting type_args from
tuple types, unwrap any RefinedType wrappers by accessing the base property
first. When processing tuples that may be wrapped in RefinedType (like {
`@Tuple`<PosInt, Int> | P }), use RefinedType.base to get the underlying AdtType
before reading component types, ensuring component obligations are properly
tracked instead of being skipped due to the refined type hiding the actual tuple
base.
- Around line 2721-2724: The current sub-pattern handling in the loops at lines
2721-2724 and 2794-2824 only processes direct BindingPattern children and does
not recursively handle ConstructorPattern children, causing nested patterns like
Some(Some(`@PosInt`)) to miss inner refine_bind obligations and source facts.
Refactor the sub-pattern obligation and fact generation logic into a recursive
helper function that accepts the projected field term and declared field type as
parameters, then have this helper recursively process both BindingPattern and
ConstructorPattern children so that nested constructor patterns generate the
appropriate refine_bind obligations and source facts at each nesting level.
---
Duplicate comments:
In `@vera/codegen/contracts.py`:
- Around line 45-49: Apply generic alias substitution in the two guard-decision
walk functions at the specified locations, similar to how _resolve_type_alias()
handles type aliases. When processing generic type aliases like Id<T>, ensure
that type parameters are properly substituted so that base_node correctly
resolves to the concrete type (e.g., Nat instead of T). This will ensure that
implicit membership guards like the >= 0 check are properly included and prevent
invalid values like -1 from satisfying boundary checks.
🪄 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: d2016bb4-4863-4025-be49-bbf7a62164b9
⛔ Files ignored due to path filters (7)
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/**examples/refinement_types.verais excluded by!**/*.veratests/conformance/ch02_refinement_types.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (22)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/02-types.mdtests/test_codegen.pytests/test_obligations.pytests/test_tester_coverage.pytests/test_verifier.pyvera/__init__.pyvera/ast.pyvera/codegen/contracts.pyvera/codegen/functions.pyvera/errors.pyvera/obligations/core.pyvera/smt.pyvera/types.pyvera/verifier.py
…ISSUES (#764) Filed #764 for the pre-existing gap surfaced during the #763 review: a call's precondition isn't statically checked when the call is at/after a let-destructure (_translate_block returns None at the LetDestruct, truncating SMT body translation), so it verifies tier1-clean though E501 never fires. Runtime-guarded by the callee's own requires(...); narrowing obligations (separate AST walks) are unaffected. Same class as #730, distinct root cause. Added a Limitations row next to #730. Limitation-sync + doc-counts green. Co-Authored-By: Claude <[email protected]>
… guard ordering + table/test fixes (CR re-review of 3948d5c) CR's re-review of 3948d5c found 6 more; fixed 5, the 6th (Critical, heavy-lift) is surfaced separately. - (Major, false Tier-1) verifier: the tuple-construction and tuple-destructure obligation sites only accept `AdtType`, so a refinement OVER a tuple (`{ @tuple<PosInt, Int> | P }`) hid the `Tuple` base and skipped the component obligation — `use_pair(Tuple(@Int.0, 3))` verified clean though codegen runtime-traps it. Now unwraps `RefinedType.base` before reading `type_args` at both sites. Confirmed: that case is now E505 statically; the valid case clean. - (Major, ordering) functions.py / contracts.py: emit the tuple-component boundary guards BEFORE the enclosing top-level refined-tuple predicate (param entry and return exit), so a refinement that reads its own components sees established values rather than running P against an unchecked FFI tuple. - (formatting) TESTING.md: escaped the literal `|` in `{ @Age | ... }` / `{ @tuple | true }` refinement examples that were breaking the table. - (tests) added a happy-path assertion to the nested refinement-over-tuple codegen test, and a generic-fast-path NEGATIVE match-arm soundness test (`Option<Int>` payload as `@PosInt` still E505). Full suite 4514 passed; conformance 90; examples 35; mypy/ruff/site-assets/ doc-counts clean. Co-Authored-By: Claude <[email protected]>
…atterns (CR finding 6 — closes an unguarded false Tier-1) CR's Critical finding on 3948d5c: a refinement/@nat narrowing bound in a NESTED constructor sub-pattern (`Some(Some(@Posint))` on `Option<Option<Int>>`) was neither obligated nor runtime-guarded — an UNGUARDED false Tier-1. (Partially pre-existing for @nat; #746 extended the non-recursive sub-pattern path to refinements, widening it.) Maintainer chose the verifier-recursion fix. - `_obligate_subpattern_narrowings` now recurses into nested ConstructorPattern sub-patterns via a new term-keyed `_obligate_subpattern_term`, obligating each inner narrowing against its projected field accessor. `_subpattern_source_facts` likewise recurses (`_subpattern_source_facts_term`) so a nested bind's source fact is carried. Cycle/infinite-type backstop at depth 16. - Soundness: a verified program now rejects a bad nested narrowing — confirmed `Some(Some(@Posint))` on `Option<Option<Int>>` is E505; the valid `Option<Option<PosInt>>` case (no narrowing) stays clean (no over-obligation). - The nested-bind RUNTIME guard stays a #758-class deferral (codegen's `_extract_constructor_fields` binds only direct sub-patterns) — filed #765 and added a KNOWN_ISSUES Limitations row. So a verified program is sound; an unverified compile is honestly unchecked at the nested site. Tests: nested-narrowing-obligated (E505) + nested-no-false-positive (clean). Full suite 4516 passed; conformance 90 (incl. ch04_match_nested); examples 35; mypy/ruff/limitations-sync/doc-counts/site-assets clean. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Two outside-diff findings, both verified — declining both, with evidence:
|
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
vera/verifier.py (1)
2677-2680:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winUnwrap refined ADT scrutinees before constructor-field instantiation.
_is_adt_type()now unwraps refined ADT bases for SMT declarations, but_instantiated_field_types()still requires the scrutinee itself to be anAdtTypewhen substituting generic constructor fields. A scrutinee such as{@option<Int> | P }therefore returnsNoneforSome, and theSome(@Posint)sub-pattern obligation/fact is skipped entirely.🐛 Proposed fix
if ci is None or ci.field_types is None: return None field_types = ci.field_types + if isinstance(scrut_ty, RefinedType): + scrut_ty = scrut_ty.base if ci.parent_type_params: # Generic constructor: its declared field types carry the parent's # TypeVars, which only the scrutinee's instantiation resolves. If🤖 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 2677 - 2680, In the _instantiated_field_types() function, the isinstance check for AdtType fails for refined ADT scrutinees (like { `@Option`<Int> | P }) because they are not direct AdtType instances. Before checking if scrut_ty is an AdtType with type_args, unwrap the refined ADT base from the scrutinee using the same logic that _is_adt_type() uses, then perform the isinstance check on the unwrapped type to ensure refined ADT scrutinees are properly handled for constructor field type instantiation.vera/codegen/contracts.py (2)
43-49:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the substituting alias resolver for refinement guard decisions.
These two local alias walks still drop generic type arguments, even though
_resolve_type_alias()below preserves them. A top-level generic refinement alias such asNonEmpty<T> = {@array<T> | ... }can therefore resolve to a predicate overArray<T>and returnNoneat Line 64, skipping the boundary guard; similarlyId<Nat>will not be recognised as Nat-based for the implicit membership check.Proposed fix
- node: ast.TypeExpr = te - seen: set[str] = set() - while (isinstance(node, ast.NamedType) - and node.name in self._type_aliases - and node.name not in seen): - seen.add(node.name) - node = self._type_aliases[node.name] + node: ast.TypeExpr = self._resolve_type_alias(te) @@ - base_node: ast.TypeExpr = base - bseen: set[str] = set() - while (isinstance(base_node, ast.NamedType) - and base_node.name in self._type_aliases - and base_node.name not in bseen): - bseen.add(base_node.name) - base_node = self._type_aliases[base_node.name] + base_node: ast.TypeExpr = self._resolve_type_alias(base)Also applies to: 77-83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/codegen/contracts.py` around lines 43 - 49, The manual type alias resolution in the while loop that iterates through self._type_aliases drops generic type arguments, whereas the _resolve_type_alias() method below preserves them. This causes issues with generic refinement aliases like NonEmpty<T> losing their type parameters during resolution. Replace the manual while loop that walks through self._type_aliases with a call to _resolve_type_alias() instead. This same issue also appears in another location (lines 77-83), so apply the same fix to both occurrences to ensure generic type arguments are preserved for refinement guard decisions.
110-129:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve Byte base membership in refinement guards.
Bytefalls through toreturn (predicate, name)with only the user predicate. For{@byte|@Byte.0< 10 }, an FFI/public caller can pass-1or300and satisfy the lowered predicate without satisfying theBytebase range. Either conjoin0 <=@Byte.0<= 255like the Nat invariant, or classify Byte as unguarded consistently rather than emitting a partial guard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/codegen/contracts.py` around lines 110 - 129, The code currently handles Unit and Nat base types with special guards in the boundary predicate check, but Byte type is not explicitly handled and falls through with only the user predicate, allowing invalid values outside the 0-255 range to pass. Add a new conditional block after the Nat handling to detect when base_node is an ast.NamedType with name equal to "Byte", and construct a BinaryExpr similar to the Nat case that conjuncts two range checks (0 <= the slot reference AND the slot reference <= 255) with the existing predicate, then return the combined predicate with the name tuple.
♻️ Duplicate comments (1)
vera/verifier.py (1)
1955-1960:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnwrap refined tuple sources before seeding component facts.
This source-fact path still extracts
type_argsonly from a bareAdtType. A guaranteed source typed as{@tuple<PosInt, Int> | P }binds fresh component slots but drops the source component invariant, so a later valid re-narrowing can false-E505. This appears to be the same tuple-unwrapping gap previously flagged for this area.🐛 Proposed fix
src_tuple_ty = self._resolved_type_of(stmt.value) + if isinstance(src_tuple_ty, RefinedType): + src_tuple_ty = src_tuple_ty.base src_args = ( src_tuple_ty.type_args if isinstance(src_tuple_ty, AdtType)🤖 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 1955 - 1960, The code extracting type_args from src_tuple_ty only handles bare AdtType instances but misses refined tuple types like { `@Tuple`<PosInt, Int> | P }. Before the isinstance check for AdtType in the block starting at line 1955, unwrap any type refinements or constraints from src_tuple_ty to expose the underlying tuple type, then extract type_args from the unwrapped tuple type. This ensures component invariants are preserved when seeding component facts and prevents false E505 errors during later re-narrowing of the source value.
🤖 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 3879-3880: Replace the loose `assert any(d.error_code == "E505"
for d in result.diagnostics if d.severity == "error")` assertions at both
locations with more precise checks that verify the exact count of E505 error
diagnostics and ensure no other unexpected error diagnostics are present.
Instead of just checking that at least one E505 exists, count the filtered
results (e.g., using `assert len([d for d in result.diagnostics if d.severity ==
"error" and d.error_code == "E505"]) == <expected_count>`) and separately assert
that the total error count matches expectations or that only E505 errors are
present in the error diagnostics to prevent false-positive passes from unrelated
error diagnostics.
In `@vera/verifier.py`:
- Around line 2732-2733: The depth guard using `_depth > 16` silently drops
obligations for deeper nested patterns by returning an empty list, which allows
legitimate pattern checking to be bypassed. Since the pattern AST is finite,
remove the depth cap check from the condition that includes `_depth > 16`,
allowing the obligation walk to continue at any depth. Alternatively, instead of
returning an empty list, emit an unguarded Tier-3 diagnostic to report the deep
nesting issue rather than silently ignoring it. Apply the same fix to the
similar condition also mentioned around lines 2779-2780.
- Around line 2881-2896: The nested constructor pattern recursion in the branch
checking `if sort is not None and idx is not None` only handles opaque path
cases but excludes literal constructor scrutinees where `lit_args` is set and
`sort` remains `None`. Add an additional condition to also recurse when handling
literal nested constructor arguments by checking if `lit_args` exists, and call
`_obligate_subpattern_term` with the appropriate literal argument accessor
instead of the sort-based accessor, ensuring the inner narrowing obligation is
verified for both opaque and literal nested constructors.
---
Outside diff comments:
In `@vera/codegen/contracts.py`:
- Around line 43-49: The manual type alias resolution in the while loop that
iterates through self._type_aliases drops generic type arguments, whereas the
_resolve_type_alias() method below preserves them. This causes issues with
generic refinement aliases like NonEmpty<T> losing their type parameters during
resolution. Replace the manual while loop that walks through self._type_aliases
with a call to _resolve_type_alias() instead. This same issue also appears in
another location (lines 77-83), so apply the same fix to both occurrences to
ensure generic type arguments are preserved for refinement guard decisions.
- Around line 110-129: The code currently handles Unit and Nat base types with
special guards in the boundary predicate check, but Byte type is not explicitly
handled and falls through with only the user predicate, allowing invalid values
outside the 0-255 range to pass. Add a new conditional block after the Nat
handling to detect when base_node is an ast.NamedType with name equal to "Byte",
and construct a BinaryExpr similar to the Nat case that conjuncts two range
checks (0 <= the slot reference AND the slot reference <= 255) with the existing
predicate, then return the combined predicate with the name tuple.
In `@vera/verifier.py`:
- Around line 2677-2680: In the _instantiated_field_types() function, the
isinstance check for AdtType fails for refined ADT scrutinees (like {
`@Option`<Int> | P }) because they are not direct AdtType instances. Before
checking if scrut_ty is an AdtType with type_args, unwrap the refined ADT base
from the scrutinee using the same logic that _is_adt_type() uses, then perform
the isinstance check on the unwrapped type to ensure refined ADT scrutinees are
properly handled for constructor field type instantiation.
---
Duplicate comments:
In `@vera/verifier.py`:
- Around line 1955-1960: The code extracting type_args from src_tuple_ty only
handles bare AdtType instances but misses refined tuple types like {
`@Tuple`<PosInt, Int> | P }. Before the isinstance check for AdtType in the block
starting at line 1955, unwrap any type refinements or constraints from
src_tuple_ty to expose the underlying tuple type, then extract type_args from
the unwrapped tuple type. This ensures component invariants are preserved when
seeding component facts and prevents false E505 errors during later re-narrowing
of the source value.
🪄 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: 1e10f925-a4f4-46c5-87d9-ee0495636717
📒 Files selected for processing (10)
CHANGELOG.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdtests/test_codegen.pytests/test_verifier.pyvera/codegen/contracts.pyvera/codegen/functions.pyvera/verifier.py
…sion depth cap (CR re-review of 0dd385f) CR posted 3 outside-diff + inline + a duplicate. Verified each; fixed 4, skipped 3. Fixed: - (Major, false Tier-1) `_instantiated_field_types`: the `isinstance(scrut_ty, AdtType)` gate failed for a REFINED ADT scrutinee (`{ @option<Int> | P }`), so a generic constructor's fields went uninstantiated and a sub-pattern narrowing was MISSED. Now unwraps `RefinedType.base` first — `Some(@Posint)` on a refined `Option<Int>` is E505; the valid `Option<PosInt>` case stays clean. - (Major, false E505) the destructure source-fact seeding (`src_tuple_ty`) had the same gate; a REFINED tuple source (`{ @tuple<PosInt, Int> | P }`) wasn't unwrapped, so a component's source fact wasn't seeded and a later re-narrowing (`@PosInt.0` into `@NonNeg`) wrongly E505'd. Now unwraps the refined base. - (depth cap) removed the `_depth > 16` cap from `_obligate_subpattern_term` / `_subpattern_source_facts_term`. The recursion descends the FINITE pattern AST (each step a strict sub-pattern), so it terminates without a cap — and the cap was a silent-drop for deep patterns. - (tests) tightened the loose `assert any(E505)` soundness assertions to `assert [codes] == ["E505"]` (exact, no unrelated-error false-passes). Skipped (with reason): - `contracts._refinement_guard_parts` alias-walk -> `_resolve_type_alias` (duplicate of the prior round): would BREAK the guard. `substitute_type_vars` substitutes a refinement's base but not its predicate (Expr, not TypeExpr), so base-name and predicate-binder would mismatch -> no guard emitted (verified: an empty array into `@NonEmpty<Int>` stops trapping). Only cosmetic today. - Byte base range guard: `{ @byte | P }` runtime guards hit a deeper codegen support gap (the type is unsupported, the guard doesn't compile at all), so a 0..255 conjoin is premature dead code until Byte refinement codegen lands. - literal nested constructor scrutinee (`match Some(Some(-5)) {}`): the literal isn't Z3-translated (scrut_z3 None) so the term recursion can't reach it, and the repro is entangled with the #755 Int<op>Nat->Nat mistyping. A degenerate case (no real program matches a literal); the opaque nested case IS obligated. New tests: refined-ADT-scrutinee obligated + no-false-positive; refined-tuple- source facts seeded. Full suite 4519 passed; conformance 90; examples 35; mypy/ruff/doc-counts/site-assets clean. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai The outside-diff + duplicate findings, all verified:
Byte base range guard (outside-diff) — declining: |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
…_ISSUES (#766) Filed #766 for the bug surfaced during the #763 review: a `{ @byte | P }` parameter passes check/verify/compile but fails to instantiate at `vera run` — the refinement guard lowers the predicate as an i64 comparison while the Byte value is i32 (an i32/i64 stack-type mismatch wasmtime rejects). A plain @byte param runs fine, so it's specific to the refinement guard. The CR range-conjoin suggestion was declined because the guard doesn't compile at all; #766 covers both the type fix and the 0..255 range conjoin. Added a Bugs row. Loud failure (module won't instantiate), not a silent wrong answer. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Follow-up on the Byte range-guard finding: I dug into why the guard doesn't compile and filed it as #766. Root cause confirmed from the WAT — |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
…ion (missed CR outside-diff) Full audit of all 26 unique outside-diff findings across the PR: 25 were already addressed (fixed / declined-with-evidence / tracked / in-code); 1 was genuinely missed. CR re-flagged my (E)-fix's tautological `ensures(@Int.result == @Int.result)` as a Duplicate comment on 88b4581, then it dropped off on line-drift without being picked up. The ADT-sort regression test (`test_refinement_over_adt_base_declared_with_adt_sort`) now uses a NON-tautological postcondition `ensures(@Int.result == 0)` with body `Pair(@int, @int) -> @Int.1 - @Int.1`, so the verifier must model the result through the match projection and prove it cancels to 0 — genuinely exercising the refined-ADT-sort declaration rather than passing vacuously. mypy/ruff/doc-counts clean. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai On the "Use a non-tautological postcondition to actually pin ADT-sort translation" finding ( For the record, I re-audited every Outside-diff finding across the PR (26 unique). This was the only one that had slipped through — it was re-posted as a Duplicate on 88b4581 and then dropped on line-drift before I picked it up. The rest are all fixed, declined-with-evidence, or tracked (#764/#765/#766). |
|
Tip For best results, initiate chat on the files or code changes.
Good catch on the re-audit — 26 findings tracked and reconciled is a thorough sweep, and having |
Summary
Closes the Tier-0 silent failure where a user refinement type parsed but its predicate was never checked:
type PosInt = { @Int | @Int.0 > 0 }accepted a violating value withvera verifyreporting OK. The verifier now discharges the predicate as a Tier-1 obligation at every narrowing site and at refined return positions, and codegen emits a runtime guard for every refinement base — generalising the proven@Natmachinery from the baked-in>= 0to an arbitrary translated predicate.Ships as v0.0.174 (bundling the #759/#760 guard-hardening already in
[Unreleased]).Static verification
A refinement predicate is structurally identical to a
requiresclause, so the existing SMT translation + discharge spine is reused. The binder is substituted by pushing the refined value's Z3 term onto a freshSlotEnvunder the base type-name, thencheck_validdischarges exactly like the@Natpath.let, call arguments (incl. piped and generic-instantiated formals), constructor fields, effect-operation arguments, match bindings, ADT sub-pattern binds, and literal / projected tuple-destructure components.E505(predicate may be violated) renders the predicate + counterexample;E506(informational — a non-primitive base or undecidable predicate is statically Tier-3 but checked at run time). Newrefine_bindObligationKind.Runtime guards (#762, closed here)
Every refinement predicate is also guarded at runtime, reusing the existing contract machinery (
translate_expr+$vera.contract_fail): a refined parameter is checked at function entry, a refined return at exit. So a program compiled withoutvera verifytraps on a refinement-violating value at apublic/FFI boundary.vera run: param/return guards trap on violating values, pass on valid ones.@Array<Int>): Z3 can't decidearray_length, so it's Tier-3 statically, but codegen compiles the predicate to WASM directly —head([])into a@NonEmptyArrayparameter traps.Soundness risks covered (with tests)
R1 (param-assume ↔ call-site), R3 (already-refined source via predicate-AST equality, not
types_equal), R5 (return binder actually bound), R7 (untranslatable → Tier-3, never silent), R9 (@Nat/refine_binddisjointness).Validation
Full suite green. mypy + both ruff gates clean. All doc gates pass: conformance (90), examples (35), version-sync (6 files), doc-counts, limitations-sync, site-assets, changelog, spec-examples. Runtime guards exercised by
TestRefinementRuntimeGuards(primitive, String, Array, generic-monomorphised).Bare
@Natreturn positions remain under #758 (out of scope here —@Natstays on its own path).Closes #746
Closes #762
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation / Tests