Skip to content

Close #602 bug class fully (#630 + #632 + #635 + #636)#631

Merged
aallan merged 12 commits into
mainfrom
claude/issue-630-canonicalise-vera-type
May 9, 2026
Merged

Close #602 bug class fully (#630 + #632 + #635 + #636)#631
aallan merged 12 commits into
mainfrom
claude/issue-630-canonicalise-vera-type

Conversation

@aallan

@aallan aallan commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Structural close-out for the #602 bug class — ten reactive fixes across PRs #627 and #629, with the 9th and 10th triggers landing within hours of #630 being filed. Trigger discovery velocity had outpaced local fix throughput; this PR closes the bug class at all four sites it manifests in.

Closes #630.
Closes #632.
Closes #635.
Closes #636.

Tier 1 — centralised canonicalisation. Six overlapping canonicalisation helpers in vera/wasm/inference.py (plus an unaudited seventh in vera/wasm/calls_arrays.py) consolidated into a single _canonical_named_type walker (RefinementType unwrap + alias-chain follow + generic substitution + type_args formatting) plus a _canonical_wasm_type convenience wrapper. All callers migrated to delegate. Deleted _resolve_type_name_to_wasm_canonical — a previously-unnoticed duplicate of _resolve_base_type_name. The apply_fn dispatcher in _infer_fncall_vera_type collapsed from a 75-line nested isinstance ladder to 18 lines; same shape in _infer_apply_fn_return_type.

Tier 2 — loud diagnostic. New error code [E615] "Cannot interpolate value of unknown type". Converted the silent to_string(...) fallthrough at vera/wasm/operators.py:482-486 (the load-bearing amplifier behind every #602-class miscompilation) to record the offending segment on WasmContext._interp_inference_failures. _compile_fn harvests them and emits [E615] for each before the existing [E602] skip — specific, source-located diagnostic instead of invalid WASM at validation.

Net. 6 helpers → 2. 75-line dispatcher × 2 sites → 18 lines × 2. Silent miscompilation → clean compile-time skip with specific E-code. All ten existing #602-class regression tests continue to pass — pure refactor + diagnostic conversion, no behavioural change for valid programs. New test TestE615LoudInterpolationFallthrough630::test_e615_fires_on_adt_in_interpolation pins the new diagnostic surface.

Test plan

Follow-ups

Three items unblocked or refocused by this PR, queued separately:

  • #628 — cross-module _fn_ret_type_exprs propagation. Same bug class, different machinery (modules.py registry harvest); unblocked by the canonical helper as the natural propagation target.
  • #626 Layer 1 — pre-commit gate on [E602] across the conformance suite. The diagnostic infrastructure landed here generalises to the whole channel.
  • Duplicate _type_expr_name / _type_expr_to_slot_name in inference.py — separate concern from canonicalisation drift, surfaced during this PR's audit, deferred to a cleanup PR.

Pragma audit: closed for the canonicalisation cluster (the disproven closure-return-RefinementType pragma was removed in PR #629 and superseded here). Broader audit of remaining # pragma: no cover claims across the WASM codegen is a deferred follow-up — the surviving claims are non-load-bearing defensive fallthroughs in unrelated machinery.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved WASM type canonicalisation and inference so interpolation-related mismatches no longer silently fall through; problematic fragments now cause compile-time skips.
  • Diagnostics

    • Added E615 and E616 diagnostics to surface interpolation and closure/apply_fn inference failures; affected top-level functions are skipped when necessary.
  • Tests

    • New regression tests covering E615/E616, closure failures, per-function isolation and multiple-failure scenarios.
  • Documentation

    • Version bumped to v0.0.142; changelog, README, HISTORY, ROADMAP and test metrics updated.

… fallthrough

Tier 1 — centralised canonicalisation.

Pre-#630 the codebase carried six overlapping canonicalisation
helpers in vera/wasm/inference.py (plus an unaudited seventh in
vera/wasm/calls_arrays.py), each handling a subset of (a)
RefinementType unwrap, (b) alias-chain follow, (c) generic
substitution, (d) type_args formatting.  Site by site, ad-hoc
walks at the apply_fn dispatchers, FnType-return helpers, and
IndexExpr branch independently re-implemented combinations of
these concerns and missed the rest — accumulating the ten
triggers of the i32_pair-into-i64 mismatch bug class addressed
across PRs #627 and #629, with the 9th and 10th landing within
hours of #630 being filed.

Replaced with two helpers in inference.py:

  - `_canonical_named_type(te, alias_map=None) -> NamedType | None`
    — the core walker.  Iteratively unwraps RefinementType layers,
    applies optional alias_map for generic substitution, follows
    NamedType alias chains, returns canonical NamedType or None.

  - `_canonical_wasm_type(te, alias_map=None) -> str | None` —
    convenience wrapper that maps the canonical name to its WASM
    type ("i32_pair" for String/Array, otherwise via
    `_named_type_to_wasm`).

Migrated all callers to delegate:

  - `_format_named_type_canonical` → 3-line delegate
  - `_resolve_i32_pair_ret_te` → 2-line delegate
  - `_fn_type_return_wasm` → single-line delegate
  - `_resolve_generic_fn_return` → builds alias_map, single delegate
  - `_infer_fncall_vera_type` apply_fn dispatcher → 75-line nested
    isinstance ladder collapsed to 18 lines (extract closure return
    TypeExpr + alias_map, call walker once)
  - `_infer_apply_fn_return_type` → same consolidation
  - `_infer_index_element_type_expr` FnCall branch → uses canonical
    NamedType directly to feed `_alias_array_element`
  - `_infer_closure_return_vera_type` (in calls_arrays.py) →
    previously bare-NamedType-only; now handles refinements and
    alias chains uniformly.  Closes a previously-unaudited
    canonicalisation drift site.

Deleted `_resolve_type_name_to_wasm_canonical` — functionally
identical to `_resolve_base_type_name`, an unnoticed duplicate
that had evolved in parallel.  All callers redirected.

Tier 2 — loud diagnostic on the silent amplifier.

The actual silent failure for ten triggers wasn't the inference
miss itself — it was vera/wasm/operators.py:482-486, the else
branch in `_translate_interpolated_string` that wrapped any
unrecognised-type segment with `to_string(...)`.  `to_string`
reads its argument as i64; an i32_pair (String/Array) value then
tripped `expected i64, found i32` at WASM validation.  That
fallthrough turned every canonicalisation gap into invalid
emission rather than a clean compile-time skip.

Added new error code [E615] "Cannot interpolate value of unknown
type — type inference failed".  Converted the silent fallthrough
to record the offending segment on `WasmContext._interp_inference_failures`,
then return None.  `CodeGenerator._compile_fn` harvests the
failures and emits [E615] for each before falling through to the
existing [E602] skip — same loud-skip mechanism that any other
unsupported expression triggers, but now with a specific E-code
pointing at the actual inference gap rather than a generic
"unsupported expressions".

Net effect.

Six canonicalisation helpers → two.  Seventy-five-line apply_fn
dispatcher × two sites → eighteen lines × two.  Silent
miscompilation on inference miss → clean compile-time skip with
specific [E615] diagnostic.  All ten existing #602-class
regression tests in `TestStringInterpolation` continue to pass
— pure refactor + diagnostic conversion, no behavioural change
for valid programs.

New regression test `TestE615LoudInterpolationFallthrough630::test_e615_fires_on_adt_in_interpolation`
pins the new diagnostic surface (interpolating an ADT-typed
value, which the canonicaliser correctly returns a non-recognised
name for, now emits E615 + E602 instead of silently miscompiling).

Pragma audit closed for the canonicalisation cluster.  The
disproved `# pragma: no cover` on closure-return-RefinementType
(PR #629) was already removed during the cluster migration; the
remaining bare `# pragma: no cover` claims in vera/wasm/inference.py
are non-load-bearing defensive fallthroughs in unrelated machinery.
Broader audit (verifying every prose-bearing pragma claim across
the WASM codegen) is queued as a follow-up.

Follow-ups in scope of close-out:

  - #628 (cross-module `_fn_ret_type_exprs` propagation) — same
    bug class, different machinery, unblocked by this PR's
    canonical helper as the natural propagation target.

  - #626 Layer 1 (pre-commit gate on [E602] across the conformance
    suite) — the diagnostic infrastructure landed here generalises
    to the whole channel.

  - Duplicate `_type_expr_name` / `_type_expr_to_slot_name` in
    inference.py — separate concern from canonicalisation,
    surfaced during this audit, deferred to a cleanup PR.

Closes #630.

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

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

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

Centralises TypeExpr canonicalisation into central walkers, converts silent interpolation/apply_fn fallthroughs into recorded failures, harvests and emits per-fragment [E615] / per-closure-arg [E616] diagnostics before existing [E602] skips, propagates closure failures to omit affected functions, adds regression tests, and bumps release to v0.0.142.

Changes

Type Inference Centralisation & Loud Interpolation Errors

Layer / File(s) Summary
Canonicalisation Walkers
vera/wasm/inference.py
Adds substitute_type_vars and InferenceMixin._canonical_named_type to unwrap refinements, follow alias chains with cycle detection, preserve terminal type_args, plus _canonical_wasm_type for deterministic WASM mapping; _format_named_type_canonical delegates to them.
Inference Site Migrations
vera/wasm/inference.py
Migrations update apply_fn, index-element, generic-return and fn-type-return helpers to use the canonicalisation walkers and alias_map substitution.
Array Combinator Inference
vera/wasm/calls_arrays.py
_infer_closure_return_vera_type now canonicalises anonymous-function return types and returns the canonical name for element inference.
WasmContext Failure Accumulators
vera/wasm/context.py
WasmContext adds _interp_inference_failures and _apply_fn_inference_failures lists to record failing interpolation fragments and unhandled apply_fn closure args.
Silent-to-Loud Conversion
vera/wasm/operators.py
_translate_interpolated_string records failing fragments into _interp_inference_failures and returns None when any failure occurs instead of silently coercing via to_string(...).
apply_fn Early Guard
vera/wasm/closures.py
_translate_apply_fn rejects unhandled closure-arg shapes, records them in _apply_fn_inference_failures, and returns None.
E615/E616 Diagnostic Harvesting
vera/codegen/core.py, vera/codegen/functions.py
Adds CodeGenerator._harvest_interp_inference_failures to emit [E615] and [E616] from WasmContext; _compile_fn and closure-lifting invoke the harvester before emitting [E602] and may return None when failures require skipping.
Codegen Alias Substitution
vera/codegen/core.py
_type_expr_to_wasm_type now substitutes parameterised alias parameters with concrete type_args (via substitution helper) before recursing to avoid false "unsupported" classifications.
Closure Lifting Propagation
vera/codegen/closures.py
_lift_pending_closures returns boolean any_failed; _compile_lifted_closure harvests inner-context failures when closure-body translation fails so enclosing top-level functions can be omitted.
Regression Tests
tests/test_codegen.py
Adds TestE615LoudInterpolationFallthrough630 with tests asserting [E615] emission across ADT/interpolation and closures, per-function failure isolation, multiple failing segments handling, E616 for apply_fn shape failures, alias propagation, array-map/refinement-return cases, and function-skipping via [E602].
Version & Documentation
pyproject.toml, vera/__init__.py, CHANGELOG.md, HISTORY.md, KNOWN_ISSUES.md, README.md, ROADMAP.md, TESTING.md
Bumps project version to 0.0.142; updates changelog, history, KNOWN_ISSUES (remove #630; add #633/#634), README and ROADMAP counts and ordering, and TESTING metrics and changelog compare links.

Sequence Diagram(s)

sequenceDiagram
  participant Src as Source AST
  participant Operators as _translate_interpolated_string
  participant Context as WasmContext
  participant CodeGen as CodeGenerator
  participant FnCompile as FunctionCompilationMixin
  Src->>Operators: interpolation fragments
  Operators->>Context: record failures (if any)
  Operators-->>Operators: return None on failure
  FnCompile->>CodeGen: _harvest_interp_inference_failures(ctx)
  CodeGen-->>FnCompile: emit E615/E616 warnings per fragment/arg
  FnCompile->>FnCompile: emit E602 skip and return None when necessary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • aallan/vera#627 — Related fixes to inference and interpolation handling; this PR generalises and consolidates that logic.
  • aallan/vera#629 — Prior refactor whose overlapping canonicalisation helpers are consolidated by this PR.
  • aallan/vera#536 — Related closure-lifting changes that intersect with propagation edits in this PR.

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 Title directly references the four linked issues (#630, #632, #635, #636) and characterises the PR's intent to close the #602 bug class. It is specific and informative about the main change.
Linked Issues check ✅ Passed Code changes comprehensively address all four linked-issue objectives: #630 centralisation (two-helper canonicaliser), #632 E616 diagnostics, #635 parameterised-alias substitution, #636 closure-body failure propagation.
Out of Scope Changes check ✅ Passed All changes are scoped to the four linked issues. Documentation updates (CHANGELOG, HISTORY, ROADMAP, KNOWN_ISSUES, README, TESTING) correctly reflect version increment and test-count adjustments. No unrelated refactoring or scope creep.
Docstring Coverage ✅ Passed Docstring coverage is 97.44% 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/issue-630-canonicalise-vera-type

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

@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.94624% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.97%. Comparing base (f71c6c4) to head (a7b0334).

Files with missing lines Patch % Lines
vera/wasm/inference.py 74.31% 28 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #631      +/-   ##
==========================================
+ Coverage   90.94%   90.97%   +0.03%     
==========================================
  Files          59       59              
  Lines       23063    23134      +71     
  Branches      259      259              
==========================================
+ Hits        20974    21047      +73     
+ Misses       2082     2080       -2     
  Partials        7        7              
Flag Coverage Δ
javascript 57.36% <ø> (ø)
python 94.77% <84.94%> (+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.

@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 `@HISTORY.md`:
- Line 289: The HISTORY roll-up footer total was not incremented after adding
the v0.0.142 entry; update the cumulative tagged-release total in the document
footer (where the previous total reads 141) to reflect the new release by
changing it to 142 so the footer matches the newly added v0.0.142 entry.

In `@tests/test_codegen.py`:
- Around line 8663-8680: The test currently only checks presence of E615 and
E602 but not their order; update the assertions in tests/test_codegen.py after
building warnings/e615/e602 to verify that the first E615 diagnostic appears
before the first E602 diagnostic by locating their indices in the warnings list
(use the existing warnings variable) and asserting index_of_e615 < index_of_e602
so the intended failure path (E615 then E602) is enforced.

In `@vera/wasm/inference.py`:
- Around line 486-561: The function _canonical_named_type currently only keeps
the first NamedType's type_args (outer_type_args) which loses type arguments
from alias bodies; change it to record the most-recently-seen NamedType's
type_args as you walk aliases so the resolved alias-body args are preserved.
Concretely, in _canonical_named_type update the logic that captures type_args
(and the variable name if you prefer) so it assigns te.type_args on each
iteration when te is a NamedType (including after following an alias from
self._type_aliases or after alias_map substitution) and then return
ast.NamedType(name=te.name, type_args=that_latest_args) instead of always using
the first seen args. Ensure behavior for non-NamedType termination (returning
None) remains unchanged.
🪄 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: e5da70eb-9b5a-4e7a-bb30-d317602d2119

📥 Commits

Reviewing files that changed from the base of the PR and between f71c6c4 and ab8e88c.

⛔ 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 (15)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/codegen/functions.py
  • vera/errors.py
  • vera/wasm/calls_arrays.py
  • vera/wasm/context.py
  • vera/wasm/inference.py
  • vera/wasm/operators.py
💤 Files with no reviewable changes (1)
  • KNOWN_ISSUES.md

Comment thread HISTORY.md Outdated
Comment thread tests/test_codegen.py
Comment thread vera/wasm/inference.py Outdated
Stage 12 had drifted to multi-sentence paragraphs per row (the
v0.0.142 entry I just added inherited and worsened the pattern).
HISTORY format is single very short sentences — see Stage 1
(\"Type checker. Decidable type checking, slot reference
resolution, effect tracking, vera typecheck command, 91 new
tests.\") and Stage 2 (\"Closures — closure conversion,
call_indirect.\") for the canonical shape.

Rewrote all five Stage 12 rows. Detail belongs in CHANGELOG.md
(per-release prose) and in the issue tracker; HISTORY is the
chronological index.

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

🤖 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 289: Update the HISTORY.md entry that currently says "collapsed to one
`_canonical_named_type` walker" to reflect the accurate change: state that six
helpers were consolidated into two helpers, naming both `_canonical_named_type`
and `_canonical_wasm_type` (describe `_canonical_named_type` as the walker and
`_canonical_wasm_type` as the convenience wrapper) so the summary matches the PR
objectives.
🪄 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: 5b3c0813-64b2-41a4-a127-5b1d59b76e70

📥 Commits

Reviewing files that changed from the base of the PR and between ab8e88c and f591013.

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

Comment thread HISTORY.md Outdated
…ents + CodeRabbit

Four review agents (code-reviewer, pr-test-analyzer, comment-analyzer,
silent-failure-hunter) plus three CodeRabbit comments produced a
batch of cross-cutting findings.  This commit addresses them all
in one pass to avoid the iterative round-trip cost the bug class
itself was filed to eliminate.

**Critical fixes:**

1. **Closure body E615 harvest gap (silent-failure-hunter C1).**
   `_compile_lifted_closure` constructed its own WasmContext but
   never harvested `_interp_inference_failures` — closure bodies
   with E615-triggering interpolation segments were silently
   dropped, producing call_indirect→missing-table-entry WASM
   validation traps with no source-located diagnostic.  Extracted
   the harvest into `CodeGenerator._harvest_interp_inference_failures`
   on the codegen base and called it from both functions.py and
   closures.py.  Removed the now-disproved `# pragma: no cover —
   defensive` claim on closures.py's body-instrs-None path.  New
   regression test `test_e615_in_closure_body_emits_diagnostic`.

2. **`_canonical_named_type` type_args from terminal NamedType
   (CodeRabbit #3 + code-reviewer I1).**  The walker's
   `outer_type_args` capture rule (always read from first NamedType)
   lost type_args when alias_map substitution bound a generic
   param to a parameterised type.  Empirically reachable today via
   `type Mapper<A, B> = fn(A -> B); Mapper<Int, Array<Int>>` etc.
   Fix: drop the "outermost" rule, return type_args from the
   terminal NamedType reached.  All 21 existing #602-class
   regression tests still pass.  New regression test
   `test_canonical_named_type_terminal_args_propagation`.

3. **HISTORY.md footer (CodeRabbit #1).**  141 tagged releases →
   142 to match the v0.0.142 row added in PR #631.

**UX fix:**

4. **Multiple E615 per function (silent-failure-hunter H2).**
   Pre-this-fix `_translate_interpolated_string` returned None on
   the first failing segment, requiring N round-trips for N bad
   segments in one interpolation.  Now collects every failing
   segment via `had_failure` flag, then bails at the end.  New
   regression test `test_multiple_e615_in_one_interpolation`.

**Doc-quality fixes (comment-analyzer):**

5. **Trigger-count drift** in `operators.py:491-494` —
   "every canonicalisation gap (#602 and ten subsequent triggers)"
   implied 11; rewrote to "the ten triggers of the #602 bug class
   accumulated across PRs #627 + #629".

6. **`_format_named_type_canonical` docstring drift** — claim
   "matches the pre-#630 fallback shape" was empirically wrong
   (pre-#630 resolved through aliases, post-#630 fallback doesn't).
   Rewrote to acknowledge the deliberate behavioural change.

7. **"Future closure-arg shapes plug in here without further
   dispatch ladder"** — overstatement.  Adding a `FnCall`
   returning a closure still needs an `elif`; what's saved is
   canonicalisation logic, not dispatch logic.  `IfExpr` between
   closures has no single `return_type` field — example was
   misleading.  Rewrote in both `_infer_fncall_vera_type` and
   `_infer_apply_fn_return_type`.

8. **`_canonical_wasm_type` docstring** — added explicit `Unit →
   None` documentation; clarified that `None` (no WASM type) and
   `"i64"` (unreachable-NamedType default) are distinct return
   shapes that callers must not conflate.

**Tests added (besides the regressions tied to fixes):**

9. **`test_per_function_isolation_of_failures_list`** — pins
   that a clean function and a dirty function in the same source
   produce independent `_interp_inference_failures` lists.

10. **`test_e615_fires_on_result_in_interpolation`** — adjacent
    ADT shape (Result vs Option) so a future Option-handling
    broadening doesn't accidentally regress the parallel Result
    path.

11. **`test_array_map_refinement_returning_closure`** — pins the
    previously-unaudited `_infer_closure_return_vera_type` path
    in `calls_arrays.py`, which the #630 migration broadened to
    handle refinements.

**Test enhancements:**

12. **E615 ordering before matching E602** in the existing test
    (CodeRabbit #2) — pinned via `warnings.index(e615[0])` <
    `warnings.index(matching_e602)`.  Filtered to the E602 that
    mentions the offending function so prelude-skip E602s don't
    confound the assertion.

13. **E615 source-location attached** in the existing test —
    softened to "line > 0" (any location) because SlotRef-inside-
    InterpolatedString spans are unreliable today (#634, separate
    follow-up).  Tightening to "line points at the segment" is
    the natural acceptance test for that follow-up.

**Skipped with reasons:**

- **Cycle-guard regression on `_resolve_base_type_name`** —
  test-analyzer C1 / code-reviewer S4.  Verified pre-existing
  and dead-code-safe today (cycles crash an upstream resolver
  first).  Filed as #633.

- **`_canonical_wasm_type`'s `i64` default for unhandled
  apply_fn / call_indirect shapes** — silent-failure-hunter H1.
  Bigger refactor parallel to Tier 2 but for the WASM-side
  dispatch.  Filed as #632.

- **SlotRef-inside-InterpolatedString span fidelity** —
  surfaced when writing the E615 source-location assertion.
  Diagnostic-quality concern, separate from #630's
  canonicalisation scope.  Filed as #634.

- **`_canonical_named_type` worked-example docstring**
  (comment-analyzer I6) — superseded by fix #2 above (semantics
  changed; docstring rewritten with concrete examples).

- **Per-context isolation prose-vs-test** (comment-analyzer I4)
  — addressed via fix #9 (test added).

- **`_resolve_i32_pair_ret_te` 30-line upper docstring** —
  comment-analyzer style nit.  The prose is load-bearing
  context (#628 cross-module narrative); trimming would lose
  history without obvious gain.  Defer.

**Follow-ups:** #632 (apply_fn / call_indirect E616), #633
(_resolve_base_type_name cycle guard), #634 (interpolation
SlotRef source spans).  Added to KNOWN_ISSUES.md alongside #628.

Counts: 3,792 tests (3,778 passed, 14 skipped) — six new tests
under `TestE615LoudInterpolationFallthrough630`.  CHANGELOG +
KNOWN_ISSUES + ROADMAP + TESTING.md + HISTORY footer all
synced.  mypy clean.

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

aallan commented May 8, 2026

Copy link
Copy Markdown
Owner Author

Review-pass synthesis

Ran four review agents in parallel against this PR (code-reviewer, pr-test-analyzer, comment-analyzer, silent-failure-hunter), plus three CodeRabbit findings landed in the same window. Total: ~25 distinct findings, with significant overlap on three high-confidence items. All addressed in commit 0afd4e2.

Critical (fixed)

Finding Sources Fix
Closure-body E615 harvest gap_compile_lifted_closure constructed its own WasmContext but never harvested _interp_inference_failures. Closure bodies with E615-triggering interpolation segments were silently dropped. silent-failure-hunter C1 Extracted harvest into CodeGenerator._harvest_interp_inference_failures; called from both functions.py and closures.py. Removed the disproved # pragma: no cover — defensive claim. New test test_e615_in_closure_body_emits_diagnostic.
_canonical_named_type type_args from terminal NamedType — walker's outer_type_args capture rule lost type_args when alias_map substitution bound a generic param to a parameterised type. Empirically reachable today (type Mapper<A,B> = fn(A->B); Mapper<Int, Array<Int>>). CodeRabbit #3 + code-reviewer I1 (independent convergence) Drop the "outermost" rule, return te.type_args from the terminal NamedType. New test test_canonical_named_type_terminal_args_propagation.
HISTORY.md footer count141 tagged releases after adding the v0.0.142 row CodeRabbit #1 Bumped to 142.

UX (fixed)

Finding Sources Fix
One [E615] per failing segment — pre-fix returned None on first failure, requiring N round-trips for N bad segments. silent-failure-hunter H2 had_failure flag in _translate_interpolated_string; bail at end. New test test_multiple_e615_in_one_interpolation.
E615 ordering before matching E602 in the existing ADT test CodeRabbit #2 warnings.index(e615[0]) < warnings.index(main_e602). Filtered to the E602 mentioning main so prelude-skip E602s don't confound.

Doc-quality (fixed)

Finding Sources Fix
Trigger-count drift in operators.py:491-494 ("ten subsequent triggers" implied 11) comment-analyzer C1 Rewrote to "the ten triggers of the #602 bug class accumulated across PRs #627 + #629".
_format_named_type_canonical docstring claim "matches the pre-#630 fallback shape" empirically wrong comment-analyzer C2 Rewrote to acknowledge the deliberate behavioural change.
"Future closure-arg shapes plug in here without further dispatch ladder" overstatement — still needs elif per shape; what's saved is canonicalisation logic. IfExpr example misleading. comment-analyzer C3 Rewrote in both apply_fn dispatchers.
_canonical_wasm_type docstring — added Unit → None doc; clarified None vs "i64" semantic distinction comment-analyzer I5 Done.

Tests added (beyond the ones tied to fixes above)

  • test_per_function_isolation_of_failures_list — pins per-context independence (comment-analyzer I4)
  • test_e615_fires_on_result_in_interpolation — adjacent ADT shape (test-analyzer C3)
  • test_array_map_refinement_returning_closure — pins the previously-unaudited _infer_closure_return_vera_type path (test-analyzer C2)

Skipped with reasons

Finding Source Reason
Cycle-guard regression on _resolve_base_type_name test-analyzer C1, silent-failure-hunter M1, code-reviewer S4 Verified pre-existing and dead-code-safe today (cycles crash an upstream resolver first). Filed as #633.
_canonical_wasm_type "i64" default for unhandled apply_fn shapes silent-failure-hunter H1 Parallel to Tier 2 but for WASM-side dispatch. Bigger refactor; out of scope for #630's interpolation focus. Filed as #632.
SlotRef-in-InterpolatedString source-span fidelity Surfaced when writing E615 location assertion Diagnostic-quality concern in transformer/parser layer, not codegen. The new test's line > 0 assertion is softened to accommodate; tightening is acceptance for the follow-up. Filed as #634.
_canonical_named_type worked-example docstring comment-analyzer I6 Superseded by the type_args fix above (semantics changed; docstring rewritten with concrete examples — IntList → Array<Int>, Str → String, T → Array<Int> via alias_map).
Per-context isolation prose-vs-test comment-analyzer I4 Addressed via the new test_per_function_isolation_of_failures_list.
_resolve_i32_pair_ret_te upper-docstring length comment-analyzer style nit Prose is load-bearing #628 narrative; trimming would lose history. Defer.

Follow-ups filed

All three added to KNOWN_ISSUES.md alongside #628.

Validation

  • pytest tests/: 3,778 passed, 14 skipped (+6 new tests in TestE615LoudInterpolationFallthrough630)
  • mypy vera/: clean (0 issues, 58 source files)
  • python scripts/check_doc_counts.py: consistent
  • All pre-commit hooks pass (conformance suite, examples, browser parity, license check, site assets, etc.)

Commits on this PR

★ Insight ─────────────────────────────────────

Ready for re-review.

Per CodeRabbit on PR #631 — the row said "Six canonicalisation
helpers collapsed to one _canonical_named_type walker" but the
PR actually consolidates them into TWO helpers
(_canonical_named_type as the walker + _canonical_wasm_type as
the WASM-type convenience wrapper).  Matches the CHANGELOG entry
which already names both.

Kept to a single sentence per Stage 1/2 HISTORY style.

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: 5

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

Inline comments:
In `@tests/test_codegen.py`:
- Around line 8863-8900: The test currently uses a plain alias `type IntList =
Array<Int>` which doesn't exercise the generic-substitution bug; change it to a
generic alias and return an instantiated generic so the terminal NamedType
carries type_args. Specifically, replace `type IntList = Array<Int>;` with
something like `type Id<T> = T;` and update the `make_list` signature from
`private fn make_list(`@Unit` -> `@IntList`)` to `private fn make_list(`@Unit` ->
`@Id`<Array<Int>>)` (and keep the body `[10, 20, 30]` and the `main` caller as-is)
so the test `test_canonical_named_type_terminal_args_propagation` forces
propagation from the terminal NamedType through generic substitution.
- Around line 8757-8804: The test test_per_function_isolation_of_failures_list
currently defines clean before dirty so a leak in _interp_inference_failures
wouldn't be detected; reorder the snippet in the local variable source so the
failing function (dirty) appears before a clean function, or add a second clean
(e.g., clean_after_dirty) after dirty and assert that its name appears in
result.exports; update the assertions that check result.exports (referencing the
source string, the function names "clean"/"dirty"/new name, and the test's
result variable) so the test actually verifies that a clean function compiled
after dirty is unaffected by leaked failures.

In `@vera/wasm/inference.py`:
- Around line 557-560: The alias_map substitution loop in inference.py can spin
forever when alias_map maps a type variable name to a NamedType of the same name
(self-reference); update the substitution branch that checks "if alias_map is
not None and te.name in alias_map" to detect self-referential mappings (e.g.,
alias_map[te.name] is the same NamedType/name as te or would not change te) and
in that case do not "continue" the loop — instead break out and return a safe
fallback or leave te unchanged; reference the variables/identifiers alias_map,
te, and the NamedType/NamedType.name used in this block so the patch replaces
the unconditional continue with a guard that prevents infinite cycles.
- Around line 604-609: The helper that maps canonical named types to WASM types
needs to treat Future<T> as transparent: after getting canonical =
self._canonical_named_type(te, alias_map) detect if canonical represents Future
and has a single type argument, then use that inner type (not the Future
wrapper) when deciding the WASM mapping (apply the existing String/Array ->
"i32_pair" rule and otherwise call self._named_type_to_wasm on the inner type
name); keep the fallback to "i64" when canonical is None and preserve existing
calls to _named_type_to_wasm for non-Future types.
- Around line 567-572: The loop that follows type aliases (variables te and
alias using self._type_aliases) ignores parameter bindings in
self._type_alias_params, so parameterised aliases like Box<T>=Array<T> lose
their concrete arguments; fix it by looking up self._type_alias_params for the
alias name when alias is a NamedType/RefinementType and applying a substitution
of the alias's type parameters with the actual type arguments from te before
assigning te = alias (or replacing te with the alias body instantiated with
those arguments). Ensure you perform this parameter substitution (using the same
substitution utility or mechanism used elsewhere in inference) before continuing
the alias-following loop.
🪄 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: 9bdc10fc-154f-4dda-9ab5-83fbd2142f69

📥 Commits

Reviewing files that changed from the base of the PR and between f591013 and e27eda5.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/wasm/inference.py
  • vera/wasm/operators.py

Comment thread tests/test_codegen.py
Comment thread tests/test_codegen.py
Comment thread vera/wasm/inference.py
Comment thread vera/wasm/inference.py
Comment thread vera/wasm/inference.py
**Fixed:**

1. **Walker cycle guard for alias_map self-reference (Finding 3).**
   `_canonical_named_type` previously had separate cycle guards
   for alias_map substitution (none) and `_type_aliases` chain
   following (`seen` set, alias-follow only).  An alias_map
   self-reference (`{T: NamedType("T")}`) would loop forever.
   Unified: single `seen` set covers both paths, populated before
   either substitution or alias-follow.

2. **Future<T> transparency in `_canonical_wasm_type` (Finding 4).**
   `_named_type_to_wasm("Future")` returned `"i32"` (default), so
   a closure return of `Future<String>` at apply_fn / FnType-alias
   positions canonicalised to a wrong WASM type.  Parallel to
   `_slot_name_to_wasm_type`'s existing strip-and-recurse handling
   for Future<T>.  Fix: when canonical name is "Future" with one
   type_arg, recurse into the inner type.

3. **Parameterised-alias substitution (Finding 5).**  Following
   `type Box<T> = Array<T>` with concrete `Box<Int>` previously
   left `T` unsubstituted in the alias body, leaking type
   variables into the canonical NamedType.  Added a
   `_substitute_type_vars` helper (handles NamedType +
   RefinementType; FnType bodies fall through to existing
   None-return) and wired it into the alias-follow path.

4. **Test 1 (Finding 1) updated.**  Switched
   `test_canonical_named_type_terminal_args_propagation` from
   `type IntList = Array<Int>` (non-parameterised — doesn't
   exercise substitution) to `type Box<T> = Array<T>` (which
   does).  CodeRabbit suggested `type Id<T> = T`, but that hits
   a parallel parameterised-alias gap in
   `_type_expr_to_wasm_type` (compilability check, codegen/core.py)
   that's outside #630's scope — filed as #635.  Box<T>=Array<T>
   exercises the same substitution path and runs end-to-end.

5. **Test 2 (Finding 2) reordered + clean_after_dirty.**
   `test_per_function_isolation_of_failures_list` previously had
   `clean` before `dirty`, so a forward leak from dirty would
   never reach a clean function and the test would silently pass.
   Added `clean_after` after `dirty`; both `clean_before` and
   `clean_after` must remain in exports, and exactly one E615
   must fire.

**Follow-up filed:**

- **#635** — `_type_expr_to_wasm_type` parameterised-alias gap.
  Walker fix in `_canonical_named_type` closes the inference half;
  this issue tracks the parallel gap in the compilability check.
  Added to KNOWN_ISSUES.md alongside #628, #632, #633, #634.

**Validation:**

- pytest: 3,778 passed, 14 skipped (no new tests; existing tests
  strengthened in place per Findings 1 and 2)
- mypy: clean (0 issues, 58 source files)
- check_doc_counts: consistent

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

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

Inline comments:
In `@tests/test_codegen.py`:
- Around line 8884-8889: The test currently allows len(e615) >= 2 which can mask
duplicates; update the assertion in tests/test_codegen.py to require the exact
count by changing the assertion to assert len(e615) == 2 and adjust the failure
message to reflect "Expected exactly 2 [E615] diagnostics" (keep the existing
diagnostic list interpolation using {len(e615)}: {e615}). Locate the block that
computes e615 = [d for d in warnings if d.error_code == "E615"] and replace the
>= 2 check with == 2 so the test pins the expected behavior.
🪄 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: 2ad76685-eb03-420a-a8f0-324f2214ecef

📥 Commits

Reviewing files that changed from the base of the PR and between e27eda5 and bb906f5.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • TESTING.md
  • tests/test_codegen.py
  • vera/wasm/inference.py

Comment thread tests/test_codegen.py
Per CodeRabbit on PR #631 — the assertion `len(e615) >= 2` would
silently pass with 3, 4, or N duplicates from a regression where
the harvest accidentally walks the failures list more than once
or the per-segment recording emits multiple entries per failure.
Tightened to `== 2` (the program has exactly two failing
interpolation segments — Option<Int> and Result<Int, String>) so
duplicates surface.

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

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

Inline comments:
In `@tests/test_codegen.py`:
- Around line 8745-8755: The test currently only asserts an E615 closure-body
warning; update it to also assert that a matching E602 was emitted for the
"main" symbol and that "main" is not exportable by checking result.exports.
Specifically, after computing e615 and warnings, filter warnings for error_code
"E602" (e.g., e602 = [d for d in warnings if d.error_code == "E602"]) and assert
e602 is non-empty and assert "main" not in result.exports to ensure the closure
failure skips the main export.
🪄 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: 4da3b078-04ae-4f4c-816e-bed29d84faf7

📥 Commits

Reviewing files that changed from the base of the PR and between bb906f5 and 402fba0.

📒 Files selected for processing (2)
  • TESTING.md
  • tests/test_codegen.py

Comment thread tests/test_codegen.py
aallan and others added 2 commits May 8, 2026 22:49
… dropped" surface

Per CodeRabbit on PR #631 — finding partially valid, requires nuance.

CodeRabbit suggested asserting `"main" not in result.exports` plus
non-empty E602 in the closure-body E615 test.  Verified empirically
that today:

- E615 fires for the closure body's bad interpolation ✓
- E602 fires (3 times) but for prelude combinators (option_map,
  option_and_then, result_map), NOT for `main`
- `main` IS in result.exports — closure-body failure doesn't
  propagate to drop the enclosing fn
- Module fails at WASM validation/instantiation: call_indirect to
  the dropped closure_id

So CodeRabbit's `"main" not in result.exports` assertion would FAIL
today.  Reason: the closure-compile worklist runs after main's
WAT is already generated, so dropping the closure leaves main's
call_indirect dangling but doesn't drop main itself.  This is a
real gap separate from PR #631's diagnostic-emit work.

Disposition: pin the current surface explicitly with the inverse
assertion (`"main" in result.exports` — documents the bug) plus a
docstring note explaining that the diagnostic is correctly emitted
but the enclosing fn isn't yet dropped.  Filed as #636 with the
acceptance criterion: when #636 lands, the assertion flips and
this test's docstring needs updating.

Added #636 to KNOWN_ISSUES.md alongside #628, #632, #633, #634, #635.

Co-Authored-By: Claude <noreply@anthropic.invalid>
PR #631 originally closed the inference-side half of the
canonicalisation drift (#630) but generated three follow-up
issues for parallel sites in adjacent code paths.  Per Option B
discussion: rather than landing those as a series of small PRs
post-merge, fold them inline so the PR actually delivers the
"structural close-out" its framing claims.

**#635 — parameterised-alias substitution in compilability check.**
`_type_expr_to_wasm_type` in `vera/codegen/core.py` previously
recursed on alias bodies without binding the alias's own type
params, classifying `type Id<T> = T; @id<Array<Int>>` as
"unsupported".  Extracted `substitute_type_vars` from
`InferenceMixin._substitute_type_vars` to a module-level free
function in `vera/wasm/inference.py` so both call sites use the
same substitution logic.  The walker fix from earlier in this
PR + the compilability fix here together close the
parameterised-alias gap end-to-end.

**#632 — apply_fn / call_indirect E616 diagnostic.**
`_translate_apply_fn` in `vera/wasm/closures.py` now checks the
closure_arg shape early.  If it's neither `SlotRef` (into a
`FnType` alias) nor `AnonFn`, the failing arg is recorded on
`ctx._apply_fn_inference_failures` and translation bails.
Added `[E616] "Cannot infer closure return type for
call_indirect"` to the error registry; harvested in the codegen
base alongside `[E615]`.  Pre-fix: `apply_fn(make_mapper(()), 7)`
where `make_mapper` returns a closure produced a WASM-validation
trap with no source-located diagnostic.  Post-fix: source-located
[E616] + clean function skip via [E602].

**#636 — closure-body fail drops enclosing fn.**
`_lift_pending_closures` in `vera/codegen/closures.py` now
returns `bool` indicating whether any closure body failed.
`_compile_fn` in `vera/codegen/functions.py` checks the flag and
drops the enclosing top-level fn with a specific [E602] noting
the closure-failure cause.  Pre-fix: closure body's [E615]
emitted but main remained in `result.exports`, producing a
`call_indirect`-to-missing-entry trap at WASM
validation/instantiation.  Post-fix: cause + effect diagnostics
both visible, no invalid module emission.

**Tests:**

- `test_e616_apply_fn_unhandled_closure_arg_shape` (#632)
- `test_e635_parameterised_alias_compilability` (#635)
- `test_e615_in_closure_body_emits_diagnostic` flipped from
  asserting `"main" in exports` (documenting the bug) to
  `"main" not in exports` (asserting the fix), with a docstring
  explaining the lifecycle.
- All 11 tests in `TestE615LoudInterpolationFallthrough630`
  pass; 3,780 total tests pass (+2 vs prior commit).

**Docs:**

- Closed #632, #635, #636 on GitHub with explanatory comments
- Removed the three from `KNOWN_ISSUES.md`
- Updated `CHANGELOG.md` to reflect the four-site close-out
- Updated `HISTORY.md` v0.0.142 row similarly

**Why fold rather than follow-up:**

The user's pushback after seeing 6 follow-up issues filed during
PR #631's reviews ("So fixing this has raised 6 more bugs. I'm
not convinced we're making progress here") prompted a re-scope
discussion.  Option B was to expand this PR to actually close
the cluster rather than ship reactive fixes one at a time.
That's what this commit does.

The PR is now genuinely "structural close-out for the #602 bug
class" rather than "Tier 1 of N".  Three parallel sites
(interpolation, apply_fn, compilability) + the closure-body
propagation gap all close in one commit, all pinned by tests.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan changed the title Close #630: centralise canonical Vera-type-name resolution; loud E615 fallthrough Close #602 bug class fully (#630 + #632 + #635 + #636) May 8, 2026
@aallan

aallan commented May 8, 2026

Copy link
Copy Markdown
Owner Author

Re-scoped per Option B discussion

After landing the canonicalisation walker (Tier 1) + the loud E615 diagnostic (Tier 2), the review pass surfaced six follow-up issues. The user pushed back: "So fixing this has raised 6 more bugs. I'm not convinced we're making progress here."

Fair critique. The PR was framed as "structural close-out" but the bug class lived in four parallel sites and we'd only closed one. Three of the six follow-ups (#632, #635, #636) were the same bug class manifesting in adjacent code paths. Folding them into this PR rather than chasing them in a series of small follow-ups was the only way to make the framing accurate.

Commit 936f848 does that.

Sites now closed

Site Code path Failure mode Fix
Interpolation (#630) _translate_interpolated_stringto_string(...) over i32_pair expected i64, found i32 at WASM validation Centralised _canonical_named_type walker + [E615] loud diagnostic
apply_fn / call_indirect (#632) _translate_apply_fn_infer_apply_fn_return_type defaulting to "i64" call_indirect sig mismatch at WASM validation Shape check before call_indirect emit + [E616] diagnostic
Compilability check (#635) _type_expr_to_wasm_type recursing on alias body without param substitution [E605] unsupported return type for type Id<T> = T; Same substitute_type_vars helper now used at both inference and compilability sites
Closure-body propagation (#636) _lift_pending_closures silently dropping failed closures call_indirect to missing function-table entry at WASM instantiation Propagate failure flag to _compile_fn; drop enclosing fn cleanly via [E602]

Every silent miscompilation in the bug class is now either structurally impossible (centralised walker) or surfaces as a source-located diagnostic + clean function skip.

Follow-ups remaining (genuinely smaller polish, not core bug class)

These are scope-appropriate as separate PRs — they don't extend the #602 bug class, they're orthogonal cleanups.

Test coverage

11 tests in TestE615LoudInterpolationFallthrough630 pin all four sites:

3,780 total tests pass; mypy clean; all pre-commit hooks pass (conformance, examples, browser parity, etc.).

What this means for "are we making progress"

Before this PR After this PR Direction
7 issues in the cluster 4 issues (the smaller polish items) down
10+ silent-miscompilation triggers reachable via valid Vera 0 in the bug class down
Failure mode Silent invalid WASM Source-located diagnostic + clean skip
Centralised canonicalisation infra None Walker + free substitute_type_vars

The headline bug count is now genuinely down (not up by 5), and the closures landed are ones that would have been four separate PRs. The user's pushback was the right call — without folding these in, we'd have shipped a misleadingly-framed PR and chased the actual close-out across four iterations.

★ Insight ─────────────────────────────────────

  • The "fold-or-follow-up" decision is structural, not stylistic. Three of the six follow-ups (Cross-cutting: _canonical_wasm_type i64 default leaves apply_fn / call_indirect path source-decoupled #632, Parameterised-alias substitution missing in _type_expr_to_wasm_type (compilability check) #635, Closure-body compile failure doesn't drop enclosing top-level function #636) were the same bug class in different code paths. Filing them as separate issues then merging Close #602 bug class fully (#630 + #632 + #635 + #636) #631 as-is would have meant four PRs to close one bug class — each with its own review cycle, version bump, CHANGELOG entry. Folding them in cost ~150 lines and one extra commit. The trade-off is: more time on a single PR vs. more cycles on follow-up PRs. When the follow-ups share machinery (the same substitute_type_vars helper, the same _harvest_* infrastructure, the same diagnostic-emit pattern), folding wins on total cost.
  • Inverted assertions caught their own fix. The closure-body test was written in the previous review pass to assert "main" in result.exports (documenting the bug). When Closure-body compile failure doesn't drop enclosing top-level function #636 landed, the test failed automatically — the inverted assertion was a regression sentinel that fired the moment the fix landed. That's exactly what those assertions are for: they make "fixed the bug" observable in CI without needing a separate test-update step. Pattern worth using more.
  • The user's "are we making progress" check is a useful framing tool. It forced a re-examination of whether incremental discovery (each review pass exposing more parallel sites) was actually converging. Once the question was asked, the answer was clear: no, the bug class was bigger than we'd scoped, and Option B (expand to close it properly) was the right call. The discipline change going forward: when filing follow-ups during a structural-close-out PR, ask "is this a separate concern or the same bug class in a parallel site?" before filing.
    ─────────────────────────────────────────────────

Ready for re-review.

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

1 similar comment
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@aallan

aallan commented May 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@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 `@vera/codegen/functions.py`:
- Around line 262-279: The partial append of lifted closures causes a mismatch
when _lift_pending_closures fails: snapshot or defer module-state changes so
earlier successful siblings aren't left in _closure_fns_wat, _closure_table, and
_fn_source_map if any closure in the worklist fails. Modify
_lift_pending_closures to either (a) take a local snapshot/copy of
_closure_fns_wat, _closure_table and _fn_source_map at entry and restore them on
failure, or (b) accumulate new closures in local temporary lists and only extend
the real _closure_fns_wat/_closure_table/_fn_source_map after all worklist
closures succeed; ensure closure_id stored in each closure struct and any
call_indirect indices remain consistent with the final table ordering.

In `@vera/wasm/calls_arrays.py`:
- Around line 588-591: The current branch in the closure return-type handling
returns only canonical.name (losing inner T for Future<T>), so change the logic
in the closure handling (where closure_arg is an ast.AnonFn and
_canonical_named_type is called) to detect a Future wrapper and return the inner
element layout/type instead of the outer "Future" name; specifically, when
canonical exists and represents the Future alias/param, unwrap
closure_arg.return_type to extract the inner type (T), run the same canonical
resolution on that inner type (or otherwise derive its layout/name) and return
that result so callers like _translate_array_map and _translate_array_mapi see
the inner T layout rather than "Future".

In `@vera/wasm/inference.py`:
- Around line 683-685: The branch in _canonical_named_type that returns "i64"
when canonical is None incorrectly treats higher-order function types as i64;
instead preserve closure-pointer returns by returning "i32" for FnType or
aliases that resolve to FnType so downstream code (_infer_apply_fn_return_type
and _fn_type_return_wasm) emits the closure pointer ABI and correct
call_indirect signature; update the branch in _canonical_named_type to return
"i32" when te is an ast.FnType (or when alias_map resolution would yield an
FnType), leaving other None cases unchanged.
🪄 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: 23d70b09-7585-4c88-baf9-a822228fda9c

📥 Commits

Reviewing files that changed from the base of the PR and between f71c6c4 and 936f848.

⛔ 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 (18)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/errors.py
  • vera/wasm/calls_arrays.py
  • vera/wasm/closures.py
  • vera/wasm/context.py
  • vera/wasm/inference.py
  • vera/wasm/operators.py

Comment thread vera/codegen/functions.py
Comment thread vera/wasm/calls_arrays.py
Comment thread vera/wasm/inference.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: 2

♻️ Duplicate comments (2)
vera/wasm/calls_arrays.py (1)

588-591: ⚠️ Potential issue | 🟠 Major

Keep Future<T> transparent in closure return-element inference.

Line 591 returns only canonical.name, so a closure returning Future<String> or Future<Array<Int>> is reduced to "Future". The array element-layout helpers then lose the inner representation and can reject the call or lower it with the wrong ABI.

🤖 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/wasm/calls_arrays.py` around lines 588 - 591, The code in
calls_arrays.py returns only canonical.name for closure_arg of type ast.AnonFn,
which collapses Future<T> to "Future" and loses the inner element type; update
the logic in the branch handling ast.AnonFn (and the call to
_canonical_named_type) to detect when the canonical represents a generic Future
and instead return the inner type's canonical name (e.g., unwrap Future<T> to
the canonical name of T) so the array element-layout helpers see the actual
inner representation; use closure_arg.return_type's type parameter(s) or the
canonical's type arguments to extract and return the inner type rather than just
canonical.name.
vera/wasm/inference.py (1)

683-685: ⚠️ Potential issue | 🟠 Major

Do not collapse higher-order returns to i64.

When _canonical_named_type() bottoms out at ast.FnType or an alias body resolving to FnType, Line 685 still forces "i64". That gives closure-returning apply_fn / FnType sites the wrong ABI, and it also prevents the new failure path from surfacing an inference miss upstream.

🤖 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/wasm/inference.py` around lines 683 - 685, The code forces a fallback
"i64" when canonical = self._canonical_named_type(te, alias_map) returns None,
which hides FnType/closure returns and masks inference failures; change that
branch so it does not coerce to "i64" — instead return None (or propagate the
original te) so callers can detect unresolved/closure types and surface an
inference miss; update the logic around _canonical_named_type, canonical, te and
alias_map to propagate None rather than hard-coding "i64".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_codegen.py`:
- Around line 8862-8888: Update the test_e615_fires_on_result_in_interpolation
test to also assert the loud-skip behavior: after confirming E615, filter
result.diagnostics for error_code "E602" (or severity "error") and assert an
E602 is present for the `main` function, and assert that "main" is not in
result.exports; reference the existing `result` and `"main"` symbol and add the
two assertions immediately after the current E615 check to lock the expected
skip path.

In `@vera/codegen/closures.py`:
- Around line 71-76: The stored closure_id written into the closure environment
can diverge from the final function-table index when some lifts fail in a batch;
inspect whether a single top-level function can experience mixed lift outcomes
and then fix either by reserving table slots for every attempted lift (e.g.,
append placeholders to _closure_table from _lift_pending_closures for failed
lifts so indices remain stable) or by remapping the stored closure_id into the
compacted table index before finalising the closure environment (update the
environment value written by the code that sets closure_id after all successful
lifts are known). Locate uses of closure_id, lifted_wat,
_compile_lifted_closure, _lift_pending_closures and the _closure_table and
ensure call_indirect consumers see the final compacted table indices rather than
pre-compaction ids.

---

Duplicate comments:
In `@vera/wasm/calls_arrays.py`:
- Around line 588-591: The code in calls_arrays.py returns only canonical.name
for closure_arg of type ast.AnonFn, which collapses Future<T> to "Future" and
loses the inner element type; update the logic in the branch handling ast.AnonFn
(and the call to _canonical_named_type) to detect when the canonical represents
a generic Future and instead return the inner type's canonical name (e.g.,
unwrap Future<T> to the canonical name of T) so the array element-layout helpers
see the actual inner representation; use closure_arg.return_type's type
parameter(s) or the canonical's type arguments to extract and return the inner
type rather than just canonical.name.

In `@vera/wasm/inference.py`:
- Around line 683-685: The code forces a fallback "i64" when canonical =
self._canonical_named_type(te, alias_map) returns None, which hides
FnType/closure returns and masks inference failures; change that branch so it
does not coerce to "i64" — instead return None (or propagate the original te) so
callers can detect unresolved/closure types and surface an inference miss;
update the logic around _canonical_named_type, canonical, te and alias_map to
propagate None rather than hard-coding "i64".
🪄 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: bd2c7976-b779-4e33-a24a-034c4aea1d74

📥 Commits

Reviewing files that changed from the base of the PR and between f71c6c4 and 936f848.

⛔ 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 (18)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/errors.py
  • vera/wasm/calls_arrays.py
  • vera/wasm/closures.py
  • vera/wasm/context.py
  • vera/wasm/inference.py
  • vera/wasm/operators.py

Comment thread tests/test_codegen.py
Comment thread vera/codegen/closures.py
… 1 duplicate

Six findings; verified each.  Fixed 4, skipped the assembly.py:340
finding as duplicate (already addressed in d33a778's
$gc_worklist_end refactor).

Finding A (test_codegen.py:15931 — stale class docstring) —
FIXED.  ``TestHostHandleReclamation573``'s docstring still said
"Map only" with Set/Decimal "in their own follow-ups", but the
class now contains all three.  Rewrote to enumerate the three
failure modes per type (chain reclaims transients, value
correct after pressure, JSON-only / HTML-only wrap-table
inclusion).

Finding B (test_codegen.py:16220 — missing HTML-only test) —
FIXED.  Added ``test_html_only_module_includes_wrap_table`` as
a sibling of ``test_json_only_module_includes_wrap_table``.
Asserts ``host_decref_handle`` import, ``$register_wrapper``
helper, and the export are all present in WAT for an
html_parse-using program.  Note: html_parse transitively pulls
in the prelude's ``html_attr`` (which calls map_get) so
``_map_ops_used`` ends up set anyway in practice — but the
``_needs_wrap_table`` flip on ``_html_ops_used`` is the load-
bearing fix if that prelude transitivity ever changes (e.g.
if a future codegen DCE eliminates unused prelude functions).

Finding C (api.py:1729 — host_decref_handle map_ops_used gating)
— FIXED.  Real bug: ``kind == 1 and result.map_ops_used`` skips
the pop for JSON-only / HTML-only programs (where
``map_ops_used`` is False but JObject / HtmlElement allocations
still create Map wrappers — ``_map_store`` exists because
its creation is gated on ``map_ops_used or json_ops_used or
html_ops_used``).  Drop the ``map_ops_used`` part of the gate;
the runtime invariant "kind == 1 ⟹ _map_store exists" holds
because the wrap-table only emits kind=1 when the Map store
is created.  ``kind == 2`` and ``kind == 3`` keep their gates
because their stores have a single creation condition.

Finding D (api.py:3114 outside-of-diff — ExecuteResult missing
host_store_sizes in IO.exit early returns) — FIXED.  The two
early-return ``ExecuteResult(...)`` sites in the IO.exit error
paths omitted ``host_store_sizes``, leaving the field empty
even when host stores existed.  Threaded
``{k: len(v) for k, v in _host_store_refs.items()}`` through
both sites so the field is always populated, mirroring the
normal-completion path.

Skipped:
* assembly.py:340 (duplicate) — reviewer suggests using
  ``gc_wraptable_base`` as the worklist end.  This is
  mathematically equivalent to the ``$gc_worklist_end`` global
  introduced in d33a778's round-2 fix
  (``gc_worklist_end = gc_stack_limit + worklist_size`` and
  ``gc_wraptable_base = data_end + gc_stack_size +
  worklist_size``; they refer to the same address when wrap-
  table is enabled and the new constant is independent of
  whether wrap-table is enabled).  Already fixed.

Validation: 3,715 pytest, 99 browser, mypy clean, 82
conformance, 33 examples, all sync scripts pass.

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: 3

♻️ Duplicate comments (3)
vera/wasm/inference.py (1)

683-685: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat higher-order returns as closure pointers, not the "i64" fallback.

If te is an ast.FnType — or a named alias that eventually resolves to one — _canonical_named_type() returns None, so this branch currently reports "i64". That gives closure-returning functions the wrong ABI and can register an invalid call_indirect signature instead of the expected i32 closure pointer.

Proposed fix
     def _canonical_wasm_type(
         self,
         te: ast.TypeExpr,
         alias_map: dict[str, ast.TypeExpr] | None = None,
     ) -> str | None:
+        if isinstance(te, ast.FnType):
+            return "i32"
+
         canonical = self._canonical_named_type(te, alias_map)
         if canonical is None:
+            probe = te
+            seen: set[str] = set()
+            while isinstance(probe, ast.NamedType) and probe.name not in seen:
+                seen.add(probe.name)
+                alias = self._type_aliases.get(probe.name)
+                if isinstance(alias, ast.FnType):
+                    return "i32"
+                if not isinstance(alias, ast.NamedType):
+                    break
+                probe = alias
             return "i64"
🤖 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/wasm/inference.py` around lines 683 - 685, The fallback branch that
returns "i64" when canonical = self._canonical_named_type(te, alias_map) is None
incorrectly treats higher-order (function) types as i64; update the logic in the
function containing this branch to detect when te is an ast.FnType (or is a
named alias that resolves to an ast.FnType via alias_map) and return "i32" for
closure pointers instead of "i64"; use the existing _canonical_named_type helper
or a small resolver to follow aliases and check for ast.FnType so
closure-returning functions get the i32 ABI and correct call_indirect
signatures.
vera/wasm/calls_arrays.py (1)

588-591: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep Future<T> transparent when inferring the mapped element type.

Returning only canonical.name drops the inner layout for Future<T>. A closure returning Future<String> or Future<Array<Int>> is then treated as plain "Future", so array_map / array_mapi no longer see the pair layout of the inner T.

Proposed fix
         if isinstance(closure_arg, ast.AnonFn):
             canonical = self._canonical_named_type(closure_arg.return_type)
             if canonical is not None:
-                return canonical.name
+                if (
+                    canonical.name == "Future"
+                    and canonical.type_args
+                    and len(canonical.type_args) == 1
+                ):
+                    inner = self._canonical_named_type(canonical.type_args[0])
+                    if inner is not None:
+                        return self._format_named_type(inner)
+                return self._format_named_type(canonical)
🤖 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/wasm/calls_arrays.py` around lines 588 - 591, The current code returns
only the canonical.name for closure_arg return types, which drops inner layout
details for generic types like Future<T>. To fix this, when the closure_arg is
an ast.AnonFn and its return_type is a generic type like Future<T>, ensure that
the inner type T is preserved by returning the full type representation
including the inner layout, not just the outer canonical.name. Modify the
related block that returns canonical.name inside the method handling closure_arg
to reconstruct or return a transparent type string or structure preserving
Future's inner type layout.
tests/test_codegen.py (1)

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

Strengthen the Result-path regression by pinning loud-skip behaviour.

This test currently passes on [E615] presence alone; it can false-pass if main still exports or if the matching [E602] path regresses.

Proposed minimal patch
         e615 = [d for d in warnings if d.error_code == "E615"]
         assert e615, (
             f"Expected [E615] for Result<Int, String> interpolation; "
             f"got warnings: {warnings}"
         )
+        e602_main = [
+            d for d in warnings
+            if d.error_code == "E602" and "main" in d.description
+        ]
+        assert e602_main, (
+            f"Expected an [E602] for `main` alongside E615; "
+            f"warnings were: {warnings}"
+        )
+        assert "main" not in result.exports, (
+            f"main should be skipped on Result interpolation fallthrough; "
+            f"exports: {result.exports}"
+        )

As per coding guidelines, tests/**/*.py: “Review for correctness and coverage gaps.”

🤖 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.py` around lines 8883 - 8888, The test currently only
checks that an E615 warning exists, which can false-pass; update the assertion
logic in the test (where variable e615 is computed from warnings) to also assert
that no E602 warnings are present for the same run (e.g., compute e602 = [d for
d in warnings if d.error_code == "E602"] and assert not e602) and keep the
existing assert e615 to ensure E615 is present; this pins the loud-skip
behaviour and prevents regressions where the E602 path or an exported main cause
a misleading pass.
🤖 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: Update the test count in the README sentence that starts "Vera is in
**active development** at v0.0.142 — 810+ commits, 142 releases, 3,772 tests,
96% code coverage..." to match TESTING.md and ROADMAP.md by changing "3,772
tests" to "3,794 tests" so the documented test counts are consistent and will
pass scripts/check_doc_counts.py.

In `@tests/test_codegen.py`:
- Around line 9009-9025: The test currently only checks that E616 and E602
appear but not their order; update the assertions around the warnings list (the
variable warnings and the filtered e616) to assert that the first E616 warning
occurs before any E602 warning (e.g., find indices of warnings with error_code
"E616" and "E602" and assert index(E616) < index(E602)), and keep the existing
checks that "apply_fn" or "closure" is mentioned in e616[0].description and that
"main" is absent from result.exports.

In `@vera/codegen/closures.py`:
- Around line 71-79: The code checks lifted_wat twice: first `if lifted_wat is
None: ... continue` then immediately `if lifted_wat is not None:` which is
redundant; remove the second conditional and unconditionally append the results
after the continue path so that when execution reaches the append it is
guaranteed lifted_wat is not None. Modify the block that follows
`_compile_lifted_closure` to drop the `if lifted_wat is not None:` guard and
directly call `self._closure_fns_wat.append(lifted_wat)` and
`self._closure_table.append(f"$anon_{closure_id}")`, leaving the existing
`any_failed` handling and `continue` logic intact.

---

Duplicate comments:
In `@tests/test_codegen.py`:
- Around line 8883-8888: The test currently only checks that an E615 warning
exists, which can false-pass; update the assertion logic in the test (where
variable e615 is computed from warnings) to also assert that no E602 warnings
are present for the same run (e.g., compute e602 = [d for d in warnings if
d.error_code == "E602"] and assert not e602) and keep the existing assert e615
to ensure E615 is present; this pins the loud-skip behaviour and prevents
regressions where the E602 path or an exported main cause a misleading pass.

In `@vera/wasm/calls_arrays.py`:
- Around line 588-591: The current code returns only the canonical.name for
closure_arg return types, which drops inner layout details for generic types
like Future<T>. To fix this, when the closure_arg is an ast.AnonFn and its
return_type is a generic type like Future<T>, ensure that the inner type T is
preserved by returning the full type representation including the inner layout,
not just the outer canonical.name. Modify the related block that returns
canonical.name inside the method handling closure_arg to reconstruct or return a
transparent type string or structure preserving Future's inner type layout.

In `@vera/wasm/inference.py`:
- Around line 683-685: The fallback branch that returns "i64" when canonical =
self._canonical_named_type(te, alias_map) is None incorrectly treats
higher-order (function) types as i64; update the logic in the function
containing this branch to detect when te is an ast.FnType (or is a named alias
that resolves to an ast.FnType via alias_map) and return "i32" for closure
pointers instead of "i64"; use the existing _canonical_named_type helper or a
small resolver to follow aliases and check for ast.FnType so closure-returning
functions get the i32 ABI and correct call_indirect signatures.
🪄 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: 8a34f9cf-e6c7-4f4e-93ac-b1b91406c6bb

📥 Commits

Reviewing files that changed from the base of the PR and between f71c6c4 and 936f848.

⛔ 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 (18)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/errors.py
  • vera/wasm/calls_arrays.py
  • vera/wasm/closures.py
  • vera/wasm/context.py
  • vera/wasm/inference.py
  • vera/wasm/operators.py

Comment thread README.md Outdated
Comment thread tests/test_codegen.py
Comment thread vera/codegen/closures.py Outdated
aallan and others added 3 commits May 9, 2026 11:25
…orted)

Per CodeRabbit on PR #631 — Result test only asserted E615
fires, not the full loud-skip surface (E602 for main + main
dropped from exports).  Parallel to the ADT test's assertions.
Without these, a regression that emits E615 but fails to drop
the function via E602 would silently pass this test.

Three other findings in the same review pass triaged:

- closure_id <-> table_index integrity (calls_arrays.py): Already
  addressed by the snapshot/commit-on-success fix in b29de12.
  Within a single fn's worklist, mixed outcomes can't reach the
  module — either all closures succeed and append in closure_id
  order, or any fail and the whole fn is dropped via #636.
  Plus _next_closure_id rolls back on failure for cross-fn
  integrity.  Skip.

- Future<T> in calls_arrays.py:_infer_closure_return_vera_type
  (duplicate from prior round): Type checker rejects Async
  closures into array_map upstream.  Not reachable.  Skip.

- Return None instead of "i64" default (duplicate from prior
  round): The reachable case (FnType return) was fixed via
  _reaches_fn_type in b29de12.  Remaining cases (refinement-
  over-non-NamedType, alias cycles) aren't empirically
  reachable.  Returning None would conflate Unit with
  inference-miss; needs a separate sentinel which is a
  contract change beyond this PR's scope.  Skip.

Co-Authored-By: Claude <noreply@anthropic.invalid>
CI failed across all 5 test matrix entries (Python 3.11/3.12/3.13
on Linux + macOS) with:

  SyntaxError: unterminated string literal (detected at line 143)
    f"$closure_sig_{len(self._closure_sigs)
    ^

The b29de12 commit introduced a multi-line f-string that's only
valid syntax from Python 3.12+; PEP 701 lifted the restriction
on newlines and arbitrary expressions inside f-strings.  Local
pre-commit runs on Python 3.14 didn't catch it because the
syntax IS valid in newer versions.

Fix: pre-compute the index outside the f-string so the whole
expression fits on one line.  Trivially 3.11-compatible.

The CI matrix testing 3.11 is the load-bearing check here —
without it, this would have shipped to a release.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Two fixes from CodeRabbit's third "♻️ Duplicate" round:

- README.md test count drift: said 3,772 (PR-pre-#631 figure)
  vs canonical 3,796 in TESTING.md / ROADMAP.md.
  scripts/check_doc_counts.py audits TESTING.md + ROADMAP.md but
  not README, so this slipped past local validation.  Worth
  flagging for the doc-counts script's coverage as a follow-up.

- test_e616_apply_fn_unhandled_closure_arg_shape: E616 ordering
  assertion added.  Test now asserts E616 precedes the matching
  E602 (main) in warnings stream — parallel to the E615 ordering
  assertion in test_e615_fires_on_adt_in_interpolation.

Four findings skipped with reason:

- closures.py "redundant if lifted_wat is not None": already
  removed during the snapshot/commit-on-success refactor in
  b29de12; current code only has the None check + continue form.

- Result test "assert no E602": contradicts the post-#636 design
  where E615 → E602 IS the loud-skip path.  Asserting `not e602`
  would actively break the test.  CodeRabbit's "duplicate" here
  is recapitulating their original finding from before #636
  landed.

- Future<T> in calls_arrays.py:_infer_closure_return_vera_type:
  not reachable; type checker rejects Async-effect closures into
  array_map upstream.

- inference.py "i64 default → None": the FnType case (the
  empirically reachable one) was fixed via _reaches_fn_type in
  b29de12.  Returning None for other walker-bail cases would
  conflate Unit (existing meaning) with inference-miss; needs
  a new sentinel which is a contract change beyond this PR's
  scope.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan merged commit 4866318 into main May 9, 2026
20 checks passed
@aallan aallan deleted the claude/issue-630-canonicalise-vera-type branch May 9, 2026 10:57
aallan added a commit that referenced this pull request May 10, 2026
Three findings on PR #646's docs additions:

1. CLAUDE.md and TESTING.md examples used `tmp_path.replace(os.sep,
   "/")` to get POSIX form.  Works on str, but BROKEN on
   pathlib.Path (Path.replace is the rename method — different
   signature, would silently move the file).  Variable name
   `tmp_path` evokes pytest's Path-typed fixture.  Switched to
   `Path(tmp_path).as_posix()` which works for both str and Path.
   The actual test code at tests/test_codegen.py:7760 / 7787
   still uses str.replace because the variables there are str
   (from `tempfile.NamedTemporaryFile().name` and `os.path.join`)
   — that's correct in context, no change needed.

2. TESTING.md Overview row line 18 said "6 combinations
   (Ubuntu/macOS)" — pre-Windows.  CI Pipeline section line 484
   said the same.  Both updated to "9 combinations
   (Ubuntu/macOS/Windows)".

Same shape as the README test-count drift caught in PR #631 —
matrix-related docs across multiple files don't auto-sync.
Worth flagging the doc-counts script's coverage gap as a
follow-up if it recurs.

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