diff --git a/examples/241-error-trace-buffer.sx b/examples/241-error-trace-buffer.sx new file mode 100644 index 0000000..97f9554 --- /dev/null +++ b/examples/241-error-trace-buffer.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b42bb1e..f0782ff 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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); diff --git a/tests/expected/241-error-trace-buffer.exit b/tests/expected/241-error-trace-buffer.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/241-error-trace-buffer.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/241-error-trace-buffer.txt b/tests/expected/241-error-trace-buffer.txt new file mode 100644 index 0000000..7436bf8 --- /dev/null +++ b/tests/expected/241-error-trace-buffer.txt @@ -0,0 +1,3 @@ +in catch: len=0 +after catch: len=0 +after success: len=0