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