[compiler-v2] Fix type inference to allow unifying struct constraints and receiver function constraints (#18929)
What specific code changed
- File
third_party/move/move-model/src/ty.rsgot a new match arm in theConstraint::compatibleimplementation that treatsSomeStructandSomeReceiverFunctionconstraints as orthogonal and therefore non‑conflicting. - Four new test assets were added under
third_party/move/move-compiler-v2/tests/checking/receiver/:constraint_compatible.moveand its expected model dumpconstraint_compatible.exp– a positive test that should type‑check.constraint_incompatible.moveand its expected diagnosticsconstraint_incompatible.exp– a negative test that must still fail.
Why this change was made
The Move type‑inference engine previously considered any two constraints on a type variable to be potentially conflicting. When a lambda parameter received both a struct‑field constraint (e.g., the field value exists) and a receiver‑function constraint (e.g., the method get_value exists), the engine incorrectly reported a conflict, preventing the compiler from inferring the lambda’s type. The change explicitly marks those two constraint kinds as compatible because they describe independent aspects of a type: one is about the presence of a field, the other about the presence of a receiver method.
How it works technically (reference the diff)
In ty.rs the Constraint::compatible match now contains:
+ // SomeStruct and SomeReceiverFunction constrain orthogonal aspects of a type
+ // (field existence vs receiver function existence), so they can coexist.
+ (Constraint::SomeStruct(..), Constraint::SomeReceiverFunction(..))
+ | (Constraint::SomeReceiverFunction(..), Constraint::SomeStruct(..)) => Ok(false),
Returning Ok(false) signals that the two constraints do not conflict, allowing the inference algorithm to keep both on the same type variable. All other constraint pairs still follow the existing conflict‑detection logic.
The new positive test constraint_compatible.move defines:
fun test(s: &S): u64 {
apply(s, |x| x.value + x.get_value())
}
Here the lambda parameter x receives a SomeStruct{value} constraint from the field access and a SomeReceiverFunction(get_value) constraint from the method call. With the new compatibility rule the compiler can infer x as &S and emit the expected model dump shown in constraint_compatible.exp.
The negative test constraint_incompatible.move still fails because it combines a SomeStruct{is_isolated} constraint with a SomeReceiverFunction(get_nav) constraint, but no struct in scope satisfies both. The inference algorithm now correctly reports the original diagnostics, confirming that the change only relaxes the orthogonal case.
Where it fits in the Aptos pipeline
This modification lives in the Move compiler (the move-compiler-v2 crate). The affected code runs during the **type‑checking / type‑inference** phase, which is part of the compilation pipeline that transforms Move source into bytecode before the execution stage. The change does not touch the runtime Move VM, consensus, or storage layers; it only influences whether source code can be successfully compiled into executable bytecode.
Implications
- Developers can now write generic inline functions that accept lambdas using both field accesses and receiver calls on the same parameter, expanding the expressive power of Move’s higher‑order functions.
- The compiler’s error messages become more precise: only truly incompatible constraints will be flagged, reducing false‑positive type‑inference failures.
- No runtime impact: the generated bytecode is unchanged except that previously failing programs now compile successfully.
- Future extensions that introduce additional orthogonal constraint kinds can follow the same pattern by adding similar match arms.
ELI5 — Explain Like I'm 5
Vineeth added a tiny rule to the Move compiler so it can understand two different kinds of clues about a variable at the same time. Imagine you’re trying to guess what kind of toy a mystery box contains. One clue says the toy has a button (a field), another clue says the toy can play a song when you press it (a receiver function). Before, the compiler thought those clues contradicted each other and gave up.
Now the compiler knows that having a button and being able to play a song are separate features that can coexist, so it can correctly say the box holds a specific toy (the struct S in the test). The change lives in the type‑checking part of the Move compiler, which runs before any transaction hits the blockchain.
The new test constraint_compatible.move shows the happy path: a lambda receives a struct, reads its value field and calls get_value() on it, and the compiler happily compiles it. The old failing test still fails because no struct has both an is_isolated field and a get_nav method, proving the rule only relaxes the truly independent cases.
In short, Vineeth fixed a false‑positive type‑inference error, letting developers write richer higher‑order functions without breaking the compiler.
Other Deep Dives
View this report interactively with Advanced / ELI5 tabs at https://aptos-intelligence.vercel.app/#d22380f. Plain-text version: /reports/d22380f.txt.