From 39d77ff886d2d65656ff5dcbae1e9a4f29897ee5 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 30 May 2026 03:00:58 +0300 Subject: [PATCH] lang: tuple element assignment + named-tuple field names Two fixes: - Element assignment `t.0 = v` (the known Phase-4.2 gap): the lvalue path looked the element up by NAME via getStructFields, never matched a tuple (positional), and left field_ty .unresolved -> ptr(.unresolved) -> codegen panic. Added a tuple branch to the field-assignment lowering that indexes by position (numeric) or name (tup.names), mirroring the read path. Fixes `c.sources.0 = v` on a generic-instance pack field too. - Named tuples: the parser dropped captured field names for a tuple TYPE `(x: T, y: U)` (passed field_names=null), and resolveTupleTypeWithBindings also nulled them. Both now preserve names (synthesizing _ for any unnamed slot), so `t.x` reads/writes by name and `.0` by position. examples/208. 243 examples + unit green. --- examples/208-tuple-element-assign.sx | 25 ++++++++++ src/ir/lower.zig | 49 +++++++++++++++++++- src/parser.zig | 14 +++++- tests/expected/208-tuple-element-assign.exit | 1 + tests/expected/208-tuple-element-assign.txt | 3 ++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 examples/208-tuple-element-assign.sx create mode 100644 tests/expected/208-tuple-element-assign.exit create mode 100644 tests/expected/208-tuple-element-assign.txt diff --git a/examples/208-tuple-element-assign.sx b/examples/208-tuple-element-assign.sx new file mode 100644 index 0000000..7b2c76c --- /dev/null +++ b/examples/208-tuple-element-assign.sx @@ -0,0 +1,25 @@ +// Tuple element assignment + named tuples. +// - `t.0 = v` writes one element in place (was a known gap: the lvalue path +// looked the element up by name via getStructFields and left the pointee +// `.unresolved`; now it indexes the tuple positionally like the read path). +// - Named tuples `(x: T, y: U)` keep their field names through parsing and +// type resolution, so `t.x` reads/writes by name (and `.0` by position). + +#import "modules/std.sx"; + +main :: () -> s32 { + // Positional element assignment. + a : (s32, string) = ---; + a.0 = 11; + a.1 = "x"; + print("a: {} {}\n", a.0, a.1); + + // Named tuple: write + read by name, and read by position. + p : (x: s32, y: string) = ---; + p.x = 22; + p.y = "y"; + print("p: x={} y={} .0={}\n", p.x, p.y, p.0); + p.0 = 33; // position write reaches the same slot as .x + print("p.x after .0=33: {}\n", p.x); + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 17025ba..0759c0e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1920,6 +1920,38 @@ pub const Lowering = struct { } } + // Tuple field element: `t.0 = v` / `t.x = v` — indexed by + // position (numeric) or by name, not via `getStructFields` + // (a tuple's elements aren't named-struct fields). Mirrors + // the read path's tuple handling. + if (!obj_ty.isBuiltin()) { + const ti = self.module.types.get(obj_ty); + if (ti == .tuple) { + const tup = ti.tuple; + var elem_idx: ?usize = null; + if (std.fmt.parseInt(usize, fa.field, 10)) |n| { + if (n < tup.fields.len) elem_idx = n; + } else |_| { + if (tup.names) |names| { + for (names, 0..) |nm, i| { + if (nm == field_name_id and i < tup.fields.len) { + elem_idx = i; + break; + } + } + } + } + if (elem_idx) |idx| { + const elem_ty = tup.fields[idx]; + const gep = self.builder.structGepTyped(obj_ptr, @intCast(idx), self.module.types.ptrTo(elem_ty), obj_ty); + const src_ty = self.builder.getRefType(val); + const coerced = self.coerceToType(val, src_ty, elem_ty); + self.storeOrCompound(gep, coerced, asgn.op, elem_ty); + return; + } + } + } + const struct_fields = self.getStructFields(obj_ty); var field_idx: u32 = 0; var field_ty: TypeId = .unresolved; @@ -11199,19 +11231,34 @@ pub const Lowering = struct { fn resolveTupleTypeWithBindings(self: *Lowering, tt: *const ast.TupleTypeExpr) TypeId { var field_ids = std.ArrayList(TypeId).empty; defer field_ids.deinit(self.alloc); + var had_spread = false; 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; + had_spread = true; continue; } } field_ids.append(self.alloc, self.resolveTypeWithBindings(ft)) catch return .void; } + // Preserve field names for a named tuple `(x: T, y: U)` so `t.x` resolves + // (matches type_bridge.resolveTupleType). A spread expands to unnamed + // pack elements, so names only apply when there was no spread. + var name_ids: ?[]const types.StringId = null; + if (!had_spread) { + if (tt.field_names) |names| { + if (names.len == field_ids.items.len) { + var ids = std.ArrayList(types.StringId).empty; + for (names) |n| ids.append(self.alloc, self.module.types.internString(n)) catch return .void; + name_ids = ids.toOwnedSlice(self.alloc) catch null; + } + } + } return self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, field_ids.items) catch return .void, - .names = null, + .names = name_ids, } }); } diff --git a/src/parser.zig b/src/parser.zig index b8fbf7d..bb68541 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -558,10 +558,20 @@ pub const Parser = struct { .call_conv = call_conv, } }); } - // No '->': tuple type (even for single element) + // No '->': tuple type (even for single element). Keep field names + // for a named tuple `(x: T, y: U)` so `t.x` resolves. `field_names` + // is non-optional per slot, so synthesize `_` for any unnamed one. + var field_names: ?[]const []const u8 = null; + if (has_names) { + var fns = std.ArrayList([]const u8).empty; + for (param_names.items, 0..) |pn, i| { + try fns.append(self.allocator, pn orelse try std.fmt.allocPrint(self.allocator, "_{d}", .{i})); + } + field_names = try fns.toOwnedSlice(self.allocator); + } return try self.createNode(start, .{ .tuple_type_expr = .{ .field_types = try param_types.toOwnedSlice(self.allocator), - .field_names = null, + .field_names = field_names, } }); } diff --git a/tests/expected/208-tuple-element-assign.exit b/tests/expected/208-tuple-element-assign.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/208-tuple-element-assign.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/208-tuple-element-assign.txt b/tests/expected/208-tuple-element-assign.txt new file mode 100644 index 0000000..f9ab285 --- /dev/null +++ b/tests/expected/208-tuple-element-assign.txt @@ -0,0 +1,3 @@ +a: 11 x +p: x=22 y=y .0=22 +p.x after .0=33: 33