From ca840ff6c8674291b2c1b5fdd24ce880f8c1f112 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 20 May 2026 10:27:33 +0300 Subject: [PATCH] ffi 2.10: `#jni_method_descriptor` override precedence in deriveMethod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a `ForeignMethodDecl` carries a non-null `jni_descriptor_override` (parsed in step 2.6), `deriveMethod` short-circuits — auto-derivation is skipped entirely and the override is returned (duplicated through the caller's allocator so ownership semantics stay uniform regardless of which branch ran). Two new tests: override beats normal derivation, and override bypasses cross-class refs that would otherwise fail with `UnknownClassAlias`. Confirms the escape-hatch semantics from 2.6 — users can paste an explicit JNI signature when auto-derivation doesn't match (synthetic methods, ambiguous overloads, unknown-to-sx JVM internals). 15 jni_descriptor tests pass; 126/126 examples still green. --- src/ir/jni_descriptor.test.zig | 52 ++++++++++++++++++++++++++++++++++ src/ir/jni_descriptor.zig | 7 +++++ 2 files changed, 59 insertions(+) diff --git a/src/ir/jni_descriptor.test.zig b/src/ir/jni_descriptor.test.zig index cadf9a8..21d0a83 100644 --- a/src/ir/jni_descriptor.test.zig +++ b/src/ir/jni_descriptor.test.zig @@ -160,6 +160,58 @@ test "cross-class *Foo with empty registry errors" { try std.testing.expectError(desc.DeriveError.UnknownClassAlias, result); } +test "deriveMethod respects #jni_method_descriptor override verbatim" { + const a = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(a); + defer arena.deinit(); + const aa = arena.allocator(); + + // The actual sx signature `(self: *Self) -> s32` would derive to + // `()I`. The override should win regardless. + const self_te = try makeTypeExpr(aa, "Self"); + const self_ptr = try makePointer(aa, self_te); + const ret = try makeTypeExpr(aa, "s32"); + + const method: ast.ForeignMethodDecl = .{ + .name = "weirdMethod", + .params = &.{self_ptr}, + .param_names = &.{"self"}, + .return_type = ret, + .is_static = false, + .jni_descriptor_override = "(Ljava/lang/Object;)Ljava/util/List;", + }; + const out = try desc.deriveMethod(a, .{ .enclosing_path = "com/example/Foo" }, method); + defer a.free(out); + try std.testing.expectEqualStrings("(Ljava/lang/Object;)Ljava/util/List;", out); +} + +test "deriveMethod override bypasses unresolvable cross-class refs" { + const a = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(a); + defer arena.deinit(); + const aa = arena.allocator(); + + // The signature references `*UnknownClass` that isn't in the + // registry — would normally fail with `UnknownClassAlias`. The + // override short-circuits derivation, so it succeeds. + const self_te = try makeTypeExpr(aa, "Self"); + const self_ptr = try makePointer(aa, self_te); + const unknown = try makeTypeExpr(aa, "UnknownClass"); + const unknown_ptr = try makePointer(aa, unknown); + + const method: ast.ForeignMethodDecl = .{ + .name = "weirdMethod", + .params = &.{self_ptr}, + .param_names = &.{"self"}, + .return_type = unknown_ptr, + .is_static = false, + .jni_descriptor_override = "()V", + }; + const out = try desc.deriveMethod(a, .{ .enclosing_path = "com/example/Foo" }, method); + defer a.free(out); + try std.testing.expectEqualStrings("()V", out); +} + test "deriveMethod chains *Foo returns and params" { const a = std.testing.allocator; var arena = std.heap.ArenaAllocator.init(a); diff --git a/src/ir/jni_descriptor.zig b/src/ir/jni_descriptor.zig index 0eafd5c..c0b2085 100644 --- a/src/ir/jni_descriptor.zig +++ b/src/ir/jni_descriptor.zig @@ -105,6 +105,13 @@ pub fn deriveMethod( ctx: Context, method: ast.ForeignMethodDecl, ) DeriveError![]u8 { + // `#jni_method_descriptor("(Sig)Ret")` short-circuits derivation + // entirely. Allocate a copy so the caller has uniform ownership + // semantics regardless of which branch ran. + if (method.jni_descriptor_override) |override| { + return allocator.dupe(u8, override); + } + var buf: std.ArrayList(u8) = .empty; errdefer buf.deinit(allocator);