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:
148
src/main.zig
148
src/main.zig
@@ -573,12 +573,24 @@ fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, out
|
||||
/// 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 {
|
||||
const BuildHooksCtx = struct {
|
||||
comp: *sx.core.Compilation,
|
||||
obj_path: [:0]const u8,
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
base_config: sx.target.TargetConfig,
|
||||
has_jni_main: bool,
|
||||
|
||||
/// `emit_object()` — verify + emit the codegen'd module to its object file,
|
||||
/// return the path. The compiler no longer auto-emits; the sx driver calls this.
|
||||
fn emitObject(ctx_opaque: *anyopaque) anyerror![]const u8 {
|
||||
const self: *BuildHooksCtx = @ptrCast(@alignCast(ctx_opaque));
|
||||
const e = if (self.comp.ir_emitter) |*p| p else return error.NoEmitter;
|
||||
try e.verifyWithMessage();
|
||||
try e.emitObject(self.obj_path.ptr);
|
||||
return self.obj_path;
|
||||
}
|
||||
|
||||
fn link(
|
||||
ctx_opaque: *anyopaque,
|
||||
objects: []const []const u8,
|
||||
@@ -590,14 +602,12 @@ const LinkHooksCtx = struct {
|
||||
) 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));
|
||||
const self: *BuildHooksCtx = @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;
|
||||
// The passed `flags` are already the full merged set (`build_flags()` returns
|
||||
// the merged CLI + `#run` flags), so use them as-is rather than re-unioning.
|
||||
cfg.extra_link_flags = flags;
|
||||
try sx.target.link(self.allocator, self.io, objects[0], objects[1..], output, libraries, frameworks, cfg, self.has_jni_main);
|
||||
}
|
||||
};
|
||||
@@ -615,6 +625,21 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("parse");
|
||||
|
||||
// Auto-import the stdlib build driver so `default_pipeline` (+ the build
|
||||
// primitives) is always present to drive the build — the program need not
|
||||
// import the prelude (e.g. minimal asm tests). A flat import is idempotent if
|
||||
// it's already pulled in transitively. BUILD-path only: the JIT `sx run` path
|
||||
// emits + executes in-process and never invokes default_pipeline.
|
||||
if (comp.root) |r| {
|
||||
const imp = try allocator.create(sx.ast.Node);
|
||||
imp.* = .{ .span = r.span, .source_file = input_path, .data = .{ .import_decl = .{ .path = "modules/build.sx", .name = null } } };
|
||||
const old_decls = r.data.root.decls;
|
||||
const new_decls = try allocator.alloc(*sx.ast.Node, old_decls.len + 1);
|
||||
new_decls[0] = imp;
|
||||
@memcpy(new_decls[1..], old_decls);
|
||||
r.data.root.decls = new_decls;
|
||||
}
|
||||
|
||||
timer.mark();
|
||||
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("imports");
|
||||
@@ -630,52 +655,16 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
|
||||
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}/main.o", .{tmp_dir}, 0);
|
||||
|
||||
// Cache: compute key and check for cached binary/.o.
|
||||
// Disabled for programs with top-level #run (same guard as the JIT
|
||||
// path): the #run interp runs during codegen, and skipping codegen
|
||||
// loses its effects — build config (link flags, frameworks, output
|
||||
// path, bundling) and print side effects alike.
|
||||
const use_cache = enable_cache and !hasTopLevelRun(root);
|
||||
const key = computeCacheKey(source, &comp.import_sources, target_config);
|
||||
const cache_obj = try cachePath(allocator, key, "o");
|
||||
const cache_bin = try cachePath(allocator, key, "bin");
|
||||
|
||||
// Level 1: Try cached binary (skip everything — no codegen, no link).
|
||||
// Skipped under --emit-obj, which needs the freshly-emitted object kept.
|
||||
if (use_cache and !target_config.emit_obj) bin_cache: {
|
||||
std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache;
|
||||
timer.record("cache");
|
||||
return;
|
||||
}
|
||||
|
||||
// Level 2: Try cached .o (skip codegen+emit, still need link)
|
||||
const used_obj_cache = blk: {
|
||||
if (!use_cache) break :blk false;
|
||||
std.Io.Dir.copyFile(.cwd(), cache_obj, .cwd(), obj_path, io, .{}) catch break :blk false;
|
||||
break :blk true;
|
||||
};
|
||||
|
||||
if (used_obj_cache) {
|
||||
timer.record("cache");
|
||||
} else {
|
||||
// Cache MISS — full codegen + emit
|
||||
timer.mark();
|
||||
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("codegen");
|
||||
|
||||
timer.mark();
|
||||
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
|
||||
timer.record("verify");
|
||||
|
||||
timer.mark();
|
||||
comp.ir_emitter.?.emitObject(obj_path.ptr) catch return error.CompileError;
|
||||
timer.record("emit");
|
||||
|
||||
// Save .o to cache
|
||||
if (use_cache) {
|
||||
std.Io.Dir.copyFile(.cwd(), obj_path, .cwd(), cache_obj, io, .{ .make_path = true }) catch {};
|
||||
}
|
||||
}
|
||||
// Codegen only. There is NO auto-emit / auto-link: the build is driven
|
||||
// entirely by the sx `default_pipeline` (or a user `#run on_build(...)`
|
||||
// override), invoked after codegen below. `emit_object` (verify + object
|
||||
// emission) and `link` run as sx-called ACTIONS through the build hooks.
|
||||
// (The build cache short-circuited codegen, which the always-run sx driver
|
||||
// can't tolerate — removed; a future cache can live inside default_pipeline.)
|
||||
_ = enable_cache;
|
||||
timer.mark();
|
||||
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
||||
timer.record("codegen");
|
||||
|
||||
// Compile C sources from #import c blocks to .o files
|
||||
timer.mark();
|
||||
@@ -721,23 +710,22 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
}
|
||||
}
|
||||
|
||||
// Link (sx .o + C .o files)
|
||||
timer.mark();
|
||||
sx.target.link(allocator, io, obj_path, c_obj_paths, final_output, libs, fws, merged_config, comp.getJniMainEmissions().len > 0) catch {
|
||||
std.debug.print("error: linking failed\n", .{});
|
||||
return error.CompileError;
|
||||
};
|
||||
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{
|
||||
// NO auto-link here — the sx `default_pipeline` (or a user `on_build`
|
||||
// override) calls `link` (and `emit_object`) as actions through these hooks.
|
||||
// The ctx lives on this stack frame so it outlives the callback below.
|
||||
var build_ctx = BuildHooksCtx{
|
||||
.comp = &comp,
|
||||
.obj_path = obj_path,
|
||||
.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 };
|
||||
var build_hooks = sx.ir.compiler_hooks.BuildHooks{
|
||||
.ctx = &build_ctx,
|
||||
.emit_object = BuildHooksCtx.emitObject,
|
||||
.link = BuildHooksCtx.link,
|
||||
};
|
||||
|
||||
// Make the linked binary's path + bundling config visible to the
|
||||
// post-link callback via `BuildOptions.binary_path()`,
|
||||
@@ -763,11 +751,12 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
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.
|
||||
// `c_object_paths()` / `link_libraries()` / `build_*()` primitives. Slices
|
||||
// reference compileWithTimer locals that outlive the callback.
|
||||
e.build_config.c_object_paths = c_obj_paths;
|
||||
e.build_config.link_libraries = libs;
|
||||
e.build_config.object_path = obj_path;
|
||||
e.build_config.output_path = final_output;
|
||||
e.build_config.merged_link_flags = merged_config.extra_link_flags;
|
||||
// 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;
|
||||
@@ -850,6 +839,24 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
std.debug.print("error: post-link module '{s}.bundle_main' not found\n", .{mod_name});
|
||||
return error.CompileError;
|
||||
}
|
||||
} else {
|
||||
// No user/module override → run the stdlib default build pipeline. The
|
||||
// compiler force-lowers `default_pipeline` (well-known name); it emits +
|
||||
// links the program. Everything is sx-driven — this is the only build path
|
||||
// when the user hasn't overridden it.
|
||||
const ret_opt = comp.invokeByName("default_pipeline", true) catch |err| {
|
||||
printInterpBailDiag(&comp, "default build pipeline", err);
|
||||
return error.CompileError;
|
||||
};
|
||||
if (ret_opt) |ret| {
|
||||
if (ret.asBool() == false) {
|
||||
std.debug.print("error: default build pipeline returned false\n", .{});
|
||||
return error.CompileError;
|
||||
}
|
||||
} else {
|
||||
std.debug.print("error: default build pipeline 'default_pipeline' not found (is the prelude imported?)\n", .{});
|
||||
return error.CompileError;
|
||||
}
|
||||
}
|
||||
|
||||
// Post-process wasm HTML: inject content hash for cache busting
|
||||
@@ -857,11 +864,6 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
sx.target.postProcessWasmHtml(allocator, io, final_output);
|
||||
}
|
||||
|
||||
// Save linked binary to cache
|
||||
if (use_cache) {
|
||||
std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {};
|
||||
}
|
||||
|
||||
std.debug.print("compiled: {s}\n", .{final_output});
|
||||
|
||||
// Clean up temp directory and all build artifacts. Under --emit-obj, keep
|
||||
|
||||
Reference in New Issue
Block a user