comptime VM arc: abi(.compiler) ABI, out as sx fn, VM-native diagnostics, BuildConfig threaded
Lands the full VM/compiler-API arc on branch reify (701/0 both gates): - abi(.compiler) ABI replaces abi(.zig) extern compiler + the fake #library "compiler"; bodiless decl = compiler-API surface, bodied = user compiler-domain fn (lowered for VM eval, emit-skipped). - out is a plain sx fn (libc write) — the out builtin deleted; the VM handles it via host-FFI. trace_resolve + interp_print_frames ported. - 4B VM-native diagnostics: 1179/1180 render proper comptime type construction failed: under strict. - S5a: build_options/set_post_link_callback on abi(.compiler) with BuildConfig threaded into the VM (green intermediate). - 0522 fixed (describe(args: []Type)); regression 0638. Strict deletion-gate down to 4 compiler_call bails (1609/1614/1615/1616) + 1654 (legitimate unresolvable-symbol diagnostic).
This commit is contained in:
@@ -132,9 +132,15 @@ pub const Root = struct {
|
||||
/// - `.zig` — welded to the real internal Zig type/fn: layout follows the bound
|
||||
/// Zig type, functions dispatch over the comptime host-call bridge. The
|
||||
/// `compiler` library (`design/comptime-compiler-api.md`) binds via `abi(.zig)`.
|
||||
/// - `.compiler` — a COMPILER-DOMAIN function: it runs in the comptime evaluator
|
||||
/// (VM / interp), NEVER in the shipped binary, so the backend does not lower it.
|
||||
/// Covers the compiler-API surface (`intern`/`find_type`/`build_options`/… —
|
||||
/// bodiless decls whose Zig/VM handler is the impl) AND user compiler-domain
|
||||
/// functions like post-link callbacks (bodied, but emit-skipped). The ABI alone
|
||||
/// marks it — there is no `extern <lib>` and no fake `#library "compiler"`.
|
||||
/// - `.pure` — a pure / naked function (inline asm body), no calling-convention
|
||||
/// prologue/epilogue.
|
||||
pub const ABI = enum { default, c, zig, pure };
|
||||
pub const ABI = enum { default, c, compiler, pure };
|
||||
|
||||
/// Linkage modifier written in the postfix slot before `abi(...)`:
|
||||
/// `name :: (sig) -> Ret [extern | export] [abi(.x)] [lib] [;|{…}];`
|
||||
|
||||
@@ -1119,13 +1119,23 @@ pub const Ops = struct {
|
||||
// wrapper, `is_comptime`) is fine — that body is interp-evaluated and its
|
||||
// LLVM emission is dead, so skip the gate there.
|
||||
const enclosing = &self.e.ir_mod.functions.items[self.e.current_func_idx];
|
||||
if (callee_func.compiler_welded and !enclosing.is_comptime) {
|
||||
if ((callee_func.compiler_welded or callee_func.is_compiler_domain) and !enclosing.is_comptime) {
|
||||
const fname = self.e.ir_mod.types.getString(callee_func.name);
|
||||
std.debug.print("error: '{s}' is a comptime-only compiler-library function — it cannot be called at runtime (use it inside #run or a comptime '::')\n", .{fname});
|
||||
std.debug.print("error: '{s}' is a comptime-only compiler-domain function — it cannot be called at runtime (use it inside #run or a comptime '::')\n", .{fname});
|
||||
self.e.comptime_failed = true;
|
||||
self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty)));
|
||||
return;
|
||||
}
|
||||
// A comptime-only callee (compiler-API or compiler-domain) reached here from
|
||||
// a COMPTIME (dead) body — the enclosing `#run`/`::` wrapper whose LLVM is
|
||||
// never executed. Such a function has no runtime symbol, so emit `undef`
|
||||
// instead of a real `call` (which would leave an undefined reference for the
|
||||
// AOT linker). The comptime VALUE is produced by the interp/VM, not this dead
|
||||
// body. Mirrors the old `compiler_call` → undef.
|
||||
if (callee_func.compiler_welded or callee_func.is_compiler_domain) {
|
||||
self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (callee_func.is_comptime and call_op.args.len == 0) {
|
||||
var interp_inst = Interpreter.init(self.e.ir_mod, self.e.alloc);
|
||||
@@ -1377,25 +1387,6 @@ pub const Ops = struct {
|
||||
self.e.mapRef(c.LLVMBuildCall2(self.e.builder, self.e.getMathF64Type(), f, &args, 1, @tagName(bi.builtin)));
|
||||
}
|
||||
},
|
||||
.out => {
|
||||
// out(str): extract ptr and len from string fat pointer, call write(1, ptr, len)
|
||||
const str_val = self.e.resolveRef(bi.args[0]);
|
||||
const raw_ptr = c.LLVMBuildExtractValue(self.e.builder, str_val, 0, "str.ptr");
|
||||
const str_len = c.LLVMBuildExtractValue(self.e.builder, str_val, 1, "str.len");
|
||||
// On wasm32, count param is i32 (size_t)
|
||||
const count = if (self.e.target_config.isWasm32())
|
||||
c.LLVMBuildTrunc(self.e.builder, str_len, self.e.cached_i32, "len.tr")
|
||||
else
|
||||
str_len;
|
||||
const write_fn = self.e.getOrDeclareWrite();
|
||||
var write_args = [_]c.LLVMValueRef{
|
||||
c.LLVMConstInt(self.e.cached_i32, 1, 0), // fd = stdout
|
||||
raw_ptr,
|
||||
count,
|
||||
};
|
||||
_ = c.LLVMBuildCall2(self.e.builder, self.e.getWriteType(), write_fn, &write_args, 3, "");
|
||||
self.e.advanceRefCounter();
|
||||
},
|
||||
.type_name => {
|
||||
// Dynamic `type_name(t)` at runtime: resolve the TypeId
|
||||
// the arg denotes (reading an `Any`'s runtime type-tag,
|
||||
|
||||
@@ -58,6 +58,9 @@ pub const bound_fns = [_]BoundFn{
|
||||
.{ .sx_name = "declare_type", .handler = handleDeclareType },
|
||||
.{ .sx_name = "pointer_to", .handler = handlePointerTo },
|
||||
.{ .sx_name = "register_type", .handler = handleRegisterType },
|
||||
// ── BuildOptions (migrated off `#compiler` onto `abi(.compiler)`) ─────────
|
||||
.{ .sx_name = "build_options", .handler = handleBuildOptions },
|
||||
.{ .sx_name = "set_post_link_callback", .handler = handleSetPostLinkCallback },
|
||||
};
|
||||
|
||||
// Kind codes accepted by `register_type` — mirror `TypeTable.kindCode`. An
|
||||
@@ -307,3 +310,27 @@ fn memberPair(elem: Value) ?struct { name: []const u8, ty: types.TypeId } {
|
||||
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.
|
||||
fn handleSetPostLinkCallback(interp: *Interpreter, args: []const Value) InterpError!Value {
|
||||
if (args.len != 2) return error.TypeError;
|
||||
const bc = interp.build_config orelse return error.CannotEvalComptime;
|
||||
switch (args[1]) {
|
||||
.func_ref => |id| bc.post_link_callback_fn = id,
|
||||
else => return error.TypeError,
|
||||
}
|
||||
return .void_val;
|
||||
}
|
||||
|
||||
@@ -376,6 +376,55 @@ test "comptime_vm exec: const_string length + str_eq/str_ne" {
|
||||
try std.testing.expectEqual(@as(i64, 3), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: error_tag_name_get maps a tag id to its name string" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
_ = table.internTag("Foo");
|
||||
const bad = table.internTag("Bad"); // the tag we'll resolve
|
||||
|
||||
// return error_tag_name(<bad tag id>) → the string "Bad"
|
||||
var fb = Fb.init(alloc, &.{}, .string);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const id = fb.add(b0, inst(.{ .const_int = @intCast(bad) }, .i64));
|
||||
const name = fb.add(b0, inst(.{ .error_tag_name_get = .{ .operand = ref(id) } }, .string));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(name) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
const word = try v.run(&fb.func, &.{});
|
||||
const val = try v.regToValue(alloc, &table, word, .string);
|
||||
defer alloc.free(val.string); // regToValue dupes the bytes
|
||||
try std.testing.expectEqualStrings("Bad", val.string);
|
||||
}
|
||||
|
||||
test "comptime_vm exec: type_is_unsigned(u32) - type_is_unsigned(i64) == 1" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
|
||||
// r_u := type_is_unsigned(u32) → 1 ; r_s := type_is_unsigned(i64) → 0
|
||||
// return r_u - r_s → 1 (only the correct unsigned/signed verdicts give 1)
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const ct_u = fb.add(b0, inst(.{ .const_type = .u32 }, .type_value));
|
||||
const au = [_]Ref{ref(ct_u)};
|
||||
const r_u = fb.add(b0, inst(.{ .call_builtin = .{ .builtin = .type_is_unsigned, .args = &au } }, .bool));
|
||||
const ct_s = fb.add(b0, inst(.{ .const_type = .i64 }, .type_value));
|
||||
const as = [_]Ref{ref(ct_s)};
|
||||
const r_s = fb.add(b0, inst(.{ .call_builtin = .{ .builtin = .type_is_unsigned, .args = &as } }, .bool));
|
||||
const diff = fb.add(b0, inst(.{ .sub = .{ .lhs = ref(r_u), .rhs = ref(r_s) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(diff) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 1), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: array_to_slice + index through slice + slice length" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
@@ -1262,7 +1311,7 @@ test "comptime_vm tryEval: pure function → Value; unsupported → null" {
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(m) } }, .void));
|
||||
const ok_id = module.addFunction(fb.func);
|
||||
|
||||
const v = vm.tryEval(alloc, &module, ok_id) orelse return error.VmShouldHaveHandledIt;
|
||||
const v = vm.tryEval(alloc, &module, ok_id, null, null) orelse return error.VmShouldHaveHandledIt;
|
||||
try std.testing.expectEqual(@as(i64, 42), v.int);
|
||||
|
||||
// fn bad() { compiler_call() } → an unported op → tryEval yields null (caller
|
||||
@@ -1274,7 +1323,7 @@ test "comptime_vm tryEval: pure function → Value; unsupported → null" {
|
||||
_ = fb2.add(c0, inst(.ret_void, .void));
|
||||
const bad_id = module.addFunction(fb2.func);
|
||||
|
||||
try std.testing.expect(vm.tryEval(alloc, &module, bad_id) == null);
|
||||
try std.testing.expect(vm.tryEval(alloc, &module, bad_id, null, null) == null);
|
||||
}
|
||||
|
||||
test "comptime_vm exec: division by zero and unsupported op bail loudly" {
|
||||
@@ -1432,7 +1481,7 @@ test "comptime_vm tryEval: deref of a null pointer bails (null, not a crash)" {
|
||||
|
||||
// The hardened accessors turn the null deref into error.OutOfBounds → run
|
||||
// bails → tryEval returns null (legacy fallback), NOT a debug panic.
|
||||
try std.testing.expect(vm.tryEval(alloc, &module, bad_id) == null);
|
||||
try std.testing.expect(vm.tryEval(alloc, &module, bad_id, null, null) == null);
|
||||
}
|
||||
|
||||
test "comptime_vm: arena allocations are aligned, non-null, and stable across grows" {
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
//! The comptime evaluator is being rebuilt around a flat, byte-addressable memory
|
||||
//! so comptime values are NATIVE BYTES (like runtime), instead of the tagged
|
||||
//! `Value` union the legacy interpreter (`interp.zig`) uses. This module is the
|
||||
//! machine substrate: a linear byte memory with a bump/stack allocator, plus a
|
||||
//! per-call `Frame` holding a register file.
|
||||
//! machine substrate: byte-addressable memory backed by an ARENA of stable host
|
||||
//! allocations (each `allocBytes` never moves; freed wholesale on `deinit`), plus
|
||||
//! a per-call `Frame` holding a register file. `Addr` is the allocation's real
|
||||
//! host pointer, so a flat-memory pointer and an FFI-returned host pointer are the
|
||||
//! same kind of value.
|
||||
//!
|
||||
//! Value model (grows over later sub-steps): a register (`Reg`) is a raw 64-bit
|
||||
//! word that is EITHER an immediate scalar (its bits) OR an `Addr` into flat
|
||||
@@ -17,9 +20,10 @@
|
||||
//! bytes. Layout (sizes/offsets/pointer width) is supplied by the type table when
|
||||
//! the executor lays a value out, so cross-compilation stays correct.
|
||||
//!
|
||||
//! Sub-step 1 (this file): `Machine` (memory + bump/stack alloc + scalar word
|
||||
//! read/write + byte views) and `Frame` (register file + stack reclamation). No
|
||||
//! op execution yet — the executor + op handlers arrive in the next sub-step. The
|
||||
//! `Machine` (arena-backed memory + scalar word read/write + byte views) holds the
|
||||
//! comptime stack + heap; `Frame` is the per-call register file. A `Frame` does NOT
|
||||
//! reclaim the machine's memory on exit — a callee can return an aggregate whose
|
||||
//! register holds an `Addr` into flat memory, and reclaiming would dangle it. The
|
||||
//! legacy interpreter remains the live evaluator until the VM reaches parity.
|
||||
|
||||
const std = @import("std");
|
||||
@@ -28,6 +32,7 @@ const types = @import("types.zig");
|
||||
const mod_mod = @import("module.zig");
|
||||
const interp_mod = @import("interp.zig");
|
||||
const host_ffi = @import("host_ffi.zig");
|
||||
const errors_mod = @import("../errors.zig");
|
||||
const Value = interp_mod.Value;
|
||||
const Inst = inst_mod.Inst;
|
||||
const Ref = inst_mod.Ref;
|
||||
@@ -186,7 +191,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) ?Value {
|
||||
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 {
|
||||
last_bail_reason = null;
|
||||
const func = module.getFunction(func_id);
|
||||
if (func.is_extern or func.blocks.items.len == 0) {
|
||||
@@ -197,6 +202,8 @@ pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.
|
||||
defer vm.deinit();
|
||||
vm.table = &module.types;
|
||||
vm.module = module;
|
||||
vm.build_config = build_config;
|
||||
vm.source_map = source_map;
|
||||
|
||||
// `runEntry` materializes the implicit `*Context` (a comptime const-init /
|
||||
// `#run` wrapper is nullary in user args, so the implicit ctx is its sole
|
||||
@@ -276,6 +283,16 @@ pub const Vm = struct {
|
||||
/// The module — resolves a `call`'s callee `FuncId` to its `Function`. Optional
|
||||
/// so leaf functions (no calls) need none; a `call` bails loudly if it is absent.
|
||||
module: ?*const Module = null,
|
||||
/// The mutable build configuration (`BuildOptions` accumulator) — the SAME
|
||||
/// `BuildConfig` `EmitLLVM` owns and `main.zig` reads post-link. Threaded in at
|
||||
/// 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,
|
||||
/// 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 / "".
|
||||
source_map: ?*const std.StringHashMap([:0]const u8) = null,
|
||||
/// Current call-recursion depth, guarded against host stack overflow on deep /
|
||||
/// infinite comptime recursion (mirrors the legacy interp's `call_depth`).
|
||||
depth: u32 = 0,
|
||||
@@ -819,6 +836,76 @@ pub const Vm = struct {
|
||||
const fid: u64 = if (self.call_stack.items.len > 0) self.call_stack.items[self.call_stack.items.len - 1].index() else 0;
|
||||
return .{ .value = (fid << 32) | @as(u64, ins.span.start) };
|
||||
},
|
||||
// Dump the comptime call-frame chain (`trace.print_interpreter_frames`) —
|
||||
// the VM-native mirror of the legacy `printInterpFrames`. Walks the active
|
||||
// `call_stack` (skipping the last frame, the `print_interpreter_frames`
|
||||
// fn itself, like the legacy) and writes ` at <name>` lines straight to
|
||||
// fd 1 (consistent with `out`'s now-direct libc `write`).
|
||||
.interp_print_frames => {
|
||||
const module = self.module orelse return self.failMsg("comptime interp_print_frames: no module");
|
||||
const n = self.call_stack.items.len;
|
||||
if (n <= 1) return .{ .value = null_addr };
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
defer buf.deinit(self.gpa);
|
||||
buf.appendSlice(self.gpa, "comptime call frames (most recent call last):\n") catch return self.failMsg("comptime interp_print_frames: out of memory");
|
||||
var i: usize = 0;
|
||||
while (i < n - 1) : (i += 1) {
|
||||
const fname = module.types.getString(module.getFunction(self.call_stack.items[i]).name);
|
||||
buf.appendSlice(self.gpa, " at ") catch return self.failMsg("comptime interp_print_frames: out of memory");
|
||||
buf.appendSlice(self.gpa, fname) catch return self.failMsg("comptime interp_print_frames: out of memory");
|
||||
buf.append(self.gpa, '\n') catch return self.failMsg("comptime interp_print_frames: out of memory");
|
||||
}
|
||||
_ = std.c.write(1, buf.items.ptr, buf.items.len);
|
||||
return .{ .value = null_addr };
|
||||
},
|
||||
// Unpack a comptime frame `(func_id << 32 | span.start)` and build a
|
||||
// `Frame { file, line, col, func, line_text }` aggregate in flat memory —
|
||||
// the VM-native mirror of the legacy interp's `.trace_resolve`. `ins.ty`
|
||||
// is the `Frame` struct, so each field's type/offset comes from the table.
|
||||
.trace_resolve => |u| {
|
||||
const table = try self.requireTable();
|
||||
const module = self.module orelse return self.failMsg("comptime trace_resolve: no module");
|
||||
const raw = frame.get(u.operand.index());
|
||||
const fid: u32 = @intCast(raw >> 32);
|
||||
const offset: u32 = @truncate(raw);
|
||||
if (fid >= module.functions.items.len) return self.failMsg("comptime trace_resolve: func id out of range");
|
||||
const func = module.getFunction(inst_mod.FuncId.fromIndex(fid));
|
||||
const func_name = 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_mod.SourceLoc.compute(src, offset);
|
||||
line = @intCast(loc.line);
|
||||
col = @intCast(loc.col);
|
||||
line_text = errors_mod.lineAt(src, offset);
|
||||
}
|
||||
}
|
||||
const fty = ins.ty;
|
||||
if (fty.isBuiltin() or table.get(fty) != .@"struct")
|
||||
return self.failMsg("comptime trace_resolve: result type is not a Frame struct");
|
||||
const sfields = table.get(fty).@"struct".fields;
|
||||
if (sfields.len != 5) return self.failMsg("comptime trace_resolve: Frame struct is not 5 fields");
|
||||
const addr = self.machine.allocBytes(table.typeSizeBytes(fty), table.typeAlignBytes(fty));
|
||||
// { file, line, col, func, line_text } — positional, matching the legacy build.
|
||||
try self.writeField(table, addr + fieldOffset(table, fty, 0), sfields[0].ty, try self.makeStringValue(table, file));
|
||||
try self.writeField(table, addr + fieldOffset(table, fty, 1), sfields[1].ty, @bitCast(line));
|
||||
try self.writeField(table, addr + fieldOffset(table, fty, 2), sfields[2].ty, @bitCast(col));
|
||||
try self.writeField(table, addr + fieldOffset(table, fty, 3), sfields[3].ty, try self.makeStringValue(table, func_name));
|
||||
try self.writeField(table, addr + fieldOffset(table, fty, 4), sfields[4].ty, try self.makeStringValue(table, line_text));
|
||||
return .{ .value = addr };
|
||||
},
|
||||
// `error_tag_name(e)` — the runtime tag id (a word) → its name string via
|
||||
// the always-linked tag-name table. Pure: builds a `{ptr,len}` string in
|
||||
// flat memory. Mirrors the legacy interp's `error_tag_name_get`.
|
||||
.error_tag_name_get => |u| {
|
||||
const table = try self.requireTable();
|
||||
const id: u32 = @intCast(frame.get(u.operand.index()));
|
||||
return .{ .value = try self.makeStringValue(table, table.getTagName(id)) };
|
||||
},
|
||||
|
||||
// ── Calls ───────────────────────────────────────────
|
||||
// Direct call: resolve the static callee `FuncId` and dispatch.
|
||||
@@ -840,6 +927,21 @@ pub const Vm = struct {
|
||||
// `comptime_func` run on this same VM, or a scalar static value),
|
||||
// memoized. Mirrors the legacy interp's `getGlobal`.
|
||||
.global_get => |gid| return .{ .value = try self.evalGlobal(gid) },
|
||||
// `&global` — only `&__sx_default_context` is materialised at comptime
|
||||
// (its address sees runtime use via the implicit-ctx plumbing). Return
|
||||
// the context's flat-memory address — an aggregate value IS its address,
|
||||
// so a later `load`/field read sees the materialised Context. Mirrors the
|
||||
// legacy interp's `global_addr` (the sole supported global); any other
|
||||
// global bails to legacy fallback.
|
||||
.global_addr => |gid| {
|
||||
const module = self.module orelse return self.failMsg("comptime VM: global_addr needs a module");
|
||||
if (gid.index() < module.globals.items.len and
|
||||
std.mem.eql(u8, module.types.getString(module.globals.items[gid.index()].name), "__sx_default_context"))
|
||||
{
|
||||
return .{ .value = try self.materializeDefaultContext(module) };
|
||||
}
|
||||
return self.failMsg("comptime global_addr: only `&__sx_default_context` is materialised at comptime");
|
||||
},
|
||||
// A function value is its encoded func-ref word (see `funcRefWord`).
|
||||
.func_ref => |fid| return .{ .value = funcRefWord(fid) },
|
||||
|
||||
@@ -1005,6 +1107,14 @@ pub const Vm = struct {
|
||||
return error.Unsupported;
|
||||
}
|
||||
|
||||
/// Like `failMsg` but for a runtime-formatted reason (e.g. naming the offending
|
||||
/// variant). Allocated in `gpa` so it survives to the host's diagnostic render;
|
||||
/// the build fails on this path, so the small leak is moot.
|
||||
fn failFmt(self: *Vm, comptime fmt: []const u8, args: anytype) error{Unsupported} {
|
||||
self.detail = std.fmt.allocPrint(self.gpa, fmt, args) catch "comptime VM: out of memory formatting diagnostic";
|
||||
return error.Unsupported;
|
||||
}
|
||||
|
||||
fn badRef(self: *Vm) error{Unsupported} {
|
||||
self.detail = "comptime VM: malformed IR — operand ref out of range (unresolved name?)";
|
||||
return error.Unsupported;
|
||||
@@ -1322,6 +1432,26 @@ pub const Vm = struct {
|
||||
if (std.mem.eql(u8, name, "register_type")) {
|
||||
return self.registerTypeVm(args, frame, ref_types);
|
||||
}
|
||||
// ── BuildOptions (migrated off `#compiler` onto `abi(.compiler)`) ───────
|
||||
// `build_options()` hands back an opaque, zero-field `BuildOptions` handle;
|
||||
// the real state lives on the threaded `BuildConfig`. Return the null
|
||||
// sentinel word (the handle is never dereferenced — every operation takes it
|
||||
// as an ignored `self`). Mirrors the legacy `hookBuildOptions` (`.void_val`).
|
||||
if (std.mem.eql(u8, name, "build_options")) {
|
||||
return @as(Reg, null_addr);
|
||||
}
|
||||
// `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` word. Mirrors the legacy `hookSetPostLinkCallback`.
|
||||
if (std.mem.eql(u8, name, "set_post_link_callback")) {
|
||||
if (args.len != 2) return self.failMsg("comptime set_post_link_callback: expected (self, cb)");
|
||||
const bc = self.build_config orelse
|
||||
return self.failMsg("comptime set_post_link_callback: no build config threaded into the VM");
|
||||
const fid = funcRefToId(frame.get(args[1].index())) orelse
|
||||
return self.failMsg("comptime set_post_link_callback: cb arg is not a function value");
|
||||
bc.post_link_callback_fn = fid;
|
||||
return @as(Reg, null_addr);
|
||||
}
|
||||
return null; // not a known compiler function → caller bails to legacy
|
||||
}
|
||||
|
||||
@@ -1428,7 +1558,7 @@ pub const Vm = struct {
|
||||
/// `Type` elements with no name) from flat memory into `TypeId`s.
|
||||
fn decodeTypeSlice(self: *Vm, table: *const types.TypeTable, slice_word: Reg, slice_ty: TypeId, out: *std.ArrayList(TypeId)) Error!void {
|
||||
if (slice_ty.isBuiltin() or table.get(slice_ty) != .slice)
|
||||
return self.failMsg("comptime define: tuple elements arg is not a slice");
|
||||
return self.failMsg("comptime define(): tuple elements arg is not a slice");
|
||||
const elem_ty = table.get(slice_ty).slice.element; // Type (.type_value)
|
||||
const len = try self.sliceLen(slice_word);
|
||||
const base = try self.sliceData(table, slice_word);
|
||||
@@ -1436,10 +1566,38 @@ pub const Vm = struct {
|
||||
for (0..@intCast(len)) |i| {
|
||||
const e = base + @as(Addr, @intCast(i)) * stride;
|
||||
const t: TypeId = @enumFromInt(@as(u32, @intCast(try self.readField(table, e, .type_value))));
|
||||
out.append(self.gpa, t) catch return self.failMsg("comptime define: out of memory");
|
||||
out.append(self.gpa, t) catch return self.failMsg("comptime define(): out of memory");
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the `TypeId` a reflection builtin (`type_name` / `type_is_unsigned`)
|
||||
/// queries, given the arg's IR type `aty` and its register word `w`. A
|
||||
/// `.type_value` word IS a `TypeId`; an Any box `{ tag@0, value@8 }` yields its
|
||||
/// tag (the boxed value's runtime type), unless tag == `type_value` — a boxed
|
||||
/// Type (the `type_of(x)` shape) whose real id sits in the value slot. The
|
||||
/// VM-native mirror of the legacy `Value.reflectTypeId`.
|
||||
fn reflectArgTypeId(self: *Vm, aty: TypeId, w: Reg) Error!TypeId {
|
||||
// A `TypeId` index is a u32; a word that doesn't fit is a garbage/mis-read
|
||||
// value (e.g. a wrong slice stride yielding an `Any` element at the wrong
|
||||
// offset — see 0522). Bail loudly instead of letting `@intCast` abort: the
|
||||
// VM must never crash.
|
||||
if (aty == .type_value) return TypeId.fromIndex(try self.typeIdxOf(w));
|
||||
if (aty == .any) {
|
||||
const tag = try self.machine.readWord(w, 8);
|
||||
if (tag == @as(u64, TypeId.type_value.index()))
|
||||
return TypeId.fromIndex(try self.typeIdxOf(try self.machine.readWord(w + 8, 8)));
|
||||
return TypeId.fromIndex(try self.typeIdxOf(tag));
|
||||
}
|
||||
return self.failMsg("comptime reflection builtin: arg is not a Type value or an Any box");
|
||||
}
|
||||
|
||||
/// Narrow a 64-bit word to a `u32` `TypeId` index, bailing (never crashing) when
|
||||
/// it doesn't fit — the tripwire for a mis-read reflection arg.
|
||||
fn typeIdxOf(self: *Vm, w: u64) Error!u32 {
|
||||
return std.math.cast(u32, w) orelse
|
||||
self.failMsg("comptime reflection builtin: type word out of TypeId range (mis-read arg?)");
|
||||
}
|
||||
|
||||
/// Service a comptime metatype `#builtin` (`meta.sx`'s `declare`/`define`)
|
||||
/// natively on flat memory, the VM-native mirror of the legacy
|
||||
/// `interp.execBuiltinInner` arms. Returns the result word, or `null` for a
|
||||
@@ -1458,27 +1616,27 @@ pub const Vm = struct {
|
||||
// define(handle, info) → complete the declared slot from a TypeInfo VALUE.
|
||||
.define => {
|
||||
const table = try self.requireTable();
|
||||
if (bi.args.len != 2) return self.failMsg("comptime define: expected (handle, info)");
|
||||
if (bi.args.len != 2) return self.failMsg("comptime define(): expected (handle, info)");
|
||||
const handle = try self.argTypeId(bi.args, frame, 0);
|
||||
// `info`: a TypeInfo tagged-union value `{ tag@0, payload@tag_size }`.
|
||||
const info_ty = try self.refTy(ref_types, bi.args[1]);
|
||||
if (info_ty.isBuiltin() or table.get(info_ty) != .tagged_union)
|
||||
return self.failMsg("comptime define: info arg is not a TypeInfo tagged union");
|
||||
return self.failMsg("comptime define(): info arg is not a TypeInfo tagged union");
|
||||
const tu = table.get(info_ty).tagged_union;
|
||||
// The `{ tag@0, payload@tag_size }` read below assumes a tag-headed
|
||||
// layout (true for `TypeInfo`); a `backing_type` union is laid out
|
||||
// differently, so bail rather than read the tag from the wrong bytes.
|
||||
if (tu.backing_type != null)
|
||||
return self.failMsg("comptime define: info is a backing_type tagged union (unexpected layout)");
|
||||
return self.failMsg("comptime define(): info is a backing_type tagged union (unexpected layout)");
|
||||
const info_addr = frame.get(bi.args[1].index());
|
||||
const tag_size: Addr = @intCast(table.typeSizeBytes(tu.tag_type));
|
||||
const tag = try self.machine.readWord(info_addr, tag_size);
|
||||
if (tag >= tu.fields.len) return self.failMsg("comptime define: TypeInfo tag out of range");
|
||||
if (tag >= tu.fields.len) return self.failMsg("comptime define(): TypeInfo tag out of range");
|
||||
// The active payload (EnumInfo / StructInfo / TupleInfo) is a struct
|
||||
// holding ONE slice field; its bytes live at `info_addr + tag_size`.
|
||||
const payload_ty = tu.fields[@intCast(tag)].ty;
|
||||
if (payload_ty.isBuiltin() or table.get(payload_ty) != .@"struct" or table.get(payload_ty).@"struct".fields.len != 1)
|
||||
return self.failMsg("comptime define: TypeInfo payload is not a single-slice info struct");
|
||||
return self.failMsg("comptime define(): TypeInfo payload is not a single-slice info struct");
|
||||
return try self.defineFromInfo(table, handle, @intCast(tag), payload_ty, info_addr + tag_size);
|
||||
},
|
||||
// type_name(x) → the type's name as a string. The arg is a Type value
|
||||
@@ -1488,20 +1646,19 @@ pub const Vm = struct {
|
||||
.type_name => {
|
||||
const table = try self.requireTable();
|
||||
if (bi.args.len < 1) return self.failMsg("comptime type_name: missing argument");
|
||||
const aty = try self.refTy(ref_types, bi.args[0]);
|
||||
const w = frame.get(bi.args[0].index());
|
||||
const tid: TypeId = blk: {
|
||||
if (aty == .type_value) break :blk TypeId.fromIndex(@intCast(w));
|
||||
if (aty == .any) {
|
||||
const tag = try self.machine.readWord(w, 8);
|
||||
if (tag == @as(u64, TypeId.type_value.index()))
|
||||
break :blk TypeId.fromIndex(@intCast(try self.machine.readWord(w + 8, 8)));
|
||||
break :blk TypeId.fromIndex(@intCast(tag));
|
||||
}
|
||||
return self.failMsg("comptime type_name: arg is not a Type value or an Any box");
|
||||
};
|
||||
const tid = try self.reflectArgTypeId(try self.refTy(ref_types, bi.args[0]), frame.get(bi.args[0].index()));
|
||||
return try self.makeStringValue(table, table.typeName(tid));
|
||||
},
|
||||
// type_is_unsigned(x) → is x's type an unsigned int? Resolves the TypeId
|
||||
// the same way as type_name (a `.type_value` word, or an Any box whose tag
|
||||
// IS the boxed value's type), then queries `isUnsignedInt`. Mirrors the
|
||||
// legacy `type_is_unsigned` builtin (`reflectTypeId` + `isUnsignedInt`).
|
||||
.type_is_unsigned => {
|
||||
const table = try self.requireTable();
|
||||
if (bi.args.len < 1) return self.failMsg("comptime type_is_unsigned: missing argument");
|
||||
const tid = try self.reflectArgTypeId(try self.refTy(ref_types, bi.args[0]), frame.get(bi.args[0].index()));
|
||||
return @as(Reg, @intFromBool(table.isUnsignedInt(tid)));
|
||||
},
|
||||
// type_info($T) → reflect a type INTO a TypeInfo VALUE (the inverse of
|
||||
// define's decode). The arg folded to a `const_type` (a `.type_value`
|
||||
// word = the source TypeId); build the value in flat memory.
|
||||
@@ -1528,8 +1685,8 @@ pub const Vm = struct {
|
||||
const tbl = @constCast(table);
|
||||
const cur = table.get(handle);
|
||||
const ident = nominalIdentOf(cur) orelse
|
||||
return self.failMsg("comptime define: handle is not a declare()'d nominal slot");
|
||||
if (cur != .tagged_union) return self.failMsg("comptime define: handle is not a declare()'d slot");
|
||||
return self.failMsg("comptime define(): handle is not a declare()'d nominal slot");
|
||||
if (cur != .tagged_union) return self.failMsg("comptime define(): handle is not a declare()'d slot");
|
||||
|
||||
// The info struct's single field is the member/element slice; read its
|
||||
// fat-pointer (embedded at field-0 offset within the info struct).
|
||||
@@ -1541,7 +1698,7 @@ pub const Vm = struct {
|
||||
var members = std.ArrayList(NamedMember).empty;
|
||||
defer members.deinit(self.gpa);
|
||||
try self.decodeMemberSlice(table, slice_word, slice_field_ty, &members);
|
||||
if (members.items.len == 0) return self.failMsg("comptime define: enum has no variants");
|
||||
if (members.items.len == 0) return self.failMsg("comptime define(): enum has no variants");
|
||||
// A FULLY payloadless variant set (every payload `void`) is an actual
|
||||
// `.@"enum"` (a kind change → `replaceKeyedInfo`); minting it as an
|
||||
// all-void tagged_union trips `verifySizes` at codegen (issue 0142).
|
||||
@@ -1551,16 +1708,16 @@ pub const Vm = struct {
|
||||
break;
|
||||
};
|
||||
if (all_void) {
|
||||
const names = self.gpa.alloc(types.StringId, members.items.len) catch return self.failMsg("comptime define: out of memory");
|
||||
const names = self.gpa.alloc(types.StringId, members.items.len) catch return self.failMsg("comptime define(): out of memory");
|
||||
for (members.items, 0..) |m, i| {
|
||||
for (names[0..i]) |prev| if (prev == m.name) return self.failMsg("comptime define: duplicate variant name");
|
||||
for (names[0..i]) |prev| if (prev == m.name) return self.failFmt("comptime define(): duplicate variant name '{s}'", .{tbl.getString(m.name)});
|
||||
names[i] = m.name;
|
||||
}
|
||||
tbl.replaceKeyedInfo(handle, .{ .@"enum" = .{ .name = ident.name, .variants = names, .nominal_id = ident.nominal_id } });
|
||||
} else {
|
||||
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define: out of memory");
|
||||
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define(): out of memory");
|
||||
for (members.items, 0..) |m, i| {
|
||||
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failMsg("comptime define: duplicate variant name");
|
||||
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failFmt("comptime define(): duplicate variant name '{s}'", .{tbl.getString(m.name)});
|
||||
flds[i] = .{ .name = m.name, .ty = m.ty };
|
||||
}
|
||||
// Name/id unchanged → still a tagged_union → stable key.
|
||||
@@ -1571,10 +1728,10 @@ pub const Vm = struct {
|
||||
var members = std.ArrayList(NamedMember).empty;
|
||||
defer members.deinit(self.gpa);
|
||||
try self.decodeMemberSlice(table, slice_word, slice_field_ty, &members);
|
||||
if (members.items.len == 0) return self.failMsg("comptime define: struct has no fields");
|
||||
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define: out of memory");
|
||||
if (members.items.len == 0) return self.failMsg("comptime define(): struct has no fields");
|
||||
const flds = self.gpa.alloc(types.TypeInfo.StructInfo.Field, members.items.len) catch return self.failMsg("comptime define(): out of memory");
|
||||
for (members.items, 0..) |m, i| {
|
||||
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failMsg("comptime define: duplicate field name");
|
||||
for (flds[0..i]) |prev| if (prev.name == m.name) return self.failFmt("comptime define(): duplicate field name '{s}'", .{tbl.getString(m.name)});
|
||||
flds[i] = .{ .name = m.name, .ty = m.ty };
|
||||
}
|
||||
// tagged_union slot → struct is a kind change → `replaceKeyedInfo`.
|
||||
@@ -1584,12 +1741,12 @@ pub const Vm = struct {
|
||||
var elems = std.ArrayList(TypeId).empty;
|
||||
defer elems.deinit(self.gpa);
|
||||
try self.decodeTypeSlice(table, slice_word, slice_field_ty, &elems);
|
||||
if (elems.items.len == 0) return self.failMsg("comptime define: tuple has no elements");
|
||||
const tys = self.gpa.alloc(TypeId, elems.items.len) catch return self.failMsg("comptime define: out of memory");
|
||||
if (elems.items.len == 0) return self.failMsg("comptime define(): tuple has no elements");
|
||||
const tys = self.gpa.alloc(TypeId, elems.items.len) catch return self.failMsg("comptime define(): out of memory");
|
||||
@memcpy(tys, elems.items);
|
||||
tbl.replaceKeyedInfo(handle, .{ .tuple = .{ .fields = tys, .names = null } });
|
||||
},
|
||||
else => return self.failMsg("comptime define: unknown TypeInfo variant"),
|
||||
else => return self.failMsg("comptime define(): unknown TypeInfo variant"),
|
||||
}
|
||||
return @as(Reg, handle.index());
|
||||
}
|
||||
|
||||
@@ -403,6 +403,12 @@ pub const LLVMEmitter = struct {
|
||||
// Pass 2: Emit function bodies
|
||||
for (self.ir_mod.functions.items, 0..) |func, i| {
|
||||
if (func.is_extern or func.blocks.items.len == 0) continue;
|
||||
// A compiler-domain function (`abi(.compiler)` with a body — a post-link
|
||||
// callback / compiler-side helper) runs ONLY in the comptime evaluator,
|
||||
// never in the shipped binary. Skip its body emission (like `is_extern`);
|
||||
// its only references are in comptime code, so DCE drops the leftover
|
||||
// declaration. See current/PLAN-COMPILER-VM.md (S3).
|
||||
if (func.is_compiler_domain) continue;
|
||||
self.emitFunction(&func, @intCast(i));
|
||||
}
|
||||
|
||||
@@ -875,7 +881,7 @@ pub const LLVMEmitter = struct {
|
||||
// runs entirely on the VM (no buffered output); anything it can't handle
|
||||
// (`print`, an unported op) bails → `null` → the legacy interpreter below.
|
||||
const vm_result: ?Value = if (self.comptime_flat)
|
||||
comptime_vm.tryEval(self.alloc, self.ir_mod, func_id)
|
||||
comptime_vm.tryEval(self.alloc, self.ir_mod, func_id, &self.build_config, self.import_sources)
|
||||
else
|
||||
null;
|
||||
if (self.comptime_flat and self.comptime_flat_trace) {
|
||||
@@ -982,7 +988,7 @@ pub const LLVMEmitter = struct {
|
||||
// bail / implicit-ctx) falls through to the legacy interpreter
|
||||
// below, which produces the identical result. Default OFF.
|
||||
const vm_result: ?Value = if (self.comptime_flat)
|
||||
comptime_vm.tryEval(self.alloc, self.ir_mod, func_id)
|
||||
comptime_vm.tryEval(self.alloc, self.ir_mod, func_id, &self.build_config, self.import_sources)
|
||||
else
|
||||
null;
|
||||
// Coverage trace (gated): report whether the VM handled this
|
||||
@@ -1384,8 +1390,14 @@ pub const LLVMEmitter = struct {
|
||||
c.LLVMAddAttributeAtIndex(llvm_func, param1_idx, sret_attr);
|
||||
}
|
||||
|
||||
// Set linkage
|
||||
switch (func.linkage) {
|
||||
// Set linkage. A compiler-domain function (`abi(.compiler)` with a body) is
|
||||
// declared here but its body is Pass-2-skipped (it runs only in the comptime
|
||||
// evaluator). An INTERNAL declaration with no body fails LLVM verification, so
|
||||
// give it EXTERNAL linkage — a valid "defined elsewhere" declaration that the
|
||||
// linker drops once DCE removes its (comptime-only) references.
|
||||
if (func.is_compiler_domain) {
|
||||
c.LLVMSetLinkage(llvm_func, c.LLVMExternalLinkage);
|
||||
} else switch (func.linkage) {
|
||||
.external => c.LLVMSetLinkage(llvm_func, c.LLVMExternalLinkage),
|
||||
.internal => c.LLVMSetLinkage(llvm_func, c.LLVMInternalLinkage),
|
||||
.private => c.LLVMSetLinkage(llvm_func, c.LLVMPrivateLinkage),
|
||||
|
||||
@@ -431,7 +431,6 @@ pub const BuiltinCall = struct {
|
||||
};
|
||||
|
||||
pub const BuiltinId = enum(u16) {
|
||||
out,
|
||||
sqrt,
|
||||
sin,
|
||||
cos,
|
||||
@@ -577,13 +576,21 @@ pub const Function = struct {
|
||||
/// `__sx_ctx` value to the args of a call. Extern decls and
|
||||
/// `abi(.c)` functions have it false.
|
||||
has_implicit_ctx: bool = false,
|
||||
/// True for a `fn abi(.zig) extern compiler` welded to the comptime
|
||||
/// `compiler` library. Such a function has no real symbol — the comptime
|
||||
/// interpreter dispatches it to its registered Zig handler
|
||||
/// (`compiler_lib.findFn`) instead of dlsym. Comptime-only; a runtime call
|
||||
/// has no backing symbol. See design/comptime-compiler-api.md.
|
||||
/// True for a bodiless `abi(.compiler)` compiler-API function (`intern`,
|
||||
/// `find_type`, `build_options`, …). Such a function has no real symbol — the
|
||||
/// comptime evaluator dispatches it to its registered Zig/VM handler
|
||||
/// (`compiler_lib.findFn` / `Vm.callCompilerFn`) instead of dlsym. Comptime-only;
|
||||
/// a runtime call has no backing symbol (the `emitCall` gate rejects it).
|
||||
compiler_welded: bool = false,
|
||||
|
||||
/// True for a BODIED `abi(.compiler)` function — a user compiler-domain function
|
||||
/// (e.g. a post-link callback). Unlike `compiler_welded`, it HAS an sx body the
|
||||
/// comptime evaluator runs; but it NEVER runs in the shipped binary, so the
|
||||
/// backend does not lower it (emit_llvm Pass 2 skips it, like `is_extern`). Its
|
||||
/// only references are in comptime code (the `#run` that registers it) → DCE
|
||||
/// drops the leftover declaration. See current/PLAN-COMPILER-VM.md (S3).
|
||||
is_compiler_domain: bool = false,
|
||||
|
||||
pub const Param = struct {
|
||||
name: StringId,
|
||||
ty: TypeId,
|
||||
|
||||
@@ -1940,13 +1940,6 @@ pub const Interpreter = struct {
|
||||
|
||||
fn execBuiltinInner(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame) InterpError!ExecResult {
|
||||
switch (bi.builtin) {
|
||||
.out => {
|
||||
const str_val = frame.getRef(bi.args[0]);
|
||||
if (str_val.asString(self)) |s| {
|
||||
self.output.appendSlice(self.alloc, s) catch {};
|
||||
}
|
||||
return .{ .value = .void_val };
|
||||
},
|
||||
.size_of => {
|
||||
// Return a default size (8 bytes for most types)
|
||||
return .{ .value = .{ .int = 8 } };
|
||||
|
||||
@@ -1296,8 +1296,8 @@ pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId {
|
||||
|
||||
pub fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId {
|
||||
const builtins = .{
|
||||
// Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin
|
||||
.{ "out", inst_mod.BuiltinId.out },
|
||||
// Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin.
|
||||
// "out" is NOT here either — it's a plain sx function (libc `write`), not a builtin.
|
||||
.{ "sqrt", inst_mod.BuiltinId.sqrt },
|
||||
.{ "sin", inst_mod.BuiltinId.sin },
|
||||
.{ "cos", inst_mod.BuiltinId.cos },
|
||||
|
||||
@@ -529,7 +529,7 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty
|
||||
const comptime_flat = build_opts.comptime_flat or std.c.getenv("SX_COMPTIME_FLAT") != null or
|
||||
build_opts.comptime_flat_strict or std.c.getenv("SX_COMPTIME_FLAT_STRICT") != null;
|
||||
const vm_result: ?interp_mod.Value = if (comptime_flat)
|
||||
comptime_vm.tryEval(self.alloc, self.module, func_id)
|
||||
comptime_vm.tryEval(self.alloc, self.module, func_id, null, null)
|
||||
else
|
||||
null;
|
||||
if (comptime_flat and std.c.getenv("SX_COMPTIME_FLAT_TRACE") != null) {
|
||||
@@ -543,11 +543,16 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty
|
||||
return checkComptimeTypeResult(self, tid_vm, span);
|
||||
}
|
||||
|
||||
// Strict mode: NO fallback — a VM bail is a build-gating failure naming the
|
||||
// reason (the interp-retirement enumeration gate). Returning null leaves the
|
||||
// type unresolved → a downstream diagnostic fails the build.
|
||||
// Strict mode: NO fallback — render the VM's bail reason as the SAME
|
||||
// build-gating diagnostic the non-strict legacy path emits below (the VM and
|
||||
// legacy set identical detail strings, e.g. "comptime define(): duplicate
|
||||
// variant name 'x'"), so a comptime type-construction failure (1179/1180)
|
||||
// produces its proper user diagnostic with no legacy interp in the loop — the
|
||||
// 4B step toward deleting the fallback. (4B / VM-native diagnostics.)
|
||||
if (build_opts.comptime_flat_strict or std.c.getenv("SX_COMPTIME_FLAT_STRICT") != null) {
|
||||
std.debug.print("error: comptime type-fn bailed on the VM (strict, no fallback): {s}\n", .{comptime_vm.last_bail_reason orelse "<unknown>"});
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, span, "comptime type construction failed: {s}", .{comptime_vm.last_bail_reason orelse "<unknown>"});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -499,6 +499,12 @@ pub fn detectContextDecl(decls: []const *const Node) bool {
|
||||
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
|
||||
if (!self.implicit_ctx_enabled) return false;
|
||||
if (fd.abi == .c) return false;
|
||||
// A BODILESS `abi(.compiler)` decl (compiler-API surface) is dispatched by name
|
||||
// to a Zig/VM handler with exactly the declared args; an implicit `__sx_ctx`
|
||||
// prepend would shift every arg (breaking the handler's arity check). No sx
|
||||
// context, like an extern import. (A BODIED `abi(.compiler)` function is a real
|
||||
// sx function the VM runs — it gets the normal implicit-ctx treatment.)
|
||||
if (fnIsBodilessCompiler(fd)) return false;
|
||||
// `extern` imports and `export` defines are external C symbols —
|
||||
// C ABI, no sx context (Phase 2, gap iv).
|
||||
if (fd.extern_export != .none) return false;
|
||||
@@ -2229,7 +2235,13 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
||||
// declareExtern routing below; the optional `extern LIB "csym"` lib/rename
|
||||
// axis is extern_lib/extern_name. (`export` defines take the beginFunction
|
||||
// path, not here.) The `#import c` auto-synthesis also produces this shape.
|
||||
const is_extern_decl = fd.extern_export == .extern_;
|
||||
// A bodiless `abi(.compiler)` decl (the compiler-API surface) has no runtime
|
||||
// body — the Zig/VM handler is the impl — so it lowers exactly like an `extern`
|
||||
// import (declared, never defined; the comptime evaluator dispatches it via
|
||||
// `compiler_welded`). A BODIED `abi(.compiler)` function (a compiler-domain
|
||||
// callback) is NOT extern — it has a body the VM evaluates — and is handled
|
||||
// below (`is_compiler_domain` + `is_comptime`, body lowered, emit-skipped).
|
||||
const is_extern_decl = fd.extern_export == .extern_ or fnIsBodilessCompiler(fd);
|
||||
var is_variadic = false;
|
||||
var effective_params = fd.params;
|
||||
// A lib-less C-import with a C-variadic `...` tail: drop the trailing slice
|
||||
@@ -2298,6 +2310,15 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
||||
func.is_variadic = is_variadic;
|
||||
func.has_implicit_ctx = wants_ctx;
|
||||
if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true;
|
||||
// A BODIED `abi(.compiler)` function is a user compiler-domain function (e.g. a
|
||||
// post-link callback): the VM runs its sx body, but it NEVER runs in the binary
|
||||
// so the backend skips it (emit_llvm Pass 2). Flag `is_compiler_domain` (the
|
||||
// emit-skip) + `is_comptime` (so any compiler-API calls inside it are permitted
|
||||
// by the `emitCall` gate, and its dead LLVM decl is treated like a #run wrapper).
|
||||
if (fd.abi == .compiler and !fnIsBodilessCompiler(fd)) {
|
||||
func.is_compiler_domain = true;
|
||||
func.is_comptime = true;
|
||||
}
|
||||
// A non-generic `-> Type` builder is a comptime type constructor — only ever
|
||||
// evaluated at lowering time (`runComptimeTypeFunc`) to mint a type, never
|
||||
// called at runtime. Flag it `is_comptime` so its emitted body is dead: the
|
||||
@@ -2309,20 +2330,25 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
||||
self.fn_decl_fids.put(fd, fid) catch {};
|
||||
}
|
||||
|
||||
/// A `fn abi(.zig) extern <lib>` binds the comptime `compiler` library. Validate
|
||||
/// it (the bound lib must be `compiler`; the name must be on the function-export
|
||||
/// list) and return whether it is a welded compiler function — the interpreter
|
||||
/// dispatches such a call to its registered Zig handler instead of dlsym. Any
|
||||
/// failure is a build-gating `.err` (never a silent fall-through to dlsym).
|
||||
/// A BODILESS `abi(.compiler)` decl (ends in `;`, no sx body) — the compiler-API
|
||||
/// surface (`intern`/`find_type`/`build_options`/…), whose Zig/VM handler is the
|
||||
/// impl. Distinguished from a BODIED `abi(.compiler)` function (a user
|
||||
/// compiler-domain function, e.g. a post-link callback) by its synthesized
|
||||
/// empty-block body. The two lower differently: bodiless = declared-not-defined
|
||||
/// (extern-like); bodied = body lowered for VM eval but emit-skipped (S3).
|
||||
fn fnIsBodilessCompiler(fd: *const ast.FnDecl) bool {
|
||||
return fd.abi == .compiler and fd.body.data == .block and fd.body.data.block.stmts.len == 0;
|
||||
}
|
||||
|
||||
/// A bodiless `abi(.compiler)` decl is a compiler-API function: the comptime
|
||||
/// evaluator dispatches the call to its registered Zig/VM handler instead of dlsym.
|
||||
/// The ABI alone marks it (no `extern <lib>`, no fake `#library`). Validate the name
|
||||
/// is on the function-export list; failure is a build-gating `.err` (never a silent
|
||||
/// fall-through to dlsym).
|
||||
fn weldedCompilerFn(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) bool {
|
||||
if (fd.abi != .zig) return false;
|
||||
const diags = self.diagnostics;
|
||||
if (fd.extern_lib == null or !std.mem.eql(u8, fd.extern_lib.?, compiler_lib.lib_name)) {
|
||||
if (diags) |d| d.addFmt(.err, fd.name_span, "abi(.zig) function '{s}' must bind the compiler library — write `extern {s}`", .{ name, compiler_lib.lib_name });
|
||||
return false;
|
||||
}
|
||||
if (!fnIsBodilessCompiler(fd)) return false;
|
||||
if (compiler_lib.findFn(name) == null) {
|
||||
if (diags) |d| d.addFmt(.err, fd.name_span, "'{s}' is not a function exported by the '{s}' library", .{ name, compiler_lib.lib_name });
|
||||
if (self.diagnostics) |d| d.addFmt(.err, fd.name_span, "'{s}' is not a function exported by the compiler", .{name});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -2570,8 +2596,11 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId
|
||||
|
||||
// `extern` imports are pure declarations — never promote the stub to a real
|
||||
// function or lower the (empty placeholder) body. Mirrors the declare-only
|
||||
// handling in lowerFunction / lazyLowerFunction.
|
||||
if (fd.extern_export == .extern_) return;
|
||||
// handling in lowerFunction / lazyLowerFunction. A bodiless `abi(.compiler)`
|
||||
// decl (the compiler-API surface) is declare-only too — the Zig/VM handler is
|
||||
// the impl. A BODIED `abi(.compiler)` function DOES lower its body (for VM eval);
|
||||
// it is emit-skipped later via `is_compiler_domain`, not here.
|
||||
if (fd.extern_export == .extern_ or fnIsBodilessCompiler(fd)) return;
|
||||
|
||||
const ret_ty = self.resolveReturnType(fd);
|
||||
|
||||
@@ -2681,7 +2710,13 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i
|
||||
|
||||
// Check if the function body is a builtin or extern declaration (no body
|
||||
// needed). `extern` imports are declare-only too (empty placeholder body).
|
||||
if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr or fd.extern_export == .extern_) {
|
||||
// A bodiless `abi(.compiler)` decl (the compiler-API surface) is likewise
|
||||
// declare-only — its Zig/VM handler is the impl. A BODIED `abi(.compiler)`
|
||||
// function DOES need its body lowered for VM eval (emit-skipped later via
|
||||
// `is_compiler_domain`), so it falls through to normal lowering below.
|
||||
if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr or
|
||||
fd.extern_export == .extern_ or fnIsBodilessCompiler(fd))
|
||||
{
|
||||
// Already declared by scanDecls/declareFunction (which handles #extern renames)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,12 +228,13 @@ pub const TypeResolver = struct {
|
||||
const cc: types.TypeInfo.CallConv = switch (ft.abi) {
|
||||
.default => .default,
|
||||
.c => .c,
|
||||
// `.zig` (compiler-lib weld) and `.pure` (naked asm) are
|
||||
// `.compiler` (compiler-domain fn) and `.pure` (naked asm) are
|
||||
// decl-level ABIs with no function-pointer-type calling
|
||||
// convention of their own; the IR function-type CC models only
|
||||
// sx-default vs C. Neither occurs in a function-TYPE position in
|
||||
// current usage — treated as sx-default here.
|
||||
.zig, .pure => .default,
|
||||
// sx-default vs C. An `abi(.compiler)` function-TYPE param marks
|
||||
// the bound function compiler-domain (handled at the call/bind
|
||||
// site, not here) — its CC is still sx-default.
|
||||
.compiler, .pure => .default,
|
||||
};
|
||||
break :blk table.functionTypeCC(param_ids.items, ret_ty, cc);
|
||||
},
|
||||
|
||||
@@ -80,20 +80,18 @@ test "parser: comptime type-metaprogramming surface parses" {
|
||||
}
|
||||
|
||||
// Lock: the `compiler`-library binding surface PARSES — `name :: #library "x";`
|
||||
// (already supported) plus the new postfix `abi(.zig)` annotation (in the slot
|
||||
// before `extern`) followed by the library handle, on a function declaration. The
|
||||
// AST must carry the binding: `abi == .zig`, `extern_export == .extern_`, and the
|
||||
// library handle in `extern_lib`. No semantics yet — this is the first testable
|
||||
// sub-step of Phase 1 (parse only).
|
||||
test "parser: abi(.zig) extern <lib> binding parses on a fn decl" {
|
||||
// (already supported) plus the postfix `abi(.compiler)` annotation, marking a
|
||||
// compiler-domain / compiler-API function — no `extern`, no fake `#library`. The
|
||||
// AST must carry `abi == .compiler`, `extern_export == .none`, `extern_lib ==
|
||||
// null`, and a synthesized empty-block (bodiless) body.
|
||||
test "parser: abi(.compiler) binding parses on a bodiless fn decl" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const src =
|
||||
\\compiler :: #library "compiler";
|
||||
\\text_of :: (id: StringId) -> string abi(.zig) extern compiler;
|
||||
\\intern :: (s: string) -> StringId abi(.zig) extern compiler;
|
||||
\\text_of :: (id: StringId) -> string abi(.compiler);
|
||||
\\intern :: (s: string) -> StringId abi(.compiler);
|
||||
\\
|
||||
;
|
||||
var parser = Parser.init(alloc, src);
|
||||
@@ -101,14 +99,10 @@ test "parser: abi(.zig) extern <lib> binding parses on a fn decl" {
|
||||
|
||||
try std.testing.expect(root.data == .root);
|
||||
const decls = root.data.root.decls;
|
||||
try std.testing.expectEqual(@as(usize, 3), decls.len);
|
||||
try std.testing.expectEqual(@as(usize, 2), decls.len);
|
||||
|
||||
// The `#library` decl still parses to a `library_decl` node carrying the name.
|
||||
try std.testing.expect(decls[0].data == .library_decl);
|
||||
try std.testing.expectEqualStrings("compiler", decls[0].data.library_decl.name);
|
||||
try std.testing.expectEqualStrings("compiler", decls[0].data.library_decl.lib_name);
|
||||
|
||||
// The two `abi(.zig) extern compiler` fns: `.fn_decl` with the binding fields set.
|
||||
// The two `abi(.compiler)` fns: `.fn_decl` with the compiler-domain ABI set,
|
||||
// NO extern linkage, NO bound library.
|
||||
for ([_][]const u8{ "text_of", "intern" }) |bn| {
|
||||
var found: ?*const Node = null;
|
||||
for (decls) |d| {
|
||||
@@ -119,11 +113,10 @@ test "parser: abi(.zig) extern <lib> binding parses on a fn decl" {
|
||||
const d = found orelse return error.MissingDecl;
|
||||
try std.testing.expect(d.data == .fn_decl);
|
||||
const fd = d.data.fn_decl;
|
||||
try std.testing.expectEqual(ast.ABI.zig, fd.abi);
|
||||
try std.testing.expectEqual(ast.ExternExportModifier.extern_, fd.extern_export);
|
||||
try std.testing.expect(fd.extern_lib != null);
|
||||
try std.testing.expectEqualStrings("compiler", fd.extern_lib.?);
|
||||
// Bodyless extern import: synthesized empty block, no `#builtin`/`#compiler`.
|
||||
try std.testing.expectEqual(ast.ABI.compiler, fd.abi);
|
||||
try std.testing.expectEqual(ast.ExternExportModifier.none, fd.extern_export);
|
||||
try std.testing.expect(fd.extern_lib == null);
|
||||
// Bodyless compiler-domain decl: synthesized empty block, no `#builtin`/`#compiler`.
|
||||
try std.testing.expect(fd.body.data == .block);
|
||||
}
|
||||
}
|
||||
@@ -173,18 +166,19 @@ test "parser: abi(.c) and abi(.pure) parse standalone" {
|
||||
try std.testing.expectEqual(ast.ABI.pure, decls[1].data.fn_decl.abi);
|
||||
}
|
||||
|
||||
// Lock: the `compiler`-library binding PARSES on a STRUCT decl — `Name :: struct
|
||||
// abi(.zig) extern <lib> { … }`. The AST struct_decl must carry `abi == .zig` and
|
||||
// the library handle in `extern_lib`, with the field list intact. No semantics
|
||||
// yet (parse-only) — this is the second testable sub-step of Phase 1.
|
||||
test "parser: abi(.zig) extern <lib> binding parses on a struct decl" {
|
||||
// Lock: the postfix `abi(...)` slot PARSES on a STRUCT decl — `Name :: struct
|
||||
// abi(.compiler) extern <lib> { … }`. The AST struct_decl carries the abi + the
|
||||
// library handle in `extern_lib`, with the field list intact. Parse-only — the
|
||||
// struct-weld semantics were stripped (compiler-API types are VM-native now); this
|
||||
// just locks that the annotation slot still parses without perturbing fields.
|
||||
test "parser: abi(...) extern <lib> annotation parses on a struct decl" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const src =
|
||||
\\compiler :: #library "compiler";
|
||||
\\Field :: struct abi(.zig) extern compiler { name: StringId; ty: Type; }
|
||||
\\Field :: struct abi(.compiler) extern compiler { name: StringId; ty: Type; }
|
||||
\\
|
||||
;
|
||||
var parser = Parser.init(alloc, src);
|
||||
@@ -194,7 +188,7 @@ test "parser: abi(.zig) extern <lib> binding parses on a struct decl" {
|
||||
|
||||
try std.testing.expect(decls[1].data == .struct_decl);
|
||||
const sd = decls[1].data.struct_decl;
|
||||
try std.testing.expectEqual(ast.ABI.zig, sd.abi);
|
||||
try std.testing.expectEqual(ast.ABI.compiler, sd.abi);
|
||||
try std.testing.expect(sd.extern_lib != null);
|
||||
try std.testing.expectEqualStrings("compiler", sd.extern_lib.?);
|
||||
// Field list survives the binding annotation.
|
||||
|
||||
@@ -1998,6 +1998,17 @@ pub const Parser = struct {
|
||||
try self.expect(.semicolon);
|
||||
const stmts = try self.allocator.alloc(*Node, 0);
|
||||
break :blk try self.createNode(semi_start, .{ .block = .{ .stmts = stmts, .produces_value = false } });
|
||||
} else if (abi == .compiler and self.current.tag == .semicolon) blk: {
|
||||
// A bodiless `abi(.compiler)` decl: the compiler-API surface
|
||||
// (`intern`/`find_type`/`build_options`/…). It has no sx body — the
|
||||
// Zig/VM handler IS the implementation — so synthesize the empty-block
|
||||
// placeholder, exactly like an `extern` import. (A BODIED
|
||||
// `abi(.compiler)` function — e.g. a post-link callback — keeps its
|
||||
// `{ … }` and falls through to `parseBlock` below.)
|
||||
const semi_start = self.current.loc.start;
|
||||
self.advance();
|
||||
const stmts = try self.allocator.alloc(*Node, 0);
|
||||
break :blk try self.createNode(semi_start, .{ .block = .{ .stmts = stmts, .produces_value = false } });
|
||||
} else if (self.current.tag == .hash_builtin) blk: {
|
||||
const bi_start = self.current.loc.start;
|
||||
self.advance();
|
||||
@@ -3832,16 +3843,16 @@ pub const Parser = struct {
|
||||
try self.expect(.l_paren);
|
||||
try self.expect(.dot);
|
||||
if (self.current.tag != .identifier)
|
||||
return self.fail("expected ABI name ('.c', '.zig', or '.pure') after '.'");
|
||||
return self.fail("expected ABI name ('.c', '.compiler', or '.pure') after '.'");
|
||||
const abi_name = self.tokenSlice(self.current);
|
||||
const abi: ast.ABI = if (std.mem.eql(u8, abi_name, "c"))
|
||||
.c
|
||||
else if (std.mem.eql(u8, abi_name, "zig"))
|
||||
.zig
|
||||
else if (std.mem.eql(u8, abi_name, "compiler"))
|
||||
.compiler
|
||||
else if (std.mem.eql(u8, abi_name, "pure"))
|
||||
.pure
|
||||
else
|
||||
return self.fail("unknown ABI (expected '.c', '.zig', or '.pure')");
|
||||
return self.fail("unknown ABI (expected '.c', '.compiler', or '.pure')");
|
||||
self.advance();
|
||||
try self.expect(.r_paren);
|
||||
return abi;
|
||||
|
||||
@@ -1001,7 +1001,7 @@ pub const Analyzer = struct {
|
||||
}
|
||||
|
||||
// Built-in names that aren't declared in source
|
||||
const builtins = [_][]const u8{ "io", "true", "false", "cast", "closure", "out", "size_of", "align_of", "malloc", "free", "memcpy", "memset", "context" };
|
||||
const builtins = [_][]const u8{ "io", "true", "false", "cast", "closure", "size_of", "align_of", "malloc", "free", "memcpy", "memset", "context" };
|
||||
for (builtins) |b| {
|
||||
if (std.mem.eql(u8, name, b)) return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user