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:
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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().?);
|
||||
}
|
||||
|
||||
@@ -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 ────────────────────────────────────
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }),
|
||||
|
||||
Reference in New Issue
Block a user