From 839fa79ddb6d69a57a31c185366da8a8b03cf62a Mon Sep 17 00:00:00 2001 From: "pr-automation-bot-public[bot]" Date: Mon, 8 Jun 2026 09:31:07 +0000 Subject: [PATCH] chore: bump Motoko to v1.9.0 --- .sources/VERSIONS | 2 +- .sources/motoko | 2 +- .../fundamentals/implicit-parameters.md | 168 ++++++++++++++++++ docs/languages/motoko/reference/changelog.md | 10 +- .../motoko/reference/language-manual.md | 6 + 5 files changed, 185 insertions(+), 3 deletions(-) diff --git a/.sources/VERSIONS b/.sources/VERSIONS index 066d9663..3f72cb18 100644 --- a/.sources/VERSIONS +++ b/.sources/VERSIONS @@ -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 diff --git a/.sources/motoko b/.sources/motoko index b77651f6..e7c78d7c 160000 --- a/.sources/motoko +++ b/.sources/motoko @@ -1 +1 @@ -Subproject commit b77651f63988ec53c95b1a61760c399fcc760577 +Subproject commit e7c78d7cb57726707f643d6b4b048fc70e60692d diff --git a/docs/languages/motoko/fundamentals/implicit-parameters.md b/docs/languages/motoko/fundamentals/implicit-parameters.md index c3935854..d76098f5 100644 --- a/docs/languages/motoko/fundamentals/implicit-parameters.md +++ b/docs/languages/motoko/fundamentals/implicit-parameters.md @@ -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`). 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. @@ -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(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(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: @@ -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 diff --git a/docs/languages/motoko/reference/changelog.md b/docs/languages/motoko/reference/changelog.md index 3dc91531..6a8ba1a1 100644 --- a/docs/languages/motoko/reference/changelog.md +++ b/docs/languages/motoko/reference/changelog.md @@ -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`) @@ -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. diff --git a/docs/languages/motoko/reference/language-manual.md b/docs/languages/motoko/reference/language-manual.md index a73c193a..a2fd748a 100644 --- a/docs/languages/motoko/reference/language-manual.md +++ b/docs/languages/motoko/reference/language-manual.md @@ -2389,6 +2389,12 @@ the expanded function call expression `? ? < 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. + **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 ` ? ` 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`.