Files
sx/issues/0150-void-struct-field-unsized-llvm-trap.md
agra 4fc5411cd9 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
2026-06-21 09:21:18 +03:00

4.2 KiB

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

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).

Symptom

Declaring or instantiating any struct that has a field of type void aborts the compiler with SIGTRAP (exit 133/134) — no sx diagnostic. The trap is LLVM's llvm_unreachable("Cannot getTypeInfo() on a type that is unsized!"):

libLLVM`llvm::DataLayout::getTypeSizeInBits + 912   brk #0x1   (EXC_BREAKPOINT)

Reached via declareFunctiontoLLVMType(func.ret) when a function returns such a struct, or directly when laying out the struct.

Observed: SIGTRAP, no output, no diagnostic. Expected: either zero-size the void field (a void/zero-sized field is a legitimate construct — cf. Zig) OR emit a clean type diagnostic ("a struct field may not have type void") — never a raw backend crash.

Reproduction

#import "modules/std.sx";

Holder :: struct { v: void; ok: bool; }

main :: () -> i32 {
    h : Holder = .{ ok = true };
    if h.ok { print("ok\n"); }
    return 0;
}

./zig-out/bin/sx run repro.sx → SIGTRAP (exit 133), no output.

Also reproduces through a generic: Box :: struct($T: Type) { v: T; } then Box(void) — i.e. any monomorphization that binds a struct field to void.

Suspected area

  • src/backend/llvm/types.zig toLLVMTypeInfo (struct field loop ~line 111): a void field's LLVM type is the unsized void type, then getTypeSizeInBits on the enclosing struct traps.
  • The type layout / size code (src/ir/types.zig typeSizeBytes and the LLVM struct builder) should treat a void field as zero-sized (skip it in the LLVM struct, size 0, align 1) — the same way a zero-field struct is handled.

Investigation prompt (paste into a fresh session)

A void struct field crashes the sx compiler with an unsized-type SIGTRAP in LLVM getTypeSizeInBits (no diagnostic). Repro: issues/0150-... (run it → exit 133). Decide the semantics: a void field should be ZERO-SIZED (preferred — it is a legitimate construct, e.g. Future(void).result), laid out as nothing (size 0, align 1) and OMITTED from the LLVM struct body; OR, if zero-sized fields are out of scope, a clean front-end diagnostic ("a struct field may not have type void, found in field <name> of <Struct>") before emission — NEVER a backend trap. Likely sites: src/backend/llvm/types.zig toLLVMTypeInfo (skip void fields when building the LLVM struct element list) + src/ir/types.zig size/align (typeSizeBytes/align: a void field contributes 0). If choosing the diagnostic route, add it where struct fields are validated at type-resolution time. Verify: the repro prints ok (zero-size route) or emits the diagnostic + clean exit 1 (diagnostic route); then move the repro into examples/ as a regression test.

Why this matters for B1 (fibers)

Future($R) with $R = void is the natural shape for timeout(io, ms) -> Future(void) (B1.2 spec) and for any future-of-no-value. B1.2 deferred timeout pending this fix rather than route around it with a substitute non-void shape (which would hide the bug). Once 0150 lands, re-add timeout with Future(void) (see the saved WIP at .sx-tmp/b12-wip/io.sx).