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:
@@ -8387,6 +8387,11 @@ pub const Lowering = struct {
|
||||
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 ────────────────────────────────────────────
|
||||
@@ -13059,6 +13064,43 @@ pub const Lowering = struct {
|
||||
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
|
||||
/// declarations, ALSO register them under their qualified name `Ns.Class`
|
||||
/// 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.
|
||||
if (ret_ty == err_set) {
|
||||
const tag_ty = self.builder.getRefType(tag_ref);
|
||||
@@ -15541,10 +15587,13 @@ pub const Lowering = struct {
|
||||
const ok_bb = self.freshBlock("try.ok");
|
||||
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
||||
|
||||
// Propagation: run the function's cleanups (defers + onfails, since
|
||||
// this is an error exit), then return the caller's failure carrying
|
||||
// this tag (pure caller → `ret(tag)`; value-carrying → `ret {undef…, tag}`).
|
||||
// Propagation: push a trace frame (this `try` failure escapes to the
|
||||
// caller — ERR E3.2), run the function's cleanups (defers + onfails,
|
||||
// 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.emitTracePush(self.placeholderTraceFrame());
|
||||
self.emitErrorCleanup(self.func_defer_base, 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
|
||||
/// 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 {
|
||||
// `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);
|
||||
const saved_scope = self.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.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;
|
||||
self.target_type = succ_ty;
|
||||
const rhs_val = self.lowerExpr(bop.rhs);
|
||||
|
||||
Reference in New Issue
Block a user