v0.0.124: trap source backtrace (#516 Stage 2)#546
Conversation
Closes #516 Stage 2 (Stage 3 -- per-kind Fix paragraphs -- remains). Pre-fix, runtime traps surfaced only the kind taxonomy from Stage 1 (divide_by_zero / out_of_bounds / etc.) plus the trap message. The user got "Integer division by zero" with no indication of which of their functions divided by zero -- they had to read the raw wasmtime backtrace with hex offsets and translate WAT names back to source. Stage 2 walks wasmtime.Trap.frames after classification, looks each frame's WAT name up in a new CompileResult.fn_source_map (built during codegen by _register_fn for top-level functions and by the closure-lifting pass for $anon_N helpers), and produces a structured backtrace: list of {func, file, line_start, line_end, is_builtin} dicts, leaf-first to match wasmtime / gdb / Python convention. Per-function granularity, not per-line: wasmtime-py doesn't expose WAT-to-WASM debug-info plumbing, so byte-offset -> source-line mapping isn't viable. The issue's own success criterion is "even a coarse 'trap inside cell_at at or after line 84' would be a big step up", which is per-function. Resolution rules in _resolve_trap_frames (vera/codegen/api.py): * Built-ins are tagged as <builtin>, not dropped. WAT functions named alloc / gc_collect / contract_fail, plus anything starting with exn_ / vera. / closure_sig_, are runtime infrastructure with no Vera source. Surfacing them would claim a misleading file:line; tagging them keeps the chain visible without lying. * Monomorphized generics suffix-strip at the rightmost $ (identity$Int -> identity). $ cannot appear in user-written Vera identifiers, so any $ in a WAT name was inserted by the monomorphizer. * Lifted closures register under anon_N with the source span of the original fn(...) syntactic site, so a trap inside a closure points at the closure body, not at the synthetic top-level wrapper. * Unknown user-named frames are surfaced with <unknown> location, not dropped. Better to show the WAT name with no location than lose the frame entirely; any future source-map gap is diagnosable from the unknown markers. cmd_run text mode appends a "Source backtrace:" block after the error line. Leading runtime-helper frames are collapsed with a "(suppressed N runtime-helper frames above first user code)" marker so the user sees their own code at the top of the trace. JSON mode adds a frames array to each diagnostic with the full structured backtrace (including built-ins, so machine consumers see the full picture for telemetry). Tests: 15 new tests across 3 classes in tests/test_runtime_traps.py: * TestResolveTrapFrames516 (7) -- unit tests with a _FakeFrame shim: user-fn resolution, built-in tagging, built-in-prefix matching, monomorphized base-name fallback, unknown-name surfacing, defensive empty-frames behaviour, leaf-first ordering. * TestTrapSourceBacktrace516 (5) -- end-to-end via cmd_run: text-mode surface, leaf-first ordering across stream output, JSON envelope frames array, contract-violation backtrace, direct execute() WasmTrapError.frames attachment. * TestSourceMapPopulation516 (3) -- light-weight inspection of CompileResult.fn_source_map: top-level fns registered, lifted closures registered under anon_N, built-in helpers NOT registered (regression guard against bogus user-frame entries). 3,576 tests pass + 14 skipped (was 3,561+14). mypy clean. All 81 conformance + 33 examples pass. uv.lock regenerated for version bump. Co-Authored-By: Claude <noreply@anthropic.invalid>
|
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:
📝 WalkthroughWalkthroughAdds Stage 2 runtime-trap source mapping: compiler emits Changes
Sequence Diagram(s)sequenceDiagram
participant Codegen as CodeGenerator
participant Registry as Registration/Closures
participant CompileRes as CompileResult
participant Wasm as wasmtime
participant Resolver as _resolve_trap_frames
participant CLI as cmd_run/execute
Codegen->>Registry: register functions, lift closures
Registry-->>Codegen: populate _fn_source_map (name -> file,start,end)
Codegen->>CompileRes: return CompileResult(fn_source_map, prelude_fn_names)
Wasm-->>CompileRes: trap with wasmtime.Trap.frames
CompileRes->>Resolver: resolve_frames(wasm_frames, fn_source_map, prelude_fn_names)
Resolver-->>CompileRes: list of {func,file,line_start,line_end,is_builtin}
CompileRes->>CLI: raise/return WasmTrapError with frames
CLI->>CLI: format output (text: "Source backtrace:", JSON: diagnostics[0].frames)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
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 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #546 +/- ##
==========================================
+ Coverage 91.02% 91.04% +0.01%
==========================================
Files 58 58
Lines 22034 22126 +92
Branches 259 259
==========================================
+ Hits 20056 20144 +88
- Misses 1971 1975 +4
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: 4
🤖 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_runtime_traps.py`:
- Around line 974-1000: The test currently only checks that an "anon_N" key
exists in result.fn_source_map; enhance it to validate the mapped source span
for the lifted closure by locating one anon key from result.fn_source_map (use
the same anon_keys selection) and asserting its value contains the expected
(file, line_start, line_end) tuple (i.e., the source span for the inline fn body
in test_lifted_closure_registered_under_anon_id); reference result.fn_source_map
and anon_keys to fetch the mapping and compare the mapping's
file/line_start/line_end fields to the known coordinates of the closure in the
provided source string.
- Around line 844-910: The JSON-mode contract-violation path in
test_contract_violation_carries_backtrace() doesn't assert that stderr is empty
and doesn't verify diagnostics[0]["frames"] when cmd_run is invoked in JSON
mode; update the test (or add a new JSON variant) to call rc =
cmd_run(str(path), as_json=True), assert rc == 1, parse captured.out as JSON to
check that diagnostics[0] contains a "frames" list with the expected "positive"
and "main" user frames, and also assert captured.err == "" to enforce the
JSON-mode invariant (use existing symbols
test_contract_violation_carries_backtrace, cmd_run, captured, and diagnostics).
In `@vera/cli.py`:
- Around line 688-695: The code currently only sets diag["frames"] when
exc.frames is truthy; change it to always include the frames key so the JSON
shape is consistent: unconditionally assign diag["frames"] = exc.frames
(ensuring exc is the WasmTrapError instance carrying the frames from
_resolve_trap_frames()), instead of the existing if exc.frames: branch so
consumers receive "frames": [] when there are no resolved frames.
In `@vera/codegen/api.py`:
- Around line 247-276: The frame name needs normalization: before checking
_BUILTIN_NAMES, _BUILTIN_PREFIXES, and fn_source_map, strip a single leading '$'
from the local variable name (e.g., normalize name = name[1:] if
name.startswith('$')) so names like "$alloc" or "$main" will match builtin sets
and the fn_source_map lookup; update the docstring for the func field
(previously "wasmtime strips it already") to say "any leading `$` stripped (some
wasmtime versions omit it)". Ensure you apply the normalized name for both the
is_builtin check and the fn_source_map lookup while preserving the original name
where you still want to display the raw frame if necessary.
🪄 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: 9adf33f3-1ab0-48e2-b2ce-0ffe8fa793e9
⛔ 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 (14)
CHANGELOG.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdpyproject.tomltests/test_runtime_traps.pyvera/__init__.pyvera/cli.pyvera/codegen/api.pyvera/codegen/closures.pyvera/codegen/core.pyvera/codegen/registration.py
Four CodeRabbit findings actioned plus filed #547 to track #516 Stage 3 (per-kind Fix paragraphs) per the workflow rule "PR-generated new issues must be added to a tracker in the same PR". Code fixes: * vera/codegen/api.py _resolve_trap_frames: defensive leading-$ strip on frame name before the builtin-allowlist check and the fn_source_map lookup. Verified empirically that current wasmtime-py strips $ already (a divide-by-zero trap inside `(func $bad ...)` returns func_name='bad'), but the normalisation is one cheap line and protects against any future wasmtime version that leaves the $ in. Without it, a regression there would silently break every backtrace lookup with no clear diagnostic. * vera/cli.py JSON envelope: always include the "frames" key (even as an empty list) instead of the old `if exc.frames:` guard. Matches the always-present `trap_kind` shape; consumers can iterate `diag["frames"]` directly without `.get(..., [])` ceremony. Schema stability for a structural field outweighs the envelope-minimalism win. Test fixes: * tests/test_runtime_traps.py test_lifted_closure_registered_under_anon_id: was checking only that an anon_N key exists; now also asserts the registered span value (file = "<unknown>" via the compile-from-string path, line_start = 4, line_end = 6 — the exact lines of the inline closure literal in the test fixture). A future bug that registered the wrong AST node (e.g. picking up the enclosing array_map call instead of the AnonFn) would fail this test with a clear coordinate mismatch instead of silently surfacing the wrong file:line on a real trap inside the closure body. * tests/test_runtime_traps.py: added test_contract_violation_json_mode_includes_frames as a JSON variant of the existing text-mode contract-violation backtrace test. Pins the JSON envelope's frames array AND the JSON-mode- no-stderr-leak invariant from #543. Without it, a future refactor could surface the backtrace in text mode but drop it from the JSON path, and CI would stay green. Docs: * TESTING.md: 3,590 -> 3,591 total tests; test_runtime_traps.py row updated 39->40 / 1,021->1,098 lines. * ROADMAP.md, README.md, HISTORY.md: 3,590 -> 3,591 across all three references. #547 (Stage 3) is filed with concrete content (per-kind Fix paragraph drafts, refactor scope for _classify_trap, CLI surface plan, test plan). Will be picked up after #517 (TCO) lands so the stack_exhausted Fix paragraph can reference return_call as shipped instead of "see #517 for the planned fix". Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@HISTORY.md`:
- Line 261: Trim the HISTORY.md entry for v0.0.124 to a single concise,
user-facing sentence (e.g., "v0.0.124 — Runtime traps now include source
backtraces") and remove the implementation and UI details (references to
WasmTrapError.frames, `Source backtrace:` block, `frames` JSON field,
per-function granularity, and `vera run` text mode wording) from that row; move
the removed technical/UI specifics into CHANGELOG.md under the v0.0.124 section
so the HISTORY remains a one-line summary while CHANGELOG contains the detailed
notes.
In `@README.md`:
- Line 184: Update the README paragraph that currently claims every runtime
error ships with a tailored "Fix:" section so it accurately reflects the shipped
state for v0.0.124: change the diagnostics wording to state that source
backtraces are now included (Stage 2 for traps) but that per-kind "Fix:"
suggestion paragraphs (Stage 3) are still pending; specifically edit the
sentence containing "Vera is in **active development** at v0.0.124" and any
adjacent diagnostic claim lines to remove/soften the blanket "every runtime
error" / tailored "Fix:" wording and replace it with a concise statement that
backtraces are available while "Fix:" suggestions remain forthcoming.
In `@ROADMAP.md`:
- Line 11: Update the roadmap’s opening version string to match the documented
totals by replacing the phrase "Vera v0.0.119" with "Vera v0.0.124" in the top
summary sentence so the release baseline matches the v0.0.124-era counts
described below.
In `@TESTING.md`:
- Line 82: Update the TESTING.md summary to explicitly list the Stage 2
assertions: mention `_resolve_trap_frames` unit tests enforce leaf-first
ordering of frames and include cases for
user-fn/built-in/monomorphized/unknown-name/no-frames; and call out the
text-mode `cmd_run` end-to-end checks that assert the suppression marker for
collapsed leading runtime-helper frames (as well as the JSON-mode
backtrace/trap_kind invariants already described). Ensure the phrasing names
`_resolve_trap_frames`, `cmd_run`, "leaf-first", and "suppression marker" so the
test inventory precisely matches the shipped contract.
🪄 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: 79f38120-3123-494b-a6a6-14df5448ee74
📒 Files selected for processing (7)
HISTORY.mdREADME.mdROADMAP.mdTESTING.mdtests/test_runtime_traps.pyvera/cli.pyvera/codegen/api.py
Three of four CodeRabbit findings actioned plus two new tests for the suppression-marker code path that round 4's TESTING.md update implied existed. Actioned: * HISTORY.md v0.0.124 row trimmed to a single concise sentence. The previous wording carried implementation detail (WasmTrapError.frames, Source backtrace: block, JSON envelope shape, granularity rationale) that belongs in CHANGELOG, not in the HISTORY one-line-summary table. This is the *third* time the user has corrected the same drift (memory note tagged at v0.0.121 noted it was "the third time" then; this is now four). * ROADMAP.md opening line "Vera v0.0.119 delivers..." -> "Vera v0.0.124 delivers...". The compiler-pipeline blurb itself is unchanged (no pipeline additions since v0.0.119); the version number was just stale. * tests/test_runtime_traps.py: added test_text_mode_collapses_leading_runtime_helper_frames and test_text_mode_does_not_collapse_when_all_frames_are_builtins. Pre-fix the suppression-marker logic in cli.py was untested (real GC / allocator traps with builtin-leaf frames are timing-sensitive and not deterministically reproducible from a Vera fixture); new tests monkeypatch vera.codegen.execute to raise a WasmTrapError with synthetic frame chains, so the pure suppression logic is exercised against deterministic input. First test: 2 builtin leaf frames + 1 user frame -> assert "suppressed 2 runtime-helper frames" appears, builtin frames collapsed, user frame surfaced at top. Second test: builtin- only chain -> assert NO suppression marker, both helpers displayed (the "only collapse if a user frame remains" guard). * TESTING.md: row description updated to name _resolve_trap_frames, cmd_run, "leaf-first" ordering, and "suppression marker" explicitly per CodeRabbit's request. Coverage is now accurate (was previously claiming "order" without naming "leaf-first"; not claiming suppression-marker coverage which now exists). Not actioned: * README.md "Fix:" claim — the README's "Every error includes... how to fix it" sentence at line 74 is in the "Errors are instructions" section illustrated by an E001 example. The claim is about compile-time errors (E001-E702 with stable Diagnostic shape carrying description / rationale / fix / spec_ref) and has been accurate since v0.0.43 when stable error codes shipped. Runtime trap diagnostics are a separate surface that's still in progress (Stage 2 shipped; Stage 3 tracked as #547). The CodeRabbit instruction was to edit "the sentence containing 'Vera is in **active development** at v0.0.124'" but there's no Fix-claim adjacent to that sentence (it's at line 74, the flagged line is 184) and the actual Fix-claim it's about isn't invalidated by Stage 3 still being open. Replying inline on the PR with this analysis. Doc counts: 3,591 -> 3,593 across TESTING.md / ROADMAP.md / README.md / HISTORY.md (two new tests added in this round). Co-Authored-By: Claude <noreply@anthropic.invalid>
CodeRabbit flagged a real bug I missed in the previous round 3 commit:
prelude / inject_prelude functions were falling through to <unknown>
in the trap-frame resolver instead of being tagged as <builtin>, and
worse — they were ALREADY landing in fn_source_map with completely
bogus coordinates pointing at the prelude's *embedded* source string,
not the user's file.
Concrete failure mode pre-fix:
Source map after compiling a 3-line user program with option_unwrap_or:
'option_and_then' -> ('/tmp/foo.vera', 31, 40)
'option_map' -> ('/tmp/foo.vera', 20, 29)
'option_unwrap_or' -> ('/tmp/foo.vera', 9, 18)
'result_map' -> ('/tmp/foo.vera', 55, 64)
'result_unwrap_or' -> ('/tmp/foo.vera', 44, 53)
'run' -> ('/tmp/foo.vera', 1, 3)
The user's file is 3 lines long. The 9-64 range coordinates come
from inject_prelude's parse_to_ast on a synthetic Vera string — they
have line numbers from THAT parse, attached to the user's file path.
A trap inside option_unwrap_or would have surfaced as
"in option_unwrap_or (/tmp/foo.vera:9-18)" pointing at a region of
the user's file that doesn't exist.
Original detection criterion was decl.span is None (in registration.py)
on the assumption that synthetic AST nodes wouldn't have spans.
Wrong: inject_prelude calls parse_to_ast on inline Vera source, so
the resulting FnDecls DO have spans (just from the synthetic source).
Fix: detect prelude functions by their position in the registration
flow instead of by an AST-level marker. In core.py, the post-prelude
loop (which calls _register_fn for FnDecls not in `existing_fns`) is
exactly the set of injected prelude declarations. Each call there
now (1) registers via _register_fn as before, then (2) immediately
moves the entry from _fn_source_map to _prelude_fn_names. Result:
prelude functions are tagged as <builtin> by the resolver and the
source map only contains user code + lifted closures with valid
coordinates.
Resolver changes (api.py _resolve_trap_frames):
* New optional `prelude_fn_names: set[str] | None` parameter.
Default None preserves the existing 2-arg signature for backward
compat (test helpers, future LSP consumers).
* New is_prelude check: matches the exact name first, then tries
the base name for monomorphized generics (so `array_map$Int` —
even though array_map is a translator builtin not a prelude FnDecl
— would resolve correctly if it ever became one in the future).
This mirrors the suffix-strip rule already used for the source-
map lookup.
* Wired through execute() so the WasmTrapError.frames returned to
CLI / library consumers reflects the corrected classification.
Tests: added 4 new tests, total 46 in test_runtime_traps.py:
* test_prelude_function_tagged_as_builtin — synthetic prelude name
via _FakeFrame, asserts is_builtin=True and file=<builtin>
* test_monomorphized_prelude_tagged_as_builtin — synthetic
array_map$Int frame against {array_map} prelude set, asserts
base-name fallback works
* test_prelude_fn_names_optional_for_backward_compat — pins the
optional shape of the new parameter
* test_prelude_functions_registered_as_builtins — full compile of
a program using option_unwrap_or; asserts the prelude combinators
land in prelude_fn_names AND are NOT in fn_source_map (the bogus-
coordinates regression that motivated the fix)
Doc counts: 3,593 -> 3,597 across TESTING.md / ROADMAP.md / README.md
(four new tests).
Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
HISTORY.md (1)
261-261:⚠️ Potential issue | 🟡 MinorTrim the row to a single outcome sentence.
This still reads like CHANGELOG detail; keep only the user-facing outcome here and move the Stage 3 follow-up to
CHANGELOG.md.Based on learnings: HISTORY file is single-line summaries, and detail is for the CHANGELOG.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@HISTORY.md` at line 261, Edit the HISTORY.md table row for v0.0.124 so it contains only a single user-facing outcome sentence (e.g. "Runtime traps now include a source backtrace.") and remove the Stage 3 / per-`kind` follow-up text; move the Stage 3 tracking note (the reference to Stage 3 and issue `#547`) into CHANGELOG.md as a detailed follow-up entry. Target the table row starting with "v0.0.124" and the phrase "Runtime traps now include a source backtrace" when making the change.
🤖 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_runtime_traps.py`:
- Around line 775-778: The comment is outdated: prelude/builtin classification
no longer depends on decl.span being None; instead Update the wording to state
that prelude functions are tracked via the CompileResult.prelude_fn_names set
(populated by _register_fn when a function is registered as part of the prelude)
and that the resolver consults prelude_fn_names alongside the runtime-helper
allowlist when deciding builtin/prelude handling; apply the same replacement for
the other similar comments (previous span-based wording) near the other
occurrence.
In `@vera/codegen/api.py`:
- Around line 105-107: Update the stale comment about prelude detection to
remove the span-based heuristic and document the current registration-flow
criterion: explain that synthetic prelude Fns are identified by their
registration position during compile_program (and are registered by _register_fn
for injected preludes), not by decl.span being None; also mention that
_resolve_trap_frames consumes that registration-based metadata so future readers
won’t revert to the old decl.span check.
In `@vera/codegen/core.py`:
- Around line 137-164: The comments above self._fn_source_map and
self._prelude_fn_names are stale and describe an old heuristic (span is None /
monomorphised-name behavior); update them to reflect the current flow: note that
inject_prelude via parse_to_ast now assigns real spans and that prelude
classification is determined by registration order during _register_fn (not by
checking FnDecl.span), and remove or rewrite the monomorphised-name warning to
state that trap-time resolver strips rightmost `$` suffix when looking up base
names; also mention that built-in helpers remain tagged as "<builtin>" and that
_prelude_fn_names is populated based on registration flow rather than span
absence.
---
Duplicate comments:
In `@HISTORY.md`:
- Line 261: Edit the HISTORY.md table row for v0.0.124 so it contains only a
single user-facing outcome sentence (e.g. "Runtime traps now include a source
backtrace.") and remove the Stage 3 / per-`kind` follow-up text; move the Stage
3 tracking note (the reference to Stage 3 and issue `#547`) into CHANGELOG.md as a
detailed follow-up entry. Target the table row starting with "v0.0.124" and the
phrase "Runtime traps now include a source backtrace" when making the change.
🪄 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: 3a7368cc-8339-455c-ab99-1e47ac0cbee9
📒 Files selected for processing (8)
HISTORY.mdREADME.mdROADMAP.mdTESTING.mdtests/test_runtime_traps.pyvera/codegen/api.pyvera/codegen/core.pyvera/codegen/registration.py
All four CodeRabbit findings actioned. Three are stale-comment fixes pointing at the previous commit's prelude-detection refactor that left explanatory comments referring to the old `decl.span is None` heuristic; one is the fifth-time HISTORY drift on the same v0.0.124 row. Stale-comment fixes (all three reflect the actual current criterion "prelude detection is by registration-flow position in compile_program, not by AST-level span absence" — the failure mode being avoided is that inject_prelude calls parse_to_ast on inline Vera source, so its synthetic FnDecls do have spans, just spans pointing at synthetic source coordinates): * tests/test_runtime_traps.py — TestResolveTrapFrames516 test_prelude_function_tagged_as_builtin docstring updated. * vera/codegen/api.py — CompileResult.prelude_fn_names field comment updated; explicitly notes detection is registration-flow not span-based, with the failure mode that motivated the change. * vera/codegen/core.py — both _fn_source_map and _prelude_fn_names field comments updated; documents the post-inject_prelude loop in compile_program as the actual registration site, retains the monomorphized-name suffix-strip rule documentation, retains the built-in helper exclusion rationale. HISTORY trim (5th time on the same drift): * HISTORY.md v0.0.124 row collapsed to a single outcome sentence: "Runtime traps now include a source backtrace ([#516] Stage 2)." The Stage 3 / #547 follow-up reference moved into the CHANGELOG (new "Stage 3 follow-up" subsection in the v0.0.124 entry) where forward-looking process information belongs. Per the memory note hardened in the previous round: "more than one ` — ` separator OR more than one sentence = already drifted, cut". The row now has zero separators and one sentence — within bounds. Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
vera/codegen/core.py (1)
317-319:⚠️ Potential issue | 🟡 MinorFilter monomorphised prelude functions out of
_fn_source_maphere as well.The post-
inject_preludecleanup only removes the original prelude names. When a generic prelude helper is instantiated later,_register_fn(mdecl)adds the$-suffixed clone back with its synthetic prelude span, soCompileResult.fn_source_mapcan still contain bogus entries such asarray_map$Int. That makes correct trap attribution depend on_resolve_trap_framesconsultingprelude_fn_namesbefore any exact source-map hit.Based on learnings, "
CompileResulthas aprelude_fn_namesset ... The resolver consults this set ... It also applies the rightmost-$strip rule to monomorphized prelude calls."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@vera/codegen/core.py` around lines 317 - 319, When registering monomorphised prelude helpers in the loop that calls _register_fn for each mdecl, skip adding entries to _fn_source_map for synthetic prelude clones: compute the effective base name by removing the rightmost '$' suffix (if present) from mdecl.name and, if that base name is in CompileResult.prelude_fn_names (or self._compile_result.prelude_fn_names), do not call _register_fn for that mdecl; this prevents $-suffixed prelude instantiations (e.g. array_map$Int) from being reintroduced into _fn_source_map and aligns with how _resolve_trap_frames and prelude_fn_names are used elsewhere.
♻️ Duplicate comments (1)
tests/test_runtime_traps.py (1)
763-784:⚠️ Potential issue | 🟡 MinorCorrect the stale span-based invariant in these comments.
These docstrings still say injected prelude/builtin functions are identified by missing spans or by
decl.span is not None. That is no longer the model: prelude functions do have spans, and they stay out offn_source_mapbecause the post-prelude registration flow moves them intoprelude_fn_names.Based on learnings, the original
decl.span is Noneheuristic for prelude detection was incorrect; the correct criterion is registration-flow position incompile_program.Also applies to: 1341-1344
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_runtime_traps.py` around lines 763 - 784, Update the outdated comments that claim prelude/builtin functions are detected by missing spans (e.g., "decl.span is None") and instead document the current behavior: prelude functions are registered via the post-prelude registration loop in compile_program and are recorded in CompileResult.prelude_fn_names (which is why they are absent from fn_source_map), not by decl.span; also remove or correct any references to the old span-based heuristic (including mentions near fn_source_map, inject_prelude, and decl.span) and mirror this corrected explanation in the other affected block around lines 1341-1344.
🤖 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_runtime_traps.py`:
- Around line 1059-1194: Add a JSON-mode sibling to the existing text-mode
synthetic-frame tests: reuse the same synthetic_frames and monkeypatched
fake_execute that raises WasmTrapError, call cmd_run(str(path), as_json=True),
assert rc == 1 and capsys shows captured.err == "" and then parse the JSON
diagnostics (e.g., inspect diagnostics[0]["frames"]) to assert the builtin
helper frames (gc_collect and alloc with is_builtin=True and file "<builtin>")
are present and not filtered or rewritten; keep the existing text-mode
assertions unchanged.
In `@vera/codegen/api.py`:
- Around line 205-218: Define a dataclass (e.g., Stage2Frame or TrapFrame) that
models the trap frame fields used by _resolve_trap_frames and any consumers (CLI
formatter, JSON output) instead of using list[dict[str, object]]; update the
exception class attribute self.frames and the _resolve_trap_frames
signature/return type to use list[TrapFrame], modify the resolver to construct
and return TrapFrame instances, and adjust downstream code
(formatters/serializers) to accept TrapFrame objects (or convert them to dicts
at the serialization boundary) so mypy can type-check field access and prevent
key drift.
- Around line 2791-2799: The code currently passes the top-level exc to
_resolve_trap_frames, which misses frame data if a nested exception (e.g., a
Trap on __cause__ or __context__) carries .frames; change the call site to walk
the exception chain (using __cause__ and __context__, same pattern used for
_VeraExit handling) to find the first exception object that has a .frames
attribute and pass that exception into _resolve_trap_frames instead of the
original exc; keep existing usage of _classify_trap(exc, last_violation)
unchanged and only swap the argument fed to _resolve_trap_frames.
---
Outside diff comments:
In `@vera/codegen/core.py`:
- Around line 317-319: When registering monomorphised prelude helpers in the
loop that calls _register_fn for each mdecl, skip adding entries to
_fn_source_map for synthetic prelude clones: compute the effective base name by
removing the rightmost '$' suffix (if present) from mdecl.name and, if that base
name is in CompileResult.prelude_fn_names (or
self._compile_result.prelude_fn_names), do not call _register_fn for that mdecl;
this prevents $-suffixed prelude instantiations (e.g. array_map$Int) from being
reintroduced into _fn_source_map and aligns with how _resolve_trap_frames and
prelude_fn_names are used elsewhere.
---
Duplicate comments:
In `@tests/test_runtime_traps.py`:
- Around line 763-784: Update the outdated comments that claim prelude/builtin
functions are detected by missing spans (e.g., "decl.span is None") and instead
document the current behavior: prelude functions are registered via the
post-prelude registration loop in compile_program and are recorded in
CompileResult.prelude_fn_names (which is why they are absent from
fn_source_map), not by decl.span; also remove or correct any references to the
old span-based heuristic (including mentions near fn_source_map, inject_prelude,
and decl.span) and mirror this corrected explanation in the other affected block
around lines 1341-1344.
🪄 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: 47e149a5-33d1-4343-9ced-757f883e6077
📒 Files selected for processing (6)
CHANGELOG.mdHISTORY.mdTESTING.mdtests/test_runtime_traps.pyvera/codegen/api.pyvera/codegen/core.py
…in walk + monomorphized-prelude skip
All five CodeRabbit findings actioned. The two structural ones are
shape-of-the-shipped-API decisions worth getting right before this
PR lands; the three smaller ones are correctness / cleanliness /
test-coverage tightening.
* `TrapFrame` dataclass replaces `list[dict[str, object]]`
(vera/codegen/api.py). New `@dataclass(frozen=True)` with five
fields (func / file / line_start / line_end / is_builtin) and a
`to_dict()` method for JSON serialisation at the envelope
boundary. `_resolve_trap_frames` now returns `list[TrapFrame]`;
`WasmTrapError.frames` is `list[TrapFrame]`. Mypy can now type-
check field access — the previous stringly-typed dict allowed
silent typos like `frame["fucn"]`. Safe to change shape now
because the API hasn't shipped externally yet (Stage 2 is in
this still-open PR).
* Exception-chain walk for `.frames` via new
`_find_frames_in_exception_chain` helper (vera/codegen/api.py).
Some wasmtime call paths wrap a `Trap` (which carries `frames`)
inside a `WasmtimeError` (which doesn't); inspecting only the
outer exception silently drops the backtrace. The helper walks
`__cause__` / `__context__` until a frame sequence is found —
same pattern as the existing `_VeraExit` chain walk in
`execute()`. Folded into the resolver itself rather than at the
call site so direct callers (tests, future LSP) get the chain
walk for free.
* Skip monomorphized prelude registration (vera/codegen/core.py).
After Pass 1.5 (monomorphize), each clone's `_register_fn` would
re-add prelude entries to `_fn_source_map` with the same bogus
coordinates the post-prelude loop just scrubbed out for the base
names (e.g. `option_unwrap_or$Int` -> the original generic's
prelude span). Now: after registering each `mdecl`, suffix-strip
at the rightmost `$` and if the base is in `_prelude_fn_names`,
pop the entry from `_fn_source_map`. The trap resolver already
handled this correctly via the same suffix-strip rule, so this
is cleanliness rather than a correctness fix — but cleaner to
not have dead entries in the map.
* New `test_json_mode_preserves_full_frame_chain_including_builtins`
in tests/test_runtime_traps.py — JSON-mode sibling of the text-
mode collapse tests. Pins the contract that JSON mode preserves
the FULL backtrace (helpers included, leaf-first order) while
text mode collapses for display. Without this, a refactor
could accidentally apply the text-mode collapse to JSON output
too and downstream consumers (telemetry, LSP) would silently
start receiving filtered chains. Same monkeypatch strategy as
the text-mode tests so a single mock surface exercises both
paths.
* Stale span-based comment fixed in test (line 1341-1344). The
`test_no_spurious_entries_for_builtins` docstring still claimed
built-ins are detected via "decl.span is None" — leftover from
round 4. Replaced with the accurate explanation: WASM helpers
($alloc, $gc_collect, $contract_fail, $exn_*, $vera.*) are
emitted directly into WAT by the assembly module and never go
through `_register_fn`, which is why they don't appear in the
source map. Prelude functions go through `_register_fn` and
are then moved to `_prelude_fn_names` by the post-prelude loop
— that path is covered separately by
`test_prelude_functions_registered_as_builtins`.
Cli.py + tests updated to use `frame.func` / `frame.file` etc.
instead of `frame["func"]` / `frame.get("func")` ; the JSON
envelope serialises via `[f.to_dict() for f in exc.frames]` so
the wire format stays unchanged. Mypy clean; full suite passes
3,584 + 14 skipped (was 3,583 + 14).
Doc counts: 3,597 -> 3,598 across TESTING.md / ROADMAP.md /
README.md / HISTORY.md (one new test added).
Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ROADMAP.md`:
- Line 11: Update the Milestone 3c text that still says “expand from 30 to 50+
examples” to reflect the current canonical count of 33 examples; locate the
Milestone 3c sentence in ROADMAP.md (the line mentioning example growth) and
replace “30” (and any “50+” phrasing if intended as baseline) with “33” and add
a parenthetical note pointing to check_doc_counts.py as the authoritative source
for the example count so future updates reference the script (edit the Milestone
3c description and any nearby numeric mentions to match the new canonical
count).
In `@tests/test_runtime_traps.py`:
- Around line 831-845: Rename the test and update its docstring to reflect the
actual semantics: change the function name
test_frames_preserved_in_outermost_first_order to something like
test_frames_preserved_in_innermost_first_order (or
test_frames_preserved_leaf_first) and update the docstring to state "Order
matches wasmtime's backtrace (innermost/leaf-first)" so it matches the inline
comment and the assertion that expects ["inner", "outer"]; keep the body,
assertion and the inline comment unchanged.
In `@vera/cli.py`:
- Around line 755-762: Move the suppression message to after the "Source
backtrace:" header so it reads as part of the backtrace: in the block that
checks user_idx (variables user_idx and hidden) and prints to sys.stderr, first
print the "Source backtrace:" header (the print("Source backtrace:",
file=sys.stderr) call), then, if user_idx and user_idx > 0, print the
suppression line using the existing hidden calculation and pluralization logic
to sys.stderr; retain the exact message formatting and file=sys.stderr target
but swap the order so the header comes before the "(suppressed …)" line.
🪄 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: a4b469f2-5c5b-4049-98c3-4c44ed0381b0
📒 Files selected for processing (8)
HISTORY.mdREADME.mdROADMAP.mdTESTING.mdtests/test_runtime_traps.pyvera/cli.pyvera/codegen/api.pyvera/codegen/core.py
All three CodeRabbit findings actioned.
* ROADMAP Milestone 3c "expand from 30 to 50+ examples" -> "expand
from 33 to 50+ examples" plus a parenthetical pointing future
updates at scripts/check_doc_counts.py as the canonical source.
The "30" figure dated to v0.0.101 / Stage 9; we've shipped three
examples since (string_utilities + two others) without bumping
this baseline.
* tests/test_runtime_traps.py: renamed
test_frames_preserved_in_outermost_first_order ->
test_frames_preserved_in_leaf_first_order. The previous name
was the literal opposite of what the test asserts — the body
asserts ["inner", "outer"] (leaf-first / innermost-first) and
the inline comment correctly says "wasmtime returns inner-first;
we preserve that order so the human reading the backtrace sees
the leaf first". Docstring rewritten to use the correct
terminology with a note explaining why we follow wasmtime / gdb
(leaf-first) rather than Python's traceback convention (root-
first). Body, assertion, and inline comment unchanged — only
the name and docstring drifted.
* vera/cli.py: moved the suppression message to fire AFTER the
"Source backtrace:" header rather than before it. Reads more
naturally: the suppression line is metadata about the backtrace
below it, so it belongs under the heading. Pre-fix:
(suppressed 2 runtime-helper frames above first user code)
Source backtrace:
in main (foo.vera:1-3)
Post-fix:
Source backtrace:
(suppressed 2 runtime-helper frames above first user code)
in main (foo.vera:1-3)
* tests/test_runtime_traps.py
test_text_mode_collapses_leading_runtime_helper_frames: added
a position-ordering assertion so a future reorder regression
fails immediately. Substring checks alone wouldn't catch it.
New assertion: header_pos < suppress_pos < main_pos.
Doc counts: TESTING.md test_runtime_traps.py line count refreshed
1,440 -> 1,465 (the new ordering assertion added 25 lines). Full
suite still 3,598 / 3,584 passed + 14 skipped.
Co-Authored-By: Claude <noreply@anthropic.invalid>
Closes #516 Stage 2 (Stage 3 — per-
kindFix:paragraphs — remains).What changed
Pre-fix, runtime traps surfaced only the kind taxonomy from Stage 1 (
divide_by_zero/out_of_bounds/ etc.) plus the trap message. The user got "Integer division by zero" with no indication of which of their functions divided by zero — they had to read the raw wasmtime backtrace with hex offsets and translate WAT names back to source.Stage 2 walks
wasmtime.Trap.framesafter classification, looks each frame's WAT name up in a newCompileResult.fn_source_map(built during codegen by_register_fnfor top-level functions and by the closure-lifting pass for$anon_Nhelpers), and produces a structured backtrace: list of{func, file, line_start, line_end, is_builtin}dicts, leaf-first to match wasmtime / gdb / Python convention.Before:
After:
JSON envelope adds:
Why per-function, not per-line
Per-line requires correlating WAT instructions to WASM byte offsets after assembly, which
wasmtime-pydoesn't expose (no public WAT-to-WASM debug-info channel). Per-function is what the issue itself accepts as Stage 2 success: "even a coarse 'trap inside cell_at at or after line 84' would be a big step up".Resolution rules in
_resolve_trap_frames<builtin>, not dropped. WAT functions namedalloc/gc_collect/contract_fail, plus anything starting withexn_/vera./closure_sig_, are runtime infrastructure with no Vera source. Surfacing them would claim a misleading file:line; tagging them keeps the chain visible without lying.$.identity$Int→identity.$cannot appear in user-written Vera identifiers, so any$in a WAT name was inserted by the monomorphizer (_mangle_fn_name).anon_Nwith the source span of the originalfn(...)syntactic site, so a trap inside a closure points at the closure body, not at the synthetic top-level wrapper.<unknown>location, not dropped — better to show the WAT name with no location than lose the frame entirely.CLI surface
cmd_runtext mode appends aSource backtrace:block after the error line. Leading runtime-helper frames are collapsed with a(suppressed N runtime-helper frames above first user code)marker so the user sees their own code at the top of the trace. JSON mode adds aframesarray to each diagnostic with the full structured backtrace (including built-ins, so machine consumers see the full picture for telemetry).Tests
15 new tests across 3 classes in
tests/test_runtime_traps.py:TestResolveTrapFrames516(7 tests) — unit tests for the resolver with a_FakeFrameshim: user-fn resolution, built-in tagging, built-in-prefix matching, monomorphized base-name fallback, unknown-name surfacing, defensive empty-frames behaviour, leaf-first ordering.TestTrapSourceBacktrace516(5 tests) — end-to-end viacmd_run: text-mode surface, leaf-first ordering across stream output, JSON envelopeframesarray, contract-violation backtrace, directexecute()WasmTrapError.framesattachment.TestSourceMapPopulation516(3 tests) — light-weight inspection ofCompileResult.fn_source_map: top-level fns registered, lifted closures registered underanon_N, built-in helpers NOT registered (regression guard against bogus user-frame entries).Test plan
pytest tests/ -q— 3,576 passed, 14 skipped (was 3,561+14)mypy vera/— cleanpython scripts/check_conformance.py— all 81 passpython scripts/check_examples.py— all 33 passpython scripts/check_doc_counts.py— consistent (3,590 tests)python scripts/check_version_sync.py— version 0.0.124 consistentCo-Authored-By: Claude noreply@anthropic.invalid
Summary by CodeRabbit
New Features
Tests
Documentation