fix(ir): serialize null pointer fields in global aggregates (issue 0081)

A module-global aggregate initializer rejected a `null` literal in a
pointer (or optional-pointer) field as "must be initialized by a
compile-time constant". `Lowering.constExprValue` had no `.null_literal`
arm, so the null leaf returned no constant and the whole aggregate looked
non-constant — even though `null` is the compile-time zero pointer (a
top-level scalar `p : *s64 = null;` already serialized fine).

Add `.null_literal => .null_val` to constExprValue. While here, make the
two LLVM constant emitters exhaustive: emitConstAggregate and the
top-level init_val switch in emit_llvm.zig previously ended in a silent
`else => LLVMConstNull(...)` catch-all (the silent-arm class CLAUDE.md
mandates rooting out). They now handle every ConstantValue tag explicitly
(.null_val/.zeroinit -> all-zero constant, .undef -> LLVMGetUndef,
.func_ref resolved, nested .vtable is a hard @panic tripwire). The
reject-loud path for genuinely non-constant fields is preserved.

Regression: examples/0138 (array-of-struct null ptr fields, array of
all-null pointers, nested struct-in-struct null ptr) and the negative
examples/1126 (null ptr field beside a non-const field still errors).
Fail-before/pass-after verified.
This commit is contained in:
agra
2026-06-04 04:22:43 +03:00
parent e93879816d
commit d680b320f4
11 changed files with 215 additions and 2 deletions

View File

@@ -902,7 +902,12 @@ pub const LLVMEmitter = struct {
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |agg| self.emitConstAggregate(agg, llvm_ty),
.vtable => c.LLVMConstNull(llvm_ty), // placeholder — initialized in initVtableGlobals after function declarations
else => c.LLVMConstNull(llvm_ty),
// A top-level null-pointer global (`p : *s64 = null;`) and a
// zero-initialized global both emit as the all-zero constant
// of the global's type (issue 0081).
.null_val, .zeroinit => c.LLVMConstNull(llvm_ty),
.undef => c.LLVMGetUndef(llvm_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(llvm_ty),
};
c.LLVMSetInitializer(llvm_global, init_val);
} else {
@@ -2433,7 +2438,13 @@ pub const LLVMEmitter = struct {
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(elem_ty),
else => c.LLVMConstNull(elem_ty),
// A null pointer field and a zero-initialized field both emit as
// the all-zero constant of the leaf type (issue 0081).
.null_val, .zeroinit => c.LLVMConstNull(elem_ty),
.undef => c.LLVMGetUndef(elem_ty),
// Vtable constants are only ever produced for top-level protocol
// vtable globals (lower.zig), never as a nested aggregate leaf.
.vtable => @panic("nested vtable constant in aggregate is unsupported — vtables are top-level globals only"),
};
}
if (is_struct) {

View File

@@ -1033,6 +1033,10 @@ pub const Lowering = struct {
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.undef_literal => .zeroinit,
// A `null` in a pointer (or optional-pointer) field is a
// compile-time constant: the zero pointer. Without this arm the
// aggregate is wrongly rejected as non-constant (issue 0081).
.null_literal => .null_val,
.unary_op => |uo| switch (uo.op) {
.negate => switch (uo.operand.data) {
.int_literal => |il| .{ .int = -il.value },