-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[WIP] Implement dynamically callable types (@dynamicCallable
).
#16980
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
This proposal introduces the `@dynamicCallable` attribute, which enables nominal types to be "callable" via a simple syntactic sugar. This is the implementation of the Swift evolution proposal here: swiftlang/swift-evolution#858. Read there for more details.
Todo:
|
This test verifies that dynamic calls resolve to the correct method.
c34378d
to
a59a205
Compare
- Use known identifiers `Id_dynamicallyCall`, etc. in CSSimplify. - Update doc comment in CSDiag.
a59a205
to
f978c30
Compare
@swift-ci Please test |
Build failed |
@swift-ci Please test |
@swift-ci Please smoke test |
@swift-ci build toolchain |
lib/Sema/CSApply.cpp
Outdated
auto instanceType = metaType->getInstanceType(); | ||
isExistentialType = instanceType->isExistentialType(); | ||
} | ||
if (auto metaTy = cs.getType(fn)->getAs<AnyMetatypeType>()) { |
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.
Note: the diff at this part of the file isn't actually that big.
I simply moved the metatype-handling code (lines 7604-7655) into an if-block.
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 method is already quite long. Can you split off this part into a separate method?
@swift-ci Please smoke test |
Build failed |
Build failed |
@swift-ci build toolchain |
Linux Toolchain (Ubuntu 16.04) Install command |
macOS Toolchain Install command |
Neither of the download links for the toolchains appear to work. |
@tkremenek
|
@swift-ci comment updated with correct download links |
At some point, the @dynamicCallable implementation did not support generic `dynamicallyCall` methods. However, generic methods are now supported.
e80908c
to
3cd4265
Compare
@swift-ci Build toolchain |
1 similar comment
@swift-ci Build toolchain |
@swift-ci Please test |
Build failed |
Build failed |
@swift-ci Please test source compatibility |
|
||
// If this type has the attribute on it, then look up the methods. | ||
if (nominal->getAttrs().hasAttribute<DynamicCallableAttr>()) | ||
return lookupDynamicCallableMethods(type, CS, locator, error); |
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.
One minor nitpick. It's not apparent to me from looking at this code, but getAllProtocols() on a class includes protocols conformed to by superclasses, so I'm wondering if you're redundantly walking protocols of superclasses here when you don't need to.
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. I don't believe this is the case because getAllProtocols()
is checked before the loop, meaning all protocols conformed to by superclasses are checked once. If none of those protocols have the @dynamicCallable
attribute, the loop will recursively check superclasses of the class without rechecking protocols.
// look up the methods. | ||
for (auto p : nominal->getAllProtocols()) | ||
if (p->getAttrs().hasAttribute<DynamicCallableAttr>()) | ||
return lookupDynamicCallableMethods(type, CS, locator, error); |
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.
Currently, for (auto p : nominal->getAllProtocols())
iterates over all protocols and looks up methods in the first protocol that has the @dynamicCallable
attribute.
Instead, should calculateForComponentTypes
be called on all protocols with the @dynamicCallable
attribute? That seems more correct.
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's some initial feedback. I'm still looking over some of the details.
if (CS.DynamicCallableCache.count(protocol->getCanonicalType())) | ||
decl = protocol->getAnyNominal(); | ||
} | ||
CS.TC.diagnose(decl->getLoc(), diag::missing_dynamic_callable_kwargs_method, |
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 appears to be diagnosed multiple times, followed by another diagnostic, e.g.
@dynamicCallable
struct S {
func dynamicallyCall(withArguments: [String]) -> [Int] {
fatalError()
}
}
var s = S()
_ = s(x: "hi")
results in
/tmp/callable.swift:2:8: error: @dynamicCallable type 'S' cannot be applied with keyword arguments; missing `dynamicCall(withKeywordArguments:)` method
struct S {
^
/tmp/callable.swift:2:8: error: @dynamicCallable type 'S' cannot be applied with keyword arguments; missing `dynamicCall(withKeywordArguments:)` method
struct S {
^
/tmp/callable.swift:2:8: error: @dynamicCallable type 'S' cannot be applied with keyword arguments; missing `dynamicCall(withKeywordArguments:)` method
struct S {
^
/tmp/callable.swift:9:5: error: cannot invoke 's' with an argument list of type '(x: String)'
_ = s(x: "hi")
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 for pointing this out. I'll look into improving diagnostics.
ConstraintSystem &CS, Type type1, Type type2, | ||
ConstraintLocatorBuilder locator, | ||
ConstraintSystem::TypeMatchOptions flags) { | ||
using SolutionKind = ConstraintSystem::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.
This really doesn't behave like normal overload resolution. For example we fail to type-check the following, which I believe a user might normally expect to resolve to the overload with keyword arguments:
@dynamicCallable
struct S {
func dynamicallyCall(withArguments: [String]) -> Int {
fatalError()
}
func dynamicallyCall(withKeywordArguments: [String : String]) -> Float {
fatalError()
}
}
let s = S()
let _: Float = s("hi")
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 a good point. I describe the problems with the lookup-based approach in more detail here. Do you have ideas for a better method resolution approach?
lib/Sema/CSSimplify.cpp
Outdated
lookupDynamicCallableMethod(Type type, ConstraintSystem &CS, | ||
const ConstraintLocatorBuilder &locator, | ||
Identifier argumentName, bool hasKeywordArgs, | ||
bool &error) { |
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 not clear to me what value the separate error parameter adds here. Another option might be to have getDynamicCallableMethods
return an llvm::Optional and use the existence or lack of a value as an indication of the success or failure, unless you want to carry more semantic information in the form of a richer error type than a bool.
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 for pointing this out. I'll use llvm::Optional
instead, same for hasValidDynamicCallableMethod
in TypeCheckAttr.cpp
which currently also takes a bool &error
argument.
Hey @dan-zheng , are you still working on this? I'd really love to see this land. |
I sincerely apologize for the long absence. As @slavapestov and @rudkx pointed out, the current At a high level, I'd say // error: @dynamicCallable attribute does not support ambiguous method 'dynamicallyCall(withArguments:)'
@dynamicCallable
protocol Callable {
func dynamicallyCall(withArguments: [Int]) -> Int
}
extension Callable {
func dynamicallyCall(withArguments: [Int]) -> Int {
return 1
}
} This is unexpected behavior in my opinion. Rather than iterating on the current lookup based approach, I think a more constraint-system based solution would be more robust. Consider the following example: @dynamicCallable
protocol Callable {
// Overloaded `@dynamicCallable` methods should be supported, imo.
func dynamicallyCall(withArguments: [Int]) -> Int
func dynamicallyCall(withArguments: [Float]) -> Float
}
extension Callable {
func dynamicallyCall(withArguments: [Int]) -> Int {
return 1
}
}
func foo(x: Callable) -> Float {
return x(1, 2, 3) // how can we resolve this call?
} One source of friction is that dynamic calls are not an exact sugar for direct calls from the constraint system's perspective. (For the sake of discussion, I'll focus on the In direct calls (e.g. In
As mentioned above, this approach is problematic. Here are some other ideas I had:
Constraint system experts: do you have any ideas/suggestions for fixing the current implementation? I'm on standby because I don't know how to best resolve overloads, mimicking protocol requirement resolution. I'm happy to test suggestions and to make a final push to complete the implementation. |
Here's a more scoped question: how does overload resolution work in the example below? protocol A {
func foo(_ x: [Int]) -> Int
}
extension A {
func foo(_ x: [Int]) -> Int { return 1 }
}
func test(a: A) -> Int {
return a.foo([]) // how does this resolve to the protocol extension method?
} |
In an example like the one you've been given, we type check the expression using both declarations of That actually selects the protocol method, which dynamically dispatches to the appropriate method. So we don't statically resolve to the extension method in this case. If you had a type that extends |
@rudkx Thanks for the explanation! The I took a look at Improved
The big question here is how to implement the resolution logic in step 2. I forget how exactly
Please reply if you have feedback on this approach, especially if you think some part won't work or if you have a better idea. I have some priorities now but I'll be free to experiment and work on this next Friday and later. |
@dan-zheng Comparing solutions happens after all constraints have been solved. Also you should not emit diagnostics during constraint simplification because we can explore different parts of the solution space as we visit disjunctions, and discard invalid paths. |
@dynamicCallable
).@dynamicCallable
).
@slavapestov Thanks for sharing that info. |
Reimplement dynamic call resolution using the approach described in: swiftlang#16980 (comment) This commit is a WIP, I'm pushing it to transfer code between machines. I'll refactor things later.
@dan-zheng Can you squash this into a fewer number of patches, and remove (or fix) the various bits of commented out code? |
@slavapestov Yes, I do plan to squash commits after the code is fixed.
It's expected that all failing calls to I'm not sure how to work around this: is it possible to clone/backtrack |
@dan-zheng You can introduce disjunctions, which is the mechanism used to implement method overloads. @rudkx can give you more pointers. |
@slavapestov @rudkx Do "union constraints" exist, in addition to disjunction constraints? I need to emulate something like:
Advice on modeling this would be appreciated! |
@dan-zheng That's what our disjunction constraints do when paired to an
we generate the following constraints for the call to
The solver will then iterate over the options from the disjunction, binding each to |
@rudkx Thanks! I implemented Specifically, I generate an overload set disjunction in Code: @dynamicCallable
struct Callable {
func dynamicallyCall(withArguments: [Int]) {}
func dynamicallyCall(withArguments: [Float]) {}
}
func test(x: Callable) {
x(1, 2)
} The constraints look like:
These constraints seem sensible to me: Here are the roadblocks I ran into:
Thus, when the solver iterates over the options in the disjunction and tries to simplify the
I'm not sure how to work around this. One bad idea is to add a special case to
@dynamicCallable
struct S {
func dynamicallyCall(withArguments: [String]) -> Int {
fatalError()
}
func dynamicallyCall(withKeywordArguments: [String : String]) -> Float {
fatalError()
}
}
let s = S()
let _: Float = s("hi") From what I understand, disjunction are useful for representing overloaded methods so that the solver can "iterate" over them and attempt to solve other constraints. However, that approach is insufficient by itself here because there are two orthogonal sets of constraints to test: constraints involving the Do you have suggestions for working around these roadblocks (1. that dynamic calls aren't an exact sugar, causing straightforward disjunction + Any ideas would be much appreciated! EDIT: I'll make a post on the Swift forums explaining these challenges and ask for help. |
SE-0216 is quite clear about the ambiguity resolution rule:
The rule doesn't take into account anything except the presence of keyword arguments in the call site. I'm concerned that this PR has gone off into the weeds trying to implement a much more complicated rule that isn't what was proposed, doesn't seem to have a strong motivation, and substantially complicates the user model of this feature. |
This is a great observation @rjmccall, your reading of the proposal is exactly right and (in retrospect) I remember the discussions that led to this - I'm sorry I page faulted on that and didn't remember this sooner. I posted a note on the "amendment proposal" thread here: |
Thanks everyone for the help and insight! |
FYI: the new implementation PR was just merged. |
This proposal introduces the
@dynamicCallable
attribute, which enablesnominal types to be "callable" via a simple syntactic sugar.
This is the implementation of the Swift evolution proposal here:
swiftlang/swift-evolution#858.
Read there for more details.
@dynamicCallable
is a follow-up to SE-0195 "Dynamic Member Lookup".Forum discussion regarding
@dynamicCallable
:Proposal review and acceptance rationale: