feat: implicit argument derivation and structural record synthesis - #5903
Conversation
When no direct implicit match exists, the compiler now derives implicit arguments from functions that themselves have implicit parameters. For example, if `Array.compare<T>` has an implicit `(T, T) -> Order`, the compiler can automatically derive a `([Nat], [Nat]) -> Order` by instantiating `T := Nat` and recursively resolving `Nat.compare`. This works transitively (e.g. `[[Nat]]` derives through two levels) and with both polymorphic and monomorphic candidates. Resolution depth is bounded by `--implicit-derivation-depth` (default: 5). Error messages distinguish missing vs ambiguous inner implicits and report which candidate was tried and which inner implicit failed. Made-with: Cursor
… ambiguous candidates
…rameters and enhance error reporting for ambiguous implicit arguments - Removed the `inst` parameter from `synthesize_derived_wrapper` as it was not utilized. - Improved error messages for ambiguous implicit arguments, updating error codes and notes to provide clearer context on the candidates involved. - Updated test cases to reflect changes in error reporting for ambiguous implicit derivations.
|
Cool, any ideas how handle record and variants, where you don't have a function predefined? |
I think that records and variants need an explicit compare function to say
Then we could have helper functions defined in core (or compiler-derived) and use like: type Foo = {
name : Text;
id : Nat;
subs : List.List<Bar>;
};
module FooCompareByNameAndId {
public func compare(a : Foo, b : Foo) : Order.Order {
a.compareBy(b, func (r) { (r.name, r.id) })
// `compareBy` should be defined in `Order.mo`
}
};So that a record/variant comparison could be defined via tuple comparison. |
- Updated the implicit derivation process to clarify the search order for candidates, prioritizing direct local values over derived ones. - Improved documentation in the language manual and implicit parameters guide to reflect changes in implicit derivation and resolution order. - Fixed minor typos and inconsistencies in comments and documentation across multiple files. - Adjusted test cases to ensure clarity and accuracy in expected outcomes for implicit derivation scenarios.
- Introduced a new function `display_typ_oneline` for concise type display. - Updated `render_derivation_tree` to improve handling of error messages, including clearer differentiation between failed and attempted implicit derivations. - Adjusted test cases to reflect changes in error reporting format, ensuring consistency in the output for ambiguous and not found cases.
- Removed the unused `~tldr` parameter from `render_derivation_tree` for clarity. - Simplified the handling of inner errors by always including the detailed type display. - Adjusted indentation logic for better readability in error reporting.
…e types Detect recursion during implicit derivation by tracking in-progress goals in a shared mutable list. When a cycle is found, return a VarE referencing the already-in-progress function. After resolution succeeds, wrap the result in a BlockE with mutually recursive LetD bindings so all generated $derived_implicit_N functions can reference each other. Made-with: Cursor
…riant helpers Demonstrates how Order.compareBy, Variant3, and Variant4 reduce compare boilerplate for records and variants. Uses mutable fields, optional List<Text> payloads, and two alternative sort orderings. Made-with: Cursor
Made-with: Cursor
- Use `is_matching_typ` in `matching_vals` and `matching_fields` instead of inline `T.sub` (eliminates dead helper) - Refactor `synthesize_derived_wrapper` from mutable refs to `List.fold_left`, making data flow explicit - Document `Local/Returns` restriction on derivation candidates - Update `render_derivation_tree` comment (kept pending full review) - Fix trailing whitespace - Add subtyping edge-case test: `Int.compare` satisfies inner `(Nat, Nat) -> Order` implicit via function contravariance - Add PR number (#5903) to changelog - Accept pre-existing tc-human indentation drift Made-with: Cursor
- Add comment explaining why `assert false` in `resolve_hole` is safe (depends on no-backtracking invariant) - Fix changelog to say "functions (possibly polymorphic)" instead of "polymorphic functions" since derivation also works for monomorphic functions with implicit parameters Made-with: Cursor
Allow the compiler to 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`. Structural derivation (`__record`/`__tuple` combiners) will follow in a separate PR. Made-with: Cursor
| | _ -> None in | ||
| match T.promote candidate_typ with | ||
| | T.Func (T.Local, T.Returns, [], [T.Named ("__record", inner_typ)], [ret_typ]) -> | ||
| (match T.promote inner_typ with |
There was a problem hiding this comment.
dubious promote on negative occurrence?
| with_thunk_elem `Record thunk_typ ret_typ | ||
| | _ -> None) | ||
| | T.Func (T.Local, T.Returns, [], [T.Named ("__tuple", inner_typ)], [ret_typ]) -> | ||
| (match T.promote inner_typ with |
There was a problem hiding this comment.
Ditto: dubious promote on negative occurrence?
Not easily no, after we merge the part1 then I'll update this PR |
## Summary Part 1 of caffeinelabs#5903 — implicit argument derivation only, without structural derivation (`__record`/`__tuple` combiners). The compiler can now derive implicit arguments from functions that themselves have implicit parameters. For example, an implicit `compare : ([Nat], [Nat]) -> Order` is automatically derived from `Array.compare<Nat>` when `Nat.compare` is in scope. - Derivation works transitively (e.g. `compare` for `[[Nat]]` chains through `Array.compare<[Nat]>` → `Array.compare<Nat>` → `Nat.compare`) - Recursive derivation cycles are detected and handled via `rec_bindings` - Depth is bounded by `--implicit-derivation-depth` (default 100) - Refactored `resolve_hole` and `disambiguate_resolutions` to support the new derivation tiers while preserving existing direct-match behavior - Improved error messages: derivation failures report which inner implicits are missing and suggest imports Structural derivation (`__record`/`__tuple` combiners for records and tuples) will follow in a separate PR on top of this one.
christoph-dfinity
left a comment
There was a problem hiding this comment.
I'm still a bit worried about code size/perf degradation if this feature is applied liberally in a large-ish codebase. If that becomes a problem I'll rely on you to write the skills to guide the AI.
Some smaller change requests, but looks good otherwise.
| || hasPrefix "base-stub/" "${relPath}/" | ||
| || hasPrefix "json-stub/" "${relPath}/"; |
There was a problem hiding this comment.
I don't think we should be adding to these. The top-level directories under test/ would ideally just be test suites.
I'd rather get rid of the stubs at some point and just replace them with our $MOTOKO_CORE that's around in the tests.
- Use T.normalize (not promote) on the structural combiner's argument type — it's a negative occurrence, so we shouldn't follow Abs bounds. - MapJson._toJson now takes a _toJson implicit for the key (not toText) and encodes entries as #array [k_json, v_json], matching Tuple2Json. Co-authored-by: Cursor <cursoragent@cursor.com>
Same negative-occurrence concern as the candidate-side fix: the hole's domain is the argument type of the function we're synthesizing, so we shouldn't follow Abs bounds. Otherwise structural derivation succeeds inside a polymorphic body on T's bound and silently drops fields of the concrete T at the call site. Co-authored-by: Cursor <cursoragent@cursor.com>
Cursor AI review👍 APPROVE — looks safe to merge
VerdictDecision: APPROVE Generated for commit 4b1dda5 |
Language manual showed `[(Text, E)] -> R` / `[E] -> R` for `__record` / `__tuple` combiners but the implementation requires lazy thunks (`[(Text, () -> E)] -> R` / `[() -> E] -> R`). The fundamentals doc was already correct. Also drop unused `ImplicitHoles.partition` helper. Co-authored-by: Cursor <cursoragent@cursor.com>
Motoko has no type abstraction, so containers like Map/Set/Buffer that normalize to records can be structurally decomposed if no dedicated instance is in scope. Document the risk and the workaround. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace per-arity Tuple2Json/Tuple3Json with a single TupleJson using the __tuple structural combiner, and move toJson<R> + the __record combiner out of Json.mo into a dedicated RecordJson.mo. Json.mo now only holds the Json type and toText utility. Co-authored-by: Cursor <cursoragent@cursor.com>
Local accept produced 8-space continuation indents because NO_COLOR was set in the dev shell, which strips the ANSI escapes around the note bullet that otherwise contribute to grace's prefix-length-based continuation alignment. CI doesn't set NO_COLOR, so it produces 17-space indents. Regenerate the two affected .ok files to match CI output. Co-authored-by: Cursor <cursoragent@cursor.com>
Move the user-facing toJson<R> entry point back to Json.mo so .toJson() on primitives or tuples doesn't require importing RecordJson. RecordJson now contains only the structural __record combiner. Co-authored-by: Cursor <cursoragent@cursor.com>
| import ArrayJson "mo:json/ArrayJson"; | ||
| import ListJson "mo:json/ListJson"; | ||
| import MapJson "mo:json/MapJson"; | ||
| import TupleJson "mo:json/TupleJson"; |
There was a problem hiding this comment.
Currently this multiple-imports and flat-modules hierarchy is the recommended way because of how our import hints work.
Our hints look like this currently Did you mean to import mo:json/MapJson?
Nested modules won't show up in these hints so we either accept this style or redesign.
Cover the case where two unimported lib modules provide ambiguous derivation candidates for an implicit hole and a structural-record combiner uniquely resolves it. Without the fall-through behaviour this test would error out at the lib-derivation tier with M0231. Co-authored-by: Cursor <cursoragent@cursor.com>
Move the ambiguity-fallthrough stub package under test/run/ so it ships with the run/ suite via the existing nix sandbox filter, instead of adding another top-level test/ directory. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Extends the Motoko compiler's implicit argument resolution with two new mechanisms:
1. Implicit derivation from functions with inner implicits
The compiler can now resolve implicit arguments by composing them from functions (possibly polymorphic) that themselves have implicit parameters. For example, an implicit
comparefor[Nat]is automatically derived fromArray.compare<Nat>+Nat.compare, eliminating boilerplate wrapper modules. Works transitively (e.g.,[[Nat]]). Depth-limited with--implicit-derivation-depth(default: 5).Before:
After:
2. Structural derivation for records and tuples
A function whose sole explicit parameter is named
__record(typed[(Text, T)] -> R) or__tuple(typed[T] -> R) acts as a structural combiner. When the compiler needs an implicit for a record or tuple type, it automatically decomposes the type, resolves a per-field/per-element implicit (using the same search label), and synthesizes a wrapper. Both unary (X -> R) and binary ((X, X) -> R) hole types are supported.Example — generic JSON serialization for any record:
The
Jsonpackage defines a single_toJson(__record : [(Text, Json)]) : Jsoncombiner. That's all — the compiler handles every record type automatically, as long as each field type has a_toJsoninstance.Resolution order
Direct matches are always preferred over derived ones. Within each tier, the most specific candidate (by subtyping) wins:
--implicit-package)--implicit-package)__record/__tuplecombiner)--implicit-package)Test plan
Run tests
test/run/implicit-derivation.mo— basic derivation, monomorphic, polymorphic, multiple implicits, priority ordering, subtypingtest/run/implicit-derivation-transitive.mo—[[Nat]]through two derivation levelstest/run/implicit-derivation-recursive.mo— single recursion, mutual recursion, recursive treestest/run/implicit-derivation-record-variant.mo— real-world record/variant comparison withcompareBytest/run/implicit-derivation-core.mo— integration withmo:corelibrary (Array,Nat,Int,Text)test/run/implicit-derivation-implicit-package.mo— derivation from unimported library modulestest/run/implicit-derivation-json.mo— structural derivation for JSON serialization (records, tuples, maps, lists)test/run/implicit-derivation-structural.mo— basic structural record derivation (unary)test/run/implicit-derivation-structural-compare.mo— binary structural derivation for record/tuple comparisontest/run/implicit-derivation-structural-dispatch.mo—__recordvs__tupledispatch and binary/unary disambiguationFail tests (14 files)
test/fail/implicit-derivation.mo— missing inner implicit, basic derivation errorstest/fail/implicit-derivation-ambiguous.mo— ambiguous derived candidates at the head leveltest/fail/implicit-derivation-ambiguous-deep.mo— ambiguous inner implicit during derivationtest/fail/implicit-derivation-bimatch.mo— bi-matching limits during type instantiationtest/fail/implicit-derivation-deep.mo— derivation chain errors at multiple depthstest/fail/implicit-derivation-depth.mo—--implicit-derivation-depthlimit hittest/fail/implicit-derivation-no-backtracking.mo— no backtracking across derivation candidatestest/fail/implicit-derivation-json.mo— real-world JSON errors: missing leaves, expanded Map types, nested recordstest/fail/implicit-derivation-structural-missing.mo— record with a field type that has no instancetest/fail/implicit-derivation-structural-ambiguous.mo— two structural combiners in scopetest/fail/implicit-derivation-structural-hint.mo— structural combiner in an unimported library (import hint)test/fail/implicit-derivation-structural-depth.mo— structural derivation depth limittest/fail/implicit-derivation-structural-record2-missing.mo— binary record derivation with missing field instancetest/fail/implicit-derivation-structural-tuple-missing.mo— tuple derivation with missing element instanceTest package
test/json-stub/— minimal JSON package used by the structural derivation tests; demonstrates the__recordconvention and companion per-type modules (IntJson,TextJson,ArrayJson,ListJson,MapJson,Tuple2Json,Tuple3Json)