Skip to content

Commit 6704e80

Browse files
chore: bump Motoko to v1.9.0 (#287)
## Summary Automated bump of `.sources/motoko` from `v1.8.2` to `v1.9.0`. - Ran `npm run sync:motoko` — synced docs from the new release - Build passed ✓ ## Checklist - [ ] Review synced content for breaking changes - [ ] Check [release notes](https://github.com/caffeinelabs/motoko/releases/tag/1.9.0) for API or syntax changes that affect hand-written docs - [ ] Verify any new sections or renamed files are handled correctly ## Sync recommendation `sync from caffeinelabs/motoko doc/md` Co-authored-by: pr-automation-bot-public[bot] <pr-automation-bot-public[bot]@users.noreply.github.com>
1 parent d31d025 commit 6704e80

5 files changed

Lines changed: 185 additions & 3 deletions

File tree

.sources/VERSIONS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ chain-fusion-signer v0.3.0
5757
papi v0.1.1 168bc9d
5858
ic-pub-key v1.0.1 f89fa55
5959
icp-cli v0.2.3 caeac37
60-
motoko v1.8.2 b77651f
60+
motoko v1.9.0 e7c78d7
6161
motoko-core v2.4.0 cd37dbf
6262
cdk-rs ic-cdk v0.20.1 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 317f55c
6363
candid 2025-12-18 # candid v0.10.20, didc v0.5.4 2e4a2cf

.sources/motoko

Submodule motoko updated 205 files

docs/languages/motoko/fundamentals/implicit-parameters.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ The compiler searches for implicit arguments in the following order, stopping at
191191
1. Local values in the current scope.
192192
2. Module fields (e.g., `Array.compare<T>`).
193193
3. Fields of unimported modules (requires `--implicit-package`).
194+
3. **Structural**: structural combiners (`__record`, `__tuple` convention) applied to record or tuple types (see [Structural derivation](#structural-derivation) below):
195+
1. Local values in the current scope.
196+
2. Module fields.
197+
3. Fields of unimported modules (requires `--implicit-package`).
198+
194199
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.
195200

196201
This ordering guarantees that direct matches are always preferred over derived ones, and local definitions take precedence over imported or unimported module definitions.
@@ -223,6 +228,168 @@ The resolution depth is bounded to guarantee termination. If you encounter a dep
223228

224229
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.
225230

231+
### Structural derivation
232+
233+
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.
234+
235+
Two structural kinds are supported, distinguished by the combiner's parameter name:
236+
237+
| Parameter name | Combiner type | Implicit argument type | Description |
238+
|----------------|----------------------------|------------------------------------------|------------------------------------------------|
239+
| `__record` | `[(Text, () -> E)] -> R` | `Rec -> R` or `(Rec, Rec) -> R` | Record: one or two records, arity from implicit|
240+
| `__tuple` | `[() -> E] -> R` | `(A, B, ...) -> R` or `((A,B,...), (A,B,...)) -> R` (≥ 2 elements) | Tuple: one implicit per element |
241+
| `__variant` |: |: | Reserved for future extension |
242+
243+
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.
244+
245+
The search label used to resolve per-element implicits is the same as the implicit parameter name at the call site.
246+
247+
:::caution
248+
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.
249+
:::
250+
251+
#### Unary record derivation (`__record`)
252+
253+
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:
254+
255+
1. Decomposes `SomeRecord` into its fields (in lexicographic order).
256+
2. For each field `name : FieldType`, resolves a per-field implicit of type `FieldType -> E` using the same search label.
257+
3. Synthesises a wrapper: `func($r) { combiner([("f1", func() { inst1($r.f1) }), ...]) }`.
258+
259+
This makes it possible for a library to provide generic serialization for **any** record type as long as instances exist for all field types.
260+
261+
##### Example: JSON serialization
262+
263+
Suppose a `Json` package defines a type, a structural combiner, and an entry point:
264+
265+
```motoko no-repl
266+
public type Json = { #number : Int; #text : Text; #obj : [(Text, Json)]; /* ... */ };
267+
268+
// Structural combiner — __record parameter name triggers record-level synthesis.
269+
// Each field is a thunk; serialization evaluates all of them.
270+
public func encode(__record : [(Text, () -> Json)]) : Json =
271+
#obj(__record.map(func((k, v)) = (k, v())));
272+
273+
// Entry point using contextual dot notation
274+
public func toJson<R>(self : R, encode : (implicit : R -> Json)) : Json = encode(self);
275+
```
276+
277+
And per-type instances in companion modules:
278+
279+
```motoko no-repl
280+
// IntJson.mo
281+
public func encode(self : Int) : Json = #number self;
282+
```
283+
284+
Any record whose fields all have an `encode` instance can now be serialised with no boilerplate:
285+
286+
```motoko
287+
import Json "mo:json/Json";
288+
import IntJson "mo:json/IntJson";
289+
import TextJson "mo:json/TextJson";
290+
291+
type Person = { name : Text; age : Int };
292+
293+
let p : Person = { name = "Alice"; age = 30 };
294+
let json = p.toJson();
295+
// Result: #obj([("name", #text "Alice"), ("age", #number 30)])
296+
```
297+
298+
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.
299+
300+
#### Binary record derivation
301+
302+
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.
303+
304+
The compiler synthesizes a binary wrapper:
305+
306+
```
307+
func($r1, $r2) { combiner([("f1", func() { inst1($r1.f1, $r2.f1) }), ...]) }
308+
```
309+
310+
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.
311+
312+
##### Example: lexicographic comparison
313+
314+
```motoko
315+
import Array "mo:core/Array";
316+
import Nat "mo:core/Nat";
317+
import Text "mo:core/Text";
318+
import Order "mo:core/Order";
319+
320+
// __record combiner: fold field-wise Order values, short-circuiting at first non-equal.
321+
// Thunks enable genuine short-circuiting — remaining fields are never evaluated.
322+
func compare(__record : [(Text, () -> Order.Order)]) : Order.Order {
323+
for ((_, ordThunk) in __record.vals()) {
324+
let ord = ordThunk();
325+
if (ord != #equal) return ord
326+
};
327+
#equal
328+
};
329+
330+
type Person = { name : Text; age : Nat };
331+
332+
// Array.sort uses (implicit : (T, T) -> Order.Order) — derived from __record (binary path).
333+
// Fields resolved: age → Nat.compare, name → Text.compare (lexicographic order).
334+
let people : [Person] = [{ name = "Carol"; age = 30 }, { name = "Bob"; age = 25 }];
335+
let sorted = people.sort();
336+
// sorted[0] = { name = "Bob"; age = 25 } (age 25 < 30)
337+
```
338+
339+
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.
340+
341+
#### Tuple derivation (`__tuple`)
342+
343+
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`.
344+
345+
When found, the compiler synthesizes a wrapper:
346+
347+
```
348+
func($t) { combiner([func() { inst0($t.0) }, func() { inst1($t.1) }, ...]) }
349+
```
350+
351+
Each per-element implicit has type `ElemType_i -> E`, resolved positionally using the same search label.
352+
353+
#### Binary tuple derivation
354+
355+
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:
356+
357+
```
358+
func($t1, $t2) { combiner([func() { inst0($t1.0, $t2.0) }, func() { inst1($t1.1, $t2.1) }, ...]) }
359+
```
360+
361+
Each per-element implicit has type `(ElemType_i, ElemType_i) -> E`. This enables element-wise binary operations like comparison or equality over tuples.
362+
363+
##### Example: tuple description
364+
365+
```motoko
366+
// __tuple combiner: join per-element descriptions (evaluates all thunks)
367+
func describe(__tuple : [() -> Text]) : Text {
368+
var s = "("; var first = true;
369+
for (t in __tuple.vals()) {
370+
if (not first) { s #= ", " };
371+
s #= t(); first := false
372+
};
373+
s #= ")"; s
374+
};
375+
376+
module TextDesc { public func describe(self : Text) : Text = self };
377+
module NatDesc { public func describe(self : Nat) : Text = debug_show self };
378+
379+
func inspect<T>(x : T, describe : (implicit : T -> Text)) : Text = describe(x);
380+
381+
assert inspect(("hello", 42 : Nat)) == "(hello, 42)";
382+
```
383+
384+
#### Disambiguation: binary vs unary when both `__record` and `__tuple` are in scope
385+
386+
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:
387+
388+
- `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).
389+
- `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).
390+
391+
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.
392+
226393
### Supported types
227394

228395
The core library provides comparison functions for common types:
@@ -327,6 +494,7 @@ There is no need to update existing code unless you want to take advantage of th
327494
Implicit arguments are resolved at compile time.
328495
- For direct matches, the resulting code is identical to explicitly passing the argument.
329496
- 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.
497+
- 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.
330498

331499
## See also
332500

docs/languages/motoko/reference/changelog.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ sidebar:
88

99
# Motoko compiler changelog
1010

11+
## 1.9.0 (2026-06-02)
12+
13+
* motoko (`moc`)
14+
15+
* 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).
16+
17+
* feat: `--experimental-multi-value` flag enables function-level multi-value Wasm codegen. Off by default (#6113).
18+
1119
## 1.8.2 (2026-05-21)
1220

1321
* motoko (`moc`)
@@ -906,7 +914,7 @@ sidebar:
906914
ensures that no cleanup is required.
907915
908916
The relevant security best practices are accessible at
909-
/guides/security/inter-canister-calls#recommendation
917+
https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/inter-canister-calls#recommendation
910918
911919
BREAKING CHANGE (Minor): `finally` is now a reserved keyword,
912920
programs using this identifier will break.

docs/languages/motoko/reference/language-manual.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2389,6 +2389,12 @@ the expanded function call expression `<parenthetical>? <exp1> <T0,…​,Tn>? <
23892389
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.
23902390
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.
23912391
2392+
**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:
2393+
2394+
- `__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.
2395+
- `__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.
2396+
- `__variant` is reserved for future extension.
2397+
23922398
The call expression `<exp1> <T0,…​,Tn>? <exp2>` evaluates `<exp1>` to a result `r1`. If `r1` is `trap`, then the result is `trap`.
23932399
23942400
Otherwise, `<exp3>` (the hole expansion of `<exp2>`) is evaluated to a result `r2`. If `r2` is `trap`, the expression results in `trap`.

0 commit comments

Comments
 (0)