From 4fc5411cd98a8d9d9c32c56367acf4115592187b Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 21 Jun 2026 09:21:18 +0300 Subject: [PATCH] fix: allow void (zero-sized) struct/tuple fields instead of crashing (issue 0150) A struct/tuple/?T with a void field crashed the compiler: the field lowered to LLVM's unsized 'void' type, which traps getTypeSizeInBits. Lower a void field to a SIZED zero-byte [0 x i8] (fieldLLVMType) so the enclosing aggregate stays sized with identical element indices, and skip inserting a value for a void field in emitStructInit (the i64 placeholder would type-mismatch the [0 x i8] slot and corrupt the aggregate constant -> runtime bus error). Future(void) now works. Regression: examples/0190-types-void-struct-field-zero-sized.sx --- ...0190-types-void-struct-field-zero-sized.sx | 27 +++++++++++++++++++ ...90-types-void-struct-field-zero-sized.exit | 1 + ...-types-void-struct-field-zero-sized.stderr | 1 + ...-types-void-struct-field-zero-sized.stdout | 3 +++ ...150-void-struct-field-unsized-llvm-trap.md | 13 ++++++++- ...150-void-struct-field-unsized-llvm-trap.sx | 13 --------- src/backend/llvm/ops.zig | 12 +++++++++ src/backend/llvm/types.zig | 22 ++++++++++++--- 8 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 examples/0190-types-void-struct-field-zero-sized.sx create mode 100644 examples/expected/0190-types-void-struct-field-zero-sized.exit create mode 100644 examples/expected/0190-types-void-struct-field-zero-sized.stderr create mode 100644 examples/expected/0190-types-void-struct-field-zero-sized.stdout delete mode 100644 issues/0150-void-struct-field-unsized-llvm-trap.sx diff --git a/examples/0190-types-void-struct-field-zero-sized.sx b/examples/0190-types-void-struct-field-zero-sized.sx new file mode 100644 index 00000000..b8766a6d --- /dev/null +++ b/examples/0190-types-void-struct-field-zero-sized.sx @@ -0,0 +1,27 @@ +// A `void` (zero-sized) struct/tuple field is legitimate (e.g. `Future(void)`) +// and must NOT crash the compiler. It lowers to a zero-width `[0 x i8]` LLVM +// slot (TypeLowering.fieldLLVMType), and aggregate init skips storing a value +// into it (emitStructInit) — so the struct stays sized and field access past +// the void field is correct, instead of the old unsized-type SIGTRAP / a +// corrupt aggregate constant. +// +// Regression (issue 0150). +#import "modules/std.sx"; + +Holder :: struct { v: void; ok: bool; } + +Box :: struct($T: Type) { v: T; tag: i32; } + +main :: () -> i32 { + h : Holder = .{ ok = true }; + if h.ok { print("ok\n"); } + + // Through a generic instantiated at `void`. + b : Box(void) = .{ tag = 7 }; + print("tag={}\n", b.tag); + + // A tuple with a void element. + t : (void, i32) = .{ {}, 9 }; + print("t1={}\n", t.1); + return 0; +} diff --git a/examples/expected/0190-types-void-struct-field-zero-sized.exit b/examples/expected/0190-types-void-struct-field-zero-sized.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0190-types-void-struct-field-zero-sized.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0190-types-void-struct-field-zero-sized.stderr b/examples/expected/0190-types-void-struct-field-zero-sized.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0190-types-void-struct-field-zero-sized.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0190-types-void-struct-field-zero-sized.stdout b/examples/expected/0190-types-void-struct-field-zero-sized.stdout new file mode 100644 index 00000000..1b8f8c58 --- /dev/null +++ b/examples/expected/0190-types-void-struct-field-zero-sized.stdout @@ -0,0 +1,3 @@ +ok +tag=7 +t1=9 diff --git a/issues/0150-void-struct-field-unsized-llvm-trap.md b/issues/0150-void-struct-field-unsized-llvm-trap.md index b948f74f..bff566ef 100644 --- a/issues/0150-void-struct-field-unsized-llvm-trap.md +++ b/issues/0150-void-struct-field-unsized-llvm-trap.md @@ -1,7 +1,18 @@ # 0150 — a `void` struct field crashes the compiler (unsized-type SIGTRAP in LLVM) +> **RESOLVED.** Two coordinated changes let a `void` (zero-sized) field be a +> legitimate construct (so `Future(void)` works): (1) `TypeLowering.fieldLLVMType` +> (src/backend/llvm/types.zig) lowers a `void` struct/tuple/`?T` field to a SIZED +> zero-byte `[0 x i8]` instead of LLVM's unsized `void` (which trapped +> `getTypeSizeInBits`), keeping element count/indices identical; (2) `emitStructInit` +> (src/backend/llvm/ops.zig) skips inserting a value for a `void` field — the i64 +> placeholder would type-mismatch the `[0 x i8]` slot and corrupt the aggregate +> constant (the original runtime bus-error). Regression test: +> `examples/0190-types-void-struct-field-zero-sized.sx` (covers a plain struct, a +> generic `Box(void)`, and a tuple void element). + ## Status -OPEN — surfaced by Stream B1 (fibers) B1.2: `Future(void)` (needed by +RESOLVED (was: OPEN) — surfaced by Stream B1 (fibers) B1.2: `Future(void)` (needed by `timeout(io, ms) -> Future(void)`) instantiates a struct with a `result: void` field, which hits this bug. Independent of the fibers work (a plain `struct { v: void; }` reproduces it standalone). diff --git a/issues/0150-void-struct-field-unsized-llvm-trap.sx b/issues/0150-void-struct-field-unsized-llvm-trap.sx deleted file mode 100644 index 3451e385..00000000 --- a/issues/0150-void-struct-field-unsized-llvm-trap.sx +++ /dev/null @@ -1,13 +0,0 @@ -// Repro for issue 0150 — a `void` struct field crashes the compiler with an -// unsized-type SIGTRAP (LLVM getTypeSizeInBits). Unpinned (no expected marker) -// because it currently aborts the compiler; pin it as a regression test once -// the fix lands. -#import "modules/std.sx"; - -Holder :: struct { v: void; ok: bool; } - -main :: () -> i32 { - h : Holder = .{ ok = true }; - if h.ok { print("ok\n"); } - return 0; -} diff --git a/src/backend/llvm/ops.zig b/src/backend/llvm/ops.zig index 18d104a8..8b474354 100644 --- a/src/backend/llvm/ops.zig +++ b/src/backend/llvm/ops.zig @@ -1720,6 +1720,18 @@ pub const Ops = struct { const elem_llvm_ty = if (is_array) c.LLVMGetElementType(struct_ty) else null; var result = c.LLVMGetUndef(struct_ty); for (agg.fields, 0..) |field_ref, i| { + // A `void` (zero-sized) struct/tuple field lowers to a zero-width + // `[0 x i8]` slot (see `TypeLowering.fieldLLVMType`); it carries no + // data. Skip inserting a value — the field's lowered ref is an i64 + // placeholder (`emitConstInt`'s void path) whose type mismatches the + // slot and would corrupt the aggregate. The undef `[0 x i8]` element + // is already the correct zero-width value. + const field_is_void = switch (self.e.ir_mod.types.get(instruction.ty)) { + .@"struct" => |s| i < s.fields.len and s.fields[i].ty == .void, + .tuple => |t| i < t.fields.len and t.fields[i] == .void, + else => false, + }; + if (field_is_void) continue; var field_val = self.e.resolveRef(field_ref); if (is_vector) { // Coerce element to match vector element type diff --git a/src/backend/llvm/types.zig b/src/backend/llvm/types.zig index cf2de0c0..2c168657 100644 --- a/src/backend/llvm/types.zig +++ b/src/backend/llvm/types.zig @@ -39,6 +39,22 @@ pub const TypeLowering = struct { }; } + /// Lower a *field* (struct/tuple element, or the payload of `?T`). Identical + /// to `toLLVMType` except that a field which lowers to LLVM's unsized + /// `void` type (a `void`/`noreturn`/zero-sized field — legitimate, e.g. + /// `Future(void)`) is substituted with a SIZED zero-byte `[0 x i8]`. LLVM's + /// `getTypeSizeInBits` traps on an unsized struct element ("Cannot + /// getTypeInfo() on a type that is unsized!"); `[0 x i8]` reports size 0 and + /// keeps the element COUNT and INDICES identical, so field-access codegen + /// (GEP / extractvalue by `field_index`) needs no remapping. + pub fn fieldLLVMType(self: TypeLowering, ty: TypeId) c.LLVMTypeRef { + const lowered = self.toLLVMType(ty); + if (lowered == self.e.cached_void) { + return c.LLVMArrayType2(self.e.cached_i8, 0); + } + return lowered; + } + fn toLLVMTypeInfo(self: TypeLowering, ty: TypeId) c.LLVMTypeRef { const info = self.e.ir_mod.types.get(ty); return switch (info) { @@ -83,7 +99,7 @@ pub const TypeLowering = struct { } // ?T → { T, i1 } var field_types: [2]c.LLVMTypeRef = .{ - self.toLLVMType(opt.child), + self.fieldLLVMType(opt.child), self.e.cached_i1, }; return c.LLVMStructTypeInContext(self.e.context, &field_types, 2, 0); @@ -108,7 +124,7 @@ pub const TypeLowering = struct { const field_llvm_types = self.e.alloc.alloc(c.LLVMTypeRef, s.fields.len) catch unreachable; defer self.e.alloc.free(field_llvm_types); for (s.fields, 0..) |field, j| { - field_llvm_types[j] = self.toLLVMType(field.ty); + field_llvm_types[j] = self.fieldLLVMType(field.ty); } return c.LLVMStructTypeInContext(self.e.context, field_llvm_types.ptr, n, 0); }, @@ -162,7 +178,7 @@ pub const TypeLowering = struct { const field_llvm_types = self.e.alloc.alloc(c.LLVMTypeRef, t.fields.len) catch unreachable; defer self.e.alloc.free(field_llvm_types); for (t.fields, 0..) |f, j| { - field_llvm_types[j] = self.toLLVMType(f); + field_llvm_types[j] = self.fieldLLVMType(f); } return c.LLVMStructTypeInContext(self.e.context, field_llvm_types.ptr, n, 0); },