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:
agra
2026-05-30 02:09:41 +03:00
parent ab572359ae
commit 8a875d354c
7 changed files with 197 additions and 20 deletions

View File

@@ -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);