lang 1.2: parse pack-expansion forms in all four positions

Pack/tuple spread now parses in tuple-value `(..xs)` / `(..xs.field)`,
tuple-type `(..F(Ts))` / `(..F(Ts.Arg))`, call-arg `f(..xs)` (already),
and closure-sig `Closure(..Ts)` / `Closure(..sources.T)` positions.

Design: the uniform spread node is the existing `spread_expr` (its
operand sub-expression carries the projection `xs.field` and
type-application `F(Ts)` shapes) rather than a new PackExpansion node —
call-arg slice-spread (`..arr`) and pack-spread (`..pack`) are
syntactically identical, so they must share one node, and spread_expr
already serves it with working slice lowering. Closure-sig packs gain
`ClosureTypeExpr.pack_projection` alongside the existing `pack_name`.

Parser-only; sema/lowering land in Phase 2. 6 new parser unit tests +
examples/probes/pack-expansion-parses.sx. Build + 225-suite green.
This commit is contained in:
agra
2026-05-29 12:33:27 +03:00
parent 87f739cef2
commit 98526ab9b4
3 changed files with 161 additions and 1 deletions

View File

@@ -515,6 +515,19 @@ pub const Parser = struct {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
}
// Pack expansion in a tuple/function type: `(..F(Ts))` /
// `(..F(Ts.Arg))` / `(..Ts)`. Reuses `spread_expr`; its operand
// is the per-element type expression (e.g. `F(Ts)`), carrying any
// projection in `Ts.Arg` form.
if (self.current.tag == .dot_dot) {
const spread_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseTypeExpr();
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
try param_names.append(self.allocator, null);
try param_types.append(self.allocator, spread);
continue;
}
// Check for optional param name: `name: Type`
// An identifier followed by `:` (not `::` or `:=`) is a param name
if (self.isIdentLike() and self.peekNext() == .colon) {
@@ -580,12 +593,13 @@ pub const Parser = struct {
var param_names = std.ArrayList(?[]const u8).empty;
var has_names = false;
var pack_name: ?[]const u8 = null;
var pack_projection: ?[]const u8 = null;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
}
// Trailing pack marker: `..$name` (terminal only).
// Trailing pack marker: `..$name` or `..pack.Arg` (terminal only).
if (self.current.tag == .dot_dot) {
self.advance(); // skip '..'
if (self.current.tag == .dollar) self.advance(); // optional sigil
@@ -594,6 +608,15 @@ pub const Parser = struct {
}
pack_name = self.tokenSlice(self.current);
self.advance();
// Optional projection: `..sources.T` picks a type-arg per element.
if (self.current.tag == .dot) {
self.advance(); // skip '.'
if (!self.isIdentLike()) {
return self.fail("expected projection name after '.' in Closure pack");
}
pack_projection = self.tokenSlice(self.current);
self.advance();
}
// Pack must be the LAST item — only `)` accepted next.
if (self.current.tag != .r_paren) {
return self.fail("variadic pack must be the last parameter in Closure type");
@@ -623,6 +646,7 @@ pub const Parser = struct {
.param_names = if (has_names) try param_names.toOwnedSlice(self.allocator) else null,
.return_type = return_type,
.pack_name = pack_name,
.pack_projection = pack_projection,
} });
}
@@ -2446,6 +2470,17 @@ pub const Parser = struct {
return try self.createNode(start, .{ .tuple_literal = .{ .elements = &.{} } });
}
// Leading pack/tuple spread: `(..xs)` / `(..xs.field)` materializes
// a tuple from a pack. The spread reuses `spread_expr`; its operand
// carries the projection (`xs.field`) shape.
if (self.current.tag == .dot_dot) {
const spread_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseExpr();
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
return self.finishTupleAfterFirst(start, spread);
}
const first = try self.parseExpr();
// Check for comma → tuple
@@ -2889,6 +2924,15 @@ pub const Parser = struct {
while (self.current.tag == .comma) {
self.advance(); // skip ','
if (self.current.tag == .r_paren) break; // trailing comma: (42,)
// Spread element: `(a, ..xs, b)` — reuses `spread_expr`.
if (self.current.tag == .dot_dot) {
const spread_start = self.current.loc.start;
self.advance(); // skip '..'
const operand = try self.parseExpr();
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
try elements.append(self.allocator, .{ .name = null, .value = spread });
continue;
}
const value = try self.parseExpr();
try elements.append(self.allocator, .{ .name = null, .value = value });
}
@@ -3584,3 +3628,92 @@ test "parse comptime type-pack is NOT a protocol pack (..$args)" {
try std.testing.expect(p.is_comptime); // comptime type pack
try std.testing.expect(!p.is_pack); // not the protocol-constrained form
}
// ── Step 1.2 — pack expansion in the four positions ───────────────────
// All spread forms reuse `spread_expr` (its operand carries any projection /
// type-application); closure-sig packs use ClosureTypeExpr.pack_name +
// pack_projection. Arrow bodies wrap the expression in a block.
test "parse pack expansion: tuple value (..xs)" {
const source = "f :: () => (..xs);";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const body = root.data.root.decls[0].data.fn_decl.body;
const tup = body.data.block.stmts[0];
try std.testing.expect(tup.data == .tuple_literal);
try std.testing.expectEqual(@as(usize, 1), tup.data.tuple_literal.elements.len);
const el = tup.data.tuple_literal.elements[0].value;
try std.testing.expect(el.data == .spread_expr);
try std.testing.expect(el.data.spread_expr.operand.data == .identifier);
try std.testing.expectEqualStrings("xs", el.data.spread_expr.operand.data.identifier.name);
}
test "parse pack expansion: tuple value projection (..xs.value)" {
const source = "f :: () => (..xs.value);";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const tup = root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0];
const el = tup.data.tuple_literal.elements[0].value;
try std.testing.expect(el.data == .spread_expr);
const op = el.data.spread_expr.operand;
try std.testing.expect(op.data == .field_access);
try std.testing.expectEqualStrings("value", op.data.field_access.field);
try std.testing.expect(op.data.field_access.object.data == .identifier);
try std.testing.expectEqualStrings("xs", op.data.field_access.object.data.identifier.name);
}
test "parse pack expansion: tuple type (..F(Ts))" {
const source = "g :: (x: (..F(Ts))) => x;";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const ty = root.data.root.decls[0].data.fn_decl.params[0].type_expr;
try std.testing.expect(ty.data == .tuple_type_expr);
try std.testing.expectEqual(@as(usize, 1), ty.data.tuple_type_expr.field_types.len);
const field = ty.data.tuple_type_expr.field_types[0];
try std.testing.expect(field.data == .spread_expr);
const op = field.data.spread_expr.operand;
try std.testing.expect(op.data == .parameterized_type_expr);
try std.testing.expectEqualStrings("F", op.data.parameterized_type_expr.name);
}
test "parse pack expansion: closure sig projection Closure(..sources.T)" {
const source = "h :: (cb: Closure(..sources.T) -> s32) => cb;";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const ty = root.data.root.decls[0].data.fn_decl.params[0].type_expr;
try std.testing.expect(ty.data == .closure_type_expr);
try std.testing.expectEqualStrings("sources", ty.data.closure_type_expr.pack_name.?);
try std.testing.expectEqualStrings("T", ty.data.closure_type_expr.pack_projection.?);
}
test "parse closure sig bare pack Closure(..Ts) has no projection" {
const source = "j :: (cb: Closure(..Ts) -> s32) => cb;";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const ty = root.data.root.decls[0].data.fn_decl.params[0].type_expr;
try std.testing.expect(ty.data == .closure_type_expr);
try std.testing.expectEqualStrings("Ts", ty.data.closure_type_expr.pack_name.?);
try std.testing.expect(ty.data.closure_type_expr.pack_projection == null);
}
test "parse pack expansion: call-arg spread q(..xs) reuses spread_expr" {
const source = "k :: () => q(..xs);";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const call = root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0];
try std.testing.expect(call.data == .call);
try std.testing.expectEqual(@as(usize, 1), call.data.call.args.len);
try std.testing.expect(call.data.call.args[0].data == .spread_expr);
}