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() }),