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:
agra
2026-06-19 07:04:10 +03:00
parent fdc4ee2331
commit 2060373c16
80 changed files with 12684 additions and 11922 deletions

View File

@@ -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] [;|{…}];`

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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" {

View File

@@ -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());
}

View File

@@ -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),

View File

@@ -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,

View File

@@ -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 } };

View File

@@ -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 },

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
},

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;
}