diff --git a/src-json/meta.json b/src-json/meta.json index 08359f212ef..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", @@ -864,6 +871,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/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/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 diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index a73d5f5a8ab..0367556164d 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 @@ -19,23 +24,6 @@ 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 mk_pos_infos t = - mk_infos_t ctx callp [] t - in - let default_value name t = - if is_pos_infos t then - mk_pos_infos t - else - null (ctx.t.tnull t) callp - in - let skipped = ref [] in - let invalid_skips = ref [] in - let skip name ul t = - if not ctx.com.config.pf_can_skip_non_nullable_argument && not (is_nullable t) then - invalid_skips := name :: !invalid_skips; - skipped := (name,ul) :: !skipped; - default_value name t - in let handle_errors fn = try fn() @@ -49,6 +37,58 @@ let unify_call_args ctx el args r callp ?(call_field_p=callp) inline force_inlin !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 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 when is_pos_infos t -> + pos_override_used := true; + (try type_against "pos" t e + with WithTypeError err -> arg_error err "pos" true) + | _ -> + 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_implicit_value t then + mk_implicit_value t + else + null (ctx.t.tnull t) callp + in + let skipped = ref [] in + let invalid_skips = ref [] in + let skip name ul t = + if not ctx.com.config.pf_can_skip_non_nullable_argument && not (is_nullable t) then + invalid_skips := name :: !invalid_skips; + skipped := (name,ul) :: !skipped; + default_value name t + in let rec loop el args = match el,args with | [],[] -> begin match List.rev !invalid_skips with @@ -56,6 +96,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_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) @@ -134,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 @@ -151,7 +193,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 +203,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 (); @@ -187,6 +234,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 = @@ -728,3 +779,18 @@ 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 + 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 + | _ -> + type_expr ctx (EConst (Ident "null"),p) WithType.value + else + make_static_call_better ctx c cf tl [] t p +) 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/src/typing/macroContext.ml b/src/typing/macroContext.ml index 189654b8409..41a9951bb8b 100644 --- a/src/typing/macroContext.ml +++ b/src/typing/macroContext.ml @@ -838,6 +838,23 @@ 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,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 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 -> + ()); let margs = (* Replace "rest:haxe.Rest" in macro signatures with "rest:Array". @@ -973,6 +990,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 implicit_pos 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/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..f2a86371046 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 @@ -1070,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(); @@ -1225,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 { @@ -1435,6 +1442,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; @@ -1653,6 +1664,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); @@ -1786,6 +1835,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; 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 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/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 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/TestImplicitArgResolver.hx b/tests/unit/src/unit/TestImplicitArgResolver.hx new file mode 100644 index 00000000000..6aae401558e --- /dev/null +++ b/tests/unit/src/unit/TestImplicitArgResolver.hx @@ -0,0 +1,199 @@ +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() { + 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()); + } + + // 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); + + 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()}; + } +} + +@: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 {} diff --git a/tests/unit/src/unit/TestMain.hx b/tests/unit/src/unit/TestMain.hx index 67692b2ef7b..faee7e06483 100644 --- a/tests/unit/src/unit/TestMain.hx +++ b/tests/unit/src/unit/TestMain.hx @@ -63,6 +63,8 @@ function main() { new TestNumericCasts(), 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! */ diff --git a/tests/unit/src/unit/TestRest.hx b/tests/unit/src/unit/TestRest.hx index fd73914283b..888af24757f 100644 --- a/tests/unit/src/unit/TestRest.hx +++ b/tests/unit/src/unit/TestRest.hx @@ -17,6 +17,50 @@ 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):String { + return rest.length + ":" + pos.methodName; + } + 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() { function rest(...r:Int):Array { var a = r.toArray(); 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; + } +} diff --git a/tests/unit/src/unit/issues/Issue10955.hx b/tests/unit/src/unit/issues/Issue10955.hx new file mode 100644 index 00000000000..5d58c248804 --- /dev/null +++ b/tests/unit/src/unit/issues/Issue10955.hx @@ -0,0 +1,54 @@ +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(","); + } + + 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(","); + } + + // 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; + } +} diff --git a/tests/unit/src/unit/issues/Issue11712.hx b/tests/unit/src/unit/issues/Issue11712.hx new file mode 100644 index 00000000000..d9ede7900b0 --- /dev/null +++ b/tests/unit/src/unit/issues/Issue11712.hx @@ -0,0 +1,58 @@ +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 + 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()); + + // 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 + + // 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}; + } + + 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}; + } + + 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)}; + } +} 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); + } +} 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; + } +} 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; + } +}