Skip to content

Commit dce7adf

Browse files
authored
[jvm] auto-detect Java SAM interfaces; honor SAM in overload resolution (#12901)
* [jvm] auto-detect Java SAM interfaces; honor SAM in overload resolution Two related changes that together make Haxe match javac's lambda-vs-SAM conversion rules on the JVM target. 1. Structural SAM detection in javaModern.ml Previously, only interfaces carrying @FunctionalInterface were tagged CFunctionalInterface and so eligible for implicit conversion from a function value. The annotation is documentation in javac too — what makes a type lambda-convertible is having exactly one abstract instance method, per JLS §9.8. The Android SDK rarely annotates its listeners, which forced every consumer of the JVM target to hand-roll adapter classes for View.OnClickListener, DialogInterface.OnClickListener, MediaPlayer.OnCompletionListener and so on. This patch auto-tags any .jar-imported interface as @:functionalInterface when it has exactly one abstract instance method, excluding: - static / default / private / synthetic methods, - constructors, - Object members re-declared as abstract (equals/hashCode/toString, matched by name+arity since their signatures on j.l.Object are unambiguous — JLS §9.8 excludes these from the SAM count). The explicit @FunctionalInterface annotation path is preserved. 2. SAM-aware overload candidate filtering overloadResolution.ml's unify_cf used strict Type.unify to filter candidates before the cast layer could fire. A function value passed to an overload whose parameter is a SAM class would be discarded at the filter step. Now, when strict unify fails, the filter retries via the SAM conversion path — unifying the function's TFun against the SAM method's signature, mirroring what AbstractCast does at cast time. Only candidates whose SAM signature actually matches the function shape survive, so this isn't a blind accept. Two supporting fixes round this out: - overloads.ml rate_conv: added a TInst(SAM), TFun case so specificity ranking works when two SAM-parametered overloads both accept the same function. The rate descends through the SAM method's signature rather than raising Not_found, which previously discarded the candidate during reduce_compatible. - tOther.ml get_singular_interface_field and the parallel walker in genjvm.ml's check_functional_interface now exclude equals/hashCode/ toString by name+arity, matching the new exclusion in javaModern so interfaces like Comparator (abstract equals re-declared) — and the Android SDK's various 'override equals' interfaces — are still detected as SAMs at both typer and codegen stages. Tests: - tests/unit/.../IssueFunctionalInterfaceOverload.hx exercises the overload disambiguation path with two @:functionalInterface interfaces of disjoint shape (Int,Int -> Int vs String -> String). - tests/misc/jvm/projects/StructuralSam/ ships a .java file with six interface shapes — plain SAM, abstract-equals re-declaration, default/static method skipping, multi-abstract non-SAM, generic-arg SAM, and overloaded call sites — none annotated @FunctionalInterface. Runs the generated jar end-to-end to verify both type-checking and runtime closure-adapter dispatch. Limitations matching javac: - Abstract-class SAMs (e.g. android.content.BroadcastReceiver) remain unsupported; javac doesn't allow lambda conversion to those either. - Two SAM-parametered overloads where both signatures would accept the same function value remain ambiguous and require explicit disambiguation. * [jvm] skip JDK-internal packages in structural SAM detection Auto-tagging every structurally-SAM interface as @:functionalInterface swept in sun.reflect.ConstructorAccessor, sun.nio.ch.Interruptible, sun.reflect.generics.tree.TypeTree and similar JDK-internal classes. The existing JFI matcher then made every signature-matching closure declare `implements sun.reflect.ConstructorAccessor`, which compiles fine against the extern jar but fails to defineClass at runtime on JDK 9+ — sun.* was moved to jdk.internal.* and is inaccessible to application code. This broke unit tests at startup: Issue5556 was the first class to trigger loading the affected closure adapter, raising NoClassDefFoundError before any test ran. User code can't legitimately target these internal packages anyway, so the fix is to exclude sun.*, com.sun.*, and jdk.internal.* from structural detection. Classes that actually carry @FunctionalInterface are still honored (none of the affected internal classes do, so this is safe). Issue5556 serves as the regression test — its closure adapter previously implemented sun.reflect.ConstructorAccessor and now no longer does. * [jvm] only implement SAM interfaces the program actually uses Structural SAM detection tags every single-abstract-method interface on the --java-lib classpath as @:functionalInterface, and a closure then implements every one of them whose signature structurally matches. That includes interfaces transitively loaded from the SDK jar but never named in user code — e.g. compiling against android-34 drags in android.os.OutcomeReceiver (API 33) and SplashScreen.OnExitAnimationListener (API 31). A closure implementing those hard-fails class linking (NoClassDefFoundError) the moment it loads on an older runtime, crashing the app at activity instantiation. Restrict the promiscuous binding to interfaces the program genuinely converts a function to: - new com.functional_interfaces_used set (shared across cloned contexts, exposed on Gctx.t for the generator); - AbstractCast records each interface as its SAM-conversion branch fires, keyed by the @:native path since Native.apply_native_paths rewrites cl_path between typing and generation; - genjvm's Preprocessor only registers an interface in gctx.functional_interfaces when it is in that set, resolved by physical class identity in the modules loop (before check_path can rewrite the path again). StructuralSam gains an Unused interface — structurally identical to OnClick but never used as a conversion target — and asserts a closure implements OnClick but not Unused. * [jvm] derive used SAM interfaces from the AST, not just AbstractCast The functional_interfaces_used set populated by AbstractCast only covers interfaces reached through an implicit SAM conversion typed from source. That misses two cases: an explicit `cast` of a closure to a functional interface bypasses AbstractCast entirely, and an `--hxb-lib` build never re-runs typing so the set is empty — closures then fail to implement the interface and hard-cast at runtime (Issue9576, Issue11236, IssueFunctionalInterfaceOverload under the hxb-lib CI configs). Additionally scan the AST of non-extern types in genjvm's Preprocessor: field/static/constructor signatures and their cf_overloads, plus the etype of every sub-expression (which catches the TFun type of a called extern method whose parameter is a SAM interface). This is independent of how the module was loaded. Restricting the scan to non-extern types keeps incidental SAM interfaces from a --java-lib classpath out of the set unless user code actually references them. * [tests] move test to TestJava * [jvm] route check_functional_interface through get_singular_interface_field The JLS §9.8 SAM rule (exclude default methods and Object members) was duplicated between tOther and genjvm; the latter now just calls the shared helper. * [jvm] drop functional_interfaces_used; AST scan is the sole source AbstractCast was writing into a Hashtbl on the common context to track which SAM interfaces a program converts a function to, with @:native re-keying to bridge the typing↔generation path rewrite. The genjvm preprocessor already walks the AST and collects every SAM-typed TInst it sees, which strictly subsumes the AbstractCast set (it also catches explicit casts and --hxb-lib builds). Remove the redundant bookkeeping and the field on common.context / Gctx.t. * [jvm] swap fi_used_classes List.memq for path-keyed Hashtbl Avoid an O(n*m) lookup over the SAM-used list during the interface preprocessing loop. The list is finalized into a Hashtbl after loop 1 completes (and therefore after check_path's cl_path rewrites), so keying by cl_path is stable. * [jvm] drop functional_interface_lut; compute SAM field on demand The LUT memoized get_singular_interface_field per class, populated by typeloadFields.check_functional_interface and re-checked lazily by AbstractCast (#11549, for display-mode flows where init_class hadn't run yet by the time a SAM conversion was queried). It was a global mutable hashtbl on common.context whose only reader was AbstractCast. The cached computation is a walk over a SAM interface's field list — typically under 10 entries, and short-circuits on the second non-default method, so the perf delta from recomputing is well below noise. Doing that directly in AbstractCast collapses the two load paths into one and removes the field from common.context. Also drops the now-no-op typeloadFields.check_functional_interface (its only effect was populating the LUT) and the bridge call that re-invoked it from init_class when the flag was set via @:functionalInterface meta in typeloadModule. * [jvm] extract collect_used_functional_interfaces Move the SAM-usage AST scan out of preprocess and into its own top-level function in the Preprocessor module that returns the path-keyed hashtbl. The scan now runs after the check_path loop so cl_paths are stable when we record them.
1 parent 6c739be commit dce7adf

14 files changed

Lines changed: 369 additions & 39 deletions

File tree

src/codegen/javaModern.ml

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,16 +985,68 @@ module Converter = struct
985985
in
986986
add_meta (Meta.Annotation,args,p)
987987
end;
988+
let has_fi_annotation = ref false in
988989
List.iter (fun attr -> match attr with
989990
| AttrVisibleAnnotations ann ->
990991
List.iter (function
991992
| { ann_type = TObject( (["java";"lang"], "FunctionalInterface"), [] ) } ->
992-
add_meta (Meta.FunctionalInterface,[],p);
993+
has_fi_annotation := true
993994
| _ -> ()
994995
) ann
995996
| _ ->
996997
()
997998
) jc.jc_attributes;
999+
(* Match javac's SAM rule: an interface is functional if it declares exactly
1000+
one abstract instance method (ignoring static/default/private/synthetic
1001+
methods, constructors, and Object members re-declared as abstract). The
1002+
@FunctionalInterface annotation is advisory in javac — Android SDK
1003+
listeners are rarely annotated, so structural detection unlocks lambdas
1004+
for them without per-extern opt-in. *)
1005+
(* JLS §9.8 excludes methods that would be members of Object from the SAM
1006+
count. Match by name+arity rather than full descriptor — equals's exact
1007+
param encoding can vary across class-file shapes (raw Object, generic
1008+
erasure variants), and equals/hashCode/toString have unique arities on
1009+
java.lang.Object so name+arity is unambiguous. *)
1010+
let is_object_redeclaration jf = match jf.jf_name, jf.jf_descriptor with
1011+
| "equals", TMethod([_],_) -> true
1012+
| "hashCode", TMethod([],_) -> true
1013+
| "toString", TMethod([],_) -> true
1014+
| _ -> false
1015+
in
1016+
(* Skip JDK-internal packages: sun.*, com.sun.*, jdk.internal.*. They host
1017+
many single-method classes (sun.reflect.ConstructorAccessor,
1018+
sun.nio.ch.Interruptible, sun.reflect.generics.tree.TypeTree, ...) that
1019+
compile-time externs see but the runtime JDK may not expose — sun.* was
1020+
moved to jdk.internal.* in JDK 9+ and is inaccessible to app code.
1021+
Auto-tagging them as functional interfaces makes the JFI matcher emit
1022+
`implements sun.reflect.ConstructorAccessor` on every matching closure,
1023+
which then fails to defineClass at runtime. User code can't target these
1024+
anyway, so structural detection must skip them. The @FunctionalInterface
1025+
annotation path is still honored (none of the affected JDK-internal
1026+
classes carry it, so this is a safe restriction). *)
1027+
let is_jdk_internal_package pack = match pack with
1028+
| "sun" :: _ -> true
1029+
| "com" :: "sun" :: _ -> true
1030+
| "jdk" :: "internal" :: _ -> true
1031+
| _ -> false
1032+
in
1033+
let is_structural_sam = is_interface
1034+
&& not (is_jdk_internal_package (fst jc.jc_path))
1035+
&& (
1036+
let count = List.fold_left (fun acc jf ->
1037+
if AccessFlags.has_flag jf.jf_flags MAbstract
1038+
&& not (AccessFlags.has_flag jf.jf_flags MStatic)
1039+
&& not (AccessFlags.has_flag jf.jf_flags MPrivate)
1040+
&& not (AccessFlags.has_flag jf.jf_flags MSynthetic)
1041+
&& jf.jf_name <> "<init>"
1042+
&& not (is_object_redeclaration jf)
1043+
then acc + 1 else acc
1044+
) 0 jc.jc_methods in
1045+
count = 1
1046+
)
1047+
in
1048+
if !has_fi_annotation || is_structural_sam then
1049+
add_meta (Meta.FunctionalInterface,[],p);
9981050
let d = {
9991051
d_name = (class_name,p);
10001052
d_doc = None;

src/codegen/overloads.ml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,17 @@ struct
192192
raise Not_found)
193193
| TFun _, TFun _ -> (* unify will make sure they are compatible *)
194194
cacc,0
195+
(* SAM conversion: lambda passed where a functional-interface class is
196+
expected. Rate by how closely the lambda's signature matches the SAM
197+
method's signature so specificity ordering still works between two
198+
SAM-parametered overloads. *)
199+
| TInst(cf,tlf), TFun _ when has_class_flag cf CFunctionalInterface ->
200+
begin match TOther.TClass.get_singular_interface_field cf.cl_ordered_fields with
201+
| None -> raise Not_found
202+
| Some sam ->
203+
let map = apply_params cf.cl_params tlf in
204+
rate_conv (cacc+1) (map sam.cf_type) targ
205+
end
195206
| tfun,targ ->
196207
raise Not_found
197208

src/context/abstractCast.ml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,9 @@ and do_check_cast ctx uctx tleft eright p =
8888
loop2 a.a_to
8989
end
9090
| TInst(c,tl), TFun _ when has_class_flag c CFunctionalInterface ->
91-
let cf = try
92-
snd (ctx.com.functional_interface_lut#find c.cl_path)
93-
with Not_found -> match TClass.get_singular_interface_field c.cl_ordered_fields with
94-
| None ->
95-
raise Not_found
96-
| Some cf ->
97-
ctx.com.functional_interface_lut#add c.cl_path (c,cf);
98-
cf
91+
let cf = match TClass.get_singular_interface_field c.cl_ordered_fields with
92+
| None -> raise Not_found
93+
| Some cf -> cf
9994
in
10095
let map = apply_params c.cl_params tl in
10196
let monos = Monomorph.spawn_constrained_monos map cf.cf_params in

src/context/common.ml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,6 @@ and context = {
345345
mutable modules : Type.module_def list;
346346
mutable types : Type.module_type list;
347347
mutable resources : (string,string) Hashtbl.t;
348-
functional_interface_lut : (path,(tclass * tclass_field)) lookup;
349348
(* target-specific *)
350349
mutable flash_version : float;
351350
mutable neko_lib_paths : string list;
@@ -810,7 +809,6 @@ let create sctx request_scope part_scope display_mode =
810809
parser_cache = new hashtbl_lookup;
811810
overload_cache = new hashtbl_lookup;
812811
is_macro_context = false;
813-
functional_interface_lut = new Lookup.hashtbl_lookup;
814812
hxb_reader_api = None;
815813
hxb_reader_stats = HxbReader.create_hxb_reader_stats ();
816814
hxb_writer_config = None;
@@ -935,7 +933,6 @@ let clone com is_macro_context =
935933
parser_cache = new hashtbl_lookup;
936934
overload_cache = new hashtbl_lookup; (* ! *)
937935
is_macro_context = is_macro_context;
938-
functional_interface_lut = new Lookup.hashtbl_lookup;
939936
hxb_reader_api = None;
940937
hxb_reader_stats = HxbReader.create_hxb_reader_stats ();
941938
}

src/core/tOther.ml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,19 @@ module TClass = struct
386386
c.cl_init <- Some cf
387387

388388
let get_singular_interface_field fields =
389+
(* Re-declarations of java.lang.Object members (equals/hashCode/toString)
390+
on an interface must not count toward the SAM check — JLS §9.8.
391+
Matched by name+arity since these are unique on Object. *)
392+
let is_object_member cf = match cf.cf_name, follow cf.cf_type with
393+
| "equals", TFun([_],_) -> true
394+
| "hashCode", TFun([],_) -> true
395+
| "toString", TFun([],_) -> true
396+
| _ -> false
397+
in
389398
let is_normal_field cf =
390-
not (has_class_field_flag cf CfDefault) && match cf.cf_kind with
399+
not (has_class_field_flag cf CfDefault)
400+
&& not (is_object_member cf)
401+
&& match cf.cf_kind with
391402
| Method MethNormal -> true
392403
| _ -> false
393404
in

src/generators/genjvm.ml

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,19 +3201,7 @@ module Preprocessor = struct
32013201
mt.mt_path <- make_root mt.mt_path
32023202

32033203
let check_functional_interface gctx c =
3204-
let rec loop m l = match l with
3205-
| [] ->
3206-
m
3207-
| cf :: l ->
3208-
if not (has_class_field_flag cf CfDefault) then begin match m with
3209-
| None ->
3210-
loop (Some cf) l
3211-
| Some _ ->
3212-
None
3213-
end else
3214-
loop m l
3215-
in
3216-
match loop None c.cl_ordered_fields with
3204+
match TClass.get_singular_interface_field c.cl_ordered_fields with
32173205
| None ->
32183206
()
32193207
| Some cf ->
@@ -3224,6 +3212,57 @@ module Preprocessor = struct
32243212
| _ ->
32253213
()
32263214

3215+
(* Collect the functional interfaces actually referenced by user code, so
3216+
closures only implement SAMs the program demands. Without this filter a
3217+
closure would bind to every structurally-matching interface on the
3218+
--java-lib classpath — including incidental ones from a higher API level
3219+
than the runtime, which hard-fails class linking. An interface counts as
3220+
used iff some non-extern type, field signature, or sub-expression names
3221+
it; scanning non-extern code only keeps classpath noise out. *)
3222+
let collect_used_functional_interfaces gctx =
3223+
let used = Hashtbl.create 0 in
3224+
let rec note_fi_in_type depth t =
3225+
if depth < 32 then match follow t with
3226+
| TInst(c,tl) ->
3227+
if has_class_flag c CFunctionalInterface then Hashtbl.replace used c.cl_path ();
3228+
List.iter (note_fi_in_type (depth + 1)) tl
3229+
| TFun(args,ret) ->
3230+
List.iter (fun (_,_,t) -> note_fi_in_type (depth + 1) t) args;
3231+
note_fi_in_type (depth + 1) ret
3232+
| TAbstract(_,tl) | TEnum(_,tl) | TType(_,tl) ->
3233+
List.iter (note_fi_in_type (depth + 1)) tl
3234+
| TAnon an ->
3235+
PMap.iter (fun _ cf -> note_fi_in_type (depth + 1) cf.cf_type) an.a_fields
3236+
| TDynamic (Some t) ->
3237+
note_fi_in_type (depth + 1) t
3238+
| _ ->
3239+
()
3240+
in
3241+
let rec note_fi_in_expr e =
3242+
note_fi_in_type 0 e.etype;
3243+
Type.iter note_fi_in_expr e
3244+
in
3245+
let scan_class c =
3246+
if not (has_class_flag c CExtern) then begin
3247+
let rec scan cf =
3248+
note_fi_in_type 0 cf.cf_type;
3249+
Option.may note_fi_in_expr cf.cf_expr;
3250+
List.iter scan cf.cf_overloads
3251+
in
3252+
List.iter scan c.cl_ordered_fields;
3253+
List.iter scan c.cl_ordered_statics;
3254+
Option.may scan c.cl_constructor
3255+
end
3256+
in
3257+
(* go through com.modules so we can also pick up private typedefs *)
3258+
List.iter (fun m ->
3259+
List.iter (fun mt -> match mt with
3260+
| TClassDecl c -> scan_class c
3261+
| _ -> ()
3262+
) m.m_types
3263+
) gctx.gctx.modules;
3264+
used
3265+
32273266
let preprocess gctx =
32283267
let rec has_runtime_meta = function
32293268
| (Meta.Custom s,_,_) :: _ when String.length s > 0 && s.[0] <> ':' ->
@@ -3233,7 +3272,6 @@ module Preprocessor = struct
32333272
| [] ->
32343273
false
32353274
in
3236-
(* go through com.modules so we can also pick up private typedefs *)
32373275
List.iter (fun m ->
32383276
List.iter (fun mt ->
32393277
match mt with
@@ -3251,6 +3289,9 @@ module Preprocessor = struct
32513289
()
32523290
) m.m_types
32533291
) gctx.gctx.modules;
3292+
(* After check_path: cl_paths are stable, so we can key the used-SAM
3293+
set by cl_path without worrying about private-type rewrites. *)
3294+
let fi_used = collect_used_functional_interfaces gctx in
32543295
(* preprocess classes *)
32553296
let patch_optional c =
32563297
let apply cf =
@@ -3267,8 +3308,8 @@ module Preprocessor = struct
32673308
gctx.preprocessor#preprocess_class c
32683309
else begin
32693310
patch_optional c;
3270-
if has_class_flag c CFunctionalInterface then
3271-
check_functional_interface gctx c
3311+
if Hashtbl.mem fi_used c.cl_path then
3312+
check_functional_interface gctx c
32723313
end
32733314
| _ -> ()
32743315
) gctx.gctx.types;

src/typing/overloadResolution.ml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
11
open TType
22
open TUnification
33
open TFunctions
4+
open TOther
45
open FieldCallCandidate
56

7+
(* Try the functional-interface conversion path: if [t_param] is a SAM class
8+
and [t_arg] is a function type, unify the function against the SAM method's
9+
signature (mirroring what AbstractCast does at cast time). Returns true if
10+
compatible. Used during overload candidate filtering so SAM-parameter
11+
overloads aren't discarded before the cast layer runs — but unlike a blind
12+
accept, this still rejects lambdas whose shape doesn't match the SAM. *)
13+
let try_functional_interface_match t_param t_arg =
14+
match follow t_param, follow t_arg with
15+
| TInst(c,tl), TFun _ when has_class_flag c CFunctionalInterface ->
16+
begin match TClass.get_singular_interface_field c.cl_ordered_fields with
17+
| None -> false
18+
| Some cf ->
19+
let map = apply_params c.cl_params tl in
20+
let monos = List.map (fun _ -> mk_mono()) cf.cf_params in
21+
let expected = map (apply_params cf.cf_params monos cf.cf_type) in
22+
try Type.unify t_arg expected; true
23+
with Unify_error _ -> false
24+
end
25+
| _ -> false
26+
627
let unify_cf map_type c cf el =
728
let monos = List.map (fun _ -> mk_mono()) cf.cf_params in
829
match follow (apply_params cf.cf_params monos (map_type cf.cf_type)) with
930
| TFun(tl'',ret) as tf ->
1031
let rec loop2 acc el tl = match el,tl with
1132
| e :: el,(_,o,t) :: tl ->
1233
begin try
13-
Type.unify e.etype t;
34+
(try Type.unify e.etype t
35+
with Unify_error _ when try_functional_interface_match t e.etype -> ());
1436
loop2 (e :: acc) el tl
1537
with _ ->
1638
if Type.ExtType.is_rest (follow t) then

src/typing/typeloadFields.ml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,14 +1644,6 @@ let finalize_class cctx =
16441644
| Some r -> delay ctx.g PTypeField (fun() -> ignore(lazy_type r)))
16451645
) cctx.delayed_expr
16461646

1647-
let check_functional_interface ctx c =
1648-
match TClass.get_singular_interface_field c.cl_ordered_fields with
1649-
| None ->
1650-
()
1651-
| Some cf ->
1652-
add_class_flag c CFunctionalInterface;
1653-
ctx.com.functional_interface_lut#add c.cl_path (c,cf)
1654-
16551647
let create_class_field cctx f =
16561648
let cf = {(mk_field (fst f.cff_name) ~public:(is_public cctx f.cff_access None) t_dynamic f.cff_pos (pos f.cff_name)) with
16571649
cf_doc = f.cff_doc;
@@ -1793,7 +1785,7 @@ let init_class ctx_c cctx c p herits fields =
17931785
a.a_unops <- List.rev a.a_unops;
17941786
a.a_array <- List.rev a.a_array;
17951787
| None ->
1796-
if (has_class_flag c CFunctionalInterface) && com.platform = Jvm then check_functional_interface ctx_c c;
1788+
()
17971789
end;
17981790
c.cl_ordered_statics <- List.rev c.cl_ordered_statics;
17991791
c.cl_ordered_fields <- List.rev c.cl_ordered_fields;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import test.Listeners;
2+
import test.Listeners.Listeners_OnClick;
3+
import test.Listeners.Listeners_WithToString;
4+
import test.Listeners.Listeners_AbstractEqualsPlusOne;
5+
import test.Listeners.Listeners_WithDefaults;
6+
import test.Listeners.Listeners_StringMaker;
7+
import test.Listeners.Listeners_Unused;
8+
9+
function main() {
10+
// Plain SAM — javac would accept the lambda directly.
11+
trace(Listeners.runOnClick(id -> Sys.println("click=" + id), 7));
12+
13+
// SAM with default toString inherited from Object — still single abstract.
14+
trace(Listeners.runDescribe(v -> "v=" + v, 3));
15+
16+
// Abstract equals re-declaration must be excluded from the count.
17+
trace(Listeners.runCompute(a -> a * a, 5));
18+
19+
// default + static methods don't count toward abstractness.
20+
trace(Listeners.runTransform(x -> x + 1, 10));
21+
22+
// Non-overloaded SAM with non-void return — sanity check.
23+
trace(Listeners.runMaker(n -> "made-" + n, 2));
24+
25+
// SAM-aware overload disambiguation: distinct arg arities so the right
26+
// candidate is unambiguous.
27+
trace(Listeners.overloaded((id:Int) -> Sys.println("ovl-click=" + id), 1));
28+
trace(Listeners.overloaded((s:String) -> s + "!", "hi"));
29+
30+
// A closure converted to OnClick must implement OnClick at runtime — but
31+
// NOT Unused, which is structurally identical yet never used as a
32+
// conversion target. Guards against closures promiscuously implementing
33+
// every matching SAM interface on the classpath.
34+
var cb:Listeners_OnClick = id -> Sys.println("bound-click=" + id);
35+
Listeners.runOnClick(cb, 99);
36+
trace(Std.isOfType(cb, Listeners_OnClick));
37+
trace(Std.isOfType(cb, Listeners_Unused));
38+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import sys.FileSystem;
2+
3+
function main() {
4+
Sys.setCwd("./project");
5+
FileSystem.createDirectory("./out");
6+
Sys.command("javac", ["-d", "out", "test/Listeners.java", "-g"]);
7+
Sys.setCwd("./out");
8+
Sys.command("jar", ["cf", "test.jar",
9+
"test/Listeners.class",
10+
"test/Listeners$OnClick.class",
11+
"test/Listeners$WithToString.class",
12+
"test/Listeners$AbstractEqualsPlusOne.class",
13+
"test/Listeners$WithDefaults.class",
14+
"test/Listeners$NotSam.class",
15+
"test/Listeners$StringMaker.class",
16+
"test/Listeners$Unused.class",
17+
"test/Listeners$UnaryStringFn.class"]);
18+
}

0 commit comments

Comments
 (0)