ffi M5.A.next.3b: type_eq + has_impl comptime intrinsics

Step 3 second slice. Adds two reflection builtins used by
pack-fn bodies to branch on type identity / protocol
membership at compile time. type_name already existed
(lower.zig:8693); reused as-is.

  type_eq(T1, T2)   -> bool   structural TypeId equality
  has_impl(P, T)    -> bool   T has a reachable impl for P

Both are wired through `tryConstBoolCondition` so the inline-if
ladder folds them at lower time — `inline if type_eq(...)` /
`inline if has_impl(...)` collapse to a single branch with no
runtime instructions, perfect for guard-based dispatch inside
pack-fn bodies.

`has_impl`'s protocol arg accepts two shapes:
- plain protocol name: `has_impl(Allocator, CAllocator)` →
  walks `protocol_thunk_map["Allocator\x00CAllocator"]`.
- parameterised call: `has_impl(Into(Block), s64)` →
  builds the param_impl_map key `"Into\x00Block\x00s64"`
  and checks containment. The protocol type-args resolve
  through `resolveTypeArg` so type aliases, generics, and
  pack-indexed types all work as protocol args.

`computeHasImpl` is the shared implementation between the
runtime builtin path and the `tryConstBoolCondition` fast
path so both branches stay in sync.

`examples/168-pack-reflection-intrinsics.sx` exercises every
shape:
- type_name for primitive types.
- type_eq with both equal + unequal cases, including pointer
  types (s64 vs *s64).
- inline-if folding type_eq.
- has_impl with a real plain-protocol impl
  (Allocator/CAllocator → true; Allocator/s64 → false).
- has_impl with a user-defined parameterised protocol
  (Wrap(s64)/s32 → true; mismatched target args → false).

208/208 example tests + `zig build test` green.

Caveat: plain-protocol has_impl uses `protocol_thunk_map`
which is lazily populated when an `xx` cast or protocol
dispatch creates the thunks. For a static check before any
dispatch, that could false-negative. Allocator/CAllocator
works in 168 because stdlib's startup uses CAllocator through
the Allocator protocol — the thunks already exist by the time
has_impl runs. A more robust static check (walk fn_ast_map for
"<T_name>.<method>" entries against the protocol's method
list) is deferred to a follow-up if needed.

LSP "undefined variable" warnings on type names in expression
position (s64, *s64, Wrap(s64), etc. passed to type_eq /
has_impl) are cosmetic — sema doesn't know these intrinsics
accept types as args. Tracked separately.
This commit is contained in:
agra
2026-05-27 17:48:39 +03:00
parent 9137f4158d
commit 8b457ffc44
4 changed files with 149 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
// Variadic heterogeneous type packs — step 3: type-reflection
// intrinsics.
//
// Three comptime helpers used by pack-fn bodies to branch on
// type identity / protocol membership:
//
// type_name(T) -> string // display name of T
// type_eq(T1, T2) -> bool // structural TypeId equality
// has_impl(P, T) -> bool // T has a reachable impl for P
//
// All three fold to compile-time constants and are accepted by
// `tryConstBoolCondition`, so `inline if type_eq(...)` /
// `inline if has_impl(...)` collapse to a single branch at lower
// time — no runtime cost.
//
// `has_impl`'s protocol arg accepts both shapes:
// - plain protocol name: `has_impl(Allocator, CAllocator)`.
// - parameterised call: `has_impl(Wrap(s64), s32)` — the args
// match the impl's protocol type-args exactly.
#import "modules/std.sx";
#import "modules/allocators.sx";
// User-defined parameterised protocol + an impl, so has_impl can
// confirm parameterised matching works with a known-true case.
Wrap :: protocol(Target: Type) {
wrap :: () -> Target;
}
impl Wrap(s64) for s32 {
wrap :: (self: s32) -> s64 => xx self;
}
main :: () -> s32 {
// type_name — display names.
print("{} {} {}\n", type_name(s64), type_name(string), type_name(bool));
// type_eq — structural equality on TypeIds.
print("{} {} {} {}\n",
type_eq(s64, s64),
type_eq(s64, string),
type_eq(*s64, *s64),
type_eq(*s64, *s32));
// inline-if folds type_eq at lower time.
inline if type_eq(s64, s64) {
print("inline-if folded: same\n");
} else {
print("inline-if folded: different\n");
}
// has_impl — plain protocol (Allocator is unary).
print("Allocator/CAllocator: {}\n", has_impl(Allocator, CAllocator));
print("Allocator/s64: {}\n", has_impl(Allocator, s64));
// has_impl — parameterised protocol (Wrap takes a Target type arg).
print("Wrap(s64)/s32: {}\n", has_impl(Wrap(s64), s32));
print("Wrap(s64)/bool: {}\n", has_impl(Wrap(s64), bool));
print("Wrap(bool)/s32: {}\n", has_impl(Wrap(bool), s32));
return 0;
}

View File

@@ -2803,6 +2803,15 @@ pub const Lowering = struct {
}
return false;
}
if (std.mem.eql(u8, cname, "type_eq") and c.args.len >= 2) {
const a = self.resolveTypeArg(c.args[0]);
const b = self.resolveTypeArg(c.args[1]);
return a == b;
}
if (std.mem.eql(u8, cname, "has_impl") and c.args.len >= 2) {
const ty = self.resolveTypeArg(c.args[1]);
return self.computeHasImpl(c.args[0], ty);
}
}
},
else => {},
@@ -2810,6 +2819,50 @@ pub const Lowering = struct {
return null;
}
/// Shared implementation for the `has_impl(P, T)` builtin and its
/// `tryConstBoolCondition` arm. The protocol expression is either:
/// - Plain `Hash` (identifier / type_expr) → walks
/// `protocol_thunk_map["Hash\x00<T>"]`.
/// - Parameterised `Into(Block)` (call) → walks `param_impl_map`
/// keyed by `"<P>\x00<arg_mangled>\x00<T_mangled>"`.
/// Returns false on any malformed protocol-arg shape (caller
/// reports a diagnostic if it wants).
fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool {
switch (proto_node.data) {
.identifier => |id| return self.hasImplPlain(id.name, ty),
.type_expr => |te| return self.hasImplPlain(te.name, ty),
.call => |c| {
const p_name: []const u8 = switch (c.callee.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => return false,
};
// Resolve protocol type args. Each goes through
// `resolveTypeArg` so type aliases / generics / pack-
// indexed types all work as protocol args.
var arg_mangles = std.ArrayList(u8).empty;
defer arg_mangles.deinit(self.alloc);
for (c.args, 0..) |a, i| {
if (i > 0) arg_mangles.append(self.alloc, 0) catch return false;
const aty = self.resolveTypeArg(a);
arg_mangles.appendSlice(self.alloc, self.mangleTypeName(aty)) catch return false;
}
const ty_mangled = self.mangleTypeName(ty);
const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}\x00{s}", .{
p_name, arg_mangles.items, ty_mangled,
}) catch return false;
return self.param_impl_map.contains(key);
},
else => return false,
}
}
fn hasImplPlain(self: *Lowering, p_name: []const u8, ty: TypeId) bool {
const ty_name = self.formatTypeName(ty);
const thunk_key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ p_name, ty_name }) catch return false;
return self.protocol_thunk_map.contains(thunk_key);
}
/// Evaluate a compile-time condition for `inline if`.
/// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`.
fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool {
@@ -8697,6 +8750,31 @@ pub const Lowering = struct {
const sid = self.module.types.internString(tn_str);
return self.builder.constString(sid);
}
if (std.mem.eql(u8, name, "type_eq")) {
// type_eq(T1, T2) → const_bool — comptime TypeId equality.
// TypeIds are interned per structural shape so equality on
// them matches the user's intuition: `type_eq(s64, s64)` is
// true, `type_eq(*s64, *s64)` is true, distinct shapes are
// false. Pack-indexed types (`$args[0]`) resolve through
// `resolveTypeArg` → `resolveTypeWithBindings`.
if (c.args.len < 2) return self.builder.constBool(false);
const a = self.resolveTypeArg(c.args[0]);
const b = self.resolveTypeArg(c.args[1]);
return self.builder.constBool(a == b);
}
if (std.mem.eql(u8, name, "has_impl")) {
// has_impl(P, T) → const_bool. Returns true when type T has
// a reachable impl for protocol P. P is either:
// - plain protocol name (`Hash`, `Eq`) for unary protocols;
// - parameterised call like `Into(Block)` — for protocols
// with type args, the args must be fully spelled.
// Delegates to `computeHasImpl` (shared with the
// `tryConstBoolCondition` arm so `inline if has_impl(...)`
// folds at compile time).
if (c.args.len < 2) return self.builder.constBool(false);
const ty = self.resolveTypeArg(c.args[1]);
return self.builder.constBool(self.computeHasImpl(c.args[0], ty));
}
if (std.mem.eql(u8, name, "is_flags")) {
const ty = self.resolveTypeArg(c.args[0]);
if (!ty.isBuiltin()) {

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,8 @@
s64 string bool
true false true false
inline-if folded: same
Allocator/CAllocator: true
Allocator/s64: false
Wrap(s64)/s32: true
Wrap(s64)/bool: false
Wrap(bool)/s32: false