-
Notifications
You must be signed in to change notification settings - Fork 183
Use substitutions and suggestions in Solution; implement negation #36
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
Conversation
src/solve/infer/test.rs
Outdated
binders: vec![ParameterKind::Ty(U1), ParameterKind::Ty(U0), ParameterKind::Ty(U2)], | ||
}); | ||
} | ||
// #[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, forgot to get back to these.
I've now pushed up a commit with a preliminary implementation of negation, which is straightforward to add. However, it doesn't currently handle |
I've now pushed up an additional commit which adjusts the treatment of unification for skolemized type variables so that negation does the Right Thing. TLDR: we now treat |
Update: now includes a commit that implements normalization fallback via lowering to negation! |
This commit is a major refactoring with a few goals: - Move `Solution` to a Unique/Ambiguous model, with the ambiguous case encompassing multiple forms of guidance for type inference (including "suggestions" intended to be applied as fallback). - Remove the notion of goal refinement, and instead use substitutions for communicating solutions. - Draw a more clear line between the generic Chalk engine (which "only knows Prolog", i.e. doesn't know anything special about Rust) and lowering. Put differently, everything specific to Rust should now be encoded in the lowering, rather than though special cases in the Chalk engine. This work is somewhat incomplete: the commit removes the normalization fallback, which was one major special case, but doesn't yet replace it. A follow up PR will introduce negation and use it in lowering to recover this fallback. - Consolidate the solver code so that the "strategies" for solving leaf goals are more clear and all co-located. - Introduce some of the heuristics that rustc performs for inference, e.g. preferring `where` clauses for influencing inference. We do this in a way that decouples the heuristics from the rest of the solver; the `favor_over` method, in particular, contains the relevant logic.
Introduce `not { ... }` as a logical operator, using negation-as-failure semantics adjusted for our "three-valued" logic. WIP; currently doesn't handle `forall` correctly for unification.
…, thereby correcting the semantics with respect to negation
This commit introduces a new domain goal: `KnownProjection`. It holds whenever you're able to directly normalize a projection, i.e. through an impl or where clause. Normalizations are then expanded to include a fallback precisely in the cases where `KnownProjection` cannot be proved.
OK, this PR is as big as it's going to get -- it now does not regress Chalk in any way, and fleshes out the complete story for negation. I'll be working on the |
Do you think this will be stable enough to branch off for overlap/specialization or should I wait until its landed? |
@withoutboats I expect it to be stable, unless @nikomatsakis has a very surprising reaction to it :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! I left various nits, along with a few more serious questions. Nothing major though.
let mut lifetimes = BTreeMap::new(); | ||
|
||
for (var, ty) in &self.tys { | ||
tys.insert(*var, ty.fold_with(folder, binders)?); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can't we write this with some kind of fancy collect()
call? ;) (don't feel obligated)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ?
here is what seems to make the imperative more clear and nearly as concise, sadly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like this?
let tys = self.tys.iter()
.map(|(&var, ty)| (var, ty.fold_with(folder, binders)?))
.collect::<Result<BTreemap<_, _>>>()?;
Anyway, doesn't matter. More readable this way =)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that's exactly what I mean -- I find this iterator-chained version much harder to read...
src/ir/mod.rs
Outdated
pub trait_data: HashMap<ItemId, TraitDatum>, | ||
|
||
/// For each trait: | ||
/// For each trait (used for debugging): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: should probably be "for each associated type" (pre-existing)
src/ir/mod.rs
Outdated
@@ -91,22 +96,24 @@ impl Environment { | |||
Arc::new(env) | |||
} | |||
|
|||
pub fn elaborated_clauses(&self, program: &ProgramEnvironment) -> impl Iterator<Item = WhereClause> { | |||
/// Generate the full set of clauses that are "syntactically implied" by the | |||
/// clauses in this environment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No good deed goes unpunished -- this comment could be improved by adding a few examples.
I'd write something like:
Generate the full set of clauses that are implied by the clauses in this environment. Currently this consists of two kinds of expansions:
- Supertraits are added, so
T: Ord
would expand to{T: Ord, T: PartialOrd}
- Associated type normalizations imply that the types are implemented, so
T: Iterator<Item=Foo>
expands to{T: Iterator, T: Iterator<Item=Foo>}
.
Computes and returns the least-fixed-point of applying the above expansions.
src/ir/mod.rs
Outdated
pub enum WhereClause { | ||
/// A "domain goal" is a goal that is directly about Rust, rather than a pure | ||
/// logical statement. As much as possible, the Chalk solver should avoid | ||
/// decomposing this enum, and instead treat its values opaquely. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I prefer to put the comments above the #[derive]
line -- is this something that just I do?
src/ir/mod.rs
Outdated
pub enum Constraint { | ||
LifetimeEq(Lifetime, Lifetime), | ||
} | ||
|
||
/// A mapping of inference variables to instantiations thereof. | ||
// Uses BTreeMap for extracting in order (mostly for debugging/testing) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: was this supposed to be ///
?
src/solve/fulfill.rs
Outdated
let goal = canonicalized.quantified.value; | ||
|
||
// Negation cannot be used to resolve existential variables, and do | ||
// not have a useful (for us) logial meaning when they contain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/logial/logical/
src/solve/fulfill.rs
Outdated
return Ok(Outcome::Incomplete); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: extra blank line
src/solve/fulfill.rs
Outdated
// as ambiguous | ||
if !canonicalized.free_vars.is_empty() { | ||
return Ok(Outcome::Incomplete); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is stronger than necessary, right? For example, imagine Vec<?T>: Copy
-- given that there are no impls of Copy
for Vec
at all, and assuming a closed world, we could figure out that this is false. In particular, if we get a Unique
result with an empty substitution, we are satisfied, even if free_vars
is non-empty...?
src/solve/fulfill.rs
Outdated
Ok(Solution::Ambig(Guidance::Definite(subst.quantified))) | ||
// Next we look at the goals we must refute; doing so cannot create new | ||
// obligations of any form | ||
let negative = self.fulfill_negative()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clever to stratify these like that, though probably not the most performant thing ever (i.e., we might have been able to see that we will fail to refute a "refute goal" and hence abort the process much earlier).
src/lower/mod.rs
Outdated
// | ||
// we generate: | ||
// | ||
// ?T: SomeTrait<Assoc = (SomeTrait::Assoc)<?T>> :- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question -- currently, for each type Foo = Bar
in an impl or the where-clause list, we generate two clauses:
<T as Trait>::Foo ==> Bar
and
known { <T as Trait>::Foo ==> Bar }
Maybe we should just translate the impl declarations along with T: Trait<Foo=Bar>
where-clauses into the "known" form to start, and then, for each declaration in the trait, have two instances of the "general rule":
<?T as Trait>::Foo ==> Bar :- known { <?T as Trait>::Foo ==> Bar }
<?T as Trait>::Foo ==> <?T as Trait>::Foo :- not { known { <?T as Trait>::Foo ==> Bar } }
Obviously, this is equivalent. Just seems a bit clearer to me somehow.
src/solve/fulfill.rs
Outdated
// existential variables; treat negative goals with free variables | ||
// as ambiguous | ||
if !canonicalized.free_vars.is_empty() { | ||
return Ok(Outcome::Incomplete); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: This is stronger than necessary, right? It seems like any result of Unique
with an empty substitution is probably good enough. Consider something like not { Vec<?T>: Copy }
... we know that it is false (given closed-world, at least), simply because there are no impls of Copy
at all that could apply.
(I made this comment earlier, in my review, but GH marked it as "outdated" for some reason.)
src/solve/fulfill.rs
Outdated
// go one last time through the positive obligations, this time | ||
// applying even *tentative* inference suggestions, so that we can | ||
// yield these upwards as our own suggestions. In particular, we | ||
// yield up the first one we can find. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: First in what sense? we're popping the obligations, after all, so this isn't the first we would have encountered had we been applying the substs "in order", right? (Or am I missing something?)
(I made this comment earlier, in my review, but GH marked it as "outdated" for some reason.)
src/solve/fulfill.rs
Outdated
|
||
Ok(Solution::Ambig(Guidance::Unknown)) | ||
} else { | ||
// While we failed to prove the goal, we still leared that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: s/leared/learned/
(I made this comment earlier, in my review, but GH marked it as "outdated" for some reason.)
Closing with prejudic--- oh, no, I guess it's fine. |
Actually I think I've found a regression. Load this Chalk program: struct Foo { }
trait Marker { }
impl Marker for Foo { } and then try to solve the goal
current master branch answers: If you remove In fact, aturon's branch will see the two rules This bug is not caught by the |
OK, I've pushed up a couple of commits addressing @nikomatsakis's feedback. In particular, I added some commentary trying to explain in greater depth why we don't want to allow negations over free variables. Note also that this follows the original paper on negation as failure. Also, I've collapsed the "clever" stratification of negative and positive goals. I think this new version is nicer. @scalexm, I'll look at the issue you raised next. |
@scalexm OK, I've investigated, and believe this comes down to the imprecision discussed in this comment about combining solutions. I don't see any harm in tightening that up, but am out of time for the moment. |
Thanks, that comment made sense, but I still think we could be yet more precise (not a blocker by any means, though). In your example: struct Vec<T> {}
struct i32 {}
struct u32 {}
trait Foo {}
impl Foo for Vec<u32> {} Given the query Anyway, seems fine as is.
yeah, I agree with this diagnosis. |
Test failure:
|
On IRC, @scalexm and I were saying that maybe they will investigate this regression that they found. I also pointed them at this particular snippet of code, which I think may be the bit of logic that @aturon's branch doesn't have. (But I could be wrong.) |
Yeah, the confusion seemed to be around the precise conditions under which we'd go to ambiguous. But I agree we can punt this in any case. |
It's possible -- there are probably a few ways to deal with this. As much as possible, though, I'd like to push this kind of thing into the general logic for combining solutions. |
I agree that seems like the right place, didn't mean to suggest otherwise. |
This commit is a major refactoring with a few goals:
Move
Solution
to a Unique/Ambiguous model, with the ambiguous caseencompassing multiple forms of guidance for type inference (including
"suggestions" intended to be applied as fallback).
Remove the notion of goal refinement, and instead use substitutions
for communicating solutions.
Draw a more clear line between the generic Chalk engine (which "only
knows Prolog", i.e. doesn't know anything special about Rust) and
lowering. Put differently, everything specific to Rust should now be
encoded in the lowering, rather than though special cases in the Chalk
engine.
Consolidate the solver code so that the "strategies" for solving leaf
goals are more clear and all co-located.
Introduce some of the heuristics that rustc performs for inference,
e.g. preferring
where
clauses for influencing inference. We do thisin a way that decouples the heuristics from the rest of the solver;
the
favor_over
method, in particular, contains the relevant logic.Introduce
not { ... }
as a logical operator, using negation-as-failuresemantics adjusted for our "three-valued" logic.