Skip to content

ASTScope: Redo 'selfDC' computation #33989

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
merged 7 commits into from
Sep 19, 2020

Conversation

slavapestov
Copy link
Contributor

@slavapestov slavapestov commented Sep 18, 2020

This was motivated by getting self in capture lists working without parse-time name lookup.

@slavapestov slavapestov force-pushed the astscope-self-dc-cleanup branch from bcc7eb1 to a00b5cd Compare September 18, 2020 06:58
…Expr

We'll need this to get the right 'selfDC' when name lookup
finds a 'self' declaration in a capture list, eg

class C {
  func bar() {}
  func foo() {
    _ = { [self] in bar() }
  }
}
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.

I love this direction. Didn't look deeply enough to assure correctness, but very nice to shift the complexity as this is doing. It wouldn't be a review from me without a request to break up a long function, where I think clarity could be improved.

Comment on lines +1254 to +1268
auto *expr = ::new(mem) CaptureListExpr(captureList, closureBody);

for (auto capture : captureList)
capture.Var->setParentCaptureList(expr);

return expr;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer putting the for loop before the CaptureListExpr constructor to compute the nullable CaptureListExpr, then passing it in to the constructor. That way the instance variable could be a const and it would be a bit clearer when reading the code that it is immutable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand. The for loop references the newly-constructed CaptureListExpr instance.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, my mistake. Thanks for pointing it out so gently.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the real issue is not knowing where the var is captured when the var is created. Would be great if the var could be immutable somehow.

/// var decl.
void setParentCaptureList(CaptureListExpr *expr) {
assert(expr != nullptr);
Parent = expr;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can Parent be a const or do VarDecls move from family to family?

/// Encodes whether this is a 'let' binding.
Introducer : 1,

/// Whether this declaration was an element of a capture list.
IsCaptureList : 1,

/// Whether this declaration captures the 'self' param under the same name.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice clean up.

Comment on lines -465 to -527
#pragma mark computeSelfDC

NullablePtr<DeclContext>
GenericTypeOrExtensionWhereOrBodyPortion::computeSelfDC(
ArrayRef<const ASTScopeImpl *> history) {
ASTScopeAssert(history.size() != 0, "includes current scope");
size_t i = history.size() - 1; // skip last entry (this scope)
while (i != 0) {
Optional<NullablePtr<DeclContext>> maybeSelfDC =
history[--i]->computeSelfDCForParent();
if (maybeSelfDC) {
// If we've found a selfDC, we'll definitely be returning something.
// However, we may have captured 'self' somewhere down the tree, so we
// can't return outright without checking the nested scopes.
NullablePtr<DeclContext> nestedCapturedSelfDC =
checkNestedScopesForSelfCapture(history, i);
return nestedCapturedSelfDC ? nestedCapturedSelfDC : *maybeSelfDC;
}
}
return nullptr;
}

#pragma mark checkNestedScopesForSelfCapture

NullablePtr<DeclContext>
GenericTypeOrExtensionWhereOrBodyPortion::checkNestedScopesForSelfCapture(
ArrayRef<const ASTScopeImpl *> history, size_t start) {
NullablePtr<DeclContext> innerCapturedSelfDC;
// Start with the next scope down the tree.
size_t j = start;

// Note: even though having this loop nested inside the while loop from
// GenericTypeOrExtensionWhereOrBodyPortion::computeSelfDC may appear to
// result in quadratic blowup, complexity actually remains linear with respect
// to the size of history. This relies on the fact that
// GenericTypeOrExtensionScope::computeSelfDCForParent returns a null pointer,
// which will cause this method to bail out of the search early. Thus, this
// method is called once per type body in the lookup history, and will not
// end up re-checking the bodies of nested types that have already been
// covered by earlier calls, so the total impact of this method across all
// calls in a single lookup is O(n).
while (j != 0) {
auto *entry = history[--j];
Optional<NullablePtr<DeclContext>> selfDCForParent =
entry->computeSelfDCForParent();

// If we encounter a scope that should cause us to forget the self
// context (such as a nested type), bail out and use whatever the
// the last inner captured context was.
if (selfDCForParent && (*selfDCForParent).isNull())
break;

// Otherwise, if we have a captured self context for this scope, then
// remember it since it is now the innermost scope we have encountered.
NullablePtr<DeclContext> capturedSelfDC = entry->capturedSelfDC();
if (!capturedSelfDC.isNull())
innerCapturedSelfDC = entry->capturedSelfDC();

// Continue searching in the next scope down.
}
return innerCapturedSelfDC;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Lovely!

Comment on lines -596 to -652
#pragma mark computeSelfDCForParent

// If the lookup depends on implicit self, selfDC is its context.
// (Names in extensions never depend on self.)
// Lookup can propagate it up from, say a method to the enclosing type body.

// By default, propagate the selfDC up to a NomExt decl, body,
// or where clause
Optional<NullablePtr<DeclContext>>
ASTScopeImpl::computeSelfDCForParent() const {
return None;
}

// Forget the "self" declaration:
Optional<NullablePtr<DeclContext>>
GenericTypeOrExtensionScope::computeSelfDCForParent() const {
return NullablePtr<DeclContext>();
}

Optional<NullablePtr<DeclContext>>
PatternEntryInitializerScope::computeSelfDCForParent() const {
// Pattern binding initializers are only interesting insofar as they
// affect lookup in an enclosing nominal type or extension thereof.
if (auto *ic = getPatternEntry().getInitContext()) {
if (auto *bindingInit = dyn_cast<PatternBindingInitializer>(ic)) {
// Lazy variable initializer contexts have a 'self' parameter for
// instance member lookup.
if (bindingInit->getImplicitSelfDecl()) {
return NullablePtr<DeclContext>(bindingInit);
}
}
}
return None;
}

Optional<NullablePtr<DeclContext>>
MethodBodyScope::computeSelfDCForParent() const {
return NullablePtr<DeclContext>(decl);
}

#pragma mark capturedSelfDC

// Closures may explicitly capture the self param, in which case the lookup
// should use the closure as the context for implicit self lookups.

// By default, there is no such context to return.
NullablePtr<DeclContext> ASTScopeImpl::capturedSelfDC() const {
return NullablePtr<DeclContext>();
}

// Closures track this information explicitly.
NullablePtr<DeclContext> ClosureParametersScope::capturedSelfDC() const {
if (closureExpr->capturesSelfEnablingImplictSelf())
return NullablePtr<DeclContext>(closureExpr);
return NullablePtr<DeclContext>();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice

Comment on lines +644 to +672

// We're looking for members of a type.
//
// If we started the looking from inside a scope where a 'self' parameter
// is visible, instance members are returned with the 'self' parameter's
// DeclContext as the base, which is how the expression checker knows to
// convert the unqualified reference into a self member access.
auto resultFinder = UnqualifiedLookupFactory::ResultFinderForTypeContext(
&factory, selfDC ? selfDC.get() : scopeDC, scopeDC);
&factory, candidateSelfDC ? candidateSelfDC : scopeDC, scopeDC);
const bool isCascadingUse =
calculateIsCascadingUse(factory.getInitialIsCascadingUse());
factory.findResultsAndSaveUnavailables(scopeDC, std::move(resultFinder),
isCascadingUse, factory.baseNLOptions);
factory.recordCompletionOfAScope();

// We're done looking inside a nominal type declaration. It is possible
// that this nominal type is nested inside of another type, in which case
// we will visit the outer type next. Make sure to clear out the known
// 'self' parameeter context, since any members of the outer type are
// not accessed via the innermost 'self' parameter.
candidateSelfDC = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

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

The new comments are great!

…ut parse-time lookup

The DeclRefExpr here was resolved by parse-time lookup.
Without it, it's an UnresolvedDeclRefExpr.
@slavapestov slavapestov force-pushed the astscope-self-dc-cleanup branch from 1a73364 to e115b2f Compare September 18, 2020 17:35
@slavapestov
Copy link
Contributor Author

@swift-ci Please smoke test

This centralizes some invariants around the 'self' parameter.
While all ConstructorDecls and DestructorDecls have a 'self',
even if they're invalid because they're not nested inside a type,
we don't want to consider this as the base 'self' for lookups.

Eg, consider this invalid code:

class C {
  func f() {
    init() {
      x
    }
  }
}

The base for the lookup should be f.self, not f.init.self.
…fiedLookup

Instead of having ASTScope compute 'selfDC', the lookup
consumer can compute it on its own, by looking for
bindings named 'self'.
@slavapestov slavapestov force-pushed the astscope-self-dc-cleanup branch from e115b2f to f4083ba Compare September 18, 2020 19:06
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.

The selfDC computation is better, but maybe in a future PR it could be even better.

// references. This is used by lookInMembers().
if (auto *var = dyn_cast<VarDecl>(value)) {
if (var->getName() == factory.Ctx.Id_self) {
maybeUpdateSelfDC(var);
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that you've refactored--thanks!--I can see more clearly what is going on with candidateSelfDC. Is it a fragile piece of mutable state? Could we pass a piece of immutable state through the lookup recursion instead? It seems to be a bit odd that a consume method has a side effect. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was actually going to try to refactor the lookup to use a loop that walks the parents instead of recursing. This will allow us to implement the one-level lookup we need to do re-declaration checking for locals defined in the same scope. Maybe then the selfDC computation can go there and be totally explicit, instead of being part of consume().

@slavapestov slavapestov force-pushed the astscope-self-dc-cleanup branch from f4083ba to 0712d0b Compare September 18, 2020 19:48
@slavapestov slavapestov force-pushed the astscope-self-dc-cleanup branch from 0712d0b to d4cc35a Compare September 18, 2020 20:11
@slavapestov
Copy link
Contributor Author

swiftlang/llvm-project#1820
@swift-ci Please smoke test

@slavapestov
Copy link
Contributor Author

@swift-ci Please test source compatibility

@slavapestov slavapestov merged commit 1e0a9ca into swiftlang:master Sep 19, 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.

2 participants