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

@@ -90,6 +90,34 @@ pub const Value = union(enum) {
};
}
/// Resolve the `TypeId` a dynamic `type_name` / `type_is_unsigned` must
/// operate on, honoring the rule that a reflection builtin reads an
/// `Any`'s runtime TYPE-TAG, never its raw payload:
/// - a native `.type_tag(tid)` Value → `tid` (a first-class Type value).
/// - an `Any` aggregate `{ tag, value }`: when the tag is `.any`, the
/// box carries a *Type value* (the `box_any(.., .any)` / `const_type`
/// shape) → the TypeId is the payload; otherwise the box carries a
/// *runtime value* whose type IS the tag → the tag is the TypeId. This
/// makes `type_name(av)` for `av : Any = 6` report `s64` (the held
/// value's type) while `type_name(type_of(x))` still names the type.
/// Returns null when `self` is neither shape (the caller bails loudly).
pub fn reflectTypeId(self: Value) ?TypeId {
if (self.asTypeId()) |t| return t;
if (self == .aggregate) {
const fields = self.aggregate;
if (fields.len >= 2) {
const tag = fields[0].asInt() orelse return null;
if (tag == @as(i64, @intCast(TypeId.any.index()))) {
if (fields[1].asTypeId()) |t| return t;
if (fields[1].asInt()) |iv| return TypeId.fromIndex(@intCast(iv));
return null;
}
return TypeId.fromIndex(@intCast(tag));
}
}
return null;
}
/// Get the string content, whether from a literal or a heap-backed string aggregate.
pub fn asString(self: Value, interp: *const Interpreter) ?[]const u8 {
return switch (self) {
@@ -1870,22 +1898,14 @@ pub const Interpreter = struct {
.type_name => {
if (bi.args.len < 1) return bailDetail("comptime type_name: missing argument");
const arg = frame.getRef(bi.args[0]);
// Accept either a bare `.type_tag` Value (the
// comptime-native form) or an Any-boxed Type
// (`.aggregate { tag: int, value: .type_tag }`)
// — the latter shape is what `box_any` produces
// when const_type values flow through a `.any`-typed
// slice or struct field.
const tid = blk: {
if (arg.asTypeId()) |t| break :blk t;
if (arg == .aggregate) {
const fields = arg.aggregate;
if (fields.len >= 2) {
if (fields[1].asTypeId()) |t| break :blk t;
}
}
return bailDetail("comptime type_name: argument is not a Type value (expected `.type_tag` or Any-boxed Type)");
};
// A bare `.type_tag` Value (the comptime-native form), an
// Any-boxed Type (`{ .any, tid }`), or an Any holding a
// runtime value (`{ tag, value }`, where the tag IS the
// value's type). `reflectTypeId` reads the runtime tag so
// `type_name(av)` for `av : Any = 6` names `s64`, not the
// type whose index equals the payload.
const tid = arg.reflectTypeId() orelse
return bailDetail("comptime type_name: argument is not a Type value or boxed value (expected `.type_tag` or Any aggregate)");
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
@@ -1903,22 +1923,14 @@ pub const Interpreter = struct {
.type_is_unsigned => {
if (bi.args.len < 1) return bailDetail("comptime type_is_unsigned: missing argument");
const arg = frame.getRef(bi.args[0]);
// Accept a bare `.type_tag`, an Any-boxed Type (`{tag,
// .type_tag}`), or the `type_of(x)` shape (`{.int(any),
// .int(typeid)}`) — the last is what `any_to_string`'s
// `case int:` passes, where the inner TypeId is carried
// as a plain integer rather than a `.type_tag`.
const tid = blk: {
if (arg.asTypeId()) |t| break :blk t;
if (arg == .aggregate) {
const fields = arg.aggregate;
if (fields.len >= 2) {
if (fields[1].asTypeId()) |t| break :blk t;
if (fields[1].asInt()) |iv| break :blk TypeId.fromIndex(@intCast(iv));
}
}
return bailDetail("comptime type_is_unsigned: argument is not a Type value (expected `.type_tag`, Any-boxed Type, or `type_of(x)`)");
};
// A bare `.type_tag`, an Any-boxed Type (`{ .any, tid }`,
// the `type_of(x)` shape), or an Any holding a runtime value
// (`{ tag, value }`, where the tag IS the value's type).
// `reflectTypeId` reads the runtime tag so
// `type_is_unsigned(av)` for `av : Any = 6` answers about
// `s64`, not the type whose index equals the payload.
const tid = arg.reflectTypeId() orelse
return bailDetail("comptime type_is_unsigned: argument is not a Type value or boxed value (expected `.type_tag` or Any aggregate)");
return .{ .value = .{ .boolean = self.module.types.isUnsignedInt(tid) } };
},
.has_impl => {