From 6a976287499b2e4284d36f1fd775e0384b775dc4 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 26 Jun 2026 18:06:55 +0300 Subject: [PATCH] feat: comptime tuple-element L-values + named-tuple-literal binding (GAP 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes comptime-cursor tuple indexing (started by the read path in fee86adf) and unblocks the `race` runtime synthesis. Five enablers: 1. Named-tuple-literal type inference preserves element NAMES. A `.(a = x, b = y)` passed DIRECTLY as a `$T` argument inferred to a tuple with `.names = null`, so `field_name(T, i)` reflected "" and a `make_enum` over those labels collided on the empty name. The typer now mirrors `lowerTupleLiteral`'s name capture. 2. `inferExprType` resolves a comptime-constant tuple index to the i-th field's CONCRETE type (the inference sibling of the fee86adf read path), so `tup[i].field` / methods / comparisons on it resolve. 3. Tuple-element L-VALUES by comptime index — `tup[i] = v`, `tup[i].f = v`, `@tup[i]` — lower to a typed `structGep` of field i across all four paths (`lowerAssignment`, the multi-assign store, `lowerExprAsPtr`, and address-of-index). Previously each emitted an `index_gep` with a `ptrTo(.unresolved)` element type (a tuple has no uniform element) that panicked at LLVM emit. An out-of-range comptime index now diagnoses loudly on every path instead of falling through to that panic. 4. A user generic `($X..) -> Type` call is recognized as type-shaped (`isTypeReturningCallNode`), so it can bind a `$E: Type` parameter — e.g. `make_variant(RaceResult(T), i, …)`. The static `isTypeShapedAstNode` only knew the type-returning builtins (field_type/pointee/type_of). Locked by examples/comptime/0652 (read, fee86adf) and 0653 (store + address-of + element-pointer field store). --- .../0653-comptime-tuple-cursor-store.sx | 35 ++++++++ src/ir/expr_typer.zig | 36 +++++++- src/ir/generics.zig | 2 +- src/ir/lower.zig | 1 + src/ir/lower/expr.zig | 24 ++++- src/ir/lower/generic.zig | 27 ++++++ src/ir/lower/stmt.zig | 90 ++++++++++++++++++- 7 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 examples/comptime/0653-comptime-tuple-cursor-store.sx diff --git a/examples/comptime/0653-comptime-tuple-cursor-store.sx b/examples/comptime/0653-comptime-tuple-cursor-store.sx new file mode 100644 index 00000000..bf28eaf5 --- /dev/null +++ b/examples/comptime/0653-comptime-tuple-cursor-store.sx @@ -0,0 +1,35 @@ +// Comptime-cursor tuple-element L-VALUES: writing a named-tuple element by a +// comptime-constant index — the store/address-of siblings of 0652's read path. +// A tuple is heterogeneous, so each element L-value is a typed `structGep` of the +// i-th field (not a uniform `index_gep`): `tup[i] = v` (direct store), a field +// store through an element pointer (`tup[i].f = v`), and `@tup[i]` (address-of). +// These are what the `race` runtime needs to register a waiter on the i-th task +// handle (`tasks[i].waiter = …`). An out-of-range comptime index is a loud +// compile error on every one of these paths (no silent `ptrTo(unresolved)` panic). +#import "modules/std.sx"; + +Box :: struct ($R: Type) { value: R; } + +main :: () -> i32 { + // Direct element store by literal index. + t := .(a = 1, b = 2, c = 3); + t[0] = 100; + t[2] = 300; + print("t = ({}, {}, {})\n", t.a, t.b, t.c); + + // Address-of an element, write through the pointer. + p := @t[1]; + p.* = 200; + print("t.b via @t[1] = {}\n", t.b); + + // Field store THROUGH an element pointer — `tup[i].field = v` — the exact + // L-value shape `race` uses to register a waiter (`tasks[i].waiter = …`): the + // i-th element is a `*Box`, and `.value` writes through it to the pointee. + ba : Box(i64) = .{ value = 0 }; + bb : Box(bool) = .{ value = false }; + handles := .(x = @ba, y = @bb); + handles[0].value = 7; + handles[1].value = true; + print("ba.value = {}, bb.value = {}\n", ba.value, bb.value); + return 0; +} diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 511c05dd..4be05ecf 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -370,12 +370,29 @@ pub const ExprTyper = struct { } var field_types = std.ArrayList(TypeId).empty; defer field_types.deinit(self.l.alloc); + // Preserve the literal's element names (the NAMED-tuple form + // `.(a = x, b = y)`) so the inferred type carries them — this is + // the type bound to a generic `$T` when a named-tuple literal is + // passed DIRECTLY as a call argument. Without it `field_name(T, i)` + // reflected the empty string and a `make_enum` over those labels + // silently collided on "" (the `race` result synthesis). Mirrors + // `lowerTupleLiteral`'s name capture so the inferred type and the + // lowered value's type agree. + var names = std.ArrayList(types.StringId).empty; + defer names.deinit(self.l.alloc); + var has_names = false; for (tl.elements) |elem| { field_types.append(self.l.alloc, self.l.inferExprType(elem.value)) catch unreachable; + if (elem.name) |name| { + names.append(self.l.alloc, self.l.module.types.internString(name)) catch unreachable; + has_names = true; + } else { + names.append(self.l.alloc, self.l.module.types.internString("")) catch unreachable; + } } return self.l.module.types.intern(.{ .tuple = .{ .fields = self.l.alloc.dupe(TypeId, field_types.items) catch unreachable, - .names = null, + .names = if (has_names) self.l.alloc.dupe(types.StringId, names.items) catch unreachable else null, } }); }, .index_expr => |ie| { @@ -400,6 +417,23 @@ pub const ExprTyper = struct { return self.l.inferExprType(arg_node); } const obj_ty = self.l.inferExprType(ie.object); + // Comptime-constant index into a tuple VALUE — `tup[i]` where `i` + // folds to a compile-time integer (an `inline for` cursor or a + // literal). Mirrors the lowering in `lowerIndexExpr`: the result is + // the i-th tuple field's CONCRETE type (heterogeneous elements, so + // no single runtime element type). Without this the inference path + // returned `.unresolved` for `tup[i]`, so a following `.field` / + // method / comparison on it could not resolve (the `race` runtime's + // `tasks[i].state == .ready`). A runtime index falls through to the + // generic element-type path below. + if (!obj_ty.isBuiltin() and self.l.module.types.get(obj_ty) == .tuple) { + const tinfo = self.l.module.types.get(obj_ty).tuple; + if (self.l.comptimeIndexOf(ie.index)) |ci| { + if (ci >= 0 and @as(usize, @intCast(ci)) < tinfo.fields.len) { + return tinfo.fields[@intCast(ci)]; + } + } + } // Optional-chain index `opt?.xs[i]`: the object types as an // optional container (`?[N]T` / `?[]T` / `?[*]T`), so the whole // index expression is `?ElemType` (flattened if the element is diff --git a/src/ir/generics.zig b/src/ir/generics.zig index 00a922a9..b7af381a 100644 --- a/src/ir/generics.zig +++ b/src/ir/generics.zig @@ -210,7 +210,7 @@ pub const GenericResolver = struct { if (types_passed_explicitly) { for (fd.params, 0..) |param, pi| { if (std.mem.eql(u8, param.name, tp.name)) { - if (pi < args_ast.len and type_bridge.isTypeShapedAstNode(args_ast[pi], &self.l.module.types)) { + if (pi < args_ast.len and (type_bridge.isTypeShapedAstNode(args_ast[pi], &self.l.module.types) or self.l.isTypeReturningCallNode(args_ast[pi]))) { const ty = self.l.resolveTypeArg(args_ast[pi]); bindings.put(tp.name, ty) catch {}; found = true; diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 800c50a8..b80bf8e9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -2125,6 +2125,7 @@ pub const Lowering = struct { pub const lowerComptimeGenericInstanceMethod = lower_generic.lowerComptimeGenericInstanceMethod; pub const assertInstanceMapsCoincide = lower_generic.assertInstanceMapsCoincide; pub const isStaticTypeArg = lower_generic.isStaticTypeArg; + pub const isTypeReturningCallNode = lower_generic.isTypeReturningCallNode; pub const isStaticTypeRef = lower_generic.isStaticTypeRef; pub const resolveTupleLiteralTypeArg = lower_generic.resolveTupleLiteralTypeArg; pub const resolveTypeArg = lower_generic.resolveTypeArg; diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 8e99b820..5b87fd40 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -2593,8 +2593,30 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of if (uop.op == .address_of and uop.operand.data == .index_expr) { const ie = &uop.operand.data.index_expr; - const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); + // Comptime-constant index into a tuple VALUE — `@tup[i]`. A tuple is + // heterogeneous: the element address is a typed `structGep` of the + // i-th field, never an `index_gep` (whose `ptrTo(.unresolved)` + // element type panics at LLVM emit). Out-of-range diagnoses loudly, + // mirroring the read path. + if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .tuple) { + const tinfo = self.module.types.get(obj_ty).tuple; + if (self.comptimeIndexOf(ie.index)) |ci| { + if (ci >= 0 and @as(usize, @intCast(ci)) < tinfo.fields.len) { + const fi: u32 = @intCast(ci); + const fld_ty = tinfo.fields[fi]; + const base = self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object); + break :blk self.builder.structGepTyped(base, fi, self.module.types.ptrTo(fld_ty), obj_ty); + } + if (self.diagnostics) |d| { + d.addFmt(.err, ie.index.span, "tuple index {} out of bounds — tuple '{s}' has {} field{s}", .{ + ci, self.formatTypeName(obj_ty), tinfo.fields.len, if (tinfo.fields.len == 1) "" else "s", + }); + } + break :blk self.builder.constInt(0, .i64); // placeholder — hasErrors() aborts + } + } + const idx = self.lowerExpr(ie.index); const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // For array targets, use the storage pointer (alloca for a diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 17c69a90..95b7101c 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -368,6 +368,33 @@ pub fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId { return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } +/// True iff `node` is a call to a user-defined generic `($X..) -> Type` function +/// (e.g. `RaceResult(T)`). Such a call is type-shaped: `resolveTypeArg` resolves +/// it via `resolveTypeCallWithBindings` -> `instantiateTypeFunction`. The static +/// `isTypeShapedAstNode` only recognizes the type-returning BUILTINS +/// (`field_type`/`pointee`/`type_of`) — it has no program index — so a user +/// type-fn call in a `$E: Type` argument slot would otherwise never be seen as a +/// type and the param would fail to bind ("cannot infer generic type parameter"). +/// This lets a synthesized result type flow as a type argument, e.g. +/// `make_variant(RaceResult(T), i, winner.value)` in the `race` runtime. +pub fn isTypeReturningCallNode(self: *Lowering, node: *const Node) bool { + if (node.data != .call) return false; + const cl = node.data.call; + const callee_name: []const u8 = switch (cl.callee.data) { + .identifier => |id| id.name, + .field_access => |fa| fa.field, + else => return false, + }; + const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name; + const fd = self.program_index.fn_ast_map.get(resolved_name) orelse return false; + // Only a GENERIC `-> Type` fn resolves through `instantiateTypeFunction`; a + // non-generic one would fall to a named-type lookup that this call shape + // can't satisfy, so gate on both (matches `resolveTypeCallWithBindings`). + if (fd.type_params.len == 0) return false; + const rt = fd.return_type orelse return false; + return rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Type"); +} + pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { // Pack-index access in a type-arg slot (e.g. `type_name($args[0])` // or `type_eq($args[i], i64)`). Same shape as the diff --git a/src/ir/lower/stmt.zig b/src/ir/lower/stmt.zig index 85a1fbaf..8a0af14b 100644 --- a/src/ir/lower/stmt.zig +++ b/src/ir/lower/stmt.zig @@ -1057,8 +1057,34 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void { } }, .index_expr => |ie| { - const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); + // Comptime-constant store into a tuple element — `tup[i] = v`. A tuple + // is heterogeneous, so the destination is a typed `structGep` of field + // `i`, never an `index_gep` (whose `ptrTo(.unresolved)` element type + // panics at LLVM emit). Mirrors the read path in `lowerIndexExpr`; an + // out-of-range comptime index diagnoses loudly here too rather than + // falling through to that panic. + if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .tuple) { + const tinfo = self.module.types.get(obj_ty).tuple; + if (self.comptimeIndexOf(ie.index)) |ci| { + if (ci >= 0 and @as(usize, @intCast(ci)) < tinfo.fields.len) { + const fi: u32 = @intCast(ci); + const fld_ty = tinfo.fields[fi]; + const base = self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object); + const gep = self.builder.structGepTyped(base, fi, self.module.types.ptrTo(fld_ty), obj_ty); + const coerced = self.coerceToType(val, self.builder.getRefType(val), fld_ty); + self.storeOrCompound(gep, coerced, asgn.op, fld_ty); + return; + } + if (self.diagnostics) |d| { + d.addFmt(.err, ie.index.span, "tuple index {} out of bounds — tuple '{s}' has {} field{s}", .{ + ci, self.formatTypeName(obj_ty), tinfo.fields.len, if (tinfo.fields.len == 1) "" else "s", + }); + } + return; // hasErrors() aborts before codegen + } + } + const idx = self.lowerExpr(ie.index); const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // For fixed-size array assignment targets, use the alloca pointer directly @@ -1428,8 +1454,37 @@ pub fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref { return self.emitFieldError(obj_ty, fa.field, node.span); }, .index_expr => |ie| { - const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); + // Comptime-constant index into a tuple VALUE — the L-value sibling of + // `lowerIndexExpr`'s tuple read path. A tuple is heterogeneous, so its + // element address is a `structGep` of the i-th field (typed with that + // field's type), NOT an `index_gep` (which assumes a uniform element + // type — `getElementType(tuple)` is `.unresolved`, and an `index_gep` + // with a `ptrTo(.unresolved)` result panics at LLVM emit). Needed for + // `tasks[i].waiter = …` in the `race` runtime, where the i-th element + // is read back as a pointer to GEP into its pointee. + if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .tuple) { + const tinfo = self.module.types.get(obj_ty).tuple; + if (self.comptimeIndexOf(ie.index)) |ci| { + if (ci >= 0 and @as(usize, @intCast(ci)) < tinfo.fields.len) { + const fi: u32 = @intCast(ci); + const fld_ty = tinfo.fields[fi]; + const base = self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object); + return self.builder.structGepTyped(base, fi, self.module.types.ptrTo(fld_ty), obj_ty); + } + // Comptime index out of range — diagnose loudly (mirror the + // read path in `lowerIndexExpr`) rather than falling through to + // the `index_gep` below, whose `ptrTo(.unresolved)` element type + // would panic at LLVM emit with no source diagnostic. + if (self.diagnostics) |d| { + d.addFmt(.err, ie.index.span, "tuple index {} out of bounds — tuple '{s}' has {} field{s}", .{ + ci, self.formatTypeName(obj_ty), tinfo.fields.len, if (tinfo.fields.len == 1) "" else "s", + }); + } + return self.builder.constInt(0, .i64); // placeholder — hasErrors() aborts before codegen + } + } + const idx = self.lowerExpr(ie.index); const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); // For fixed-size arrays, use the alloca so GEP addresses the original memory @@ -1693,8 +1748,37 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void { } }, .index_expr => |ie| { - const idx = self.lowerExpr(ie.index); const obj_ty = self.inferExprType(ie.object); + // Comptime-constant direct store into a tuple element — `tup[i] = v` + // (the store sibling of the L-value tuple path above). Heterogeneous + // elements → a typed `structGep` of field `i`, never an `index_gep` + // (a uniform-element op whose `ptrTo(.unresolved)` element type would + // panic at LLVM emit). + if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .tuple) { + const tinfo = self.module.types.get(obj_ty).tuple; + if (self.comptimeIndexOf(ie.index)) |ci| { + if (ci >= 0 and @as(usize, @intCast(ci)) < tinfo.fields.len) { + const fi: u32 = @intCast(ci); + const fld_ty = tinfo.fields[fi]; + const base = self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object); + const gep = self.builder.structGepTyped(base, fi, self.module.types.ptrTo(fld_ty), obj_ty); + const v_ty = self.builder.getRefType(val); + const sv = if (v_ty != fld_ty and v_ty != .void and fld_ty != .void) self.coerceToType(val, v_ty, fld_ty) else val; + self.builder.store(gep, sv); + continue; + } + // Comptime index out of range — diagnose loudly instead of + // falling through to the `index_gep` store (whose + // `ptrTo(.unresolved)` element type would panic at LLVM emit). + if (self.diagnostics) |d| { + d.addFmt(.err, ie.index.span, "tuple index {} out of bounds — tuple '{s}' has {} field{s}", .{ + ci, self.formatTypeName(obj_ty), tinfo.fields.len, if (tinfo.fields.len == 1) "" else "s", + }); + } + continue; // hasErrors() aborts before codegen + } + } + const idx = self.lowerExpr(ie.index); const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); const val_ty = self.builder.getRefType(val);