Skip to content

Commit f7f8096

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 be26b83 commit f7f8096

File tree

12 files changed

+258
-160
lines changed

12 files changed

+258
-160
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

6061
/// TypeAttributes - These are attributes that may be applied to types.

include/swift/AST/Types.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3484,6 +3484,7 @@ END_CAN_TYPE_WRAPPER(FunctionType, AnyFunctionType)
34843484
/// has a default argument.
34853485
struct ParameterListInfo {
34863486
SmallBitVector defaultArguments;
3487+
SmallBitVector acceptsUnlabeledTrailingClosures;
34873488

34883489
public:
34893490
ParameterListInfo() { }
@@ -3494,6 +3495,10 @@ struct ParameterListInfo {
34943495
/// Whether the parameter at the given index has a default argument.
34953496
bool hasDefaultArgument(unsigned paramIdx) const;
34963497

3498+
/// Whether the parameter accepts an unlabeled trailing closure argument
3499+
/// according to the "forward-scan" rule.
3500+
bool acceptsUnlabeledTrailingClosureArgument(unsigned paramIdx) const;
3501+
34973502
/// Retrieve the number of non-defaulted parameters.
34983503
unsigned numNonDefaultedParameters() const {
34993504
return defaultArguments.count();

include/swift/Basic/LangOptions.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ namespace swift {
250250
/// Build the ASTScope tree lazily
251251
bool LazyASTScopes = true;
252252

253+
/// Whether to enable the "fuzzy" forward-scanning behavior for trailing
254+
/// closure matching, which skips over defaulted closure parameters
255+
/// to match later (non-defaulted) closure parameters
256+
///
257+
/// This is a backward-compatibility hack for unlabeled trailing closures,
258+
/// to be disabled in Swift 6+.
259+
bool EnableFuzzyForwardScanTrailingClosureMatching = true;
260+
253261
/// Use Clang function types for computing canonical types.
254262
/// If this option is false, the clang function types will still be computed
255263
/// 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
@@ -519,6 +519,16 @@ def enable_experimental_concise_pound_file : Flag<["-"],
519519
Flags<[FrontendOption, ModuleInterfaceOption]>,
520520
HelpText<"Enable experimental concise '#file' identifier">;
521521

522+
def disable_fuzzy_forward_scan_trailing_closure_matching : Flag<["-"],
523+
"disable-fuzzy-forward-scan-trailing-closure-matching">,
524+
Flags<[FrontendOption]>,
525+
HelpText<"Disable fuzzy forward-scan trailing closure matching">;
526+
527+
def enable_fuzzy_forward_scan_trailing_closure_matching : Flag<["-"],
528+
"enable-fuzzy-forward-scan-trailing-closure-matching">,
529+
Flags<[FrontendOption]>,
530+
HelpText<"Enable fuzzy forward-scan trailing closure matching">;
531+
522532
// Diagnostic control options
523533
def suppress_warnings : Flag<["-"], "suppress-warnings">,
524534
Flags<[FrontendOption]>,

lib/AST/Type.cpp

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

825+
/// Whether this parameter accepts an unlabeled trailing closure argument
826+
/// using the more-restrictive forward-scan rule.
827+
static bool allowsUnlabeledTrailingClosureParameter(const ParamDecl *param) {
828+
// inout parameters never allow an unlabeled trailing closure.
829+
if (param->isInOut())
830+
return false;
831+
832+
Type paramType =
833+
param->getInterfaceType()->getRValueType()->lookThroughAllOptionalTypes();
834+
835+
// For autoclosure parameters, look through the autoclosure result type
836+
// to get the actual argument type.
837+
if (param->isAutoClosure()) {
838+
auto fnType = paramType->getAs<AnyFunctionType>();
839+
if (!fnType)
840+
return false;
841+
842+
paramType = fnType->getResult()->lookThroughAllOptionalTypes();
843+
}
844+
845+
// After lookup through all optional types, this parameter allows an
846+
// unlabeled trailing closure if it is (structurally) a function type.
847+
return paramType->is<AnyFunctionType>();
848+
}
849+
825850
ParameterListInfo::ParameterListInfo(
826851
ArrayRef<AnyFunctionType::Param> params,
827852
const ValueDecl *paramOwner,
828853
bool skipCurriedSelf) {
829854
defaultArguments.resize(params.size());
855+
acceptsUnlabeledTrailingClosures.resize(params.size());
830856

831857
// No parameter owner means no parameter list means no default arguments
832858
// - hand back the zeroed bitvector.
833859
//
834-
// FIXME: We ought to not request default argument info in this case.
860+
// FIXME: We ought to not request paramer list info in this case.
835861
if (!paramOwner)
836862
return;
837863

@@ -865,12 +891,17 @@ ParameterListInfo::ParameterListInfo(
865891
if (params.size() != paramList->size())
866892
return;
867893

868-
// Note which parameters have default arguments and/or function builders.
894+
// Note which parameters have default arguments and/or accept unlabeled
895+
// trailing closure arguments with the forward-scan rule.
869896
for (auto i : range(0, params.size())) {
870897
auto param = paramList->get(i);
871898
if (param->isDefaultArgument()) {
872899
defaultArguments.set(i);
873900
}
901+
902+
if (allowsUnlabeledTrailingClosureParameter(param)) {
903+
acceptsUnlabeledTrailingClosures.set(i);
904+
}
874905
}
875906
}
876907

@@ -879,6 +910,12 @@ bool ParameterListInfo::hasDefaultArgument(unsigned paramIdx) const {
879910
: false;
880911
}
881912

913+
bool ParameterListInfo::acceptsUnlabeledTrailingClosureArgument(
914+
unsigned paramIdx) const {
915+
return paramIdx < acceptsUnlabeledTrailingClosures.size() &&
916+
acceptsUnlabeledTrailingClosures[paramIdx];
917+
}
918+
882919
/// Turn a param list into a symbolic and printable representation that does not
883920
/// include the types, something like (_:, b:, c:)
884921
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
@@ -252,6 +252,10 @@ void ToolChain::addCommonFrontendArgs(const OutputInfo &OI,
252252
inputArgs.AddLastArg(arguments, options::OPT_disable_parser_lookup);
253253
inputArgs.AddLastArg(arguments,
254254
options::OPT_enable_experimental_concise_pound_file);
255+
inputArgs.AddLastArg(
256+
arguments,
257+
options::OPT_enable_fuzzy_forward_scan_trailing_closure_matching,
258+
options::OPT_disable_fuzzy_forward_scan_trailing_closure_matching);
255259
inputArgs.AddLastArg(arguments,
256260
options::OPT_verify_incremental_dependencies);
257261

lib/Frontend/CompilerInvocation.cpp

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

560560
Opts.EnableConcisePoundFile =
561561
Args.hasArg(OPT_enable_experimental_concise_pound_file);
562+
Opts.EnableFuzzyForwardScanTrailingClosureMatching =
563+
Args.hasFlag(OPT_enable_fuzzy_forward_scan_trailing_closure_matching,
564+
OPT_disable_fuzzy_forward_scan_trailing_closure_matching,
565+
!Opts.EffectiveLanguageVersion.isVersionAtLeast(6));
562566

563567
Opts.EnableCrossImportOverlays =
564568
Args.hasFlag(OPT_enable_cross_import_overlays,

lib/Sema/CSApply.cpp

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,16 +1602,13 @@ namespace {
16021602
/// \param callee The callee for the function being applied.
16031603
/// \param apply The ApplyExpr that forms the call.
16041604
/// \param argLabels The argument labels provided for the call.
1605-
/// \param hasTrailingClosure Whether the last argument is a trailing
1606-
/// closure.
16071605
/// \param locator Locator used to describe where in this expression we are.
16081606
///
16091607
/// \returns the coerced expression, which will have type \c ToType.
16101608
Expr *
16111609
coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
16121610
ConcreteDeclRef callee, ApplyExpr *apply,
16131611
ArrayRef<Identifier> argLabels,
1614-
bool hasTrailingClosure,
16151612
ConstraintLocatorBuilder locator);
16161613

16171614
/// Coerce the given object argument (e.g., for the base of a
@@ -1822,7 +1819,7 @@ namespace {
18221819
auto fullSubscriptTy = openedFullFnType->getResult()
18231820
->castTo<FunctionType>();
18241821
index = coerceCallArguments(index, fullSubscriptTy, subscriptRef, nullptr,
1825-
argLabels, hasTrailingClosure,
1822+
argLabels,
18261823
locator.withPathElement(
18271824
ConstraintLocator::ApplyArgument));
18281825
if (!index)
@@ -2641,6 +2638,7 @@ namespace {
26412638
conformingType->getRValueType(), constrName);
26422639
if (!witness || !isa<AbstractFunctionDecl>(witness.getDecl()))
26432640
return nullptr;
2641+
26442642
expr->setInitializer(witness);
26452643
expr->setArg(cs.coerceToRValue(expr->getArg()));
26462644
return expr;
@@ -4981,7 +4979,7 @@ namespace {
49814979
auto *newIndexExpr =
49824980
coerceCallArguments(indexExpr, subscriptType, ref,
49834981
/*applyExpr*/ nullptr, labels,
4984-
/*hasTrailingClosure*/ false, locator);
4982+
locator);
49854983

49864984
// We need to be able to hash the captured index values in order for
49874985
// KeyPath itself to be hashable, so check that all of the subscript
@@ -5523,12 +5521,12 @@ static bool hasCurriedSelf(ConstraintSystem &cs, ConcreteDeclRef callee,
55235521
return false;
55245522
}
55255523

5526-
Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
5527-
ConcreteDeclRef callee,
5528-
ApplyExpr *apply,
5529-
ArrayRef<Identifier> argLabels,
5530-
bool hasTrailingClosure,
5531-
ConstraintLocatorBuilder locator) {
5524+
Expr *ExprRewriter::coerceCallArguments(
5525+
Expr *arg, AnyFunctionType *funcType,
5526+
ConcreteDeclRef callee,
5527+
ApplyExpr *apply,
5528+
ArrayRef<Identifier> argLabels,
5529+
ConstraintLocatorBuilder locator) {
55325530
auto &ctx = getConstraintSystem().getASTContext();
55335531
auto params = funcType->getParams();
55345532

@@ -5578,9 +5576,11 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
55785576

55795577
MatchCallArgumentListener listener;
55805578
SmallVector<ParamBinding, 4> parameterBindings;
5579+
auto unlabeledTrailingClosureIndex =
5580+
arg->getUnlabeledTrailingClosureIndexOfPackedArgument();
55815581
bool failed = constraints::matchCallArguments(args, params,
55825582
paramInfo,
5583-
arg->getUnlabeledTrailingClosureIndexOfPackedArgument(),
5583+
unlabeledTrailingClosureIndex,
55845584
/*allowFixes=*/false, listener,
55855585
parameterBindings);
55865586

@@ -5810,7 +5810,7 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
58105810
(params[0].getValueOwnership() == ValueOwnership::Default ||
58115811
params[0].getValueOwnership() == ValueOwnership::InOut)) {
58125812
assert(newArgs.size() == 1);
5813-
assert(!hasTrailingClosure);
5813+
assert(!unlabeledTrailingClosureIndex);
58145814
return newArgs[0];
58155815
}
58165816

@@ -5823,8 +5823,9 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
58235823
argParen->setSubExpr(newArgs[0]);
58245824
} else {
58255825
bool isImplicit = arg->isImplicit();
5826-
arg = new (ctx)
5827-
ParenExpr(lParenLoc, newArgs[0], rParenLoc, hasTrailingClosure);
5826+
arg = new (ctx) ParenExpr(
5827+
lParenLoc, newArgs[0], rParenLoc,
5828+
static_cast<bool>(unlabeledTrailingClosureIndex));
58285829
arg->setImplicit(isImplicit);
58295830
}
58305831
} else {
@@ -5838,8 +5839,8 @@ Expr *ExprRewriter::coerceCallArguments(Expr *arg, AnyFunctionType *funcType,
58385839
}
58395840
} else {
58405841
// Build a new TupleExpr, re-using source location information.
5841-
arg = TupleExpr::create(ctx, lParenLoc, newArgs, newLabels, newLabelLocs,
5842-
rParenLoc, hasTrailingClosure,
5842+
arg = TupleExpr::create(ctx, lParenLoc, rParenLoc, newArgs, newLabels,
5843+
newLabelLocs, unlabeledTrailingClosureIndex,
58435844
/*implicit=*/arg->isImplicit());
58445845
}
58455846
}
@@ -7277,9 +7278,6 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
72777278
auto *arg = apply->getArg();
72787279
auto *fn = apply->getFn();
72797280

7280-
bool hasTrailingClosure =
7281-
isa<CallExpr>(apply) && cast<CallExpr>(apply)->hasTrailingClosure();
7282-
72837281
auto finishApplyOfDeclWithSpecialTypeCheckingSemantics
72847282
= [&](ApplyExpr *apply,
72857283
ConcreteDeclRef declRef,
@@ -7295,7 +7293,6 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
72957293
arg = coerceCallArguments(arg, fnType, declRef,
72967294
apply,
72977295
apply->getArgumentLabels(argLabelsScratch),
7298-
hasTrailingClosure,
72997296
locator.withPathElement(
73007297
ConstraintLocator::ApplyArgument));
73017298
if (!arg) {
@@ -7503,7 +7500,6 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
75037500
if (auto fnType = cs.getType(fn)->getAs<FunctionType>()) {
75047501
arg = coerceCallArguments(arg, fnType, callee, apply,
75057502
apply->getArgumentLabels(argLabelsScratch),
7506-
hasTrailingClosure,
75077503
locator.withPathElement(
75087504
ConstraintLocator::ApplyArgument));
75097505
if (!arg) {

0 commit comments

Comments
 (0)