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
|
# 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
|
## Symptom
|
||||||
|
|
||||||
A program that (a) interns any large (~64KB+) array type and (b) uses
|
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.
|
// tail imports cli.sx which imports this file) is handled by the resolver.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/compiler.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; }
|
OperatingSystem :: enum { macos; linux; windows; wasm; ios; android; unknown; }
|
||||||
Architecture :: enum { aarch64; x86_64; wasm32; wasm64; 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 ────────────────────────────────────────────────
|
// ── The default build script ────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// `default_pipeline` is the stdlib build driver: the compiler invokes it after
|
// `emit_and_link` is the build CORE: emit the sx object, gather the C companion
|
||||||
// codegen (everything is sx-driven — there is no auto-emit/auto-link). It emits
|
// objects, and link them into the output with the build's libraries / frameworks /
|
||||||
// the sx object, gathers the C companion objects, and links them into the output
|
// flags / target. Shared by `default_pipeline` AND `platform.bundle.bundle_main`
|
||||||
// with the build's libraries / frameworks / flags / target. A user overrides the
|
// (which wraps it with the per-target `.app` / `.apk` bundling) so a bundler
|
||||||
// whole pipeline with their own `#run on_build(custom);` in main.sx (last-wins).
|
// override doesn't re-implement emit+link.
|
||||||
// The compiler FORCE-LOWERS this well-known name and auto-invokes it after
|
emit_and_link :: (opt: BuildOptions) -> bool abi(.compiler) {
|
||||||
// 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) {
|
|
||||||
obj := emit_object();
|
obj := emit_object();
|
||||||
objs := c_object_paths();
|
objs := c_object_paths();
|
||||||
objs.append(obj);
|
objs.append(obj);
|
||||||
link(objs, build_output(), link_libraries(), build_frameworks(), build_flags(), build_target());
|
link(objs, build_output(), link_libraries(), build_frameworks(), build_flags(), build_target());
|
||||||
return true;
|
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()`
|
// `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
|
// compiler-API call is permitted (it would otherwise be rejected as a
|
||||||
// comptime-only function called at runtime).
|
// comptime-only function called at runtime).
|
||||||
bundle_main :: (opt: BuildOptions) -> bool abi(.compiler) {
|
bundle_main :: (opts: BuildOptions) -> bool abi(.compiler) {
|
||||||
opts := build_options();
|
// 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();
|
binary := opts.binary_path();
|
||||||
bundle := opts.bundle_path();
|
bundle := opts.bundle_path();
|
||||||
bid := opts.bundle_id();
|
bid := opts.bundle_id();
|
||||||
|
|
||||||
if bundle.len == 0 {
|
if bundle.len == 0 {
|
||||||
// No bundle requested — nothing to do. Build succeeded.
|
// No bundle requested — emit+link already done. Build succeeded.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if bid.len == 0 {
|
if bid.len == 0 {
|
||||||
|
|||||||
@@ -336,7 +336,13 @@ any_to_string :: (val: Any) -> string {
|
|||||||
case enum: result = enum_to_string(cast(type) val);
|
case enum: result = enum_to_string(cast(type) val);
|
||||||
case error_set: { tagid : u32 = xx val; result = error_tag_name(tagid); }
|
case error_set: { tagid : u32 = xx val; result = error_tag_name(tagid); }
|
||||||
case vector: result = vector_to_string(cast(type) val);
|
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 slice: result = slice_to_string(cast(type) val);
|
||||||
case pointer: result = pointer_to_string(cast(type) val);
|
case pointer: result = pointer_to_string(cast(type) val);
|
||||||
case optional: result = optional_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, &.{});
|
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| {
|
for (match_tags, 0..) |tag, ti| {
|
||||||
self.builder.switchToBlock(case_blocks.items[ti]);
|
self.builder.switchToBlock(case_blocks.items[ti]);
|
||||||
|
|
||||||
const ty_id = TypeId.fromIndex(@intCast(tag));
|
const ty_id = TypeId.fromIndex(@intCast(tag));
|
||||||
|
|
||||||
// Unbox the Any value to the concrete type
|
// Unbox the Any value to the concrete type — except an ARRAY tag bound to a
|
||||||
const unboxed = self.builder.emit(.{ .unbox_any = .{
|
// 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,
|
.operand = any_val,
|
||||||
} }, ty_id);
|
} }, 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/`
|
// branching (iOS device vs simulator vs macOS) and `Frameworks/`
|
||||||
// embedding. Slice fields point into the long-lived target_config /
|
// embedding. Slice fields point into the long-lived target_config /
|
||||||
// CLI argv buffers, which outlive the post-link callback.
|
// 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_frameworks = fws;
|
||||||
e.build_config.target_framework_paths = merged_config.framework_paths;
|
e.build_config.target_framework_paths = merged_config.framework_paths;
|
||||||
// Phase 5: the sx-driven build pipeline reads these via the
|
// 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
|
// Post-link build driver. Either the user registered an `on_build(cb)`
|
||||||
// path (target.createBundle) has been retired; the equivalent
|
// override (bundling is `#run on_build(bundle_main);` — bundle_main runs the
|
||||||
// logic now lives in `library/modules/platform/bundle.sx`. If the
|
// emit+link core then wraps the `.app`/`.apk`), or we run the stdlib
|
||||||
// user passed `--bundle` on the command line but did NOT register
|
// `default_pipeline` (emit + link; it fails with a precise hint if a bundle was
|
||||||
// a post-link callback themselves, point the resolver at
|
// requested via `--bundle`/`--apk` but no bundler was registered). The CLI
|
||||||
// `platform.bundle.bundle_main`. The lookup is best-effort: if the
|
// bundle flags only feed `BuildConfig` (bundle_path/id/…) — there is no Zig
|
||||||
// source doesn't `#import "modules/platform/bundle.sx"`,
|
// bundler shim; bundling is entirely sx-driven. A `false` return fails the build.
|
||||||
// `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.
|
|
||||||
if (comp.getPostLinkCallback()) |fid| {
|
if (comp.getPostLinkCallback()) |fid| {
|
||||||
const ret = comp.invokeByFuncId(fid, comp.getPostLinkTakesOptions()) catch |err| {
|
const ret = comp.invokeByFuncId(fid, comp.getPostLinkTakesOptions()) catch |err| {
|
||||||
printInterpBailDiag(&comp, "post-link callback", 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", .{});
|
std.debug.print("error: post-link callback returned false\n", .{});
|
||||||
return error.CompileError;
|
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 {
|
} else {
|
||||||
// No user/module override → run the stdlib default build pipeline. The
|
// No override → run the force-lowered stdlib `default_pipeline`.
|
||||||
// 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| {
|
const ret_opt = comp.invokeByName("default_pipeline", true) catch |err| {
|
||||||
printInterpBailDiag(&comp, "default build pipeline", err);
|
printInterpBailDiag(&comp, "default build pipeline", err);
|
||||||
return error.CompileError;
|
return error.CompileError;
|
||||||
|
|||||||
Reference in New Issue
Block a user