ffi M5.A.next.4.0: activate Value.type_tag — opcode + helper + cmp

Wires the dormant `Value.type_tag(TypeId)` variant in interp.zig
so Type values flow through the comptime interpreter as
first-class kind-distinguished entities. No source-language
construction path yet — that's a follow-up. This commit is the
infrastructure foundation.

Audit findings (from interp.zig switch-walk):
- Every `else =>` arm over Value is either already loud
  (`bailDetail` / `error.TypeError`) or a pass-through helper
  (`materializeCtxArg`, `materializeForCall`, `resolveSlotChain`)
  where transit-unchanged is semantically correct for type_tag.
  No new silent paths introduced by activating the variant.
- The three pre-existing `.type_tag => return bailDetail(...)`
  arms (store-at-raw-ptr, deref-non-pointer, unbox-non-aggregate)
  already cover the disallowed paths cleanly.

New plumbing:
- `Op.const_type: TypeId` — dedicated opcode. Never piggybacks
  on `const_int`. Result IR-type is `.any` to signal "untyped
  at runtime" so downstream coercions fail loudly.
- `Builder.constType(tid)` constructor.
- Interp arm emits `Value{ .type_tag = tid }` for the op.
- emit_llvm arm bails loudly + emits an undef-i64 placeholder
  (Type is comptime-only — if a Type ever reached LLVM emit,
  some upstream builder leaked through; the diagnostic + LLVM
  verifier downstream surface the offending site).
- `print.zig` arm prints `const type(<typeName>)`.
- `Value.asTypeId() ?TypeId` helper — the kind-honest accessor
  for Type values. asInt/asFloat/asBool/asString continue to
  return null for `.type_tag` (no silent coercion).
- `evalCmp` arm for `.type_tag, .type_tag` — TypeId equality.
  Mixed `.type_tag` vs `.int` deliberately falls through to
  the typeErrorDetail bail (a Type is not an int).

Tests (src/ir/interp.test.zig):
- `const_type yields type_tag` — confirms the variant is
  produced and that asTypeId/asInt distinguish correctly.
- `type_tag comparison` — exercises cmp_eq on equal and
  unequal pairs, asserts the right bool comes back.

208/208 example tests + `zig build test` green. No user-visible
behaviour change yet — `.type_tag` is constructible from Zig-
side IR builders but no sx-level syntax produces it. Next slice
wires `$args` lowering (or `$args[i]` in expression position)
to emit `const_type` per pack element.
This commit is contained in:
agra
2026-05-27 18:30:17 +03:00
parent 8990edbec8
commit ac60d98f0e
6 changed files with 122 additions and 1 deletions

View File

@@ -647,3 +647,69 @@ test "comptime: widen/narrow passthrough" {
const result = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 42), result.asInt().?);
}
// ── Test: const_type produces a Value.type_tag ──────────────────────────
test "comptime: const_type yields type_tag" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// Build a fn that returns `s64` as a Type-typed Any value (matches
// the .any IR type assigned by `constType`). The interp returns the
// raw Value; we assert on the variant.
_ = b.beginFunction(str(&module, "test_type_tag"), &.{}, .any);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const t = b.constType(.s64);
b.ret(t, .any);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const result = try interp.call(FuncId.fromIndex(0), &.{});
// The Value MUST be a .type_tag, not an .int — proves the variant
// is honestly distinguished. asTypeId returns the inner TypeId;
// asInt MUST return null (no coercion).
try std.testing.expectEqual(@as(?TypeId, .s64), result.asTypeId());
try std.testing.expectEqual(@as(?i64, null), result.asInt());
}
// ── Test: type equality via cmp_eq on .type_tag operands ────────────────
test "comptime: type_tag comparison" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// Returns (s64 == s64) — should yield bool true via the new
// evalCmp arm for .type_tag operands.
_ = b.beginFunction(str(&module, "test_type_eq_true"), &.{}, .bool);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const a = b.constType(.s64);
const c = b.constType(.s64);
const eq = b.cmpEq(a, c);
b.ret(eq, .bool);
b.finalize();
// Different TypeIds: (s64 == s32) should be false.
_ = b.beginFunction(str(&module, "test_type_eq_false"), &.{}, .bool);
const entry2 = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry2);
const a2 = b.constType(.s64);
const c2 = b.constType(.s32);
const eq2 = b.cmpEq(a2, c2);
b.ret(eq2, .bool);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const r_true = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(true, r_true.asBool().?);
const r_false = try interp.call(FuncId.fromIndex(1), &.{});
try std.testing.expectEqual(false, r_false.asBool().?);
}