fibers B1.2: BLOCKED on compiler bugs 0150 + 0151 (Io design proven)
Stream B1 B1.2 (Io capability + context.io + Future + cancel) is blocked on two newly-discovered, independent compiler bugs, both with standalone repros: - 0150: a `void` struct field crashes the compiler with an unsized-type SIGTRAP in LLVM getTypeSizeInBits. Blocks `Future(void)` -> `timeout`. - 0151: a type-var inferred from a fn-pointer parameter's RETURN type is not bound as a usable type in the function body (`unknown type 'R'`). Blocks the central `async(io, worker: ($A)->$R, arg)` free-fn's `Future(R)`. The B1.2 design itself is validated end-to-end (the Io protocol threaded on Context like Allocator, the stateless blocking CBlockingIo default, both __sx_default_context materializers, and `context.io.now_ms()` all work live). Only the async/await/timeout ergonomic layer hits the two bugs. Per the IMPASSABLE STOP rule, all B1.2 working changes were reverted (master green, 726/0) and the work paused pending fixes; WIP is saved at .sx-tmp/b12-wip/. Checkpoint + plan updated to mark B1.2 BLOCKED with full resume notes.
This commit is contained in:
72
issues/0150-void-struct-field-unsized-llvm-trap.md
Normal file
72
issues/0150-void-struct-field-unsized-llvm-trap.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 0150 — a `void` struct field crashes the compiler (unsized-type SIGTRAP in LLVM)
|
||||
|
||||
## Status
|
||||
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 `declareFunction` → `toLLVMType(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
|
||||
```sx
|
||||
#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`).
|
||||
13
issues/0150-void-struct-field-unsized-llvm-trap.sx
Normal file
13
issues/0150-void-struct-field-unsized-llvm-trap.sx
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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;
|
||||
}
|
||||
98
issues/0151-typevar-from-fnptr-return-not-bound-in-body.md
Normal file
98
issues/0151-typevar-from-fnptr-return-not-bound-in-body.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 0151 — a type-var inferred from a fn-pointer parameter's RETURN type is not bound in the function body
|
||||
|
||||
## Status
|
||||
OPEN — blocks Stream B1 (fibers) B1.2's `async(io, worker: ($A) -> $R, arg: $A)`
|
||||
free-fn (it needs `Future(R)` in its body). Independent of the fibers work
|
||||
(reproduces with a tiny `Wrap($R)` standalone).
|
||||
|
||||
## Symptom
|
||||
A generic free function whose type-var `$R` is introduced **inside a
|
||||
fn-pointer parameter's return type** (`worker: ($A) -> $R`) infers `$R` fine for
|
||||
the call's type-checking, but `R` is **not in scope as a usable type name in the
|
||||
function body**. Referencing `Wrap(R)` (or any `R`) in the body errors:
|
||||
|
||||
```
|
||||
error: unknown type 'R'
|
||||
--> repro.sx:4:14
|
||||
|
|
||||
4 | w : Wrap(R) = .{ v = worker(arg) };
|
||||
```
|
||||
|
||||
By contrast a type-var introduced **directly** by a parameter (`arg: $A`) IS
|
||||
usable in the body (`Wrap(A)` works — see the second repro). So the gap is
|
||||
specific to type-vars that appear only nested in a fn-pointer (or closure)
|
||||
parameter's signature.
|
||||
|
||||
Observed: `error: unknown type 'R'` for the body reference.
|
||||
Expected: `R` binds to the worker's return type (here `i64`), so `Wrap(R)`
|
||||
resolves to `Wrap(i64)`, exactly as `$A` → `A` does.
|
||||
|
||||
## Reproduction (fails)
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Wrap :: struct($T: Type) { v: T; }
|
||||
|
||||
runit :: (worker: ($A) -> $R, arg: $A) -> Wrap($R) {
|
||||
w : Wrap(R) = .{ v = worker(arg) }; // error: unknown type 'R'
|
||||
return w;
|
||||
}
|
||||
|
||||
dbl :: (n: i64) -> i64 { return n * 2; }
|
||||
|
||||
main :: () -> i32 {
|
||||
r := runit(dbl, 21);
|
||||
print("{}\n", r.v); // want: 42
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Reproduction (works — shows the contrast)
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Wrap :: struct($T: Type) { v: T; }
|
||||
runit :: (arg: $A) -> Wrap($A) {
|
||||
w : Wrap(A) = .{ v = arg }; // OK — `A` (direct param type-var) binds
|
||||
return w;
|
||||
}
|
||||
main :: () -> i32 { r := runit(21); print("{}\n", r.v); return 0; } // prints 21
|
||||
```
|
||||
|
||||
## Suspected area
|
||||
Generic monomorphization / type-binding collection (the pass that walks a generic
|
||||
function's parameter signatures to discover `$X` type-vars and record the
|
||||
caller-inferred binding for use in the body). It descends into direct param types
|
||||
(`arg: $A` → binds `A`) but does NOT descend into a fn-pointer / closure
|
||||
parameter's nested signature (`($A) -> $R`) to also bind `$R` from the matched
|
||||
argument function's return type (and likewise `$A` from its params, if only named
|
||||
there). Look for where `$`-type-params are gathered from `fd.params` and the
|
||||
per-instance `type_bindings` map is seeded — likely in `src/ir/generic.zig`
|
||||
and/or the call-site argument→param type-var unifier in `src/ir/lower/call.zig`.
|
||||
The unifier already infers `$R` well enough to type-check the call (the error is
|
||||
only in the BODY), so the binding exists at the call site but isn't propagated
|
||||
into the monomorphized body's `type_bindings`.
|
||||
|
||||
## Investigation prompt (paste into a fresh session)
|
||||
> A type-var that appears only inside a fn-pointer parameter's signature
|
||||
> (`worker: ($A) -> $R`) is inferred at the call site (the call type-checks) but
|
||||
> is NOT available as a type name in the generic function's BODY — `Wrap(R)` in
|
||||
> the body errors `unknown type 'R'`, while a direct `arg: $A` makes `A` usable.
|
||||
> Repro: `issues/0151-...` (the failing + the working contrast are both inline in
|
||||
> the `.md`; the `.sx` is the failing one). Fix the generic type-binding pass so
|
||||
> that when a generic fn is monomorphized, type-vars discovered inside a
|
||||
> fn-pointer/closure parameter's nested signature (its params AND its return
|
||||
> type) are added to the instance's `type_bindings` from the matched argument
|
||||
> function's concrete signature — mirroring how direct param type-vars are bound.
|
||||
> Suspected sites: `src/ir/generic.zig` (binding collection from `fd.params`) +
|
||||
> the call-site unifier in `src/ir/lower/call.zig` (it already infers `$R` for
|
||||
> overload/type-check, so reuse that result to seed the body bindings). Verify:
|
||||
> the failing repro prints `42`; then move it to `examples/` as a regression test.
|
||||
|
||||
## Why this matters for B1 (fibers)
|
||||
`async(io, worker: ($A) -> $R, arg: $A) -> Future($R)` is the central B1.2
|
||||
ergonomic free-fn; its body builds `Future(R)`. Without this fix `async` can't be
|
||||
written in its spec-faithful form. Routing around it (an explicit `$R: Type`
|
||||
param the caller must pass) would change the surface and HIDE the gap — not done.
|
||||
The rest of the B1.2 Io surface (the `Io` protocol on `Context`, the blocking
|
||||
`CBlockingIo` default, `context.io.now_ms()`) works; only the `async`/`await`
|
||||
generics are blocked by this. Saved WIP: `.sx-tmp/b12-wip/`.
|
||||
Reference in New Issue
Block a user