lang 2.4: value-position pack projection xs.value + mixed-tuple type fix

`xs.<method>` 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.<m>` on a pack base (where <m> is a protocol
method) and synthesizes/lowers `xs[i].<m>()` 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.
This commit is contained in:
agra
2026-05-29 19:45:49 +03:00
parent 35c63a92d4
commit c03db7938c
6 changed files with 100 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1,28 @@
// Feature 1 — value-position pack projection: `xs.<method>` 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;
}

View File

@@ -3976,6 +3976,19 @@ pub const Lowering = struct {
}
}
// Pack value projection: `xs.<m>` where `<m>` is a (zero-arg) method of
// the pack's constraint protocol projects it over every element →
// a tuple `(xs[0].<m>(), …, xs[N-1].<m>())`. (`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].<m>` 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.<method>`: call the (zero-arg)
/// protocol method on each element and collect the results into a tuple
/// `(xs[0].<method>(), …, xs[N-1].<method>())`. N=0 yields the empty tuple.
/// Synthesizes `xs[i].<method>()` 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| {

View File

@@ -9,3 +9,4 @@ concat 1 4
rep 1 2
mem true
lex true
mixed 42 hi

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
0=42 1=hi
0=x 1=7