From c03db7938c4127c453b4161afa0ab7bcbb3432d0 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 29 May 2026 19:45:49 +0300 Subject: [PATCH] lang 2.4: value-position pack projection xs.value + mixed-tuple type fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xs.` over a constrained pack projects a (zero-arg) protocol method across every element into a tuple: `xs.get` ≈ `(xs[0].get(), …, xs[N-1].get())`. lowerFieldAccess intercepts `xs.` on a pack base (where is a protocol method) and synthesizes/lowers `xs[i].()` per element into a tuple_init. For a parameterised `Box(T)` the projected tuple is heterogeneous (each element returns its own T). examples/196-pack-value-projection.sx. Surfaced and fixed a pre-existing bug: inferExprType didn't handle tuple field access (`t.0` / `t.x`), so a mixed-size tuple like `(42, "hi")` inferred the string field as s64 — the wrong type then drove a bad `print` pack mangle and coerced the string to i64 (garbage). Added the tuple arm (numeric + named). Regression: a `(s64, string)` case in examples/190-tuple-values.sx. --- examples/190-tuple-values.sx | 6 ++ examples/196-pack-value-projection.sx | 28 +++++++++ src/ir/lower.zig | 62 +++++++++++++++++++ tests/expected/190-tuple-values.txt | 1 + tests/expected/196-pack-value-projection.exit | 1 + tests/expected/196-pack-value-projection.txt | 2 + 6 files changed, 100 insertions(+) create mode 100644 examples/196-pack-value-projection.sx create mode 100644 tests/expected/196-pack-value-projection.exit create mode 100644 tests/expected/196-pack-value-projection.txt diff --git a/examples/190-tuple-values.sx b/examples/190-tuple-values.sx index d3f8d34..f9a1933 100644 --- a/examples/190-tuple-values.sx +++ b/examples/190-tuple-values.sx @@ -45,5 +45,11 @@ main :: () -> s32 { print("rep {} {}\n", r.0, r.5); print("mem {}\n", 3 in (1, 2, 3)); print("lex {}\n", (1, 2) < (1, 3)); + + // Mixed-size fields: a tuple with both an s64 and a string (16-byte fat + // pointer). Field types are tracked per-position, so reading each back is + // typed correctly (s64 prints as a number, string as text). + mixed := (42, "hi"); + print("mixed {} {}\n", mixed.0, mixed.1); 0; } diff --git a/examples/196-pack-value-projection.sx b/examples/196-pack-value-projection.sx new file mode 100644 index 0000000..1635ecd --- /dev/null +++ b/examples/196-pack-value-projection.sx @@ -0,0 +1,28 @@ +// Feature 1 — value-position pack projection: `xs.` projects a +// (zero-arg) protocol method over every element into a TUPLE of the per-element +// results. For a parameterised `Box(T)`, each element's method returns its own +// `T`, so the projected tuple is heterogeneous. +// +// xs.get ≈ (xs[0].get(), xs[1].get()) + +#import "modules/std.sx"; + +Box :: protocol(T: Type) { + get :: () -> T; +} + +IntCell :: struct { v: s64; } +StrCell :: struct { s: string; } +impl Box(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; } +impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } + +show :: (..xs: Box) -> void { + vals := xs.get; // tuple (s64, string) + print("0={} 1={}\n", vals.0, vals.1); +} + +main :: () -> s32 { + show(IntCell.{ v = 42 }, StrCell.{ s = "hi" }); + show(StrCell.{ s = "x" }, IntCell.{ v = 7 }); // order swapped → (string, s64) + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5d042cb..0b92488 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -3976,6 +3976,19 @@ pub const Lowering = struct { } } + // Pack value projection: `xs.` where `` is a (zero-arg) method of + // the pack's constraint protocol projects it over every element → + // a tuple `(xs[0].(), …, xs[N-1].())`. (`xs.len` handled above.) + if (self.pack_constraint) |pcon| { + if (fa.object.data == .identifier) { + if (pcon.get(fa.object.data.identifier.name)) |proto| { + if (self.lookupProtocolField(proto, fa.field) != null) { + return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span); + } + } + } + } + // Interface-only enforcement (Decision): a member access on a // constrained pack element `xs[i].` may only name a method of the // constraint protocol — not an arbitrary concrete field. Checked here, @@ -4078,6 +4091,41 @@ pub const Lowering = struct { return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); } + /// Value-position pack projection `xs.`: call the (zero-arg) + /// protocol method on each element and collect the results into a tuple + /// `(xs[0].(), …, xs[N-1].())`. N=0 yields the empty tuple. + /// Synthesizes `xs[i].()` per element and lowers it (substitution + /// turns `xs[i]` into the concrete arg; UFCS dispatches the method). + fn lowerPackValueProjection(self: *Lowering, pack_name: []const u8, method: []const u8, span: ast.Span) Ref { + const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0; + var refs = std.ArrayList(Ref).empty; + defer refs.deinit(self.alloc); + var tys = std.ArrayList(TypeId).empty; + defer tys.deinit(self.alloc); + var i: u32 = 0; + while (i < n) : (i += 1) { + const id_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void); + id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } }; + const idx_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void); + idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = @intCast(i) } } }; + const index_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void); + index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } }; + const fa_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void); + fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = method } } }; + const call_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void); + call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } }; + const r = self.lowerExpr(call_node); + refs.append(self.alloc, r) catch return self.builder.constInt(0, .void); + tys.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void); + } + const tuple_ty = self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, tys.items) catch return self.builder.constInt(0, .void), + .names = null, + } }); + const owned = self.alloc.dupe(Ref, refs.items) catch return self.builder.constInt(0, .void); + return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); + } + /// Lower a struct-level constant value (e.g., Phys.GRAVITY). fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { const val_node = info.value; @@ -12582,6 +12630,20 @@ pub const Lowering = struct { const elem = info.vector.element; return if (is_opt_chain) self.optionalOfFlattened(elem) else elem; } + // Tuple field access: numeric `t.0` or named `t.x`. + if (info == .tuple) { + const tup = info.tuple; + if (std.fmt.parseInt(usize, fa.field, 10)) |idx| { + if (idx < tup.fields.len) + return if (is_opt_chain) self.optionalOfFlattened(tup.fields[idx]) else tup.fields[idx]; + } else |_| {} + if (tup.names) |names| { + for (names, 0..) |nm, i| { + if (nm == field_name_id and i < tup.fields.len) + return if (is_opt_chain) self.optionalOfFlattened(tup.fields[i]) else tup.fields[i]; + } + } + } // Check struct fields const fields = self.getStructFields(obj_ty); for (fields) |f| { diff --git a/tests/expected/190-tuple-values.txt b/tests/expected/190-tuple-values.txt index 0f21979..5912b21 100644 --- a/tests/expected/190-tuple-values.txt +++ b/tests/expected/190-tuple-values.txt @@ -9,3 +9,4 @@ concat 1 4 rep 1 2 mem true lex true +mixed 42 hi diff --git a/tests/expected/196-pack-value-projection.exit b/tests/expected/196-pack-value-projection.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/196-pack-value-projection.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/196-pack-value-projection.txt b/tests/expected/196-pack-value-projection.txt new file mode 100644 index 0000000..5409d7c --- /dev/null +++ b/tests/expected/196-pack-value-projection.txt @@ -0,0 +1,2 @@ +0=42 1=hi +0=x 1=7