From 7b8be86834a20df80348ebea499f5f9ec118dbc4 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 19 Jun 2026 20:05:57 +0300 Subject: [PATCH] =?UTF-8?q?P5.7=20Step=20C:=20delete=20interp.zig=20?= =?UTF-8?q?=E2=80=94=20the=20comptime=20VM=20is=20the=20sole=20evaluator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- current/CHECKPOINT-COMPILER-API.md | 24 + src/ir/compiler_lib.zig | 449 +----- src/ir/comptime_value.zig | 108 ++ src/ir/comptime_vm.test.zig | 37 +- src/ir/comptime_vm.zig | 53 +- src/ir/emit_llvm.zig | 6 +- src/ir/interp.test.zig | 844 ---------- src/ir/interp.zig | 2383 ---------------------------- src/ir/ir.zig | 6 +- src/ir/lower.zig | 1 - src/main.zig | 27 +- 11 files changed, 217 insertions(+), 3721 deletions(-) create mode 100644 src/ir/comptime_value.zig delete mode 100644 src/ir/interp.test.zig delete mode 100644 src/ir/interp.zig diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index a42bd292..0bc8b7af 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -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 diff --git a/src/ir/compiler_lib.zig b/src/ir/compiler_lib.zig index fdfd94b4..57e4a76f 100644 --- a/src/ir/compiler_lib.zig +++ b/src/ir/compiler_lib.zig @@ -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 ` with a /// different `` 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; -} diff --git a/src/ir/comptime_value.zig b/src/ir/comptime_value.zig new file mode 100644 index 00000000..0cb0b1dd --- /dev/null +++ b/src/ir/comptime_value.zig @@ -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; +} diff --git a/src/ir/comptime_vm.test.zig b/src/ir/comptime_vm.test.zig index d0fbcdaa..599160e1 100644 --- a/src/ir/comptime_vm.test.zig +++ b/src/ir/comptime_vm.test.zig @@ -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); diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index 9de53f39..6ccdf9d8 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -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 { diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 197139ba..c2580ac0 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -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 diff --git a/src/ir/interp.test.zig b/src/ir/interp.test.zig deleted file mode 100644 index 512149bc..00000000 --- a/src/ir/interp.test.zig +++ /dev/null @@ -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()); -} diff --git a/src/ir/interp.zig b/src/ir/interp.zig deleted file mode 100644 index 12d46c45..00000000 --- a/src/ir/interp.zig +++ /dev/null @@ -1,2383 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const types = @import("types.zig"); -const inst_mod = @import("inst.zig"); -const mod_mod = @import("module.zig"); -const errors = @import("../errors.zig"); - -const TypeId = types.TypeId; -const TypeTable = types.TypeTable; -const StringId = types.StringId; -const Ref = inst_mod.Ref; -const BlockId = inst_mod.BlockId; -const FuncId = inst_mod.FuncId; -const Inst = inst_mod.Inst; -const Op = inst_mod.Op; -const Function = inst_mod.Function; -const Block = inst_mod.Block; -const Module = mod_mod.Module; -const Builder = mod_mod.Builder; - -// ── 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 Interpreter.heap - 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, - }; - } - - /// Resolve the `TypeId` a dynamic `type_name` / `type_is_unsigned` must - /// operate on, honoring the rule that a reflection builtin reads an - /// `Any`'s runtime TYPE-TAG, never its raw payload: - /// - a native `.type_tag(tid)` Value → `tid` (a first-class Type value). - /// - an `Any` aggregate `{ tag, value }`: when the tag is `.any`, the - /// box carries a *Type value* (the `box_any(.., .any)` / `const_type` - /// shape) → the TypeId is the payload; otherwise the box carries a - /// *runtime value* whose type IS the tag → the tag is the TypeId. This - /// makes `type_name(av)` for `av : Any = 6` report `i64` (the held - /// value's type) while `type_name(type_of(x))` still names the type. - /// Returns null when `self` is neither shape (the caller bails loudly). - pub fn reflectTypeId(self: Value) ?TypeId { - if (self.asTypeId()) |t| return t; - if (self == .aggregate) { - const fields = self.aggregate; - if (fields.len >= 2) { - const tag = fields[0].asInt() orelse return null; - if (tag == @as(i64, @intCast(TypeId.type_value.index()))) { - if (fields[1].asTypeId()) |t| return t; - if (fields[1].asInt()) |iv| return TypeId.fromIndex(@intCast(iv)); - return null; - } - return TypeId.fromIndex(@intCast(tag)); - } - } - return null; - } - - /// Get the string content, whether from a literal or a heap-backed string aggregate. - pub fn asString(self: Value, interp: *const Interpreter) ?[]const u8 { - return switch (self) { - .string => |s| s, - .aggregate => |fields| { - // String fat pointer: { heap_ptr/string/raw_int_ptr, int(len) } - if (fields.len == 2) { - const len: usize = @intCast(fields[1].asInt() orelse return null); - switch (fields[0]) { - .heap_ptr => |hp| { - const mem = interp.heapSlice(hp) orelse return null; - return if (len <= mem.len) mem[0..len] else null; - }, - .string => |s| return if (len <= s.len) s[0..len] else s, - // Raw host pointer (e.g. from CAllocator.alloc → - // libc_malloc). Read `len` bytes back from real - // memory. - .int => |addr| { - const p: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(addr))); - return p[0..len]; - }, - else => return null, - } - } - return null; - }, - else => null, - }; - } -}; - -// ── Error ─────────────────────────────────────────────────────────────── - -pub const InterpError = error{ - CannotEvalComptime, - TypeError, - OutOfBounds, - DivisionByZero, - StackOverflow, - Unreachable, -}; - -const compiler_hooks = @import("compiler_hooks.zig"); -pub const BuildConfig = compiler_hooks.BuildConfig; -const compiler_lib = @import("compiler_lib.zig"); -const host_ffi = @import("host_ffi.zig"); - -// ── Interpreter ───────────────────────────────────────────────────────── - -pub const Interpreter = struct { - module: *const Module, - alloc: Allocator, - output: std.ArrayList(u8), - call_depth: u32 = 0, - max_call_depth: u32 = 256, - /// Active sx call-frame chain (oldest→newest), maintained across `call` for - /// `trace.print_interpreter_frames()` (ERR E4.1). Only sx-bodied frames are - /// tracked — extern calls return before the frame is pushed. - call_chain: std.ArrayList(FuncId) = .empty, - - /// File → source text (the diagnostics' import_sources). Set by the host - /// where available so `.trace_resolve` can turn a `(func_id, span.start)` - /// frame into `file:line:col` at comptime (ERR E3.0 slice 3b). Null → the - /// resolver degrades to line/col 1:1. - source_map: ?*const std.StringHashMap([:0]const u8) = null, - - /// Comptime type-MINT target — the SAME `TypeTable` the host (`Lowering`) - /// owns (aliases `self.module.types`; the const view here and the host's - /// mutable view point at one table). Set by the host before a comptime-eval - /// that may run `declare`/`define`. Null elsewhere (unit tests, emit-time - /// `#run`) → those builtins bail loudly. - mint: ?*types.TypeTable = null, - - // Heap: dynamically allocated memory blocks - heap: std.ArrayList([]u8), - - // Global values: evaluated comptime globals, indexed by GlobalId - global_values: std.AutoHashMap(u32, Value), - - // Mutable build configuration — set by LLVMEmitter, written by #run blocks - build_config: ?*BuildConfig = null, - - // First op tag that bailed with InterpError, captured the first - // time the interpreter unwinds so callers can surface "op=foo at - // :" alongside the bare error name. Static so it - // survives Interpreter teardown (lifetime: program global). - pub var last_bail_op: ?[]const u8 = null; - pub var last_bail_file: ?[]const u8 = null; - pub var last_bail_offset: u32 = 0; - pub var last_bail_builtin: ?[]const u8 = null; - /// Free-text explanation of WHY the bail happened — set at sites - /// that currently can't handle a specific Value/op combination - /// (raw-pointer loads, struct_gep through `*void`, etc.). The host - /// diagnostic renderer surfaces this so users see "load through - /// raw pointer not supported" instead of a bare `CannotEvalComptime`. - pub var last_bail_detail: ?[]const u8 = null; - - /// Set `last_bail_detail` to a static message and return the error. - /// Use at sites where a specific raw-pointer Value tag isn't handled - /// so users get a clear explanation instead of guessing. - fn bailDetail(comptime msg: []const u8) InterpError { - if (last_bail_detail == null) last_bail_detail = msg; - return error.CannotEvalComptime; - } - - /// Like `bailDetail` but returns a `TypeError` — for extern-arg - /// marshalling sites that previously erased the reason. - fn typeErrorDetail(comptime msg: []const u8) InterpError { - if (last_bail_detail == null) last_bail_detail = msg; - return error.TypeError; - } - - pub fn init(module: *const Module, alloc: Allocator) Interpreter { - return .{ - .module = module, - .alloc = alloc, - .output = std.ArrayList(u8).empty, - .heap = std.ArrayList([]u8).empty, - .global_values = std.AutoHashMap(u32, Value).init(alloc), - }; - } - - /// Provide the file→source map so `.trace_resolve` can compute file:line:col - /// for comptime trace frames. Optional — absent in unit tests. - pub fn setSourceMap(self: *Interpreter, sm: *const std.StringHashMap([:0]const u8)) void { - self.source_map = sm; - } - - /// Enable the comptime type-construction builtins (`declare`/`define`) by - /// handing the interp the host's mutable `TypeTable`. Called by `Lowering` - /// before a comptime-eval that may mint types. - pub fn setMintTable(self: *Interpreter, tbl: *types.TypeTable) void { - self.mint = tbl; - } - - pub fn deinit(self: *Interpreter) void { - // Free all heap allocations - for (self.heap.items) |block| { - self.alloc.free(block); - } - self.heap.deinit(self.alloc); - self.output.deinit(self.alloc); - self.call_chain.deinit(self.alloc); - self.global_values.deinit(); - } - - /// Write `val` to the raw host address `addr` using exactly the - /// number of bytes declared by `val_ty`. Used when the - /// protocol-dispatch chain bottoms out at a extern-libc-malloc - /// pointer and sx code stores through it. Comptime safety is the - /// caller's responsibility — wild writes will fault. - fn storeAtRawPtr(self: *Interpreter, addr: i64, val: Value, val_ty: @import("types.zig").TypeId) InterpError!void { - const dst: [*]u8 = @ptrFromInt(@as(usize, @bitCast(addr))); - const width = self.module.types.typeSizeBytes(val_ty); - switch (val) { - .int => |v| { - // Width is whatever the declared IR type says (i8..i64, - // u8..u64, usize, pointer-as-int, bool-after-extension). - // The Value tag itself is .int regardless. - if (width == 0 or width > 8) return bailDetail("comptime store of int through raw pointer: unexpected declared width (expected 1..8 bytes)"); - const bytes = std.mem.toBytes(v); - @memcpy(dst[0..width], bytes[0..width]); - }, - .float => |v| { - switch (width) { - 8 => { - const bytes = std.mem.toBytes(v); - @memcpy(dst[0..8], &bytes); - }, - 4 => { - const f32_v: f32 = @floatCast(v); - const bytes = std.mem.toBytes(f32_v); - @memcpy(dst[0..4], &bytes); - }, - else => return bailDetail("comptime store of float through raw pointer: unexpected declared width (expected 4 or 8 bytes)"), - } - }, - .boolean => |v| { - if (width == 0) return bailDetail("comptime store of bool through raw pointer: zero-width destination"); - @memset(dst[0..width], 0); - dst[0] = if (v) 1 else 0; - }, - .null_val => { - if (width == 0 or width > 8) return bailDetail("comptime store of null through raw pointer: unexpected declared width"); - @memset(dst[0..width], 0); - }, - .aggregate => return bailDetail("comptime store of aggregate through raw pointer not supported (struct field layout not threaded into Store IR op)"), - .heap_ptr => return bailDetail("comptime store of interp-heap pointer through raw pointer not supported"), - .byte_ptr => return bailDetail("comptime store of byte pointer through raw pointer not supported"), - .slot_ptr => return bailDetail("comptime store of slot pointer through raw pointer not supported (frame-local slot indices aren't meaningful as memory contents)"), - .func_ref => return bailDetail("comptime store of func_ref through raw pointer not supported"), - .closure => return bailDetail("comptime store of closure value through raw pointer not supported"), - .string, .type_tag, .void_val, .undef => return bailDetail("comptime store: unsupported Value kind at raw destination"), - } - } - - // ── Implicit Context ────────────────────────────────────────── - - /// Build the default Context aggregate for top-level interp calls. - /// Mirrors the static `__sx_default_context` LLVM global: a Context - /// whose `allocator` field is the stateless CAllocator inline-protocol - /// value (alloc/dealloc thunks bottom out at libc malloc/free). - fn defaultContextValue(self: *Interpreter) Value { - const tbl_ptr: *const @import("types.zig").TypeTable = &self.module.types; - const tbl = @constCast(tbl_ptr); - const alloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_alloc_bytes"); - const dealloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_dealloc_bytes"); - - var alloc_fn: Value = .null_val; - var dealloc_fn: Value = .null_val; - for (self.module.functions.items, 0..) |func, i| { - if (func.name == alloc_thunk_name) alloc_fn = .{ .func_ref = FuncId.fromIndex(@intCast(i)) }; - if (func.name == dealloc_thunk_name) dealloc_fn = .{ .func_ref = FuncId.fromIndex(@intCast(i)) }; - } - - const allocator_fields = self.alloc.alloc(Value, 3) catch unreachable; - allocator_fields[0] = .null_val; // CAllocator receiver — stateless - allocator_fields[1] = alloc_fn; - allocator_fields[2] = dealloc_fn; - const allocator_val: Value = .{ .aggregate = allocator_fields }; - - const ctx_fields = self.alloc.alloc(Value, 2) catch unreachable; - ctx_fields[0] = allocator_val; - ctx_fields[1] = .null_val; - return .{ .aggregate = ctx_fields }; - } - - // ── Heap operations ──────────────────────────────────────────── - - fn heapAlloc(self: *Interpreter, size: usize) Value.HeapPtr { - const mem = self.alloc.alloc(u8, size) catch unreachable; - @memset(mem, 0); - const id: u32 = @intCast(self.heap.items.len); - self.heap.append(self.alloc, mem) catch unreachable; - return .{ .id = id }; - } - - fn heapFree(self: *Interpreter, hp: Value.HeapPtr) void { - if (hp.id < self.heap.items.len) { - self.alloc.free(self.heap.items[hp.id]); - self.heap.items[hp.id] = &.{}; - } - } - - pub fn heapSlice(self: *const Interpreter, hp: Value.HeapPtr) ?[]u8 { - if (hp.id >= self.heap.items.len) return null; - const mem = self.heap.items[hp.id]; - if (hp.offset >= mem.len) return null; - return mem[hp.offset..]; - } - - fn heapMemcpy(self: *Interpreter, dst: Value.HeapPtr, src_bytes: []const u8, len: usize) void { - const dst_mem = self.heapSlice(dst) orelse return; - const copy_len = @min(len, @min(dst_mem.len, src_bytes.len)); - @memcpy(dst_mem[0..copy_len], src_bytes[0..copy_len]); - } - - fn heapMemset(self: *Interpreter, dst: Value.HeapPtr, val: u8, len: usize) void { - const dst_mem = self.heapSlice(dst) orelse return; - const set_len = @min(len, dst_mem.len); - @memset(dst_mem[0..set_len], val); - } - - fn heapStoreByte(self: *Interpreter, dst: Value.HeapPtr, val: u8) void { - const mem = self.heapSlice(dst) orelse return; - if (mem.len > 0) mem[0] = val; - } - - /// Look up a global value, lazy-evaluating its comptime_func if needed. - fn getGlobal(self: *Interpreter, gid: inst_mod.GlobalId) InterpError!Value { - const idx = gid.index(); - // Check cache first - if (self.global_values.get(idx)) |v| return v; - - // Not cached — evaluate from global definition - const global = &self.module.globals.items[idx]; - if (global.comptime_func) |func_id| { - const result = try self.call(func_id, &.{}); - self.global_values.put(idx, result) catch {}; - return result; - } - // Static init value - if (global.init_val) |iv| { - const val: Value = self.constToValue(iv); - self.global_values.put(idx, val) catch {}; - return val; - } - return .undef; - } - - /// Marshal a single sx Value into a `usize` slot for a cdecl host call. - /// Strings are made null-terminated; pointer-like values pass their - /// underlying address. The returned `usize` is only valid for the - /// duration of this call — caller-allocated buffers are tracked in - /// `tmp` so they get freed once the call returns. - fn marshalExternArg(self: *Interpreter, v: Value, tmp: *std.ArrayList([]u8)) !usize { - return switch (v) { - .int => |i| @bitCast(i), - .boolean => |b| @intFromBool(b), - .null_val => 0, - .byte_ptr => |addr| addr, - .heap_ptr => |hp| blk: { - // `heapSlice` returns the slice already advanced by `hp.offset`, - // so its `.ptr` IS the offset address. Adding `hp.offset` again - // double-counts and lands the extern call past the buffer end. - _ = self.heapSlice(hp) orelse return error.TypeError; - break :blk @intFromPtr(self.heap.items[hp.id].ptr) + hp.offset; - }, - .string => |s| blk: { - const buf = try self.alloc.alloc(u8, s.len + 1); - @memcpy(buf[0..s.len], s); - buf[s.len] = 0; - tmp.append(self.alloc, buf) catch return error.TypeError; - break :blk @intFromPtr(buf.ptr); - }, - .aggregate => |fields| blk: { - // Fat string pointer: { ptr, len }. Pass the raw bytes - // null-terminated so libc string APIs work. - if (fields.len == 2) { - const len: usize = @intCast(fields[1].asInt() orelse return error.TypeError); - switch (fields[0]) { - .heap_ptr => |hp| { - const mem = self.heapSlice(hp) orelse return error.TypeError; - const start = hp.offset; - const slice = mem[start .. start + len]; - const buf = try self.alloc.alloc(u8, len + 1); - @memcpy(buf[0..len], slice); - buf[len] = 0; - tmp.append(self.alloc, buf) catch return error.TypeError; - break :blk @intFromPtr(buf.ptr); - }, - .string => |s| { - const slice = if (len <= s.len) s[0..len] else s; - const buf = try self.alloc.alloc(u8, slice.len + 1); - @memcpy(buf[0..slice.len], slice); - buf[slice.len] = 0; - tmp.append(self.alloc, buf) catch return error.TypeError; - break :blk @intFromPtr(buf.ptr); - }, - // Raw host pointer (from libc_malloc-backed - // cstring). Read bytes from real memory and copy - // into a null-terminated buffer the extern call - // can consume. - .int => |addr| { - const src: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(addr))); - const buf = try self.alloc.alloc(u8, len + 1); - @memcpy(buf[0..len], src[0..len]); - buf[len] = 0; - tmp.append(self.alloc, buf) catch return error.TypeError; - break :blk @intFromPtr(buf.ptr); - }, - else => return typeErrorDetail("comptime extern call: unsupported aggregate data-field kind (expected heap_ptr/string/int)"), - } - } - return typeErrorDetail("comptime extern call: aggregate arg must be a {ptr, len} fat-pointer pair"); - }, - else => return typeErrorDetail("comptime extern call: unsupported arg Value kind"), - }; - } - - /// Append the current sx call-frame chain to the interp output, most-recent - /// last (ERR E4.1). The topmost frame is `print_interpreter_frames` itself - /// (the dump site), so it's skipped. Frame source locations await IR-offset - /// resolution (the comptime analog of DWARF), so only function names print. - fn printInterpFrames(self: *Interpreter) void { - const n = self.call_chain.items.len; - if (n <= 1) return; - self.output.appendSlice(self.alloc, "comptime call frames (most recent call last):\n") catch {}; - var i: usize = 0; - while (i < n - 1) : (i += 1) { - const fid = self.call_chain.items[i]; - const fname = self.module.types.getString(self.module.getFunction(fid).name); - const line = std.fmt.allocPrint(self.alloc, " at {s}\n", .{fname}) catch continue; - defer self.alloc.free(line); - self.output.appendSlice(self.alloc, line) catch {}; - } - } - - fn callExtern(self: *Interpreter, func: *const inst_mod.Function, args: []const Value) InterpError!Value { - const name = self.module.types.getString(func.name); - - // A extern call may not return (e.g. `process.exit` → `_exit`), which - // would discard the interpreter's buffered `print` output (otherwise - // flushed only after `#run` completes). Flush it first so comptime - // diagnostics emitted just before a terminating call survive. - if (self.output.items.len > 0) { - _ = std.c.write(1, self.output.items.ptr, self.output.items.len); - self.output.clearRetainingCapacity(); - } - const symbol = (host_ffi.lookupSymbol(self.alloc, name) catch return bailDetail("comptime extern call: dlsym error looking up symbol")) orelse { - if (last_bail_detail == null) last_bail_detail = "comptime extern call: symbol not found via dlsym (target-specific binding called at compile time?)"; - return error.CannotEvalComptime; - }; - - var packed_args: [8]usize = undefined; - if (args.len > packed_args.len) return bailDetail("comptime extern call: more than 8 args (host_ffi trampolines max out at 8)"); - - var tmp = std.ArrayList([]u8).empty; - defer { - for (tmp.items) |buf| self.alloc.free(buf); - tmp.deinit(self.alloc); - } - for (args, 0..) |a, i| { - packed_args[i] = self.marshalExternArg(a, &tmp) catch return error.TypeError; - } - const argv = packed_args[0..args.len]; - - // Variadic extern functions (declared `args: ..T`) must be - // dispatched through C-variadic trampolines so the trailing - // args land in the right place per the target's variadic - // ABI. The fixed-arity trampolines would put them in arg - // registers, and the callee would read garbage from the - // stack. - const fixed = func.params.len; - const variadic = func.is_variadic and args.len > fixed; - - const ret = func.ret; - if (ret == .void) { - if (variadic) { - host_ffi.callVoidRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime; - } else { - host_ffi.callVoidRet(symbol, argv) catch return error.CannotEvalComptime; - } - return .void_val; - } - if (ret == .i8 or ret == .i16 or ret == .i32 or ret == .i64 or - ret == .u8 or ret == .u16 or ret == .u32 or ret == .u64 or - ret == .usize or ret == .isize) - { - const r = if (variadic) - host_ffi.callIntRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime - else - host_ffi.callIntRet(symbol, argv) catch return error.CannotEvalComptime; - return Value{ .int = r }; - } - if (ret == .bool) { - const r = if (variadic) - host_ffi.callIntRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime - else - host_ffi.callIntRet(symbol, argv) catch return error.CannotEvalComptime; - return Value{ .boolean = r != 0 }; - } - const r = if (variadic) - host_ffi.callPtrRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime - else - host_ffi.callPtrRet(symbol, argv) catch return error.CannotEvalComptime; - return Value{ .int = @bitCast(@as(u64, r)) }; - } - - pub fn call(self: *Interpreter, func_id: FuncId, args: []const Value) InterpError!Value { - if (self.call_depth >= self.max_call_depth) return error.StackOverflow; - self.call_depth += 1; - defer self.call_depth -= 1; - - const func = self.module.getFunction(func_id); - // Welded `compiler`-library function: dispatch to its registered Zig - // handler (comptime-only), never dlsym. The binding registry IS the - // safety boundary — a name not on the export list is a clean bail. - if (func.compiler_welded) { - const fname = self.module.types.getString(func.name); - const bf = compiler_lib.findFn(fname) orelse - return bailDetail("comptime compiler call: function not exported by the compiler library"); - return bf.handler(self, args); - } - if (func.is_extern or func.blocks.items.len == 0) { - // Dispatch to host libc via dlsym. Lets `#run` (and the - // post-link bundler) call ordinary extern symbols like - // `puts`, `getenv`, `posix_spawn`, etc. - return self.callExtern(func, args); - } - - // Track the sx call chain for `trace.print_interpreter_frames()`. - self.call_chain.append(self.alloc, func_id) catch {}; - defer _ = self.call_chain.pop(); - - // Compute total refs: params + all instructions across all blocks - var total_refs: u32 = @intCast(func.params.len); - for (func.blocks.items) |blk| { - total_refs += @intCast(blk.insts.items.len); - } - - var frame = Frame.initSized(self.alloc, total_refs); - defer frame.deinit(); - - // Implicit-context bootstrap: when an entry point with implicit - // ctx is called without an explicit ctx arg, materialise the - // default context in a fresh slot and bind slot_ptr(0) to ref 0. - // This is the interp-side equivalent of FFI-inbound wrappers - // installing `&__sx_default_context` at function entry. - var skip_first: u32 = 0; - if (func.has_implicit_ctx and args.len + 1 == func.params.len) { - const ctx_val = self.defaultContextValue(); - const slot = frame.allocSlot(self.alloc); - frame.storeSlot(slot, ctx_val); - frame.setRef(0, .{ .slot_ptr = slot }); - skip_first = 1; - } - - // Bind parameters as initial refs (indices skip_first..N-1) - for (args, 0..) |arg, i| { - frame.setRef(@intCast(i + skip_first), arg); - } - - // Start at the entry block (index 0) - var current_block: BlockId = BlockId.fromIndex(0); - var block_args: []const Value = &.{}; - - while (true) { - const block_idx = current_block.index(); - const block = &func.blocks.items[block_idx]; - var ref_counter: u32 = block.first_ref; - - // Bind block params (block_param instructions handle this, but we - // also need to pre-set the values for them) - for (block_args) |_| { - // block_param instructions will read from frame refs when executed - // The block_param instruction itself produces the value - } - - for (block.insts.items) |*instruction| { - // Special handling for block_param: bind the arg value - if (instruction.op == .block_param) { - const bp = instruction.op.block_param; - if (bp.param_index < block_args.len) { - frame.setRef(ref_counter, block_args[bp.param_index]); - } - ref_counter += 1; - continue; - } - - const result = self.execInst(instruction, &frame, ¤t_block, &block_args) catch |err| { - if (last_bail_op == null) { - last_bail_op = @tagName(instruction.op); - last_bail_file = func.source_file; - last_bail_offset = instruction.span.start; - } - return err; - }; - switch (result) { - .value => |val| { - frame.setRef(ref_counter, val); - ref_counter += 1; - }, - .branch => { - ref_counter += 1; // terminator consumes a ref slot - break; - }, - .ret_val => |val| return val, - .ret_nothing => return .void_val, - } - } else { - // Fell through the block with no terminator — treat as implicit return void - return .void_val; - } - } - } - - const ExecResult = union(enum) { - value: Value, - branch, - ret_val: Value, - ret_nothing, - }; - - fn execInst(self: *Interpreter, instruction: *const Inst, frame: *Frame, current_block: *BlockId, block_args: *[]const Value) InterpError!ExecResult { - const op = instruction.op; - - switch (op) { - // ── Constants ─────────────────────────────────────── - .const_int => |v| return .{ .value = .{ .int = v } }, - .const_float => |v| return .{ .value = .{ .float = v } }, - .const_bool => |v| return .{ .value = .{ .boolean = v } }, - .const_string => |sid| return .{ .value = .{ .string = self.module.types.getString(sid) } }, - .const_null => return .{ .value = .null_val }, - .const_undef => return .{ .value = .undef }, - .is_comptime => return .{ .value = .{ .boolean = true } }, - .interp_print_frames => { - self.printInterpFrames(); - return .{ .value = .void_val }; - }, - .trace_frame => { - // Comptime frame: pack (func_id, span.start) so the slice-3b - // resolver can recover file:line:col via the IR/source tables. - // The interp never produces a `Frame*` — only the compiled - // backend does — so this stays a packed id, never a pointer. - const fid: u64 = if (self.call_chain.items.len > 0) - self.call_chain.items[self.call_chain.items.len - 1].index() - else - 0; - const packed_frame: u64 = (fid << 32) | @as(u64, instruction.span.start); - return .{ .value = .{ .int = @bitCast(packed_frame) } }; - }, - .trace_resolve => |u| { - // Unpack the comptime frame `(func_id << 32 | span.start)` and - // resolve it to a `Frame { file, line, col, func }` aggregate. - const raw: u64 = @bitCast(frame.getRef(u.operand).asInt() orelse 0); - const fid: u32 = @intCast(raw >> 32); - const offset: u32 = @truncate(raw); - const func = self.module.getFunction(FuncId.fromIndex(fid)); - const func_name = self.module.types.getString(func.name); - const file_full = func.source_file orelse ""; - const file = std.fs.path.basename(file_full); - var line: i64 = 1; - var col: i64 = 1; - var line_text: []const u8 = ""; - if (self.source_map) |sm| { - if (sm.get(file_full)) |src| { - const loc = errors.SourceLoc.compute(src, offset); - line = @intCast(loc.line); - col = @intCast(loc.col); - line_text = errors.lineAt(src, offset); - } - } - const fields = self.alloc.alloc(Value, 5) catch return .{ .value = .undef }; - fields[0] = .{ .string = file }; - fields[1] = .{ .int = line }; - fields[2] = .{ .int = col }; - fields[3] = .{ .string = func_name }; - fields[4] = .{ .string = line_text }; - return .{ .value = .{ .aggregate = fields } }; - }, - .const_type => |tid| return .{ .value = .{ .type_tag = tid } }, - - // ── Arithmetic ────────────────────────────────────── - .add => |b| return .{ .value = try self.evalArith(frame, b, .add) }, - .sub => |b| return .{ .value = try self.evalArith(frame, b, .sub) }, - .mul => |b| return .{ .value = try self.evalArith(frame, b, .mul) }, - .div => |b| return .{ .value = try self.evalArith(frame, b, .div) }, - .mod => |b| return .{ .value = try self.evalArith(frame, b, .mod) }, - .neg => |u| { - const val = frame.getRef(u.operand); - return .{ .value = switch (val) { - .int => |v| .{ .int = -v }, - .float => |v| .{ .float = -v }, - else => return typeErrorDetail("comptime unary `-`: operand is neither int nor float"), - } }; - }, - - // ── Comparison ────────────────────────────────────── - .cmp_eq => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .eq) } }, - .cmp_ne => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .ne) } }, - .cmp_lt => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .lt) } }, - .cmp_le => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .le) } }, - .cmp_gt => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .gt) } }, - .cmp_ge => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .ge) } }, - .str_eq => |b| { - const lhs = frame.getRef(b.lhs); - const rhs = frame.getRef(b.rhs); - const ls = if (lhs == .string) lhs.string else ""; - const rs = if (rhs == .string) rhs.string else ""; - return .{ .value = .{ .boolean = std.mem.eql(u8, ls, rs) } }; - }, - .str_ne => |b| { - const lhs = frame.getRef(b.lhs); - const rhs = frame.getRef(b.rhs); - const ls = if (lhs == .string) lhs.string else ""; - const rs = if (rhs == .string) rhs.string else ""; - return .{ .value = .{ .boolean = !std.mem.eql(u8, ls, rs) } }; - }, - - // ── Logical ───────────────────────────────────────── - .bool_and => |b| { - const lhs = frame.getRef(b.lhs).asBool() orelse return error.TypeError; - if (!lhs) return .{ .value = .{ .boolean = false } }; - const rhs = frame.getRef(b.rhs).asBool() orelse return error.TypeError; - return .{ .value = .{ .boolean = rhs } }; - }, - .bool_or => |b| { - const lhs = frame.getRef(b.lhs).asBool() orelse return error.TypeError; - if (lhs) return .{ .value = .{ .boolean = true } }; - const rhs = frame.getRef(b.rhs).asBool() orelse return error.TypeError; - return .{ .value = .{ .boolean = rhs } }; - }, - .bool_not => |u| { - const val = frame.getRef(u.operand).asBool() orelse return error.TypeError; - return .{ .value = .{ .boolean = !val } }; - }, - - // ── Conversions ───────────────────────────────────── - .widen, .narrow => |c| { - const val = frame.getRef(c.operand); - return .{ .value = val }; // comptime values don't truncate - }, - .bitcast => |c| { - const val = frame.getRef(c.operand); - // Loud-fail on `.type_tag → ` casts. A Type - // value can flow through bitcast only to .any (Any-boxing) - // or to itself; any other destination means the lowering - // emitted a coercion that silently pretends the TypeId is - // some other shape (e.g. an int, or a string). The most - // likely site that would trip this: the `case type:` arm - // of `any_to_string` in stdlib doing `xx val to string` — - // which expects the value field to already be a string, - // a leftover from the pre-`type_tag` era when Type values - // were string-shaped. - if (val == .type_tag) { - const allowed = c.to == .any or c.to == c.from; - if (!allowed) { - return bailDetail("comptime bitcast: Type value cast to a non-Type runtime kind — most likely a stale `xx val to string` from the pre-type_tag era; use `type_name(val)` instead"); - } - } - return .{ .value = val }; - }, - .int_to_float => |c| { - const val = frame.getRef(c.operand); - const i = val.asInt() orelse return error.TypeError; - return .{ .value = .{ .float = @floatFromInt(i) } }; - }, - .float_to_int => |c| { - const val = frame.getRef(c.operand); - const f = val.asFloat() orelse return error.TypeError; - return .{ .value = .{ .int = @intFromFloat(f) } }; - }, - - // ── Memory (stack simulation) ─────────────────────── - .alloca => { - const slot = frame.allocSlot(self.alloc); - return .{ .value = .{ .slot_ptr = slot } }; - }, - .load => |u| { - const ptr = frame.getRef(u.operand); - switch (ptr) { - .slot_ptr => |slot| { - const slot_val = frame.loadSlot(slot); - // Check if this is a field pointer (from struct_gep) - if (self.resolveFieldLoad(frame, slot_val)) |field_val| { - return .{ .value = field_val }; - } - return .{ .value = slot_val }; - }, - // The implicit __sx_ctx arrives as an aggregate after - // materializeCtxArg dereferences the caller's slot_ptr. - // `load(ref_0)` then naturally yields the Context value. - .aggregate => return .{ .value = ptr }, - // Comptime load through a raw host pointer needs the - // target IR type to know byte width — currently not - // threaded into the .load op. Add it when a comptime - // path hits this. - .int => return bailDetail("comptime load through raw host pointer not supported (IR type width not threaded)"), - .byte_ptr => return bailDetail("comptime load through raw byte pointer not supported"), - .heap_ptr => return bailDetail("comptime load through interp heap pointer not supported"), - else => return bailDetail("comptime load: unsupported pointer kind"), - } - }, - .store => |s| { - const ptr = frame.getRef(s.ptr); - const val = frame.getRef(s.val); - switch (ptr) { - .slot_ptr => |slot| { - const slot_val = frame.loadSlot(slot); - // Check if this is a field pointer (from struct_gep) - if (self.resolveFieldStore(frame, slot_val, val)) { - // Field store handled - } else { - frame.storeSlot(slot, val); - } - }, - .heap_ptr => |hp| { - // Store a byte into heap memory (from index_gep on string) - const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF); - self.heapStoreByte(hp, byte); - }, - // Raw host pointer (from extern call, e.g. libc_malloc). - // `val_ty` carries the declared destination width so we - // write exactly that many bytes — no neighbor clobber. - .int => |p| { - try storeAtRawPtr(self, p, val, s.val_ty); - }, - // Byte-granular pointer (from index_gep on a string). - // Always a 1-byte store — matches the heap_ptr arm. - .byte_ptr => |addr| { - const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF); - const dst: [*]u8 = @ptrFromInt(addr); - dst[0] = byte; - }, - else => return bailDetail("comptime store: unsupported pointer kind"), - } - return .{ .value = .void_val }; - }, - - // ── Struct ops ────────────────────────────────────── - .struct_init => |agg| { - const fields = self.alloc.alloc(Value, agg.fields.len) catch return error.CannotEvalComptime; - for (agg.fields, 0..) |ref, i| { - fields[i] = frame.getRef(ref); - } - return .{ .value = .{ .aggregate = fields } }; - }, - .struct_get => |fa| { - var base = frame.getRef(fa.base); - // Auto-deref slot_ptr → load the value - if (base == .slot_ptr) { - const loaded = frame.loadSlot(base.slot_ptr); - if (self.resolveFieldLoad(frame, loaded)) |resolved| { - base = resolved; - } else { - base = loaded; - } - } - switch (base) { - .aggregate => |fields| { - if (fa.field_index >= fields.len) return error.OutOfBounds; - const field_val = fields[fa.field_index]; - // `type_of(an_any)` lowers to `struct_get(any, 0, .type_value)`: - // the Any's field 0 is the held value's type id (a plain - // `.int`), but the SSA result type is `.type_value`, so yield a - // first-class `.type_tag` Value (a `.type_value`-typed value is - // always a `.type_tag` in the interp — mirrors `const_type`). - if (instruction.ty == .type_value) { - if (field_val.asInt()) |iv| return .{ .value = .{ .type_tag = TypeId.fromIndex(@intCast(iv)) } }; - } - return .{ .value = field_val }; - }, - .string => |s| { - // String as fat pointer: field 0 = ptr (string), field 1 = len - if (fa.field_index == 0) return .{ .value = .{ .string = s } }; - if (fa.field_index == 1) return .{ .value = .{ .int = @intCast(s.len) } }; - return error.OutOfBounds; - }, - .int => |v| { - // Scalar boxed as "struct" — field 0 is the value itself - if (fa.field_index == 0) return .{ .value = .{ .int = v } }; - return error.OutOfBounds; - }, - .type_tag => |tid| { - // A first-class Type value is the comptime form of the - // runtime Any-Type aggregate `{ tag=.type_value, value=tid }` - // (see `const_type` lowering in buildPackSliceValue). - // `type_of(any_holding_a_Type)` lowers to struct_get - // field 0, expecting that runtime layout — mirror it so - // field 0 reads the `.type_value` tag and field 1 the type id. - if (fa.field_index == 0) return .{ .value = .{ .int = @intCast(TypeId.type_value.index()) } }; - if (fa.field_index == 1) return .{ .value = .{ .type_tag = tid } }; - return error.OutOfBounds; - }, - else => return typeErrorDetail("comptime struct_get: base has no fields (not an aggregate/string/int)"), - } - }, - - // ── Enum ops ──────────────────────────────────────── - .enum_init => |ei| { - if (ei.payload.isNone()) { - return .{ .value = .{ .int = @intCast(ei.tag) } }; - } else { - const payload = frame.getRef(ei.payload); - const fields = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; - fields[0] = .{ .int = @intCast(ei.tag) }; - fields[1] = payload; - return .{ .value = .{ .aggregate = fields } }; - } - }, - .enum_tag => |u| { - const val = frame.getRef(u.operand); - switch (val) { - .int => return .{ .value = val }, - .aggregate => |fields| { - if (fields.len == 0) return typeErrorDetail("comptime enum_tag: aggregate operand has zero fields"); - return .{ .value = fields[0] }; - }, - else => return typeErrorDetail("comptime enum_tag: operand is neither an int (untagged enum) nor an aggregate (tagged union)"), - } - }, - .enum_payload => |fa| { - const base = frame.getRef(fa.base); - switch (base) { - .aggregate => |fields| { - if (fa.field_index + 1 >= fields.len) return error.OutOfBounds; - return .{ .value = fields[fa.field_index + 1] }; - }, - else => return typeErrorDetail("comptime enum_payload: base is not a tagged-union aggregate"), - } - }, - - // ── Optional ops ──────────────────────────────────── - .optional_wrap => |u| { - const val = frame.getRef(u.operand); - return .{ .value = val }; // wrapped value is just the value - }, - .optional_unwrap => |u| { - const val = frame.getRef(u.operand); - if (val.isNull()) return error.TypeError; // unwrapping null - return .{ .value = val }; - }, - .optional_has_value => |u| { - const val = frame.getRef(u.operand); - return .{ .value = .{ .boolean = !val.isNull() } }; - }, - .optional_coalesce => |b| { - const lhs = frame.getRef(b.lhs); - if (!lhs.isNull()) return .{ .value = lhs }; - return .{ .value = frame.getRef(b.rhs) }; - }, - - // ── Calls ─────────────────────────────────────────── - .call => |c| { - const args = self.alloc.alloc(Value, c.args.len) catch return error.CannotEvalComptime; - defer self.alloc.free(args); - for (c.args, 0..) |ref, i| { - // Inline any slot_ptr field-refs in the caller's frame before - // the value crosses the call boundary. slot_ptr indices are - // frame-local; if a slice/aggregate carrying one is passed to - // the callee, the callee would later resolve the index against - // its own slot table and read garbage. - args[i] = self.materializeForCall(frame, frame.getRef(ref)); - } - // The implicit __sx_ctx is logically a `*Context` but the - // interp can't dereference cross-frame slot_ptrs. Materialise - // args[0] to the loaded Context aggregate so the callee can - // treat its slot 0 as the value directly. - const callee_func = self.module.getFunction(c.callee); - if (callee_func.has_implicit_ctx and args.len >= 1) { - args[0] = self.materializeCtxArg(frame, args[0]); - } - const result = try self.call(c.callee, args); - return .{ .value = result }; - }, - - // The Obj-C runtime isn't available at comptime; any - // `#objc_call` reached during `#run` execution can't - // resolve. Fail fast so callers see a useful diagnostic. - .objc_msg_send => return bailDetail("#objc_call not available at comptime (no Obj-C runtime)"), - // Same story for JNI — no JVM at compile time. - .jni_msg_send => return bailDetail("#jni_call not available at comptime (no JVM)"), - // Inline asm executes target machine code — never comptime-evaluable. - .inline_asm => return bailDetail("inline assembly requires native execution; not available at comptime"), - - // ── Block params ──────────────────────────────────── - .block_param => { - // Block params are pushed at the start of block execution. - // This instruction is a no-op; the value was already pushed - // during block arg binding. - return .{ .value = .void_val }; - }, - - // ── Terminators ───────────────────────────────────── - .br => |b| { - const args = self.alloc.alloc(Value, b.args.len) catch return error.CannotEvalComptime; - for (b.args, 0..) |ref, i| { - args[i] = frame.getRef(ref); - } - current_block.* = b.target; - block_args.* = args; - return .branch; - }, - .cond_br => |cb| { - const cond = frame.getRef(cb.cond).asBool() orelse return error.TypeError; - if (cond) { - const args = self.alloc.alloc(Value, cb.then_args.len) catch return error.CannotEvalComptime; - for (cb.then_args, 0..) |ref, i| { - args[i] = frame.getRef(ref); - } - current_block.* = cb.then_target; - block_args.* = args; - } else { - const args = self.alloc.alloc(Value, cb.else_args.len) catch return error.CannotEvalComptime; - for (cb.else_args, 0..) |ref, i| { - args[i] = frame.getRef(ref); - } - current_block.* = cb.else_target; - block_args.* = args; - } - return .branch; - }, - .switch_br => |sb| { - // A type-category match (`type_of(x) == { case int: … }`) - // switches on a Type value — a `.type_tag` Value whose discriminant - // is its TypeId index; an enum/error switch uses a plain int. - const sb_val = frame.getRef(sb.operand); - const operand = sb_val.asInt() orelse - (if (sb_val.asTypeId()) |t| @as(i64, @intCast(t.index())) else return error.TypeError); - for (sb.cases) |case| { - if (operand == case.value) { - const args = self.alloc.alloc(Value, case.args.len) catch return error.CannotEvalComptime; - for (case.args, 0..) |ref, i| { - args[i] = frame.getRef(ref); - } - current_block.* = case.target; - block_args.* = args; - return .branch; - } - } - // Default - const args = self.alloc.alloc(Value, sb.default_args.len) catch return error.CannotEvalComptime; - for (sb.default_args, 0..) |ref, i| { - args[i] = frame.getRef(ref); - } - current_block.* = sb.default; - block_args.* = args; - return .branch; - }, - .ret => |u| { - return .{ .ret_val = frame.getRef(u.operand) }; - }, - .ret_void => return .ret_nothing, - .@"unreachable" => return error.Unreachable, - - // ── Builtin calls ────────────────────────────────── - .call_builtin => |bi| { - return self.execBuiltin(bi, frame, instruction.ty); - }, - - // ── Struct GEP (field pointer) ───────────────────── - .struct_gep => |fa| { - const base = frame.getRef(fa.base); - switch (base) { - .slot_ptr => |slot| { - // Create a field-pointer: we encode as a slot_ptr with field info - // When loading, we extract the field; when storing, we modify the field - const field_slot = frame.allocSlot(self.alloc); - // Store a field reference: { parent_slot, field_index } - const field_ref = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; - field_ref[0] = .{ .int = @intCast(slot) }; - field_ref[1] = .{ .int = @intCast(fa.field_index) }; - frame.storeSlot(field_slot, .{ .aggregate = field_ref }); - return .{ .value = .{ .slot_ptr = field_slot } }; - }, - // struct_gep through a raw host pointer requires the - // struct's field-offset table — feasible via - // `fa.base_type` but not currently wired. Add when a - // comptime path hits this. - .int => return bailDetail("comptime struct_gep through raw host pointer not supported"), - .byte_ptr => return bailDetail("comptime struct_gep through raw byte pointer not supported"), - .heap_ptr => return bailDetail("comptime struct_gep through interp heap pointer not supported"), - else => return bailDetail("comptime struct_gep: unsupported pointer kind"), - } - }, - - // ── String/slice operations ──────────────────────── - .index_get => |idx| { - const base = frame.getRef(idx.lhs); - const index_val = frame.getRef(idx.rhs); - const i: usize = @intCast(index_val.asInt() orelse return error.TypeError); - // Try as string value - if (base.asString(self)) |s| { - if (i >= s.len) return error.OutOfBounds; - return .{ .value = .{ .int = s[i] } }; - } - // Try as aggregate array or slice - switch (base) { - .aggregate => |fields| { - // Check for slice-like: {data_ptr, len} where data_ptr is slot_ptr - if (fields.len == 2 and fields[1] == .int) { - const data = fields[0]; - if (data == .slot_ptr) { - // The data field is a ptr — resolve through slots to get the array - const arr = self.resolveSlotChain(frame, data); - switch (arr) { - .aggregate => |arr_fields| { - if (i < arr_fields.len) return .{ .value = arr_fields[i] }; - return error.OutOfBounds; - }, - else => {}, - } - } else if (data == .aggregate) { - // Inline array data - const arr_fields = data.aggregate; - if (i < arr_fields.len) return .{ .value = arr_fields[i] }; - return error.OutOfBounds; - } - } - // Plain aggregate indexing - if (i >= fields.len) return error.OutOfBounds; - return .{ .value = fields[i] }; - }, - // Raw host pointer base — `buf[i]` reads one byte at - // offset i. Matches the byte-addressed `index_gep` - // semantics for the same shape. Used by comptime sx - // code that walks libc-malloc'd buffers. - .int => |p| { - const src: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(p))); - return .{ .value = .{ .int = src[i] } }; - }, - .byte_ptr => |addr| { - const src: [*]const u8 = @ptrFromInt(addr); - return .{ .value = .{ .int = src[i] } }; - }, - else => return bailDetail("comptime index_get: unsupported base kind"), - } - }, - .length => |u| { - const val = frame.getRef(u.operand); - if (val.asString(self)) |s| { - return .{ .value = .{ .int = @intCast(s.len) } }; - } - switch (val) { - .aggregate => |fields| { - // For fat pointers {ptr, len}, len is field[1] - if (fields.len == 2) { - return .{ .value = fields[1] }; - } - return .{ .value = .{ .int = @intCast(fields.len) } }; - }, - else => return bailDetail("comptime .len: operand is neither a string nor an aggregate"), - } - }, - .data_ptr => |u| { - const val = frame.getRef(u.operand); - switch (val) { - .aggregate => |fields| { - if (fields.len >= 1) return .{ .value = fields[0] }; - return error.OutOfBounds; - }, - .string => return .{ .value = val }, - else => return bailDetail("comptime .ptr: operand has no data field (not a string or slice aggregate)"), - } - }, - .subslice => |sub| { - const base = frame.getRef(sub.base); - const lo: usize = @intCast(frame.getRef(sub.lo).asInt() orelse return bailDetail("comptime subslice: lo index is not an integer")); - const hi: usize = @intCast(frame.getRef(sub.hi).asInt() orelse return bailDetail("comptime subslice: hi index is not an integer")); - if (hi < lo) return error.OutOfBounds; - if (base.asString(self)) |s| { - if (hi > s.len) return error.OutOfBounds; - return .{ .value = .{ .string = s[lo..hi] } }; - } - // Non-string aggregate (array or `{data,len}` slice). The - // underlying element list comes from the aggregate directly (an - // array) or its data field (a slice) — `sub.base_ty` picks which, - // since a 2-element array and a `{ptr,len}` pair are - // indistinguishable by Value shape alone. - const elems = self.subsliceElements(frame, base, sub.base_ty) orelse - return bailDetail("comptime subslice: base is not a sliceable array/slice value"); - if (hi > elems.len) return error.OutOfBounds; - const sub_elems = elems[lo..hi]; - // Return a proper slice VALUE `{data, len}`: data is the element - // aggregate, len the (int) count. The int len is what lets - // downstream `.length` / `index_get` / `decodeVariantElements` - // read this as a slice and not a bare array. - const pair = self.alloc.dupe(Value, &.{ .{ .aggregate = sub_elems }, .{ .int = @intCast(sub_elems.len) } }) catch return error.CannotEvalComptime; - return .{ .value = .{ .aggregate = pair } }; - }, - - // ── Addr/deref ───────────────────────────────────── - .addr_of => |u| { - const val = frame.getRef(u.operand); - return .{ .value = val }; // pass through pointer-like values - }, - .deref => |u| { - const val = frame.getRef(u.operand); - switch (val) { - .slot_ptr => |slot| return .{ .value = frame.loadSlot(slot) }, - // Real raw-memory deref needs val's IR type for byte - // width — not yet threaded. Erroring is safer than - // returning the pointer-as-int unchanged, which - // silently looks like a successful deref. - .int => return bailDetail("comptime deref through raw host pointer not supported (IR type width not threaded)"), - .byte_ptr => return bailDetail("comptime deref through raw byte pointer not supported"), - .heap_ptr => return bailDetail("comptime deref through interp heap pointer not supported"), - // Pre-dereferenced values that flow through deref as a - // no-op: an aggregate/string already IS the loaded - // value (lowering sometimes emits `deref(struct_val)` - // where the struct was previously materialized in - // place rather than via a slot). - .aggregate, .string => return .{ .value = val }, - // Null deref is UB at runtime; surface it at comptime - // instead of silently producing a null again. - .null_val => return bailDetail("comptime deref of null"), - // Scalars / handles / undef aren't pointer-shaped — - // dereffing them is a frontend bug. Bail rather than - // returning the bare value (which looked like a - // successful deref to callers). - .boolean, .float, .func_ref, .closure, .type_tag, .void_val, .undef => return bailDetail("comptime deref: operand is not a pointer"), - } - }, - - // ── Bitwise operations ───────────────────────────── - .bit_and => |b| { - const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError; - const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError; - return .{ .value = .{ .int = lhs & rhs } }; - }, - .bit_or => |b| { - const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError; - const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError; - return .{ .value = .{ .int = lhs | rhs } }; - }, - .bit_xor => |b| { - const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError; - const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError; - return .{ .value = .{ .int = lhs ^ rhs } }; - }, - .bit_not => |u| { - const val = frame.getRef(u.operand).asInt() orelse return error.TypeError; - return .{ .value = .{ .int = ~val } }; - }, - .shl => |b| { - const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError; - const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError; - const shift: u6 = @intCast(@min(rhs, 63)); - return .{ .value = .{ .int = lhs << shift } }; - }, - .shr => |b| { - const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError; - const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError; - const shift: u6 = @intCast(@min(rhs, 63)); - return .{ .value = .{ .int = lhs >> shift } }; - }, - - // ── Tuple ops (same as struct) ───────────────────── - .tuple_init => |agg| { - const fields = self.alloc.alloc(Value, agg.fields.len) catch return error.CannotEvalComptime; - for (agg.fields, 0..) |ref, i| { - fields[i] = frame.getRef(ref); - } - return .{ .value = .{ .aggregate = fields } }; - }, - .tuple_get => |fa| { - const base = frame.getRef(fa.base); - switch (base) { - .aggregate => |fields| { - if (fa.field_index >= fields.len) return error.OutOfBounds; - return .{ .value = fields[fa.field_index] }; - }, - else => return error.TypeError, - } - }, - - // ── Box/unbox (Any type) ─────────────────────────── - .box_any => |ba| { - const val = frame.getRef(ba.operand); - // Box as aggregate: { type_tag, value } — matches LLVM layout - const fields = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; - fields[0] = .{ .int = @intFromEnum(ba.source_type) }; - fields[1] = val; - return .{ .value = .{ .aggregate = fields } }; - }, - .unbox_any => |ua| { - const val = frame.getRef(ua.operand); - switch (val) { - .aggregate => |fields| { - // Value is at field 1 in { tag, value } layout - if (fields.len >= 2) return .{ .value = fields[1] }; - if (fields.len >= 1) return .{ .value = fields[0] }; - return error.OutOfBounds; - }, - // Any-typed comptime values flow through box_any first, - // which always wraps as an aggregate. If we reach here - // with a scalar / undef / null, the IR shape upstream - // diverged from the box_any contract — bail loudly so - // the offending box_any site shows in the diagnostic. - .int, .float, .boolean, .string, .null_val, .undef => return bailDetail("comptime unbox_any: operand is a bare scalar (expected { tag, value } aggregate from box_any)"), - .void_val => return bailDetail("comptime unbox_any: operand is void_val"), - .slot_ptr, .heap_ptr, .byte_ptr, .func_ref, .closure, .type_tag => return bailDetail("comptime unbox_any: operand is a pointer/handle (expected { tag, value } aggregate)"), - } - }, - - // ── Reflection ───────────────────────────────────── - .field_name_get => |fr| { - const idx_val = frame.getRef(fr.index); - const idx: usize = @intCast(switch (idx_val) { - .int => |i| i, - else => return bailDetail("comptime field_name(T, i): index operand is not an int"), - }); - const info = self.module.types.get(fr.struct_type); - const fields = switch (info) { - .@"struct" => |s| s.fields, - .@"union" => |u| u.fields, - .tagged_union => |u| u.fields, - else => return bailDetail("comptime field_name(T, i): T is not a struct/union/tagged_union"), - }; - if (idx >= fields.len) return error.OutOfBounds; - const name = self.module.types.getString(fields[idx].name); - return .{ .value = .{ .string = name } }; - }, - .error_tag_name_get => |u| { - const tag_val = frame.getRef(u.operand); - const id: u32 = @intCast(switch (tag_val) { - .int => |i| i, - else => return bailDetail("comptime error_tag_name(e): operand is not an integer tag id"), - }); - return .{ .value = .{ .string = self.module.types.tags.getName(id) } }; - }, - .field_value_get => |fr| { - const base_val = frame.getRef(fr.base); - const idx_val = frame.getRef(fr.index); - const idx: usize = @intCast(switch (idx_val) { - .int => |i| i, - else => return bailDetail("comptime field_value(s, i): index operand is not an int"), - }); - switch (base_val) { - .aggregate => |agg| { - if (idx >= agg.len) return error.OutOfBounds; - // Box as Any: { value, type_tag } - const info = self.module.types.get(fr.struct_type); - const fields = switch (info) { - .@"struct" => |s| s.fields, - .@"union" => |u| u.fields, - .tagged_union => |u| u.fields, - else => return bailDetail("comptime field_value(s, i): s's type is not a struct/union/tagged_union"), - }; - const field_ty_tag: i64 = if (idx < fields.len) @intFromEnum(fields[idx].ty) else 0; - const boxed = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; - boxed[0] = agg[idx]; - boxed[1] = .{ .int = field_ty_tag }; - return .{ .value = .{ .aggregate = boxed } }; - }, - else => return bailDetail("comptime field_value(s, i): s is not an aggregate Value (struct values must be materialized as aggregates at comptime)"), - } - }, - - // ── Global access ────────────────────────────────── - .global_get => |gid| { - const val = try self.getGlobal(gid); - return .{ .value = val }; - }, - .global_addr => |gid| { - // The implicit-context default global is the only global - // whose address sees runtime use. Return the Context - // aggregate directly so `load(args[0])` yields it via the - // aggregate-passthrough branch of the `.load` handler. - if (gid.index() < self.module.globals.items.len) { - const global = &self.module.globals.items[gid.index()]; - const name = self.module.types.getString(global.name); - if (std.mem.eql(u8, name, "__sx_default_context")) { - return .{ .value = self.defaultContextValue() }; - } - } - return bailDetail("comptime global_addr: only `&__sx_default_context` is currently materialised at comptime"); - }, - .func_ref => |fid| { - return .{ .value = .{ .func_ref = fid } }; - }, - .global_set => |gs| { - const val = frame.getRef(gs.value); - self.global_values.put(gs.global.index(), val) catch {}; - return .{ .value = .void_val }; - }, - - // ── Index GEP (array element pointer) ───────────── - .index_gep => |b| { - const base = frame.getRef(b.lhs); - const idx = frame.getRef(b.rhs); - switch (base) { - .slot_ptr => |slot| { - // Create an indexed element pointer: { parent_slot, index, is_index_gep=1 } - const field_slot = frame.allocSlot(self.alloc); - const ref = self.alloc.alloc(Value, 3) catch return error.CannotEvalComptime; - ref[0] = .{ .int = @intCast(slot) }; - ref[1] = idx; - ref[2] = .{ .int = 1 }; // marker: this is index_gep, not struct_gep - frame.storeSlot(field_slot, .{ .aggregate = ref }); - return .{ .value = .{ .slot_ptr = field_slot } }; - }, - .aggregate => |fields| { - // String/slice aggregate {data_ptr, len} — compute data_ptr + index - if (fields.len >= 2) { - const data_ptr = fields[0]; - const offset = idx.asInt() orelse return error.TypeError; - switch (data_ptr) { - .heap_ptr => |hp| { - return .{ .value = .{ .heap_ptr = .{ - .id = hp.id, - .offset = hp.offset + @as(u32, @intCast(offset)), - } } }; - }, - // Raw host pointer (from extern call return, - // e.g. libc_malloc). Byte-addressed offset - // matches the heap_ptr branch above — both - // are u8-granular for sx's string/slice ops. - // Producing `.byte_ptr` makes store-through - // this address write a single byte. - .int => |p| { - return .{ .value = .{ .byte_ptr = @intCast(p + offset) } }; - }, - else => {}, - } - } - return bailDetail("comptime index_gep: unsupported aggregate-base shape (expected {data_ptr, len} with heap_ptr or int data field)"); - }, - .string => |s| { - // String literal — copy to heap and return heap_ptr at offset - const offset: usize = @intCast(@as(u64, @bitCast(idx.asInt() orelse return error.TypeError))); - const hp = self.heapAlloc(s.len); - self.heapMemcpy(hp, s, s.len); - return .{ .value = .{ .heap_ptr = .{ - .id = hp.id, - .offset = @intCast(offset), - } } }; - }, - // Raw host pointer base — byte-addressed offset. - // Element size > 1 would silently mis-index; document - // the assumption. Callers stride past byte granularity - // must wrap the pointer in an aggregate so the - // {data_ptr, len} branch fires (which is also - // byte-addressed today — fix here when needed). - .int => |p| { - const offset = idx.asInt() orelse return error.TypeError; - return .{ .value = .{ .int = p + offset } }; - }, - else => return bailDetail("comptime index_gep: unsupported base kind"), - } - }, - - // ── Array to slice ──────────────────────────────── - .array_to_slice => |u| { - const val = frame.getRef(u.operand); - switch (val) { - .aggregate => |fields| { - // Convert array aggregate to slice: { aggregate_ref, len } - const slice = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; - slice[0] = val; // the array data - slice[1] = .{ .int = @intCast(fields.len) }; - return .{ .value = .{ .aggregate = slice } }; - }, - .slot_ptr => |slot| { - const arr = frame.loadSlot(slot); - switch (arr) { - .aggregate => |fields| { - const slice = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime; - slice[0] = arr; - slice[1] = .{ .int = @intCast(fields.len) }; - return .{ .value = .{ .aggregate = slice } }; - }, - else => return bailDetail("comptime array_to_slice: slot-backed value is not an aggregate"), - } - }, - else => return bailDetail("comptime array_to_slice: operand is neither an aggregate nor a slot pointer"), - } - }, - - // ── Call indirect (function pointer) ────────────── - .call_indirect => |ci| { - const callee = frame.getRef(ci.callee); - switch (callee) { - .func_ref => |fid| { - const args = self.alloc.alloc(Value, ci.args.len) catch return error.CannotEvalComptime; - defer self.alloc.free(args); - for (ci.args, 0..) |ref, i| { - args[i] = self.materializeForCall(frame, frame.getRef(ref)); - } - const target = self.module.getFunction(fid); - if (target.has_implicit_ctx and args.len >= 1) { - args[0] = self.materializeCtxArg(frame, args[0]); - } - const result = try self.call(fid, args); - return .{ .value = result }; - }, - else => return bailDetail("comptime call_indirect: callee is not a func_ref Value (raw fn-pointers from extern calls aren't dispatchable in interp)"), - } - }, - - // Type-as-value sentinel emitted for the type arg of - // `cast(T) val`. Result is never read (the cast lowering - // consumes the type from the AST, not the IR Ref), so an - // undef value is sufficient — matches the LLVM emitter. - .placeholder => return .{ .value = .undef }, - - // ── Not yet evaluable at comptime ────────────────── - .call_closure => return bailDetail("comptime call_closure not yet implemented (closure trampoline ABI threading required)"), - .closure_create => return bailDetail("comptime closure_create not yet implemented"), - .union_get => return bailDetail("comptime union_get not yet implemented"), - .union_gep => return bailDetail("comptime union_gep not yet implemented"), - .vec_splat => return bailDetail("comptime vec_splat not yet implemented"), - .vec_extract => return bailDetail("comptime vec_extract not yet implemented"), - .vec_insert => return bailDetail("comptime vec_insert not yet implemented"), - } - } - - // ── Arithmetic helpers ────────────────────────────────────────── - - const ArithOp = enum { add, sub, mul, div, mod }; - - fn evalArith(self: *Interpreter, frame: *Frame, b: inst_mod.BinOp, comptime aop: ArithOp) InterpError!Value { - _ = self; - const lhs = frame.getRef(b.lhs); - const rhs = frame.getRef(b.rhs); - - // Both int - if (lhs.asInt()) |li| { - if (rhs.asInt()) |ri| { - return .{ .int = switch (aop) { - .add => li +% ri, - .sub => li -% ri, - .mul => li *% ri, - .div => if (ri == 0) return error.DivisionByZero else @divTrunc(li, ri), - .mod => if (ri == 0) return error.DivisionByZero else @mod(li, ri), - } }; - } - } - - // Both float (or int promoted to float) - if (lhs.asFloat()) |lf| { - if (rhs.asFloat()) |rf| { - return .{ .float = switch (aop) { - .add => lf + rf, - .sub => lf - rf, - .mul => lf * rf, - .div => if (rf == 0.0) return error.DivisionByZero else lf / rf, - .mod => @mod(lf, rf), - } }; - } - } - - return typeErrorDetail("comptime arithmetic: operand pair is neither both-int nor both-float-coercible"); - } - - // ── Comparison helpers ────────────────────────────────────────── - - const CmpOp = enum { eq, ne, lt, le, gt, ge }; - - fn evalCmp(self: *Interpreter, frame: *Frame, b: inst_mod.BinOp, comptime cop: CmpOp) InterpError!bool { - _ = self; - const lhs = frame.getRef(b.lhs); - const rhs = frame.getRef(b.rhs); - - // Both int - if (lhs.asInt()) |li| { - if (rhs.asInt()) |ri| { - return switch (cop) { - .eq => li == ri, - .ne => li != ri, - .lt => li < ri, - .le => li <= ri, - .gt => li > ri, - .ge => li >= ri, - }; - } - } - - // Both float - if (lhs.asFloat()) |lf| { - if (rhs.asFloat()) |rf| { - return switch (cop) { - .eq => lf == rf, - .ne => lf != rf, - .lt => lf < rf, - .le => lf <= rf, - .gt => lf > rf, - .ge => lf >= rf, - }; - } - } - - // Bool equality - if (lhs.asBool()) |lb| { - if (rhs.asBool()) |rb| { - return switch (cop) { - .eq => lb == rb, - .ne => lb != rb, - else => return error.TypeError, - }; - } - } - - // Type-as-value equality. Compares TypeIds structurally. - // `.type_tag` vs `.int(N)` deliberately does NOT compare — - // a Type is not an int even if the underlying enum value - // matches; falls through to the typeErrorDetail below. - if (lhs.asTypeId()) |la| { - if (rhs.asTypeId()) |ra| { - return switch (cop) { - .eq => la == ra, - .ne => la != ra, - else => return error.TypeError, - }; - } - } - - return typeErrorDetail("comptime comparison: operand pair has no shared comparable shape (int/float/bool/string/type)"); - } - - // ── Slot chain resolution ──────────────────────────────────── - - /// Walk an aggregate Value and rewrite any embedded `slot_ptr` that points - /// to a field-ref slot in `frame` (the marker shape `{parent_slot, idx, ..}` - /// emitted by `struct_gep` / `index_gep`) into the resolved parent value. - /// Slot indices are frame-local; a slice passed across a call would otherwise - /// read its data_ptr out of the callee's slot table. - /// Resolve the implicit __sx_ctx arg to its loaded Context value so - /// callees can treat their own slot 0 as the aggregate directly - /// (no cross-frame slot_ptr indirection). - fn materializeCtxArg(self: *Interpreter, frame: *Frame, val: Value) Value { - _ = self; - return switch (val) { - .slot_ptr => |slot| frame.loadSlot(slot), - else => val, - }; - } - - fn materializeForCall(self: *Interpreter, frame: *Frame, val: Value) Value { - switch (val) { - .aggregate => |fields| { - const new_fields = self.alloc.alloc(Value, fields.len) catch return val; - for (fields, 0..) |f, i| { - new_fields[i] = self.materializeForCall(frame, f); - } - return .{ .aggregate = new_fields }; - }, - .slot_ptr => |slot| { - const stored = frame.loadSlot(slot); - if (stored == .aggregate) { - const ref_fields = stored.aggregate; - if (ref_fields.len >= 2) { - const parent_slot_val = ref_fields[0].asInt() orelse return val; - if (ref_fields[1].asInt() == null) return val; - const parent_slot: u32 = @intCast(parent_slot_val); - const parent = frame.loadSlot(parent_slot); - return self.materializeForCall(frame, parent); - } - } - return val; - }, - else => return val, - } - } - - /// Follow a slot_ptr through field-pointer / index-gep chains - /// to get the underlying value. Handles nested dereferences. - fn resolveSlotChain(self: *Interpreter, frame: *Frame, val: Value) Value { - _ = self; - var current = val; - var depth: u32 = 0; - while (depth < 16) : (depth += 1) { - switch (current) { - .slot_ptr => |slot| { - const stored = frame.loadSlot(slot); - switch (stored) { - .aggregate => |ref_fields| { - if (ref_fields.len >= 2) { - // Field-pointer or index-gep reference: {parent_slot, index, [marker]} - const parent_slot_val = ref_fields[0].asInt() orelse return stored; - const parent_slot: u32 = @intCast(parent_slot_val); - const parent = frame.loadSlot(parent_slot); - return parent; // Return the parent array/struct - } - return stored; - }, - .slot_ptr => { - current = stored; - continue; - }, - else => return stored, - } - }, - else => return current, - } - } - return current; - } - - /// The element list backing a comptime array/slice VALUE, for `subslice`. - /// `base_ty` (threaded onto the op at lower time) disambiguates the two - /// shapes that look identical as Values: an ARRAY's aggregate holds its - /// elements directly, while a SLICE is a `{data, len}` fat pointer whose - /// `data` field holds them. Returns null for any other shape (caller bails). - fn subsliceElements(self: *Interpreter, frame: *Frame, base: Value, base_ty: TypeId) ?[]const Value { - var b = base; - if (b == .slot_ptr) b = self.resolveSlotChain(frame, b); - const fields = switch (b) { - .aggregate => |f| f, - else => return null, - }; - const is_slice = !base_ty.isBuiltin() and self.module.types.get(base_ty) == .slice; - if (is_slice) { - if (fields.len != 2) return null; - const len: usize = @intCast(fields[1].asInt() orelse return null); - var data = fields[0]; - if (data == .slot_ptr) data = self.resolveSlotChain(frame, data); - return switch (data) { - .aggregate => |arr| if (len <= arr.len) arr[0..len] else null, - else => null, - }; - } - // Array (or unknown base_ty fallback): the fields ARE the elements. - return fields; - } - - // ── Constant → Value conversion ───────────────────────────── - - fn constToValue(self: *Interpreter, cv: inst_mod.ConstantValue) Value { - return switch (cv) { - .int => |v| .{ .int = v }, - .float => |v| .{ .float = v }, - .boolean => |v| .{ .boolean = v }, - .string => |sid| .{ .string = self.module.types.getString(sid) }, - .null_val => .null_val, - .undef, .zeroinit => .undef, - .aggregate => |items| { - const fields = self.alloc.alloc(Value, items.len) catch return .undef; - for (items, 0..) |item, i| { - fields[i] = self.constToValue(item); - } - return .{ .aggregate = fields }; - }, - .vtable => |func_ids| { - // Vtable is a struct of function refs — represent as aggregate of func_ref values - const fields = self.alloc.alloc(Value, func_ids.len) catch return .undef; - for (func_ids, 0..) |fid, i| { - fields[i] = .{ .func_ref = fid }; - } - return .{ .aggregate = fields }; - }, - .func_ref => |fid| .{ .func_ref = fid }, - }; - } - - // ── Field pointer helpers (for struct_gep load/store) ───────── - - /// Check if a slot value is a field pointer { parent_slot, field_index [, is_index_gep] }. - /// If so, load the parent aggregate and return the field value. - fn resolveFieldLoad(self: *Interpreter, frame: *Frame, slot_val: Value) ?Value { - _ = self; - switch (slot_val) { - .aggregate => |fields| { - if (fields.len >= 2) { - const parent_slot_val = fields[0].asInt() orelse return null; - const field_idx_val = fields[1].asInt() orelse return null; - // A real field-pointer's parent_slot is a small frame - // index; a struct aggregate whose first field happens - // to be a wide integer (e.g. a stored pointer-as-int - // or a u64) would otherwise mis-trigger this branch. - if (parent_slot_val < 0 or parent_slot_val > std.math.maxInt(u32)) return null; - if (field_idx_val < 0 or field_idx_val > std.math.maxInt(u32)) return null; - const parent_slot: u32 = @intCast(parent_slot_val); - const field_idx: usize = @intCast(field_idx_val); - const parent = frame.loadSlot(parent_slot); - switch (parent) { - .aggregate => |parent_fields| { - if (field_idx < parent_fields.len) return parent_fields[field_idx]; - }, - .string => |s| { - // String fat pointer: field 0 = ptr (as string), field 1 = len - if (field_idx == 0) return .{ .string = s }; - if (field_idx == 1) return .{ .int = @intCast(s.len) }; - }, - else => {}, - } - } - }, - else => {}, - } - return null; - } - - /// Check if a slot value is a field pointer. If so, modify the field - /// in the parent aggregate. Returns true if handled. - fn resolveFieldStore(self: *Interpreter, frame: *Frame, slot_val: Value, new_val: Value) bool { - switch (slot_val) { - .aggregate => |fields| { - if (fields.len >= 2) { - const parent_slot_val = fields[0].asInt() orelse return false; - const field_idx_val = fields[1].asInt() orelse return false; - // Same field-pointer-vs-real-struct disambiguation as - // resolveFieldLoad — a wide integer in fields[0] is a - // stored pointer, not a frame index. - if (parent_slot_val < 0 or parent_slot_val > std.math.maxInt(u32)) return false; - if (field_idx_val < 0 or field_idx_val > std.math.maxInt(u32)) return false; - const parent_slot: u32 = @intCast(parent_slot_val); - const field_idx: usize = @intCast(field_idx_val); - const parent = frame.loadSlot(parent_slot); - switch (parent) { - .aggregate => |parent_fields| { - const new_len = @max(field_idx + 1, parent_fields.len); - const new_fields = self.alloc.alloc(Value, new_len) catch return false; - @memcpy(new_fields[0..parent_fields.len], parent_fields); - for (new_fields[parent_fields.len..]) |*f| f.* = .undef; - new_fields[field_idx] = new_val; - frame.storeSlot(parent_slot, .{ .aggregate = new_fields }); - return true; - }, - .undef => { - // Initialize a new aggregate from undef - const num_fields: usize = @max(field_idx + 1, 2); // at least 2 for strings - const new_fields = self.alloc.alloc(Value, num_fields) catch return false; - for (new_fields) |*f| f.* = .undef; - new_fields[field_idx] = new_val; - frame.storeSlot(parent_slot, .{ .aggregate = new_fields }); - return true; - }, - else => {}, - } - } - }, - else => {}, - } - return false; - } - - // ── Builtin call dispatch ────────────────────────────────────── - - fn execBuiltin(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame, _: TypeId) InterpError!ExecResult { - const result = self.execBuiltinInner(bi, frame) catch |err| { - if (last_bail_builtin == null) last_bail_builtin = @tagName(bi.builtin); - return err; - }; - return result; - } - - fn execBuiltinInner(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame) InterpError!ExecResult { - switch (bi.builtin) { - .size_of => { - // Return a default size (8 bytes for most types) - return .{ .value = .{ .int = 8 } }; - }, - .align_of => { - return .{ .value = .{ .int = 8 } }; - }, - .sqrt => { - const val = frame.getRef(bi.args[0]); - const f = val.asFloat() orelse return error.TypeError; - return .{ .value = .{ .float = @sqrt(f) } }; - }, - .sin => { - const val = frame.getRef(bi.args[0]); - const f = val.asFloat() orelse return error.TypeError; - return .{ .value = .{ .float = @sin(f) } }; - }, - .cos => { - const val = frame.getRef(bi.args[0]); - const f = val.asFloat() orelse return error.TypeError; - return .{ .value = .{ .float = @cos(f) } }; - }, - .floor => { - const val = frame.getRef(bi.args[0]); - const f = val.asFloat() orelse return error.TypeError; - return .{ .value = .{ .float = @floor(f) } }; - }, - .cast => return bailDetail("comptime #builtin cast: handled at lowering, not the interp (you reached this if a #builtin cast leaked into IR)"), - .type_of => return bailDetail("comptime #builtin type_of: handled at lowering, not the interp"), - .alloc => return bailDetail("comptime #builtin alloc unused (use context.allocator.alloc)"), - .dealloc => return bailDetail("comptime #builtin dealloc unused (use context.allocator.dealloc)"), - - // ── Comptime reflection (Type-as-Value path) ───────── - // These are only reached when lower.zig emitted a real - // builtin_call — i.e. the type argument was NOT statically - // resolvable (e.g. inside a builder body where `args[i]` is - // a `.type_tag(TypeId)` Value bound at interp time). Static - // calls fold to `const_string` / `const_bool` at lower time - // and never hit this dispatch. - .type_name => { - if (bi.args.len < 1) return bailDetail("comptime type_name: missing argument"); - const arg = frame.getRef(bi.args[0]); - // A bare `.type_tag` Value (the comptime-native form), an - // Any-boxed Type (`{ .any, tid }`), or an Any holding a - // runtime value (`{ tag, value }`, where the tag IS the - // value's type). `reflectTypeId` reads the runtime tag so - // `type_name(av)` for `av : Any = 6` names `i64`, not the - // type whose index equals the payload. - const tid = arg.reflectTypeId() orelse - return bailDetail("comptime type_name: argument is not a Type value or boxed value (expected `.type_tag` or Any aggregate)"); - const name = self.module.types.typeName(tid); - // Copy the slice into the interp's allocator so it - // outlives any TypeTable churn during the rest of the - // interp execution. The TypeTable's strings are stable - // for now but copying is the safe pattern. - const owned = self.alloc.dupe(u8, name) catch return error.CannotEvalComptime; - return .{ .value = .{ .string = owned } }; - }, - .type_eq => { - if (bi.args.len < 2) return bailDetail("comptime type_eq: needs two Type arguments"); - const a = frame.getRef(bi.args[0]).asTypeId() orelse return bailDetail("comptime type_eq: first argument is not a Type value"); - const b = frame.getRef(bi.args[1]).asTypeId() orelse return bailDetail("comptime type_eq: second argument is not a Type value"); - return .{ .value = .{ .boolean = a == b } }; - }, - .type_is_unsigned => { - if (bi.args.len < 1) return bailDetail("comptime type_is_unsigned: missing argument"); - const arg = frame.getRef(bi.args[0]); - // A bare `.type_tag`, an Any-boxed Type (`{ .any, tid }`, - // the `type_of(x)` shape), or an Any holding a runtime value - // (`{ tag, value }`, where the tag IS the value's type). - // `reflectTypeId` reads the runtime tag so - // `type_is_unsigned(av)` for `av : Any = 6` answers about - // `i64`, not the type whose index equals the payload. - const tid = arg.reflectTypeId() orelse - return bailDetail("comptime type_is_unsigned: argument is not a Type value or boxed value (expected `.type_tag` or Any aggregate)"); - return .{ .value = .{ .boolean = self.module.types.isUnsignedInt(tid) } }; - }, - .has_impl => { - // has_impl at interp time needs access to the host's - // protocol-registration maps (protocol_thunk_map + - // param_impl_map). These live on `Lowering`, not on - // the Interpreter. Plumbing a queryable snapshot is - // its own slice — until then, bail loudly so the user - // gets a clear "not yet wired" message instead of a - // silent false. Static-arg has_impl still works via - // `tryConstBoolCondition` in lower.zig. - return bailDetail("comptime has_impl: interp-time evaluation not yet wired (use static type args for now — they fold at lower time)"); - }, - - // ── Comptime type CONSTRUCTION primitives ──────────── - .declare => { - const tbl = self.mint orelse - return bailDetail("comptime declare(): no type-mint target (declare/define are comptime-only — reached at runtime/emit?)"); - if (bi.args.len != 1) return bailDetail("comptime declare(name): needs the name argument"); - const nm = frame.getRef(bi.args[0]).asString(self) orelse - return bailDetail("comptime declare(): name is not a string"); - const name_id = tbl.internString(nm); - // Lowering already registered this named forward slot (so a - // `*Name` self-reference in the body resolved); return THAT slot - // so `define` completes the same one. Mint it if somehow absent. - if (tbl.findByName(name_id)) |existing| return .{ .value = .{ .type_tag = existing } }; - const info: types.TypeInfo = .{ .tagged_union = .{ - .name = name_id, - .fields = &.{}, - .tag_type = .i64, - } }; - const tid = tbl.internNominal(info, 0); - return .{ .value = .{ .type_tag = tid } }; - }, - .define => { - const tbl = self.mint orelse - return bailDetail("comptime define(): no type-mint target (declare/define are comptime-only — reached at runtime/emit?)"); - if (bi.args.len != 2) return bailDetail("comptime define(handle, info): needs exactly two arguments"); - const handle = frame.getRef(bi.args[0]).asTypeId() orelse - return bailDetail("comptime define(): first argument is not a Type handle (use a `declare()` result)"); - const info_val = frame.getRef(bi.args[1]); - return self.defineType(tbl, handle, info_val); - }, - .type_info => { - // Reflect a type INTO a `TypeInfo` value — the inverse of - // `define`'s decode. Lowering already validated the arg is an - // enum/tagged-union and passed it as a `const_type`. - if (bi.args.len != 1) return bailDetail("comptime type_info: missing type argument"); - const tid = frame.getRef(bi.args[0]).asTypeId() orelse - return bailDetail("comptime type_info: argument is not a Type value"); - return self.reflectTypeInfo(tid); - }, - } - } - - /// Build the `.enum(EnumInfo{ variants })` `TypeInfo` value for `tid` — the - /// exact shape `defineEnum` decodes, so `define(declare(n), type_info(T))` - /// round-trips. A `tagged_union` reflects each field as - /// `EnumVariant{ name, payload = field.ty }` (tagless variants already carry - /// `void`); a payloadless `@"enum"` reflects every variant with `void`. - /// Value layout mirrors how the interp evaluates the hand-written literal: - /// variant = { string(name), type_tag(payload) } - /// variants = { aggregate(variant…), int(len) } (slice fat pointer) - /// EnumInfo = { variants } - /// TypeInfo = { int(0), EnumInfo } (`.enum` tag = 0) - fn reflectTypeInfo(self: *Interpreter, tid: TypeId) InterpError!ExecResult { - var elems = std.ArrayList(Value).empty; - const info = self.module.types.get(tid); - // The TypeInfo variant tag (declaration order in `meta.sx`: `enum`=0, - // `struct`=1). Each member reflects as `{ string(name), type_tag(ty) }` - // regardless of kind — payload type for an enum variant, field type for a - // struct field (a payloadless `@"enum"` variant carries `void`). - const tag: i64 = switch (info) { - .tagged_union => |u| blk: { - for (u.fields) |f| { - const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime; - const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime; - elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime; - } - break :blk 0; - }, - .@"enum" => |e| blk: { - for (e.variants) |vname| { - const nm = self.alloc.dupe(u8, self.module.types.getString(vname)) catch return error.CannotEvalComptime; - const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = .void } }) catch return error.CannotEvalComptime; - elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime; - } - break :blk 0; - }, - .@"struct" => |s| blk: { - for (s.fields) |f| { - const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime; - const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime; - elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime; - } - break :blk 1; - }, - .tuple => |t| blk: { - // Tuple elements are POSITIONAL — bare `type_tag` values, not - // `{ name, type }` pairs (TupleInfo carries no field names). - for (t.fields) |elem_ty| { - elems.append(self.alloc, .{ .type_tag = elem_ty }) catch return error.CannotEvalComptime; - } - break :blk 2; - }, - else => return bailDetail("comptime type_info: only enum / tagged-union / struct / tuple types reflect today"), - }; - if (elems.items.len == 0) return bailDetail("comptime type_info: type has no members"); - - // Wrap: members → `{ data, len }` slice → info struct `{ members }` → - // TypeInfo `{ int(tag), info }`. Identical shape for `.enum` / `.struct`. - const members_slice = self.alloc.dupe(Value, &.{ - .{ .aggregate = elems.items }, - .{ .int = @intCast(elems.items.len) }, - }) catch return error.CannotEvalComptime; - const inner = self.alloc.dupe(Value, &.{.{ .aggregate = members_slice }}) catch return error.CannotEvalComptime; - const typeinfo = self.alloc.dupe(Value, &.{ .{ .int = tag }, .{ .aggregate = inner } }) catch return error.CannotEvalComptime; - return .{ .value = .{ .aggregate = typeinfo } }; - } - - /// Complete a `declare()`d slot from a `TypeInfo` VALUE, dispatching on the - /// TypeInfo tag (`{ tag, payload }`): `0` → `.enum(EnumInfo)` (tagged_union), - /// `1` → `.struct(StructInfo)`. The tag is the variant index in `meta.sx`'s - /// `TypeInfo` enum declaration order (`enum` then `struct`). - fn defineType(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { - const ti_fields = switch (info_val) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): info did not evaluate to a TypeInfo value"), - }; - if (ti_fields.len != 2) return bailDetail("comptime define(): malformed TypeInfo value (expected `{ tag, info }`)"); - const tag = ti_fields[0].asInt() orelse return bailDetail("comptime define(): TypeInfo tag is not an integer"); - return switch (tag) { - 0 => self.defineEnum(tbl, handle, info_val), - 1 => self.defineStruct(tbl, handle, info_val), - 2 => self.defineTuple(tbl, handle, info_val), - else => bailDetail("comptime define(): unknown TypeInfo variant (only `.enum` / `.struct` / `.tuple` are supported)"), - }; - } - - /// Complete a `declare()`d slot from a `TypeInfo` VALUE. The value is the - /// `.enum(EnumInfo)` tagged-union (`{ tag, EnumInfo }`), EnumInfo is - /// `{ variants }`, and each variant is `{ name: string, payload: Type }`. - /// Decodes those into a `tagged_union` byte-identical to a source enum's - /// `buildEnumInfo` output (default `i64` tag, no backing) and fills the slot - /// via `updatePreservingKey` (the handle's name + nominal id are unchanged). - /// Every decode failure is a loud bail — never a silent default. - fn defineEnum(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { - // Unwrap TypeInfo `.enum(EnumInfo)` → EnumInfo `{ variants }`. - const ti_fields = switch (info_val) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): info did not evaluate to a TypeInfo value"), - }; - if (ti_fields.len != 2) return bailDetail("comptime define(): only the `.enum(...)` TypeInfo variant is supported"); - const einfo = ti_fields[1]; - const einfo_fields = switch (einfo) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): `.enum` payload is not an EnumInfo struct value"), - }; - // EnumInfo = `{ variants: []EnumVariant }`. The name was given to - // `declare` (it's already the slot's name) — `define` only fills the body. - if (einfo_fields.len != 1) return bailDetail("comptime define(): EnumInfo must have a `variants` field"); - const elems = decodeVariantElements(einfo_fields[0]) orelse - return bailDetail("comptime define(): `variants` is not a slice/array of EnumVariant"); - if (elems.len == 0) return bailDetail("comptime define(): enum has no variants"); - - var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - for (elems) |elem| { - const ev = switch (elem) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): EnumVariant did not evaluate to a struct value"), - }; - if (ev.len != 2) return bailDetail("comptime define(): EnumVariant must have `name` and `payload`"); - const vname = ev[0].asString(self) orelse return bailDetail("comptime define(): EnumVariant `name` is not a string"); - const payload_tid = ev[1].asTypeId() orelse return bailDetail("comptime define(): EnumVariant `payload` is not a Type value"); - const vname_id = tbl.internString(vname); - // Reject a duplicate variant name loudly — two same-named variants - // make construction (`.a`) and matching ambiguous and would silently - // pick one. The name is dynamic, so set the bail detail directly - // (bailDetail takes a comptime string); evalComptimeType renders it. - for (fields.items) |existing| { - if (existing.name == vname_id) { - last_bail_detail = std.fmt.allocPrint(self.alloc, "comptime define(): duplicate variant name '{s}'", .{vname}) catch "comptime define(): duplicate variant name"; - return error.CannotEvalComptime; - } - } - fields.append(self.alloc, .{ .name = vname_id, .ty = payload_tid }) catch return error.CannotEvalComptime; - } - - // Complete the declared slot IN PLACE: it already has its name + nominal - // id (from `declare`); fill the body. - const cur = tbl.get(handle); - if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d enum slot"); - - // A FULLY payloadless variant set (every payload `void`) is an actual - // enum — mint a `.@"enum"`, exactly like a hand-written `enum { a; b; }`. - // Minting it as an all-void `tagged_union` instead gives a type whose IR - // size disagrees with its LLVM size (a tag, but no payload storage), which - // trips `verifySizes` at codegen. A kind change re-keys the slot, so - // `replaceKeyedInfo` (not `updatePreservingKey`, which asserts the kind is - // stable — true only for the tagged_union path below). - var all_void = true; - for (fields.items) |f| { - if (f.ty != .void) { - all_void = false; - break; - } - } - if (all_void) { - var variants = std.ArrayList(types.StringId).empty; - for (fields.items) |f| variants.append(self.alloc, f.name) catch return error.CannotEvalComptime; - const en: types.TypeInfo = .{ .@"enum" = .{ - .name = cur.tagged_union.name, - .variants = variants.items, - .nominal_id = cur.tagged_union.nominal_id, - } }; - tbl.replaceKeyedInfo(handle, en); - return .{ .value = .{ .type_tag = handle } }; - } - - // Payload-carrying enum → tagged_union. Name/id unchanged → stable key. - const full: types.TypeInfo = .{ .tagged_union = .{ - .name = cur.tagged_union.name, - .fields = fields.items, - .tag_type = .i64, - .backing_type = null, - .explicit_tag_values = null, - .nominal_id = cur.tagged_union.nominal_id, - } }; - tbl.updatePreservingKey(handle, full); - // Return the handle so the one-shot form chains: `T :: define(declare("T"), info)`. - return .{ .value = .{ .type_tag = handle } }; - } - - /// Complete a `declare()`d slot from a `.struct(StructInfo)` `TypeInfo` VALUE. - /// Mirror of `defineEnum` for structs: StructInfo is `{ fields }`, each field - /// `{ name: string, type: Type }`. Fills the (tagged_union-shaped) declare - /// slot in place as a `.@"struct"`, preserving its name + nominal id. - fn defineStruct(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { - // Unwrap TypeInfo `.struct(StructInfo)` → StructInfo `{ fields }`. - const ti_fields = info_val.aggregate; // defineType already checked the shape - const sinfo = ti_fields[1]; - const sinfo_fields = switch (sinfo) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): `.struct` payload is not a StructInfo struct value"), - }; - if (sinfo_fields.len != 1) return bailDetail("comptime define(): StructInfo must have a `fields` field"); - const elems = decodeVariantElements(sinfo_fields[0]) orelse - return bailDetail("comptime define(): `fields` is not a slice/array of StructField"); - if (elems.len == 0) return bailDetail("comptime define(): struct has no fields"); - - var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - for (elems) |elem| { - const sf = switch (elem) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): StructField did not evaluate to a struct value"), - }; - if (sf.len != 2) return bailDetail("comptime define(): StructField must have `name` and `type`"); - const fname = sf[0].asString(self) orelse return bailDetail("comptime define(): StructField `name` is not a string"); - const fty = sf[1].asTypeId() orelse return bailDetail("comptime define(): StructField `type` is not a Type value"); - const fname_id = tbl.internString(fname); - // Reject duplicate field names (a struct can't have two same-named - // fields). Dynamic name → set the detail directly (bailDetail is - // comptime-only); evalComptimeType renders it. - for (fields.items) |existing| { - if (existing.name == fname_id) { - last_bail_detail = std.fmt.allocPrint(self.alloc, "comptime define(): duplicate field name '{s}'", .{fname}) catch "comptime define(): duplicate field name"; - return error.CannotEvalComptime; - } - } - fields.append(self.alloc, .{ .name = fname_id, .ty = fty }) catch return error.CannotEvalComptime; - } - - // Complete the declare slot as a struct. It was minted as an (empty) - // tagged_union by `declare`; we keep its TypeId + name + nominal id but - // SWAP THE KIND to struct. A kind change moves the intern key, so use - // `replaceKeyedInfo` (re-keys) rather than `updatePreservingKey` (which - // asserts the key is unchanged — true for the enum path, false here). - const cur = tbl.get(handle); - if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d slot"); - const full: types.TypeInfo = .{ .@"struct" = .{ - .name = cur.tagged_union.name, - .fields = fields.items, - .nominal_id = cur.tagged_union.nominal_id, - } }; - tbl.replaceKeyedInfo(handle, full); - return .{ .value = .{ .type_tag = handle } }; - } - - /// Complete a `declare()`d slot from a `.tuple(TupleInfo)` `TypeInfo` VALUE. - /// TupleInfo is `{ elements }`, each element a bare `Type` (positional, no - /// name). Tuples are structural, so the declared NAME is vestigial — we still - /// complete the slot in place (so `define` returns the handle, like the - /// enum/struct paths) via `replaceKeyedInfo` (kind change: tagged_union slot → - /// tuple, re-keyed structurally by element types). - fn defineTuple(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult { - const ti_fields = info_val.aggregate; // defineType already checked the shape - const tinfo = ti_fields[1]; - const tinfo_fields = switch (tinfo) { - .aggregate => |f| f, - else => return bailDetail("comptime define(): `.tuple` payload is not a TupleInfo struct value"), - }; - if (tinfo_fields.len != 1) return bailDetail("comptime define(): TupleInfo must have an `elements` field"); - const elems = decodeVariantElements(tinfo_fields[0]) orelse - return bailDetail("comptime define(): `elements` is not a slice/array of Type"); - if (elems.len == 0) return bailDetail("comptime define(): tuple has no elements"); - - var field_tys = std.ArrayList(TypeId).empty; - for (elems) |elem| { - const ety = elem.asTypeId() orelse return bailDetail("comptime define(): tuple element is not a Type value"); - field_tys.append(self.alloc, ety) catch return error.CannotEvalComptime; - } - - const cur = tbl.get(handle); - if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d slot"); - const full: types.TypeInfo = .{ .tuple = .{ .fields = field_tys.items, .names = null } }; - tbl.replaceKeyedInfo(handle, full); - return .{ .value = .{ .type_tag = handle } }; - } -}; - -/// Normalize an interpreter 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; -} - -// ── Frame ─────────────────────────────────────────────────────────────── -// Holds SSA values (by Ref index) and local mutable slots (for alloca). - -const Frame = struct { - refs: []Value, - ref_alloc: Allocator, - slots: std.ArrayList(Value), - - /// Create a frame pre-allocated with `num_refs` slots (all undef). - fn initSized(alloc: Allocator, num_refs: u32) Frame { - const refs = alloc.alloc(Value, num_refs) catch unreachable; - @memset(refs, .undef); - return .{ - .refs = refs, - .ref_alloc = alloc, - .slots = std.ArrayList(Value).empty, - }; - } - - fn deinit(self: *Frame) void { - self.ref_alloc.free(self.refs); - } - - fn setRef(self: *Frame, idx: u32, val: Value) void { - if (idx < self.refs.len) { - self.refs[idx] = val; - } - } - - fn getRef(self: *const Frame, ref: Ref) Value { - if (ref.isNone()) return .void_val; - const idx = ref.index(); - if (idx >= self.refs.len) return .undef; - return self.refs[idx]; - } - - fn allocSlot(self: *Frame, alloc: Allocator) u32 { - const idx: u32 = @intCast(self.slots.items.len); - self.slots.append(alloc, .undef) catch unreachable; - return idx; - } - - fn loadSlot(self: *const Frame, slot: u32) Value { - if (slot >= self.slots.items.len) return .undef; - return self.slots.items[slot]; - } - - fn storeSlot(self: *Frame, slot: u32, val: Value) void { - if (slot < self.slots.items.len) { - self.slots.items[slot] = val; - } - } -}; - diff --git a/src/ir/ir.zig b/src/ir/ir.zig index e6def641..1959e3f1 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -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"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3f9155c0..20204869 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.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"); diff --git a/src/main.zig b/src/main.zig index 8b7c8063..c8d244d4 100644 --- a/src/main.zig +++ b/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 {