Skip to content

Commit ed541f3

Browse files
committed
Forward matching of trailing closure arguments.
Introsuce a new "forward" algorithm for trailing closures where the unlabeled trailing closure argument matches the next parameter in the parameter list that can accept an unlabeled trailing closure. The "can accept an unlabeled trailing closure" criteria looks at the parameter itself. The parameter accepts an unlabeled trailing closure if all of the following are true: * The parameter is not 'inout' * The adjusted type of the parameter (defined below) is a function type The adjusted type of the parameter is the parameter's type as declared, after performing two adjustments: * If the parameter is an @autoclosure, use the result type of the parameter's declared (function) type, before performing the second adjustment. * Remove all outer "optional" types. For example, the following function illustrates both adjustments to determine that the parameter "body" accepts an unlabeled trailing closure: func doSomething(body: @autoclosure () -> (((Int) -> String)?)) This is a source-breaking change. However, there is a "fuzzy" matching rule that that addresses the source break we've observed in practice, where a defaulted closure parameter precedes a non-defaulted closure parameter: func doSomethingElse( onError: ((Error) -> Void)? = nil, onCompletion: (Int) -> Void ) { } doSomethingElse { x in print(x) } With the existing "backward" scan rule, the trailing closure matches onCompletion, and onError is given the default of "nil". With the forward scanning rule, the trailing closure matches onError, and there is no "onCompletion" argument, so the call fails. The fuzzy matching rule proceeds as follows: * if the call has a single, unlabeled trailing closure argument, and * the parameter that would match the unlabeled trailing closure argument has a default, and * there are parameters *after* that parameter that require an argument (i.e., they are not variadic and do not have a default argument) then the forward scan skips this parameter and considers the next parameter that could accept the unlabeled trailing closure. Note that APIs like doSomethingElse(onError:onCompletion:) above should probably be reworked to put the defaulted parameters at the end, which works better with the forward scan and with multiple trailing closures: func doSomethingElseBetter( onCompletion: (Int) -> Void, onError: ((Error) -> Void)? = nil ) { } doSomethingElseBetter { x in print(x) } doSomethingElseBetter { x in print(x) } onError: { error in throw error }
1 parent 5d1ade5 commit ed541f3

File tree

12 files changed

+257
-161
lines changed

12 files changed

+257
-161
lines changed

include/swift/AST/Attr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class GenericFunctionType;
5555
class LazyConformanceLoader;
5656
class LazyMemberLoader;
5757
class PatternBindingInitializer;
58+
enum class TrailingClosureMatching: uint8_t;
5859
class TrailingWhereClause;
5960
class TypeExpr;
6061

include/swift/AST/Types.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3499,6 +3499,7 @@ END_CAN_TYPE_WRAPPER(FunctionType, AnyFunctionType)
34993499
/// has a default argument.
35003500
struct ParameterListInfo {
35013501
SmallBitVector defaultArguments;
3502+
SmallBitVector acceptsUnlabeledTrailingClosures;
35023503

35033504
public:
35043505
ParameterListInfo() { }
@@ -3509,6 +3510,10 @@ struct ParameterListInfo {
35093510
/// Whether the parameter at the given index has a default argument.
35103511
bool hasDefaultArgument(unsigned paramIdx) const;
35113512

3513+
/// Whether the parameter accepts an unlabeled trailing closure argument
3514+
/// according to the "forward-scan" rule.
3515+
bool acceptsUnlabeledTrailingClosureArgument(unsigned paramIdx) const;
3516+
35123517
/// Retrieve the number of non-defaulted parameters.
35133518
unsigned numNonDefaultedParameters() const {
35143519
return defaultArguments.count();

include/swift/Basic/LangOptions.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,14 @@ namespace swift {
271271
/// behavior. This is a staging flag, and will be removed in the future.
272272
bool EnableNewOperatorLookup = false;
273273

274+
/// Whether to enable the "fuzzy" forward-scanning behavior for trailing
275+
/// closure matching, which skips over defaulted closure parameters
276+
/// to match later (non-defaulted) closure parameters
277+
///
278+
/// This is a backward-compatibility hack for unlabeled trailing closures,
279+
/// to be disabled in Swift 6+.
280+
bool EnableFuzzyForwardScanTrailingClosureMatching = true;
281+
274282
/// Use Clang function types for computing canonical types.
275283
/// If this option is false, the clang function types will still be computed
276284
/// but will not be used for checking type equality.

include/swift/Option/Options.td

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,16 @@ def enable_experimental_concise_pound_file : Flag<["-"],
546546
Flags<[FrontendOption, ModuleInterfaceOption]>,
547547
HelpText<"Enable experimental concise '#file' identifier">;
548548

549+
def disable_fuzzy_forward_scan_trailing_closure_matching : Flag<["-"],
550+
"disable-fuzzy-forward-scan-trailing-closure-matching">,
551+
Flags<[FrontendOption]>,
552+
HelpText<"Disable fuzzy forward-scan trailing closure matching">;
553+
554+
def enable_fuzzy_forward_scan_trailing_closure_matching : Flag<["-"],
555+
"enable-fuzzy-forward-scan-trailing-closure-matching">,
556+
Flags<[FrontendOption]>,
557+
HelpText<"Enable fuzzy forward-scan trailing closure matching">;
558+
549559
def enable_direct_intramodule_dependencies : Flag<["-"],
550560
"enable-direct-intramodule-dependencies">,
551561
Flags<[FrontendOption, HelpHidden]>,

lib/AST/Type.cpp

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,16 +832,42 @@ Type TypeBase::replaceCovariantResultType(Type newResultType,
832832
return FunctionType::get(inputType, resultType, fnType->getExtInfo());
833833
}
834834

835+
/// Whether this parameter accepts an unlabeled trailing closure argument
836+
/// using the more-restrictive forward-scan rule.
837+
static bool allowsUnlabeledTrailingClosureParameter(const ParamDecl *param) {
838+
// inout parameters never allow an unlabeled trailing closure.
839+
if (param->isInOut())
840+
return false;
841+
842+
Type paramType =
843+
param->getInterfaceType()->getRValueType()->lookThroughAllOptionalTypes();
844+
845+
// For autoclosure parameters, look through the autoclosure result type
846+
// to get the actual argument type.
847+
if (param->isAutoClosure()) {
848+
auto fnType = paramType->getAs<AnyFunctionType>();
849+
if (!fnType)
850+
return false;
851+
852+
paramType = fnType->getResult()->lookThroughAllOptionalTypes();
853+
}
854+
855+
// After lookup through all optional types, this parameter allows an
856+
// unlabeled trailing closure if it is (structurally) a function type.
857+
return paramType->is<AnyFunctionType>();
858+
}
859+
835860
ParameterListInfo::ParameterListInfo(
836861
ArrayRef<AnyFunctionType::Param> params,
837862
const ValueDecl *paramOwner,
838863
bool skipCurriedSelf) {
839864
defaultArguments.resize(params.size());
865+
acceptsUnlabeledTrailingClosures.resize(params.size());
840866

841867
// No parameter owner means no parameter list means no default arguments
842868
// - hand back the zeroed bitvector.
843869
//
844-
// FIXME: We ought to not request default argument info in this case.
870+
// FIXME: We ought to not request paramer list info in this case.
845871
if (!paramOwner)
846872
return;
847873

@@ -869,12 +895,17 @@ ParameterListInfo::ParameterListInfo(
869895
if (params.size() != paramList->size())
870896
return;
871897

872-
// Note which parameters have default arguments and/or function builders.
898+
// Note which parameters have default arguments and/or accept unlabeled
899+
// trailing closure arguments with the forward-scan rule.
873900
for (auto i : range(0, params.size())) {
874901
auto param = paramList->get(i);
875902
if (param->isDefaultArgument()) {
876903
defaultArguments.set(i);
877904
}
905+
906+
if (allowsUnlabeledTrailingClosureParameter(param)) {
907+
acceptsUnlabeledTrailingClosures.set(i);
908+
}
878909
}
879910
}
880911

@@ -883,6 +914,12 @@ bool ParameterListInfo::hasDefaultArgument(unsigned paramIdx) const {
883914
: false;
884915
}
885916

917+
bool ParameterListInfo::acceptsUnlabeledTrailingClosureArgument(
918+
unsigned paramIdx) const {
919+
return paramIdx < acceptsUnlabeledTrailingClosures.size() &&
920+
acceptsUnlabeledTrailingClosures[paramIdx];
921+
}
922+
886923
/// Turn a param list into a symbolic and printable representation that does not
887924
/// include the types, something like (_:, b:, c:)
888925
std::string swift::getParamListAsString(ArrayRef<AnyFunctionType::Param> params) {

lib/Driver/ToolChains.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ void ToolChain::addCommonFrontendArgs(const OutputInfo &OI,
264264
inputArgs.AddLastArg(arguments, options::OPT_disable_parser_lookup);
265265
inputArgs.AddLastArg(arguments,
266266
options::OPT_enable_experimental_concise_pound_file);
267+
inputArgs.AddLastArg(
268+
arguments,
269+
options::OPT_enable_fuzzy_forward_scan_trailing_closure_matching,
270+
options::OPT_disable_fuzzy_forward_scan_trailing_closure_matching);
267271
inputArgs.AddLastArg(arguments,
268272
options::OPT_verify_incremental_dependencies);
269273
inputArgs.AddLastArg(arguments,

lib/Frontend/CompilerInvocation.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,10 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
572572

573573
Opts.EnableConcisePoundFile =
574574
Args.hasArg(OPT_enable_experimental_concise_pound_file);
575+
Opts.EnableFuzzyForwardScanTrailingClosureMatching =
576+
Args.hasFlag(OPT_enable_fuzzy_forward_scan_trailing_closure_matching,
577+
OPT_disable_fuzzy_forward_scan_trailing_closure_matching,
578+
!Opts.EffectiveLanguageVersion.isVersionAtLeast(6));
575579

576580
Opts.EnableCrossImportOverlays =
577581
Args.hasFlag(OPT_enable_cross_import_overlays,

lib/Sema/CSApply.cpp

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,16 +1529,13 @@ namespace {
15291529
/// \param callee The callee for the function being applied.
15301530
/// \param apply The ApplyExpr that forms the call.
15311531
/// \param argLabels The argument labels provided for the call.
1532-
/// \param hasTrailingClosure Whether the last argument is a trailing
1533-
/// closure.
15341532
/// \param locator Locator used to describe where in this expression we are.
15351533
///
15361534
/// \returns the coerced expression, which will have type \c ToType.
15371535
Expr *
15381536
coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
15391537
ConcreteDeclRef callee, ApplyExpr *apply,
15401538
ArrayRef<Identifier> argLabels,
1541-
bool hasTrailingClosure,
15421539
ConstraintLocatorBuilder locator);
15431540

15441541
/// Coerce the given object argument (e.g., for the base of a
@@ -1749,7 +1746,7 @@ namespace {
17491746
auto fullSubscriptTy = openedFullFnType->getResult()
17501747
->castTo<FunctionType>();
17511748
index = coerceCallArguments(index, fullSubscriptTy, subscriptRef, nullptr,
1752-
argLabels, hasTrailingClosure,
1749+
argLabels,
17531750
locator.withPathElement(
17541751
ConstraintLocator::ApplyArgument));
17551752
if (!index)
@@ -2573,7 +2570,6 @@ namespace {
25732570
auto newArg = coerceCallArguments(
25742571
expr->getArg(), fnType, witness,
25752572
/*applyExpr=*/nullptr, expr->getArgumentLabels(),
2576-
expr->hasTrailingClosure(),
25772573
cs.getConstraintLocator(expr, ConstraintLocator::ApplyArgument));
25782574

25792575
expr->setInitializer(witness);
@@ -4926,7 +4922,7 @@ namespace {
49264922
auto *newIndexExpr =
49274923
coerceCallArguments(indexExpr, subscriptType, ref,
49284924
/*applyExpr*/ nullptr, labels,
4929-
/*hasTrailingClosure*/ false, locator);
4925+
locator);
49304926

49314927
// We need to be able to hash the captured index values in order for
49324928
// KeyPath itself to be hashable, so check that all of the subscript
@@ -5459,12 +5455,12 @@ static bool hasCurriedSelf(ConstraintSystem &cs, ConcreteDeclRef callee,
54595455
return false;
54605456
}
54615457

5462-
Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
5463-
ConcreteDeclRef callee,
5464-
ApplyExpr *apply,
5465-
ArrayRef<Identifier> argLabels,
5466-
bool hasTrailingClosure,
5467-
ConstraintLocatorBuilder locator) {
5458+
Expr *ExprRewriter::coerceCallArguments(
5459+
Expr *arg, AnyFunctionType *funcType,
5460+
ConcreteDeclRef callee,
5461+
ApplyExpr *apply,
5462+
ArrayRef<Identifier> argLabels,
5463+
ConstraintLocatorBuilder locator) {
54685464
auto &ctx = getConstraintSystem().getASTContext();
54695465
auto params = funcType->getParams();
54705466

@@ -5517,9 +5513,11 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
55175513

55185514
MatchCallArgumentListener listener;
55195515
SmallVector<ParamBinding, 4> parameterBindings;
5516+
auto unlabeledTrailingClosureIndex =
5517+
arg->getUnlabeledTrailingClosureIndexOfPackedArgument();
55205518
bool failed = constraints::matchCallArguments(args, params,
55215519
paramInfo,
5522-
arg->getUnlabeledTrailingClosureIndexOfPackedArgument(),
5520+
unlabeledTrailingClosureIndex,
55235521
/*allowFixes=*/false, listener,
55245522
parameterBindings);
55255523

@@ -5748,7 +5746,7 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
57485746
(params[0].getValueOwnership() == ValueOwnership::Default ||
57495747
params[0].getValueOwnership() == ValueOwnership::InOut)) {
57505748
assert(newArgs.size() == 1);
5751-
assert(!hasTrailingClosure);
5749+
assert(!unlabeledTrailingClosureIndex);
57525750
return newArgs[0];
57535751
}
57545752

@@ -5761,8 +5759,9 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
57615759
argParen->setSubExpr(newArgs[0]);
57625760
} else {
57635761
bool isImplicit = arg->isImplicit();
5764-
arg = new (ctx)
5765-
ParenExpr(lParenLoc, newArgs[0], rParenLoc, hasTrailingClosure);
5762+
arg = new (ctx) ParenExpr(
5763+
lParenLoc, newArgs[0], rParenLoc,
5764+
static_cast<bool>(unlabeledTrailingClosureIndex));
57665765
arg->setImplicit(isImplicit);
57675766
}
57685767
} else {
@@ -5776,8 +5775,8 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
57765775
}
57775776
} else {
57785777
// Build a new TupleExpr, re-using source location information.
5779-
arg = TupleExpr::create(ctx, lParenLoc, newArgs, newLabels, newLabelLocs,
5780-
rParenLoc, hasTrailingClosure,
5778+
arg = TupleExpr::create(ctx, lParenLoc, rParenLoc, newArgs, newLabels,
5779+
newLabelLocs, unlabeledTrailingClosureIndex,
57815780
/*implicit=*/arg->isImplicit());
57825781
}
57835782
}
@@ -7128,9 +7127,6 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
71287127
auto *arg = apply->getArg();
71297128
auto *fn = apply->getFn();
71307129

7131-
bool hasTrailingClosure =
7132-
isa<CallExpr>(apply) && cast<CallExpr>(apply)->hasTrailingClosure();
7133-
71347130
auto finishApplyOfDeclWithSpecialTypeCheckingSemantics
71357131
= [&](ApplyExpr *apply,
71367132
ConcreteDeclRef declRef,
@@ -7146,7 +7142,6 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
71467142
arg = coerceCallArguments(arg, fnType, declRef,
71477143
apply,
71487144
apply->getArgumentLabels(argLabelsScratch),
7149-
hasTrailingClosure,
71507145
locator.withPathElement(
71517146
ConstraintLocator::ApplyArgument));
71527147
if (!arg) {
@@ -7350,7 +7345,6 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
73507345
if (auto fnType = cs.getType(fn)->getAs<FunctionType>()) {
73517346
arg = coerceCallArguments(arg, fnType, callee, apply,
73527347
apply->getArgumentLabels(argLabelsScratch),
7353-
hasTrailingClosure,
73547348
locator.withPathElement(
73557349
ConstraintLocator::ApplyArgument));
73567350
if (!arg) {

0 commit comments

Comments
 (0)