-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[SE-0268] [Typechecker] Refine didSet semantics #26632
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
cc @jckarter @rjmccall @DougGregor can you please review? I am not sure if this requires an evolution proposal or not, but I am happy to create a proposal if needed! Thank you. |
This is a semantics-breaking change, and a really subtle one when it comes to |
I'm also concerned with the impact this will have on the quality of debug info. If we disappear this parameter at Sema-time we won't be able to recover it later, even if we wanted to. |
Sure, I can create a proposal and pitch it, but need I confirmation from Core Team before I do it. We could also version-gate it so it only works for Swift 5.2/6/whatever or 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.
I would definitely bring this up in the forums. The core team might decide the breakage is acceptable. After all, a getter should ideally not have side effects or depend on evaluation order.
With lazy, you can argue that not forcing the value to be computed before calling the setter is a feature. And yes, it might change observed behavior, but I would expect this to happen very rarely if at all in real code.
I have created a pitch thread here with a draft proposal: https://forums.swift.org/t/skipping-call-to-getter-when-oldvalue-is-not-used-in-didset/27858 John says it does need to go through evolution but will likely be accepted, so I'll run it through the process. |
I wouldn't go so far as "will likely be accepted"; I'm just saying it seems viable. |
Just rebased on top of master because lots of conflicts started to creep in. EDIT: Oh well, all the requestification that's going on keeps introducing new conflicts 😅 |
if (isLValue && | ||
(storageReadWriteImpl == ReadWriteImplKind::StoredWithSimpleDidSet || | ||
storageReadWriteImpl == ReadWriteImplKind::InheritedWithSimpleDidSet)) { | ||
return synthesizeModifyCoroutineBodyWithSimpleDidSet(accessor, ctx); |
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.
@theblixguy Should we be checking for property wrappers / @objc
, and avoid using synthesizeModifyCoroutineBodyWithSimpleDidSet()
in those cases?
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 think we'd need to suppress it for @objc
properties. I am not sure about property wrappers though.
@rjmccall Do you mind taking a look at the |
How about this -- the oldValue parameter is always present, but it has an Optional type. References to oldValue inside the body are implicitly unwrapped. A post-pass (or request) that runs after body type checking looks for references to oldValue in the type checked body. If there are none, set a flag (or the result of the request) on the AccessorDecl noting that. Then when you synthesize the body of the setter (where didSet gets called), pass in .none instead of loading the old value if that flag in the accessor is set (if computation of this flag is a request, this is where it would be kicked off. Synthesizing the body of the setter is done sufficiently "late" (in SILGen) that you can kick off/depend on the type checking to have completed on the didSet body. What do you think? |
Hmm. I want to lay out the longer-term vision here, and then we can decide what's reasonable to do in the short term for this PR. Right now, I think |
Slava's optional-parameter idea is interesting, but while it avoids the circularity problem, it also introduces a lot of special cases and might be hard to root out in the long term. And the circularity problem seems directly solvable: as Slava says, we need to be figuring this out as part of computing the interface type of the observer. Normally, that would cause really bad circularity problems, but there should never be any direct uses of the observer that would need its interface type. |
Hmm, okay. The interface type for (1) delay I tested out (2) (see defeca6) and it seems to work fine. What do you think of this approach? |
Sorry for the ping, but it’s been a while... do you have any thoughts on above? I would like to get this wrapped up and merged soon. |
(2) seems reasonable to me, but I'd like Slava's opinion. |
(2) sounds fine. Can you fix the merge conflicts and we'll look over this PR one more time? |
@slavapestov Done! |
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 very nice! I have one small bundle of changes requested, but otherwise I'd this is ready to go in. Adding new requests to the evaluator happens all the time, so once you've got this tweaked and rebased let's test & merge before someone else gets something in that conflicts.
@swift-ci please test |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Hmm, interesting SILVerifier crash:
|
Reproducer: open class Foo {
open var bar: Int = 1 {
didSet {}
}
} Same with |
Hmm, seems like in above code the |
I suspect that |
Changing the modify to not be transparent in this case makes sense, yeah. Alternatively, you could have modify call the setter instead of accessing the stored property directly, but I guess that's not as efficient. |
You're absolutely right, there's even a comment in @swift-ci please test |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
There's a few other failing tests because we have a lot of tests that use didSet
without oldValue
(so SIL output has some changes for example) and there was one crasher somewhere in SILGen - I am gonna take a look at it now. I need to run tests again because I can't access the old logs.
In the meantime, I made changes to two tests but I am not too sure if its correct. Can someone take a look?
@swift-ci please test |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@swift-ci please test |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@swift-ci please test |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@swift-ci please test |
@swift-ci please test source compatibility |
At the moment, we load and pass the
oldValue
todidSet
when it's called, even if we do not reference theoldValue
inside the body of thedidSet
. This means we end up calling the getter to do redundant work and prevent certain code from working (such as aDelayed
/LateInitialized
property wrapper).This PR introduces two changes:
didSet
does not reference theoldValue
in its body, then the call to fetch theoldValue
will be skipped. We refer to this as a "simple"didSet
.didSet
and nowillSet
, then we could allow for modifications to happen in-place.Resolves SR-11297, SR-11280 and SR-5982
Resolves rdar://problem/54133764
Resolves rdar://problem/29733553
Resolves FB6975769
Swift Evolution thread: https://forums.swift.org/t/skipping-call-to-getter-when-oldvalue-is-not-used-in-didset/27858