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:
38
examples/241-error-trace-buffer.sx
Normal file
38
examples/241-error-trace-buffer.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -8387,6 +8387,11 @@ pub const Lowering = struct {
|
|||||||
scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true });
|
scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destructuring a failable's result binds the error slot to a variable:
|
||||||
|
// the user now owns the error explicitly, so the trace is absorbed
|
||||||
|
// (ERR E3.2). A plain (non-failable) tuple destructure clears nothing.
|
||||||
|
if (self.errorChannelOf(ty) != null) self.emitTraceClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Comptime lowering ────────────────────────────────────────────
|
// ── Comptime lowering ────────────────────────────────────────────
|
||||||
@@ -13059,6 +13064,43 @@ pub const Lowering = struct {
|
|||||||
return .{ .push = self.trace_push_fid.?, .clear = self.trace_clear_fid.? };
|
return .{ .push = self.trace_push_fid.?, .clear = self.trace_clear_fid.? };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error return-traces are emitted in debug-ish builds and skipped in
|
||||||
|
/// release (ERR E3.2 build-mode gating). `sx run` defaults to `-O0`
|
||||||
|
/// (`.none`), the common dev path; `.default`/`.aggressive` are release.
|
||||||
|
/// The spec's `--release-traces` opt-in + a `BuildOptions.error_traces`
|
||||||
|
/// accessor are a later refinement; for now the opt level is the gate.
|
||||||
|
fn tracesEnabled(self: *Lowering) bool {
|
||||||
|
const tc = self.target_config orelse return true; // no target → treat as debug
|
||||||
|
return tc.opt_level == .none or tc.opt_level == .less;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a trace-buffer push of `frame` (an opaque u64) at a failure site.
|
||||||
|
/// No-op when traces are disabled (release). `frame` is a placeholder until
|
||||||
|
/// DWARF (E3.0) supplies real return-address PCs and E3.3 resolves them.
|
||||||
|
fn emitTracePush(self: *Lowering, frame: Ref) void {
|
||||||
|
if (!self.tracesEnabled()) return;
|
||||||
|
const fids = self.getTraceFids();
|
||||||
|
const coerced = self.coerceToType(frame, self.builder.getRefType(frame), .u64);
|
||||||
|
const args = self.alloc.dupe(Ref, &.{coerced}) catch return;
|
||||||
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.push, .args = args } }, .void);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a trace-buffer clear at an absorbing site (`catch` / `or value` /
|
||||||
|
/// destructure). No-op when traces are disabled.
|
||||||
|
fn emitTraceClear(self: *Lowering) void {
|
||||||
|
if (!self.tracesEnabled()) return;
|
||||||
|
const fids = self.getTraceFids();
|
||||||
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.clear, .args = &.{} } }, .void);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A placeholder trace frame for a failure site (ERR E3.2). Until DWARF
|
||||||
|
/// (E3.0) provides return-address PCs, push a nonzero constant so the buffer
|
||||||
|
/// records that a frame occurred (the formatter notes "frame unavailable").
|
||||||
|
/// Nonzero because frame value 0 is the buffer's out-of-range / empty marker.
|
||||||
|
fn placeholderTraceFrame(self: *Lowering) Ref {
|
||||||
|
return self.builder.constInt(1, .u64);
|
||||||
|
}
|
||||||
|
|
||||||
/// When a namespaced import (`Ns :: #import "..."`) contains foreign-class
|
/// When a namespaced import (`Ns :: #import "..."`) contains foreign-class
|
||||||
/// declarations, ALSO register them under their qualified name `Ns.Class`
|
/// declarations, ALSO register them under their qualified name `Ns.Class`
|
||||||
/// so receiver types like `*Ns.Class` can find the fcd. The recursive
|
/// so receiver types like `*Ns.Class` can find the fcd. The recursive
|
||||||
@@ -15329,7 +15371,11 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3) Emit the failure return. Pure-failable: the return type IS the
|
// (3) Push a trace frame: `raise` always escapes the function (ERR E3.2).
|
||||||
|
// Before cleanup, so the frame records the raise site itself.
|
||||||
|
self.emitTracePush(self.placeholderTraceFrame());
|
||||||
|
|
||||||
|
// (4) Emit the failure return. Pure-failable: the return type IS the
|
||||||
// error set, so return the tag value directly.
|
// error set, so return the tag value directly.
|
||||||
if (ret_ty == err_set) {
|
if (ret_ty == err_set) {
|
||||||
const tag_ty = self.builder.getRefType(tag_ref);
|
const tag_ty = self.builder.getRefType(tag_ref);
|
||||||
@@ -15541,10 +15587,13 @@ pub const Lowering = struct {
|
|||||||
const ok_bb = self.freshBlock("try.ok");
|
const ok_bb = self.freshBlock("try.ok");
|
||||||
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
||||||
|
|
||||||
// Propagation: run the function's cleanups (defers + onfails, since
|
// Propagation: push a trace frame (this `try` failure escapes to the
|
||||||
// this is an error exit), then return the caller's failure carrying
|
// caller — ERR E3.2), run the function's cleanups (defers + onfails,
|
||||||
// this tag (pure caller → `ret(tag)`; value-carrying → `ret {undef…, tag}`).
|
// since this is an error exit), then return the caller's failure
|
||||||
|
// carrying this tag (pure caller → `ret(tag)`; value-carrying →
|
||||||
|
// `ret {undef…, tag}`).
|
||||||
self.builder.switchToBlock(prop_bb);
|
self.builder.switchToBlock(prop_bb);
|
||||||
|
self.emitTracePush(self.placeholderTraceFrame());
|
||||||
self.emitErrorCleanup(self.func_defer_base, err_val);
|
self.emitErrorCleanup(self.func_defer_base, err_val);
|
||||||
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
||||||
|
|
||||||
@@ -15667,6 +15716,10 @@ pub const Lowering = struct {
|
|||||||
/// catch), returns the body's value (or null if the body diverged); when
|
/// catch), returns the body's value (or null if the body diverged); when
|
||||||
/// null (pure-failable catch), runs the body for effect and returns null.
|
/// null (pure-failable catch), runs the body for effect and returns null.
|
||||||
fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref {
|
fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref {
|
||||||
|
// `catch` absorbs the LHS's failure: clear the trace buffer before the
|
||||||
|
// handler runs (ERR E3.2), so a failure consumed here leaves no residue.
|
||||||
|
// Runs on the error/handle path only (this fn is called from handle_bb).
|
||||||
|
self.emitTraceClear();
|
||||||
var handle_scope = Scope.init(self.alloc, self.scope);
|
var handle_scope = Scope.init(self.alloc, self.scope);
|
||||||
const saved_scope = self.scope;
|
const saved_scope = self.scope;
|
||||||
self.scope = &handle_scope;
|
self.scope = &handle_scope;
|
||||||
@@ -15731,6 +15784,9 @@ pub const Lowering = struct {
|
|||||||
self.builder.condBr(is_err, fail_bb, &.{}, merge_bb, &.{succ_val});
|
self.builder.condBr(is_err, fail_bb, &.{}, merge_bb, &.{succ_val});
|
||||||
|
|
||||||
self.builder.switchToBlock(fail_bb);
|
self.builder.switchToBlock(fail_bb);
|
||||||
|
// The `or value` terminator absorbs the LHS's failure: clear the trace
|
||||||
|
// buffer before producing the fallback (ERR E3.2).
|
||||||
|
self.emitTraceClear();
|
||||||
const saved_target = self.target_type;
|
const saved_target = self.target_type;
|
||||||
self.target_type = succ_ty;
|
self.target_type = succ_ty;
|
||||||
const rhs_val = self.lowerExpr(bop.rhs);
|
const rhs_val = self.lowerExpr(bop.rhs);
|
||||||
|
|||||||
1
tests/expected/241-error-trace-buffer.exit
Normal file
1
tests/expected/241-error-trace-buffer.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
3
tests/expected/241-error-trace-buffer.txt
Normal file
3
tests/expected/241-error-trace-buffer.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
in catch: len=0
|
||||||
|
after catch: len=0
|
||||||
|
after success: len=0
|
||||||
Reference in New Issue
Block a user