diff --git a/examples/probes/pack-expansion-parses.sx b/examples/probes/pack-expansion-parses.sx new file mode 100644 index 0000000..3c97b05 --- /dev/null +++ b/examples/probes/pack-expansion-parses.sx @@ -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; diff --git a/src/ast.zig b/src/ast.zig index 7cc0f09..51f5fc8 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -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 { diff --git a/src/parser.zig b/src/parser.zig index bc6ebbe..e47e530 100644 --- a/src/parser.zig +++ b/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); +}