P5.4 core: drive the whole build from sx default_pipeline (no auto-emit/link)

The compiler's post-IR role shrinks to: codegen -> invoke the build callback.
There is NO Zig auto-emit / auto-link anymore; emit + link are sx-called actions.

- emit_object() is now an ACTION (verify + emit via a host BuildHooks vtable),
  returning the object path. New query primitives build_output/build_target/
  build_frameworks/build_flags (data reads from the merged BuildConfig).
- library/modules/build.sx imports compiler.sx and defines default_pipeline:
  emit_object -> gather c_object_paths -> link(objs, output, libs, fws, flags,
  target). The std<->build import cycle is handled by the resolver.
- The compiler FORCE-LOWERS default_pipeline (well-known name) and AUTO-INVOKES
  it post-codegen when no on_build/set_post_link_callback override was
  registered (driver's final fallback: invokeByName default_pipeline).
- Prelude-less programs (e.g. asm tests) don't import build.sx, so the BUILD
  path auto-imports modules/build.sx (idempotent if already transitive) so
  default_pipeline is always available. JIT sx run is untouched (emits in-process).
- Removed the build cache short-circuits (incompatible with the always-run sx
  driver; a future cache can live in default_pipeline).

Benign 37-.ir churn (build.sx grew); zero behavior changes (verified diff is
.ir-only). 705/0 both gates.
This commit is contained in:
agra
2026-06-19 09:22:54 +03:00
parent 1f796e92ec
commit d178454841
44 changed files with 93042 additions and 80510 deletions

View File

@@ -71,11 +71,11 @@ pub const BuildConfig = struct {
c_object_paths: []const []const u8 = &.{},
link_libraries: []const []const u8 = &.{},
/// Path of the object file the compiler emitted for this build (`.sx-tmp/main.o`
/// or the cached `.o`). Forwarded by main.zig before the post-link callback so
/// the sx build driver can read it via the `emit_object()` compiler primitive
/// (the compiler emits the object eagerly; the primitive returns its path).
object_path: ?[]const u8 = null,
/// The fully-merged link flags (CLI `extra_link_flags` + `#run` build-block
/// flags), forwarded by main.zig. The sx driver reads them via `build_flags()`
/// and passes them to `link`. (Distinct from `link_flags`, which holds only
/// the `#run`-accumulated subset.)
merged_link_flags: []const []const u8 = &.{},
/// Host-installed callbacks for build-pipeline ACTIONS the comptime VM can't
/// perform itself (it can't depend on the driver — `core`/`main`/`target`).
@@ -126,6 +126,10 @@ pub const BuildConfig = struct {
/// error here and the VM surfaces it as a hard build error.
pub const BuildHooks = struct {
ctx: *anyopaque,
/// Verify + emit the codegen'd module to its object file; return the path
/// (ctx-owned). The `emit_object()` primitive — an ACTION, since the driver
/// no longer auto-emits (everything is sx-driven via `default_pipeline`).
emit_object: *const fn (ctx: *anyopaque) anyerror![]const u8,
/// Link `objects` → `output`, with the given `libraries` / `frameworks` /
/// link `flags` / `target` triple. (`objects` is the full object list; the
/// adapter splits it for the underlying linker.)

View File

@@ -72,6 +72,10 @@ pub const bound_fns = [_]BoundFn{
.{ .sx_name = "link_libraries", .handler = handleBuildPipelineQuery },
.{ .sx_name = "emit_object", .handler = handleBuildPipelineQuery },
.{ .sx_name = "link", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_output", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_target", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_frameworks", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_flags", .handler = handleBuildPipelineQuery },
};
/// Legacy-path stub for the Phase 5 build-pipeline primitives — see the

View File

@@ -1523,17 +1523,42 @@ pub const Vm = struct {
return self.failMsg("comptime link_libraries: no build config threaded into the VM");
return try self.makeStringList(table, result_ty, bc.link_libraries);
}
// `emit_object() -> string` — the compiler emits the object eagerly (the Zig
// driver, before the post-link callback); this returns its path. A query,
// not an action — so no driver callback is needed (unlike `link`).
// `emit_object() -> string` — ACTION: verify + emit the codegen'd module
// to its object file and return the path. Dispatches through the
// host-installed hook (the VM can't emit itself); the driver no longer
// auto-emits (everything is sx-driven via `default_pipeline`).
if (std.mem.eql(u8, name, "emit_object")) {
if (args.len != 0) return self.failMsg("comptime emit_object: expected no args");
const bc = self.build_config orelse
return self.failMsg("comptime emit_object: no build config threaded into the VM");
const path = bc.object_path orelse
return self.failMsg("comptime emit_object: no object was emitted (object_path unset)");
const hooks = bc.build_hooks orelse
return self.failMsg("comptime emit_object: no build hooks installed (emit is a post-codegen-only action)");
const path = hooks.emit_object(hooks.ctx) catch
return self.failMsg("comptime emit_object: object emission failed");
return try self.makeStringValue(table, path);
}
// Build-config metadata the sx driver passes to `link`. Read-only data
// forwarded by `main.zig` (the merged CLI + `#run` build config).
if (std.mem.eql(u8, name, "build_output")) {
if (args.len != 0) return self.failMsg("comptime build_output: expected no args");
const bc = self.build_config orelse return self.failMsg("comptime build_output: no build config");
return try self.makeStringValue(table, bc.output_path orelse "");
}
if (std.mem.eql(u8, name, "build_target")) {
if (args.len != 0) return self.failMsg("comptime build_target: expected no args");
const bc = self.build_config orelse return self.failMsg("comptime build_target: no build config");
return try self.makeStringValue(table, bc.target_triple orelse "");
}
if (std.mem.eql(u8, name, "build_frameworks")) {
if (args.len != 0) return self.failMsg("comptime build_frameworks: expected no args");
const bc = self.build_config orelse return self.failMsg("comptime build_frameworks: no build config");
return try self.makeStringList(table, result_ty, bc.target_frameworks);
}
if (std.mem.eql(u8, name, "build_flags")) {
if (args.len != 0) return self.failMsg("comptime build_flags: expected no args");
const bc = self.build_config orelse return self.failMsg("comptime build_flags: no build config");
return try self.makeStringList(table, result_ty, bc.merged_link_flags);
}
// `link(objects, output, libraries, frameworks, flags, target)` — the one
// genuine ACTION: dispatch to the host-installed linker (the VM can't link
// itself). Void return (the build callback isn't fallible — Phase 5

View File

@@ -155,6 +155,14 @@ fn isExportedEntryName(name: []const u8) bool {
std.mem.startsWith(u8, name, "Java_");
}
/// The well-known stdlib build driver (`library/modules/build.sx`). It is invoked
/// by the compiler post-codegen when no `#run on_build(...)` override exists, but
/// is never CALLED from sx — so it must be force-lowered like an OS entry point,
/// else lazy lowering leaves it a bodiless `declare` stub the VM can't run.
fn isDefaultBuildPipeline(name: []const u8) bool {
return std.mem.eql(u8, name, "default_pipeline");
}
/// Lower all top-level declarations from a root node.
/// Pass 1: Scan all declarations (register ASTs, types, extern stubs).
/// Pass 2: Lower only `main` (everything else is lowered lazily on demand).
@@ -222,6 +230,12 @@ pub fn lowerRoot(self: *Lowering, root: *const Node) void {
self.checkInfiniteSize();
// Pass 2: lower main (and comptime side-effects)
self.lowerMainAndComptime(decls);
// Pass 2b: force-lower the stdlib build driver `default_pipeline` (in the
// flat-imported `modules/build.sx`, so NOT in the main `decls` above). The
// compiler auto-invokes it post-codegen when no `#run on_build(...)` override
// exists, but nothing CALLS it from sx — so without this it stays a bodiless
// stub the build VM can't run. No-ops when build.sx isn't imported.
self.lazyLowerFunction("default_pipeline");
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
self.lowerDeferredTypeFns();
// Pass 4: target-specific entry-point sanity checks
@@ -1457,7 +1471,7 @@ pub fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void {
// consumption (often never called from sx), so force-lower
// them like OS-called entry points — else lazy lowering
// leaves them as bodiless `declare` stubs (Phase 2).
if (isExportedEntryName(cd.name) or cd.value.data.fn_decl.extern_export == .export_) {
if (isExportedEntryName(cd.name) or cd.value.data.fn_decl.extern_export == .export_ or isDefaultBuildPipeline(cd.name)) {
self.lazyLowerFunction(cd.name);
}
} else if (cd.value.data == .comptime_expr) {