Skip to content

[clang] WIP: Improved Context Declaration tracking #107942

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mizvekov
Copy link
Contributor

@mizvekov mizvekov commented Sep 10, 2024

This patch aims to improve how parenting relationships are represented in the AST.

Currently regular declarations can only be children of special AST nodes which inherit from DeclContext, except for a few special cases which are required by itanium mangling.

Having this parenting relationship well described helps in tracking which entities are dependent, and an improvement here would allow us to drop a lot of workarounds and to get some difficult cases right, such as lambdas and requires expressions appearing within return types and function parameter types of templated functions.

This patch extends the ContextDecl tracking which currently is used for mangling, in order to cover almost needed cases.

Template type aliases represent a specially difficult case which is addressed by this patch.
They can be contexts for other declarations, but besides not being DeclContexts, they also lack a declaration which represents their specialization.

This patch addresses the type alias context problem by storing the specialization arguments along with the ContextDecl for entities declared in their context.

TODO:

  • Move away from using ExpressionEvaluationContexts, and use the same stack used for tracking the CurContext. Remove the separate ContextDecl field, and have that functionally built in to the base Decl class, so that we don't separatelly track both a parent Decl and a parent DeclContext.

  • A function type has ParmVarDecls, and these can appear inside aliases and template type aliases in particular, so a ParmVarDecl needs a ContextDecl too.

This patch aims to improve how parenting relationships
are represented in the AST.

Currently regular declarations can only be children of
special AST nodes which inherit from DeclContext, except
for a few special cases which are required by itanium mangling.

Having this parenting relationship well described helps
in tracking which entities are dependent, and an improvement
here would allow us to drop a lot of workarounds and to
get some difficult cases right.

This patch extends the ContextDecl tracking which currently
is used for mangling, in order to cover almost needed cases.

Template type aliases represent a specially difficult case
which is addressed by this patch.
They can be contexts for other declarations, but besides not
being DeclContexts, they also lack a declaration which
represents their specialization.

This patch addresses the type alias context problem by
storing the specialization arguments along with the ContextDecl
for entities declared in their context.

TODO:

* Move away from using ExpressionEvaluationContexts, and use the same
stack used for tracking the CurContext. Remove the separate ContextDecl
field, and have that functionally built in to the base Decl class,
so that we don't separatelly treack both a parent Decl and a parent
DeclContext.

* A function type has ParmVarDecls, and these can appear inside
aliases and template type aliases in particular, so a ParmvarDecl
needs a ContextDecl too.
@mizvekov mizvekov requested a review from zygoloid September 10, 2024 00:38
@mizvekov mizvekov self-assigned this Sep 10, 2024
// so early, e.g. because we want to see if any of the *substituted*
// parameters are dependent.
DependencyKind = getDerived().ComputeLambdaDependency(&LSICopy);
Class->setLambdaDependencyKind(DependencyKind);
Copy link
Contributor

Choose a reason for hiding this comment

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

So after the removal of such, lines 14911 - 14914 would also be useless.

Copy link
Contributor

@zyn0217 zyn0217 left a comment

Choose a reason for hiding this comment

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

Thanks. Just some thoughts/questions in my mind, and I may need more time to understand the PR.

@@ -6291,7 +6307,8 @@ bool Parser::isDeclarationSpecifier(
bool Parser::isConstructorDeclarator(bool IsUnqualified, bool DeductionGuide,
DeclSpec::FriendSpecified IsFriend,
const ParsedTemplateInfo *TemplateInfo) {
RevertingTentativeParsingAction TPA(*this);
RevertingTentativeParsingAction TPA(*this, /*Unannotated=*/true);
Sema::SFINAETrap Trap(Actions);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need a SFINAETrap here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To suppress a bunch of diagnostics that would otherwise be duplicated when we parse the thing again for real.

@@ -961,6 +961,7 @@ static const Expr *SubstituteConstraintExpressionWithoutSatisfaction(

if (MLTAL.getNumSubstitutedLevels() == 0)
return ConstrExpr;
MLTAL.setKind(TemplateSubstitutionKind::Rewrite);
Copy link
Contributor

Choose a reason for hiding this comment

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

I presume this avoids creating Subst* nodes for the substitution, right?
Could you explain what we would gain from / the difference it makes this way?

Copy link
Contributor

Choose a reason for hiding this comment

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

I remember I had tried using RewriteKind while fixing the mismatched constraint issue. But that turned out to cause some regressions/crashes in the end. Don't know what kind of situation it would be with this patch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new difference introduced in this patch is that a Rewrite is depth preserving, while a specialization is not.

In this case, we want to rewrite a requires clause from another potential redeclaration to see if it matches, and confirm it really is a redeclaration.

If we don't preserve the depth here, this would trip a new assert, where we would be instantiating a requires clause at template depth 0, but then parenting it to a declaration at depth 1, which is incorrect.

Comment on lines +15268 to +15271
if (auto *TD = dyn_cast<FunctionTemplateDecl>(D))
D = TD->getTemplatedDecl();
else
assert(isa<FunctionDecl>(D));
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe

assert((isa<FunctionDecl, FunctionTemplateDecl>(D)));
if (auto *FTD = dyn_cast<>)
  ...

?

Comment on lines +18812 to +18817
static auto skipRequiresBody(DeclContext *DC) {
while (DC->isRequiresExprBody())
DC = DC->getParent();
return DC;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

We could move it to tryCaptureVariable, inlining it as a lambda to avoid such small functions.

// Exception: Function parameters are not tied to the function's DeclContext
// until we enter the function definition. Capturing them anyway would result
// in an out-of-bounds error while traversing DC and its parents.
if (isa<ParmVarDecl>(Var) && !VarDC->isFunctionOrMethod())
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the rationale behind it is that every ParmVarDecl now has a context decl pointing to e.g. its function? Do we still need to set the ParmVarDecl's context separately (by calling setOwningFunction) now?
Sorry I haven't gotten around to the changes on VarDecls yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We still do need to reparent them the old way, but this is going to change once I give them a ContextDecl too.

The difference right now is that we don't create the ParmVarDecls always at the translation unit context anymore, we just create them at the CurContext.

@@ -19000,7 +19056,7 @@ bool Sema::tryCaptureVariable(
Explicit = false;
FunctionScopesIndex--;
if (IsInScopeDeclarationContext)
DC = ParentDC;
DC = skipRequiresBody(ParentDC);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, do you have a test for it? e.g. the requires expression contains a lambda that contains another requires expression which would cause a crash for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is one of your tests actually:

clang/test/SemaCXX/lambda-unevaluated.cpp

// GH88081: Test if we evaluate the requires expression with lambda captures properly.
namespace GH88081 {

// Test that ActOnLambdaClosureQualifiers() is called only once.
void foo(auto value)
  requires requires { [&] -> decltype(value) {}; }
  // expected-error@-1 {{non-local lambda expression cannot have a capture-default}}
{}

The difference is that now the requires expression is properly reparented to foo, where as before it would just sit at the namespace.

ExprEvalContexts.back().ManglingContextDecl)) {
if (auto ContextDecl = currentEvaluationContext().ContextDecl;
auto *VD =
dyn_cast_or_null<VarDecl>(ContextDecl ? *ContextDecl : nullptr)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

dyn_cast_if_present, if we have to touch it anyway :)

Comment on lines +17261 to +17265
const auto &PrevRec = ExprEvalContexts.back();
PushExpressionEvaluationContext(NewContext, PrevRec.ContextDecl,
PrevRec.ContextArgs, ExprContext);
ExprEvalContexts.back().HasReusedDeclContext = true;
ExprEvalContexts.back().LazyContextDeclPos = PrevRec.LazyContextDeclPos;
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI, PrevRec would be invalidated after PushExpressionEvaluationContext(), so you probably want to save the value before the call.

This caused clang/test/SemaTemplate/stack-exhaustion.cpp to fail on my machine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I saw this failure on CI, but not on my machine (MacOS).

Thanks for taking a look.

Copy link
Contributor

@zyn0217 zyn0217 left a comment

Choose a reason for hiding this comment

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

I took another pass through the code, and it looks good overall.

Given the scale of this patch, input from others would be valuable. It would be nice if you could move the patch out of draft status to get more visibility :)

@@ -7946,6 +7980,8 @@ class Sema final : public SemaBase {
/// A stack of expression evaluation contexts.
SmallVector<ExpressionEvaluationContextRecord, 8> ExprEvalContexts;

SmallVector<Decl *, 8> PendingLazyContextDecls;
Copy link
Contributor

Choose a reason for hiding this comment

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

So from a quick reading, PendingLazyContextDecls is used for lambdas created as part of other declarations, such as being part of a default argument for a template parameter. Their context declarations are updated only at the end of the evaluation context or right after the outer declaration is created.

I think we'd better document its intent, wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intention is that they are used for any declarations where its parent is created later. Sure will document this.

ContextDecl.hasValue() ? *ContextDecl
: ContextDeclOrSentinel(TemplateDepth),
currentEvaluationContext().ContextArgs);
if (!ContextDecl.hasValue())
Copy link
Contributor

Choose a reason for hiding this comment

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

I find it more likely for one to conflate the semantics of hasValue() with those of operator bool whereas the latter actually indicates whether this is in a null state (i.e. neither the pointer nor the depth is assigned).

For clarity, could we rename it to something more descriptive, like hasDeclPointer()?

else if (!(isa<FunctionDecl>(D) || isa<VarDecl>(D) ||
isa<VarTemplateSpecializationDecl>(D))) {
D->dumpColor();
llvm_unreachable("FU");
Copy link
Contributor

Choose a reason for hiding this comment

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

FU?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Debugging leftover, will remove.

assert(Rec.LazyContextDeclPos <= PendingLazyContextDecls.size());
if (!Rec.HasReusedDeclContext) {
if (Rec.ContextDecl.hasValue()) {
assert(Rec.LazyContextDeclPos == PendingLazyContextDecls.size());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this an assumption for that

  1. if the lambda (or something else we want to later add context decl for) is aware of its parent context decl at the point of its creation, then there should not be any pointer in PendingLazyContextDecls tracking the lambda.

  2. the subsequent on-stack evaluation contexts should have handled PendingLazyContextDecls properly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's basically it. If we have already created the parent, we shouldn't be using the Pending list at all.
If this originally started as a lazy context, then we are in a state where we know the parent, so we should have already handled and emptied the lazy list, and shouldn't be adding new stuff to it anymore.

} else {
while (PendingLazyContextDecls.size() > Rec.LazyContextDeclPos) {
Decl *D = PendingLazyContextDecls.pop_back_val();
setContextDecl(*this, D, nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there anything special for the third argument being null? Both setLambdaContextDecl() and handleLambdaNumbering() would bail out for such cases, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the error path. Basically we had a bunch of declarations waiting for their parents to be created, but for some reason we bailed out and we are never going to create their parents anymore.

Before this patch, these declarations would have stayed parented to their parent's parent.

These declarations would never make it to codegen, so don't matter much except for analysis.
The upside is that by staying as lazycontext entities, we don't lose track of their template depth.

Comment on lines +3656 to +3658
LocalInstantiationScope InstScope(getActions());
for (ParmVarDecl *D : LocalParameterDecls)
InstScope.InstantiatedLocal(D, D);
Copy link
Contributor

Choose a reason for hiding this comment

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

Where would we use that?

@mizvekov
Copy link
Contributor Author

Noted. Sorry I haven't responded yet, I have other priorities and it might take a while before I get back to this patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants