fix(ir): halt cleanly when a global initializer can't be serialized

The global-init constant serializers in emit_llvm.zig printed a diagnostic
on an unserializable value and then RETURNED an undef/null placeholder and
CONTINUED emitting. For a comptime `#run` global that yields a function
reference (`fp :: #run pick();` where pick returns a function), the build
fell through to the JIT and segfaulted calling through the undef pointer
(exit 134) — a silent miscompile dressed up as a printed error.

Route every genuine bail in the serialization family through a new
`failGlobalInit` helper: it sets `comptime_failed` (so core.generateCode
aborts with a non-zero exit after emit()) and returns an undef placeholder
that never ships, because the halt fires before object emission / JIT. This
covers the comptime func_ref leaf, the require_resolved aggregate func_ref
leaf, the top-level + vtable func_ref globals, the comptime-init catch, and
the remaining heap-walk / aggregate-shape bails. Unresolved-function
diagnostics now name the function instead of its (stdlib-unstable) IR index.

The require_resolved=false Pass-0 placeholder is unchanged (func_map is
empty until Pass 1; the aggregate is re-emitted with require_resolved=true).

Regression: examples/1128-diagnostics-comptime-global-funcref-rejected.sx —
a `#run` global returning a function ref now exits 1 with the diagnostic
(was: exit 134 segfault). Fail-before/pass-after verified.
This commit is contained in:
agra
2026-06-04 05:25:19 +03:00
parent 263333bd26
commit d87bad2ec4
5 changed files with 78 additions and 19 deletions

View File

@@ -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