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:
48
examples/comptime/0649-comptime-typecall-composition.sx
Normal file
48
examples/comptime/0649-comptime-typecall-composition.sx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
a (i64) = 42
|
||||||
|
b (bool) = true
|
||||||
|
c (f64) = 2.500000
|
||||||
|
R: variants=3 names=(a,b,c)
|
||||||
@@ -2298,9 +2298,24 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C
|
|||||||
return self.builder.constInt(0, .void);
|
return self.builder.constInt(0, .void);
|
||||||
}
|
}
|
||||||
if (std.mem.eql(u8, name, "field_name")) {
|
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(""));
|
if (c.args.len < 2) return self.builder.constString(self.module.types.internString(""));
|
||||||
const ty = self.resolveTypeArg(c.args[0]);
|
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]);
|
const idx = self.lowerExpr(c.args[1]);
|
||||||
return self.builder.emit(.{ .field_name_get = .{
|
return self.builder.emit(.{ .field_name_get = .{
|
||||||
.base = .none,
|
.base = .none,
|
||||||
@@ -2376,6 +2391,19 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C
|
|||||||
return self.builder.constType(arg_ty);
|
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")) {
|
if (std.mem.eql(u8, name, "field_index")) {
|
||||||
// field_index(T, val) → extract tag from tagged union
|
// field_index(T, val) → extract tag from tagged union
|
||||||
if (c.args.len < 2) return self.builder.constInt(0, .i64);
|
if (c.args.len < 2) return self.builder.constInt(0, .i64);
|
||||||
|
|||||||
@@ -339,6 +339,18 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool {
|
|||||||
.comptime_pack_ref,
|
.comptime_pack_ref,
|
||||||
=> true,
|
=> true,
|
||||||
.identifier => |id| table.findByName(table.internString(id.name)) != null,
|
.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: {
|
.tuple_literal => |tl| blk: {
|
||||||
for (tl.elements) |el| {
|
for (tl.elements) |el| {
|
||||||
if (!isTypeShapedAstNode(el.value, table)) break :blk false;
|
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 {
|
fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
|
||||||
// Strip module prefix (e.g. "std.Vector" → "Vector")
|
// 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;
|
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
|
||||||
|
|||||||
Reference in New Issue
Block a user