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

Fix capability checking for gencap-constrained type parameters. #1593

Merged
merged 4 commits into from
Feb 24, 2017

Conversation

jemc
Copy link
Member

@jemc jemc commented Feb 17, 2017

This PR fixes the type system bug described in #1328.

@jemc jemc force-pushed the fix/gencap-violation branch from 332b4dd to 3cf0fc3 Compare February 17, 2017 07:24
@jemc
Copy link
Member Author

jemc commented Feb 17, 2017

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.

@jemc jemc requested a review from sylvanc February 17, 2017 07:28
@jemc jemc force-pushed the fix/gencap-violation branch from 3cf0fc3 to 63afe88 Compare February 17, 2017 07:29
@Praetonus
Copy link
Member

I haven't followed the investigation made on this bug ; out of curiosity, what was causing the bad type checking?

Copy link
Member Author

@jemc jemc left a 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:
Copy link
Member Author

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.

Copy link
Contributor

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:
Copy link
Member Author

@jemc jemc Feb 17, 2017

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:

  1. #read == {iso, trn, ref, val, box}
  2. and trn is not a subtype of iso
  3. 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?

Copy link
Contributor

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.

Copy link
Contributor

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.

// 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.
Copy link
Member Author

@jemc jemc Feb 17, 2017

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.

Copy link
Contributor

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.

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}
Copy link
Member Author

@jemc jemc Feb 17, 2017

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).

Copy link
Contributor

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.

@jemc
Copy link
Member Author

jemc commented Feb 17, 2017

@Praetonus the core of the issue was a conceptual conflation of how type parameter rcaps are checked in subtype.c. Compared to the normal logic for rcap subtype checking, we need special logic for checking constraint rcaps (implemented in this PR as is_cap_sub_cap_constraint), and we need yet a different kind of special logic for checking varying caps of a type parameter reference against eachother (is_cap_sub_cap_bound).

Previously these two different special case rcap checking rules were conflated as one, leading to the over-permissiveness in the previous logic for is_cap_sub_cap_bound, which you found and demonstrated in your snippet.

This PR separates the different cases into different functions in cap.c, makes sure that each case is following the correct (I hope!) rules, and reworks subtype.c so we can keep track of what kind of check we want to do when we finally get to the point of invoking the functions from cap.c.

@jemc
Copy link
Member Author

jemc commented Feb 17, 2017

As a side note, GitHub made the diff view of cap.c really impossible to read by jumbling all the lines together from different functions, so when reviewing I suggest maybe just looking at the final revision of the file here instead.

@jemc jemc added the changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge label Feb 17, 2017
@@ -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:
Copy link
Contributor

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:
Copy link
Contributor

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.

return true;

default: {}
}
break;

case TK_BOX:
case TK_CAP_READ: // {iso, trn, ref, val, box}
Copy link
Contributor

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 :)

// possible instantiation of super.
switch(super)
{
case TK_CAP_READ: // {iso, trn, ref, val, box}
Copy link
Contributor

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}
Copy link
Contributor

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.


case TK_TRN_BIND:
case TK_CAP_SHARE: // {val, tag}
case TK_CAP_ALIAS: // {ref, val, box, tag}
Copy link
Contributor

Choose a reason for hiding this comment

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

Also TK_CAP_READ.

case TK_CAP_READ:
case TK_CAP_ALIAS:
case TK_CAP_SHARE: // {val, tag}
case TK_CAP_ALIAS: // {ref, val, box, tag}
Copy link
Contributor

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.

return true;

default: {}
}
break;

case TK_CAP_READ:
case TK_CAP_ALIAS:
case TK_CAP_READ: // {iso, trn, ref, val, box}
Copy link
Contributor

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 ?

Copy link
Contributor

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.

Copy link
Member Author

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 it
    • val 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 it
    • box is a supertype of val, 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

Copy link
Contributor

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.

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),
Copy link
Contributor

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^ =>
Copy link
Contributor

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!

@sylvanc
Copy link
Contributor

sylvanc commented Feb 17, 2017

This looks great. Only problem is my bad choice of names for #read lead you to the conclusion that it should be demanding something... readable. But really it wants "readable and aliasable". Sorry about that.

@jemc
Copy link
Member Author

jemc commented Feb 17, 2017

@sylvanc Thanks for the quick review.

Yes, my mistaken understanding of what #read is makes me quite glad I added the comments showing the set of what I thought it was, since it made it easy for you to see the mistake that tripped me up. I will do a commit fixing up the points you noted.

@sylvanc
Copy link
Contributor

sylvanc commented Feb 17, 2017

Yup, the comments served an immediate and highly useful purpose :)

@jemc
Copy link
Member Author

jemc commented Feb 17, 2017

@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 is_cap_sub_cap_bound:

Using the rule: "every instantiation of the super rcap must be a supertype of some instantiation of the sub rcap"

  • #any includes iso
  • iso is a subtype of every rcap
  • therefore, #any is a subtype of every rcap (by the above rule).

Same would be true for #send, which also includes iso

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 #any started failing (successfully violating) again.

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)?

@jemc
Copy link
Member Author

jemc commented Feb 24, 2017

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 is_cap_sub_cap_bound was correct (only for cases where we compare gencap to gencap - more on this later), but the way it was worded ("some instantiation of sub must be a subtype of every instantiation of super") was somewhat ambiguous, in that it could be read to mean either "there exists some instantiation of sub which is a subtype of every instantiation of super", or "for every instantiation of super, there exists some instantiation of sub which is a subtype of it" - the former is how I was reading it, but the latter is the correct interpretation. I believe that my rephrasing ("every instantiation of super must be a supertype of some instantiation of sub") makes the correct interpretation a bit clearer and more obvious.

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 is_cap_sub_cap. This logic is now encoded in the switch/case "truth table" for is_cap_sub_cap_bound.

The PR should be ready for final review, and merging.

@sylvanc sylvanc merged commit 97f119c into master Feb 24, 2017
ponylang-main added a commit that referenced this pull request Feb 24, 2017
@jemc jemc deleted the fix/gencap-violation branch July 6, 2017 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants