diff --git a/src/context/abstractCast.ml b/src/context/abstractCast.ml index f9fe11507c8..51a3df0f39b 100644 --- a/src/context/abstractCast.ml +++ b/src/context/abstractCast.ml @@ -96,7 +96,13 @@ and do_check_cast ctx uctx tleft eright p = let monos = Monomorph.spawn_constrained_monos map cf.cf_params in unify_raise_custom native_unification_context eright.etype (map (apply_params cf.cf_params monos cf.cf_type)) p; 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; - eright + (* Wrap the function expression in a TCast to the SAM type so + the SAM TInst lives in the typed AST. Without this the + genjvm AST scan can't see SAM conversions in argument + position (TNew has no callee subexpression; the + constructor's parameter types are unreachable), and the + emitted closure would not implement the SAM interface. *) + mk_cast eright tleft p | _ -> raise Not_found end diff --git a/tests/misc/jvm/projects/StructuralSam/Main.hx b/tests/misc/jvm/projects/StructuralSam/Main.hx index dd855e8312c..47ec0efbaf9 100644 --- a/tests/misc/jvm/projects/StructuralSam/Main.hx +++ b/tests/misc/jvm/projects/StructuralSam/Main.hx @@ -5,6 +5,7 @@ import test.Listeners.Listeners_AbstractEqualsPlusOne; import test.Listeners.Listeners_WithDefaults; import test.Listeners.Listeners_StringMaker; import test.Listeners.Listeners_Unused; +import test.Listeners.Listeners_CtorSam; function main() { // Plain SAM — javac would accept the lambda directly. @@ -35,4 +36,28 @@ function main() { Listeners.runOnClick(cb, 99); trace(Std.isOfType(cb, Listeners_OnClick)); trace(Std.isOfType(cb, Listeners_Unused)); + + // Constructor-position SAM conversion with a bound instance-method + // reference — the exact shape that crashed RideAssist's + // `new TextToSpeech(this, onTtsInit)`. CtorOnly is never named anywhere + // in user code (no typed local, no field, no import, no explicit cast). + // The AST scan in genjvm.collect_used_functional_interfaces cannot + // discover this conversion: a TNew has no callee subexpression, so the + // constructor's parameter types (where CtorOnly's TInst lives) are never + // visited — and the scan deliberately skips extern classes. Only + // AbstractCast's writeback to functional_interfaces_used carries this + // information from typing to codegen. If that writeback regresses, the + // emitted closure won't implement CtorOnly and this `new` call will + // ClassCastException at runtime. + new Holder().run(); +} + +class Holder { + public function new() {} + public function run() { + new Listeners_CtorSam(onCtor, 42); + } + function onCtor(n:Int):Void { + Sys.println("ctor=" + n); + } } diff --git a/tests/misc/jvm/projects/StructuralSam/Setup.hx b/tests/misc/jvm/projects/StructuralSam/Setup.hx index 67bda0192cb..290d405def0 100644 --- a/tests/misc/jvm/projects/StructuralSam/Setup.hx +++ b/tests/misc/jvm/projects/StructuralSam/Setup.hx @@ -14,5 +14,8 @@ function main() { "test/Listeners$NotSam.class", "test/Listeners$StringMaker.class", "test/Listeners$Unused.class", - "test/Listeners$UnaryStringFn.class"]); + "test/Listeners$UnaryStringFn.class", + "test/Listeners$ArgOnly.class", + "test/Listeners$CtorOnly.class", + "test/Listeners$CtorSam.class"]); } diff --git a/tests/misc/jvm/projects/StructuralSam/compile.hxml.stdout b/tests/misc/jvm/projects/StructuralSam/compile.hxml.stdout index 09bc5568e2a..b31346241c0 100644 --- a/tests/misc/jvm/projects/StructuralSam/compile.hxml.stdout +++ b/tests/misc/jvm/projects/StructuralSam/compile.hxml.stdout @@ -1,12 +1,13 @@ click=7 -Main.hx:11: ok -Main.hx:14: v=3 -Main.hx:17: 25 -Main.hx:20: 11 -Main.hx:23: made-2 +Main.hx:12: ok +Main.hx:15: v=3 +Main.hx:18: 25 +Main.hx:21: 11 +Main.hx:24: made-2 ovl-click=1 -Main.hx:27: click -Main.hx:28: hi! +Main.hx:28: click +Main.hx:29: hi! bound-click=99 -Main.hx:36: true -Main.hx:37: false +Main.hx:37: true +Main.hx:38: false +ctor=42 diff --git a/tests/misc/jvm/projects/StructuralSam/project/test/Listeners.java b/tests/misc/jvm/projects/StructuralSam/project/test/Listeners.java index 21c640d1933..cbdcb48f393 100644 --- a/tests/misc/jvm/projects/StructuralSam/project/test/Listeners.java +++ b/tests/misc/jvm/projects/StructuralSam/project/test/Listeners.java @@ -52,6 +52,40 @@ public interface Unused { void onUnused(int id); } + // SAM exercised ONLY in argument position from Haxe — no typed local, no + // field signature, no explicit cast. The genjvm AST scan can't see this + // case: AbstractCast's SAM branch leaves the closure expression with its + // original TFun type (no TCast wrapper), so the only place ArgOnly's + // TInst exists is the extern callee's parameter signature, which the + // scan deliberately skips. Without AbstractCast's writeback to + // functional_interfaces_used, the emitted closure does not implement + // ArgOnly and the call ClassCastExceptions at runtime. + public interface ArgOnly { + void onArg(int n); + } + + public static String runArgOnly(ArgOnly cb, int n) { + cb.onArg(n); + return "arg-ok"; + } + + // Constructor-position SAM: a Haxe `new CtorSam(closure)` is a TNew + // expression in the typed AST. Unlike TCall (where the callee field has a + // TFun etype that exposes the parameter types to the scan), TNew has no + // callee subexpression — the only place CtorOnly's TInst would appear is + // on this extern's cl_constructor.cf_type, which the AST scan never + // visits. This is the exact shape that crashed RideAssist: + // `new TextToSpeech(this, onTtsInit)`. + public interface CtorOnly { + void onCtor(int n); + } + + public static class CtorSam { + public CtorSam(CtorOnly cb, int n) { + cb.onCtor(n); + } + } + public static String runOnClick(OnClick cb, int id) { cb.onClick(id); return "ok";