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:
24
examples/probes/pack-expansion-parses.sx
Normal file
24
examples/probes/pack-expansion-parses.sx
Normal file
@@ -0,0 +1,24 @@
|
||||
// Feature 1 / Step 1.2 — pack-expansion forms PARSE in all four positions.
|
||||
//
|
||||
// Parse-only probe. Spread reuses the existing `spread_expr` node (its operand
|
||||
// carries projection `xs.field` / type-application `F(Ts)`); closure-sig packs
|
||||
// use `ClosureTypeExpr.pack_name` + the new `pack_projection`. Sema/lowering
|
||||
// arrive in Phase 2 — do NOT expect this to compile/run yet. The authoritative
|
||||
// checks are the parser unit tests in src/parser.zig ("parse pack expansion: …").
|
||||
|
||||
// 1. Tuple value position — `(..pack)` / `(..pack.field)`:
|
||||
tv1 :: () => (..xs);
|
||||
tv2 :: () => (..xs.value);
|
||||
tv3 :: () => (a, ..xs, b); // mixed positional + spread
|
||||
|
||||
// 2. Tuple type position — `(..F(Ts))` / `(..F(Ts.Arg))`:
|
||||
tt1 :: (x: (..ValueListenable(Ts))) => x;
|
||||
tt2 :: (x: (..ValueListenable(Ts.Arg))) => x;
|
||||
|
||||
// 3. Call-arg position — `..pack` / `..pack.field` (reuses spread_expr):
|
||||
ca1 :: () => f(..xs);
|
||||
ca2 :: () => f(..xs.value);
|
||||
|
||||
// 4. Closure-sig position — `Closure(..Ts)` / `Closure(..Ts.Arg)`:
|
||||
cs1 :: (cb: Closure(..Ts) -> s32) => cb;
|
||||
cs2 :: (cb: Closure(..sources.T) -> s32) => cb;
|
||||
@@ -522,6 +522,9 @@ pub const ClosureTypeExpr = struct {
|
||||
/// `Closure(..$args) -> R` ⇒ pack_name = "args", param_types = [].
|
||||
/// `Closure(Prefix, ..$args)` ⇒ pack_name = "args", param_types = [Prefix].
|
||||
pack_name: ?[]const u8 = null,
|
||||
/// Projection on the pack: `Closure(..sources.T) -> R` ⇒ pack_name =
|
||||
/// "sources", pack_projection = "T". Null for a bare `..pack`.
|
||||
pack_projection: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const TupleTypeExpr = struct {
|
||||
|
||||
135
src/parser.zig
135
src/parser.zig
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user