lang F1 6: (..sources) materializes a pack into a protocol-typed tuple field

lowerTupleLiteral now coerces/erases each spliced spread element to the
contextual target tuple field type (computed even when a spread is present,
indexed by output position). New coerceOrErase: protocol target -> xx-erase
via buildProtocolErasure, else coerceToType. So c.sources = (..sources) on a
(..VL(Ts)) field erases each concrete pack element to its VL(Ti) slot.

examples/210 (build(IntCell, StrCell) -> 10 hi). 245 examples + unit green.
This commit is contained in:
agra
2026-05-30 03:11:55 +03:00
parent e395a08331
commit 87ee3d3e65
4 changed files with 79 additions and 13 deletions

View File

@@ -0,0 +1,28 @@
// Phase 6 — `c.sources = (..sources)`: materialize a pack into a
// protocol-typed tuple field, erasing each concrete pack element to the field's
// protocol slot. The pack `..sources: VL` holds concrete cells; `(..sources)`
// into a `(..VL(Ts))` field `xx`-erases each to its `VL(Ti)` value.
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
IntCell :: struct { v: s64; }
StrCell :: struct { s: string; }
impl VL(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; }
impl VL(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..VL(Ts));
value: $R;
}
build :: (..sources: VL) -> void {
c : Combined(s64, ..sources.T) = ---;
c.sources = (..sources); // pack → tuple, per-element erase
print("{} {}\n", c.sources.0.get(), c.sources.1.get());
}
main :: () -> s32 {
build(IntCell.{ v = 10 }, StrCell.{ s = "hi" }); // 10 hi
0;
}

View File

@@ -5058,28 +5058,49 @@ pub const Lowering = struct {
if (elem.value.data == .spread_expr) has_spread = true;
}
// Contextual target tuple field types. Without a spread we require
// exact arity (existing behavior); with a spread we index positionally
// by output position (so `(..sources)` into a `(VL(T0), …)` field coerces
// / erases each spliced element to its slot's type).
var target_fields: ?[]const TypeId = null;
if (!has_spread) {
if (self.target_type) |tt| {
if (!tt.isBuiltin()) {
const tinfo = self.module.types.get(tt);
if (tinfo == .tuple and tinfo.tuple.fields.len == tl.elements.len) {
target_fields = tinfo.tuple.fields;
}
if (self.target_type) |tt| {
if (!tt.isBuiltin()) {
const tinfo = self.module.types.get(tt);
if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) {
target_fields = tinfo.tuple.fields;
}
}
}
const saved_target = self.target_type;
for (tl.elements, 0..) |elem, i| {
var out_idx: usize = 0;
for (tl.elements) |elem| {
// Pack-spread element → splice its per-element values as fields.
if (elem.value.data == .spread_expr) {
if (self.packSpreadRefs(elem.value.data.spread_expr.operand, elem.value.span)) |refs| {
const sp_operand = elem.value.data.spread_expr.operand;
if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| {
defer self.alloc.free(refs);
for (refs) |r| {
elems.append(self.alloc, r) catch unreachable;
field_type_ids.append(self.alloc, self.builder.getRefType(r)) catch unreachable;
// Element AST nodes (for protocol-erasure lvalue/name fallback)
// when the spread is a bare pack name.
const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null)
self.pack_arg_nodes.?.get(sp_operand.data.identifier.name)
else
null;
for (refs, 0..) |r, ri| {
var val = r;
var vty = self.builder.getRefType(r);
if (target_fields) |tf| {
if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) {
const want = tf[out_idx];
const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value;
val = self.coerceOrErase(r, vty, want, node);
vty = want;
}
}
elems.append(self.alloc, val) catch unreachable;
field_type_ids.append(self.alloc, vty) catch unreachable;
name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable;
out_idx += 1;
}
continue;
}
@@ -5087,7 +5108,7 @@ pub const Lowering = struct {
_ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic
continue;
}
const field_ty = if (target_fields) |tf| tf[i] else self.inferExprType(elem.value);
const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value);
self.target_type = field_ty;
var val = self.lowerExpr(elem.value);
self.target_type = saved_target;
@@ -5103,6 +5124,7 @@ pub const Lowering = struct {
} else {
name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable;
}
out_idx += 1;
}
// Reuse the contextual target tuple type when it drove lowering so the
@@ -14018,6 +14040,20 @@ pub const Lowering = struct {
}
/// Build a protocol value from a concrete value via xx conversion.
/// Coerce `val` (type `src`) to `dst`: if `dst` is a protocol, `xx`-erase
/// the concrete value into it; otherwise fall back to numeric/struct
/// coercion. Used to materialize a pack into a protocol-typed tuple field.
fn coerceOrErase(self: *Lowering, val: Ref, src: TypeId, dst: TypeId, node: *const Node) Ref {
if (src == dst) return val;
if (!dst.isBuiltin()) {
const di = self.module.types.get(dst);
if (di == .@"struct" and di.@"struct".is_protocol) {
return self.buildProtocolErasure(val, node, src, dst);
}
}
return self.coerceToType(val, src, dst);
}
fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref {
const dst_info = self.module.types.get(dst_ty);
if (dst_info != .@"struct") return operand;

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
10 hi