Skip to content

Commit 83a3da2

Browse files
committed
[jvm] fix self-recursing Object bridge for closures with non-Object typed return
When a closure's typed return is a specific reference type (e.g. SyncStats) and a functional interface in the conversion set has an Object-erased abstract method also named `invoke` — the shape of Function0<T>, Callable<V> after erasure — the FI loop in jvmFunctions.generate_invoke synthesized an `Object invoke()` bridge but pointed it at (meth.name, meth.dargs, meth.dret). At that program point meth came from register_signature with dret already declassified to Object, so the bridge body emitted `invokevirtual invoke()Object` against itself. The later `loop meth_typed` block that would have produced the proper Object→typed bridge was then short-circuited by has_method (the slot was already taken). First invocation StackOverflowError'd. Forward to (meth.name, arg_sigs, ret) — the typed invoke that was actually spawned earlier in generate_invoke — so the Object-returning bridge dispatches to the real implementation and casts the result on return. Repro shape (also added as a misc/jvm test extending StructuralSam): public interface GenericInvoke<T> { T invoke(); } Listeners.runGenericInvoke(() -> new Boxed(7));
1 parent 52e31ac commit 83a3da2

5 files changed

Lines changed: 60 additions & 11 deletions

File tree

src/generators/jvm/jvmFunctions.ml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,15 @@ class typed_function
411411
let msig = method_sig jfi.jargs jfi.jret in
412412
if not (jc_closure#has_method jfi.jname msig) then begin
413413
let jm_invoke_next = spawn_invoke_next jfi.jname msig false in
414-
functions#make_forward_method_jsig jc_closure jm_invoke_next meth.name jfi.jargs jfi.jret meth.dargs meth.dret
414+
(* Forward to the typed invoke (arg_sigs, ret) rather than the
415+
Object-declassified (meth.dargs, meth.dret). When the FI's
416+
own method is also named "invoke" with an erased Object
417+
return (Function0<T>, Callable<V>, …), forwarding to
418+
meth.dret would emit `invokevirtual invoke()Object` against
419+
this very bridge and self-recurse — the later loop that
420+
would have synthesized the Object→typed bridge is then
421+
short-circuited by has_method. *)
422+
functions#make_forward_method_jsig jc_closure jm_invoke_next meth.name jfi.jargs jfi.jret arg_sigs ret
415423
end
416424
) functional_interfaces;
417425
let rec loop meth =

tests/misc/jvm/projects/StructuralSam/Main.hx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import test.Listeners.Listeners_WithDefaults;
66
import test.Listeners.Listeners_StringMaker;
77
import test.Listeners.Listeners_Unused;
88
import test.Listeners.Listeners_CtorSam;
9+
import test.Listeners.Listeners_Boxed;
910

1011
function main() {
1112
// Plain SAM — javac would accept the lambda directly.
@@ -50,6 +51,20 @@ function main() {
5051
// emitted closure won't implement CtorOnly and this `new` call will
5152
// ClassCastException at runtime.
5253
new Holder().run();
54+
55+
// Generic SAM whose abstract method is `T invoke()` — same shape as
56+
// kotlin.jvm.functions.Function0<T>. The closure's typed invoke returns
57+
// Boxed (a specific class), but the FI bridge spawned for the erased
58+
// `Object invoke()` slot was forwarded to (meth.name, meth.dargs,
59+
// meth.dret) where meth.dret had already been declassified to Object — so
60+
// the bridge body invoked itself rather than the typed invoke, and the
61+
// later loop that would have emitted the proper Object→Boxed bridge was
62+
// short-circuited by has_method. Calling cb.invoke() from Java therefore
63+
// went into infinite recursion (StackOverflowError) instead of returning
64+
// the Boxed instance. RideAssist crash repro: a `() -> SyncStats` closure
65+
// passed to `new Thread(...)` against a classpath carrying both
66+
// kotlin Function0<SyncStats> and j.u.c.Callable<SyncStats>.
67+
trace(Listeners.runGenericInvoke(() -> new Listeners_Boxed(7)));
5368
}
5469

5570
class Holder {

tests/misc/jvm/projects/StructuralSam/Setup.hx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ function main() {
1717
"test/Listeners$UnaryStringFn.class",
1818
"test/Listeners$ArgOnly.class",
1919
"test/Listeners$CtorOnly.class",
20-
"test/Listeners$CtorSam.class"]);
20+
"test/Listeners$CtorSam.class",
21+
"test/Listeners$GenericInvoke.class",
22+
"test/Listeners$Boxed.class"]);
2123
}
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
click=7
2-
Main.hx:12: ok
3-
Main.hx:15: v=3
4-
Main.hx:18: 25
5-
Main.hx:21: 11
6-
Main.hx:24: made-2
2+
Main.hx:13: ok
3+
Main.hx:16: v=3
4+
Main.hx:19: 25
5+
Main.hx:22: 11
6+
Main.hx:25: made-2
77
ovl-click=1
8-
Main.hx:28: click
9-
Main.hx:29: hi!
8+
Main.hx:29: click
9+
Main.hx:30: hi!
1010
bound-click=99
11-
Main.hx:37: true
12-
Main.hx:38: false
11+
Main.hx:38: true
12+
Main.hx:39: false
1313
ctor=42
14+
Main.hx:67: boxed=7

tests/misc/jvm/projects/StructuralSam/project/test/Listeners.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,27 @@ public static String overloaded(OnClick cb, int id) {
123123
public static String overloaded(UnaryStringFn cb, String s) {
124124
return cb.apply(s);
125125
}
126+
127+
// Generic SAM whose abstract method is named `invoke` and whose erased
128+
// return type is Object — the shape of kotlin.jvm.functions.Function0<T>
129+
// and similar interfaces. Probes a codegen bug in
130+
// jvmFunctions.generate_invoke: the FI loop synthesizes a bridge for
131+
// `Object invoke()` and forwards it to (meth.name, meth.dargs, meth.dret),
132+
// but meth.dret is Object-declassified at that point — so the bridge body
133+
// becomes `invokevirtual invoke()Object` against itself, and the later
134+
// loop that would have emitted the proper Object→typed bridge is
135+
// short-circuited by has_method. The first call StackOverflowErrors.
136+
public interface GenericInvoke<T> {
137+
T invoke();
138+
}
139+
140+
public static String runGenericInvoke(GenericInvoke<Boxed> cb) {
141+
return cb.invoke().describe();
142+
}
143+
144+
public static class Boxed {
145+
public final int value;
146+
public Boxed(int value) { this.value = value; }
147+
public String describe() { return "boxed=" + value; }
148+
}
126149
}

0 commit comments

Comments
 (0)