Skip to content

Fixes for expression type checking with parse-time name lookup disabled #34088

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

Merged

Conversation

slavapestov
Copy link
Contributor

@slavapestov slavapestov commented Sep 25, 2020

When parse-time lookup is enabled, the parser resolves DeclRefExprs that refer to local bindings. Any remaining UnresolvedDeclRefExprs were handled by preCheckExpression(). When parse-time lookup is disabled, preCheckExpression() is now responsible for resolving all UnresolvedDeclRefExprs itself.

This PR fixes two problems with that:

  • In a handful of places, the constraint solver would walk not-yet or partially type checked function and closure bodies, expecting to find DeclRefExprs for references to locals. This is perhaps a layering violation and better solutions would include either doing preCheckExpression() more eagerly, or finding a way to ensure these walks only happen after type checking has completed. The fix I went with was a suggestion by @DougGregor, which is to just perform the unqualified lookup right there when we see an UnresolvedDeclRefExpr. To help with this, I added a new ASTScope::lookupSingleLocalDecl() entry point that skips lookups into types and at the top level, and does not perform any shadowing checks which might trigger further type checking.

  • When resolving a reference to a ParamDecl, preCheckExpression() would trigger computation of ImplInfoRequest by computing the access kind. This would cache the wrong value and fail later if the parameter was later inferred as an inout by the constraint solver. Previously this problem was masked by parse-time lookup resolving all references to ParamDecls, so we never actually hit this case. My fix is to move the access kind computation to CSApply, since we always have full information there, and nothing in CSGen needs to know the access kind.

Copy link
Contributor

@davidungar davidungar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, except for that one very long function which I bet isn't all your doing. I wouldn't object if you just landed this, but I'd rather that thing were chopped up first.

@@ -6770,6 +6763,37 @@ enum class CtorInitializerKind {
Factory
};

/// Specifies the kind of initialization call performed within the body
/// of the constructor, e.g., self.init or super.init.
enum class BodyInitKind {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good choice to use the enum class vs a straight enum.

@@ -6770,6 +6763,37 @@ enum class CtorInitializerKind {
Factory
};

/// Specifies the kind of initialization call performed within the body
/// of the constructor, e.g., self.init or super.init.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally optional thought: It could nice to add a sentence like: "Needed to ... when ..."

@@ -6818,38 +6842,11 @@ class ConstructorDecl : public AbstractFunctionDecl {

ParamDecl **getImplicitSelfDeclStorage() { return &SelfDecl; }

/// Specifies the kind of initialization call performed within the body
/// of the constructor, e.g., self.init or super.init.
enum class BodyInitKind {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, saw the insertion of this before the deletion. Didn't realize it wasn't a pure addition.

#endif // NDEBUG
namespace {

class ASTScopeDeclConsumerForLocalLookup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the use of an otology here.

Comment on lines 475 to 583
FindReferenceToInitializer(const ConstructorDecl *decl,
ASTContext &ctx)
: Decl(decl), ctx(ctx) { }

bool walkToDeclPre(class Decl *D) override {
// Don't walk into further nominal decls.
return !isa<NominalTypeDecl>(D);
}

std::pair<bool, Expr*> walkToExprPre(Expr *E) override {
// Don't walk into closures.
if (isa<ClosureExpr>(E))
return { false, E };

// Look for calls of a constructor on self or super.
auto apply = dyn_cast<ApplyExpr>(E);
if (!apply)
return { true, E };

auto Callee = apply->getSemanticFn();

Expr *arg;

if (isa<OtherConstructorDeclRefExpr>(Callee)) {
arg = apply->getArg();
} else if (auto *CRE = dyn_cast<ConstructorRefCallExpr>(Callee)) {
arg = CRE->getArg();
} else if (auto *dotExpr = dyn_cast<UnresolvedDotExpr>(Callee)) {
if (dotExpr->getName().getBaseName() != DeclBaseName::createConstructor())
return { true, E };

arg = dotExpr->getBase();
} else {
// Not a constructor call.
return { true, E };
}

// Look for a base of 'self' or 'super'.
arg = arg->getSemanticsProvidingExpr();

BodyInitKind myKind;
if (arg->isSuperExpr())
myKind = BodyInitKind::Chained;
else if (arg->isSelfExprOf(Decl, /*sameBase*/true))
myKind = BodyInitKind::Delegating;
else if (auto *declRef = dyn_cast<UnresolvedDeclRefExpr>(arg)) {
// FIXME: We can see UnresolvedDeclRefExprs here because we have
// not yet run preCheckExpression() on the entire function body
// yet.
//
// We could consider pre-checking more eagerly.
auto name = declRef->getName();
auto loc = declRef->getLoc();
if (name.isSimpleName(ctx.Id_self)) {
auto *otherSelfDecl =
ASTScope::lookupSingleLocalDecl(Decl->getParentSourceFile(),
name.getFullName(), loc);
if (otherSelfDecl == Decl->getImplicitSelfDecl())
myKind = BodyInitKind::Delegating;
}
}
else {
// We're constructing something else.
return { true, E };
}

if (Kind == BodyInitKind::None) {
Kind = myKind;

InitExpr = apply;
return { true, E };
}

// If the kind changed, complain.
if (Kind != myKind) {
// The kind changed. Complain.
ctx.Diags.diagnose(E->getLoc(), diag::init_delegates_and_chains);
ctx.Diags.diagnose(InitExpr->getLoc(), diag::init_delegation_or_chain,
Kind == BodyInitKind::Chained);
}

return { true, E };
}
};

auto &ctx = decl->getASTContext();
FindReferenceToInitializer finder(decl, ctx);
decl->getBody()->walk(finder);

// get the kind out of the finder.
auto Kind = finder.Kind;

auto *NTD = decl->getDeclContext()->getSelfNominalTypeDecl();

// Protocol extension and enum initializers are always delegating.
if (Kind == BodyInitKind::None) {
if (isa<ProtocolDecl>(NTD) || isa<EnumDecl>(NTD)) {
Kind = BodyInitKind::Delegating;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really need a shorthand for "This-function-is-too-long-for-David-to-understand". I couldn't even select it all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you noted I just cut and paste the body of getDelegatingOrChainedInitKind() into BodyInitKindRequest::evaluate() here. Do you mind if I leave this as-is?

@slavapestov
Copy link
Contributor Author

@swift-ci Please smoke test

// yet.
//
// We could consider pre-checking more eagerly.
if (auto *declRef = dyn_cast<UnresolvedDeclRefExpr>(expr)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that this is a body of a multi-statement closure because preCheckExpression should walk into single-statement ones and resolve their refs? I think we can move closure->walk(collectVarRefs); under shouldTypeCheckInEnclosingExpression(closure) to avoid having to handle not-yet-unresolved references, because currently solver handles only single-statement closures...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately test/ConstraintSystem/function_builders.swift ends up with a multi-statement closure here that has not been pre-checked, and if I skip it, the solver fails with unbound free variables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if pre-check should walk to any closure body then, since it's impossible to say whether it's going to be used as a function builder body?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that since preCheckExpression() uses TypeChecker::lookupUnqualified(), it can end up calling getInterfaceType() on the closure’s parameters for example, so in general it’s not valid to pre-check the body before having applied a solution to the expression containing the multi-statement closure. Avoiding the possibility of getInterfaceType() calls on local parameters with inferred type is also why I implemented lookupSingleLocalDecl() separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, could you elaborate on why getInterfaceType() on a parameter is a problem? Since preCheckExpression currently walks into single-statement closures it could find nested closures which wouldn't have any information in the AST from their parents either...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be misunderstanding but it's concerning because this problem with getInterfaceType() on parameter sounds like it would affect any kind of closure...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today, parameter references are resolved earlier in the parser. The unqualified lookup in preCheckExpression() only finds type members and globals, and forward references of local functions.

In general the type checker’s lookup also won’t call getInterfaceType() unless it finds multiple decls and has to do shadowing checks, but that’s not guaranteed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what does this mean for single-statement closures which don't have their type resolved yet but share parameter names with some other declarations from outer context?

var x: Int = 0
let _: (Int) -> Void = { x in
  _ = { x, y in x + y }(x, 42)
}

here either parameter x would be resolved until solution is applied to the AST.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like what we really want to do here is instead of mutating AST generate constraints for UnresolvedDeclRefExpr directly and that way delay lookup until solver just like we do for members today.

… request

This method had a messy contract:

- Setting the diags parameter to nullptr inhibited caching

- The initExpr out parameter could only used if no result
  had yet been cached

Let's instead use the request evaluator here.
This is used in a few places that used to expect parsed but
not yet type-checked code to contain DeclRefExprs that
reference local bindings.

Instead, we can call lookupSingleLocalDecl() with an
UnresolvedDeclRefExpr instead.
… pass

When parse-time lookup is disabled, we have to resolve UnresolvedDeclRefExprs
inside the closure body, since there's no guarantee that preCheckExpression()
has run yet.
It's too early to do that here, because we may be building a reference
to a ParamDecl that is not yet known to be inout, because the constraint
solver has not run yet.

Instead, always compute the access semantics in CSApply.
@slavapestov slavapestov force-pushed the constraint-solver-fixes-for-astscope branch from c6dc676 to 6a82f24 Compare September 25, 2020 22:02
@slavapestov
Copy link
Contributor Author

@swift-ci Please smoke test

@slavapestov
Copy link
Contributor Author

@swift-ci Please test source compatibility

@slavapestov slavapestov merged commit f1c0a49 into swiftlang:main Sep 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants