From ac60d98f0e143fc19de1d846cf694fd3dcc5278a Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 18:30:17 +0300 Subject: [PATCH] =?UTF-8?q?ffi=20M5.A.next.4.0:=20activate=20Value.type=5F?= =?UTF-8?q?tag=20=E2=80=94=20opcode=20+=20helper=20+=20cmp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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()`. - `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. --- src/ir/emit_llvm.zig | 11 +++++++ src/ir/inst.zig | 6 ++++ src/ir/interp.test.zig | 66 ++++++++++++++++++++++++++++++++++++++++++ src/ir/interp.zig | 29 ++++++++++++++++++- src/ir/module.zig | 10 +++++++ src/ir/print.zig | 1 + 6 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 3c778c1..9c13ba7 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1668,6 +1668,17 @@ pub const LLVMEmitter = struct { self.mapRef(llvm_val); } }, + .const_type => |tid| { + // Type values are comptime-only — they MUST be erased by + // the time we emit LLVM. Reaching here means a Type leaked + // into a runtime path (e.g. a builder returned a Type from + // a fn the compiler didn't strip). Loud failure: emit an + // undef placeholder so the verifier catches downstream + // use, AND log the offending TypeId so the diagnostic is + // actionable. + std.debug.print("emit_llvm: Type value reached runtime (TypeId={}). Type is comptime-only; check the builder path that produced this.\n", .{@intFromEnum(tid)}); + self.mapRef(c.LLVMGetUndef(self.cached_i64)); + }, // ── Arithmetic ───────────────────────────────────────── .add => |bin| { diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 881b86d..1989172 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -85,6 +85,12 @@ pub const Op = union(enum) { const_string: StringId, const_null, const_undef, // `---` undefined initializer + /// Comptime-only Type value. Carried as a `Value.type_tag(TypeId)` + /// in the interpreter. NEVER emitted to LLVM — types are erased + /// after lowering. `emit_llvm` bails loudly if it sees one, + /// surfacing a "Type value reached runtime" diagnostic instead of + /// silently lowering to a stale int. + const_type: TypeId, // ── Arithmetic ────────────────────────────────────────────────── add: BinOp, diff --git a/src/ir/interp.test.zig b/src/ir/interp.test.zig index 8e62d66..2f2f7d6 100644 --- a/src/ir/interp.test.zig +++ b/src/ir/interp.test.zig @@ -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().?); +} diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 47aeb00..fce6d1a 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -77,6 +77,18 @@ pub const Value = union(enum) { return self == .null_val; } + /// Extract the TypeId from a first-class Type value. Returns null + /// for anything else — including `.int(N)` where N happens to be + /// a valid TypeId enum value. The kinds are distinct: a Type IS + /// NOT an int. Use this helper instead of `asInt` when reading a + /// TypeId out of a Value to keep the kind-distinction honest. + pub fn asTypeId(self: Value) ?TypeId { + return switch (self) { + .type_tag => |id| id, + else => 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) { @@ -587,6 +599,7 @@ pub const Interpreter = struct { .const_string => |sid| return .{ .value = .{ .string = self.module.types.getString(sid) } }, .const_null => return .{ .value = .null_val }, .const_undef => return .{ .value = .undef }, + .const_type => |tid| return .{ .value = .{ .type_tag = tid } }, // ── Arithmetic ────────────────────────────────────── .add => |b| return .{ .value = try self.evalArith(frame, b, .add) }, @@ -1470,7 +1483,21 @@ pub const Interpreter = struct { } } - return typeErrorDetail("comptime comparison: operand pair has no shared comparable shape (int/float/bool/string)"); + // Type-as-value equality. Compares TypeIds structurally. + // `.type_tag` vs `.int(N)` deliberately does NOT compare — + // a Type is not an int even if the underlying enum value + // matches; falls through to the typeErrorDetail below. + if (lhs.asTypeId()) |la| { + if (rhs.asTypeId()) |ra| { + return switch (cop) { + .eq => la == ra, + .ne => la != ra, + else => return error.TypeError, + }; + } + } + + return typeErrorDetail("comptime comparison: operand pair has no shared comparable shape (int/float/bool/string/type)"); } // ── Slot chain resolution ──────────────────────────────────── diff --git a/src/ir/module.zig b/src/ir/module.zig index bb144a0..208324f 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -378,6 +378,16 @@ pub const Builder = struct { return self.emit(.const_undef, ty); } + /// Comptime-only Type value. Produces a `Value.type_tag(tid)` in + /// the interp; bails loudly in LLVM emit (Type is comptime-only). + /// The result-Ref's IR type is `.any` to flag the value as + /// "untyped at runtime" — emitters that try to coerce it will + /// fail loudly rather than silently materialise the TypeId as an + /// int. + pub fn constType(self: *Builder, tid: TypeId) Ref { + return self.emit(.{ .const_type = tid }, .any); + } + // ── Arithmetic ────────────────────────────────────────────────── pub fn add(self: *Builder, lhs: Ref, rhs: Ref, ty: TypeId) Ref { diff --git a/src/ir/print.zig b/src/ir/print.zig index 42d0adb..9135e19 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -143,6 +143,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write }, .const_null => try writer.writeAll("const null : "), .const_undef => try writer.writeAll("const undef : "), + .const_type => |tid| try writer.print("const type({s}) : ", .{tt.typeName(tid)}), // ── Arithmetic ────────────────────────────────────────── .add => |b| try writer.print("add %{d}, %{d} : ", .{ b.lhs.index(), b.rhs.index() }),