feat: comptime tuple-element L-values + named-tuple-literal binding (GAP 2)

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).
This commit is contained in:
agra
2026-06-26 18:06:55 +03:00
parent fee86adf2c
commit 6a97628749
7 changed files with 209 additions and 6 deletions

View File

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

View File

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

View File

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