Skip to content

Commit 4fcac39

Browse files
authored
Merge pull request #26286 from rjmccall/precheck-function-builders
Function builders: pre-check the original closure body in-place
2 parents 755c02c + 33fbfaf commit 4fcac39

File tree

4 files changed

+186
-61
lines changed

4 files changed

+186
-61
lines changed

lib/Sema/BuilderTransform.cpp

Lines changed: 108 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -451,45 +451,6 @@ class BuilderClosureVisitor
451451

452452
} // end anonymous namespace
453453

454-
/// Determine whether the given statement contains a 'return' statement anywhere.
455-
static bool hasReturnStmt(Stmt *stmt) {
456-
class ReturnStmtFinder : public ASTWalker {
457-
public:
458-
bool hasReturnStmt = false;
459-
460-
std::pair<bool, Expr *> walkToExprPre(Expr *expr) override {
461-
return { false, expr };
462-
}
463-
464-
std::pair<bool, Stmt *> walkToStmtPre(Stmt *stmt) override {
465-
// Did we find a 'return' statement?
466-
if (isa<ReturnStmt>(stmt)) {
467-
hasReturnStmt = true;
468-
}
469-
470-
return { !hasReturnStmt, stmt };
471-
}
472-
473-
Stmt *walkToStmtPost(Stmt *stmt) override {
474-
return hasReturnStmt ? nullptr : stmt;
475-
}
476-
477-
std::pair<bool, Pattern*> walkToPatternPre(Pattern *pattern) override {
478-
return { false, pattern };
479-
}
480-
481-
bool walkToDeclPre(Decl *D) override { return false; }
482-
483-
bool walkToTypeLocPre(TypeLoc &TL) override { return false; }
484-
485-
bool walkToTypeReprPre(TypeRepr *T) override { return false; }
486-
};
487-
488-
ReturnStmtFinder finder{};
489-
stmt->walk(finder);
490-
return finder.hasReturnStmt;
491-
}
492-
493454
BraceStmt *
494455
TypeChecker::applyFunctionBuilderBodyTransform(FuncDecl *FD,
495456
BraceStmt *body,
@@ -520,26 +481,36 @@ ConstraintSystem::TypeMatchResult ConstraintSystem::applyFunctionBuilder(
520481
assert(builder && "Bad function builder type");
521482
assert(builder->getAttrs().hasAttribute<FunctionBuilderAttr>());
522483

523-
// Check the form of this closure to see if we can apply the function-builder
524-
// translation at all.
525-
{
526-
// FIXME: Right now, single-expression closures suppress the function
527-
// builder translation.
528-
if (closure->hasSingleExpressionBody())
529-
return getTypeMatchSuccess();
530-
531-
// The presence of an explicit return suppresses the function builder
532-
// translation.
533-
if (hasReturnStmt(closure->getBody())) {
534-
return getTypeMatchSuccess();
535-
}
484+
// FIXME: Right now, single-expression closures suppress the function
485+
// builder translation.
486+
if (closure->hasSingleExpressionBody())
487+
return getTypeMatchSuccess();
488+
489+
// Pre-check the closure body: pre-check any expressions in it and look
490+
// for return statements.
491+
switch (TC.preCheckFunctionBuilderClosureBody(closure)) {
492+
case FunctionBuilderClosurePreCheck::Okay:
493+
// If the pre-check was okay, apply the function-builder transform.
494+
break;
536495

537-
// Check whether we can apply this function builder.
496+
case FunctionBuilderClosurePreCheck::Error:
497+
// If the pre-check had an error, flag that.
498+
return getTypeMatchFailure(locator);
499+
500+
case FunctionBuilderClosurePreCheck::HasReturnStmt:
501+
// If the closure has a return statement, suppress the transform but
502+
// continue solving the constraint system.
503+
return getTypeMatchSuccess();
504+
}
505+
506+
// Check the form of this closure to see if we can apply the
507+
// function-builder translation at all.
508+
{
509+
// Check whether we can apply this specific function builder.
538510
BuilderClosureVisitor visitor(getASTContext(), this,
539511
/*wantExpr=*/false, builderType);
540512
(void)visitor.visit(closure->getBody());
541513

542-
543514
// If we saw a control-flow statement or declaration that the builder
544515
// cannot handle, we don't have a well-formed function builder application.
545516
if (visitor.unhandledNode) {
@@ -584,8 +555,12 @@ ConstraintSystem::TypeMatchResult ConstraintSystem::applyFunctionBuilder(
584555
/*wantExpr=*/true, builderType);
585556
Expr *singleExpr = visitor.visit(closure->getBody());
586557

587-
if (TC.precheckedClosures.insert(closure).second &&
588-
TC.preCheckExpression(singleExpr, closure))
558+
// We've already pre-checked all the original expressions, but do the
559+
// pre-check to the generated expression just to set up any preconditions
560+
// that CSGen might have.
561+
//
562+
// TODO: just build the AST the way we want it in the first place.
563+
if (TC.preCheckExpression(singleExpr, closure))
589564
return getTypeMatchFailure(locator);
590565

591566
singleExpr = generateConstraints(singleExpr, closure);
@@ -613,3 +588,80 @@ ConstraintSystem::TypeMatchResult ConstraintSystem::applyFunctionBuilder(
613588
locator);
614589
return getTypeMatchSuccess();
615590
}
591+
592+
namespace {
593+
594+
/// Pre-check all the expressions in the closure body.
595+
class PreCheckFunctionBuilderClosure : public ASTWalker {
596+
TypeChecker &TC;
597+
ClosureExpr *Closure;
598+
bool HasReturnStmt = false;
599+
bool HasError = false;
600+
public:
601+
PreCheckFunctionBuilderClosure(TypeChecker &tc, ClosureExpr *closure)
602+
: TC(tc), Closure(closure) {}
603+
604+
FunctionBuilderClosurePreCheck run() {
605+
Stmt *oldBody = Closure->getBody();
606+
607+
Stmt *newBody = oldBody->walk(*this);
608+
609+
// If the walk was aborted, it was because we had a problem of some kind.
610+
assert((newBody == nullptr) == (HasError || HasReturnStmt) &&
611+
"unexpected short-circuit while walking closure body");
612+
if (!newBody) {
613+
if (HasError)
614+
return FunctionBuilderClosurePreCheck::Error;
615+
616+
return FunctionBuilderClosurePreCheck::HasReturnStmt;
617+
}
618+
619+
assert(oldBody == newBody && "pre-check walk wasn't in-place?");
620+
621+
return FunctionBuilderClosurePreCheck::Okay;
622+
}
623+
624+
std::pair<bool, Expr *> walkToExprPre(Expr *E) override {
625+
// Pre-check the expression. If this fails, abort the walk immediately.
626+
// Otherwise, replace the expression with the result of pre-checking.
627+
// In either case, don't recurse into the expression.
628+
if (TC.preCheckExpression(E, /*DC*/ Closure)) {
629+
HasError = true;
630+
return std::make_pair(false, nullptr);
631+
}
632+
633+
return std::make_pair(false, E);
634+
}
635+
636+
std::pair<bool, Stmt *> walkToStmtPre(Stmt *S) override {
637+
// If we see a return statement, abort the walk immediately.
638+
if (isa<ReturnStmt>(S)) {
639+
HasReturnStmt = true;
640+
return std::make_pair(false, nullptr);
641+
}
642+
643+
// Otherwise, recurse into the statement normally.
644+
return std::make_pair(true, S);
645+
}
646+
};
647+
648+
}
649+
650+
FunctionBuilderClosurePreCheck
651+
TypeChecker::preCheckFunctionBuilderClosureBody(ClosureExpr *closure) {
652+
// Single-expression closures should already have been pre-checked.
653+
if (closure->hasSingleExpressionBody())
654+
return FunctionBuilderClosurePreCheck::Okay;
655+
656+
// Check whether we've already done this analysis.
657+
auto it = precheckedFunctionBuilderClosures.find(closure);
658+
if (it != precheckedFunctionBuilderClosures.end())
659+
return it->second;
660+
661+
auto result = PreCheckFunctionBuilderClosure(*this, closure).run();
662+
663+
// Cache the result.
664+
precheckedFunctionBuilderClosures.insert(std::make_pair(closure, result));
665+
666+
return result;
667+
}

lib/Sema/TypeChecker.h

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,17 @@ enum class CheckedCastContextKind {
529529
EnumElementPattern,
530530
};
531531

532+
enum class FunctionBuilderClosurePreCheck : uint8_t {
533+
/// There were no problems pre-checking the closure.
534+
Okay,
535+
536+
/// There was an error pre-checking the closure.
537+
Error,
538+
539+
/// The closure has a return statement.
540+
HasReturnStmt,
541+
};
542+
532543
/// The Swift type checker, which takes a parsed AST and performs name binding,
533544
/// type checking, and semantic analysis to produce a type-annotated AST.
534545
class TypeChecker final : public LazyResolver {
@@ -617,8 +628,10 @@ class TypeChecker final : public LazyResolver {
617628
/// when executing scripts.
618629
bool InImmediateMode = false;
619630

620-
/// Closure expressions that have already been prechecked.
621-
llvm::SmallPtrSet<ClosureExpr *, 2> precheckedClosures;
631+
/// Closure expressions whose bodies have already been prechecked as
632+
/// part of trying to apply a function builder.
633+
llvm::DenseMap<ClosureExpr *, FunctionBuilderClosurePreCheck>
634+
precheckedFunctionBuilderClosures;
622635

623636
TypeChecker(ASTContext &Ctx);
624637
friend class ASTContext;
@@ -1169,6 +1182,16 @@ class TypeChecker final : public LazyResolver {
11691182
/// expression and folding sequence expressions.
11701183
bool preCheckExpression(Expr *&expr, DeclContext *dc);
11711184

1185+
/// Pre-check the body of the given closure, which we are about to
1186+
/// generate constraints for.
1187+
///
1188+
/// This mutates the body of the closure, but only in ways that should be
1189+
/// valid even if we end up not actually applying the function-builder
1190+
/// transform: it just does a normal pre-check of all the expressions in
1191+
/// the closure.
1192+
FunctionBuilderClosurePreCheck
1193+
preCheckFunctionBuilderClosureBody(ClosureExpr *closure);
1194+
11721195
/// \name Name lookup
11731196
///
11741197
/// Routines that perform name lookup.

test/Constraints/function_builder.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,52 @@ func testAcceptColorTagged(b: Bool, i: Int, s: String, d: Double) {
301301
}
302302

303303
testAcceptColorTagged(b: true, i: 17, s: "Hello", d: 3.14159)
304+
305+
// rdar://53325810
306+
307+
// Test that we don't have problems with expression pre-checking when
308+
// type-checking an overloaded function-builder call. In particular,
309+
// we need to make sure that expressions in the closure are pre-checked
310+
// before we build constraints for them. Note that top-level expressions
311+
// that need to be rewritten by expression prechecking (such as the operator
312+
// sequences in the boolean conditions and statements below) won't be
313+
// rewritten in the original closure body if we just precheck the
314+
// expressions produced by the function-builder transformation.
315+
struct ForEach1<Data : RandomAccessCollection, Content> {
316+
var data: Data
317+
var content: (Data.Element) -> Content
318+
319+
func show() {
320+
print(content(data.first!))
321+
print(content(data.last!))
322+
}
323+
}
324+
extension ForEach1 where Data.Element: StringProtocol {
325+
// Checking this overload shouldn't trigger inappropriate caching that
326+
// affects checking the next overload.
327+
init(_ data: Data,
328+
@TupleBuilder content: @escaping (Data.Element) -> Content) {
329+
self.init(data: data, content: content)
330+
}
331+
}
332+
extension ForEach1 where Data == Range<Int> {
333+
// This is the overload we actually want.
334+
init(_ data: Data,
335+
@TupleBuilder content: @escaping (Int) -> Content) {
336+
self.init(data: data, content: content)
337+
}
338+
}
339+
let testForEach1 = ForEach1(-10 ..< 10) { i in
340+
"testForEach1"
341+
if i < 0 {
342+
"begin"
343+
i < -5
344+
} else {
345+
i > 5
346+
"end"
347+
}
348+
}
349+
testForEach1.show()
350+
351+
// CHECK: ("testForEach1", main.Either<(Swift.String, Swift.Bool), (Swift.Bool, Swift.String)>.first("begin", true))
352+
// CHECK: ("testForEach1", main.Either<(Swift.String, Swift.Bool), (Swift.Bool, Swift.String)>.second(true, "end"))

test/Constraints/function_builder_diags.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func testDiags() {
7676
tuplify(true) { _ in
7777
17
7878
for c in name { // expected-error{{closure containing control flow statement cannot be used with function builder 'TupleBuilder'}}
79+
// expected-error@-1 {{use of unresolved identifier 'name'}}
7980
}
8081
}
8182

@@ -97,11 +98,11 @@ func testDiags() {
9798
struct A { }
9899
struct B { }
99100

100-
func overloadedTuplify<T>(_ cond: Bool, @TupleBuilder body: (Bool) -> T) -> A {
101+
func overloadedTuplify<T>(_ cond: Bool, @TupleBuilder body: (Bool) -> T) -> A { // expected-note {{found this candidate}}
101102
return A()
102103
}
103104

104-
func overloadedTuplify<T>(_ cond: Bool, @TupleBuilderWithoutIf body: (Bool) -> T) -> B {
105+
func overloadedTuplify<T>(_ cond: Bool, @TupleBuilderWithoutIf body: (Bool) -> T) -> B { // expected-note {{found this candidate}}
105106
return B()
106107
}
107108

@@ -114,7 +115,7 @@ func testOverloading(name: String) {
114115

115116
let _: A = a1
116117

117-
_ = overloadedTuplify(true) { b in
118+
_ = overloadedTuplify(true) { b in // expected-error {{ambiguous use of 'overloadedTuplify(_:body:)'}}
118119
b ? "Hello, \(name)" : "Goodbye"
119120
42
120121
overloadedTuplify(false) {

0 commit comments

Comments
 (0)