ERR/E3.1: thread-local error return-trace ring buffer (runtime)
Add the trace buffer that raise/try push to and catch/or/destructure clear, following the JNI-TLS precedent exactly (a thread_local IR global doesn't work under the ORC JIT, which doesn't init TLS for AddObjectFile'd objects). - library/vendors/sx_trace_runtime/sx_trace.c: a `_Thread_local` fixed-cap ring (32 frames) of opaque u64s + C API (push / clear / len / truncated / frame_at). Overflow keeps the newest CAP frames and latches `truncated` (Zig-style); frame_at returns oldest-to-newest. The frame is opaque — the E3.3 formatter dispatches on context (PC at runtime, packed (func_id, offset) at comptime). - build.zig: link the .c into the compiler so the JIT resolves sx_trace_* via dlsym (and so the unit test links against it). - src/runtime_trace.test.zig: exercises push / overflow-survives-newest / clear / len / truncated / ordering against the linked C — grounds the buffer logic without shipping throwaway sx builtins. - lower.zig getTraceFids(): lazily declares the sx_trace_push/clear externs + sets needs_trace_runtime. Declared now; the raise/try push sites and the absorbing clear sites get wired at E3.2. - core.zig: auto-injects the .c as a #source for AOT when needs_trace_runtime, mirroring the JNI env runtime. Gates: zig build, zig build test (incl. the new buffer tests), bash tests/run_examples.sh (277 passed; no codegen change this step — lone failure is the user's uncommitted 213-canonical-map pack WIP).
This commit is contained in:
14
src/core.zig
14
src/core.zig
@@ -304,6 +304,20 @@ pub const Compilation = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Same pattern for the ERR E3.1 error return-trace runtime.
|
||||
if (lowering.needs_trace_runtime) {
|
||||
if (try self.resolveStdlibPath("vendors/sx_trace_runtime/sx_trace.c")) |abs_path| {
|
||||
var sources = std.ArrayList([]const u8).empty;
|
||||
try sources.append(self.allocator, abs_path);
|
||||
try self.lowering_extra_c_sources.append(self.allocator, .{
|
||||
.sources = try sources.toOwnedSlice(self.allocator),
|
||||
.includes = &.{},
|
||||
.defines = &.{},
|
||||
.flags = &.{},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try self.collectJniMainEmissions(&lowering);
|
||||
|
||||
return module;
|
||||
|
||||
@@ -124,6 +124,9 @@ pub const Lowering = struct {
|
||||
jni_env_tl_get_fid: ?FuncId = null, // extern `sx_jni_env_tl_get` (from library/vendors/sx_jni_runtime/sx_jni_env_tl.c)
|
||||
jni_env_tl_set_fid: ?FuncId = null, // extern `sx_jni_env_tl_set`
|
||||
needs_jni_env_tl_runtime: bool = false, // set when lowering touches the JNI env TL; signals Compilation to auto-link the runtime .c
|
||||
trace_push_fid: ?FuncId = null, // extern `sx_trace_push` (ERR E3.1, from library/vendors/sx_trace_runtime/sx_trace.c)
|
||||
trace_clear_fid: ?FuncId = null, // extern `sx_trace_clear`
|
||||
needs_trace_runtime: bool = false, // set when lowering emits a trace push/clear; signals Compilation to auto-link sx_trace.c
|
||||
foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator), // sx alias → ForeignClassDecl (jni_class / objc_class / swift_class / ... — registered in scan pass)
|
||||
current_foreign_class: ?*const ast.ForeignClassDecl = null, // set while lowering a `#jni_main` (or any sx-defined `#jni_class`) bodied method — `super.method(args)` dispatch resolves the parent class against this fcd's `#extends`
|
||||
current_foreign_method: ?ast.ForeignMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
|
||||
@@ -13029,6 +13032,33 @@ pub const Lowering = struct {
|
||||
return .{ .get = self.jni_env_tl_get_fid.?, .set = self.jni_env_tl_set_fid.? };
|
||||
}
|
||||
|
||||
/// Lazily declare the `sx_trace_push(u64)` / `sx_trace_clear()` runtime
|
||||
/// externs (ERR E3.1). Storage is a `_Thread_local` ring buffer in
|
||||
/// `library/vendors/sx_trace_runtime/sx_trace.c` — kept OUT of the user's IR
|
||||
/// module (same JIT-TLS reason as the JNI env slot). Setting
|
||||
/// `needs_trace_runtime` signals Compilation to auto-link the .c for AOT.
|
||||
/// Wired into the `raise` / `try` push sites and the absorbing clear sites
|
||||
/// at ERR E3.2.
|
||||
fn getTraceFids(self: *Lowering) struct { push: FuncId, clear: FuncId } {
|
||||
self.needs_trace_runtime = true;
|
||||
if (self.trace_push_fid == null) {
|
||||
const name = self.module.types.internString("sx_trace_push");
|
||||
const frame_param = self.module.types.internString("frame");
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = frame_param, .ty = .u64 }) catch unreachable;
|
||||
const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void);
|
||||
self.module.getFunctionMut(fid).call_conv = .c;
|
||||
self.trace_push_fid = fid;
|
||||
}
|
||||
if (self.trace_clear_fid == null) {
|
||||
const name = self.module.types.internString("sx_trace_clear");
|
||||
const fid = self.builder.declareExtern(name, &.{}, .void);
|
||||
self.module.getFunctionMut(fid).call_conv = .c;
|
||||
self.trace_clear_fid = fid;
|
||||
}
|
||||
return .{ .push = self.trace_push_fid.?, .clear = self.trace_clear_fid.? };
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -9,6 +9,7 @@ pub const target = @import("target.zig");
|
||||
pub const builtins = @import("builtins.zig");
|
||||
pub const errors = @import("errors.zig");
|
||||
pub const errors_tests = @import("errors.test.zig");
|
||||
pub const trace_runtime_tests = @import("runtime_trace.test.zig");
|
||||
pub const sema = @import("sema.zig");
|
||||
pub const imports = @import("imports.zig");
|
||||
pub const core = @import("core.zig");
|
||||
|
||||
75
src/runtime_trace.test.zig
Normal file
75
src/runtime_trace.test.zig
Normal file
@@ -0,0 +1,75 @@
|
||||
// Unit tests for the ERR E3.1 error return-trace ring buffer
|
||||
// (library/vendors/sx_trace_runtime/sx_trace.c). The .c is linked into the
|
||||
// module via build.zig, so these extern symbols resolve at test-link time.
|
||||
// This grounds the buffer logic (push / overflow-survives-newest / clear /
|
||||
// len / truncated / oldest-to-newest ordering) before E3.2 wires the
|
||||
// push/clear calls into codegen.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
extern fn sx_trace_push(frame: u64) void;
|
||||
extern fn sx_trace_clear() void;
|
||||
extern fn sx_trace_len() u32;
|
||||
extern fn sx_trace_truncated() u32;
|
||||
extern fn sx_trace_frame_at(i: u32) u64;
|
||||
|
||||
const CAP: u32 = 32;
|
||||
|
||||
test "trace buffer: push / len / frame_at oldest-to-newest" {
|
||||
sx_trace_clear();
|
||||
try std.testing.expectEqual(@as(u32, 0), sx_trace_len());
|
||||
try std.testing.expectEqual(@as(u32, 0), sx_trace_truncated());
|
||||
|
||||
sx_trace_push(10);
|
||||
sx_trace_push(20);
|
||||
sx_trace_push(30);
|
||||
try std.testing.expectEqual(@as(u32, 3), sx_trace_len());
|
||||
try std.testing.expectEqual(@as(u64, 10), sx_trace_frame_at(0));
|
||||
try std.testing.expectEqual(@as(u64, 20), sx_trace_frame_at(1));
|
||||
try std.testing.expectEqual(@as(u64, 30), sx_trace_frame_at(2));
|
||||
// Out-of-range frame → 0.
|
||||
try std.testing.expectEqual(@as(u64, 0), sx_trace_frame_at(3));
|
||||
try std.testing.expectEqual(@as(u32, 0), sx_trace_truncated());
|
||||
}
|
||||
|
||||
test "trace buffer: clear resets length and truncation" {
|
||||
sx_trace_clear();
|
||||
sx_trace_push(1);
|
||||
sx_trace_push(2);
|
||||
sx_trace_clear();
|
||||
try std.testing.expectEqual(@as(u32, 0), sx_trace_len());
|
||||
try std.testing.expectEqual(@as(u32, 0), sx_trace_truncated());
|
||||
try std.testing.expectEqual(@as(u64, 0), sx_trace_frame_at(0));
|
||||
}
|
||||
|
||||
test "trace buffer: overflow keeps the newest CAP frames, latches truncated" {
|
||||
sx_trace_clear();
|
||||
// Push CAP + 5 frames with distinguishable values (i = 1..CAP+5).
|
||||
var i: u64 = 1;
|
||||
while (i <= CAP + 5) : (i += 1) sx_trace_push(i);
|
||||
|
||||
// Length saturates at CAP; truncation latched.
|
||||
try std.testing.expectEqual(CAP, sx_trace_len());
|
||||
try std.testing.expectEqual(@as(u32, 1), sx_trace_truncated());
|
||||
|
||||
// The surviving frames are the newest CAP, oldest-to-newest. The first 5
|
||||
// (values 1..5) were overwritten, so frame_at(0) is value 6.
|
||||
try std.testing.expectEqual(@as(u64, 6), sx_trace_frame_at(0));
|
||||
try std.testing.expectEqual(@as(u64, CAP + 5), sx_trace_frame_at(CAP - 1));
|
||||
// Strictly increasing by 1 across the surviving window.
|
||||
var k: u32 = 0;
|
||||
while (k < CAP) : (k += 1) {
|
||||
try std.testing.expectEqual(@as(u64, 6 + k), sx_trace_frame_at(k));
|
||||
}
|
||||
}
|
||||
|
||||
test "trace buffer: exactly CAP frames, no truncation" {
|
||||
sx_trace_clear();
|
||||
var i: u64 = 0;
|
||||
while (i < CAP) : (i += 1) sx_trace_push(i * 100);
|
||||
try std.testing.expectEqual(CAP, sx_trace_len());
|
||||
try std.testing.expectEqual(@as(u32, 0), sx_trace_truncated());
|
||||
try std.testing.expectEqual(@as(u64, 0), sx_trace_frame_at(0));
|
||||
try std.testing.expectEqual(@as(u64, (CAP - 1) * 100), sx_trace_frame_at(CAP - 1));
|
||||
sx_trace_clear();
|
||||
}
|
||||
Reference in New Issue
Block a user