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)"); + }, } }