-
Notifications
You must be signed in to change notification settings - Fork 182
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
Elaborating where clauses directly in the logic #49
Conversation
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 a partial review -- I didn't get a chance to finish everything. =)
src/lower/mod.rs
Outdated
// | ||
// we generate the following clause: | ||
// | ||
// for<?T> WF(Foo<?T>) :- (?T: Eq). | ||
// for<?T> WF(Foo<?T>) :- WF(?T), WF(Bar), (?T: Eq), WF(?T: Eq). |
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.
Hmm, I don't think we want to include WF(Bar)
in this list. That is, I think that WF(Foo<T>)
is just saying if a reference to the type Foo<T>
is well-formed. Not if the struct definition is well-formed -- for the latter, I would expect us to generate some rules like:
StructDefnWf(Foo) :-
for<T> { if (T: Eq) { WF(Bar) } }.
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.
I'm not sure to understand, when would we use WF(Foo<T>)
and ˋStructDefnWF`?
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.
We would never use StructDefnWF
. Rather, StructDefnWF
would be one of the things we must prove to decide if the whole program type-checks. This is basically a modularity thing -- it's the same as saying that if you have some function foo
, we just need its signature to type-check the callers, but then we separately type-check the body of foo
against its signature.
Here, the idea is that, for some struct S, the users of S need not know the types of its fields; they can assume that if the type S is well-formed, then the types of its fields are well-formed.
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.
Yeah ok I see!
src/solve/test.rs
Outdated
@@ -175,7 +175,7 @@ fn prove_forall() { | |||
// Here, we do know that `T: Clone`, so we can. | |||
goal { | |||
forall<T> { | |||
if (T: Clone) { | |||
if (WellFormed(T: Clone), T: Clone) { |
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.
Man. I have to review the plan. I sort of forget why we need the WF's again. ;)
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.
Yeah I was waiting for your review here
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.
Basically given:
trait Clone { }
impl<T> Clone for Vec<T> where T: Clone { }
we need to generate (Vec<T>: Clone) :- (T: Clone), WF(T: Clone)
because of this example:
trait Clone where Self: Marker { }
impl<T> Clone for Vec<T> where T: Clone { }
here we need to have WF(T: Clone)
in order to have Vec<T>: Clone
because WF(T: Clone)
implies that T
correctly implements Marker
whereas this is not the case with the assumption T: Clone
alone
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.
Yes. So I wonder if it's worth having two forms -- e.g., if
and if-raw
or something -- where the former "elaborates" so that T: Clone
is "short" for WF(T: Clone), T: Clone
, just as it would be in an impl. It seems useful to be able to suppress that elaboration, but I imagine that we'll almost always want it to happen, no?
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.
I thought about this indeed, and I'm in favor of adding an if_raw
.
src/solve/test.rs
Outdated
} | ||
|
||
goal { | ||
forall<T, U> { | ||
if (T: Item<Out = U>) { | ||
if (WellFormed(T: Item), T: Item<Out = U>) { |
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.
why is this WF(T: Item)
and not WF(T: Item<Out = U>)
? Because the former suffices?
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.
Here indeed the former suffices. At first I thought we didn't need something like WF(Projection). But now that you say it, there's indeed no way to have something like that:
trait Foo where <Self as Foo>::Item: Eq {
type Item;
}
if (WellFormed(T: Foo<Item = Bar>)) { ... }
Currently, we have to add T: Foo<Item = Bar>
into the environment in order to trigger the normalization, and rely on WF(T: Foo)
. But as a consequence we are also pushing T: Foo
into the environnement (it is implied by T: Foo<Item = Bar>
) so basically we are making a stronger hypothesis that initially wanted.
It may be not so difficult to fix however.
Sorry for delay @scalexm ! Will try to wrap this up ASAP. |
We also need more reverse rules, like: struct S<T> where T: Clone { }
// current:
// WF(S<T>) :- T: Clone, WF(T: Clone)
//
// should add:
// (T: Clone) :- WF(S<T>)
// WF(T: Clone) :- WF(S<T>)
impl<T> Foo for T { }
// current:
// WF(T: Foo) :- WF(T)
//
// should add:
// WF(T) :- WF(T: Foo) |
src/lower/mod.rs
Outdated
.iter() | ||
.cloned() | ||
.flat_map(|wc| wc.expanded(program)) | ||
.map(|wc| wc.cast()) |
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.
apropos of nothing, we should make a .casted()
adapter just to make this cuter :)
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.
Yeah right!
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.
I think that to land this PR we should, at minimum, tweak the handling of struct fields and well-formedness. I'd also like to see a test that we get an error when the user fails to provide the full set of impls (if there is already such a test, my bad, I missed it). Something like this:
trait Foo: Bar { }
trait Bar { }
struct A { }
impl Foo for A { } // NB: No impl of Bar for A
and then a query that WF(A: Foo)
ought to fail, right? (Even though A: Foo
would succeed.) I think this is the basic idea we were going for.
Other things that we could do in this PR, but maybe would be better left for a follow-up:
- Elaborate the clauses in an
if
, which would also eliminate some portion of the diffs. - Consider adding an "unelaborated" form, though we could wait until we need it.
- These would lower to the same thing, presumably, so the difference is just skin-deep.
chalk-parse/src/parser.lalrpop
Outdated
@@ -28,7 +28,8 @@ pub Goal: Box<Goal> = { | |||
Goal1: Box<Goal> = { | |||
"forall" "<" <p:Comma<ParameterKind>> ">" "{" <g:Goal> "}" => Box::new(Goal::ForAll(p, g)), | |||
"exists" "<" <p:Comma<ParameterKind>> ">" "{" <g:Goal> "}" => Box::new(Goal::Exists(p, g)), | |||
"if" "(" <w:Comma<WhereClause>> ")" "{" <g:Goal> "}" => Box::new(Goal::Implies(w, g)), | |||
"if" "(" <w:Comma<WhereClause>> ")" "{" <g:Goal> "}" => Box::new(Goal::Implies(w, g, true)), | |||
"if_raw" "(" <w:Comma<WhereClause>> ")" "{" <g:Goal> "}" => Box::new(Goal::Implies(w, g, false)), |
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.
@nikomatsakis, do you have an idea of how I could factorize these two lines?
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.
not sure you need to, but you could do:
Goal1: Box<Goal> = {
...
<i:IfKeyword> ... => Box::new(Goal::Implies(w, g, i)),
};
IfKeyword: bool = {
"if" => true,
"if_raw" => false,
};
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.
(if that generates conflicts, you can add #[inline]
onto the IfKeyword
, but I can't see why it would)
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.
Great, that's what I was looking for
}); | ||
} | ||
Ok(Box::new(ir::Goal::Implies(where_clauses, g.lower(env)?))) | ||
} |
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.
@nikomatsakis not sure about using ir::with_current_program
, basically it prevents from having user provided higher ranked bounds like where if (I: Iterator<Item = U>) { U: Eq }
(but we don't support them).
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.
hmm -- seems ok for now -- we can revisit at a later point if we ever care, I think.
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.
Really all we need is enough data to know how many parameters are declared on trait vs associated types. I think eventually we are going to have to refactor to support a more general "query-style" lowering, like rustc does, which should enable us to uncover that data without full access to other things. But, as you say, for now we don't support hypotheses in user-defined goals anyhow... (though it seems like we may want that eventually)
58365b7
to
fdcf099
Compare
@@ -130,7 +131,7 @@ impl<T, U> Cast<Vec<U>> for Vec<T> | |||
where T: Cast<U> | |||
{ | |||
fn cast(self) -> Vec<U> { | |||
self.into_iter().map(|v| v.cast()).collect() | |||
self.into_iter().casted().collect() |
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.
Nice :)
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.
looks great; just one comment would be nice
}); | ||
} | ||
Ok(Box::new(ir::Goal::Implies(where_clauses, g.lower(env)?))) | ||
} |
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.
hmm -- seems ok for now -- we can revisit at a later point if we ever care, I think.
Vec<T>: Clone | ||
} | ||
} | ||
} yields { | ||
"Unique; substitution [], lifetime constraints []" | ||
} | ||
|
||
goal { |
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 good to add a comment here that we are explicitly not "knowing" that WF(T: Clone)
, right?
Slice<Int>: Eq<Slice<Int>> | ||
} yields { | ||
"No possible solution" | ||
} | ||
|
||
// not WF because previous equation doesn't hold | ||
// not WF because previous equation doesn't hold, despite Slice<Int> having an impl for Ord<Int> |
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.
👍
Rebased, added comment. |
These commits aim at implementing the additional rules described in #12 for elaborating where clauses
directly in the logic. Basically, each where clause of the form
T: Foo
expands toT: Foo, WF(T: Foo)
and each where clause of the form
T: Foo<Item = U>
expands toT: Foo<Item = U>, T: Foo, WF(T: Foo)
.We also added reverse rules on traits and associated types, and well-formedness checking of struct
parameters and fields.
Also, the
where_clauses
field onAssociatedTyDatum
was removed since this can be emulated by writing:Traits
The rules generated by a trait declaration are:
Structs
The rules generated by a struct declaration are:
Impls
The rules generated by an impl declaration are:
Associated types
The rules generated by an associated type definition are: