bundling: Android APK pipeline moved into sx; android.sx state-on-plat

Week 7 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
plus the android.sx refactor + three sx-compiler fixes hit along the way
to get chess on Pixel 7 Pro responding to touch end-to-end.

library/modules/platform/bundle.sx now covers the Android APK shape
alongside macOS / iOS-sim / iOS-device. `android_bundle_main` discovers
the SDK ($ANDROID_HOME / $ANDROID_SDK_ROOT / $HOME/Library/Android/sdk),
picks the highest-versioned build-tools + platforms via
`process.run("ls .. | sort -V | tail -1")`, stages
`<apk>.stage/lib/arm64-v8a/<libfoo.so>`, synthesizes
AndroidManifest.xml (NativeActivity vs `#jni_main` Activity branch),
writes each `#jni_main` decl's Java source under
`<stage>/java/<pkg>/<Cls>.java`, runs javac --release 11 + d8 to
produce classes.dex, aapt2-links the unaligned APK, appends lib/ +
classes.dex + each registered asset tree via zip, zipalign + ensure
debug keystore via keytool + apksigner sign.

Compiler-side accessors (src/ir/compiler_hooks.zig + library/modules/compiler.sx):
- is_android predicate.
- set_manifest_path / manifest_path + set_keystore_path / keystore_path.
- jni_main_count / jni_main_foreign_path_at(i) /
  jni_main_java_source_at(i) surface the `#jni_main` emissions that
  the Zig createApk previously consumed directly.
- main.zig wires manifest_path, keystore_path, and the per-decl
  (foreign_path, java_source) parallel slices into BuildConfig before
  invoking the post-link callback.

CLI `--apk <path>` keeps working as a transitional alias: it now feeds
bundle_path so the existing auto-`post_link_module = "platform.bundle"`
shim fires the same way as `--bundle`. main.zig no longer calls
target.createApk directly.

Deletions in src/target.zig: createApk, compileJniMainSources,
buildJniMainManifest, buildAndroidManifest, ensureDebugKeystore,
libNameFromSoBasename, plus helpers splitForeignPath / discoverJavac /
discoverAndroidSdk / findHighestSubdir / runProcess / runProcessIn
(~400 lines). git grep returns only the obituary comment.

library/modules/platform/android.sx refactor (chess Android dependency):
- Module-level globals retired (g_app_window, g_egl_*, g_viewport_*,
  g_dpi_scale, g_should_stop, g_render_thread*, g_user_main_fn,
  g_touch_*) → AndroidPlatform struct fields.
- All sx_android_* helpers take `plat: *AndroidPlatform` as first arg.
  Render thread receives plat via pthread_create's arg.
- New `logical_w: f32 = 0.0` field. Consumers set it before init() to
  define the design width in points; `recompute_scale` derives
  `dpi_scale = pixel_w / logical_w` (or 1.0 if unset). Called on
  init / set_viewport / egl_init. drain_touches divides incoming
  physical pixel coords by dpi_scale so chess sees logical-space
  positions matching its layout. Touch lands on the right squares.

Three sx-compiler bugs hit + fixed along the way:

1. Top-level `inline if OS == .X { decls }` body decls were silently
   dropped because scanDecls/lowerDecls had no .if_expr arm. New
   `flattenComptimeConditionals` pre-pass in src/imports.zig
   (threaded via ComptimeContext from core.zig) hoists matching arms
   recursively. Regression at examples/124-inline-if-hoist-toplevel.sx.

2. Parser rejected `#import` / `#framework` inside inline-if bodies
   because parseStmt in src/parser.zig only had arms for `#insert`.
   Added the missing arms. Regression at
   examples/123-inline-if-import-in-body.sx (landed earlier).

3. JNI `Call<T>Method` switches in src/ir/emit_llvm.zig (instance /
   nonvirtual / static) were missing `.f32` rows — jfloat returns
   (e.g. MotionEvent.getX/getY) fell into the silent-undef else arm.
   Chess's sx_android_push_touch(plat, getAction(), getX(), getY())
   delivered garbage f32 coords to the touch ring, so taps landed
   nowhere recognisable. Added `.f32 => Jni.Call{Static,Nonvirtual,}FloatMethod`
   rows to all three switches; lifted unsupported-type detection
   from emit_llvm into lowerForeignMethodCall with proper
   source-spanned diagnostics (`isJniReturnTypeSupported`). Regressions
   at examples/ffi-jni-call-10-jfloat-return.sx,
   examples/ffi-jni-class-09-multi-float-args.sx,
   examples/ffi-jni-call-11-unsupported-return-diag.sx.

Stale-snapshot drift in tests/expected/ffi-objc-call-03-selector-sharing.ir
and ffi-objc-call-06-sret-return.ir picks up the new BuildOptions
accessor extern decls (is_android, set_manifest_path,
set_keystore_path, jni_main_count, jni_main_foreign_path_at,
jni_main_java_source_at). Verified diff is dead-decl-only.

Chess on Pixel 7 Pro: tap on e2 white pawn -> yellow selection +
green dots on legal e3/e4 targets; tap on e4 -> board updates with
1. e4, "Black to move" + "1. e4" in info panel.

zig build && zig build test && bash tests/run_examples.sh -> 145/145
green. bash tests/cross_compile.sh -> 7/7 green.
This commit is contained in:
agra
2026-05-23 01:28:32 +03:00
parent 5cc62e63c3
commit 632e64512b
26 changed files with 1437 additions and 567 deletions

View File

@@ -138,6 +138,7 @@ pub const Interpreter = struct {
pub var last_bail_op: ?[]const u8 = null;
pub var last_bail_file: ?[]const u8 = null;
pub var last_bail_offset: u32 = 0;
pub var last_bail_builtin: ?[]const u8 = null;
pub fn init(module: *const Module, alloc: Allocator) Interpreter {
var hooks = compiler_hooks.Registry.init(alloc);
@@ -656,7 +657,12 @@ pub const Interpreter = struct {
const args = self.alloc.alloc(Value, c.args.len) catch return error.CannotEvalComptime;
defer self.alloc.free(args);
for (c.args, 0..) |ref, i| {
args[i] = frame.getRef(ref);
// Inline any slot_ptr field-refs in the caller's frame before
// the value crosses the call boundary. slot_ptr indices are
// frame-local; if a slice/aggregate carrying one is passed to
// the callee, the callee would later resolve the index against
// its own slot table and read garbage.
args[i] = self.materializeForCall(frame, frame.getRef(ref));
}
const result = try self.call(c.callee, args);
return .{ .value = result };
@@ -1218,6 +1224,38 @@ pub const Interpreter = struct {
// ── Slot chain resolution ────────────────────────────────────
/// Walk an aggregate Value and rewrite any embedded `slot_ptr` that points
/// to a field-ref slot in `frame` (the marker shape `{parent_slot, idx, ..}`
/// emitted by `struct_gep` / `index_gep`) into the resolved parent value.
/// Slot indices are frame-local; a slice passed across a call would otherwise
/// read its data_ptr out of the callee's slot table.
fn materializeForCall(self: *Interpreter, frame: *Frame, val: Value) Value {
switch (val) {
.aggregate => |fields| {
const new_fields = self.alloc.alloc(Value, fields.len) catch return val;
for (fields, 0..) |f, i| {
new_fields[i] = self.materializeForCall(frame, f);
}
return .{ .aggregate = new_fields };
},
.slot_ptr => |slot| {
const stored = frame.loadSlot(slot);
if (stored == .aggregate) {
const ref_fields = stored.aggregate;
if (ref_fields.len >= 2) {
const parent_slot_val = ref_fields[0].asInt() orelse return val;
if (ref_fields[1].asInt() == null) return val;
const parent_slot: u32 = @intCast(parent_slot_val);
const parent = frame.loadSlot(parent_slot);
return self.materializeForCall(frame, parent);
}
}
return val;
},
else => return val,
}
}
/// Follow a slot_ptr through field-pointer / index-gep chains
/// to get the underlying value. Handles nested dereferences.
fn resolveSlotChain(self: *Interpreter, frame: *Frame, val: Value) Value {
@@ -1354,6 +1392,14 @@ pub const Interpreter = struct {
// ── Builtin call dispatch ──────────────────────────────────────
fn execBuiltin(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame, _: TypeId) InterpError!ExecResult {
const result = self.execBuiltinInner(bi, frame) catch |err| {
if (last_bail_builtin == null) last_bail_builtin = @tagName(bi.builtin);
return err;
};
return result;
}
fn execBuiltinInner(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame) InterpError!ExecResult {
switch (bi.builtin) {
.malloc => {
const size_val = frame.getRef(bi.args[0]);
@@ -1378,10 +1424,16 @@ pub const Interpreter = struct {
.heap_ptr => |hp| hp,
else => return error.CannotEvalComptime,
};
// Get source bytes
const src_bytes: []const u8 = switch (src) {
.heap_ptr => |hp| self.heapSlice(hp) orelse return error.CannotEvalComptime,
.string => |s| s,
// Raw host address (e.g. a `*u8` returned by a foreign
// call like getenv). Read `len` bytes across the FFI
// boundary into the sx-managed dst.
.int => |addr| blk: {
const raw: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(addr)));
break :blk raw[0..len];
},
else => return error.CannotEvalComptime,
};
self.heapMemcpy(dst_hp, src_bytes, len);