Skip to content

refactor(#657): codegen INVARIANT guards → CodegenInvariantError [E699]; preserve [E615]-reachable PROPAGATE#833

Merged
aallan merged 6 commits into
mainfrom
feat/657-invariant-defensive
Jul 1, 2026
Merged

refactor(#657): codegen INVARIANT guards → CodegenInvariantError [E699]; preserve [E615]-reachable PROPAGATE#833
aallan merged 6 commits into
mainfrom
feat/657-invariant-defensive

Conversation

@aallan

@aallan aallan commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What & why

Follow-up to #626 (closed by #658). The #626 audit classified every return None in vera/codegen/** / vera/wasm/**; #658 converted the 104 user-actionable SILENT_SKIP sites to CodegenSkip[E602]. This closes #657 by handling the remaining buckets — but the landed change is deliberately smaller than the audit's numeric targets, because implementing it surfaced a flaw in the audit's core premise.

⚠️ The finding: the audit's PROPAGATE premise doesn't hold

The audit assumed that after #658 every codegen leaf raises, making the PROPAGATE if x is None: return None forwards dead (removable). That is false for the #630 [E615] string-interpolation channel: when interpolation inference fails, _translate_interpolated_string records the failing segments to _interp_inference_failures and returns None, which propagates up and is dropped loudly as [E615] at the _compile_fn boundary. So translate_expr / translate_block still return None reachably, and any if x is None: return None that forwards them is load-bearing PROPAGATE, not dead.

Converting such a forward to assert/raise turns a graceful [E615] function-drop into a crash. The test suite caught this empirically: an over-eager assert conversion fired AssertionError across TestE615LoudInterpolationFallthrough630.

Changes

  • Track 2 — 21 genuine non-forwarding guards → raise CodegenInvariantError ([E699]): 2 in codegen/closures.py, 19 in wasm/operators.py (slot-ref/pipe/float-op/dispatch-fallthrough/ADT-eq/result-ref/quantifier-shape/state-lookup). A compiler bug now surfaces as a structured [E699] "internal compiler error, file a bug", not a silent skip or a mis-attributed [E602]. Pinned by the new tests/test_codegen_invariant_e699.py (forces the raise via a translate_block monkeypatch); the previously-# pragma: no cover handler in codegen/functions.py is now covered.
  • PROPAGATE preserved (reachable via [E615]): all the forwarding sites stay return None — satisfying Follow-up to #626: convert INVARIANT_DEFENSIVE sites + audit PROPAGATE cleanup #657's own acceptance criterion, "either preserved (still reachable) or removed (unreachable)." Five operators.py sites the audit had tagged INVARIANT were in fact such forwards and are kept. inference.py's defensive sites are kept as Optional-by-contract (callers already raise [E602]).
  • Guardrail comments so this can't recur: a new "Reachable None via the [E615] channel" section in vera/skip.py's taxonomy docstring, a # #657 / #630 [E615] comment at each of the 5 preserved forwards, and a note at the canonical None source (_translate_interpolated_string).

Exit checklist

  • Genuine INVARIANT guards raise CodegenInvariantError[E699] (21 sites); reachable-via-E615 forwards preserved
  • tests/test_codegen_invariant_e699.py pins the [E699] contract; functions.py handler un-pragma'd
  • full pytest · mypy · ruff · check_e602_clean · E615 suite (62) — green
  • release prep v0.0.191: version ×6, CHANGELOG [0.0.191], HISTORY row + "By the numbers" footer, ROADMAP Follow-up to #626: convert INVARIANT_DEFENSIVE sites + audit PROPAGATE cleanup #657 removed, TESTING row, doc-counts (5,558 / 53)

Closes #657

See the issue comment for the full re-scope rationale.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved compiler/codegen “should be impossible” handling so invariant violations consistently raise structured [E699], rather than being silently skipped or producing mixed downstream diagnostics.
    • Tightened closure lifting and function compilation so closure invariants surface as a single [E699], while preserving intentional reachable skip/control-flow semantics (including the [E615] channel).
    • Strengthened operator translation invariants to avoid unsafe “return None” paths; added regression coverage to ensure [E699] is not misreported as [E602].
  • Documentation / Release Hygiene
    • Updated versioning and release/test metrics across documentation.

T and others added 3 commits July 1, 2026 14:37
…rds raise CodegenInvariantError

Follow-up to #626 (closed by #658). Converts the 2 INVARIANT_DEFENSIVE sites in
`_compile_lifted_closure` (unsupported closure param / return WASM type — states
the type-checker should have rejected) from `return None` to
`raise CodegenInvariantError(msg, anon_fn)`, so a compiler bug surfaces as an
[E699] "internal compiler error" instead of a silent skip. Updated the stale
catch-handler comment ("no production code raises CodegenInvariantError yet").

Both sites stay `# pragma: no cover` — they guard type-check-impossible states,
so no test input reaches them; the change is behaviour-preserving for all
well-typed programs. Establishes the canonical Track-2 conversion pattern for
the remaining files (operators.py 25, inference.py 10).

mypy clean; 50 closure codegen tests pass; e602 gate clean; ruff clean.

Co-Authored-By: Claude <[email protected]>
…t 19 non-forwarding guards

Converts the 19 genuine INVARIANT_DEFENSIVE guards in operators.py (slot-ref
shape, pipe RHS, float-op lookup, binary/unary dispatch fallthroughs, ADT-eq,
result-ref, quantifier predicate shape, old/new/_extract_state_type_name
lookups) from `return None` to `raise CodegenInvariantError` — a compiler bug
now surfaces as [E699] rather than a silent skip.

Deliberately PRESERVES 5 sites the #657 audit mis-classified as
INVARIANT_DEFENSIVE that actually FORWARD translate_expr / translate_block
results (binary operand, unary operand, assert condition, quantifier domain,
quantifier body). Those results return None *reachably* via the #630 [E615]
string-interpolation channel (translate_expr on a failing InterpolatedString
records to _interp_inference_failures and returns None, dropped loudly at the
_compile_fn boundary). So those `return None`s are load-bearing PROPAGATE, not
dead — converting them would turn a graceful [E615] function-drop into an [E699]
crash. See #657 comment for the full finding.

All converted paths stay `# pragma: no cover` (type-check-impossible).
Behaviour-preserving: 62 E615/interpolation tests pass, 1233 codegen+wasm tests
pass, e602 gate clean, ruff + mypy clean.

Co-Authored-By: Claude <[email protected]>
…99, release v0.0.191

Guardrail comments so the PROPAGATE mis-classification can't recur:
- vera/skip.py: new "Reachable None via the [E615] channel" section in the #626
  taxonomy docstring — translate_expr/translate_block still return None reachably
  (failed string interpolation, #630), so forwards of them are load-bearing
  PROPAGATE, not dead; only non-forwarding guards are CodegenInvariantError
  candidates.
- vera/wasm/operators.py: a `# #657 / #630 [E615]` comment at each of the 5
  preserved forwards (binary/unary operand, assert cond, quantifier domain/body)
  explaining why they stay `return None`, plus a note at the canonical source
  (_translate_interpolated_string's `if had_failure: return None`).

Pin the Track-2 contract:
- tests/test_codegen_invariant_e699.py: forces a CodegenInvariantError via a
  translate_block monkeypatch and asserts it surfaces as [E699] (severity error).
- vera/codegen/functions.py: the _compile_fn CodegenInvariantError handler is now
  reachable/covered, so its `# pragma: no cover` + stale "no production code
  raises yet" comment are removed.

Release v0.0.191 (Closes #657): version bump x6, CHANGELOG [0.0.191] (both the
E699 conversion and the corrected PROPAGATE/E615 premise), HISTORY row + "By the
numbers" footer (189 -> 191 tagged releases), ROADMAP #657 -> removed, TESTING
row + counts (5,558 / 53 files).

All gates green: full suite, e602, E615 (62), E699 (1), mypy, ruff, doc-counts,
version-sync, site-assets, changelog.

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

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 14a45923-374c-4c89-a38a-e61d4ae1b282

📥 Commits

Reviewing files that changed from the base of the PR and between e715ab5 and 61d697b.

📒 Files selected for processing (2)
  • TESTING.md
  • tests/test_codegen_invariant_e699.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

📝 Walkthrough

Walkthrough

Codegen guard paths now raise CodegenInvariantError and surface [E699], while reachable [E615] None propagation remains documented and preserved. Release metadata, version numbers, roadmap/testing counts, and regression coverage were updated accordingly.

Changes

E699 invariant handling rollout

Layer / File(s) Summary
Release metadata and docs
CHANGELOG.md, HISTORY.md, README.md, ROADMAP.md, TESTING.md, pyproject.toml, vera/__init__.py
Version 0.0.191 is recorded across release metadata, status text, and count tables, with matching changelog, history, roadmap, testing, and package version updates.
Codegen invariant handling
vera/skip.py, vera/codegen/closures.py, vera/codegen/functions.py
Closure compilation and function-level handling now treat invariant failures as CodegenInvariantError, while the skip documentation keeps reachable [E615] forwarding distinct from invariant cases.
WASM operator invariant checks
vera/wasm/operators.py
Operator translation paths now raise CodegenInvariantError for unsupported or impossible states, and the existing [E615]/[E630] propagation comments remain in place.
E699 regression tests
tests/test_codegen_invariant_e699.py
New regression tests force invariant failures during translation and closure lifting, then assert [E699] is emitted instead of [E602].

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

  • aallan/vera#598: Touches the same closure codegen paths that this PR extends at the lifted-closure boundary.
  • aallan/vera#658: Closely related to the CodegenInvariantError boundary handling that this PR refines.

Suggested labels: compiler, tests, docs

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the main refactor: invariant guards now raise CodegenInvariantError [E699] while preserving reachable [E615] PROPAGATE paths.
Docstring Coverage ✅ Passed Docstring coverage is 84.21% which is sufficient. The required threshold is 80.00%.
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 PASS: CHANGELOG 0.0.191 explicitly covers the new E699 diagnostic, and I found no changed CLI/LSP/ABI surface in ExecuteResult or command signatures.
Spec And Implementation Move Together ✅ Passed PASS — the code changes only reclassify impossible codegen states to E699 and preserve E615 forwards; the formal spec’s compile behaviour stays aligned.
Diagnostics Carry An Error Code ✅ Passed All changed diagnostic helper calls in functions.py and closures.py include explicit error_code values (E600/E601/E602/E699); no code-less new diagnostics found.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/657-invariant-defensive

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

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.06%. Comparing base (25a86df) to head (61d697b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #833      +/-   ##
==========================================
+ Coverage   92.05%   92.06%   +0.01%     
==========================================
  Files          90       90              
  Lines       27307    27321      +14     
  Branches      324      324              
==========================================
+ Hits        25137    25154      +17     
+ Misses       2162     2159       -3     
  Partials        8        8              
Flag Coverage Δ
javascript 65.58% <ø> (ø)
python 95.06% <100.00%> (+0.01%) ⬆️

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

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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_invariant_e699.py`:
- Around line 61-67: The test in test_codegen_invariant_e699.py only checks that
result.diagnostics contains E699, so it can still pass if the old
unsupported-construct diagnostic E602 is also emitted. Update the assertion
block around e699 to explicitly verify that no diagnostic in result.diagnostics
has error_code "E602", alongside the existing checks on severity and
description, so the new compiler path is confirmed to avoid the legacy fallback.

In `@vera/codegen/closures.py`:
- Around line 261-263: The unsupported WASM type path in
`_compile_lifted_closure()` is being converted into a normal closure failure,
which causes `_lift_pending_closures()` and `_compile_fn()` to fall through and
emit the generic skip warning. Update the handling around
`CodegenInvariantError` in `closures.py` so the invariant failure is preserved
for the E699 path, either by re-raising it or by returning a distinct failure
signal that `_lift_pending_closures()` and `_compile_fn()` can recognize and
avoid treating as a user-facing unsupported-expression skip. Apply the same fix
consistently in the related closure handling blocks referenced by
`_compile_lifted_closure`, `_lift_pending_closures`, and `_compile_fn`.

In `@vera/wasm/operators.py`:
- Around line 693-702: The quantifier predicate handling in the predicate
translation path only checks for zero parameters and then blindly uses
pred.params[0], which can hide malformed multi-parameter predicates. Update the
logic around expr.predicate in the quantifier codegen to assert exactly one
parameter is present before accessing it, and raise CodegenInvariantError for
both zero-parameter and multi-parameter cases. Keep the existing NamedType
validation on pred.params[0] and align the new invariant check with the
surrounding patterns in this code path.
🪄 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: b22ab662-871e-44f6-97db-9e18413bd83f

📥 Commits

Reviewing files that changed from the base of the PR and between 25a86df and 2358c95.

⛔ Files ignored due to path filters (5)
  • 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/**
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (12)
  • CHANGELOG.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen_invariant_e699.py
  • vera/__init__.py
  • vera/codegen/closures.py
  • vera/codegen/functions.py
  • vera/skip.py
  • vera/wasm/operators.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

Comment thread tests/test_codegen_invariant_e699.py
Comment thread vera/codegen/closures.py
Comment thread vera/wasm/operators.py
…y guard; test no-[E602]

CodeRabbit (PR #833), 3 findings, all valid:

- vera/codegen/closures.py + functions.py: a closure-body CodegenInvariantError
  was caught in _compile_lifted_closure (emitted [E699] AND returned None), so
  _compile_fn ALSO emitted a spurious [E602] "closure skipped" — mixing the
  compiler-bug and user-skip signals. The invariant now PROPAGATES to
  _compile_fn's handler (new try/except around _lift_pending_closures) for a
  single [E699]; the local handler is removed. Same shape as the [E615] lesson:
  propagate the signal, don't swallow it.
- vera/wasm/operators.py: the quantifier-predicate guard tightened from
  `if not pred.params` to `if len(pred.params) != 1`, so a malformed
  multi-parameter predicate raises [E699] instead of silently using params[0].
- tests/test_codegen_invariant_e699.py: the E699 test now also asserts the path
  is NOT reported as [E602] (proving correct attribution); a second test pins
  the closure-lift -> [E699] propagation (covering the new _compile_fn handler).

Doc counts reconciled (5,559 tests; checker E699 file 2/104). Full codegen suite
+ E615 + e602 gate + mypy + ruff green.

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

aallan commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Pushed 3235391 addressing all three findings: (1) the E699 test now also asserts the path is not [E602] + a second test pins the closure-lift→[E699] propagation; (2) a closure-body CodegenInvariantError now propagates to _compile_fn (new handler around _lift_pending_closures) for a single [E699] instead of [E699]+spurious[E602] — the local handler in _compile_lifted_closure is removed; (3) the quantifier-predicate guard is now if len(pred.params) != 1.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
git log --oneline -10
echo "---"
git show 3235391 --stat

Length of output: 2087


🏁 Script executed:

#!/bin/bash
gh api repos/aallan/vera/commits/3235391 --jq '.files[].filename'
echo "---"
gh api repos/aallan/vera/commits/3235391 --jq '.files[] | select(.filename | test("closures.py|operators.py|functions.py|test_codegen_invariant_e699.py")) | .patch'

Length of output: 6914


🏁 Script executed:

#!/bin/bash
gh api repos/aallan/vera/contents/vera/codegen/closures.py?ref=3235391 --jq '.content' | base64 -d > closures.py
gh api repos/aallan/vera/contents/vera/codegen/functions.py?ref=3235391 --jq '.content' | base64 -d > functions.py
gh api repos/aallan/vera/contents/vera/wasm/operators.py?ref=3235391 --jq '.content' | base64 -d > operators.py
echo "=== closures.py: _compile_lifted_closure full ==="
awk '/def _compile_lifted_closure/,/^    def [a-zA-Z_]+\(/{print NR": "$0}' closures.py | sed -n '1,140p'
echo "=== _lift_pending_closures ==="
grep -n "_lift_pending_closures" closures.py functions.py

Length of output: 1376


🏁 Script executed:

#!/bin/bash
echo "=== _lift_pending_closures (closures.py) ==="
sed -n '1,70p' closures.py
echo "=== _compile_lifted_closure full body ==="
sed -n '177,420p' closures.py

Length of output: 16513


🏁 Script executed:

#!/bin/bash
sed -n '70,177p' closures.py

Length of output: 5499


🏁 Script executed:

#!/bin/bash
sed -n '380,460p' functions.py
echo "=== quantifier fix context ==="
grep -n "quantifier predicate" operators.py
sed -n '675,715p' operators.py

Length of output: 6720


@aallan Verified all three fixes in 3235391 — they land correctly:

  1. Test coverage: test_codegen_invariant_e699.py now explicitly asserts not any(d.error_code == "E602" ...) alongside E699, and the new test_closure_lifting_invariant_error_surfaces_as_e699 pins the closure-lift propagation via monkeypatch.setattr(ClosureLiftingMixin, "_lift_pending_closures", _boom). Both assertions are meaningful and correctly target the regression this PR fixes.

  2. Single-signal propagation: Confirmed _lift_pending_closures (vera/codegen/closures.py) calls _compile_lifted_closure with no intervening try/except CodegenInvariantError — the removed local handler means the exception now propagates cleanly out to vera/codegen/functions.py::_compile_fn's new try/except around self._lift_pending_closures(ctx). decl is correctly in scope for the error node fallback and message. This structurally guarantees a single [E699] rather than [E699] + spurious [E602], since the only catch point left is at the function boundary.

  3. Quantifier guard: if len(pred.params) != 1 in _translate_quantifier is the correct invariant — the old if not pred.params only rejected the empty case, silently allowing a hypothetical multi-param predicate to fall through to pred.params[0] without complaint. Now any predicate arity other than exactly one raises CodegenInvariantError before indexing.

No further issues found — the reachable-None [E615]/[E630] forwarding paths in the same functions (domain/body translation in the quantifier, etc.) remain untouched and correctly documented.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@tests/test_codegen_invariant_e699.py`:
- Around line 77-104: The test is patching
ClosureLiftingMixin._lift_pending_closures directly, which bypasses
_compile_lifted_closure and cannot detect a regression where that method
re-catches CodegenInvariantError and reintroduces the old [E699]+[E602]
behavior. Update the test to exercise the lifted-closure body translation path
through _compile_lifted_closure (or the specific closure-body helper it calls),
then assert the diagnostic stream contains [E699] and does not contain the
closure-skipped [E602] so the intended single-signal behavior is pinned.

In `@vera/codegen/functions.py`:
- Around line 421-442: The `try`/`except` around
`self._lift_pending_closures(ctx)` in `vera/codegen/functions.py` handles the
invariant failure correctly, but it can exit before the closure-lifting rollback
footer runs, leaving state like `_next_closure_id` and registered closure
signatures desynchronised. Update this `CodegenInvariantError` path to
explicitly restore the same closure-lifting state that
`_lift_pending_closures()` would normally roll back before calling `_error()`
and returning `None`. Use the existing closure-lifting bookkeeping in
`Codegen`/`_lift_pending_closures` as the source of truth so later functions see
consistent closure IDs and table indices.
🪄 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: 7631920d-9808-4214-b37e-de867dc285c9

📥 Commits

Reviewing files that changed from the base of the PR and between 2358c95 and 3235391.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen_invariant_e699.py
  • vera/codegen/closures.py
  • vera/codegen/functions.py
  • vera/wasm/operators.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

Comment thread tests/test_codegen_invariant_e699.py Outdated
Comment thread vera/codegen/functions.py
…bort; deepen the E699 test

Two CodeRabbit findings on the review round, both valid:

- vera/codegen/closures.py: my propagate-fix let a closure-body
  CodegenInvariantError abort _lift_pending_closures mid-worklist, skipping the
  commit/rollback footer — leaving self._next_closure_id advanced (module state
  desync for later functions' closure_id <-> table_index). Now the
  _compile_lifted_closure call is wrapped: on CodegenInvariantError it restores
  _next_closure_id (mirroring the any_failed rollback) and re-raises, so
  _compile_fn still surfaces a single [E699] with clean state.
- tests/test_codegen_invariant_e699.py: the closure test now patches
  _compile_lifted_closure (not _lift_pending_closures), driving the real
  worklist + rollback + _compile_fn handler — so it guards the local-swallow
  regression too. Uses a real closure program. It asserts [E699]; it can't
  assert "no [E602]" because a full closure compile emits incidental [E602]/
  [E604] warnings from the prelude regardless (single-signal property is
  inspection-verified).

50 closure tests + both E699 tests pass; mypy + ruff + e602 clean; doc counts 114 lines.

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

aallan commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Pushed e715ab5 for the two review-round findings: (1) _lift_pending_closures now rolls back _next_closure_id (mirroring its any_failed path) when a closure-body CodegenInvariantError aborts the worklist, then re-raises to _compile_fn — no module-state desync; (2) the closure test now patches _compile_lifted_closure to drive the real worklist+rollback+handler path.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
git log --oneline -5
echo "---"
git show e715ab5 --stat
echo "=== patches ==="
gh api repos/aallan/vera/commits/e715ab5 --jq '.files[] | select(.filename | test("closures.py|test_codegen_invariant_e699.py")) | .patch'

Length of output: 2248


🏁 Script executed:

#!/bin/bash
gh api repos/aallan/vera/commits/e715ab5 --jq '.files[].filename'
echo "---"
gh api repos/aallan/vera/commits/e715ab5 --jq '.files[] | select(.filename | test("closures.py|test_codegen_invariant_e699.py")) | .patch'

Length of output: 5355


🏁 Script executed:

#!/bin/bash
gh api repos/aallan/vera/contents/vera/codegen/closures.py?ref=e715ab5 --jq '.content' | base64 -d > closures_new.py
sed -n '55,140p' closures_new.py

Length of output: 5062


🏁 Script executed:

#!/bin/bash
sed -n '140,190p' closures_new.py

Length of output: 2523


Verified. Both fixes in e715ab5 hold up:

  1. Rollback correctness: prev_next_closure_id is snapshotted before the worklist starts, and the new except CodegenInvariantError block restores self._next_closure_id = prev_next_closure_id before re-raising — an exact mirror of the existing any_failed rollback at the bottom of the function. Since local buffers (new_closure_fns_wat, new_closure_table, new_source_map, new_sigs) are only committed on the all-success path (and are discarded with the stack frame on the exception path), there's nothing else that needs unwinding. The closure_id ↔ table_index correspondence for subsequent top-level functions is preserved correctly.

  2. Test now exercises the real path: test_closure_body_invariant_error_surfaces_as_e699 patches _compile_lifted_closure (not _lift_pending_closures), so the actual worklist loop, the new rollback, and the try/except in _compile_fn are all genuinely driven. Good catch on documenting why the no E602 assertion was dropped here — the rationale (incidental [E602]/[E604] warnings from unrelated prelude paths even in the unpatched compile) is sound and correctly scoped; the single-signal property is instead pinned by the earlier test_codegen_invariant_error_surfaces_as_e699 test, which is the right division of labour.

No further issues — this closes out the #657 review thread cleanly.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🤖 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_invariant_e699.py`:
- Around line 82-94: The test is patching the wrong layer by replacing
`_compile_lifted_closure` itself, which bypasses the real implementation and
would miss a reintroduced local `except CodegenInvariantError` in that method.
Update the test to trigger the failure through a helper that
`_compile_lifted_closure` calls, so the actual propagation path into
`_compile_fn` and the `_next_closure_id` rollback is exercised while still
asserting the `[E699]` signal.
🪄 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: 50114003-6009-43e8-b923-7109b8fa8613

📥 Commits

Reviewing files that changed from the base of the PR and between 3235391 and e715ab5.

📒 Files selected for processing (3)
  • TESTING.md
  • tests/test_codegen_invariant_e699.py
  • vera/codegen/closures.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

Comment thread tests/test_codegen_invariant_e699.py Outdated
…mpile_lifted_closure

CodeRabbit: patching `_compile_lifted_closure` wholesale bypassed the real
method, so a reintroduced local `except CodegenInvariantError` there would go
undetected. The test now injects the failure at a callee — `translate_block`,
keyed by AST-node identity so it fires ONLY on the closure body — so the real
`_compile_lifted_closure` runs end-to-end, propagates through
`_lift_pending_closures` (with the `_next_closure_id` rollback), and reaches
`_compile_fn` for the `[E699]`. Added a generic `_find_anon_fn` AST walker to
locate the closure body.

Mutation-validated: simulating the re-added local swallow removes E699 (test
goes RED), confirming it now guards that regression. Both E699 tests pass; mypy
+ ruff clean; doc counts (156 lines).

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

aallan commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Pushed 61d697b: the closure E699 test now injects the failure at a callee (translate_block, AST-identity-keyed to the closure body) so the real _compile_lifted_closure runs and must propagate — mutation-validated that a re-added local swallow turns the test RED.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
git log --oneline -5
echo "---"
git show 61d697b --stat
echo "=== patch ==="
gh api repos/aallan/vera/commits/61d697b --jq '.files[] | select(.filename | test("test_codegen_invariant_e699.py|closures.py")) | .patch'

Length of output: 2246


🏁 Script executed:

#!/bin/bash
gh api repos/aallan/vera/commits/61d697b --jq '.files[].filename'
echo "---"
gh api repos/aallan/vera/commits/61d697b --jq '.files[] | select(.filename | test("test_codegen_invariant_e699.py|closures.py")) | .patch'

Length of output: 4434


🏁 Script executed:

#!/bin/bash
git fetch origin pull/833/head:pr833 2>&1 | tail -5
git checkout pr833 2>&1 | tail -5
ast-grep run --pattern 'def translate_block($$$ARGS):' --lang python vera/wasm | head -30
grep -n "def translate_block" -r vera/wasm | head

Length of output: 562


🏁 Script executed:

#!/bin/bash
sed -n '540,570p' vera/wasm/context.py

Length of output: 1561


🏁 Script executed:

#!/bin/bash
grep -n "class AnonFn" -A 15 vera/ast.py
echo "---"
sed -n '1,180p' tests/test_codegen_invariant_e699.py

Length of output: 6619


🏁 Script executed:

#!/bin/bash
grep -n "except CodegenInvariantError" vera/codegen/closures.py vera/codegen/functions.py

Length of output: 660


This confirms the only two catch points are the rollback-and-reraise in _lift_pending_closures (Line 99) and the two _compile_fn boundary handlers — no local swallow remains in _compile_lifted_closure itself.

@aallan Verified 61d697b — this is a solid strengthening of the test.

  • _find_anon_fn's generic dataclass/list/tuple walk correctly locates anon.body: Block, and the monkeypatch signature _boom(self, block, env) matches WasmContext.translate_block(self, block: ast.Block, env: WasmSlotEnv) (vera/wasm/context.py, Line 548) exactly.
  • The block is closure_body check is an identity comparison, so it's unaffected by AnonFn/Block being frozen dataclasses with structural __eq__ — it fires only for the specific closure body node produced by this one parse_file/transform run, and delegates to original_translate_block for everything else, so the rest of the compile (prelude, other blocks) proceeds normally.
  • Re-confirmed the only except CodegenInvariantError sites left in the codebase are the rollback-and-reraise in _lift_pending_closures (vera/codegen/closures.py, Line 99) and the two _compile_fn boundary handlers (vera/codegen/functions.py, Lines 334 and 423) — no local swallow remains in _compile_lifted_closure. This matches the mutation-testing claim: reintroducing a local catch there would intercept the exception before it ever reaches _compile_fn, turning this test RED as described.
  • This test now genuinely exercises the full real path (_compile_lifted_closure_lift_pending_closures rollback → _compile_fn handler) rather than a patched stand-in.

No further issues — this closes out the #657 review thread.

[resolve]

@aallan aallan merged commit acca643 into main Jul 1, 2026
28 checks passed
@aallan aallan deleted the feat/657-invariant-defensive branch July 1, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow-up to #626: convert INVARIANT_DEFENSIVE sites + audit PROPAGATE cleanup

1 participant