ffi M5.A.next.4.1: interp arms for reflection builtins on .type_tag
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.
This commit is contained in:
@@ -2963,6 +2963,18 @@ pub const LLVMEmitter = struct {
|
|||||||
_ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, "");
|
_ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, "");
|
||||||
self.advanceRefCounter();
|
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 => {
|
else => {
|
||||||
// size_of, cast — handled by lowering or codegen glue
|
// size_of, cast — handled by lowering or codegen glue
|
||||||
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
|
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
|
||||||
|
|||||||
@@ -367,6 +367,17 @@ pub const BuiltinId = enum(u16) {
|
|||||||
type_of,
|
type_of,
|
||||||
alloc,
|
alloc,
|
||||||
dealloc,
|
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 {
|
pub const CompilerCall = struct {
|
||||||
|
|||||||
@@ -713,3 +713,50 @@ test "comptime: type_tag comparison" {
|
|||||||
const r_false = try interp.call(FuncId.fromIndex(1), &.{});
|
const r_false = try interp.call(FuncId.fromIndex(1), &.{});
|
||||||
try std.testing.expectEqual(false, r_false.asBool().?);
|
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().?);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1740,6 +1740,43 @@ pub const Interpreter = struct {
|
|||||||
.type_of => return bailDetail("comptime #builtin type_of: handled at lowering, not the interp"),
|
.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)"),
|
.alloc => return bailDetail("comptime #builtin alloc unused (use context.allocator.alloc)"),
|
||||||
.dealloc => return bailDetail("comptime #builtin dealloc unused (use context.allocator.dealloc)"),
|
.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)");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user