Skip to content

Commit 3bf7e74

Browse files
aallanClaude
andcommitted
Address PR review findings on #673
Comprehensive PR review (code-reviewer + pr-test-analyzer + comment-analyzer + silent-failure-hunter, all in parallel) surfaced 9 actionable items. Addressed 7, deferred 2 with reasons. **Critical** 1. **CHANGELOG test count** (comment-analyzer + code-reviewer). The Tests section said "5 new tests" but the class actually has 6 (the JSON sibling added in round 3 wasn't back- propagated). Updated to 6 with the new bullet. 2. **`host_html_parse` broad-except swallowed `_wrap_handle` invariant violations** (silent-failure-hunter). The `except Exception as exc:` at `vera/codegen/api.py:2784` converted any RuntimeError (including the new #578 range- check raise) into a user-domain `Result.Err` string, masking real invariant violations as "malformed HTML" errors. Narrowed to `(ValueError, TypeError, AttributeError)` matching the sibling `host_json_parse` pattern. **Important** 3. **`_wrap_handle` range guard had zero unit tests** (pr-test- analyzer, rating 7). Extracted the validator to a module- level `_validate_wrap_handle` helper so it can be unit- tested directly without standing up a wasmtime instance. `_wrap_handle` now calls the helper. Added 5 tests covering all failure modes: - `test_validate_wrap_handle_accepts_valid_range` (0, 1, 12345, 0x7FFFFFFE, 0x7FFFFFFF) - `test_validate_wrap_handle_rejects_negative` (-1, -12345) - `test_validate_wrap_handle_rejects_at_2gb_boundary` (0x80000000) - `test_validate_wrap_handle_rejects_above_32bit` (0x100000000, 0x100000001 — the round-1 bit-only check would have missed these) - `test_validate_wrap_handle_rejects_non_int` (None, "5", 1.5, [1], {}) 4. **GC mark phase missing defense-in-depth narrative** (silent- failure-hunter). The conservative scan's heap-range check relies on the disjointness invariant (`heap_ptr < 2 GiB` so tagged handles `>= 2 GiB` can never match). Added a substantial comment block at the scan site documenting the invariant and pointing at the two structural tests that pin it. Comment-only (no runtime cost in the GC hot path); the structural tests are the mechanical defense. **Comment accuracy** (comment-analyzer) 5. `test_json_round_trip_uses_host_side_mask` referenced `json_serde.py` line 213 by hardcoded number — drifts on any nearby edit. Replaced with the "unknown JObject handle" warning reference. 6. `2 GB` → `2 GiB` throughout (assembly.py + tests/test_ codegen.py). Strictly `0x80000000 = 2 GiB` (binary), not 2 GB (decimal = ~1.86 GiB). 7. `"Practical Vera programs use <100 MB"` was an unsubstantiated claim stated as fact. Reworded to `"Programs we have measured stay well below the 2 GiB ceiling"`. 8. The class docstring's `gc_heap_start (~147 KiB)` figure actually depends on the string-pool size (`data_end + 144 KiB`). Softened to `~144 KiB above the data section, so roughly 144 KiB plus the string-pool size`. **Deferred with reason** 9. `$alloc` heap-ceiling trap is bare `unreachable`, surfaces via the trap classifier as `"unreachable"` kind with the match-arm Fix message (silent-failure-hunter, Important). Polishing this requires either a host-import call from `$alloc` to populate `last_violation` (cost on the hot path) or a new classifier kind with a heuristic detector (brittle). Practical programs never reach this trap (heap << 2 GiB), so deferred. Marked with a TODO inside `assembly.py` as a Python comment (NOT a WAT comment — a WAT comment between `if` and `unreachable` would break the adjacent-sequence regex in `test_alloc_emits_heap_ceiling_ guard`). The other declined findings from the agents: - silent-failure-hunter `read_json` warn → raise (pre-existing behavior, separate concern) - silent-failure-hunter `_call_register_wrapper` early-return → assert (pre-existing pattern, out of scope) - silent-failure-hunter wasmtime trap-text enrichment (would need last_violation channel work; separate PR) - code-reviewer i32.add wraparound at heap-ceiling guard (confidence 60 — self-flagged; defended upstream by `memory.grow` and 31-bit-size invariant) Validation: pytest 3,874/3,874 ✓ (added 5 new tests), mypy clean, ruff S clean, all 18 pre-commit hooks green, doc-counts consistent (1,111 → 1,116 tests in test_codegen.py; total 3,899 → 3,904). Co-Authored-By: Claude <noreply@anthropic.invalid>
1 parent a281e47 commit 3bf7e74

7 files changed

Lines changed: 168 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1717

1818
### Tests
1919

20-
- `tests/test_codegen.py::TestWrapperHandleTagging578`5 new tests pinning the contract: (1) wrap site emits `i32.const 0x80000000; i32.or`, (2) unwrap site emits `i32.load offset=4; i32.const 0x7FFFFFFF; i32.and`, (3) `$alloc` body contains the heap-ceiling guard, (4) end-to-end wrap/unwrap round trip preserves the original handle (a Map insert + lookup), (5) `html_to_string` produces the correct length output — pinning that the host-side `read_html` mask is in place (without it the attribute dict lookup would miss and the rendered HTML would be missing the `title="..."` attribute).
20+
- `tests/test_codegen.py::TestWrapperHandleTagging578`6 new tests pinning the contract: (1) wrap site emits `i32.const 0x80000000; i32.or`, (2) unwrap site emits `i32.load offset=4; i32.const 0x7FFFFFFF; i32.and`, (3) `$alloc` body contains the heap-ceiling guard (ordered 8-instruction sequence pinned by adjacent-sequence regex), (4) end-to-end wrap/unwrap round trip preserves the original handle (a Map insert + lookup), (5) `html_to_string` produces the correct length output — pinning that the host-side `read_html` mask is in place (without it the attribute dict lookup would miss and the rendered HTML would be missing the `title="..."` attribute), (6) `json_stringify(JObject(...))` produces the correct length — sibling test for the host-side `read_json` mask (which lives in `vera/wasm/json_serde.py` and bypasses the WAT unwrap helper just like `read_html` does).
2121

2222
### Documentation
2323

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ cp /path/to/vera/SKILL.md ~/.claude/skills/vera-language/SKILL.md
181181

182182
## Project status
183183

184-
Vera is in **active development** at v0.0.155 — 810+ commits, 155 releases, 3,899 tests, 96% code coverage, 86 conformance programs, 34 examples, and a 13-chapter specification. See **[HISTORY.md](HISTORY.md)** for how the compiler was built.
184+
Vera is in **active development** at v0.0.155 — 810+ commits, 155 releases, 3,904 tests, 96% code coverage, 86 conformance programs, 34 examples, and a 13-chapter specification. See **[HISTORY.md](HISTORY.md)** for how the compiler was built.
185185

186186
The reference compiler — parser, AST, type checker, contract verifier (Z3), WASM code generator, module system, browser runtime, and runtime contract insertion — is working. The language specification is in draft across [13 chapters](spec/).
187187

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Where the project is going. See [HISTORY.md](HISTORY.md) for what's been built
44

55
## Where we are
66

7-
3,899 tests, 86 conformance programs, 34 examples, 13 spec chapters.
7+
3,904 tests, 86 conformance programs, 34 examples, 13 spec chapters.
88

99
## What's next
1010

TESTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This is the single source of truth for Vera's testing infrastructure, coverage d
66

77
| Metric | Value |
88
|--------|-------|
9-
| **Tests** | 3,899 across 32 files (~53,184 lines of test code; 3,869 passed + 16 stress, 14 skipped) |
9+
| **Tests** | 3,904 across 32 files (~53,241 lines of test code; 3,874 passed + 16 stress, 14 skipped) |
1010
| **Compiler code coverage** | 96% of 15,149 statements (CI minimum: 80%) |
1111
| **Conformance programs** | 86 programs across 9 spec chapters, validating every language feature |
1212
| **Example programs** | 34, all validated through `vera check` + `vera verify` |
@@ -58,7 +58,7 @@ python scripts/fix_allowlists.py --fix # auto-fix stale allowlists
5858
| `test_ast.py` | 127 | 1,130 | AST transformation, node structure, serialisation, string escape sequences, ability declarations |
5959
| `test_checker.py` | 521 | 5,939 | Type synthesis, slot resolution, effects, effect subtyping, contracts, exhaustiveness, cross-module typing, visibility, error codes, string built-ins, generic rejection, IO operation types, Markdown types, Regex types, abilities, Map collection, Set collection, Decimal type, Json type, Html type, Http effect, Inference effect, removed legacy name regression |
6060
| `test_verifier.py` | 145 | 2,094 | Z3 verification, counterexamples, tier classification, call-site preconditions, branch-aware preconditions, pipe operator, cross-module contracts, match/ADT verification, decreases verification, mutual recursion, refined Bool/String/Float64 param sorts, **@Nat subtraction underflow obligation** (#520 — Path-A obligation discharge via requires/path-conditions/path-aware Z3 refutation, pure-literal exclusion, Int-Int and Nat-Int → Int exemptions) |
61-
| `test_codegen.py` | 1,111 | 18,861 | WASM compilation, arithmetic, Float64, Byte, arrays (incl. compound element types), ADTs, match (incl. nested patterns), generics, State\<T\>, Exn\<E\> handlers, control flow, strings, string escape sequences, IO (read\_line, read\_file, write\_file, args, exit, get\_env, sleep, time, stderr), bounds checking, quantifiers, assert/assume, refinement type aliases, pipe operator, string built-ins, built-in shadowing, parse\_nat Result, GC, Markdown host bindings, Regex host bindings, Map collection, Set collection, Decimal type, Json type, Html type, Http effect, Inference effect, Random effect, example round-trips, GC shadow stack overflow, **WASM tail-call optimization** (#517 — `return_call` emission for tail-position calls, 50K- and 1M-iteration stress, structural assertions on `return_call`/plain `call` boundary, **GC-aware TCO for allocating fns (#549 — `$gc_sp` restore before each `return_call`)**, postcondition-fallback regression (still reverts to plain `call`), analyzer unit tests covering Block-trailing / IfExpr-both-branches / MatchExpr-arm-bodies / let-value-NOT-marked / call-args-NOT-marked / ExprStmt-statement-NOT-marked / IfExpr-condition-NOT-marked / MatchExpr-scrutinee-NOT-marked) |
61+
| `test_codegen.py` | 1,116 | 18,918 | WASM compilation, arithmetic, Float64, Byte, arrays (incl. compound element types), ADTs, match (incl. nested patterns), generics, State\<T\>, Exn\<E\> handlers, control flow, strings, string escape sequences, IO (read\_line, read\_file, write\_file, args, exit, get\_env, sleep, time, stderr), bounds checking, quantifiers, assert/assume, refinement type aliases, pipe operator, string built-ins, built-in shadowing, parse\_nat Result, GC, Markdown host bindings, Regex host bindings, Map collection, Set collection, Decimal type, Json type, Html type, Http effect, Inference effect, Random effect, example round-trips, GC shadow stack overflow, **WASM tail-call optimization** (#517 — `return_call` emission for tail-position calls, 50K- and 1M-iteration stress, structural assertions on `return_call`/plain `call` boundary, **GC-aware TCO for allocating fns (#549 — `$gc_sp` restore before each `return_call`)**, postcondition-fallback regression (still reverts to plain `call`), analyzer unit tests covering Block-trailing / IfExpr-both-branches / MatchExpr-arm-bodies / let-value-NOT-marked / call-args-NOT-marked / ExprStmt-statement-NOT-marked / IfExpr-condition-NOT-marked / MatchExpr-scrutinee-NOT-marked) |
6262
| `test_codegen_contracts.py` | 32 | 576 | Runtime pre/postconditions, contract fail messages, old/new state postconditions |
6363
| `test_codegen_monomorphize.py` | 71 | 1,326 | Generic instantiation, type inference, monomorphization edge cases, ability constraint satisfaction (Eq/Ord/Hash/Show), operation rewriting (eq/compare), show/hash dispatch, ADT auto-derivation, array operations (slice/map/filter/fold) |
6464
| `test_codegen_closures.py` | 50 | 1,624 | Closure lifting, captured variables, higher-order functions, iterative-builder shadow-stack regressions (#570), closure return-value shadow-push balance for both i32-pair and i32-ADT branches across array_map and array_mapi, plus VERA_EAGER_GC injection self-test (#593), IndexExpr-of-FnCall element-type inference (#614), non-contiguous capture and walker-order miscompiles (#615) |

tests/test_codegen.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15436,7 +15436,8 @@ class TestWrapperHandleTagging578:
1543615436

1543715437
Pre-#578 the raw host handle (a small positive integer) was
1543815438
stored at offset 4. For typical programs the handle stays
15439-
below `gc_heap_start` (~147 KiB) so the heap-range check
15439+
below `gc_heap_start` (~144 KiB above the data section, so
15440+
roughly 144 KiB plus the string-pool size) so the heap-range check
1544015441
rejects it. But for very-long-running programs allocating
1544115442
>100K host handles per `execute()`, the handle counter could
1544215443
exceed `gc_heap_start` and (with the right alignment) be
@@ -15446,7 +15447,7 @@ class TestWrapperHandleTagging578:
1544615447
retention for long sessions.
1544715448

1544815449
Post-#578 the handle is stored as `handle | 0x80000000` so
15449-
the in-heap field is always >= 2 GB, structurally outside
15450+
the in-heap field is always >= 2 GiB, structurally outside
1545015451
any heap-range check (the `$alloc` heap-ceiling guard
1545115452
enforces `heap_ptr < 0x80000000`). The unwrap site ANDs
1545215453
with 0x7FFFFFFF to recover the raw handle. Two host-side
@@ -15515,8 +15516,8 @@ def test_alloc_emits_heap_ceiling_guard(self) -> None:
1551515516

1551615517
The structural counterpart to the wrap-site tag: the
1551715518
guard ensures `heap_ptr < 0x80000000` always, so tagged
15518-
handles (>= 2 GB) and heap pointers (< 2 GB) are
15519-
guaranteed disjoint. Without this guard a 3+ GB heap
15519+
handles (>= 2 GiB) and heap pointers (< 2 GiB) are
15520+
guaranteed disjoint. Without this guard a 3+ GiB heap
1552015521
could produce real pointers in the tagged-handle range,
1552115522
reintroducing the spurious-retention bug.
1552215523

@@ -15568,7 +15569,7 @@ def test_alloc_emits_heap_ceiling_guard(self) -> None:
1556815569
)
1556915570
assert guard_match is not None, (
1557015571
f"Heap-ceiling guard sequence not found (ordered) in "
15571-
f"$alloc body. Without it, a >2 GB heap would let "
15572+
f"$alloc body. Without it, a >2 GiB heap would let "
1557215573
f"real heap pointers collide with the tagged-handle "
1557315574
f"pattern, reintroducing #578. $alloc body:\n"
1557415575
f"{alloc_body[:2000]}"
@@ -15637,9 +15638,9 @@ def test_json_round_trip_uses_host_side_mask(self) -> None:
1563715638
helper. Post-#578 that read sees the TAGGED value and
1563815639
must AND with 0x7FFFFFFF before looking up the host-side
1563915640
`map_store`. Without the mask the lookup would miss,
15640-
`read_json` would fall through to the warning + empty-
15641-
dict path (`json_serde.py` line 213), and
15642-
`json_stringify` would emit `{}` instead of the object.
15641+
`read_json` would fall through to the "unknown JObject
15642+
handle" warning + empty-dict path, and `json_stringify`
15643+
would emit `{}` instead of the object.
1564315644
"""
1564415645
source = """\
1564515646
public fn main(-> @Int)
@@ -15655,6 +15656,62 @@ def test_json_round_trip_uses_host_side_mask(self) -> None:
1565515656
# empty and json_stringify would emit `{}` = 2 characters.
1565615657
assert _run(source) == 14
1565715658

15659+
# --- Unit tests for the _validate_wrap_handle helper ---
15660+
#
15661+
# The validator is module-scope in `vera/codegen/api.py` so it
15662+
# can be tested directly without standing up a wasmtime
15663+
# instance. `_wrap_handle` (nested inside `execute()`) calls
15664+
# this helper. These tests pin all 5 failure modes the
15665+
# validator rejects.
15666+
15667+
def test_validate_wrap_handle_accepts_valid_range(self) -> None:
15668+
"""[0, 0x80000000) is the accepted range — no raise."""
15669+
from vera.codegen.api import _validate_wrap_handle
15670+
# Boundary lo, mid, boundary hi (last valid).
15671+
for raw in (0, 1, 12345, 0x7FFFFFFE, 0x7FFFFFFF):
15672+
_validate_wrap_handle(raw, kind=1, body_ptr=0x1000)
15673+
15674+
def test_validate_wrap_handle_rejects_negative(self) -> None:
15675+
"""Negative ints have bit 31 set in two's complement."""
15676+
from vera.codegen.api import _validate_wrap_handle
15677+
with pytest.raises(RuntimeError, match="#578.*outside the valid"):
15678+
_validate_wrap_handle(-1, kind=1, body_ptr=0x1000)
15679+
with pytest.raises(RuntimeError, match="#578"):
15680+
_validate_wrap_handle(-12345, kind=2, body_ptr=0x2000)
15681+
15682+
def test_validate_wrap_handle_rejects_at_2gb_boundary(self) -> None:
15683+
"""0x80000000 is the FIRST invalid value (range is half-open)."""
15684+
from vera.codegen.api import _validate_wrap_handle
15685+
with pytest.raises(RuntimeError, match="0x80000000"):
15686+
_validate_wrap_handle(0x80000000, kind=1, body_ptr=0x1000)
15687+
15688+
def test_validate_wrap_handle_rejects_above_32bit(self) -> None:
15689+
"""Values >= 2^32 truncate on _write_i32 — must be caught here.
15690+
15691+
The pre-tightening (round 1) bit-31-only check let these
15692+
through: `0x100000001 & 0x80000000 == 0`, so the check
15693+
passed, but `_write_i32` would truncate to `0x00000001`
15694+
and the unwrap mask would return that — a silent wrong
15695+
handle.
15696+
"""
15697+
from vera.codegen.api import _validate_wrap_handle
15698+
with pytest.raises(RuntimeError, match="#578"):
15699+
_validate_wrap_handle(0x100000000, kind=1, body_ptr=0x1000)
15700+
with pytest.raises(RuntimeError, match="#578"):
15701+
_validate_wrap_handle(0x100000001, kind=1, body_ptr=0x1000)
15702+
15703+
def test_validate_wrap_handle_rejects_non_int(self) -> None:
15704+
"""Non-int sentinels surface here, not deeper in the stack.
15705+
15706+
Without the isinstance check, `None` / `"5"` / etc. would
15707+
raise `TypeError` from the bitwise `&` operation in the
15708+
old check, producing a less actionable error.
15709+
"""
15710+
from vera.codegen.api import _validate_wrap_handle
15711+
for bad in (None, "5", 1.5, [1], {}):
15712+
with pytest.raises(RuntimeError, match="#578"):
15713+
_validate_wrap_handle(bad, kind=1, body_ptr=0x1000)
15714+
1565815715

1565915716
# =====================================================================
1566015717
# @Nat subtraction underflow runtime guard (#520)

vera/codegen/api.py

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,53 @@ class ConstructorLayout:
4040
total_size: int # total bytes, 8-byte aligned
4141

4242

43+
def _validate_wrap_handle(
44+
raw_handle: object, kind: int, body_ptr: int,
45+
) -> None:
46+
"""#578 invariant: raw_handle must fit in 31 unsigned bits.
47+
48+
Wrapper ADTs store ``raw_handle | 0x80000000`` at body offset 4 so
49+
the in-heap field is structurally outside the conservative-scan
50+
heap-range check (`heap_ptr` is hard-capped at 0x80000000 by the
51+
`$alloc` heap-ceiling guard). The unwrap site recovers the raw
52+
handle with ``& 0x7FFFFFFF``. Both directions break silently
53+
outside ``[0, 0x80000000)``:
54+
55+
- Negative ints have bit 31 set in two's complement.
56+
``raw_handle | 0x80000000`` is a no-op and the unwrap mask
57+
returns the WRONG handle.
58+
- Values ``>= 0x80000000`` alias into the top half and collide
59+
with the tag-bit pattern.
60+
- Values ``>= 0x100000000`` truncate on ``_write_i32`` and
61+
silently lose information.
62+
- Non-int values would ``TypeError`` deeper in the stack;
63+
catching them here makes the diagnostic actionable.
64+
65+
Practical alloc counters are bounded well below 2^31 — a 2B-handle
66+
session is wall-clock infeasible — but a silent round-trip
67+
failure is exactly the corruption class #578 sought to eliminate.
68+
Fail fast.
69+
70+
Module-level helper so it can be unit-tested directly without
71+
standing up a wasmtime instance (``_wrap_handle`` is nested
72+
inside ``execute()`` and not importable on its own).
73+
"""
74+
if not (
75+
isinstance(raw_handle, int)
76+
and 0 <= raw_handle < 0x80000000
77+
):
78+
raise RuntimeError(
79+
f"#578: raw_handle={raw_handle!r} (kind={kind!r}, "
80+
f"body_ptr={body_ptr!r}) is outside the valid "
81+
f"range [0, 0x80000000); cannot tag for the "
82+
f"conservative-scan disjointness invariant. "
83+
f"Host-store handle counters must be unsigned "
84+
f"31-bit integers. Either a counter overflowed, "
85+
f"a negative sentinel flowed in, or a non-integer "
86+
f"value flowed into _wrap_handle."
87+
)
88+
89+
4390
def _wasm_type_size(wt: str) -> int:
4491
"""Byte size of a WASM value type."""
4592
if wt == "i32":
@@ -956,37 +1003,11 @@ def _wrap_handle(
9561003
raise ValueError(f"#573: unknown wrap kind {kind}")
9571004
body_ptr = _call_alloc(caller, _WRAPPER_BODY_SIZE)
9581005
_write_i32(caller, body_ptr, tag)
959-
# #578 invariant: raw_handle must be an unsigned 31-bit
960-
# integer so ``raw_handle | 0x80000000`` round-trips
961-
# cleanly through the unwrap mask ``& 0x7FFFFFFF`` and
962-
# through ``_write_i32``'s 32-bit truncation. Both
963-
# directions break silently outside [0, 0x80000000):
964-
# - Negative ints have bit 31 set in two's complement,
965-
# so `_write_i32` truncates and the unwrap returns a
966-
# wrong handle.
967-
# - Values >= 0x80000000 alias into the top half and
968-
# collide with the tag-bit pattern.
969-
# - Values >= 0x100000000 wrap on ``_write_i32`` and
970-
# silently truncate to a wrong handle.
971-
# Practical alloc counters are bounded well below 2^31
972-
# — a 2B-handle session is wall-clock infeasible — but
973-
# a silent round-trip failure is exactly the kind of
974-
# latent corruption #578 itself sought to eliminate.
975-
# Fail fast.
976-
if not (
977-
isinstance(raw_handle, int)
978-
and 0 <= raw_handle < 0x80000000
979-
):
980-
raise RuntimeError(
981-
f"#578: raw_handle={raw_handle!r} (kind={kind!r}, "
982-
f"body_ptr={body_ptr!r}) is outside the valid "
983-
f"range [0, 0x80000000); cannot tag for the "
984-
f"conservative-scan disjointness invariant. "
985-
f"Host-store handle counters must be unsigned "
986-
f"31-bit integers. Either a counter overflowed, "
987-
f"a negative sentinel flowed in, or a non-integer "
988-
f"value flowed into _wrap_handle."
989-
)
1006+
# #578: validate raw_handle is in the unsigned-31-bit range
1007+
# before tagging. See ``_validate_wrap_handle`` at module
1008+
# scope (extracted so unit tests can exercise the 5 failure
1009+
# modes without standing up a wasmtime instance).
1010+
_validate_wrap_handle(raw_handle, kind, body_ptr)
9901011
# #578: store the handle ORed with 0x80000000 so the
9911012
# in-heap field can't be mistaken for a heap pointer by
9921013
# the conservative GC scan. Mirrors the WAT-side
@@ -2781,7 +2802,16 @@ def host_html_parse(
27812802
_alloc_string, _alloc_map_wrapper, root,
27822803
)
27832804
return _alloc_result_ok_i32(caller, html_ptr)
2784-
except Exception as exc:
2805+
except (ValueError, TypeError, AttributeError) as exc:
2806+
# Narrow catch matches `host_json_parse` above —
2807+
# parser failures surface as Result.Err. We
2808+
# deliberately do NOT catch invariant violations
2809+
# (e.g. the `_wrap_handle` RuntimeError from #578
2810+
# for an out-of-range handle, or any AssertionError
2811+
# from internal compiler bugs); those propagate
2812+
# to wasmtime as traps so the diagnostic text
2813+
# reaches the user instead of being repackaged
2814+
# as a user-domain parse error.
27852815
return _alloc_result_err_string(caller, str(exc))
27862816

27872817
linker.define_func(

0 commit comments

Comments
 (0)