mem: Phase 1.4 — serialize every interp Value variant for #run globals
`valueToLLVMConst` in emit_llvm previously handled int / float / boolean
and collapsed everything else into `LLVMConstNull(ty)`. A `#run` returning
a struct, string, function pointer, or anything aggregate produced a
zero-initialized global silently — the comptime result was computed by
the interp, then thrown away when emit_llvm couldn't represent it.
Replaced with a real walk:
- int / float / boolean — as before.
- null_val — `LLVMConstNull`.
- void_val / undef — `LLVMGetUndef`.
- func_ref — `func_map` lookup (already populated for the implicit-Context
static initializer of `__sx_default_context`).
- string — `emitConstStringGlobal`, returns a pointer to the byte array.
- aggregate — recurse field-by-field. Struct: walk
`LLVMStructGetTypeAtIndex` and emit `LLVMConstNamedStruct`. Array:
walk `LLVMGetElementType` and emit `LLVMConstArray2`.
The remaining variants (heap_ptr, byte_ptr, slot_ptr, closure, type_tag)
bail loudly with a `std.debug.print` carrying the global name — per
CLAUDE.md REJECTED PATTERNS, no more silent unimplemented arms. heap_ptr
serialization requires threading the IR `TypeId` so the heap content can
be walked recursively; deferred to Phase 1.4a alongside cycle detection.
The call site at emit_llvm.zig:676 now passes `global.name` so the
diagnostic locates the offending `#run` binding.
Type-inference fix at the binding site: `NAME :: #run expr;` with no
annotation used to default to `s64` via `resolveType(null) -> .s64`,
so even a successful Phase 1.4 serialization would emit `{0, 0}` —
the global's destination type was wrong. `lowerComptimeGlobal` now
calls `inferExprType(expr)` when no annotation is given, so the
inferred type matches the comptime function's return type. The
broader `resolveType(null)` fallback is left in place for other
callers — flagged in the MEM checkpoint as a follow-up audit.
Regression: `examples/134-comptime-aggregate-global.sx` exercises
`POINT :: #run make_point()` returning a `Point { x: s32, y: s32 }`.
Both interp (`sx run`) and codegen (`sx build`) now print
`POINT.x = 7 / POINT.y = 13` instead of `0 / 0`. 156/156 example
tests pass; chess unchanged.
This commit is contained in:
@@ -673,7 +673,7 @@ pub const LLVMEmitter = struct {
|
||||
std.debug.print("error: comptime init of '{s}' failed: {s} (op={s}{s}{s})\n", .{ gname, @errorName(err), op, sep, detail });
|
||||
break :blk .void_val;
|
||||
};
|
||||
const init_val = self.valueToLLVMConst(result, llvm_ty);
|
||||
const init_val = self.valueToLLVMConst(result, llvm_ty, self.ir_mod.types.getString(global.name));
|
||||
c.LLVMSetInitializer(llvm_global, init_val);
|
||||
} else if (global.init_val) |iv| {
|
||||
const init_val = switch (iv) {
|
||||
@@ -731,13 +731,74 @@ pub const LLVMEmitter = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn valueToLLVMConst(self: *LLVMEmitter, val: Value, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef {
|
||||
_ = self;
|
||||
/// Serialize an interp `Value` to an LLVM constant for use as a static
|
||||
/// global initializer. `global_name` 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 surfaced diagnostic surfaces the
|
||||
/// problem clearly.
|
||||
fn valueToLLVMConst(
|
||||
self: *LLVMEmitter,
|
||||
val: Value,
|
||||
llvm_ty: c.LLVMTypeRef,
|
||||
global_name: []const u8,
|
||||
) c.LLVMValueRef {
|
||||
return switch (val) {
|
||||
.int => |v| c.LLVMConstInt(llvm_ty, @bitCast(v), 1),
|
||||
.float => |v| c.LLVMConstReal(llvm_ty, v),
|
||||
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
|
||||
else => c.LLVMConstNull(llvm_ty),
|
||||
.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),
|
||||
.string => |s| self.emitConstStringGlobal(s),
|
||||
.aggregate => |fields| blk: {
|
||||
const kind = c.LLVMGetTypeKind(llvm_ty);
|
||||
if (kind == c.LLVMStructTypeKind) {
|
||||
const field_count = c.LLVMCountStructElementTypes(llvm_ty);
|
||||
if (field_count != @as(c_uint, @intCast(fields.len))) {
|
||||
std.debug.print(
|
||||
"error: comptime init of '{s}' produced aggregate with {} fields but the destination type expects {}\n",
|
||||
.{ global_name, fields.len, field_count },
|
||||
);
|
||||
break :blk c.LLVMGetUndef(llvm_ty);
|
||||
}
|
||||
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
|
||||
defer field_vals.deinit(self.alloc);
|
||||
for (fields, 0..) |f, i| {
|
||||
const field_ty = c.LLVMStructGetTypeAtIndex(llvm_ty, @intCast(i));
|
||||
field_vals.append(self.alloc, self.valueToLLVMConst(f, field_ty, global_name)) catch unreachable;
|
||||
}
|
||||
break :blk c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
|
||||
}
|
||||
if (kind == c.LLVMArrayTypeKind) {
|
||||
const elem_ty = c.LLVMGetElementType(llvm_ty);
|
||||
var elem_vals = std.ArrayList(c.LLVMValueRef).empty;
|
||||
defer elem_vals.deinit(self.alloc);
|
||||
for (fields) |f| {
|
||||
elem_vals.append(self.alloc, self.valueToLLVMConst(f, elem_ty, global_name)) catch unreachable;
|
||||
}
|
||||
break :blk c.LLVMConstArray2(elem_ty, elem_vals.items.ptr, @intCast(elem_vals.items.len));
|
||||
}
|
||||
std.debug.print(
|
||||
"error: comptime init of '{s}' produced an aggregate but the destination LLVM type is neither struct nor array (kind={})\n",
|
||||
.{ global_name, kind },
|
||||
);
|
||||
break :blk c.LLVMGetUndef(llvm_ty);
|
||||
},
|
||||
// The remaining Value variants cannot become static binary
|
||||
// constants. Bail loudly with the global name so the user can
|
||||
// identify the offending #run site.
|
||||
// - heap_ptr / byte_ptr: pointer into interp/host memory; can't survive into a binary const without type-threaded serialization (Phase 1.4a follow-up).
|
||||
// - slot_ptr: frame-local; meaningless outside the call that produced it.
|
||||
// - closure: env is dynamic.
|
||||
// - type_tag: compile-time-only Type value.
|
||||
.heap_ptr, .byte_ptr, .slot_ptr, .closure, .type_tag => blk: {
|
||||
std.debug.print(
|
||||
"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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6382,7 +6382,14 @@ pub const Lowering = struct {
|
||||
/// Creates a comptime function wrapping the expression (for later
|
||||
/// interpretation), plus a global constant to hold the result.
|
||||
fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void {
|
||||
const ret_ty = self.resolveType(type_ann);
|
||||
// When the user writes `NAME :: #run expr;` with no type annotation,
|
||||
// infer the global's type from the comptime expression's return
|
||||
// shape. `resolveType(null)` returns `.s64` for legacy reasons —
|
||||
// good for primitive helpers, silently wrong for anything else.
|
||||
const ret_ty: TypeId = if (type_ann) |n|
|
||||
self.resolveTypeWithBindings(n)
|
||||
else
|
||||
self.inferExprType(expr);
|
||||
const func_id = self.createComptimeFunction(name, expr, ret_ty);
|
||||
|
||||
// Add a global constant whose initializer will be filled by the interpreter.
|
||||
|
||||
Reference in New Issue
Block a user