Fix #692: host-walker GC rooting — release v0.0.158#693
Conversation
Closes #692. html_parse, json_parse, and md_parse no longer trap with Out-of-bounds memory access on inputs large enough to pressure GC during host-side tree marshalling. Root cause: missing- shadow-stack rooting in vera/wasm/html_serde.py::write_html, vera/wasm/json_serde.py::write_json, and vera/wasm/markdown.py::write_md_block/write_md_inline — same #570 / #515 / #593 bug class but on the host side rather than WAT-emitted user code. Python-held intermediate pointers (arr_ptr / name_ptr / wrapper_ptr) were invisible to the conservative GC scan; if a sub-walk triggered $gc_collect those blocks were reclaimed and subsequent writes corrupted the free list, trapping at 0xfffffffd inside $alloc's free-list traversal. Fix design (Option 3 from the issue body — architectural): 1. Export $gc_sp and $gc_stack_limit as globals from the emitted WAT module so host imports can manipulate the shadow-stack pointer. Inline export syntax on the globals; WAT-side gc_shadow_push helper for user code unchanged. 2. New _ShadowGuard context manager in vera/codegen/api.py. On __enter__ snapshots $gc_sp; on __exit__ resets $gc_sp to the snapshot — atomically popping every push in the block, on both success and exception paths. Callers push freely without counting. 3. Thread guard parameter through write_html, write_json, write_md_block, write_md_inline, and the markdown array helpers (_write_inline_array, _write_block_array, _write_array_of_block_arrays, _write_table_data). Each intermediate alloc pushes its pointer onto the guard before any subsequent alloc that could trigger GC. 4. Markdown walkers also adopt the fields-first-then-body convention — allocate child contents first (rooted via guard), then allocate the body last so the body pointer is never held across another alloc. 5. host_html_parse, host_html_query, host_json_parse, host_md_parse wrap the walk + final Result.Ok alloc in `with _ShadowGuard(caller) as guard:`. Tests: - tests/conformance/ch09_host_walker_gc_rooting.vera (run- level, 4 sub-tests) for html_parse (500 element siblings), json_parse (1000-element number array + 500-element string array), and md_parse (200 H1 + paragraph blocks). Manifest entry added — conformance suite now 88 programs. - tests/test_codegen.py::TestHostWalkerGCRooting692 — 4 in- process regression tests for the same scenarios at the codegen layer, alongside the existing host-side GC-rooting regression classes. - Updated tests/test_codegen.py::TestWorklistOverflow348 structural assertion to match the new exported $gc_stack_limit WAT shape. Release prep (per the version-bump-in-feature-PR rule): - Version 0.0.157 → 0.0.158 in pyproject.toml, vera/__init__.py, README.md, docs/index.html, uv.lock. - CHANGELOG cut: [Unreleased] → [0.0.158] - 2026-05-19 with link references for both the new version and the bumped Unreleased pointer. - HISTORY.md row + release counter (157 → 158 tagged). - 87 → 88 conformance count sweep across CLAUDE.md, AGENTS.md, SKILL.md, TESTING.md, FAQ.md, docs/index.html, ROADMAP.md. Validation: 3,815 pytest pass + 99 browser pass, mypy clean, ruff S clean, 88 conformance programs pass, 35 examples pass, all 11 doc validators green, version sync clean across 6 files, doc counts consistent. Co-Authored-By: Claude <noreply@anthropic.invalid>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #693 +/- ##
==========================================
+ Coverage 90.88% 90.90% +0.02%
==========================================
Files 60 60
Lines 23529 23606 +77
Branches 259 259
==========================================
+ Hits 21384 21459 +75
- Misses 2138 2140 +2
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:
|
|
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:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughExports WASM shadow-stack globals and adds a host-side ChangesHost-side GC shadow-stack rooting for tree marshalling
Sequence DiagramsequenceDiagram
participant Host
participant ShadowGuard as _ShadowGuard
participant Serde as SerdeWriter
participant WASM as WASM_memory
participant GC as gc_collect
Host->>ShadowGuard: __enter__ (snapshot gc_sp)
Host->>Serde: write(..., guard, input)
Serde->>WASM: alloc(field_size)
WASM-->>Serde: field_ptr
Serde->>ShadowGuard: push(field_ptr)
Serde->>WASM: alloc(body_size)
WASM->>GC: gc_collect (if memory full)
GC->>WASM: scan shadow_stack (sees field_ptr)
GC-->>WASM: mark field_ptr live
WASM-->>Serde: body_ptr
Serde->>Serde: recurse with same guard
Host->>ShadowGuard: __exit__ (restore gc_sp)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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)
TESTING.md (1)
193-193:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the parametrised test count from 435 to 440.
The comment still references the old count (87 programmes × 5 stages = 435 tests). With the new conformance test added, this should be 88 × 5 = 440 tests, matching the table entry at line 82.
📝 Proposed fix
-# Via pytest (parametrized — 435 tests) +# Via pytest (parametrized — 440 tests)As per coding guidelines, when updating conformance counts, all literal occurrences of the old number must be updated consistently across the file.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@TESTING.md` at line 193, Update the hard-coded test count in the TESTING.md header string "# Via pytest (parametrized — 435 tests)" to reflect the new total of 440 tests (88 programmes × 5 stages) and ensure any other literal occurrences of "435" in this file are changed to "440" so the textual count matches the table entry and new conformance test; search for the exact header text and any standalone "435" tokens in TESTING.md and replace them with "440".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@vera/codegen/api.py`:
- Around line 1791-1803: The new guarded marshalling using _ShadowGuard and
write_md_block is currently inside the outer except that converts all exceptions
into a Result.Err(String) for md_parse; update md_parse so that construction/use
of _ShadowGuard and the call to write_md_block (and the call to
_alloc_result_ok_i32 on success) are executed outside the broad
exception-to-Result conversion window so host-side invariant failures (e.g.
shadow-stack overflows raised by _ShadowGuard) propagate as real exceptions
instead of being swallowed; keep only true user/parse error handling inside the
except that produces Result.Err(String), and let any exceptions from
_ShadowGuard/write_md_block escape (or re-raise them) rather than being turned
into a parse error.
In `@vera/wasm/markdown.py`:
- Around line 119-120: The code is re-pushing arr_ptr into the guard after
calling helper functions that already root the same buffer, doubling
shadow-stack usage; remove the redundant guard.push(arr_ptr) calls that
immediately follow calls to _write_inline_array, _write_block_array,
_write_array_of_block_arrays, and _write_table_data so the helpers remain the
single point that roots the backing buffers. Locate each occurrence of
guard.push(arr_ptr) directly after those helper calls (several places mirror the
snippet shown) and delete the duplicate push; keep a guard.push only in callers
that allocate or return an unrooted pointer (i.e., when a helper does not
already push). Ensure no other logic depends on the extra push so the outer
guard still unwinds correctly.
---
Outside diff comments:
In `@TESTING.md`:
- Line 193: Update the hard-coded test count in the TESTING.md header string "#
Via pytest (parametrized — 435 tests)" to reflect the new total of 440 tests (88
programmes × 5 stages) and ensure any other literal occurrences of "435" in this
file are changed to "440" so the textual count matches the table entry and new
conformance test; search for the exact header text and any standalone "435"
tokens in TESTING.md and replace them with "440".
🪄 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: e0081842-0212-4f4a-be22-96675287d637
⛔ Files ignored due to path filters (8)
docs/SKILL.mdis excluded by!docs/**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/**docs/sitemap.xmlis excluded by!docs/**tests/conformance/ch09_host_walker_gc_rooting.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (18)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomltests/conformance/manifest.jsontests/test_codegen.pyvera/__init__.pyvera/codegen/api.pyvera/codegen/assembly.pyvera/wasm/html_serde.pyvera/wasm/json_serde.pyvera/wasm/markdown.py
👮 Files not reviewed due to content moderation or server errors (1)
- tests/test_codegen.py
1. host_md_parse exception scope (#693 inline #1): the broad except Exception was swallowing host-side invariant violations (shadow-stack overflow from _ShadowGuard, ValueError from write_md_block exhaustive match, internal AssertionErrors). Restructured to put only the parse step inside the try; shadow-stack work + write_md_block + Result.Ok alloc now sit outside, so invariant violations propagate as wasmtime traps rather than being repackaged as user-domain parse errors. Mirrors the narrow-catch pattern in host_html_parse and host_json_parse. 2. Redundant guard.push in markdown.py walkers (#693 inline #2): the _write_inline_array / _write_block_array / _write_array_of_block_arrays / _write_table_data helpers already push their backing buffer onto the shadow stack internally. Removed the duplicate guard.push(arr_ptr) calls immediately after these helpers in write_md_inline (MdEmph, MdStrong, MdLink) and write_md_block (MdParagraph, MdHeading, MdBlockQuote, MdList, MdTable, MdDocument). Functionally equivalent (the entry IS rooted, just twice) but halves shadow-stack consumption from those branches and removes the only correct way to overflow the guard on pathologically deep walks. Pushes after alloc_string calls are KEPT - alloc_string does not push. 3. TESTING.md "435 tests" -> "440 tests" (outside-diff): the pytest-parametrized shell-block comment now matches the table entry and the new 88-program x 5-stage total. 4. Tracking: added #694 (Windows test_browser timeout flake) to KNOWN_ISSUES.md "Test coverage gaps" with the symptom, asymmetry-test diagnostic that proves it is a flake (3.11 and 3.13 pass on the same Windows runner where 3.12 timed out), and the one-line fix (bump timeout=30 to timeout=60 at the two sites). Per the PR-generated-new-issues-must-be- tracked-in-the-same-PR rule. Validation: 3,815 pytest pass + 88 conformance + mypy clean + all doc validators green + limitations sync consistent. Co-Authored-By: Claude <noreply@anthropic.invalid>
|
@coderabbitai also applied the outside-diff TESTING.md finding in 4b8c127: line 193's pytest-parametrized shell-block comment is now |
|
Tip For best results, initiate chat on the files or code changes.
|
Four agents in parallel: code-reviewer, pr-test-analyzer, comment-analyzer, silent-failure-hunter. Each caught items the others missed. All applied. Critical: 1. host_html_parse had its _ShadowGuard block INSIDE the narrow `except (ValueError, TypeError, AttributeError)`, while host_md_parse (just restructured per the previous CodeRabbit round) and host_json_parse have it OUTSIDE. The in-file comment claimed they matched. They didn't. Caught by silent-failure-hunter AND comment-analyzer from different angles — same finding via two different paths. Restructured host_html_parse to match: parse-only-in-try, shadow-stack work + write_html + Result.Ok alloc outside. Host-side invariant violations (`_ShadowGuard` overflow, `_wrap_handle` RuntimeError from #578, AssertionErrors) now propagate as wasmtime traps consistent with the other two walkers. Important: 2. _ShadowGuard.__init__ wraps the gc_sp / gc_stack_limit lookups in a try/except that re-raises as a clearer RuntimeError naming the missing exports and #692. Diagnostic-quality fix — a hand-crafted .wat fixture without the GC globals exported would otherwise produce a bare "python exception" trap. 3. _ShadowGuard.__exit__ tightened: the `if self._initial_sp is not None` guard is now an `assert`. The only way the None case is reachable is if someone calls __exit__ without going through __enter__ (misuse of the context manager outside a `with` block). The assert pins the invariant; permissive `if` was silently allowing misuse. 4. host_html_query comment softened — previous text claimed "WASM codegen at the call site shadow-pushes it via the standard mechanism", overstating the guard's protection. The guard exits before the function returns; the WASM- side caller's local-store push is what roots the result thereafter. Safe in practice because no allocation happens between return and local-store. Comment now says exactly that. 5. host_md_parse comment phrasing fixed — previously said "Mirrors the narrow-catch pattern in host_html_parse and host_json_parse" but host_html_parse had different structure pre-fix. Now says "Matches the parse-only-in- try structure of host_html_parse and host_json_parse" which is structurally accurate post-fix. 6. JObject branch of write_json gains an exception-safety note documenting why map_dict partial state is safe to discard on mid-loop raise (function exits via the raise before map_alloc is called; the val_ptrs are guaranteed- invalid after guard exit and a future "rescue" would trigger use-after-free). 7. Rooting-contract docstrings added to all four markdown array helpers (_write_inline_array, _write_block_array, _write_array_of_block_arrays, _write_table_data) so future maintainers see the convention from the function signature. 8. Markdown helpers' alloc-then-push allocation order is now documented as load-bearing — a future "fix" must NOT try to push before alloc or wrap the push in try/except. Test coverage additions (pr-test-analyzer): 9. test_html_query_30_matches — exercises the host_html_query _ShadowGuard path that re-walks every matched subtree. Bounded at 30 (rather than 500 like the html_parse tests) because each match's recursive write_html accumulates pushes within a single guard window; empirically 100 overflows the 4096-entry stack when combined with the prior html_parse's retained pushes. 30 still triggers GC during the walk. 10. test_json_parse_500_key_object — exercises the JObject branch of write_json (val_ptr-pushed-per-iteration pattern) that motivated the whole fix. Skipped with reason: - Pre-existing _alloc_array_of_strings has the same bug shape but predates #692 and isn't in scope; file as a follow-up issue rather than expand this PR. - _ShadowGuard unit tests for the exit-on-exception contract: the class is a closure inside compile_to_wasm, not module-level, so direct unit testing requires extracting it. The behavioural contract is implicitly verified by 6 in-process tests + 4 conformance tests all passing under VERA_EAGER_GC=1. - Narrow host_md_parse's `except Exception` further: parse_markdown currently has zero raise statements but could grow them; keep broad as defensive. - Dedicated RuntimeError subclass for shadow-stack overflow: the message starts with #692 which already carries the needed context. Validation: 3,817 pytest pass (added 2 tests, +2 from 3,815) + mypy clean + ruff S clean + 88 conformance pass + 35 examples pass + all doc validators green + version sync clean + limitations sync consistent. Co-Authored-By: Claude <noreply@anthropic.invalid>
pr-review-toolkit:
|
pr-review-toolkit:
|
pr-review-toolkit:
|
pr-review-toolkit:
|
There was a problem hiding this comment.
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/wasm/json_serde.py (1)
180-193:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift
JObjectstill loses reachability of nestedJsonnodes after the guard unwinds.The
map_dictvalues here are plain Python ints, and the wrapper written at Lines 188-192 stores only the map handle. Once the caller’s_ShadowGuardexits, the earlierguard.push(val_ptr)roots disappear, so a later GC can reclaim those childJsonnodes even though theJObjectitself is still live. That leaves_map_store[handle]holding dangling heap pointers for later object traversal/stringify. This needs either a WASM-visible representation for object members or a tracing/rooting scheme forMap<String, Json>entries. Based on learnings:Jsonvalues are vera-heap-allocated tagged ADTs, and aMaphandle insideJObjectis only an internal layout detail.🤖 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/json_serde.py` around lines 180 - 193, The JObject creation currently stores plain Python ints for child Json pointers (built in write_json) so guard.push(val_ptr) unwinds and children can be GC'd; change the construction so the Map/Map handle produced by map_alloc retains WASM-visible references to its member Json nodes (or register them in the heap rooting/tracing structures) instead of relying on the temporary guard. Concretely, in the block that calls write_json and then map_alloc: either have map_alloc accept and copy the child val_ptrs into vera-heap objects (so the resulting wrapper_ptr references live heap pointers), or create a small wasm-allocated container per member (via alloc) that stores the child pointer and pass those containers to map_alloc; alternatively, record the child pointers into _map_store[handle] (or the global tracer) before popping the guard so JObject (the _TAG_JOBJECT node) holds reachable roots. Update the code paths around write_json, guard.push, map_alloc, and the place that writes _TAG_JOBJECT to ensure children remain rooted after the guard unwinds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_codegen.py`:
- Around line 19226-19227: The test currently uses the private helper _run_io;
replace each assertion that calls _run_io(...) == "ok" (and other direct _run_io
uses) with the approved test helpers: use _check_ok(...) or _verify_ok(...) for
expected-success cases and _check_err(...) or _verify_err(...) for
expected-failure cases. Locate occurrences of _run_io in the test file (symbol:
_run_io) and update each to call the corresponding wrapper helper
(_check_ok/_verify_ok/_check_err/_verify_err) with the same input and expected
outputs, ensuring any existing assertions or exception checks are moved into the
appropriate helper call.
---
Outside diff comments:
In `@vera/wasm/json_serde.py`:
- Around line 180-193: The JObject creation currently stores plain Python ints
for child Json pointers (built in write_json) so guard.push(val_ptr) unwinds and
children can be GC'd; change the construction so the Map/Map handle produced by
map_alloc retains WASM-visible references to its member Json nodes (or register
them in the heap rooting/tracing structures) instead of relying on the temporary
guard. Concretely, in the block that calls write_json and then map_alloc: either
have map_alloc accept and copy the child val_ptrs into vera-heap objects (so the
resulting wrapper_ptr references live heap pointers), or create a small
wasm-allocated container per member (via alloc) that stores the child pointer
and pass those containers to map_alloc; alternatively, record the child pointers
into _map_store[handle] (or the global tracer) before popping the guard so
JObject (the _TAG_JOBJECT node) holds reachable roots. Update the code paths
around write_json, guard.push, map_alloc, and the place that writes _TAG_JOBJECT
to ensure children remain rooted after the guard unwinds.
🪄 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: 66525426-e206-497d-a92a-05865793d108
📒 Files selected for processing (7)
CHANGELOG.mdROADMAP.mdTESTING.mdtests/test_codegen.pyvera/codegen/api.pyvera/wasm/json_serde.pyvera/wasm/markdown.py
CodeRabbit's outside-diff observation on json_serde.py JObject correctly identifies a real pre-existing architectural bug: Map values that are heap pointers (Map<K, Json>, Map<K, HtmlNode>, etc.) are stored in Python-side _map_store but invisible to the conservative GC scan, so a post-walk gc_collect frees them. This pre-dates #692 — the #692 fix improved DURING-walk safety (intermediate val_ptrs pushed to shadow stack via guard) but does not address POST-walk reachability. Files as #695 with reproducer sketch, three candidate fixes (host-side WASM container per entry / extend GC tracing / serialise into WASM), and the affected-Map-types breakdown. Added to KNOWN_ISSUES.md "Bugs" section per the PR-generated- new-issues-must-be-tracked-in-the-same-PR rule. CodeRabbit inline finding on tests/test_codegen.py (_run_io -> _check_ok) is invalid and skipped — _check_ok only type-checks the program; my regression tests need to ACTUALLY EXECUTE the code to exercise the GC pressure path that motivated #692. The reply on the PR explains. Co-Authored-By: Claude <noreply@anthropic.invalid>
|
@coderabbitai outside-diff finding ( Confirmed by inspecting The bug affects all
Filed as #695 with a reproducer sketch, three candidate fix options (host-side WASM container per entry / extend GC tracing to walk Deferring rather than expanding #693 because: (a) the bug is pre-existing, (b) the fix is a substantive architectural change touching the Map contract or the GC tracer, (c) #693 is already at 27 files / +710 lines / 5 commit rounds and adding a Map-contract change muddles the review surface. Closing #692's during-walk-safety in this PR, leaving #695 as a clean follow-up. |
|
Tip For best results, initiate chat on the files or code changes.
The critical asymmetry you've identified is precise: The affected/safe breakdown is also sound — Deferral to ✏️ Learnings added
|
README's "3,949 tests" was set during the v0.0.158 release- prep commit when I bumped the count to match my then-estimate; the actual count after all PR work is 3,947. Caught during the final pre-merge audit. Not flagged by check_doc_counts.py because the README's "v0.0.X - N+ commits, N releases, X tests, Y%" line uses a comma- separated prose pattern that the script's regex doesn't anchor on. Worth adding to the validation in a future PR. Skip-changelog: stale-count fix, no behaviour change. Co-Authored-By: Claude <noreply@anthropic.invalid>
Summary
Closes #692.
html_parse,json_parse, andmd_parseno longer trap withOut-of-bounds memory accesson inputs large enough to pressure GC during host-side tree marshalling. Root cause: missing-shadow-stack rooting in the three host walkers — same #570 / #515 / #593 bug class but on the host side rather than WAT-emitted user code. Python-held intermediate pointers (arr_ptr/name_ptr/wrapper_ptr) were invisible to the conservative GC scan; if a sub-walk triggered$gc_collect, those blocks were reclaimed and the next write into them corrupted the free list. Concrete trap signature: out-of-bounds access at0xfffffffd(=1 - 4in i32) from inside$alloc's free-list traversal, with1being theHtmlText/Result.Errtag that overwrote a freed block's free-list link.External report (an agent's
summarise_urlsexample runninghtml_parseover the currentFAQ.mdbody) verified empirically; same shape proven inwrite_json(large nested JArray) andwrite_md_block(many headings).Fix design — Option 3 from the issue body (architectural)
The fix follows the three-step architectural option described in #692 rather than the surgical Option 1 (pre-grow memory). Rationale: Option 1 closes only the reported call sites; Option 3 closes the bug class. Each future host walker that allocates more than one WASM object across Python-held pointers will inherit shadow-stack discipline by default.
1. Export
$gc_spand$gc_stack_limitfrom the emitted WAT module (vera/codegen/assembly.py). Inline export syntax on the global declarations; WAT-sidegc_shadow_pushhelper for user code unchanged.2. New
_ShadowGuardcontext manager invera/codegen/api.py.__enter__snapshots$gc_sp.guard.push(ptr)writesptrat[gc_sp]and advances$gc_spby 4. Re-acquiresdata_ptreach push (memory may have moved viamemory.grow).__exit__resets$gc_spto the snapshot — atomically pops every push in the block, on both success and exception paths. Callers push freely without counting; the outerwithis the unwind boundary.buf[sp + i] = bper byte), notstruct.pack_into— wasmtime-py'sdata_ptrreturnsLP_c_ubytewhich lacks the buffer protocolpack_intorequires.3. Thread
guardthrough the host walkers:vera/wasm/html_serde.py::write_html— pushesname_ptr,wrapper_ptr,arr_ptracross element children walks; pushes string body for text/comment branches.vera/wasm/json_serde.py::write_json— pushesarr_ptrfor JArray, eachval_ptrandwrapper_ptrfor JObject (so each iteration's value survives the next sub-walk's GC); reordered JString to allocate the string before the body.vera/wasm/markdown.py::write_md_block/write_md_inline+ four array helpers — adopt the fields-first-then-body convention everywhere: allocate child contents first (rooted via guard), then allocate the body last so the body pointer is never held across another alloc.4. Host import callers wrap the walk + final
Result.Okalloc in a guard:host_html_parse,host_html_query(forhtml_parseandhtml_query)host_json_parsehost_md_parseTests
tests/conformance/ch09_host_walker_gc_rooting.vera(run-level, 4 sub-tests) — html_parse with 500 element siblings, json_parse with 1000-element number array, json_parse with 500-element string array, md_parse with 200 H1 + paragraph blocks. Sizes chosen to provoke real heap growth and multiple$gc_collectcycles during the walk while staying under Python's default recursion limit on tear-down paths. Conformance suite now 88 programs (up from 87).tests/test_codegen.py::TestHostWalkerGCRooting692— 4 in-process regression tests for the same scenarios. Joins the existing host-side GC-rooting regression classes for GC shadow-stack overflow in array_map (and sibling iterative builders) at ~4000 heap-allocating elements #570 / GC collect itself faults with out-of-bounds memory access under sustained allocation pressure #515 / Conway's Life at 12x30 still corrupts strings from gen 1 onwards (additional trigger beyond #588) #593.TestWorklistOverflow348::test_worklist_size_quadrupled_in_watfor the new exported$gc_stack_limitWAT shape.Test plan
pytest tests/— 3,815 pass (88 conformance, including 4 new GC-rooting regressions)pytest tests/test_browser.py— 99 passmypy vera/cleanruff check --select S vera/clean (the previously-fixed S110 rule plus all other security lints)python scripts/check_doc_counts.py— consistent (3,945 tests, 32 files, 88 conformance, 35 examples)python scripts/check_version_sync.py— v0.0.158 across 6 filesvera runon the original FAQ.md reproducer returnsOk(was: trap)vera runon a 1000-element JSON array returnsOk(was: trap)VERA_EAGER_GC=1(forces GC on every alloc — most aggressive test) — both reproducers returnOkRelease prep
Per the v0.0.157 lesson (version bump in feature PR, not a follow-up PR):
0.0.157→0.0.158inpyproject.toml,vera/__init__.py,README.md,docs/index.html,uv.lock.CHANGELOG.md:[Unreleased]→[0.0.158] - 2026-05-19with link references for the new version and the bumped Unreleased pointer.HISTORY.md: new Stage 12 row for v0.0.158; release counter157 → 158.87 → 88conformance-count sweep acrossCLAUDE.md,AGENTS.md,SKILL.md,TESTING.md,FAQ.md,docs/index.html,ROADMAP.md.docs/SKILL.md,docs/index.md,docs/llms.txt,docs/llms-full.txt,docs/sitemap.xml).After merge: single
git tag v0.0.158 <merge-commit>+gh release createwith the awk-extracted CHANGELOG section.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Documentation
Tests
Known Issues