Skip to content

Emit iterative WASM for array_map (#480 PR 1)#485

Merged
aallan merged 4 commits into
mainfrom
iterative-array-map
Apr 17, 2026
Merged

Emit iterative WASM for array_map (#480 PR 1)#485
aallan merged 4 commits into
mainfrom
iterative-array-map

Conversation

@aallan

@aallan aallan commented Apr 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace the recursive prelude array_map / array_map_go with a single WAT loop driven by call_indirect on the closure. Shadow-stack usage is now O(1) regardless of input length — the old recursive version hit the 16K ceiling (post-Type-checked pure program produces incorrect runtime output that isolated probes cannot reproduce #464) around 4K elements.
  • Preserves the generic forall<A, B> signature through the existing FunctionInfo built-in registration; source programs are unchanged.
  • Covered by 4 new tests in TestArrayOperations: 8K-element stress, Int→Bool type change, closure capturing outer variable, Int→String pair output.

Discovered along the way

Filed #484 while debugging the stress test: the GC's object header size field is 16-bit, so allocations ≥65536 bytes make the sweeper link middle-of-payload bytes into the free list and clobber the live object. Test is capped at 8K Int elements (64,000 bytes) until #484 lands; full reproducer and fix options are in the issue.

Part of #480 — PR 2 (array_filter) and PR 3 (array_fold) to follow.

Test plan

  • mypy vera/ clean (57 files)
  • pytest tests/ -q — 3,312 passed, 11 skipped
  • 19 TestArrayOperations tests pass including the 4 new ones
  • Pre-commit hooks (doc counts, CHANGELOG, allowlists, etc.) green

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Array mapping now uses an iterative implementation, reducing shadow‑stack use to O(1) and improving large‑array reliability while preserving generic behaviour.
  • Known Issues

    • Documented GC header limitation: 16‑bit size field can corrupt allocations above 65,535 bytes.
  • Tests

    • Added five regression tests covering large arrays, mixed‑type maps, closure captures, pair‑typed results and zero‑length boundary cases.
  • Documentation

    • Updated changelog, roadmap and testing metrics.

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fc56d6c7-da90-4f4e-b1a2-54c0b9526f78

📥 Commits

Reviewing files that changed from the base of the PR and between 761658f and 839e41d.

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

📝 Walkthrough

Walkthrough

Migrates array_map from a recursive prelude implementation to an iterative WASM loop using call_indirect for closure dispatch; updates docs and tests; records a 16‑bit GC header size limitation that caps large allocations; test totals bumped to 3,324.

Changes

Cohort / File(s) Summary
Documentation & Metrics
CHANGELOG.md, KNOWN_ISSUES.md, ROADMAP.md, TESTING.md
Documented iterative WASM array_map codegen and O(1) shadow‑stack behaviour; added 16‑bit GC header size bug entry; updated test counts and test‑metric lines (3,319 → 3,324, passed 3,308 → 3,313).
Test Suite Expansions
tests/test_codegen_monomorphize.py, tests/test_prelude.py
Appended five new array_map tests (8k stress case, Int→Bool, closure capture, Int→String/pair, zero‑length) and updated prelude test to assert array_map/array_map_go are not injected.
Prelude & Call‑Dispatch
vera/prelude.py, vera/codegen/modules.py, vera/wasm/calls.py
Removed prelude array_map/array_map_go from injected combinators; registered array_map as a known built‑in; added call‑dispatch branch to route array_map(arg0, arg1) to the array‑map translator.
WASM Array Translation
vera/wasm/calls_arrays.py
Implemented _translate_array_map: infer element types A/B, allocate destination, root pointers via shadow‑stack, register closure call_indirect signature, emit single iterative WAT loop that loads A, calls closure, and stores B (handles scalar/pair layouts and pair‑typed returns).

Sequence Diagram

sequenceDiagram
    participant Frontend as Vera Frontend
    participant CallDispatch as CallsMixin._translate_call
    participant ArrayTranslator as CallsArrays._translate_array_map
    participant CodeGen as WASM Emitter
    participant Runtime as WASM Runtime

    Frontend->>CallDispatch: array_map(arr, closure)
    CallDispatch->>ArrayTranslator: recognize array_map (2 args)
    ArrayTranslator->>ArrayTranslator: infer A and B types
    ArrayTranslator->>ArrayTranslator: allocate dst (len × sizeof(B)), gc_shadow_push roots
    ArrayTranslator->>CodeGen: register closure signature for call_indirect
    ArrayTranslator->>CodeGen: emit loop i=0..len-1
    CodeGen->>CodeGen: load element A from (arr_ptr + i*sizeof(A))
    CodeGen->>Runtime: push closure env, call_indirect -> execute closure
    Runtime->>CodeGen: return B (scalar or pair)
    CodeGen->>CodeGen: store B into dst at i
    CodeGen->>ArrayTranslator: return (dst, len)
    ArrayTranslator-->>Frontend: compiled WASM loop (iterative array_map)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the primary change: migrating array_map from recursive prelude implementation to iterative WASM loop emission, which is the core correctness and performance improvement in this PR.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch iterative-array-map

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

@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.63636% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.38%. Comparing base (e9a1d24) to head (839e41d).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
vera/wasm/calls_arrays.py 88.46% 15 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #485    +/-   ##
========================================
  Coverage   90.37%   90.38%            
========================================
  Files          58       58            
  Lines       19588    19720   +132     
  Branches      225      225            
========================================
+ Hits        17703    17824   +121     
- Misses       1881     1892    +11     
  Partials        4        4            
Flag Coverage Δ
javascript 50.67% <ø> (ø)
python 95.26% <88.63%> (-0.03%) ⬇️

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.

aallan and others added 2 commits April 17, 2026 10:53
Replace the recursive prelude array_map/array_map_go with an iterative
WAT loop that drives a call_indirect on the closure. Shadow-stack depth
is now O(1) regardless of input size — the old recursion hit the 16K
shadow-stack ceiling (post-#464) around 4K elements.

Kept the generic forall<A, B> signature via the existing FunctionInfo
built-in registration; source code is unchanged. New CallsArraysMixin
method _translate_array_map with helper _infer_closure_return_vera_type.
Covered by 4 new tests in TestArrayOperations: large input (8K
elements, capped by the separate 16-bit GC header size-field — see
#484), Int→Bool type change, closure capturing outer variable, and
Int→String pair output.

Stress test hit a pre-existing GC bug: the object-header size field is
masked to 16 bits, so allocations ≥ 65536 bytes make the sweeper
interpret middle-of-payload i64 words as fake zero-size headers and
link each 8-byte chunk into the free list, shredding the input array.
Test capped at 8K elements (64,000 bytes) as a workaround. Full
write-up and reproducer filed as #484.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Filed while diagnosing the array_map stress test failure — an
allocation >65535 bytes gets its size truncated to 16 bits on GC
readback, shredding live payloads via the free-list linking path.
Out of scope for #480 PR 1 but worth fixing early in Stage 11 so
downstream large-allocation tests can exercise realistic sizes.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan force-pushed the iterative-array-map branch from f86de93 to c400cf3 Compare April 17, 2026 09:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

563-571: ⚠️ Potential issue | 🟠 Major

Keep ArrayMapFn injection independent from the recursive helper bundle.

Removing array_map from array_fn_names means any module that defines the filter/fold helpers now skips _ARRAY_TYPE_ALIASES as well, so ArrayMapFn disappears even though builtin array_map is still available. That is a source-compat break for code that annotates mapper values with ArrayMapFn<...>.

Suggested fix
-    if not array_fn_names.issubset(user_names):
-        if not array_alias_names.issubset(user_names):
-            source_parts.append(_ARRAY_TYPE_ALIASES)
+    if not array_alias_names.issubset(user_names):
+        source_parts.append(_ARRAY_TYPE_ALIASES)
+    if not array_fn_names.issubset(user_names):
         source_parts.append(_ARRAY_COMBINATORS)

Based on learnings: the prelude injection uses a deliberate two-layer design, and making bundles fully atomic is intentionally avoided because shadowing one array helper should not suppress unrelated declarations.

Also applies to: 589-593

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vera/prelude.py` around lines 563 - 571, The removal of "array_map" from
array_fn_names inadvertently causes ArrayMapFn to be omitted because alias
injection is tied to that bundle; restore ArrayMapFn injection independently by
ensuring array_alias_names (or the alias bundle used by _ARRAY_TYPE_ALIASES)
still contains "ArrayMapFn" regardless of whether "array_map" is present in
array_fn_names—i.e., decouple the alias set from the recursive helper set so
ArrayMapFn remains injected even when array_map was removed (apply the same
adjustment wherever the array alias bundle is built, e.g., the other occurrence
referenced around the later block).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vera/wasm/calls_arrays.py`:
- Around line 581-595: The current _infer_closure_return_vera_type
implementation only inspects syntax (ast.AnonFn) and returns None for other
valid call shapes; instead, when syntax doesn't expose types, look up the
resolved types produced by the typechecker for the closure and array arguments
and extract the function return type from that resolved function type (e.g., use
the resolved function Type/FuncType for closure_arg and the resolved
ArrayType/ElementType for array args), then map that resolved return/element
type via _resolve_type_name_to_wasm_canonical; apply the same pattern to the
similar helper around lines 633-646 so both element and mapper return types are
derived from resolved function/array types when AST inference is insufficient.

---

Outside diff comments:
In `@vera/prelude.py`:
- Around line 563-571: The removal of "array_map" from array_fn_names
inadvertently causes ArrayMapFn to be omitted because alias injection is tied to
that bundle; restore ArrayMapFn injection independently by ensuring
array_alias_names (or the alias bundle used by _ARRAY_TYPE_ALIASES) still
contains "ArrayMapFn" regardless of whether "array_map" is present in
array_fn_names—i.e., decouple the alias set from the recursive helper set so
ArrayMapFn remains injected even when array_map was removed (apply the same
adjustment wherever the array alias bundle is built, e.g., the other occurrence
referenced around the later block).
🪄 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: 4cc3a009-bd04-41cc-b69b-ccb223d4f023

📥 Commits

Reviewing files that changed from the base of the PR and between e9a1d24 and f86de93.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen_monomorphize.py
  • tests/test_prelude.py
  • vera/codegen/modules.py
  • vera/prelude.py
  • vera/wasm/calls.py
  • vera/wasm/calls_arrays.py

Comment thread vera/wasm/calls_arrays.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: 5

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

581-595: ⚠️ Potential issue | 🟠 Major

Don’t make array_map depend on syntax-only type inference.

This still rejects valid, type-checked calls when A or B is only known from the resolved checker state rather than the raw AST — for example array_map([], fn(@int->@Bool) { ... }) or a let-bound mapper value. The old prelude path handled those shapes; this built-in path now returns None instead. Please fall back to the resolved array/closure types before bailing out here.

Also applies to: 633-646

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vera/wasm/calls_arrays.py` around lines 581 - 595, The helper
_infer_closure_return_vera_type currently only inspects raw AST
(ast.AnonFn.return_type) and returns None for cases where types are resolved
later; update it to fall back to the checker/resolved types when AST info is
absent: after the existing AST branch, query the resolved type for the closure
node (via the same resolver used elsewhere in this module — e.g., the
checker/type_map attached to self or a helper like
get_resolved_type(closure_arg)) and, if that resolved type is a function/closure
type with a named return type, convert that name with
_resolve_type_name_to_wasm_canonical and return it; apply the same fallback
pattern to the sibling helper referenced around lines 633-646 (the
array/element-type inference helper) so array element types also use resolved
types before returning None.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_codegen_monomorphize.py`:
- Line 947: The inline comment for the boolean vector example is contradictory
to the asserted outcome: update the comment that currently reads "-- [false,
false, true, true] → sum of true-indices = 2 + 3 = 5" (and the other occurrence
noted) so it matches the actual assertion/result in the test; specifically
ensure the comment for the array [false, false, true, true] correctly states the
sum of true-indices as 2 + 3 = 5 (or adjust to whatever the test asserts) and
make the same correction at the second occurrence referenced, keeping the
comments consistent with the asserted values in the test functions in
tests/test_codegen_monomorphize.py.
- Around line 899-905: Add a zero-length boundary regression to the "array_map —
regression tests for the iterative implementation" block: add a test that calls
the iterative array_map path with an empty input array and asserts an empty
output (covering captured closures, A != B pair-output and scalar Int→Bool
variants if the block tests those cases) so the loop initialisation/termination
is exercised. Locate the existing array_map regression block in
tests/test_codegen_monomorphize.py and insert an empty-array case (e.g.,
array_map([], ...) expecting []) alongside the existing
large/closure/type-change cases to ensure the zero-length edge is validated.

In `@tests/test_prelude.py`:
- Around line 50-53: The test suite removed direct guarding for
array_map/array_map_go by narrowing _ARRAY_FN_NAMES, so update the
test_array_operations_injected test to explicitly assert that "array_map" and
"array_map_go" are not present (i.e., absent from whatever injection registry or
the inspected symbols) to prevent accidental re-injection; locate
test_array_operations_injected and add presence checks for the two names
alongside existing assertions that use _ARRAY_FN_NAMES so the contract is
enforced even if _ARRAY_FN_NAMES changes.

In `@vera/prelude.py`:
- Around line 563-566: Update the public docstring of the function
inject_prelude to reflect current behavior: remove or change the claim that
array_map is "always injected" (array_map is now emitted as iterative WASM and
has no prelude body), and state that array_filter and array_fold remain injected
as recursive helpers until their iterative migration lands; ensure the
description matches the comment above about which helpers are emitted vs
injected and keep the docstring concise and accurate.

In `@vera/wasm/calls_arrays.py`:
- Around line 622-626: The generated WAT calls to $alloc in array_map (and the
similar block later) must be pre-guarded to avoid triggering the 16-bit
GC-header bug: compute the requested allocation size as arr_len * sizeof(B) and
if that value exceeds 65535 (i.e. >= 65536) trap (e.g. unreachable/wasm trap)
before invoking $alloc; update both guarded sites referenced in the diff (the
allocation call in array_map and the analogous allocation at the later block) so
the check uses the same sizeof(B) computation and branches to a trap path rather
than calling $alloc for oversized requests.

---

Duplicate comments:
In `@vera/wasm/calls_arrays.py`:
- Around line 581-595: The helper _infer_closure_return_vera_type currently only
inspects raw AST (ast.AnonFn.return_type) and returns None for cases where types
are resolved later; update it to fall back to the checker/resolved types when
AST info is absent: after the existing AST branch, query the resolved type for
the closure node (via the same resolver used elsewhere in this module — e.g.,
the checker/type_map attached to self or a helper like
get_resolved_type(closure_arg)) and, if that resolved type is a function/closure
type with a named return type, convert that name with
_resolve_type_name_to_wasm_canonical and return it; apply the same fallback
pattern to the sibling helper referenced around lines 633-646 (the
array/element-type inference helper) so array element types also use resolved
types before returning None.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f07d749a-19a9-4103-b8ed-2b7ff742aace

📥 Commits

Reviewing files that changed from the base of the PR and between f86de93 and c400cf3.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen_monomorphize.py
  • tests/test_prelude.py
  • vera/codegen/modules.py
  • vera/prelude.py
  • vera/wasm/calls.py
  • vera/wasm/calls_arrays.py

Comment thread tests/test_codegen_monomorphize.py
Comment thread tests/test_codegen_monomorphize.py Outdated
Comment thread tests/test_prelude.py
Comment thread vera/prelude.py
Comment thread vera/wasm/calls_arrays.py
@aallan

aallan commented Apr 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Verified — the coupling you describe is pre-existing and doesn't break any current test or example.

Traced vera/prelude.py:590-593:

if not array_fn_names.issubset(user_names):          # triggers ~always
    if not array_alias_names.issubset(user_names):   # triggers unless user shadows aliases
        source_parts.append(_ARRAY_TYPE_ALIASES)     # injects all 3 aliases
    source_parts.append(_ARRAY_COMBINATORS)

To skip alias injection, a user program has to shadow every name in array_fn_names. Before this PR that meant shadowing all 5 (array_map + array_filter + array_filter_go + array_fold + array_fold_go); after the PR it means shadowing all 4 of the remaining recursive helpers. Either way it's a corner case: a user would have to both shadow array_filter and array_fold and reference ArrayMapFn in a type annotation to hit it.

Crucially, array_map itself no longer depends on ArrayMapFn — its full forall<A, B> signature lives in the FunctionInfo built-in registration in environment.py, so the built-in works whether or not the alias is injected. The alias is purely a user-facing convenience for spelling function types.

No current test, example, or conformance program hits that intersection. Decoupling would add a _ARRAY_ALIASES_ONLY emission branch with no test to exercise it, which is exactly the "dead code, no test" pattern we've been trimming elsewhere. Leaving as-is; happy to revisit when a real user program trips it, or as part of a future cleanup that refactors all three alias-bundle sites (Option, Result, Array) consistently.

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Tip

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

@aallan — clear, thank you. The array_map-no-longer-depends-on-ArrayMapFn point is the key correctness argument: the alias gate can remain coupled to the recursive-helper set without affecting any array_map caller, and a dedicated _ARRAY_ALIASES_ONLY branch would be untested dead code against the current trimming policy. Makes sense to defer until a real program triggers it or all three alias-bundle sites are refactored together.


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

Timestamp: 2026-04-17T10:00:06.847Z
Learning: In `vera/prelude.py` (post-PR `#485`), the `_ARRAY_TYPE_ALIASES` injection gate remains coupled to `array_fn_names` (the set of recursive prelude helpers: `array_filter`, `array_filter_go`, `array_fold`, `array_fold_go`). Type aliases are skipped only when the user shadows ALL names in that set — a contrived corner case. This is intentional: `array_map` itself no longer depends on `ArrayMapFn` because its full `forall<A, B>` signature lives in the `FunctionInfo` built-in registration in `environment.py`, making the alias purely a user-facing convenience. Adding a separate `_ARRAY_ALIASES_ONLY` emission branch would be dead code with no test. Decoupling is deferred until a real program triggers the intersection or all three alias-bundle sites (Option, Result, Array) are refactored together. Do NOT flag this coupling as a bug.

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


🧠 Learnings used
Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:28.783Z
Learning: In vera/prelude.py, the uncovered lines (user-defined type alias shadowing path and the early-return-when-every-prelude-declaration-is-shadowed path) are correct but rare edge cases that would require contrived test programs to exercise. They are intentional defensive guards, not missing test coverage.

Learnt from: aallan
Repo: aallan/vera PR: 485
File: vera/wasm/calls_arrays.py:581-595
Timestamp: 2026-04-17T09:59:31.848Z
Learning: In `vera/wasm/calls_arrays.py`, `_infer_closure_return_vera_type` intentionally only handles `ast.AnonFn`. The Vera parser (as of PR `#485` / Stage ≤10) rejects non-literal closure arguments with E005, so passing a named function reference or let-bound value as the `fn` argument to `array_map`/`array_filter`/`array_fold` is a syntax error. Every call in `examples/` and `tests/conformance/` uses an inline `fn(...) -> T { ... }` literal. Consulting the typechecker's resolved types for the fallback is explicitly deferred to Stage 11+ (first-class function references). Do NOT flag the AnonFn-only branch as incomplete inference.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:13.465Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (around lines 347-367): an `issubset` check skips an entire bundle (e.g. all array ops) only when the user has defined ALL functions in the group — prevents partial injection of the bundle's source text.
2. **Declaration-level filter** (around lines 374-384): after parsing the bundle source, individual declarations are filtered out if the user already defines a declaration with that name — so partial shadowing of internal helpers (e.g. `array_map_go`) is supported. The prelude's public function (e.g. `array_map`) is still injected and will call the user's version; a signature mismatch is caught at type-check time.
Making bundles fully atomic (skip all if any member is shadowed) is intentionally avoided because a user defining `array_map` should not suppress `array_filter`/`array_fold`.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:03.183Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (lines ~347–367): the `issubset` check skips parsing/injecting an entire source bundle only when the user has defined ALL public functions in that group, preventing partial source-text injection with broken internal references.
2. **Declaration-level filter** (lines ~374–384): after parsing, individual declarations are skipped if the user has defined a name that shadows them. This means a user defining only `array_map_go` (an internal helper) will get prelude `array_map` injected (which calls their `array_map_go`), and a type error fires if signatures are incompatible.
Making bundles fully atomic would be incorrect: a user defining `array_map` alone would suppress `array_filter` and `array_fold`, which is surprising and undesirable.

Learnt from: aallan
Repo: aallan/vera PR: 474
File: vera/wasm/calls_containers.py:136-160
Timestamp: 2026-04-16T19:47:48.724Z
Learning: In `vera/wasm/calls_containers.py` (post-PR `#474` refactor), `_map_wasm_tag` has the same intentional restriction as the original `vera/codegen/api.py`: `Array<T>` as a Map value type is unsupported and must NOT be silently folded into the `'b'` (i32) tag. The correct fix (explicit rejection of Array-valued maps) is tracked in issue `#475`. Do not flag this as a new bug in code-motion PRs — it is a known pre-existing limitation.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-29T17:17:11.974Z
Learning: In `tests/test_codegen.py`, `test_option_unwrap_or_map` (lines ~9936–9949) and `test_option_unwrap_or_set` (lines ~9950–9963) are regular, unconditionally-passing tests with NO `xfail`, `skip`, or `skipif` marks. `grep -rn "xfail" tests/` returns zero results across the entire test suite. The PR `#428` fix resolved the underlying monomorphiser inference gap for opaque i32 handle types (Map, Set) in `option_unwrap_or` generics. Do NOT flag these tests as xfail or claim there is a monomorphiser limitation for Map/Set in this context.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:28.783Z
Learning: In vera/codegen/core.py, the `self._register_data(decl)` branch inside the post-prelude-injection loop (guarded by `decl.name not in existing_adts`) is architecturally unreachable: all four current prelude ADTs (Option, Result, Ordering, UrlParts) are already pre-registered by `_register_builtin_adts()` in Pass 1, so the guard always evaluates to False. The guard is a forward-compatibility safety net for future prelude ADTs that might not be pre-registered.

Learnt from: aallan
Repo: aallan/vera PR: 344
File: tests/test_codegen.py:8630-8834
Timestamp: 2026-03-29T17:13:40.986Z
Learning: In `tests/test_codegen.py` (`TestDecimalMonomorphization`), there is a deliberate asymmetry in opaque-handle monomorphization for Map and Set:
- `match` on `Option<Map<...>>` / `Option<Set<...>>` compiles and runs correctly because `match` is emitted inline by the codegen (no standalone monomorphized function is required).
- `option_unwrap_or<Map<...>>` / `option_unwrap_or<Set<...>>` currently fails (pinned as `xfail` in `test_option_unwrap_or_map`) because it requires the monomorphizer to generate a standalone specialised function body (e.g. `option_unwrap_or$Map`), which is not yet supported for opaque i32 handle types (Map, Set). `option_unwrap_or<Decimal>` works because Decimal support was added in PR `#344`. The remaining Map/Set gap is tracked as a follow-up to issue `#341`.

Learnt from: aallan
Repo: aallan/vera PR: 344
File: tests/conformance/manifest.json:815-831
Timestamp: 2026-03-25T10:03:24.704Z
Learning: In `vera/wasm/inference.py`, `_infer_fncall_wasm_type` must explicitly handle Map and Set builtin functions (e.g. `map_new`, `set_new`, and related ops) to return `"i32"` so that WASM `if (result i32)` blocks containing Map/Set function calls are correctly annotated. Missing these entries was the root cause of Map/Set if-expression result-type failures (fixed in PR `#344`, commit 741f19e).

Learnt from: aallan
Repo: aallan/vera PR: 350
File: tests/test_codegen.py:8630-8633
Timestamp: 2026-03-25T16:32:11.316Z
Learning: Repo aallan/vera PR `#350`: tests/test_codegen.py includes explicit WAT import‑gating coverage for JSON host imports — tests test_json_parse_wat_import, test_json_stringify_wat_import, and test_json_no_imports_when_unused verify that "vera.json_parse"/"vera.json_stringify" imports are emitted only when referenced and absent when unused.

Learnt from: aallan
Repo: aallan/vera PR: 350
File: vera/wasm/calls.py:4456-4463
Timestamp: 2026-03-25T14:53:24.176Z
Learning: Repo aallan/vera: In vera/wasm/calls.py, JSON host-import helpers must record usage via self._json_ops_used.add("json_parse") and self._json_ops_used.add("json_stringify") inside CallsMixin._translate_json_parse/_translate_json_stringify so codegen propagates result.json_ops_used and conditional imports are injected in assembly/api; otherwise WAT may reference $vera.json_parse/$vera.json_stringify without matching imports.

Learnt from: aallan
Repo: aallan/vera PR: 474
File: vera/wasm/calls_parsing.py:80-90
Timestamp: 2026-04-16T19:47:52.584Z
Learning: In `vera/wasm/calls_parsing.py` (originally in `vera/wasm/calls.py`), the `_translate_parse_nat` and `_translate_parse_int` state-machine loops skip ASCII space (byte 32) unconditionally at every iteration, not just during leading/trailing whitespace. This means embedded spaces (e.g. `"12 34"`) are silently accepted. This is known pre-existing behaviour, intentionally left unchanged in PR `#474` (pure code-motion refactor) and tracked for fixing in issue `#475`.

Learnt from: aallan
Repo: aallan/vera PR: 474
File: vera/wasm/calls_handlers.py:302-307
Timestamp: 2026-04-16T19:47:52.306Z
Learning: In `vera/wasm/calls_handlers.py`, `_translate_handle_exn` infers the catch-arm result type only when `expr.clauses[0].body` is an `ast.Block` (via `_infer_block_result_type`). Expression-bodied handlers (e.g. `throw(x) -> x + 1`) are not handled, leaving `result_wt` as `None` and omitting the needed `(result ...)` in the generated `block`/`try_table` WAT. This is pre-existing behavior (not introduced by PR `#474`, which was pure code motion). The fix is tracked in issue `#475`.

Learnt from: aallan
Repo: aallan/vera PR: 414
File: vera/codegen/core.py:258-265
Timestamp: 2026-03-28T22:06:21.085Z
Learning: In `vera/codegen/core.py` and `vera/codegen/api.py` (PR `#414`), `CompileResult.fn_param_types` carries erased WASM-level tags (`i64`, `f64`, `i32`, `i32_pair`) rather than Vera-level kinds. This is intentional for the current CLI: `i32_pair` = String only (Array CLI not yet supported), `i64` = Int/Nat, `f64` = Float64, `i32` = Bool/Byte (identical CLI parsing). Carrying Vera-level parameter kinds through CompileResult to distinguish Nat vs Int (negative rejection) and Bool vs Byte is a future-proofing refactor explicitly deferred from PR `#414` and filed as a follow-up note by aallan.

Learnt from: aallan
Repo: aallan/vera PR: 357
File: tests/test_codegen.py:9059-9078
Timestamp: 2026-03-26T09:40:51.556Z
Learning: Repo aallan/vera PR `#357`: In tests/test_codegen.py, per-operation import-gating for the Http effect is covered — tests ensure only the used host import ("http_get" or "http_post") appears in WAT and the sibling import is absent. This mirrors the existing JSON import-gating pattern.

Learnt from: aallan
Repo: aallan/vera PR: 473
File: vera/codegen/assembly.py:508-514
Timestamp: 2026-04-16T10:35:10.852Z
Learning: In vera/codegen/assembly.py `_emit_gc_collect`, the worklist push sites (Phase 2 seed loop and Phase 2b mark loop) intentionally silently skip enqueuing when the worklist is full (`wl_ptr >= wl_end`). This is deliberate bounded-marking-worklist design: overflow means some objects may not be transitively marked and get conservatively freed, which is acceptable. A trap/unreachable at that point would be incorrect because Phase 1 has already cleared all mark bits and Phase 3 has not yet run, leaving the heap in an unrecoverable inconsistent state. The worklist is 16 KiB (4,096 i32 entries), making overflow require >4,096 live pointers at a single marking depth. The known limitation for deeply nested object graphs is tracked in issue `#348`. The shadow stack overflow guard (added in this PR) is the critical correctness defence.

Four small fixes from the review:

- test_codegen_monomorphize.py: the Int→Bool test comment claimed
  "sum of true-indices = 2 + 3 = 5" but the expression sums 1 for
  each true and asserts 2 — corrected comment to match the actual
  computation.

- test_codegen_monomorphize.py: added `test_array_map_empty_input`
  covering the n=0 boundary (loop's `idx >= arr_len` guard must
  fire on the first iteration; closure never invoked; output
  length 0).

- test_prelude.py: `test_array_operations_injected` now explicitly
  asserts `array_map` and `array_map_go` are NOT in the injected
  names — regression guard against accidental re-injection if
  `_ARRAY_FN_NAMES` is ever widened without updating the prelude.

- prelude.py: `inject_prelude` docstring updated — array_map is no
  longer always injected; it's emitted as iterative WASM by codegen
  (#480). array_filter / array_fold remain as recursive helpers
  until their own iterative migration lands.

The suggestion to pre-guard `$alloc` with a 65535-byte trap in the
array_map site was declined: the same silent-corruption risk
applies to array_range, array_concat, array_append, array_slice
today, so a per-site bandaid wouldn't make the system safer.  The
root fix is #484 (widen the 16-bit GC-header size field), which
cures every call site in one change.

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.

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

974-975: ⚠️ Potential issue | 🟡 Minor

Fix the contradictory expectation comment in the closure-capture test.

Line 974 says 102, but Line 975 correctly asserts 103. Please align the comment with the assertion to avoid confusion during failures.

Suggested fix
-        # Outer `@Int.0` = 100 (captured); element 2 + captured 100 = 102
+        # Outer `@Int.0` = 100 (captured); element 3 + captured 100 = 103
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_codegen_monomorphize.py` around lines 974 - 975, The comment in
the closure-capture test contradicts the assertion: update the inline comment
that currently reads "element 2 + captured 100 = 102" to reflect the actual
asserted result 103 so it matches the assertion assert _run(source, fn="main")
== 103; locate the comment near the closure-capture check in
tests/test_codegen_monomorphize.py (around the assertion that calls _run with
fn="main") and change the numeric expectation to 103.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/test_codegen_monomorphize.py`:
- Around line 974-975: The comment in the closure-capture test contradicts the
assertion: update the inline comment that currently reads "element 2 + captured
100 = 102" to reflect the actual asserted result 103 so it matches the assertion
assert _run(source, fn="main") == 103; locate the comment near the
closure-capture check in tests/test_codegen_monomorphize.py (around the
assertion that calls _run with fn="main") and change the numeric expectation to
103.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4293abd-8e40-4a9a-b622-1397fa692e32

📥 Commits

Reviewing files that changed from the base of the PR and between c400cf3 and 761658f.

📒 Files selected for processing (5)
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen_monomorphize.py
  • tests/test_prelude.py
  • vera/prelude.py

Comment claimed "element 2 + captured 100 = 102" but the test maps over
[1, 2, 3] and reads index 2 (= 3), then adds the captured 100 → 103
(which is what the assertion checks). Comment now correctly states
"[1, 2, 3][2] = 3; closure returns element + captured = 3 + 100 = 103".

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

aallan commented Apr 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Fixed in 839e41d — the closure-capture test comment now matches the assertion. [1, 2, 3][2] = 3, so closure returns element + captured = 3 + 100 = 103, which is exactly what the assert _run(source, fn="main") == 103 checks. Your suggested diff had the arithmetic right (element 3, not element 2); I expanded it slightly to spell out the array-index step for future readers:

# Outer @Int.0 = 100 (captured); [1, 2, 3][2] = 3;
# closure returns element + captured = 3 + 100 = 103.
assert _run(source, fn="main") == 103

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Tip

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

@aallan — confirmed, the expanded comment is clearer than my original suggestion. Spelling out the array-index step ([1, 2, 3][2] = 3) before the addition makes the arithmetic self-documenting for future readers. Looks good.


🧠 Learnings used
Learnt from: aallan
Repo: aallan/vera PR: 485
File: vera/wasm/calls_arrays.py:581-595
Timestamp: 2026-04-17T09:59:31.848Z
Learning: In `vera/wasm/calls_arrays.py`, `_infer_closure_return_vera_type` intentionally only handles `ast.AnonFn`. The Vera parser (as of PR `#485` / Stage ≤10) rejects non-literal closure arguments with E005, so passing a named function reference or let-bound value as the `fn` argument to `array_map`/`array_filter`/`array_fold` is a syntax error. Every call in `examples/` and `tests/conformance/` uses an inline `fn(...) -> T { ... }` literal. Consulting the typechecker's resolved types for the fallback is explicitly deferred to Stage 11+ (first-class function references). Do NOT flag the AnonFn-only branch as incomplete inference.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-04-17T10:00:06.847Z
Learning: In `vera/prelude.py` (post-PR `#485`), the `_ARRAY_TYPE_ALIASES` injection gate remains coupled to `array_fn_names` (the set of recursive prelude helpers: `array_filter`, `array_filter_go`, `array_fold`, `array_fold_go`). Type aliases are skipped only when the user shadows ALL names in that set — a contrived corner case. This is intentional: `array_map` itself no longer depends on `ArrayMapFn` because its full `forall<A, B>` signature lives in the `FunctionInfo` built-in registration in `environment.py`, making the alias purely a user-facing convenience. Adding a separate `_ARRAY_ALIASES_ONLY` emission branch would be dead code with no test. Decoupling is deferred until a real program triggers the intersection or all three alias-bundle sites (Option, Result, Array) are refactored together. Do NOT flag this coupling as a bug.

Learnt from: aallan
Repo: aallan/vera PR: 485
File: vera/wasm/calls_arrays.py:622-626
Timestamp: 2026-04-17T10:13:56.879Z
Learning: In `vera/wasm/calls_arrays.py`, do NOT add per-site `arr_len * sizeof(B) > 65535` trap guards in individual array combinators (`array_map`, `array_range`, `array_concat`, `array_append`, `array_slice`). All of these can allocate >65535 bytes and are equally affected by the 16-bit GC object-header size field truncation bug. The correct, single-point fix is issue `#484`: widen the GC header size field from 16-bit to 31-bit in `vera/codegen/assembly.py` by updating the two `i32.const 65535 / i32.and` masks in `$alloc` and three in `$gc_collect`. Per-site guards would shift the landmine without defusing it.

Learnt from: aallan
Repo: aallan/vera PR: 453
File: tests/test_codegen.py:2806-2879
Timestamp: 2026-04-09T19:13:00.009Z
Learning: In tests/test_codegen.py (Python, pytest), TestEffectHandlers.test_nested_state_outer_readable_after_inner intentionally captures the inner handle[State<Int>] result (inner body returns get(())) via let Int = handle[...] ... in { ... get(()) }; the outer block then calls get(()) and asserts 42. This shape specifically covers the non-void state_pop_Int path (pop with a live i64 on the stack) and guards against pop-ordering/result-propagation regressions for nested same-type State handlers.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:28.783Z
Learning: In vera/prelude.py, the uncovered lines (user-defined type alias shadowing path and the early-return-when-every-prelude-declaration-is-shadowed path) are correct but rare edge cases that would require contrived test programs to exercise. They are intentional defensive guards, not missing test coverage.

Learnt from: aallan
Repo: aallan/vera PR: 344
File: tests/conformance/manifest.json:815-831
Timestamp: 2026-03-25T10:03:24.704Z
Learning: In `vera/wasm/inference.py`, `_infer_fncall_wasm_type` must explicitly handle Map and Set builtin functions (e.g. `map_new`, `set_new`, and related ops) to return `"i32"` so that WASM `if (result i32)` blocks containing Map/Set function calls are correctly annotated. Missing these entries was the root cause of Map/Set if-expression result-type failures (fixed in PR `#344`, commit 741f19e).

Learnt from: aallan
Repo: aallan/vera PR: 344
File: tests/test_codegen.py:8452-8628
Timestamp: 2026-03-25T11:11:27.827Z
Learning: In `tests/test_codegen.py` (`TestDecimalCollection`), the tests `test_decimal_if_expr_result_type`, `test_map_if_expr_result_type`, and `test_set_if_expr_result_type` target `_infer_block_result_type` in `vera/wasm/inference.py` for opaque handle types (Decimal, Map, Set). They always execute the then-branch by design: the WASM `(if (result i32) ...)` type annotation is emitted for both branches at compile time, so a single branch execution is sufficient to verify the inference path. False-branch runtime correctness for these types is already covered separately in `TestDecimalCollection`, `TestMapCollection`, and `TestSetCollection`. Do not flag these tests as lacking else-branch coverage.

Learnt from: aallan
Repo: aallan/vera PR: 474
File: vera/wasm/calls_containers.py:136-160
Timestamp: 2026-04-16T19:47:48.724Z
Learning: In `vera/wasm/calls_containers.py` (post-PR `#474` refactor), `_map_wasm_tag` has the same intentional restriction as the original `vera/codegen/api.py`: `Array<T>` as a Map value type is unsupported and must NOT be silently folded into the `'b'` (i32) tag. The correct fix (explicit rejection of Array-valued maps) is tracked in issue `#475`. Do not flag this as a new bug in code-motion PRs — it is a known pre-existing limitation.

Learnt from: aallan
Repo: aallan/vera PR: 473
File: vera/codegen/assembly.py:508-514
Timestamp: 2026-04-16T10:35:10.852Z
Learning: In vera/codegen/assembly.py `_emit_gc_collect`, the worklist push sites (Phase 2 seed loop and Phase 2b mark loop) intentionally silently skip enqueuing when the worklist is full (`wl_ptr >= wl_end`). This is deliberate bounded-marking-worklist design: overflow means some objects may not be transitively marked and get conservatively freed, which is acceptable. A trap/unreachable at that point would be incorrect because Phase 1 has already cleared all mark bits and Phase 3 has not yet run, leaving the heap in an unrecoverable inconsistent state. The worklist is 16 KiB (4,096 i32 entries), making overflow require >4,096 live pointers at a single marking depth. The known limitation for deeply nested object graphs is tracked in issue `#348`. The shadow stack overflow guard (added in this PR) is the critical correctness defence.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-29T17:17:11.974Z
Learning: In `tests/test_codegen.py`, `test_option_unwrap_or_map` (lines ~9936–9949) and `test_option_unwrap_or_set` (lines ~9950–9963) are regular, unconditionally-passing tests with NO `xfail`, `skip`, or `skipif` marks. `grep -rn "xfail" tests/` returns zero results across the entire test suite. The PR `#428` fix resolved the underlying monomorphiser inference gap for opaque i32 handle types (Map, Set) in `option_unwrap_or` generics. Do NOT flag these tests as xfail or claim there is a monomorphiser limitation for Map/Set in this context.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:03.183Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (lines ~347–367): the `issubset` check skips parsing/injecting an entire source bundle only when the user has defined ALL public functions in that group, preventing partial source-text injection with broken internal references.
2. **Declaration-level filter** (lines ~374–384): after parsing, individual declarations are skipped if the user has defined a name that shadows them. This means a user defining only `array_map_go` (an internal helper) will get prelude `array_map` injected (which calls their `array_map_go`), and a type error fires if signatures are incompatible.
Making bundles fully atomic would be incorrect: a user defining `array_map` alone would suppress `array_filter` and `array_fold`, which is surprising and undesirable.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:13.465Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (around lines 347-367): an `issubset` check skips an entire bundle (e.g. all array ops) only when the user has defined ALL functions in the group — prevents partial injection of the bundle's source text.
2. **Declaration-level filter** (around lines 374-384): after parsing the bundle source, individual declarations are filtered out if the user already defines a declaration with that name — so partial shadowing of internal helpers (e.g. `array_map_go`) is supported. The prelude's public function (e.g. `array_map`) is still injected and will call the user's version; a signature mismatch is caught at type-check time.
Making bundles fully atomic (skip all if any member is shadowed) is intentionally avoided because a user defining `array_map` should not suppress `array_filter`/`array_fold`.

Learnt from: aallan
Repo: aallan/vera PR: 350
File: tests/test_codegen.py:8630-8633
Timestamp: 2026-03-25T16:32:11.316Z
Learning: Repo aallan/vera PR `#350`: tests/test_codegen.py includes explicit WAT import‑gating coverage for JSON host imports — tests test_json_parse_wat_import, test_json_stringify_wat_import, and test_json_no_imports_when_unused verify that "vera.json_parse"/"vera.json_stringify" imports are emitted only when referenced and absent when unused.

Learnt from: aallan
Repo: aallan/vera PR: 414
File: vera/codegen/core.py:258-265
Timestamp: 2026-03-28T22:06:21.085Z
Learning: In `vera/codegen/core.py` and `vera/codegen/api.py` (PR `#414`), `CompileResult.fn_param_types` carries erased WASM-level tags (`i64`, `f64`, `i32`, `i32_pair`) rather than Vera-level kinds. This is intentional for the current CLI: `i32_pair` = String only (Array CLI not yet supported), `i64` = Int/Nat, `f64` = Float64, `i32` = Bool/Byte (identical CLI parsing). Carrying Vera-level parameter kinds through CompileResult to distinguish Nat vs Int (negative rejection) and Bool vs Byte is a future-proofing refactor explicitly deferred from PR `#414` and filed as a follow-up note by aallan.

@aallan aallan merged commit e29cc6b into main Apr 17, 2026
19 checks passed
@aallan aallan deleted the iterative-array-map branch April 17, 2026 14:02
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.

1 participant