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:
62
examples/168-pack-reflection-intrinsics.sx
Normal file
62
examples/168-pack-reflection-intrinsics.sx
Normal 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;
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
1
tests/expected/168-pack-reflection-intrinsics.exit
Normal file
1
tests/expected/168-pack-reflection-intrinsics.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
8
tests/expected/168-pack-reflection-intrinsics.txt
Normal file
8
tests/expected/168-pack-reflection-intrinsics.txt
Normal 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
|
||||
Reference in New Issue
Block a user