Files
sx/src/ir/compiler_hooks.zig
agra d178454841 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.
2026-06-19 09:22:54 +03:00

584 lines
25 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const interp_mod = @import("interp.zig");
const Value = interp_mod.Value;
const Interpreter = interp_mod.Interpreter;
const inst = @import("inst.zig");
const FuncId = inst.FuncId;
// ── BuildConfig ─────────────────────────────────────────────────────────
// Mutable build configuration accumulated by #run blocks via #compiler methods.
/// `(src_dir, dest_in_bundle)` pair recorded by
/// `BuildOptions.add_asset_dir(src, dest)`. The sx bundler walks the
/// list and recursively copies each `src` directory into the bundle
/// at the relative `dest` path (e.g. `("assets", "assets")` copies
/// `./assets/` to `<bundle>/assets/`). Android's Week-7 APK path will
/// zip the same pairs into the unaligned APK.
pub const AssetDir = struct {
src: []const u8,
dest: []const u8,
};
pub const BuildConfig = struct {
link_flags: std.ArrayList([]const u8) = .empty,
frameworks: std.ArrayList([]const u8) = .empty,
asset_dirs: std.ArrayList(AssetDir) = .empty,
output_path: ?[]const u8 = null,
wasm_shell_path: ?[]const u8 = null,
/// Post-link callback registered via
/// `BuildOptions.set_post_link_callback(fn)`. When set, the
/// compiler re-enters the IR interpreter after `target.link()`
/// and invokes this function with no args. A `false` return is
/// treated as a build failure.
post_link_callback_fn: ?FuncId = null,
/// True when the post-link callback was registered via `on_build(cb)` (the
/// Phase 5 form, `cb: (opt: BuildOptions) -> bool`) rather than the legacy
/// `set_post_link_callback(cb)` (`cb: () -> bool`). When set, the compiler
/// invokes the callback with the opaque `BuildOptions` handle as its arg.
post_link_takes_options: bool = false,
/// Alternative to `post_link_callback_fn`: the qualified name of
/// a module whose `bundle_main` function should be called
/// post-link.
post_link_module: ?[]const u8 = null,
/// Path of the freshly-linked binary, populated by `main.zig`
/// right before the post-link callback runs. The sx-side bundler
/// reads this via `binary_path()` to know what file to wrap.
binary_path: ?[]const u8 = null,
// Apple `.app` / Android `.apk` bundling parameters. Set either
// by the sx-side `BuildOptions.set_bundle_*` methods (preferred)
// or by main.zig from CLI flags (transitional fallback). The sx
// bundler reads them via the matching accessor methods.
bundle_path: ?[]const u8 = null,
bundle_id: ?[]const u8 = null,
codesign_identity: ?[]const u8 = null,
provisioning_profile: ?[]const u8 = null,
/// Target triple as supplied to `--target` (canonicalized).
/// Populated by main.zig before the post-link callback runs so the
/// sx bundler can switch on iOS vs. macOS vs. simulator.
target_triple: ?[]const u8 = null,
/// C companion object files (`#import c { #source ... }`, compiled to `.o`)
/// and `#library` link names, forwarded by main.zig before the post-link
/// callback so the sx-driven build pipeline (Phase 5) can read them via the
/// `c_object_paths()` / `link_libraries()` compiler primitives and pass them
/// to `link`. Slices reference compiler-owned memory that outlives the
/// callback.
c_object_paths: []const []const u8 = &.{},
link_libraries: []const []const u8 = &.{},
/// 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`).
/// main.zig installs this before the post-link callback; the VM's `link`
/// primitive dispatches through it. Null outside a post-link build (a `link`
/// call then bails loudly — it's a post-codegen-only action).
build_hooks: ?*const BuildHooks = null,
/// Frameworks the binary links against (`-framework` names) and
/// the search paths to look them up in (`-F` directories), forwarded
/// from the link step so the sx bundler can embed them into
/// `<bundle>/Frameworks/`.
target_frameworks: []const []const u8 = &.{},
target_framework_paths: []const []const u8 = &.{},
/// User-supplied `AndroidManifest.xml` override (`--manifest <path>`
/// or `BuildOptions.set_manifest_path("...")`). When null, the
/// Android bundler synthesizes a default manifest.
manifest_path: ?[]const u8 = null,
/// User-supplied debug keystore path (`--keystore <path>` or
/// `BuildOptions.set_keystore_path("...")`). When null, the Android
/// bundler uses `$HOME/.android/debug.keystore` (auto-generated on
/// first use via `keytool`).
keystore_path: ?[]const u8 = null,
/// `#jni_main #jni_class("path") { ... }` decls discovered during
/// lowering, paired with their pre-rendered Java source. The
/// Android bundler writes each entry to
/// `<stage>/java/<pkg>/<Class>.java`, compiles via `javac` + `d8`,
/// and bundles the resulting `classes.dex` into the APK. Slices
/// reference compiler-owned memory that outlives the post-link
/// callback.
jni_main_runtime_paths: []const []const u8 = &.{},
jni_main_java_sources: []const []const u8 = &.{},
pub fn deinit(self: *BuildConfig, alloc: Allocator) void {
self.link_flags.deinit(alloc);
self.frameworks.deinit(alloc);
self.asset_dirs.deinit(alloc);
}
};
/// Host-installed callbacks for build-pipeline ACTIONS the comptime VM dispatches
/// but can't perform itself (it must not depend on the driver: `core`/`main`/
/// `target`). main.zig builds the concrete `ctx` + functions and points
/// `BuildConfig.build_hooks` at it before invoking the post-link callback. The
/// build callback is NOT fallible (Phase 5 decision) — a failed action returns an
/// 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.)
link: *const fn (
ctx: *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,
};
// ── Hook system ─────────────────────────────────────────────────────────
pub const HookError = error{
CannotEvalComptime,
TypeError,
};
/// Hook function signature. Receives the interpreter (for heap/string access),
/// resolved argument values, and the mutable build config.
pub const HookFn = *const fn (
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value;
pub const Registry = struct {
hooks: std.StringHashMap(HookFn),
pub fn init(alloc: Allocator) Registry {
return .{ .hooks = std.StringHashMap(HookFn).init(alloc) };
}
pub fn deinit(self: *Registry) void {
self.hooks.deinit();
}
pub fn get(self: *const Registry, name: []const u8) ?HookFn {
return self.hooks.get(name);
}
/// Register all built-in compiler hooks.
pub fn registerDefaults(self: *Registry) void {
self.hooks.put("build_options", &hookBuildOptions) catch {};
self.hooks.put("BuildOptions.add_link_flag", &hookAddLinkFlag) catch {};
self.hooks.put("BuildOptions.add_framework", &hookAddFramework) catch {};
self.hooks.put("BuildOptions.add_asset_dir", &hookAddAssetDir) catch {};
self.hooks.put("BuildOptions.asset_dir_count", &hookAssetDirCount) catch {};
self.hooks.put("BuildOptions.asset_dir_src_at", &hookAssetDirSrcAt) catch {};
self.hooks.put("BuildOptions.asset_dir_dest_at", &hookAssetDirDestAt) catch {};
self.hooks.put("BuildOptions.set_output_path", &hookSetOutputPath) catch {};
self.hooks.put("BuildOptions.set_wasm_shell", &hookSetWasmShell) catch {};
self.hooks.put("BuildOptions.set_post_link_callback", &hookSetPostLinkCallback) catch {};
self.hooks.put("BuildOptions.set_post_link_module", &hookSetPostLinkModule) catch {};
self.hooks.put("BuildOptions.binary_path", &hookGetBinaryPath) catch {};
// Bundling setters
self.hooks.put("BuildOptions.set_bundle_path", &hookSetBundlePath) catch {};
self.hooks.put("BuildOptions.set_bundle_id", &hookSetBundleId) catch {};
self.hooks.put("BuildOptions.set_codesign_identity", &hookSetCodesignIdentity) catch {};
self.hooks.put("BuildOptions.set_provisioning_profile", &hookSetProvisioningProfile) catch {};
// Bundling accessors
self.hooks.put("BuildOptions.bundle_path", &hookGetBundlePath) catch {};
self.hooks.put("BuildOptions.bundle_id", &hookGetBundleId) catch {};
self.hooks.put("BuildOptions.codesign_identity", &hookGetCodesignIdentity) catch {};
self.hooks.put("BuildOptions.provisioning_profile", &hookGetProvisioningProfile) catch {};
// Target accessors — mirror TargetConfig.is{MacOS,IOS,IOSDevice,IOSSimulator,Android}()
self.hooks.put("BuildOptions.target_triple", &hookGetTargetTriple) catch {};
self.hooks.put("BuildOptions.is_macos", &hookIsMacOS) catch {};
self.hooks.put("BuildOptions.is_ios", &hookIsIOS) catch {};
self.hooks.put("BuildOptions.is_ios_device", &hookIsIOSDevice) catch {};
self.hooks.put("BuildOptions.is_ios_simulator", &hookIsIOSSimulator) catch {};
self.hooks.put("BuildOptions.is_android", &hookIsAndroid) catch {};
// Android-specific setters + accessors
self.hooks.put("BuildOptions.set_manifest_path", &hookSetManifestPath) catch {};
self.hooks.put("BuildOptions.manifest_path", &hookGetManifestPath) catch {};
self.hooks.put("BuildOptions.set_keystore_path", &hookSetKeystorePath) catch {};
self.hooks.put("BuildOptions.keystore_path", &hookGetKeystorePath) catch {};
// #jni_main class emissions, exposed by index so bundle.sx can iterate.
self.hooks.put("BuildOptions.jni_main_count", &hookJniMainCount) catch {};
self.hooks.put("BuildOptions.jni_main_runtime_path_at", &hookJniMainRuntimePathAt) catch {};
self.hooks.put("BuildOptions.jni_main_java_source_at", &hookJniMainJavaSourceAt) catch {};
// Framework list accessors (for `.app/Frameworks/` embedding)
self.hooks.put("BuildOptions.framework_count", &hookFrameworkCount) catch {};
self.hooks.put("BuildOptions.framework_at", &hookFrameworkAt) catch {};
self.hooks.put("BuildOptions.framework_path_count", &hookFrameworkPathCount) catch {};
self.hooks.put("BuildOptions.framework_path_at", &hookFrameworkPathAt) catch {};
}
};
// ── build_options() hook ────────────────────────────────────────────────
fn hookBuildOptions(
_: *const Interpreter,
_: []const Value,
_: *BuildConfig,
_: Allocator,
) HookError!Value {
// build_options() returns a sentinel value; the real work happens
// when methods like add_link_flag/set_output_path are called on it.
return .void_val;
}
// ── BuildOptions hooks ──────────────────────────────────────────────────
fn hookAddLinkFlag(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
// args: [self (BuildOptions value), flag_string]
if (args.len < 2) return .void_val;
const str_val = args[1];
if (str_val.asString(interp)) |s| {
bc.link_flags.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookAddFramework(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
// args: [self (BuildOptions value), framework_name]
if (args.len < 2) return .void_val;
const str_val = args[1];
if (str_val.asString(interp)) |s| {
bc.frameworks.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookAddAssetDir(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
// args: [self (BuildOptions value), src_path, dest_path_in_bundle]
if (args.len < 3) return .void_val;
const src = args[1].asString(interp) orelse return error.TypeError;
const dest = args[2].asString(interp) orelse return error.TypeError;
const src_dup = alloc.dupe(u8, src) catch return error.CannotEvalComptime;
const dest_dup = alloc.dupe(u8, dest) catch return error.CannotEvalComptime;
bc.asset_dirs.append(alloc, .{ .src = src_dup, .dest = dest_dup }) catch return error.CannotEvalComptime;
return .void_val;
}
fn hookAssetDirCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .int = @intCast(bc.asset_dirs.items.len) };
}
fn hookAssetDirSrcAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (args.len < 2) return Value{ .string = "" };
const idx = args[1].asInt() orelse return error.TypeError;
if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len) return Value{ .string = "" };
return Value{ .string = bc.asset_dirs.items[@intCast(idx)].src };
}
fn hookAssetDirDestAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (args.len < 2) return Value{ .string = "" };
const idx = args[1].asInt() orelse return error.TypeError;
if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len) return Value{ .string = "" };
return Value{ .string = bc.asset_dirs.items[@intCast(idx)].dest };
}
fn hookSetOutputPath(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
// args: [self (BuildOptions value), path_string]
if (args.len < 2) return .void_val;
const str_val = args[1];
if (str_val.asString(interp)) |s| {
bc.output_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookSetWasmShell(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
// args: [self (BuildOptions value), path_string]
if (args.len < 2) return .void_val;
const str_val = args[1];
if (str_val.asString(interp)) |s| {
bc.wasm_shell_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookSetPostLinkCallback(
_: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
_: Allocator,
) HookError!Value {
// args: [self (BuildOptions value), fn_value]. We accept a function
// value (.func_ref) and stash the FuncId so `main.zig` can re-enter
// the interpreter after linking.
if (args.len < 2) return .void_val;
switch (args[1]) {
.func_ref => |id| bc.post_link_callback_fn = id,
else => return error.TypeError,
}
return .void_val;
}
fn hookSetPostLinkModule(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.post_link_module = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
/// Read the linked-binary path that main.zig populated right before
/// invoking the post-link callback. Returns the fat-string aggregate
/// the interpreter normally hands out for sx `string` values.
fn hookGetBinaryPath(
interp: *const Interpreter,
_: []const Value,
bc: *BuildConfig,
_: Allocator,
) HookError!Value {
_ = interp;
const path = bc.binary_path orelse "";
return Value{ .string = path };
}
// ── Bundling setters & accessors ─────────────────────────────────────
// Same pattern as set_output_path: take a string arg, dupe into the
// long-lived allocator, store on BuildConfig. The companion accessor
// reads back the same field; empty string when unset.
fn hookSetBundlePath(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.bundle_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookSetBundleId(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.bundle_id = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookSetCodesignIdentity(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.codesign_identity = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookSetProvisioningProfile(
interp: *const Interpreter,
args: []const Value,
bc: *BuildConfig,
alloc: Allocator,
) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.provisioning_profile = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookGetBundlePath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.bundle_path orelse "" };
}
fn hookGetBundleId(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.bundle_id orelse "" };
}
fn hookGetCodesignIdentity(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.codesign_identity orelse "" };
}
fn hookGetProvisioningProfile(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.provisioning_profile orelse "" };
}
// ── Target accessors ──────────────────────────────────────────────────
// These look at the target_triple that main.zig populates and answer
// the same questions TargetConfig's helpers do for Zig callers.
fn tripleContains(triple: ?[]const u8, needle: []const u8) bool {
const t = triple orelse return false;
return std.mem.indexOf(u8, t, needle) != null;
}
fn isIOSTriple(triple: ?[]const u8) bool {
return tripleContains(triple, "apple-ios");
}
fn hookGetTargetTriple(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.target_triple orelse "" };
}
fn hookIsMacOS(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (isIOSTriple(bc.target_triple)) return Value{ .boolean = false };
const t = bc.target_triple orelse "";
const is_mac = std.mem.indexOf(u8, t, "apple-macosx") != null or
std.mem.indexOf(u8, t, "apple-macos") != null or
std.mem.indexOf(u8, t, "apple-darwin") != null;
return Value{ .boolean = is_mac };
}
fn hookIsIOS(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .boolean = isIOSTriple(bc.target_triple) };
}
fn hookIsIOSDevice(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
const ios = isIOSTriple(bc.target_triple);
const sim = tripleContains(bc.target_triple, "simulator");
return Value{ .boolean = ios and !sim };
}
fn hookIsIOSSimulator(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
const ios = isIOSTriple(bc.target_triple);
const sim = tripleContains(bc.target_triple, "simulator");
return Value{ .boolean = ios and sim };
}
fn hookIsAndroid(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .boolean = tripleContains(bc.target_triple, "android") };
}
// ── Android-specific bundling setters + accessors ─────────────────────
fn hookSetManifestPath(interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.manifest_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookGetManifestPath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.manifest_path orelse "" };
}
fn hookSetKeystorePath(interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator) HookError!Value {
if (args.len < 2) return .void_val;
if (args[1].asString(interp)) |s| {
bc.keystore_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
}
return .void_val;
}
fn hookGetKeystorePath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .string = bc.keystore_path orelse "" };
}
// ── #jni_main emission accessors ──────────────────────────────────────
// The Android bundler walks these as `0..jni_main_count()` and reads
// each entry's `(runtime_path, java_source)` pair so it can write a
// `.java` file per decl, compile via javac, and produce classes.dex
// via d8 before zipping into the APK.
fn hookJniMainCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return Value{ .int = @intCast(bc.jni_main_runtime_paths.len) };
}
fn hookJniMainRuntimePathAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (args.len < 2) return Value{ .string = "" };
const idx = args[1].asInt() orelse return error.TypeError;
if (idx < 0 or @as(usize, @intCast(idx)) >= bc.jni_main_runtime_paths.len) return Value{ .string = "" };
return Value{ .string = bc.jni_main_runtime_paths[@intCast(idx)] };
}
fn hookJniMainJavaSourceAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (args.len < 2) return Value{ .string = "" };
const idx = args[1].asInt() orelse return error.TypeError;
if (idx < 0 or @as(usize, @intCast(idx)) >= bc.jni_main_java_sources.len) return Value{ .string = "" };
return Value{ .string = bc.jni_main_java_sources[@intCast(idx)] };
}
// ── Framework list accessors ──────────────────────────────────────────
// The Apple .app bundler in `library/modules/platform/bundle.sx` walks
// the framework list to recursively copy each `<Name>.framework`
// directory from the user's -F search paths into `<bundle>/Frameworks/`.
// Slice-of-string returns aren't natively expressible as a Value, so we
// expose count + indexed lookups instead.
fn intValue(n: i64) Value {
return Value{ .int = n };
}
fn hookFrameworkCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return intValue(@intCast(bc.target_frameworks.len));
}
fn hookFrameworkAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (args.len < 2) return Value{ .string = "" };
const idx_i64 = args[1].asInt() orelse return error.TypeError;
if (idx_i64 < 0 or @as(usize, @intCast(idx_i64)) >= bc.target_frameworks.len) {
return Value{ .string = "" };
}
return Value{ .string = bc.target_frameworks[@intCast(idx_i64)] };
}
fn hookFrameworkPathCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
return intValue(@intCast(bc.target_framework_paths.len));
}
fn hookFrameworkPathAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
if (args.len < 2) return Value{ .string = "" };
const idx_i64 = args[1].asInt() orelse return error.TypeError;
if (idx_i64 < 0 or @as(usize, @intCast(idx_i64)) >= bc.target_framework_paths.len) {
return Value{ .string = "" };
}
return Value{ .string = bc.target_framework_paths[@intCast(idx_i64)] };
}