| # The solver |
| |
| Also consider reading the documentation for [the recursive solver in chalk][chalk] |
| as it is very similar to this implementation and also talks about limitations of this approach. |
| |
| [chalk]: https://rust-lang.github.io/chalk/book/recursive.html |
| |
| ## A rough walkthrough |
| |
| The entry-point of the solver is `InferCtxtEvalExt::evaluate_root_goal`. |
| This function sets up the root `EvalCtxt` and then calls `EvalCtxt::evaluate_goal`, |
| to actually enter the trait solver. |
| |
| `EvalCtxt::evaluate_goal` handles [canonicalization](./canonicalization.md), caching, |
| overflow, and solver cycles. |
| Once that is done, it creates a nested `EvalCtxt` with a |
| separate local `InferCtxt` and calls `EvalCtxt::compute_goal`, which is responsible for the |
| 'actual solver behavior'. |
| We match on the `PredicateKind`, delegating to a separate function for each one. |
| |
| For trait goals, such a `Vec<T>: Clone`, `EvalCtxt::compute_trait_goal` has |
| to collect all the possible ways this goal can be proven via |
| `EvalCtxt::assemble_and_evaluate_candidates`. |
| Each candidate is handled in a separate "probe", |
| to not leak inference constraints to the other candidates. |
| We then try to merge the assembled candidates via `EvalCtxt::merge_candidates`. |
| |
| |
| ## Important concepts and design patterns |
| |
| ### `EvalCtxt::add_goal` |
| |
| To prove nested goals, we don't directly call `EvalCtxt::compute_goal`, but instead |
| add the goal to the `EvalCtxt` with `EvalCtxt::add_goal`. |
| We then prove all nested goals together in either `EvalCtxt::try_evaluate_added_goals` or |
| `EvalCtxt::evaluate_added_goals_and_make_canonical_response`. |
| This allows us to handle inference constraints from later goals. |
| |
| E.g. if we have both `?x: Debug` and `(): ConstrainToU8<?x>` as nested goals, |
| then proving `?x: Debug` is initially ambiguous, but after proving `(): ConstrainToU8<?x>` |
| we constrain `?x` to `u8` and proving `u8: Debug` succeeds. |
| |
| ### Matching on `TyKind` |
| |
| We lazily normalize types in the solver, so we always have to assume that any types |
| and constants are potentially unnormalized. |
| This means that matching on `TyKind` can easily be incorrect. |
| |
| We handle normalization in two different ways. |
| When proving `Trait` goals when normalizing associated types, |
| we separately assemble candidates depending on whether they structurally |
| match the self type. |
| Candidates which match on the self type are handled in |
| `EvalCtxt::assemble_candidates_via_self_ty` which recurses via |
| `EvalCtxt::assemble_candidates_after_normalizing_self_ty`, |
| which normalizes the self type by one level. |
| In all other cases where we have to match on a `TyKind`, |
| we first use `EvalCtxt::try_normalize_ty` to normalize the type as much as possible. |
| |
| ### Higher ranked goals |
| |
| In case the goal is higher-ranked, e.g. `for<'a> F: FnOnce(&'a ())`, `EvalCtxt::compute_goal` |
| eagerly instantiates `'a` with a placeholder and then recursively proves |
| `F: FnOnce(&'!a ())` as a nested goal. |
| |
| ### Dealing with choice |
| |
| Some goals can be proven in multiple ways. |
| In these cases, |
| we try each option in a separate "probe", |
| and then attempt to merge the resulting responses by using `EvalCtxt::try_merge_responses`. |
| If merging the responses fails, we use `EvalCtxt::flounder` instead, returning ambiguity. |
| For some goals, |
| we try to incompletely prefer some choices over others |
| in case `EvalCtxt::try_merge_responses` fails. |
| |
| ## Learning more |
| |
| The solver should be fairly self-contained. |
| I hope that the above information provides a good foundation when looking at the code itself. |
| Please reach out on Zulip if you get stuck |
| while doing so or there are some quirks and design decisions which were unclear and deserve |
| better comments or should be mentioned here. |