lang F1 2.7: pack-as-value diagnostics (Phase 2 complete)
Using a bare pack name where a runtime value is required was silent garbage (f(xs)/return xs produced a stray pointer). Now a clear, context-tailored compile error: isPackName + diagPackAsValue, caught at lowerVarDecl (storage), lowerReturn (return), lowerFor (iterate), and an identifier-arm catch-all for call/other. Storage binds a placeholder so there is no cascade error. Suggestions point at WORKING fixes -- materialize (..xs), or declare the slice form ..xs: []P for runtime use. The plan category-B "spread ..xs" is broken (spreading a comptime pack into a []Any param crashes the LLVM verifier; filed issue 0053), so the diagnostics steer to the slice-of-protocol variadic instead. Repurposed examples/162-pack-bare-args.sx (was an aspirational bare-$args->[]Any auto-materialise, contradicting Decision 1) into the slice-form forward (..args: []Any). examples/203 is the four-category negative test. specs.md "Pack as value" updated. 238 examples + unit green.
This commit is contained in:
@@ -1,25 +1,19 @@
|
||||
// Variadic heterogeneous type packs — follow-up #3 (bare `args`
|
||||
// reference). The pack-mono doesn't materialise a slice value
|
||||
// for the pack name itself, so any body that references `args`
|
||||
// without indexing (e.g. forwarding to a `[]Any`-typed helper,
|
||||
// or just computing `args.len` through a non-comptime path)
|
||||
// fails with "unresolved 'args'".
|
||||
// Step 2.7 — forwarding a variadic to a `[]Any` helper.
|
||||
//
|
||||
// Next commit materialises an `[]Any` slice on demand inside
|
||||
// the mono: each pack param is boxed into Any, stored in a
|
||||
// stack [N x Any] array, and the slice {data_ptr, len} is bound
|
||||
// to the pack name. `args` then resolves as a runtime value
|
||||
// like the pre-2b inline path did.
|
||||
// A comptime pack `..$args` is comptime-only (Decision 1): `args` bare is NOT a
|
||||
// runtime value, so `log_count(args)` on a pack is an error (see the
|
||||
// pack-as-value tests). To forward a variadic to a runtime `[]Any` helper,
|
||||
// declare it as the *slice* variadic `..args: []Any` — then `args` is a real
|
||||
// `[]Any` slice that passes straight through.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
// Helper that takes a slice. Today the pack body can't pass
|
||||
// `args` here because `args` isn't in scope as a value.
|
||||
log_count :: (items: []Any) -> s64 {
|
||||
return items.len;
|
||||
}
|
||||
|
||||
forward :: (..$args) -> s64 {
|
||||
// Slice variadic: `args` is a runtime []Any, forwarded directly.
|
||||
forward :: (..args: []Any) -> s64 {
|
||||
return log_count(args);
|
||||
}
|
||||
|
||||
|
||||
25
examples/203-pack-as-value.sx
Normal file
25
examples/203-pack-as-value.sx
Normal file
@@ -0,0 +1,25 @@
|
||||
// Step 2.7 — pack-as-value diagnostics. A pack is comptime-only (Decision 1),
|
||||
// so using the bare pack name where a runtime value is required is an error,
|
||||
// with a context-tailored suggestion. All four categories below fire (the
|
||||
// functions are monomorphized when called from main).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Show :: protocol { show :: () -> string; }
|
||||
A :: struct {}
|
||||
impl Show for A { show :: (self: *A) -> string => "A"; }
|
||||
|
||||
sink :: (v: s64) -> void { _ = v; }
|
||||
|
||||
storage :: (..xs: Show) -> void { y := xs; _ = y; } // A: store
|
||||
call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
|
||||
ret :: (..xs: Show) -> s64 { return xs; } // C: return
|
||||
iter :: (..xs: Show) -> void { for xs : (x) { _ = x; } } // D: runtime iterate
|
||||
|
||||
main :: () -> s32 {
|
||||
storage(A.{});
|
||||
call(A.{});
|
||||
_ = ret(A.{});
|
||||
iter(A.{});
|
||||
0;
|
||||
}
|
||||
50
issues/0053-comptime-pack-spread-into-any-slice.md
Normal file
50
issues/0053-comptime-pack-spread-into-any-slice.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Symptom
|
||||
|
||||
Spreading a comptime pack `..$args` into a `[]Any` parameter — `f(..args)` where
|
||||
`f` takes `items: []Any` — fails LLVM verification:
|
||||
|
||||
```
|
||||
LLVM verification failed: Incorrect number of arguments passed to called function!
|
||||
%call = call i64 @log_count(ptr %0, { ptr, i64 }, { ptr, i64 }, double ...)
|
||||
```
|
||||
|
||||
The spread passes the pack's N elements as N separate positional args instead of
|
||||
materialising a single `[]Any` slice for the one `items` parameter.
|
||||
|
||||
# Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
log_count :: (items: []Any) -> s64 { return items.len; }
|
||||
forward :: (..$args) -> s64 { return log_count(..args); }
|
||||
main :: () -> s32 { print("{}\n", forward(1, "hi", 2.5)); return 0; }
|
||||
```
|
||||
|
||||
Expected: `3` (the pack spreads into the `[]Any` slice, like calling
|
||||
`log_count(1, "hi", 2.5)` against a `[]Any` variadic would).
|
||||
|
||||
# Workaround / current advice
|
||||
|
||||
Declare the forwarder as the **slice** variadic instead of a pack — then it's
|
||||
already a runtime `[]Any` and forwards directly (no spread needed):
|
||||
|
||||
```sx
|
||||
forward :: (..args: []Any) -> s64 { return log_count(args); } // works -> 3
|
||||
```
|
||||
|
||||
This is what `examples/162-pack-bare-args.sx` now demonstrates, and what the
|
||||
Step 2.7 pack-as-value diagnostic recommends (declare `..xs: []P` for runtime
|
||||
use rather than spreading a pack).
|
||||
|
||||
# Suspected area
|
||||
|
||||
The call-arg spread lowering (`packSpreadRefs` / `lowerVariadicArgs` /
|
||||
`packVariadicCallArgs` interaction in [src/ir/lower.zig](../src/ir/lower.zig)):
|
||||
when the spread source is a comptime pack and the callee parameter is a single
|
||||
`[]Any` (not itself variadic/pack), the spread must **collect** the pack
|
||||
elements into one `[]Any` slice arg, not splat them as separate positional args.
|
||||
Compare the working path where the callee is itself a `[]Any` variadic.
|
||||
|
||||
# Verification
|
||||
|
||||
The reproduction above should print `3` and pass `sx ir` LLVM verification.
|
||||
22
specs.md
22
specs.md
@@ -1068,13 +1068,23 @@ changes.
|
||||
|
||||
#### Pack as value
|
||||
|
||||
Because a pack has no runtime representation, any expression of pack type in a
|
||||
value-requiring position is a compile error with a tailored suggestion:
|
||||
Because a pack has no runtime representation, using the **bare pack name** where
|
||||
a runtime value is required is a compile error with a context-tailored
|
||||
suggestion:
|
||||
|
||||
- storing/binding it (`let x = xs;`, `self.f = xs;`) → suggest `(..xs)`;
|
||||
- passing to a non-pack-taking call (`f(xs)`) → suggest `..xs`;
|
||||
- returning it (`return xs;`) → suggest a tuple return with `(..xs)`;
|
||||
- iterating at runtime (`for xs : (x)`, `xs[runtime_i]`) → suggest `inline for`.
|
||||
- storing/binding it (`x := xs;`, `self.f = xs;`) → materialize a tuple `(..xs)`;
|
||||
- passing it to a runtime call (`f(xs)`) → declare the parameter as a *slice*
|
||||
variadic `..xs: []P` (a runtime slice) instead of a pack `..xs: P`;
|
||||
- returning it (`return xs;`) → return a tuple `(..xs)` (and make the return
|
||||
type that tuple);
|
||||
- iterating it (`for xs : (x)`, `xs[runtime_i]`) → `inline for 0..xs.len (i)`
|
||||
for a comptime unroll, or take `..xs: []P` for a runtime loop.
|
||||
|
||||
The recurring runtime escape hatch is the **slice-of-protocol variadic**
|
||||
`..xs: []P` (see "Variadic Functions"): it is the runtime, protocol-erased
|
||||
counterpart to the comptime pack. A pack indexed/iterated/forwarded at runtime
|
||||
is almost always better expressed by declaring `xs` as `..xs: []P` in the first
|
||||
place.
|
||||
|
||||
#### Storage and protocol conformance
|
||||
|
||||
|
||||
@@ -1495,6 +1495,17 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
|
||||
if (vd.value) |val| {
|
||||
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
||||
const ph = self.diagPackAsValue(val.data.identifier.name, val.span, .storage);
|
||||
// Bind the name to the placeholder so later uses don't cascade
|
||||
// into a second "unresolved" error after this one.
|
||||
if (self.scope) |scope| {
|
||||
scope.put(vd.name, .{ .ref = ph, .ty = .unresolved, .is_alloca = false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (vd.type_annotation) |ta| {
|
||||
// Explicit type annotation — resolve type first, then lower value
|
||||
const ty = self.resolveType(ta);
|
||||
@@ -1646,6 +1657,12 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void {
|
||||
if (rs.value) |val| {
|
||||
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
||||
_ = self.diagPackAsValue(val.data.identifier.name, val.span, .return_value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Set target_type to function return type so null_literal etc. get the right type.
|
||||
// When inlining a comptime body, the *inlined* fn's declared return type wins
|
||||
// over the caller's — otherwise `return 42` inside a `-> s64` body lowered into
|
||||
@@ -2185,6 +2202,13 @@ pub const Lowering = struct {
|
||||
.undef_literal => self.builder.constUndef(self.target_type orelse .void),
|
||||
|
||||
.identifier => |id| blk: {
|
||||
// A bare pack name in value position has no runtime
|
||||
// representation (Decision 1). Projections (`xs.len`, `xs[i]`,
|
||||
// `xs.value`) are field/index nodes handled elsewhere, so a bare
|
||||
// `xs` reaching here is always a pack-as-value misuse.
|
||||
if (self.isPackName(id.name)) {
|
||||
break :blk self.diagPackAsValue(id.name, node.span, .generic);
|
||||
}
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(id.name)) |binding| {
|
||||
if (binding.is_alloca) {
|
||||
@@ -3223,6 +3247,11 @@ pub const Lowering = struct {
|
||||
if (fe.is_inline) return self.lowerInlineRangeFor(fe, end_node);
|
||||
return self.lowerRuntimeRangeFor(fe, end_node);
|
||||
}
|
||||
// Collection-form `for xs : (x)` over a pack: a pack has no runtime
|
||||
// value to iterate (Decision 1) — point the user at `inline for`.
|
||||
if (fe.iterable.data == .identifier and self.isPackName(fe.iterable.data.identifier.name)) {
|
||||
return self.diagPackAsValue(fe.iterable.data.identifier.name, fe.iterable.span, .runtime_iter);
|
||||
}
|
||||
|
||||
// Lower iterable
|
||||
const iterable = self.lowerExpr(fe.iterable);
|
||||
@@ -4877,6 +4906,31 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
const PackValueKind = enum { storage, call_arg, return_value, runtime_iter, generic };
|
||||
|
||||
/// `xs` is a pack name used where a runtime value is required. A pack is
|
||||
/// comptime-only (Decision 1), so this is an error — with a context-tailored
|
||||
/// suggestion for how to express the intent instead.
|
||||
fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref {
|
||||
if (self.diagnostics) |d| {
|
||||
const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name});
|
||||
switch (kind) {
|
||||
.storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}),
|
||||
.call_arg => d.addHelpFmt(id, span, null, "to pass it on at runtime, declare `{s}` as a slice variadic `..{s}: []P` (a protocol slice) instead of a pack `..{s}: P`", .{ name, name, name }),
|
||||
.return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}),
|
||||
.runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }),
|
||||
.generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or declare `{s}` as a slice variadic `..{s}: []P` for runtime use instead of a pack `..{s}: P`", .{ name, name, name, name }),
|
||||
}
|
||||
}
|
||||
return self.emitPlaceholder(name);
|
||||
}
|
||||
|
||||
/// True when `name` is a pack parameter bound in the current mono body.
|
||||
fn isPackName(self: *Lowering, name: []const u8) bool {
|
||||
const ppc = self.pack_param_count orelse return false;
|
||||
return ppc.contains(name);
|
||||
}
|
||||
|
||||
fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref {
|
||||
const obj = self.lowerExpr(se.object);
|
||||
const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64);
|
||||
|
||||
1
tests/expected/203-pack-as-value.exit
Normal file
1
tests/expected/203-pack-as-value.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
43
tests/expected/203-pack-as-value.txt
Normal file
43
tests/expected/203-pack-as-value.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
|
||||
--> /Users/agra/projects/sx/examples/203-pack-as-value.sx:14:40
|
||||
|
|
||||
14 | storage :: (..xs: Show) -> void { y := xs; _ = y; } // A: store
|
||||
| ^^
|
||||
|
||||
help: to store it, materialize a tuple: `(..xs)`
|
||||
|
|
||||
14 | storage :: (..xs: Show) -> void { y := xs; _ = y; } // A: store
|
||||
| ^^
|
||||
|
||||
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
|
||||
--> /Users/agra/projects/sx/examples/203-pack-as-value.sx:15:40
|
||||
|
|
||||
15 | call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
|
||||
| ^^
|
||||
|
||||
help: materialize a tuple `(..xs)` to store it, or declare `xs` as a slice variadic `..xs: []P` for runtime use instead of a pack `..xs: P`
|
||||
|
|
||||
15 | call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
|
||||
| ^^
|
||||
|
||||
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
|
||||
--> /Users/agra/projects/sx/examples/203-pack-as-value.sx:16:42
|
||||
|
|
||||
16 | ret :: (..xs: Show) -> s64 { return xs; } // C: return
|
||||
| ^^
|
||||
|
||||
help: to return it, return a tuple `(..xs)` and make the return type that tuple
|
||||
|
|
||||
16 | ret :: (..xs: Show) -> s64 { return xs; } // C: return
|
||||
| ^^
|
||||
|
||||
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
|
||||
--> /Users/agra/projects/sx/examples/203-pack-as-value.sx:17:39
|
||||
|
|
||||
17 | iter :: (..xs: Show) -> void { for xs : (x) { _ = x; } } // D: runtime iterate
|
||||
| ^^
|
||||
|
||||
help: to iterate at comptime use `inline for 0..xs.len (i)`; for a runtime loop declare it as `..xs: []P` (a protocol slice) instead of a pack
|
||||
|
|
||||
17 | iter :: (..xs: Show) -> void { for xs : (x) { _ = x; } } // D: runtime iterate
|
||||
| ^^
|
||||
Reference in New Issue
Block a user