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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -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>` + `Nat.compare`). Works transitively and is depth-limited via `--implicit-derivation-depth` (#5966).

## 1.6.0 (2026-04-21)

* motoko (`moc`)
Expand Down
1 change: 1 addition & 0 deletions doc/md/15-compiler-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n>` | 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 <n>` | Set maximum number of pages available for library `ExperimentStableMemory.mo` (default 65536). |
Expand Down
5 changes: 5 additions & 0 deletions doc/md/16-language-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,11 @@ the expanded function call expression `<parenthetical>? <exp1> <T0,…​,Tn>? <
* Ds is the disambiguated set of candidates, filtered by generality.
* `<mid>.<idi>` 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<Nat>` when `Nat.compare` is in scope. The derivation depth is bounded by the `--implicit-derivation-depth` flag.

The call expression `<exp1> <T0,…​,Tn>? <exp2>` evaluates `<exp1>` to a result `r1`. If `r1` is `trap`, then the result is `trap`.

Otherwise, `<exp3>` (the hole expansion of `<exp2>`) is evaluated to a result `r2`. If `r2` is `trap`, the expression results in `trap`.
Expand Down
70 changes: 58 additions & 12 deletions doc/md/fundamentals/11-implicit-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@
## 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.
Comment thread
Kamirus marked this conversation as resolved.
Other exampes are `equal` and `toText` functions.
Other examples are `equal` and `toText` functions.

## Basic usage

### Declaring implicit parameters

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<K, V>(self: Map<K, V>, compare : (implicit : (K, K) -> Order), key : K, value : V) {
// ...
}
```

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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -81,7 +81,7 @@ let map = Map.empty<Nat, Text>();
// 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");
```

Expand Down Expand Up @@ -147,7 +147,7 @@ let scores = Map.empty<Text, Nat>();
// 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);
Expand All @@ -158,7 +158,7 @@ if (scores.containsKey("Alice")) {
};

// Get size
let playerCount = scores.size()
let playerCount = scores.size();
```

## How inference works
Expand All @@ -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<T>`).
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anything here clearly states that the hole must have a (monomorphic) function type although perhaps its implied by saying that stripping the implicit arguments from the candidate produces a matching type.

I now realize that's probably why my example in #6054 fails.

Copy link
Copy Markdown
Contributor Author

@Kamirus Kamirus Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've explored this extension in #5991 (see comments there)
but this approach of deriving values (records/modules with funcs) instead of funcs is weaker, as it does not extend to __record to allow generic folds over records/tuples/etc
We could have both but this complicates the PR even more
So I'd just refactor your example to have two separate implicits, instead of a single record that ships both
This approach is simpler and extends to __record.
I'd just define toCandid and fromCandid separately instead of rep.

btw, if you want to allow context-dot syntax like myObj.toCandid() then you'd need an indirection like

func toCandid<A>(self : A, toCandidImpl : (implicit : A -> Blob)) : Blob {
  toCandidImpl(self)
}


For example, suppose `Array.compare` is declared as:

```motoko no-repl
public func compare<T>(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<Nat>`, 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<Nat>`, 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:
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/mo_config/args.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
1 change: 1 addition & 0 deletions src/mo_config/flags.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will affect all imported code too. Maybe that's ok but seems fragile.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but we don't have flags per file/package and we need some default cap

let generate_view_queries = ref false

let default_warning_levels = M.empty
Expand Down
Loading
Loading