llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT--> @llvm/pr-subscribers-clang Author: Yan (likeamahoney) <details> <summary>Changes</summary> Fixes [174301](https://github.com/llvm/llvm-project/issues/174301) When a dependent qualified type is written without the `typename` keyword (the C++20 / extension "implicit typename" recovery), its nested-name-specifier is parsed while "entering" a context. In a class or at namespace scope the parser cannot yet tell a type-specifier apart from an out-of-line member definition, so it parses the prefix in entering mode. As a side effect an intermediate dependent member -- e.g. `B` in `A<T>::B::C` - is speculatively resolved against the template pattern instead of being left as a dependent name. Such a resolved nested-name-specifier does not survive instantiation: the member is never re-resolved against the instantiated parent, so the qualified type stays spuriously dependent in otherwise non-dependent code. That dependent type then makes CTAD / `auto` deduction silently bail out, leaving an undeduced placeholder that crashes codegen with: Assertion `!A->getDeducedType().isNull() && "cannot request the size of an undeduced or dependent auto type"' failed. Once `getTypeName` knows the qualified-id names a type (the implicit-typename path), rebuild the qualifier so that every component resolved against a dependent (non current-instantiation) context becomes a `DependentNameType` - exactly the type produced when `typename` is written explicitly. This is the recovery the existing FIXME in `ActOnTemplateIdType` refers to. Out-of-line definitions are parsed as declarators and do not reach this path, so they are unaffected. --- Full diff: https://github.com/llvm/llvm-project/pull/205762.diff 3 Files Affected: - (modified) clang/docs/ReleaseNotes.rst (+4) - (modified) clang/lib/Sema/SemaDecl.cpp (+91) - (added) clang/test/SemaTemplate/implicit-typename-instantiation.cpp (+52) ``````````diff diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index d4c286644033b..a0764ae606492 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -812,6 +812,10 @@ Bug Fixes to C++ Support - Fixed an issue where Clang incorrectly accepted invalid unqualified uses of local nested class names outside their declaring scope. (#GH184622) - Fixed a crash when parsing invalid friend declaration with storage-class specifier. (#GH186569) - Fixed a missing vtable for ``dynamic_cast<FinalClass *>(this)`` in a function template. (#GH198511) +- Fixed a crash when a dependent qualified type whose nested-name-specifier contains an + intermediate dependent member (e.g. ``A<T>::B::C``) was written without the ``typename`` + keyword. The intermediate member was speculatively resolved against the template pattern and + the resulting type spuriously stayed dependent after instantiation. (#GH174301) Bug Fixes to AST Handling ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index d45c3eb35094f..71f9c9f016f8b 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -273,6 +273,82 @@ static ParsedType recoverFromTypeInKnownDependentBase(Sema &S, return S.CreateParsedType(T, Builder.getTypeSourceInfo(Context, T)); } +/// A dependent qualified type written without the 'typename' keyword (accepted +/// as the C++20 / extension "implicit typename" recovery) has its +/// nested-name-specifier parsed while "entering" a context. As a side effect, +/// intermediate dependent members -- e.g. 'B' in 'A<T>::B::C' -- are +/// speculatively resolved against the template pattern instead of being left as +/// dependent names. Such a resolved nested-name-specifier does not survive +/// template instantiation: the member is never re-resolved against the +/// instantiated parent, so a spuriously dependent type leaks into otherwise +/// non-dependent code (and later crashes e.g. CTAD / codegen). +/// +/// Rebuild the qualifier so that every component resolved against a dependent +/// (non current-instantiation) context becomes a DependentNameType, matching +/// the type produced when 'typename' is written explicitly. +static NestedNameSpecifierLoc +rebuildDependentlyResolvedQualifier(Sema &S, NestedNameSpecifierLoc QLoc) { + if (!QLoc || QLoc.getNestedNameSpecifier().getKind() != + NestedNameSpecifier::Kind::Type) + return QLoc; + + TypeLoc TL = QLoc.castAsTypeLoc(); + + // Only members that were resolved to a concrete declaration (typedef, tag, or + // using) carry a qualifier + name we could re-express as a dependent name. + NestedNameSpecifierLoc PrefixLoc; + SourceLocation KeywordLoc, NameLoc; + const IdentifierInfo *II = nullptr; + if (auto T = TL.getAs<TypedefTypeLoc>()) { + PrefixLoc = T.getQualifierLoc(); + KeywordLoc = T.getElaboratedKeywordLoc(); + NameLoc = T.getNameLoc(); + II = T.getDecl()->getIdentifier(); + } else if (auto T = TL.getAs<TagTypeLoc>()) { + PrefixLoc = T.getQualifierLoc(); + KeywordLoc = T.getElaboratedKeywordLoc(); + NameLoc = T.getNameLoc(); + II = T.getDecl()->getIdentifier(); + } else if (auto T = TL.getAs<UsingTypeLoc>()) { + PrefixLoc = T.getQualifierLoc(); + KeywordLoc = T.getElaboratedKeywordLoc(); + NameLoc = T.getNameLoc(); + II = T.getDecl()->getIdentifier(); + } else if (auto T = TL.getAs<UnresolvedUsingTypeLoc>()) { + PrefixLoc = T.getQualifierLoc(); + KeywordLoc = T.getElaboratedKeywordLoc(); + NameLoc = T.getNameLoc(); + II = T.getDecl()->getIdentifier(); + } else { + return QLoc; + } + + if (!II || !PrefixLoc) + return QLoc; + + NestedNameSpecifierLoc NewPrefixLoc = + rebuildDependentlyResolvedQualifier(S, PrefixLoc); + + // The member can only be left unresolved (a dependent name) if its prefix is + // itself dependent; otherwise the original resolution is correct. + if (!NewPrefixLoc.getNestedNameSpecifier().isDependent()) + return QLoc; + + ASTContext &Ctx = S.Context; + QualType DNT = Ctx.getDependentNameType( + ElaboratedTypeKeyword::None, NewPrefixLoc.getNestedNameSpecifier(), II); + + TypeLocBuilder TLB; + auto DTL = TLB.push<DependentNameTypeLoc>(DNT); + DTL.setElaboratedKeywordLoc(KeywordLoc); + DTL.setQualifierLoc(NewPrefixLoc); + DTL.setNameLoc(NameLoc); + + CXXScopeSpec Rebuilt; + Rebuilt.Make(Ctx, TLB.getTypeLocInContext(Ctx, DNT), QLoc.getEndLoc()); + return Rebuilt.getWithLocInContext(Ctx); +} + ParsedType Sema::getTypeName(const IdentifierInfo &II, SourceLocation NameLoc, Scope *S, CXXScopeSpec *SS, bool isClassName, bool HasTrailingDot, ParsedType ObjectTypePtr, @@ -298,6 +374,21 @@ ParsedType Sema::getTypeName(const IdentifierInfo &II, SourceLocation NameLoc, if (!LookupCtx) { if (isDependentScopeSpecifier(*SS)) { + // The qualifier is dependent and we now know (from the grammar) that + // this names a type. If it was parsed while entering a context, an + // intermediate dependent member may have been speculatively resolved + // against the template pattern; rebuild it as a dependent name so it + // survives instantiation. See rebuildDependentlyResolvedQualifier. + if (NestedNameSpecifierLoc QLoc = SS->getWithLocInContext(Context)) { + NestedNameSpecifierLoc NewQLoc = + rebuildDependentlyResolvedQualifier(*this, QLoc); + if (NewQLoc.getNestedNameSpecifier() != + QLoc.getNestedNameSpecifier()) { + SS->clear(); + SS->Adopt(NewQLoc); + } + } + // C++ [temp.res]p3: // A qualified-id that refers to a type and in which the // nested-name-specifier depends on a template-parameter (14.6.2) diff --git a/clang/test/SemaTemplate/implicit-typename-instantiation.cpp b/clang/test/SemaTemplate/implicit-typename-instantiation.cpp new file mode 100644 index 0000000000000..22c8e23b2d92c --- /dev/null +++ b/clang/test/SemaTemplate/implicit-typename-instantiation.cpp @@ -0,0 +1,52 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=cxx17 -std=c++17 %s +// RUN: %clang_cc1 -fsyntax-only -verify=cxx20 -std=c++20 %s +// cxx20-no-diagnostics + +// When 'typename' is omitted from a dependent qualified type whose +// nested-name-specifier contains an intermediate dependent member +// (e.g. the 'Inner' / 'Node_traits' below), the implicit-typename recovery +// used to leave the intermediate member speculatively resolved against the +// template pattern. That nested-name-specifier did not survive instantiation, +// producing a spuriously dependent type that crashed CTAD and codegen. +// Omitting and writing 'typename' must produce the same type. + +namespace intermediate_typedef { +template <class T> struct Iter { T *p; }; +template <class T> struct Traits { typedef Iter<T> Const_iterator; }; +template <class T> struct ListBase { typedef Traits<T> Node_traits; }; + +template <class T> struct List { + // cxx17-warning@+1 {{missing 'typename' prior to dependent type name 'ListBase<T>::Node_traits::Const_iterator' is a C++20 extension}} + typedef ListBase<T>::Node_traits::Const_iterator const_iterator; + const_iterator begin() { return const_iterator(); } +}; + +struct S {}; +template <class U> void use(U) {} + +void test() { + List<S> lst; + Iter it = lst.begin(); // CTAD; previously crashed. + use(it); + static_assert(__is_same(decltype(it), Iter<S>)); +} +} // namespace intermediate_typedef + +namespace intermediate_tag { +// Reduced from llvm/llvm-project#174301. +template <class T> struct Tester { + struct Inner { using T2 = int; }; + static void test(); +}; + +template <class T> +// cxx17-warning@+1 {{missing 'typename' prior to dependent type name 'Tester<T>::Inner::T2' is a C++20 extension}} +Tester<T>::Inner::T2 getInnerT2() { return {}; } + +template <class T> void Tester<T>::test() { + auto x = getInnerT2<T>(); + static_assert(__is_same(decltype(x), int)); +} + +template struct Tester<int>; +} // namespace intermediate_tag `````````` </details> https://github.com/llvm/llvm-project/pull/205762 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
