From eb18bbc6fd5578753985575356e7fa2f32b573ef Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 26 Jun 2026 13:40:52 +0300 Subject: [PATCH] feat: comptime type-call composition (field_type/pointee/field_name in value position) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A comptime-type-call's `Type` result (`field_type(T, i)`, `pointee(P)`) could only be used in a type-arg slot — not as a `Type`-typed struct-field value, a generic `$P: Type` argument, or a nested type-call arg — when the index was an `inline for` loop variable. It routed through value / generic-fn lowering ("cannot infer generic type parameter" / "unknown #builtin field_type") instead of the type-call fold. This is what blocked the variable-arity `race` result synthesis: a `($T) -> Type` builder looping `field_type(pointee(field_type(T, i)), 0)` to mint a tagged-union. Three coordinated changes route these through the SAME type-call fold (which folds the index, including a loop var), so type-arg and value positions never disagree: - `isTypeShapedAstNode` (type_bridge.zig): a `.call` to a type-returning builtin (`field_type`/`pointee`/`type_of`, via new `isTypeReturningBuiltinName`) is type-shaped, so generic-arg inference (buildTypeBindings Strategy 1) resolves it via `resolveTypeArg` rather than failing value inference. - `tryLowerReflectionCall` (call.zig): value-position `field_type`/`pointee` fold to `constType(resolveTypeCallWithBindings(c))` — the value twin of the existing `type_of` fold (every failure path already diagnoses before `.unresolved`). - `field_name` (call.zig): folds to a const STRING via `memberName` when the type resolves and the index is a compile-time constant (matching the runtime `field_name_get` array exactly — same `memberName`, same "" for nameless members); a dynamic index still emits the `field_name_get` instruction. Adversarially reviewed (SHIP): no over-broadening (only type-demanding slots consult isTypeShapedAstNode; only `$T: Type` slots are affected), no silent defaults (every fold failure is preceded by a diagnostic; "" is the runtime-matching value for a nameless member). Locked by examples/comptime/0649-comptime-typecall-composition.sx (reflect a named tuple of `*Box(..)` handles → mint a tagged-union with the tuple's labels, projecting `*Box(A)` -> `A`). Suite green (821/0). Unblocks PLAN-RACE step 2. --- .../0649-comptime-typecall-composition.sx | 48 +++++++++++++++++++ .../0649-comptime-typecall-composition.exit | 1 + .../0649-comptime-typecall-composition.stderr | 1 + .../0649-comptime-typecall-composition.stdout | 4 ++ src/ir/lower/call.zig | 30 +++++++++++- src/ir/type_bridge.zig | 22 +++++++++ 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 examples/comptime/0649-comptime-typecall-composition.sx create mode 100644 examples/comptime/expected/0649-comptime-typecall-composition.exit create mode 100644 examples/comptime/expected/0649-comptime-typecall-composition.stderr create mode 100644 examples/comptime/expected/0649-comptime-typecall-composition.stdout diff --git a/examples/comptime/0649-comptime-typecall-composition.sx b/examples/comptime/0649-comptime-typecall-composition.sx new file mode 100644 index 00000000..8984d1f0 --- /dev/null +++ b/examples/comptime/0649-comptime-typecall-composition.sx @@ -0,0 +1,48 @@ +// Comptime type-call COMPOSITION: a `($T) -> Type` builder reflects a named +// tuple, projects each element type through `pointee` + `field_type`, and mints a +// tagged-union whose variant labels mirror the tuple's labels — the shape the +// `race` result synthesis needs (`(a: *Task(A), b: *Task(B))` -> `{ a: A; b: B }`). +// +// Exercises three things that previously failed when the index was an `inline for` +// loop var: a type-call RESULT used as (1) a `Type`-typed struct field value +// (`payload = field_type(...)`), (2) a nested type-call arg +// (`field_type(pointee(field_type(T, i)), 0)`), and a `field_name(T, i)` folded to +// a comptime string for a minted variant NAME. All resolve through the same +// type-call fold as a literal index would. +#import "modules/std.sx"; +#import "modules/std/meta.sx"; + +// Stand-in for a task handle: a pointer to a generic box carrying the result. +Box :: struct ($R: Type) { value: R; } + +// Mint a tagged-union mirroring a named tuple of `*Box(..)` handles: +// variant name = tuple label, payload = the box's value type (`*Box(A)` -> `A`). +ResultOf :: ($T: Type) -> Type { + vs : [field_count(T)]EnumVariant = ---; + inline for 0..field_count(T) (i) { + vs[i] = EnumVariant.{ + name = field_name(T, i), // folded to a const string + payload = field_type(pointee(field_type(T, i)), 0), // *Box(A) -> Box(A) -> A + }; + } + return make_enum("ResultOf", vs[0..field_count(T)]); +} + +R :: ResultOf(Tuple(a: *Box(i64), b: *Box(bool), c: *Box(f64))); + +use :: (r: R) { + if r == { + case .a: (v) { print("a (i64) = {}\n", v); } + case .b: (v) { print("b (bool) = {}\n", v); } + case .c: (v) { print("c (f64) = {}\n", v); } + } +} + +main :: () -> i32 { + use(.a(42)); + use(.b(true)); + use(.c(2.5)); + print("R: variants={} names=({},{},{})\n", + field_count(R), field_name(R, 0), field_name(R, 1), field_name(R, 2)); + return 0; +} diff --git a/examples/comptime/expected/0649-comptime-typecall-composition.exit b/examples/comptime/expected/0649-comptime-typecall-composition.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/comptime/expected/0649-comptime-typecall-composition.exit @@ -0,0 +1 @@ +0 diff --git a/examples/comptime/expected/0649-comptime-typecall-composition.stderr b/examples/comptime/expected/0649-comptime-typecall-composition.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/comptime/expected/0649-comptime-typecall-composition.stderr @@ -0,0 +1 @@ + diff --git a/examples/comptime/expected/0649-comptime-typecall-composition.stdout b/examples/comptime/expected/0649-comptime-typecall-composition.stdout new file mode 100644 index 00000000..90f0d1f1 --- /dev/null +++ b/examples/comptime/expected/0649-comptime-typecall-composition.stdout @@ -0,0 +1,4 @@ +a (i64) = 42 +b (bool) = true +c (f64) = 2.500000 +R: variants=3 names=(a,b,c) diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 4eba58f9..593b9762 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -2298,9 +2298,24 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C return self.builder.constInt(0, .void); } if (std.mem.eql(u8, name, "field_name")) { - // field_name(T, i) → field_name_get instruction if (c.args.len < 2) return self.builder.constString(self.module.types.internString("")); const ty = self.resolveTypeArg(c.args[0]); + // Fold to a comptime STRING constant when the type resolves AND the index + // is a compile-time constant (incl. an `inline for` loop var) — so a + // minted variant NAME / any comptime use gets a const string the + // type-construction VM can evaluate, mirroring the `field_type` / + // `field_count` folds. A member with no name (positional-tuple / array / + // vector element) folds to "". A dynamic (runtime) index falls back to + // the `field_name_get` instruction. + if (ty != .unresolved) { + switch (program_index_mod.foldDimU32(c.args[1], self, 0)) { + .ok => |n| { + const nm = self.module.types.memberName(ty, @intCast(n)) orelse self.module.types.internString(""); + return self.builder.constString(nm); + }, + else => {}, + } + } const idx = self.lowerExpr(c.args[1]); return self.builder.emit(.{ .field_name_get = .{ .base = .none, @@ -2376,6 +2391,19 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C return self.builder.constType(arg_ty); } } + if (std.mem.eql(u8, name, "field_type") or std.mem.eql(u8, name, "pointee")) { + // VALUE-position `field_type(T, i)` / `pointee(P)` — produce a comptime + // Type value. Both ALSO resolve in TYPE position (a type-arg slot routes + // through `resolveTypeArg` → `resolveTypeCallWithBindings`); this is the + // value-position twin (e.g. assigned to a `Type` field like + // `EnumVariant.payload`, or a `$P: Type` arg's value), folding the index + // — including an `inline for` loop var — through the SAME + // `resolveTypeCallWithBindings` so the two positions never disagree. + // Without this they fall through to generic-function lowering, which + // can't fold the index → "cannot infer …" / "unknown #builtin". + const ty = self.resolveTypeCallWithBindings(c); + return self.builder.constType(ty); + } if (std.mem.eql(u8, name, "field_index")) { // field_index(T, val) → extract tag from tagged union if (c.args.len < 2) return self.builder.constInt(0, .i64); diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index c6a404d2..bbf153d5 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -339,6 +339,18 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool { .comptime_pack_ref, => true, .identifier => |id| table.findByName(table.internString(id.name)) != null, + // A call to a comptime type-query / projection builtin whose RESULT is a + // Type — `field_type(T, i)`, `pointee(P)`, `type_of(x)`. These are + // type-shaped, so an arg / initializer like `field_type(T, i)` resolves + // through `resolveTypeArg` (which routes `.call` to + // `resolveTypeCallWithBindings`, folding the index — incl. an `inline for` + // loop var) rather than through value inference (which cannot fold the + // index → "cannot infer generic type parameter"). Value-returning calls + // stay non-type-shaped (the `else` below). + .call => |c| switch (c.callee.data) { + .identifier => |id| isTypeReturningBuiltinName(id.name), + else => false, + }, .tuple_literal => |tl| blk: { for (tl.elements) |el| { if (!isTypeShapedAstNode(el.value, table)) break :blk false; @@ -349,6 +361,16 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool { }; } +/// Comptime builtins whose call result IS a `Type` (so a call to one is +/// type-shaped). The type-CONSTRUCTOR builtins `Vector`/generic-struct heads are +/// already covered by `.parameterized_type_expr`; this names the type-QUERY / +/// projection builtins that parse as a plain `.call`. +pub fn isTypeReturningBuiltinName(name: []const u8) bool { + return std.mem.eql(u8, name, "field_type") or + std.mem.eql(u8, name, "pointee") or + std.mem.eql(u8, name, "type_of"); +} + fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { // Strip module prefix (e.g. "std.Vector" → "Vector") const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;