ERR/E3.2: wire trace push/clear into raise/try/catch/or/destructure

Connect the E3.1 buffer to codegen. Push sites: `raise` (always escapes — push
before cleanup) and `try`'s propagation branch (the failure that escapes to the
caller). Clear sites: `catch` handler entry (via runCatchBody, error path only),
the `or value` terminator's failure branch, and a destructure that binds a
failable's error slot — so an absorbed failure leaves no residue.

Helpers in lower.zig: emitTracePush / emitTraceClear (call getTraceFids, no-op
when traces are off), tracesEnabled (opt_level == .none/.less — `sx run`
defaults to -O0, so on in dev; .default/.aggressive are release → off, zero
overhead), and placeholderTraceFrame (a nonzero u64 until DWARF/E3.0 supplies
real PCs and E3.3 resolves them).

Verified end-to-end via a #foreign sx_trace_len probe: catch/or/multi-slot-
destructure drive len back to 0; release (--opt default) emits no push/clear at
all (debug showed a residual where release showed 0).

examples/241-error-trace-buffer.sx is a focused regression (white-box: reads
sx_trace_len directly, pending E3.3's public trace.print_current).

KNOWN GAP (documented, deferred to the E1.8 flow-check binding-site work): a
single-binding capture of a PURE failable (`er := pure_failable()`, not a
comma destructure) goes through lowerVarDecl, not lowerDestructureDecl, so it
doesn't clear — the trace over-retains until the next absorbing site. Harmless
today (nothing reads the buffer at function exit yet) but wrong per spec.

Gates: zig build, zig build test, bash tests/run_examples.sh (278 passed; lone
failure is the user's uncommitted 213-canonical-map pack WIP).
This commit is contained in:
agra
2026-06-01 08:28:46 +03:00
parent 51f5277380
commit ea40724b61
4 changed files with 102 additions and 4 deletions

View File

@@ -0,0 +1,38 @@
// Error return-trace buffer push/clear wiring (ERR step E3.2). A `raise` and a
// propagating `try` each push a frame; an absorbing site (`catch`, `or value`,
// a destructure that binds the error) clears the buffer. In debug builds
// (`sx run` defaults to -O0) these calls are emitted; in release they're
// skipped entirely (zero overhead). Until E3.3 ships `trace.print_current`,
// this example observes the buffer directly via the runtime's `sx_trace_len`
// (linked in for the JIT) — a white-box probe, not the eventual public API.
#import "modules/std.sx";
// Internal runtime symbol (library/vendors/sx_trace_runtime/sx_trace.c).
sx_trace_len :: () -> u32 #foreign;
E :: error { Bad }
fail :: (n: s32) -> !E {
if n < 0 { raise error.Bad; } // pushes a frame
return;
}
propagate :: (n: s32) -> !E {
try fail(n); // on failure: pushes a frame, propagates
return;
}
main :: () -> s32 {
// `catch` absorbs the failure → buffer cleared before the handler runs.
propagate(-1) catch e {
// The pushes from `raise` + `try` were cleared on catch entry.
print("in catch: len={}\n", sx_trace_len()); // 0
};
print("after catch: len={}\n", sx_trace_len()); // 0
// A success leaves the buffer empty (nothing pushed).
propagate(1) catch e { };
print("after success: len={}\n", sx_trace_len()); // 0
return 0;
}