feat: comptime type-call composition (field_type/pointee/field_name in value position)

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.
This commit is contained in:
agra
2026-06-26 13:40:52 +03:00
parent 18443ea2e9
commit eb18bbc6fd
6 changed files with 105 additions and 1 deletions

View File

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