fix(ir): reflection builtins on an Any read its runtime tag, not payload [F0.8]

`type_name` / `type_is_unsigned` on an `Any` argument unconditionally read
the Any's payload as a TypeId index. That is correct only when the Any holds
a Type value (`{ .any, tid }`); for an Any holding a runtime *value*
(`av : Any = 6`, tag s64, payload 6) it returned `types[6]` — `type_name(av)`
gave "u8" and `type_is_unsigned(av)` gave true.

Both backends now branch on the Any's runtime type-tag: tag == `.any` → the
box is a Type value, use the payload as the TypeId; otherwise the tag IS the
held value's type. So `type_name(av)` → "s64", `type_is_unsigned(av)` → false,
while `type_name(type_of(x))` still names the held type. The `{}` formatter is
unchanged (it already passed `type_of(val)`, a proper Type value).

- src/ir/interp.zig: shared `Value.reflectTypeId` tag-branching resolver; the
  `type_name` / `type_is_unsigned` interp arms route through it.
- src/backend/llvm/ops.zig: shared `Ops.reflectArgTypeId` emits
  extractvalue-tag / icmp-eq-.any / select for the runtime path; both
  reflection arms route through it. The two backends agree.
- examples/0164-types-reflection-any-tag.sx: regression pinning type_name /
  type_is_unsigned / print on an Any holding a value vs a Type.
- src/ir/interp.test.zig: unit test for `reflectTypeId`.
- 22 .ir snapshots: the new select appears in every std-importing program's
  IR (any_to_string embeds these builtins) — benign, verified structurally
  identical apart from the three new instructions.
- issues/0090, specs.md: documented the Any-tag rule.
This commit is contained in:
agra
2026-06-05 12:09:52 +03:00
parent b053c64149
commit 5f64ee4426
31 changed files with 3379 additions and 3105 deletions

View File

@@ -35,6 +35,7 @@ const BlockParam = ir_inst.BlockParam;
const FieldReflect = ir_inst.FieldReflect;
const TypeId = ir_types.TypeId;
const StringId = ir_types.StringId;
const Ref = ir_inst.Ref;
/// Instruction-emission handlers for `emitInst`: every opcode group — the
/// constant, arithmetic, bitwise, comparison, logical, memory, globals,
@@ -972,6 +973,35 @@ pub const Ops = struct {
}
// ── Call extensions ───────────────────────────────────────
/// Resolve the `TypeId` (as a runtime `i64`) that a dynamic
/// `type_name` / `type_is_unsigned` must operate on. A reflection
/// builtin reads an `Any`'s runtime TYPE-TAG, never its raw payload:
/// - `.bare`: a `Type` value already lowered to a bare i64 `TypeId`
/// index (an unboxed direct call site) → the value itself.
/// - `.boxed`: an `Any` aggregate `{ tag, value }`. When the tag is
/// `.any`, the box carries a *Type value* (the `{ .any, tid }` shape
/// `const_type` / `type_of` produce) → the TypeId is the payload.
/// Otherwise the box carries a *runtime value* whose type IS the tag
/// → use the tag as the TypeId. This is what makes `type_name(av)`
/// for `av : Any = 6` report `s64` (the held value's type), while
/// `type_name(type_of(x))` still names the held type.
/// `.unresolved` is a hard tripwire: a type-resolution failure reached
/// emission without a diagnostic.
fn reflectArgTypeId(self: Ops, arg_ref: Ref, comptime label: []const u8) c.LLVMValueRef {
const arg_val = self.e.resolveRef(arg_ref);
return switch (self.e.reflectArgRepr(arg_ref)) {
.unresolved => @panic(label ++ ": reflection arg IR-type unresolved — a type-resolution failure reached LLVM emission without a diagnostic"),
.bare => arg_val,
.boxed => blk: {
const tag = c.LLVMBuildExtractValue(self.e.builder, arg_val, 0, "refl.tag");
const payload = c.LLVMBuildExtractValue(self.e.builder, arg_val, 1, "refl.val");
const any_tag = c.LLVMConstInt(self.e.cached_i64, @intCast(TypeId.any.index()), 0);
const holds_type = c.LLVMBuildICmp(self.e.builder, c.LLVMIntEQ, tag, any_tag, "refl.istype");
break :blk c.LLVMBuildSelect(self.e.builder, holds_type, payload, tag, "refl.tid");
},
};
}
pub fn emitCallBuiltin(self: Ops, instruction: *const Inst, bi: BuiltinCall) void {
// Builtins that map to libc functions or LLVM intrinsics
switch (bi.builtin) {
@@ -1010,26 +1040,12 @@ pub const Ops = struct {
self.e.advanceRefCounter();
},
.type_name => {
// Dynamic `type_name(t)` at runtime: extract
// the TypeId from the arg (an Any-boxed Type
// value: tag=`.s64.index()`, value=tid), GEP
// into the compiler-emitted `__sx_type_names`
// global, load the string. The arg's LLVM
// shape is the `{i64, i64}` Any aggregate
// (because the IR-side arg type is `.any`
// when boxed); for unboxed direct call sites
// (the arg IR type is `.s64` from
// `const_type`), the value IS the TypeId
// index directly.
const arg_ref = bi.args[0];
const arg_val = self.e.resolveRef(arg_ref);
const tid_idx = switch (self.e.reflectArgRepr(arg_ref)) {
.unresolved => @panic("type_name: reflection arg IR-type unresolved — a type-resolution failure reached LLVM emission without a diagnostic"),
// Boxed: extract value field from the Any aggregate.
.boxed => c.LLVMBuildExtractValue(self.e.builder, arg_val, 1, "tn.tid"),
// Bare i64 (TypeId index).
.bare => arg_val,
};
// Dynamic `type_name(t)` at runtime: resolve the TypeId
// the arg denotes (reading an `Any`'s runtime type-tag,
// not its payload — see `reflectArgTypeId`), GEP into the
// compiler-emitted `__sx_type_names` global, load the
// string.
const tid_idx = self.reflectArgTypeId(bi.args[0], "type_name");
const arr_global = self.e.reflection().getOrBuildTypeNameArray();
const arr_len = self.e.type_name_array_len;
const string_ty = self.e.getStringStructType();
@@ -1065,17 +1081,12 @@ pub const Ops = struct {
self.e.mapRef(eq_res);
},
.type_is_unsigned => {
// Dynamic `type_is_unsigned(t)`: extract the TypeId from
// the arg (Any-boxed Type → value field, or bare i64
// index), GEP into the `__sx_type_is_unsigned` table, load
// the i1. Mirrors the `type_name` runtime lookup.
const arg_ref = bi.args[0];
const arg_val = self.e.resolveRef(arg_ref);
const tid_idx = switch (self.e.reflectArgRepr(arg_ref)) {
.unresolved => @panic("type_is_unsigned: reflection arg IR-type unresolved — a type-resolution failure reached LLVM emission without a diagnostic"),
.boxed => c.LLVMBuildExtractValue(self.e.builder, arg_val, 1, "tiu.tid"),
.bare => arg_val,
};
// Dynamic `type_is_unsigned(t)`: resolve the TypeId the arg
// denotes (reading an `Any`'s runtime type-tag, not its
// payload — see `reflectArgTypeId`), GEP into the
// `__sx_type_is_unsigned` table, load the i1. Mirrors the
// `type_name` runtime lookup.
const tid_idx = self.reflectArgTypeId(bi.args[0], "type_is_unsigned");
const arr_global = self.e.reflection().getOrBuildTypeIsUnsignedArray();
const arr_len = self.e.type_is_unsigned_array_len;
const arr_ty = c.LLVMArrayType(self.e.cached_i1, arr_len);