diff --git a/examples/162-pack-bare-args.sx b/examples/162-pack-bare-args.sx index d97e577..07ed348 100644 --- a/examples/162-pack-bare-args.sx +++ b/examples/162-pack-bare-args.sx @@ -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); } diff --git a/examples/203-pack-as-value.sx b/examples/203-pack-as-value.sx new file mode 100644 index 0000000..11e52ad --- /dev/null +++ b/examples/203-pack-as-value.sx @@ -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; +} diff --git a/issues/0053-comptime-pack-spread-into-any-slice.md b/issues/0053-comptime-pack-spread-into-any-slice.md new file mode 100644 index 0000000..60874a1 --- /dev/null +++ b/issues/0053-comptime-pack-spread-into-any-slice.md @@ -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. diff --git a/specs.md b/specs.md index fb52f47..07c7a90 100644 --- a/specs.md +++ b/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 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d41cae6..1666289 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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); diff --git a/tests/expected/203-pack-as-value.exit b/tests/expected/203-pack-as-value.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/203-pack-as-value.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/203-pack-as-value.txt b/tests/expected/203-pack-as-value.txt new file mode 100644 index 0000000..6825693 --- /dev/null +++ b/tests/expected/203-pack-as-value.txt @@ -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 + | ^^