Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .sources/VERSIONS
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ chain-fusion-signer v0.3.0
papi v0.1.1 168bc9d
ic-pub-key v1.0.1 f89fa55
icp-cli v0.2.3 caeac37
motoko v1.8.2 b77651f
motoko v1.9.0 e7c78d7
motoko-core v2.4.0 cd37dbf
cdk-rs ic-cdk v0.20.1 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 317f55c
candid 2025-12-18 # candid v0.10.20, didc v0.5.4 2e4a2cf
Expand Down
2 changes: 1 addition & 1 deletion .sources/motoko
Submodule motoko updated 205 files
168 changes: 168 additions & 0 deletions docs/languages/motoko/fundamentals/implicit-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ The compiler searches for implicit arguments in the following order, stopping at
1. Local values in the current scope.
2. Module fields (e.g., `Array.compare<T>`).
3. Fields of unimported modules (requires `--implicit-package`).
3. **Structural**: structural combiners (`__record`, `__tuple` convention) applied to record or tuple types (see [Structural derivation](#structural-derivation) below):
1. Local values in the current scope.
2. Module fields.
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.
Expand Down Expand Up @@ -223,6 +228,168 @@ The resolution depth is bounded to guarantee termination. If you encounter a dep

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.

### Structural derivation

When an implicit is needed for a **record or tuple type**, the compiler can synthesize it automatically using a *structural combiner*: a function whose single parameter name begins with `__` and encodes the structural decomposition kind. Structural combiners must not have implicit parameters.

Two structural kinds are supported, distinguished by the combiner's parameter name:

| Parameter name | Combiner type | Implicit argument type | Description |
|----------------|----------------------------|------------------------------------------|------------------------------------------------|
| `__record` | `[(Text, () -> E)] -> R` | `Rec -> R` or `(Rec, Rec) -> R` | Record: one or two records, arity from implicit|
| `__tuple` | `[() -> E] -> R` | `(A, B, ...) -> R` or `((A,B,...), (A,B,...)) -> R` (≥ 2 elements) | Tuple: one implicit per element |
| `__variant` |: |: | Reserved for future extension |

Each per-field/element result is wrapped in a **thunk** (`() -> E`), giving the combiner full control over evaluation order. Combiners that need all values (like serialization) simply call every thunk. Combiners that can short-circuit (like comparison) can stop early: remaining thunks are never evaluated.

The search label used to resolve per-element implicits is the same as the implicit parameter name at the call site.

:::caution
Motoko has no type abstraction (no newtypes or private types), so a named type that expands to a record: including stdlib containers like `Map`, `Set`, or `Buffer`: is structurally indistinguishable from a plain data record and may be decomposed into its internal fields by structural derivation; provide a dedicated instance (e.g. `MapJson`) to take precedence over structural synthesis for such types.
:::

#### Unary record derivation (`__record`)

When the compiler is looking for an implicit of type `SomeRecord -> R` and finds a unique structural combiner for `R` (parameter named `__record`, type `[(Text, () -> E)] -> R`), it:

1. Decomposes `SomeRecord` into its fields (in lexicographic order).
2. For each field `name : FieldType`, resolves a per-field implicit of type `FieldType -> E` using the same search label.
3. Synthesises a wrapper: `func($r) { combiner([("f1", func() { inst1($r.f1) }), ...]) }`.

This makes it possible for a library to provide generic serialization for **any** record type as long as instances exist for all field types.

##### Example: JSON serialization

Suppose a `Json` package defines a type, a structural combiner, and an entry point:

```motoko no-repl
public type Json = { #number : Int; #text : Text; #obj : [(Text, Json)]; /* ... */ };

// Structural combiner — __record parameter name triggers record-level synthesis.
// Each field is a thunk; serialization evaluates all of them.
public func encode(__record : [(Text, () -> Json)]) : Json =
#obj(__record.map(func((k, v)) = (k, v())));

// Entry point using contextual dot notation
public func toJson<R>(self : R, encode : (implicit : R -> Json)) : Json = encode(self);
```

And per-type instances in companion modules:

```motoko no-repl
// IntJson.mo
public func encode(self : Int) : Json = #number self;
```

Any record whose fields all have an `encode` instance can now be serialised with no boilerplate:

```motoko
import Json "mo:json/Json";
import IntJson "mo:json/IntJson";
import TextJson "mo:json/TextJson";

type Person = { name : Text; age : Int };

let p : Person = { name = "Alice"; age = 30 };
let json = p.toJson();
// Result: #obj([("name", #text "Alice"), ("age", #number 30)])
```

The compiler finds `Json.encode(__record)` as the unique structural combiner for `Json`, resolves per-field `encode` instances from `TextJson` and `IntJson`, and synthesizes the wrapper automatically.

#### Binary record derivation

When the compiler is looking for an implicit of type `(Rec, Rec) -> R` where `Rec` is a record type and both arguments have the same type, it searches for a `__record` combiner for `R`: the same combiner that handles the unary case. The arity is determined entirely by the implicit argument's type; the combiner itself is unaware of it.

The compiler synthesizes a binary wrapper:

```
func($r1, $r2) { combiner([("f1", func() { inst1($r1.f1, $r2.f1) }), ...]) }
```

Each per-field implicit has type `(FieldType, FieldType) -> E`, resolved recursively with the same search label. This allows binary operations like comparison or equality to be derived field-by-field from a single `__record` combiner.

##### Example: lexicographic comparison

```motoko
import Array "mo:core/Array";
import Nat "mo:core/Nat";
import Text "mo:core/Text";
import Order "mo:core/Order";

// __record combiner: fold field-wise Order values, short-circuiting at first non-equal.
// Thunks enable genuine short-circuiting — remaining fields are never evaluated.
func compare(__record : [(Text, () -> Order.Order)]) : Order.Order {
for ((_, ordThunk) in __record.vals()) {
let ord = ordThunk();
if (ord != #equal) return ord
};
#equal
};

type Person = { name : Text; age : Nat };

// Array.sort uses (implicit : (T, T) -> Order.Order) — derived from __record (binary path).
// Fields resolved: age → Nat.compare, name → Text.compare (lexicographic order).
let people : [Person] = [{ name = "Carol"; age = 30 }, { name = "Bob"; age = 25 }];
let sorted = people.sort();
// sorted[0] = { name = "Bob"; age = 25 } (age 25 < 30)
```

Nested record types are handled automatically: a `Team` with a `Person` field will derive `compare` for `Team` by first deriving `compare` for `Person` at depth+1.

#### Tuple derivation (`__tuple`)

When the compiler is looking for an implicit of type `(A, B, ...) -> R` (a tuple domain with at least two elements), it searches for a structural combiner whose parameter is named `__tuple` and has type `[() -> E] -> R`.

When found, the compiler synthesizes a wrapper:

```
func($t) { combiner([func() { inst0($t.0) }, func() { inst1($t.1) }, ...]) }
```

Each per-element implicit has type `ElemType_i -> E`, resolved positionally using the same search label.

#### Binary tuple derivation

Like `__record`, the `__tuple` combiner also supports binary implicit arguments. When the implicit argument has type `((A, B, ...), (A, B, ...)) -> R` (two arguments of the same tuple type with ≥ 2 elements), the compiler synthesizes a binary wrapper:

```
func($t1, $t2) { combiner([func() { inst0($t1.0, $t2.0) }, func() { inst1($t1.1, $t2.1) }, ...]) }
```

Each per-element implicit has type `(ElemType_i, ElemType_i) -> E`. This enables element-wise binary operations like comparison or equality over tuples.

##### Example: tuple description

```motoko
// __tuple combiner: join per-element descriptions (evaluates all thunks)
func describe(__tuple : [() -> Text]) : Text {
var s = "("; var first = true;
for (t in __tuple.vals()) {
if (not first) { s #= ", " };
s #= t(); first := false
};
s #= ")"; s
};

module TextDesc { public func describe(self : Text) : Text = self };
module NatDesc { public func describe(self : Nat) : Text = debug_show self };

func inspect<T>(x : T, describe : (implicit : T -> Text)) : Text = describe(x);

assert inspect(("hello", 42 : Nat)) == "(hello, 42)";
```

#### Disambiguation: binary vs unary when both `__record` and `__tuple` are in scope

Having `__record` and `__tuple` combiners in scope simultaneously is safe: the compiler picks the right path by inspecting the **number of arguments** in the implicit argument's function type. The dispatch depends on where the tuple appears in the source, not on what the type expands to:

- `implicit : (X, X) -> T`: the inline tuple `(X, X)` is flattened into two separate args. The compiler sees a **two-argument** function, checks that both args are the same type, and uses the binary path: `__record` if `X` is a record type, `__tuple` if `X` is a tuple type (≥ 2 elements).
- `implicit : P -> T` where `P` is a **type alias** for `(A, B, ...)`: `P` is not a tuple in the source, so it stays as a single arg. The compiler sees a **one-argument** function, promotes `P` to a tuple, and uses the `__tuple` combiner (unary path).

In practice: write `(X, X) -> T` directly as two args to trigger the binary path. Going through a type alias `type Pair = (R, R)` and writing `Pair -> T` will route to `__tuple` (unary) instead.

### Supported types

The core library provides comparison functions for common types:
Expand Down Expand Up @@ -327,6 +494,7 @@ There is no need to update existing code unless you want to take advantage of th
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.
- For `__record` structural derivation, the synthesized wrapper invokes one implicit per record field (two invocations per field for the binary path), so runtime cost scales linearly with record width. For `__tuple`, cost scales with tuple arity. For hot paths with wide types, consider writing the combiner explicitly.

## See also

Expand Down
10 changes: 9 additions & 1 deletion docs/languages/motoko/reference/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ sidebar:

# Motoko compiler changelog

## 1.9.0 (2026-06-02)

* motoko (`moc`)

* feat: Structural implicit derivation for records and tuples via `__record` and `__tuple` combiners. Per-field results are lazy thunks, enabling short-circuiting for operations like `compare` (#5903).

* feat: `--experimental-multi-value` flag enables function-level multi-value Wasm codegen. Off by default (#6113).

## 1.8.2 (2026-05-21)

* motoko (`moc`)
Expand Down Expand Up @@ -906,7 +914,7 @@ sidebar:
ensures that no cleanup is required.

The relevant security best practices are accessible at
/guides/security/inter-canister-calls#recommendation
https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/inter-canister-calls#recommendation

BREAKING CHANGE (Minor): `finally` is now a reserved keyword,
programs using this identifier will break.
Expand Down
6 changes: 6 additions & 0 deletions docs/languages/motoko/reference/language-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,12 @@ the expanded function call expression `<parenthetical>? <exp1> <T0,…​,Tn>? <
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.

**Structural derivation**: When derivation also fails, the compiler additionally searches for *structural combiners*: first among local values, then module fields, then library fields (gated on `--implicit-package`). The combiner's parameter name determines the structural kind:

- `__record` (parameter type `[(Text, () -> E)] -> R`): handles both unary holes (`SomeRecord -> R`) and binary holes (`(SomeRecord, SomeRecord) -> R` where both args are the same record type). For a unary hole it synthesizes `func($r) { combiner([("f", func() = inst($r.f)), ...]) }` with per-field implicits `FieldType -> E`. For a binary hole it synthesizes `func($r1, $r2) { combiner([("f", func() = inst($r1.f, $r2.f)), ...]) }` with per-field implicits `(FieldType, FieldType) -> E`. Per-field thunks let the combiner short-circuit (e.g. comparison). The arity is determined by the hole type, not the combiner.
- `__tuple` (parameter type `[() -> E] -> R`): handles both unary holes (`(A, B, ...) -> R` with at least two elements) and binary holes (`((A, B, ...), (A, B, ...)) -> R` where both args are the same tuple type with ≥ 2 elements). For a unary hole it synthesizes `func($t) { combiner([func() = inst0($t.0), func() = inst1($t.1), ...]) }` with per-element implicits `ElemType_i -> E`. For a binary hole it synthesizes `func($t1, $t2) { combiner([func() = inst0($t1.0, $t2.0), ...]) }` with per-element implicits `(ElemType_i, ElemType_i) -> E`. Tuples with fewer than two elements are not synthesized: single-element tuples reduce to the element type, and unit `()` is treated as a scalar.
- `__variant` is reserved for future extension.

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
Loading