-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
[ConstraintSystem][SE-0249] Key Path Expressions as Functions #26054
Conversation
@swift-ci Please smoke test. |
From the smoke test, it looks like that didn't regress after all. |
lib/Sema/CSRanking.cpp
Outdated
@@ -1139,17 +1139,6 @@ SolutionCompareResult ConstraintSystem::compareSolutions( | |||
continue; | |||
} | |||
|
|||
// If one is a function type and the other is a key path, prefer the other. | |||
if (type1->is<FunctionType>() != type2->is<FunctionType>()) { |
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.
How do we avoid needing a scoring rule?
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 change in the implementation of simplifyKeyPathConstraint
avoids creating a disjunction between the two forms now, so there shouldn't ever be two solutions. It leaves a single KeyPathConstraint
until there's enough info so that the simplify can pick either function or KeyPath nominal.
I went this way because the constraint system isn't smart enough / has too much ambiguity to figure out key paths with optional chaining without additional help. Thus the somewhat obscure condition at the end of simplify'ing !anyComponentsUnresolved || (definitelyKeyPathType && capability == ReadOnly)
-- even if we haven't figured out all the key path component overloads, if we can tell that the literal path will result in a KeyPath nominal and that it'll be read-only (as in an optional chain), then we can immediately bind to the correct nominal type (because no possible component overload can make it non-read-only once a component specifies that it is).
That's essentially a replacement for the logic that used to be in CSGen that immediately bound to a type if there was an optional chain involved instead of creating a KeyPathConstraint
at all. The constraint system needs that top-down info in order to figure out the bottom-up overloads for the components.
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.
What happens if you write a key path in a context with no type information at all, like this?
let kp = \A.property
let _: KeyPath<A, Prop> = kp
(There should probably be a test like this if there isn't one already.)
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 there isn't context that definitively asks for function type vs nominal type, then once the components are all resolved, simplification defaults to the nominal type. (And I'll specifically add tests like that in a moment.)
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...could we end up in a situation where we currently don't have any context, but the typechecker would discover more context in the future if we left the constraint unsolved? @xedin, any thoughts?
Woo! Some other recent change must've improved this, nice! |
@swift-ci Please smoke test. |
1 similar comment
@swift-ci Please smoke test. |
@swift-ci Please smoke test Linux. |
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 left a couple of comments inline. But I really want for us to consider a meta-question here - should keypath handling be re-designed in a way where keypath type drives access to the components instead of components determining what access is going to be? That way it should be much easier to diagnose invalid writable calls and avoid creating all of the conversions from l-value result to r-value result types.
While generating constraints check whether there is a contextual type - if so, everything is easy, otherwise start with assumption that it's a WritableKeyPath
generate constraints for components and try to solve (I think it might make sense to introduce <baseType> KeyPathComponent #N <resultType>
), if that fails try read-only keypath and/or function type.
Solving keypath would mean solving all of the associated components so we no longer have to wait for overload choices to be made and re-generate the same constraint or iterate over components multiple times.
lib/Sema/CSSimplify.cpp
Outdated
@@ -5514,6 +5519,26 @@ ConstraintSystem::simplifyKeyPathConstraint(Type keyPathTy, | |||
return SolutionKind::Solved; | |||
}; | |||
|
|||
auto tryMatchRootAndValueFromFunctionType = | |||
[&](FunctionType *fnTy) -> SolutionKind { |
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.
Could you please reuse logic for matching for root/value types between here and tryMatchRootAndValueFromKeyPathType
?
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.
Even better would be to make it so tryMatchRootAndValueFromKeyPathType
accepts a Type
and then figures out whether it's a function or bound generic, or something else. That way we wouldn't need special cases for function type vs. bound generic in multiple places.
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 idea, done!
lib/Sema/CSSimplify.cpp
Outdated
// type variable that's some sort of conversion to a function type, use that | ||
// constraint's second type. | ||
if (keyPathTy->isTypeVariableOrMember()) { | ||
for (auto constraint : getActiveConstraints()) { |
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.
Can we delay solving this constraint until keyPathTy
is property resolved instead of trying to guess what it might be?
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.
No, unfortunately. In a simple function type example like:
struct Struct { var i: Int }
_ = (Array<Struct>()).map(\.i)
The keyPathTy
is convertible to (Struct)->T
, but since T is unknown without figuring out the key path components, and keyPathTy
itself is still just a type variable, the constraint system can't make progress without this code to bind the root and value types, so the result is an error: type of expression is ambiguous without more context
.
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.
Interesting, this sounds like we don't model this "correctly", if there is a conversion constraint associated with keypath in theory we should be able to delay solving keypath constraint
until type for keypath type is attempted. I'm going to pull these changes in and try it out locally to see what is up.
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 have looked at this for a bit tonight and it looks like what we need is special handling in matchTypes
in cases where argument is a type variable representing a keypath e.g.:
@@ -2657,8 +2657,16 @@ ConstraintSystem::matchTypes(Type type1, Type type2, ConstraintKind kind,
case ConstraintKind::Subtype:
case ConstraintKind::Conversion:
case ConstraintKind::ArgumentConversion:
- case ConstraintKind::OperatorArgumentConversion:
+ case ConstraintKind::OperatorArgumentConversion: {
+ if (typeVar1) {
+ if (auto *locator = typeVar1->getImpl().getLocator()) {
+ if (locator->isLastElement(ConstraintLocator::KeyPathType))
+ return matchTypesBindTypeVar(typeVar1, type2, kind, flags, locator,
+ formUnsolvedResult);
+ }
+ }
return formUnsolvedResult();
+ }
This seems to work, although logic at the end of simplifyKeyPathConstraint
which deals with keyPathTy
being a function type has to be adjusted too because it doesn't have to bind function types together anymore if keyPathTy
is already a function type.
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 logic at the end of simplifyKeyPathConstraint
is easy to fix for function types:
if (definitelyFunctionType) {
return SolutionKind::Solved;
The problem then is with KeyPath
bound generic types, which regress in a bunch of tests. This is most easily fixable if you adjust the matchTypes
handling to only work on function types:
if (locator->isLastElement(ConstraintLocator::KeyPathType) && type2->is<AnyFunctionType>())
That sound okay?
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 seems reasonable, sorry I didn't run all of the tests at the time. Do you happen to have an example of how they fail? I'm just curious whether we could fix that without special case in matchTypes
.
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 looked like mostly bad key path type error diagnoses because the solver wasn't left in a state that the diagnostic code was ready for. E.g.:
struct S { let i: Int }
let _ : WritableKeyPath<S, Int> = \.i
The key path tyvar gets bound to WritableKeyPath
from the contextual type because of the matchTypes
change, then the key path constraint gets simplified, sees that i
is a let
and tries to bind (read only) KeyPath
as the type, and the two don't match. The regression is to ambiguous without more context
instead of the targeted diagnosis.
Related:
let _: KeyPath<S, Int>? = \.i
Binds the key path tyvar to an optional directly, instead of resolving the path and being convertible to optional. So a failure to compile something which should work.
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 guess it would be fair to fix cases like that separately, but please leave a comment with TODO(diagnostics):
in relevant place of matchTypes
.
@swift-ci Please smoke test. |
@xedin Long-term, your suggestion for how to rewrite key path constraint generation and simplification would be great, and would probably also make diagnoses for key path errors a lot better. But I think that that future potential improvement is separable from this feature / PR. |
@swift-ci Please smoke test. |
auto closureTy = | ||
FunctionType::get({ FunctionType::Param(baseTy) }, leafTy); | ||
auto closure = new (ctx) | ||
AutoClosureExpr(E, leafTy, discriminator, cs.DC); |
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 not just a regular implicit closure? I believe that by conversion autoclosure is not supposed to have any arguments.
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 problem is the discriminator
. ClosureExpr
s get numbered during parsing, and AutoClosureExpr
s get numbered during a walk after type checking. The parsing context is already gone by the time this code is running, so the only way to correctly set the discriminator
is via an AutoClosureExpr
.
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.
Interesting, and there is no way to change that behavior to allow implicit closures? It seems a bit like a hack to use autoclosure here, because autoclosure is not supposed to have any parameters and used only in @autoclosure
marked positions, so we are kind of stretching its design 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.
Well, the superclass AbstractClosureExpr
is what handles parameters, not either of the subclasses. The main difference between ClosureExpr
and AutoClosureExpr
is that ClosureExpr
is supposed to be for explicit closures parsed from the source code, and AutoClosureExpr
is supposed to be for implicitly created closures around an expression.
So, it's true, this is the only place where AutoClosureExpr
will currently have parameters. But the design of the classes is such that I think we'd be stretching the design much more to try to use ClosureExpr
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.
Maybe it would make sense to add a separate sub-class of AbstractClosureExpr
which deals with general cases of implicit closures? I'm just trying to think through ideas here since AutoClosureExpr
just seems like a specialized use-case to me. Maybe it doesn't really matter that much anyway since the only code which has to deal with that in in SILGen... I can't think of any special cases in CSDiag which deal with expressions like 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.
At the very least comment associated with AutoClosureExpr
has to be updated to cover new use-case.
@swift-ci Please smoke test. |
Fix autoclosure param interface type when it involves archetypes. Had some repeated locals from moving this block of code around - cleaned up. Alternate implementation where KeyPathExpr is essentially a literal type and can be either a KeyPath(R,V) or (R)->V. Some unneccessary code now. Implicit closure pieces need valid sourceLocs in case a coerce expr needs to refer to the beginning of the closure in `\.foo as (A) -> B`. Removed explicit noescape from function type so `let a: (A) -> B = \.foo` is valid. Remove optimization that optional path is always read-only KP type in CSGen, since it can also now be of function type.
Necessary to keep SE-0249 from failing tests.
…uish function type and keypath BGT type without explicit disjunctions.
(Happened when merging latest master into this branch.)
@swift-ci Please smoke test. |
@swift-ci Please smoke 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.
LGTM! @gregomni thank you for sticking with this!
@swift-ci please test source compatibility |
@gregomni Do you mind adding this to changelog under Swift 5.2 and update the proposal as "Implemented" on Swift evolution repo? |
This builds on @brentdax's work in #23435 to handle optional chaining key paths as both KeyPath BGTs and as function types, and also does away with explicit disjunctions to determine which kind of type to turn the key path literal into, so ought to be more performant.
There is a single test diagnosis that gets worse here (a contextual type which matches root/value but specifies an incorrect writability is now ambiguous because the diagnostic path goes into the old CSDiag stuff, and the first step is to discard contextual type, and then the remainder is entirely ambiguous). The solution is future work to improve key path diagnoses in general, I think.