Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/context/abstractCast.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions tests/misc/jvm/projects/StructuralSam/Main.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
5 changes: 4 additions & 1 deletion tests/misc/jvm/projects/StructuralSam/Setup.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
}
19 changes: 10 additions & 9 deletions tests/misc/jvm/projects/StructuralSam/compile.hxml.stdout
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions tests/misc/jvm/projects/StructuralSam/project/test/Listeners.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading