lang 2.3: TYPE-position pack projection xs.T (tuple type + closure sig)

`xs.T` projects each pack element's protocol type-arg into a type list, usable
in TYPE/signature positions:
- tuple type `(..xs.T)` → e.g. `(s64, string)` (new resolveTupleTypeWithBindings)
- closure sig `Closure(..xs.T) -> R` → e.g. `Closure(s64, s64) -> s64`, which
  contextually types a closure literal (resolveClosureTypeWithBindings now
  expands a protocol pack via packTypeArgs).

Wired `tuple_type_expr` into `resolveTypeWithBindings` (type_bridge's tuple
resolver is stateless — can't see packs). `packTypeArgs(pack_name, projection)`
is shared: bare `..xs` → element types (`pack_arg_types`); `..xs.T` → each
element's `impl Box(args) for elem` target_arg (`elementProtocolTypeArg` scans
`param_impl_map`). In type position `xs.T` parses as a dotted `type_expr`, so
packTypeElems splits on '.'. examples/199-pack-type-projection.sx.

This completes 2.3's core: all spread/projection forms — call-arg, tuple value,
tuple type, closure sig — now lower. The canonical's `Closure(..sources.T)` /
`mapper(..sources.value)` / `(..sources)` shapes are functional.
This commit is contained in:
agra
2026-05-29 20:39:57 +03:00
parent 72731f97ee
commit 27fd5e1e6a
4 changed files with 142 additions and 0 deletions

View File

@@ -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 <elem>`.)
#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;
}

View File

@@ -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`.

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
0=42 1=hi
fold=20