From d1aba56c0accbd14440f200e8d611a6121ba3cf5 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Tue, 9 Jun 2026 09:16:38 +0200 Subject: [PATCH 01/22] [tests] add test for 10955 --- tests/unit/src/unit/issues/Issue10955.hx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/unit/src/unit/issues/Issue10955.hx diff --git a/tests/unit/src/unit/issues/Issue10955.hx b/tests/unit/src/unit/issues/Issue10955.hx new file mode 100644 index 00000000000..7e0c524592b --- /dev/null +++ b/tests/unit/src/unit/issues/Issue10955.hx @@ -0,0 +1,14 @@ +package unit.issues; + +class Issue10955 extends Test { + function test() { + eq("false|", foo()); + eq("false|Haxe is great!", foo("Haxe is great!")); + eq("false|Haxe is,great!", foo("Haxe is", "great!")); + eq("true|a,b,c", foo(true, "a", "b", "c")); + } + + static function foo(?b:Bool = false, ...args:String) { + return (b ? "true" : "false") + "|" + args.toArray().join(","); + } +} From 8460618a0bdfef3d56f2bf5bcb3141550b7e62d1 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 07:26:40 +0200 Subject: [PATCH 02/22] [tests] cover optional-before-rest skip on overload candidates (#10955) --- tests/unit/src/unit/issues/Issue10955.hx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/src/unit/issues/Issue10955.hx b/tests/unit/src/unit/issues/Issue10955.hx index 7e0c524592b..8f7ca7a3e72 100644 --- a/tests/unit/src/unit/issues/Issue10955.hx +++ b/tests/unit/src/unit/issues/Issue10955.hx @@ -11,4 +11,20 @@ class Issue10955 extends Test { static function foo(?b:Bool = false, ...args:String) { return (b ? "true" : "false") + "|" + args.toArray().join(","); } + + function testOverload() { + eq("opt:null|", bar()); + eq("opt:null|a", bar("a")); + eq("opt:null|a,b", bar("a", "b")); + eq("opt:true|a,b", bar(true, "a", "b")); + eq("int:3|a,b", bar(3, "a", "b")); + } + + overload extern inline static function bar(?b:Bool, ...args:String) { + return "opt:" + b + "|" + args.toArray().join(","); + } + + overload extern inline static function bar(n:Int, ...args:String) { + return "int:" + n + "|" + args.toArray().join(","); + } } From c9455f03ca65f9d8feaf573fa15cc5a921fb2ced Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Tue, 9 Jun 2026 08:53:33 +0200 Subject: [PATCH 03/22] [typer] fix might_skip vs rest arguments --- src/typing/callUnification.ml | 39 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index a73d5f5a8ab..d5b5e680791 100644 --- a/src/typing/callUnification.ml +++ b/src/typing/callUnification.ml @@ -151,7 +151,6 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin | (s,ul) :: _ -> arg_error ul s true end | e :: el,(name,opt,t) :: args -> - let might_skip = List.length el < List.length args in let body_capture = reset_call_arg_body_capture ctx in let restore_monos = monomorph_transaction ctx in let committed = ref false in @@ -162,22 +161,28 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin let e = type_against name t e in commit (); e :: loop el args - with - | WithTypeError ul when opt && might_skip -> - drop (); - restore_monos(); - let e_def = skip name ul t in - e_def :: loop (e :: el) args - | WithTypeError _ when !body_capture <> [] && (match follow t with TFun _ -> false | _ -> true) -> - commit (); - restore_monos(); - let e_def = default_value name t in - e_def :: loop el args - | WithTypeError ul -> - commit (); - match List.rev !skipped with - | [] -> arg_error ul name opt - | (s,ul) :: _ -> arg_error ul s true + with WithTypeError ul -> + let might_skip = opt && List.length el < List.length args in + let opt_followed_by_rest = opt && match args with + | [(_,_,t)] -> ExtType.is_rest (follow t) + | _ -> false + in + if might_skip || opt_followed_by_rest then begin + drop (); + restore_monos(); + let e_def = skip name ul t in + e_def :: loop (e :: el) args + end else if !body_capture <> [] && (match follow t with TFun _ -> false | _ -> true) then begin + commit (); + restore_monos(); + let e_def = default_value name t in + e_def :: loop el args + end else begin + commit (); + match List.rev !skipped with + | [] -> arg_error ul name opt + | (s,ul) :: _ -> arg_error ul s true + end end with exc -> commit (); From df6bd8bfc0e709cc6d3cc9c82fbe0345e8ac5c3d Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 11:48:00 +0200 Subject: [PATCH 04/22] [typer] centralize implicit (PosInfos) argument handling; fix accessors (#6095) Introduce a single notion of an "implicit" trailing argument (one the compiler auto-supplies when omitted; today only an optional haxe.PosInfos) in Typecore: is_implicit_arg, split_implicit_trailing_args, strip_implicit_trailing_args, has_only_implicit_args (replacing the former is_empty_or_pos_infos). Route the previously open-coded "tolerate a trailing optional PosInfos" sites through it: @:from, @:to (validity + forwarding), @:op binop and op-apply. Fix #6095: property accessors may now declare ?pos:haxe.PosInfos. The candidate accessor's implicit trailing args are stripped before unifying against the property signature, so get_a(?pos)/set_b(v, ?pos) type-check while a real extra argument still errors. is_implicit_arg is the single extension point for future user-defined auto-filled arguments. --- src/context/abstractCast.ml | 4 ++-- src/context/typecore.ml | 17 ++++++++++---- src/typing/operators.ml | 9 +++---- src/typing/typeloadFields.ml | 31 +++++++++++++++---------- tests/unit/src/unit/issues/Issue6095.hx | 29 +++++++++++++++++++++++ 5 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 tests/unit/src/unit/issues/Issue6095.hx diff --git a/src/context/abstractCast.ml b/src/context/abstractCast.ml index 51a3df0f39b..18bc9465035 100644 --- a/src/context/abstractCast.ml +++ b/src/context/abstractCast.ml @@ -153,7 +153,7 @@ let find_array_read_access_raise ctx a pl e1 p = | cf :: cfl -> let map,check_constraints,get_ta = prepare_array_access_field ctx a pl cf p in match follow (map cf.cf_type) with - | TFun((_,_,tab) :: (_,_,ta1) :: args,r) as tf when is_empty_or_pos_infos args -> + | TFun((_,_,tab) :: (_,_,ta1) :: args,r) as tf when has_only_implicit_args args -> begin try Type.unify tab (get_ta()); let e1 = cast_or_unify_raise ctx ta1 e1 p in @@ -173,7 +173,7 @@ let find_array_write_access_raise ctx a pl e1 e2 p = | cf :: cfl -> let map,check_constraints,get_ta = prepare_array_access_field ctx a pl cf p in match follow (map cf.cf_type) with - | TFun((_,_,tab) :: (_,_,ta1) :: (_,_,ta2) :: args,r) as tf when is_empty_or_pos_infos args -> + | TFun((_,_,tab) :: (_,_,ta1) :: (_,_,ta2) :: args,r) as tf when has_only_implicit_args args -> begin try Type.unify tab (get_ta()); let e1 = cast_or_unify_raise ctx ta1 e1 p in diff --git a/src/context/typecore.ml b/src/context/typecore.ml index 5f4fa6af5ce..efe52d53a83 100644 --- a/src/context/typecore.ml +++ b/src/context/typecore.ml @@ -759,11 +759,18 @@ let rec is_pos_infos = function | _ -> false -let is_empty_or_pos_infos args = - match args with - | [_,true,t] -> is_pos_infos t - | [] -> true - | _ -> false +let is_implicit_arg (_,opt,t) = opt && is_pos_infos t + +let split_implicit_trailing_args args = + let rec loop implicit = function + | x :: rest when is_implicit_arg x -> loop (x :: implicit) rest + | rest -> (List.rev rest, implicit) + in + loop [] (List.rev args) + +let strip_implicit_trailing_args args = fst (split_implicit_trailing_args args) + +let has_only_implicit_args args = strip_implicit_trailing_args args = [] let get_next_stored_typed_expr_id = let uid = ref 0 in diff --git a/src/typing/operators.ml b/src/typing/operators.ml index 9add0c67899..4df20d2fc82 100644 --- a/src/typing/operators.ml +++ b/src/typing/operators.ml @@ -461,12 +461,9 @@ let find_abstract_binop_overload ctx op e1 e2 a c tl left is_assign_op p = let is_impl = has_class_field_flag cf CfImpl in begin match follow cf.cf_type with - | TFun((_,_,t1) :: (_,_,t2) :: pos_infos, tret) -> - (match pos_infos with - | [] -> () - | [_,true,t] when is_pos_infos t -> () - | _ -> die ~p:cf.cf_pos ("Unexpected arguments list of function " ^ cf.cf_name) __LOC__ - ); + | TFun((_,_,t1) :: (_,_,t2) :: trailing, tret) -> + if not (has_only_implicit_args trailing) then + die ~p:cf.cf_pos ("Unexpected arguments list of function " ^ cf.cf_name) __LOC__; let check e1 e2 swapped = let map_arguments () = let monos = Monomorph.spawn_constrained_monos (fun t -> t) cf.cf_params in diff --git a/src/typing/typeloadFields.ml b/src/typing/typeloadFields.ml index df89a954e29..7eb778725ef 100644 --- a/src/typing/typeloadFields.ml +++ b/src/typing/typeloadFields.ml @@ -951,9 +951,12 @@ let check_abstract (ctx,cctx,fctx) a c cf fd t ret p = let r = make_lazy ctx.g t (fun () -> (* the return type of a from-function must be the abstract, not the underlying type *) if not fctx.is_macro then (try type_eq EqStrict ret ta with Unify_error l -> raise_typing_error_ext (make_error (Unify l) p)); - match t with - | TFun([_,_,t],_) -> t - | TFun([(_,_,t1);(_,true,t2)],_) when is_pos_infos t2 -> t1 + let visible_args = match t with + | TFun(args,_) -> strip_implicit_trailing_args args + | _ -> [] + in + match visible_args with + | [(_,_,t1)] -> t1 | _ -> raise_typing_error ("@:from cast functions must accept exactly one argument") p ) "@:from" in a.a_from_field <- (TLazy r,cf) :: a.a_from_field; @@ -961,9 +964,8 @@ let check_abstract (ctx,cctx,fctx) a c cf fd t ret p = let handle_to () = if fctx.is_macro then invalid_modifier ctx.com fctx "macro" "cast function" p; let are_valid_args args = - match args with + match strip_implicit_trailing_args args with | [_] -> true - | [_; (_,true,t)] when is_pos_infos t -> true | _ -> false in (match cf.cf_kind, cf.cf_type with @@ -1004,7 +1006,10 @@ let check_abstract (ctx,cctx,fctx) a c cf fd t ret p = args end else match cf.cf_type with - | TFun([_;(_,true,t)],_) when is_pos_infos t -> [t] + | TFun(args,_) -> + (match split_implicit_trailing_args args with + | ([_],implicit) -> List.map (fun (_,_,t) -> t) implicit + | _ -> []) | _ -> [] in let t = resolve_m args in @@ -1026,11 +1031,11 @@ let check_abstract (ctx,cctx,fctx) a c cf fd t ret p = end in begin match follow t with - | TFun((_,_,t1) :: (_,_,t2) :: args,_) when is_empty_or_pos_infos args -> + | TFun((_,_,t1) :: (_,_,t2) :: args,_) when has_only_implicit_args args -> if a.a_read <> None then display_error ctx.com "Multiple resolve-read methods are not supported" cf.cf_pos; check_fun t1 t2; a.a_read <- Some cf; - | TFun((_,_,t1) :: (_,_,t2) :: (_,_,t3) :: args,_) when is_empty_or_pos_infos args -> + | TFun((_,_,t1) :: (_,_,t2) :: (_,_,t3) :: args,_) when has_only_implicit_args args -> if a.a_write <> None then display_error ctx.com "Multiple resolve-write methods are not supported" cf.cf_pos; check_fun t1 t2; a.a_write <- Some cf; @@ -1052,10 +1057,8 @@ let check_abstract (ctx,cctx,fctx) a c cf fd t ret p = if fctx.is_macro then invalid_modifier ctx.com fctx "macro" "operator function" p; let targ = if fctx.is_abstract_member then tthis else ta in let left_eq,right_eq = - match follow t with - | TFun([(_,_,t1);(_,_,t2)],_) -> - type_iseq targ t1,type_iseq targ t2 - | TFun([(_,_,t1);(_,_,t2);(_,true,t3)],_) when is_pos_infos t3 -> + match (match follow t with TFun(args,_) -> Some (strip_implicit_trailing_args args) | _ -> None) with + | Some [(_,_,t1);(_,_,t2)] -> type_iseq targ t1,type_iseq targ t2 | _ -> if fctx.is_abstract_member then @@ -1435,6 +1438,10 @@ let create_property (ctx,cctx,fctx) c f cf (get,set,t,eo) p = make_error (Custom (compl_msg (f2.cf_name ^ ": Accessor method is here"))) f2.cf_pos; ] p); | _ -> ()); + let t2 = match follow t2 with + | TFun(args,ret) -> TFun(strip_implicit_trailing_args args,ret) + | _ -> t2 + in unify_raise t2 t f2.cf_pos; if (fctx.is_abstract_member && not (has_class_field_flag f2 CfImpl)) || (has_class_field_flag f2 CfImpl && not (fctx.is_abstract_member)) then display_error ctx.com "Mixing abstract implementation and static properties/accessors is not allowed" f2.cf_pos; diff --git a/tests/unit/src/unit/issues/Issue6095.hx b/tests/unit/src/unit/issues/Issue6095.hx new file mode 100644 index 00000000000..783ce6b3c6a --- /dev/null +++ b/tests/unit/src/unit/issues/Issue6095.hx @@ -0,0 +1,29 @@ +package unit.issues; + +class Issue6095 extends unit.Test { + var a(get, null):String; + + function get_a(?pos:haxe.PosInfos):String { + return a; + } + + var b(get, set):Int; + var _b:Int = 0; + + function get_b(?pos:haxe.PosInfos):Int { + return _b; + } + + function set_b(v:Int, ?pos:haxe.PosInfos):Int { + _b = v; + return v; + } + + function test() { + a = "12345"; + eq("12345", a); + + b = 7; + eq(7, b); + } +} From 3cdc8ab0e27a13ce2fa0753527f07bc8dcf68ce6 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 12:03:57 +0200 Subject: [PATCH 05/22] [typer] support a trailing PosInfos after a rest argument `(...rest, ?pos:haxe.PosInfos)` is now allowed. The implicit trailing PosInfos is canonicalized to sit just before the rest (canonicalize_rest_order), keeping the rest physically last so native varargs codegen is unchanged on every target (no array-wrapping, no generator changes). check_rest is relaxed accordingly. In call unification, an implicit argument immediately before a rest is always auto-filled and never consumes a positional, so a Dynamic rest cannot eat the first positional as the PosInfos value. Rest detection in the canonicalization is shallow (no follow) and short-circuits when no rest is present, so for_type does not force lazy types during class building. Test: TestRest.testPosInfos. --- src/typing/callUnification.ml | 2 ++ src/typing/functionArguments.ml | 39 +++++++++++++++++++++++++++++---- tests/unit/src/unit/TestRest.hx | 28 +++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index d5b5e680791..179b52f9dc6 100644 --- a/src/typing/callUnification.ml +++ b/src/typing/callUnification.ml @@ -56,6 +56,8 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin | name :: _ -> call_error (Cannot_skip_non_nullable name) callp; end; [] + | _,(_,true,t) :: ((_,false,tr) :: _ as args) when is_pos_infos t && ExtType.is_rest (follow tr) -> + mk_pos_infos t :: loop el args | _,[name,false,TAbstract({ a_path = ["cpp"],"Rest" },[t])] -> (try List.map (fun e -> type_against name t e) el with WithTypeError e -> arg_error e name false) diff --git a/src/typing/functionArguments.ml b/src/typing/functionArguments.ml index 3c1fa470cfb..3a62bfe2c14 100644 --- a/src/typing/functionArguments.ml +++ b/src/typing/functionArguments.ml @@ -66,6 +66,26 @@ let type_function_arg_value ctx t c do_display = in loop false e +let is_rest_shallow = function + | TAbstract({a_path=["haxe"],"Rest"},_) + | TType({t_path=["haxe";"extern"],"Rest"},_) -> true + | _ -> false + +let canonicalize_rest_order get_type is_implicit args = + let is_rest x = is_rest_shallow (get_type x) in + match List.rev args with + | last :: _ when is_rest last -> args + | _ when not (List.exists is_rest args) -> args + | rev -> + let rec peel impl = function + | x :: tl when is_implicit x -> peel (x :: impl) tl + | x :: tl when is_rest x -> Some (List.rev tl,x,impl) + | _ -> None + in + (match peel [] rev with + | Some (before,rest,impl) -> before @ impl @ [rest] + | None -> args) + class function_arguments (com : Common.context) (type_arg : int -> bool -> type_hint option -> pos -> Type.t) @@ -112,14 +132,20 @@ object(self) l | None -> let l = List.map (fun (n,eo,t) -> n,eo <> None,t) with_default in + let l = canonicalize_rest_order + (fun (_,_,t) -> t) + is_implicit_arg + l + in type_repr <- Some l; l - method private check_rest (is_last : bool) (eo : expr option) (opt : bool) (t : Type.t) (pn : pos) = + method private check_rest (tail : (string * expr option * Type.t) list) (eo : expr option) (opt : bool) (t : Type.t) (pn : pos) = if ExtType.is_rest (follow t) then begin if opt then raise_typing_error "Rest argument cannot be optional" pn; begin match eo with None -> () | Some (_,p) -> raise_typing_error "Rest argument cannot have default value" p end; - if not is_last then raise_typing_error "Rest should only be used for the last function argument" pn; + let tail_ok = List.for_all (fun (_,eo,t) -> eo <> None && is_pos_infos t) tail in + if not tail_ok then raise_typing_error "Rest should only be used for the last function argument" pn; end (* Returns the `(tvar * texpr option) list` for `tf_args`. Also checks the validity of argument names and whether or not @@ -140,7 +166,7 @@ object(self) v.v_meta <- (Meta.This,[],null_pos) :: v.v_meta; loop ((v,None) :: acc) false syntax typed | ((_,pn),opt,m,_,_) :: syntax,(name,eo,t) :: typed -> - delay ctx.g PTypeField (fun() -> self#check_rest (typed = []) eo opt t pn); + delay ctx.g PTypeField (fun() -> self#check_rest typed eo opt t pn); if not is_extern then begin Naming.check_local_variable_name ctx.com name TVOArgument pn; if name <> "_" && List.exists (fun (v,_) -> v.v_name = name) acc then @@ -159,6 +185,11 @@ object(self) die "" __LOC__ in let l = loop [] (abstract_this <> None) syntax with_default in + let l = canonicalize_rest_order + (fun (v,_) -> v.v_type) + (fun (v,eo) -> eo <> None && is_pos_infos v.v_type) + l + in expr_repr <- Some l; l @@ -168,7 +199,7 @@ object(self) | syntax,(name,_,t) :: typed when is_abstract_this -> loop false syntax typed | ((_,pn),opt,m,_,_) :: syntax,(name,eo,t) :: typed -> - delay ctx.g PTypeField (fun() -> self#check_rest (typed = []) eo opt t pn); + delay ctx.g PTypeField (fun() -> self#check_rest typed eo opt t pn); ignore(type_function_arg_value ctx t eo do_display); loop false syntax typed | [],[] -> diff --git a/tests/unit/src/unit/TestRest.hx b/tests/unit/src/unit/TestRest.hx index fd73914283b..7986e365604 100644 --- a/tests/unit/src/unit/TestRest.hx +++ b/tests/unit/src/unit/TestRest.hx @@ -17,6 +17,34 @@ class TestRest extends Test { eq(4, rest(1, 2, 3, 4)); } + function testPosInfos() { + // a trailing optional PosInfos may follow a rest argument; it is auto-filled + // and never consumes a positional (which all flow into the rest) + function log(...rest:Int, ?pos:haxe.PosInfos):String { + return rest.length + "@" + pos.methodName; + } + eq("3@testPosInfos", log(1, 2, 3)); + eq("0@testPosInfos", log()); + + function collect(...rest:Int, ?pos:haxe.PosInfos):Array { + return rest.toArray(); + } + aeq([1, 2, 3], collect(1, 2, 3)); + + // fixed argument before the rest + function tag(label:String, ...rest:Int, ?pos:haxe.PosInfos):String { + return label + rest.length + "/" + (pos != null); + } + eq("x2/true", tag("x", 1, 2)); + + // a Dynamic rest must not greedily unify a positional with PosInfos + function dyn(...rest:Dynamic, ?pos:haxe.PosInfos):Int { + return rest.length; + } + eq(2, dyn("a", "b")); + eq(0, dyn()); + } + function testToArray() { function rest(...r:Int):Array { var a = r.toArray(); From d078da8f2072e6810c6f917bb41beb75534db7aa Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 13:16:17 +0200 Subject: [PATCH 06/22] [tests] cover rest/PosInfos vs overloads and arg skipping Extend Issue10955 with a trailing PosInfos past the rest, exercising optional-before-rest skipping and overload resolution with auto-filled pos. Strengthen TestRest.testPosInfos to also assert the auto-filled pos identity on a Dynamic rest (which must not consume a positional as PosInfos). Manual-PosInfos semantics (overriding an auto-filled arg) are intentionally not asserted yet; that behavior is an open design question. --- tests/unit/src/unit/TestRest.hx | 8 ++++---- tests/unit/src/unit/issues/Issue10955.hx | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/unit/src/unit/TestRest.hx b/tests/unit/src/unit/TestRest.hx index 7986e365604..a0f042dc8ae 100644 --- a/tests/unit/src/unit/TestRest.hx +++ b/tests/unit/src/unit/TestRest.hx @@ -38,11 +38,11 @@ class TestRest extends Test { eq("x2/true", tag("x", 1, 2)); // a Dynamic rest must not greedily unify a positional with PosInfos - function dyn(...rest:Dynamic, ?pos:haxe.PosInfos):Int { - return rest.length; + function dyn(...rest:Dynamic, ?pos:haxe.PosInfos):String { + return rest.length + ":" + pos.methodName; } - eq(2, dyn("a", "b")); - eq(0, dyn()); + eq("2:testPosInfos", dyn("a", "b")); + eq("0:testPosInfos", dyn()); } function testToArray() { diff --git a/tests/unit/src/unit/issues/Issue10955.hx b/tests/unit/src/unit/issues/Issue10955.hx index 8f7ca7a3e72..5d58c248804 100644 --- a/tests/unit/src/unit/issues/Issue10955.hx +++ b/tests/unit/src/unit/issues/Issue10955.hx @@ -27,4 +27,28 @@ class Issue10955 extends Test { overload extern inline static function bar(n:Int, ...args:String) { return "int:" + n + "|" + args.toArray().join(","); } + + // same patterns, now with a trailing PosInfos auto-filled past the rest + function testPosInfos() { + eq("F||testPosInfos", foop()); + eq("F|a,b|testPosInfos", foop("a", "b")); + eq("T|a,b|testPosInfos", foop(true, "a", "b")); + + eq("opt:null||testPosInfos", barp()); + eq("opt:null|a,b|testPosInfos", barp("a", "b")); + eq("opt:true|a,b|testPosInfos", barp(true, "a", "b")); + eq("int:3|a,b|testPosInfos", barp(3, "a", "b")); + } + + static function foop(?b:Bool = false, ...args:String, ?pos:haxe.PosInfos) { + return (b ? "T" : "F") + "|" + args.toArray().join(",") + "|" + pos.methodName; + } + + overload extern inline static function barp(?b:Bool, ...args:String, ?pos:haxe.PosInfos) { + return "opt:" + b + "|" + args.toArray().join(",") + "|" + pos.methodName; + } + + overload extern inline static function barp(n:Int, ...args:String, ?pos:haxe.PosInfos) { + return "int:" + n + "|" + args.toArray().join(",") + "|" + pos.methodName; + } } From 0e3ef20a4be5621145d32f3996aa34e6ff8bfad4 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 13:32:54 +0200 Subject: [PATCH 07/22] [typer] add @:posInfos to explicitly override an implicit PosInfos argument A call argument marked `@:posInfos e` designates `e` as the value for the function's implicit haxe.PosInfos parameter, overriding the auto-filled position. The marked argument is pulled out of the positional flow, so it targets the pos slot regardless of a greedy rest: f(...rest:String, ?pos) f("a","b", @:posInfos c) -> rest=["a","b"], pos=c Previously such a value was either swallowed into the rest (Dynamic) or a type error (typed rest), with no way to set pos. For non-rest functions it is now an explicit override rather than relying on skip-by-type-mismatch. Errors: multiple @:posInfos rejected; a mismatched override reports "should be haxe.PosInfos"; an override with no matching PosInfos parameter is flagged. Meta registered in src-json/meta.json. Test: TestRest.testPosInfos. --- src-json/meta.json | 6 ++++ src/typing/callUnification.ml | 49 +++++++++++++++++++++++---------- tests/unit/src/unit/TestRest.hx | 16 +++++++++++ 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src-json/meta.json b/src-json/meta.json index 08359f212ef..b07173d13a7 100644 --- a/src-json/meta.json +++ b/src-json/meta.json @@ -864,6 +864,12 @@ "targets": ["TExpr"], "links": ["https://haxe.org/manual/macro-reification.html"] }, + { + "name": "PosInfos", + "metadata": ":posInfos", + "doc": "Explicitly designates a call argument as the value for an implicit `haxe.PosInfos` parameter, overriding the auto-filled position.", + "targets": ["TExpr"] + }, { "name": "Public", "metadata": ":public", diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index 179b52f9dc6..aa6982a6b4d 100644 --- a/src/typing/callUnification.ml +++ b/src/typing/callUnification.ml @@ -19,8 +19,38 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin raise_error { e with err_message = (Call_error (Could_not_unify e.err_message)) } in + let handle_errors fn = + try + fn() + with Error e when (match e.err_message with Call_error _ | Module_not_found _ -> false | _ -> true) -> + raise (WithTypeError e) + in + (* let force_inline, is_extern = match cf with Some(TInst(c,_),f) -> is_forced_inline (Some c) f, (has_class_flag c CExtern) | _ -> false, false in *) + let type_against name t e = + handle_errors (fun() -> + let e = type_expr ctx e (WithType.with_argument t name) in + !cast_or_unify_raise_ref ctx t e e.epos + ) + in + let pos_override = ref None in + let el = List.filter (fun e -> match e with + | (EMeta((Meta.PosInfos,_,p),e1),_) -> + (match !pos_override with + | Some _ -> raise_typing_error "Multiple @:posInfos arguments are not allowed" p + | None -> pos_override := Some e1); + false + | _ -> + true + ) el in + let pos_override_used = ref false in let mk_pos_infos t = - mk_infos_t ctx callp [] t + match !pos_override with + | Some e -> + pos_override_used := true; + (try type_against "pos" t e + with WithTypeError err -> arg_error err "pos" true) + | None -> + mk_infos_t ctx callp [] t in let default_value name t = if is_pos_infos t then @@ -36,19 +66,6 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin skipped := (name,ul) :: !skipped; default_value name t in - let handle_errors fn = - try - fn() - with Error e when (match e.err_message with Call_error _ | Module_not_found _ -> false | _ -> true) -> - raise (WithTypeError e) - in - (* let force_inline, is_extern = match cf with Some(TInst(c,_),f) -> is_forced_inline (Some c) f, (has_class_flag c CExtern) | _ -> false, false in *) - let type_against name t e = - handle_errors (fun() -> - let e = type_expr ctx e (WithType.with_argument t name) in - !cast_or_unify_raise_ref ctx t e e.epos - ) - in let rec loop el args = match el,args with | [],[] -> begin match List.rev !invalid_skips with @@ -194,6 +211,10 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin let restore = enter_call_args ctx ~in_overload in let el = try loop el args with exc -> restore(); raise exc; in restore(); + (match !pos_override with + | Some e when not !pos_override_used -> + raise_typing_error "@:posInfos argument has no matching haxe.PosInfos parameter" (snd e) + | _ -> ()); el type overload_kind = diff --git a/tests/unit/src/unit/TestRest.hx b/tests/unit/src/unit/TestRest.hx index a0f042dc8ae..888af24757f 100644 --- a/tests/unit/src/unit/TestRest.hx +++ b/tests/unit/src/unit/TestRest.hx @@ -43,6 +43,22 @@ class TestRest extends Test { } eq("2:testPosInfos", dyn("a", "b")); eq("0:testPosInfos", dyn()); + + // @:posInfos explicitly targets the pos slot, so the value is NOT consumed by the + // greedy rest and pos is not auto-filled + var custom:haxe.PosInfos = {fileName: "X", lineNumber: 1, className: "C", methodName: "manual"}; + function sr(...rest:String, ?pos:haxe.PosInfos):String { + return rest.toArray().join(",") + ":" + pos.methodName; + } + eq("a,b:manual", sr("a", "b", @:posInfos custom)); + eq(":testPosInfos", sr()); // auto-fill still applies without the marker + + // works for non-rest too (an explicit override rather than skip-by-mismatch) + function nb(?b:Bool, ?pos:haxe.PosInfos):String { + return (b == null ? "_" : "" + b) + ":" + pos.methodName; + } + eq("_:manual", nb(@:posInfos custom)); + eq("true:manual", nb(true, @:posInfos custom)); } function testToArray() { From 73f146f14efbef0026866bec73ae250300919d6d Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 14:41:32 +0200 Subject: [PATCH 08/22] [typer] prototype @:implicitArgResolver for user-defined auto-filled arguments An abstract marked `@:implicitArgResolver(name)` can auto-fill an omitted optional argument of that type; the named static function resolves the value at each call site. This generalizes the PosInfos auto-fill to user types (#4583). - Detection: cheap `Meta.has a.a_meta` on the abstract (no for_type involvement). - The former `is_pos_infos`/`mk_pos_infos` fill guards in unify_call_args become `is_implicit_value`/`mk_implicit_value`; PosInfos remains the built-in fallback. - Dispatch on the resolver field kind: a macro runs at the call site (Context API available, currentPos resolves to the call site); a plain function becomes a runtime call. A resolver's own optional/implicit args are filled normally, so e.g. a ?pos:PosInfos forwards the original call site. - Cycle guard (module-level rec_stack) wraps both paths: self- and mutual- referential resolvers error with "Cyclic @:implicitArgResolver" instead of recursing forever. Prototype scope: PosInfos is not yet migrated onto this mechanism; resolver types after a rest argument are not canonicalized; validation is minimal. Meta registered in src-json/meta.json. --- src-json/meta.json | 7 +++++ src/typing/callUnification.ml | 51 +++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src-json/meta.json b/src-json/meta.json index b07173d13a7..1295d35777c 100644 --- a/src-json/meta.json +++ b/src-json/meta.json @@ -487,6 +487,13 @@ "platforms": ["python"], "targets": ["TClass"] }, + { + "name": "ImplicitArgResolver", + "metadata": ":implicitArgResolver", + "doc": "Marks an abstract whose values auto-fill an omitted optional argument of that type. The named static function (macro or not) resolves the value at each call site.", + "params": ["Resolver function name"], + "targets": ["TAbstract"] + }, { "name": "ImplicitCast", "metadata": ":implicitCast", diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index aa6982a6b4d..d5d97c27915 100644 --- a/src/typing/callUnification.ml +++ b/src/typing/callUnification.ml @@ -7,6 +7,11 @@ open Error open FieldAccess open FieldCallCandidate +let implicit_resolver_stack = new_rec_stack() + +let mk_implicit_resolver_value_ref : (typer -> tclass -> tclass_field -> t list -> t -> pos -> texpr) ref = + ref (fun _ _ _ _ _ _ -> assert false) + let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inline in_overload = let call_error err p = raise_error_msg (Call_error err) p in @@ -43,18 +48,36 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin true ) el in let pos_override_used = ref false in - let mk_pos_infos t = + let implicit_resolver t = + match follow t with + | TAbstract(a,pl) when Meta.has Meta.ImplicitArgResolver a.a_meta -> + (match Meta.get Meta.ImplicitArgResolver a.a_meta, a.a_impl with + | (_,[(EConst(Ident name),_)],_), Some c -> + (try Some(a,c,pl,PMap.find name c.cl_statics) with Not_found -> None) + | _ -> None) + | _ -> + None + in + let is_implicit_value t = is_pos_infos t || implicit_resolver t <> None in + let mk_implicit_value t = match !pos_override with - | Some e -> + | Some e when is_pos_infos t -> pos_override_used := true; (try type_against "pos" t e with WithTypeError err -> arg_error err "pos" true) - | None -> - mk_infos_t ctx callp [] t + | _ -> + match implicit_resolver t with + | Some(a,c,pl,cf) -> + if rec_stack_memq c implicit_resolver_stack then + raise_typing_error ("Cyclic @:implicitArgResolver for " ^ s_type_path a.a_path) callp; + rec_stack_loop implicit_resolver_stack c + (fun () -> !mk_implicit_resolver_value_ref ctx c cf pl t callp) () + | None -> + mk_infos_t ctx callp [] t in let default_value name t = - if is_pos_infos t then - mk_pos_infos t + if is_implicit_value t then + mk_implicit_value t else null (ctx.t.tnull t) callp in @@ -74,7 +97,7 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin end; [] | _,(_,true,t) :: ((_,false,tr) :: _ as args) when is_pos_infos t && ExtType.is_rest (follow tr) -> - mk_pos_infos t :: loop el args + mk_implicit_value t :: loop el args | _,[name,false,TAbstract({ a_path = ["cpp"],"Rest" },[t])] -> (try List.map (fun e -> type_against name t e) el with WithTypeError e -> arg_error e name false) @@ -153,7 +176,7 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin [] end else begin match loop [] args with | [] when not (inline && (ctx.com.doinline || force_inline)) && not ctx.com.config.pf_pad_nulls -> - if is_pos_infos t then [mk_pos_infos t] + if is_implicit_value t then [mk_implicit_value t] else [] | args -> let e_def = default_value name t in @@ -756,3 +779,15 @@ let make_static_call_better ctx c cf tl el t p = let fa = FieldAccess.create e1 cf fh false p in let fcc = unify_field_call ctx fa el [] p false in fcc.fc_data() + +let () = mk_implicit_resolver_value_ref := (fun ctx c cf tl t p -> + if cf.cf_kind = Method MethMacro then + match ctx.g.do_macro ctx MExpr c.cl_path cf.cf_name [] p with + | MSuccess e -> + let e = type_expr ctx e (WithType.with_type t) in + !cast_or_unify_raise_ref ctx t e p + | _ -> + type_expr ctx (EConst (Ident "null"),p) WithType.value + else + make_static_call_better ctx c cf tl [] t p +) From 44c2a5d270476fafd993d770a2216e198de87bb1 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 15:07:40 +0200 Subject: [PATCH 09/22] [tests] cover @:implicitArgResolver (plain, macro, multiple, before-rest, pos forwarding) TestImplicitArgResolver: a plain resolver fills an omitted optional arg; a resolver's own ?pos forwards the original call site; a macro resolver runs at the call site (captures the enclosing method); multiple implicit args (resolver + PosInfos, and two resolvers) all fill independently; a resolver-typed optional may precede a rest argument. Green on macro + js. --- .../unit/src/unit/TestImplicitArgResolver.hx | 74 +++++++++++++++++++ tests/unit/src/unit/TestMain.hx | 1 + 2 files changed, 75 insertions(+) create mode 100644 tests/unit/src/unit/TestImplicitArgResolver.hx diff --git a/tests/unit/src/unit/TestImplicitArgResolver.hx b/tests/unit/src/unit/TestImplicitArgResolver.hx new file mode 100644 index 00000000000..7b2362ac7e3 --- /dev/null +++ b/tests/unit/src/unit/TestImplicitArgResolver.hx @@ -0,0 +1,74 @@ +package unit; + +class TestImplicitArgResolver extends Test { + // a plain (non-macro) resolver fills an omitted optional argument + function testPlain() { + eq("plain", plain()); + eq("given", plain("given")); + } + + // a resolver may itself take ?pos:PosInfos, which forwards the original call site + function testPosForwarding() { + eq("testPosForwarding", withPos()); + } + + // a macro resolver runs at the call site (here: capturing the enclosing method) + function testMacro() { + eq("testMacro", withMacro()); + } + + // several implicit arguments (resolver + PosInfos) all fill independently + function testMultiple() { + eq("plain/testMultiple", multi()); + eq("plain/plain", two()); + } + + // a resolver-typed optional argument may sit before a rest argument + function testBeforeRest() { + eq("plain|1,2,3", beforeRest(1, 2, 3)); + eq("plain|", beforeRest()); + } + + static function plain(?c:Plain):String + return (c : String); + + static function withPos(?c:PosCtx):String + return (c : String); + + static function withMacro(?c:MacroCtx):String + return (c : String); + + static function multi(?c:Plain, ?pos:haxe.PosInfos):String + return (c : String) + "/" + pos.methodName; + + static function two(?a:Plain, ?b:Plain):String + return (a : String) + "/" + (b : String); + + static function beforeRest(?c:Plain, ...rest:Int):String + return (c : String) + "|" + rest.toArray().join(","); +} + +@:implicitArgResolver(resolve) +private abstract Plain(String) from String to String { + public inline function new(s:String) + this = s; + + static function resolve():Plain + return new Plain("plain"); +} + +@:implicitArgResolver(resolve) +private abstract PosCtx(String) to String { + public inline function new(s:String) + this = s; + + static function resolve(?pos:haxe.PosInfos):PosCtx + return new PosCtx(pos.methodName); +} + +@:implicitArgResolver(resolve) +private abstract MacroCtx(String) from String to String { + macro static function resolve() { + return macro $v{haxe.macro.Context.getLocalMethod()}; + } +} diff --git a/tests/unit/src/unit/TestMain.hx b/tests/unit/src/unit/TestMain.hx index 67692b2ef7b..4c1aca5f8c8 100644 --- a/tests/unit/src/unit/TestMain.hx +++ b/tests/unit/src/unit/TestMain.hx @@ -63,6 +63,7 @@ function main() { new TestNumericCasts(), new TestHashMap(), new TestRest(), + new TestImplicitArgResolver(), #if (!php && !lua) /* This is annoying and causes spurious CI failures. Let's just make an effort to not break it! */ From d153dfd4dc9d7565284444ca4dc1ee294839baec Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 21:45:55 +0200 Subject: [PATCH 10/22] [cpp] fill implicit accessor args in reflection (#6095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Property accessors may now declare a trailing implicit argument (?pos:haxe.PosInfos). The C++ reflective accessors in __Field/__GetStatic/__SetField/__SetStatic emit the get_X/set_X calls directly, bypassing the typer's implicit-arg injection, and the generated methods carry no C++ default value — so get_a() / set_b(v) failed to compile. Supply null() for each trailing implicit arg the accessor declares (one helper per get_/set_ shape, looked up from the class fields/statics). --- .../cpp/gen/cppGenClassImplementation.ml | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/generators/cpp/gen/cppGenClassImplementation.ml b/src/generators/cpp/gen/cppGenClassImplementation.ml index e7094dedfac..6d8ad4215fb 100644 --- a/src/generators/cpp/gen/cppGenClassImplementation.ml +++ b/src/generators/cpp/gen/cppGenClassImplementation.ml @@ -318,6 +318,27 @@ let generate_native_class base_ctx tcpp_class = cpp_file#close +let implicit_accessor_arg_count class_def accessor_name base_arity = + let field = + try Some (PMap.find accessor_name class_def.cl_fields) + with Not_found -> + (try Some (PMap.find accessor_name class_def.cl_statics) with Not_found -> None) + in + match field with + | Some cf -> + (match follow cf.cf_type with + | TFun (args, _) -> max 0 (List.length args - base_arity) + | _ -> 0) + | None -> 0 + +let implicit_getter_args class_def field = + let n = implicit_accessor_arg_count class_def ("get_" ^ field.cf_name) 0 in + String.concat ", " (List.init n (fun _ -> "null()")) + +let implicit_setter_args class_def field = + let n = implicit_accessor_arg_count class_def ("set_" ^ field.cf_name) 1 in + String.concat "" (List.init n (fun _ -> ", null()")) + let generate_managed_class base_ctx tcpp_class = let common_ctx = base_ctx.ctx_common in let class_def = tcpp_class.tcl_class in @@ -638,7 +659,7 @@ let generate_managed_class base_ctx tcpp_class = if var.tcv_has_getter then let prop_check = checkPropCall var.tcv_field in - let getter = Printf.sprintf "get_%s()" var.tcv_field.cf_name |> get_wrapper var.tcv_field in + let getter = Printf.sprintf "get_%s(%s)" var.tcv_field.cf_name (implicit_getter_args class_def var.tcv_field) |> get_wrapper var.tcv_field in (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, get_printer prop_check getter variable) :: acc else @@ -660,7 +681,7 @@ let generate_managed_class base_ctx tcpp_class = let print_property printer var acc = if var.tcv_has_getter && var.tcv_is_reflective && not (is_abstract_impl class_def) then ( let prop_check = checkPropCall var.tcv_field in - let getter = Printf.sprintf "get_%s()" var.tcv_field.cf_name |> get_wrapper var.tcv_field in + let getter = Printf.sprintf "get_%s(%s)" var.tcv_field.cf_name (implicit_getter_args class_def var.tcv_field) |> get_wrapper var.tcv_field in (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, printer prop_check getter) :: acc) else acc @@ -726,7 +747,7 @@ let generate_managed_class base_ctx tcpp_class = | Var { v_write = AccCall | AccPrivateCall } -> let prop_call = checkPropCall var.tcv_field in let setter = Printf.sprintf "set_%s" var.tcv_field.cf_name |> get_wrapper var.tcv_field in - let call = Printf.sprintf "if (%s) { return ::hx::Val( %s(inValue.Cast< %s >()) ); } else { %s }" prop_call setter casted default in + let call = Printf.sprintf "if (%s) { return ::hx::Val( %s(inValue.Cast< %s >()%s) ); } else { %s }" prop_call setter casted (implicit_setter_args class_def var.tcv_field) default in (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, call) :: acc | Var { v_write = AccNormal | AccNo | AccNever } -> @@ -745,7 +766,7 @@ let generate_managed_class base_ctx tcpp_class = | Var { v_write = AccCall | AccPrivateCall } -> let prop_call = checkPropCall var.tcv_field in let setter = Printf.sprintf "set_%s" var.tcv_field.cf_name |> get_wrapper var.tcv_field in - let call = Printf.sprintf "if (%s) { return ::hx::Val( %s(inValue.Cast< %s >()) ); }" prop_call setter casted in + let call = Printf.sprintf "if (%s) { return ::hx::Val( %s(inValue.Cast< %s >()%s) ); }" prop_call setter casted (implicit_setter_args class_def var.tcv_field) in (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, call) :: acc | _ -> @@ -772,7 +793,7 @@ let generate_managed_class base_ctx tcpp_class = | Var { v_write = AccCall | AccPrivateCall } -> let prop_call = checkPropCall var.tcv_field in let setter = Printf.sprintf "set_%s" var.tcv_field.cf_name |> get_wrapper var.tcv_field in - let call = Printf.sprintf "if (%s) { ioValue = %s(ioValue.Cast< %s >()); } else { %s = ioValue.Cast< %s >(); } return true;" prop_call setter casted var.tcv_name casted in + let call = Printf.sprintf "if (%s) { ioValue = %s(ioValue.Cast< %s >()%s); } else { %s = ioValue.Cast< %s >(); } return true;" prop_call setter casted (implicit_setter_args class_def var.tcv_field) var.tcv_name casted in (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, call) :: acc | Var { v_write = AccNormal | AccNo } -> @@ -791,7 +812,7 @@ let generate_managed_class base_ctx tcpp_class = let setter = Printf.sprintf "set_%s" var.tcv_field.cf_name |> get_wrapper var.tcv_field in let casted = castable var in - (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, Printf.sprintf "if (%s) { ioValue = %s(ioValue.Cast< %s >()); }" prop_call setter casted) :: acc + (var.tcv_field.cf_name, String.length var.tcv_field.cf_name, Printf.sprintf "if (%s) { ioValue = %s(ioValue.Cast< %s >()%s); }" prop_call setter casted (implicit_setter_args class_def var.tcv_field)) :: acc | _ -> acc else From 06967b94fe525e7a96dbb995978b44b590c8cc3f Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 21:56:39 +0200 Subject: [PATCH 11/22] [typer] validate @:implicitArgResolver declarations Report clean errors at declaration time when an @:implicitArgResolver abstract is malformed, instead of letting misuse surface as confusing failures at call sites: - resolver function name missing / not found - resolver is not static (abstract instance methods carry CfImpl) - resolver has a required argument (it is invoked with no explicit args) - plain resolver does not return the abstract (or its underlying) type Macro resolvers skip the return-type check (their returned expression is type-checked against the target at each call site). Signature-dependent checks run in a PForce delay. --- src/typing/typeloadFields.ml | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/typing/typeloadFields.ml b/src/typing/typeloadFields.ml index 7eb778725ef..5e80a93fcc8 100644 --- a/src/typing/typeloadFields.ml +++ b/src/typing/typeloadFields.ml @@ -1660,6 +1660,44 @@ let create_class_field cctx f = } in cf +let check_implicit_arg_resolver ctx_c c a = + match Meta.get Meta.ImplicitArgResolver a.a_meta with + | (_,params,meta_pos) -> + let com = ctx_c.com in + (match params with + | [(EConst(Ident name),_)] -> + let static = try Some (PMap.find name c.cl_statics) with Not_found -> None in + (match static with + | None -> + if PMap.mem name c.cl_fields then + display_error com (Printf.sprintf "@:implicitArgResolver: resolver function '%s' must be static" name) meta_pos + else + display_error com (Printf.sprintf "@:implicitArgResolver: resolver function '%s' not found" name) meta_pos + | Some cf when has_class_field_flag cf CfImpl -> + display_error com (Printf.sprintf "@:implicitArgResolver: resolver function '%s' must be static" name) cf.cf_pos + | Some cf -> + (match cf.cf_kind with + | Method _ -> () + | _ -> display_error com (Printf.sprintf "@:implicitArgResolver: resolver '%s' must be a function" name) cf.cf_pos); + let is_macro = cf.cf_kind = Method MethMacro in + delay ctx_c.g PForce (fun () -> + match follow cf.cf_type with + | TFun (args,ret) -> + List.iter (fun (aname,opt,_) -> + if not opt then + display_error com (Printf.sprintf "@:implicitArgResolver: resolver '%s' must not have a required argument ('%s')" name aname) cf.cf_pos + ) args; + if not is_macro then begin + let at = TAbstract(a,extract_param_types a.a_params) in + if not (does_unify ret at || does_unify ret a.a_this) then + display_error com (Printf.sprintf "@:implicitArgResolver: resolver '%s' must return %s" name (s_type_path a.a_path)) cf.cf_pos + end + | _ -> + ()) + ) + | _ -> + display_error com "@:implicitArgResolver expects a single resolver function name" meta_pos) + let init_class ctx_c cctx c p herits fields = let com = ctx_c.com in if cctx.is_class_debug then print_endline ("Created class context: " ^ dump_class_context cctx); @@ -1793,6 +1831,7 @@ let init_class ctx_c cctx c p herits fields = a.a_ops <- List.rev a.a_ops; a.a_unops <- List.rev a.a_unops; a.a_array <- List.rev a.a_array; + if Meta.has Meta.ImplicitArgResolver a.a_meta then check_implicit_arg_resolver ctx_c c a; | None -> () end; From 9c34c67d392d1c0a63cfeb09902a5e5ba1afb2f0 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 22:05:56 +0200 Subject: [PATCH 12/22] [tests] cover @:implicitArgResolver compile-fail cases Misc compile-fail projects for the error paths of @:implicitArgResolver: - AfterRest: a resolver-typed optional after a rest argument (only a trailing PosInfos is reordered before the rest). - SelfCycle / MutualCycle: a resolver whose implicit argument resolves back to a type already being resolved, caught by the cycle guard. --- .../ImplicitArgResolverAfterRest/Main.hx | 12 ++++++++++++ .../compile-fail.hxml | 2 ++ .../compile-fail.hxml.stderr | 1 + .../ImplicitArgResolverMutualCycle/Main.hx | 18 ++++++++++++++++++ .../compile-fail.hxml | 2 ++ .../compile-fail.hxml.stderr | 1 + .../ImplicitArgResolverSelfCycle/Main.hx | 13 +++++++++++++ .../compile-fail.hxml | 2 ++ .../compile-fail.hxml.stderr | 1 + 9 files changed, 52 insertions(+) create mode 100644 tests/misc/eval/projects/ImplicitArgResolverAfterRest/Main.hx create mode 100644 tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml create mode 100644 tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml.stderr create mode 100644 tests/misc/eval/projects/ImplicitArgResolverMutualCycle/Main.hx create mode 100644 tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml create mode 100644 tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml.stderr create mode 100644 tests/misc/eval/projects/ImplicitArgResolverSelfCycle/Main.hx create mode 100644 tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml create mode 100644 tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml.stderr diff --git a/tests/misc/eval/projects/ImplicitArgResolverAfterRest/Main.hx b/tests/misc/eval/projects/ImplicitArgResolverAfterRest/Main.hx new file mode 100644 index 00000000000..6f8581b03fc --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverAfterRest/Main.hx @@ -0,0 +1,12 @@ +class Main { + static function main() {} + + // a resolver-typed optional cannot follow a rest argument + // (only a trailing PosInfos is reordered before the rest) + static function f(...rest:Int, ?c:Ctx):Void {} +} + +@:implicitArgResolver(resolve) +abstract Ctx(String) { + static function resolve():Ctx return cast "x"; +} diff --git a/tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml b/tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml new file mode 100644 index 00000000000..e2a3d27a190 --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml @@ -0,0 +1,2 @@ +--main Main +--interp diff --git a/tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml.stderr b/tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml.stderr new file mode 100644 index 00000000000..baf7da3642f --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverAfterRest/compile-fail.hxml.stderr @@ -0,0 +1 @@ +Main.hx:6: characters 23-27 : Rest should only be used for the last function argument diff --git a/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/Main.hx b/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/Main.hx new file mode 100644 index 00000000000..c8cd453d0d7 --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/Main.hx @@ -0,0 +1,18 @@ +class Main { + static function main() { + f(); + } + + static function f(?a:A):Void {} +} + +// two resolvers whose implicit arguments reference each other cannot terminate +@:implicitArgResolver(resolve) +abstract A(String) { + static function resolve(?b:B):A return cast "x"; +} + +@:implicitArgResolver(resolve) +abstract B(String) { + static function resolve(?a:A):B return cast "x"; +} diff --git a/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml b/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml new file mode 100644 index 00000000000..e2a3d27a190 --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml @@ -0,0 +1,2 @@ +--main Main +--interp diff --git a/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml.stderr b/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml.stderr new file mode 100644 index 00000000000..1e056a76995 --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverMutualCycle/compile-fail.hxml.stderr @@ -0,0 +1 @@ +Main.hx:3: characters 3-6 : Cyclic @:implicitArgResolver for A diff --git a/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/Main.hx b/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/Main.hx new file mode 100644 index 00000000000..0983c9b927a --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/Main.hx @@ -0,0 +1,13 @@ +class Main { + static function main() { + f(); + } + + static function f(?c:Ctx):Void {} +} + +// a resolver whose own implicit argument is its own type cannot terminate +@:implicitArgResolver(resolve) +abstract Ctx(String) { + static function resolve(?self:Ctx):Ctx return cast "x"; +} diff --git a/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml b/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml new file mode 100644 index 00000000000..e2a3d27a190 --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml @@ -0,0 +1,2 @@ +--main Main +--interp diff --git a/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml.stderr b/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml.stderr new file mode 100644 index 00000000000..90a74c7ca9c --- /dev/null +++ b/tests/misc/eval/projects/ImplicitArgResolverSelfCycle/compile-fail.hxml.stderr @@ -0,0 +1 @@ +Main.hx:3: characters 3-6 : Cyclic @:implicitArgResolver for Ctx From c706335252836fe8414196c142a661033313734d Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 22:15:12 +0200 Subject: [PATCH 13/22] [typer] allow trailing PosInfos on @:op unary operators (#7362) The unary @:op declaration check compared the whole member type against `targ -> mono` with a strict type_eq, so a trailing implicit argument (?pos:haxe.PosInfos) made it fail. Strip implicit trailing args before the comparison, mirroring the binary @:op path. Auto-fill at the operator call site was already handled by the centralized implicit-arg machinery, so both prefix (@:op(!A)) and postfix (@:op(A!)) overloads now accept ?pos and forward the original call site. --- src/typing/typeloadFields.ml | 4 ++++ tests/unit/src/unit/issues/Issue7362.hx | 31 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/unit/src/unit/issues/Issue7362.hx diff --git a/src/typing/typeloadFields.ml b/src/typing/typeloadFields.ml index 5e80a93fcc8..ce557329ce6 100644 --- a/src/typing/typeloadFields.ml +++ b/src/typing/typeloadFields.ml @@ -1073,6 +1073,10 @@ let check_abstract (ctx,cctx,fctx) a c cf fd t ret p = | EUnop(op,flag,_) -> if fctx.is_macro then invalid_modifier ctx.com fctx "macro" "operator function" p; let targ = if fctx.is_abstract_member then tthis else ta in + let t = match follow t with + | TFun(args,ret) -> TFun(strip_implicit_trailing_args args,ret) + | _ -> t + in (try type_eq EqStrict t (tfun [targ] (mk_mono())) with Unify_error l -> raise_error_msg (Unify l) cf.cf_pos); a.a_unops <- (op,flag,cf) :: a.a_unops; allow_no_expr(); diff --git a/tests/unit/src/unit/issues/Issue7362.hx b/tests/unit/src/unit/issues/Issue7362.hx new file mode 100644 index 00000000000..6bde310395e --- /dev/null +++ b/tests/unit/src/unit/issues/Issue7362.hx @@ -0,0 +1,31 @@ +package unit.issues; + +class Issue7362 extends unit.Test { + function test() { + // postfix @:op(A!) + var post:Post = 7; + eq(7, post!); + var badPost:Post = null; + eq("unwrap null in test", try { badPost!; "?"; } catch (e:String) e); + + // prefix @:op(!A) + var pre:Pre = 5; + eq(5, !pre); + var badPre:Pre = null; + eq("unwrap null in test", try { !badPre; "?"; } catch (e:String) e); + } +} + +private abstract Post(Null) from Null { + @:op(A!) public inline function unwrap(?pos:haxe.PosInfos):T { + if (this == null) throw 'unwrap null in ${pos.methodName}'; + return this; + } +} + +private abstract Pre(Null) from Null { + @:op(!A) public inline function unwrap(?pos:haxe.PosInfos):T { + if (this == null) throw 'unwrap null in ${pos.methodName}'; + return this; + } +} From 6b7ab44db08fdbe6aa82c1d60b6b7594e9654921 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 22:15:12 +0200 Subject: [PATCH 14/22] [tests] cover haxe.Rest + trailing PosInfos (#10808) --- tests/unit/src/unit/issues/Issue10808.hx | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/unit/src/unit/issues/Issue10808.hx diff --git a/tests/unit/src/unit/issues/Issue10808.hx b/tests/unit/src/unit/issues/Issue10808.hx new file mode 100644 index 00000000000..24809d7dabc --- /dev/null +++ b/tests/unit/src/unit/issues/Issue10808.hx @@ -0,0 +1,13 @@ +package unit.issues; + +class Issue10808 extends unit.Test { + function test() { + eq("a,b in test", log("a", "b")); + eq(" in test", log()); + } + + // haxe.Rest and a trailing haxe.PosInfos as last arguments + static function log(...rest:String, ?pos:haxe.PosInfos):String { + return rest.toArray().join(",") + " in " + pos.methodName; + } +} From c64cd7057ad5b774458e86ac3284ba8020b7706f Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 22:33:13 +0200 Subject: [PATCH 15/22] [tests] cover trailing PosInfos on abstract casts and operators A trailing optional haxe.PosInfos is auto-filled and forwards the call site on every abstract cast/operator-overload form, exercised here: @:from (static), @:to (member and static), @:arrayAccess (read and write), @:op(a.b) resolve (read and write), and @:op(a()) callable. --- tests/unit/src/unit/TestAbstractPosInfos.hx | 82 +++++++++++++++++++++ tests/unit/src/unit/TestMain.hx | 1 + 2 files changed, 83 insertions(+) create mode 100644 tests/unit/src/unit/TestAbstractPosInfos.hx diff --git a/tests/unit/src/unit/TestAbstractPosInfos.hx b/tests/unit/src/unit/TestAbstractPosInfos.hx new file mode 100644 index 00000000000..c78ddc3468c --- /dev/null +++ b/tests/unit/src/unit/TestAbstractPosInfos.hx @@ -0,0 +1,82 @@ +package unit; + +// A trailing optional haxe.PosInfos is auto-filled (and forwards the call site) +// on every abstract cast / operator-overload form, not just on plain functions. +class TestAbstractPosInfos extends Test { + function testFromStatic() { + var f:FromS = 7; + eq("7@testFromStatic", f.get()); + } + + function testToMember() { + var b = new ToMember("a"); + var s:String = b; + eq("a@testToMember", s); + } + + function testToStatic() { + var t = new ToStatic("a"); + var arr:Array = t; + eq("a", arr[0]); + eq("testToStatic", arr[1]); + } + + function testArrayAccess() { + var a = new Arr(); + a[0] = "x"; // set: stores "x/testArrayAccess" + eq("x/testArrayAccess|testArrayAccess", a[0]); // get: appends + } + + function testResolve() { + var d = new Dyn(); + d.foo = "bar"; // write resolve: stores "bar/testResolve" + eq("bar/testResolve|testResolve", d.foo); // read resolve: appends + eq("?|testResolve", d.missing); + } + + function testCallable() { + var fn = new Fn("f"); + eq("f(42)@testCallable", fn(42)); + } +} + +private abstract FromS(String) { + public inline function new(s) this = s; + public inline function get():String return this; + @:from static function fromInt(i:Int, ?pos:haxe.PosInfos):FromS return new FromS(i + "@" + pos.methodName); +} + +private abstract ToMember(String) { + public inline function new(s) this = s; + @:to function toStr(?pos:haxe.PosInfos):String return this + "@" + pos.methodName; +} + +private abstract ToStatic(String) { + public inline function new(s) this = s; + // static @:to: first argument is the underlying type + @:to static function toArr(u:String, ?pos:haxe.PosInfos):Array return [u, pos.methodName]; +} + +private abstract Arr(Array) { + public inline function new() this = []; + @:arrayAccess function get(i:Int, ?pos:haxe.PosInfos):String return this[i] + "|" + pos.methodName; + @:arrayAccess function set(i:Int, v:String, ?pos:haxe.PosInfos):String { + this[i] = v + "/" + pos.methodName; + return this[i]; + } +} + +private abstract Dyn(Map) { + public inline function new() this = new Map(); + @:op(a.b) function field(name:String, ?pos:haxe.PosInfos):String + return (this.exists(name) ? this[name] : "?") + "|" + pos.methodName; + @:op(a.b) function setField(name:String, value:String, ?pos:haxe.PosInfos):String { + this[name] = value + "/" + pos.methodName; + return value; + } +} + +private abstract Fn(String) { + public inline function new(s) this = s; + @:op(a()) function call(x:Int, ?pos:haxe.PosInfos):String return this + "(" + x + ")@" + pos.methodName; +} diff --git a/tests/unit/src/unit/TestMain.hx b/tests/unit/src/unit/TestMain.hx index 4c1aca5f8c8..faee7e06483 100644 --- a/tests/unit/src/unit/TestMain.hx +++ b/tests/unit/src/unit/TestMain.hx @@ -64,6 +64,7 @@ function main() { new TestHashMap(), new TestRest(), new TestImplicitArgResolver(), + new TestAbstractPosInfos(), #if (!php && !lua) /* This is annoying and causes spurious CI failures. Let's just make an effort to not break it! */ From d973f74f5a9dffe2a75954d2d596ca2118797029 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 22:46:15 +0200 Subject: [PATCH 16/22] [macro] allow trailing PosInfos on macro functions (#11712) A macro function may now declare a trailing ?pos:haxe.PosInfos; it is filled with the *call-site* position (methodName/className/fileName/lineNumber), not the macro's own position (which is what Context.currentPos() returns). - typeloadFields: stop banning haxe.PosInfos on the caller-side macro signature. - macroContext.type_macro: strip the trailing implicit pos before the Expr-argument machinery (handling the canonicalized "?pos before a trailing rest" placement too), build the call-site PosInfos with the caller's typer context via mk_infos_t, evaluate it, and insert it at the matching argument position. --- src/typing/macroContext.ml | 17 +++++++++++ src/typing/typeloadFields.ml | 2 +- tests/unit/src/unit/issues/Issue11712.hx | 36 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/unit/src/unit/issues/Issue11712.hx diff --git a/src/typing/macroContext.ml b/src/typing/macroContext.ml index 189654b8409..19e7ee50530 100644 --- a/src/typing/macroContext.ml +++ b/src/typing/macroContext.ml @@ -838,6 +838,13 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = let mctx = get_macro_context ctx in let api = make_macro_api ctx mctx p in let mctx, (margs,mret,mclass,mfield), call_macro = load_macro ctx ctx.com mctx api (mode = MDisplay) cpath f p in + let margs,pos_infos = match List.rev margs with + | (_,o,t) :: margs_rev when o && is_pos_infos t -> + List.rev margs_rev,Some(t,false) + | ((_,_,rt) as rest) :: (_,o,t) :: margs_rev when o && is_pos_infos t && ExtType.is_rest (follow rt) -> + List.rev (rest :: margs_rev),Some(t,true) + | _ -> margs,None + in let margs = (* Replace "rest:haxe.Rest" in macro signatures with "rest:Array". @@ -973,6 +980,16 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = | [] -> args | _ -> (match List.rev args with _::args -> List.rev args | [] -> []) @ [Interp.encode_array (List.map Interp.encode_expr el2)] in + let args = match pos_infos with + | None -> args + | Some(t,before_rest) -> + let einfos = mk_infos_t ctx p [] t in + let v = match Interp.eval_expr (Interp.get_ctx()) einfos with Some v -> v | None -> Interp.vnull in + if before_rest then + (match List.rev args with last :: rev -> List.rev (last :: v :: rev) | [] -> [v]) + else + args @ [v] + in let call() = match call_macro args with | None -> diff --git a/src/typing/typeloadFields.ml b/src/typing/typeloadFields.ml index ce557329ce6..f2a86371046 100644 --- a/src/typing/typeloadFields.ml +++ b/src/typing/typeloadFields.ml @@ -1232,7 +1232,7 @@ let create_method (ctx,cctx,fctx) c f cf fd p = let to_dyn p ptp = match ptp.path with | { tpackage = ["haxe";"macro"]; tname = "Expr"; tsub = Some ("ExprOf"); tparams = [TPType t] } -> Some t | { tpackage = []; tname = ("ExprOf"); tsub = None; tparams = [TPType t] } -> Some t - | { tpackage = ["haxe"]; tname = ("PosInfos"); tsub = None; tparams = [] } -> raise_typing_error "haxe.PosInfos is not allowed on macro functions, use Context.currentPos() instead" p + | { tpackage = ["haxe"]; tname = ("PosInfos"); tsub = None; tparams = [] } -> Some (make_ptp_th (mk_type_path (["haxe"],"PosInfos")) p) | _ -> tdyn in { diff --git a/tests/unit/src/unit/issues/Issue11712.hx b/tests/unit/src/unit/issues/Issue11712.hx new file mode 100644 index 00000000000..9d4ca4bc9e8 --- /dev/null +++ b/tests/unit/src/unit/issues/Issue11712.hx @@ -0,0 +1,36 @@ +package unit.issues; + +import unit.Test; + +class Issue11712 extends Test { + #if !macro + function test() { + // ?pos on a macro function is filled with the *call site*, not the macro's own pos + eq("test", whereMethod()); + eq("unit.issues.Issue11712", whereClass()); + + // works alongside a regular Expr argument + eq("test", afterExpr("ignored")); + + // works alongside a rest argument + eq("2:test", afterRest(1, 2)); + eq("0:test", afterRest()); + } + #end + + macro static function whereMethod(?pos:haxe.PosInfos) { + return macro $v{pos.methodName}; + } + + macro static function whereClass(?pos:haxe.PosInfos) { + return macro $v{pos.className}; + } + + macro static function afterExpr(e:haxe.macro.Expr, ?pos:haxe.PosInfos) { + return macro $v{pos.methodName}; + } + + macro static function afterRest(...rest:haxe.macro.Expr, ?pos:haxe.PosInfos) { + return macro $v{rest.length + ":" + pos.methodName}; + } +} From f8beb0b560ac698b2fbcc45473dfa17cbce8174d Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 23:01:26 +0200 Subject: [PATCH 17/22] [macro] clear error for explicit PosInfos on macro calls; cover forwarding (#11712) A macro's ?pos is compiler-filled (macro arguments resolve at compile time), so an explicit value for it cannot be honored. Detect an extra trailing argument or an @:posInfos override on a macro call and report a clear message instead of the opaque "Too many arguments". Tests: Issue11712 now also covers forwarding a macro's auto-filled pos to a regular helper run in macro context; MacroPosInfosExplicit (misc) covers the explicit-pass error. --- src/typing/macroContext.ml | 8 ++++++++ .../eval/projects/MacroPosInfosExplicit/Main.hx | 14 ++++++++++++++ .../MacroPosInfosExplicit/compile-fail.hxml | 2 ++ .../MacroPosInfosExplicit/compile-fail.hxml.stderr | 1 + tests/unit/src/unit/issues/Issue11712.hx | 13 +++++++++++++ 5 files changed, 38 insertions(+) create mode 100644 tests/misc/eval/projects/MacroPosInfosExplicit/Main.hx create mode 100644 tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml create mode 100644 tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml.stderr diff --git a/src/typing/macroContext.ml b/src/typing/macroContext.ml index 19e7ee50530..a1b593f5f14 100644 --- a/src/typing/macroContext.ml +++ b/src/typing/macroContext.ml @@ -845,6 +845,14 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = List.rev (rest :: margs_rev),Some(t,true) | _ -> margs,None in + (match pos_infos with + | Some _ -> + let has_override = List.exists (fun e -> match fst e with EMeta((Meta.PosInfos,_,_),_) -> true | _ -> false) el in + let has_rest = match List.rev margs with (_,_,t) :: _ -> ExtType.is_rest (follow t) | _ -> false in + if has_override || (not has_rest && List.length el = List.length margs + 1) then + raise_typing_error "haxe.PosInfos is auto-filled on macro functions and cannot be passed explicitly" p + | None -> + ()); let margs = (* Replace "rest:haxe.Rest" in macro signatures with "rest:Array". diff --git a/tests/misc/eval/projects/MacroPosInfosExplicit/Main.hx b/tests/misc/eval/projects/MacroPosInfosExplicit/Main.hx new file mode 100644 index 00000000000..de8043dd0be --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosExplicit/Main.hx @@ -0,0 +1,14 @@ +class Main { + #if !macro + static function main() { + // a macro function's ?pos is auto-filled and cannot be passed explicitly + foo(@:posInfos here()); + } + + static function here():haxe.PosInfos return null; + #end + + macro static function foo(?pos:haxe.PosInfos) { + return macro null; + } +} diff --git a/tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml b/tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml new file mode 100644 index 00000000000..e2a3d27a190 --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml @@ -0,0 +1,2 @@ +--main Main +--interp diff --git a/tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml.stderr b/tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml.stderr new file mode 100644 index 00000000000..e1488107a69 --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosExplicit/compile-fail.hxml.stderr @@ -0,0 +1 @@ +Main.hx:5: characters 3-25 : haxe.PosInfos is auto-filled on macro functions and cannot be passed explicitly diff --git a/tests/unit/src/unit/issues/Issue11712.hx b/tests/unit/src/unit/issues/Issue11712.hx index 9d4ca4bc9e8..5e8ab3e59ca 100644 --- a/tests/unit/src/unit/issues/Issue11712.hx +++ b/tests/unit/src/unit/issues/Issue11712.hx @@ -15,9 +15,17 @@ class Issue11712 extends Test { // works alongside a rest argument eq("2:test", afterRest(1, 2)); eq("0:test", afterRest()); + + // a macro can forward its (call-site) pos to a regular helper run in macro context + eq("test", forwarded()); } #end + // a regular function, also callable from the macro above at compile time + static function helper(?pos:haxe.PosInfos):String { + return pos.methodName; + } + macro static function whereMethod(?pos:haxe.PosInfos) { return macro $v{pos.methodName}; } @@ -33,4 +41,9 @@ class Issue11712 extends Test { macro static function afterRest(...rest:haxe.macro.Expr, ?pos:haxe.PosInfos) { return macro $v{rest.length + ":" + pos.methodName}; } + + macro static function forwarded(?pos:haxe.PosInfos) { + // forward the auto-filled call-site pos to a regular function (runs in macro context) + return macro $v{helper(pos)}; + } } From 597c02e0a1ab514ceefc0e0b49e218498149faae Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 23:10:07 +0200 Subject: [PATCH 18/22] [tests] cover @:genericBuild ?pos use-site position (#11712) A @:genericBuild macro may take a trailing ?pos:haxe.PosInfos, filled with the use-site position where the built type is referenced. Asserted via typedAs; the @:genericBuild class is fenced under #if !macro since the surrounding module also declares expression macros (which drag it into macro context, where @:genericBuild is disallowed). --- tests/unit/src/unit/issues/Issue11712.hx | 9 +++++++++ .../src/unit/issues/misc/Issue11712Macro.hx | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/unit/src/unit/issues/misc/Issue11712Macro.hx diff --git a/tests/unit/src/unit/issues/Issue11712.hx b/tests/unit/src/unit/issues/Issue11712.hx index 5e8ab3e59ca..d9ede7900b0 100644 --- a/tests/unit/src/unit/issues/Issue11712.hx +++ b/tests/unit/src/unit/issues/Issue11712.hx @@ -1,6 +1,12 @@ package unit.issues; import unit.Test; +#if !macro +import unit.issues.misc.Issue11712Macro; + +@:genericBuild(unit.issues.misc.Issue11712Macro.build()) +private class GBuild {} +#end class Issue11712 extends Test { #if !macro @@ -18,6 +24,9 @@ class Issue11712 extends Test { // a macro can forward its (call-site) pos to a regular helper run in macro context eq("test", forwarded()); + + // a @:genericBuild macro's ?pos is the use-site position + unit.HelperMacros.typedAs((null : GBuild), (null : Issue11712Result<"test">)); } #end diff --git a/tests/unit/src/unit/issues/misc/Issue11712Macro.hx b/tests/unit/src/unit/issues/misc/Issue11712Macro.hx new file mode 100644 index 00000000000..0ccd07e9d5f --- /dev/null +++ b/tests/unit/src/unit/issues/misc/Issue11712Macro.hx @@ -0,0 +1,20 @@ +package unit.issues.misc; + +import haxe.macro.Expr; + +// carries the captured call-site method name as a const type parameter +class Issue11712Result {} + +class Issue11712Macro { + // a @:genericBuild macro may take a trailing ?pos:haxe.PosInfos, filled with the + // use-site position (where the built type is referenced) + macro static public function build(?pos:haxe.PosInfos):ComplexType { + var ct = TPath({ + name: "Issue11712Macro", + pack: ["unit", "issues", "misc"], + sub: "Issue11712Result", + params: [TPExpr(macro $v{pos.methodName})] + }); + return macro : $ct; + } +} From bdc5a9bb735b37d14340f89a4792a8d70b298a73 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Fri, 19 Jun 2026 23:14:47 +0200 Subject: [PATCH 19/22] [tests] cover init macro with trailing PosInfos (#11712) An init macro (--macro) may take a trailing ?pos:haxe.PosInfos without breaking compilation; it is filled with the synthetic command-line position. Success project with a stdout check documenting the resolved values. --- tests/misc/eval/projects/MacroPosInfosInit/Macros.hx | 8 ++++++++ tests/misc/eval/projects/MacroPosInfosInit/Main.hx | 1 + tests/misc/eval/projects/MacroPosInfosInit/compile.hxml | 3 +++ .../eval/projects/MacroPosInfosInit/compile.hxml.stdout | 1 + 4 files changed, 13 insertions(+) create mode 100644 tests/misc/eval/projects/MacroPosInfosInit/Macros.hx create mode 100644 tests/misc/eval/projects/MacroPosInfosInit/Main.hx create mode 100644 tests/misc/eval/projects/MacroPosInfosInit/compile.hxml create mode 100644 tests/misc/eval/projects/MacroPosInfosInit/compile.hxml.stdout diff --git a/tests/misc/eval/projects/MacroPosInfosInit/Macros.hx b/tests/misc/eval/projects/MacroPosInfosInit/Macros.hx new file mode 100644 index 00000000000..c05498a7151 --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosInit/Macros.hx @@ -0,0 +1,8 @@ +class Macros { + // an init macro may take a trailing ?pos:haxe.PosInfos; it is filled with the + // (synthetic) command-line position and does not break compilation + macro static function init(?pos:haxe.PosInfos) { + Sys.println("init pos: file=" + pos.fileName + " line=" + pos.lineNumber + " class=" + pos.className + " method=" + pos.methodName); + return macro null; + } +} diff --git a/tests/misc/eval/projects/MacroPosInfosInit/Main.hx b/tests/misc/eval/projects/MacroPosInfosInit/Main.hx new file mode 100644 index 00000000000..a71cf3b3e00 --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosInit/Main.hx @@ -0,0 +1 @@ +function main() {} diff --git a/tests/misc/eval/projects/MacroPosInfosInit/compile.hxml b/tests/misc/eval/projects/MacroPosInfosInit/compile.hxml new file mode 100644 index 00000000000..6388ee155c0 --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosInit/compile.hxml @@ -0,0 +1,3 @@ +--main Main +--macro Macros.init() +--interp diff --git a/tests/misc/eval/projects/MacroPosInfosInit/compile.hxml.stdout b/tests/misc/eval/projects/MacroPosInfosInit/compile.hxml.stdout new file mode 100644 index 00000000000..fffc832befc --- /dev/null +++ b/tests/misc/eval/projects/MacroPosInfosInit/compile.hxml.stdout @@ -0,0 +1 @@ +init pos: file=--macro Macros.init() line=1 class= method=null From c7f0695514517a98354a1d316249f18404b7ef06 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Sat, 20 Jun 2026 11:37:52 +0200 Subject: [PATCH 20/22] [macro] clarify explicit-PosInfos rejection on macro calls Reuse the before_rest flag already carried by the stripped implicit-pos tuple instead of re-deriving "ends with a rest" (which re-followed the last argument type), rename pos_infos -> implicit_pos (it holds the stripped parameter, not a value), and name the arity test fills_pos_slot with a note on why it is `= +1` (more than one extra positional is a genuine "too many arguments"). No behavior change. --- src/typing/macroContext.ml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/typing/macroContext.ml b/src/typing/macroContext.ml index a1b593f5f14..41a9951bb8b 100644 --- a/src/typing/macroContext.ml +++ b/src/typing/macroContext.ml @@ -838,18 +838,20 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = let mctx = get_macro_context ctx in let api = make_macro_api ctx mctx p in let mctx, (margs,mret,mclass,mfield), call_macro = load_macro ctx ctx.com mctx api (mode = MDisplay) cpath f p in - let margs,pos_infos = match List.rev margs with + let margs,implicit_pos = match List.rev margs with | (_,o,t) :: margs_rev when o && is_pos_infos t -> List.rev margs_rev,Some(t,false) | ((_,_,rt) as rest) :: (_,o,t) :: margs_rev when o && is_pos_infos t && ExtType.is_rest (follow rt) -> List.rev (rest :: margs_rev),Some(t,true) | _ -> margs,None in - (match pos_infos with - | Some _ -> - let has_override = List.exists (fun e -> match fst e with EMeta((Meta.PosInfos,_,_),_) -> true | _ -> false) el in - let has_rest = match List.rev margs with (_,_,t) :: _ -> ExtType.is_rest (follow t) | _ -> false in - if has_override || (not has_rest && List.length el = List.length margs + 1) then + (match implicit_pos with + | Some (_,before_rest) -> + let has_override = List.exists (fun (e,_) -> match e with EMeta((Meta.PosInfos,_,_),_) -> true | _ -> false) el in + (* the stripped pos slot can't be reached positionally past a rest; without one, + exactly one extra positional lands in it (more than that is genuinely too many) *) + let fills_pos_slot = not before_rest && List.length el = List.length margs + 1 in + if has_override || fills_pos_slot then raise_typing_error "haxe.PosInfos is auto-filled on macro functions and cannot be passed explicitly" p | None -> ()); @@ -988,7 +990,7 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = | [] -> args | _ -> (match List.rev args with _::args -> List.rev args | [] -> []) @ [Interp.encode_array (List.map Interp.encode_expr el2)] in - let args = match pos_infos with + let args = match implicit_pos with | None -> args | Some(t,before_rest) -> let einfos = mk_infos_t ctx p [] t in From 08bdefddc21ff0bfd0a168eb2401c7d1147fcaa8 Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Sun, 21 Jun 2026 11:54:46 +0200 Subject: [PATCH 21/22] [macro] expose expected type to @:implicitArgResolver macros A macro resolver now runs with the awaited argument type pushed onto the with_type stack, so Context.getExpectedType() resolves to it. This lets a generic abstract resolve a value depending on its type parameter. --- src/typing/callUnification.ml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index d5d97c27915..0367556164d 100644 --- a/src/typing/callUnification.ml +++ b/src/typing/callUnification.ml @@ -782,7 +782,10 @@ let make_static_call_better ctx c cf tl el t p = let () = mk_implicit_resolver_value_ref := (fun ctx c cf tl t p -> if cf.cf_kind = Method MethMacro then - match ctx.g.do_macro ctx MExpr c.cl_path cf.cf_name [] p with + let _ = ctx.e.with_type_stack <- (WithType.with_type t) :: ctx.e.with_type_stack in + let r = ctx.g.do_macro ctx MExpr c.cl_path cf.cf_name [] p in + ctx.e.with_type_stack <- List.tl ctx.e.with_type_stack; + match r with | MSuccess e -> let e = type_expr ctx e (WithType.with_type t) in !cast_or_unify_raise_ref ctx t e p From d55f461614ff0df2cd489afd2cbf471b7307059b Mon Sep 17 00:00:00 2001 From: Rudy Ges Date: Sun, 21 Jun 2026 11:55:17 +0200 Subject: [PATCH 22/22] [tests] cover type-parameter-dependent implicit arg resolver (#6616) Ports TestFromNothing onto @:implicitArgResolver: a generic abstract Dep whose macro resolver picks a value per type parameter via the expected type. --- .../unit/src/unit/TestImplicitArgResolver.hx | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/unit/src/unit/TestImplicitArgResolver.hx b/tests/unit/src/unit/TestImplicitArgResolver.hx index 7b2362ac7e3..6aae401558e 100644 --- a/tests/unit/src/unit/TestImplicitArgResolver.hx +++ b/tests/unit/src/unit/TestImplicitArgResolver.hx @@ -1,5 +1,11 @@ package unit; +#if macro +import haxe.macro.Expr; +import haxe.macro.Context; +import haxe.macro.Type; +#end + class TestImplicitArgResolver extends Test { // a plain (non-macro) resolver fills an omitted optional argument function testPlain() { @@ -29,6 +35,95 @@ class TestImplicitArgResolver extends Test { eq("plain|", beforeRest()); } + // a macro resolver may inspect the expected type, so a generic abstract can + // resolve a different value per type parameter (ported from #6616) + function testGenericResolver() { + function foo1(?a:Dep) { + return a; + } + + t(foo1() == 1); + t(foo1(2) == 2); + + function foo2(?a:Dep, ?b:Dep) { + return (a : Int) + (b : Int); + } + + t(foo2() == 2); + t(foo2(2, 3) == 5); + t(foo2(2) == 3); + + function foo2(?a:Dep, ?b:Dep) { + return (a : Int) + (b : Int); + } + + function foo3(?a:Dep, ?b:Dep, ?c:Dep) { + return (a : Int) + (b : Int) + (c : Int); + } + + t(foo2() == 3); + t(foo3() == 6); + + t(foo2(7) == 9); + t(foo3(7) == 12); + + t(foo2.bind()() == 3); + t(foo3.bind()() == 6); + + t(foo2.bind(7)() == 9); + t(foo3.bind(7)() == 12); + + t(foo2.bind(_)(7) == 9); + t(foo2.bind(_, _)(7, 3) == 10); + + function foo4(x:T, plus:T->T->T, ?a:Dep) { + return plus(x, a); + } + + t(foo4(1, (a, b) -> a + b) == 2); + + var f = foo4.bind(_, (a, b) -> a + b); + t(f(1) == 2); + + t(foo4((1 : Int2), (a, b) -> (a : Int) + (b : Int)) == 3); + + var f = foo4.bind(_, (a:Int2, b:Int2) -> ((a : Int) + (b : Int) : Int2)); + t(f(1) == 3); + + function foo5(?x:Dep, ?y:Dep) { + return Std.string(x) + "-" + Std.string(y); + } + + t(foo5(3) == "3-1"); + + function foo(?x:Int = 5, ?y:Dep) { + return Std.string(x) + "-" + Std.string(y); + } + + t(foo() == "5-1"); + t(foo.bind()() == "5-1"); + + function foo(?x:Dep, ?y:Int = 5) { + return Std.string(x) + "-" + Std.string(y); + } + + t(foo() == "1-5"); + t(foo.bind()() == "1-5"); + + function foo(?w:Int = 7, ?x:Dep, ?y:Int = 5) { + return Std.string(w) + "-" + Std.string(x) + "-" + Std.string(y); + } + + t(foo() == "7-1-5"); + t(foo.bind()() == "7-1-5"); + + function foo(?x:Int = 5, ?y:haxe.PosInfos) { + return Std.string(x) + "-" + (y != null); + } + + t(foo.bind()() == "5-true"); + } + static function plain(?c:Plain):String return (c : String); @@ -72,3 +167,33 @@ private abstract MacroCtx(String) from String to String { return macro $v{haxe.macro.Context.getLocalMethod()}; } } + +@:implicitArgResolver(resolve) +private abstract Dep(T) to T { + inline function new(x:T) + this = x; + + @:from public static inline function fromT(t:T):Dep + return new Dep(t); + + // the resolver inspects the expected type to pick a value per type parameter + macro static function resolve():Expr { + var unexpected = () -> Context.fatalError("unexpected", Context.currentPos()); + return switch Context.follow(Context.getExpectedType()) { + case TAbstract(_.toString() => "unit._TestImplicitArgResolver.Dep", [t]): + switch Context.follow(t) { + case TAbstract(_.toString() => "Int", []): macro 1; + case TAbstract(_.toString() => "unit._TestImplicitArgResolver.Int2", []): macro(2 : Int2); + case TAbstract(_.toString() => "unit._TestImplicitArgResolver.Int3", []): macro(3 : Int3); + case _: unexpected(); + } + case _: unexpected(); + } + } +} + +@:transitive +private abstract Int2(Int) from Int to Int {} + +@:transitive +private abstract Int3(Int) from Int to Int {}