fix(ir): serialize enum-literal global initializers (issue 0082)

A module-global initialized with an enum literal silently zero-initialized
to the first tag (`chosen : Color = .green` read back as `.red`), and an
enum tag inside a global array/struct was rejected as non-constant. The
constant serializer had no enum-literal arm.

Add `Lowering.constEnumLiteral`: serialize an enum literal to a
`ConstantValue.int` holding the variant's tag value, resolved against the
destination enum type and respecting explicit variant values; the global's
type drives the backing width at emit time. Wired into `globalInitValue`
(scalar global) and `constExprValue` (array element / struct field / nested
aggregate). A non-enum destination or unknown variant is diagnosed loudly,
never silently zero-initialized. The compiler-injected OS/ARCH globals now
serialize to their real `.unknown` tag (6 / 4); runtime reads are unchanged
(they resolve through comptime_constants), so only the static initializer in
the pinned .ir snapshots changes.

Remove the silent `func_ref => orelse LLVMConstNull` fallbacks in the LLVM
constant emitters: aggregate func_ref leaves carry a `require_resolved` flag
(transient null in Pass 0, loud diagnostic if still unresolved in the
Pass-1.5 re-emit), a top-level func_ref global is resolved in
initVtableGlobals, and the comptime (#run) path bails loudly instead of
emitting a null function pointer.

Regression: examples/0139-types-global-enum-literal-init.sx (scalar, array,
struct field, explicit-value enum u16 stride, struct-array with enum field);
negative: examples/1127-diagnostics-global-enum-literal-bad-variant.sx.
Mark issue 0082 RESOLVED.
This commit is contained in:
agra
2026-06-04 04:52:42 +03:00
parent d680b320f4
commit 263333bd26
21 changed files with 294 additions and 33 deletions

View File

@@ -900,14 +900,16 @@ pub const LLVMEmitter = struct {
.float => |v| c.LLVMConstReal(llvm_ty, v),
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |agg| self.emitConstAggregate(agg, llvm_ty),
.aggregate => |agg| self.emitConstAggregate(agg, llvm_ty, false),
.vtable => c.LLVMConstNull(llvm_ty), // placeholder — initialized in initVtableGlobals after function declarations
// 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),
// func_map is empty in Pass 0 (functions are declared in
// Pass 1). Emit a placeholder and resolve in initVtableGlobals.
.func_ref => c.LLVMConstNull(llvm_ty),
};
c.LLVMSetInitializer(llvm_global, init_val);
} else {
@@ -946,10 +948,22 @@ pub const LLVMEmitter = struct {
.aggregate => |agg| {
// Re-emit. The first pass in `emitGlobals` already ran,
// but func_ref leaves resolved to null then (func_map
// wasn't populated yet). Now they resolve properly.
const init_val = self.emitConstAggregate(agg, llvm_ty);
// wasn't populated yet). Now they must resolve — a still-
// unresolved func_ref here is a loud diagnostic, never a
// silent null.
const init_val = self.emitConstAggregate(agg, llvm_ty, true);
c.LLVMSetInitializer(llvm_global, init_val);
},
.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() },
);
continue;
};
c.LLVMSetInitializer(llvm_global, llvm_func);
},
else => continue,
}
}
@@ -1009,7 +1023,17 @@ pub const LLVMEmitter = struct {
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
.null_val => c.LLVMConstNull(llvm_ty),
.void_val, .undef => c.LLVMGetUndef(llvm_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(llvm_ty),
// Comptime globals are serialized here in Pass 0, before functions
// are declared (Pass 1) and with no later re-emit. A func_ref can
// therefore never resolve to a real function pointer at this point;
// 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() },
);
break :blk c.LLVMGetUndef(llvm_ty);
},
.string => |s| self.emitConstStringGlobal(s),
.aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name),
// The remaining Value variants cannot become static binary
@@ -2420,7 +2444,13 @@ pub const LLVMEmitter = struct {
return c.LLVMConstStructInContext(self.context, &fields, 2, 0);
}
fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef {
/// Serialize a constant aggregate to an LLVM constant. `require_resolved`
/// governs the func_ref leaves: in Pass 0 (`emitGlobals`) func_map is empty,
/// so func_refs are left as a transient null placeholder (`false`) and the
/// whole aggregate is re-emitted by `initVtableGlobals` after Pass 1 with
/// `true`, where any still-unresolved func_ref is a loud diagnostic — never
/// a silently-null function pointer.
fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef, require_resolved: bool) c.LLVMValueRef {
const kind = c.LLVMGetTypeKind(llvm_ty);
const is_struct = kind == c.LLVMStructTypeKind;
const n: c_uint = @intCast(agg.len);
@@ -2436,8 +2466,14 @@ pub const LLVMEmitter = struct {
.float => |v| c.LLVMConstReal(elem_ty, v),
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
.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),
.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()},
);
break :blk 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),

View File

@@ -940,11 +940,12 @@ pub const Lowering = struct {
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name });
break :blk null;
},
// Enum-literal shorthand globals (`OS : OperatingSystem = .unknown;`)
// keep their established zero-init: it is load-bearing for
// compile-time `inline if OS == .X` in the stdlib (issue 0071 scope
// note). Carved out explicitly — not folded into a silent fallthrough.
.enum_literal => null,
// An enum-literal global (`chosen : Color = .green;`) serializes to
// the variant's tag value against the destination enum type (issue
// 0082). The compiler-injected `OS`/`ARCH` globals flow through here
// too; their runtime reads resolve via `comptime_constants`, so the
// serialized tag only affects the static initializer.
.enum_literal => |el| self.constEnumLiteral(&el, var_ty, v.span),
// Any other initializer shape (`.field_access` on a const, a call, an
// arithmetic expression, …) is not a static constant the compiler can
// evaluate here. Diagnose loudly rather than emit a null payload that
@@ -1047,10 +1048,44 @@ pub const Lowering = struct {
},
.array_literal => |al| self.constArrayLiteral(al.elements, expected_ty),
.struct_literal => |sl| self.constStructLiteral(&sl, expected_ty),
// An enum tag as an aggregate leaf (`[2]Color = .[.green, .blue]`, or
// an enum field inside a global struct) serializes to its tag int
// against the leaf's declared enum type (issue 0082).
.enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span),
else => null,
};
}
/// Serialize an enum-literal initializer (`.Variant`) into a static
/// `ConstantValue.int` holding the variant's tag value, resolved against the
/// destination enum type `ty`. The tag respects explicit variant values
/// (`enum { a; b :: 5; }`); the enum's backing width is applied by the
/// const emitters via the destination type's LLVM type. Plain enums only —
/// a tagged-union or non-enum destination is diagnosed loudly rather than
/// silently zero-initialized (issue 0082).
fn constEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral, ty: TypeId, span: ast.Span) ?inst_mod.ConstantValue {
if (!ty.isBuiltin()) {
const info = self.module.types.get(ty);
if (info == .@"enum") {
const e = info.@"enum";
const name_id = self.module.types.internString(el.name);
for (e.variants, 0..) |variant, i| {
if (variant != name_id) continue;
if (e.explicit_values) |vals| {
if (i < vals.len) return .{ .int = vals[i] };
}
return .{ .int = @intCast(i) };
}
if (self.diagnostics) |d|
d.addFmt(.err, span, "'.{s}' is not a variant of enum '{s}'", .{ el.name, self.module.types.getString(e.name) });
return null;
}
}
if (self.diagnostics) |d|
d.addFmt(.err, span, "enum-literal global initializer '.{s}' is only supported for a plain enum destination type", .{el.name});
return null;
}
/// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the
/// struct's fields in declaration order, filling missing fields from the struct's
/// field defaults. Returns null if any value is not constant-foldable.