P5.2b: link() build-pipeline action on the VM via a host vtable

The one genuine action primitive: link(objects, output, libraries, frameworks,
flags, target) in library/modules/std/build.sx. Per the user decision to drop
fallibility from the build callback, link is plain VOID — a link failure bails
on the VM (hard build error), no -> ! / failable-tuple needed.

comptime_vm.zig can't depend on the driver (core/main/target), so link
dispatches through a new compiler_hooks.BuildHooks { ctx, link } vtable that
main.zig installs into BuildConfig.build_hooks before the post-link callback.
The driver side is main.LinkHooksCtx (unions explicit + CLI link flags, calls
target.link). New VM readers readStringList / readStringArg (inverse of
makeStringList) decode the List(string)/string args from flat memory.

Smoke test examples/1663-platform-build-pipeline-link (AOT): a post-link
callback re-links the build's own objects (c_object_paths + emit_object) into a
temp output via sx link — the relinked binary is a functional executable that
runs. Negative-probe verified (bad path -> ld fails -> ComptimeVmBail -> build
exit 1). The Zig driver still auto-links; removing that is P5.4.

704/0 both gates.
This commit is contained in:
agra
2026-06-19 08:11:36 +03:00
parent 83de0fa04d
commit f7362ee013
12 changed files with 206 additions and 14 deletions

View File

@@ -568,6 +568,40 @@ fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, out
timer.printAll();
}
/// Driver-side adapter behind the `link` build-pipeline primitive (Phase 5). The
/// comptime VM can't link itself (it must not depend on `target`), so it
/// dispatches `link(...)` through a `BuildHooks` whose `ctx` is one of these. The
/// VM passes the full object list; `target.link` takes (first object, rest), but
/// treats both as plain inputs, so the split is immaterial.
const LinkHooksCtx = struct {
allocator: std.mem.Allocator,
io: std.Io,
base_config: sx.target.TargetConfig,
has_jni_main: bool,
fn link(
ctx_opaque: *anyopaque,
objects: []const []const u8,
output: []const u8,
libraries: []const []const u8,
frameworks: []const []const u8,
flags: []const []const u8,
target: []const u8,
) anyerror!void {
_ = target; // the triple is already encoded in base_config (CLI-derived);
// explicit-triple reconciliation is a P5.4 concern when sx owns the config.
const self: *LinkHooksCtx = @ptrCast(@alignCast(ctx_opaque));
if (objects.len == 0) return error.NoObjects;
var cfg = self.base_config;
// Union the explicit `flags` with the CLI-derived ones (don't drop either).
var all_flags: std.ArrayList([]const u8) = .empty;
for (self.base_config.extra_link_flags) |f| try all_flags.append(self.allocator, f);
for (flags) |f| try all_flags.append(self.allocator, f);
cfg.extra_link_flags = all_flags.items;
try sx.target.link(self.allocator, self.io, objects[0], objects[1..], output, libraries, frameworks, cfg, self.has_jni_main);
}
};
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool, stdlib_paths: []const []const u8) !void {
// Phase A: read + parse + resolveImports (fast: ~0.5ms)
timer.mark();
@@ -695,6 +729,16 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
};
timer.record("link");
// Driver-side linker adapter behind the sx `link` primitive (Phase 5). Lives
// on this stack frame so it outlives the post-link callback invocation below.
var link_ctx = LinkHooksCtx{
.allocator = allocator,
.io = io,
.base_config = merged_config,
.has_jni_main = comp.getJniMainEmissions().len > 0,
};
var build_hooks = sx.ir.compiler_hooks.BuildHooks{ .ctx = &link_ctx, .link = LinkHooksCtx.link };
// Make the linked binary's path + bundling config visible to the
// post-link callback via `BuildOptions.binary_path()`,
// `BuildOptions.bundle_path()`, etc. CLI flags
@@ -702,6 +746,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// bundler doesn't need a separate code path.
if (comp.ir_emitter) |*e| {
e.build_config.binary_path = final_output;
e.build_config.build_hooks = &build_hooks;
// `--apk <path>` is a transitional alias for the bundle_path
// → post_link_module = "platform.bundle" auto-fallback. The
// sx Android bundler reads `bundle_path()` regardless of which