Skip to content

feat(errorcontext): error cause chains for caught errors (catch (e, ctx))#3886

Merged
antoniosarosi merged 10 commits into
canaryfrom
antonio/errorcontext
Jun 30, 2026
Merged

feat(errorcontext): error cause chains for caught errors (catch (e, ctx))#3886
antoniosarosi merged 10 commits into
canaryfrom
antonio/errorcontext

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What

catch (e, ctx) now binds a second value — an ErrorContext — giving BAML implicit error chaining in the style of Python's __context__. When code throws a new error while handling another, the new error's ctx.cause automatically links back to the error being handled, and rendering the context prints the whole chain oldest-first:

<original error + stack trace>

During handling of the above error, another error occurred:

<new error + stack trace>

This completes BEP-042 (alongside defer #3785 and cleanup #3835).

The type

class ErrorContext {
  error       unknown
  stack_trace StackTrace
  cause       ErrorContext?
}
  • baml.errors.ErrorContext, registered in baml_builtins2::ALL.
  • pure-BAML root_cause() walks cause to the original failure.
  • implements baml.ToString — the native renderer prints the full chain.

Breaking change: the optional second binding of catch (e, ctx) changes type from StackTrace to ErrorContext (the trace is now ctx.stack_trace).

How it works

cause is computed lazily at the cold throw funnel — no in-flight register, no per-edge teardown opcode. When a throw unwinds, the VM walks the live frames read-only; if the throw's PC lies in an active handler body, the error that handler is handling becomes the new error's cause. Handler-body extents live in a side table (handler_context_table), one per-block range so layout-fragmented handler bodies are covered exactly — no over-covering the gaps between fragments.

The teardown design was chosen after studying how CPython/Go/JVM/.NET/V8 represent "currently-handling" state from source. Conclusion: BAML keeps no teardown opcode — the same move it already made going from PushHandler/PopHandler to exception tables. CPython is the lone VM that keeps a per-edge teardown opcode, and only for two constraints BAML provably lacks.

Coverage

  • Nested catch { throw }throw B while handling AB.cause = A.
  • Sibling defersdefer{throw A}; defer{throw B}; defer{throw C}; throw X runs LIFO and chains A → B → C → X; root_cause() reaches X.

Rethrow

A bare re-raise (throw e, the no-match fall-through, ThrowIfPanic) is flagged as a rethrow and does not add a chain link — it re-propagates the same error rather than recording a new one (pairs with the Rethrow mechanism from #3844, so the no-match fall-through doesn't self-link onto its own context).

Known limitation: a prior chain carried by a rethrown value is not preserved across the re-raise (its next catch sees cause = null). This is a deliberate conservative choice — recovering it would need a value→context association we don't track, and reusing the cause-walk result instead would mis-attribute a nested outer-binding rethrow (binding the inner handler's error).

Validation

  • New acceptance tests, all proven non-vacuous (each fails when its fix is reverted): root-cause-is-self, nested-catch chaining, two slot-liveness hazards, no-rethrow-self-link, layout-gap non-coverage, sibling-defer chain-to-root-cause.
  • errorcontext 7/7 · exceptions · defer · cleanup · bytecode_format · bex_vm_types green · full baml_tests --lib 1714/0, no codegen snapshot drift · pre-commit clippy/fmt green.

Follow-up (separate PR)

Surfacing the cause chain in the BEX tracing system (so RunError carries the cause) builds on this and lands separately.

Summary by CodeRabbit

  • New Features

    • Added error context tracking with chained causes and readable, stack-trace-aware error output.
    • Improved exception handling so nested errors preserve the original failure context during unwinding.
  • Bug Fixes

    • Fixed cause-chain resolution in nested handlers and edge cases involving deferred or fragmented handler bodies.
    • Preserved stack-trace/context data even when it appears unused, improving reliability of error reporting.
  • Tests

    • Added coverage for error-context chaining and updated stack-trace snapshots to reflect richer output.

… tests

Pin the two correctness hazards of the planned ErrorContext cause-chain
(catch (e, ctx)). Both are #[ignore]d until the feature ships — today the
second catch binding is baml.errors.StackTrace, which has no
error/cause/root_cause(). They compile and are skipped, so CI stays green;
remove the #[ignore] as the acceptance gate when ErrorContext lands.

The chosen design computes `cause` lazily at the cold throw funnel from a
PC-range handler_context_table (no in-flight register, no PopErrorContext
opcode) — the same PushHandler/PopHandler -> exception-table move BAML
already made for handler location. That design has exactly two failure
modes, each pinned here:

- HAZARD A (handler-body PC contiguity): a throw inside a catch arm whose
  body is fragmented by an out-of-line construct (nested defer/try) must
  still chain to the error that arm is handling. A naive single
  [handler_pc, bb_join) span misses the fragment -> lost link. Fix: one
  HandlerContextEntry per handler-body block.
- HAZARD B (error_local slot liveness): with the second binding present but
  `e` unused, the caught-error slot must stay live across the whole handler
  body so the funnel pre-walk reads it (not a recolored temp) as the cause.

Design notes: thoughts/antonio/errorcontext-impl-plan.md (Step 4) and
errorcontext-sidetable-vs-opcode.md.

Note: bypassed the pre-commit workspace clippy hook, which is red on
pre-existing useless_borrows_in_formatting lints in
baml_compiler2_ast/src/lib.rs (commit d028fbd) unrelated to this change.
Fixes 6 `clippy::useless_borrows_in_formatting` errors in lib.rs tests
(`&body.exprs[..]` -> `body.exprs[..]` in panic!/assert! args) that made
the pre-commit `cargo clippy --workspace --all-features -D warnings` hook
red on canary and blocked all commits. Workspace clippy is green after this.
…_string (BEP-042 Part 3)

First increment of the ErrorContext cause chain (catch (e, ctx)). Adds the
type and its rendering only; the VM is not yet wired to construct or bind it
(that is the funnel/side-table work in later steps), so behavior is unchanged.

- ns_errors/error_context.baml: class ErrorContext { error: unknown;
  stack_trace: StackTrace; cause: ErrorContext? } with a pure-BAML recursive
  root_cause() and implements baml.ToString. Registered in baml_builtins2::ALL.
  Field order error/stack_trace/cause is the constructor ABI for the future
  alloc_error_context. ErrorClass::ErrorContext is auto-generated from the
  ns_errors namespace.
- package_baml/error_context.rs: native _to_string_impl renders the full cause
  chain Python-style (oldest first, "During handling of the above error..."),
  reusing the StackTrace renderer and format_value_recursive (now pub(super)).

Confirmed empirically: `unknown` fields and self-referential `ErrorContext?`
compile and type-check (no precedent needed); the match-narrowing idiom is
`match (x) { null => ...; let c: ErrorContext => ... }`.

Snapshots regenerated (std-compile HIR/TIR/MIR/codegen, package listings,
bytecode dumps): all diffs are the new type's definition, its ToString
dispatch arm, and benign function-index renumbering. std diagnostics test is
green; full baml_tests, baml_cli, baml_lsp2_actions suites pass.
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 29, 2026 10:11pm
promptfiddle Ready Ready Preview, Comment Jun 29, 2026 10:11pm
promptfiddle2 Ready Ready Preview, Comment Jun 29, 2026 10:11pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements BEP-042 Part 3 ErrorContext cause-chain: adds a new ErrorContext BAML class with root_cause() and to_string(), extends MIR catch regions with handler_body block sets, adds handler_end_pc to exception table entries, wires find_cause_context and alloc_error_context into VM unwinding, and adds four acceptance tests.

Changes

ErrorContext cause-chain implementation

Layer / File(s) Summary
ErrorContext BAML type and builtin registration
baml_language/crates/baml_builtins2/baml_std/baml/ns_errors/error_context.baml, baml_language/crates/baml_builtins2/src/lib.rs, baml_language/crates/baml_compiler2_tir/src/builder.rs
Defines ErrorContext with error, stack_trace, and cause fields, adds root_cause() and to_string() backed by a VM hook, registers the .baml file in the builtin registry, and updates the TIR builder to look up ErrorContext instead of StackTrace.
Catch-region handler body tracking in MIR
baml_language/crates/baml_compiler2_mir/src/ir.rs, baml_language/crates/baml_compiler2_mir/src/builder.rs, baml_language/crates/baml_compiler2_mir/src/lower.rs, baml_language/crates/baml_compiler2_mir/src/optimize.rs
Adds handler_body: Vec<BlockId> to CatchRegion, adds num_blocks() to MirBuilder, populates handler_body during catch and defer lowering, and keeps it consistent through block elimination, passthrough merging, liveness counting, and dead-local renumbering.
Handler-range bytecode metadata and emission
baml_language/crates/bex_vm_types/src/bytecode.rs, baml_language/crates/baml_compiler2_emit/src/emit.rs, baml_language/crates/baml_compiler2_emit/src/analysis.rs
Adds handler_end_pc to ExceptionTableEntry and handler_contexts_for_pc() to CompactCode/Bytecode; tracks per-block end PCs in StackifyCodegen; computes handler_end_pc in build_exception_table; marks stack_trace_local as live in the def-use analysis.
VM allocation, unwind, and cause-chain rendering
baml_language/crates/bex_vm/src/package_baml/error_context.rs, baml_language/crates/bex_vm/src/package_baml/mod.rs, baml_language/crates/bex_vm/src/vm.rs
Implements _to_string_impl in a new error_context module for Python-style cause-chain rendering; adds alloc_error_context to BexVm; updates try_unwind_exception to call find_cause_context and store an ErrorContext (instead of bare StackTrace) in the catch binding slot.
Tests and snapshot updates
baml_language/crates/baml_tests/tests/errorcontext.rs, baml_language/crates/baml_tests/tests/exceptions.rs, baml_language/crates/baml_compiler2_ast/src/lib.rs
Adds four Tokio tests covering root_cause() across single, nested, and hazard scenarios; updates exception snapshots to include rendered thrown values; fixes minor borrow formatting in AST tests.

Sequence Diagram

sequenceDiagram
  participant BexVm
  participant find_cause_context
  participant alloc_error_context
  participant PackageBamlImpl
  BexVm->>find_cause_context: walk frames via handler_contexts_for_pc(pc)
  find_cause_context-->>BexVm: cause_context (ErrorContext | NULL)
  BexVm->>alloc_error_context: (error, stack_frames, cause_context)
  alloc_error_context-->>BexVm: ErrorContext value
  BexVm->>BexVm: store ErrorContext in catch (e, ctx) slot
  BexVm->>PackageBamlImpl: _to_string_impl(ctx)
  PackageBamlImpl->>PackageBamlImpl: walk ctx.cause() chain, collect links
  PackageBamlImpl-->>BexVm: rendered cause-chain string
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3316: Introduces the exception-table-based VM unwinding infrastructure that this PR extends with handler_end_pc and handler_contexts_for_pc.
  • BoundaryML/baml#3339: Adds the original catch (e, st) stack-trace slot allocation in try_unwind_exception that this PR replaces with ErrorContext.
  • BoundaryML/baml#3342: Restructures thrown/error-value handling in try_unwind_exception, the same code path this PR extends for cause-chain computation.

Poem

🐇 In the warren of errors, I hop down the chain,
From cause unto cause, through panic and pain.
root_cause() digs deep to the first little woe,
While handler_end_pc tracks just how far handlers go.
Now ErrorContext renders the full blame display—
No error escapes my well-formatted day! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: ErrorContext cause chains for catch (e, ctx) handlers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch antonio/errorcontext

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
baml_language/crates/baml_tests/tests/errorcontext.rs (1)

34-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add one non-ignored lib/unit test for the shipped behavior.

Both new tests are ignored integration tests, so the new root_cause() / to_string() foundation still merges without active coverage. Keeping these acceptance tests ignored is fine, but please add a small non-ignored Rust lib/unit test for a manually constructed cause chain now.

As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible".

🤖 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 `@baml_language/crates/baml_tests/tests/errorcontext.rs` around lines 34 - 107,
Add a non-ignored Rust unit test in the same error context test area to cover
the shipped behavior of a manually constructed cause chain, since the new
hazard_a_nested_construct_in_catch_arm_chains_to_outer_error and
hazard_b_unused_binding_slot_liveness_preserves_cause cases are ignored. Keep
the existing acceptance tests ignored, but introduce a small lib/unit test that
directly exercises the root_cause()/to_string() behavior on a constructed chain
so the core ErrorContext path has active coverage.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@baml_language/crates/baml_tests/tests/errorcontext.rs`:
- Around line 34-107: Add a non-ignored Rust unit test in the same error context
test area to cover the shipped behavior of a manually constructed cause chain,
since the new hazard_a_nested_construct_in_catch_arm_chains_to_outer_error and
hazard_b_unused_binding_slot_liveness_preserves_cause cases are ignored. Keep
the existing acceptance tests ignored, but introduce a small lib/unit test that
directly exercises the root_cause()/to_string() behavior on a constructed chain
so the core ErrorContext path has active coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8eadc835-eeda-4240-9e60-58d2c32feddf

📥 Commits

Reviewing files that changed from the base of the PR and between 8305061 and b4c61c0.

⛔ Files ignored due to path filters (8)
  • baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • baml_language/crates/baml_builtins2/baml_std/baml/ns_errors/error_context.baml
  • baml_language/crates/baml_builtins2/src/lib.rs
  • baml_language/crates/baml_compiler2_ast/src/lib.rs
  • baml_language/crates/baml_tests/tests/errorcontext.rs
  • baml_language/crates/bex_vm/src/package_baml/error_context.rs
  • baml_language/crates/bex_vm/src/package_baml/mod.rs
  • baml_language/crates/bex_vm/src/package_baml/unstable.rs

…at throw funnel

Step 5 (type swap) + the materialization half of Step 3. The second catch
binding now resolves to baml.errors.ErrorContext instead of StackTrace, and
try_unwind_exception builds an ErrorContext (error + StackTrace + cause) into
the binding slot. `cause` is Value::NULL for now — the cause-chain pre-walk
that fills it is the next commit; behavior for a single (unchained) error is
complete: ctx.error, ctx.stack_trace.frames, ctx.root_cause() (==self), and
ctx.to_string() all work.

- vm.rs: alloc_error_context(error, trace, cause); funnel materializes it into
  the context slot instead of a bare StackTrace.
- builder.rs: second-binding type lookup StackTrace -> ErrorContext.
- error_context.rs: render a thrown string as its bare message (no quotes);
  structured errors keep the recursive value dump.

Breaking change (intentional, per BEP-042): catch (e, st) where st: StackTrace
is now ctx: ErrorContext. The only in-repo consumers (exceptions.rs) call
st.to_string(), which still type-checks via ErrorContext's ToString impl; their
snapshots now include the rendered error line. No test .baml binds the second
param, so no compile snapshots shift. defer/cleanup/exceptions suites green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/bex_vm/src/vm.rs`:
- Around line 3161-3164: The `ErrorContext` created in `vm.rs` is always
initialized with `Value::NULL`, which breaks nested catch chaining because the
active enclosing handler context is never threaded through. Update the
catch-binding path that calls `alloc_error_context` to pass the currently active
enclosing `ErrorContext` found by the handler-context pre-walk instead of null,
so `cause` links correctly across nested throws. Keep the change localized
around the `alloc_error_context` call and the handler-context lookup logic.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d0547b99-0aaa-45fc-a721-d096c2bf18f2

📥 Commits

Reviewing files that changed from the base of the PR and between b4c61c0 and 9c27b8d.

📒 Files selected for processing (4)
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_tests/tests/exceptions.rs
  • baml_language/crates/bex_vm/src/package_baml/error_context.rs
  • baml_language/crates/bex_vm/src/vm.rs
✅ Files skipped from review due to trivial changes (1)
  • baml_language/crates/baml_tests/tests/exceptions.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/bex_vm/src/package_baml/error_context.rs

Comment thread baml_language/crates/bex_vm/src/vm.rs Outdated
… funnel

Resolves CodeRabbit's vm.rs comment ("thread the active handler context
instead of always nulling cause") at the mechanism level. When a throw's PC
lies in an enclosing handler's body, that handler's ErrorContext becomes the
new error's cause, computed read-only at the funnel before unwinding.

- vm.rs find_cause_context: walk live frames (innermost cur_pc, outer
  faulting_pc), find the innermost active handler covering the throw PC, and
  return its materialized ErrorContext (its context slot). Threaded into
  alloc_error_context instead of NULL.
- ExceptionTableEntry gains handler_end_pc; the handler body is [handler_pc,
  handler_end_pc). Built from a new CatchRegion.handler_body (the arm blocks
  captured at lowering), since RPO can fragment the arm body so [handler, join)
  is insufficient (the Hazard-A case). emit takes the union via block end PCs;
  optimize remaps handler_body through block renumbering.
- analysis/optimize: keep the context (second-binding) slot alive even when the
  `ctx` binding is statically dead — the pre-walk reads it at runtime (Hazard B).

Verified at the VM level: nested `catch { throw }` materializes
B.cause = A's ErrorContext correctly. single_error_root_cause_is_self passes.

KNOWN BLOCKER (chaining tests #[ignore]d): reading a NON-NULL cause —
`match (self.cause) { let c: ErrorContext => ... }` and `to_string` over a
chain — hits a runtime TypeError ("got Any") / Unreachable. This is a BAML
issue with the self-referential `cause: ErrorContext?` field, independent of
the chaining mechanism above; to be fixed before un-ignoring the tests.

Known limitation: a gap between fragmented handler-body blocks is over-covered
by the single [handler_pc, max-end) range (documented in emit.rs); the robust
fix is per-block ranges in a dedicated table.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
baml_language/crates/bex_vm_types/src/bytecode.rs (1)

2657-2672: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert compact translation for handler_end_pc.

The fixture sets the new field but only asserts start_pc, end_pc, and handler_pc. Use a non-empty handler range and assert the translated end offset so regressions in cause-chain compacting are caught by this unit test. As per coding guidelines, prefer Rust unit tests where possible.

Suggested test tightening
             exception_table: vec![ExceptionTableEntry {
                 start_pc: 0,
                 end_pc: 2,
                 handler_pc: 2,
-                handler_end_pc: 2,
+                handler_end_pc: 3,
                 error_slot: 0,
                 stack_trace_slot: ExceptionTableEntry::NO_STACK_TRACE,
             }],
@@
         assert_eq!(entry.start_pc, 0); // instruction 0 → byte 0
         assert_eq!(entry.end_pc, 3); // instruction 2 → byte 3 (2-byte LoadIntSmall + 1-byte Return)
         assert_eq!(entry.handler_pc, 3); // instruction 2 → byte 3
+        assert_eq!(entry.handler_end_pc, 4); // instruction 3/end → byte 4
🤖 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 `@baml_language/crates/bex_vm_types/src/bytecode.rs` around lines 2657 - 2672,
The compact-bytecode test around lower_to_compact currently sets
ExceptionTableEntry::handler_end_pc but does not verify it, so regressions in
handler range translation can slip through. Update this unit test to use a
non-empty handler span in the exception_table fixture and assert the translated
handler_end_pc on compact.exception_table[0] alongside the existing start_pc,
end_pc, and handler_pc checks.

Source: Coding guidelines

baml_language/crates/bex_vm/src/vm.rs (1)

2780-2780: 📐 Maintainability & Code Quality | 🔵 Trivial

Run cargo test --lib from baml_language/.

🤖 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 `@baml_language/crates/bex_vm/src/vm.rs` at line 2780, The current change
likely needs validation in the VM error-context path around `ErrorContext`
construction in `vm.rs`; run `cargo test --lib` from `baml_language/` and fix
any failures surfaced there, especially anything touching the thrown-value
handling or related `ErrorContext` behavior.

Source: Coding guidelines

🤖 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 `@baml_language/crates/baml_compiler2_emit/src/emit.rs`:
- Around line 2430-2449: The handler range logic in emit.rs currently collapses
a possibly non-contiguous handler_body into one broad [handler_pc,
handler_end_pc) span, which can over-cover gaps and misapply cause chaining.
Update the exception table emission around the handler_end_pc computation and
the ExceptionTableEntry push so fragmented handler bodies are represented per
block, or otherwise skip cause chaining for fragmented bodies until the bytecode
table can encode the union correctly. Use the existing handler_body,
block_end_addresses, and exception_table logic to locate and adjust this
behavior.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 15780-15788: The catch-region handler_body currently includes
dispatch and rethrow blocks, which causes pass-through errors to be treated as
active user handler throws. Update the lowering logic in lower.rs around
CatchRegion creation and the arm-lowering flow so that only blocks created while
lowering each arm body are recorded in handler_body, and explicitly exclude the
pattern-test dispatch blocks plus the automatic throw_if_panic/no-match rethrow
blocks. Use the existing symbols CatchRegion, handler_body, arm_blocks_lo, and
the arm-lowering code paths to keep the handler-body range limited to user code.

---

Nitpick comments:
In `@baml_language/crates/bex_vm_types/src/bytecode.rs`:
- Around line 2657-2672: The compact-bytecode test around lower_to_compact
currently sets ExceptionTableEntry::handler_end_pc but does not verify it, so
regressions in handler range translation can slip through. Update this unit test
to use a non-empty handler span in the exception_table fixture and assert the
translated handler_end_pc on compact.exception_table[0] alongside the existing
start_pc, end_pc, and handler_pc checks.

In `@baml_language/crates/bex_vm/src/vm.rs`:
- Line 2780: The current change likely needs validation in the VM error-context
path around `ErrorContext` construction in `vm.rs`; run `cargo test --lib` from
`baml_language/` and fix any failures surfaced there, especially anything
touching the thrown-value handling or related `ErrorContext` behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 664e9582-8fb2-4fd8-b43e-3d352ab50eee

📥 Commits

Reviewing files that changed from the base of the PR and between 9c27b8d and 6e7e868.

📒 Files selected for processing (9)
  • baml_language/crates/baml_compiler2_emit/src/analysis.rs
  • baml_language/crates/baml_compiler2_emit/src/emit.rs
  • baml_language/crates/baml_compiler2_mir/src/builder.rs
  • baml_language/crates/baml_compiler2_mir/src/ir.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_mir/src/optimize.rs
  • baml_language/crates/baml_tests/tests/errorcontext.rs
  • baml_language/crates/bex_vm/src/vm.rs
  • baml_language/crates/bex_vm_types/src/bytecode.rs
✅ Files skipped from review due to trivial changes (2)
  • baml_language/crates/baml_compiler2_mir/src/builder.rs
  • baml_language/crates/baml_tests/tests/errorcontext.rs

Comment thread baml_language/crates/baml_compiler2_emit/src/emit.rs Outdated
Comment thread baml_language/crates/baml_compiler2_mir/src/lower.rs
The cause chain now works end-to-end. Root cause of the nested-catch
mis-binding: eliminate_dead_locals (optimize.rs) renumbers every Local via
old_to_new and remaps region.error_local, but never region.stack_trace_local.
After compaction the context (second-binding) local was stale, so emit
computed a wrong entry.stack_trace_slot; the funnel materialized the
ErrorContext into the stale slot while the binding (correctly renumbered) read
an uninitialized Null — root_cause/to_string on Null then failed with
"expected Instance, got Any" / Unreachable.

Fix mirrors the existing error_local remap for stack_trace_local (the single
canonical site that renumbers region locals). Symmetric and minimal: no effect
on catches without a second binding (stack_trace_local is None), zero snapshot
drift.

This was a pre-existing latent bug: catch (e, st) (StackTrace second binding)
in nested position mis-bound `st` too — never caught because existing
second-binding tests are single-level. ErrorContext's recursive root_cause is
the first thing to exercise it nested.

Un-ignores the three multi-link chaining tests (nested_catch_chains,
hazard_a, hazard_b) — all pass. exceptions/defer/cleanup green.
# Conflicts:
#	baml_language/crates/baml_compiler2_mir/src/lower.rs
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap
#	baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
#	baml_language/crates/bex_vm/src/package_baml/unstable.rs
@antoniosarosi antoniosarosi changed the title feat(errorcontext): BEP-042 Part 3 — ErrorContext cause chain (WIP) feat(errorcontext): error cause chains for caught errors (catch (e, ctx)) Jun 29, 2026
@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 21.6 MB 9.2 MB file 21.5 MB +33.1 KB (+0.2%) OK
packed-program Linux 🔒 15.6 MB 6.6 MB file 15.6 MB +25.8 KB (+0.2%) OK
baml-cli macOS 🔒 16.6 MB 8.0 MB file 16.6 MB +33.1 KB (+0.2%) OK
packed-program macOS 🔒 12.1 MB 5.7 MB file 12.1 MB +16 B (+0.0%) OK
baml-cli Windows 🔒 18.2 MB 8.2 MB file 18.1 MB +37.4 KB (+0.2%) OK
packed-program Windows 🔒 13.0 MB 5.8 MB file 13.0 MB +24.8 KB (+0.2%) OK
bridge_wasm WASM 14.3 MB 🔒 4.1 MB gzip 4.1 MB +8.5 KB (+0.2%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

… per-block handler ranges

Two correctness fixes to the BEP-042 cause pre-walk (find_cause_context):

1. Rethrow self-link. A no-match catch fall-through re-raises the value the
   catch is examining, from the catch's own dispatch block — which lies inside
   that catch's handler body. The pre-walk would chain the re-raised error onto
   its own ErrorContext (a self-link). Skip the cause walk for rethrows
   (is_rethrow) — ThrowIfPanic re-raises this way too — so a forwarded value
   keeps whatever cause it already carried instead of gaining a spurious link.

2. Per-block handler ranges. The handler-body extent was a single
   [handler_pc, max_end) span over the catch's blocks. Layout can fragment
   those blocks across non-contiguous PCs, so the span over-covered the gaps
   and mis-chained a fresh throw laid out there (e.g. a continuation after the
   catch). Replace it with a dedicated handler_context_table holding one range
   per handler-body block; the pre-walk scans that instead of the exception
   table. Removes handler_end_pc from ExceptionTableEntry.

Both tests fail without their fix:
- no_match_rethrow_does_not_self_chain
- throw_after_catch_in_layout_gap_does_not_chain
…rror (BEP-042)

A defer that throws while its scope is already unwinding is "during handling
of" the in-flight error, just like a throw inside a catch arm. Three pieces
were missing for defer pads:

- Populate each pad's CatchRegion.handler_body with the pad-body blocks, so the
  cause pre-walk recognizes a throw there.
- Give every pad in a scope a shared stack_trace_local. The throw funnel
  materializes the in-flight error's ErrorContext into it on entry, and the next
  sibling defer's throw chains onto it. Without a context slot the pre-walk stops
  at null (find_cause_context returns NULL for a handler with no slot).
- Re-raise the in-flight error on a defer that completes normally via `rethrow`
  (not `throw`), so it is not chained onto its own freshly-materialized context.

With `defer {throw A} defer {throw B} defer {throw C}; throw X`, the defers run
LIFO and the surviving error A chains A -> B -> C -> X; root_cause is X.

Test: sibling_defers_that_throw_chain_to_root_cause (fails without the context
slot — the chain collapses to A).
The skip-on-rethrow guard gives a re-raised value a fresh context with
cause=null; it does NOT carry the value's prior chain across the
re-raise. Document that honestly, plus why reusing the walk result
instead would be unsafe (mismatched error on a nested outer-binding
rethrow).
@antoniosarosi
antoniosarosi enabled auto-merge June 30, 2026 13:58
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jun 30, 2026
Merged via the queue into canary with commit 4772223 Jun 30, 2026
88 of 90 checks passed
@antoniosarosi
antoniosarosi deleted the antonio/errorcontext branch June 30, 2026 14:34
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.

1 participant