-
-
Notifications
You must be signed in to change notification settings - Fork 418
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
Fix capability checking for gencap-constrained type parameters. #1593
Conversation
332b4dd
to
3cf0fc3
Compare
This is a pretty important bug fix, since the compiler is currently allowing some very unsafe forms when using generics. Due to that, it would be nice to squeeze this into the 0.11.0 release (#1543) if possible. However, I do want @sylvanc to review this first to make sure that my type system choices here were sound from a formal perspective. |
3cf0fc3
to
63afe88
Compare
I haven't followed the investigation made on this bug ; out of curiosity, what was causing the bad type checking? |
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.
@sylvanc here are some specific places where I request your scrutiny.
switch(sub) | ||
{ | ||
case TK_ISO: | ||
switch(super) | ||
{ | ||
case TK_ISO: |
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 case was already handled by the if (sub == super) return true
check above.
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.
Yup.
@@ -121,11 +101,8 @@ bool is_cap_sub_cap(token_id sub, token_id subalias, token_id super, | |||
case TK_REF: | |||
case TK_VAL: | |||
case TK_BOX: | |||
case TK_CAP_READ: |
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.
@sylvanc Note that I removed this case here:
#read == {iso, trn, ref, val, box}
- and
trn
is not a subtype ofiso
- therefore, using the rule written in the comment above that "every possible instantiation of sub must be a subtype of every possible instantiation of super",
trn
is not a subtype of#read
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.
Your logic is excellent, but no :) #read = {ref, val, box}
. This is because iso
and trn
, while they can be read from, cannot be aliased as the same capability, which is the guarantee that #read
demands.
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.
Nevertheless, iso
is a subtype of #read
, which is what's being checked here, so your code is correct.
src/libponyc/type/cap.c
Outdated
// Sub and super share the same initial bounds. Some instantiation of the | ||
// sub rcap must be a subtype of every instantiation of the super rcap. | ||
// Sub and super share the same initial bounds. Every instantiation of the | ||
// super rcap must be a supertype of some instantiation of the sub rcap. |
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.
Note the subtle difference in the description here, which actually does change the semantics.
This distinction was actually the bulk of the bug, in my opinion, so please check my logic here carefully, as this is a part of the fix that I discovered on my own and didn't discuss with you in our last conversation about this issue.
I basically rewrote the entire switch-case from scratch here. It is very similar to the semantics of is_cap_sub_cap
, except some extra cases are allowed when comparing gen-caps to gen-caps.
This is to be expected given the semantics I chose in this description, because the only real distinction from is_cap_sub_cap
is the use of "some" instead of "every" in reference to the instantiations of the sub rcap, and this is a distinction without a difference when there is only one possible instantiation of the sub rcap.
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, exactly! And this only appears when checking if a typeparamref is a subtype of itself, given some (possibly different) cap.
src/libponyc/type/cap.c
Outdated
case TK_CAP_SHARE: // {val, tag} | ||
case TK_CAP_SEND: // {iso, val, tag} | ||
case TK_CAP_ALIAS: // {ref, val, box, tag} | ||
case TK_CAP_ANY: // {iso, trn, ref, val, box, tag} |
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.
As you can see, I've added comments for all the gencaps to show their expansion inline. I found this really helped me to keep my thinking straight when double-checking all the cases, including the case where I think I found a mistaken return true
for the case of trn <: #read
(see my next comment).
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.
Comments for those are great.
@Praetonus the core of the issue was a conceptual conflation of how type parameter rcaps are checked in Previously these two different special case rcap checking rules were conflated as one, leading to the over-permissiveness in the previous logic for This PR separates the different cases into different functions in |
As a side note, GitHub made the diff view of |
@@ -121,11 +101,8 @@ bool is_cap_sub_cap(token_id sub, token_id subalias, token_id super, | |||
case TK_REF: | |||
case TK_VAL: | |||
case TK_BOX: | |||
case TK_CAP_READ: |
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.
Your logic is excellent, but no :) #read = {ref, val, box}
. This is because iso
and trn
, while they can be read from, cannot be aliased as the same capability, which is the guarantee that #read
demands.
@@ -121,11 +101,8 @@ bool is_cap_sub_cap(token_id sub, token_id subalias, token_id super, | |||
case TK_REF: | |||
case TK_VAL: | |||
case TK_BOX: | |||
case TK_CAP_READ: |
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.
Nevertheless, iso
is a subtype of #read
, which is what's being checked here, so your code is correct.
src/libponyc/type/cap.c
Outdated
return true; | ||
|
||
default: {} | ||
} | ||
break; | ||
|
||
case TK_BOX: | ||
case TK_CAP_READ: // {iso, trn, ref, val, box} |
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.
#read = {ref, val, box}
. The code is correct, but the comment isn't :)
src/libponyc/type/cap.c
Outdated
// possible instantiation of super. | ||
switch(super) | ||
{ | ||
case TK_CAP_READ: // {iso, trn, ref, val, box} |
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 one is wrong. Only ref, val, box
should be valid as #read
constraints.
return true; | ||
|
||
default: {} | ||
} | ||
break; | ||
|
||
case TK_CAP_ALIAS: | ||
switch(super) | ||
case TK_CAP_ALIAS: // {ref, val, box, tag} |
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.
Also valid: TK_CAP_SHARE, TK_CAP_READ
.
src/libponyc/type/cap.c
Outdated
|
||
case TK_TRN_BIND: | ||
case TK_CAP_SHARE: // {val, tag} | ||
case TK_CAP_ALIAS: // {ref, val, box, tag} |
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.
Also TK_CAP_READ
.
src/libponyc/type/cap.c
Outdated
case TK_CAP_READ: | ||
case TK_CAP_ALIAS: | ||
case TK_CAP_SHARE: // {val, tag} | ||
case TK_CAP_ALIAS: // {ref, val, box, tag} |
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.
TK_TRN
is also in this case a subtype of TK_CAP_READ
.
src/libponyc/type/cap.c
Outdated
return true; | ||
|
||
default: {} | ||
} | ||
break; | ||
|
||
case TK_CAP_READ: | ||
case TK_CAP_ALIAS: | ||
case TK_CAP_READ: // {iso, trn, ref, val, box} |
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.
#read = {ref, val, box}
. So also a subtype of TK_CAP_ALIAS
and TK_CAP_SHARE
?
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.
Actually, hmm, not so sure.
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 on both counts.
Using the rule: "every instantiation of the super rcap must be a supertype of some instantiation of the sub rcap"
-
#share == {val, tag}
tag
is a supertype of everything, so you can pick any instantiation of#read
and it's supertype of itval
is a supertype of itself, which is included in#read
-
#alias == {ref, val, box, tag}
tag
is a supertype of everything, so you can pick any instantiation of#read
and it's supertype of itbox
is a supertype ofval
, which is included in#read
val
is a supertype of itself, which is included in#read
ref
is a supertype of itself, which is included in#read
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.
Ah ha! Yes. You're right.
src/libponyc/type/subtype.c
Outdated
ast_t* sub_cap = cap_fetch(sub); | ||
ast_t* sub_eph = ast_sibling(sub_cap); | ||
ast_t* super_cap = cap_fetch(super); | ||
ast_t* super_eph = ast_sibling(super_cap); | ||
|
||
if(!is_cap_sub_cap(ast_id(sub_cap), ast_id(sub_eph), | ||
ast_id(super_cap), ast_id(super_eph))) | ||
if(check_cap == CHECK_CAP_SUB && !is_cap_sub_cap(ast_id(sub_cap), |
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.
Should these clauses be in a switch statement on check_cap
?
@@ -84,7 +84,7 @@ class Iter[A] is Iterator[A] | |||
end | |||
false | |||
|
|||
fun ref collect[B: Seq[A!] = Array[A!]](coll: B): B^ => | |||
fun ref collect[B: Seq[A!] ref = Array[A!]](coll: B): B^ => |
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 love that this bug is caught by this PR!
This looks great. Only problem is my bad choice of names for |
@sylvanc Thanks for the quick review. Yes, my mistaken understanding of what |
Yup, the comments served an immediate and highly useful purpose :) |
@sylvanc I made the fixes you mentioned in 36e032d. However, I'm now a bit uneasy about this, because I observed the following about this rule for Using the rule: "every instantiation of the super rcap must be a supertype of some instantiation of the sub rcap"
Same would be true for I don't think this is desirable though, because it leads to the allowing the very same unsafe cases that we are intending to disallow in this PR. In fact, when I tried adding that clause, my test case for violating capabilities via This leads me to believe that the rule is still wrong. Can you do some more thinking about this, (I trust your thinking more than mine on disentangling these ideas)? |
So, I've revised this branch based on lengthy discussion in this week's sync call in which we finished ironing out the logic together. The original rule for However, for cases where either sub or super is a single/specific cap, we want to abandon the above rule and fall back to the original rule for The PR should be ready for final review, and merging. |
This PR fixes the type system bug described in #1328.