diff --git a/examples/0640-comptime-list-grown-variant-define.sx b/examples/0640-comptime-list-grown-variant-define.sx new file mode 100644 index 00000000..3d88fc8c --- /dev/null +++ b/examples/0640-comptime-list-grown-variant-define.sx @@ -0,0 +1,38 @@ +// Comptime type construction from a List grown at compile time: assemble the +// variant set in a `List(EnumVariant)` via `.append` (which allocates + grows +// its backing through the comptime `context.allocator`), then mint an enum from +// the grown backing sliced to its length — `vs.items[0..vs.len]`. The comptime +// VM evaluates the List growth, the slice, and `define`/`declare` end-to-end. +// +// A `[*]T` many-pointer (the List's bare `items` field) carries no length, so it +// must be sliced with `[0..len]` to form a `[]T`; passing `vs.items` bare is a +// rejected coercion (see the diagnostic example for that path). +// +// Regression (issue 0141): the List-grown form used to segfault in the comptime +// VM's slice decoder. +#import "modules/std.sx"; +#import "modules/std/meta.sx"; + +make_enum :: (name: string, variants: []EnumVariant) -> Type { + return define(declare(name), .enum(.{ variants = variants })); +} + +build_color :: () -> Type { + vs : List(EnumVariant) = .{}; + vs.append(EnumVariant.{ name = "red", payload = void }); + vs.append(EnumVariant.{ name = "green", payload = i64 }); + vs.append(EnumVariant.{ name = "blue", payload = void }); + return make_enum("Color", vs.items[0..vs.len]); +} + +Color :: build_color(); + +main :: () -> i32 { + c : Color = .green(7); + if c == { + case .red: { print("red\n"); } + case .green: (v) { print("green={}\n", v); } + case .blue: { print("blue\n"); } + } + return 0; +} diff --git a/examples/1183-diagnostics-many-pointer-to-slice-rejected.sx b/examples/1183-diagnostics-many-pointer-to-slice-rejected.sx new file mode 100644 index 00000000..a40b918f --- /dev/null +++ b/examples/1183-diagnostics-many-pointer-to-slice-rejected.sx @@ -0,0 +1,24 @@ +// A many-pointer `[*]T` carries NO length, so it cannot coerce to a slice `[]T` +// implicitly — doing so would pass a bare 8-byte pointer where a 16-byte +// `{ptr,len}` fat pointer is expected, silently corrupting the callee's view of +// the data (garbage length, mis-aligned element reads). The compiler rejects it +// loudly and tells the user to supply the length via `ptr[0..len]`. +// +// Regression (issue 0141): this silent mis-coercion segfaulted the comptime VM +// and failed LLVM verification at runtime; it now produces a clean diagnostic. +#import "modules/std.sx"; + +sum :: (s: []i64) -> i64 { + total := 0; + for s (x) { total += x; } + return total; +} + +main :: () -> i32 { + xs : List(i64) = .{}; + xs.append(10); + xs.append(20); + r := sum(xs.items); // [*]i64 → []i64 — needs xs.items[0..xs.len] + print("{}\n", r); + return 0; +} diff --git a/examples/expected/0640-comptime-list-grown-variant-define.exit b/examples/expected/0640-comptime-list-grown-variant-define.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0640-comptime-list-grown-variant-define.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0640-comptime-list-grown-variant-define.stderr b/examples/expected/0640-comptime-list-grown-variant-define.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0640-comptime-list-grown-variant-define.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0640-comptime-list-grown-variant-define.stdout b/examples/expected/0640-comptime-list-grown-variant-define.stdout new file mode 100644 index 00000000..06a4c129 --- /dev/null +++ b/examples/expected/0640-comptime-list-grown-variant-define.stdout @@ -0,0 +1 @@ +green=7 diff --git a/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.exit b/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.stderr b/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.stderr new file mode 100644 index 00000000..c03f8a62 --- /dev/null +++ b/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.stderr @@ -0,0 +1,5 @@ +error: a many-pointer '[*]T' does not coerce to a slice '[]T' implicitly (it carries no length) — slice it with a length: ptr[0..len] + --> examples/1183-diagnostics-many-pointer-to-slice-rejected.sx:21:10 + | +21 | r := sum(xs.items); // [*]i64 → []i64 — needs xs.items[0..xs.len] + | ^^^^^^^^^^^^^ diff --git a/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.stdout b/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1183-diagnostics-many-pointer-to-slice-rejected.stdout @@ -0,0 +1 @@ + diff --git a/issues/0141-comptime-list-growth-in-type-construction.md b/issues/0141-comptime-list-growth-in-type-construction.md index d8a1040d..a0c99a8b 100644 --- a/issues/0141-comptime-list-growth-in-type-construction.md +++ b/issues/0141-comptime-list-growth-in-type-construction.md @@ -1,5 +1,52 @@ # 0141 — `List(T).append` at comptime (in a type-construction `::`) bails +> **Status: RESOLVED (2026-06-19).** +> +> **Root cause (final, after the legacy interp was deleted and the comptime VM +> became the sole evaluator):** the original repro passes `vs.items` — a bare +> many-pointer `[*]EnumVariant` — where a slice `[]EnumVariant` is expected. A +> `[*]T` carries NO length, so there was no valid implicit coercion to `[]T`; the +> classifier fell through to `.none` and passed the bare 8-byte data pointer +> UNCHANGED where a 16-byte `{ptr,len}` fat pointer was expected. The callee then +> read the slice header off the wrong bytes. At RUNTIME this failed LLVM +> verification (8-byte ptr into a `{ptr,len}` param slot); at COMPTIME the VM read +> `.ptr`/`.len` from adjacent bytes and dereferenced a garbage data pointer (a +> comptime `Addr` is a REAL host pointer), faulting at address `0x646572` ("red") +> inside `comptime_vm.decodeMemberSlice`. This was a silent-wrong-coercion (a +> CLAUDE.md REJECTED PATTERN), not a List/allocator/slot_ptr problem — the +> List growth, the comptime allocator, and `define`/`decodeMemberSlice` are all +> correct (the prior multi-layer analyses below predate the VM rewrite and are +> SUPERSEDED). +> +> **Fix:** +> 1. `src/ir/conversions.zig` — `CoercionResolver.classify` now classifies +> `[*]T → []T` as the new `.many_to_slice_reject` plan (added to `CoercionPlan`). +> 2. `src/ir/lower/coerce.zig` — `coerceMode`'s new `.many_to_slice_reject` arm +> emits a build-gating diagnostic: *"a many-pointer '[*]T' does not coerce to a +> slice '[]T' implicitly (it carries no length) — slice it with a length: +> ptr[0..len]"*. +> 3. `src/ir/lower/comptime.zig` — `runComptimeTypeFunc` now skips the VM eval when +> `diagnostics.hasErrors()` is already set. A type-fn whose body failed coercion +> holds malformed IR; running the VM on it would deref garbage (the VM's +> bail-not-crash guards catch malformed *Refs*, not malformed comptime *data*). +> The user's real diagnostic is already on the list, so the build aborts cleanly +> instead of segfaulting. +> +> **Correct spelling** for the List-grown form is `vs.items[0..vs.len]` (a +> subslice that supplies the length), exactly like the array form's `dirs[0..2]` +> in `examples/0621`. +> +> **Regression tests:** +> - `examples/0640-comptime-list-grown-variant-define.sx` — the List-grown enum +> construction with the correct `vs.items[0..vs.len]` spelling → prints +> `green=7`, exit 0 (the feature this issue tracked, now working on the VM). +> - `examples/1183-diagnostics-many-pointer-to-slice-rejected.sx` — the bare +> `[*]T → []T` mis-coercion now produces the clean diagnostic (exit 1), no crash. +> +> --- +> +> *Original (now-stale) writeup follows — kept for history.* +> > **Status: OPEN — deferred enhancement, NOT a blocker.** Building a comptime > variant/field list with an array-literal local already works > (`examples/0620`/`0624`); only the `List`-grown form fails. Filed to record the diff --git a/issues/0141-comptime-list-growth-in-type-construction.sx b/issues/0141-comptime-list-growth-in-type-construction.sx deleted file mode 100644 index 9cf4a01f..00000000 --- a/issues/0141-comptime-list-growth-in-type-construction.sx +++ /dev/null @@ -1,34 +0,0 @@ -// Repro for issue 0141 — a `List(T)` grown at comptime inside a type-construction -// `::` const bails. `make_enum` assembles its variant list in a `List`, appends, -// then mints from `vs.items`. The append fails at comptime ("struct_get: base has -// no fields") even though the identical code works at runtime AND via `#run`. -// -// Expected: `Color` constructs from the List-built variant list and `.green(7)` -// matches (prints "green=7"), exit 0 — the same as the array-literal form -// (examples/0620), which already works. -#import "modules/std.sx"; -#import "modules/std/meta.sx"; - -make_enum :: (name: string, variants: []EnumVariant) -> Type { - return define(declare(name), .enum(.{ variants = variants })); -} - -build_color :: () -> Type { - vs : List(EnumVariant) = .{}; - vs.append(EnumVariant.{ name = "red", payload = void }); - vs.append(EnumVariant.{ name = "green", payload = i64 }); - vs.append(EnumVariant.{ name = "blue", payload = void }); - return make_enum("Color", vs.items); -} - -Color :: build_color(); - -main :: () -> i32 { - c : Color = .green(7); - if c == { - case .red: { print("red\n"); } - case .green: (v) { print("green={}\n", v); } - case .blue: { print("blue\n"); } - } - return 0; -} diff --git a/src/ir/conversions.zig b/src/ir/conversions.zig index b17aea26..a8e1fa25 100644 --- a/src/ir/conversions.zig +++ b/src/ir/conversions.zig @@ -43,6 +43,7 @@ pub const CoercionResolver = struct { widen, // same kind, dst wider narrow, // same kind, dst narrower array_to_slice, // [N]T → []T (materialize backing storage + header) + many_to_slice_reject, // [*]T → []T (no length — needs ptr[0..len]; diagnostic) string_to_cstring, // literal-only implicit; other strings need to_cstring cstring_to_string_reject, // explicit from_cstring required (diagnostic) none, // nothing applies — pass the value through @@ -82,6 +83,15 @@ pub const CoercionResolver = struct { if (si == .array and di == .slice and si.array.element == di.slice.element) { return .array_to_slice; } + // `[*]T → []T`: a many-pointer carries NO length, so it cannot form a + // `{ptr,len}` slice header implicitly. Silently passing the bare 8-byte + // pointer where a 16-byte fat pointer is expected corrupts the callee's + // view (garbage `.len`, mis-aligned reads) — at comptime it segfaults + // (issue 0141), at runtime it fails LLVM verification. Reject loudly so + // the user supplies the length via `ptr[0..len]`. + if (si == .many_pointer and di == .slice) { + return .many_to_slice_reject; + } } // Optional → Concrete unwrap (narrowing). diff --git a/src/ir/lower/coerce.zig b/src/ir/lower/coerce.zig index a651155c..56b82eb5 100644 --- a/src/ir/lower/coerce.zig +++ b/src/ir/lower/coerce.zig @@ -730,6 +730,15 @@ pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mod .narrow => return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .widen => return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .array_to_slice => return self.builder.emit(.{ .array_to_slice = .{ .operand = val } }, dst_ty), + // `[*]T → []T`: a many-pointer has no length, so it can't form a slice + // header implicitly. Diagnose and tell the user to slice with a length. + .many_to_slice_reject => { + if (self.diagnostics) |d| { + const cs = self.builder.current_span; + d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "a many-pointer '[*]T' does not coerce to a slice '[]T' implicitly (it carries no length) — slice it with a length: ptr[0..len]", .{}); + } + return val; + }, } } diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 64820fa4..213fa78c 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -504,6 +504,15 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty } } + // If lowering this type-fn's BODY already emitted an error (e.g. a rejected + // coercion like `[*]T → []T`, issue 0141), the function holds malformed IR — + // a slice value that is really a bare 8-byte pointer, etc. Running the VM on it + // would dereference garbage (a comptime Addr is a real host pointer, so a bad + // data pointer FAULTS, defeating the VM's bail-not-crash guards which only catch + // malformed Refs, not malformed comptime DATA). The user's real diagnostic is + // already on the list; skip the eval and let `hasErrors()` abort the build. + if (self.diagnostics) |d| if (d.hasErrors()) return null; + // The comptime VM is the SOLE evaluator (P5.7) — no legacy fallback. A // type-fn runs on the VM; a bail is ALWAYS a build-gating diagnostic, never a // fallback. The VM is hardened against malformed lowering-time IR (it BAILS,