P5.2 metadata queries: c_object_paths / link_libraries on the VM

Two abi(.compiler) build-pipeline primitives the sx driver will pass to link:
- c_object_paths() -> List(string)  (#import c companion objects)
- link_libraries() -> List(string)  (#library names)

They live in a new stdlib home library/modules/std/build.sx and are serviced
by comptime_vm.callCompilerFn reading two new BuildConfig fields that main.zig
forwards before the post-link callback. New reusable VM helper makeStringList
builds a List(string) in flat memory from the call's result type offsets
(target-aware); invoke/callCompilerFn now thread ins.ty for that. Legacy
handlers bail loudly (VM-only by nature — post-link; List(string) isn't
faithfully buildable in the legacy Value model, 0141).

Smoke test examples/1662-platform-build-pipeline-queries (AOT + a 1-line C
#source → one object): a post-link callback verifies the VM-built list is
well-formed; build exit 0 only if so (negative-probe confirmed a real guard).

emit_object + link (the actions) deferred to P5.2b — they replace the Zig
driver's auto-emit/auto-link and need a host-installed callback vtable.

703/0 both gates.
This commit is contained in:
agra
2026-06-19 07:42:27 +03:00
parent 7cba33ea6d
commit 44dfdcddf9
13 changed files with 191 additions and 11 deletions

View File

@@ -57,6 +57,15 @@ pub const BuildConfig = struct {
/// sx bundler can switch on iOS vs. macOS vs. simulator.
target_triple: ?[]const u8 = null,
/// C companion object files (`#import c { #source ... }`, compiled to `.o`)
/// and `#library` link names, forwarded by main.zig before the post-link
/// callback so the sx-driven build pipeline (Phase 5) can read them via the
/// `c_object_paths()` / `link_libraries()` compiler primitives and pass them
/// to `link`. Slices reference compiler-owned memory that outlives the
/// callback.
c_object_paths: []const []const u8 = &.{},
link_libraries: []const []const u8 = &.{},
/// Frameworks the binary links against (`-framework` names) and
/// the search paths to look them up in (`-F` directories), forwarded
/// from the link step so the sx bundler can embed them into

View File

@@ -61,8 +61,25 @@ pub const bound_fns = [_]BoundFn{
// ── BuildOptions (migrated off `#compiler` onto `abi(.compiler)`) ─────────
.{ .sx_name = "build_options", .handler = handleBuildOptions },
.{ .sx_name = "set_post_link_callback", .handler = handleSetPostLinkCallback },
// ── build-pipeline metadata queries (Phase 5.2) ──────────────────────────
// VM-only: the post-link callback that calls these always runs on the VM
// (`core.invokeByFuncId`), so `comptime_vm.callCompilerFn` services them and
// these legacy handlers are never reached (they bail loudly rather than
// fabricate a silent empty List). Registered here only so `weldedCompilerFn`
// recognizes the names as compiler-API functions.
.{ .sx_name = "c_object_paths", .handler = handleBuildPipelineQuery },
.{ .sx_name = "link_libraries", .handler = handleBuildPipelineQuery },
};
/// Legacy-path stub for the Phase 5 build-pipeline metadata queries — see the
/// `bound_fns` comment. These return a `List(string)` the legacy `Value` model
/// can't faithfully build (issue 0141), and the only caller (the post-link
/// callback) runs on the VM, so bail loudly here instead of guessing.
fn handleBuildPipelineQuery(_: *Interpreter, _: []const Value) InterpError!Value {
Interpreter.last_bail_detail = "build-pipeline query (c_object_paths/link_libraries) is VM-only (post-link); not available on the legacy interpreter";
return error.CannotEvalComptime;
}
// Kind codes accepted by `register_type` — mirror `TypeTable.kindCode`. An
// enum-like type is minted as a `tagged_union` (the general payload-carrying
// form, as `define` does), so both 2 (`enum`) and 3 (`tagged_union`) are taken.

View File

@@ -909,7 +909,7 @@ pub const Vm = struct {
// ── Calls ───────────────────────────────────────────
// Direct call: resolve the static callee `FuncId` and dispatch.
.call => |c| return .{ .value = try self.invoke(c.callee, c.args, frame, ref_types) },
.call => |c| return .{ .value = try self.invoke(c.callee, c.args, frame, ref_types, ins.ty) },
// Indirect call: the callee is a `func_ref` value (its `FuncId.index()`
// as a word) in a register — e.g. an allocator protocol's `alloc_fn`.
// A null (0) function pointer can't be dispatched → bail.
@@ -919,7 +919,7 @@ pub const Vm = struct {
self.detail = "comptime VM: call_indirect through a null function pointer";
return error.Unsupported;
};
return .{ .value = try self.invoke(fid, ci.args, frame, ref_types) };
return .{ .value = try self.invoke(fid, ci.args, frame, ref_types, ins.ty) };
},
// ── Globals / function values ───────────────────────
@@ -1135,7 +1135,7 @@ pub const Vm = struct {
/// extern/bodyless callee routes to the native libc memory builtins (else
/// bails); a normal callee runs on the VM. Aggregate args pass as their Addr
/// over the shared flat memory (no copy).
fn invoke(self: *Vm, fid: inst_mod.FuncId, args: []const Ref, frame: *Frame, ref_types: []const TypeId) Error!Reg {
fn invoke(self: *Vm, fid: inst_mod.FuncId, args: []const Ref, frame: *Frame, ref_types: []const TypeId, result_ty: TypeId) Error!Reg {
const module = self.module orelse return self.failMsg("comptime VM: call needs a module (not provided)");
if (fid.index() >= module.functions.items.len) return self.failMsg("comptime VM: call to an out-of-range function id");
const callee = module.getFunction(fid);
@@ -1149,7 +1149,7 @@ pub const Vm = struct {
// the comptime compiler-API, serviced natively on flat memory (Phase 3
// seed). The `compiler_welded` flag is the safety boundary.
if (callee.compiler_welded) {
if (try self.callCompilerFn(name, args, frame, ref_types)) |r| return r;
if (try self.callCompilerFn(name, args, frame, ref_types, result_ty)) |r| return r;
}
// General host-FFI escape: any other extern resolves via dlsym and is
// dispatched through the host_ffi trampolines. Because `Addr` is a real
@@ -1336,7 +1336,7 @@ pub const Vm = struct {
return @enumFromInt(try self.argHandle(args, frame, i));
}
fn callCompilerFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame, ref_types: []const TypeId) Error!?Reg {
fn callCompilerFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame, ref_types: []const TypeId, result_ty: TypeId) Error!?Reg {
const table = try self.requireTable();
if (std.mem.eql(u8, name, "intern")) {
if (args.len != 1) return self.failMsg("comptime intern: expected one string arg");
@@ -1452,6 +1452,23 @@ pub const Vm = struct {
bc.post_link_callback_fn = fid;
return @as(Reg, null_addr);
}
// ── build-pipeline metadata queries (Phase 5.2) ─────────────────────
// Read-only: the compiler answers them from the `BuildConfig` `main.zig`
// forwards before the post-link callback runs. Each builds a fresh
// `List(string)` in flat memory (the result type drives its layout) — no
// driver action, so they're pure data even in the sx-driven end state.
if (std.mem.eql(u8, name, "c_object_paths")) {
if (args.len != 0) return self.failMsg("comptime c_object_paths: expected no args");
const bc = self.build_config orelse
return self.failMsg("comptime c_object_paths: no build config threaded into the VM");
return try self.makeStringList(table, result_ty, bc.c_object_paths);
}
if (std.mem.eql(u8, name, "link_libraries")) {
if (args.len != 0) return self.failMsg("comptime link_libraries: expected no args");
const bc = self.build_config orelse
return self.failMsg("comptime link_libraries: no build config threaded into the VM");
return try self.makeStringList(table, result_ty, bc.link_libraries);
}
return null; // not a known compiler function → caller bails to legacy
}
@@ -2199,4 +2216,35 @@ pub const Vm = struct {
fn sliceData(self: *Vm, table: *const types.TypeTable, base: Addr) Error!Addr {
return self.machine.readWord(base, table.pointer_size);
}
/// Build a `List(string)` aggregate in flat memory from host strings and
/// return its Addr (the VM's aggregate value IS its address). `list_ty` is
/// the result type of the calling primitive (`List(string)`); its field
/// offsets/types drive the layout (target-aware via the table), so this works
/// for any `{ items: [*]string, len: i64, cap: i64 }`-shaped struct. Used by
/// the metadata-query compiler primitives (`c_object_paths`/`link_libraries`).
fn makeStringList(self: *Vm, table: *const types.TypeTable, list_ty: TypeId, items: []const []const u8) Error!Reg {
if (list_ty.isBuiltin() or table.get(list_ty) != .@"struct")
return self.failMsg("comptime List builder: result type is not a List struct");
const str_size = table.typeSizeBytes(.string);
// Backing array of `items.len` `string` fat pointers (null when empty —
// the List's `items` is then a null `[*]string`, matching `len`/`cap` 0).
const backing: Addr = if (items.len == 0) null_addr else self.machine.allocBytes(items.len * str_size, 8);
for (items, 0..) |s, i| {
try self.writeField(table, backing + i * str_size, .string, try self.makeStringValue(table, s));
}
// The List struct: field 0 = items ([*]string), 1 = len (i64), 2 = cap (i64).
const addr = self.machine.allocBytes(table.typeSizeBytes(list_ty), 8);
const items_fty = table.memberType(list_ty, 0) orelse
return self.failMsg("comptime List builder: result type has no items field");
const len_fty = table.memberType(list_ty, 1) orelse
return self.failMsg("comptime List builder: result type has no len field");
const cap_fty = table.memberType(list_ty, 2) orelse
return self.failMsg("comptime List builder: result type has no cap field");
const n: Reg = @bitCast(@as(i64, @intCast(items.len)));
try self.writeField(table, addr + fieldOffset(table, list_ty, 0), items_fty, backing);
try self.writeField(table, addr + fieldOffset(table, list_ty, 1), len_fty, n);
try self.writeField(table, addr + fieldOffset(table, list_ty, 2), cap_fty, n);
return addr;
}
};

View File

@@ -717,6 +717,11 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
if (merged_config.triple) |t| e.build_config.target_triple = std.mem.span(t);
e.build_config.target_frameworks = fws;
e.build_config.target_framework_paths = merged_config.framework_paths;
// Phase 5: the sx-driven build pipeline reads these via the
// `c_object_paths()` / `link_libraries()` compiler primitives. Both
// slices reference compileWithTimer locals that outlive the callback.
e.build_config.c_object_paths = c_obj_paths;
e.build_config.link_libraries = libs;
// Android-specific bundling state.
if (e.build_config.manifest_path == null) e.build_config.manifest_path = merged_config.manifest_path;
if (e.build_config.keystore_path == null) e.build_config.keystore_path = merged_config.keystore_path;