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

[ConstraintSystem] Allow function-function conversions for keypath literals #39612

Merged
merged 1 commit into from
Feb 3, 2024

Conversation

Jumhyn
Copy link
Member

@Jumhyn Jumhyn commented Oct 6, 2021

Currently the keypath-to-function conversion is extremely narrow—root and value types of the keypath must exactly match the function argument and result types of the function, respectively.

This PR allows the keypath-to-function conversion to partake in the full generality of function-function type conversion. This means a keypath literal can be covariant in result type and contravariant in argument type, etc.

@Jumhyn Jumhyn force-pushed the keypath-function-conversion branch from d6eb734 to 69ffb18 Compare October 7, 2021 15:05
@Jumhyn Jumhyn changed the title [WIP][ConstraintSystem] Allow function-function conversions for keypath literals [ConstraintSystem] Allow function-function conversions for keypath literals Oct 8, 2021
@MaxDesiatov
Copy link
Contributor

@swift-ci please smoke test

@Jumhyn Jumhyn force-pushed the keypath-function-conversion branch from 69ffb18 to 5ac32fe Compare October 8, 2021 20:58
@Jumhyn
Copy link
Member Author

Jumhyn commented Oct 8, 2021

@swift-ci please smoke test

1 similar comment
@MaxDesiatov

This comment has been minimized.

@xedin
Copy link
Contributor

xedin commented Oct 15, 2021

@Jumhyn Is this ready for review or just a prototype?

@Jumhyn
Copy link
Member Author

Jumhyn commented Oct 15, 2021

@xedin It still has some kinks to be worked out so it’s not ready for a detailed review just yet, but if you have any thoughts about the high level approach here I’d love to hear them.

@xedin
Copy link
Contributor

xedin commented Oct 15, 2021

Sounds good, just making sure that we don't drop a ball on this one. Please feel free to assign me and/or Holly as a reviewer once it's ready.

@Jumhyn Jumhyn force-pushed the keypath-function-conversion branch from 5ac32fe to 0e8cfe5 Compare February 8, 2022 04:18
@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 8, 2022

@swift-ci please smoke test

Comment on lines 1212 to 1211
// FIXME: This error text is bogus due to KeyPath base covariance.
let _: (Base?) -> Base = \Base.base // expected-error {{value of optional type 'Optional<Base>' must be unwrapped to refer to member 'base' of wrapped base type 'Base'}} expected-note {{use unwrapped type 'Base' as key path root}} {{33-37=Base}}
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: we get the same error without the function conversion, e.g.,

let _: KeyPath<Base?, Base> = \Base.base

Filed here.

@Jumhyn Jumhyn requested review from hborla and xedin February 9, 2022 02:42
@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 9, 2022

Hey @hborla @xedin I finally had a chance to iron out the last details here, this should be ready for review!

@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 9, 2022

@swift-ci please test

@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 9, 2022

@swift-ci please test source compatibility

@swift-ci
Copy link
Contributor

swift-ci commented Feb 9, 2022

Build failed
Swift Test OS X Platform
Git Sha - 0e8cfe5aa72e5b5353fe35d651ec48ef57229a6c

@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 9, 2022

@swift-ci please test macOS

@Jumhyn Jumhyn added the swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review label Feb 9, 2022
@swift-ci
Copy link
Contributor

swift-ci commented Feb 9, 2022

Build failed
Swift Test OS X Platform
Git Sha - 0e8cfe5aa72e5b5353fe35d651ec48ef57229a6c

@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 14, 2022

@swift-ci please test macOS

@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 16, 2022

@swift-ci please test Linux

@Jumhyn
Copy link
Member Author

Jumhyn commented Feb 16, 2022

@swift-ci please test linux

@xedin
Copy link
Contributor

xedin commented Feb 17, 2022

@Jumhyn This is in my queue, will try to take a look before the end of this week.

Copy link
Contributor

@xedin xedin left a comment

Choose a reason for hiding this comment

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

LGTM! I have left a couple of comments inline but nothing major.

lib/Sema/CSApply.cpp Outdated Show resolved Hide resolved
lib/Sema/CSApply.cpp Outdated Show resolved Hide resolved
lib/Sema/CSSimplify.cpp Show resolved Hide resolved
@xedin
Copy link
Contributor

xedin commented Feb 22, 2022

@swift-ci please test source compatibility

1 similar comment
@xedin
Copy link
Contributor

xedin commented Mar 18, 2022

@swift-ci please test source compatibility

@Jumhyn Jumhyn force-pushed the keypath-function-conversion branch from 970aed5 to 01cd88c Compare October 11, 2023 14:39
@Jumhyn
Copy link
Member Author

Jumhyn commented Oct 11, 2023

@swift-ci please test

Comment on lines 12558 to 12559
} else if ((!flags.contains(TMF_GenerateConstraints) &&
!anyComponentsUnresolved) ||
Copy link
Member Author

Choose a reason for hiding this comment

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

@xedin one other thing for you to look at when you have a chance to check on this PR—I originally thought your recent changes to keypath simplification would remove the need for this, but we were still failing to properly convert the paths in the testMinimalKeypaths() test below.

Since those keypaths don't have anything to formally 'resolve' we were hitting this path during constraint generation before the proper disjunction/applicable function constraints had even been added to the system.

Also now probably makes sense to rename anyComponentsUnresolved since it's become extremely narrow, basically only used for code completion... I'll update to rename that soon.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to figure out a better way here. Maybe, temporarily, it would be okay to just check

 if (Phase == ConstraintSystemPhase::ConstraintGeneration)
    return formUnsolved();

at the very start of this method.

But what @amritpan and I worked toward this summer (but couldn't quite land it yet) is moving key path "opening" into the resolveKeyPath and allowing it to happen only when there is either a contextual type or it's known that there wouldn't be one, this way we don't actually have to worry about eagerly resolving key path literals.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm how could we conclusively determine whether we'll be able to infer a type before we actually attempt solving? Right now, this is the only place we will 'default' to an actual KeyPath type if no other context is available, so if we move this to resolveKeyPath it seems like we'd need 100% accuracy, otherwise we risk either 1) binding to KeyPath too early when we should have inferred a function type somewhere along the way, or else 2) failing to bind to KeyPath because we were unsure, but then down the line failing to solve the system because we needed the KeyPath context to come from the literal...

Copy link
Member Author

Choose a reason for hiding this comment

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

Also—what is the significance of using flags vs. Phase here? I guess flags could be set for constraint generation even if we are not actually walking an expression to set the Phase?

Copy link
Contributor

@xedin xedin Oct 12, 2023

Choose a reason for hiding this comment

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

Hmm how could we conclusively determine whether we'll be able to infer a type before we actually attempt solving?

Only inference can tell us conclusively in certain conditions that's why resolveKeyPath is going to be called on that path (as in in matchTypesBindTypeVar just like we do for closures). We should only let simplifyKeyPathConstraint set the type if there is definitely contextual type we could use, once we have that we can remove this change to iterate over constraints I added as well.

Also—what is the significance of using flags vs. Phase here?

Phase is a safer bet because it identifies explicitly what phase constraint system is currently in, TMF_GenerateConstraints could technically be set randomly and not as narrow.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah I was confusing resolveKeyPath and resolveKeyPathExpr from PreCheckExpr 🤦 This makes more sense!

Is there a WIP branch anywhere? I'll make the stopgap change in this PR but would be interested in taking a look that the proper fix next unless @amritpan you still had plans to tackle that?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think @amritpan opened a PR for the work she was doing because it wasn't complete but I guess it would be reasonable to open a draft PR?

Copy link
Member

@amritpan amritpan Oct 30, 2023

Choose a reason for hiding this comment

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

unless @amritpan you still had plans to tackle that

Here's a draft PR of some of the resolveKeyPath expansion that I worked on with @xedin, in case it helps.

if (isKnownKeyPathType(lhs) && isKnownKeyPathType(rhs)) {
// If we have keypath capabilities for both sides and one of the bases
// is unresolved, it is too early to record fix.
if (hasConversionOrRestriction(ConversionRestrictionKind::DeepEquality))
Copy link
Contributor

Choose a reason for hiding this comment

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

The possible failure modes are:

  • key path types mismatch on capability - KeyPath vs. WritableKeyPath for example
  • one of the generic parameters (either Root or Value) mismatch:
    • If there is any conversion recorded against this match we should let that happen (not just deep equality)
    • otherwise we could either just record whole types or try to deep deeper and recursively call repairFailures on Root and Value.

Copy link
Member Author

Choose a reason for hiding this comment

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

@xedin This bit I had just pulled up from lines 5296-5305 below for reuse. Happy to update it but not sure I totally understand the restricted conversion machinery. From your bullets it sounds like you're saying we should attempt the fix unconditionally if we have a capability mismatch, but if they agree on capability then we should only attempt the fix if we have no restrictions, does that sound right? So that would amount to updating the check to something like the following:

Suggested change
if (hasConversionOrRestriction(ConversionRestrictionKind::DeepEquality))
if (lhs->getAnyGeneric() == rhs->getAnyGeneric() && llvm::any_of(conversionsOrFixes,
[](const RestrictionOrFix &correction) {
return bool(correction.getRestriction());
}))

(seems like this may be the best way to check keypath capability from just a Type instance, unless I'm missing something?)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say:

if (lhs->getAnyGeneric() != rhs->getAnyGeneric() || llvm::any_of(conversionsOrFixes, ...))

Because if key paths have different capatibilities we don't want to diagnose it here, same goes for restrictions - each restriction would diagnose separately when applied.

Unfortunate consequence of the current state of things is that every key path conversion anchored on KeyPathExpr ends up forming a disjunction where second choice has a fix and we'd really match try to avoid that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, it appears there are some circumstances where this is the only place we end up applying a fix for mismatched capabilities, e.g.:

func issue_65965() {
  struct S {
	  var s: String
	  let v: String
  }
	
  let refKP: ReferenceWritableKeyPath<S, String>
  refKP = \.s
  // expected-error@-1 {{key path value type 'WritableKeyPath<S, String>' cannot be converted to contextual type 'ReferenceWritableKeyPath<S, String>'}}
}

If we don't apply the fix here then we end up with the general 'type of expression is ambiguous without type annotation' fallback error as opposed to the specific error. Where would you have expected this to get diagnosed if not here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks like what happens is that 1) we infer ReferenceWritableKeyPath as the type for the literal via the assignment operator, and then in simplifyKeyPathConstraint attempts to bind to that type rather than convert, so we never have a chance to apply conversion restriction, and 2) even if we change things to allow conversion here since the mismatch ends up happening directly at the keypath constraint we don't even qualify for applying the fix in simplifyRestrictedConstraint:

if (loc->isForAssignment() || loc->isForCoercion() ||
        loc->isForContextualType() ||
        loc->isLastElement<LocatorPathElt::ApplyArgToParam>() ||
        loc->isForOptionalTry()) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, we can leave capability out of this for now, I think the only way to fix it would be to always let simplifyKeyPathConstraint assign a contextual type instead of eagerly binding it, that way it'd always end up with a mismatch in the right place.

Comment on lines 12558 to 12559
} else if ((!flags.contains(TMF_GenerateConstraints) &&
!anyComponentsUnresolved) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to figure out a better way here. Maybe, temporarily, it would be okay to just check

 if (Phase == ConstraintSystemPhase::ConstraintGeneration)
    return formUnsolved();

at the very start of this method.

But what @amritpan and I worked toward this summer (but couldn't quite land it yet) is moving key path "opening" into the resolveKeyPath and allowing it to happen only when there is either a contextual type or it's known that there wouldn't be one, this way we don't actually have to worry about eagerly resolving key path literals.

unittests/Sema/KeypathFunctionConversionTests.cpp Outdated Show resolved Hide resolved
unittests/Sema/KeypathFunctionConversionTests.cpp Outdated Show resolved Hide resolved
unittests/Sema/KeypathFunctionConversionTests.cpp Outdated Show resolved Hide resolved
unittests/Sema/KeypathFunctionConversionTests.cpp Outdated Show resolved Hide resolved
unittests/Sema/KeypathFunctionConversionTests.cpp Outdated Show resolved Hide resolved
lib/Sema/CSSimplify.cpp Outdated Show resolved Hide resolved
@Jumhyn
Copy link
Member Author

Jumhyn commented Dec 12, 2023

@swift-ci please test

@Jumhyn
Copy link
Member Author

Jumhyn commented Dec 14, 2023

@swift-ci please test source compatibility

@rjmccall
Copy link
Contributor

@swift-ci Please build toolchain

@Jumhyn
Copy link
Member Author

Jumhyn commented Dec 14, 2023

@swift-ci please test compiler performance

@Jumhyn
Copy link
Member Author

Jumhyn commented Dec 14, 2023

@swift-ci please test Linux platform

@Jumhyn Jumhyn added swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process and removed swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review labels Jan 6, 2024
@Jumhyn Jumhyn requested a review from xedin January 6, 2024 15:55
@Jumhyn
Copy link
Member Author

Jumhyn commented Jan 6, 2024

Hey @xedin I rebased this against your latest work on keypaths and the proposal has been approved! And further work you'd want to see on this before merge? Source compatibility suite is passing but think we should do any broader analysis?

@Jumhyn
Copy link
Member Author

Jumhyn commented Jan 17, 2024

@xedin just bumping now that we're a bit further into the new year!

@xedin
Copy link
Contributor

xedin commented Jan 17, 2024

Sorry, I'll try to get some time this week to review this!

@xedin
Copy link
Contributor

xedin commented Jan 23, 2024

I didn't forget, this is in my queue for tomorrow. @Jumhyn could you please rebase meanwhile?

Remove keypath subtype asserts; always use cached root type

Add tests for keypaths converted to funcs with inout param

Add unit test for overload selection
@Jumhyn Jumhyn force-pushed the keypath-function-conversion branch from a44f501 to 0d79f45 Compare January 23, 2024 13:52
@Jumhyn
Copy link
Member Author

Jumhyn commented Jan 23, 2024

@xedin thanks, done!

@Jumhyn
Copy link
Member Author

Jumhyn commented Jan 23, 2024

@swift-ci please test

@Jumhyn
Copy link
Member Author

Jumhyn commented Jan 23, 2024

@swift-ci please test source compatibility

Copy link
Contributor

@xedin xedin left a comment

Choose a reason for hiding this comment

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

Looks good! I left a few comments inline but they can be addressed in a follow-up.

@@ -697,6 +697,12 @@ ERROR(expr_smart_keypath_application_type_mismatch,none,
"key path of type %0 cannot be applied to a base of type %1",
(Type, Type))
ERROR(expr_keypath_root_type_mismatch, none,
"key path root type %0 cannot be converted to contextual type %1",
Copy link
Contributor

Choose a reason for hiding this comment

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

We should align the wording here with other "cannot convert" diagnostics, how about - cannot convert key path root type %0 to a contextual type %1?

"key path root type %0 cannot be converted to contextual type %1",
(Type, Type))
ERROR(expr_keypath_type_mismatch, none,
"key path of type %0 cannot be converted to contextual type %1",
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, there is a "cannot convert value of type ... to a contextual type" diagnostic, would be good to align them.

fnTy->getExtInfo());

return matchTypes(kpFnTy, paramFnTy, ConstraintKind::Conversion, subflags,
locator).isSuccess();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the condition here be !matchTypes(...).isFailure(); which is a bit more defensive even if we cannot get delayed in this case (yet).

@Jumhyn Jumhyn merged commit 0735629 into swiftlang:main Feb 3, 2024
7 checks passed
Jumhyn added a commit to Jumhyn/swift that referenced this pull request Feb 19, 2024
In \swiftlang#39612 we added subtyping for keypath-function conversions. However
the implementation naively coerced the keypath expression itself to the
inferred supertype, resulting in erroneous results. This patch updates the
solution application logic to build the keypath-function conversion expression
based entirely on the 'natural' keypath type, only converting to the inferred
supertype at the end via the usual coerceToType machinery for function
conversions.
Jumhyn added a commit to Jumhyn/swift that referenced this pull request Feb 19, 2024
In swiftlang#39612 we added subtyping for keypaths-as-functions, but during application
the implementation naively coerced the keypath expression itself to the
inferred supertype, resulting in erroneous results. This patch updates the
solution application logic to build the keypath-function conversion expression
based entirely on the 'natural' keypath type, only converting to the inferred
supertype at the end via the usual coerceToType machinery for function
conversions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants