Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve coinduction handling in recursive solver #457

Closed

Conversation

zaharidichev
Copy link
Contributor

@zaharidichev zaharidichev commented May 15, 2020

This change is an attempt to handle coinduction semantics better when solving with the recursive solver. The main idea is that we introduce the concept of solutions with delayed goals that can be refined later. The main concepts behind the approach are borrowed from: https://rust-lang.github.io/chalk/book/engine/logic/coinduction.html Would love to know if I missed something obvious.

Fix: #399

Signed-off-by: Zahari Dichev [email protected]

@shepmaster
Copy link
Member

reecursive

typo

@jackh726
Copy link
Member

@flodiebold do you think you could take a look at this?

@flodiebold
Copy link
Member

It seems fine code-wise, but I don't really know a lot about coinduction or how it works, so probably it would be better if @nikomatsakis can take a look as well.

@zaharidichev zaharidichev changed the title Improve coinduction handling in reecursive solver Improve coinduction handling in recursive solver May 18, 2020
Copy link
Contributor

@nikomatsakis nikomatsakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this! It was really helpful to read. I'm going to take a bit of time and do a review of how the recursive solver works, and in particular to sketch out how I think caching should be working, taking into account the desire to do (a) salsa-style caching and (b) parallel solving and also now (c) getting coinduction correct. I think it might yield a slightly different variation on this approach, but I'm not sure yet.

@@ -16,10 +16,37 @@ use rustc_hash::FxHashMap;

type UCanonicalGoal<I> = UCanonical<InEnvironment<Goal<I>>>;

#[derive(Clone, Debug, PartialEq, Eq)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a comment to this struct clarifying its role

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Answer<I: Interner> {
pub solution: Solution<I>,
pub delayed_goals: Vec<UCanonicalGoal<I>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and we should comment this field especially

}
}

pub fn needs_refinement(&self) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should comment this =)

!self.delayed_goals.is_empty()
}

pub fn combine(self, other: Answer<I>, interner: &I) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly an example would be great here

@@ -105,7 +132,35 @@ impl<'me, I: Interner> Solver<'me, I> {
debug!("solve_root_goal(canonical_goal={:?})", canonical_goal);
assert!(self.context.stack.is_empty());
let minimums = &mut Minimums::new();
self.solve_goal(canonical_goal.clone(), minimums)
self.solve_goal(canonical_goal.clone(), minimums).map(|a| {
assert!(a.delayed_goals.is_empty()); // at this point there should be no delayed goals
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? In some of the more extreme examples I found that you had to have delayed goals that required iteration even at the root level I think, but maybe that is hapenning in the solve_goal routine...

})
}

fn refine_answer(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: when is this invoked in the process of solving, and what is an example input/output?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is goal and what is answer?

return value.clone();
return value.clone().and_then(|a| {
if a.needs_refinement() {
self.refine_answer(goal.clone(), a, minimums)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the main role of this routine is to remove "trivial" self-cycles, right? How can those come about here -- i.e., if we are retrieving a value from the cache, would it ever have trivial self-cycles?

self.apply_solution(free_vars, universes, constrained_subst);
progress = true;
}
}

solution.is_ambig()
delayed_goals.extend(answer.delayed_goals);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plausibly we might filter out delayed goals here in this step

binders: goal.clone().canonical.binders,
}),
vec![goal],
));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was to think a bit about this -- in particular, I think the recursive solver can handle this case differently than the SLG solver. Unlike the SLG solver, it's ok for intermediate queries in the recursive solver to not get cached (and/or get cached at the end, along with everything else). I have to review how we handle caching and minimums, but I suspect that we want to work towards a setup where the cache itself never has delayed goals, they are always ephemeral things that exist on the stack during solving.

@zaharidichev
Copy link
Contributor Author

@nikomatsakis Thanks a bunch for the feedback. I will address the comments and revise my logic here. If you happen to arrive at any more insights, feel free to share :)

@zaharidichev
Copy link
Contributor Author

@nikomatsakis I think I am a bit stuck on that. I understand the overall intended approach but have a few questions:

  • In .fulfill() we effectivelly want to check whether the delayed goals that we get from solving a subgoal are actually trivial cycles. I wonder what is the most practical way to do that since the goal that fullfill is instantiated with could be of type UCanonical<InEnvironment<Goal<I>>> or UCanonical<InEnvironment<DomainGoal<I>>> but what we get as delayed goals in the Answer is UCanonical<InEnvironment<Goal<I>>> Could there be a delayed DomainGoal actually?

  • Is it correct to assume that we want to store all of these partially solved goals on the stack and attempt to solve all of their delayed subgoals once we enchounter them again and only if that succeeds put the solution in the cache.

For some reason all of this makes my head hurt at times :)

@jackh726
Copy link
Member

Closing this. I think we want to go with the alternative approach @nikomatsakis described.

@jackh726 jackh726 closed this Jun 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

coinduction semantics in the recursive solver
5 participants