From 9600ba5cdcad0e18db5c7467e4371ddd745ccd05 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 18:43:10 +0300 Subject: [PATCH] ffi M5.A.next.4.1: interp arms for reflection builtins on .type_tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second slice of the .type_tag activation. The reflection intrinsics (`type_name`, `type_eq`, `has_impl`) now have interp-time implementations that read `.type_tag` Values directly. Today's lower-time fast path (folding to `const_string`/`const_bool` when the type arg is statically resolvable) stays — these interp arms are the fallback path for when lowering emits a real `builtin_call` because the arg is interp-time-only (e.g. `args[i]` inside a builder body where the pack element is bound at interp execution). Plumbing: - New BuiltinId entries: `type_name`, `type_eq`, `has_impl`. - Interp arms in `execBuiltinInner`: - `type_name(t)`: reads `.type_tag` via `asTypeId`, looks up via `module.types.typeName`, dupes the slice into the interp allocator, returns `.string`. Non-`.type_tag` arg → `bailDetail` ("argument is not a Type value"). - `type_eq(a, b)`: both args must be `.type_tag`; compares TypeIds. Either side missing → `bailDetail`. - `has_impl(P, T)`: bails with a "not yet wired" message — interp-time has_impl needs a queryable snapshot of the host's `protocol_thunk_map` + `param_impl_map`, which is its own follow-up slice. Static-arg has_impl still works via the lower-time `tryConstBoolCondition` fast path. - emit_llvm: explicit arms for the three new builtins that log + map to undef-i64 (Type values are comptime-only; if one of these reaches LLVM emit, lowering produced wrong IR — the LLVM verifier downstream surfaces the offending site). Three new Zig unit tests in interp.test.zig: - `type_name builtin on type_tag` — emits a `builtin_call` to `type_name` with a `const_type(s64)` operand, asserts the result is the string "s64". - `type_eq builtin on type_tag values` — two equal Type operands compare equal. - (Pre-existing) `const_type yields type_tag` + `type_tag comparison` from 4.0 still pass. 208/208 example tests + `zig build test` green. No source- language path constructs `.type_tag` yet — the foundation is ready for the `$args`-in-expression-position slice that turns it on for users. --- src/ir/emit_llvm.zig | 12 +++++++++++ src/ir/inst.zig | 11 ++++++++++ src/ir/interp.test.zig | 47 ++++++++++++++++++++++++++++++++++++++++++ src/ir/interp.zig | 37 +++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 9c13ba7..8f5bc39 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -2963,6 +2963,18 @@ pub const LLVMEmitter = struct { _ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, ""); self.advanceRefCounter(); }, + .type_name, .type_eq, .has_impl => { + // Comptime-only reflection builtins. Reaching + // LLVM emit means lowering DIDN'T fold the call + // (static-arg fast path) AND lowering emitted a + // real `builtin_call` — but the resulting IR + // shouldn't survive past the comptime interp + // that's supposed to consume it. Loud failure: + // log + undef placeholder so the LLVM verifier + // catches downstream use. + std.debug.print("emit_llvm: comptime reflection builtin '{s}' reached runtime emit — Type values are interp-only.\n", .{@tagName(bi.builtin)}); + self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); + }, else => { // size_of, cast — handled by lowering or codegen glue self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 1989172..59eaeb5 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -367,6 +367,17 @@ pub const BuiltinId = enum(u16) { type_of, alloc, dealloc, + // Comptime-only reflection builtins. Today's `tryLowerReflectionCall` + // folds these at lower time when the type argument is statically + // resolvable — emits a `const_string` / `const_bool` directly. + // These BuiltinId entries are the FALLBACK path: when the arg is + // a runtime/interp-time value (e.g. `args[i]` inside a builder + // body, carrying a `.type_tag(TypeId)` only at interp execution), + // lowering emits a `builtin_call` to one of these. The interp + // implements them; emit_llvm bails (Type is comptime-only). + type_name, + type_eq, + has_impl, }; pub const CompilerCall = struct { diff --git a/src/ir/interp.test.zig b/src/ir/interp.test.zig index 2f2f7d6..4901481 100644 --- a/src/ir/interp.test.zig +++ b/src/ir/interp.test.zig @@ -713,3 +713,50 @@ test "comptime: type_tag comparison" { const r_false = try interp.call(FuncId.fromIndex(1), &.{}); try std.testing.expectEqual(false, r_false.asBool().?); } + +// ── Test: type_name builtin reads .type_tag, returns the typeName ─────── + +test "comptime: type_name builtin on type_tag" { + const alloc = std.testing.allocator; + var module = Module.init(alloc); + defer module.deinit(); + var b = Builder.init(&module); + + _ = b.beginFunction(str(&module, "test_type_name"), &.{}, .string); + const entry = b.appendBlock(str(&module, "entry"), &.{}); + b.switchToBlock(entry); + const t = b.constType(.s64); + var args = [_]inst_mod.Ref{t}; + const r = b.callBuiltin(.type_name, &args, .string); + b.ret(r, .string); + b.finalize(); + + var interp = Interpreter.init(&module, alloc); + defer interp.deinit(); + const result = try interp.call(FuncId.fromIndex(0), &.{}); + try std.testing.expectEqualStrings("s64", result.asString(&interp).?); +} + +// ── Test: type_eq builtin on two .type_tag operands ──────────────────── + +test "comptime: type_eq builtin on type_tag values" { + const alloc = std.testing.allocator; + var module = Module.init(alloc); + defer module.deinit(); + var b = Builder.init(&module); + + _ = b.beginFunction(str(&module, "test_type_eq_builtin"), &.{}, .bool); + const entry = b.appendBlock(str(&module, "entry"), &.{}); + b.switchToBlock(entry); + const a = b.constType(.string); + const c = b.constType(.string); + var args = [_]inst_mod.Ref{ a, c }; + const r = b.callBuiltin(.type_eq, &args, .bool); + b.ret(r, .bool); + b.finalize(); + + var interp = Interpreter.init(&module, alloc); + defer interp.deinit(); + const result = try interp.call(FuncId.fromIndex(0), &.{}); + try std.testing.expectEqual(true, result.asBool().?); +} diff --git a/src/ir/interp.zig b/src/ir/interp.zig index fce6d1a..9f6b93a 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -1740,6 +1740,43 @@ pub const Interpreter = struct { .type_of => return bailDetail("comptime #builtin type_of: handled at lowering, not the interp"), .alloc => return bailDetail("comptime #builtin alloc unused (use context.allocator.alloc)"), .dealloc => return bailDetail("comptime #builtin dealloc unused (use context.allocator.dealloc)"), + + // ── Comptime reflection (Type-as-Value path) ───────── + // These are only reached when lower.zig emitted a real + // builtin_call — i.e. the type argument was NOT statically + // resolvable (e.g. inside a builder body where `args[i]` is + // a `.type_tag(TypeId)` Value bound at interp time). Static + // calls fold to `const_string` / `const_bool` at lower time + // and never hit this dispatch. + .type_name => { + if (bi.args.len < 1) return bailDetail("comptime type_name: missing argument"); + const arg = frame.getRef(bi.args[0]); + const tid = arg.asTypeId() orelse return bailDetail("comptime type_name: argument is not a Type value (expected `.type_tag`, got a different Value kind)"); + const name = self.module.types.typeName(tid); + // Copy the slice into the interp's allocator so it + // outlives any TypeTable churn during the rest of the + // interp execution. The TypeTable's strings are stable + // for now but copying is the safe pattern. + const owned = self.alloc.dupe(u8, name) catch return error.CannotEvalComptime; + return .{ .value = .{ .string = owned } }; + }, + .type_eq => { + if (bi.args.len < 2) return bailDetail("comptime type_eq: needs two Type arguments"); + const a = frame.getRef(bi.args[0]).asTypeId() orelse return bailDetail("comptime type_eq: first argument is not a Type value"); + const b = frame.getRef(bi.args[1]).asTypeId() orelse return bailDetail("comptime type_eq: second argument is not a Type value"); + return .{ .value = .{ .boolean = a == b } }; + }, + .has_impl => { + // has_impl at interp time needs access to the host's + // protocol-registration maps (protocol_thunk_map + + // param_impl_map). These live on `Lowering`, not on + // the Interpreter. Plumbing a queryable snapshot is + // its own slice — until then, bail loudly so the user + // gets a clear "not yet wired" message instead of a + // silent false. Static-arg has_impl still works via + // `tryConstBoolCondition` in lower.zig. + return bailDetail("comptime has_impl: interp-time evaluation not yet wired (use static type args for now — they fold at lower time)"); + }, } }