feat: pointee($P: Type) -> Type comptime reflection builtin

Project a pointer type to its target: `pointee(*X)` -> `X`. The one reflection
primitive missing for the `race` result synthesis (`*Task(A)` -> `A` via
`field_type(pointee(*Task(A)), 0)`) — reflection could read aggregate fields but
was blind to a pointer's target type.

Mirrors the `field_type` builtin: declared `#builtin` in std/core.sx, resolved as
a lower-time type-call fold in resolveTypeCallWithBindings (src/ir/lower/generic.zig)
so it composes in any type-arg slot. `.pointer` -> pointee, `.many_pointer` ->
element; a non-pointer arg is a loud diagnostic + `.unresolved` sentinel (no silent
fallback). Adversarially reviewed (SHIP). Locked by
examples/comptime/0647-comptime-pointee-reflection.sx. Suite green (819/0).

PLAN-RACE step 1 of 6.
This commit is contained in:
agra
2026-06-26 12:47:02 +03:00
parent dea96bd66a
commit f1d298764f
6 changed files with 57 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
// `pointee($P: Type) -> Type` — comptime reflection for a pointer's target type
// (`pointee(*X)` -> `X`). Folds at lower time like `field_type`, so it composes
// inside other type-arg slots — e.g. project a generic handle `*Box(A)` to its
// payload type `A` via `field_type(pointee(*Box(A)), 0)`. (Foundation for the
// `race` result synthesis, which projects `*Task(A)` -> `A`.)
#import "modules/std.sx";
Box :: struct ($R: Type) { value: R; tag: i64; }
// Project a pointer-to-Box to the Box's payload type: *Box(A) -> A.
Payload :: ($P: Type) -> Type { return field_type(pointee(P), 0); }
main :: () -> i32 {
// plain pointer
print("pointee(*i64) = {}\n", type_name(pointee(*i64)));
print("pointee(*bool) = {}\n", type_name(pointee(*bool)));
// pointer to a generic struct → the struct type
print("pointee(*Box(f64)) field0 = {}\n", type_name(field_type(pointee(*Box(f64)), 0)));
// composed projection, used as a real type
v : Payload(*Box(i64)) = 42;
print("Payload(*Box(i64)) value = {}\n", v);
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,4 @@
pointee(*i64) = i64
pointee(*bool) = bool
pointee(*Box(f64)) field0 = f64
Payload(*Box(i64)) value = 42

View File

@@ -38,6 +38,10 @@ type_of :: (val: $T) -> Type #builtin;
type_name :: ($T: Type) -> string #builtin; type_name :: ($T: Type) -> string #builtin;
field_count :: ($T: Type) -> i64 #builtin; field_count :: ($T: Type) -> i64 #builtin;
field_name :: ($T: Type, idx: i64) -> string #builtin; field_name :: ($T: Type, idx: i64) -> string #builtin;
// The target type of a pointer: `pointee(*X)` -> `X`. Comptime reflection,
// folded at lower time like `field_type` (so it composes in any type-arg slot).
// A non-pointer argument is a loud compile error, not a silent fallthrough.
pointee :: ($P: Type) -> Type #builtin;
field_value :: (s: $T, idx: i64) -> Any #builtin; field_value :: (s: $T, idx: i64) -> Any #builtin;
is_flags :: ($T: Type) -> bool #builtin; is_flags :: ($T: Type) -> bool #builtin;
type_is_unsigned :: ($T: Type) -> bool #builtin; type_is_unsigned :: ($T: Type) -> bool #builtin;

View File

@@ -1424,6 +1424,27 @@ pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId
}; };
return self.fieldTypeOf(t, idx, cl.callee.span); return self.fieldTypeOf(t, idx, cl.callee.span);
} }
// pointee($P) -> Type — comptime reflection: the target type of a pointer
// (`pointee(*X)` -> `X`). Folds at lower time like `field_type` so it
// composes inside any type-arg slot. A non-pointer arg is a loud error.
if (std.mem.eql(u8, callee_name, "pointee")) {
if (cl.args.len != 1) {
if (self.diagnostics) |d|
d.addFmt(.err, cl.callee.span, "pointee takes one type: pointee($P)", .{});
return .unresolved;
}
const t = self.resolveTypeArg(cl.args[0]);
if (t == .unresolved) return .unresolved;
return switch (self.module.types.get(t)) {
.pointer => |p| p.pointee,
.many_pointer => |p| p.element,
else => blk: {
if (self.diagnostics) |d|
d.addFmt(.err, cl.callee.span, "pointee: '{s}' is not a pointer type", .{self.formatTypeName(t)});
break :blk .unresolved;
},
};
}
// Built-in: Vector(N, T) // Built-in: Vector(N, T)
if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) { if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) {
const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved;