lang F1 4.2 (core): generic struct pack type-param + (..$Ts) tuple field

A generic struct can take a pack type-param ..$Ts: []Type that binds the
remaining type args as a sequence, and a pack-shaped tuple field (..$Ts)
resolves to a tuple of those per-position types.

- parser/ast: accept a leading .. on a struct generic param; StructTypeParam
  gains is_variadic.
- registration: TemplateParam carries is_variadic (and is a type param).
- instantiateGenericStruct: a variadic type-param consumes the remaining args
  into pack_bindings + pack_arg_types (mangled into the name); restored after.
- resolveTypeWithBindings: a tuple-literal-as-type containing a pack spread
  (e.g. (..$Ts)) expands via packTypeElems.

Instantiate + correct per-position field types + whole-tuple store + element
read all work (examples/205). Not yet: protocol-applied field (..F(Ts)) (the
canonical (..VL(Ts)) shape) and nested element assignment b.pair.0 = v.
240 examples + unit green.
This commit is contained in:
agra
2026-05-30 02:30:49 +03:00
parent 82b46bc412
commit b48766d153
6 changed files with 99 additions and 6 deletions

View File

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

View File

@@ -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 {

View File

@@ -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,
};
}

View File

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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
a: r=7 0=42 1=hi
b: 0=x 1=true