diff --git a/examples/1128-diagnostics-comptime-global-funcref-rejected.sx b/examples/1128-diagnostics-comptime-global-funcref-rejected.sx new file mode 100644 index 0000000..38be1aa --- /dev/null +++ b/examples/1128-diagnostics-comptime-global-funcref-rejected.sx @@ -0,0 +1,26 @@ +// A comptime `#run` global initializer that yields a function reference cannot +// be serialized to a static constant: at global-init time (Pass 0) functions +// are not yet declared, and the comptime serialization path has no later +// re-emit, so the func_ref can never resolve to a real function pointer. The +// compiler must reject this with a diagnostic AND a CLEAN non-zero exit — never +// print the error and then fall through into an undef initializer that crashes +// (pre-fix: the diagnostic printed, emission continued, and the JIT segfaulted +// calling through the undef pointer → exit 134). +// Regression (issue 0079 follow-up): every global-init serialization bail now +// routes through `failGlobalInit`, which sets the halt flag so the driver aborts +// after emit() instead of shipping the placeholder. +// Expected: "comptime init of 'fp' produced a reference to function 'add'…"; +// exit 1, no segfault. + +#import "modules/std.sx"; + +add :: (a: s32, b: s32) -> s32 { a + b } + +pick :: () -> (s32, s32) -> s32 { return add; } + +fp :: #run pick(); + +main :: () -> s32 { + print("{}\n", fp(3, 4)); + return 0; +} diff --git a/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.exit b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stderr b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stderr new file mode 100644 index 0000000..dea1cc8 --- /dev/null +++ b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stderr @@ -0,0 +1 @@ +error: comptime init of 'fp' produced a reference to function 'add', which cannot be serialized as a static constant (function declarations are not available at global-init time) diff --git a/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stdout b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index d7458a4..508ef13 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -106,8 +106,11 @@ pub const LLVMEmitter = struct { // IR Module being emitted ir_mod: *const Module, - // Set when a comptime `#run` raised an unhandled error (E5.2). The driver - // (core.generateCode) aborts with a non-zero exit after emit() when set. + // Set when a comptime `#run` raised an unhandled error (E5.2), or when a + // global initializer could not be serialized to a valid static constant. + // The driver (core.generateCode) aborts with a non-zero exit after emit() + // when set, so an invalid/placeholder initializer never reaches the object + // file or the JIT — the emit-time diagnostic is the surfaced error. comptime_failed: bool = false, // Allocator for temporary bookkeeping @@ -875,6 +878,7 @@ pub const LLVMEmitter = struct { const sep: []const u8 = if (detail.len > 0) ": " else ""; const gname = self.ir_mod.types.getString(global.name); std.debug.print("error: comptime init of '{s}' failed: {s} (op={s}{s}{s})\n", .{ gname, @errorName(err), op, sep, detail }); + self.comptime_failed = true; break :blk .void_val; }; // A bare failable `NAME :: #run f();`: the comptime function @@ -936,7 +940,14 @@ pub const LLVMEmitter = struct { defer field_vals.deinit(self.alloc); for (func_ids) |fid| { const llvm_func = self.func_map.get(fid.index()) orelse { + std.debug.print( + "error: vtable global '{s}' references function '{s}' which has no declaration\n", + .{ self.ir_mod.types.getString(global.name), self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name) }, + ); + // Keep the struct shape so module construction can + // finish; comptime_failed halts before it ships. field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable; + self.comptime_failed = true; continue; }; field_vals.append(self.alloc, llvm_func) catch unreachable; @@ -957,9 +968,10 @@ pub const LLVMEmitter = struct { .func_ref => |fid| { const llvm_func = self.func_map.get(fid.index()) orelse { std.debug.print( - "error: global '{s}' references function #{d} which has no declaration\n", - .{ self.ir_mod.types.getString(global.name), fid.index() }, + "error: global '{s}' references function '{s}' which has no declaration\n", + .{ self.ir_mod.types.getString(global.name), self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name) }, ); + self.comptime_failed = true; continue; }; c.LLVMSetInitializer(llvm_global, llvm_func); @@ -981,6 +993,17 @@ pub const LLVMEmitter = struct { return ptr[0..len]; } + /// Record that a global initializer could not be serialized to a valid + /// static constant: set the halt flag (the driver aborts with a non-zero + /// exit after `emit()`) and return an `undef` placeholder so in-process + /// LLVM module construction can finish without tripping over an invalid + /// value before the halt is observed. The placeholder is never shipped — + /// `comptime_failed` guarantees we stop before object emission / JIT. + fn failGlobalInit(self: *LLVMEmitter, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { + self.comptime_failed = true; + return c.LLVMGetUndef(llvm_ty); + } + /// Serialize an interp `Value` to an LLVM constant for use as a static /// global initializer. `ty` is the IR-level type of the destination; /// the LLVM type is derived from it. `interp` gives access to the @@ -988,8 +1011,10 @@ pub const LLVMEmitter = struct { /// is included in any diagnostic the path produces so the user can /// locate the offending `#run` site. /// - /// Returns `LLVMGetUndef` on bail — the build continues so adjacent - /// constants can still emit, but the diagnostic makes the problem clear. + /// On bail, prints the diagnostic and routes through `failGlobalInit` + /// (sets `comptime_failed`, returns `undef`): the in-process module + /// finishes constructing, but the driver halts with a non-zero exit + /// before object emission / JIT, so the placeholder never ships. fn valueToLLVMConst( self: *LLVMEmitter, val: Value, @@ -1015,7 +1040,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a raw integer for a pointer field — needs IR-typed heap-walk serialization (Phase 1.4a heap-walk follow-up)\n", .{global_name}, ); - break :blk c.LLVMGetUndef(llvm_ty); + break :blk self.failGlobalInit(llvm_ty); } break :blk c.LLVMConstInt(llvm_ty, @bitCast(v), 1); }, @@ -1029,10 +1054,10 @@ pub const LLVMEmitter = struct { // bail loudly rather than ship a silently-null function pointer. .func_ref => |fid| blk: { std.debug.print( - "error: comptime init of '{s}' produced a reference to function #{d}, which cannot be serialized as a static constant (function declarations are not available at global-init time)\n", - .{ global_name, fid.index() }, + "error: comptime init of '{s}' produced a reference to function '{s}', which cannot be serialized as a static constant (function declarations are not available at global-init time)\n", + .{ global_name, self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name) }, ); - break :blk c.LLVMGetUndef(llvm_ty); + break :blk self.failGlobalInit(llvm_ty); }, .string => |s| self.emitConstStringGlobal(s), .aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name), @@ -1046,7 +1071,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a {s} value, which cannot be serialized as a static constant\n", .{ global_name, @tagName(val) }, ); - break :blk c.LLVMGetUndef(llvm_ty); + break :blk self.failGlobalInit(llvm_ty); }, }; } @@ -1083,7 +1108,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a fat-pointer aggregate whose len field is not an integer\n", .{global_name}, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); }; const len: usize = @intCast(len_i); @@ -1107,7 +1132,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a fat-pointer aggregate whose data field ({s}) cannot be resolved to {} bytes — needs Phase 1.4a heap-walk for this shape\n", .{ global_name, @tagName(data), len }, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); }; return self.emitConstStringGlobal(bytes); @@ -1123,7 +1148,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced aggregate with {} fields but struct '{s}' expects {}\n", .{ global_name, fields.len, self.ir_mod.types.getString(info.@"struct".name), ir_fields.len }, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); } var field_vals = std.ArrayList(c.LLVMValueRef).empty; defer field_vals.deinit(self.alloc); @@ -1148,7 +1173,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced an aggregate but the destination type ({s}) is neither struct, array, string, nor slice\n", .{ global_name, self.ir_mod.types.typeName(ty) }, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); } // ── Function declaration ──────────────────────────────────────── @@ -2468,10 +2493,15 @@ pub const LLVMEmitter = struct { .string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)), .aggregate => |inner| self.emitConstAggregate(inner, elem_ty, require_resolved), .func_ref => |fid| self.func_map.get(fid.index()) orelse blk: { - if (require_resolved) std.debug.print( - "error: static initializer references function #{d} which has no declaration\n", - .{fid.index()}, - ); + if (require_resolved) { + std.debug.print( + "error: static initializer references function '{s}' which has no declaration\n", + .{self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name)}, + ); + break :blk self.failGlobalInit(elem_ty); + } + // Pass 0 placeholder: func_map is empty until Pass 1, so the + // whole aggregate is re-emitted with require_resolved=true. break :blk c.LLVMConstNull(elem_ty); }, // A null pointer field and a zero-initialized field both emit as