diff --git a/examples/205-generic-struct-pack-field.sx b/examples/205-generic-struct-pack-field.sx new file mode 100644 index 0000000..5951148 --- /dev/null +++ b/examples/205-generic-struct-pack-field.sx @@ -0,0 +1,25 @@ +// Phase 4.2 (core) — a generic struct with a pack type-param `..$Ts: []Type` +// and a pack-shaped tuple field `(..$Ts)`. Each instantiation binds the +// remaining type args as the pack, so the field is a tuple of those per-position +// types. Storing the whole tuple field and reading its elements both work. + +#import "modules/std.sx"; + +Box :: struct($R: Type, ..$Ts: []Type) { + r: $R; + pair: (..$Ts); // tuple of the pack's element types +} + +main :: () -> s32 { + // Box(s64, s32, string): R=s64, Ts=[s32, string], pair: (s32, string). + a : Box(s64, s32, string) = ---; + a.r = 7; + a.pair = (42, "hi"); // whole-tuple field store + print("a: r={} 0={} 1={}\n", a.r, a.pair.0, a.pair.1); + + // A different shape → a different per-position tuple field. + b : Box(bool, string, bool) = ---; // Ts=[string, bool], pair: (string, bool) + b.pair = ("x", true); + print("b: 0={} 1={}\n", b.pair.0, b.pair.1); + 0; +} diff --git a/src/ast.zig b/src/ast.zig index 0323c19..25c75ea 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -328,6 +328,9 @@ pub const StructTypeParam = struct { name: []const u8, // e.g. "N" or "T" (without $) constraint: *Node, // type_expr: "u32" for value param, "Type" for type param protocol_constraints: []const []const u8 = &.{}, // e.g. ["Eq", "Hashable"] for $T/Eq/Hashable + /// `..$Ts: []Type` — a pack type-param binding the remaining type args as a + /// sequence (must be last). Field types reference it via `(..$Ts)` etc. + is_variadic: bool = false, }; pub const UsingEntry = struct { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index be5a209..7a9a799 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -264,6 +264,7 @@ pub const Lowering = struct { const TemplateParam = struct { name: []const u8, is_type_param: bool, // true for $T: Type, false for $N: u32 + is_variadic: bool = false, // `..$Ts: []Type` — binds remaining type args as a pack }; const GlobalInfo = struct { id: inst_mod.GlobalId, ty: TypeId }; @@ -11116,6 +11117,36 @@ pub const Lowering = struct { .tuple_type_expr => |tt| { return self.resolveTupleTypeWithBindings(&tt); }, + // `(..$Ts)` in a type position (e.g. a struct field) parses as a + // tuple LITERAL whose elements include a pack spread; expand it to + // the bound pack's element types, same as `resolveTupleTypeWithBindings`. + .tuple_literal => |tl| { + var any_spread = false; + for (tl.elements) |el| { + if (el.value.data == .spread_expr) { + any_spread = true; + break; + } + } + if (any_spread) { + var field_ids = std.ArrayList(TypeId).empty; + defer field_ids.deinit(self.alloc); + for (tl.elements) |el| { + if (el.value.data == .spread_expr) { + if (self.packTypeElems(el.value.data.spread_expr.operand)) |elems| { + defer self.alloc.free(elems); + for (elems) |e| field_ids.append(self.alloc, e) catch return .void; + continue; + } + } + field_ids.append(self.alloc, self.resolveTypeWithBindings(el.value)) catch return .void; + } + return self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, field_ids.items) catch return .void, + .names = null, + } }); + } + }, else => {}, } // Alias resolution (`ShaderHandle :: u32`, `Vec4 :: @@ -11367,11 +11398,28 @@ pub const Lowering = struct { // Bind type params to args and build name suffix const saved_type_bindings = self.type_bindings; const saved_value_bindings = self.comptime_value_bindings; + const saved_pack_bindings = self.pack_bindings; + const saved_pack_arg_types = self.pack_arg_types; var tb = std.StringHashMap(TypeId).init(self.alloc); var cvb = std.StringHashMap(i64).init(self.alloc); + var pb = std.StringHashMap([]const TypeId).init(self.alloc); for (tmpl.type_params, 0..) |tp, i| { if (i >= args.len) break; + + // `..$Ts: []Type` — bind the REMAINING args as a type pack. + if (tp.is_variadic) { + var pack_tys = std.ArrayList(TypeId).empty; + for (args[i..]) |a| { + const ty = self.resolveTypeWithBindings(a); + pack_tys.append(self.alloc, ty) catch {}; + name_parts.appendSlice(self.alloc, "__") catch {}; + name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {}; + } + pb.put(tp.name, pack_tys.toOwnedSlice(self.alloc) catch &.{}) catch {}; + break; // a pack param is always last + } + name_parts.appendSlice(self.alloc, "__") catch {}; if (tp.is_type_param) { @@ -11404,9 +11452,12 @@ pub const Lowering = struct { } } - // Set up bindings and resolve fields + // Set up bindings and resolve fields. `pack_bindings` makes a + // pack-shaped field type like `(..$Ts)` resolve to the bound type list. self.type_bindings = tb; self.comptime_value_bindings = cvb; + self.pack_bindings = pb; + self.pack_arg_types = pb; var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| { @@ -11420,6 +11471,8 @@ pub const Lowering = struct { // Restore bindings self.type_bindings = saved_type_bindings; self.comptime_value_bindings = saved_value_bindings; + self.pack_bindings = saved_pack_bindings; + self.pack_arg_types = saved_pack_arg_types; // Register the monomorphized struct const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; @@ -11646,15 +11699,17 @@ pub const Lowering = struct { for (sd.type_params, 0..) |tp, i| { tps[i] = .{ .name = self.alloc.dupe(u8, tp.name) catch return, - // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params - // Only value params like $N: u32 are non-type - .is_type_param = if (tp.constraint.data == .type_expr) blk: { + // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params. + // `..$Ts: []Type` (variadic) is a type-pack param. Only value + // params like $N: u32 are non-type. + .is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: { const cname = tp.constraint.data.type_expr.name; // "Type" or a protocol name → type param break :blk std.mem.eql(u8, cname, "Type") or self.protocol_decl_map.contains(cname) or self.protocol_ast_map.contains(cname); - } else false, + } else false), + .is_variadic = tp.is_variadic, }; } diff --git a/src/parser.zig b/src/parser.zig index a8f3ccd..b8fbf7d 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -852,6 +852,13 @@ pub const Parser = struct { try self.expect(.comma); if (self.current.tag == .r_paren) break; } + // Optional leading `..` — a pack type-param `..$Ts: []Type` + // (must be the last param; binds the remaining type args). + var is_variadic = false; + if (self.current.tag == .dot_dot) { + is_variadic = true; + self.advance(); + } // Expect $name : constraint try self.expect(.dollar); if (self.current.tag != .identifier) { @@ -874,7 +881,7 @@ pub const Parser = struct { } } const pc = try pc_list.toOwnedSlice(self.allocator); - try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint, .protocol_constraints = pc }); + try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint, .protocol_constraints = pc, .is_variadic = is_variadic }); } try self.expect(.r_paren); } diff --git a/tests/expected/205-generic-struct-pack-field.exit b/tests/expected/205-generic-struct-pack-field.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/205-generic-struct-pack-field.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/205-generic-struct-pack-field.txt b/tests/expected/205-generic-struct-pack-field.txt new file mode 100644 index 0000000..3247df4 --- /dev/null +++ b/tests/expected/205-generic-struct-pack-field.txt @@ -0,0 +1,2 @@ +a: r=7 0=42 1=hi +b: 0=x 1=true