From 20c767e33654ab623dae0e9180e3ed0202ff51b3 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 08:28:41 +0300 Subject: [PATCH] refactor(ir): move pure JNI helpers into jni_descriptor.zig (A6.2 step 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocate the two pure JNI decision helpers out of lower.zig into jni_descriptor.zig (already the JNI helper module), alongside the descriptor derivation. Behavior-preserving move — no facade, since neither takes *Lowering. - jniMangleNativeName(allocator, foreign_path, method_name) and isJniReturnTypeSupported(table, ret_ty) moved verbatim as pub free fns; added a types import + TypeId alias to jni_descriptor.zig. - Rerouted lower.zig's 2 call sites (synthesizeJniMainStub; the JNI return-type guard at lower.zig:6000) through jni_descriptor.* — lower.zig already imported the module. - Moved the 2 unit tests lower.test.zig -> jni_descriptor.test.zig (re-pointed to desc.*; a standalone TypeTable.init replaces the Module setup). Dropped the now-unused lower_mod alias. - Stayed in lower.zig per PLAN A6.2 step 5/6: jniMapParamType (trivial resolveType wrapper), synthesizeJniMainStub(s), lowerJniCall, lowerJniConstructor, lowerSuperCall, getJniEnvTlFids. Java rendering stays in jni_java_emit.zig. Phase A6 complete. Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (9 JNI .ir snapshots + 26 14xx examples green, no churn). --- src/ir/jni_descriptor.test.zig | 47 ++++++++++++++++++++++++++++++++ src/ir/jni_descriptor.zig | 47 ++++++++++++++++++++++++++++++++ src/ir/lower.test.zig | 49 ---------------------------------- src/ir/lower.zig | 49 ++-------------------------------- 4 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/ir/jni_descriptor.test.zig b/src/ir/jni_descriptor.test.zig index 85b269e..13c8551 100644 --- a/src/ir/jni_descriptor.test.zig +++ b/src/ir/jni_descriptor.test.zig @@ -352,3 +352,50 @@ test "deriveMethod with slice param" { defer a.free(out); try std.testing.expectEqualStrings("([B)I", out); } + +// ── A6.2: native-name mangling + return-type dispatchability ───────── + +const types = @import("types.zig"); + +test "jniMangleNativeName mangles package path + method (/ -> _, _ -> _1)" { + const alloc = std.testing.allocator; + + // Plain path + method: `/` separators collapse to `_`, `Java_` prefix, + // `_sx_1` infix before the (mangled) method name. + const m1 = try desc.jniMangleNativeName(alloc, "com/sx/App", "tick"); + defer alloc.free(m1); + try std.testing.expectEqualStrings("Java_com_sx_App_sx_1tick", m1); + + // Underscores in BOTH the path and the method escape to `_1` (so the JNI + // resolver can round-trip them), distinct from the `/`->`_` separator. + const m2 = try desc.jniMangleNativeName(alloc, "a_b/C", "do_it"); + defer alloc.free(m2); + try std.testing.expectEqualStrings("Java_a_1b_C_sx_1do_1it", m2); +} + +test "isJniReturnTypeSupported accepts the dispatchable set + pointers only" { + const alloc = std.testing.allocator; + var table = types.TypeTable.init(alloc); + defer table.deinit(); + const t = &table; + + // The CallMethod-dispatchable primitives. + inline for (.{ types.TypeId.void, types.TypeId.bool, types.TypeId.s32, types.TypeId.s64, types.TypeId.f32, types.TypeId.f64 }) |ty| { + try std.testing.expect(desc.isJniReturnTypeSupported(t, ty)); + } + + // Other primitive widths are NOT dispatchable (would hit emit_llvm's + // undef-producing `else` arm — the footgun this predicate guards). + inline for (.{ types.TypeId.s8, types.TypeId.s16, types.TypeId.u8, types.TypeId.u32, types.TypeId.u64 }) |ty| { + try std.testing.expect(!desc.isJniReturnTypeSupported(t, ty)); + } + + // Pointer / many-pointer returns route through CallObjectMethod → true. + try std.testing.expect(desc.isJniReturnTypeSupported(t, table.ptrTo(.void))); + try std.testing.expect(desc.isJniReturnTypeSupported(t, table.manyPtrTo(.u8))); + + // A pass-by-value struct return is unsupported. + const sname = table.internString("CGRectish"); + const sty = table.intern(.{ .@"struct" = .{ .name = sname, .fields = &.{} } }); + try std.testing.expect(!desc.isJniReturnTypeSupported(t, sty)); +} diff --git a/src/ir/jni_descriptor.zig b/src/ir/jni_descriptor.zig index 2d51278..4dd2a80 100644 --- a/src/ir/jni_descriptor.zig +++ b/src/ir/jni_descriptor.zig @@ -24,8 +24,10 @@ const std = @import("std"); const ast = @import("../ast.zig"); +const types = @import("types.zig"); const Node = ast.Node; +const TypeId = types.TypeId; pub const DeriveError = error{ UnknownPrimitive, @@ -149,3 +151,48 @@ fn primitiveChar(name: []const u8) ?u8 { } return null; } + +/// Whether emit_llvm's `jni_msg_send` lowering can dispatch a CallMethod +/// for this return type. Anything outside this set falls into the `else` +/// arm of the switches in `emit_llvm.zig` and would silently produce +/// `LLVMGetUndef` — a footgun that previously shipped (chess Android touch +/// went undef because `MotionEvent.getX() -> f32` wasn't in the switch). +/// Pointer-typed returns route through `CallObjectMethod`. +pub fn isJniReturnTypeSupported(table: *const types.TypeTable, ret_ty: TypeId) bool { + return switch (ret_ty) { + .void, .bool, .s32, .s64, .f32, .f64 => true, + else => blk: { + if (ret_ty.isBuiltin()) break :blk false; + const info = table.get(ret_ty); + break :blk info == .pointer or info == .many_pointer; + }, + }; +} + +/// Encode a (foreign_path, method_name) pair as the JNI-resolved symbol +/// `Java___sx_1`. JNI mangling: +/// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side +/// `private native sx_(...)` delegate. +pub fn jniMangleNativeName(allocator: std.mem.Allocator, foreign_path: []const u8, method_name: []const u8) ![]u8 { + var buf = std.ArrayList(u8).empty; + try buf.appendSlice(allocator, "Java_"); + for (foreign_path) |ch| { + if (ch == '/') { + try buf.append(allocator, '_'); + } else if (ch == '_') { + try buf.appendSlice(allocator, "_1"); + } else { + try buf.append(allocator, ch); + } + } + try buf.append(allocator, '_'); + try buf.appendSlice(allocator, "sx_1"); + for (method_name) |ch| { + if (ch == '_') { + try buf.appendSlice(allocator, "_1"); + } else { + try buf.append(allocator, ch); + } + } + return buf.toOwnedSlice(allocator); +} diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 263bb4f..f957eb8 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -765,55 +765,6 @@ test "lower: objcPropertyKind defaults + explicit ARC modifiers" { try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "raw", .field_type = obj_ty, .is_property = true, .property_modifiers = &assign_mods }) == .assign); } -// ── A6.2 scaffolding: pure JNI decision helpers ───────────────────── -// Lock JNI native-name mangling and return-type support before they move -// out of lower.zig (to the JNI module) in A6.2 sub-step 2. - -const lower_mod = @import("lower.zig"); - -test "lower: jniMangleNativeName mangles package path + method (/ -> _, _ -> _1)" { - const alloc = std.testing.allocator; - - // Plain path + method: `/` separators collapse to `_`, `Java_` prefix, - // `_sx_1` infix before the (mangled) method name. - const m1 = try lower_mod.jniMangleNativeName(alloc, "com/sx/App", "tick"); - defer alloc.free(m1); - try std.testing.expectEqualStrings("Java_com_sx_App_sx_1tick", m1); - - // Underscores in BOTH the path and the method escape to `_1` (so the JNI - // resolver can round-trip them), distinct from the `/`->`_` separator. - const m2 = try lower_mod.jniMangleNativeName(alloc, "a_b/C", "do_it"); - defer alloc.free(m2); - try std.testing.expectEqualStrings("Java_a_1b_C_sx_1do_1it", m2); -} - -test "lower: isJniReturnTypeSupported accepts the dispatchable set + pointers only" { - const alloc = std.testing.allocator; - var module = ir_mod.Module.init(alloc); - defer module.deinit(); - const t = &module.types; - - // The CallMethod-dispatchable primitives. - inline for (.{ TypeId.void, TypeId.bool, TypeId.s32, TypeId.s64, TypeId.f32, TypeId.f64 }) |ty| { - try std.testing.expect(lower_mod.isJniReturnTypeSupported(t, ty)); - } - - // Other primitive widths are NOT dispatchable (would hit emit_llvm's - // undef-producing `else` arm — the footgun this predicate guards). - inline for (.{ TypeId.s8, TypeId.s16, TypeId.u8, TypeId.u32, TypeId.u64 }) |ty| { - try std.testing.expect(!lower_mod.isJniReturnTypeSupported(t, ty)); - } - - // Pointer / many-pointer returns route through CallObjectMethod → true. - try std.testing.expect(lower_mod.isJniReturnTypeSupported(t, module.types.ptrTo(.void))); - try std.testing.expect(lower_mod.isJniReturnTypeSupported(t, module.types.manyPtrTo(.u8))); - - // A pass-by-value struct return is unsupported. - const sname = module.types.internString("CGRectish"); - const sty = module.types.intern(.{ .@"struct" = .{ .name = sname, .fields = &.{} } }); - try std.testing.expect(!lower_mod.isJniReturnTypeSupported(t, sty)); -} - // ── Pack projection name resolution (Feature 1, Step 2.2) ──────────── const errors = @import("../errors.zig"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index e8da498..6a84203 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -5997,7 +5997,7 @@ pub const Lowering = struct { // would silently lower to LLVMGetUndef and produce wrong arguments at // the call site (chess Android touch shipped broken because s32→s32+ // f32 returns hit the undef path before .f32 was wired up). - if (!isJniReturnTypeSupported(&self.module.types, ret_ty)) { + if (!jni_descriptor.isJniReturnTypeSupported(&self.module.types, ret_ty)) { if (self.diagnostics) |d| { d.addFmt(.err, span, "JNI method '{s}.{s}' returns '{s}', which isn't supported by the JNI call-method lowering yet — only void/bool/s32/s64/f32/f64 and pointers are wired up", .{ fcd.name, method.name, self.module.types.typeName(ret_ty) }); } @@ -16445,7 +16445,7 @@ pub const Lowering = struct { } fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void { - const mangled = jniMangleNativeName(self.alloc, fcd.foreign_path, md.name) catch return; + const mangled = jni_descriptor.jniMangleNativeName(self.alloc, fcd.foreign_path, md.name) catch return; const name_id = self.module.types.internString(mangled); const ptr_void = self.module.types.ptrTo(.void); @@ -16568,48 +16568,3 @@ pub const Lowering = struct { fn jniMapParamType(self: *Lowering, type_node: *ast.Node) TypeId { return self.resolveType(type_node); } - -/// Whether emit_llvm's `jni_msg_send` lowering can dispatch a CallMethod -/// for this return type. Anything outside this set falls into the `else` -/// arm of the switches in `emit_llvm.zig` and would silently produce -/// `LLVMGetUndef` — a footgun that previously shipped (chess Android touch -/// went undef because `MotionEvent.getX() -> f32` wasn't in the switch). -/// Pointer-typed returns route through `CallObjectMethod`. -pub fn isJniReturnTypeSupported(table: *const @import("types.zig").TypeTable, ret_ty: TypeId) bool { - return switch (ret_ty) { - .void, .bool, .s32, .s64, .f32, .f64 => true, - else => blk: { - if (ret_ty.isBuiltin()) break :blk false; - const info = table.get(ret_ty); - break :blk info == .pointer or info == .many_pointer; - }, - }; -} - -/// Encode a (foreign_path, method_name) pair as the JNI-resolved symbol -/// `Java___sx_1`. JNI mangling: -/// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side -/// `private native sx_(...)` delegate. -pub fn jniMangleNativeName(allocator: std.mem.Allocator, foreign_path: []const u8, method_name: []const u8) ![]u8 { - var buf = std.ArrayList(u8).empty; - try buf.appendSlice(allocator, "Java_"); - for (foreign_path) |ch| { - if (ch == '/') { - try buf.append(allocator, '_'); - } else if (ch == '_') { - try buf.appendSlice(allocator, "_1"); - } else { - try buf.append(allocator, ch); - } - } - try buf.append(allocator, '_'); - try buf.appendSlice(allocator, "sx_1"); - for (method_name) |ch| { - if (ch == '_') { - try buf.appendSlice(allocator, "_1"); - } else { - try buf.append(allocator, ch); - } - } - return buf.toOwnedSlice(allocator); -}