diff --git a/Changelog.md b/Changelog.md index 15f1e6e20fe..569d2dddfd2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Motoko compiler changelog +* motoko (`moc`) + + * feat: Implicit argument derivation — the compiler can derive implicit arguments from functions that themselves have implicit parameters (e.g., `compare` for `[Nat]` from `Array.compare` + `Nat.compare`). Works transitively and is depth-limited via `--implicit-derivation-depth` (#5966). + ## 1.6.0 (2026-04-21) * motoko (`moc`) diff --git a/doc/md/15-compiler-ref.md b/doc/md/15-compiler-ref.md index 705031777a2..9cae6754703 100644 --- a/doc/md/15-compiler-ref.md +++ b/doc/md/15-compiler-ref.md @@ -52,6 +52,7 @@ You can use the following options with the `moc` command. | `--incremental-gc` | Use incremental GC (default, works with both enhanced orthogonal persistence and legacy/classical persistence). | | `--idl` | Compile binary and emit Candid IDL specification to `.did` file. | | `-i` | Runs the compiler in an interactive read–eval–print loop (REPL) shell so you can evaluate program execution (implies -r). | +| `--implicit-derivation-depth ` | Maximum recursion depth for [implicit](./fundamentals/11-implicit-parameters.md) argument derivation (default 100). Raise if a complex derivation is rejected as depth-limited. | | `--legacy-persistence` | Use legacy (classical) persistence. This also enables the usage of --copying-gc, --compacting-gc, and --generational-gc. Deprecated in favor of the new enhanced orthogonal persistence, which is default. Legacy persistence will be removed in the future.| | `--map` | Outputs a JavaScript source map. | | `--max-stable-pages ` | Set maximum number of pages available for library `ExperimentStableMemory.mo` (default 65536). | diff --git a/doc/md/16-language-manual.md b/doc/md/16-language-manual.md index 0766a563972..aebefd1e4c2 100644 --- a/doc/md/16-language-manual.md +++ b/doc/md/16-language-manual.md @@ -2373,6 +2373,11 @@ the expanded function call expression `? ? < * Ds is the disambiguated set of candidates, filtered by generality. * `.` is the name of the unique disambiguation, if one exists (that is, when Ds is a singleton set). + **Implicit derivation**: When no direct candidate is found (neither from local values, module fields, nor library fields of unimported modules when `--implicit-package` is set), the compiler additionally searches for *derivable* candidates — first among local values, then among module fields, then among library fields. + A derivable candidate is a function (possibly polymorphic) that has implicit parameters of its own, and whose type, after removing its implicit parameters and instantiating its type parameters, matches the required hole type. + If the derivable candidate's own implicit parameters can be recursively resolved (up to a configurable depth limit), the compiler synthesizes a wrapper function that calls the candidate with the resolved inner implicits. + This allows, for example, an implicit `compare : ([Nat], [Nat]) -> Order` to be derived from `Array.compare` when `Nat.compare` is in scope. The derivation depth is bounded by the `--implicit-derivation-depth` flag. + The call expression ` ? ` evaluates `` to a result `r1`. If `r1` is `trap`, then the result is `trap`. Otherwise, `` (the hole expansion of ``) is evaluated to a result `r2`. If `r2` is `trap`, the expression results in `trap`. diff --git a/doc/md/fundamentals/11-implicit-parameters.md b/doc/md/fundamentals/11-implicit-parameters.md index fae63745de3..ab8ed7f14d6 100644 --- a/doc/md/fundamentals/11-implicit-parameters.md +++ b/doc/md/fundamentals/11-implicit-parameters.md @@ -3,7 +3,7 @@ ## Overview Implicit parameters allow you to omit frequently-used function arguments at call sites when the compiler can infer them from context. This feature is particularly useful when working with ordered collections like `Map` and `Set` from the `core` library, which require comparison functions but where the comparison logic is usually obvious from the key type. -Other exampes are `equal` and `toText` functions. +Other examples are `equal` and `toText` functions. ## Basic usage @@ -11,7 +11,7 @@ Other exampes are `equal` and `toText` functions. When declaring a function, any function parameter can be declared implicit using the `implicit` type constructor: -For example, the core Map library, declares a function: +For example, the core `Map` library declares a function: ```motoko no-repl public func add(self: Map, compare : (implicit : (K, K) -> Order), key : K, value : V) { @@ -19,9 +19,9 @@ public func add(self: Map, compare : (implicit : (K, K) -> Order), k } ``` -The `implicit` marker on the type of parameter `compare` indicates the call-site can omit it the `compare` argument, provided it can be inferred the call site. +The `implicit` marker on the type of parameter `compare` indicates the call-site can omit the `compare` argument, provided it can be inferred at the call site. -A function can declare more than on implicit parameter, even of the same name. +A function can declare more than one implicit parameter, even of the same name. ```motoko @@ -55,7 +55,7 @@ Map.add(map, 5, "five"); ``` The compiler automatically finds an appropriate comparison function based on the type of the key argument. -The availabe candidates are: +The available candidates are: * Any value named `compare` whose type matches the parameter type. If there is no such value, @@ -66,7 +66,7 @@ An ambiguous call can always be disambiguated by supplying the explicit argument ### Contextual dot notation -Implicit parameters dovetail nicely with the [contextual dot notation](contextual-dot). +Implicit parameters dovetail nicely with [contextual dot notation](10-contextual-dot.md). The dot notation and implicit arguments can be used in conjunction to shorten code. For example, since the first parameter of `Map.add` is called `self`, we can both use `map` as the receiver of `add` "method" calls @@ -81,7 +81,7 @@ let map = Map.empty(); // Using contextual dot notation, without implicits - must provide compare function explicitly map.add(Nat.compare, 5, "five"); -// Using contextual dot nation together with implicits - compare function inferred from key type +// Using contextual dot notation together with implicits - compare function inferred from key type map.add(5, "five"); ``` @@ -147,7 +147,7 @@ let scores = Map.empty(); // Add player scores scores.add("Alice", 100); scores.add("Bob", 85); -scores.add( "Charlie", 92); +scores.add("Charlie", 92); // Update a score scores.add("Bob", 95); @@ -158,7 +158,7 @@ if (scores.containsKey("Alice")) { }; // Get size -let playerCount = scores.size() +let playerCount = scores.size(); ``` ## How inference works @@ -167,13 +167,57 @@ The compiler infers an implicit argument by: 1. Examining the types of the explicit arguments provided. 2. Looking for all candidate values for the implicit argument in the current scope that match the required type and name. -3. From these, selecting the best unique candidate based on type specifity. +3. From these, selecting the best unique candidate based on type specificity. If there is no unique best candidate the compiler rejects the call as ambiguous. -If a callee takes several implicits parameter, either all implicit arguments must be omitted, or all explicit and implicit arguments must be provided at the call site, +If a callee takes several implicit parameters, either all implicit arguments must be omitted, or all explicit and implicit arguments must be provided at the call site, in their declared order. +### Resolution order + +The compiler searches for implicit arguments in the following order, stopping at the first tier that produces a unique match: + +1. **Direct** — values whose type directly matches: + 1. Local values in the current scope. + 2. Module fields of modules in scope (e.g., `Nat.compare`). + 3. Fields of unimported modules (requires `--implicit-package`). +2. **Derived** — functions with implicit parameters that, after stripping their own implicits and instantiating type parameters, match the required type (see [Implicit derivation](#implicit-derivation) below): + 1. Local values in the current scope. + 2. Module fields (e.g., `Array.compare`). + 3. Fields of unimported modules (requires `--implicit-package`). +Within each tier, if multiple candidates match, the compiler picks the most specific one (by subtyping). If no unique best candidate exists, the call is rejected as ambiguous. + +This ordering guarantees that direct matches are always preferred over derived ones, and local definitions take precedence over imported or unimported module definitions. + +### Implicit derivation + +When no direct match exists, the compiler can **derive** an implicit argument from a function that itself has implicit parameters. This eliminates the need for boilerplate wrapper functions. The candidate function can be polymorphic (the compiler infers the type instantiation) or monomorphic. + +For example, suppose `Array.compare` is declared as: + +```motoko no-repl +public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order +``` + +and a function requires an implicit `compare : ([Nat], [Nat]) -> Order`. Without derivation, you would need to write a wrapper: + +```motoko no-repl +module MyArray { + public func compare(a : [Nat], b : [Nat]) : Order { + Array.compare(a, b) // resolves inner `compare` to Nat.compare + }; +}; +``` + +With derivation, the compiler handles this automatically. It recognizes that `Array.compare`, after removing its implicit `compare` parameter and instantiating `T := Nat`, has the right type. It then recursively resolves the inner implicit (`Nat.compare`) and synthesizes the wrapper for you. + +This works transitively: a `compare` for `[[Nat]]` is derived via `Array.compare<[Nat]>`, which needs `[Nat]` compare, which is derived via `Array.compare`, which needs `Nat.compare` — all resolved automatically. + +The resolution depth is bounded to guarantee termination. If you encounter a depth limit, you can increase it with `--implicit-derivation-depth` or provide the argument explicitly. + +When derivation is attempted but fails (for example, because an inner implicit can't be resolved), the compiler reports which inner implicits were missing and, when applicable, a hint about which module to import. + ### Supported types The core library provides comparison functions for common types: @@ -275,7 +319,9 @@ There is no need to update existing code unless you want to take advantage of th ## Performance considerations -Implicit arguments have no runtime overhead. The comparison function is resolved at compile time, so there is no performance difference between using implicit and explicit arguments. The resulting code is identical. +Implicit arguments are resolved at compile time. +- For direct matches, the resulting code is identical to explicitly passing the argument. +- For derived implicits, the compiler synthesizes a wrapper function at each call site. This creates a small overhead per call site, which could be mitigated by caching in the future. For now, if this becomes a performance issue, consider defining the function explicitly so all call sites share a single definition. ## See also diff --git a/src/mo_config/args.ml b/src/mo_config/args.ml index 1361583512c..3c31a0f4fc9 100644 --- a/src/mo_config/args.ml +++ b/src/mo_config/args.ml @@ -73,7 +73,8 @@ let inclusion_args = [ let ai_args = [ "--ai-errors", Arg.Set Flags.ai_errors, " emit AI tailored errors"; "--all-libs", Arg.Set Flags.all_libs, " load all library files from all packages, enabling better diagnostics, e.g. hinting at non-imported items (increases compilation time)"; - "--implicit-package", Arg.String (fun s -> Flags.implicit_package := Some s), _UNDOCUMENTED_ " allow contextual dot and implicits resolution from all modules in the given package" + "--implicit-package", Arg.String (fun s -> Flags.implicit_package := Some s), _UNDOCUMENTED_ " allow contextual dot and implicits resolution from all modules in the given package"; + "--implicit-derivation-depth", Arg.Set_int Flags.implicit_derivation_depth, " maximum depth for recursive implicit derivation (default: 100)" ] let migration_args = [ diff --git a/src/mo_config/flags.ml b/src/mo_config/flags.ml index bd02bf86f22..b5819ce4f74 100644 --- a/src/mo_config/flags.ml +++ b/src/mo_config/flags.ml @@ -88,6 +88,7 @@ let experimental_stable_memory_default = 0 (* _ < 0: error; _ = 0: warn, _ > 0: let experimental_stable_memory = ref experimental_stable_memory_default let typechecker_combine_srcs = ref false (* useful for the language server *) let blob_import_placeholders = ref false (* when enabled, blob:file imports resolve as empty blobs *) +let implicit_derivation_depth = ref 100 let generate_view_queries = ref false let default_warning_levels = M.empty diff --git a/src/mo_frontend/typing.ml b/src/mo_frontend/typing.ml index f4ef62700f8..28d7194bf87 100644 --- a/src/mo_frontend/typing.ml +++ b/src/mo_frontend/typing.ml @@ -177,6 +177,8 @@ let display_lab = Lib.Format.display T.pp_lab let display_typ = Lib.Format.display T.pp_typ +let display_typ_oneline ppf t = Format.pp_print_string ppf (T.string_of_typ t) + let display_typ_expand = Lib.Format.display T.pp_typ_expand let display_typ_expand_inline = Lib.Format.display_inline T.pp_typ_expand @@ -1457,8 +1459,9 @@ let disambiguate_resolutions (rel : 'candidate -> 'candidate -> bool) (candidate go frontiers in match List.fold_left add_candidate [] candidates with - | [dom] -> Some dom - | _ -> None + | [dom] -> `Single dom + | [] -> `Empty + | frontier -> `Many frontier let is_lib_module (n, t) = match T.normalize t with @@ -1470,16 +1473,12 @@ let is_val_module (n, ((t, _, _, _) : val_info)) = let module_exp in_libs module_ref = if not in_libs then - VarE {it = module_ref; at = no_region; note = (Const, None)} + VarE (module_ref @~ no_region) else ImplicitLibE module_ref let dot_module_exp module_exp name = - DotE({ - it = module_exp; - at = name.at; - note = empty_typ_note - }, name, ref None) @? name.at + DotE(module_exp @? name.at, name, ref None) @? name.at let module_ref_of_dot_module_exp (path : exp) = match path.it with @@ -1487,62 +1486,369 @@ let module_ref_of_dot_module_exp (path : exp) = | DotE ({ it = ImplicitLibE module_path; _ }, _, _) -> Some module_path | _ -> None +let as_implicit = function + | T.Named (_inf_arg_name, (T.Named ("implicit", T.Named (arg_name, t)))) -> + (* override inferred arg_name *) + Some (arg_name, t) + | T.Named (inf_arg_name, (T.Named ("implicit", t))) -> + (* non-overriden, use inferred arg_name *) + Some (inf_arg_name, t) + | _ -> None + +(* Partition a function's arg types into (non_implicit_args, (name, inner_type) list) *) +let erase_implicits args = + List.partition_map (fun arg -> + match as_implicit arg with + | Some (name, inner_typ) -> Either.Right (name, inner_typ) + | None -> Either.Left arg + ) args + type hole_error = - | HoleSuggestions of hole_candidate list - | HoleAmbiguous of (env -> unit) + | HoleSuggestions of hole_candidate list * derivation_note option + | HoleAmbiguous of hole_candidate list -(** Searches for hole resolutions for [name] of a given [typ]. - Returns [Ok(candidate)] when a single resolution is - found, [Error(file_paths)] when no resolution was found, but a - matching module could be imported, and reports an ambiguity error - when finding multiple resolutions. - *) -let resolve_hole env at name typ = - let is_matching_typ typ1 = T.sub typ1 typ in - let has_matching_field_typ = function - | T.{ lab; typ = Mut t; _ } -> None - | T.{ lab = lab1; typ = typ1; src } -> - if is_matching_typ typ1 - then Some (lab1, typ1, src.T.region) - else None - in - let find_candidate_field in_libs (module_ref, fs) = - let open Lib.Option.Syntax in - let* field = T.find_val_field_opt name fs in - let* (lab, typ, region) = has_matching_field_typ field in - let path = dot_module_exp (module_exp in_libs module_ref) (lab @@ no_region) in - Some { path; typ; module_ref_opt = Some module_ref; id = lab } - in - let candidates in_libs vals f = - T.Env.to_seq vals |> - Seq.filter_map f |> - Seq.filter_map (find_candidate_field in_libs) |> - List.of_seq +and derivation_note = hole_candidate * derivation_error + +and derivation_error = + | InnerErrors of (T.lab * T.typ * hole_error) list + | DepthLimited + +let render_derivation_leaves env = function + | None -> [] + | Some note -> + let rec collect_note ((_, deriv_err) : derivation_note) acc = + match deriv_err with + | DepthLimited -> + Printf.sprintf "depth limit reached (increase with `--implicit-derivation-depth`, current: %d)" + !(Flags.implicit_derivation_depth) :: acc + | InnerErrors inner_errors -> List.fold_left collect_error acc inner_errors + and collect_error acc (name, typ, err) = match err with + | HoleSuggestions (lib_terms, note_opt) -> + let imports = List.filter_map import_suggestion_of_candidate lib_terms in + if imports <> [] then + Format.asprintf env "`%s : %a` not found, try importing %s" + name display_typ_oneline typ (List.hd imports) :: acc + else (match note_opt with + | Some note -> collect_note note acc + | None -> + Format.asprintf env "`%s : %a` not found" + name display_typ_oneline typ :: acc) + | HoleAmbiguous ambiguous_candidates -> + Format.asprintf env "`%s : %a` ambiguous: %s" + name display_typ_oneline typ + (String.concat ", " (List.map desc_of_candidate ambiguous_candidates)) :: acc in - let eligible_terms = match T.Env.find_opt name env.vals with - | Some (t, region, _, _) when is_matching_typ t -> - (* Prefer local match over module entries *) - let path = VarE(name @~ no_region) @? no_region in - [{ path; typ = t; module_ref_opt = None; id = name }] - | _ -> candidates false env.vals is_val_module in + let leaves = collect_note note [] |> List.rev in + if leaves = [] then [] else + let lines = List.map (fun l -> "\n " ^ l) leaves in + ["Implicit derivation failed:" ^ String.concat "" lines] + +module SynthesizeWrapper = struct + (* Fresh AST nodes are required at each use site — type-checking annotates in + place, so sharing a node triggers an "already-annotated" assertion. *) + let mk e = e @? no_region + let var n = mk (VarE (n @~ no_region)) + let inst () = Source.annotate [] None no_region + let var_pat n = VarP (n @@ no_region) @! no_region + let call path arg = + mk (CallE (None, path, inst (), (false, ref arg))) + let func_ ~name param_names body = + let sort_pat = T.Local @@ no_region in + let pat = match param_names with + | [p] -> var_pat p + | ps -> TupP (List.map var_pat ps) @! no_region in + mk (FuncE (name, sort_pat, [], pat, None, false, body)) + + (** Wraps resolved implicit paths into a function that calls [candidate_path], + threading explicit params through and substituting implicits. *) + let derived_wrapper cand_args ~name candidate_path resolved_paths = + let param_idx = ref 0 in + let fresh_name () = + let n = !param_idx in incr param_idx; + Printf.sprintf "$impl_arg%d" n in + let call_args_rev, param_names_rev, remaining = + List.fold_left (fun (args_acc, params_acc, impls) arg_typ -> + match as_implicit arg_typ with + | Some _ -> + (match impls with + | path :: rest -> (path :: args_acc, params_acc, rest) + | [] -> assert false) + | None -> + let n = fresh_name () in + (var n :: args_acc, n :: params_acc, impls) + ) ([], [], resolved_paths) cand_args in + assert (remaining = []); + let call_arg_exp = match List.rev call_args_rev with + | [arg] -> arg | args -> mk (TupE args) in + func_ ~name (List.rev param_names_rev) (call candidate_path call_arg_exp) + +end + +(** Checks [args -> rets <: req_args -> req_rets] via subtyping or + bidirectional matching when [tbs] are present. Returns [Some inst] or [None]. *) +let sub_or_bimatch_func tbs args rets req_args req_rets = + assert (List.length args = List.length req_args); + assert (List.length rets = List.length req_rets); + if tbs = [] then + if List.for_all2 (fun a b -> T.sub a b) req_args args + && List.for_all2 (fun a b -> T.sub a b) rets req_rets + then Some [] else None + else + let arg_subs = List.map2 (fun ra ea -> (ra, ea, no_region)) req_args args in + let ret_subs = List.map2 (fun cr rr -> (cr, rr, no_region)) rets req_rets in + try + let (inst, c) = Bi_match.bi_match_subs None tbs None (arg_subs @ ret_subs) ~must_solve:[] in + ignore (Bi_match.finalize inst c []); + Some inst + with Bi_match.Bimatch _ -> None + +module ImplicitHoles = struct + type hole = { + hole_name : string; + hole_typ : T.typ; + } + + type rec_entry = { + entry_name : string; + hole : hole; + mutable func_exp : exp option; + } + + let find_matching_entry rec_bindings {hole_name; hole_typ} = + !rec_bindings |> List.find_opt (fun entry -> + entry.hole.hole_name = hole_name && + (try T.eq entry.hole.hole_typ hole_typ + with T.Undecided -> false)) + + (* Candidates for implicits match the required type either directly ... *) + let is_matching_typ hole candidate_typ = T.sub candidate_typ hole.hole_typ + + (* ... or by filling the implicit holes in the candidate type with recursively resolved implicits *) + type func_with_holes = { + cand_args : T.typ list; (* Not substituted! Use only to determine the argument name *) + holes : hole list; + func_without_holes : T.typ; + } + + (* Only Local/Returns functions are eligible for derivation: + Shared and Composite functions (actors, async) are excluded + since implicits are a local-scope, synchronous mechanism. *) + let is_matching_typ_with_holes hole candidate_typ = + match hole.hole_typ, T.promote candidate_typ with + | T.Func (T.Local, T.Returns, [], req_args, req_rets), + T.Func (T.Local, T.Returns, cand_tbs, cand_args, cand_rets) -> + let (explicit_args, implicit_args) = erase_implicits cand_args in + if implicit_args = [] then None + else if List.length explicit_args <> List.length req_args then None + else if List.length cand_rets <> List.length req_rets then None + else + sub_or_bimatch_func cand_tbs explicit_args cand_rets req_args req_rets + |> Option.map (fun inst -> + let inst_args = List.map (T.open_ inst) explicit_args in + let inst_rets = List.map (T.open_ inst) cand_rets in + let func_without_holes = T.Func (T.Local, T.Returns, [], inst_args, inst_rets) in + let holes = List.map (fun (hole_name, t) -> {hole_name; hole_typ = T.open_ inst t}) implicit_args in + { cand_args; holes; func_without_holes}) + | _ -> None + + module type CandidateSource = sig + type entry + val get_typ : entry -> T.typ + val make_ref_exp : string -> exp' + end + + module ValCandidateSource : CandidateSource with type entry = val_info = struct + type entry = val_info + let get_typ ((t, _, _, _) : val_info) = t + let make_ref_exp r = VarE (r @~ no_region) + end + + module LibCandidateSource : CandidateSource with type entry = T.typ = struct + type entry = T.typ + let get_typ t = t + let make_ref_exp r = ImplicitLibE r + end + + open Lib.Option.Syntax + + module MakeFromModule (M : CandidateSource) = struct + let fields_from_module (n, entry) = + match T.normalize (M.get_typ entry) with + | T.Obj (T.Module, fs, _) -> Some (n, fs) + | _ -> None + + let make_field_candidate module_ref T.{lab; typ; _} = + let path = dot_module_exp (M.make_ref_exp module_ref) (lab @@ no_region) in + ({ path; typ; module_ref_opt = Some module_ref; id = lab} : hole_candidate) + + let filter_fields hole on_field (entries : M.entry T.Env.t) = + T.Env.to_seq entries + |> Seq.filter_map fields_from_module + |> Seq.filter_map (fun (module_ref, fs) -> + let* field = T.find_val_field_opt hole.hole_name fs in + if T.is_mut field.T.typ then None else + on_field module_ref field) + |> List.of_seq + + let matching_fields hole = filter_fields hole (fun module_ref field -> + if not (is_matching_typ hole field.T.typ) then None else + Some (make_field_candidate module_ref field)) + + let matching_fields_with_holes hole = filter_fields hole (fun module_ref field -> + is_matching_typ_with_holes hole field.T.typ + |> Option.map (fun holes -> holes, make_field_candidate module_ref field)) + + end + + let make_val_candidate id t = + let path = VarE (id @~ no_region) @? no_region in + { path; typ = t; module_ref_opt = None; id } + + let matching_val hole (vals : val_env) = + let* (t, _, _, _) = T.Env.find_opt hole.hole_name vals in + if T.is_mut t then None else + if not (is_matching_typ hole t) then None else + Some (make_val_candidate hole.hole_name t) + + let matching_val_with_holes hole (vals : val_env) = + let* (t, _, _, _) = T.Env.find_opt hole.hole_name vals in + if T.is_mut t then None else + let* holes = is_matching_typ_with_holes hole t in + Some (holes, make_val_candidate hole.hole_name t) + + module FromModuleVal = MakeFromModule(ValCandidateSource) + module FromModuleLib = MakeFromModule(LibCandidateSource) (* All candidates are subtypes of the required type. The "greatest" of these types is the "closest" to the required type. - If we can uniquely identify a single candidate that is the supertype of all other candidates we pick it. *) - let disambiguate_holes = disambiguate_resolutions (fun (c1 : hole_candidate) c2 -> T.sub c1.typ c2.typ) in - match eligible_terms with - | [term] -> Ok term - | [] -> - let lib_terms = candidates true env.libs is_lib_module in - (match if Option.is_some !Flags.implicit_package then disambiguate_holes lib_terms else None with - | Some term -> Ok term - | None -> Error (HoleSuggestions lib_terms)) - | terms -> - match disambiguate_holes terms with - | Some term -> Ok term - | None -> Error (HoleAmbiguous (fun env -> - let terms = List.map desc_of_candidate terms in - let notes = [Printf.sprintf "The ambiguous implicit candidates are: %s." (String.concat ", " terms)] in - error env at "M0231" ~notes "ambiguous implicit argument %s of type %a." ("named " ^ quote name) display_typ typ)) + If we can uniquely identify a single candidate that is the supertype of all other candidates we pick it. *) + let disambiguate_holes = disambiguate_resolutions (fun (c1 : hole_candidate) c2 -> T.sub c1.typ c2.typ) + let disambiguate_func_with_holes = disambiguate_resolutions (fun ((x : func_with_holes), (_ : hole_candidate)) (y, _) -> + T.sub x.func_without_holes y.func_without_holes) + + (** Searches for hole resolutions for a given [hole_name] and [typ]. + Returns [Ok(candidate)] when a single resolution is + found, [Error(file_paths)] when no resolution was found, but a + matching module could be imported, and reports an ambiguity error + when finding multiple resolutions. + When direct resolution fails, attempts implicit derivation from + polymorphic candidates whose inner implicits can be recursively resolved + (up to [Flags.implicit_derivation_depth]). + Detects recursive derivation cycles via [rec_bindings] and generates + self-referential wrapper functions. + *) + let rec resolve_hole ~depth ~rec_bindings env at ({hole_name; hole_typ} as hole) = + + match find_matching_entry rec_bindings hole with + | Some entry -> + let id = entry.entry_name in + Ok { path = SynthesizeWrapper.var id; typ = hole_typ; module_ref_opt = None; id } + | None -> + + let try_derive_with holes wrapper candidates ~depth = + match candidates with + | `Single ((_, (candidate : hole_candidate)) as derivation) -> `Committed ( + (* Check depth limit *) + if depth >= !(Flags.implicit_derivation_depth) then Error (candidate, DepthLimited) else + + (* Add entry to rec_bindings *) + let my_rec_name = Printf.sprintf "$derived_implicit_%d" (List.length !rec_bindings) in + let entry = { entry_name = my_rec_name; hole; func_exp = None } in + rec_bindings := entry :: !rec_bindings; + + (* Resolve inner holes *) + let failed, resolved = holes derivation |> List.partition_map (fun inner_hole -> + match resolve_hole ~depth:(depth + 1) ~rec_bindings env at inner_hole with + | Error err -> Either.Left (inner_hole.hole_name, inner_hole.hole_typ, err) + | Ok ok -> Either.Right ok.path) in + if failed = [] then begin + entry.func_exp <- Some (wrapper derivation ~name:my_rec_name candidate.path resolved); + Ok { candidate with path = SynthesizeWrapper.var my_rec_name; typ = hole_typ } + end else + Error (candidate, InnerErrors failed) + ) + | `Many matches -> `Ambiguous (List.map (fun (_, c) -> c) matches) + | `Empty -> `Empty + in + let holes (h, _) = h.holes in + let wrapper (h, _) = SynthesizeWrapper.derived_wrapper h.cand_args in + let try_derive candidates = try_derive_with holes wrapper (disambiguate_func_with_holes candidates) in + + (* Try direct local candidate first (matching local env value by name) *) + match matching_val hole env.vals with + | Some term -> Ok term + | None -> + + (* Try direct candidates from module fields *) + let matching_fields = FromModuleVal.matching_fields hole env.vals in + match disambiguate_holes matching_fields with + | `Single term -> Ok term + | `Many _ -> Error (HoleAmbiguous matching_fields) + | `Empty -> + + (* Get direct module field candidates from libs (unimported modules) *) + (* Use them for resolution only when the feature flag is set! *) + let lib_fields = FromModuleLib.matching_fields hole env.libs in + match if Option.is_some !Flags.implicit_package then disambiguate_holes lib_fields else `Empty with + | `Single term -> Ok term + | `Many _ | `Empty -> + + (* No direct candidate : try implicit derivation + 1. Find a matching candidate with holes + 2. Resolve holes recursively + 3. Synthesize wrapper function that applies the candidate to the resolved inner implicits *) + + (* Try derivations from local scope *) + match try_derive ~depth (Option.to_list (matching_val_with_holes hole env.vals)) with + | `Committed (Ok term) -> Ok term + | `Committed (Error e) -> Error (HoleSuggestions (lib_fields, Some e)) + | `Ambiguous cs -> Error (HoleAmbiguous cs) + | `Empty -> + + (* Try derivations from module fields *) + match try_derive ~depth (FromModuleVal.matching_fields_with_holes hole env.vals) with + | `Committed (Ok term) -> Ok term + | `Committed (Error e) -> Error (HoleSuggestions (lib_fields, Some e)) + | `Ambiguous derivable_terms -> Error (HoleAmbiguous derivable_terms) + | `Empty -> + + (* Get candidates for derivations from libs *) + let lib_fields_with_holes = FromModuleLib.matching_fields_with_holes hole env.libs in + let lib_fields = lib_fields @ List.map (fun (_, c) -> c) lib_fields_with_holes in + match + if Option.is_some !Flags.implicit_package + then try_derive ~depth lib_fields_with_holes + else `Empty + with + | `Committed (Ok term) -> Ok term + | `Committed (Error e) -> Error (HoleSuggestions (lib_fields, Some e)) + | `Ambiguous _ | `Empty -> Error (HoleSuggestions (lib_fields, None)) + +end + +let resolve_hole env at hole_name hole_typ = + let open ImplicitHoles in + let rec_bindings = ref [] in + resolve_hole ~depth:0 ~rec_bindings env at {hole_name; hole_typ} + (* [func_exp = None] only exists when the derivation failed (when the result is Error). + So the assert below is safe as long as the no-backtracking invariant holds *) + |> Result.map (fun candidate -> + let bindings = !rec_bindings + |> List.rev + |> List.map (fun entry -> + match entry.func_exp with + | Some e -> (entry.entry_name, entry.hole.hole_typ, e) + | None -> assert false) + in + (candidate, bindings)) + +let mk_recursive_block at bindings outermost_path hole_typ = + let mk_dec typ d = { Source.it = d; at; note = {empty_typ_note with note_typ = typ} } in + let mk_pat name typ = Source.annotate typ (VarP (name @@ no_region)) no_region in + let let_decs = bindings |> List.map (fun (name, typ, func_exp) -> + mk_dec typ (LetD (mk_pat name typ, func_exp, None))) in + let exp_dec = mk_dec hole_typ (ExpD outermost_path) in + { Source.it = BlockE (let_decs @ [exp_dec]); + at; note = {empty_typ_note with note_typ = hole_typ; note_eff = T.Triv} } type ctx_dot_candidate = { module_ref : T.lab option; (* optional module reference : name (from `vals`) or path (from `libs`) *) @@ -1610,20 +1916,16 @@ let contextual_dot env name receiver_ty : (ctx_dot_candidate, 'a context_dot_err match local_candidate with | Some c -> Ok c | None -> - (match candidates false env.vals is_val_module with - | [c] -> Ok c - | [] -> - (match candidates true env.libs is_lib_module with - | [c] when Option.is_some !Flags.implicit_package -> Ok c - | lib_candidates -> - match if Option.is_some !Flags.implicit_package then disambiguate_candidates lib_candidates else None with - | Some c -> Ok c - | None -> Error (DotSuggestions (fun env -> List.filter_map (fun candidate -> Option.map Suggest.module_name_as_url candidate.module_ref) lib_candidates))) - | cs -> match disambiguate_candidates cs with - | Some c -> Ok c - | None -> Error (DotAmbiguous (fun env -> - let modules = String.concat ", " (List.filter_map (fun c -> c.module_ref) cs) in - error env name.at "M0224" "overlapping resolution for `%s` in scope from these modules: %s" name.it modules))) + match disambiguate_candidates (candidates false env.vals is_val_module) with + | `Single c -> Ok c + | `Many cs -> Error (DotAmbiguous (fun env -> + let modules = String.concat ", " (List.filter_map (fun c -> c.module_ref) cs) in + error env name.at "M0224" "overlapping resolution for `%s` in scope from these modules: %s" name.it modules)) + | `Empty -> + let lib_candidates = candidates true env.libs is_lib_module in + match if Option.is_some !Flags.implicit_package then disambiguate_candidates lib_candidates else `Empty with + | `Single c -> Ok c + | `Many _ | `Empty -> Error (DotSuggestions (fun env -> List.filter_map (fun candidate -> Option.map Suggest.module_name_as_url candidate.module_ref) lib_candidates)) type contextual_dot_suggestion = { module_url : T.lab; @@ -1635,7 +1937,7 @@ let contextual_dot_suggestions libs receiver_ty = List.to_seq fs |> Seq.filter_map (fun fld -> CtxDot.is_matching_func fld.T.typ receiver_ty |> - Option.map (fun (_, func_ty, inst) -> + Option.map (fun (_, func_ty, _inst) -> { module_url = Suggest.module_name_as_url module_path; func_name = fld.T.lab; func_ty })) in T.Env.to_seq libs |> @@ -1655,6 +1957,7 @@ let contextual_dot_module (exp : Syntax.exp) = let check_can_dot env ctx_dot (exp : Syntax.exp) tys es at = if not env.pre then if Flags.get_warning_level "M0236" <> Flags.Allow then + if at = Source.no_region then () else (* no warnings for compiler-generated calls *) match ctx_dot with | Some _ -> () (* already dotted *) | None -> @@ -2403,29 +2706,9 @@ and check_exp env t exp = and check_exp' env0 t exp : T.typ = let env = {env0 with in_prog = false; in_actor = false; context = exp.it :: env0.context } in match exp.it, t with - | HoleE (s, e), t -> - begin match resolve_hole env exp.at s t with - | Ok {path; _} -> - e := path; - check_exp env t path; - t - | Error (HoleAmbiguous mk_error) -> - mk_error env; - t - | Error (HoleSuggestions lib_terms) -> - if not env.pre then begin - let import_sug = - if lib_terms = [] then - Stdlib.Format.sprintf - "If you're trying to omit an implicit argument named %s you need to have a matching declaration named %s in scope." - (quote s) (quote s) - else Stdlib.Format.sprintf "Did you mean to import %s?" (String.concat " or " (List.filter_map import_suggestion_of_candidate lib_terms)) - in - let notes = [import_sug] in - local_error ~notes env exp.at "M0230" "Cannot determine implicit argument %s of type%a" (quote s) display_typ t - end; - t - end + | HoleE (hole_name, exp_ref), hole_typ -> + check_hole env exp.at hole_name hole_typ exp_ref; + t | PrimE s, T.Func _ -> t | LitE lit, _ -> @@ -2599,6 +2882,41 @@ and check_exp' env0 t exp : T.typ = let t' = infer_exp env0 exp in check_inferred env0 env t t' exp +and check_hole env at hole_name hole_typ exp_ref = + match resolve_hole env at hole_name hole_typ with + | Ok ({path; _}, []) -> + exp_ref := path; + check_exp env hole_typ path + | Ok ({path; _}, bindings) -> + let env_rec = List.fold_left (fun env (name, typ, _) -> + { env with vals = T.Env.add name + (typ, Source.no_region, Scope.Declaration, Available) env.vals } + ) env bindings in + List.iter (fun (_, typ, func_exp) -> check_exp env_rec typ func_exp) bindings; + check_exp env_rec hole_typ path; + exp_ref := mk_recursive_block at bindings path hole_typ + | Error (HoleAmbiguous ambiguous_candidates) -> + let descs = List.map desc_of_candidate ambiguous_candidates in + let notes = [Printf.sprintf "The ambiguous implicit candidates are: %s." (String.concat ", " descs)] in + error env at "M0231" ~notes "ambiguous implicit argument %s of type %a." + ("named " ^ quote hole_name) display_typ hole_typ + | Error (HoleSuggestions (lib_terms, derivation_notes)) -> + if env.pre then () else + let derivation_sug = render_derivation_leaves env derivation_notes in + let import_sug = + if lib_terms = [] then + if derivation_sug <> [] then [] else + let desc = " named " ^ quote hole_name in + [Stdlib.Format.sprintf + "If you're trying to omit an implicit argument%s you need to have a matching declaration%s in scope." + desc desc] + else [Stdlib.Format.sprintf "Did you mean to import %s?" (String.concat " or " (List.filter_map import_suggestion_of_candidate lib_terms))] + in + let notes = import_sug @ derivation_sug in + local_error ~notes env at "M0230" "Cannot determine implicit argument %s of type%a" + (quote hole_name) + display_typ hole_typ + and check_inferred env0 env t t' exp = (match sub_explained env exp.at t' t with | T.Incompatible explanation -> @@ -2762,14 +3080,6 @@ and infer_callee env exp = end | _ -> infer_exp_promote env exp, None -and as_implicit = function - | T.Named (_inf_arg_name, (T.Named ("implicit", T.Named (arg_name, t)))) -> - (* override inferred arg_name *) - Some arg_name - | T.Named (inf_arg_name, (T.Named ("implicit", t))) -> - (* non-overriden, use inferred arg_name *) - Some inf_arg_name - | _ -> None (** With implicits we can either fully specify all implicit arguments or none Saturated arity is the number of expected arguments when all arguments are fully specified @@ -2791,7 +3101,7 @@ and insert_holes at ts es = | [] -> es | t :: ts1 -> match as_implicit t with - | Some arg_name -> + | Some (arg_name, _) -> mk_hole n arg_name :: go (n + 1) ts1 es | None -> match es with @@ -2812,10 +3122,10 @@ and check_explicit_arguments env saturated_arity implicits_arity arg_typs syntax Some arg, match as_implicit typ with | None -> acc - | Some name -> + | Some (name, _) -> match resolve_hole env arg.at name typ with | Error _ -> acc - | Ok {path;_} -> + | Ok ({path;_}, _) -> match path.it, arg.it with | VarE {it = id0; _}, VarE {it = id1; note = (Const, _); _} @@ -2833,6 +3143,7 @@ and check_explicit_arguments env saturated_arity implicits_arity arg_typs syntax in if (List.length explicit_implicits) = saturated_arity - implicits_arity then List.iter (fun (name, exp, next_arg) -> + if exp.at = Source.no_region then () else (* no warnings for compiler-generated calls *) let to_remove = match next_arg with None -> exp.at | Some next -> { exp.at with right = next.at.left } in warn env exp.at "M0237" ~edits:[edit to_remove ""] diff --git a/test/fail/implicit-derivation-ambiguous-deep.mo b/test/fail/implicit-derivation-ambiguous-deep.mo new file mode 100644 index 00000000000..b16f4c8cab3 --- /dev/null +++ b/test/fail/implicit-derivation-ambiguous-deep.mo @@ -0,0 +1,31 @@ +type Order = { #less; #greater; #equal }; + +// Single derivable candidate for [T] — no head-level ambiguity +module Array { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + #equal; + }; +}; + +// Two leaf implicits for Nat — inner resolution is ambiguous +module Nat { + public func compare(a : Nat, b : Nat) : Order { + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module MyNat { + public func compare(a : Nat, b : Nat) : Order { + #greater; + }; +}; + +func needsCompare( + a : [Nat], + b : [Nat], + compare : (implicit : ([Nat], [Nat]) -> Order), +) : Order { + compare(a, b); +}; + +ignore needsCompare([1], [2]); // Array.compare is unique head, but inner Nat compare is ambiguous diff --git a/test/fail/implicit-derivation-ambiguous.mo b/test/fail/implicit-derivation-ambiguous.mo new file mode 100644 index 00000000000..bf01df18d77 --- /dev/null +++ b/test/fail/implicit-derivation-ambiguous.mo @@ -0,0 +1,30 @@ +type Order = { #less; #greater; #equal }; + +// Head-level ambiguity: two derivable candidates for [T] +module Nat1 { + public func compare(a : Nat, b : Nat) : Order { + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module Array1 { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + #equal; + }; +}; + +module Array2 { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + #less; + }; +}; + +func needsCompare( + a : [Nat], + b : [Nat], + compare : (implicit : ([Nat], [Nat]) -> Order), +) : Order { + compare(a, b); +}; + +ignore needsCompare([1], [2]); // ambiguous head: Array1.compare and Array2.compare diff --git a/test/fail/implicit-derivation-bimatch.mo b/test/fail/implicit-derivation-bimatch.mo new file mode 100644 index 00000000000..8393f21f343 --- /dev/null +++ b/test/fail/implicit-derivation-bimatch.mo @@ -0,0 +1,23 @@ +// Tests for bi-matching limits in implicit candidate selection. + +// B is fully phantom (no bounds at all); C has an upper bound from ret_subs +// (C ≤ Nat) but no lower bound — bivariant solver still picks lb = None for both. +module Pipeline { + public func chain( + x : A, + step : (implicit : A -> B), + finish : (implicit : B -> C), + ) : C { finish(step(x)) }; +}; + +func needsChain(x : Nat, chain : (implicit : Nat -> Nat)) : Nat { chain(x) }; +ignore needsChain(42); + +// Over-constrained: arg_subs gives T ≥ Nat, ret_subs gives T ≤ Bool. +// Nat (x : T, fn : (implicit : T -> T)) : T { fn(x) }; +}; + +func needsWrap(x : Nat, wrap : (implicit : Nat -> Bool)) : Bool { wrap(x) }; +ignore needsWrap(42); diff --git a/test/fail/implicit-derivation-deep.mo b/test/fail/implicit-derivation-deep.mo new file mode 100644 index 00000000000..aab111b709d --- /dev/null +++ b/test/fail/implicit-derivation-deep.mo @@ -0,0 +1,38 @@ +//MOC-FLAG --package core $MOTOKO_CORE --all-libs +import Array "mo:core/Array"; +import { type Order } "mo:core/Order"; + +module Pair { + public func compare( + a : (A, B), + b : (A, B), + cmpA : (implicit : (compare : (A, A) -> Order)), + cmpB : (implicit : (compare : (B, B) -> Order)), + ) : Order { #equal }; +}; + +// Shallow: leaf suggests importing mo:core/Bool +func needsBoolArrayCompare( + a : [Bool], + b : [Bool], + compare : (implicit : ([Bool], [Bool]) -> Order), +) : Order { compare(a, b) }; +ignore needsBoolArrayCompare([true], [false]); + +// Deep chain: [[Bool]] → Array<[Bool]> → Array → suggest mo:core/Bool +func needsNestedBoolArrayCompare( + a : [[Bool]], + b : [[Bool]], + compare : (implicit : ([[Bool]], [[Bool]]) -> Order), +) : Order { compare(a, b) }; +ignore needsNestedBoolArrayCompare([[true]], [[false]]); + +type Color = { #red; #green; #blue }; + +// Multi-branch: Pair<[Color], [Int]> with two failing branches, different reasons +func needsPairCompare( + a : ([Color], [Int]), + b : ([Color], [Int]), + compare : (implicit : (([Color], [Int]), ([Color], [Int])) -> Order), +) : Order { compare(a, b) }; +ignore needsPairCompare(([#red], [1]), ([#blue], [2])); diff --git a/test/fail/implicit-derivation-depth.mo b/test/fail/implicit-derivation-depth.mo new file mode 100644 index 00000000000..f8b6c3a3b36 --- /dev/null +++ b/test/fail/implicit-derivation-depth.mo @@ -0,0 +1,28 @@ +//MOC-FLAG --implicit-derivation-depth 1 + +type Order = { #less; #greater; #equal }; + +module Nat { + public func compare(a : Nat, b : Nat) : Order { + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module Array { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + #equal; + }; +}; + +// [[Nat]] requires two levels of derivation: +// 1. Array.compare<[Nat]> (depth 0 -> 1) +// 2. Array.compare (depth 1 -> 2, but blocked by limit=1) +func needsNestedArrayCompare( + a : [[Nat]], + b : [[Nat]], + compare : (implicit : ([[Nat]], [[Nat]]) -> Order), +) : Order { + compare(a, b); +}; + +ignore needsNestedArrayCompare([[1]], [[2]]); diff --git a/test/fail/implicit-derivation-no-backtracking.mo b/test/fail/implicit-derivation-no-backtracking.mo new file mode 100644 index 00000000000..231f8d70437 --- /dev/null +++ b/test/fail/implicit-derivation-no-backtracking.mo @@ -0,0 +1,39 @@ +// Two candidates (ArrayA.compare and ArrayB.compare) both structurally match +// ([Nat], [Nat]) -> Order after erasing implicits. +// With commit-first resolution, this is an ambiguity error regardless of +// whether inner implicits can be resolved. + +type Order = { #less; #greater; #equal }; + +module Nat { + public func compare(a : Nat, b : Nat) : Order { + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module ArrayA { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + #equal; + }; +}; + +// ArrayB also matches structurally, but its inner implicit has a different name. +module ArrayB { + public func compare(a : [T], b : [T], cmp : (implicit : (T, T) -> Order)) : Order { + #less; + }; +}; + +func needsCompare( + a : [Nat], + b : [Nat], + compare : (implicit : ([Nat], [Nat]) -> Order), +) : Order { + compare(a, b); +}; + +// ambiguous: ArrayA.compare and ArrayB.compare both match as initial candidates +// even though ArrayB needs `cmp` implicit that is NOT available in scope and would fail to resolve. +// with backtracking we could resolve and pick ArrayA, but it would make the resolution fragile +// because importing/defining `cmp` would suddenly break the resolution. +ignore needsCompare([1], [2]); diff --git a/test/fail/implicit-derivation.mo b/test/fail/implicit-derivation.mo new file mode 100644 index 00000000000..678c2c8e3a7 --- /dev/null +++ b/test/fail/implicit-derivation.mo @@ -0,0 +1,44 @@ +type Order = { #less; #greater; #equal }; + +// Only Nat.compare is available as a leaf implicit +module Nat { + public func compare(a : Nat, b : Nat) : Order { + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module Array { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + #equal; + }; +}; + +// Missing inner implicit +// Derivation of [Bool] compare needs Bool.compare, which doesn't exist +func needsBoolArrayCompare( + a : [Bool], + b : [Bool], + compare : (implicit : ([Bool], [Bool]) -> Order), +) : Order { + compare(a, b); +}; + +ignore needsBoolArrayCompare([true], [false]); // M0230: no Bool.compare + +// Deep nesting: [[Bool]] needs two derivation levels, both shown in diagnostics +func needsNestedBoolArrayCompare( + a : [[Bool]], + b : [[Bool]], + compare : (implicit : ([[Bool]], [[Bool]]) -> Order), +) : Order { + compare(a, b); +}; + +ignore needsNestedBoolArrayCompare([[true]], [[false]]); // M0230: full chain shown + +// Non-function implicit cannot be derived +func needsNatValue(x : Nat, n : (implicit : Nat)) : Nat { + n; +}; + +ignore needsNatValue(42); // M0230: no Nat value in scope diff --git a/test/fail/ok/implicit-derivation-ambiguous-deep.tc-human.ok b/test/fail/ok/implicit-derivation-ambiguous-deep.tc-human.ok new file mode 100644 index 00000000000..b3b872d6af0 --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous-deep.tc-human.ok @@ -0,0 +1,7 @@ +error[M0230]: Cannot determine implicit argument `compare` of type + ([Nat], [Nat]) -> Order + ┌─ implicit-derivation-ambiguous-deep.mo:31:8 + 31 │ ignore needsCompare([1], [2]); // Array.compare is unique head, but inner Nat compare is ambiguous + │ ^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `compare : (Nat, Nat) -> Order` ambiguous: `MyNat.compare`, `Nat.compare` diff --git a/test/fail/ok/implicit-derivation-ambiguous-deep.tc-human.ret.ok b/test/fail/ok/implicit-derivation-ambiguous-deep.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous-deep.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-ambiguous-deep.tc.ok b/test/fail/ok/implicit-derivation-ambiguous-deep.tc.ok new file mode 100644 index 00000000000..f490f31e10a --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous-deep.tc.ok @@ -0,0 +1,4 @@ +implicit-derivation-ambiguous-deep.mo:31.8-31.30: type error [M0230], Cannot determine implicit argument `compare` of type + ([Nat], [Nat]) -> Order +note: Implicit derivation failed: + `compare : (Nat, Nat) -> Order` ambiguous: `MyNat.compare`, `Nat.compare` diff --git a/test/fail/ok/implicit-derivation-ambiguous-deep.tc.ret.ok b/test/fail/ok/implicit-derivation-ambiguous-deep.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous-deep.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-ambiguous.tc-human.ok b/test/fail/ok/implicit-derivation-ambiguous.tc-human.ok new file mode 100644 index 00000000000..296dfda0ecf --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous.tc-human.ok @@ -0,0 +1,6 @@ +error[M0231]: ambiguous implicit argument named `compare` of type + ([Nat], [Nat]) -> Order. + ┌─ implicit-derivation-ambiguous.mo:30:8 + 30 │ ignore needsCompare([1], [2]); // ambiguous head: Array1.compare and Array2.compare + │ ^^^^^^^^^^^^^^^^^^^^^^ + = note: The ambiguous implicit candidates are: `Array1.compare`, `Array2.compare`. diff --git a/test/fail/ok/implicit-derivation-ambiguous.tc-human.ret.ok b/test/fail/ok/implicit-derivation-ambiguous.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-ambiguous.tc.ok b/test/fail/ok/implicit-derivation-ambiguous.tc.ok new file mode 100644 index 00000000000..1aede00b8a4 --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous.tc.ok @@ -0,0 +1,3 @@ +implicit-derivation-ambiguous.mo:30.8-30.30: type error [M0231], ambiguous implicit argument named `compare` of type + ([Nat], [Nat]) -> Order. +note: The ambiguous implicit candidates are: `Array1.compare`, `Array2.compare`. diff --git a/test/fail/ok/implicit-derivation-ambiguous.tc.ret.ok b/test/fail/ok/implicit-derivation-ambiguous.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-ambiguous.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-bimatch.tc-human.ok b/test/fail/ok/implicit-derivation-bimatch.tc-human.ok new file mode 100644 index 00000000000..e6b75a35ddb --- /dev/null +++ b/test/fail/ok/implicit-derivation-bimatch.tc-human.ok @@ -0,0 +1,14 @@ +error[M0230]: Cannot determine implicit argument `chain` of type + Nat -> Nat + ┌─ implicit-derivation-bimatch.mo:14:8 + 14 │ ignore needsChain(42); + │ ^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `step : Nat -> None` not found + `finish : None -> None` not found +error[M0230]: Cannot determine implicit argument `wrap` of type + Nat -> Bool + ┌─ implicit-derivation-bimatch.mo:23:8 + 23 │ ignore needsWrap(42); + │ ^^^^^^^^^^^^^ + = note: If you're trying to omit an implicit argument named `wrap` you need to have a matching declaration named `wrap` in scope. diff --git a/test/fail/ok/implicit-derivation-bimatch.tc-human.ret.ok b/test/fail/ok/implicit-derivation-bimatch.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-bimatch.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-bimatch.tc.ok b/test/fail/ok/implicit-derivation-bimatch.tc.ok new file mode 100644 index 00000000000..228a539ba74 --- /dev/null +++ b/test/fail/ok/implicit-derivation-bimatch.tc.ok @@ -0,0 +1,8 @@ +implicit-derivation-bimatch.mo:14.8-14.22: type error [M0230], Cannot determine implicit argument `chain` of type + Nat -> Nat +note: Implicit derivation failed: + `step : Nat -> None` not found + `finish : None -> None` not found +implicit-derivation-bimatch.mo:23.8-23.21: type error [M0230], Cannot determine implicit argument `wrap` of type + Nat -> Bool +note: If you're trying to omit an implicit argument named `wrap` you need to have a matching declaration named `wrap` in scope. diff --git a/test/fail/ok/implicit-derivation-bimatch.tc.ret.ok b/test/fail/ok/implicit-derivation-bimatch.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-bimatch.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-deep.tc-human.ok b/test/fail/ok/implicit-derivation-deep.tc-human.ok new file mode 100644 index 00000000000..c9d47c85a38 --- /dev/null +++ b/test/fail/ok/implicit-derivation-deep.tc-human.ok @@ -0,0 +1,22 @@ +error[M0230]: Cannot determine implicit argument `compare` of type + ([Bool], [Bool]) -> Order + ┌─ implicit-derivation-deep.mo:20:8 + 20 │ ignore needsBoolArrayCompare([true], [false]); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found, try importing mo:core/Bool +error[M0230]: Cannot determine implicit argument `compare` of type + ([[Bool]], [[Bool]]) -> Order + ┌─ implicit-derivation-deep.mo:28:8 + 28 │ ignore needsNestedBoolArrayCompare([[true]], [[false]]); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found, try importing mo:core/Bool +error[M0230]: Cannot determine implicit argument `compare` of type + (([Color], [Int]), ([Color], [Int])) -> Order + ┌─ implicit-derivation-deep.mo:38:8 + 38 │ ignore needsPairCompare(([#red], [1]), ([#blue], [2])); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `compare : (Color, Color) -> Order` not found + `compare : (Int, Int) -> Order` not found, try importing mo:core/Int diff --git a/test/fail/ok/implicit-derivation-deep.tc-human.ret.ok b/test/fail/ok/implicit-derivation-deep.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-deep.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-deep.tc.ok b/test/fail/ok/implicit-derivation-deep.tc.ok new file mode 100644 index 00000000000..be5448d3df3 --- /dev/null +++ b/test/fail/ok/implicit-derivation-deep.tc.ok @@ -0,0 +1,13 @@ +implicit-derivation-deep.mo:20.8-20.46: type error [M0230], Cannot determine implicit argument `compare` of type + ([Bool], [Bool]) -> Order +note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found, try importing mo:core/Bool +implicit-derivation-deep.mo:28.8-28.56: type error [M0230], Cannot determine implicit argument `compare` of type + ([[Bool]], [[Bool]]) -> Order +note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found, try importing mo:core/Bool +implicit-derivation-deep.mo:38.8-38.55: type error [M0230], Cannot determine implicit argument `compare` of type + (([Color], [Int]), ([Color], [Int])) -> Order +note: Implicit derivation failed: + `compare : (Color, Color) -> Order` not found + `compare : (Int, Int) -> Order` not found, try importing mo:core/Int diff --git a/test/fail/ok/implicit-derivation-deep.tc.ret.ok b/test/fail/ok/implicit-derivation-deep.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-deep.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-depth.tc-human.ok b/test/fail/ok/implicit-derivation-depth.tc-human.ok new file mode 100644 index 00000000000..36ae05641cc --- /dev/null +++ b/test/fail/ok/implicit-derivation-depth.tc-human.ok @@ -0,0 +1,7 @@ +error[M0230]: Cannot determine implicit argument `compare` of type + ([[Nat]], [[Nat]]) -> Order + ┌─ implicit-derivation-depth.mo:28:8 + 28 │ ignore needsNestedArrayCompare([[1]], [[2]]); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + depth limit reached (increase with `--implicit-derivation-depth`, current: 1) diff --git a/test/fail/ok/implicit-derivation-depth.tc-human.ret.ok b/test/fail/ok/implicit-derivation-depth.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-depth.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-depth.tc.ok b/test/fail/ok/implicit-derivation-depth.tc.ok new file mode 100644 index 00000000000..3b00cc971fb --- /dev/null +++ b/test/fail/ok/implicit-derivation-depth.tc.ok @@ -0,0 +1,4 @@ +implicit-derivation-depth.mo:28.8-28.45: type error [M0230], Cannot determine implicit argument `compare` of type + ([[Nat]], [[Nat]]) -> Order +note: Implicit derivation failed: + depth limit reached (increase with `--implicit-derivation-depth`, current: 1) diff --git a/test/fail/ok/implicit-derivation-depth.tc.ret.ok b/test/fail/ok/implicit-derivation-depth.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-depth.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-no-backtracking.tc-human.ok b/test/fail/ok/implicit-derivation-no-backtracking.tc-human.ok new file mode 100644 index 00000000000..78232c7aa59 --- /dev/null +++ b/test/fail/ok/implicit-derivation-no-backtracking.tc-human.ok @@ -0,0 +1,6 @@ +error[M0231]: ambiguous implicit argument named `compare` of type + ([Nat], [Nat]) -> Order. + ┌─ implicit-derivation-no-backtracking.mo:39:8 + 39 │ ignore needsCompare([1], [2]); + │ ^^^^^^^^^^^^^^^^^^^^^^ + = note: The ambiguous implicit candidates are: `ArrayA.compare`, `ArrayB.compare`. diff --git a/test/fail/ok/implicit-derivation-no-backtracking.tc-human.ret.ok b/test/fail/ok/implicit-derivation-no-backtracking.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-no-backtracking.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation-no-backtracking.tc.ok b/test/fail/ok/implicit-derivation-no-backtracking.tc.ok new file mode 100644 index 00000000000..d8d35f34127 --- /dev/null +++ b/test/fail/ok/implicit-derivation-no-backtracking.tc.ok @@ -0,0 +1,3 @@ +implicit-derivation-no-backtracking.mo:39.8-39.30: type error [M0231], ambiguous implicit argument named `compare` of type + ([Nat], [Nat]) -> Order. +note: The ambiguous implicit candidates are: `ArrayA.compare`, `ArrayB.compare`. diff --git a/test/fail/ok/implicit-derivation-no-backtracking.tc.ret.ok b/test/fail/ok/implicit-derivation-no-backtracking.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation-no-backtracking.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation.tc-human.ok b/test/fail/ok/implicit-derivation.tc-human.ok new file mode 100644 index 00000000000..ba11e80846c --- /dev/null +++ b/test/fail/ok/implicit-derivation.tc-human.ok @@ -0,0 +1,20 @@ +error[M0230]: Cannot determine implicit argument `compare` of type + ([Bool], [Bool]) -> Order + ┌─ implicit-derivation.mo:26:8 + 26 │ ignore needsBoolArrayCompare([true], [false]); // M0230: no Bool.compare + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found +error[M0230]: Cannot determine implicit argument `compare` of type + ([[Bool]], [[Bool]]) -> Order + ┌─ implicit-derivation.mo:37:8 + 37 │ ignore needsNestedBoolArrayCompare([[true]], [[false]]); // M0230: full chain shown + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found +error[M0230]: Cannot determine implicit argument `n` of type + Nat + ┌─ implicit-derivation.mo:44:8 + 44 │ ignore needsNatValue(42); // M0230: no Nat value in scope + │ ^^^^^^^^^^^^^^^^^ + = note: If you're trying to omit an implicit argument named `n` you need to have a matching declaration named `n` in scope. diff --git a/test/fail/ok/implicit-derivation.tc-human.ret.ok b/test/fail/ok/implicit-derivation.tc-human.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation.tc-human.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/implicit-derivation.tc.ok b/test/fail/ok/implicit-derivation.tc.ok new file mode 100644 index 00000000000..b0b4c9bcd49 --- /dev/null +++ b/test/fail/ok/implicit-derivation.tc.ok @@ -0,0 +1,11 @@ +implicit-derivation.mo:26.8-26.46: type error [M0230], Cannot determine implicit argument `compare` of type + ([Bool], [Bool]) -> Order +note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found +implicit-derivation.mo:37.8-37.56: type error [M0230], Cannot determine implicit argument `compare` of type + ([[Bool]], [[Bool]]) -> Order +note: Implicit derivation failed: + `compare : (Bool, Bool) -> Order` not found +implicit-derivation.mo:44.8-44.25: type error [M0230], Cannot determine implicit argument `n` of type + Nat +note: If you're trying to omit an implicit argument named `n` you need to have a matching declaration named `n` in scope. diff --git a/test/fail/ok/implicit-derivation.tc.ret.ok b/test/fail/ok/implicit-derivation.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/implicit-derivation.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/run/implicit-derivation-core.mo b/test/run/implicit-derivation-core.mo new file mode 100644 index 00000000000..ce2fc8d0e73 --- /dev/null +++ b/test/run/implicit-derivation-core.mo @@ -0,0 +1,100 @@ +//MOC-FLAG --package core $MOTOKO_CORE --implicit-package core +import Array "mo:core/Array"; +import Nat "mo:core/Nat"; +import Int "mo:core/Int"; +import Text "mo:core/Text"; +import { type Order } "mo:core/Order"; + +// Derive Array.compare for [Nat] from core +func compareNatArrays( + a : [Nat], + b : [Nat], + compare : (implicit : ([Nat], [Nat]) -> Order), +) : Order { + compare(a, b); +}; + +assert compareNatArrays([1, 2, 3], [1, 2, 3]) == #equal; +assert compareNatArrays([1, 2], [1, 3]) == #less; +assert compareNatArrays([1, 2, 3], [1, 2]) == #greater; + +// Derive Array.compare for [Int] +func compareIntArrays( + a : [Int], + b : [Int], + compare : (implicit : ([Int], [Int]) -> Order), +) : Order { + compare(a, b); +}; + +assert compareIntArrays([-1, 0, 1], [-1, 0, 1]) == #equal; +assert compareIntArrays([-2], [-1]) == #less; + +// Derive Array.compare for [Text] +func compareTextArrays( + a : [Text], + b : [Text], + compare : (implicit : ([Text], [Text]) -> Order), +) : Order { + compare(a, b); +}; + +assert compareTextArrays(["a", "b"], ["a", "b"]) == #equal; + +// Transitive: [[Nat]] +func compareNestedNatArrays( + a : [[Nat]], + b : [[Nat]], + compare : (implicit : ([[Nat]], [[Nat]]) -> Order), +) : Order { + compare(a, b); +}; + +assert compareNestedNatArrays([[1, 2], [3]], [[1, 2], [3]]) == #equal; +assert compareNestedNatArrays([[1]], [[2]]) == #less; + +// Derive Array.sort for [Nat] +do { + let sorted = Array.sort([3, 1, 2]); + assert sorted == [1, 2, 3]; +}; + +// Derive Array.equal for [Nat] +func arraysEqual( + a : [Nat], + b : [Nat], + equal : (implicit : ([Nat], [Nat]) -> Bool), +) : Bool { + equal(a, b); +}; + +assert arraysEqual([1, 2, 3], [1, 2, 3]); +assert not arraysEqual([1, 2], [1, 3]); + +// Derivation inside a module body (ObjBlockE) +do { + module CoreOps { + public func sortNats(arr : [Nat]) : [Nat] = Array.sort(arr); + public func eqNatArrays( + a : [Nat], b : [Nat], + equal : (implicit : ([Nat], [Nat]) -> Bool), + ) : Bool = equal(a, b); + }; + + assert CoreOps.sortNats([3, 1, 2]) == [1, 2, 3]; + assert CoreOps.eqNatArrays([1, 2, 3], [1, 2, 3]); + assert not CoreOps.eqNatArrays([1, 2], [1, 3]); +}; + +// Direct implicit still preferred over derivation +do { + var localCalled = false; + func compare(_a : [Nat], _b : [Nat]) : Order { + localCalled := true; + #equal; + }; + assert compareNatArrays([1], [2]) == #equal; + assert localCalled; +}; + +//SKIP comp diff --git a/test/run/implicit-derivation-implicit-package.mo b/test/run/implicit-derivation-implicit-package.mo new file mode 100644 index 00000000000..3bb09b32503 --- /dev/null +++ b/test/run/implicit-derivation-implicit-package.mo @@ -0,0 +1,30 @@ +//MOC-FLAG --package core $MOTOKO_CORE --implicit-package core +import { type Order } "mo:core/Order"; + +// Direct candidates from --implicit-package should take precedence over derived ones +// Note: this avoids breaking changes + +module MyNat { + public func compare(_ : Nat, _ : Nat, foo : (implicit : Nat)) : Order { + ignore foo; + #equal; // always returns #equal, unlike the real Nat.compare + }; +}; + +let foo : Nat = 42; +ignore foo; + +func compareNats( + a : Nat, + b : Nat, + compare : (implicit : (Nat, Nat) -> Order), +) : Order { + compare(a, b); +}; + +// This should use the original Nat.compare, not MyNat.compare +assert compareNats(1, 2) == #less; +assert compareNats(5, 5) == #equal; +assert compareNats(3, 1) == #greater; + +//SKIP comp diff --git a/test/run/implicit-derivation-record-variant.mo b/test/run/implicit-derivation-record-variant.mo new file mode 100644 index 00000000000..fe79d96fa89 --- /dev/null +++ b/test/run/implicit-derivation-record-variant.mo @@ -0,0 +1,290 @@ +//MOC-FLAG --package core $MOTOKO_CORE +import Array "mo:core/Array"; +import Nat "mo:core/Nat"; +import Text "mo:core/Text"; +import Option "mo:core/Option"; +import List "mo:core/List"; +import { Tuple2; Tuple3; Tuple4 } "mo:core/Tuples"; +import { type Order } "mo:core/Order"; + +// --- Helpers (to be moved to mo:core) --- + +module Unit { + public func compare(_ : (), _ : ()) : Order { #equal }; +}; + +module Order { + public func compareBy( + a : T, + b : T, + key : T -> K, + compare : (implicit : (compare : (K, K) -> Order)), + ) : Order { + compare(key(a), key(b)); + }; +}; + +module Variant3 { + public func compare( + v1 : { #a : A; #b : B; #c : C }, + v2 : { #a : A; #b : B; #c : C }, + compareA : (implicit : (compare : (A, A) -> Order)), + compareB : (implicit : (compare : (B, B) -> Order)), + compareC : (implicit : (compare : (C, C) -> Order)), + ) : Order { + switch (v1, v2) { + case (#a a1, #a a2) compareA(a1, a2); + case (#a _, _) #less; + case (_, #a _) #greater; + case (#b b1, #b b2) compareB(b1, b2); + case (#b _, _) #less; + case (_, #b _) #greater; + case (#c c1, #c c2) compareC(c1, c2); + }; + }; +}; + +module Variant4 { + public func compare( + v1 : { #a : A; #b : B; #c : C; #d : D }, + v2 : { #a : A; #b : B; #c : C; #d : D }, + compareA : (implicit : (compare : (A, A) -> Order)), + compareB : (implicit : (compare : (B, B) -> Order)), + compareC : (implicit : (compare : (C, C) -> Order)), + compareD : (implicit : (compare : (D, D) -> Order)), + ) : Order { + switch (v1, v2) { + case (#a a1, #a a2) compareA(a1, a2); + case (#a _, _) #less; + case (_, #a _) #greater; + case (#b b1, #b b2) compareB(b1, b2); + case (#b _, _) #less; + case (_, #b _) #greater; + case (#c c1, #c c2) compareC(c1, c2); + case (#c _, _) #less; + case (_, #c _) #greater; + case (#d d1, #d d2) compareD(d1, d2); + }; + }; +}; + +// --- Types --- + +type Status = { + #pending; + #inProgress : { assignees : ?List.List }; + #completed : { completedAt : Nat; score : Nat }; +}; + +type Priority = { #low; #medium; #high; #critical }; + +type Task = { + id : Nat; + var name : Text; + var priority : Priority; + var status : Status; + var description : Text; // not used in comparison +}; + +// --- Compare functions --- + +module Status { + public func compare(a : Status, b : Status) : Order { + Order.compareBy( + a, + b, + func(s) { + switch s { + case (#pending) #a; + case (#inProgress { assignees }) (#b assignees); + case (#completed { completedAt; score }) (#c(completedAt, score)); + }; + }, + ); + }; +}; + +module Priority { + public func compare(a : Priority, b : Priority) : Order { + Order.compareBy( + a, + b, + func(p) { + switch p { + case (#low) #a; + case (#medium) #b; + case (#high) #c; + case (#critical) #d; + }; + }, + ); + }; +}; + +module Task { + public func compare(a : Task, b : Task) : Order { + Order.compareBy(a, b, func(t) { (t.priority, t.status, t.id, t.name) }); + }; +}; + +module TaskByStatus { + public func compare(a : Task, b : Task) : Order { + Order.compareBy(a, b, func(t) { (t.status, t.priority, t.id) }); + }; +}; + +module WithoutImplicitDerivation { + module Status { + public func compare(a : Status, b : Status) : Order { + switch (a, b) { + case (#pending, #pending) #equal; + case (#pending, _) #less; + case (_, #pending) #greater; + case (#inProgress r1, #inProgress r2) { + switch (r1.assignees, r2.assignees) { + case (null, null) #equal; + case (null, _) #less; + case (_, null) #greater; + case (?l1, ?l2) List.compare(l1, l2, Text.compare); + }; + }; + case (#inProgress _, _) #less; + case (_, #inProgress _) #greater; + case (#completed r1, #completed r2) { + switch (Nat.compare(r1.completedAt, r2.completedAt)) { + case (#equal) Nat.compare(r1.score, r2.score); + case (ord) ord; + }; + }; + }; + }; + }; + + module Priority { + public func compare(a : Priority, b : Priority) : Order { + switch (a, b) { + case (#low, #low) #equal; + case (#low, _) #less; + case (_, #low) #greater; + case (#medium, #medium) #equal; + case (#medium, _) #less; + case (_, #medium) #greater; + case (#high, #high) #equal; + case (#high, _) #less; + case (_, #high) #greater; + case (#critical, #critical) #equal; + }; + }; + }; + + module Task { + public func compare(a : Task, b : Task) : Order { + switch (Priority.compare(a.priority, b.priority)) { + case (#equal) switch (Status.compare(a.status, b.status)) { + case (#equal) switch (Nat.compare(a.id, b.id)) { + case (#equal) Text.compare(a.name, b.name); + case (ord) ord; + }; + case (ord) ord; + }; + case (ord) ord; + }; + }; + }; + + module TaskByStatus { + public func compare(a : Task, b : Task) : Order { + switch (Status.compare(a.status, b.status)) { + case (#equal) switch (Priority.compare(a.priority, b.priority)) { + case (#equal) Nat.compare(a.id, b.id); + case (ord) ord; + }; + case (ord) ord; + }; + }; + }; + + public func taskCompare(a : Task, b : Task) : Order { Task.compare(a, b) }; + public func taskByStatusCompare(a : Task, b : Task) : Order { + TaskByStatus.compare(a, b); + }; +}; + +// --- Tests --- + +let tasks : [Task] = [ + { + id = 3; + var name = "Write docs"; + var priority = #low; + var status = #pending; + var description = "..."; + }, + { + id = 1; + var name = "Fix crash"; + var priority = #critical; + var status = #completed { completedAt = 100; score = 5 }; + var description = "..."; + }, + { + id = 2; + var name = "Add tests"; + var priority = #high; + var status = #inProgress { + assignees = ?List.fromArray(["Alice", "Bob"]); + }; + var description = "..."; + }, + { + id = 5; + var name = "Review PR"; + var priority = #medium; + var status = #completed { completedAt = 50; score = 3 }; + var description = "..."; + }, + { + id = 4; + var name = "Deploy"; + var priority = #critical; + var status = #pending; + var description = "..."; + }, + { + id = 6; + var name = "Refactor"; + var priority = #high; + var status = #inProgress { assignees = null }; + var description = "..."; + }, +]; + +// Two compare functions exist for Task, so we must pass explicitly + +let sorted = Array.sort(tasks, Task.compare); + +assert sorted[0].name == "Write docs"; // low +assert sorted[1].name == "Review PR"; // medium +assert sorted[2].name == "Refactor"; // high, inProgress (null assignees) +assert sorted[3].name == "Add tests"; // high, inProgress (?[Alice, Bob]) +assert sorted[4].name == "Deploy"; // critical, pending +assert sorted[5].name == "Fix crash"; // critical, completed + +let byStatus = Array.sort(tasks, TaskByStatus.compare); + +assert byStatus[0].name == "Write docs"; // pending, low +assert byStatus[1].name == "Deploy"; // pending, critical +assert byStatus[2].name == "Refactor"; // inProgress, null assignees +assert byStatus[3].name == "Add tests"; // inProgress, ?[Alice, Bob] +assert byStatus[4].name == "Review PR"; // completed, medium +assert byStatus[5].name == "Fix crash"; // completed, critical + +// Verify manual compare functions produce the same results + +let sorted2 = Array.sort(tasks, WithoutImplicitDerivation.taskCompare); +for (i in sorted.keys()) { assert sorted[i].id == sorted2[i].id }; + +let byStatus2 = Array.sort(tasks, WithoutImplicitDerivation.taskByStatusCompare); +for (i in byStatus.keys()) { assert byStatus[i].id == byStatus2[i].id }; + +//SKIP comp diff --git a/test/run/implicit-derivation-recursive.mo b/test/run/implicit-derivation-recursive.mo new file mode 100644 index 00000000000..242dc373245 --- /dev/null +++ b/test/run/implicit-derivation-recursive.mo @@ -0,0 +1,145 @@ +//MOC-FLAG --package core $MOTOKO_CORE +import { Tuple2; Tuple4 } "mo:core/Tuples"; +import Option "mo:core/Option"; +import Nat "mo:core/Nat"; +import { type Order } "mo:core/Order"; +import Int "mo:core/Int"; + +// --- Single recursion: MyList = ?(Nat, MyList) --- + +type MyList = ?(Nat, MyList); + +func compareMyLists( + a : MyList, + b : MyList, + compare : (implicit : (MyList, MyList) -> Order), +) : Order { compare(a, b) }; + +assert compareMyLists(null, null) == #equal; +assert compareMyLists(null, ?(1, null)) == #less; +assert compareMyLists(?(1, null), null) == #greater; +assert compareMyLists(?(1, null), ?(1, null)) == #equal; +assert compareMyLists(?(1, null), ?(2, null)) == #less; +assert compareMyLists(?(2, null), ?(1, null)) == #greater; + +assert compareMyLists( + ?(1, ?(2, ?(3, null))), + ?(1, ?(2, ?(3, null))), +) == #equal; + +assert compareMyLists( + ?(1, ?(2, ?(3, null))), + ?(1, ?(2, ?(4, null))), +) == #less; + +assert compareMyLists( + ?(1, ?(2, ?(3, null))), + ?(1, ?(2, null)), +) == #greater; + +// Deep nesting +assert compareMyLists( + ?(1, ?(2, ?(3, ?(4, ?(5, ?(6, ?(7, ?(8, ?(9, ?(10, ?(11, ?(12, null)))))))))))), + ?(1, ?(2, ?(3, ?(4, ?(5, ?(6, ?(7, ?(8, ?(9, ?(10, ?(11, ?(12, null)))))))))))), +) == #equal; + +assert compareMyLists( + ?(1, ?(2, ?(3, ?(4, ?(5, ?(6, ?(7, ?(8, ?(9, ?(10, ?(11, ?(12, null)))))))))))), + ?(1, ?(2, ?(3, ?(4, ?(5, ?(6, ?(7, ?(8, ?(9, ?(10, ?(11, ?(99, null)))))))))))), +) == #less; + +// --- Mutual recursion: A = ?(Nat, B), B = ?(Nat, A) --- + +type A = ?(Nat, B); +type B = ?(Nat, A); + +func compareAs( + a1 : A, + a2 : A, + compare : (implicit : (A, A) -> Order), +) : Order { compare(a1, a2) }; + +assert compareAs(null, null) == #equal; +assert compareAs(null, ?(1, null)) == #less; +assert compareAs(?(1, null), ?(1, null)) == #equal; +assert compareAs(?(1, ?(2, null)), ?(1, ?(2, null))) == #equal; +assert compareAs(?(1, ?(2, null)), ?(1, ?(3, null))) == #less; + +// Deep mutual recursion +assert compareAs( + ?(1, ?(2, ?(3, ?(4, null)))), + ?(1, ?(2, ?(3, ?(4, null)))), +) == #equal; + +assert compareAs( + ?(1, ?(2, ?(3, ?(4, null)))), + ?(1, ?(2, ?(3, ?(5, null)))), +) == #less; + +// --- Recursive type with multiple non-recursive fields --- + +type Tree = ?(Nat, Nat, Tree, Tree); + +func compareTrees( + a : Tree, + b : Tree, + compare : (implicit : (Tree, Tree) -> Order), +) : Order { compare(a, b) }; + +assert compareTrees(null, null) == #equal; +assert compareTrees(null, ?(1, 2, null, null)) == #less; +assert compareTrees(?(1, 2, null, null), ?(1, 2, null, null)) == #equal; +assert compareTrees(?(1, 2, null, null), ?(1, 3, null, null)) == #less; + +assert compareTrees( + ?(1, 2, ?(3, 4, null, null), null), + ?(1, 2, ?(3, 4, null, null), null), +) == #equal; + +assert compareTrees( + ?(1, 2, ?(3, 4, null, null), null), + ?(1, 2, ?(3, 5, null, null), null), +) == #less; + +// Left vs right subtree ordering +assert compareTrees( + ?(1, 2, ?(3, 4, null, null), ?(5, 6, null, null)), + ?(1, 2, ?(3, 4, null, null), ?(5, 7, null, null)), +) == #less; + +// --- Recursive type used in a larger expression --- + +type IntList = ?(Int, IntList); + +func sortedInsert( + x : Int, + xs : IntList, + compare : (implicit : (Int, Int) -> Order), +) : IntList { + switch xs { + case null ?(x, null); + case (?(h, t)) { + switch (compare(x, h)) { + case (#less) ?(x, xs); + case (#equal) ?(x, xs); + case (#greater) ?(h, sortedInsert(x, t)); + } + } + } +}; + +// Not testing the derivation of IntList compare here, +// just that recursive types with implicits work in broader contexts + +func compareIntLists( + a : IntList, + b : IntList, + compare : (implicit : (IntList, IntList) -> Order), +) : Order { compare(a, b) }; + +let list1 = sortedInsert(3, sortedInsert(1, sortedInsert(2, null))); +let list2 = sortedInsert(3, sortedInsert(1, sortedInsert(2, null))); +let list3 = sortedInsert(4, sortedInsert(1, sortedInsert(2, null))); + +assert compareIntLists(list1, list2) == #equal; +assert compareIntLists(list1, list3) == #less; diff --git a/test/run/implicit-derivation-transitive.mo b/test/run/implicit-derivation-transitive.mo new file mode 100644 index 00000000000..f9b6d4cacd0 --- /dev/null +++ b/test/run/implicit-derivation-transitive.mo @@ -0,0 +1,81 @@ +type Order = { #less; #greater; #equal }; + +var natCompareCalls = 0; +var arrayCompareCalls = 0; + +module Nat { + public func compare(a : Nat, b : Nat) : Order { + natCompareCalls += 1; + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module Array { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + arrayCompareCalls += 1; + let len = a.size(); + if (len != b.size()) { + if (len < b.size()) #less else #greater; + } else { + var i = 0; + var result : Order = #equal; + label l while (i < len) { + let c = compare(a[i], b[i]); + switch (c) { + case (#equal) {}; + case _ { result := c; break l }; + }; + i += 1; + }; + result; + }; + }; +}; + +// Transitive derivation: [[Nat]] -> [Nat] -> Nat +// Array.compare<[Nat]> needs [Nat].compare, which is Array.compare, +// which needs Nat.compare +func compareNestedArrays( + a : [[Nat]], + b : [[Nat]], + compare : (implicit : ([[Nat]], [[Nat]]) -> Order), +) : Order { + compare(a, b); +}; + +natCompareCalls := 0; +arrayCompareCalls := 0; +assert compareNestedArrays([[1, 2], [3]], [[1, 2], [3]]) == #equal; +assert arrayCompareCalls == 3; // 1 outer [[Nat]] + 2 inner [Nat] comparisons +assert natCompareCalls == 3; // (1,1) (2,2) (3,3) + +natCompareCalls := 0; +arrayCompareCalls := 0; +assert compareNestedArrays([[1]], [[2]]) == #less; +assert arrayCompareCalls == 2; // outer [[Nat]] + inner [Nat] +assert natCompareCalls == 1; + +natCompareCalls := 0; +arrayCompareCalls := 0; +assert compareNestedArrays([[1, 2], [3]], [[1, 2], [4]]) == #less; +assert arrayCompareCalls == 3; // outer + 2 inner +assert natCompareCalls == 3; // 1==1, 2==2, 3<4 + +// Derivation inside a module body (ObjBlockE) +do { + module NestedOps { + public func compareNested( + a : [[Nat]], + b : [[Nat]], + compare : (implicit : ([[Nat]], [[Nat]]) -> Order), + ) : Order = compare(a, b); + }; + + natCompareCalls := 0; + arrayCompareCalls := 0; + assert NestedOps.compareNested([[1, 2], [3]], [[1, 2], [3]]) == #equal; + assert arrayCompareCalls == 3; + assert natCompareCalls == 3; + + assert NestedOps.compareNested([[1]], [[2]]) == #less; +}; diff --git a/test/run/implicit-derivation.mo b/test/run/implicit-derivation.mo new file mode 100644 index 00000000000..14ee8639c42 --- /dev/null +++ b/test/run/implicit-derivation.mo @@ -0,0 +1,382 @@ +type Order = { #less; #greater; #equal }; + +// Base compare functions (monomorphic, act as leaf implicits) +var natCompareCalls = 0; +var intCompareCalls = 0; +var arrayCompareCalls = 0; + +module Nat { + public func compare(a : Nat, b : Nat) : Order { + natCompareCalls += 1; + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module Int { + public func compare(a : Int, b : Int) : Order { + intCompareCalls += 1; + if (a < b) #less else if (a == b) #equal else #greater; + }; +}; + +module Text { + public func compare(_a : Text, _b : Text) : Order { + #equal; + }; +}; + +// Polymorphic higher-order compare (has implicit parameter) +module Array { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + arrayCompareCalls += 1; + let len = a.size(); + if (len != b.size()) { + if (len < b.size()) #less else #greater; + } else { + var i = 0; + var result : Order = #equal; + label l while (i < len) { + let c = compare(a[i], b[i]); + switch (c) { + case (#equal) {}; + case _ { result := c; break l }; + }; + i += 1; + }; + result; + }; + }; +}; + +// Basic derivation with [Nat] +func compareNatArrays(a : [Nat], b : [Nat], compare : (implicit : ([Nat], [Nat]) -> Order)) : Order { + compare(a, b); +}; + +natCompareCalls := 0; +arrayCompareCalls := 0; +assert compareNatArrays([1, 2, 3], [1, 2, 3]) == #equal; +assert arrayCompareCalls == 1; +assert natCompareCalls == 3; + +assert compareNatArrays([1, 2], [1, 3]) == #less; + +// Derivation with [Int] +func compareIntArrays(a : [Int], b : [Int], compare : (implicit : ([Int], [Int]) -> Order)) : Order { + compare(a, b); +}; + +intCompareCalls := 0; +arrayCompareCalls := 0; +assert compareIntArrays([1, 2, 3], [1, 2, 3]) == #equal; +assert arrayCompareCalls == 1; +assert intCompareCalls == 3; + +// Explicit still works alongside derivation +func myArrayCompare(a : [Nat], b : [Nat]) : Order { + Array.compare(a, b, Nat.compare); +}; +assert compareNatArrays([1, 2], [1, 3], myArrayCompare) == #less; + +// Derivation with [Text] +func compareTextArrays(a : [Text], b : [Text], compare : (implicit : ([Text], [Text]) -> Order)) : Order { + compare(a, b); +}; + +assert compareTextArrays(["a"], ["b"]) == #equal; + +// Direct implicit still preferred over derivation +do { + var localCalled = false; + func compare(_a : [Nat], _b : [Nat]) : Order { + localCalled := true; + #equal; + }; + + assert compareNatArrays([1, 2], [1, 3]) == #equal; + assert localCalled; +}; + +// Monomorphic derivation (no type params) +module Pair { + public func compare(a : (Nat, Nat), b : (Nat, Nat), compare : (implicit : (Nat, Nat) -> Order)) : Order { + let c1 = compare(a.0, b.0); + switch (c1) { + case (#equal) { compare(a.1, b.1) }; + case _ c1; + }; + }; +}; + +func comparePairs(a : (Nat, Nat), b : (Nat, Nat), compare : (implicit : ((Nat, Nat), (Nat, Nat)) -> Order)) : Order { + compare(a, b); +}; + +assert comparePairs((1, 2), (1, 3)) == #less; +assert comparePairs((1, 2), (1, 2)) == #equal; + +// Polymorphic function uses derived implicit at call site +func polySort(a : [T], b : [T], compare : (implicit : ([T], [T]) -> Order)) : Order { + compare(a, b); +}; + +assert polySort([1, 2], [1, 3]) == #less; + +// Multiple implicits on the same function (one derived, one direct) +module Hasher { + public func hash(x : T, hash : (implicit : T -> Nat)) : Nat { + hash(x); + }; +}; + +module Nat2 { + public func hash(x : Nat) : Nat { + x; + }; +}; + +func needsBothImplicits( + a : [Nat], + b : [Nat], + compare : (implicit : ([Nat], [Nat]) -> Order), + hash : (implicit : Nat -> Nat), +) : (Order, Nat) { (compare(a, b), hash(42)) }; + +let (ord, h) = needsBothImplicits([1], [2]); +assert ord == #less; +assert h == 42; + +// Local module shadowing +// Local module's compare should win over outer Array.compare derivation +do { + var localArrayCalled = false; + module Array { + public func compare(_a : [Nat], _b : [Nat]) : Order { + localArrayCalled := true; + #equal; + }; + }; + + func needsCompare(a : [Nat], b : [Nat], compare : (implicit : ([Nat], [Nat]) -> Order)) : Order { + compare(a, b); + }; + + assert needsCompare([1], [2]) == #equal; + assert localArrayCalled; +}; + +// Direct resolution with zero non-implicit candidate in scope +module Const { + public func compare(compare : (implicit : (T, T) -> Order)) : (T, T) -> Order { + compare; + }; +}; + +func needsConstCompare(compare : (implicit : (Nat, Nat) -> Order)) : (Nat, Nat) -> Order { + compare; +}; + +let cmp = needsConstCompare(); +assert cmp(1, 2) == #less; + +// Multiple type parameters with multiple inner implicits +do { + module PairCmp { + public func compare( + a : (A, B), + b : (A, B), + cmpA : (implicit : (compare : (A, A) -> Order)), + cmpB : (implicit : (compare : (B, B) -> Order)), + ) : Order { + let c1 = cmpA(a.0, b.0); + switch (c1) { + case (#equal) { cmpB(a.1, b.1) }; + case _ c1; + }; + }; + }; + + func compareMixedPairs( + a : (Nat, Int), + b : (Nat, Int), + compare : (implicit : ((Nat, Int), (Nat, Int)) -> Order), + ) : Order { + compare(a, b); + }; + + assert compareMixedPairs((1, -2), (1, -3)) == #greater; + assert compareMixedPairs((1, -2), (1, -2)) == #equal; + assert compareMixedPairs((1, -2), (2, -2)) == #less; +}; + +// Derivation from local scope (top-level func, not module field) +do { + var localDeriveCalled = false; + func compare(_ : [T], _ : [T], compare : (implicit : (T, T) -> Order)) : Order { + ignore compare; + localDeriveCalled := true; + #equal; + }; + + func needsCompare(a : [Nat], b : [Nat], compare : (implicit : ([Nat], [Nat]) -> Order)) : Order { + compare(a, b); + }; + + assert needsCompare([1], [2]) == #equal; + assert localDeriveCalled; +}; + +// Local-derived (from local val) preferred over derived from module field +do { + var localDeriveCalled = false; + var moduleDeriveCalled = false; + + module _ArrMod { + public func compare(_ : [T], _ : [T], compare : (implicit : (T, T) -> Order)) : Order { + ignore compare; + moduleDeriveCalled := true; + #equal; + }; + }; + + func compare(_ : [T], _ : [T], compare : (implicit : (T, T) -> Order)) : Order { + ignore compare; + localDeriveCalled := true; + #equal; + }; + + func needsCompare(a : [Nat], b : [Nat], compare : (implicit : ([Nat], [Nat]) -> Order)) : Order { + compare(a, b); + }; + + assert needsCompare([1], [2]) == #equal; + assert localDeriveCalled; + assert not moduleDeriveCalled; +}; + +// Direct local val preferred over direct module field +do { + var localDirectCalled = false; + var moduleDirectCalled = false; + + module _M { + public func compare(_ : Nat, _ : Nat) : Order { + moduleDirectCalled := true; + #equal; + }; + }; + + func compare(_ : Nat, _ : Nat) : Order { + localDirectCalled := true; + #equal; + }; + + func needsCompare(a : Nat, b : Nat, compare : (implicit : (Nat, Nat) -> Order)) : Order { + compare(a, b); + }; + + assert needsCompare(1, 2) == #equal; + assert localDirectCalled; + assert not moduleDirectCalled; +}; + +// Direct module field preferred over derived local +do { + var directModuleCalled = false; + var derivedLocalCalled = false; + + module M { + public func compare(_ : [Nat], _ : [Nat]) : Order { + directModuleCalled := true; + #equal; + }; + }; + + func compare(_ : [T], _ : [T], compareT : (implicit : (compare : (T, T) -> Order))) : Order { + ignore compareT; + derivedLocalCalled := true; + #equal; + }; + ignore compare; + + func needsCompare(a : [Nat], b : [Nat], compare : (implicit : ([Nat], [Nat]) -> Order)) : Order { + compare(a, b); + }; + + assert needsCompare([1], [2]) == #equal; + assert directModuleCalled; + assert not derivedLocalCalled; +}; + +// Direct module field preferred over derived module field +do { + var directModuleCalled = false; + var derivedModuleCalled = false; + + module DirectM { + public func compare(_ : [Nat], _ : [Nat]) : Order { + directModuleCalled := true; + #equal; + }; + }; + + module _DerivedM { + public func compare(_ : [T], _ : [T], compare : (implicit : (T, T) -> Order)) : Order { + ignore compare; + derivedModuleCalled := true; + #equal; + }; + }; + + func needsCompare(a : [Nat], b : [Nat], compare : (implicit : ([Nat], [Nat]) -> Order)) : Order { + compare(a, b); + }; + + assert needsCompare([1], [2]) == #equal; + assert directModuleCalled; + assert not derivedModuleCalled; +}; + +// Subtyping in derivation: inner implicit resolved via supertype (Int.compare for Nat args) +do { + var intCompareCalled = false; + + // Shadow outer modules so only local definitions are in scope + module Nat {}; + module Int { + public func compare(a : Int, b : Int) : Order { + intCompareCalled := true; + if (a < b) #less else if (a == b) #equal else #greater; + }; + }; + + module Array { + public func compare(a : [T], b : [T], compare : (implicit : (T, T) -> Order)) : Order { + let len = a.size(); + if (len != b.size()) { + if (len < b.size()) #less else #greater; + } else { + var i = 0; + var result : Order = #equal; + label l while (i < len) { + switch (compare(a[i], b[i])) { + case (#equal) {}; + case other { result := other; break l }; + }; + i += 1; + }; + result; + }; + }; + }; + + func needsNatArrayCompare( + a : [Nat], + b : [Nat], + compare : (implicit : ([Nat], [Nat]) -> Order), + ) : Order { compare(a, b) }; + + // Int.compare : (Int, Int) -> Order satisfies (Nat, Nat) -> Order via Nat <: Int + assert needsNatArrayCompare([1, 2], [1, 3]) == #less; + assert intCompareCalled; +};