Skip to content

fix: isolate nested handle[State<T>] via push/pop host protocol (#417)#453

Merged
aallan merged 5 commits into
mainfrom
fix/nested-state-handlers-417
Apr 9, 2026
Merged

fix: isolate nested handle[State<T>] via push/pop host protocol (#417)#453
aallan merged 5 commits into
mainfrom
fix/nested-state-handlers-417

Conversation

@aallan

@aallan aallan commented Apr 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Root cause: nested handle[State<T>] of the same type shared one state cell; inner handler mutations corrupted the outer handler value.
  • Fix: state_store is now a per-type stack. state_push_T (handler entry) pushes a fresh default cell; state_pop_T (handler exit) restores the outer cell. Get/put operate on the top of the stack.
  • Also fixes: HandleExpr was missing from _is_void_expr, causing a spurious drop for void-body handlers used as statements (WASM validation error).
  • Python runtime (api.py) and browser/Node runtime (runtime.mjs) both updated.

Test plan

  • 3 new nested State isolation tests in TestEffectHandlers
  • pytest tests/ — 3,230 passed, 11 skipped
  • mypy vera/ — clean
  • All conformance programs pass, all pre-commit hooks pass

Closes #417

Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed WASM tag encoding for string exceptions and ensured nested state handlers do not share or corrupt outer state.
  • Behaviour Changes

    • Runtime state is now stack-based per handler; outer state is restored after nested handlers. Runtime inspection and reset reflect top-of-stack semantics.
  • Tests

    • Added tests validating nested handler isolation and stricter WASM import expectations.
  • Documentation

    • Updated test counts, roadmap and release history entries.

aallan and others added 2 commits April 9, 2026 19:02
Remove #416 from KNOWN_ISSUES.md (fixed in PR #452).
Add 9 Apr bug-fix row to HISTORY.md Stage 10 table.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Each handle[State<T>] now calls state_push_T on entry and state_pop_T
on exit, giving every handler its own independent state cell. Nested
handlers of the same type no longer share a global cell.

Changes:
- vera/codegen/api.py: state_store becomes dict[str, list[int|float]];
  get/put operate on top-of-stack; new state_push_T/state_pop_T host fns
- vera/codegen/assembly.py: emit state_push_T/state_pop_T WAT imports
- vera/wasm/calls.py: emit push before init, pop after body
- vera/wasm/context.py: add HandleExpr to _is_void_expr (fixes spurious
  drop for void-body handlers used as statements)
- vera/browser/runtime.mjs: stateCells becomes stacks; add push/pop bindings
- tests/test_codegen.py: 3 new nested State<T> isolation tests
- KNOWN_ISSUES.md: remove #417 (fixed)
- HISTORY.md: add 9 Apr bug-fix entry
- TESTING.md / ROADMAP.md: update test counts (3227 → 3230)

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

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f4a245c-ce01-42ba-b5bb-eed024ca409e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d63118 and b88d937.

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

📝 Walkthrough

Walkthrough

Switch State from single global cells to per-handler stacks with state_push_*/state_pop_* host imports; update codegen, WASM emission, runtime, and executor to operate on stacks; add tests for nested handle[State<T>] isolation; docs updated and HandleExpr voidness propagation tweaked. (50 words)

Changes

Cohort / File(s) Summary
Documentation
HISTORY.md, KNOWN_ISSUES.md, ROADMAP.md, TESTING.md
Add HISTORY entry for 9 Apr (fixes: Exn WASM tag #416, nested State isolation #417); remove two KNOWN_ISSUES rows; update reported test counts from 3,227 → 3,230 and testing metadata.
Tests
tests/test_codegen.py
Strengthen WAT import assertions; add three nested handle[State<Int>] tests to verify per-handler state push/pop isolation and restoration; adjust test-count assertions.
WASM assembly
vera/codegen/assembly.py
Emit additional imports vera.state_push_{type} and vera.state_pop_{type} for each state type.
WASM codegen / calls
vera/wasm/calls.py
Emit calls to $vera.state_push_{type} before handler init/body and $vera.state_pop_{type} after handler body so handlers get isolated state stacks.
Runtime (browser)
vera/browser/runtime.mjs
Replace per-key scalar cells with per-key stacks; implement state_get_*/state_put_* operating on stack top; add state_push_*/state_pop_*; update getState()/resetState() to reflect stacks.
Python executor API
vera/codegen/api.py
Change host state to per-key stacks (lists); add host bindings vera.state_push_<type> and vera.state_pop_<type>; ExecuteResult.state now reports top-of-stack values.
WASM context typing
vera/wasm/context.py
Treat ast.HandleExpr as void when its body is void (voidness propagation tweak).

Sequence Diagram

sequenceDiagram
    participant Runtime as Runtime (Browser / Python)
    participant Module as WASM Module
    participant Stack as State Stack (per-key)

    Note over Runtime,Stack: Enter outer handle[State<T>]
    Runtime->>Module: vera.state_push_Int()
    Module->>Stack: push outer value (stack: [10])

    Runtime->>Module: vera.state_get_Int()
    Module-->>Runtime: return top -> 10

    Note over Runtime,Stack: Enter inner handle[State<T>]
    Runtime->>Module: vera.state_push_Int()
    Module->>Stack: push inner value (stack: [10,99])

    Runtime->>Module: vera.state_put_Int(42)
    Module->>Stack: write top -> 42 (stack: [10,42])

    Note over Runtime,Stack: Exit inner handler
    Runtime->>Module: vera.state_pop_Int()
    Module->>Stack: pop -> restores top (stack: [10])

    Runtime->>Module: vera.state_get_Int()
    Module-->>Runtime: return top -> 10

    Note over Runtime,Stack: Exit outer handler
    Runtime->>Module: vera.state_pop_Int()
    Module->>Stack: pop -> stack restored/default
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.87% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: isolate nested handle[State] via push/pop host protocol (#417)' directly and clearly summarizes the main change—fixing nested same-type State handlers through a stack-based protocol.
Linked Issues check ✅ Passed The pull request fully addresses issue #417 by implementing per-type state stacks with push/pop protocol [#417], adding isolated nested handler tests [#417], and fixing the void-expression handling for HandleExpr [#417].
Out of Scope Changes check ✅ Passed All changes are scoped to fixing nested State handler isolation: runtime stacks (runtime.mjs, api.py), code generation (calls.py, assembly.py), void-expression logic (context.py), tests, and documentation. No unrelated features introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nested-state-handlers-417

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

@codecov

codecov Bot commented Apr 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (b2cf06e) to head (b88d937).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
vera/browser/runtime.mjs 62.06% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #453      +/-   ##
==========================================
- Coverage   90.32%   90.32%   -0.01%     
==========================================
  Files          50       50              
  Lines       19419    19459      +40     
  Branches      220      225       +5     
==========================================
+ Hits        17541    17576      +35     
- Misses       1874     1879       +5     
  Partials        4        4              
Flag Coverage Δ
javascript 50.67% <62.06%> (+0.11%) ⬆️
python 95.27% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

8242-8274: ⚠️ Potential issue | 🔴 Critical

Bracket the pushed state frame around the body, not just the happy path.

Line 8244 pushes the new State<T> cell before init_expr is evaluated, so a nested same-type initialiser such as handle[State<Int>](@int = get(())) now reads the fresh default cell instead of the outer handler’s state. Then Line 8274 only pops on fall-through, so if the body reuses an outer throw mapping and unwinds to an enclosing handle[Exn<_>], the pushed cell leaks and later get/put calls keep talking to the wrong frame. Please evaluate the initialiser first, then push+initialise the new cell, and make the pop run on both normal and exceptional exits.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_codegen.py`:
- Around line 2831-2852: The test test_nested_state_inner_does_not_corrupt_outer
currently returns the inner handler's value (999) and doesn't verify the outer
handler's state; update the src string in this test so that after the inner
handle[State<Int>] block finishes you perform another get(()) in the outer
handler to read the outer state (initialized with `@Int` = 5) and return that
value, then assert _run(src, "test") == 5 to confirm the inner put did not
corrupt the outer state; locate the test by the function name
test_nested_state_inner_does_not_corrupt_outer and the use of _run and modify
the nested handle bodies accordingly.
🪄 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: f1ed239c-7358-4383-93c6-454ce07b2ff2

📥 Commits

Reviewing files that changed from the base of the PR and between b2cf06e and 6701d87.

📒 Files selected for processing (10)
  • HISTORY.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_codegen.py
  • vera/browser/runtime.mjs
  • vera/codegen/api.py
  • vera/codegen/assembly.py
  • vera/wasm/calls.py
  • vera/wasm/context.py
💤 Files with no reviewable changes (1)
  • KNOWN_ISSUES.md

Comment thread tests/test_codegen.py
…r state

Inner body now discards its result and outer body reads get(()) after,
asserting outer state (5) was not corrupted by inner put(999).

Also condense 9 Apr HISTORY.md entries to one line per project convention.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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)

2799-2805: 🧹 Nitpick | 🔵 Trivial

Assert the actual import declarations.

These checks still pass if the names only appear in call sites or symbol references. Please assert the concrete import lines so this test proves the host imports are emitted, not just mentioned.

Suggested tightening
-        assert "state_get_Int" in result.wat
-        assert "state_put_Int" in result.wat
-        assert "state_push_Int" in result.wat
-        assert "state_pop_Int" in result.wat
+        assert 'import "vera" "state_get_Int"' in result.wat
+        assert 'import "vera" "state_put_Int"' in result.wat
+        assert 'import "vera" "state_push_Int"' in result.wat
+        assert 'import "vera" "state_pop_Int"' in result.wat

Based on learnings: tests/test_codegen.py includes explicit WAT import‑gating coverage for JSON host imports — tests verify that imports are emitted only when referenced and absent when unused.

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

In `@tests/test_codegen.py` around lines 2799 - 2805, The current test only checks
that the symbols (state_get_Int, state_put_Int, state_push_Int, state_pop_Int)
appear anywhere in result.wat; update the assertions to verify concrete WAT
import declarations are emitted by asserting the actual import lines for those
hosts instead of mere occurrences — e.g. assert that result.wat contains import
declarations that include the import prefix and each function name (look for
'(import' plus the host function names) so the test proves the host imports are
emitted, not just referenced; update the assertions around result.wat where
state_get_Int/state_put_Int/state_push_Int/state_pop_Int are checked to perform
string matches for import declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_codegen.py`:
- Around line 2807-2875: Modify one of the three new tests (e.g.,
test_nested_state_outer_readable_after_inner) so the inner handle[State<Int>]
produces and returns a value that the outer block consumes instead of being used
as a Unit statement; specifically, change the inner in { ... } to evaluate to an
Int (for example return the value put by the inner handler) and have the outer
block both capture that returned Int and then call get(()) to verify the outer
handler's state is restored; update the final assert on _run(src, "test") to
expect the consumed inner result while also ensuring the outer state check still
passes.

---

Outside diff comments:
In `@tests/test_codegen.py`:
- Around line 2799-2805: The current test only checks that the symbols
(state_get_Int, state_put_Int, state_push_Int, state_pop_Int) appear anywhere in
result.wat; update the assertions to verify concrete WAT import declarations are
emitted by asserting the actual import lines for those hosts instead of mere
occurrences — e.g. assert that result.wat contains import declarations that
include the import prefix and each function name (look for '(import' plus the
host function names) so the test proves the host imports are emitted, not just
referenced; update the assertions around result.wat where
state_get_Int/state_put_Int/state_push_Int/state_pop_Int are checked to perform
string matches for import declarations.
🪄 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: a4aa2040-9594-4567-ab18-164446bcd5da

📥 Commits

Reviewing files that changed from the base of the PR and between 6701d87 and 159ee58.

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

Comment thread tests/test_codegen.py
test_handle_state_wat_has_imports: assert full import declaration strings
('(import "vera" "state_*_Int"') rather than bare symbol occurrence, so
the test proves host imports are emitted, not just referenced in call sites.

test_nested_state_outer_readable_after_inner: make inner body return Int
(put(7); get(())) so state_pop_Int is exercised with a live WASM value on
the stack, verifying it is truly stack-neutral. Outer block captures the
inner result via let, then reads outer state (42) as the return value.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TESTING.md`:
- Line 9: Update the "Skipped tests" counts in TESTING.md to match the current
test run (change the value 9 to 11 wherever the skipped-test figure appears),
ensuring both tables/occurrences in the "Skipped tests" section and any summary
table are synchronized so totals remain consistent; locate the "Skipped tests"
heading and the two tables that list test counts and replace the old skipped
count with 11 and verify the derived totals in those tables still add up
correctly.
🪄 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: e376f351-f5f5-4123-943f-d3e5bf36d52a

📥 Commits

Reviewing files that changed from the base of the PR and between 159ee58 and 8d63118.

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

Comment thread TESTING.md
ch03_typed_holes is a check-level program; its verify and run stages are
both skipped by the conformance framework. Add both rows to the
level-limited skips table and update the summary sentence.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan merged commit 107606e into main Apr 9, 2026
19 checks passed
@aallan aallan deleted the fix/nested-state-handlers-417 branch April 9, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested same-type State<T> handlers share a single global state cell

1 participant