-
Notifications
You must be signed in to change notification settings - Fork 285
Allow redeclaring trait methods #6280
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
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.
SO excited for this, but still exercising caution
|
|
||
| trait T { | ||
| method foo() returns (r: int) { | ||
| return 3; |
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 would add ensures r >= 3, ensures r >= 5, etc to all the foo methods and a few precise assertions in Main that depend on the concrete types, just to show that verification is aware of the overriding and not just allowing them to be declared but ignoring 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.
just to show that verification is aware of the overriding and not just allowing them to be declared but ignoring them.
Overriding bodyless members and narrowing the contract was already allowed and is already tested. This test focusses on the bodies and for that it needs to run the code and inspect the result, which it does. I've renamed the test to redeclare_body.dfy
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 still think testing that the contract narrowing works when there's overriding is worth it from an opaque testing coverage POV, but if you think the coverage is adequate given how this is all implemented I won't block on 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.
I still think testing that the contract narrowing works when there's overriding
You're right. I'll add it
| $"To silence this warning, please add an {{:axiom}} attribute or use the option '--allow-external-contracts'"); | ||
| } | ||
| if (!AutoGeneratedOrigin.Is(Origin) && HasVerifyFalseAttribute) { | ||
| if (!AutoGeneratedOrigin.Is(Origin) && HasVerifyFalseAttribute && !resolver.Options.Get(CommonOptionBag.AllowAxioms)) { |
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.
Still blocking on this as per #6279, but AFAICT you can remove those changes from this PR?
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.
Removed it
| GeneralNewtypeMemberResolution.dfy(103,14): Error: field 'Items' is inherited from type 'map' and is not allowed to be re-declared in newtype 'Map' | ||
| GeneralNewtypeMemberResolution.dfy(107,14): Error: field 'Keys' is inherited from type 'imap' and is not allowed to be re-declared in newtype 'IMap' | ||
| GeneralNewtypeMemberResolution.dfy(108,14): Error: field 'Values' is inherited from type 'imap' and is not allowed to be re-declared in newtype 'IMap' | ||
| GeneralNewtypeMemberResolution.dfy(51,13): Error: fully defined function 'F' is inherited from trait 'Bool' and is not allowed to be re-declared |
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.
These are regressions, the error message is referring to Bool as if it's a trait, but it's actually a newtype. Probably there's another place this is checked past the part you changed that just needs tweaking?
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.
Thanks, fixed it.
| if (trait is not TraitDecl) { | ||
| reporter.Error(MessageSource.Resolver, member.Origin, | ||
| $"{traitMember.WhatKindAndName} is inherited from {trait.WhatKindAndName} and is not allowed to be re-declared in {cl.WhatKindAndName}"); | ||
| } else if (traitMember.IsStatic) { |
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.
Awesome that this seems to be all that's needed, but I think we're in Chesterton's Fence territory and I want to hear from @RustanLeino that there isn't some secret soundness reasons for this we aren't seeing. :)
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 you could argue that Dafny should not allow providing a body for the same member twice, since both bodies must satisfy the contract so having two is redundant. That said, I think putting unspecified behavior in the body, like logging, is a valid use-case so it should be allowed.
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.
It's even more important for compiled code, because you can provide a body that's semantically equivalent but much more efficient! IOW it should be allowed for the same reason we have function-by-method :) I'm really looking forward to declaring these kinds of default implementations once in the traits instead of copying them over and over: https://github.com/dafny-lang/dafny/blob/master/Source/DafnyStandardLibraries/src/Std/Actions/Producers.dfy#L878-L910
This is a fairly plausible motivation for why it wasn't allowed historically though, especially if traits were added before the first compiler.
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.
Awesome that this seems to be all that's needed, but I think we're in Chesterton's Fence territory and I want to hear from @RustanLeino that there isn't some secret soundness reasons for this we aren't seeing. :)
Prefer that we merge and revert if there's a problem. If someone added this restriction to avoid a soundness bug, then they should have added a test that checks whether the soundness bug is there, but there is not such test (since all the tests are passing).
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 absolutely agree that's the optimal approach and would like to think that's what either of us would have done :) but I don't want to trust that whoever set this up in years past would have.
How about keeping this behind an internal flag for the near future, so we can use this functionality right away but buy ourselves some time before we need to feel confident on soundness for general use?
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 are some additional tests to add.
Refinement checks
Robin had suggests adding some tests that make sure the right specs are used. Good that you've added such. In addition, please add tests that ensure that refinement tests are still being done (that is, to make sure the verifier doesn't skip those when it sees the base method already has a body).
Termination
Make sure that virtually dispatched calls are properly checked for termination
trait T extends object {
method M() {
if this is C {
var c := this as C;
c.M(); // error: cannot prove termination
}
}
method N() {
}
}
class C extends T {
method M() {
var t := this as T;
t.M(); // error: cannot prove termination
}
method N() {
var t := this as T;
t.N(); // error: cannot prove termination
}
}Diamond property
The following should not be allowed:
trait W { method M() { } }
trait X extends W { method M() { } }
trait Y extends W { method M() { } }
trait Z extends X, Y { } // error: Z inherits M with a body in two different waysDafny already gives an error in Z if W does not give a body for M. Please add a test that makes sure this error is reported also if W does give a body for M. In fact, an error should also be reported if Z also re-declares M with a body.
| } | ||
|
|
||
| trait T2 extends T { | ||
| method foo() returns (r: 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.
Rename foo Foo here and throughout to adhere to standard Dafny naming conventions.
| var t: T := t2; | ||
| var rt := t.foo(); | ||
| assert rt >= 3; | ||
| print rc, ", ", rd, ", ", re, ", ", rf, ", ", rt, ", ", rt2; |
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 good to have some tests that cause verification failures, lest all of the assertions above pass because of an accidental contradiction in the generated Boogie.
|
|
||
| var traitMember = member.OverriddenMember; | ||
| var trait = traitMember.EnclosingClass; | ||
| if (trait is not TraitDecl) { |
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 removal of this test is a regression. It used to trigger the error GeneralNewtypeMemberResolution.dfy(48,11), which is now missing.
| @@ -1,13 +1,12 @@ | |||
| GeneralNewtypeMemberResolution.dfy(6,8): Error: mutable fields are allowed only in reference types (consider declaring the field as a 'const') | |||
| GeneralNewtypeMemberResolution.dfy(48,11): Error: method 'PrintMe' is inherited from newtype 'Bool' and is not allowed to be re-declared in newtype 'BoolBool' | |||
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 removal of the branch trait is not TraitDecl in ModuleResolver.cs is what's causing this error message to disappear. The test is needed.
| var rd := d.foo(); | ||
| var e := new E; | ||
| var re := e.foo(); | ||
| var f := new F; |
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 would add : F as well, just in case type inference gets the crazy idea to make the type of f T2 as well.
| return 6; | ||
| } | ||
| } | ||
|
|
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 add a datatype G extends T = ... to the tests.
- windows-2019 is deprecated by GitHub Actions - Updated all workflow files to use windows-2022 - Issue dafny-lang#1906 that previously blocked windows-2022 is now closed - Files updated: - .github/workflows/release-downloads-nuget.yml - .github/workflows/release-downloads.yml - .github/workflows/xunit-tests-reusable.yml - .github/workflows/integration-tests-reusable.yml
Chores don't warrant news items as they have no user-visible impact
- Scripts/package.py also referenced windows-2019 for release packaging - Updated to windows-2022 for consistency with CI workflows - Ensures release packages are built with current Windows runner
Co-authored-by: Mikaël Mayer <[email protected]>
e908e3d to
3d7b4e3
Compare
| trait Common { method M() { } } | ||
| trait Left extends Common { method M() { } } | ||
| trait Right extends Common { method M() { } } | ||
| trait Both extends Left, Right { } // error: Z inherits M with a body in two different ways |
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.
Minor: no Z in the actual test case
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.
Minor: snake case file names look out of place to me (but open to being convinced it's better for some reason)
docs/dev/news/6280.feat
Outdated
| @@ -0,0 +1 @@ | |||
| Dafny classes and traits can now redeclare members defined by traits they inherit from. No newline at end of file | |||
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 be only methods and not all members, 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.
Good point
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.
Is there a case where verification of a call only succeeds because a class overrides a trait method and provides a stronger specification? I thought there was one in an earlier version I don't see now.
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.
Added a 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.
🚀
|
Thanks, @keyboardDrummer and @robin-aws! |
What was changed?
How has this been tested?
overrides.dfyand updated existing testsBy submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.