From 87ee3d3e65bd5205f39884380c51418498f3bfd3 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 30 May 2026 03:11:55 +0300 Subject: [PATCH] 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. --- examples/210-pack-to-protocol-tuple.sx | 28 +++++++++ src/ir/lower.zig | 62 +++++++++++++++---- .../expected/210-pack-to-protocol-tuple.exit | 1 + tests/expected/210-pack-to-protocol-tuple.txt | 1 + 4 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 examples/210-pack-to-protocol-tuple.sx create mode 100644 tests/expected/210-pack-to-protocol-tuple.exit create mode 100644 tests/expected/210-pack-to-protocol-tuple.txt diff --git a/examples/210-pack-to-protocol-tuple.sx b/examples/210-pack-to-protocol-tuple.sx new file mode 100644 index 0000000..6a9ef57 --- /dev/null +++ b/examples/210-pack-to-protocol-tuple.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2e46b0a..cd2ac80 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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; diff --git a/tests/expected/210-pack-to-protocol-tuple.exit b/tests/expected/210-pack-to-protocol-tuple.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/210-pack-to-protocol-tuple.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/210-pack-to-protocol-tuple.txt b/tests/expected/210-pack-to-protocol-tuple.txt new file mode 100644 index 0000000..e1b0676 --- /dev/null +++ b/tests/expected/210-pack-to-protocol-tuple.txt @@ -0,0 +1 @@ +10 hi