diff --git a/examples/199-pack-type-projection.sx b/examples/199-pack-type-projection.sx new file mode 100644 index 0000000..5b05835 --- /dev/null +++ b/examples/199-pack-type-projection.sx @@ -0,0 +1,38 @@ +// Feature 1 — TYPE-position pack projection `xs.T`. The per-element protocol +// type-arg `T` projects into a Pack of types, usable in type/signature +// positions: a tuple type `(..xs.T)` and a closure signature +// `Closure(..xs.T) -> R`. (`T` of each element comes from its +// `impl Box(T) for `.) + +#import "modules/std.sx"; + +Box :: protocol(T: Type) { + get :: () -> T; +} + +IntCell :: struct { v: s64; } +StrCell :: struct { s: string; } +Dbl :: struct { n: s64; } +impl Box(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; } +impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } +impl Box(s64) for Dbl { get :: (self: *Dbl) -> s64 => self.n * 2; } + +// Tuple type `(..xs.T)` — heterogeneous (s64, string), matched by the +// value-projection `(..xs.get)`. +snap :: (..xs: Box) -> void { + t : (..xs.T) = (..xs.get); + print("0={} 1={}\n", t.0, t.1); +} + +// Closure signature `Closure(..xs.T) -> s64` — here `Closure(s64, s64) -> s64`. +// The closure literal's params are contextually typed from the projection. +fold :: (..xs: Box) -> s64 { + cb : Closure(..xs.T) -> s64 = (a, b) => a + b; + return cb(xs[0].get(), xs[1].get()); +} + +main :: () -> s32 { + snap(IntCell.{ v = 42 }, StrCell.{ s = "hi" }); // (s64, string) + print("fold={}\n", fold(IntCell.{ v = 10 }, Dbl.{ n = 5 })); // 10 + 10 = 20 + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 57951ca..5a9673d 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -10732,6 +10732,9 @@ pub const Lowering = struct { .function_type_expr => |ft| { return self.resolveFunctionTypeWithBindings(&ft); }, + .tuple_type_expr => |tt| { + return self.resolveTupleTypeWithBindings(&tt); + }, else => {}, } // Alias resolution (`ShaderHandle :: u32`, `Vec4 :: @@ -10752,6 +10755,14 @@ pub const Lowering = struct { param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void; } if (ct.pack_name) |pn| { + // Protocol pack (`Closure(..sources.T)` / `Closure(..sources)`): + // expand the bound pack's per-element type-args. + if (self.packTypeArgs(pn, ct.pack_projection)) |elems| { + defer self.alloc.free(elems); + for (elems) |t| param_ids.append(self.alloc, t) catch return .void; + const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; + return self.module.types.closureType(param_ids.items, ret_ty); + } if (self.pack_bindings) |pb| { if (pb.get(pn)) |pack_tys| { for (pack_tys) |t| param_ids.append(self.alloc, t) catch return .void; @@ -10770,6 +10781,96 @@ pub const Lowering = struct { return self.module.types.closureType(param_ids.items, ret_ty); } + /// Resolve a tuple type expression with active pack bindings: a spread field + /// `(..xs)` / `(..xs.T)` expands to the pack's per-element types via + /// `packTypeElems`. Non-spread fields resolve normally. + fn resolveTupleTypeWithBindings(self: *Lowering, tt: *const ast.TupleTypeExpr) TypeId { + var field_ids = std.ArrayList(TypeId).empty; + defer field_ids.deinit(self.alloc); + for (tt.field_types) |ft| { + if (ft.data == .spread_expr) { + if (self.packTypeElems(ft.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(ft)) catch return .void; + } + return self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, field_ids.items) catch return .void, + .names = null, + } }); + } + + /// TYPE-position pack expansion: given a spread operand, return the + /// per-element types. `..xs` → the pack's element types (`pack_arg_types`). + /// `..xs.T` → each element's protocol type-arg `T` (from its + /// `impl P(args) for elem` in `param_impl_map`). Null when not a pack spread. + /// Caller owns the returned slice. + fn packTypeElems(self: *Lowering, operand: *const Node) ?[]TypeId { + if (self.pack_arg_types == null) return null; + // In type position `xs` / `xs.T` parse to a (possibly dotted) type_expr + // name; `field_access` covers any value-shaped form. + var pack_name: []const u8 = ""; + var projection: ?[]const u8 = null; + switch (operand.data) { + .type_expr, .identifier => { + const full = if (operand.data == .type_expr) operand.data.type_expr.name else operand.data.identifier.name; + if (std.mem.indexOfScalar(u8, full, '.')) |dot| { + pack_name = full[0..dot]; + projection = full[dot + 1 ..]; + } else { + pack_name = full; + } + }, + .field_access => |fa| { + pack_name = switch (fa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => return null, + }; + projection = fa.field; + }, + else => return null, + } + return self.packTypeArgs(pack_name, projection); + } + + /// Per-element types for a bound protocol pack: `pack_name` alone → the + /// element types; with `projection` (`xs.T`) → each element's protocol + /// type-arg. Null when `pack_name` isn't a bound pack. Caller owns the slice. + fn packTypeArgs(self: *Lowering, pack_name: []const u8, projection: ?[]const u8) ?[]TypeId { + const pat = self.pack_arg_types orelse return null; + const elems = pat.get(pack_name) orelse return null; + if (projection == null) return self.alloc.dupe(TypeId, elems) catch null; + const proto = if (self.pack_constraint) |pc| (pc.get(pack_name) orelse return null) else return null; + const arg_idx = self.lookupProtocolArg(proto, projection.?) orelse return null; + var out = std.ArrayList(TypeId).empty; + for (elems) |elem| { + out.append(self.alloc, self.elementProtocolTypeArg(proto, elem, arg_idx) orelse .void) catch return null; + } + return out.toOwnedSlice(self.alloc) catch null; + } + + /// For a concrete `elem` conforming to parameterised `proto`, return the + /// `arg_idx`-th protocol type-arg from its `impl proto(args) for elem` + /// (scans `param_impl_map` for `proto\x00…\x00mangle(elem)`). + fn elementProtocolTypeArg(self: *Lowering, proto: []const u8, elem: TypeId, arg_idx: u32) ?TypeId { + const prefix = std.fmt.allocPrint(self.alloc, "{s}\x00", .{proto}) catch return null; + const suffix = std.fmt.allocPrint(self.alloc, "\x00{s}", .{self.mangleTypeName(elem)}) catch return null; + var it = self.param_impl_map.iterator(); + while (it.next()) |entry| { + const k = entry.key_ptr.*; + if (std.mem.startsWith(u8, k, prefix) and std.mem.endsWith(u8, k, suffix)) { + for (entry.value_ptr.items) |impl| { + if (arg_idx < impl.target_args.len) return impl.target_args[arg_idx]; + } + } + } + return null; + } + /// Resolve a `(Params...) -> Ret` function type expression with the /// active type/pack bindings applied. Mirrors /// `resolveClosureTypeWithBindings` but for `function_type_expr`. diff --git a/tests/expected/199-pack-type-projection.exit b/tests/expected/199-pack-type-projection.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/199-pack-type-projection.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/199-pack-type-projection.txt b/tests/expected/199-pack-type-projection.txt new file mode 100644 index 0000000..e0d1584 --- /dev/null +++ b/tests/expected/199-pack-type-projection.txt @@ -0,0 +1,2 @@ +0=42 1=hi +fold=20