Skip to content

v0.0.139 — closure codegen pair (#614 + #615)#625

Merged
aallan merged 3 commits intomainfrom
claude/closure-lifter-bug-pair-614-615
May 8, 2026
Merged

v0.0.139 — closure codegen pair (#614 + #615)#625
aallan merged 3 commits intomainfrom
claude/closure-lifter-bug-pair-614-615

Conversation

@aallan
Copy link
Copy Markdown
Owner

@aallan aallan commented May 8, 2026

Summary

Closes #614 and #615 — two distinct closure-codegen root causes that share a failure path. Both bugs were filed during the Tetris-experiment design review on PR #613; the original hypothesis (one fix would close both) turned out to be wrong, but the two fixes together close the user-visible failure mode for every shape we've reproduced.

Bumps version v0.0.138 → v0.0.139 per the release-on-fix convention.

Root causes (independent)

#614f()[i] element-type inference gap. _infer_index_element_type_expr in vera/wasm/inference.py only handled SlotRef and nested-IndexExpr collections. When the collection was a FnCall returning Array<T>, inference fell through to return None, _translate_index_expr returned None too, and the enclosing function got dropped from the WAT output.

Surface Pre-fix observable
Top-level f(x)[i] [E602] "function body contains unsupported expressions — skipped" warning, function silently absent
Closure body f(x)[i] _compile_lifted_closure returns None → closure dropped → unknown table 0: table index out of bounds at WASM validation

Same gap, two surfaces. Closing the inference path closes both manifestations.

#615 — closure capture order miscompile. _collect_free_vars returned captures in walker (DFS) order without filling missing prefix indices or sorting per-type. Two failure shapes from one root cause:

  1. Non-contiguous outer slot. Body refs @Int.k while skipping @Int.j (j<k) → lift-side env had no entry for j → env.resolve("Int", k) returned None → closure dropped → WASM validation trap.
  2. Ascending walker-order silent miscompute. Even contiguous captures: when source order put the lower outer_idx first (e.g. body @Int.1 - @Int.2), the walker added (Int, 0) before (Int, 1) → ascending push order → wrong stack layout under WasmSlotEnv.resolve (which uses pos = len-1-index) → body's slot refs resolved to the WRONG captured locals. No trap, just wrong output.

Discovered during the diagnosis: silent miscompute applied even to programs that were "supposedly" working — the sum tests in test_closure_with_capture happened to pass only because of commutative addition.

Fixes

Site Change
vera/codegen/registration.py Register each FnDecl's full Vera return-type expression in a new _fn_ret_type_exprs dict alongside the WAT-type _fn_sigs.
vera/codegen/core.py New _fn_ret_type_exprs: dict[str, ast.TypeExpr] field.
vera/codegen/functions.py Propagate to per-function WasmContext.
vera/codegen/closures.py Propagate to closure WasmContext.
vera/wasm/context.py New _fn_ret_type_exprs field + set_fn_ret_type_exprs() setter.
vera/wasm/inference.py Extend _infer_index_element_type_expr to handle FnCall collection — look up return TypeExpr, extract Array element.
vera/wasm/closures.py Rewrite _collect_free_vars to group captures by type, fill prefix [0, max], sort descending per type.

Regression tests

New test classes in tests/test_codegen_closures.py:

Test count: 3,766 → 3,773 (+7).

Test plan

What this doesn't do

  • Doesn't strengthen the failure-mode itself. When _compile_lifted_closure returns None, the closure_id was already registered at the call site, so the call_indirect references a missing table entry → WASM validation trap. A safer pattern is to propagate the lift failure back to the parent function (so it gets the [E602] "skipped" warning rather than producing malformed WAT), or to track unrealised closure_ids and emit a runtime trap with a Vera-level diagnostic. Worth a follow-up issue; out of scope for this fix because closing the two underlying causes makes the failure unreachable for known programs.
  • Doesn't generalise the inference walker for FnCall returning aliased Array. If a fn returns a type alias type Row = Array<Bool>, the alias path in _alias_array_element already handles it, but the FnCall branch could miss aliases that resolve only after monomorphisation. Not exercised by current tests; can extend if a real program hits it.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected WASM generation for indexing function-call results (e.g., f()[i]).
    • Fixed closure capture-ordering to prevent miscompiles for non‑contiguous captures.
  • Tests

    • Added regression tests covering function-call indexing and non‑contiguous closure capture scenarios.
  • Documentation

    • Updated changelog, README, ROADMAP, KNOWN_ISSUES and TESTING stats for v0.0.139.
  • Chore

    • Bumped package version to v0.0.139.

Closes #614
Closes #615

Two distinct root causes that share a failure path: closure dropped
from the function table -> `unknown table 0` at WASM validation, or
silent miscompute when later `let` pushes mask the bad index.

#614: `f()[i]` where `f` returns `Array<T>` was silently dropped.
`_infer_index_element_type_expr` only handled SlotRef and nested-
IndexExpr collections; FnCall fell through to None, propagating up
until either `_compile_fn` skipped the function with [E602] (top-
level case, same shape as #604) or `_compile_lifted_closure`
returned None (closure case -- silent: closure_id was registered at
the call site but the lifted function never made it to the table).

Fix: register each FnDecl's full Vera return-type expression in a
new `_fn_ret_type_exprs` dict on `CodeGenerator`, propagate to per-
function and closure WasmContexts, and extend
`_infer_index_element_type_expr` to look up the called fn's return
type and extract the `Array<T>` element when applicable.

#615: closure capture order miscompile, two failure shapes from one
root cause:
1. Non-contiguous outer slot.  Body refs `@Int.k` while skipping
   `@Int.j` (j<k) -> lift-side env had no entry for j -> resolve
   returned None -> closure dropped -> WASM validation trap.
2. Ascending walker-order silent miscompute.  Even contiguous
   captures: walker visited lower outer_idx first (e.g. body
   `@Int.1 - @Int.2`) -> ascending push order -> wrong stack
   layout under `WasmSlotEnv.resolve` -> body's slot refs resolved
   to WRONG captured locals.  No trap, just wrong output.

Fix: in `_collect_free_vars`, group captures by type, fill prefix
[0, max] per type with synthetic entries (their wasm_type matches
the type's other captures since type_name deterministically maps to
a single WAT type), and sort each group descending by outer_idx so
the lift-side push lands the highest outer_idx at the deepest stack
position.

Regression tests: new `TestIndexExprOfFnCall614` (3 tests) and
`TestNonContiguousCapture615` (4 tests) classes cover all five
concrete failure shapes plus a baseline that prevents regression of
the case that was previously coincidentally working.

Test count 3,766 -> 3,773 (added 7).  Closes #614 and #615 from the
ROADMAP stabilisation tier; HISTORY Stage 12 gains the v0.0.139
entry; KNOWN_ISSUES bug entries for #614 / #615 removed.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

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

Run ID: ddb8139f-3c57-404c-8806-acb343b651bc

📥 Commits

Reviewing files that changed from the base of the PR and between 431584d and b8fba5f.

📒 Files selected for processing (1)
  • HISTORY.md

📝 Walkthrough

Walkthrough

Version 0.0.139 fixes two closure-codegen correctness bugs: function-call indexing loses return-type information when inferring indexed-access element types (#614), and non-contiguous free-variable captures miscompile due to incorrect stack-layout ordering (#615). The fixes introduce return-type-expression tracking through codegen, normalise closure capture ordering via type-grouped gap-filling and descending-index sorting, and add regression test coverage.

Changes

Closure Codegen Pair Fixes

Layer / File(s) Summary
Data Contracts for Return Types
vera/codegen/core.py, vera/wasm/context.py
CodeGenerator and WasmContext add _fn_ret_type_exprs and set_fn_ret_type_exprs() to retain full ast.TypeExpr return types for inference.
Function Registration and Type Recording
vera/codegen/registration.py
_register_fn records each function's full return_type TypeExpr into _fn_ret_type_exprs.
Codegen Integration: Propagating Return Types
vera/codegen/functions.py, vera/codegen/closures.py
_compile_fn and _compile_lifted_closure call ctx.set_fn_ret_type_exprs(self._fn_ret_type_exprs) so WasmContext has return-type expressions during body compilation.
Type Inference for Function-Call Indexing (Fix #614)
vera/wasm/inference.py
_infer_index_element_type_expr extended to handle ast.FnCall by resolving the function's registered return TypeExpr and extracting the array element NamedType.
Closure Capture Normalisation (Fix #615)
vera/wasm/closures.py
_collect_free_vars groups captures by wasm type, fills missing outer indices to form contiguous prefixes per type, and sorts captures descending by outer index to align lifted-closure env layout with resolver expectations.
Regression Tests and Release Documentation
tests/test_codegen_closures.py, CHANGELOG.md, HISTORY.md, KNOWN_ISSUES.md, README.md, ROADMAP.md, TESTING.md, pyproject.toml, vera/__init__.py
Added TestIndexExprOfFnCall614 and TestNonContiguousCapture615 test cases; bumped package/version to 0.0.139; updated changelog, history, known-issues, README, roadmap, testing metrics and release compare links.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • aallan/vera#598: modifies the same _compile_lifted_closure code path in closure lifting.
  • aallan/vera#594: fixes free-variable collection in vera/wasm/closures.py, related to captured-index handling.
  • aallan/vera#569: changes closure capture handling and related layout serialization.

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title precisely identifies the two closure codegen fixes (#614 and #615) being merged as v0.0.139, directly matching the changeset.
Linked Issues check ✅ Passed The PR fully addresses both #614 and #615 objectives: full return-type inference for FnCall indexing, closure capture-order fixes via type-grouping and prefix-fill, plus comprehensive regression tests.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to #614/#615 fixes: propagating return TypeExpr through codegen, extending inference for FnCall collections, and normalising free-variable capture order.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/closure-lifter-bug-pair-614-615

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.94%. Comparing base (553abfb) to head (b8fba5f).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #625      +/-   ##
==========================================
+ Coverage   90.92%   90.94%   +0.02%     
==========================================
  Files          59       59              
  Lines       22997    23024      +27     
  Branches      259      259              
==========================================
+ Hits        20910    20940      +30     
+ Misses       2080     2077       -3     
  Partials        7        7              
Flag Coverage Δ
javascript 57.36% <ø> (ø)
python 94.76% <100.00%> (+0.02%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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)
HISTORY.md (1)

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

Tagged-release count needs incrementing.

The footer shows "138 tagged releases" but this PR ships v0.0.139. Based on learnings, the tagged-release total in HISTORY.md (and the corresponding count in ROADMAP.md) should be incremented together on each release.

Proposed fix
-Total: **810+ commits, 138 tagged releases, 55 active development days.**
+Total: **810+ commits, 139 tagged releases, 55 active development days.**
🤖 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 `@HISTORY.md` at line 327, The footer in HISTORY.md still reads "138 tagged
releases" but this PR adds v0.0.139; update the tagged-release count string to
"139 tagged releases" in HISTORY.md (replace the "138 tagged releases" token)
and make the same corresponding change in ROADMAP.md so both documents stay in
sync for each release.
🤖 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 `@README.md`:
- Line 184: The README has an inconsistent example count: the summary line (the
sentence containing "Vera is in **active development** at v0.0.139 — ... 34
examples ...") and the later sentence that says "33 example Vera programs"
disagree; run python scripts/check_doc_counts.py to get the canonical examples
count and update both occurrences to that single authoritative number (update
the sentence containing "34 examples" and the sentence that currently reads "33
example Vera programs") so the README is internally consistent.

---

Outside diff comments:
In `@HISTORY.md`:
- Line 327: The footer in HISTORY.md still reads "138 tagged releases" but this
PR adds v0.0.139; update the tagged-release count string to "139 tagged
releases" in HISTORY.md (replace the "138 tagged releases" token) and make the
same corresponding change in ROADMAP.md so both documents stay in sync for each
release.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32102452-72e3-48a5-abb1-4bd2c685f5b6

📥 Commits

Reviewing files that changed from the base of the PR and between 553abfb and a805f94.

⛔ Files ignored due to path filters (6)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • docs/sitemap.xml is excluded by !docs/**
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (16)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen_closures.py
  • vera/__init__.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/registration.py
  • vera/wasm/closures.py
  • vera/wasm/context.py
  • vera/wasm/inference.py
💤 Files with no reviewable changes (1)
  • KNOWN_ISSUES.md

Comment thread README.md
- README.md line 228: "33 example Vera programs" -> 34 (line 184 already says 34; fixes the internal inconsistency).
- HISTORY.md footer line 327: "138 tagged releases, 55 active development days" -> "139 tagged releases, 56 active development days" to match this PR's release.

Skipped: ROADMAP.md doesn't have a corresponding "tagged releases" counter — the previous trim removed it, so there's no stale string to update.

Also fixed the PR description out-of-band so both #614 and #615 auto-close on merge: the original "Closes #614 and #615" form only triggered the link for #614 (GitHub's parser requires a close keyword before each issue number.  Added explicit Closes
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@HISTORY.md`:
- Line 327: The HISTORY.md document has inconsistent "active development days"
values: the header says "55" while the summary line reads "56 active development
days"; pick the correct number and make both occurrences identical by updating
the header value or the summary line (the exact text to locate: the header "55"
near the top and the summary line "Total: **810+ commits, 139 tagged releases,
56 active development days.**") so the release history metadata is consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: faa33e89-64a9-42bf-9f21-4c0d22809352

📥 Commits

Reviewing files that changed from the base of the PR and between a805f94 and 431584d.

📒 Files selected for processing (2)
  • HISTORY.md
  • README.md

Comment thread HISTORY.md
Line 3 said "through Stage 11, across 55 active development days"
while line 327 footer (just bumped to 56 in commit 431584d) said
"56 active development days".

Ground truth: `git log --pretty=format:'%ad' --date=short | sort -u
| wc -l` returns 56 — so 56 is correct.

Line 3 also had a stale "through Stage 11" scope qualifier — Stage
12 has been open since v0.0.138 (PR #612) but that PR's bump didn't
touch line 3.  Fixed both at once: "through Stage 12, across 56
active development days".

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