Skip to content

v0.0.141 — RefinementType in i32_pair return inference (#602 family)#629

Merged
aallan merged 8 commits intomainfrom
claude/refinementtype-i32pair-followup
May 8, 2026
Merged

v0.0.141 — RefinementType in i32_pair return inference (#602 family)#629
aallan merged 8 commits intomainfrom
claude/refinementtype-i32pair-followup

Conversation

@aallan
Copy link
Copy Markdown
Owner

@aallan aallan commented May 8, 2026

Summary

Follow-up to PR #627 (v0.0.140, closed #602). Third trigger of the same bug class — surfaced during PR #627's CodeRabbit review (duplicate-comment escalation flagging that the fix needed to handle RefinementType in addition to NamedType), but the merge happened before the fix landed in #627 itself.

Bumps version v0.0.140 → v0.0.141.

The bug shape

Three triggers in sequence, all producing the same expected i64, found i32 at WASM validation:

Trigger Status
Plain @String return (s_arr-shape) Closed by #602 / v0.0.140
type Str = String; alias return Closed by #602 / v0.0.140 (alias resolution)
Inline @{ @String | predicate } return This PR
IndexExpr-of-FnCall returning refinement @{ @Array<T> | ... } This PR (parallel path in _infer_index_element_type_expr)

Root cause

_register_fn stores the literal AST in _fn_ret_type_exprs. v0.0.140's fix used isinstance(ret_te, ast.NamedType) which silently fell through for RefinementType. Same gap in _infer_index_element_type_expr's FnCall branch (the path #614 added).

Fix

Extracted a helper _resolve_i32_pair_ret_te(ret_te) that handles both NamedType (with alias resolution via the existing _resolve_base_type_name) and RefinementType (unwrap to base_type, then resolve). Applied to:

  • _infer_fncall_vera_type non-generic branch
  • _infer_fncall_vera_type generic-mono branch
  • _infer_index_element_type_expr FnCall branch (RefinementType-unwrap inlined since it returns a different shape)

Verified reproducer

private fn make(@Unit -> @{ @String | string_length(@String.0) > 0 })
  requires(true) ensures(true) effects(pure)
{ "hello" }

public fn main(@Unit -> @Unit)
  requires(true) ensures(true) effects(<IO>)
{ IO.print("\(make(()))\n") }
  • Pre-fix: Invalid input WebAssembly code at offset 911: type mismatch: expected i64, found i32
  • Post-fix: prints hello

Regression tests

Two new tests in TestStringInterpolation:

  • test_inline_refinement_string_in_interpolation — the minimum reproducer above.
  • test_inline_refinement_array_in_indexed_interpolation@{ @Array<Int> | ... } indexed via f()[i] inside interpolation; exercises the parallel _infer_index_element_type_expr fix path.

Test count 3,777 → 3,779 (+2).

Test plan

  • All four shapes verified to print hello / 20 post-fix
  • pytest tests/ -q -n auto — 3,765 passed, 14 skipped
  • mypy vera/ — clean
  • python scripts/check_doc_counts.py — consistent
  • python scripts/check_version_sync.py — v0.0.141 across 6 files
  • python scripts/check_site_assets.py — up to date
  • python scripts/check_limitations_sync.py — consistent

Summary by CodeRabbit

  • Bug Fixes

    • Fixed return-type inference for inline refinement returns, resolving incorrect behaviour that affected string interpolation and indexed/array-access expressions.
  • Tests

    • Added end-to-end regression tests covering inline-refinement interpolation, nested refinements and indexed-access scenarios, replacing prior alias-only cases.
  • Documentation

    • Updated changelog, history, README, roadmap, testing status and known-issues to reflect the new release and tracked issue.
  • Chore

    • Bumped project/package version to v0.0.141.

Third trigger of the same bug class as #602 (closed in v0.0.140 by
PR #627) and the type-alias case fixed in the same PR.  This one
slipped through because PR #627 was merged before the
RefinementType-handling commit landed.

Verified reproducer:
  private fn make(@Unit -> @{ @string | string_length(@String.0) > 0 })
    requires(true) ensures(true) effects(pure)
  { "hello" }

  public fn main(@Unit -> @Unit) requires(true) ensures(true) effects(<IO>)
  { IO.print("\(make(()))\n") }

Pre-fix: `expected i64, found i32` at WASM validation (offset 911,
identical to original #602).  Post-fix: prints "hello".

Root cause:
- `_register_fn` stores the literal AST in `_fn_ret_type_exprs`.
  An inline-refinement return type produces a `RefinementType`
  AST node, not a `NamedType`.
- v0.0.140's fix was `if isinstance(ret_te, ast.NamedType): ...`
  which silently fell through to None for `RefinementType`.
- `_translate_interpolated_string` then wrapped the FnCall with
  `to_string(...)`, which reads i64 — but the FnCall pushed
  i32_pair.  Same #602 trap.

Same gap also lived in `_infer_index_element_type_expr`'s FnCall
branch (the path #614 added) for refinement-`Array<T>` returns
indexed via `f()[i]` in interpolation.

Fix:
- Extracted the i32_pair return-type resolution into a helper
  `_resolve_i32_pair_ret_te` that handles both `NamedType` (with
  alias resolution via `_resolve_base_type_name`) and
  `RefinementType` (unwrap to `base_type`, then resolve).
- Applied to both i32_pair branches in `_infer_fncall_vera_type`
  (non-generic + generic-mono) since they share the shape.
- Mirrored the RefinementType-unwrap in
  `_infer_index_element_type_expr`'s FnCall branch for the
  parallel inference path.

Two new regression tests in TestStringInterpolation:
- test_inline_refinement_string_in_interpolation
- test_inline_refinement_array_in_indexed_interpolation

All four shapes verified post-fix:
  - Plain `@String` return (#602 baseline)
  - `type Str = String` alias (v0.0.140)
  - Inline `@{ @string | predicate }` (this PR)
  - IndexExpr-of-FnCall returning refinement-Array (this PR)

Test count 3,777 → 3,779 (+2).

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

coderabbitai Bot commented May 8, 2026

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Release v0.0.141 fixes i32_pair return-type inference for function-call and indexed-access paths when functions declare inline or nested refinement return types. A new helper _resolve_i32_pair_ret_te canonicalises NamedType and nested RefinementType return-type shapes; index-element inference and FnType→WASM mapping now unwrap refinements; five regression interpolation tests were added and package/docs bumped to v0.0.141.

Changes

Inline Refinement Return Type Fix

Layer / File(s) Summary
Return Type Canonisation Helper
vera/wasm/inference.py
New _resolve_i32_pair_ret_te canonicalises i32_pair-compatible return TypeExprs by unwrapping RefinementType layers and resolving aliases to a base NamedType.
NamedType Canonical Formatter
vera/wasm/inference.py
New _format_named_type_canonical resolves alias chains for NamedType while preserving type_args, used in apply_fn/FnType return handling.
Function Call Return Type Inference (apply_fn / aliased FnType)
vera/wasm/inference.py
_infer_fncall_vera_type apply_fn path now iteratively unwraps RefinementType at FnType return positions and uses canonical formatting to avoid alias-name mismatches.
Function Call Return Type Inference (generic)
vera/wasm/inference.py
Generic-call branch delegates i32_pair resolution to the new helper using the mangled function’s registered return TypeExpr.
Function Call Return Type Inference (non-generic)
vera/wasm/inference.py
Non-generic-call branch also delegates i32_pair handling to the helper using the per-function registered return TypeExpr.
Index Element Type Inference
vera/wasm/inference.py
When an indexed collection originates from a FnCall, the code unwraps RefinementType to base NamedType before alias/element resolution, enabling correct Array<T>/String element inference.
FnType → WASM mapping
vera/wasm/inference.py
_resolve_generic_fn_return and _fn_type_return_wasm now unwrap nested RefinementType at FnType return positions before mapping canonical NamedType to WASM return forms (including i32_pair).
Regression Tests
tests/test_codegen.py
Five end-to-end interpolation tests added: direct String refinement, nested refinements, apply_fn alias returning nested refinement, refinement over a String type alias, and Array refinement indexed inside interpolation; older String type-alias interpolation test and companion structural assertion removed.
Version and Documentation
vera/__init__.py, pyproject.toml, CHANGELOG.md, HISTORY.md, README.md, ROADMAP.md, TESTING.md, KNOWN_ISSUES.md
Version bumped to 0.0.141; changelog and history entries added/updated; compare links advanced; project/test/release metrics updated; KNOWN_ISSUES documents the canonicalisation drift bug (#630).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #630: The introduced resolver and per-site unwrapping implement the central fixes described in issue #630 (i32_pair→i64 mismatch from refinement-wrapped returns).

Possibly related PRs

  • aallan/vera#454: Related earlier i32_pair handling fixes in wasm inference; similar canonicalisation work.
  • aallan/vera#627: Extends earlier i32_pair return-type fixes in vera/wasm/inference.py; closely related.
  • aallan/vera#625: Prior changes to FnCall/indexed element-type inference in the same module; related to indexed-access fixes.

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 title precisely describes the primary fix: handling RefinementType in i32_pair return inference, with reference to the root issue family (#602).
Linked Issues check ✅ Passed All coding objectives from #602 are met: return-type inference now recognizes String-shaped returns in NamedType, RefinementType (including nested), aliases, and refinement-over-alias; correct resolution applied in interpolation and index-element inference; regression tests cover all reachable shapes.
Out of Scope Changes check ✅ Passed All changes directly address #602 objectives: helper extraction, nested-refinement unwrapping loops, regression test coverage, and version/documentation bumps are in-scope artifacts of the fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.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/refinementtype-i32pair-followup

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

❌ Patch coverage is 64.58333% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.94%. Comparing base (13f8d58) to head (32d5dcd).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
vera/wasm/inference.py 63.04% 17 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #629   +/-   ##
=======================================
  Coverage   90.93%   90.94%           
=======================================
  Files          59       59           
  Lines       23033    23063   +30     
  Branches      259      259           
=======================================
+ Hits        20946    20974   +28     
- Misses       2080     2082    +2     
  Partials        7        7           
Flag Coverage Δ
javascript 57.36% <ø> (ø)
python 94.74% <64.58%> (-0.01%) ⬇️

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

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

Inline comments:
In `@CHANGELOG.md`:
- Line 19: Update the verification summary header text so the numeric count
matches the enumerated items: replace "All four shapes verified post-fix" with
"All five shapes verified post-fix" (or remove the explicit count) to match the
listed items `string_concat`, `f()` direct in interpolation, type alias over
String, inline refinement over String, and IndexExpr-of-FnCall returning
refinement-Array in the same line.
🪄 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: 958bd6f2-79eb-48d1-b6bb-fcd290f91be9

📥 Commits

Reviewing files that changed from the base of the PR and between 13f8d58 and e772cd5.

⛔ 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 (9)
  • CHANGELOG.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_codegen.py
  • vera/__init__.py
  • vera/wasm/inference.py

Comment thread CHANGELOG.md Outdated
…test

Four agents reviewed PR #629; the silent-failure-hunter agent
identified a CRITICAL gap that the other three missed: nested
RefinementType (`@{ @{ @string | p1 } | p2 }`) is reachable per
the grammar AND reproduces the original #602 trap.  Empirically
verified — `vera check` passes, `vera run` traps at WASM
validation with the same `expected i64, found i32` signature.

This PR's initial fix used `if isinstance(ret_te, ast.RefinementType):
base = ret_te.base_type` — only one level of unwrap.  Nested
refinements fell through to None, downstream wraps with
`to_string(...)`, same #602 trap.  Fifth trigger of the same bug
class.

Fix: replaced the one-level unwrap with a `while isinstance(ret_te,
ast.RefinementType): ret_te = ret_te.base_type` loop in both call
sites (`_resolve_i32_pair_ret_te` and the parallel IndexExpr-of-
FnCall path).  Handles arbitrary nesting depth.

Also addressed:
- comment-analyzer: `core.py:333` ambiguity → `codegen/core.py:333`
- comment-analyzer + silent-failure-hunter: added #626 reference
  to the helper's docstring tying its None-return paths to the
  broader silent-skip tracking issue
- code-reviewer + comment-analyzer: justified the helper-vs-inline
  choice in the IndexExpr branch (helper returns `str`, the branch
  needs the `NamedType` AST node for `_alias_array_element`)

Two new regression tests in TestStringInterpolation:
- test_nested_refinement_string_in_interpolation
- test_refinement_over_type_alias_in_interpolation (sibling case
  that already worked but was previously untested — pins the
  alias-recursion behaviour against future regressions)

Skipped (with reason in the PR thread):
- Cross-module gap reproducing the same trap — that's #628,
  explicitly deferred when filed during PR #627's review
- Structural-WAT test for RefinementType — duplicates the
  existing PR #627 structural test which would catch any
  inference regression; nice-to-have but redundant
- Logging at None-return sites — broader change that lives in
  #626's Layer 1/2/3 work
- Parameterised cross-product test enumerating all shapes ×
  all targets — over-scoped for this PR

Test count 3,779 → 3,781 (+2).

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

aallan commented May 8, 2026

Self-review via /pr-review-toolkit:review-pr — findings and actions

Ran the four-agent review toolkit (code-reviewer, pr-test-analyzer, comment-analyzer, silent-failure-hunter) on the initial commit e772cd5. One CRITICAL finding required a code change; three IMPORTANT findings were addressed; four suggestions were skipped with reasons. All addressed in commit 416ca9c.

Critical issue fixed

Finding Flagged by Action
Nested RefinementType is reachable AND reproduces the original #602 trap. Initial fix used a single-level if isinstance(ret_te, ast.RefinementType): base = ret_te.base_type — a @{ @{ @String | p1 } | p2 } return falls through to None, downstream wraps with to_string(...), same expected i64, found i32 at WASM validation. Empirically verified before fix. Fifth trigger of the same bug class. silent-failure-hunter (CRITICAL); code-reviewer + pr-test-analyzer rated it as a low-priority edge Replaced one-level unwrap with while isinstance(ret_te, ast.RefinementType): ret_te = ret_te.base_type in both call sites (_resolve_i32_pair_ret_te and the parallel IndexExpr-of-FnCall path). Handles arbitrary nesting depth. New regression test test_nested_refinement_string_in_interpolation.

Important issues addressed

# Finding Flagged by Action
1 core.py:333 reference is ambiguous (two files match) comment-analyzer Updated to codegen/core.py:333.
2 Helper docstring doesn't reference #626 (the broader silent-skip tracking pattern) — every return None here is an instance comment-analyzer + silent-failure-hunter Added explicit #626 callout in the helper's docstring; flagged that None-return paths include the unfixed cross-module case (#628).
3 IndexExpr branch's inline RefinementType-unwrap doesn't justify why it didn't reuse _resolve_i32_pair_ret_te code-reviewer + comment-analyzer Added one-line explanation: helper returns str, this branch needs the unwrapped NamedType AST node to feed _alias_array_element (which inspects .type_args).

Suggestions skipped

# Suggestion Skip reason
1 Cross-module gap reproduces the same trap (silent-failure-hunter HIGH) That's #628, explicitly deferred when filed during PR #627's review. In KNOWN_ISSUES.md.
2 Structural-WAT test for RefinementType (code-reviewer + pr-test-analyzer) PR #627's test_no_to_string_wrap_on_string_returning_fncall_602 already catches inference regressions across the helper. Adding a parallel structural test for refinement would be redundant.
3 Defensive logging at None-return sites (silent-failure-hunter HIGH) Lives in #626's Layer 1/2/3 work — adding partial logging here without the broader plumbing would create another inconsistent state.
4 Parameterised cross-product test enumerating all shapes × targets × cross-module (silent-failure-hunter LOW) Over-scoped for this PR. The structural-fix-shape (cross-product enumeration) is the right way to close the bug class wholesale, but it deserves its own scoped issue rather than being bundled here.

Strengths confirmed

  • pr-test-analyzer: tests now cover all three reachable helper sub-cases (NamedType, NamedType-via-alias, RefinementType-with-NamedType-base) plus nested-refinement and refinement-over-alias.
  • comment-analyzer: all issue/PR cross-references valid; helper docstring explains the bug-class history at a single discoverable place.
  • code-reviewer: release artefacts complete (CHANGELOG / HISTORY / version sync / docs / counts).

Bonus regression test added

While verifying the silent-failure-hunter finding, also tested the refinement-over-type-alias shape (type Str = String; fn make(-> @{ @Str | p })). That case already worked because _resolve_base_type_name is recursive — but it had no test. Added test_refinement_over_type_alias_in_interpolation to pin the working behaviour against future regressions in the alias-resolution path.

Test count 3,779 → 3,781 (+2). The bug class now has five concrete tests covering: plain (#602), type alias (#627), inline refinement, nested refinement, refinement-over-alias, plus the IndexExpr-of-FnCall variant for the Array case.

…n line

Header said 'All five shapes now verified' but enumerated six items
(the IndexExpr variant got added during the review-pass commit,
pushing the count off).  Restructured: '5 return-type shapes ...
plus the parallel IndexExpr-of-FnCall path' separates the
return-type cases (5) from the qualitatively different element-
type-of-IndexExpr inference (1), so the count matches and the
two distinct fix paths are visible.

Co-Authored-By: Claude <noreply@anthropic.invalid>
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: 4

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

Inline comments:
In `@CHANGELOG.md`:
- Line 17: Update the v0.0.141 CHANGELOG note to correctly state four regression
tests were added (inline refinement String, indexed interpolation, nested
refinement, refinement-over-alias) instead of three; edit the sentence that
references the i32_pair helper `_resolve_i32_pair_ret_te`, the i32_pair
branches, and the IndexExpr inference path so it mentions "four new regression
tests in TestStringInterpolation" to match the actual tests added (including the
Array-indexed shape).

In `@TESTING.md`:
- Line 9: Update the stale counts in TESTING.md's Validation Scripts table to
match the authoritative output of python scripts/check_doc_counts.py: change
check_examples.py from 33 to 34 examples and check_spec_examples.py from 148 to
164 spec blocks; run python scripts/check_doc_counts.py to verify the numbers
and commit the updated table entries in the same PR so the overview totals and
per-script counts are consistent.

In `@tests/test_codegen.py`:
- Around line 8319-8346: Update the
test_inline_refinement_array_in_indexed_interpolation case to include a nested
refinement return type so the IndexExpr-of-FnCall path is exercised with nested
unwrapping; specifically, change the `make` function's return refinement from
`@{ `@Array`<Int> | array_length(`@Array`<Int>.0) > 0 }` to a nested form like `@{
@{ `@Array`<Int> | array_length(`@Array`<Int>.0) > 0 } | <outer-predicate> }`
(keeping a predicate that still allows the array length check) and keep the
`main` call and expected assertion `assert _run_io(source, fn="main") == "20\n"`
so the test verifies the nested-refinement case on the FnCall+IndexExpr path.

In `@vera/wasm/inference.py`:
- Around line 820-870: The apply_fn path still treats only bare ast.NamedType
and peels at most one ast.RefinementType, so nested or closure-return
refinements can be misclassified; update apply_fn and the FnType WASM helpers to
canonicalise return TypeExprs by reusing the existing _resolve_i32_pair_ret_te
logic (or extract its loop into a shared helper) so that before checking
vera_type == "String" / "Array" you iteratively unwrap ast.RefinementType layers
and then resolve NamedType via _resolve_base_type_name; ensure any place that
inspects FnType.return_type (the apply_fn flow and the FnType->WASM conversion
helpers) calls that canonicaliser and treats a None result as non-i32_pair.
🪄 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: cc550555-3124-4a96-b71c-5fc710566ecb

📥 Commits

Reviewing files that changed from the base of the PR and between e772cd5 and 5d37567.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • vera/wasm/inference.py

Comment thread CHANGELOG.md Outdated
Comment thread TESTING.md Outdated
Comment thread tests/test_codegen.py
Comment thread vera/wasm/inference.py


Sixth review pass on PR #629 surfaced a third pair of inference sites
walking return-types that only handled NamedType directly. When
apply_fn(@FnAlias.0, ()) is used inside an interpolation and
FnAlias is a FnType whose return type wraps refinements, three sites
in vera/wasm/inference.py returned None for nested refinements and
the interpolation fell through to the to_string(...) wrapper,
reproducing the original #602 trap with yet another trigger:

  - _infer_fncall_vera_type apply_fn branch
  - _resolve_generic_fn_return
  - _fn_type_return_wasm

Fix: same `while isinstance(ret, ast.RefinementType): ret =
ret.base_type` loop applied symmetrically at all three sites,
matching the FnCall path shape.

Also:

  - Updated test_inline_refinement_array_in_indexed_interpolation
    to use the nested-refinement form so the array branch
    exercises the while-loop unwrap (per CodeRabbit finding).
  - Added test_apply_fn_nested_refinement_in_interpolation
    pinning the new path.
  - Fixed CHANGELOG count Three new regression tests - actually
    five, with accurate enumeration of all return-type shapes
    now verified.
  - Refreshed test counts in TESTING.md and ROADMAP.md (3,781
    -> 3,782).

Skipped:

  - TESTING.md line 9 stale-counts finding - verified those
    numbers are already current (33 examples and 148 spec blocks
    no longer in the file).

Co-Authored-By: Claude <noreply@anthropic.invalid>
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

♻️ Duplicate comments (1)
TESTING.md (1)

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

Sync the validation scripts table with the overview totals (still outstanding).

The overview table at lines 12-13 correctly states 34 examples and 164 spec blocks, but the Validation Scripts table here still shows the stale figures (33 examples at line 356 and 148 spec blocks at line 357). This inconsistency was flagged in a previous review and remains unaddressed.

Based on learnings, python scripts/check_doc_counts.py is the authoritative source for verifying these counts.

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

In `@TESTING.md` around lines 356 - 357, Update the Validation Scripts table
entries for `check_examples.py` and `check_spec_examples.py` to match the
overview totals by changing the counts from 33→34 for the `.vera` examples and
from 148→164 for the spec code blocks; verify the updated numbers by running the
authoritative script `python scripts/check_doc_counts.py` and commit the
corrected table rows referencing `check_examples.py` and
`check_spec_examples.py`.
🤖 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/wasm/inference.py`:
- Around line 657-673: The branch in _infer_apply_fn_return_type (around the
alias substitution that currently returns self._format_named_type(ta) /
self._format_named_type(base_ret)) returns alias names instead of the canonical
underlying type; change it to fully resolve aliases before formatting: when you
find a NamedType (either base_ret or ta) that appears in alias_map, follow the
mapping, unwrap any ast.RefinementType/base_type layers and recursively follow
any further NamedType aliases until you reach a non-NamedType base, then call
self._format_named_type on that resolved type; apply the same resolution logic
in both the alias-substitution branch and the fallback branch so
_infer_apply_fn_return_type matches the canonicalization behavior of
_infer_fncall_vera_type.

---

Duplicate comments:
In `@TESTING.md`:
- Around line 356-357: Update the Validation Scripts table entries for
`check_examples.py` and `check_spec_examples.py` to match the overview totals by
changing the counts from 33→34 for the `.vera` examples and from 148→164 for the
spec code blocks; verify the updated numbers by running the authoritative script
`python scripts/check_doc_counts.py` and commit the corrected table rows
referencing `check_examples.py` and `check_spec_examples.py`.
🪄 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: 59ae9b6d-7e08-4e05-a598-11b0657c2eee

📥 Commits

Reviewing files that changed from the base of the PR and between 5d37567 and c39a20e.

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

Comment thread vera/wasm/inference.py Outdated
Eighth trigger of the #602 bug class — surfaced by CodeRabbit on
PR #629's final review.

Path: `apply_fn(@Maker.0, ())` inside an interpolation, where
`Maker = fn(Unit -> Str) effects(pure)` and `type Str = String;`.
`_infer_fncall_vera_type`'s apply_fn branch called
`_format_named_type` on `NamedType("Str")` which returns the
alias name "Str" — downstream `_translate_interpolated_string`
checks `vera_type == "String"`, the alias name missed, and the
value fell through to the `to_string(...)` wrapper over an
`i32_pair`, reproducing the canonical `expected i64, found i32`
WASM-validation surface.

Fix: introduced `_format_named_type_canonical` (resolves
`te.name` through the alias chain via `_resolve_base_type_name`,
then formats with original `type_args`).  Replaced both
`_format_named_type` calls in the apply_fn branch — substitution
and fallback — with the canonical variant.  Also unwraps
RefinementType layers from the substituted type arg before
canonicalising, mirroring the `while`-loop already applied to
`alias_te.return_type`.

This matches the canonicalisation behaviour of
`_resolve_i32_pair_ret_te` for the regular FnCall path —
previously the FnCall path resolved alias chains but the
apply_fn path did not.

Also addresses CodeRabbit duplicate finding on TESTING.md:
Validation Scripts table at lines 356-357 had stale counts
(33 examples, 148 spec blocks) — refreshed to 34 / 164.  The
earlier-skipped duplicate of this finding was for line 9
(already current); this hit was at a different line that hadn't
been updated.

New regression test `test_apply_fn_aliased_string_in_interpolation`
pins the path.  Test counts refreshed (3,782 → 3,783).

Co-Authored-By: Claude <noreply@anthropic.invalid>
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.

Caution

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

⚠️ Outside diff range comments (1)
vera/wasm/inference.py (1)

671-725: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle direct anonymous closures in apply_fn Vera-type inference.

This branch now canonicalises aliased SlotRef closures, but apply_fn with a direct ast.AnonFn still falls through to None in _infer_fncall_vera_type. _infer_apply_fn_return_type() already treats that same call shape as i32_pair, so interpolating apply_fn(fn (...) -> String { ... }, ...) will still hit the old to_string(...) fallback and recreate the expected i64, found i32 validation failure.

Possible minimal fix
         if call.name == "apply_fn" and call.args:
             closure_arg = call.args[0]
+            if isinstance(closure_arg, ast.AnonFn):
+                ret: ast.TypeExpr = closure_arg.return_type
+                while isinstance(ret, ast.RefinementType):
+                    ret = ret.base_type
+                if isinstance(ret, ast.NamedType):
+                    return self._format_named_type_canonical(ret)
             if isinstance(closure_arg, ast.SlotRef):
                 alias_te = self._type_aliases.get(closure_arg.type_name)
                 if isinstance(alias_te, ast.FnType):
                     alias_params = self._type_alias_params.get(
                         closure_arg.type_name)
🤖 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 671 - 725, The apply_fn path currently
handles SlotRef closures but falls through for direct anonymous closures
(ast.AnonFn), causing _infer_fncall_vera_type to return None; add a branch
alongside the SlotRef handling in _infer_fncall_vera_type to detect
isinstance(closure_arg, ast.AnonFn), extract the anonymous function return type
(e.g. closure_arg.fn_type.return_type or the appropriate field on ast.AnonFn),
iteratively unwrap ast.RefinementType layers to get the base return type, and if
that base is an ast.NamedType return self._format_named_type_canonical(base) so
anonymous closures are canonicalised the same way as aliased FnTypes (mirroring
the existing use of _format_named_type_canonical and the RefinementType
unwrapping).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@vera/wasm/inference.py`:
- Around line 671-725: The apply_fn path currently handles SlotRef closures but
falls through for direct anonymous closures (ast.AnonFn), causing
_infer_fncall_vera_type to return None; add a branch alongside the SlotRef
handling in _infer_fncall_vera_type to detect isinstance(closure_arg,
ast.AnonFn), extract the anonymous function return type (e.g.
closure_arg.fn_type.return_type or the appropriate field on ast.AnonFn),
iteratively unwrap ast.RefinementType layers to get the base return type, and if
that base is an ast.NamedType return self._format_named_type_canonical(base) so
anonymous closures are canonicalised the same way as aliased FnTypes (mirroring
the existing use of _format_named_type_canonical and the RefinementType
unwrapping).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5edfbbe3-4cdc-4589-bdb1-c6c9308dc371

📥 Commits

Reviewing files that changed from the base of the PR and between c39a20e and 1ca93d9.

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

aallan and others added 2 commits May 8, 2026 20:23
PR #629 patched eight distinct triggers of the same canonical-name
canonicalisation drift, each one fixed locally. The underlying
pattern — five overlapping canonicalisation helpers in
`vera/wasm/inference.py` plus a silent fallthrough at
`vera/wasm/operators.py:482` that amplifies any inference miss
into invalid WASM — remains exposed.  The 9th trigger is a matter
of when, not if.

Filed #630 to close the bug class structurally:

  - Tier 1: fold the five helpers (`_format_named_type`,
    `_format_named_type_canonical`, `_resolve_i32_pair_ret_te`,
    `_resolve_base_type_name`, ad-hoc while-loops) into a single
    `_canonical_vera_type` covering RefinementType unwrap +
    alias-chain follow + generic substitution + format-with-args.
    Apply at every type-walking inference site.

  - Tier 2: convert the silent fallthrough at operators.py:482
    to a hard compile-time error. Dovetails with #626 Layer 1.

Together they make the 9th trigger either impossible (T1) or
instantly diagnosable (T2). Pure refactor + diagnostic
conversion — no behavioural change for any valid program; all
8 existing regression tests in TestStringInterpolation continue
to pin the now-fixed triggers.

Added to ROADMAP stabilisation tier as item #3 (after #604 and
#626; #630 is a sibling of #626 — single canonicalisation site
rather than the broader translator-returns-None pattern).
Renumbered the rest of the table.

Added to KNOWN_ISSUES.md "Bugs" section alongside #628 — both
are latent bug-class entries where individual instances are
patched but the underlying pattern remains exposed.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…er 9)

Ninth trigger of the #602 bug class — surfaced by CodeRabbit
during PR #629's final review pass, less than an hour after
filing #630 (the structural close-out tracking issue).
Empirical confirmation that local-fix throughput can't keep up
with discovery velocity; this is exactly the argument made in
#630 for the Tier 1 + Tier 2 structural fix.

Path: `apply_fn(fn(@Unit -> @string) effects(pure) { ... }, ())`
inside an interpolation — apply_fn called directly on an inline
`AnonFn` literal rather than a `SlotRef` to a let-bound closure.
Pre-fix `_infer_fncall_vera_type` only handled the SlotRef arg
shape; the AnonFn case fell through to None, downstream
interpolation produced `to_string(...)` over an `i32_pair`, and
WASM validation rejected the module with the canonical
`expected i64, found i32` surface.

Fix: added an `elif isinstance(closure_arg, ast.AnonFn)` branch
alongside the SlotRef branch.  Simpler than the SlotRef path
(no alias substitution — AnonFn carries `return_type` directly),
but the same RefinementType-unwrap +
`_format_named_type_canonical` shape applies.

New regression test `test_apply_fn_anon_inline_string_in_interpolation`
pins the path.  Test counts refreshed (3,783 → 3,784).
KNOWN_ISSUES entry for #630 updated to reflect the 9th trigger.

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

aallan commented May 8, 2026

@coderabbitai re: outside-diff finding on vera/wasm/inference.py apply_fn / inline AnonFn — valid and reachable, fixed in 8eead68.

Empirically reproduced apply_fn(fn(@Unit -> @String) effects(pure) { helper(()) }, ()) inside an interpolation → Invalid input WebAssembly code at offset 983: type mismatch: expected i64, found i32. This is the 9th trigger of the #602 bug class and the second one found in this review cycle.

Fixed by adding an elif isinstance(closure_arg, ast.AnonFn) branch alongside the existing SlotRef branch in _infer_fncall_vera_type. Simpler than the SlotRef path (AnonFn carries return_type: TypeExpr directly — no alias-substitution machinery), but the same RefinementType-unwrap + _format_named_type_canonical shape applies. New regression test test_apply_fn_anon_inline_string_in_interpolation pins it.

Notable: the 9th trigger landed within an hour of filing #630 (the structural close-out tracking issue), which is exactly the empirical argument made there — local fix throughput can't keep up with discovery velocity. Updated #630's body and KNOWN_ISSUES.md to reflect the 9th trigger; #630 is queued as item #3 in the stabilisation tier of ROADMAP.md, ahead of further reactive fixing.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at the apply_fn / AnonFn branch in _infer_fncall_vera_type
rg -n "AnonFn" vera/wasm/inference.py -C 10

# Also look at the full _infer_fncall_vera_type function around the apply_fn section
rg -n "apply_fn\|SlotRef\|closure_arg\|_format_named_type_canonical\|_resolve_i32_pair_ret_te" vera/wasm/inference.py -C 3

Length of output: 3273


🏁 Script executed:

#!/bin/bash
# Get full context around the second AnonFn branch (lines 1140-1200)
sed -n '1130,1210p' vera/wasm/inference.py

# Also get method name context around line 1158
rg -n "def _" vera/wasm/inference.py | awk -F: '$2 <= 1158 {last=$0} END {print last}'

Length of output: 4008


@aallan — the primary fix at the apply_fn / _infer_fncall_vera_type path (lines 726–745) is correct: while isinstance(anon_ret, ast.RefinementType) handles arbitrary nesting depth, and _format_named_type_canonical is the right canonicalisation call. Docstring attribution is thorough.

One potential gap surfaced in the second AnonFn branch — the one inside the call_indirect sig-inference helper (around Line 1158–1178):

if isinstance(ret, ast.RefinementType):  # pragma: no cover — closure returns are not refinement types
    base = ret.base_type
    if isinstance(base, ast.NamedType):
        ...

Two issues with this branch:

  1. # pragma: no cover is now provably wrong. The 9th trigger demonstrated that an inline AnonFn can carry a RefinementType return (@String). The assumption baked into that comment was disproved empirically. The pragma should be removed (and a test added to this path).

  2. Single-level unwrap — same pattern that was already fixed twice elsewhere. If the AnonFn return is @{ @{ @string | p1 } | p2 } and this branch is reached, base = ret.base_type yields another RefinementType, the inner isinstance(base, ast.NamedType) check fails, and the method falls through to return "i64" — the same expected i64, found i32 surface. The fix is the established pattern:

if isinstance(ret, ast.RefinementType):
    while isinstance(ret, ast.RefinementType):
        ret = ret.base_type
    if isinstance(ret, ast.NamedType):
        resolved_name = self._resolve_type_name_to_wasm_canonical(ret.name)
        if resolved_name in ("String", "Array"):
            return "i32_pair"
        return self._named_type_to_wasm(resolved_name)

This would be the 10th trigger if left as-is. Whether to fix it here or file it against #630 as another instance is your call — but the # pragma: no cover at minimum should be removed given what's now known.

 trigger 10)

Tenth trigger of the #602 bug class — surfaced by CodeRabbit on
PR #629 immediately after the 9th was fixed.  Inverse surface:
`expected i32, found i64` rather than the usual `expected i64,
found i32`, because this site is on the *WASM-type* inference
half of the dispatcher (`_infer_apply_fn_return_type`, which
infers the call_indirect sig) rather than the Vera-type-name
half (`_infer_fncall_vera_type`, where the 9th trigger hit).

Path: `apply_fn(fn(@Unit -> @{ @{ @string | p1 } | p2 })
effects(pure) { ... }, ())` — inline AnonFn declaring a
nested-refinement return.

Two issues with the pre-fix branch:

1. `# pragma: no cover — closure returns are not refinement
   types` was empirically disproved.  The 9th trigger (PR #629)
   already established that an inline AnonFn *can* declare a
   RefinementType return per the grammar (`fn(@Unit -> @{ @string
   | p })`); the type checker accepts it.  The pragma's claim was
   load-bearing and wrong.

2. Single-level unwrap `if isinstance(ret, ast.RefinementType):
   base = ret.base_type` — same pattern fixed twice elsewhere in
   this PR.  Nested forms left `base` as another RefinementType,
   the inner NamedType check missed, and the method fell through
   to `return "i64"` despite the call site emitting i32_pair.

Fix: replaced the single-level if-unwrap with the established
`while`-loop shape and removed the disproven pragma comment.

This is the second trigger landing within hours of filing #630
(the structural close-out tracking issue).  Both surfaces of the
bug class are now visible — Vera-type-name half (#629 triggers
1–9) and WASM-type half (this trigger).  Both route through #630's
centralised canonicaliser plan; verifying #630 closes both surfaces
is part of its acceptance criteria.

Tension noted with the policy stated when filing #630 ("from here
forward, the right move on any new trigger is to fold it into
#630's PR rather than landing it as a one-off"). Exception applied
here because: (a) the `# pragma: no cover` claim is provably wrong
right now and is a misleading code comment that would mislead any
reader before #630 lands — removing it is hygiene, not drift back
to reactive fixing; (b) the `while`-loop is the same one-line
shape applied 4 other times in this PR; cost of local fix is
trivial. The structural close-out remains queued as #630.

New regression test `test_apply_fn_anon_nested_refinement_in_interpolation`
pins the WASM-side path.  Test counts refreshed (3,784 → 3,785).
KNOWN_ISSUES entry for #630 updated to reflect the 10th trigger.

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

aallan commented May 8, 2026

@coderabbitai re: _infer_apply_fn_return_type AnonFn branch (lines 1158-1178) — both findings valid, both fixed in 32d5dcd.

The pragma was disproved. # pragma: no cover — closure returns are not refinement types was load-bearing and wrong. The 9th trigger (one PR earlier) already established that AnonFn returns can be RefinementType per the grammar; the type checker accepts fn(@Unit -> @{ @String | p }) and nested forms. Removed the pragma comment along with the fix.

Single-level unwrap was the 10th trigger. Empirically reproduced apply_fn(fn(@Unit -> @{ @{ @String | p1 } | p2 }) effects(pure) { helper(()) }, ()) inside an interpolation → Invalid input WebAssembly code at offset 980: type mismatch: expected i32, found i64. Inverse surface to triggers 1–9 because this site is on the WASM-type half of the dispatcher, not the Vera-type-name half. Fixed with the established while isinstance(ret, ast.RefinementType): ret = ret.base_type shape applied 4 other times in this PR; new regression test test_apply_fn_anon_nested_refinement_in_interpolation pins it.

Tension with the #630 'no more reactive fixing' policy noted. Stated when filing #630 that further triggers should fold into #630's PR, not land as one-offs. Exception applied here because (a) the disproven pragma is a misleading comment that would mislead any reader of this file before #630 lands — removing it is hygiene, not policy drift; (b) cost of the local fix is trivial (one line). The structural close-out remains queued as #630, now with two new audit items folded in: both surfaces of the dispatcher (Vera-type-name + WASM-type) and a pragma-claim audit pass.

This is the 10th trigger across 4 PRs over ~3 days, with #9 and #10 both landing within hours of filing #630. The empirical case for the structural fix is now decisive.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the fix at the AnonFn branch in _infer_apply_fn_return_type / sig-inference helper
rg -n "pragma: no cover" vera/wasm/inference.py

# Show the current state of the patched region
sed -n '1140,1200p' vera/wasm/inference.py

# Confirm the while-loop pattern is in place
rg -n "while isinstance.*RefinementType" vera/wasm/inference.py

Length of output: 5125


@aallan — fix confirmed correct from the current branch state.

The while-loop at Line 1183 is in place:

ret: ast.TypeExpr = closure_arg.return_type
while isinstance(ret, ast.RefinementType):
    ret = ret.base_type
if isinstance(ret, ast.NamedType):
    resolved_name = self._resolve_type_name_to_wasm_canonical(ret.name)
    if resolved_name in ("String", "Array"):
        return "i32_pair"
    return self._named_type_to_wasm(resolved_name)

The disproved # pragma: no cover is no longer an active directive — it now appears only as historical prose inside the docstring, which is exactly the right treatment: it documents why the assumption was wrong rather than silently erasing the context. The fallthrough guard at Line 1192 (return "i64" # pragma: no cover — safe default) is correct; it remains genuinely unreachable from the AnonFn path for String/Array returns after the while-loop.

The file now has 7 consistent while-loop unwrap sites (Lines 691, 742, 952, 1052, 1183, 1220, 1277), which is the right shape for the local stop-gap ahead of #630's _canonical_vera_type consolidation. The #630 policy exception reasoning (disproven pragma = hygiene, trivial fix cost) is sound.

@aallan aallan merged commit f71c6c4 into main May 8, 2026
20 checks passed
@aallan aallan deleted the claude/refinementtype-i32pair-followup branch May 8, 2026 19:48
aallan added a commit that referenced this pull request May 8, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

String-returning function call in string interpolation produces invalid WASM (i64/i32 mismatch)

1 participant