v0.0.132 — Opaque-handle GC-rooting hygiene (closes #347 and #490; #346 deferred to #573)#572
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdded a module-private host-handle classifier ( ChangesOpaque-Handle GC-Rooting Hygiene
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #572 +/- ##
==========================================
- Coverage 90.96% 90.93% -0.03%
==========================================
Files 59 59
Lines 22588 22596 +8
Branches 259 259
==========================================
+ Hits 20547 20548 +1
- Misses 2034 2041 +7
Partials 7 7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 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 `@ROADMAP.md`:
- Line 11: The ROADMAP.md statement and the CI tooling table are inconsistent:
the top summary says "3,700 tests" while the CI tooling table still lists
"3,658". Update the downstream reference in the CI tooling table to match the
canonical test count or replace the hardcoded number with a dynamic/non-numeric
phrase (e.g., "tests (see CI)"); locate the CI tooling table entry referenced in
the file (the line shown around the table) and change the numeric value to the
canonical count produced by running pytest tests/ --collect-only -q or python
scripts/check_doc_counts.py, or swap the literal with a non-numeric reference to
avoid future drift.
- Line 25: The ROADMAP contains a stale backlog entry for issue `#490` in the
"Compiler internals" table that contradicts the paragraph (which states `#490` was
closed in v0.0.132); remove or update the table row referencing "#490" so it no
longer appears as open — either delete that row entirely or change its
status/notes to indicate it was closed in v0.0.132 (and optionally add a short
parenthetical "closed in v0.0.132" to match the paragraph). Ensure you update
only the table entry for "#490" in ROADMAP.md so the paragraph and table are
consistent.
In `@tests/test_codegen.py`:
- Around line 15573-15600: Update test_transient_map_handles_freed_after_gc to
assert that intermediate transient maps are actually freed: after executing the
program via _run (or the executor used), invoke the host GC sweep
(host_gc_sweep) or trigger gc_collect, then query the host-side map store size
(expose or call a helper like _map_store_size or inspect _map_store) and assert
it decreased to only contain the final live Map; keep the functional assertion
(result == 3) but add the explicit post-GC assertion that the internal map store
length/count equals the expected number (e.g., 1) so the test fails pre-fix.
Ensure you reference test_transient_map_handles_freed_after_gc, _run,
host_gc_sweep/_gc_collect and the map store accessor when making the changes.
- Around line 15476-15502: The test test_array_fold_with_decimal_accumulator
only calls _compile_ok and checks result.exports, so it never exercises the
runtime fold; change it to instantiate and invoke the compiled "main" export so
the runtime path runs and would trap on regression: after obtaining result from
_compile_ok (or its returned module/instance), instantiate or get the runtime
export for "main" (refer to how other runtime-executing tests invoke exports in
this test suite), call/invoke that export and assert it completes without
raising/trapping (e.g., no exception or specific success value), ensuring the
Decimal accumulator fold actually runs; update references to
result.exports/result.instance as needed to match the test harness.
- Around line 15424-15458: Replace the brittle sp_advance_count check (counting
"global.set $gc_sp") in the lookup_or_zero WAT test with a direct detection of
the shadow-push idiom that roots the first parameter ($p0): search fn_body for
the sequence that indicates a param shadow-push — "global.get $gc_sp" followed
shortly by "local.get 0" (or "local.get $p0") and then "i32.store" (and
optionally the matching "global.set $gc_sp") — and assert that this exact
sequence does NOT appear for $p0; keep the existing explanatory message but base
the assertion on that concrete pattern instead of counting global.set
occurrences.
In `@vera/browser/runtime.mjs`:
- Around line 2044-2045: The early return in imports.vera.host_gc_sweep (the
line "if (heapPtr <= heapStart) return;") prevents store pruning when the live
heap is empty; update the condition so pruning still runs when heapPtr ===
heapStart (e.g., change to "if (heapPtr < heapStart) return;" or remove the
return) so that candidates can be empty and transient host handles/stores are
still dropped; ensure the rest of the function still uses heapStart, heapPtr and
candidates as before so pruning logic executes.
- Around line 2065-2074: The current pruning loop for mapStore, setStore, and
decimalStore only keeps keys present in candidates, dropping nested handles
reachable only through those stores; modify the sweep to compute a reachable set
(starting from candidates) and expand it by traversing entries in mapStore,
setStore, and decimalStore to include any handles referenced as values/contents
(repeat until fixed point), then delete keys from mapStore, setStore, and
decimalStore that are not in this reachable set; ensure you reference the
existing variables mapStore, setStore, decimalStore and candidates and perform
the traversal before the deletion loop so nested live host handles are
preserved.
In `@vera/codegen/api.py`:
- Around line 1616-1622: The stores _map_store, _set_store, and _decimal_store
allocate handles starting at 1 which collides when the sweep routine prunes by
raw positive i32 membership; change allocation so handles are globally unique by
either (A) using a shared allocator used by all stores (move their counters into
a common generator) or (B) bit-encode the store kind into the handle value
(e.g., top bits or a tag) so the sweep's numeric membership test can distinguish
kinds before pruning; update any creation/lookup code that references
_map_store/_set_store/_decimal_store handles and the _handle_stores registration
so lookups still work with the new global allocator or encoded handles (also
apply the same fix at the other occurrences mentioned).
In `@vera/wasm/calls_arrays.py`:
- Around line 1065-1076: The current logic sets u_is_adt/t_is_adt to false for
host-handle types via _is_host_handle_type(), which clears
u_needs_root/t_needs_root and causes accumulators/elements in array_fold and
array_sort_by to live only in locals (unrooted) across call_indirect; to fix,
ensure host-handle values are shadow-stack rooted when they can survive a
call_indirect: either stop excluding host-handle types in the u_is_adt/t_is_adt
check (remove the "and not _is_host_handle_type(...)" condition) or explicitly
force u_needs_root/t_needs_root true for host-handle types before emitting the
local/accumulator stores in array_fold (array_fold accumulator at ~lines
1106–1108) and array_sort_by (element at ~2169–2170), so that the generated code
pushes/populates the shadow stack around call_indirect and host_gc_sweep can see
them.
🪄 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: f4d8f5c8-833c-474e-84e8-2ed3ac8ae490
⛔ Files ignored due to path filters (5)
docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**uv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (16)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdpyproject.tomltests/test_codegen.pyvera/__init__.pyvera/browser/runtime.mjsvera/codegen/api.pyvera/codegen/assembly.pyvera/codegen/closures.pyvera/codegen/functions.pyvera/wasm/calls_arrays.pyvera/wasm/helpers.py
💤 Files with no reviewable changes (1)
- KNOWN_ISSUES.md
Big revision driven by a critical correctness finding that reframes the whole #347/#490 fix approach. Inline (5): (1) ROADMAP.md line 11 (CI tooling table 3,658 → dynamic phrase) Replaced "3,658 tests" with "the test suite" to avoid future drift between paragraph and table. (2) ROADMAP.md line 25 (stale #490 row) Removed the now-stale "Tighten GC-rooting heuristic" row from Compiler Internals — closed in v0.0.132. Also updated the campaign-tracker prose to accurately describe the host_gc_sweep mechanism (was misleading before). (3) test_transient_map_handles_freed_after_gc Now asserts result.host_store_sizes["map"] == 3 (i.e., the empty map_new() was freed; 3 let-bound handles remain). Required new ExecuteResult.host_store_sizes field exposing post-execute store state. Test uses array_range(100, 10100) values to avoid handle-index collision (1..4) with Int payloads in heap. (4) test_array_fold_with_decimal_accumulator Now actually runs via _run with a decimal_eq comparison (instead of just _compile_ok + exports check). Verifies 0+1+2+3+4 = 10 in Decimal arithmetic. (5) test_map_param_*_shadow_pushed (renamed from test_map_param_NOT_shadow_pushed — the assertion FLIPPED after the revert below). Replaced sp_advance_count with a direct re.compile pattern for the shadow_push idiom `global.get $gc_sp; local.get 0; i32.store`. The test now asserts the push EXISTS — Map params MUST be rooted so host_gc_sweep can see them. Browser (2): (6) runtime.mjs early-return Removed the "if heapPtr <= heapStart return" guard that blocked store pruning when the heap was empty. Now: shadow stack is still scanned, candidates can be empty, transient handles still get dropped. (7) runtime.mjs nested handles Added transitive closure of `candidates` via store contents. A Map containing Set handles as values now keeps those Sets alive. Mirror of the Python-side _handle_value_ints + fixed-point iteration. Code (2): (9) calls_arrays.py rooting (THE CRITICAL FINDING) My #490 fix excluded host handles from u_is_adt/t_is_adt, meaning array_fold/array_map accumulators of host handles (e.g. Decimal) lived only in WASM locals across call_indirect. When GC fired in the closure body, host_gc_sweep walked the heap, didn't find the handle in live objects, and PRUNED the store entry — corrupting the next iteration. Reverted that exclusion. Same correctness logic applies to the #347 fix in functions.py + closures.py (the same pruning would affect Map/Set/Decimal parameters), so I reverted those too. Host handles are back on the GC shadow stack at every rooting site. The original #347/#490 framings (don't waste shadow stack on non-pointers; avoid spurious heap retention from handle values in heap range) become weaker concerns now that #346's host_gc_sweep mechanism makes the rooting necessary. Tradeoff documented in `_is_host_handle_type` docstring. Knock-on changes to make host_gc_sweep work correctly: - Sweep signature changed from (heap_start, heap_ptr) to (heap_start, heap_ptr, gc_stack_base, gc_sp). - Sweep now scans BOTH heap AND shadow stack for candidate handles. - Transitive closure through store contents (so a retained Map keeps its referenced Sets alive). - New re-entrancy guard `_in_host_alloc` flag — when host_gc_sweep is called from a nested $alloc inside a host helper, skip the sweep (the helper has handles in flight on the Python stack that aren't visible to the heap or shadow stack). Skipping is safe — the handles will be freed on the next top-level cycle. - context.py's let-binding handler now shadow_push host-handle let-bindings (previously only ADT constructors emitted the push, leaving host-handle lets unrooted). Skipped (1): (8) api.py handle collision (globally-unique handles) Skipped — current behaviour is over-retention only (a heap value matching a Map handle index also retains a Set entry with the same index). This is a precision improvement, not a correctness fix. Documented in the `host_gc_sweep` docstring and `_is_host_handle_type` module comment. Test count: 3,700 → unchanged (5 tests modified, 0 added). ruff S clean, mypy clean, 82 conformance + 33 examples pass. Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
vera/codegen/api.py (1)
1682-1688:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftRaw-integer pruning still needs a shared handle namespace.
_map_store,_set_store, and_decimal_storestill allocate from separate counters starting at1, whilehost_gc_sweepretains entries by membership in oneset[int]. Unrelated integers and same-numbered handles across stores therefore keep each other alive, so pruning remains lossy.Also applies to: 1964-1967, 2139-2141, 2923-3059
🤖 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/codegen/api.py` around lines 1682 - 1688, The code currently uses separate per-store integer counters for _map_store, _set_store, and _decimal_store while host_gc_sweep tracks live handles as plain ints, causing cross-store collisions that prevent proper pruning; change all stores to allocate handles from a single shared counter/namespace (e.g., _next_handle or a shared make_handle() function) so every store inserts keys using that global handle and update any creation functions that return handles (map/set/decimal constructors and places referenced by symbols _map_store, _set_store, _decimal_store and host_gc_sweep) to consume the shared generator; ensure any existing places that assumed per-store counters are adjusted to use the unified handle allocation and that host_gc_sweep continues to check membership against those global integer handles.
🤖 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 `@ROADMAP.md`:
- Line 37: Update the `#570` entry to accurately reflect current implementation:
note that _translate_array_map() roots arr_ptr, fn_tmp, and dst once before the
loop and does not push each produced element onto the GC shadow stack per
iteration, so reword the failure mode accordingly (remove the incorrect claim
that each iteration pushes a new ptr). Also delete or soften the blanket
assertion that siblings like array_flatten share the pattern (array_flatten has
no callback path); suggest the correct mitigation options (perform per-iteration
shadow-stack unwind in array_map codegen or avoid per-element roots by writing
directly into the already-rooted dst) and keep the issue link to `#570`.
In `@tests/test_codegen.py`:
- Around line 15512-15633: The test only asserts pruning of _map_store but `#346`
also affects _set_store and _decimal_store; update
TestHostGcSweep346.test_transient_map_handles_freed_after_gc to also assert
exec_result.host_store_sizes.get("set") and
exec_result.host_store_sizes.get("decimal") have the expected sizes (likely 0 or
the appropriate retained count for this scenario) alongside the existing map
assertion so regression in Set/Decimal sweeping is caught; locate the test by
the method name test_transient_map_handles_freed_after_gc and add the two
mirrored assertions immediately after the map assertion.
- Around line 15498-15505: The Decimal fold test using array_fold with
accumulator `@Decimal` doesn't force a GC while the accumulator is live; modify
the fold callback (the anonymous fn used in array_fold that calls decimal_add
and decimal_from_int) to create deliberate heap pressure so a collection is
unavoidable (for example, allocate and populate a large temporary array or
perform many small allocations inside the fold body, or invoke the runtime
gc_collect API if available) so the rooting behavior is exercised during the
fold; ensure the added allocations are confined to the callback and don't change
the final numeric logic checked by decimal_eq.
In `@vera/browser/runtime.mjs`:
- Around line 2055-2123: The sweep currently uses a single numeric Set
candidates which mixes independent per-store handle namespaces (mapStore,
setStore, decimalStore) and lets low numeric handles from one store keep entries
in other stores alive; fix by tracking candidates per store: create separate
Sets (e.g., mapCandidates, setCandidates, decimalCandidates), populate the
appropriate per-store set when scanning heap/stack/iterating mapStore/setStore
(use the same logic that recognizes whether a numeric handle refers to
map/set/decimal), update the transitive-closure loops to iterate and add into
the matching per-store candidate Set (use mapStore, setStore, decimalStore as
the sources), and finally prune each store by checking its own candidate Set
rather than the global candidates.
- Around line 2050-2124: The host_gc_sweep implementation can run re-entrantly
(e.g., via wasm.alloc) and must bail out if already sweeping; add a
module-scoped boolean guard (e.g., hostGcSweeping) and wrap the body of
imports.vera.host_gc_sweep with: if (hostGcSweeping) return; hostGcSweeping =
true; try { ... } finally { hostGcSweeping = false; } so the logic that scans
heap/stack and prunes mapStore, setStore, decimalStore only runs when not
already in progress; update references to imports.vera.host_gc_sweep, mapStore,
setStore, decimalStore accordingly.
In `@vera/codegen/assembly.py`:
- Around line 262-277: The import gate for host_gc_sweep only triggers when
self._map_ops_used/_set_ops_used/_decimal_ops_used are true, but
write_json()/write_html() populate self._map_store without setting those flags
so JSON/HTML-backed map entries are never GC-scanned; update the condition in
assembly generation (the block guarded by self._needs_alloc and the tuple of op
flags, and the similar block at the second occurrence around the other import)
to also check for the presence of map-backed data (e.g., if self._map_store is
non-empty) or set a new boolean flag (e.g., self._map_store_used) inside
write_json() and write_html() and include that flag in the conditional, ensuring
the host_gc_sweep import is emitted when map-backed entries exist.
In `@vera/wasm/helpers.py`:
- Around line 265-268: The usage comment for _is_host_handle_type is stale;
update its docstring to state that the classifier IS consulted by GC-rooting
code paths—specifically mention that vera/wasm/context.py::translate_block()
uses _is_host_handle_type to root let-bound host handles—so readers are not
redirected to the old files (vera/codegen/functions.py and
vera/wasm/calls_arrays.py) as the sole rooting references; keep the note that it
documents which types are host-handles but explicitly note its current runtime
usage in translate_block.
---
Duplicate comments:
In `@vera/codegen/api.py`:
- Around line 1682-1688: The code currently uses separate per-store integer
counters for _map_store, _set_store, and _decimal_store while host_gc_sweep
tracks live handles as plain ints, causing cross-store collisions that prevent
proper pruning; change all stores to allocate handles from a single shared
counter/namespace (e.g., _next_handle or a shared make_handle() function) so
every store inserts keys using that global handle and update any creation
functions that return handles (map/set/decimal constructors and places
referenced by symbols _map_store, _set_store, _decimal_store and host_gc_sweep)
to consume the shared generator; ensure any existing places that assumed
per-store counters are adjusted to use the unified handle allocation and that
host_gc_sweep continues to check membership against those global integer
handles.
🪄 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: 9dccfedb-e0da-4ee0-b919-9ebe91efb111
📒 Files selected for processing (11)
ROADMAP.mdTESTING.mdtests/test_codegen.pyvera/browser/runtime.mjsvera/codegen/api.pyvera/codegen/assembly.pyvera/codegen/closures.pyvera/codegen/functions.pyvera/wasm/calls_arrays.pyvera/wasm/context.pyvera/wasm/helpers.py
Two critical bugs and several test/doc improvements.
Critical (2):
(188) vera/browser/runtime.mjs — re-entrancy guard
Mirror the Python `_in_host_alloc` flag in JS. Added
module-scoped `inHostAlloc` counter, incremented in
the wrapping `alloc()` helper, checked at the top of
`host_gc_sweep`. Without this, browser-compiled
programs would prune in-flight handles when the WASM
allocator triggered GC mid-host-helper.
(198) vera/codegen/assembly.py — JSON/HTML gates
`_map_store` is also populated by `write_json()`
(JObject keyed-by-string members) and `write_html()`
(HtmlElement attrs), but the original gate only
checked map/set/decimal_ops_used. A JSON-only or
HTML-only program would emit no host_gc_sweep at all,
so its backing map entries would leak for the entire
execute() lifetime. Extended the gate to include
json_ops_used and html_ops_used.
Doc accuracy (1):
(173) ROADMAP.md / KNOWN_ISSUES.md #570 description
My earlier description was wrong — array_map shadow-
pushes arr_ptr/fn_tmp/dst once before the loop, NOT
per-iteration. The actual leak comes from the lifted
closure's epilogue: it pushes the return-value root
AFTER the gc_sp restore (so the caller can root the
in-flight return), but iterative-builder callers
like array_map's loop store the return into dst[idx]
immediately and never unwind the post-restore push.
One slot leaks per iteration → 4096-element array_map
exhausts the shadow stack. Updated both ROADMAP.md
and KNOWN_ISSUES.md to describe the actual mechanism;
also corrected the sibling-builder list (array_flatten
has no callback path so isn't affected).
Test improvements (2):
(181) test_array_fold_with_decimal_accumulator — force GC
Added `let @array<Int> = array_range(100, 10100)`
inside the fold callback so each iteration allocates
~80 KB, forcing multiple $gc_collect cycles while
the @decimal accumulator is live. Bumped the iteration
count to 20 (sum 0..19 = 190) so the GC pressure
accumulates. Pre-fix this would prune the in-flight
accumulator and the next decimal_add would KeyError.
(186) test_transient_*_handles_freed_after_gc — Set + Decimal
Added two new tests mirroring the Map case for the
Set and Decimal stores. A regression isolated to
either Set or Decimal sweeping would have passed the
Map-only test pre-fix.
Doc (1):
(206) vera/wasm/helpers.py — _is_host_handle_type comment
The "isn't currently consulted" note was stale —
context.py's translate_block now uses the classifier
to decide whether a let-bound i32 needs an explicit
shadow_push. Updated to point readers at the actual
usage site.
Skipped (1):
(195) vera/browser/runtime.mjs — per-store candidate sets
Same shape as the Python-side handle-collision
finding (already skipped). Current shared-namespace
candidates set causes over-retention only (Map[5]
and Set[5] coexist; both retained when value 5 is in
candidates). Globally-unique handles or per-store
candidate sets would prune more aggressively but are
a precision improvement, not a correctness fix.
Test count: 3,700 → 3,702 (2 new tests; 1 modified to add
GC pressure).
ruff S clean, mypy clean, 82 conformance + 33 examples pass.
Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
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 `@HISTORY.md`:
- Line 269: Update the v0.0.132 HISTORY.md row to reflect that the change
introduces a shared classifier `_is_host_handle_type` (which treats Map, Set,
Decimal as host-handle candidates) combined with a new host import
`host_gc_sweep` that prunes transient handles after each `$gc_collect`, rather
than saying Map/Set/Decimal are simply excluded from GC rooting; mention
classification + sweep behavior and that host handles remain on the shadow stack
for `host_gc_sweep` to discover before pruning.
In `@tests/test_codegen.py`:
- Around line 15411-15459: Extend the test to cover Set and Decimal handle
parameters the same way as Map: for each of the types `@Set<_>` and `@Decimal`
create equivalent `src` snippets (or parameterize the existing one) that declare
a function like `lookup_or_zero` taking that handle as param 0, compile with
`_compile_ok`, extract the function body using the same `fn_start`/`fn_end`
logic, and assert the same `push_pattern` (`global.get $gc_sp` + `local.get 0` +
`i32.store`) is present. Keep the current `push_pattern` and error message but
update or duplicate it to refer to Set and Decimal (or generalize the message)
and ensure the new assertions run within `test_map_param_is_shadow_pushed` or in
new tests named e.g. `test_set_param_is_shadow_pushed` and
`test_decimal_param_is_shadow_pushed`.
- Around line 15541-15590: The existing assertions in
test_host_gc_sweep_sweep_import_emitted_for_map_program and
test_no_host_gc_sweep_for_pure_adt_program only check for the string
"host_gc_sweep" in result.wat which can be misleading if other output formats
(JSON/HTML) are emitting it; update the tests to explicitly compile/request WAT
output (call _compile_ok with an explicit output format or assert the result is
WAT) before checking result.wat and ensure the negative test similarly confirms
no host_gc_sweep in WAT only—modify usages of _compile_ok and the checks around
result.wat in these two tests
(test_host_gc_sweep_sweep_import_emitted_for_map_program and
test_no_host_gc_sweep_for_pure_adt_program) so the gate is exercised against WAT
output only.
In `@vera/codegen/assembly.py`:
- Around line 627-633: Update the Phase 4 docstring to reflect the actual
arguments passed to the imported $host_gc_sweep call when uses_host_handles is
True: it pushes four globals—gc_heap_start, heap_ptr, gc_stack_base, and
gc_sp—so replace the current mention of "(gc_heap_start, heap_ptr)" with the
correct four-tuple or list of parameters and mention $host_gc_sweep and
uses_host_handles to make the behavior explicit.
🪄 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: 2dc85870-0112-4988-bba4-9d03fac1f770
📒 Files selected for processing (9)
HISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdtests/test_codegen.pyvera/browser/runtime.mjsvera/codegen/assembly.pyvera/wasm/helpers.py
Closes #347 and #490 — the codegen-time half of the opaque-handle hygiene work. #346 (active reclamation half — host-store leak) is deferred and tracked separately at #573. This is a much smaller PR than an earlier draft attempted: that draft tried to close all three issues with a host_gc_sweep mechanism (heap walk + shadow stack scan + transitive closure + re-entrancy guard + let-binding shadow_push + JSON/HTML emission gates) but the resulting six-piece design had too much complexity for the practical impact. This revision drops that machinery and ships only the cleanly-fixable codegen-time work. Shared infrastructure ===================== New _is_host_handle_type classifier in vera/wasm/helpers.py distinguishes types that lower to i32 indices into Python-side host stores (Map / Set / Decimal) from real Vera-heap pointers. #347 — opaque handles no longer pushed to GC shadow stack ========================================================= Pre-fix vera/codegen/functions.py and vera/codegen/closures.py excluded only Bool / Byte from gc_pointer_params, so a Map / Set / Decimal parameter (i32 handle index) was treated as a heap pointer and pushed onto the shadow stack at every function entry. Wasted shadow-stack space; a handle value that landed in the heap-pointer range with valid alignment would have caused the conservative mark phase to spuriously mark an unrelated heap object as live (memory retention, not corruption). Post-fix the new classifier excludes opaque handles at four rooting decision sites: top-level params + return type in functions.py, closure params + captures + return type in closures.py. #490 — array_fold/array_map no longer over-root opaque accumulators ==================================================================== Pre-fix the u_is_adt/t_is_adt heuristics in calls_arrays.py classified Map/Set/Decimal accumulators as ADT pointers. Same classifier excludes them post-fix. Tests ===== Two new test classes (3 tests): TestOpaqueHandleParamRooting347 — pins #347 fix via WAT inspection test_map_param_not_shadow_pushed: the canonical local.get $p0; i32.store shadow-push idiom is absent for a Map parameter. TestArrayFoldHandleRooting490 — pins #490 fix test_decimal_accumulator_not_rooted: global.set $gc_sp count parity between Int and Decimal fold accumulators. test_array_fold_with_decimal_runs_correctly: functional pin. Test count: 3,695 -> 3,698. mypy clean, ruff S clean, 82 conformance programs pass, 33 examples pass, browser parity passes. #346 follow-up ============== The active-reclamation half (the actual host-store leak) is deferred to #573. Recommended path: heap-wrap-as-ADT — have handle-creating ops return a Vera-heap MapHandle(i32) so the existing mark-sweep GC handles reclamation via a destructor callback. Single mechanism instead of six interlocking pieces; integrates with mature infrastructure. Co-Authored-By: Claude <noreply@anthropic.invalid>
6fb0711 to
a270514
Compare
|
@coderabbitai Reset notice: This PR has been force-pushed to a substantially smaller scope after maintainer feedback that the original design was "overly complex (but correct)". What changedThe earlier draft (commits
All three were correctly identified and fixed. But the cumulative complexity (six interlocking pieces: heap walk, shadow-stack scan, transitive closure, re-entrancy guard, let-binding shadow_push, JSON/HTML gates) was disproportionate to the actual practical impact — What this PR ships nowOnly the codegen-time half: No What
|
|
Tip For best results, initiate chat on the files or code changes.
A few observations on the revised scope before the review runs: The simplification is the right call. The six-piece The The heap-wrap-as-ADT path for I'll flag anything on the classifier logic, its call sites in [full_review] |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
vera/wasm/calls_arrays.py (1)
2043-2050:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winBring the sort-by GC-rooting comment back into sync with the code.
The block above still says
Map/Set/Regex/Decimalflow throught_needs_root, but the new predicate now explicitly excludes host handles, andRegexis not a value type here. Leaving that comment stale documents the opposite invariant from the implementation on a very GC-sensitive path.Also applies to: 2150-2157
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/wasm/calls_arrays.py` around lines 2043 - 2050, The comment describing GC-rooting for sort-by is out of sync: it claims Map/Set/Regex/Decimal are handled via the t_needs_root path and that Regex is a value type, but the current predicate explicitly excludes host handles and Regex is not a value type; update the prose around the t_needs_root, tmp_a, tmp and comparator call_indirect discussion (and the similar block at 2150-2157) to accurately reflect that host handles are excluded from t_needs_root, list the exact types that do require shadow-stack rooting (e.g., pair-typed T like String/Array<X> via tmp_a, and ADTs/user handles via tmp), and remove/replace the incorrect references to Regex and host-handle flow so the comment matches the implementation.
♻️ Duplicate comments (2)
tests/test_codegen.py (2)
15481-15568:⚠️ Potential issue | 🟠 Major | ⚡ Quick winThis
Decimalfold regression still misses the real failure mode.The structural check encodes the wrong invariant for host handles, and the runtime test never forces a collection while the
Decimalaccumulator is live. That means this block can stay green even whenarray_foldletshost_gc_sweepdrop the in-flight accumulator.Suggested fix
- def test_decimal_accumulator_not_rooted(self) -> None: - """`array_fold` over `Decimal` accumulator should NOT emit - the per-iteration accumulator root that ADT accumulators - get. - - Structural pin via `global.set $gc_sp` count: the same - fold over `Int` (no rooting needed) and `Decimal` (post- - fix: also no rooting per `#490`) should produce the same - count of `global.set $gc_sp` idioms in `$main`. An ADT - accumulator (which IS a Vera-heap pointer) would emit one - more. This test pins that Decimal counts match Int. - """ - # Reference: fold over Int (no rooting needed) — baseline - int_src = """ -public fn main(`@Unit` -> `@Int`) - requires(true) ensures(true) effects(pure) -{ - array_fold( - array_range(0, 5), - 0, - fn(`@Int`, `@Int` -> `@Int`) effects(pure) { `@Int.0` + `@Int.1` } - ) -} -""" - # Test: fold over Decimal (post-fix: also no rooting per `#490`) - decimal_src = """ -public fn main(`@Unit` -> `@Decimal`) - requires(true) ensures(true) effects(pure) -{ - array_fold( - array_range(0, 5), - decimal_from_int(0), - fn(`@Decimal`, `@Int` -> `@Decimal`) effects(pure) { - decimal_add(`@Decimal.0`, decimal_from_int(`@Int.0`)) - } - ) -} -""" - int_wat = _compile_ok(int_src).wat - decimal_wat = _compile_ok(decimal_src).wat - - def count_main_pushes(wat: str) -> int: - fn_start = wat.index("(func $main") - if "\n (func " in wat[fn_start + 1:]: - fn_end = wat.index("\n (func ", fn_start + 1) - else: - fn_end = len(wat) - return wat[fn_start:fn_end].count("global.set $gc_sp") - - int_count = count_main_pushes(int_wat) - decimal_count = count_main_pushes(decimal_wat) - assert decimal_count == int_count, ( - f"`array_fold` over a Decimal accumulator emits " - f"{decimal_count} `global.set $gc_sp` idioms in $main " - f"vs. {int_count} for an Int accumulator. Post-#490 " - f"these should be equal — Decimal is an opaque host " - f"handle, not a Vera-heap pointer, and shouldn't be " - f"rooted around the call_indirect. An extra count " - f"means `_is_host_handle_type` isn't firing in the " - f"`u_is_adt` heuristic." - ) - - def test_array_fold_with_decimal_runs_correctly(self) -> None: - """Functional pin: the fold over Decimal still produces the - right result. Pre- and post-fix this works (the - conservative GC's heap-range check rejects small handle - values either way), so this test passes in both states — - but it pins that the structural optimisation didn't break - anything. - - Sum 0+1+2+3+4 = 10; comparing to `decimal_from_int(10)` - via `decimal_eq` returns 1. - """ + def test_array_fold_with_decimal_accumulator_survives_gc(self) -> None: + """Force GC while the Decimal accumulator is live.""" src = """ public fn main(`@Unit` -> `@Int`) requires(true) ensures(true) effects(pure) { let `@Decimal` = array_fold( - array_range(0, 5), + array_range(0, 20), decimal_from_int(0), fn(`@Decimal`, `@Int` -> `@Decimal`) effects(pure) { + let `@Array`<Int> = array_range(100, 10100); decimal_add(`@Decimal.0`, decimal_from_int(`@Int.0`)) } ); - if decimal_eq(`@Decimal.0`, decimal_from_int(10)) then { 1 } else { 0 } + if decimal_eq(`@Decimal.0`, decimal_from_int(190)) then { 1 } else { 0 } } """ assert _run(src) == 1Based on learnings: Decimal-result tests should use
_runwithdecimal_eq(...), and handle-rooting regressions need deliberate GC pressure such asarray_range(100, 10100)inside the fold body.
15409-15459:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFlip this regression:
Mapparams must stay shadow-rooted.This assertion is backwards for the current host-handle model.
Map/Set/Decimalparams need the shadow-push sohost_gc_sweepcan discover a live host-store handle during/after$gc_collect; asserting that the push is absent bakes the reverted bug into the suite.Suggested fix
-class TestOpaqueHandleParamRooting347: - """`#347`: opaque host handles (Map / Set / Decimal) MUST NOT be - pushed to the GC shadow stack as roots when they appear as - function parameters. +class TestOpaqueHandleParamRooting347: + """`#347`: opaque host handles (Map / Set / Decimal) must stay + discoverable by `host_gc_sweep` when passed as function + parameters. @@ - def test_map_param_not_shadow_pushed(self) -> None: - """A `Map<Nat, Nat>` parameter must not appear in any - gc_shadow_push sequence in the function's prologue. + def test_map_param_is_shadow_pushed(self) -> None: + """A `Map<Nat, Nat>` parameter must be shadow-pushed in the + function prologue. @@ - assert not push_pattern.search(fn_body), ( - "Found a shadow_push of param 0 (the Map handle) in " - "lookup_or_zero — the opaque-handle exclusion (`#347`) " - "isn't being applied. Map / Set / Decimal handles are " - "i32 indices into Python-side stores, not Vera-heap " - "pointers; rooting them wastes shadow-stack space and " - "could cause spurious heap-object retention via the " - "conservative GC's heap-range check." + assert push_pattern.search(fn_body), ( + "Map handle parameter $p0 was not shadow-pushed. Live " + "host handles must stay on the shadow stack so " + "`host_gc_sweep` can find them during/after " + "`$gc_collect`." )Based on learnings: Map/Set/Decimal are opaque host-handle types and MUST be shadow-stack rooted when passed as function parameters so
host_gc_sweepcan discover live handles.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_codegen.py` around lines 15409 - 15459, The test test_map_param_not_shadow_pushed is asserting the wrong behavior: Map/Set/Decimal params must be shadow-pushed so change the assertion to require the canonical shadow_push sequence is present; in the test body (function lookup_or_zero) replace the negative check "assert not push_pattern.search(fn_body)" with a positive check "assert push_pattern.search(fn_body)" and update the failure message to state that the Map param (param 0) must be shadow-rooted so host_gc_sweep can find live host-store handles; keep references to push_pattern, lookup_or_zero and the host-handle exclusion/_is_host_handle_type rationale.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@HISTORY.md`:
- Line 269: Update the v0.0.132 release row for "Opaque-handle GC-rooting
hygiene" to explicitly reference the deferred follow-up issue `#573`: replace or
augment the phrase "`#346` deferred — active reclamation needs a
heap-wrap-as-ADT design, tracked as follow-up" with wording that includes a link
to issue `#573` (e.g., "tracked as follow-up (see `#573`)") so the release note
for v0.0.132 clearly names the concrete deferred issue.
In `@vera/codegen/functions.py`:
- Around line 129-133: The i32 branch guarding GC-pointer rooting misses Json
because _is_host_handle_type() doesn't currently classify Json; update the
classifier so Json is considered a host-handle (i.e., add "Json" to the set of
types returned true by _is_host_handle_type or otherwise extend its logic) so
the condition in the if-block that checks wt == "i32" and type_name not in
("Bool","Byte", None) will correctly exclude Json; apply the same fix for the
identical pattern used around the wt/type_name check at the other occurrence
(the one referenced at 285-288) so Json is consistently treated as an opaque
host handle across both branches.
---
Outside diff comments:
In `@vera/wasm/calls_arrays.py`:
- Around line 2043-2050: The comment describing GC-rooting for sort-by is out of
sync: it claims Map/Set/Regex/Decimal are handled via the t_needs_root path and
that Regex is a value type, but the current predicate explicitly excludes host
handles and Regex is not a value type; update the prose around the t_needs_root,
tmp_a, tmp and comparator call_indirect discussion (and the similar block at
2150-2157) to accurately reflect that host handles are excluded from
t_needs_root, list the exact types that do require shadow-stack rooting (e.g.,
pair-typed T like String/Array<X> via tmp_a, and ADTs/user handles via tmp), and
remove/replace the incorrect references to Regex and host-handle flow so the
comment matches the implementation.
---
Duplicate comments:
In `@tests/test_codegen.py`:
- Around line 15409-15459: The test test_map_param_not_shadow_pushed is
asserting the wrong behavior: Map/Set/Decimal params must be shadow-pushed so
change the assertion to require the canonical shadow_push sequence is present;
in the test body (function lookup_or_zero) replace the negative check "assert
not push_pattern.search(fn_body)" with a positive check "assert
push_pattern.search(fn_body)" and update the failure message to state that the
Map param (param 0) must be shadow-rooted so host_gc_sweep can find live
host-store handles; keep references to push_pattern, lookup_or_zero and the
host-handle exclusion/_is_host_handle_type rationale.
🪄 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: 6fc9b9cd-9f66-4be2-adc5-495fd11921c0
⛔ Files ignored due to path filters (5)
docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**uv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (13)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdpyproject.tomltests/test_codegen.pyvera/__init__.pyvera/codegen/closures.pyvera/codegen/functions.pyvera/wasm/calls_arrays.pyvera/wasm/helpers.py
…t + Decimal Two cleanups responding to maintainer + CodeRabbit feedback after the v0.0.132 reset: 1. Close #346 as superseded by #573 The previous commit kept #346 in the priority table with a "→ #573" pointer, which was muddled — two issues tracking the same bug. This commit closes #346 as superseded (via gh issue close), updates the ROADMAP priority table + campaign tracker prose to reference #573 only, and updates the KNOWN_ISSUES tracking-ID column to #573 (with #346 mentioned only as a parenthetical "originally tracked at #346"). HISTORY + CHANGELOG language updated from "deferred" to "superseded by #573" to match the GitHub state. 2. Extend TestOpaqueHandleParamRooting347 to Set + Decimal CodeRabbit (PR #572 round 3) noted that the test only covered Map parameters, leaving Set + Decimal uncovered. Refactored the pattern check into a static helper `_assert_param0_not_shadow_pushed` and added two new tests: test_set_param_not_shadow_pushed — Set<Nat> param via contains_or_false fn test_decimal_param_not_shadow_pushed — Decimal param via is_positive_or_false fn A regression isolated to either Set or Decimal codegen would now be caught (previously only the Map path was pinned). Test count: 3,698 -> 3,700. mypy clean, ruff S clean, full pytest + 82 conformance + 33 examples pass. Other CodeRabbit findings from round 3 (HISTORY framing, host_gc_sweep import test, Phase 4 docstring) reference the previous-design code that was reverted in the simplification — skipped as moot. Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
ROADMAP.md (2)
11-11:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSync the remaining stale test-count reference.
ROADMAP.mdnow says 3,700 tests here, but the CI tooling table still says “3,658 tests” (Line 217). Please align that downstream reference (or keep it non-numeric, e.g. “the test suite”) to avoid drift.Based on learnings, the canonical test count in this repo should come from
pytest tests/ --collect-only -qorpython scripts/check_doc_counts.py.🤖 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 `@ROADMAP.md` at line 11, Update the stale test-count in the CI tooling table to match the canonical count used elsewhere (change “3,658 tests” to “3,700 tests” or replace with a non-numeric phrase like “the test suite”) so ROADMAP.md is consistent; locate the CI tooling table text that currently reads “3,658 tests” and edit it to either the numeric 3,700 or a non-numeric reference, and add/update a short comment or note (if present) to indicate the canonical source for the count is produced by running `pytest tests/ --collect-only -q` or `python scripts/check_doc_counts.py`.
25-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the stale open-issue reference for
#490in this same roadmap.This line says
#490was closed in v0.0.132, but the “Compiler internals” section still lists#490as open (Line 244). Please delete or update that backlog row so the roadmap is internally consistent.As per coding guidelines, “Review Markdown files for factual accuracy against the codebase, broken links, and outdated information.”
🤖 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 `@ROADMAP.md` at line 25, The ROADMAP.md entry incorrectly lists issue `#490` as still open under the "Compiler internals" backlog; open ROADMAP.md, find the "Compiler internals" section and remove or update the backlog row that references `#490` (it was closed in the v0.0.132 note), ensuring the backlog no longer lists `#490` as open and that any related row/entry is adjusted for consistency with the v0.0.132 closure mentioned in the v0.0.132 changelog paragraph.
🤖 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 `@KNOWN_ISSUES.md`:
- Line 9: Update the KNOWN_ISSUES.md sentence that reads "v0.0.132 attempted a
`host_gc_sweep` design but the resulting six-piece interlocking machinery..." to
unambiguously state the timeline: say that an earlier draft of v0.0.132
attempted the `host_gc_sweep` design but it was reverted during development and
did not ship in v0.0.132, and then mention that the released v0.0.132 only
included the codegen fixes for `#347` and `#490`; keep the rest of the description
about the six-piece machinery and the recommended heap-wrap-as-ADT path
unchanged.
In `@tests/test_codegen.py`:
- Around line 15421-15434: The shadow-push detector currently only matches
"local.get 0" which misses forms like "local.get $p0" and also lacks a
non-vacuous check that GC prologue exists in the target function; update the
logic around result.wat/fn_body and push_pattern so the regex accepts both
numeric and named local.get forms (e.g., allow "local.get 0" or "local.get
$name"/any "$..." identifier) and after slicing fn_body confirm the prologue was
actually found (i.e., assert that the push_pattern search/match is non-empty)
before treating the push as detected; adjust the symbol references in tests to
use push_pattern, fn_body, fn_marker, and _compile_ok accordingly.
---
Duplicate comments:
In `@ROADMAP.md`:
- Line 11: Update the stale test-count in the CI tooling table to match the
canonical count used elsewhere (change “3,658 tests” to “3,700 tests” or replace
with a non-numeric phrase like “the test suite”) so ROADMAP.md is consistent;
locate the CI tooling table text that currently reads “3,658 tests” and edit it
to either the numeric 3,700 or a non-numeric reference, and add/update a short
comment or note (if present) to indicate the canonical source for the count is
produced by running `pytest tests/ --collect-only -q` or `python
scripts/check_doc_counts.py`.
- Line 25: The ROADMAP.md entry incorrectly lists issue `#490` as still open under
the "Compiler internals" backlog; open ROADMAP.md, find the "Compiler internals"
section and remove or update the backlog row that references `#490` (it was
closed in the v0.0.132 note), ensuring the backlog no longer lists `#490` as
open and that any related row/entry is adjusted for consistency with the
v0.0.132 closure mentioned in the v0.0.132 changelog paragraph.
🪄 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: faf33f0c-94a6-412e-b451-d2141f0e003c
📒 Files selected for processing (7)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdtests/test_codegen.py
…fixed
Inline (2):
KNOWN_ISSUES line 9 — timeline language clarified
Pre-fix: "v0.0.132 attempted a host_gc_sweep design but the
resulting six-piece interlocking machinery was too complex"
— ambiguous about whether it shipped.
Post-fix: "An earlier draft of v0.0.132 attempted a host_gc_sweep
design but it was reverted during development — v0.0.132 as
released only ships the codegen-time #347 + #490 fixes, not
active reclamation." Then explains the reverted design's
complexity. Unambiguous about what shipped vs. what was tried.
test_codegen.py — TestOpaqueHandleParamRooting347 helper hardened
Two improvements to _assert_param0_not_shadow_pushed:
1. Non-vacuity check: assert the function's GC prologue WAS
emitted (`global.get $gc_sp; local.set ...` pattern) before
checking absence of param-0 push. Without this, a function
with no allocator activity passes trivially regardless of
whether the exclusion fires.
2. Push pattern accepts both numeric (`local.get 0`) and named
(`local.get $p0`, `local.get $name`) forms. Codegen currently
emits numeric, but a future rename to named params shouldn't
silently break this regression test.
Tests still pass (3 host-handle types × hardened check = 3/3).
Duplicates (2 — regressions from the force-reset to main that lost
earlier fixes):
ROADMAP CI tooling table — "3,658 tests" → "the test suite"
The earlier complex draft (commits aa45b89..6fb0711) had this
fixed. When the branch was force-reset to main and reimplemented
cleanly, the earlier fix was lost. Re-applied: replaced the
hardcoded count with a non-numeric phrase to avoid future drift.
ROADMAP Compiler internals section — stale #490 row removed
Same regression pattern. #490 closed in v0.0.132; the backlog
row "Tighten GC-rooting heuristic in iterative combinators"
contradicts the campaign-tracker prose that says #490 closed.
Removed the row (it was the only entry in "Compiler internals")
and the section header along with it.
Test count: 3,700 → 3,700 (no new tests; helper restructured).
mypy clean, ruff S clean, full pytest + 82 conformance + 33 examples
pass.
Co-Authored-By: Claude <noreply@anthropic.invalid>
|
@coderabbitai On the 2 duplicate findings from review 4231171595:\n\nROADMAP CI tooling table — |
|
Tip For best results, initiate chat on the files or code changes.
The "audit previously-fixed comments after a force-reset" note is a good process observation — the diff-scope change makes it easy to lose track of which editorial fixes were in the discarded chain. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/test_codegen.py (1)
15560-15666: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd a mirrored
array_mapregression test for the handle-classifier path.This block validates
array_foldonly, but#490also touches thearray_maprooting heuristic (t_is_adt). Please add one structural/functionalarray_mapcase with a host-handle type so a regression isolated toarray_mapcannot pass unnoticed. As per coding guidelines: "DO flag: Missing edge cases for new compiler features."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_codegen.py` around lines 15560 - 15666, Add a mirrored test for array_map similar to test_decimal_accumulator_not_rooted: the issue is missing coverage for the array_map path (t_is_adt) so regressions there can slip by. Create a new test (e.g. test_decimal_mapper_not_rooted) that compiles two sources—one mapping to an Int accumulator and one mapping to a Decimal host-handle—and uses the same global.set $gc_sp counting helper (count_main_pushes) to assert the Decimal case emits the same number of global.set $gc_sp idioms as the Int case; also add a functional run test mirroring test_array_fold_with_decimal_runs_correctly to ensure array_map produces the correct Decimal results. Target symbols to modify/add: test_decimal_accumulator_not_rooted (as model), count_main_pushes, array_map, and ensure the classifier exercised is t_is_adt.
♻️ Duplicate comments (1)
tests/test_codegen.py (1)
15578-15638: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winReplace the
global.set $gc_spcount proxy with a direct accumulator shadow-push pattern check.Counting
global.set $gc_spin$mainis fragile and can flip on unrelated GC prologue/epilogue changes. Please anchor this to the concrete push idiom for the fold accumulator in the generated fold helper, as you already do for param-0 in the#347tests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_codegen.py` around lines 15578 - 15638, In test_decimal_accumulator_not_rooted replace the brittle global.set $gc_sp counting logic (the count_main_pushes function and its uses on int_wat/decimal_wat) with a direct check for the accumulator shadow-push idiom inside the generated fold helper: locate the test function test_decimal_accumulator_not_rooted, call _compile_ok(...) to get wat, find the fold helper function emitted for array_fold (same helper referenced by the `#347` param-0 tests), and assert the presence/absence (or equal counts) of the concrete shadow-push instruction sequence used for the fold accumulator (the same pattern you use in the `#347` tests) rather than counting "global.set $gc_sp" in $main; update count_main_pushes to scan the fold helper body for that exact shadow-push pattern and compare int_wat vs decimal_wat.
🤖 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 `@tests/test_codegen.py`:
- Around line 15560-15666: Add a mirrored test for array_map similar to
test_decimal_accumulator_not_rooted: the issue is missing coverage for the
array_map path (t_is_adt) so regressions there can slip by. Create a new test
(e.g. test_decimal_mapper_not_rooted) that compiles two sources—one mapping to
an Int accumulator and one mapping to a Decimal host-handle—and uses the same
global.set $gc_sp counting helper (count_main_pushes) to assert the Decimal case
emits the same number of global.set $gc_sp idioms as the Int case; also add a
functional run test mirroring test_array_fold_with_decimal_runs_correctly to
ensure array_map produces the correct Decimal results. Target symbols to
modify/add: test_decimal_accumulator_not_rooted (as model), count_main_pushes,
array_map, and ensure the classifier exercised is t_is_adt.
---
Duplicate comments:
In `@tests/test_codegen.py`:
- Around line 15578-15638: In test_decimal_accumulator_not_rooted replace the
brittle global.set $gc_sp counting logic (the count_main_pushes function and its
uses on int_wat/decimal_wat) with a direct check for the accumulator shadow-push
idiom inside the generated fold helper: locate the test function
test_decimal_accumulator_not_rooted, call _compile_ok(...) to get wat, find the
fold helper function emitted for array_fold (same helper referenced by the `#347`
param-0 tests), and assert the presence/absence (or equal counts) of the
concrete shadow-push instruction sequence used for the fold accumulator (the
same pattern you use in the `#347` tests) rather than counting "global.set $gc_sp"
in $main; update count_main_pushes to scan the fold helper body for that exact
shadow-push pattern and compare int_wat vs decimal_wat.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 59663bda-8667-487a-a5f1-c52dad2f77c4
📒 Files selected for processing (7)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdtests/test_codegen.py
…ardening
Two changes responding to outside-diff and duplicate findings on
TestArrayFoldHandleRooting490:
1. Add array_map mirror — test_decimal_mapper_not_rooted
The existing test_decimal_accumulator_not_rooted only exercised the
array_fold u_is_adt heuristic. array_map has its own t_is_adt
heuristic (calls_arrays.py:2151) that gates element rooting; a
regression isolated there would have slipped past. New test
compiles `array_length(array_map(0..5, fn(i) { decimal_from_int(i) }))`
alongside an Int reference and asserts equal global.set $gc_sp
counts in $main.
Plus a new functional regression test_array_map_with_decimal_runs_correctly
that builds a Decimal array via array_map, folds it, and asserts
the sum (`array_map([0..5), fn(i) { decimal_from_int(i*2) })` →
`[0, 2, 4, 6, 8]` → folded sum 20).
2. Refactor count_main_pushes into a shared helper + add non-vacuity
assertion
Pulled count_main_pushes out of the inline closure into a class-
level @staticmethod _count_main_pushes, and added a new helper
_assert_handle_not_extra_rooted that:
- asserts int_count > 0 (non-vacuity — without this, a degenerate
case where neither version emits any pushes would pass with
0 == 0, exactly the same vacuity concern CodeRabbit raised on
the param-rooting test in an earlier round)
- asserts decimal_count == int_count (post-fix equality)
Both fold and map tests now go through the same helper, so a future
structural shift in count_main_pushes can't silently break either.
Test count: 3,700 → 3,702 (2 new tests; helper extracted).
mypy clean, ruff S clean, full pytest + 82 conformance + 33 examples
pass.
Co-Authored-By: Claude <noreply@anthropic.invalid>
|
@coderabbitai Both findings from review 4231366577 addressed in e3a1f23: Outside-diff: array_map mirror test ( Added Also added Duplicate: brittle count check ✅ Refactored to address the underlying concern. Note on "fold helper function" in the original suggestion: there isn't actually a separate fold helper — Test count: 3,700 → 3,702. ruff S clean, mypy clean, full pytest + 82 conformance + 33 examples pass. |
|
Tip For best results, initiate chat on the files or code changes.
array_map mirror test — Brittle count / shared helper — The refactor is the right shape. The clarification on 3,702 tests, ruff S + mypy clean, full conformance suite passing — clean state. |
Summary
_is_host_handle_typeclassifier invera/wasm/helpers.py.What this PR ships
vera/codegen/functions.py:123+ 3 sites invera/codegen/closures.py_is_host_handle_typeto excludeMap/Set/Decimalfromgc_pointer_paramsarray_fold/array_mapu_is_adtandt_is_adtheuristics invera/wasm/calls_arrays.pyWhy #346 is deferred
The original issue tracker grouped #346 + #347 + #490 under "opaque-handle hygiene" with a comment that "fix is a shared 'is-this-actually-a-Vera-heap-pointer' classifier." In practice the three issues split into two fundamentally different problems:
gc_shadow_pushsite, do we treat this i32 as a Vera-heap pointer? Solved by a 5-line classifier at each site.An earlier draft of this PR (forced-pushed away in this revision) attempted to close all three by adding a
host_gc_sweephost import that walked the live Vera heap + shadow stack. The design grew six interlocking pieces: heap walk, shadow-stack scan, transitive closure through store contents, re-entrancy guard, let-binding shadow_push, JSON/HTML emission gates. Each piece was necessary for the previous one to work. CodeRabbit's review caught two critical correctness bugs that would have shipped to browser users.The complexity was disproportionate to the practical impact: the leak is bounded by Python GC at
execute()exit, and Vera doesn't yet have long-running execution contexts where the leak would matter. Single-shot programs (vera run, conformance, examples) are unaffected.The recommended follow-up design (tracked in #573) is heap-wrap-as-ADT: have handle-creating ops return a Vera-heap
MapHandle(i32)so the existing mark-sweep GC handles reclamation via a destructor callback. One mechanism instead of six interlocking pieces; integrates with infrastructure that's already mature.Tests
TestOpaqueHandleParamRooting347local.get $p0; i32.storeshadow-push idiom is absent for aMap<K, V>parameterTestArrayFoldHandleRooting490global.set $gc_spcount parity between Int and Decimal accumulators) + functional regressionTest plan
mypy vera/cleanruff check --select S vera/cleanCloses #347
Closes #490
(Defers #346 — see #573 for the active-reclamation follow-up.)
Summary by CodeRabbit
Bug Fixes
Tests
Documentation