P5.6 (macOS): default_pipeline drives bundling; fix issue 0125 (array-format blowup)
build.sx now `#import`s the sx bundler and `default_pipeline` delegates to its
`bundle_main` when a bundle was requested (emit + link, then wrap the binary into
the `.app`/`.apk`); otherwise it just emit+links via the shared `emit_and_link`
core. The Zig `--bundle`/`post_link_module` dispatch shim is removed — the CLI
bundle flags only feed `BuildConfig`, and `default_pipeline` branches on
`bundle_path()`. Validated end-to-end on macOS: `sx build --bundle App.app
--bundle-id … foo.sx` on a plain program AND auto-bundle from `set_bundle_path`
both produce a valid signed `.app` (correct `Contents/MacOS/` layout, Info.plist,
passes `codesign`, binary runs). Also fixed a pre-existing host-build bug:
target_triple was left empty for host builds → `is_macos()` false → wrong flat
layout; main.zig now exposes the host triple when `--target` is absent.
bundle_main no longer re-calls `build_options()` (the handle is already its `opts`
param).
Fix issue 0125 (root cause): the type-match dispatcher unboxed each interned array
tag to the concrete array type — a whole-array load — and passed it to
`array_to_string` by value, which LLVM scalarized into one SelectionDAG node per
element (~12s / segfault at [65536]u8). The bundler's `format("…{}…")` instantiates
`any_to_string`, so importing it into the prelude surfaced 0125 for any large-array
program. Fix (route 1): `any_to_string`'s `case array:` arm calls `slice_to_string`,
and `lowerRuntimeDispatchCall` detects an ARRAY tag bound to a SLICE param and builds
a `{ptr,len}` slice VIEW of the payload pointer (`unbox_any → [*]elem` is an
int-to-ptr with NO load, paired with the array length) instead of loading the array.
Output is byte-identical (`[a, b, c]`). Pinned as
examples/0056-basic-large-array-format-no-blowup.sx; 0055 drops 12s → 0.2s.
37 `.ir` snapshots regenerated (build.sx now pulls in the bundler's types + the
array-format lowering changed); verified `.ir`-only, zero behavior-stream diffs.
705/0 both gates.
This commit is contained in:
27
examples/0056-basic-large-array-format-no-blowup.sx
Normal file
27
examples/0056-basic-large-array-format-no-blowup.sx
Normal file
@@ -0,0 +1,27 @@
|
||||
// Interning a large (~64KB) array type and using `{}` formatting elsewhere must
|
||||
// NOT scalarize into an O(N) SelectionDAG (which crashed `sx build` / made
|
||||
// `sx run` take ~12s). The array Any-unbox formats via a SLICE VIEW of its
|
||||
// storage — no whole-array load.
|
||||
//
|
||||
// Regression (issue 0125): `any_to_string`'s `case array:` arm used to do
|
||||
// `array_to_string(cast(type) val)`, loading the whole [65536]u8 by value and
|
||||
// reading each element off the loaded aggregate. Now the dispatcher builds a
|
||||
// `{ptr,len}` slice view of the payload pointer and formats that — output is
|
||||
// identical (`[a, b, c]`), and a large unrelated array type costs nothing.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
f :: () {
|
||||
buf : [65536]u8 = ---;
|
||||
buf[0] = 65; // 'A'
|
||||
out(string.{ ptr = @buf[0], len = 1 });
|
||||
out("\n");
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
f();
|
||||
print("{}\n", 5); // an int format — unaffected by the big array
|
||||
small : [3]i64 = .[7, 8, 9];
|
||||
print("{}\n", small); // array format still renders the element list
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
A
|
||||
5
|
||||
[7, 8, 9]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,16 @@
|
||||
# 0125 — any_to_string's array arms materialize every interned array type by value
|
||||
|
||||
> **RESOLVED (2026-06-19).** Root cause as described below: the type-match
|
||||
> dispatcher (`lowerRuntimeDispatchCall`, src/ir/lower/call.zig) unboxed each
|
||||
> interned array tag to the concrete array type — a whole-array load — and fed it
|
||||
> to `array_to_string` by value, which LLVM scalarized to one DAG node per element.
|
||||
> **Fix (route 1):** `any_to_string`'s `case array:` arm now calls `slice_to_string`
|
||||
> (library/modules/std/fmt.sx); the dispatcher detects an ARRAY tag bound to a SLICE
|
||||
> param and builds a `{ptr,len}` slice VIEW of the payload pointer (`unbox_any →
|
||||
> [*]elem` is an int-to-ptr with NO load, paired with the array length) instead of
|
||||
> loading the array. Output is byte-identical (`[a, b, c]`). The repro compiles fast
|
||||
> and prints correctly; pinned as `examples/0056-basic-large-array-format-no-blowup.sx`.
|
||||
|
||||
## Symptom
|
||||
|
||||
A program that (a) interns any large (~64KB+) array type and (b) uses
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
// tail imports cli.sx which imports this file) is handled by the resolver.
|
||||
#import "modules/std.sx";
|
||||
#import "modules/compiler.sx";
|
||||
// The sx-side `.app`/`.apk` bundler. `default_pipeline` delegates to its
|
||||
// `bundle_main` when a bundle was requested. The bundler is `abi(.compiler)`
|
||||
// (comptime-only, never emitted into the binary), and the build↔bundle import
|
||||
// cycle resolves like the std↔build one.
|
||||
#import "modules/platform/bundle.sx";
|
||||
|
||||
OperatingSystem :: enum { macos; linux; windows; wasm; ios; android; unknown; }
|
||||
Architecture :: enum { aarch64; x86_64; wasm32; wasm64; unknown; }
|
||||
@@ -112,18 +117,34 @@ on_build :: (cb: (opt: BuildOptions) -> bool abi(.compiler)) abi(.compiler);
|
||||
|
||||
// ── The default build script ────────────────────────────────────────────────
|
||||
//
|
||||
// `default_pipeline` is the stdlib build driver: the compiler invokes it after
|
||||
// codegen (everything is sx-driven — there is no auto-emit/auto-link). It emits
|
||||
// the sx object, gathers the C companion objects, and links them into the output
|
||||
// with the build's libraries / frameworks / flags / target. A user overrides the
|
||||
// whole pipeline with their own `#run on_build(custom);` in main.sx (last-wins).
|
||||
// The compiler FORCE-LOWERS this well-known name and auto-invokes it after
|
||||
// codegen when no `#run on_build(custom);` override was registered (no library
|
||||
// `#run` needed). A user override takes over entirely.
|
||||
default_pipeline :: (opt: BuildOptions) -> bool abi(.compiler) {
|
||||
// `emit_and_link` is the build CORE: emit the sx object, gather the C companion
|
||||
// objects, and link them into the output with the build's libraries / frameworks /
|
||||
// flags / target. Shared by `default_pipeline` AND `platform.bundle.bundle_main`
|
||||
// (which wraps it with the per-target `.app` / `.apk` bundling) so a bundler
|
||||
// override doesn't re-implement emit+link.
|
||||
emit_and_link :: (opt: BuildOptions) -> bool abi(.compiler) {
|
||||
obj := emit_object();
|
||||
objs := c_object_paths();
|
||||
objs.append(obj);
|
||||
link(objs, build_output(), link_libraries(), build_frameworks(), build_flags(), build_target());
|
||||
return true;
|
||||
}
|
||||
|
||||
// `default_pipeline` is the stdlib build driver: the compiler invokes it after
|
||||
// codegen (everything is sx-driven — there is no auto-emit/auto-link). It emits +
|
||||
// links the program via `emit_and_link`. A user overrides the whole pipeline with
|
||||
// their own `#run on_build(custom);` in main.sx (last-wins) — e.g. bundling is
|
||||
// `#import "modules/platform/bundle.sx"; #run on_build(bundle_main);`, where
|
||||
// `bundle_main` runs `emit_and_link` then wraps the binary into a `.app` / `.apk`.
|
||||
// The compiler FORCE-LOWERS this well-known name and auto-invokes it after codegen
|
||||
// when no `on_build` override was registered (no library `#run` needed).
|
||||
//
|
||||
// When a bundle was requested (`--bundle`/`--apk` or `set_bundle_path`), delegate
|
||||
// to the per-target bundler (`bundle_main` runs the emit+link core then wraps the
|
||||
// `.app`/`.apk`); otherwise just emit + link.
|
||||
default_pipeline :: (opt: BuildOptions) -> bool abi(.compiler) {
|
||||
if opt.bundle_path().len > 0 {
|
||||
return bundle_main(opt);
|
||||
}
|
||||
return emit_and_link(opt);
|
||||
}
|
||||
|
||||
@@ -29,14 +29,18 @@
|
||||
// `abi(.compiler)` so the backend doesn't lower it and its `build_options()`
|
||||
// compiler-API call is permitted (it would otherwise be rejected as a
|
||||
// comptime-only function called at runtime).
|
||||
bundle_main :: (opt: BuildOptions) -> bool abi(.compiler) {
|
||||
opts := build_options();
|
||||
bundle_main :: (opts: BuildOptions) -> bool abi(.compiler) {
|
||||
// Run the standard build core first (emit the object + link the binary) — an
|
||||
// `on_build` override REPLACES default_pipeline, so the bundler owns the whole
|
||||
// pipeline: emit + link, then wrap the linked binary into the `.app` / `.apk`.
|
||||
if !emit_and_link(opts) { return false; }
|
||||
|
||||
binary := opts.binary_path();
|
||||
bundle := opts.bundle_path();
|
||||
bid := opts.bundle_id();
|
||||
|
||||
if bundle.len == 0 {
|
||||
// No bundle requested — nothing to do. Build succeeded.
|
||||
// No bundle requested — emit+link already done. Build succeeded.
|
||||
return true;
|
||||
}
|
||||
if bid.len == 0 {
|
||||
|
||||
@@ -336,7 +336,13 @@ any_to_string :: (val: Any) -> string {
|
||||
case enum: result = enum_to_string(cast(type) val);
|
||||
case error_set: { tagid : u32 = xx val; result = error_tag_name(tagid); }
|
||||
case vector: result = vector_to_string(cast(type) val);
|
||||
case array: result = array_to_string(cast(type) val);
|
||||
// Arrays format via `slice_to_string` over a SLICE VIEW of the payload
|
||||
// (the Any payload for an array IS a pointer to its storage). The dispatcher
|
||||
// (lowerRuntimeDispatchCall) builds the `{ptr,len}` view without ever
|
||||
// loading the whole array as a value — avoiding the O(N) SelectionDAG
|
||||
// scalarization that crippled `[65536]u8` (issue 0125). Output is identical
|
||||
// to the old `array_to_string` (`[a, b, c]`).
|
||||
case array: result = slice_to_string(cast(type) val);
|
||||
case slice: result = slice_to_string(cast(type) val);
|
||||
case pointer: result = pointer_to_string(cast(type) val);
|
||||
case optional: result = optional_to_string(cast(type) val);
|
||||
|
||||
@@ -1483,13 +1483,33 @@ pub fn lowerRuntimeDispatchCall(
|
||||
|
||||
self.builder.switchBr(type_tag, cases.items, default_bb, &.{});
|
||||
|
||||
// Whether the cast-arg parameter is a SLICE (`[]$T`). When it is and the tag
|
||||
// is an ARRAY, we pass a slice VIEW of the array's storage rather than loading
|
||||
// the whole array as a value (issue 0125 — the giant load + per-element
|
||||
// SelectionDAG scalarization). The Any payload for an array IS a pointer to its
|
||||
// storage, so `unbox_any → [*]elem` (int-to-ptr, no load) + the array length
|
||||
// gives a `{ptr,len}` slice for free.
|
||||
const cast_param_is_slice = cast_arg_idx < fd.params.len and
|
||||
fd.params[cast_arg_idx].type_expr.data == .slice_type_expr;
|
||||
|
||||
for (match_tags, 0..) |tag, ti| {
|
||||
self.builder.switchToBlock(case_blocks.items[ti]);
|
||||
|
||||
const ty_id = TypeId.fromIndex(@intCast(tag));
|
||||
|
||||
// Unbox the Any value to the concrete type
|
||||
const unboxed = self.builder.emit(.{ .unbox_any = .{
|
||||
// Unbox the Any value to the concrete type — except an ARRAY tag bound to a
|
||||
// SLICE param, which becomes a no-load slice view of the array storage.
|
||||
const tag_is_array = !ty_id.isBuiltin() and self.module.types.get(ty_id) == .array;
|
||||
const unboxed = if (cast_param_is_slice and tag_is_array) blk: {
|
||||
const elem_ty = self.getElementType(ty_id);
|
||||
const arr_len = self.module.types.get(ty_id).array.length;
|
||||
const slice_ty = self.module.types.sliceOf(elem_ty);
|
||||
const ptr_ty = self.module.types.manyPtrTo(elem_ty);
|
||||
// The Any payload (the array's storage address) → `[*]elem` (no load).
|
||||
const ptr = self.builder.emit(.{ .unbox_any = .{ .operand = any_val } }, ptr_ty);
|
||||
const len = self.builder.constInt(@intCast(arr_len), .i64);
|
||||
break :blk self.builder.structInit(&.{ ptr, len }, slice_ty);
|
||||
} else self.builder.emit(.{ .unbox_any = .{
|
||||
.operand = any_val,
|
||||
} }, ty_id);
|
||||
|
||||
|
||||
64
src/main.zig
64
src/main.zig
@@ -747,7 +747,17 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
// branching (iOS device vs simulator vs macOS) and `Frameworks/`
|
||||
// embedding. Slice fields point into the long-lived target_config /
|
||||
// CLI argv buffers, which outlive the post-link callback.
|
||||
if (merged_config.triple) |t| e.build_config.target_triple = std.mem.span(t);
|
||||
if (merged_config.triple) |t| {
|
||||
e.build_config.target_triple = std.mem.span(t);
|
||||
} else {
|
||||
// Host build (no `--target`): expose the HOST triple so the sx
|
||||
// bundler's `is_macos()`/`is_ios()`/… predicates resolve correctly.
|
||||
// Left empty, a host macOS `.app` would get the flat iOS-style layout
|
||||
// (is_macos() == false) instead of `Contents/MacOS/`.
|
||||
const host = sx.llvm_api.c.LLVMGetDefaultTargetTriple();
|
||||
defer sx.llvm_api.c.LLVMDisposeMessage(host);
|
||||
e.build_config.target_triple = allocator.dupe(u8, std.mem.span(host)) catch null;
|
||||
}
|
||||
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
|
||||
@@ -790,28 +800,13 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
}
|
||||
}
|
||||
|
||||
// CLI `--bundle <path>` migration shim. The legacy Zig bundler
|
||||
// path (target.createBundle) has been retired; the equivalent
|
||||
// logic now lives in `library/modules/platform/bundle.sx`. If the
|
||||
// user passed `--bundle` on the command line but did NOT register
|
||||
// a post-link callback themselves, point the resolver at
|
||||
// `platform.bundle.bundle_main`. The lookup is best-effort: if the
|
||||
// source doesn't `#import "modules/platform/bundle.sx"`,
|
||||
// `invokeByName` returns null and the existing "not found" branch
|
||||
// prints a clear migration message.
|
||||
if (comp.ir_emitter) |*e| {
|
||||
if (e.build_config.bundle_path != null and
|
||||
e.build_config.post_link_callback_fn == null and
|
||||
e.build_config.post_link_module == null)
|
||||
{
|
||||
e.build_config.post_link_module = "platform.bundle";
|
||||
}
|
||||
}
|
||||
|
||||
// Post-link callback: if the user registered one via
|
||||
// `BuildOptions.set_post_link_callback(fn)` or
|
||||
// `set_post_link_module("name")`, re-enter the IR interpreter and
|
||||
// invoke that sx function now. A `false` return fails the build.
|
||||
// Post-link build driver. Either the user registered an `on_build(cb)`
|
||||
// override (bundling is `#run on_build(bundle_main);` — bundle_main runs the
|
||||
// emit+link core then wraps the `.app`/`.apk`), or we run the stdlib
|
||||
// `default_pipeline` (emit + link; it fails with a precise hint if a bundle was
|
||||
// requested via `--bundle`/`--apk` but no bundler was registered). The CLI
|
||||
// bundle flags only feed `BuildConfig` (bundle_path/id/…) — there is no Zig
|
||||
// bundler shim; bundling is entirely sx-driven. A `false` return fails the build.
|
||||
if (comp.getPostLinkCallback()) |fid| {
|
||||
const ret = comp.invokeByFuncId(fid, comp.getPostLinkTakesOptions()) catch |err| {
|
||||
printInterpBailDiag(&comp, "post-link callback", err);
|
||||
@@ -821,29 +816,8 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
std.debug.print("error: post-link callback returned false\n", .{});
|
||||
return error.CompileError;
|
||||
}
|
||||
} else if (comp.getPostLinkModule()) |mod_name| {
|
||||
const qualified = try std.fmt.allocPrint(allocator, "{s}.bundle_main", .{mod_name});
|
||||
defer allocator.free(qualified);
|
||||
const ret_opt = comp.invokeByName(qualified, true) catch |err| {
|
||||
const label = try std.fmt.allocPrint(allocator, "post-link module '{s}.bundle_main'", .{mod_name});
|
||||
defer allocator.free(label);
|
||||
printInterpBailDiag(&comp, label, err);
|
||||
return error.CompileError;
|
||||
};
|
||||
if (ret_opt) |ret| {
|
||||
if (ret.asBool() == false) {
|
||||
std.debug.print("error: post-link module '{s}.bundle_main' returned false\n", .{mod_name});
|
||||
return error.CompileError;
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
// No override → run the force-lowered stdlib `default_pipeline`.
|
||||
const ret_opt = comp.invokeByName("default_pipeline", true) catch |err| {
|
||||
printInterpBailDiag(&comp, "default build pipeline", err);
|
||||
return error.CompileError;
|
||||
|
||||
Reference in New Issue
Block a user