refactor(ir): move pure JNI helpers into jni_descriptor.zig (A6.2 step 2)
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).
This commit is contained in:
@@ -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 Call<T>Method-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));
|
||||
}
|
||||
|
||||
@@ -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 Call<T>Method
|
||||
/// 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_<pkg-mangled>_<Class>_sx_1<method-mangled>`. JNI mangling:
|
||||
/// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side
|
||||
/// `private native sx_<name>(...)` 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);
|
||||
}
|
||||
|
||||
@@ -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 Call<T>Method-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");
|
||||
|
||||
@@ -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 Call<T>Method
|
||||
/// 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_<pkg-mangled>_<Class>_sx_1<method-mangled>`. JNI mangling:
|
||||
/// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side
|
||||
/// `private native sx_<name>(...)` 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user