P5.7 Step C: delete interp.zig — the comptime VM is the sole evaluator

The legacy tagged-Value Interpreter is gone. Relocate the Value result-DTO
+ decodeVariantElements into a new comptime_value.zig (the VM<->host
materialization boundary); repoint comptime_vm/emit_llvm/ir-barrel Value to
it and BuildConfig to compiler_hooks; delete the dead valueToReg bridge;
slim compiler_lib.zig to just the name registry (BoundFn{sx_name} + bound_fns
+ findFn — weldedCompilerFn only validates names); simplify printInterpBailDiag
to comptime_vm.last_bail_reason; drop the unused interp_mod import in lower.zig.
rm src/ir/interp.zig + interp.test.zig.

Value is relocated (not eliminated): it survives only as the slim result DTO
at the VM->valueToLLVMConst boundary; the execution-time marshaling the VM
pivot targeted is gone. Drop dead Value.asString/reflectTypeId.

706/0 corpus + 476/476 unit.
This commit is contained in:
agra
2026-06-19 20:05:57 +03:00
parent 103a156b26
commit 7b8be86834
11 changed files with 217 additions and 3721 deletions

View File

@@ -431,6 +431,30 @@ when reached (sentinels or accessor fns; see the design doc Risks).
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
## Log
- **P5.7 Step C — DELETED `interp.zig` (the legacy tagged-`Value` interpreter); the VM is the SOLE comptime
evaluator (2026-06-19).** Five green commits. **C1** (`#insert` → VM): `evalComptimeString` was the last
caller of `Interpreter.call`; routed through `comptime_vm.tryEval` (the VM bails-not-panics on malformed
lowering-time IR like 0737's `ret Ref.none`; `regToValue` dupes the result string into the lowering allocator).
**C2a** (ops.zig inline comptime-call fold → VM): the `emitCall` zero-arg comptime-callee fold now uses
`tryEval`. **C2b** (emit_llvm): dropped the `*const Interpreter` materialization param from `valueToLLVMConst`/
`serializeAggregateValue` (it was used ONLY for the `.heap_ptr` data arm, which the VM's `regToValue` never
produces) + the dead `interp_inst`. **C3** (the atomic delete, done by a delegated worker + independently
verified): moved the `Value` result-DTO + `decodeVariantElements` into a new `src/ir/comptime_value.zig`
(the VM↔host materialization boundary type); repointed `comptime_vm`/`emit_llvm`/`ir.zig`-barrel `Value` to it
and `BuildConfig` to `compiler_hooks`; deleted the dead `valueToReg` bridge; slimmed `compiler_lib.zig` to just
the name registry (`BoundFn{sx_name}` + `bound_fns` + `findFn`, all names preserved — `weldedCompilerFn` only
validates names; deleted `FnHandler` + all `handle*` + the `Interpreter`/`Value`/`InterpError` imports);
simplified `main.printInterpBailDiag` to use only `comptime_vm.last_bail_reason`; dropped the unused
`interp_mod` import in `lower.zig`; **`rm src/ir/interp.zig` (2383 lines) + `src/ir/interp.test.zig` (844
lines)** + their barrel entries. **DEVIATION from the plan's literal "delete Value":** `Value` is RELOCATED
(not eliminated) as the slim result/materialization DTO — the byte-addressable VM executes natively, and
`Value` survives ONLY at the VM→`valueToLLVMConst` boundary (the marshaling the pivot killed was at EXECUTION
time, which is gone). Eliminating it entirely (materializing LLVM consts straight from VM `Machine` bytes) is a
larger, riskier rewrite deferred as optional follow-up; the plan's PRIMARY goal — ONE evaluator, no legacy
interpreter, no fallback — is fully met. **706/0 corpus + 476/476 unit** (24 from the deleted interp unit
tests + 1 `valueToReg` round-trip test). Also dropped dead `Value.asString`/`reflectTypeId` (no callers).
NEXT: Step D — re-express `define`/`make_enum` as sx over the compiler-API (they were legacy interp arms);
Step E — land the 0141 repro + finalize.
- **P5.7 Step B — deleted the `#compiler`/`compiler_call`/hook-Registry mechanism end-to-end (2026-06-19).**
All superseded by `abi(.compiler)` VM-native dispatch (P5.5) — no sx code emits any of it. Two green commits:
**B1** (`e2971f2`) removed the `compiler_call` IR op: the op variant + `CompilerCall` struct (`inst.zig`), the

View File

@@ -1,149 +1,98 @@
//! The comptime `compiler` library's function bridge — the curated set of the
//! The comptime `compiler` library's name registry — the curated set of the
//! compiler's own functions reachable from comptime sx via
//! `abi(.zig) extern compiler`. See `current/PLAN-COMPILER-VM.md`.
//!
//! **This registry IS the safety boundary.** Only the functions registered here
//! are bindable from user comptime code; a name not on the export list is
//! rejected at declaration (`weldedCompilerFn`), and the interpreter dispatches a
//! welded call to the matching Zig handler instead of dlsym.
//! **This registry IS the safety boundary.** Only the names registered here are
//! bindable from user comptime code; a name not on the export list is rejected
//! at declaration (`weldedCompilerFn`). The comptime VM
//! (`comptime_vm.callCompilerFn`) services every welded call by name — this file
//! only carries the list of recognized names.
//!
//! **Direction note (2026-06-17 pivot).** The byte-weld of TYPES (sx structs whose
//! layout was validated to mirror the compiler's Zig records) was stripped — it
//! bolted a parallel layout regime + hand-marshaling onto a comptime value model
//! that isn't bytes. The replacement is a comptime VM where values are
//! native bytes, so the compiler-API needs no weld/validation/marshaling (Phase 3
//! of the plan re-homes the type/function exposure on that VM). `intern`/`text_of`
//! survive here as the first compiler-call seed: clean scalar host-calls (string in,
//! handle out), no weld involved.
//! native bytes, so the compiler-API needs no weld/validation/marshaling.
const std = @import("std");
const types = @import("types.zig");
const interp_mod = @import("interp.zig");
const Value = interp_mod.Value;
const Interpreter = interp_mod.Interpreter;
const InterpError = interp_mod.InterpError;
const StringId = types.StringId;
/// The name of the only compiler library. A `fn abi(.zig) extern <lib>` with a
/// different `<lib>` is rejected — `compiler` is the sole comptime bind source.
pub const lib_name = "compiler";
// ── Functions (comptime-only, host-call bridged) ────────────────────────────
/// A welded `compiler` function: dispatched under the comptime interpreter to its
/// Zig handler (never dlsym'd). The handler receives the interpreter (for the
/// string pool / type table) and the already-evaluated argument `Value`s, and
/// returns the result `Value`.
pub const FnHandler = *const fn (interp: *Interpreter, args: []const Value) InterpError!Value;
// ── Functions (comptime-only, serviced by the comptime VM) ──────────────────
pub const BoundFn = struct {
sx_name: []const u8,
handler: FnHandler,
};
/// The compiler-function export list. The `StringId` round-trip readers are the
/// seed; the type-table API (lookup / register) is re-homed onto the comptime
/// VM in Phase 3 of `PLAN-COMPILER-VM.md`.
/// The compiler-function export list. Every entry is serviced by name in
/// `comptime_vm.callCompilerFn`; `weldedCompilerFn` consults this list to decide
/// whether a `abi(.compiler)` name is a recognized compiler-API function.
pub const bound_fns = [_]BoundFn{
.{ .sx_name = "intern", .handler = handleIntern },
.{ .sx_name = "text_of", .handler = handleTextOf },
.{ .sx_name = "find_type", .handler = handleFindType },
.{ .sx_name = "type_field_count", .handler = handleTypeFieldCount },
.{ .sx_name = "type_nominal_name", .handler = handleTypeNominalName },
.{ .sx_name = "type_field_name", .handler = handleTypeFieldName },
.{ .sx_name = "type_field_type", .handler = handleTypeFieldType },
.{ .sx_name = "type_kind", .handler = handleTypeKind },
.{ .sx_name = "type_field_value", .handler = handleTypeFieldValue },
.{ .sx_name = "intern" },
.{ .sx_name = "text_of" },
.{ .sx_name = "find_type" },
.{ .sx_name = "type_field_count" },
.{ .sx_name = "type_nominal_name" },
.{ .sx_name = "type_field_name" },
.{ .sx_name = "type_field_type" },
.{ .sx_name = "type_kind" },
.{ .sx_name = "type_field_value" },
// ── write side (lowering-time, mints into the type table) ────────────────
.{ .sx_name = "declare_type", .handler = handleDeclareType },
.{ .sx_name = "pointer_to", .handler = handlePointerTo },
.{ .sx_name = "register_type", .handler = handleRegisterType },
.{ .sx_name = "declare_type" },
.{ .sx_name = "pointer_to" },
.{ .sx_name = "register_type" },
// ── BuildOptions (migrated off `#compiler` onto `abi(.compiler)`) ─────────
.{ .sx_name = "build_options", .handler = handleBuildOptions },
.{ .sx_name = "on_build", .handler = handleOnBuild },
.{ .sx_name = "build_options" },
.{ .sx_name = "on_build" },
// ── build-pipeline metadata queries (Phase 5.2) ──────────────────────────
// VM-only: the post-link callback that calls these always runs on the VM
// (`core.invokeByFuncId`), so `comptime_vm.callCompilerFn` services them and
// these legacy handlers are never reached (they bail loudly rather than
// fabricate a silent empty List). Registered here only so `weldedCompilerFn`
// recognizes the names as compiler-API functions.
.{ .sx_name = "c_object_paths", .handler = handleBuildPipelineQuery },
.{ .sx_name = "link_libraries", .handler = handleBuildPipelineQuery },
.{ .sx_name = "emit_object", .handler = handleBuildPipelineQuery },
.{ .sx_name = "link", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_output", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_target", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_frameworks", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_flags", .handler = handleBuildPipelineQuery },
.{ .sx_name = "c_object_paths" },
.{ .sx_name = "link_libraries" },
.{ .sx_name = "emit_object" },
.{ .sx_name = "link" },
.{ .sx_name = "build_output" },
.{ .sx_name = "build_target" },
.{ .sx_name = "build_frameworks" },
.{ .sx_name = "build_flags" },
// ── BuildOptions accessors (Phase 5.5) ───────────────────────────────────
// Migrated off the `struct #compiler` hook surface to free `abi(.compiler)`
// functions serviced by `comptime_vm.callCompilerFn`. VM-only: any `#run` /
// const-init reaching them is routed to the VM (emit_llvm `entryNeedsVm`), so
// these legacy stubs are never reached — registered only so `weldedCompilerFn`
// recognizes the names. They bail loudly rather than fabricate a silent result.
.{ .sx_name = "add_link_flag", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "add_framework", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_output_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_wasm_shell", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "add_asset_dir", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "asset_dir_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "asset_dir_src_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "asset_dir_dest_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_post_link_module", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "binary_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_bundle_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_bundle_id", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_codesign_identity", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_provisioning_profile", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "bundle_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "bundle_id", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "codesign_identity", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "provisioning_profile", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "target_triple", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_macos", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_ios", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_ios_device", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_ios_simulator", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_android", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_path_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_path_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_manifest_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_keystore_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "manifest_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "keystore_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "jni_main_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "jni_main_runtime_path_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "jni_main_java_source_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "add_link_flag" },
.{ .sx_name = "add_framework" },
.{ .sx_name = "set_output_path" },
.{ .sx_name = "set_wasm_shell" },
.{ .sx_name = "add_asset_dir" },
.{ .sx_name = "asset_dir_count" },
.{ .sx_name = "asset_dir_src_at" },
.{ .sx_name = "asset_dir_dest_at" },
.{ .sx_name = "set_post_link_module" },
.{ .sx_name = "binary_path" },
.{ .sx_name = "set_bundle_path" },
.{ .sx_name = "set_bundle_id" },
.{ .sx_name = "set_codesign_identity" },
.{ .sx_name = "set_provisioning_profile" },
.{ .sx_name = "bundle_path" },
.{ .sx_name = "bundle_id" },
.{ .sx_name = "codesign_identity" },
.{ .sx_name = "provisioning_profile" },
.{ .sx_name = "target_triple" },
.{ .sx_name = "is_macos" },
.{ .sx_name = "is_ios" },
.{ .sx_name = "is_ios_device" },
.{ .sx_name = "is_ios_simulator" },
.{ .sx_name = "is_android" },
.{ .sx_name = "framework_count" },
.{ .sx_name = "framework_at" },
.{ .sx_name = "framework_path_count" },
.{ .sx_name = "framework_path_at" },
.{ .sx_name = "set_manifest_path" },
.{ .sx_name = "set_keystore_path" },
.{ .sx_name = "manifest_path" },
.{ .sx_name = "keystore_path" },
.{ .sx_name = "jni_main_count" },
.{ .sx_name = "jni_main_runtime_path_at" },
.{ .sx_name = "jni_main_java_source_at" },
};
/// Legacy-path stub for the Phase 5.5 BuildOptions accessors — see the `bound_fns`
/// comment. Any `#run` / const-init reaching a BuildOptions accessor is routed to
/// the VM (`emit_llvm.entryNeedsVm`), so this is never reached; it bails loudly
/// rather than fabricate a silent result.
fn handleBuildOptionsAccessor(_: *Interpreter, _: []const Value) InterpError!Value {
Interpreter.last_bail_detail = "BuildOptions accessor is VM-only (Phase 5.5); not available on the legacy interpreter";
return error.CannotEvalComptime;
}
/// Legacy-path stub for the Phase 5 build-pipeline primitives — see the
/// `bound_fns` comment. The only caller (the post-link build driver) runs on the
/// VM (`core.invokeByFuncId`), so these legacy handlers are never reached; they
/// bail loudly instead of fabricating a silent result.
fn handleBuildPipelineQuery(_: *Interpreter, _: []const Value) InterpError!Value {
Interpreter.last_bail_detail = "build-pipeline primitive (emit_object/c_object_paths/link_libraries) is VM-only (post-link); not available on the legacy interpreter";
return error.CannotEvalComptime;
}
// Kind codes accepted by `register_type` — mirror `TypeTable.kindCode`. An
// enum-like type is minted as a `tagged_union` (the general payload-carrying
// form, as `define` does), so both 2 (`enum`) and 3 (`tagged_union`) are taken.
const kind_struct: i64 = 1;
const kind_enum: i64 = 2;
const kind_tagged_union: i64 = 3;
const kind_tuple: i64 = 4;
/// Look up a compiler function by its sx name. Returns null when the name is not
/// on the export list.
pub fn findFn(sx_name: []const u8) ?*const BoundFn {
@@ -152,265 +101,3 @@ pub fn findFn(sx_name: []const u8) ?*const BoundFn {
}
return null;
}
/// The comptime type table to intern into: the host's mutable mint target when
/// set (the metatype-construction path), else the module's table reached through
/// a const-cast — the same access the interp's mint path uses (interp.zig). The
/// underlying table is genuinely mutable; the interp merely holds it `const`.
fn mintTable(interp: *Interpreter) *types.TypeTable {
return interp.mint orelse @constCast(&interp.module.types);
}
/// `intern(s: string) -> StringId` — intern `s` into the compiler's string pool
/// and return its handle. The inverse of `text_of`.
fn handleIntern(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .string) return error.TypeError;
const id = mintTable(interp).internString(args[0].string);
return Value{ .int = @intFromEnum(id) };
}
/// `text_of(id: StringId) -> string` — resolve a string handle back to its text.
/// The inverse of `intern`.
fn handleTextOf(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .int) return error.TypeError;
if (args[0].int < 0 or args[0].int > std.math.maxInt(u32)) return error.TypeError;
const id: StringId = @enumFromInt(@as(u32, @intCast(args[0].int)));
return Value{ .string = interp.module.types.getString(id) };
}
/// `find_type(name: StringId) -> TypeId` — look up a named type (struct / enum /
/// union / tagged-union / error-set) by its interned name and return its handle.
/// A name with no matching type yields the dedicated `unresolved` sentinel (a
/// `TypeId` of 0), the codebase-blessed "no type" marker — NOT an `?Type` (a
/// `Type` value is `.any`-typed, which the comptime VM does not represent, and
/// an optional can't cross the legacy↔VM eval boundary). The caller checks the
/// handle against 0 / `unresolved`. The VM mirrors this in `comptime_vm.callCompilerFn`.
fn handleFindType(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .int) return error.TypeError;
if (args[0].int < 0 or args[0].int > std.math.maxInt(u32)) return error.TypeError;
const name: StringId = @enumFromInt(@as(u32, @intCast(args[0].int)));
const tid = interp.module.types.findByName(name) orelse types.TypeId.unresolved;
return Value{ .int = tid.index() };
}
/// `type_field_count(t: TypeId) -> i64` — the member count of an aggregate type
/// (struct/union/tagged-union fields, enum variants, array/vector length), read
/// through `TypeTable.memberCount`. A type with no member count (scalar, pointer,
/// the `unresolved` sentinel, …) is a loud error — never a silent 0.
fn handleTypeFieldCount(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .int) return error.TypeError;
if (args[0].int < 0 or args[0].int > std.math.maxInt(u32)) return error.TypeError;
const tid: types.TypeId = @enumFromInt(@as(u32, @intCast(args[0].int)));
const count = interp.module.types.memberCount(tid) orelse return error.TypeError;
return Value{ .int = count };
}
/// Read an integer `Value` arg as a `u32` handle (StringId / TypeId). Errors on a
/// non-int or out-of-u32-range value — never a silent clamp.
fn handleArg(args: []const Value, i: usize) InterpError!u32 {
if (args[i] != .int) return error.TypeError;
if (args[i].int < 0 or args[i].int > std.math.maxInt(u32)) return error.TypeError;
return @intCast(args[i].int);
}
/// `type_nominal_name(t: TypeId) -> StringId` — the nominal name handle of a named
/// type (struct/enum/union/…). Loud error for an unnamed type (no silent default).
fn handleTypeNominalName(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1) return error.TypeError;
const tid: types.TypeId = @enumFromInt(try handleArg(args, 0));
const sid = interp.module.types.nominalName(tid) orelse return error.TypeError;
return Value{ .int = @intFromEnum(sid) };
}
/// `type_field_name(t: TypeId, idx: i64) -> StringId` — name handle of member `idx`
/// (struct/union/tagged-union field, enum variant, named-tuple element). Loud
/// error for an out-of-range idx or a type with no named members.
fn handleTypeFieldName(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 2 or args[1] != .int) return error.TypeError;
const tid: types.TypeId = @enumFromInt(try handleArg(args, 0));
const sid = interp.module.types.memberName(tid, args[1].int) orelse return error.TypeError;
return Value{ .int = @intFromEnum(sid) };
}
/// `type_field_type(t: TypeId, idx: i64) -> TypeId` — type handle of member `idx`
/// (struct/union/tagged-union field, tuple/array/vector element). Loud error for
/// an out-of-range idx or a type with no member types (e.g. a payloadless enum).
fn handleTypeFieldType(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 2 or args[1] != .int) return error.TypeError;
const tid: types.TypeId = @enumFromInt(try handleArg(args, 0));
const mty = interp.module.types.memberType(tid, args[1].int) orelse return error.TypeError;
return Value{ .int = mty.index() };
}
/// `type_kind(t: TypeId) -> i64` — the stable kind discriminant (see
/// `TypeTable.kindCode`). Total: an unnamed/non-aggregate type reads `other` (0).
fn handleTypeKind(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1) return error.TypeError;
const tid: types.TypeId = @enumFromInt(try handleArg(args, 0));
return Value{ .int = interp.module.types.kindCode(tid) };
}
/// `type_field_value(t: TypeId, idx: i64) -> i64` — enum variant `idx`'s integer
/// value (explicit or ordinal). Loud error for a non-enum or out-of-range idx.
fn handleTypeFieldValue(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 2 or args[1] != .int) return error.TypeError;
const tid: types.TypeId = @enumFromInt(try handleArg(args, 0));
const v = interp.module.types.memberValue(tid, args[1].int) orelse return error.TypeError;
return Value{ .int = v };
}
// ── write side: declare_type / pointer_to / register_type ───────────────────
//
// These MINT into the type table, so they only make sense at LOWERING time —
// where the compiler still resolves references to the new types and the `mint`
// target is open (`runComptimeTypeFunc`). They take/return real `Type` values
// (`.type_tag`), the comptime-native form, matching meta.sx's `StructField` /
// `declare` / `define`. This is the unified re-expression of the metatype:
// `declare_type` ≈ `declare`, `register_type` ≈ a single kind-branching `define`,
// and `pointer_to` builds `*T` references so a graph of types can refer to each
// other (forward handles + pointers) before their bodies are filled.
/// `declare_type(name: string) -> Type` — mint a NEW empty forward nominal type
/// named `name` (or return the existing slot, so a self/sibling reference by name
/// resolves to the same one). Mirrors the `declare` builtin: the forward slot is
/// an empty `tagged_union` until `register_type` fills it.
fn handleDeclareType(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .string) return error.TypeError;
const tbl = mintTable(interp);
const name_id = tbl.internString(args[0].string);
if (tbl.findByName(name_id)) |existing| return Value{ .type_tag = existing };
const info: types.TypeInfo = .{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .i64 } };
return Value{ .type_tag = tbl.internNominal(info, 0) };
}
/// `pointer_to(t: Type) -> Type` — intern `*t`. Lets a member reference a type by
/// pointer (e.g. a recursive `*A`) from a `Type` handle.
fn handlePointerTo(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .type_tag) return error.TypeError;
const tbl = mintTable(interp);
return Value{ .type_tag = tbl.intern(.{ .pointer = .{ .pointee = args[0].type_tag } }) };
}
/// `register_type(handle: Type, kind: i64, members: []Member) -> Type` — fill a
/// `declare_type`'d forward slot, branching on `kind` IN THE COMPILER (subsuming
/// `define`'s per-kind dispatch). `Member` is `{ name: string, ty: Type }`:
/// struct → fields `{ name, ty }` (dup names rejected)
/// enum/t-union → variants `{ name, payload = ty }` (minted as a tagged_union)
/// tuple → positional element types (names ignored)
/// Returns the (now completed) handle. Every malformed input is a loud error.
fn handleRegisterType(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 3 or args[0] != .type_tag or args[1] != .int) return error.TypeError;
const handle = args[0].type_tag;
const kind = args[1].int;
const elems = interp_mod.decodeVariantElements(args[2]) orelse return error.TypeError;
if (elems.len == 0) return error.TypeError; // a type with no members is never valid
const tbl = mintTable(interp);
// The slot's nominal identity. Accept the forward `tagged_union` from
// `declare_type` AND an already-completed nominal of the same name — so
// re-evaluating the same type-fn (e.g. a minting module reached via two
// import edges) RE-FILLS the slot idempotently instead of erroring. A
// non-nominal handle is rejected (not a `declare_type`'d slot).
const ident = nominalIdent(tbl.get(handle)) orelse return error.TypeError;
if (kind == kind_tuple) {
var tys = std.ArrayList(types.TypeId).empty;
for (elems) |elem| {
const m = memberPair(elem) orelse return error.TypeError;
tys.append(interp.alloc, m.ty) catch return error.CannotEvalComptime;
}
tbl.replaceKeyedInfo(handle, .{ .tuple = .{ .fields = tys.items, .names = null } });
return Value{ .type_tag = handle };
}
if (kind == kind_enum) {
// An ACTUAL (payloadless) enum: members are variant NAMES. A non-void
// payload means the caller wants a payload-carrying variant — that's a
// tagged_union (kind 3), so reject it loudly rather than dropping it.
var variants = std.ArrayList(StringId).empty;
for (elems) |elem| {
const m = memberPair(elem) orelse return error.TypeError;
if (m.ty != .void) return error.TypeError; // payload variant → use kind 3 (tagged_union)
const name_id = tbl.internString(m.name);
for (variants.items) |existing| if (existing == name_id) return error.TypeError; // dup variant
variants.append(interp.alloc, name_id) catch return error.CannotEvalComptime;
}
tbl.replaceKeyedInfo(handle, .{ .@"enum" = .{ .name = ident.name, .variants = variants.items, .nominal_id = ident.nominal_id } });
return Value{ .type_tag = handle };
}
// struct / tagged_union collect `{ name, ty }` fields.
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
for (elems) |elem| {
const m = memberPair(elem) orelse return error.TypeError;
const name_id = tbl.internString(m.name);
for (fields.items) |existing| if (existing.name == name_id) return error.TypeError; // dup member name
fields.append(interp.alloc, .{ .name = name_id, .ty = m.ty }) catch return error.CannotEvalComptime;
}
const full: types.TypeInfo = switch (kind) {
kind_struct => .{ .@"struct" = .{ .name = ident.name, .fields = fields.items, .nominal_id = ident.nominal_id } },
kind_tagged_union => .{ .tagged_union = .{ .name = ident.name, .fields = fields.items, .tag_type = .i64, .nominal_id = ident.nominal_id } },
else => return error.TypeError, // unknown kind code
};
tbl.replaceKeyedInfo(handle, full);
return Value{ .type_tag = handle };
}
/// The nominal identity (`name` + stable `nominal_id`) of a declare_type'd slot —
/// from the forward `tagged_union` OR an already-completed nominal (so a re-fill
/// preserves identity). A `tuple` is structural (no nominal name); null for a
/// non-nominal handle (not a `declare_type` result).
fn nominalIdent(info: types.TypeInfo) ?struct { name: StringId, nominal_id: u32 } {
return switch (info) {
.tagged_union => |u| .{ .name = u.name, .nominal_id = u.nominal_id },
.@"enum" => |e| .{ .name = e.name, .nominal_id = e.nominal_id },
.@"struct" => |s| .{ .name = s.name, .nominal_id = s.nominal_id },
.tuple => .{ .name = StringId.empty, .nominal_id = 0 }, // structural; name vestigial
else => null,
};
}
/// Decode one `Member` value — a `{ name: string, ty: Type }` aggregate.
fn memberPair(elem: Value) ?struct { name: []const u8, ty: types.TypeId } {
const f = switch (elem) {
.aggregate => |a| a,
else => return null,
};
if (f.len != 2) return null;
const name = switch (f[0]) {
.string => |s| s,
else => return null,
};
const ty = f[1].asTypeId() orelse return null;
return .{ .name = name, .ty = ty };
}
// ── BuildOptions handlers (legacy dual-path, gate-OFF) ──────────────────────
// The `abi(.compiler)` re-expression of `build_options` + `set_post_link_callback`,
// reading the build config off the interpreter (`interp.build_config`). The VM
// services the same names in `comptime_vm.callCompilerFn`; both stay in lockstep.
/// `build_options() -> BuildOptions` — hand back the opaque zero-field handle. The
/// state lives on `interp.build_config`; the handle is never dereferenced.
fn handleBuildOptions(_: *Interpreter, _: []const Value) InterpError!Value {
return .void_val;
}
/// `set_post_link_callback(self, cb)` — record the callback `FuncId` on the build
/// config so `main.zig` re-enters the evaluator post-link. The `cb` arg is a
/// `.func_ref` value.
/// `on_build(cb)` — register the build callback (the Phase 5 form, a free fn; cb
/// is arg 0, and `cb: (opt: BuildOptions) -> bool` so the callback is invoked with
/// the `BuildOptions` handle). Sets `post_link_takes_options` to distinguish it
/// from the legacy `set_post_link_callback` (`() -> bool`).
fn handleOnBuild(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1) return error.TypeError;
const bc = interp.build_config orelse return error.CannotEvalComptime;
switch (args[0]) {
.func_ref => |id| {
bc.post_link_callback_fn = id;
bc.post_link_takes_options = true;
},
else => return error.TypeError,
}
return .void_val;
}

108
src/ir/comptime_value.zig Normal file
View File

@@ -0,0 +1,108 @@
//! Comptime VALUE — the result/materialization representation produced by the
//! comptime VM (`comptime_vm.regToValue`) and consumed by
//! `emit_llvm.valueToLLVMConst`. The byte-addressable VM executes natively;
//! this type is only the result DTO at the VM↔host boundary.
const std = @import("std");
const types = @import("types.zig");
const inst_mod = @import("inst.zig");
const TypeId = types.TypeId;
const FuncId = inst_mod.FuncId;
// ── Value ───────────────────────────────────────────────────────────────
pub const Value = union(enum) {
int: i64,
float: f64,
boolean: bool,
string: []const u8,
null_val,
void_val,
undef,
aggregate: []const Value,
slot_ptr: u32, // index into the frame's local slots
func_ref: FuncId,
closure: ClosureVal,
type_tag: TypeId,
heap_ptr: HeapPtr, // pointer into heap-allocated memory
/// Byte-granular raw pointer. Produced by `index_gep` on a string /
/// `[*]u8` aggregate whose data field is itself a raw integer pointer
/// (e.g. from libc_malloc). Store/load through this variant operate
/// on a single byte — matching the heap_ptr semantics for the same
/// op shape.
byte_ptr: usize,
pub const ClosureVal = struct {
func: FuncId,
env: ?[]const Value,
};
/// A pointer to heap-allocated memory, with an optional byte offset.
pub const HeapPtr = struct {
id: u32, // index into the legacy heap (historical)
offset: u32 = 0,
};
pub fn asInt(self: Value) ?i64 {
return switch (self) {
.int => |v| v,
else => null,
};
}
pub fn asFloat(self: Value) ?f64 {
return switch (self) {
.float => |v| v,
.int => |v| @floatFromInt(v), // implicit int→float for convenience
else => null,
};
}
pub fn asBool(self: Value) ?bool {
return switch (self) {
.boolean => |v| v,
else => null,
};
}
pub fn isNull(self: Value) bool {
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,
};
}
};
/// Normalize a comptime value into the list of EnumVariant element values.
/// A `[]EnumVariant` slice evaluates to a `{ data, len }` aggregate (`len` an
/// int); a `[N]EnumVariant` array literal evaluates to the element aggregate
/// directly. Returns null for any other shape (the caller bails loudly).
pub fn decodeVariantElements(result: Value) ?[]const Value {
const fields = switch (result) {
.aggregate => |f| f,
else => return null,
};
// Slice fat pointer `{ data, len }`: a 2-field aggregate whose 2nd field is
// an integer length. (A 2-VARIANT array can't collide — its 2nd field is an
// EnumVariant aggregate, so `asInt` is null.)
if (fields.len == 2) {
if (fields[1].asInt()) |len_i| {
const len: usize = @intCast(len_i);
switch (fields[0]) {
.aggregate => |arr| return if (len <= arr.len) arr[0..len] else null,
else => return null,
}
}
}
return fields;
}

View File

@@ -12,7 +12,7 @@ const FuncId = inst_mod.FuncId;
const Function = inst_mod.Function;
const Block = inst_mod.Block;
const Module = @import("module.zig").Module;
const Value = @import("interp.zig").Value;
const Value = @import("comptime_value.zig").Value;
const TypeId = types.TypeId;
const dummy: types.StringId = @enumFromInt(0);
@@ -1262,41 +1262,6 @@ test "comptime_vm exec: recursive call (sum 0..n)" {
try std.testing.expectEqual(@as(i64, 55), toI64(try v.run(module.getFunction(sum_id), &.{fromI64(10)})));
}
test "comptime_vm bridge: Value <-> Reg round-trips (scalar, string, struct)" {
const alloc = std.testing.allocator;
var table = types.TypeTable.init(alloc);
defer table.deinit();
const pfields = [_]types.TypeInfo.StructInfo.Field{
.{ .name = table.internString("x"), .ty = .i64 },
.{ .name = table.internString("y"), .ty = .i64 },
};
const point = table.intern(.{ .@"struct" = .{ .name = table.internString("Point"), .fields = &pfields } });
var v = vm.Vm.init(alloc);
v.table = &table;
defer v.deinit();
// scalar i64
const r_i = try v.valueToReg(&table, .{ .int = 42 }, .i64);
try std.testing.expectEqual(@as(i64, 42), toI64(r_i));
const back_i = try v.regToValue(alloc, &table, r_i, .i64);
try std.testing.expectEqual(@as(i64, 42), back_i.int);
// string (materialized into comptime memory, read back + deep-copied out)
const r_s = try v.valueToReg(&table, .{ .string = "hi" }, .string);
const back_s = try v.regToValue(alloc, &table, r_s, .string);
defer alloc.free(back_s.string);
try std.testing.expectEqualStrings("hi", back_s.string);
// struct {x:i64, y:i64}
const fvals = [_]Value{ .{ .int = 3 }, .{ .int = 4 } };
const r_p = try v.valueToReg(&table, .{ .aggregate = &fvals }, point);
const back_p = try v.regToValue(alloc, &table, r_p, point);
defer alloc.free(back_p.aggregate);
try std.testing.expectEqual(@as(i64, 3), back_p.aggregate[0].int);
try std.testing.expectEqual(@as(i64, 4), back_p.aggregate[1].int);
}
test "comptime_vm tryEval: pure function → Value; unsupported → null" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);

View File

@@ -30,10 +30,11 @@ const std = @import("std");
const inst_mod = @import("inst.zig");
const types = @import("types.zig");
const mod_mod = @import("module.zig");
const interp_mod = @import("interp.zig");
const comptime_value = @import("comptime_value.zig");
const compiler_hooks = @import("compiler_hooks.zig");
const host_ffi = @import("host_ffi.zig");
const errors_mod = @import("../errors.zig");
const Value = interp_mod.Value;
const Value = comptime_value.Value;
const Inst = inst_mod.Inst;
const Ref = inst_mod.Ref;
const BlockId = inst_mod.BlockId;
@@ -191,7 +192,7 @@ pub var last_bail_reason: ?[]const u8 = null;
/// hardened to return `error.OutOfBounds` (not a debug panic) on a null/out-of-
/// range/oversized access, so a malformed run bails to `null` (→ legacy fallback)
/// rather than crashing the compiler. On a bail, `last_bail_reason` names the cause.
pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*interp_mod.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8)) ?Value {
pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*compiler_hooks.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8)) ?Value {
last_bail_reason = null;
const func = module.getFunction(func_id);
if (func.is_extern or func.blocks.items.len == 0) {
@@ -229,7 +230,7 @@ pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.
/// `cb: (opt: BuildOptions) -> bool`): when `pass_options` is set, the handle (a
/// null sentinel — the real state is the threaded `BuildConfig`) is passed after
/// the implicit ctx. Returns null on a bail (`last_bail_reason` names the cause).
pub fn runBuildCallback(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*interp_mod.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8), pass_options: bool) ?Value {
pub fn runBuildCallback(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*compiler_hooks.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8), pass_options: bool) ?Value {
last_bail_reason = null;
const func = module.getFunction(func_id);
if (func.is_extern or func.blocks.items.len == 0) {
@@ -354,7 +355,7 @@ pub const Vm = struct {
/// the `#run`/const-init eval sites so an `abi(.compiler)` `BuildOptions` function
/// (e.g. `set_post_link_callback`) records into it directly. Null at lowering-time
/// type-fn evals (no build config exists yet); such a function bails loudly.
build_config: ?*interp_mod.BuildConfig = null,
build_config: ?*compiler_hooks.BuildConfig = null,
/// File → source text (the diagnostics' `import_sources`), threaded from the host
/// so `trace_resolve` can turn a packed `(func_id, span.start)` comptime frame into
/// `file:line:col` + the source line. Null → line/col degrade to 1 / "".
@@ -2146,48 +2147,6 @@ pub const Vm = struct {
// owns comptime end-to-end. Covers scalars + strings + structs; other aggregate
// shapes bail loudly (added as wiring surfaces them).
/// Convert a legacy `Value` of type `ty` into a VM `Reg`, materializing
/// aggregates into comptime memory (returning their `Addr`).
pub fn valueToReg(self: *Vm, table: *const types.TypeTable, value: Value, ty: TypeId) Error!Reg {
switch (kindOf(table, ty)) {
.word => return switch (value) {
.int => |i| @bitCast(i),
.boolean => |b| @intFromBool(b),
.float => |f| @bitCast(f),
.null_val => null_addr,
.type_tag => |t| t.index(),
else => self.failMsg("value→reg: scalar value kind mismatch"),
},
.aggregate => {
if (ty == .string) {
const text = switch (value) {
.string => |s| s,
else => return self.failMsg("value→reg: expected a string literal value"),
};
const data = self.machine.allocBytes(text.len + 1, 1);
if (text.len > 0) @memcpy(try self.machine.bytes(data, text.len), text);
return self.makeSlice(table, data, text.len);
}
const info = table.get(ty);
if (info == .@"struct") {
const fvals = switch (value) {
.aggregate => |a| a,
else => return self.failMsg("value→reg: expected a struct aggregate"),
};
const addr = self.machine.allocBytes(table.typeSizeBytes(ty), table.typeAlignBytes(ty));
for (info.@"struct".fields, 0..) |f, i| {
if (i >= fvals.len) break;
const fr = try self.valueToReg(table, fvals[i], f.ty);
try self.writeField(table, addr + fieldOffset(table, ty, @intCast(i)), f.ty, fr);
}
return addr;
}
return self.failMsg("value→reg: aggregate shape not bridged yet (slice/array/optional/tuple/enum)");
},
.unsupported => return self.failMsg("value→reg: unsupported type"),
}
}
/// Convert a VM `Reg` (+ comptime memory) of type `ty` back into a legacy `Value`.
/// Strings/aggregates are deep-copied into `alloc` (they must outlive comptime memory).
pub fn regToValue(self: *Vm, alloc: std.mem.Allocator, table: *const types.TypeTable, reg: Reg, ty: TypeId) Error!Value {

View File

@@ -29,8 +29,8 @@ const Function = ir_inst.Function;
const Global = ir_inst.Global;
const ir_module = @import("module.zig");
const Module = ir_module.Module;
const interp_mod = @import("interp.zig");
const Value = interp_mod.Value;
const compiler_hooks = @import("compiler_hooks.zig");
const Value = @import("comptime_value.zig").Value;
const comptime_vm = @import("comptime_vm.zig");
const build_opts = @import("build_opts");
@@ -210,7 +210,7 @@ pub const LLVMEmitter = struct {
target_config: TargetConfig,
// Build configuration accumulated from #run blocks
build_config: interp_mod.BuildConfig,
build_config: compiler_hooks.BuildConfig,
// ── DWARF debug info (ERR E3.0) ──────────────────────────────────
// Emitted only when the build keeps error traces (opt_level

View File

@@ -1,844 +0,0 @@
// Tests for the IR interpreter (interp.zig).
// Includes basic interpreter tests and comptime parity tests.
const std = @import("std");
const types = @import("types.zig");
const inst_mod = @import("inst.zig");
const mod_mod = @import("module.zig");
const interp_mod = @import("interp.zig");
const TypeId = types.TypeId;
const Ref = inst_mod.Ref;
const BlockId = inst_mod.BlockId;
const FuncId = inst_mod.FuncId;
const Function = inst_mod.Function;
const Module = mod_mod.Module;
const Builder = mod_mod.Builder;
const Interpreter = interp_mod.Interpreter;
const Value = interp_mod.Value;
// ── Helper ──────────────────────────────────────────────────────────────
fn str(module: *Module, s: []const u8) types.StringId {
return module.types.internString(s);
}
// ── Basic interpreter tests (migrated from interp.zig) ──────────────────
test "interpret: compute(5) = 25" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func compute(x: i64) -> i64 { return x * x; }
const params = &[_]Function.Param{.{ .name = str(&module, "compute"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "compute"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const x_ref = Ref.fromIndex(0);
const result = b.mul(x_ref, x_ref, .i64);
b.ret(result, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 5 }});
try std.testing.expectEqual(@as(i64, 25), val.asInt().?);
}
test "interpret: if/else branching" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
const params = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "abs"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
const then_bb = b.appendBlock(str(&module, "then"), &.{});
const else_bb = b.appendBlock(str(&module, "else"), &.{});
b.switchToBlock(entry);
const x = Ref.fromIndex(0);
const zero = b.constInt(0, .i64);
const is_neg = b.cmpLt(x, zero);
b.condBr(is_neg, then_bb, &.{}, else_bb, &.{});
b.switchToBlock(then_bb);
const neg_x = b.emit(.{ .neg = .{ .operand = x } }, .i64);
b.ret(neg_x, .i64);
b.switchToBlock(else_bb);
b.ret(x, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val1 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = -7 }});
try std.testing.expectEqual(@as(i64, 7), val1.asInt().?);
const val2 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 3 }});
try std.testing.expectEqual(@as(i64, 3), val2.asInt().?);
}
test "interpret: function calling another function" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func square(x: i64) -> i64 { return x * x; }
const params_sq = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "square"), params_sq, .i64);
const entry1 = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry1);
const x = Ref.fromIndex(0);
const sq = b.mul(x, x, .i64);
b.ret(sq, .i64);
b.finalize();
// func sum_of_squares(a, b) -> i64 { return square(a) + square(b); }
const params_ss = &[_]Function.Param{
.{ .name = str(&module, "a"), .ty = .i64 },
.{ .name = str(&module, "b"), .ty = .i64 },
};
_ = b.beginFunction(str(&module, "sum_of_squares"), params_ss, .i64);
const entry2 = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry2);
const a = Ref.fromIndex(0);
const b_param = Ref.fromIndex(1);
const sq_a = b.call(FuncId.fromIndex(0), &.{a}, .i64);
const sq_b = b.call(FuncId.fromIndex(0), &.{b_param}, .i64);
const sum = b.add(sq_a, sq_b, .i64);
b.ret(sum, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(1), &.{ .{ .int = 3 }, .{ .int = 4 } });
try std.testing.expectEqual(@as(i64, 25), val.asInt().?);
}
test "interpret: alloca/store/load" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const slot = b.alloca(.i64);
const ten = b.constInt(10, .i64);
b.store(slot, ten);
const loaded = b.load(slot, .i64);
const five = b.constInt(5, .i64);
const sum = b.add(loaded, five, .i64);
b.store(slot, sum);
const result = b.load(slot, .i64);
b.ret(result, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 15), val.asInt().?);
}
// ── Comptime parity tests ───────────────────────────────────────────────
// ── Test: while loop (sumOf10 from 15-while.sx) ─────────────────────────
// sumOf10 :: () -> i32 { i:=1; s:=0; while i<=10 { s+=i; i+=1; } s; }
// Expected: 55
test "comptime: while loop — sumOf10 = 55" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "sumOf10"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
const hdr = b.appendBlock(str(&module, "while.hdr"), &.{});
const body = b.appendBlock(str(&module, "while.body"), &.{});
const exit = b.appendBlock(str(&module, "while.exit"), &.{});
// entry: i=1, s=0, br while.hdr
b.switchToBlock(entry);
const i_slot = b.alloca(.i64);
const one = b.constInt(1, .i64);
b.store(i_slot, one);
const s_slot = b.alloca(.i64);
const zero = b.constInt(0, .i64);
b.store(s_slot, zero);
b.br(hdr, &.{});
// while.hdr: if i <= 10 → body, else → exit
b.switchToBlock(hdr);
const i_load = b.load(i_slot, .i64);
const ten = b.constInt(10, .i64);
const cond = b.emit(.{ .cmp_le = .{ .lhs = i_load, .rhs = ten } }, .bool);
b.condBr(cond, body, &.{}, exit, &.{});
// while.body: s += i; i += 1; br while.hdr
b.switchToBlock(body);
const s_load = b.load(s_slot, .i64);
const i_load2 = b.load(i_slot, .i64);
const s_new = b.add(s_load, i_load2, .i64);
b.store(s_slot, s_new);
const i_load3 = b.load(i_slot, .i64);
const one2 = b.constInt(1, .i64);
const i_new = b.add(i_load3, one2, .i64);
b.store(i_slot, i_new);
b.br(hdr, &.{});
// while.exit: return s
b.switchToBlock(exit);
const s_final = b.load(s_slot, .i64);
b.ret(s_final, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 55), val.asInt().?);
}
// ── Test: optional coalesce (ct_sum from 32-optionals.sx) ────────────────
// ct_sum :: () -> i32 { x:?i32=42; y:?i32=null; return (x??0)+(y??99); }
// Expected: 42 + 99 = 141
test "comptime: optional coalesce — ct_sum = 141" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "ct_sum"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
// x: ?i32 = 42 → alloca, store 42
const x_slot = b.alloca(.i64);
const forty_two = b.constInt(42, .i64);
b.store(x_slot, forty_two);
// y: ?i32 = null → alloca, store null
const y_slot = b.alloca(.i64);
const null_val = b.constNull(.i64);
b.store(y_slot, null_val);
// (x ?? 0)
const x_load = b.load(x_slot, .i64);
const zero = b.constInt(0, .i64);
const x_coalesced = b.emit(.{ .optional_coalesce = .{ .lhs = x_load, .rhs = zero } }, .i64);
// (y ?? 99)
const y_load = b.load(y_slot, .i64);
const ninety_nine = b.constInt(99, .i64);
const y_coalesced = b.emit(.{ .optional_coalesce = .{ .lhs = y_load, .rhs = ninety_nine } }, .i64);
// return x_coalesced + y_coalesced
const sum = b.add(x_coalesced, y_coalesced, .i64);
b.ret(sum, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 141), val.asInt().?);
}
// ── Test: optional unwrap (ct_opt_unwrap from 50-smoke.sx) ───────────────
// ct_opt_unwrap :: () -> i32 { x:?i32 = 77; return x!; }
// Expected: 77
test "comptime: optional unwrap — 77" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "ct_opt_unwrap"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const slot = b.alloca(.i64);
const val77 = b.constInt(77, .i64);
b.store(slot, val77);
const loaded = b.load(slot, .i64);
const unwrapped = b.optionalUnwrap(loaded, .i64);
b.ret(unwrapped, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 77), val.asInt().?);
}
// ── Test: recursive fibonacci ────────────────────────────────────────────
// fib :: (n: i64) -> i64 { if n <= 1 return n; return fib(n-1) + fib(n-2); }
// Expected: fib(10) = 55
test "comptime: recursive fibonacci — fib(10) = 55" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
const params = &[_]Function.Param{.{ .name = str(&module, "n"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "fib"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
const base_bb = b.appendBlock(str(&module, "base"), &.{});
const rec_bb = b.appendBlock(str(&module, "recurse"), &.{});
// entry: if n <= 1 → base, else → recurse
b.switchToBlock(entry);
const n = Ref.fromIndex(0);
const one = b.constInt(1, .i64);
const is_base = b.emit(.{ .cmp_le = .{ .lhs = n, .rhs = one } }, .bool);
b.condBr(is_base, base_bb, &.{}, rec_bb, &.{});
// base: return n
b.switchToBlock(base_bb);
b.ret(n, .i64);
// recurse: return fib(n-1) + fib(n-2)
b.switchToBlock(rec_bb);
const n_minus_1 = b.sub(n, one, .i64);
const two = b.constInt(2, .i64);
const n_minus_2 = b.sub(n, two, .i64);
const fib1 = b.call(FuncId.fromIndex(0), &.{n_minus_1}, .i64);
const fib2 = b.call(FuncId.fromIndex(0), &.{n_minus_2}, .i64);
const sum = b.add(fib1, fib2, .i64);
b.ret(sum, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 10 }});
try std.testing.expectEqual(@as(i64, 55), val.asInt().?);
}
// ── Test: compute(5) = 7 (from 05-run.sx) ──────────────────────────────
// compute :: (v: i32) -> i32 => v + 2;
// Expected: compute(5) = 7
test "comptime: compute(5) = 7" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
const params = &[_]Function.Param{.{ .name = str(&module, "v"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "compute"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const v = Ref.fromIndex(0);
const two = b.constInt(2, .i64);
const result = b.add(v, two, .i64);
b.ret(result, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 5 }});
try std.testing.expectEqual(@as(i64, 7), val.asInt().?);
}
// ── Test: chained comptime (CT_CHAIN from 50-smoke.sx) ───────────────────
// add :: (a: i32, b: i32) -> i32 => a + b;
// CT_VAL :: #run add(10, 15); → 25
// CT_CHAIN :: #run add(CT_VAL, 5); → 30
// Simulates calling add(25, 5) to verify chaining works.
test "comptime: chained — add(add(10,15), 5) = 30" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func add(a, b) -> i64 { return a + b; }
const params = &[_]Function.Param{
.{ .name = str(&module, "a"), .ty = .i64 },
.{ .name = str(&module, "b"), .ty = .i64 },
};
_ = b.beginFunction(str(&module, "add"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const a = Ref.fromIndex(0);
const b_ref = Ref.fromIndex(1);
const sum = b.add(a, b_ref, .i64);
b.ret(sum, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
// First: add(10, 15) = 25
const ct_val = try interp.call(FuncId.fromIndex(0), &.{ .{ .int = 10 }, .{ .int = 15 } });
try std.testing.expectEqual(@as(i64, 25), ct_val.asInt().?);
// Then: add(25, 5) = 30 (chained)
const ct_chain = try interp.call(FuncId.fromIndex(0), &.{ ct_val, .{ .int = 5 } });
try std.testing.expectEqual(@as(i64, 30), ct_chain.asInt().?);
}
// ── Test: struct init + field access ─────────────────────────────────────
// p := Point{x: 3, y: 4}; return p.x + p.y;
// Expected: 7
test "comptime: struct init and field access — 7" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test_struct"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
// Point{x: 3, y: 4}
const three = b.constInt(3, .i64);
const four = b.constInt(4, .i64);
const point = b.structInit(&.{ three, four }, .i64);
// p.x + p.y
const px = b.structGet(point, 0, .i64);
const py = b.structGet(point, 1, .i64);
const sum = b.add(px, py, .i64);
b.ret(sum, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 7), val.asInt().?);
}
// ── Test: float arithmetic ──────────────────────────────────────────────
// compute :: (x: f64) -> f64 { return x * 2.5 + 1.0; }
// Expected: compute(3.0) = 8.5
test "comptime: float arithmetic — 8.5" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
const params = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .f64 }};
_ = b.beginFunction(str(&module, "compute_f"), params, .f64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const x = Ref.fromIndex(0);
const two_five = b.constFloat(2.5, .f64);
const product = b.mul(x, two_five, .f64);
const one = b.constFloat(1.0, .f64);
const result = b.add(product, one, .f64);
b.ret(result, .f64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{.{ .float = 3.0 }});
try std.testing.expectEqual(@as(f64, 8.5), val.asFloat().?);
}
// ── Test: boolean logic ─────────────────────────────────────────────────
// test :: (a: bool, b: bool) -> bool { return (a and b) or (not a); }
// Expected: test(true, false) = true (because not a = false, a and b = false, false or false... wait)
// Actually: a=true, b=false → (true and false) or (not true) = false or false = false
// test(false, true) → (false and true) or (not false) = false or true = true
test "comptime: boolean logic" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
const params = &[_]Function.Param{
.{ .name = str(&module, "a"), .ty = .bool },
.{ .name = str(&module, "b"), .ty = .bool },
};
_ = b.beginFunction(str(&module, "bool_test"), params, .bool);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const a_ref = Ref.fromIndex(0);
const b_ref = Ref.fromIndex(1);
const and_ab = b.emit(.{ .bool_and = .{ .lhs = a_ref, .rhs = b_ref } }, .bool);
const not_a = b.emit(.{ .bool_not = .{ .operand = a_ref } }, .bool);
const result = b.emit(.{ .bool_or = .{ .lhs = and_ab, .rhs = not_a } }, .bool);
b.ret(result, .bool);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
// test(true, false) = false or false = false
const val1 = try interp.call(FuncId.fromIndex(0), &.{ .{ .boolean = true }, .{ .boolean = false } });
try std.testing.expectEqual(false, val1.asBool().?);
// test(false, true) = false or true = true
const val2 = try interp.call(FuncId.fromIndex(0), &.{ .{ .boolean = false }, .{ .boolean = true } });
try std.testing.expectEqual(true, val2.asBool().?);
}
// ── Test: negation ──────────────────────────────────────────────────────
test "comptime: negation — int and float" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func neg_int(x: i64) -> i64 { return -x; }
const params = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "neg_int"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const x = Ref.fromIndex(0);
const neg = b.emit(.{ .neg = .{ .operand = x } }, .i64);
b.ret(neg, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 42 }});
try std.testing.expectEqual(@as(i64, -42), val.asInt().?);
}
// ── Test: modulo ────────────────────────────────────────────────────────
test "comptime: modulo — 17 mod 5 = 2" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test_mod"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const seventeen = b.constInt(17, .i64);
const five = b.constInt(5, .i64);
const result = b.emit(.{ .mod = .{ .lhs = seventeen, .rhs = five } }, .i64);
b.ret(result, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 2), val.asInt().?);
}
// ── Test: switch_br (enum tag dispatch) ──────────────────────────────────
// Simulates: match tag { 0 => 10, 1 => 20, else => 30 }
test "comptime: switch_br dispatch" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
const params = &[_]Function.Param{.{ .name = str(&module, "tag"), .ty = .i64 }};
_ = b.beginFunction(str(&module, "dispatch"), params, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
const case0 = b.appendBlock(str(&module, "case0"), &.{});
const case1 = b.appendBlock(str(&module, "case1"), &.{});
const default = b.appendBlock(str(&module, "default"), &.{});
b.switchToBlock(entry);
const tag = Ref.fromIndex(0);
b.switchBr(tag, &.{
.{ .value = 0, .target = case0, .args = &.{} },
.{ .value = 1, .target = case1, .args = &.{} },
}, default, &.{});
b.switchToBlock(case0);
const ten = b.constInt(10, .i64);
b.ret(ten, .i64);
b.switchToBlock(case1);
const twenty = b.constInt(20, .i64);
b.ret(twenty, .i64);
b.switchToBlock(default);
const thirty = b.constInt(30, .i64);
b.ret(thirty, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const v0 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 0 }});
try std.testing.expectEqual(@as(i64, 10), v0.asInt().?);
const v1 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 1 }});
try std.testing.expectEqual(@as(i64, 20), v1.asInt().?);
const v2 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 99 }});
try std.testing.expectEqual(@as(i64, 30), v2.asInt().?);
}
// ── Test: enum init + tag extraction ────────────────────────────────────
test "comptime: enum init and tag" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test_enum"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
// Create enum with tag=2, no payload
const e = b.enumInit(2, Ref.none, .i64);
const tag = b.emit(.{ .enum_tag = .{ .operand = e } }, .i64);
b.ret(tag, .i64);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const val = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(@as(i64, 2), val.asInt().?);
}
// ── Test: conversion (widen/narrow passthrough) ─────────────────────────
test "comptime: widen/narrow passthrough" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test_conv"), &.{}, .i64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const val = b.constInt(42, .i32);
const widened = b.emit(.{ .widen = .{ .operand = val, .from = .i32, .to = .i64 } }, .i64);
const narrowed = b.emit(.{ .narrow = .{ .operand = widened, .from = .i64, .to = .i32 } }, .i32);
b.ret(narrowed, .i32);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
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" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// Build a fn that returns `i64` 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(.i64);
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, .i64), 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" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// Returns (i64 == i64) — 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(.i64);
const c = b.constType(.i64);
const eq = b.cmpEq(a, c);
b.ret(eq, .bool);
b.finalize();
// Different TypeIds: (i64 == i32) 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(.i64);
const c2 = b.constType(.i32);
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().?);
}
// ── Test: type_name builtin reads .type_tag, returns the typeName ───────
test "comptime: type_name builtin on type_tag" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test_type_name"), &.{}, .string);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const t = b.constType(.i64);
var args = [_]inst_mod.Ref{t};
const r = b.callBuiltin(.type_name, &args, .string);
b.ret(r, .string);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const result = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqualStrings("i64", result.asString(&interp).?);
}
// ── Test: type_eq builtin on two .type_tag operands ────────────────────
test "comptime: type_eq builtin on type_tag values" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "test_type_eq_builtin"), &.{}, .bool);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const a = b.constType(.string);
const c = b.constType(.string);
var args = [_]inst_mod.Ref{ a, c };
const r = b.callBuiltin(.type_eq, &args, .bool);
b.ret(r, .bool);
b.finalize();
var interp = Interpreter.init(&module, alloc);
defer interp.deinit();
const result = try interp.call(FuncId.fromIndex(0), &.{});
try std.testing.expectEqual(true, result.asBool().?);
}
// ── Test: reflectTypeId reads an Any's runtime TYPE-TAG, not its payload ──
// A reflection builtin on an Any must report the type OF a held value (the
// tag) and only read the payload when the Any holds a Type value (tag ==
// `.any`). Regression: a boxed value like
// `av : Any = 6` (`{ tag = i64, value = 6 }`) must resolve to `i64`, NOT
// `types[6]` (`u8`).
test "reflect: reflectTypeId branches on the Any tag" {
// The "Any holds a Type" meta-marker tag is `.type_value` (an Any boxing a
// Type value carries `{ tag = .type_value, value = tid }`), distinct from a
// boxed runtime value whose tag is the held value's own type.
const type_marker: i64 = @intCast(TypeId.type_value.index());
// Native first-class Type value → the held TypeId directly.
try std.testing.expectEqual(@as(?TypeId, .u64), (Value{ .type_tag = .u64 }).reflectTypeId());
// Any holding a VALUE: `{ tag = i64, value = 6 }` → i64 (the tag),
// never `types[6]` (u8). This is the bug the fix closes.
var held_value = [_]Value{ .{ .int = @intCast(TypeId.i64.index()) }, .{ .int = 6 } };
try std.testing.expectEqual(@as(?TypeId, .i64), (Value{ .aggregate = &held_value }).reflectTypeId());
// Any holding a VALUE of an unsigned type: `{ tag = u32, value = 7 }` → u32.
var held_u32 = [_]Value{ .{ .int = @intCast(TypeId.u32.index()) }, .{ .int = 7 } };
try std.testing.expectEqual(@as(?TypeId, .u32), (Value{ .aggregate = &held_u32 }).reflectTypeId());
// Any holding a TYPE value (the `type_of(x)` / `const_type` shape):
// `{ tag = .type_value, value = u64 }` → u64 (the payload). Payload as a plain
// int (the runtime box shape) ...
var held_type_int = [_]Value{ .{ .int = type_marker }, .{ .int = @intCast(TypeId.u64.index()) } };
try std.testing.expectEqual(@as(?TypeId, .u64), (Value{ .aggregate = &held_type_int }).reflectTypeId());
// ... and payload as a `.type_tag` (the comptime box shape) → same result.
var held_type_tag = [_]Value{ .{ .int = type_marker }, .{ .type_tag = .u64 } };
try std.testing.expectEqual(@as(?TypeId, .u64), (Value{ .aggregate = &held_type_tag }).reflectTypeId());
// Neither shape → null (the caller bails loudly, never guesses a TypeId).
try std.testing.expectEqual(@as(?TypeId, null), (Value{ .int = 6 }).reflectTypeId());
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ pub const types = @import("types.zig");
pub const inst = @import("inst.zig");
pub const module = @import("module.zig");
pub const print = @import("print.zig");
pub const interp = @import("interp.zig");
pub const comptime_value = @import("comptime_value.zig");
pub const lower = @import("lower.zig");
pub const program_index = @import("program_index.zig");
pub const resolver = @import("resolver.zig");
@@ -40,8 +40,7 @@ pub const Builder = module.Builder;
pub const ImplTable = module.ImplTable;
pub const printModule = print.printModule;
pub const Interpreter = interp.Interpreter;
pub const Value = interp.Value;
pub const Value = comptime_value.Value;
pub const Lowering = lower.Lowering;
pub const ProgramIndex = program_index.ProgramIndex;
pub const TypeResolver = type_resolver.TypeResolver;
@@ -75,7 +74,6 @@ pub const types_tests = @import("types.test.zig");
pub const inst_tests = @import("inst.test.zig");
pub const module_tests = @import("module.test.zig");
pub const print_tests = @import("print.test.zig");
pub const interp_tests = @import("interp.test.zig");
pub const lower_tests = @import("lower.test.zig");
pub const program_index_tests = @import("program_index.test.zig");
pub const resolver_tests = @import("resolver.test.zig");

View File

@@ -8,7 +8,6 @@ const mod_mod = @import("module.zig");
const type_bridge = @import("type_bridge.zig");
const unescape = @import("../unescape.zig");
const parser_mod = @import("../parser.zig");
const interp_mod = @import("interp.zig");
const errors = @import("../errors.zig");
const jni_descriptor = @import("jni_descriptor.zig");
const program_index_mod = @import("program_index.zig");

View File

@@ -461,30 +461,13 @@ fn deriveOutputName(input_path: []const u8) []const u8 {
/// Format the "interpreter bailed during X" message, attaching the IR op
/// and the source location (line:col) when the interpreter captured them.
fn printInterpBailDiag(comp: *const sx.core.Compilation, label: []const u8, err: anyerror) void {
const op = sx.ir.Interpreter.last_bail_op orelse {
// The post-link build driver runs on the comptime VM (core.invokeByFuncId),
// so a bail there sets `comptime_vm.last_bail_reason`, not the legacy
// interp's statics. Surface that reason when present.
if (sx.ir.comptime_vm.last_bail_reason) |reason| {
std.debug.print("error: {s} failed: {s}: {s}\n", .{ label, @errorName(err), reason });
return;
}
std.debug.print("error: {s} failed: {s}\n", .{ label, @errorName(err) });
return;
};
const op_detail: []const u8 = if (sx.ir.Interpreter.last_bail_builtin) |b| b else op;
const explanation = sx.ir.Interpreter.last_bail_detail orelse "";
const sep: []const u8 = if (explanation.len > 0) ": " else "";
if (sx.ir.Interpreter.last_bail_file) |file| {
if (comp.import_sources.get(file)) |source| {
const loc = sx.errors.SourceLoc.compute(source, sx.ir.Interpreter.last_bail_offset);
std.debug.print("error: {s} failed: {s} (op={s}/{s}{s}{s}) at {s}:{d}:{d}\n", .{ label, @errorName(err), op, op_detail, sep, explanation, file, loc.line, loc.col });
return;
}
std.debug.print("error: {s} failed: {s} (op={s}/{s}{s}{s}) at {s}:+{d}\n", .{ label, @errorName(err), op, op_detail, sep, explanation, file, sx.ir.Interpreter.last_bail_offset });
_ = comp;
// The comptime VM is the sole evaluator; a bail sets comptime_vm.last_bail_reason.
if (sx.ir.comptime_vm.last_bail_reason) |reason| {
std.debug.print("error: {s} failed: {s}: {s}\n", .{ label, @errorName(err), reason });
return;
}
std.debug.print("error: {s} failed: {s} (op={s}/{s}{s}{s})\n", .{ label, @errorName(err), op, op_detail, sep, explanation });
std.debug.print("error: {s} failed: {s}\n", .{ label, @errorName(err) });
}
fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 {