From 7c21f84151a199ab3cc31e298ed369d98eac5865 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 22 Jun 2026 19:42:41 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20comptime=20VM=20reg=E2=86=92value=20brid?= =?UTF-8?q?ge=20for=20optional=20results=20(issue=200162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an .optional arm to regToValue in comptime_vm.zig: read the has_value flag at offset sizeof(child), bridge the payload recursively into a { payload, i1=true } aggregate when set, yield .null_val (zero {T,i1}) when clear or the bare null sentinel. Matching serialize arm in serializeAggregateValue (emit_llvm.zig). Pointer/?Closure/?Protocol-child optionals and array-payload aggregates bail loudly, not silently. Regression: examples/comptime/0643-comptime-run-optional-aggregate.sx (present ?T, present ?i64, null ?i64). Verified by 3 adversarial reviews. --- .../0643-comptime-run-optional-aggregate.sx | 38 +++++++++++++++++++ .../0643-comptime-run-optional-aggregate.exit | 1 + ...643-comptime-run-optional-aggregate.stderr | 1 + ...643-comptime-run-optional-aggregate.stdout | 4 ++ ...mptime-run-returning-optional-aggregate.md | 12 ++++++ src/ir/comptime_vm.zig | 31 +++++++++++++++ src/ir/emit_llvm.zig | 19 ++++++++++ 7 files changed, 106 insertions(+) create mode 100644 examples/comptime/0643-comptime-run-optional-aggregate.sx create mode 100644 examples/comptime/expected/0643-comptime-run-optional-aggregate.exit create mode 100644 examples/comptime/expected/0643-comptime-run-optional-aggregate.stderr create mode 100644 examples/comptime/expected/0643-comptime-run-optional-aggregate.stdout diff --git a/examples/comptime/0643-comptime-run-optional-aggregate.sx b/examples/comptime/0643-comptime-run-optional-aggregate.sx new file mode 100644 index 00000000..130af008 --- /dev/null +++ b/examples/comptime/0643-comptime-run-optional-aggregate.sx @@ -0,0 +1,38 @@ +// `#run` of a function returning an OPTIONAL value must bridge the comptime +// VM register → host Value through the reg→value optional arm. +// +// Regression (issue 0162): a `#run` whose function returned `?T` / `?i64` +// previously failed comptime evaluation with +// "reg→value: aggregate shape not bridged yet" +// because the VM's regToValue bridge handled scalars/structs/slices/tuples +// but bailed on an OPTIONAL-typed result. The fix reads the optional's +// has_value flag (at offset sizeof(child)); when set it bridges the payload +// recursively into a `{ payload, i1=true }` aggregate (the host serializes +// that to `{T, i1}`), and when clear (or the value is the bare null sentinel) +// it yields `.null_val` (serialized to a zero `{T, i1}` = absent). +// +// Exercises: present `?T` (optional of struct), present `?i64` (optional of +// scalar), and a NULL-returning `?i64`. The values are read via `!` (unwrap) +// and `??` (coalesce), which read the has_value flag correctly. + +#import "modules/std.sx"; + +T :: struct { a: i64 = 0; b: i64 = 0; } + +mk_struct :: () -> ?T { t : T = .{ a = 7, b = 11 }; return t; } +mk_scalar :: () -> ?i64 { return 5; } +mk_null :: () -> ?i64 { return null; } + +X :: #run mk_struct(); // present ?T +Y :: #run mk_scalar(); // present ?i64 +N :: #run mk_null(); // null ?i64 + +main :: () { + // Present optional-of-struct: payload bridged field-by-field. + print("X.a = {}\n", X!.a); + print("X.b = {}\n", X!.b); + // Present optional-of-scalar. + print("Y = {}\n", Y ?? -1); + // Null optional: coalesce takes the default. + print("N = {}\n", N ?? -1); +} diff --git a/examples/comptime/expected/0643-comptime-run-optional-aggregate.exit b/examples/comptime/expected/0643-comptime-run-optional-aggregate.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/comptime/expected/0643-comptime-run-optional-aggregate.exit @@ -0,0 +1 @@ +0 diff --git a/examples/comptime/expected/0643-comptime-run-optional-aggregate.stderr b/examples/comptime/expected/0643-comptime-run-optional-aggregate.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/comptime/expected/0643-comptime-run-optional-aggregate.stderr @@ -0,0 +1 @@ + diff --git a/examples/comptime/expected/0643-comptime-run-optional-aggregate.stdout b/examples/comptime/expected/0643-comptime-run-optional-aggregate.stdout new file mode 100644 index 00000000..e21960b9 --- /dev/null +++ b/examples/comptime/expected/0643-comptime-run-optional-aggregate.stdout @@ -0,0 +1,4 @@ +X.a = 7 +X.b = 11 +Y = 5 +N = -1 diff --git a/issues/0162-comptime-run-returning-optional-aggregate.md b/issues/0162-comptime-run-returning-optional-aggregate.md index 983536b7..e0f8e74a 100644 --- a/issues/0162-comptime-run-returning-optional-aggregate.md +++ b/issues/0162-comptime-run-returning-optional-aggregate.md @@ -1,5 +1,17 @@ # 0162 — `#run` returning an optional aggregate fails the comptime VM reg→value bridge +> **RESOLVED.** Root cause: the comptime VM's `regToValue` bridge +> (`src/ir/comptime_vm.zig`) had no `.optional` arm, so any OPTIONAL-typed +> `#run` result hit the `"aggregate shape not bridged yet"` bail. Fix: added an +> `.optional` arm that reads the has_value flag (at offset `sizeof(child)`), +> bridges the payload recursively into a `{ payload, i1=true }` aggregate when +> set, and yields `.null_val` (→ zero `{T, i1}`) when clear or the bare null +> sentinel; plus a matching serialize arm in `serializeAggregateValue` +> (`src/ir/emit_llvm.zig`). Pointer/`?Closure`/`?Protocol`-child optionals and +> array-payload aggregates bail loudly (out of scope, not silent). Regression +> test: `examples/comptime/0643-comptime-run-optional-aggregate.sx` (present +> `?T`, present `?i64`, null `?i64`). Verified by 3 adversarial reviews. + ## Symptom A `#run` (or comptime const init) whose function returns an OPTIONAL value diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index a555683d..a88274c1 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -2236,6 +2236,37 @@ pub const Vm = struct { } return .{ .aggregate = out }; } + if (info == .optional) { + // Only the `{ payload@0, has_value@sizeof(child) }` aggregate + // shape lands here — a pointer-child optional is a word and + // bridges through the `.word` arm. A closure / protocol child + // has a different layout (sentinel func-ref / ctx-ptr-as-flag), + // so guard against it and bail loudly rather than mis-read. + const child = info.optional.child; + if (optChildIsPtr(table, child)) + return self.failMsg("reg→value: pointer optional reached the aggregate arm"); + if (!child.isBuiltin()) switch (table.get(child)) { + .closure => return self.failMsg("reg→value: ?Closure optional not bridged (sentinel layout)"), + .@"struct" => |s| if (s.is_protocol) + return self.failMsg("reg→value: ?Protocol optional not bridged (ctx-ptr layout)"), + else => {}, + }; + // has_value flag lives at offset sizeof(child); clear → null. + // The `const_null` form is a bare `null_addr` (no allocation); + // treat that as none too (mirrors `optHas`). + if (reg == null_addr) return .null_val; + const has = (try self.machine.readWord(reg + table.typeSizeBytes(child), 1)) != 0; + if (!has) return .null_val; + // Present: bridge the payload (read from offset 0) as the child + // type, and present it as the `{ payload, i1=true }` LLVM-struct + // shape the host's optional serializer expects. + const payload_reg = try self.readField(table, reg, child); + const payload = try self.regToValue(alloc, table, payload_reg, child); + const out = alloc.alloc(Value, 2) catch return self.failMsg("reg→value: out of memory (optional)"); + out[0] = payload; + out[1] = .{ .boolean = true }; + return .{ .aggregate = out }; + } return self.failMsg("reg→value: aggregate shape not bridged yet"); }, .unsupported => return self.failMsg("reg→value: unsupported type"), diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 7ed3259a..c54d0284 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1230,6 +1230,25 @@ pub const LLVMEmitter = struct { } return c.LLVMConstArray2(llvm_elem_ty, elem_vals.items.ptr, @intCast(elem_vals.items.len)); } + // Present optional `?T` → `{ , i1 1 }`, matching the + // anonymous-struct layout `toLLVMType` builds for a non-pointer + // optional (the absent case is a `.null_val`, serialized at the + // `valueToLLVMConst` top level as `LLVMConstNull` of `{T,i1}`). + // The VM's reg→value optional arm only produces this 2-field + // `[payload, bool]` aggregate for present, non-pointer optionals. + if (info == .optional) { + if (fields.len != 2) { + std.debug.print( + "error: comptime init of '{s}' produced an optional aggregate with {} fields (expected 2: payload, has_value)\n", + .{ global_name, fields.len }, + ); + return self.failGlobalInit(llvm_ty); + } + const payload = self.valueToLLVMConst(fields[0], info.optional.child, global_name); + const has = self.valueToLLVMConst(fields[1], .bool, global_name); + var ofields = [_]c.LLVMValueRef{ payload, has }; + return c.LLVMConstStructInContext(self.context, &ofields, 2, 0); + } } std.debug.print(