Skip to content

[clang] Improve nested name specifier AST representation #147835

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 2 commits into from
Aug 9, 2025

Conversation

mizvekov
Copy link
Contributor

@mizvekov mizvekov commented Jul 9, 2025

This is a major change on how we represent nested name qualifications in the AST.

  • The nested name specifier itself and how it's stored is changed. The prefixes for types are handled within the type hierarchy, which makes canonicalization for them super cheap, no memory allocation required. Also translating a type into nested name specifier form becomes a no-op. An identifier is stored as a DependentNameType. The nested name specifier gains a lightweight handle class, to be used instead of passing around pointers, which is similar to what is implemented for TemplateName. There is still one free bit available, and this handle can be used within a PointerUnion and PointerIntPair, which should keep bit-packing aficionados happy.
  • The ElaboratedType node is removed, all type nodes in which it could previously apply to can now store the elaborated keyword and name qualifier, tail allocating when present.
  • TagTypes can now point to the exact declaration found when producing these, as opposed to the previous situation of there only existing one TagType per entity. This increases the amount of type sugar retained, and can have several applications, for example in tracking module ownership, and other tools which care about source file origins, such as IWYU. These TagTypes are lazily allocated, in order to limit the increase in AST size.

This patch offers a great performance benefit.

It greatly improves compilation time for stdexec. For one datapoint, for test_on2.cpp in that project, which is the slowest compiling test, this patch improves -c compilation time by about 7.2%, with the -fsyntax-only improvement being at ~12%.

This has great results on compile-time-tracker as well:
image

This patch also further enables other optimziations in the future, and will reduce the performance impact of template specialization resugaring when that lands.

It has some other miscelaneous drive-by fixes.

About the review: Yes the patch is huge, sorry about that. Part of the reason is that I started by the nested name specifier part, before the ElaboratedType part, but that had a huge performance downside, as ElaboratedType is a big performance hog. I didn't have the steam to go back and change the patch after the fact.

There is also a lot of internal API changes, and it made sense to remove ElaboratedType in one go, versus removing it from one type at a time, as that would present much more churn to the users. Also, the nested name specifier having a different API avoids missing changes related to how prefixes work now, which could make existing code compile but not work.

How to review: The important changes are all in clang/include/clang/AST and clang/lib/AST, with also important changes in clang/lib/Sema/TreeTransform.h.

The rest and bulk of the changes are mostly consequences of the changes in API.

PS: TagType::getDecl is renamed to getOriginalDecl in this patch, just for easier to rebasing. I plan to rename it back after this lands.

Fixes #136624
Fixes #43179
Fixes #68670
Fixes #92757

@mizvekov
Copy link
Contributor Author

We are seeing an assertion failure in one of Fuchsia's builders due to this:

Can you please attach the crash reproducer which clang should have generated? Thanks.

@stbergmann
Copy link
Collaborator

After this commit, the comment in clang/include/clang/AST/Decl.h for clang::TypeDecl::getTypeForDecl,

  // Low-level accessor. If you just want the type defined by this node,
  // check out ASTContext::getTypeDeclType or one of
  // ASTContext::getTypedefType, ASTContext::getRecordType, etc. if you
  // already know the specific kind of node this is.

isn't accurate any more, there's no more ASTContext::getRecordType.

@kadircet
Copy link
Member

Observation using clangd: In the following construct
...
The "arcana" field of the AST node of "Foo" inside the function reports the "QualType" as "Foo", whereas before it was "N::Foo". Is that expected?

It looks correct in that this is the as-written type.

Now I am not sure clangd wants to present the type that way, maybe they want to fully qualify it for printing instead? @kadircet @hokein

clangd doesn't really have a preference here, we just surface -ast-dump in a editor friendly format, for debugging purposes.

one can repro the situation with clang -xc++ -fsyntax-only -Xclang=-ast-dump a.cc

where a.cc is:

namespace N {
struct Foo {};
}  // namespace N
void f() {
  using namespace N;
  Foo foo;
  N::Foo x;
}

pre this change output looks like:

`-FunctionDecl 0x5621625282a0 <line:4:1, line:8:1> line:4:6 f 'void ()'
  `-CompoundStmt 0x562162528d10 <col:10, line:8:1>
    |-DeclStmt 0x5621625283e8 <line:5:3, col:20>
    | `-UsingDirectiveDecl 0x562162528390 <col:3, col:19> col:19 Namespace 0x562162528018 'N'
    |-DeclStmt 0x562162528b88 <line:6:3, col:10>
    | `-VarDecl 0x562162528440 <col:3, col:7> col:7 foo 'Foo':'N::Foo' callinit
    |   `-CXXConstructExpr 0x562162528b60 <col:7> 'Foo':'N::Foo' 'void () noexcept'
    `-DeclStmt 0x562162528cf8 <line:7:3, col:11>
      `-VarDecl 0x562162528c38 <col:3, col:10> col:10 x 'N::Foo' callinit
        `-CXXConstructExpr 0x562162528cd0 <col:10> 'N::Foo' 'void () noexcept'

emphasis on -VarDecl 0x562162528440 <col:3, col:7> col:7 foo 'Foo':'N::Foo' callinit

after thins change this is now reported as VarDecl 0x5652a7615860 <col:3, col:7> col:7 foo 'Foo' callinit

this was handled by https://github.com/llvm/llvm-project/blob/main/clang/lib/AST/TextNodeDumper.cpp#L909-L926, I am not sure if change is intended (i think we should still be able to detect & desugar)

@bolshakov-a
Copy link
Contributor

The new TraverseQualifier argument through the whole Traverse.*Type hierarchy ...

@mizvekov, anyway, it would be good to know if TraverseQualifier is a customization point for users who implement their own RecursiveASTVisitor subclasses, or the default argument value should always be true? There is an assertion inside TraverseQualifiedTypeLoc:

assert(TraverseQualifier &&

However, I don't see where in the world QualifiedTypeLoc is created...

@mizvekov
Copy link
Contributor Author

After this commit, the comment in clang/include/clang/AST/Decl.h for clang::TypeDecl::getTypeForDecl,
isn't accurate any more, there's no more ASTContext::getRecordType.

Thanks for reporting, I'll fix in a follow up patch.

Observation using clangd: In the following construct
this was handled by https://github.com/llvm/llvm-project/blob/main/clang/lib/AST/TextNodeDumper.cpp#L909-L926, I am not sure if change is intended (i think we should still be able to detect & desugar)

Right, the difference here is that before this patch, if you print a fully desugared tag type (that's what the 'aka' in the diagnostic does), the type is printed fully qualified. That's because the name qualifiers were carried in the 'ElaboratedType' sugar node, and we used to handle this situation specially: we assumed a bare tag type only occurred because of canonicalization.

With this patch the tag types themselves carry the name qualifier, so just removing the top level sugar doesn't make them lose name qualifier information.

We still fully qualify canonical tag types when printing them though, but that now happens because a canonical tag type is a special value which is not identical to any other tag type for the same declaration.

We may still want to fully qualify types for the 'aka' diagnostic, but I deferred that for a follow up patch in order to limit the amount of test churn.

For clangd, I assume the same thing is happening here, for this arcana" field, you are fully desugaring the type before printing it.

@mizvekov, anyway, it would be good to know if TraverseQualifier is a customization point for users who implement their own RecursiveASTVisitor subclasses, or the default argument value should always be true?

The TraverseQualifier flag means, 'should I traverse NestedNameSpecifiers?'

If you are simply customizing one of the Traverse* methods which take it, but still delegating to the base class Traverse method, you should just forward that flag along to the base method.

If you are implementing your own Traverse method for a type from scratch, then if this flag is false, you should not traverse the nested name specifier for that type.

There is an assertion inside TraverseQualifiedTypeLoc:

assert(TraverseQualifier &&

However, I don't see where in the world QualifiedTypeLoc is created...

QualifiedTypeLoc is created implicitly for QualTypes which have top level qualifiers, and they represent the source locations for type qualifiers (not name qualifiers, different thing.)

This assert here means that a top level type qualifier can't appear within a nested name specifier (but it could appear for example in the underlying type for a typedef).

The C++ grammar doesn't allow you to write const / volatile in a way which applies to an entity in the name qualifier itself.

While such a NestedNameSpecifier could still be produced as part of template argument deduction, we simply remove the top level qualifiers in that case, as the NestedNameSpecifier AST node itself still can't represent it.

@mysterymath
Copy link
Contributor

We are seeing an assertion failure in one of Fuchsia's builders due to this:

Can you please attach the crash reproducer which clang should have generated? Thanks.

https://storage.googleapis.com/fuchsia-artifacts/builds/8707018386865583409/build-debug/clang-crashreports/zircon-startup-bad996.tar

@bolshakov-a
Copy link
Contributor

@mizvekov, thanks for the clarifying! In fact, it means that at least TraverseTypeLoc should not, in general, be given false as TraverseQualifier argument because (it looks like) nothing inside RecursiveASTVisitor changes it to true before TraverseQualifiedTypeLoc call. Note that it may be called not only from the NNS traversal.

@mizvekov
Copy link
Contributor Author

mizvekov commented Aug 12, 2025

@mizvekov, thanks for the clarifying! In fact, it means that at least TraverseTypeLoc should not, in general, be given false as TraverseQualifier argument because (it looks like) nothing inside RecursiveASTVisitor changes it to true before TraverseQualifiedTypeLoc call. Note that it may be called not only from the NNS traversal.

Yeah, it may only given false in the implementation of TraverseNestedNameSpecifier, which for out-of-tree users is only relevant if you are overriding that. If you mistakenly passed true instead, the name qualifiers would be traversed twice.

@bolshakov-a
Copy link
Contributor

I mean that I cannot do this:

struct MyVisitor : RecursiveASTVisitor<MyVisitor> {
  using Base = RecursiveASTVisitor<MyVisitor>;

  bool TraverseTypeLoc(TypeLoc TL, bool TraverseQualifier = false) {
    return Base::TraverseTypeLoc(TL, TraverseQualifier);
  }
};

Calling MyVisitor::TraverseTypeLoc on e.g. const int would cause the assertion failure.

aarongable pushed a commit to chromium/chromium that referenced this pull request Aug 12, 2025
llvm/llvm-project#147835 makes minor diagnostic changes.

Accept the previous and new diagnostic messages.

Bug: 437910658
Change-Id: Iec24f4e20944465fe12a87539822c095bb88979f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6842043
Reviewed-by: Nico Weber <[email protected]>
Commit-Queue: Arthur Eubanks <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1500458}
@emaxx-google
Copy link
Contributor

We are seeing an assertion failure in one of Fuchsia's builders due to this:

Can you please attach the crash reproducer which clang should have generated? Thanks.

https://storage.googleapis.com/fuchsia-artifacts/builds/8707018386865583409/build-debug/clang-crashreports/zircon-startup-bad996.tar

Minimized this using C-Vise to 620 bytes: https://pastebin.com/yegBAfpe

ArthurSonzogni pushed a commit to ArthurSonzogni/PartitionAlloc that referenced this pull request Aug 13, 2025
llvm/llvm-project#147835 makes minor diagnostic changes.

Accept the previous and new diagnostic messages.

Bug: 437910658
Change-Id: Iec24f4e20944465fe12a87539822c095bb88979f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6842043
Reviewed-by: Nico Weber <[email protected]>
Commit-Queue: Arthur Eubanks <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1500458}
NOKEYCHECK=True
GitOrigin-RevId: 83aa2e61488debc4572256e81768123605224687
github-actions bot pushed a commit to kaidokert/chrome_base_mirror that referenced this pull request Aug 13, 2025
llvm/llvm-project#147835 makes minor diagnostic changes.

Accept the previous and new diagnostic messages.

Bug: 437910658
Change-Id: Iec24f4e20944465fe12a87539822c095bb88979f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6842043
Reviewed-by: Nico Weber <[email protected]>
Commit-Queue: Arthur Eubanks <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1500458}
NOKEYCHECK=True
GitOrigin-RevId: 83aa2e61488debc4572256e81768123605224687
aarongable pushed a commit to chromium/chromium that referenced this pull request Aug 13, 2025
Upstream recently made sweeping changes to some AST representations
(llvm/llvm-project#147835), so we need to update our plugin to match. Since it involves syntax/type changes, we need to
check if we're building at head or not.

Some tests have slightly different diagnostic messages (e.g. an extra `base::`). Since the test framework doesn't make it easy to have two golden files, just suppress these tests until we roll past this.

Bug: 437910658
Change-Id: Icf3ffbb5eed6f3728cd267407eaf1821ea378845
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6842037
Auto-Submit: Arthur Eubanks <[email protected]>
Reviewed-by: Devon Loehr <[email protected]>
Reviewed-by: Nico Weber <[email protected]>
Commit-Queue: Devon Loehr <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1500577}
mizvekov added a commit that referenced this pull request Aug 13, 2025
…153344)

This fixes a regression reported here
#147835 (comment),
where getTrivialTemplateArgumentLoc can't see through template name
sugar when producing a trivial TemplateArgumentLoc for template template
arguments.

Since this regression was never released, there are no release notes.
@mizvekov
Copy link
Contributor Author

Minimized this using C-Vise to 620 bytes: https://pastebin.com/yegBAfpe

Thanks, that has been fixed here: #153344

llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Aug 13, 2025
…e argument (#153344)

This fixes a regression reported here
llvm/llvm-project#147835 (comment),
where getTrivialTemplateArgumentLoc can't see through template name
sugar when producing a trivial TemplateArgumentLoc for template template
arguments.

Since this regression was never released, there are no release notes.
@bolshakov-a
Copy link
Contributor

The following fails as well:

struct MyVisitor : RecursiveASTVisitor<MyVisitor> {
} visitor;
visitor.TraverseTypeLoc(typeloc, false);

if the given typeloc is const int.

So you insist that TraverseQualifier is not a customization point, and users should keep the default argument of both TraverseType and TraverseTypeLoc untouched? It is sad a little because IWYU needs to avoid NNS traversal in some places. Previously, it was achieved just by desugaring ElaboratedTypeLoc. Now, I tried to do that by means of passing false as the TraverseQualifier argument, and it even seems to work because IWYU AST visitor transforms QualifiedTypeLoc into UnqualTypeLoc, and the assertion is not reached. However, I may be missing some of consequences, or if it is just "not a documented way", then we need to implement our own flag to block the traversal inside an own overload of TraverseNestedNameSpecifier...

@mizvekov
Copy link
Contributor Author

If you want to avoid traversal of NNSes, why don't you just override TraverseNestedNameSpecifier{,Loc} with no-ops?
I think that's the intended way to do it.

@bolshakov-a
Copy link
Contributor

They should be avoided not always but in certain circumstances, so there should be a new flag field in the visitor class. Not many new code, TBH, but using TraverseQualifier was so tempting...

@mizvekov
Copy link
Contributor Author

We can make it work, we could move the assert elsewhere.

@bolshakov-a
Copy link
Contributor

bolshakov-a commented Aug 13, 2025

The assert doesn't hurt us, as I've said. I just asked if it is a "documented way" of working with the new visitor interface. If it is not to be avoided, then we will probably just proceed with the TraverseQualifier manipulation.

@mizvekov
Copy link
Contributor Author

It would be hard for us to document this as an intended interface, but leave that assert in the way, even if you found yourself a way to avoid it. This wouldn't look very polished.

@bolshakov-a
Copy link
Contributor

@kimgr, what do you think?

facebook-github-bot pushed a commit to facebook/folly that referenced this pull request Aug 13, 2025
Summary:
Clang recently made major changes to the AST representation of nested name specifiers in [llvm-project#147835](llvm/llvm-project#147835). While waiting for Bindgen to be updated to understand the new representation, this diff changes our Bindgen invocation on `folly::IOBuf` to disregard the nested name `folly::IOBuf::Iterator`, which is unused anyway in the Rust `IOBuf` binding.

This unblocks staging builds for the LLVM Server Compiler team.

This diff causes the following difference in the Bindgen-generated Rust binding observed through `buck2 build fbcode//mode/opt fbcode//folly/rust/iobuf:iobuf-sys-bindgen`:

```lang=diff
@@ -571,8 +571,6 @@
         pub const IOBuf_CombinedOption_SEPARATE: root::folly::IOBuf_CombinedOption = 2;
         pub type IOBuf_CombinedOption = ::std::os::raw::c_int;
         pub type IOBuf_value_type = root::folly::ByteRange;
-        pub type IOBuf_iterator = root::folly::IOBuf_Iterator;
-        pub type IOBuf_const_iterator = root::folly::IOBuf_Iterator;
         pub type IOBuf_FreeFunction = ::std::option::Option<
             unsafe extern "C" fn(
                 buf: *mut ::std::os::raw::c_void,
@@ -765,31 +763,6 @@
                 "Offset of field: IOBuf::sharedInfo_",
             ][::std::mem::offset_of!(IOBuf, sharedInfo_) - 48usize];
         };
-        #[repr(C)]
-        #[derive(Debug, Copy, Clone)]
-        pub struct IOBuf_Iterator {
-            pub pos_: *const root::folly::IOBuf,
-            pub end_: *const root::folly::IOBuf,
-            pub val_: root::folly::ByteRange,
-        }
-        #[allow(clippy::unnecessary_operation, clippy::identity_op)]
-        const _: () = {
-            [
-                "Size of IOBuf_Iterator",
-            ][::std::mem::size_of::<IOBuf_Iterator>() - 32usize];
-            [
-                "Alignment of IOBuf_Iterator",
-            ][::std::mem::align_of::<IOBuf_Iterator>() - 8usize];
-            [
-                "Offset of field: IOBuf_Iterator::pos_",
-            ][::std::mem::offset_of!(IOBuf_Iterator, pos_) - 0usize];
-            [
-                "Offset of field: IOBuf_Iterator::end_",
-            ][::std::mem::offset_of!(IOBuf_Iterator, end_) - 8usize];
-            [
-                "Offset of field: IOBuf_Iterator::val_",
-            ][::std::mem::offset_of!(IOBuf_Iterator, val_) - 16usize];
-        };
     }
     pub mod facebook {
         #[allow(unused_imports)]
```

Bindgen issue: [bindgen#3264](rust-lang/rust-bindgen#3264)

Reviewed By: HighW4y2H3ll

Differential Revision: D80186965

fbshipit-source-id: 9f0546a2964b5d4e5c67e6bdd91b542e3e5f7b2c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend:AArch64 backend:AMDGPU backend:ARC backend:ARM backend:CSKY backend:Hexagon backend:Lanai backend:loongarch backend:MIPS backend:PowerPC backend:RISC-V backend:Sparc backend:SystemZ backend:WebAssembly backend:X86 clang:analysis clang:as-a-library libclang and C++ API clang:bytecode Issues for the clang bytecode constexpr interpreter clang:codegen IR generation bugs: mangling, exceptions, etc. clang:dataflow Clang Dataflow Analysis framework - https://clang.llvm.org/docs/DataFlowAnalysisIntro.html clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang:openmp OpenMP related changes to Clang clang:static analyzer clang Clang issues not falling into any other category clang-tidy clang-tools-extra clangd coroutines C++20 coroutines debuginfo HLSL HLSL Language Support libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. lldb
Projects
None yet