Skip to content

Enforce decreases clauses at run time (the E525 promise) - #1179

Merged
aallan merged 5 commits into
mainfrom
fix/1172-decreases-runtime-guard
Aug 3, 2026
Merged

Enforce decreases clauses at run time (the E525 promise)#1179
aallan merged 5 commits into
mainfrom
fix/1172-decreases-runtime-guard

Conversation

@aallan

@aallan aallan commented Aug 3, 2026

Copy link
Copy Markdown
Owner

E525 told users a Tier-3 termination metric "will be checked at runtime". Nothing checked it: decreases had no runtime lowering at all, so a non-terminating recursion passed check, passed verify, and hung at run (VeraBench v0.0.18, VB-T4-006). Its siblings E521–E524 fall back on requires/ensures, which emit real assertions — E525 was the one member of the family whose promised fallback was empty.

The guard

Every function with a decreases clause now carries a runtime termination guard, mirroring _verify_decreases's Z3 rule:

  • Entry: this function's chain state (per-function $dec_prev_<f>_<k> / $dec_active_<f> globals) is saved to locals; the measure components are evaluated over the parameters — scalars by value, an ADT component through a generated structural-size helper ($dec_size_<T>); if a previous activation is live, the components must decrease lexicographically while staying non-negative (spec §5.6.1's tuples — the runtime side now handles what the prover still reads only the first component of), or the program traps through $vera.contract_fail with a message naming the function.
  • Every exit restores the saved state, so a finished call leaves no residue that would spuriously trap a sibling.
  • Tail calls keep TCO for self-recursion — the property Tail-call optimization missing: deep recursion blows the WASM call stack #517 exists for, and mandatory on pure recursion means demoting guarded functions would have un-fixed the documented iteration idiom (the existing 1M-depth tests caught exactly that on the first design). A self-recursive return_call is prefixed with a call-site check: arguments captured into locals, the measure evaluated over them against the live chain globals, the activation's state closed out (the $gc_sp pattern), arguments re-pushed, transfer. The chain rides the site checks at constant stack depth. A mutual-tail call between two guarded functions falls back to a plain call (with the frame elided, no restore placement both preserves the chain and unwinds it); an untranslatable site demotes the same way — a check is never partially emitted. Closes the TCO-restoration follow-up in the same change.

Alongside, because building it surfaced them

  • New E127: a measure with no well-founded ordering (Float64 halves forever without crossing a floor; String/Bool have no order) was accepted silently, making the clause decorative. Spec §5.6.1(3) is normative; the checker now rejects, with a conformance negative. Zero collateral across the corpus.
  • Spec §5.6.1's own lexicographic Ackermann example was non-terminating as written — the recursive calls permute the arguments into an A(1,1) → A(2,0) cycle, "protected" by the empty promise this PR closes. Corrected, and the classical version is the new run-level conformance program (with a structural ADT measure alongside).
  • The spec's well-founded list gains ADTs (ordered by concrete structural size), matching what the checker and verifier already accept.

Honest limits (disclosed, not silent)

  • A parameterized ADT measure (List<Int>, or a concrete type whose recursion rides a parameterized field) gets no guard: the registered generic layout does not describe concrete construction, and both wrong alternatives were caught live — a frozen rank false-trapping ch02_adt_recursive, and a wild pointer walk on the base-name variant. Such measures stay statically checked and Tier-3-disclosed; per-instantiation helpers (the $eq_<type> pattern) are Runtime-rank parameterized ADT decreases measures (per-instantiation size helpers) #1177, with a KNOWN_ISSUES limitation row.

Tests

tests/test_codegen_decreases_guard.py (17): six non-terminating shapes trap instead of hanging (the issue's ADT repro, constant-measure tail call, growing measure, negative floor, lexicographic violation, mutual where recursion — subprocess-with-timeout, RED as hangs pre-fix); terminating programs stay clean with the guard emitted (Tier-1 countdown, corrected Ackermann, sequential siblings pinning the exit restore, concrete ADT measure); the tier3-obligation ⇒ emitted-guard differential the issue asked for; E127 rejection × 4 + acceptance of the well-founded family; the parameterized-measure no-guard pin. Five-mutant matrix (entry check, exit restore, lex chain, rank helper, floor leg) — each killed by a distinct named test. The existing #517/#549/#983 TCO batteries pass unchanged, pinning the preserved 1M-depth property.

Conformance: 169 → 171 (ch05_decreases_guard run-level positive, ch05_decreases_float_rejected E127 negative).

Closes #1172
Closes #1176

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added runtime enforcement for decreases clauses using scalar, structural ADT, and lexicographic measures.
    • Added diagnostics for unsupported or non-well-founded measures.
    • Preserved applicable self-recursive tail-call optimisation.
  • Documentation

    • Updated guidance, examples, limitations, and changelog information for termination measures.
  • Tests

    • Added coverage for terminating, non-terminating, recursive, and mutually recursive cases.
    • Updated project metrics to 8,707 tests and 173 conformance programmes.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The checker validates decreases measures. The WebAssembly backend emits runtime guards, structural ADT ranking helpers, and recursive-call state handling. Tests cover traps and successful execution. Documentation and project metrics are updated.

Changes

Decreases enforcement

Layer / File(s) Summary
Measure contracts and diagnostics
spec/05-functions.md, vera/checker/core.py, vera/errors.py, vera/_since.py
decreases now supports Int, Nat, and eligible ADTs. Unsupported measures produce E127.
Guard state and structural ranking
vera/codegen/core.py, vera/codegen/contracts.py, vera/codegen/assembly.py
The compiler collects guarded functions, emits per-function state, evaluates scalar and structural measures, and emits ranking helpers.
Recursive calls and state restoration
vera/codegen/functions.py, vera/codegen/contracts.py
Self-tail calls retain optimisation when guard generation succeeds. Other recursive exits restore guard state.
Runtime guard validation
tests/test_codegen_decreases_guard.py, tests/test_verifier_adt_decreases.py
Tests cover traps, successful termination, mutual recursion, lexicographic measures, ADTs, diagnostics, helper rollback, and parameterised ADT behaviour.
Documentation and project records
AGENTS.md, CHANGELOG.md, CLAUDE.md, FAQ.md, KNOWN_ISSUES.md, README.md, ROADMAP.md, SKILL.md, TESTING.md, tests/conformance/manifest.json, vera/README.md
Documentation records runtime behaviour, the known limitation, E127, corrected examples, and updated project counts. The manifest is reformatted without data changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Checker
  participant CodeGenerator
  participant WASMRuntime
  Checker->>Checker: validate decreases measure type
  Checker->>CodeGenerator: provide decreases obligation
  CodeGenerator->>WASMRuntime: emit guard and ranking helpers
  WASMRuntime->>WASMRuntime: compare measure and restore guard state
Loading

Possibly related PRs

  • aallan/vera#826: Both changes update the shared diagnostic registry in vera/errors.py.

Suggested labels: compiler, tests, spec, docs

🚥 Pre-merge checks | ✅ 6 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Spec And Implementation Move Together ⚠️ Warning §5.6.1 promises Tier-3 checks at every recursive call, but _compile_decreases_entry declines concrete ADT call measures and per-function state skips the first mutual f→g edge; spec documents ne... Document these no-guard and mutual-edge limits in spec/05-functions.md, or implement runtime ranking for all accepted concrete ADT measures and shared mutual-call comparisons.
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Changelog Covers Public-Surface Changes ✅ Passed The PR changes only spec/05-functions.md and adds E127 in vera/errors.py among the listed public surfaces; CHANGELOG.md explicitly describes runtime decreases semantics, ADT ordering, Ackermann cor...
Diagnostics Carry An Error Code ✅ Passed The PR adds E127 and E699 compiler diagnostics; both use valid E### codes, and check_diagnostic_fields.py reports all diagnostics fully tagged.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: runtime enforcement of decreases clauses.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1172-decreases-runtime-guard

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

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.18644% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.75%. Comparing base (60f0d0e) to head (30a428b).

Files with missing lines Patch % Lines
vera/codegen/contracts.py 93.43% 13 Missing ⚠️
vera/verifier.py 78.12% 7 Missing ⚠️
vera/codegen/functions.py 83.87% 5 Missing ⚠️
vera/checker/core.py 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1179      +/-   ##
==========================================
- Coverage   93.78%   93.75%   -0.03%     
==========================================
  Files          99       99              
  Lines       33651    33945     +294     
  Branches      458      458              
==========================================
+ Hits        31558    31826     +268     
- Misses       2080     2106      +26     
  Partials       13       13              
Flag Coverage Δ
javascript 78.61% <ø> (ø)
python 95.44% <91.18%> (-0.05%) ⬇️

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

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

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

@aallan
aallan force-pushed the fix/1172-decreases-runtime-guard branch from 91ea207 to db7567f Compare August 3, 2026 11:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🤖 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 `@AGENTS.md`:
- Line 11: Update the negative-fixture documentation in AGENTS.md by changing
“seventeen” to “eighteen” and adding ch05_decreases_float_rejected to both
enumerated negative-fixture lists, including the list near the conformance
overview and the corresponding list near the later invariant. Keep the lists
synchronized and preserve the existing formatting.

In `@CHANGELOG.md`:
- Line 14: Update the runtime-guard documentation to qualify that entry guards
are generated only for translatable decreases measures, and that self-tail-call
optimization applies only when the call-site check can be generated; otherwise
use plain-call fallbacks. In CHANGELOG.md at line 14 and SKILL.md at lines
1319-1320, retain the existing mutual-tail fallback description while explicitly
including untranslatable measures and demoted self-tail calls.

In `@TESTING.md`:
- Line 170: Synchronize all conformance documentation totals in TESTING.md:
update the stale 840 parametrized-test count to 855, add
ch05_decreases_float_rejected with its E127 expectation to the check-level
enumeration, change the negative-test total from 17 to 18, and correct the
skipped-test summary to 59 level-limited plus four environment-gated skips.
- Line 9: Regenerate the aggregate test counts in the Tests row of TESTING.md
using pytest collection or scripts/check_doc_counts.py as the canonical source,
ensuring the total equals the sum of passed, stress, and skipped tests. Update
all displayed component values and the total consistently.

In `@tests/conformance/manifest.json`:
- Around line 1313-1324: Update the expected_error value for the
ch07_bare_effect_op_rejected manifest entry from E217 to E219, keeping the rest
of the conformance case unchanged.

In `@tests/test_codegen_decreases_guard.py`:
- Around line 329-367: Update test_lexicographic_ackermann_runs to use an
asymmetric Ackermann entry point instead of only ackermann(2, 2), asserting the
expected non-equal result for either ackermann(1, 2) or ackermann(2, 1) so
argument order and decreases tuple slot mapping are exercised. Keep the
compilation and generated $dec_active_ackermann assertions intact.
- Around line 235-255: Update test_lexicographic_violation_traps to use distinct
initial arguments so the decreases tuple slot order is observable, then add a
mirrored recursive case where the first component decreases while the second
grows and assert that it does not trap. Keep the existing lexicographic
violation coverage while ensuring the cases distinguish the correct component
ordering from a swapped ordering.
- Around line 600-606: Update all four `$dec_active_*` WAT assertions, including
the negative assertion in the shown test and the positive checks near the other
referenced locations, to match complete symbol names with a trailing boundary
lookahead rather than plain substring membership. Ensure suffixed symbols such
as `$dec_active_length$Int` do not satisfy an assertion intended for the bare
parameterized measure.
- Around line 47-60: Update the shared _compile helper to unlink its
delete=False temporary .vera file after parsing/compilation, while preserving
the path for parse_file and compile. Refactor
test_tier3_decreases_obligation_has_emitted_guard to reuse a shared helper
returning (prog, source, path) instead of creating its own tempfile block,
ensuring cleanup occurs for every compilation path.
- Around line 77-84: Remove the unused _run_trap_message helper and the wasmtime
import that exists only for it; keep trap assertions routed through _run_cli and
_assert_termination_trap. If retaining the helper for an in-process test,
restrict its exception handling to the repository’s WasmTrapError
contract_violation path instead of accepting generic wasmtime or RuntimeError
exceptions.

In `@vera/checker/core.py`:
- Around line 875-886: Update the well-foundedness check around the local base
calculation to repeatedly unwrap RefinedType layers until reaching the
underlying type, rather than peeling only one layer. Preserve the existing
UnknownType skip and INT/NAT or registered AdtType checks once the fully
unwrapped base is reached.

In `@vera/codegen/contracts.py`:
- Around line 1139-1212: Update _dec_rank_helper to snapshot
self._dec_rank_helpers on entry and restore its contents in place whenever the
recursive walk fails, including nested helper reservations and bodies created
during the attempt. Preserve successful helper generation and the existing
recursive reservation behavior, and do not rebind the shared accumulator object.

In `@vera/codegen/functions.py`:
- Around line 587-590: Wrap the `_compile_decreases_entry` call in the same
contract-predicate degradation dispatcher used by the precondition,
refinement-guard, and postcondition paths. Ensure `CodegenSkip`,
`AdtEqNotDerivableError`, and `CodegenInvariantError` from decreases expression
translation are converted to the established `E602` degradation behavior rather
than escaping as a traceback, while preserving normal instruction emission for
supported predicates.
- Around line 1134-1142: Prevent decreases guards from being generated for
functions whose effect declares Exn by adding an early return in
_compile_decreases_entry after locating the Decreases contract, preserving the
existing no-contract behavior. Update the trailing restore comment near
dec_restore_instrs to acknowledge that return_call paths restore inline and that
the block is not exhaustive; remove the incorrect claim that guarded functions
cannot contain return_call.

In `@vera/README.md`:
- Line 135: Fix the Markdown table row for contracts.py in the module map to
remove leading spaces from inside the inline code span while preserving the
intended tree indentation outside the span, or use an equivalent HTML code
representation with non-breaking spaces.
🪄 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 Plus

Run ID: 75a64e04-da56-4c18-abb0-2fb518f7f2e2

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0909e and 91ea207.

⛔ Files ignored due to path filters (8)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • examples/gc_pressure.vera is excluded by !**/*.vera
  • tests/conformance/ch05_decreases_float_rejected.vera is excluded by !**/*.vera
  • tests/conformance/ch05_decreases_guard.vera is excluded by !**/*.vera
📒 Files selected for processing (21)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • spec/05-functions.md
  • tests/conformance/manifest.json
  • tests/test_codegen_decreases_guard.py
  • tests/test_verifier_adt_decreases.py
  • vera/README.md
  • vera/_since.py
  • vera/checker/core.py
  • vera/codegen/assembly.py
  • vera/codegen/contracts.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/errors.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread CHANGELOG.md Outdated
Comment thread TESTING.md Outdated
Comment thread TESTING.md Outdated
Comment thread tests/conformance/manifest.json Outdated
Comment thread tests/test_codegen_decreases_guard.py Outdated
Comment thread vera/checker/core.py
Comment thread vera/codegen/contracts.py
Comment thread vera/codegen/functions.py Outdated
Comment thread vera/codegen/functions.py
Comment thread vera/README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (3)
vera/codegen/functions.py (2)

1134-1142: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The comment contradicts the code, and an exception unwind skips the restores.

Two separate points.

  1. The comment claims "no return_call survives in a guarded function (the revert above)". The elif dec_restore_instrs: branch at Lines 970-1016 deliberately keeps return_call for a self-recursive site (Line 1005) and for an unguarded target (Line 1015). Behaviour is correct — those sites carry their restores inline — but the comment will mislead the next maintainer into treating this block as exhaustive.

  2. vera/codegen/assembly.py Lines 373-376 emit (tag $exn_<T>), so a guarded function that declares Exn<E> can throw. A WASM throw unwinds past these trailing instructions, so $dec_active_<f> stays 1 and $dec_prev_<f>_k keeps the aborted activation's measure. A later unrelated call to f reads saved_active == 1, compares against that stale baseline, and traps with decreases() measure in 'f' failed to decrease on a terminating program. The per-activation baseline lives in locals, so the restore cannot move into a catch.

🐛 Proposed fix — correct the comment and decline the guard for `Exn` functions

Gate _compile_decreases_entry in vera/codegen/contracts.py right after the contract lookup:

        if contract is None:
            return [], [], None
        # An `Exn` throw unwinds past the exit restores, leaving
        # `$dec_active_<fn>` set and `$dec_prev_<fn>_k` holding the aborted
        # activation's measure — a later unrelated call would then trap
        # spuriously.  Emit no guard rather than a leaky one.
        if _declares_exn(decl.effect):
            return [], [], None

And correct the comment here:

-        # `#1172`: decreases-guard restores — every non-trap exit puts the
-        # function's chain state back to what this activation saved, so a
-        # finished call leaves no residue that would spuriously trap a
-        # sibling call.  Trap paths need no restore (the instance dies),
-        # and no `return_call` survives in a guarded function (the revert
-        # above), so this single exit point covers every live return.
+        # `#1172`: decreases-guard restores for the FALL-THROUGH exit — the
+        # function's chain state goes back to what this activation saved, so
+        # a finished call leaves no residue that would spuriously trap a
+        # sibling call.  Trap paths need no restore (the instance dies).  A
+        # surviving `return_call` does NOT reach here: the tail-call patching
+        # above prepends `dec_self_tail` (self-recursive) or
+        # `dec_restore_instrs` (unguarded target) at each such site instead.
🤖 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/functions.py` around lines 1134 - 1142, Update
_compile_decreases_entry in vera/codegen/contracts.py to return no decreases
guard when _declares_exn(decl.effect) is true, immediately after the contract
lookup and its None check, preventing exception unwinds from leaving stale guard
state. Also revise the trailing restore comment near dec_restore_instrs to
acknowledge that return_call sites can retain inline restores and that this
block is not exhaustive.

577-591: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

_compile_decreases_entry still sits outside the contract-predicate degradation net.

The call lowers contract predicates: vera/codegen/contracts.py Lines 878 and 1024 call ctx.translate_expr on every decreases component. That path raises CodegenSkip for an unsupported construct, AdtEqNotDerivableError for a non-Eq composite ==, and CodegenInvariantError for a dangling slot.

The precondition and refinement-guard block above (Lines 513-573) and the postcondition block below (Lines 839-867) both carry the full net. _emit_contract_predicate_degradation states the invariant at Lines 99-102: a user program must never surface a Python traceback from a contract predicate. decreases was not lowered before this PR, so the gap is newly reachable — decreases(hash(@Tree.0)) now crashes vera compile on a check-green program.

🔒️ Proposed fix — reuse the existing dispatcher
-        dec_entry_instrs, dec_restore_instrs, dec_self_tail = (
-            self._compile_decreases_entry(ctx, decl, env)
-        )
+        try:
+            dec_entry_instrs, dec_restore_instrs, dec_self_tail = (
+                self._compile_decreases_entry(ctx, decl, env)
+            )
+        except (AdtEqNotDerivableError, CodegenSkip) as exc:
+            self._emit_contract_predicate_degradation(ctx, exc, decl)
+            return None
+        except CodegenInvariantError as inv:
+            self._harvest_interp_inference_failures(ctx)
+            self._error(
+                inv.node if inv.node is not None else decl,
+                f"Internal compiler error while compiling '{decl.name}': "
+                f"{inv.msg}",
+                rationale="This is a codegen invariant violation — the type "
+                "checker should have rejected the input before it reached this "
+                "point.  Please file a bug report with the offending program.",
+                error_code="E699",
+            )
+            return None
         pre_instrs = pre_instrs + dec_entry_instrs
🤖 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/functions.py` around lines 577 - 591, Wrap the
_compile_decreases_entry call in the same contract-predicate degradation
dispatcher used by the precondition and postcondition lowering blocks, so
CodegenSkip, AdtEqNotDerivableError, and CodegenInvariantError are degraded
consistently instead of escaping as Python tracebacks. Preserve the existing
decreases entry, restore, and tail-instruction behavior for successfully lowered
predicates.
vera/codegen/contracts.py (1)

1139-1212: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

A partially-successful rank walk still commits an orphaned helper.

Line 1207 removes only this walk's own reservation. A nested helper committed a real body at Line 1211 during the same walk, and that body contains call $dec_size_<T> for the name just deleted. vera/codegen/assembly.py Lines 586-588 emit every non-empty entry, so wat2wasm fails with unknown func and the whole compile dies on a check-green program — worse than the intended "no guard".

Snapshot the accumulator on entry and restore it in place on failure, the same discipline _lift_pending_closures uses.

🔒️ Proposed fix — roll back the whole accumulator
         layouts = self._adt_layouts.get(adt_name)
         if not layouts:
             return None
+        # Roll back EVERY helper this walk committed, not just our own
+        # reservation: a nested helper stored a real body that calls our
+        # short-circuited name, so keeping it dangles `unknown func`.
+        snapshot = dict(self._dec_rank_helpers)
         # Reserve the name first so a recursive field type terminates.
         self._dec_rank_helpers[fn_name] = ""
         if not ok:
-            del self._dec_rank_helpers[fn_name]
+            self._dec_rank_helpers.clear()
+            self._dec_rank_helpers.update(snapshot)
             return None

Mutate the dict in place. AssemblyMixin and ContractsMixin share the one attribute on CodeGenerator.

🤖 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 1139 - 1212, In _dec_rank_helper,
snapshot the entire _dec_rank_helpers accumulator before reserving the current
helper, and on any failure restore that snapshot in place rather than deleting
only fn_name. Preserve the shared dictionary object used by AssemblyMixin and
ContractsMixin, while retaining successful helper bodies when the walk
completes.

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 `@AGENTS.md`:
- Line 11: Update the conformance documentation in AGENTS.md to match the
complete cross-module program set documented by SKILL.md, including all
companion-module exceptions from Chapters 7–9. Prefer deriving or validating the
list against tests/conformance/manifest.json so AGENTS.md and SKILL.md use one
canonical description and remain synchronized with the codebase.

In `@spec/05-functions.md`:
- Line 135: Update the ordering requirement sentence in the specification to use
the RFC 2119 keyword “MUST” instead of lowercase “must,” preserving the
surrounding wording and semantics.
- Line 135: Update the termination-measure description in the relevant section
to state that Float64 is rejected with E127 because it lacks a supported
well-founded ordering, not because its domain is unbounded. Document how NaN and
positive or negative infinity are handled by the runtime check.
- Line 160: Update the runtime-checking description to specify tuple handling
separately: on each re-entry, first validate that every lexicographic measure
component is non-negative, then require the tuple to be strictly smaller
lexicographically than the previous activation. Do not state or imply that every
component must decrease; retain the existing scalar and ADT structural-size
behavior.
- Line 163: Update the ADT measure guidance in the specification to state that
parameterisation disables runtime ranking, but static verification still
classifies the obligation as Tier 1, Tier 2, or Tier 3; reserve Tier 3 only for
obligations that static verification cannot discharge.

In `@tests/test_codegen_decreases_guard.py`:
- Around line 154-189: Extend TestNonTerminatingTraps with coverage for guarded
self-tail-call preservation: compile a decreasing recursive function and assert
its generated WAT retains return_call for the self-recursive site, then execute
it with sufficiently deep recursion and verify the expected result to pin
constant-stack-depth behavior. Reuse the existing _compile_ok and _run helpers,
and add the check as a separate test from
test_constant_int_measure_tail_call_traps.

---

Duplicate comments:
In `@vera/codegen/contracts.py`:
- Around line 1139-1212: In _dec_rank_helper, snapshot the entire
_dec_rank_helpers accumulator before reserving the current helper, and on any
failure restore that snapshot in place rather than deleting only fn_name.
Preserve the shared dictionary object used by AssemblyMixin and ContractsMixin,
while retaining successful helper bodies when the walk completes.

In `@vera/codegen/functions.py`:
- Around line 1134-1142: Update _compile_decreases_entry in
vera/codegen/contracts.py to return no decreases guard when
_declares_exn(decl.effect) is true, immediately after the contract lookup and
its None check, preventing exception unwinds from leaving stale guard state.
Also revise the trailing restore comment near dec_restore_instrs to acknowledge
that return_call sites can retain inline restores and that this block is not
exhaustive.
- Around line 577-591: Wrap the _compile_decreases_entry call in the same
contract-predicate degradation dispatcher used by the precondition and
postcondition lowering blocks, so CodegenSkip, AdtEqNotDerivableError, and
CodegenInvariantError are degraded consistently instead of escaping as Python
tracebacks. Preserve the existing decreases entry, restore, and tail-instruction
behavior for successfully lowered predicates.
🪄 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 Plus

Run ID: 3d6c7acf-52c2-49b1-8589-0b6cb3ae5324

📥 Commits

Reviewing files that changed from the base of the PR and between 91ea207 and db7567f.

⛔ Files ignored due to path filters (8)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • examples/gc_pressure.vera is excluded by !**/*.vera
  • tests/conformance/ch05_decreases_float_rejected.vera is excluded by !**/*.vera
  • tests/conformance/ch05_decreases_guard.vera is excluded by !**/*.vera
📒 Files selected for processing (21)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • spec/05-functions.md
  • tests/conformance/manifest.json
  • tests/test_codegen_decreases_guard.py
  • tests/test_verifier_adt_decreases.py
  • vera/README.md
  • vera/_since.py
  • vera/checker/core.py
  • vera/codegen/assembly.py
  • vera/codegen/contracts.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/errors.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread AGENTS.md Outdated
Comment thread spec/05-functions.md Outdated
Comment thread spec/05-functions.md Outdated
Comment thread spec/05-functions.md Outdated
Comment thread tests/test_codegen_decreases_guard.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

668-668: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the site-fact validation documentation aligned.

Line 668 says check_site_assets.py now covers generated-asset freshness only. CHANGELOG.md Lines 11–13 says check_fact_coherence() independently validates facts across docs/index.html and docs/index.md. Keep both checks documented, or update the implementation and CHANGELOG together.

As per path instructions, Markdown documentation must match the codebase.

🤖 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 668, Update the site-fact validation documentation around
check_site_assets.py to also describe check_fact_coherence() and its validation
of facts across docs/index.html and docs/index.md. Keep TESTING.md and
CHANGELOG.md consistent with the current implementation, or update the
implementation and both documents together if the validation behavior is
changing.

Source: Path instructions

🤖 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 9-11: Update the TESTING.md validation inventory by adding
test_eq_contract_874.py to the Test Files table with its 13 tests and adjusting
the overview totals to include them. Also expand the check_site_assets.py entry
to document check_fact_coherence() validation for docs/index.html and
docs/index.md.

---

Outside diff comments:
In `@TESTING.md`:
- Line 668: Update the site-fact validation documentation around
check_site_assets.py to also describe check_fact_coherence() and its validation
of facts across docs/index.html and docs/index.md. Keep TESTING.md and
CHANGELOG.md consistent with the current implementation, or update the
implementation and both documents together if the validation behavior is
changing.
🪄 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 Plus

Run ID: 635c54bd-44c4-4c6b-ac77-02e6ba9d911d

📥 Commits

Reviewing files that changed from the base of the PR and between db7567f and dc5c799.

⛔ Files ignored due to path filters (2)
  • docs/SKILL.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (12)
  • AGENTS.md
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • spec/05-functions.md
  • tests/test_codegen_decreases_guard.py
  • vera/checker/core.py
  • vera/codegen/contracts.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread TESTING.md Outdated
@aallan
aallan force-pushed the fix/1172-decreases-runtime-guard branch from dc5c799 to 45ded6b Compare August 3, 2026 12:43
Comment thread vera/README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

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

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Both measure-component loops can commit a rank helper and then decline the guard. _dec_rank_helper rolls back what a failed rank walk commits, but neither component loop snapshots self._dec_rank_helpers before it can succeed on one component and bail on a later one. vera/codegen/assembly.py Lines 586-588 then emit the orphaned $dec_size_<T> for a function that received no guard, and the entry path only sets self._needs_memory at Line 959 — after the loop that emitted a helper containing i32.load.

  • vera/codegen/contracts.py#L888-904: snapshot self._dec_rank_helpers before the loop and restore it at each of the three early returns.
  • vera/codegen/contracts.py#L1033-1048: apply the same snapshot and restore at each return None in this loop.
🤖 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` at line 1, Update both measure-component loops in
the relevant contract-generation methods to snapshot self._dec_rank_helpers
before iteration and restore that snapshot before every early return (including
all three returns in the first loop and each return None in the second).
Preserve helpers only when the complete loop succeeds, preventing orphaned rank
helpers from being emitted without a corresponding guard.

Source: Learnings

♻️ Duplicate comments (1)
tests/test_codegen_decreases_guard.py (1)

145-180: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The headline TCO claim is still unpinned.

The module docstring at Lines 14-21 states that a self-recursive return_call keeps TCO under the guard, and vera/codegen/functions.py Lines 1022-1029 implement exactly that. test_constant_int_measure_tail_call_traps proves only that the trap fires; it passes equally if that branch demoted every self-tail site to a plain call. Both the structural property and the constant-stack-depth property remain unchecked, so a regression that silently demotes guarded self-tail calls surfaces only when a deep program exhausts the stack.

💚 Pin the structural and the depth property
    def test_guarded_self_tail_keeps_return_call(self) -> None:
        """The guard must not demote a self-recursive tail call (`#517`)."""
        source = """\
private fn drain(`@Int`, `@Int` -> `@Int`)
  requires(true)
  ensures(true)
  decreases(`@Int.1`)
  effects(pure)
{
  if `@Int.1` <= 0 then {
    `@Int.0`
  } else {
    drain(`@Int.0` + 1, `@Int.1` - 1)
  }
}

public fn main(`@Unit` -> `@Int`)
  requires(true)
  ensures(true)
  effects(pure)
{
  drain(0, 200000)
}
"""
        result = _compile_ok(source)
        assert re.search(r"return_call \$drain(?![A-Za-z0-9_])", result.wat), (
            "a guarded self-recursive tail call must keep return_call"
        )
        # Without TCO this depth exhausts the native stack.
        assert _run(source, fn="main") == 200000

Note the asymmetric slot use: decreases(@Int.1) measures the counter while @Int.0 accumulates, so a swapped slot mapping changes the result rather than staying green.

As per path instructions: "Missing edge cases for new compiler features" and "A test for a non-commutative op that only checks a commutative case (or whose inputs are symmetric) cannot catch a slot-order bug."
🤖 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 `@tests/test_codegen_decreases_guard.py` around lines 145 - 180, Add a
regression test alongside TestNonTerminatingTraps named
test_guarded_self_tail_keeps_return_call that compiles a guarded self-recursive
drain function with decreases(`@Int.1`) and an accumulating `@Int.0`, asserts the
generated WAT retains return_call $drain, and runs a sufficiently deep counter
to verify constant-stack execution and the expected result. Use asymmetric
argument roles so slot-order regressions are detected.

Source: Path instructions

🤖 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 `@FAQ.md`:
- Line 252: Update the test count from 8,558 to 8,639 in FAQ.md at lines
252-252. README.md at lines 266-266 and ROADMAP.md at lines 15-15 already use
the correct count and require no direct changes.

In `@spec/05-functions.md`:
- Line 162: Revise the self-recursion statement in the tail-call optimization
documentation to qualify the constant-stack guarantee: it applies only when the
self-tail site is successfully translated. Document that when
_dec_self_tail_prefix returns None, _compile_fn lowers the self-targeted
return_call to call; the existing entry guard still checks the measure on each
re-entry, but execution uses the native stack.

In `@vera/codegen/contracts.py`:
- Around line 888-904: Snapshot the rank-helper accumulator at the start of the
enclosing component-processing method, and restore that snapshot before every
early return caused by translation failure, unsupported measure type, missing
ADT name, or missing rank helper. Apply the same rollback discipline to the
related _dec_self_tail_prefix path so partial processing cannot leave committed
helpers behind.
- Around line 1033-1048: Apply the same rollback/checkpoint pattern used by the
entry loop to the component loop building comp_values. Ensure any helpers
registered through _dec_rank_helper for earlier expressions are removed when a
later translation, type, ADT-name, or rank-helper lookup returns None, so failed
compilation leaves no unused helper registrations.

In `@vera/README.md`:
- Line 135: Update the contracts.py module-map entry in README.md to mention
runtime enforcement of decreases clauses, including the associated
termination-state handling, while preserving the existing description of runtime
pre/postconditions and old state snapshots.

---

Outside diff comments:
In `@vera/codegen/contracts.py`:
- Line 1: Update both measure-component loops in the relevant
contract-generation methods to snapshot self._dec_rank_helpers before iteration
and restore that snapshot before every early return (including all three returns
in the first loop and each return None in the second). Preserve helpers only
when the complete loop succeeds, preventing orphaned rank helpers from being
emitted without a corresponding guard.

---

Duplicate comments:
In `@tests/test_codegen_decreases_guard.py`:
- Around line 145-180: Add a regression test alongside TestNonTerminatingTraps
named test_guarded_self_tail_keeps_return_call that compiles a guarded
self-recursive drain function with decreases(`@Int.1`) and an accumulating `@Int.0`,
asserts the generated WAT retains return_call $drain, and runs a sufficiently
deep counter to verify constant-stack execution and the expected result. Use
asymmetric argument roles so slot-order regressions are detected.
🪄 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 Plus

Run ID: 2c5a5319-578d-4eef-88fa-41db1dd3713e

📥 Commits

Reviewing files that changed from the base of the PR and between dc5c799 and 45ded6b.

⛔ Files ignored due to path filters (8)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • examples/gc_pressure.vera is excluded by !**/*.vera
  • tests/conformance/ch05_decreases_float_rejected.vera is excluded by !**/*.vera
  • tests/conformance/ch05_decreases_guard.vera is excluded by !**/*.vera
📒 Files selected for processing (21)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • spec/05-functions.md
  • tests/conformance/manifest.json
  • tests/test_codegen_decreases_guard.py
  • tests/test_verifier_adt_decreases.py
  • vera/README.md
  • vera/_since.py
  • vera/checker/core.py
  • vera/codegen/assembly.py
  • vera/codegen/contracts.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/errors.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread FAQ.md Outdated
Comment thread spec/05-functions.md Outdated
Comment thread vera/codegen/contracts.py Outdated
Comment thread vera/codegen/contracts.py Outdated
Comment thread vera/README.md Outdated
@aallan

aallan commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai On the three outside-diff findings: all fixed in b34c082. (1) TESTING.md's check_site_assets entries — both the one-line script row and the test_build_site row — now document check_fact_coherence() and its docs/index.html↔docs/index.md validation. (2) Both measure-component loops in vera/codegen/contracts.py (entry and self-tail prefix) snapshot the rank-helper accumulator and restore it on every early return; the escaped helpers were internally valid dead code rather than dangling references, rolled back regardless so the helper⇔guard invariant is total. (3) tests/test_codegen_decreases_guard.py's test_guarded_self_tail_keeps_return_call pins return_call in the guarded WAT plus a 300,000-hop constant-stack run with asymmetric counter/accumulator roles.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format=fuller --stat b34c082d
printf '\n--- changed hunks ---\n'
git show --no-ext-diff --format= --unified=35 b34c082d -- \
  TESTING.md \
  vera/codegen/contracts.py \
  tests/test_codegen_decreases_guard.py

Length of output: 126780


@aallan, the shell output confirms the three fixes in b34c082d.

The documentation entry is present. Both measure-component loops restore the rank-helper accumulator on early return. The self-tail regression test checks return_call and completes the 300,000-hop asymmetric case.

No further action is required.

You are interacting with an AI system.

aallan added a commit that referenced this pull request Aug 3, 2026
Reconcile every test total against one live collected number (8,621 from
`pytest --collect-only -q`): the TESTING.md headline parenthetical summed to
8,601, and the per-file table summed to 8,607 because `test_eq_contract_874.py`
carried stale counts (11/378, live 13/430) inside a padded `|      378 |` cell
that `check_doc_counts.py`'s row regex could not match, so the gate never saw
it. Normalising the cell puts all 131 files under the gate, and the headline,
the table sum, and `8,503 passed + 26 stress, 92 skipped` now all agree.
FAQ.md's 8,558 is brought to the same number. The `test_stress.py` row's 16 is
correct and stays: 16 instances live in that file, the suite-wide 26 adds the
10 stress-marked tests in `test_codegen_gc_reclamation.py`.

Fix the `test_exn_redeclaration_rejected` rationale, which still described
`Exn<E>` as codegen-only and absent from `TypeEnv.effects` — this PR is what
changed that. Add `test_exn_needs_no_declaration` for the other half: `throw`
and `handle[Exn<E>]` in scope with no block, which is E330 without the registry
entry.

Complete the §9.5.1 IO operations table with `read_char`, `sleep`, `time` and
`stderr` — the registry has eleven operations and the table listed seven, which
the amended sentence ("the operations above are in scope") now leans on. Correct
§9.5.2: for `State.get` it is the state-type selection that is implicit, not the
`Unit` argument, which is written out as `get(())`.

Assert the `vera effects --json` payload's exact (name, kind) set against both
registries rather than only its length: a count-only check stays green if an
effect is dropped and an ability added, or if a name is emitted under the wrong
`kind`, which is what a consumer discriminates on.

Rename the two `unresolved_db_*_spelling` tests to
`shadow_declaring_other_op_still_gated_*`. Traced which frame reaches
`_check_sql_provenance`: the `ping`-shadow fixture now goes through the
resolved-op gate, not the unresolved-spelling fallback its name claimed. With
the built-in `DB` always registering `query`/`execute`, no source can make that
lookup fail, so the fallback is unreachable from source; it is kept as defence
in depth and both the branch comment and the tests now say so rather than
claiming coverage they no longer provide.

Two findings declined, both on file-wide convention. The `vera/README.md` module
map spells every indented row with leading spaces inside the code span, so
converting one row for MD038 breaks the convention (identical findings were
declined on #1178 and #1179). MD040 on the §7.2 fences: `spec/07-effects.md`
has 0 ```vera and 42 bare fences, the spec has 94 against 657, and no
markdownlint runs in pre-commit or CI — tagging three fences in a chapter with
none makes it less consistent, not more.

Co-Authored-By: Claude <noreply@anthropic.invalid>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

887-915: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated rank-helper rollback loop across two call sites. _compile_decreases_entry and _dec_self_tail_prefix each carry a near-identical loop that translates every decreases() component, resolves ADT ranks via _dec_rank_helper, and rolls back self._dec_rank_helpers to a pre-loop snapshot on any failure. This duplication is exactly why the rollback fix needed two separate review rounds to land — one per copy. Extract one shared _dec_translate_measure(ctx, contract, env) -> list[list[str]] | None helper that both call sites use, so the rollback discipline is written once.

  • vera/codegen/contracts.py#L887-L915: replace the inline loop in _compile_decreases_entry with a call to the shared helper, mapping None to return [], [], None.
  • vera/codegen/contracts.py#L1044-L1068: replace the inline loop in _dec_self_tail_prefix with the same shared helper, mapping None to return None.
🤖 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 887 - 915, Extract the duplicated
decreases-component translation and rank-helper rollback logic into a shared
_dec_translate_measure(ctx, contract, env) helper returning list[list[str]] |
None, preserving the pre-loop snapshot and rollback on every failure. In
vera/codegen/contracts.py lines 887-915, update _compile_decreases_entry to call
it and map None to return [], [], None; in lines 1044-1068, update
_dec_self_tail_prefix to call it and map None to return None.
🤖 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/05-functions.md`:
- Around line 160-164: Revise the Tier 3 runtime-checking paragraph to limit
enforcement to statically unproved decreases obligations for which the backend
can generate a supported runtime guard. Qualify the non-terminating-recursion
trap guarantee with the same condition, and explicitly exclude parameterized or
indirectly parameterized ADT measures and functions declaring Exn, preserving
the existing details for supported guards.

---

Outside diff comments:
In `@vera/codegen/contracts.py`:
- Around line 887-915: Extract the duplicated decreases-component translation
and rank-helper rollback logic into a shared _dec_translate_measure(ctx,
contract, env) helper returning list[list[str]] | None, preserving the pre-loop
snapshot and rollback on every failure. In vera/codegen/contracts.py lines
887-915, update _compile_decreases_entry to call it and map None to return [],
[], None; in lines 1044-1068, update _dec_self_tail_prefix to call it and map
None to return None.
🪄 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 Plus

Run ID: c48ee9d6-01d6-49f4-895e-80bda49a3175

📥 Commits

Reviewing files that changed from the base of the PR and between 45ded6b and b34c082.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (9)
  • AGENTS.md
  • FAQ.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • spec/05-functions.md
  • tests/test_codegen_decreases_guard.py
  • vera/README.md
  • vera/codegen/contracts.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread spec/05-functions.md Outdated
@aallan
aallan force-pushed the fix/1172-decreases-runtime-guard branch from b34c082 to e93b10d Compare August 3, 2026 13:55
aallan added a commit that referenced this pull request Aug 3, 2026
Reconcile every test total against one live collected number (8,621 from
`pytest --collect-only -q`): the TESTING.md headline parenthetical summed to
8,601, and the per-file table summed to 8,607 because `test_eq_contract_874.py`
carried stale counts (11/378, live 13/430) inside a padded `|      378 |` cell
that `check_doc_counts.py`'s row regex could not match, so the gate never saw
it. Normalising the cell puts all 131 files under the gate, and the headline,
the table sum, and `8,503 passed + 26 stress, 92 skipped` now all agree.
FAQ.md's 8,558 is brought to the same number. The `test_stress.py` row's 16 is
correct and stays: 16 instances live in that file, the suite-wide 26 adds the
10 stress-marked tests in `test_codegen_gc_reclamation.py`.

Fix the `test_exn_redeclaration_rejected` rationale, which still described
`Exn<E>` as codegen-only and absent from `TypeEnv.effects` — this PR is what
changed that. Add `test_exn_needs_no_declaration` for the other half: `throw`
and `handle[Exn<E>]` in scope with no block, which is E330 without the registry
entry.

Complete the §9.5.1 IO operations table with `read_char`, `sleep`, `time` and
`stderr` — the registry has eleven operations and the table listed seven, which
the amended sentence ("the operations above are in scope") now leans on. Correct
§9.5.2: for `State.get` it is the state-type selection that is implicit, not the
`Unit` argument, which is written out as `get(())`.

Assert the `vera effects --json` payload's exact (name, kind) set against both
registries rather than only its length: a count-only check stays green if an
effect is dropped and an ability added, or if a name is emitted under the wrong
`kind`, which is what a consumer discriminates on.

Rename the two `unresolved_db_*_spelling` tests to
`shadow_declaring_other_op_still_gated_*`. Traced which frame reaches
`_check_sql_provenance`: the `ping`-shadow fixture now goes through the
resolved-op gate, not the unresolved-spelling fallback its name claimed. With
the built-in `DB` always registering `query`/`execute`, no source can make that
lookup fail, so the fallback is unreachable from source; it is kept as defence
in depth and both the branch comment and the tests now say so rather than
claiming coverage they no longer provide.

Two findings declined, both on file-wide convention. The `vera/README.md` module
map spells every indented row with leading spaces inside the code span, so
converting one row for MD038 breaks the convention (identical findings were
declined on #1178 and #1179). MD040 on the §7.2 fences: `spec/07-effects.md`
has 0 ```vera and 42 bare fences, the spec has 94 against 657, and no
markdownlint runs in pre-commit or CI — tagging three fences in a chapter with
none makes it less consistent, not more.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai On the outside-diff extraction: done in 68605e5 — _dec_translate_measure is the single home for the component translation, ADT ranking, and snapshot/rollback discipline; _compile_decreases_entry maps its None to ([], [], None) and _dec_self_tail_prefix maps it to None, exactly as proposed. Guard suite 23/23, mypy clean.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format=fuller --stat 68605e5f
printf '\n--- relevant diff ---\n'
git show --no-ext-diff --format= --unified=25 68605e5f -- vera/codegen/contracts.py
printf '\n--- named symbols on PR branch ---\n'
ast-grep outline vera/codegen/contracts.py --items all --type method,function --match 'dec_translate_measure|compile_decreases_entry|dec_self_tail_prefix'

Length of output: 100742


@aallan Confirmed. Commit 68605e5f makes _dec_translate_measure the shared implementation for component translation, ADT ranking, and rank-helper rollback.

_compile_decreases_entry maps a failed translation to ([], [], None). _dec_self_tail_prefix maps it to None. The rollback restores self._dec_rank_helpers for every identified translation failure path.

I did not independently run the guard suite or mypy in the sandbox.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/1179

Timestamp: 2026-08-03T14:04:19.043Z
Learning: In `vera/codegen/contracts.py`, `ContractsMixin._dec_translate_measure` is the single source for decreases-measure component translation, ADT structural ranking, and `self._dec_rank_helpers` snapshot/rollback. `ContractsMixin._compile_decreases_entry` treats its `None` result as `([], [], None)`, while `ContractsMixin._dec_self_tail_prefix` treats it as `None`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@aallan

aallan commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review — independent attacker, 18 repro programs, branch tip 68605e5

Verdict: sound-with-findings. The guard machinery itself survived every structural attack — per-clone globals for monomorphized instantiations, module/where-helper name mangling, interleaved cross-instantiation nesting, 1M-depth self-tail TCO, the amended lexicographic semantics verified component-by-component against the WAT, State-effect interleaving, vera test/serve instance isolation, and browser/wasi-p2 linkage all held. What is broken is the tier-agreement contract around it, in three reproducible ways:

F1 (High — pre-existing verifier bug, made observable by this PR). The verifier's _walk_for_calls enumerates only FnCall/If/Block/Binary/Unary/Match — recursive calls inside a handle clause or a closure body are invisible to _verify_decreases, so a measure-violating call there does not block a Tier-1 proof. Two repro programs show verify reporting the obligation verified and run trapping decreases() … failed to decrease on terminating executions. The unconditional guard emission converts this silent false proof into a user-visible verify-green→run-trap contradiction. Disposition: fix the walker in this PR (Handle clauses, closure bodies, plus the unenumerated IndexExpr carrier), and add a verify-tier↔runtime differential over the repro shapes.

F2 (Medium-High — this PR's own gap). A guarded function tail-calling an unguarded trampoline that tail-calls back loops forever with zero checks: the unguarded-target tail branch prepends the restores (chain zeroed) and keeps return_call, so every re-entry sees $dec_active = 0 and re-baselines. Repro hangs under timeout with the obligation Tier-3-disclosed — the exact VB-T4-006 hang class this PR exists to close, surviving in a two-function shape. Disposition: demote tail calls from a guarded function to unguarded targets (the same treatment mutual-tail already gets; the documented #517 constant-stack property is self-recursion, which keeps its return_call + site check).

F3 (Medium — inherent, newly exposed). Measure evaluation uses machine i64 arithmetic where Z3 proved over unbounded integers: decreases(@Int.0 + 9000000000000000000) is verified, and run overflow-traps near the i64 edge on a program that ran clean before this PR. Disposition: documented limitation (KNOWN_ISSUES/spec note) — saturating arithmetic would silently weaken the comparison, and suppressing the guard on Tier-1 obligations would re-hide F1.

F4 (Low — fidelity note). Spec §5.6.1's runtime paragraph implies only unproved obligations are enforced; the implementation guards all tiers (belt-and-braces — precisely what made F1/F3 observable). Disposition: reword the spec sentence to match the implementation; the dynamic-re-entry-vs-static-call-site strengthening is already disclosed by the "on each re-entry" wording.

Clean lenses, for the record: lexicographic deciding-component semantics (later components unconstrained — confirmed clean; deciding-component floor — confirmed trap), mono-clone guard isolation, imported/where-helper naming, i32_pair self-tail capture ordering, mutual-tail demotion is not a regression (v0.1.8 parity confirmed), Exn is the only unwind mechanism (State clauses are single-tail-resume or skip), no return_call_indirect escape exists.

Fixes for F1/F2 land in this PR before any merge-ready call; F3/F4 land as documentation in the same round.

aallan added a commit that referenced this pull request Aug 3, 2026
Reconcile every test total against one live collected number (8,621 from
`pytest --collect-only -q`): the TESTING.md headline parenthetical summed to
8,601, and the per-file table summed to 8,607 because `test_eq_contract_874.py`
carried stale counts (11/378, live 13/430) inside a padded `|      378 |` cell
that `check_doc_counts.py`'s row regex could not match, so the gate never saw
it. Normalising the cell puts all 131 files under the gate, and the headline,
the table sum, and `8,503 passed + 26 stress, 92 skipped` now all agree.
FAQ.md's 8,558 is brought to the same number. The `test_stress.py` row's 16 is
correct and stays: 16 instances live in that file, the suite-wide 26 adds the
10 stress-marked tests in `test_codegen_gc_reclamation.py`.

Fix the `test_exn_redeclaration_rejected` rationale, which still described
`Exn<E>` as codegen-only and absent from `TypeEnv.effects` — this PR is what
changed that. Add `test_exn_needs_no_declaration` for the other half: `throw`
and `handle[Exn<E>]` in scope with no block, which is E330 without the registry
entry.

Complete the §9.5.1 IO operations table with `read_char`, `sleep`, `time` and
`stderr` — the registry has eleven operations and the table listed seven, which
the amended sentence ("the operations above are in scope") now leans on. Correct
§9.5.2: for `State.get` it is the state-type selection that is implicit, not the
`Unit` argument, which is written out as `get(())`.

Assert the `vera effects --json` payload's exact (name, kind) set against both
registries rather than only its length: a count-only check stays green if an
effect is dropped and an ability added, or if a name is emitted under the wrong
`kind`, which is what a consumer discriminates on.

Rename the two `unresolved_db_*_spelling` tests to
`shadow_declaring_other_op_still_gated_*`. Traced which frame reaches
`_check_sql_provenance`: the `ping`-shadow fixture now goes through the
resolved-op gate, not the unresolved-spelling fallback its name claimed. With
the built-in `DB` always registering `query`/`execute`, no source can make that
lookup fail, so the fallback is unreachable from source; it is kept as defence
in depth and both the branch comment and the tests now say so rather than
claiming coverage they no longer provide.

Two findings declined, both on file-wide convention. The `vera/README.md` module
map spells every indented row with leading spaces inside the code span, so
converting one row for MD038 breaks the convention (identical findings were
declined on #1178 and #1179). MD040 on the §7.2 fences: `spec/07-effects.md`
has 0 ```vera and 42 bare fences, the spec has 94 against 657, and no
markdownlint runs in pre-commit or CI — tagging three fences in a chapter with
none makes it less consistent, not more.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan
aallan force-pushed the fix/1172-decreases-runtime-guard branch from dec11b3 to e101650 Compare August 3, 2026 15:27
aallan and others added 5 commits August 3, 2026 16:42
E525 said a Tier-3 termination metric "will be checked at runtime";
decreases had no runtime lowering, so a non-terminating recursion
passed check, passed verify, and hung at run (VeraBench v0.0.18,
VB-T4-006). Every decreases function now carries the guard: entry
saves the per-function chain globals, evaluates the measure (scalars
by value, concrete ADTs by a generated structural-size helper,
lexicographic tuples componentwise per spec 5.6.1), and traps through
the contract-violation channel unless a re-entry strictly decreases
while staying non-negative; every exit restores the saved state.

Self-recursive tail calls keep TCO: the return_call is prefixed with a
call-site check (arguments captured, measure evaluated over them
against the live chain globals, activation state closed out, arguments
re-pushed), so the documented pure-iteration idiom keeps its
constant-stack 1M-depth property (#517 batteries pass unchanged) —
closing #1176's ask in the same change. Mutual-tail calls between
guarded functions demote to plain calls (no restore placement both
preserves and unwinds the chain across an elided frame); an
untranslatable site demotes identically, never a partial check.

Alongside: new E127 rejects measures with no well-founded ordering
(Float64/String/Bool were silently decorative; spec 5.6.1(3) is
normative), spec 5.6.1's own lexicographic Ackermann example is
corrected (as printed its permuted arguments cycled forever), the
well-founded list gains ADTs, and a parameterized ADT measure is
honestly unguarded (generic layouts do not describe concrete
construction — #1177) rather than wrongly ranked.

Closes #1172
Closes #1176

Co-Authored-By: Claude <noreply@anthropic.invalid>
…wrap, skip net

Review round (15 findings, 4 behavioral — each live-reproduced first):

- A failed rank walk now rolls back EVERY helper it committed
  (snapshot/restore, the _lift_pending_closures discipline): a nested
  helper completed through the reservation short-circuit could survive
  calling the deleted name, killing the whole compile at wat2wasm on a
  check-green program.
- An Exn-declaring function gets no runtime guard: a thrown exception
  unwinds past the exit restores, and the stale chain state trapped a
  later terminating call. _dec_declares_exn gates both the entry
  emission and the _dec_collect pre-pass so tail-call patches stay in
  sync; spec §5.6.1, SKILL, and CHANGELOG state the limit.
- The E127 well-foundedness check unwraps nested refinement layers in a
  loop; { { Int | P } | Q } no longer draws a spurious error.
- _compile_decreases_entry now sits in the contract-predicate
  degradation net (CodegenSkip -> E602, CodegenInvariantError -> E699),
  matching the sibling pre/postcondition paths.

Tests: four RED-first regressions plus a lexicographic component-order
mirror (asymmetric lexy/Ackermann entries; reversing the guard's
component order is mutation-verified RED), boundary-safe WAT pins,
tempfile unlink, unused helper removed. Docs: TESTING enumeration and
counts regenerated from live runs (incl. the stale E219 -> actual E217),
AGENTS negative lists synced, guard-limit qualifications everywhere.

Co-Authored-By: Claude <noreply@anthropic.invalid>
- Spec section 5.6.1: the runtime lexicographic rule now states the true
  semantics (first differing component strictly less AND non-negative,
  earlier components equal, later components checked on the hop where
  they decide) instead of an ambiguous componentwise claim; the Float64
  rejection names the real reason (no well-founded ordering the runtime
  can check, NaN/infinities unordered, floats never reach the runtime
  check); the parameterized-ADT and Exn limitation bullets no longer
  imply Tier 3 is forced (static classification is independent; only
  the runtime fallback is absent); the self-tail TCO claim is qualified
  by the untranslatable-site demotion; the well-foundedness requirement
  uses MUST per the chapter's RFC 2119 convention.
- Both measure-component loops snapshot and restore the rank-helper
  accumulator on every early return, making "a helper exists iff a
  guard calls it" total rather than incidental (the escaped helpers
  were valid-but-unused dead code, not dangling).
- New test pins the #517 property in both directions: return_call
  survives in the guarded self-tail WAT, and a 300,000-hop run
  completes — plain-call recursion at that depth exhausts the native
  stack, so finishing is the constant-stack proof. Asymmetric
  counter/accumulator roles make a slot-order swap trap.
- AGENTS.md conformance description no longer enumerates companions
  (17 fixtures import; a non-enumerating sentence cannot drift); FAQ
  test count refreshed; TESTING gains the 874 row correction and the
  check_fact_coherence() documentation; vera/README's contracts.py row
  describes the guard machinery.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Review round: the spec's Runtime checking paragraph promised enforcement
unconditionally in its lead and walked it back in bullets — the lead now
carries the supported-guard condition and names the exclusions, and the
trap guarantee is scoped to guarded functions. The duplicated
component-translation/rank-rollback loops collapse into one
_dec_translate_measure helper shared by the entry check and the
self-tail site check.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Independent adversarial review of the guard (18 attack programs; the
full report is on the PR):

- F1: the verifier's decreases call-walker skipped HandleExpr (body,
  clauses, state exprs), AnonFn bodies, constructor/qualified/module
  call arguments, IndexExpr, ArrayLit, InterpolatedString, and
  assert/assume — a measure-violating recursive call hidden there rode
  a false Tier-1 proof that the new guard exposed as a verify-green
  run-time trap. The walker now descends into every value-carrying
  node, conservatively (no path-condition extension). Two review repro
  programs pin the obligations as not-verified.
- F2: a guarded function tail-calling an UNGUARDED trampoline that
  tail-calls back looped forever with zero checks — the old
  prepend-restores-then-return_call sequence zeroed the chain before
  every re-entry. Guarded-to-unguarded tail sites now demote to plain
  calls (the mutual-tail treatment); the review's trampoline repro
  traps instead of hanging.
- F3 (documented limitation): the measure evaluates with machine i64
  arithmetic where the prover reasons over unbounded integers, so a
  near-edge measure can overflow-trap on a proved obligation —
  KNOWN_ISSUES limitation row + spec note.
- F4: spec section 5.6.1's runtime paragraph now states the guard runs
  for every tier (belt-and-braces), not only Tier 3.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan
aallan force-pushed the fix/1172-decreases-runtime-guard branch from e101650 to 30a428b Compare August 3, 2026 15:57
@aallan

aallan commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai The remaining outside-diff finding (duplicated rank-helper rollback loop in vera/codegen/contracts.py) is fixed in e583b17: _dec_translate_measure(ctx, contract, env) now owns component translation, ADT ranking, and the snapshot/rollback discipline in one place — _compile_decreases_entry maps its None to return [], [], None and _dec_self_tail_prefix maps it to return None, exactly as suggested. The two earlier outside-diff findings (the TESTING.md fact-coherence row and rollback totality in both measure-component loops) were addressed in the comment above referencing b34c082.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format=fuller --stat e583b170
printf '\n--- relevant commit diff ---\n'
git show --no-ext-diff --format= --unified=30 e583b170 -- vera/codegen/contracts.py

printf '\n--- relevant symbols on PR branch ---\n'
ast-grep outline vera/codegen/contracts.py \
  --items all \
  --type method,function \
  --match 'dec_translate_measure|compile_decreases_entry|dec_self_tail_prefix'

printf '\n--- relevant call and return contexts ---\n'
rg -n -A18 -B12 \
  'def _dec_translate_measure|def _compile_decreases_entry|def _dec_self_tail_prefix|_dec_translate_measure\(' \
  vera/codegen/contracts.py

Length of output: 1168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh api \
  -H 'Accept: application/vnd.github+json' \
  'repos/aallan/vera/commits/e583b170' \
  --jq '{
    sha,
    html_url,
    commit: {
      message: .commit.message,
      author: .commit.author
    },
    files: [
      .files[]
      | select(.filename == "vera/codegen/contracts.py")
      | {
          filename,
          status,
          additions,
          deletions,
          patch
        }
    ]
  }'

Length of output: 7279


@aallan Confirmed in e583b170.

_dec_translate_measure() is now the single implementation for component translation, ADT ranking, and self._dec_rank_helpers snapshot and rollback.

On a translation failure, _compile_decreases_entry() returns [], [], None. _dec_self_tail_prefix() returns None. The commit removes both duplicated loops.

The remaining outside-diff finding is addressed.

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format=fuller --stat b34c082d
printf '\n--- requested file diffs ---\n'
git show --no-ext-diff --format= --unified=20 b34c082d -- \
  TESTING.md \
  vera/codegen/contracts.py \
  tests/test_codegen_decreases_guard.py

Length of output: 795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh api \
  -H 'Accept: application/vnd.github+json' \
  'repos/aallan/vera/commits/b34c082d' \
  --jq '{
    sha,
    html_url,
    files: [
      .files[]
      | select(
          .filename == "TESTING.md"
          or .filename == "vera/codegen/contracts.py"
          or .filename == "tests/test_codegen_decreases_guard.py"
        )
      | {filename, status, additions, deletions, patch}
    ]
  }'

Length of output: 21593


@aallan Confirmed in b34c082d.

TESTING.md now documents check_fact_coherence() and the docs/index.htmldocs/index.md validation in both required entries.

Both measure-component loops snapshot and restore self._dec_rank_helpers for every reported early return. This preserves the helper-to-guard invariant.

test_guarded_self_tail_keeps_return_call checks the guarded return_call $drain WAT instruction and completes the 300,000-hop asymmetric case with result 600000.

The three outside-diff findings are addressed.

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format='commit %H%n%s' --stat b34c082d
printf '\n--- targeted diff ---\n'
git show --no-ext-diff --format= --unified=20 b34c082d -- \
  TESTING.md \
  vera/codegen/contracts.py \
  tests/test_codegen_decreases_guard.py

printf '\n--- relevant symbols at commit ---\n'
git show b34c082d:vera/codegen/contracts.py > /tmp/contracts-b34c082d.py
ast-grep outline /tmp/contracts-b34c082d.py \
  --items all --type method,function \
  --match 'compile_decreases_entry|dec_self_tail_prefix|dec_translate_measure'

printf '\n--- targeted source contexts at commit ---\n'
git show b34c082d:TESTING.md | rg -n -C3 'check_site_assets|check_fact_coherence|test_build_site'
git show b34c082d:vera/codegen/contracts.py | rg -n -C10 \
  'def _compile_decreases_entry|def _dec_self_tail_prefix|rank_helpers|_dec_rank_helpers'
git show b34c082d:tests/test_codegen_decreases_guard.py | rg -n -C12 \
  'def test_guarded_self_tail_keeps_return_call|return_call|300_000|300000'

Length of output: 1534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh api \
  -H 'Accept: application/vnd.github+json' \
  'repos/aallan/vera/commits/b34c082d' \
  --jq '{
    sha,
    html_url,
    message: .commit.message,
    files: [
      .files[]
      | select(
          .filename == "TESTING.md"
          or .filename == "vera/codegen/contracts.py"
          or .filename == "tests/test_codegen_decreases_guard.py"
        )
      | {filename, status, additions, deletions, patch}
    ]
  }'

Length of output: 23440


@aallan Confirmed in b34c082d.

TESTING.md now documents check_fact_coherence() in both required entries. It states the docs/index.html and docs/index.md fact-agreement check.

Both component loops snapshot and restore self._dec_rank_helpers on every identified early return. This preserves the helper-to-guard invariant.

test_guarded_self_tail_keeps_return_call checks for return_call $drain and completes the 300,000-hop asymmetric run with result 600000.

The three outside-diff findings are addressed.

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- commit summary ---'
git show --no-ext-diff --format=fuller --stat e583b170

printf '%s\n' '--- relevant commit diff ---'
git show --no-ext-diff --format= --unified=35 e583b170 -- vera/codegen/contracts.py

printf '%s\n' '--- method outline ---'
ast-grep outline vera/codegen/contracts.py \
  --items all \
  --type method,function \
  --match 'dec_translate_measure|compile_decreases_entry|dec_self_tail_prefix'

printf '%s\n' '--- implementation contexts ---'
rg -n -A100 -B8 \
  'def _dec_translate_measure|def _compile_decreases_entry|def _dec_self_tail_prefix' \
  vera/codegen/contracts.py

Length of output: 1190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- current revision ---'
git rev-parse HEAD
git status --short --branch

printf '%s\n' '--- relevant method outline ---'
ast-grep outline vera/codegen/contracts.py \
  --items all \
  --type method,function \
  --match 'dec_translate_measure|compile_decreases_entry|dec_self_tail_prefix'

printf '%s\n' '--- method bodies and call sites ---'
rg -n -A130 -B10 \
  'def _dec_translate_measure|def _compile_decreases_entry|def _dec_self_tail_prefix|_dec_translate_measure\(' \
  vera/codegen/contracts.py

Length of output: 19502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh api \
  -H 'Accept: application/vnd.github+json' \
  'repos/aallan/vera/commits/e583b170' \
  --jq '{
    sha,
    html_url,
    commit: {message: .commit.message},
    files: [
      .files[]
      | select(.filename == "vera/codegen/contracts.py")
      | {filename, status, patch}
    ]
  }'

Length of output: 7027


@aallan Confirmed in e583b1707f2aed6e007fea17663587f9c14bf898.

_dec_translate_measure() is now the single implementation for component translation, ADT ranking, and self._dec_rank_helpers snapshot and rollback.

_compile_decreases_entry() returns [], [], None when the helper returns None. _dec_self_tail_prefix() returns None in the same case.

This removes the duplicated rollback loops and preserves rollback on every failed translation path. The outside-diff finding is addressed.

You are interacting with an AI system.

@aallan
aallan merged commit f67040e into main Aug 3, 2026
29 checks passed
@aallan
aallan deleted the fix/1172-decreases-runtime-guard branch August 3, 2026 16:09
aallan added a commit that referenced this pull request Aug 3, 2026
CodeRabbit round on PR #1188: TESTING.md's test-level inventory had
drifted across three merges — the check-level prose lists and the
level-limited skip table were missing ch05_reserved_fn_name_rejected
(E153, this PR), ch07_old_outside_ensures_rejected (E174, #1180), and
ch09_builtin_effect_redefinition_rejected (E152, #1182), and the
skip-table rows for ch05_decreases_float_rejected (E127, #1179) were
absent too.  Regenerated against the manifest: twenty-eight check-level
programs, twenty-one negatives, 69 level-limited skips, and the table
verified complete for every check-level fixture.  FAQ.md's test count
catches up to the live 8,724 (the doc-counts oracle does not gate that
line — noted for the wave-4 hardening).

Skip-changelog: documentation-inventory reconciliation only, no compiler change

Co-Authored-By: Claude <noreply@anthropic.invalid>
aallan added a commit that referenced this pull request Aug 6, 2026
…1202)

Four review lenses (code review, test-coverage mutation battery,
comment-accuracy audit, silent-failure hunt) produced this round:

- The nat-binding walker now descends assert/assume CONDITIONS — a
  provably-negative narrowing argument inside one
  (`assert(takes_nat(0 - 5) > 0)`) recorded nothing and passed
  silently; it now fires the same loud E503 as direct position — and
  carries the #597 WALKER_COVERAGE marker, so check_walker_coverage.py
  (now 13 walkers) enforces its case split from here on.

- Fourteen mutation-derived tests close the review's headline hole: a
  combined mutant deleting BOTH of the nat-binding walker's new
  container arms and walking closures with the dishonest enclosing env
  passed the entire suite. The new battery kills it: scope-honesty
  pins for the nat-binding walker (closure and handler-clause
  narrowings never prove against the outer requires), enclosing-scope
  precision pins (handle body, handler state-init, forall domain),
  the exists-predicate leg, clause state-update descent, refined
  closure narrowing disclosure (tier3_unguarded + E506), loud-parity
  for assert/index in closures, and the ensures-position quantifier
  shape. A compile-and-run differential proves the interior closure
  let-guard the tier3 records claim (TestClosureInteriorBindingDifferential779).

- Comment/doc accuracy: the stale "AnonFn stays a terminal" and
  "#1179 mirror" comments now describe the descent; OldExpr/NewExpr
  dispositions are per-walker-accurate; the spec §7.5.1 pointer,
  FAQ's pre-fix parenthetical, the primitive-ops class docstring, and
  the universal-Tier-3 over-claims in spec/comment prose are all
  corrected to the slot-dependent/literal split; KNOWN_ISSUES #439's
  headline narrows to value-dependent contracts; clause-scope comments
  note the handler state slot.

- The review also surfaced a pre-existing false Tier-1 (State<Nat>
  cell writes neither obligated nor guarded) — filed as #1203 with a
  KNOWN_ISSUES row and a #754 cross-link; deferred from this PR
  because the fix is codegen guard emission with its own blast
  radius, not walker scope.

The silent-failure hunt's HIGH finding is fixed: a NESTED constructor
sub-pattern narrowing on an unprojectable scrutinee (under the descent,
every match in a closure) silently vanished from the stream while
codegen's per-bind guard trapped it — verify-clean, bare anon_0 trap.
The nested branch now records the same honest static fallbacks as its
direct siblings (nat: guarded tier3, probe-verified in both positions;
refined: unguarded E506 — the refined nested bind genuinely does NOT
trap, so #765 narrows to that residual, probe-verified and commented;
widen: guarded tier3), mutation-validated with a run differential.
Four stale `pragma: no cover` annotations on now-hot fallback branches
are removed, and two pre-existing discoveries are tracked: #1203
(State<Nat> cell writes unobligated+unguarded, KNOWN_ISSUES row) and
#1204 (array-domain quantifiers fail WASM translation, KNOWN_ISSUES
row).

The nested-closure differential pin tightens to an exact-count
assertion, and the CHANGELOG names the dominant new corpus obligation
kind (int_overflow).

Co-Authored-By: Claude <noreply@anthropic.invalid>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant