A direct trait obligation
Consider one trait, one impl, and a generic function that requires the trait:
#![allow(unused)] fn main() { trait Marker {} struct Widget {} impl Marker for Widget {} fn require_marker<T: Marker>(value: T) {} fn demo(widget: Widget) { require_marker(widget); } }
The logical question is Widget: Marker. This chapter follows that question
from the call expression to a Yes response. The next chapter opens up the
candidate proof itself.
1. The call publishes an obligation
The function signature checker lowers T: Marker into the function's
CheckedParameterEnv. When body checking resolves the callee, it instantiates
the generic signature with fresh call-site inference variables and publishes
that instantiated environment:
example_instantiate_callee_obligations
At this instant the predicate is conceptually ?T: Marker. Checking the
argument subsequently equates ?T with Widget. The obligation manager can
attempt the non-ground form and receive Maybe, or observe the equality first;
either way, the committed equality wakes and re-canonicalizes the retained
obligation.
Publishing a parameter environment turns each represented predicate into an atomic solver goal:
example_publish_parameter_env
The solver IR separates logical structure from atomic predicates:
example_solver_goal_ir
For the settled call-site type, the relevant value is:
Goal::Atom(Atom::TraitImpl {
self_ty: Widget,
trait_ref: Marker<[]>,
})
2. Canonicalization makes a context-free query
Inference-variable IDs and stash pointers are local implementation details, so
they cannot be Salsa keys or shared proof-frame identities. canonicalize_goal
copies the environment and goal into a fresh stash, replacing free inference
variables and rigid parameters with ordered canonical parameters:
example_canonicalize_goal
The returned pair has two sides:
datais the context-freeGoalQueryData, including the local crate, assumptions, canonical variables, and goal;mappingremembers how canonical inputs correspond to this caller's rigid parameters or inference variables.
Once ?T has become Widget, this query has no canonical variables. If it is
attempted earlier, ?T becomes an ExistentialInput and the mapping points
back to the caller variable.
3. Salsa enters the per-query proof runtime
An obligation attempt interns the canonical data, invokes the tracked prove
query, and transactionally applies the returned response:
example_run_trait_query
The tracked method is deliberately thin:
example_goal_query
prove_query validates the canonical input, instantiates it in a fresh
producer-owned proof context, creates the per-execution whiteboard, drives the
root future, and extracts a canonical response:
example_prove_query
Salsa memoizes completed canonical queries. The whiteboard has a different job: it shares in-progress atomic work within this one proof execution.
4. The atom gets a whiteboard frame
Every atomic goal is canonicalized at its current inference version before it requests a frame. The completed frame response is then applied back into that requester's version:
example_prove_atom
The whiteboard first checks the parent chain for an inductive cycle, then looks for an identical frame under the same parent and remaining depth:
example_whiteboard_lookup
For this first request there is no ancestor or existing frame, so the
whiteboard starts a producer for Widget: Marker. Another sibling requesting
the same canonical atom under the same parent would subscribe to that producer
instead of recomputing it.
5. Candidate discovery finds the impl
The frame first considers matching assumptions and then local impl clauses:
example_assemble_candidates
impl Marker for Widget is eligible and has the requested trait, so it becomes
the sole applicable clause. Matching its head against Widget: Marker succeeds
without a substitution, and its empty body is trivially true.
The implementation shown above deliberately exposes two current MVP limits: it scans only local impls, and it scans them before filtering by trait. The Trait Impl Candidate Discovery RFD specifies the planned global, trait-keyed query and its incremental tests.
6. Yes returns to inference
With one unconditional candidate, merging returns
Yes { subst: [], modulo: true }. Applying any response happens in a child
inference version: substitutions are copied and unified there, then the child
is collapsed only if the whole application succeeds.
example_apply_response_transaction
There is no substitution to apply for this ground query. The obligation
manager sees an unconditional Yes, marks the obligation terminal, and body
checking can complete. A Maybe would instead retain the original obligation;
a Yes with non-trivial modulo would retain the residual proof condition.
The complete path is therefore:
function bound
-> instantiated call-site obligation
-> canonical GoalQuery
-> atomic whiteboard frame
-> local impl candidate
-> canonical Yes response
-> transactional application