Skip to content

Commit 2c58f5b

Browse files
authored
[jvm] Fix SAM (#12903)
* [jvm] restore functional_interfaces_used; AST scan misses argument-position SAMs Revert the AbstractCast/Common/Gctx parts of a134fd8. That commit dropped the functional_interfaces_used hashtbl on the claim that the genjvm AST scan in collect_used_functional_interfaces "strictly subsumes" the AbstractCast writeback. It does not. AbstractCast.do_check_cast's SAM branch unifies eright.etype against the SAM signature and returns eright **unchanged** — no TCast wrapper. So when a function expression is passed where a Java SAM is expected, the SAM TInst never appears in the typed AST. The only place it exists is on the extern callee's signature, which the scan deliberately skips (`if not (has_class_flag c CExtern)`) to keep classpath noise out. For TCall sites this is masked: the callee TField has a TFun etype whose parameter types are visited, so the SAM TInst is reached transitively. But TNew has no callee subexpression — the constructor's parameter types live on c.cl_constructor.cf_type, which Type.iter never visits. A bound instance-method reference passed to a Java extern's constructor (e.g. `new TextToSpeech(this, onTtsInit)` against android.jar) therefore produces a closure that does not implement the SAM interface, hitting ClassCastException at runtime. Restore the field on Common.context + Gctx.t (shared in Common.clone), the AbstractCast writeback keyed by @:native path, and seed collect_used_functional_interfaces's `used` hashtbl from it before the AST scan runs. * [tests] StructuralSam: cover constructor-position SAM with method reference The existing cases either bind to a typed local (SAM TInst lands in the AST directly) or call a static method (TCall callee's TField has a TFun etype that exposes parameter types to the scan). Neither exercises the path that AbstractCast's functional_interfaces_used writeback is needed for: a bound instance-method reference passed to a Java extern's constructor, with the SAM type never named anywhere else in user code. Add CtorOnly + CtorSam to test.Listeners and a Holder class in Main.hx that does `new Listeners_CtorSam(onCtor, 42)`. Without the writeback the emitted closure does not implement CtorOnly and the JVM throws ClassCastException at runtime — same shape as RideAssist's `new TextToSpeech(this, onTtsInit)` crash against android.jar. * [tests] update traces positions * [jvm] AbstractCast: wrap SAM conversion with TCast to surface SAM type in AST Replace the functional_interfaces_used hashtbl plumbing (Common.context + Gctx.t + AbstractCast writeback + genjvm seeding) with a one-line change in AbstractCast: wrap the function expression in mk_cast — a TCast(eright, None) with the SAM type as the cast's etype. The SAM TInst now lives in the typed AST at every conversion site, so genjvm's existing collect_used_functional_interfaces scan finds it naturally via note_fi_in_type on the TCast's etype. No cross-module mutable state, no typing↔codegen back-channel, no @:native path-rewrite bridging. mk_cast is already the established pattern in this file — it's what the MultiType abstract path uses a few lines up.
1 parent dce7adf commit 2c58f5b

5 files changed

Lines changed: 80 additions & 11 deletions

File tree

src/context/abstractCast.ml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,13 @@ and do_check_cast ctx uctx tleft eright p =
9696
let monos = Monomorph.spawn_constrained_monos map cf.cf_params in
9797
unify_raise_custom native_unification_context eright.etype (map (apply_params cf.cf_params monos cf.cf_type)) p;
9898
if has_mono tright then raise_typing_error ("Cannot use this function as a functional interface because it has unknown types: " ^ (s_type (print_context()) tright)) p;
99-
eright
99+
(* Wrap the function expression in a TCast to the SAM type so
100+
the SAM TInst lives in the typed AST. Without this the
101+
genjvm AST scan can't see SAM conversions in argument
102+
position (TNew has no callee subexpression; the
103+
constructor's parameter types are unreachable), and the
104+
emitted closure would not implement the SAM interface. *)
105+
mk_cast eright tleft p
100106
| _ ->
101107
raise Not_found
102108
end

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import test.Listeners.Listeners_AbstractEqualsPlusOne;
55
import test.Listeners.Listeners_WithDefaults;
66
import test.Listeners.Listeners_StringMaker;
77
import test.Listeners.Listeners_Unused;
8+
import test.Listeners.Listeners_CtorSam;
89

910
function main() {
1011
// Plain SAM — javac would accept the lambda directly.
@@ -35,4 +36,28 @@ function main() {
3536
Listeners.runOnClick(cb, 99);
3637
trace(Std.isOfType(cb, Listeners_OnClick));
3738
trace(Std.isOfType(cb, Listeners_Unused));
39+
40+
// Constructor-position SAM conversion with a bound instance-method
41+
// reference — the exact shape that crashed RideAssist's
42+
// `new TextToSpeech(this, onTtsInit)`. CtorOnly is never named anywhere
43+
// in user code (no typed local, no field, no import, no explicit cast).
44+
// The AST scan in genjvm.collect_used_functional_interfaces cannot
45+
// discover this conversion: a TNew has no callee subexpression, so the
46+
// constructor's parameter types (where CtorOnly's TInst lives) are never
47+
// visited — and the scan deliberately skips extern classes. Only
48+
// AbstractCast's writeback to functional_interfaces_used carries this
49+
// information from typing to codegen. If that writeback regresses, the
50+
// emitted closure won't implement CtorOnly and this `new` call will
51+
// ClassCastException at runtime.
52+
new Holder().run();
53+
}
54+
55+
class Holder {
56+
public function new() {}
57+
public function run() {
58+
new Listeners_CtorSam(onCtor, 42);
59+
}
60+
function onCtor(n:Int):Void {
61+
Sys.println("ctor=" + n);
62+
}
3863
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ function main() {
1414
"test/Listeners$NotSam.class",
1515
"test/Listeners$StringMaker.class",
1616
"test/Listeners$Unused.class",
17-
"test/Listeners$UnaryStringFn.class"]);
17+
"test/Listeners$UnaryStringFn.class",
18+
"test/Listeners$ArgOnly.class",
19+
"test/Listeners$CtorOnly.class",
20+
"test/Listeners$CtorSam.class"]);
1821
}
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
click=7
2-
Main.hx:11: ok
3-
Main.hx:14: v=3
4-
Main.hx:17: 25
5-
Main.hx:20: 11
6-
Main.hx:23: made-2
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
77
ovl-click=1
8-
Main.hx:27: click
9-
Main.hx:28: hi!
8+
Main.hx:28: click
9+
Main.hx:29: hi!
1010
bound-click=99
11-
Main.hx:36: true
12-
Main.hx:37: false
11+
Main.hx:37: true
12+
Main.hx:38: false
13+
ctor=42

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,40 @@ public interface Unused {
5252
void onUnused(int id);
5353
}
5454

55+
// SAM exercised ONLY in argument position from Haxe — no typed local, no
56+
// field signature, no explicit cast. The genjvm AST scan can't see this
57+
// case: AbstractCast's SAM branch leaves the closure expression with its
58+
// original TFun type (no TCast wrapper), so the only place ArgOnly's
59+
// TInst exists is the extern callee's parameter signature, which the
60+
// scan deliberately skips. Without AbstractCast's writeback to
61+
// functional_interfaces_used, the emitted closure does not implement
62+
// ArgOnly and the call ClassCastExceptions at runtime.
63+
public interface ArgOnly {
64+
void onArg(int n);
65+
}
66+
67+
public static String runArgOnly(ArgOnly cb, int n) {
68+
cb.onArg(n);
69+
return "arg-ok";
70+
}
71+
72+
// Constructor-position SAM: a Haxe `new CtorSam(closure)` is a TNew
73+
// expression in the typed AST. Unlike TCall (where the callee field has a
74+
// TFun etype that exposes the parameter types to the scan), TNew has no
75+
// callee subexpression — the only place CtorOnly's TInst would appear is
76+
// on this extern's cl_constructor.cf_type, which the AST scan never
77+
// visits. This is the exact shape that crashed RideAssist:
78+
// `new TextToSpeech(this, onTtsInit)`.
79+
public interface CtorOnly {
80+
void onCtor(int n);
81+
}
82+
83+
public static class CtorSam {
84+
public CtorSam(CtorOnly cb, int n) {
85+
cb.onCtor(n);
86+
}
87+
}
88+
5589
public static String runOnClick(OnClick cb, int id) {
5690
cb.onClick(id);
5791
return "ok";

0 commit comments

Comments
 (0)