P5.7 Step E: fix issue 0141 (reject silent [*]T -> []T coercion); land regression
The 0141 repro relied on a silent-wrong coercion: passing List.items (a
[*]T many-pointer, no length) to a []T parameter passed the bare 8-byte
pointer into a 16-byte {ptr,len} slot — garbage .len, at comptime a segfault
in the VM slice decoder (decodeMemberSlice), at runtime an LLVM verify failure.
Fix (root cause): classify [*]T -> []T as many_to_slice_reject in
conversions.zig and emit a build-gating diagnostic in coerce.zig telling the
user to slice with a length (ptr[0..len]). Guard runComptimeTypeFunc to skip
VM eval once diagnostics.hasErrors() — a type-fn body that failed coercion
holds malformed comptime data (a real host Addr) that would fault the VM's
Ref-level guards.
Land the corrected feature as examples/0640 (List-grown comptime enum via
vs.items[0..vs.len] -> green=7) and the rejection as
examples/1183-diagnostics-many-pointer-to-slice-rejected. Mark issue 0141
RESOLVED.
708/0 corpus + 476/476 unit.
This commit is contained in:
38
examples/0640-comptime-list-grown-variant-define.sx
Normal file
38
examples/0640-comptime-list-grown-variant-define.sx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
24
examples/1183-diagnostics-many-pointer-to-slice-rejected.sx
Normal file
24
examples/1183-diagnostics-many-pointer-to-slice-rejected.sx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
green=7
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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]
|
||||||
|
| ^^^^^^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,5 +1,52 @@
|
|||||||
# 0141 — `List(T).append` at comptime (in a type-construction `::`) bails
|
# 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
|
> **Status: OPEN — deferred enhancement, NOT a blocker.** Building a comptime
|
||||||
> variant/field list with an array-literal local already works
|
> variant/field list with an array-literal local already works
|
||||||
> (`examples/0620`/`0624`); only the `List`-grown form fails. Filed to record the
|
> (`examples/0620`/`0624`); only the `List`-grown form fails. Filed to record the
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -43,6 +43,7 @@ pub const CoercionResolver = struct {
|
|||||||
widen, // same kind, dst wider
|
widen, // same kind, dst wider
|
||||||
narrow, // same kind, dst narrower
|
narrow, // same kind, dst narrower
|
||||||
array_to_slice, // [N]T → []T (materialize backing storage + header)
|
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
|
string_to_cstring, // literal-only implicit; other strings need to_cstring
|
||||||
cstring_to_string_reject, // explicit from_cstring required (diagnostic)
|
cstring_to_string_reject, // explicit from_cstring required (diagnostic)
|
||||||
none, // nothing applies — pass the value through
|
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) {
|
if (si == .array and di == .slice and si.array.element == di.slice.element) {
|
||||||
return .array_to_slice;
|
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).
|
// Optional → Concrete unwrap (narrowing).
|
||||||
|
|||||||
@@ -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),
|
.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),
|
.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),
|
.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;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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,
|
// fallback. The VM is hardened against malformed lowering-time IR (it BAILS,
|
||||||
|
|||||||
Reference in New Issue
Block a user