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:
@@ -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
|
||||
|
||||
@@ -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
108
src/ir/comptime_value.zig
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
2383
src/ir/interp.zig
2383
src/ir/interp.zig
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
27
src/main.zig
27
src/main.zig
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user