Skip to content

[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

Closed
wants to merge 21 commits into from

Conversation

dan-zheng
Copy link
Contributor

@dan-zheng dan-zheng commented Jun 4, 2018

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.


@dynamicCallable is a follow-up to SE-0195 "Dynamic Member Lookup".

Forum discussion regarding @dynamicCallable:

Proposal review and acceptance rationale:

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.
@dan-zheng
Copy link
Contributor Author

dan-zheng commented Jun 4, 2018

Todo:

  • Add a -emit-silgen test to verify that dynamic calls resolve to the correct method (either the withArguments: or withKeywordArguments: method).

dan-zheng added 2 commits June 4, 2018 08:52
This test verifies that dynamic calls resolve to the correct method.
@dan-zheng dan-zheng added the swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review label Jun 4, 2018
- Use known identifiers `Id_dynamicallyCall`, etc. in CSSimplify.
- Update doc comment in CSDiag.
@dan-zheng
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

swift-ci commented Jun 4, 2018

Build failed
Swift Test OS X Platform
Git Sha - 40e3fb9

@dan-zheng
Copy link
Contributor Author

@swift-ci Please test

@dan-zheng
Copy link
Contributor Author

@swift-ci Please smoke test

@tkremenek
Copy link
Member

@swift-ci build toolchain

auto instanceType = metaType->getInstanceType();
isExistentialType = instanceType->isExistentialType();
}
if (auto metaTy = cs.getType(fn)->getAs<AnyMetatypeType>()) {
Copy link
Contributor Author

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.

Copy link
Contributor

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?

@dan-zheng
Copy link
Contributor Author

@swift-ci Please smoke test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - f978c30

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - f978c30

@tkremenek
Copy link
Member

@swift-ci build toolchain

@swift-ci
Copy link
Contributor

swift-ci commented Jun 14, 2018

Linux Toolchain (Ubuntu 16.04)
Download Toolchain
Git Sha - e1b36c7

Install command
tar zxf swift-PR-16980-26-ubuntu16.04.tar.gz
More info

@swift-ci
Copy link
Contributor

swift-ci commented Jun 14, 2018

macOS Toolchain
Download Toolchain
Git Sha - e1b36c7

Install command
tar -zxf swift-PR-16980-33-osx.tar.gz --directory ~/

@tkremenek
Copy link
Member

Neither of the download links for the toolchains appear to work.

@dan-zheng
Copy link
Contributor Author

dan-zheng commented Jun 14, 2018

@tkremenek
I'm able to download the toolchains by navigating to the build job pages and downloading the artifacts there.

@shahmishal
Copy link
Member

@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.
@dan-zheng
Copy link
Contributor Author

@swift-ci Build toolchain

1 similar comment
@shahmishal
Copy link
Member

@swift-ci Build toolchain

@rudkx
Copy link
Contributor

rudkx commented Jul 19, 2018

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - a6de931

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - a6de931

@rudkx
Copy link
Contributor

rudkx commented Jul 20, 2018

@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);
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor Author

@dan-zheng dan-zheng Jul 20, 2018

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.

Copy link
Contributor

@rudkx rudkx left a 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,
Copy link
Contributor

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")

Copy link
Contributor Author

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;
Copy link
Contributor

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")

Copy link
Contributor Author

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?

lookupDynamicCallableMethod(Type type, ConstraintSystem &CS,
const ConstraintLocatorBuilder &locator,
Identifier argumentName, bool hasKeywordArgs,
bool &error) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@lattner
Copy link
Contributor

lattner commented Aug 3, 2018

Hey @dan-zheng , are you still working on this? I'd really love to see this land.

@dan-zheng
Copy link
Contributor Author

dan-zheng commented Aug 3, 2018

I sincerely apologize for the long absence.

As @slavapestov and @rudkx pointed out, the current @dynamicCallable implementation has some big problems (many of which are shared with @dynamicMemberLookup). Dynamic call resolution doesn’t behave like standard overload resolution: it’s ad-hoc, lookup based, and frustratingly doesn’t utilize the constraint system.

At a high level, I'd say @dynamicCallable method lookup should work just like protocol requirement lookup (please comment if you disagree). The current lookup based process cannot achieve this: overloaded dynamicallyCall methods are not at all supported, leading the following code to fail.

// 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 withArguments method and ignore withKeywordArguments for now.)

In direct calls (e.g. x.dynamicallyCall(withArguments: [1, 2, 3])), only one type variable is created for the ArrayExpr element type.
In dynamic calls (e.g. x(1, 2, 3)), one type variable is created for each element of the argument tuple (three total here).

In CSSimplify.cpp, the current logic is:

  • Find a suitable dynamicallyCall(withArguments:) method via lookup. Overloaded methods are not supported. Let $A and $R respectively be the argument and result type of the method. $A is an ArraySliceType with base type $X.
  • For each type variable in the argument expression tuple (e.g. $T0, $T1, ...), add an ArgumentConversion constraint from it to $X. Add a Bind constraint from the result type variable to $R.

As mentioned above, this approach is problematic. Here are some other ideas I had:

  • Look up all dynamicallyCall(withArguments:) methods. For each of them, apply constraints like the current logic, then add a disjunction constraint of all those sub-constraints. I don't know if it's possible to "pair up" the argument/result type constraints per method, though, so that either both or neither constraints solve. I also don't know if a big disjunction constraint is suitable here.
  • Extend the current lookup based approach to support overloads. I'm not sure how to rank overloads though, after all methods have been found.

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.

@dan-zheng
Copy link
Contributor Author

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?
}

@rudkx
Copy link
Contributor

rudkx commented Aug 4, 2018

In an example like the one you've been given, we type check the expression using both declarations of foo. Both result in valid solutions to type checking, so we disambiguate by comparing the solutions in compareSolutions of CSRanking.cpp.

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 A and provided an implementation of foo, and passed that to test, we would call that type's implementation of foo from test.

@dan-zheng
Copy link
Contributor Author

dan-zheng commented Aug 4, 2018

@rudkx Thanks for the explanation!

The @dynamicCallable method resolution logic could be changed to select protocol (and class?) methods when possible, to avoid the need to statically resolve methods.

I took a look at compareSolutions in CSRanking.cpp; it probably doesn't make sense to call compareSolutions directly in CSSimplify.cpp since it's from a different part of the constraint system code path.

Improved @dynamicCallable method resolution logic should support overloaded methods and work even in trickier situations like the one described here. Here are some pseudo-steps to implement this:

  1. In CSSimplify.cpp, check if the "called" nominal type has or somehow "inherits" the @dynamicCallable attribute. Then, look up and cache all dynamicallyCall(withArguments:) and dynamicallyCall(withKeywordArguments:)methods within the nominal type.
  2. In CSSimplify.cpp, resolve the correct method, or return SolutionKind::Error (no compatible methods, ambiguous methods, etc). This requires somehow checking whether the argument/result type of the dynamic call expression can convert/bind the argument/result type of each method.
    • The resolution logic should prefer protocol (and class?) methods when possible to leverage dynamic dispatch.
    • The resolution logic should be aware that dynamic calls with no labels can still resolve to the withKeywordArguments method (e.g. s("x") in the code here).
  3. If resolution failed, we're done. If not, in CSApply.cpp, it must be possible to access the resolved method for expression rewriting.

The big question here is how to implement the resolution logic in step 2. I forget how exactly CS.matchTypes works, but perhaps it can be used in the following approach:

  • Iterate through all candidate methods.
    • If type-checking a dynamic call with labels, candidates are only withKeywordArguments methods.
    • If type-checking a dynamic call with no labels, candidates are both withArguments and withKeywordArguments methods.
  • Filter the candidates, using CS.matchTypes to see if each method's argument/result types are compatible with the dynamic call's argument/result types.
    • If there are no valid candidates, return SolutionKind::Error.
    • If there is exactly one valid candidate, somehow mark it as resolved so that it's accessible in CSApply.cpp.
    • If there are multiple valid candidates, things are trickier.
      • If there are two methods with the same type, where one is a protocol requirement and the other is a concrete method (e.g. non protocol requirement), prefer the protocol requirement. (Does this rule always hold or need extra qualification?)
      • What should be done if there are multiple protocol requirements with the same type? (Tentative answer: randomly select one of them, if they have the exact same type. I believe this is the current logic for compositions of protocols that have identical required methods.)
      • What if there are multiple valid class methods where the classes inherit from one another? Does there need to be explicit logic to select the innermost subclass method, or is it possible to leverage dynamic dispatch?

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.

@slavapestov
Copy link
Contributor

@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.

@dan-zheng dan-zheng changed the title Implement dynamically callable types (@dynamicCallable). [WIP] Implement dynamically callable types (@dynamicCallable). Sep 5, 2018
@dan-zheng
Copy link
Contributor Author

@slavapestov Thanks for sharing that info.
I edited my approach, replacing "emit a diagnostic during constraint simplification" with "return SolutionKind::Error".

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.
@slavapestov
Copy link
Contributor

@dan-zheng Can you squash this into a fewer number of patches, and remove (or fix) the various bits of commented out code?

@dan-zheng
Copy link
Contributor Author

@slavapestov Yes, I do plan to squash commits after the code is fixed.
Unfortunately, my approach for fixing constraint simplification didn't quite work, this part in particular:

Filter the candidates, using CS.matchTypes to see if each method's argument/result types are compatible with the dynamic call's argument/result types.

It's expected that all failing calls to matchTypes are immediately followed with return SolutionKind::Error (because matchTypes mutates the ConstraintSystem). This is problematic for the approach.

I'm not sure how to work around this: is it possible to clone/backtrack ConstraintSystem instances? Bigger changes to the approach may be necessary, advice would be much appreciated.

@slavapestov
Copy link
Contributor

@dan-zheng You can introduce disjunctions, which is the mechanism used to implement method overloads. @rudkx can give you more pointers.

@dan-zheng
Copy link
Contributor Author

@slavapestov @rudkx Do "union constraints" exist, in addition to disjunction constraints?

I need to emulate something like:

Call site type variables: (T, U) -> R. I need to work with these somehow.
candidate1: (A1, B1) -> R1
candidate2: (A2, B2) -> R2

Evaluate: ((T match A1) /\ (U match B1) /\ (R match R1)) \/ ((T match A2) /\ (U match B2) /\ (R match R2))

Advice on modeling this would be appreciated!

@rudkx
Copy link
Contributor

rudkx commented Sep 21, 2018

@dan-zheng That's what our disjunction constraints do when paired to an ApplicableFunction constraint. For example for:

func foo(_ i: Int) -> Int { return i }
func foo(_ d: Double) -> Double { return d }

func test() {
  _ = foo(1)
}

we generate the following constraints for the call to foo(1) from generateConstraints():

  disjunction [[locator@0x10c806a00 [OverloadedDeclRef@/tmp/overload.swift:5:7]]]:$T1 bound to decl overload.(file).foo@/tmp/overload.swift:1:6 : (Int) -> Int at /tmp/overload.swift:1:6 [[locator@0x10c806a00 [OverloadedDeclRef@/tmp/overload.swift:5:7]]]; or $T1 bound to decl overload.(file).foo@/tmp/overload.swift:2:6 : (Double) -> Double at /tmp/overload.swift:2:6 [[locator@0x10c806a00 [OverloadedDeclRef@/tmp/overload.swift:5:7]]];
  $T2 literal conforms to ExpressibleByIntegerLiteral [[locator@0x10c806bd8 [IntegerLiteral@/tmp/overload.swift:5:11]]];
  ($T2) -> $T3 applicable fn $T1 [[locator@0x10c806d10 [Call@/tmp/overload.swift:5:7 -> apply function]]];
  $T3 conv $T0 [[locator@0x10c806de0 [Assign@/tmp/overload.swift:5:5]]];

The solver will then iterate over the options from the disjunction, binding each to $T1 and simplify()ing the ApplicableFunction constraint which produces the argument conversions from $T2 to a newly create type variable for the argument to the option in the disjunction.

@dan-zheng
Copy link
Contributor Author

dan-zheng commented Sep 22, 2018

@rudkx Thanks!

I implemented @dynamicCallable method resolution using a disjunction paired with an ApplicableFunction constraint (unpolished code in this commit) but ran into some road blocks.

Specifically, I generate an overload set disjunction in simplifyDynamicCallableApplicableFunction, following the example you shared above.

Code:

@dynamicCallable
struct Callable {
  func dynamicallyCall(withArguments: [Int]) {}
  func dynamicallyCall(withArguments: [Float]) {}
}
func test(x: Callable) {
  x(1, 2)
}

The constraints look like:

Type Variables:
  $T0 [lvalue allowed] as Callable @ locator@0x7ff540021000 [[email protected]:7:3]
  $T1 fully_bound literal=3 involves_type_vars bindings={(subtypes of) (default from ExpressibleByIntegerLiteral) Int} @ locator@0x7ff540021080 [[email protected]:7:5]
  $T2 fully_bound literal=3 involves_type_vars bindings={(subtypes of) (default from ExpressibleByIntegerLiteral) Int} @ locator@0x7ff540021120 [[email protected]:7:8]
  $T3 fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@0x7ff540021208 [[email protected]:7:3 -> function result]
  $T4 [lvalue allowed] subtype_of_existential involves_type_vars bindings={} @ locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]
  $T5 fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]

Active Constraints:

Inactive Constraints:
  $T1 literal conforms to ExpressibleByIntegerLiteral [[locator@0x7ff540021080 [[email protected]:7:5]]];
  $T2 literal conforms to ExpressibleByIntegerLiteral [[locator@0x7ff540021120 [[email protected]:7:8]]];
  disjunction [[locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]]]:$T4 bound to decl d2.(file).Callable.dynamicallyCall(withArguments:)@d2.swift:3:8 : (Callable) -> ([Int]) -> () at d2.swift:3:8 [[locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]]]; or $T4 bound to decl d2.(file).Callable.dynamicallyCall(withArguments:)@d2.swift:4:8 : (Callable) -> ([Float]) -> () at d2.swift:4:8 [[locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]]];
  $T5 conforms to ExpressibleByArrayLiteral [[locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]]];
  $T5.ArrayLiteralElement can default to Any [[locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]]];
  $T1 arg conv $T5.ArrayLiteralElement [[locator@0x7ff540021548 [[email protected]:7:3 -> apply function -> tuple element #0]]];
  $T2 arg conv $T5.ArrayLiteralElement [[locator@0x7ff5400215d8 [[email protected]:7:3 -> apply function -> tuple element #1]]];
  ($T5) -> $T3 applicable fn $T4 [[locator@0x7ff5400212a0 [[email protected]:7:3 -> apply function]]];

These constraints seem sensible to me: $T4 is bound to each method in the disjunction, and various constraints use it: ($T5) -> $T3 applicable fn $T4, $T5 conforms to ExpressibleByArrayLiteral, etc.

Here are the roadblocks I ran into:

  1. One problem is that dynamic calls are not a pure syntactic sugar. Original call expressions have a tuple of multiple arguments (e.g. x(1, 2)), but the dynamicallyCall methods expect a single argument of array type (e.g. x.dynamicallyCall(withArguments: [1, 2]).

Thus, when the solver iterates over the options in the disjunction and tries to simplify the ($T5) -> $T3 applicable fn $T4 constraint, there is a crash in matchCallArguments because x(1, 2) does not fit the expected form x.dynamicallyCall(WithArguments: [1, 2]). The call site has two empty labels but the method has one label:

Assertion failed: (params.size() == labels.size()), function relabelParams, file /Users/dan/swift-build/swift/lib/AST/ASTContext.cpp, line 3623.
Stack dump:
...
swift::AnyFunctionType::relabelParams(llvm::MutableArrayRef<swift::AnyFunctionType::Param>, llvm::ArrayRef<swift::Identifier>) + 119
swift::constraints::matchCallArguments(swift::constraints::ConstraintSystem&, bool, llvm::ArrayRef<swift::AnyFunctionType::Param>, llvm::ArrayRef<swift::AnyFunctionType::Param>, swift::constraints::ConstraintLocatorBuilder) + 1616
swift::constraints::ConstraintSystem::simplifyApplicableFnConstraint(swift::Type, swift::Type, swift::OptionSet<swift::constraints::ConstraintSystem::TypeMatchFlags, unsigned int>, swift::constraints::ConstraintLocatorBuilder) + 1574
swift::constraints::ConstraintSystem::simplifyConstraint(swift::constraints::Constraint const&) + 1150
swift::constraints::ConstraintSystem::simplify(bool) + 210
swift::constraints::ConstraintSystem::solveRec(llvm::SmallVectorImpl<swift::constraints::Solution>&) + 83
swift::constraints::DisjunctionChoice::solve(llvm::SmallVectorImpl<swift::constraints::Solution>&) + 113
swift::constraints::ConstraintSystem::solveForDisjunctionChoices(swift::constraints::Disjunction&, llvm:)

I'm not sure how to work around this. One bad idea is to add a special case to matchCallArguments for resolving dynamic calls, but even this is difficult because when the solver iterates over the disjunction and tries to simplify the ($T5) -> $T3 applicable fn $T4 constraint, it's not known whether the call is a dynamic call or not.

  1. Also, I'm not sure how to use disjunctions to handle tricker cases like this:
@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 withArguments method (e.g. $T5 conforms to ExpressibleByArrayLiteral) and ones involving the withKeywordArguments method (e.g. $T5 conforms to ExpressibleByDictionaryLiteral). I'm not sure how to reconcile the two: generating both sets of constraints at once is sure to fail.


Do you have suggestions for working around these roadblocks (1. that dynamic calls aren't an exact sugar, causing straightforward disjunction + ApplicableFunction constraints to fail, and 2. how to reconcile withArguments and withKeywordArguments constraints)?

Any ideas would be much appreciated!

EDIT: I'll make a post on the Swift forums explaining these challenges and ask for help.

@rjmccall
Copy link
Contributor

SE-0216 is quite clear about the ambiguity resolution rule:

We propose that the type checker resolve this ambiguity towards the tightest match based on syntactic form of the expression. If a type implements both the withArguments: and withKeywordArguments: methods, the compiler will use the withArguments: method for call sites that have no keyword arguments and the withKeywordArguments: method for call sites that have at least one keyword argument.

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.

@lattner
Copy link
Contributor

lattner commented Oct 31, 2018

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:
https://forums.swift.org/t/amend-se-0216-dynamic-callable-reduce-overloads/16639/28

@dan-zheng
Copy link
Contributor Author

Thanks everyone for the help and insight!
I've revamped (and squashed) the implementation of @dynamicCallable in a fresh PR: #20305
Let us continue the discussion there.

@dan-zheng
Copy link
Contributor Author

FYI: the new implementation PR was just merged.
If you have feedback/suggestions, I'm happy to address them in master.

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.

9 participants