From add953ae1da01d5ab2722bc65585d769657a4ee7 Mon Sep 17 00:00:00 2001 From: ykiko Date: Wed, 29 Jul 2026 22:58:24 +0800 Subject: [PATCH 01/32] refactor(semantic): rewrite TemplateResolver without Sema/TreeTransform --- src/compile/compilation.cpp | 4 +- src/semantic/resolver.cpp | 1255 +++++++++-------- src/semantic/resolver.h | 26 +- src/semantic/unifier.cpp | 411 ++++++ src/semantic/unifier.h | 83 ++ .../unit/semantic/template_resolver_tests.cpp | 48 + 6 files changed, 1224 insertions(+), 603 deletions(-) create mode 100644 src/semantic/unifier.cpp create mode 100644 src/semantic/unifier.h diff --git a/src/compile/compilation.cpp b/src/compile/compilation.cpp index e1e10adce..f78a138a9 100644 --- a/src/compile/compilation.cpp +++ b/src/compile/compilation.cpp @@ -320,8 +320,8 @@ CompilationStatus CompilationUnitRef::Self::run_clang( self.run_tidy(); - if(instance.hasSema()) { - self.resolver.emplace(instance.getSema()); + if(instance.hasASTContext()) { + self.resolver.emplace(instance.getASTContext()); } return CompilationStatus::Completed; diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 53f61da0a..4b2740f3a 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -3,29 +3,33 @@ #include #include +#include "semantic/unifier.h" #include "support/logging.h" -#include "clang/Sema/Template.h" -#include "clang/Sema/TemplateDeduction.h" -#include "clang/Sema/TreeTransform.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/DeclTemplate.h" /// Template Resolver — pseudo-instantiation of dependent C++ types. /// /// Architecture: -/// PseudoInstantiator (TreeTransform) — heuristic lookup in primary templates/partial specs -/// ├─ TransformDependentNameType — lookup member in template, substitute, recurse -/// ├─ TransformDependentTemplateSPTType — resolve DTST via hole()/lookup, CTD→TST -/// ├─ TransformTemplateTypeParmType — substitute from stack (+ default arg fallback) -/// ├─ TransformTypedefType — delegate to SubstituteOnly (no lookup) -/// └─ TransformType — depth guard + null safety +/// PseudoInstantiator — heuristic lookup in primary templates/partial specs, +/// driven by a hand-written QualType → QualType rewriter with two policies: +/// ├─ Policy::Substitute — expand typedefs/aliases and substitute template +/// │ parameters from the stack; dependent names pass +/// │ through untouched (no lookup) +/// └─ Policy::Resolve — Substitute plus heuristic resolution of +/// DependentNameType/DependentTemplateSpecializationType +/// via member lookup and argument deduction /// -/// SubstituteOnly (TreeTransform) — Phase 2: typedef expansion + param substitution only -/// Does NOT override TransformDependentNameType → no heuristic lookup → breaks cycles. +/// Key invariant: typedef/alias expansion always runs under Policy::Substitute, +/// so it can never re-enter heuristic lookup. Violating this causes +/// typedef ↔ lookup infinite cycles. /// -/// Key invariant: Phase 2 (SubstituteOnly) must NEVER trigger Phase 1 (heuristic lookup). -/// Violating this causes typedef ↔ lookup infinite cycles. -/// -/// See docs: temp/template-resolver-analysis.md, temp/resolver-vector-pipeline.md +/// Everything is pure AST computation (TypeUnifier + ASTContext node +/// construction); Sema and TreeTransform are deliberately not used, so +/// resolution cannot emit diagnostics or mutate the unit's semantic state. namespace clice { @@ -68,77 +72,27 @@ void visit_template_decl_contexts(clang::Decl* decl, const Callback& callback) { } } -/// Resugar canonical TemplateTypeParmType with original parameter declarations. -/// TreeTransform's TransformType(QualType) materializes a trivial -/// TypeSourceInfo at getBaseLocation(). That location must be valid: -/// keyword-carrying dependent types (e.g. `typename T::type`) otherwise -/// produce a TypeLoc whose keyword is set but whose KeywordLoc is invalid, -/// tripping Sema::CheckTypenameType's assertion (and reading garbage -/// location data in release builds). -inline clang::SourceLocation transform_base_location(clang::Sema& sema) { - auto& SM = sema.getSourceManager(); - return SM.getLocForStartOfFile(SM.getMainFileID()); -} - -class ResugarOnly : public clang::TreeTransform { -public: - ResugarOnly(clang::Sema& sema, clang::Decl* decl) : - TreeTransform(sema), context(sema.getASTContext()), - base_location(transform_base_location(sema)) { - visit_template_decl_contexts(decl, - [&](clang::Decl* decl, clang::TemplateParameterList* params) { - lists.push_back(params); - }); - std::ranges::reverse(lists); - } - - /// TreeTransform's setBase is a no-op; the CRTP contract is to override - /// getBaseLocation. See transform_base_location for why it must be valid. - clang::SourceLocation getBaseLocation() { - return base_location; - } - - clang::QualType TransformTemplateTypeParmType(clang::TypeLocBuilder& TLB, - clang::TemplateTypeParmTypeLoc TL, - bool = false) { - clang::QualType type = TL.getType(); - auto TTPT = TL.getTypePtr(); - if(!TTPT->getDecl()) { - auto depth = TTPT->getDepth(); - if(depth >= lists.size()) { - auto NewTL = TLB.push(type); - NewTL.setNameLoc(getBaseLocation()); - return NewTL.getType(); - } - auto index = TTPT->getIndex(); - auto isPack = TTPT->isParameterPack(); - auto param = llvm::cast(lists[depth]->getParam(index)); - type = context.getTemplateTypeParmType(depth, index, isPack, param); - } - auto NewTL = TLB.push(type); - NewTL.setNameLoc(getBaseLocation()); - return NewTL.getType(); - } - -private: - clang::ASTContext& context; - clang::SourceLocation base_location; - llvm::SmallVector lists; -}; - /// A helper class to record the instantiation stack. struct InstantiationStack { using Arguments = llvm::SmallVector; using TemplateArguments = llvm::ArrayRef; - llvm::SmallVector> data; + struct Frame { + clang::Decl* decl; + clang::TemplateParameterList* params; + Arguments arguments; + }; + + llvm::SmallVector data; bool empty() const { return data.empty(); } - void push(clang::Decl* decl, TemplateArguments arguments) { - data.emplace_back(decl, arguments); + void push(clang::Decl* decl, + clang::TemplateParameterList* params, + TemplateArguments arguments) { + data.emplace_back(decl, params, Arguments(arguments.begin(), arguments.end())); } void pop() { @@ -149,42 +103,51 @@ struct InstantiationStack { return data; } - /// Look up a template type parameter in the stack by matching its depth against - /// each frame's template parameter list depth. Searches from innermost (top) to - /// outermost (bottom). Returns nullptr if no matching frame or index out of range. + /// Look up a template parameter's binding, innermost frame first. /// - /// IMPORTANT: depth alone identifies the template "level", not the specific template. - /// Different templates at the same depth (e.g. vector and test both at depth 0) will - /// match the FIRST frame found. Callers must ensure the stack only contains relevant - /// frames when calling this. - const clang::TemplateArgument* find_argument(const clang::TemplateTypeParmType* T) const { - auto depth = T->getDepth(); - auto index = T->getIndex(); - for(auto it = data.rbegin(); it != data.rend(); ++it) { - clang::TemplateParameterList* params = nullptr; - if(auto* CTD = llvm::dyn_cast(it->first)) { - params = CTD->getTemplateParameters(); - } else if(auto* CTPSD = llvm::dyn_cast( - it->first)) { - params = CTPSD->getTemplateParameters(); - } else if(auto* TATD = llvm::dyn_cast(it->first)) { - params = TATD->getTemplateParameters(); - } else if(auto* FTD = llvm::dyn_cast(it->first)) { - params = FTD->getTemplateParameters(); + /// When the parameter's declaration is known, only the frame whose + /// parameter list actually contains that declaration matches — unrelated + /// templates that merely share the same depth (e.g. `test` and + /// `__alloc_traits`, both at depth 0) never capture each other's + /// parameters. If no frame owns the declaration, the parameter is left + /// unsubstituted rather than guessed. + /// + /// Canonical parameters carry no declaration; those fall back to matching + /// the frame's parameter list depth. + const clang::TemplateArgument* find_argument(const clang::NamedDecl* decl, + unsigned depth, + unsigned index) const { + if(decl) { + for(const auto& frame: std::ranges::reverse_view(data)) { + if(frame.params && index < frame.params->size() && + frame.params->getParam(index) == decl) { + if(index < frame.arguments.size()) { + return &frame.arguments[index]; + } + return nullptr; + } } - if(params && params->getDepth() == depth) { - if(index < it->second.size()) { - return &it->second[index]; + return nullptr; + } + + for(const auto& frame: std::ranges::reverse_view(data)) { + if(frame.params && frame.params->getDepth() == depth) { + if(index < frame.arguments.size()) { + return &frame.arguments[index]; } return nullptr; } } return nullptr; } + + const clang::TemplateArgument* find_argument(const clang::TemplateTypeParmType* T) const { + return find_argument(T->getDecl(), T->getDepth(), T->getIndex()); + } }; /// Helper to extract underlying type from a Decl. -static clang::QualType get_decl_type(clang::Decl* decl) { +clang::QualType get_decl_type(clang::Decl* decl) { if(!decl) return clang::QualType(); if(auto* TND = llvm::dyn_cast(decl)) @@ -194,31 +157,51 @@ static clang::QualType get_decl_type(clang::Decl* decl) { return clang::QualType(); } -/// Phase 2 substitution transform. Expands typedefs and substitutes template parameters -/// from the InstantiationStack, but does NOT override TransformDependentNameType. +/// The core pseudo-instantiation engine. Resolves dependent names by looking up +/// members in primary templates and partial specializations — a capability +/// clang's own instantiation machinery does not have (it only ever sees +/// concrete arguments, so e.g. alias templates never appear on its paths). /// -/// This is critical: the base class TransformDependentNameType just substitutes params -/// in the qualifier and rebuilds the DependentNameType — it does NOT do our heuristic -/// lookup. This breaks the typedef ↔ lookup cycle that would occur if typedef expansion -/// triggered PseudoInstantiator's TransformDependentNameType. +/// Resolution flow for `typename A::type`: +/// 1. rewrite() dispatches the DependentNameType to resolve_dependent_name +/// 2. lookup(A, "type") → deduce_template_arguments → find member decl +/// 3. substitute(underlying_type) expands typedefs + substitutes params +/// 4. Pop lookup frames, then rewrite the result for further resolution /// -/// Handles: TypedefType, ElaboratedType, InjectedClassNameType, alias TST, TTPT. -/// Does NOT handle: multi-element pack expansion, NTTP, template template params. -class SubstituteOnly : public clang::TreeTransform { - using Base = clang::TreeTransform; - +/// Uses active_resolutions / active_ctd_lookups for cycle detection. +class PseudoInstantiator { public: - SubstituteOnly(clang::Sema& sema, InstantiationStack& stack) : - Base(sema), context(sema.getASTContext()), stack(stack), - base_location(transform_base_location(sema)) {} + using TemplateArguments = llvm::ArrayRef; - clang::SourceLocation getBaseLocation() { - return base_location; + PseudoInstantiator(clang::ASTContext& context, + llvm::DenseMap& resolved, + unsigned parent_indent = 0) : + context(context), resolved(resolved), indent(parent_indent) {} + + /// Rewrite policy. The two-phase split is the resolver's core invariant: + /// typedef/alias expansion must never re-enter heuristic lookup, or + /// mutually recursive typedefs cycle forever. + enum class Policy { + /// Expand sugar and substitute stack parameters only. + Substitute, + /// Substitute plus dependent name resolution through lookup. + Resolve, + }; + + clang::QualType resolve(clang::QualType type) { + return rewrite(type, Policy::Resolve); } - using Base::TransformType; + clang::QualType substitute(clang::QualType type) { + return rewrite(type, Policy::Substitute); + } - clang::QualType TransformType(clang::QualType type) { + /// Entry point for all type rewriting. Guards against: + /// - Null types (return as-is) + /// - Non-dependent types (no transformation needed) + /// - Excessive recursion depth (bail out to prevent runaway recursion) + /// - Null results (return original type instead) + clang::QualType rewrite(clang::QualType type, Policy policy) { if(type.isNull() || !type->isDependentType()) { return type; } @@ -226,152 +209,29 @@ class SubstituteOnly : public clang::TreeTransform { return type; } ++depth; - auto result = Base::TransformType(type); + auto result = rewrite_type(type, policy); --depth; return result.isNull() ? type : result; } - /// Desugar dependent typedefs to expose template parameters for substitution. - clang::QualType TransformTypedefType(clang::TypeLocBuilder& TLB, clang::TypedefTypeLoc TL) { - if(auto* TND = TL.getTypedefNameDecl()) { - auto underlying = TND->getUnderlyingType(); - if(underlying->isDependentType()) { - auto type = TransformType(underlying); - if(!type.isNull()) { - if(auto ET = llvm::dyn_cast(type)) { - type = ET->getNamedType(); - } - TLB.pushTrivial(context, type, getBaseLocation()); - return type; - } - } - } - return Base::TransformTypedefType(TLB, TL); - } - - clang::QualType TransformElaboratedType(clang::TypeLocBuilder& TLB, - clang::ElaboratedTypeLoc TL) { - clang::QualType type = TransformType(TL.getNamedTypeLoc().getType()); - if(type.isNull()) { - return Base::TransformElaboratedType(TLB, TL); - } - TLB.pushTrivial(context, type, getBaseLocation()); - return type; - } - - clang::QualType TransformInjectedClassNameType(clang::TypeLocBuilder& TLB, - clang::InjectedClassNameTypeLoc TL) { - auto ICT = TL.getTypePtr(); - clang::QualType type = TransformType(ICT->getInjectedSpecializationType()); - if(type.isNull()) { - return Base::TransformInjectedClassNameType(TLB, TL); - } - TLB.pushTrivial(context, type, getBaseLocation()); - return type; - } - - using Base::TransformTemplateSpecializationType; - - clang::QualType TransformTemplateSpecializationType(clang::TypeLocBuilder& TLB, - clang::TemplateSpecializationTypeLoc TL) { - if(TL.getTypePtr()->isTypeAlias()) { - clang::QualType type = TransformType(TL.getTypePtr()->desugar()); - if(!type.isNull()) { - TLB.pushTrivial(context, type, getBaseLocation()); - return type; - } - } - return Base::TransformTemplateSpecializationType(TLB, TL); - } - - /// Substitute template parameters from the stack. - clang::QualType TransformTemplateTypeParmType(clang::TypeLocBuilder& TLB, - clang::TemplateTypeParmTypeLoc TL, - bool = false) { - auto* T = TL.getTypePtr(); - - if(auto* arg = stack.find_argument(T)) { - clang::QualType type; - - if(arg->getKind() == clang::TemplateArgument::Type) { - type = arg->getAsType(); - } else if(arg->getKind() == clang::TemplateArgument::Pack) { - auto pack = arg->getPackAsArray(); - if(pack.size() == 1 && pack[0].getKind() == clang::TemplateArgument::Type) { - type = pack[0].getAsType(); - } - } - - // TODO(pack): Only handles single-element packs (common pack forwarding case). - // Multi-element packs (e.g. Us... = {int, float}) are not expanded here and - // will fall through to return the original type. - if(!type.isNull()) { - TLB.pushTrivial(context, type, TL.getNameLoc()); - return type; - } - } - - // No substitution: return original type unchanged. - TLB.push(TL.getType()).setNameLoc(TL.getNameLoc()); - return TL.getType(); - } - - // TransformDependentNameType is NOT overridden. - // Base class behavior: transforms the qualifier (substitutes params there), - // then rebuilds the DependentNameType. No lookup. - -private: - clang::ASTContext& context; - clang::SourceLocation base_location; - InstantiationStack& stack; - unsigned depth = 0; -}; - -/// The core pseudo-instantiation engine. Extends TreeTransform to resolve dependent -/// names by looking up members in primary templates and partial specializations — -/// a capability clang's own TemplateInstantiator does not have. -/// -/// Resolution flow for `typename A::type`: -/// 1. TransformDependentNameType intercepts the DependentNameType -/// 2. lookup(A, "type") → deduce_template_arguments → find member decl -/// 3. substitute(underlying_type) → SubstituteOnly expands typedefs + substitutes params -/// 4. Pop lookup frames, then TransformType on result for further resolution -/// -/// Uses SubstituteOnly for Phase 2 to avoid typedef ↔ lookup cycles. -/// Uses active_resolutions / active_ctd_lookups for cycle detection. -class PseudoInstantiator : public clang::TreeTransform { -public: - using Base = clang::TreeTransform; - - using TemplateArguments = llvm::ArrayRef; - - using TemplateDeductionInfo = clang::sema::TemplateDeductionInfo; - - PseudoInstantiator(clang::Sema& sema, - llvm::DenseMap& resolved, - unsigned parent_indent = 0) : - Base(sema), sema(sema), context(sema.getASTContext()), resolved(resolved), - indent(parent_indent), base_location(transform_base_location(sema)) {} - - clang::SourceLocation getBaseLocation() { - return base_location; - } + using lookup_result = clang::DeclContext::lookup_result; -public: - /// Use SubstituteOnly to expand typedefs and substitute parameters without doing lookup. - clang::QualType substitute(clang::QualType type) { - if(type.isNull() || !type->isDependentType()) { - return type; - } - SubstituteOnly subst(sema, stack); - auto result = subst.TransformType(type); - return result.isNull() ? type : result; + /// When DeclContext::lookup returns multiple declarations (e.g. a member in + /// both a base class and derived class), take the last one. This heuristic + /// favors the most-derived declaration, though the ordering depends on clang's + /// internal DeclContext storage. + clang::Decl* preferred(lookup_result members) { + clang::Decl* decl = nullptr; + std::ranges::for_each(members, [&](auto member) { decl = member; }); + return decl; } /// Verify that `arguments` match `TD`'s parameter list, filling in default - /// template arguments where needed. Default args are substituted using the - /// current stack (via SubstituteOnly), so parameters already provided can - /// appear in default expressions (e.g. `allocator<_Tp>` for vector's `_Alloc`). + /// template arguments where needed. Type defaults are substituted using the + /// current stack, so parameters already provided can appear in default + /// expressions (e.g. `allocator<_Tp>` for vector's `_Alloc`). Non-type and + /// template template defaults are filled when representable without + /// building expressions. bool check_template_arguments(clang::TemplateDecl* TD, TemplateArguments& arguments, llvm::SmallVectorImpl& out) { @@ -381,73 +241,91 @@ class PseudoInstantiator : public clang::TreeTransform { out.emplace_back(arg); } - if(out.size() != list->size()) { - for(auto i = out.size(); i < list->size(); ++i) { - auto param = list->getParam(i); - // TODO(nttp): Only TemplateTypeParmDecl default arguments are handled. - // NonTypeTemplateParmDecl and TemplateTemplateParmDecl defaults are skipped, - // causing check_template_arguments to return false for templates like: - // template struct S; - auto TTPD = llvm::dyn_cast(param); - if(TTPD && TTPD->hasDefaultArgument()) { - auto type = TTPD->getDefaultArgument().getArgument().getAsType(); - - stack.push(TD, out); - auto result = substitute(type); - stack.pop(); + for(auto i = out.size(); i < list->size(); ++i) { + auto param = list->getParam(i); - if(result.isNull()) { - return false; - } + if(auto TTPD = llvm::dyn_cast(param); + TTPD && TTPD->hasDefaultArgument()) { + auto type = TTPD->getDefaultArgument().getArgument().getAsType(); - LOG_DEBUG( - "{}" "default arg: '{}' = '{}'", - pad(), - TTPD->getNameAsString(), - result.getAsString()); - out.emplace_back(result); + stack.push(TD, list, out); + auto result = substitute(type); + stack.pop(); + + if(result.isNull()) { + return false; } + + LOG_DEBUG( + "{}" "default arg: '{}' = '{}'", + pad(), + TTPD->getNameAsString(), + result.getAsString()); + out.emplace_back(result); + continue; + } + + if(auto NTTPD = llvm::dyn_cast(param); + NTTPD && NTTPD->hasDefaultArgument()) { + auto& argument = NTTPD->getDefaultArgument().getArgument(); + if(argument.getKind() != clang::TemplateArgument::Expression) { + out.emplace_back(argument); + continue; + } + auto expr = argument.getAsExpr(); + if(!expr->isValueDependent()) { + if(auto value = expr->getIntegerConstantExpr(context)) { + out.emplace_back( + clang::TemplateArgument(context, *value, NTTPD->getType())); + continue; + } + } + break; } + + if(auto TTPD = llvm::dyn_cast(param); + TTPD && TTPD->hasDefaultArgument()) { + out.emplace_back(TTPD->getDefaultArgument().getArgument()); + continue; + } + + break; } - if(out.size() != list->size()) { - return false; + if(out.size() == list->size()) { + return true; } - return true; + /// A parameter pack absorbs any number of surplus arguments (and may + /// stay empty), so exact arity only applies to pack-free lists. + return list->hasParameterPack() && out.size() + 1 >= list->size(); } template bool deduce_template_arguments(Decl* decl, TemplateArguments arguments) { clang::TemplateParameterList* list = nullptr; - TemplateArguments params = {}; + TemplateArguments patterns = {}; if constexpr(std::is_same_v) { const clang::ClassTemplateDecl* CTD = decl; list = CTD->getTemplateParameters(); - params = list->getInjectedTemplateArgs(context); + patterns = list->getInjectedTemplateArgs(context); } else if constexpr(std::is_same_v) { const clang::ClassTemplatePartialSpecializationDecl* CTPSD = decl; list = CTPSD->getTemplateParameters(); - params = CTPSD->getTemplateArgs().asArray(); + patterns = CTPSD->getTemplateArgs().asArray(); } else if constexpr(std::is_same_v) { const clang::TypeAliasTemplateDecl* TATD = decl; list = TATD->getTemplateParameters(); - params = list->getInjectedTemplateArgs(context); + patterns = list->getInjectedTemplateArgs(context); } else { static_assert(dependent_false, "Unknown declaration type"); } assert(list && "No template parameters found"); - TemplateDeductionInfo info = {clang::SourceLocation(), list->getDepth()}; - llvm::SmallVector deduced(list->size()); - - auto result = sema.DeduceTemplateArguments(list, params, arguments, info, deduced, true); - bool success = - result == clang::TemplateDeductionResult::Success && !info.hasSFINAEDiagnostic(); - - if(!success) { + llvm::SmallVector deduced; + if(!deduce_arguments(context, list, patterns, arguments, deduced)) { return false; } @@ -460,13 +338,12 @@ class PseudoInstantiator : public clang::TreeTransform { visit_template_decl_contexts( llvm::dyn_cast(decl->getDeclContext()), [&](clang::Decl* decl, clang::TemplateParameterList* params) { - stack.push(decl, params->getInjectedTemplateArgs(context)); + stack.push(decl, params, params->getInjectedTemplateArgs(context)); }); std::ranges::reverse(stack.frames()); } - llvm::SmallVector output(deduced.begin(), deduced.end()); - stack.push(decl, output); + stack.push(decl, list, deduced); LOG_DEBUG( "{}deduce {}: {{{}}}", @@ -481,18 +358,18 @@ class PseudoInstantiator : public clang::TreeTransform { }(), [&] { std::string mapping; - for(unsigned j = 0; j < output.size(); ++j) { + for(unsigned j = 0; j < deduced.size(); ++j) { if(j > 0) mapping += ", "; if(j < list->size()) { mapping += list->getParam(j)->getNameAsString(); mapping += "="; } - if(output[j].getKind() == clang::TemplateArgument::Type) { + if(deduced[j].getKind() == clang::TemplateArgument::Type) { mapping += "'"; - mapping += output[j].getAsType().getAsString(); + mapping += deduced[j].getAsType().getAsString(); mapping += "'"; - } else if(output[j].getKind() == clang::TemplateArgument::Pack) + } else if(deduced[j].getKind() == clang::TemplateArgument::Pack) mapping += ""; else mapping += ""; @@ -503,26 +380,14 @@ class PseudoInstantiator : public clang::TreeTransform { return true; } - using lookup_result = clang::DeclContext::lookup_result; - - /// When DeclContext::lookup returns multiple declarations (e.g. a member in - /// both a base class and derived class), take the last one. This heuristic - /// favors the most-derived declaration, though the ordering depends on clang's - /// internal DeclContext storage. - clang::Decl* preferred(lookup_result members) { - clang::Decl* decl = nullptr; - std::ranges::for_each(members, [&](auto member) { decl = member; }); - return decl; - } - - /// Look up `name` in the given type. First transforms the type (to substitute + /// Look up `name` in the given type. First rewrites the type (to substitute /// any template parameters in it), then extracts the ClassTemplateDecl or /// TypeAliasTemplateDecl from the resulting TST/DTST and dispatches to the /// appropriate lookup overload. lookup_result lookup(clang::QualType type, clang::DeclarationName name) { clang::Decl* TD = nullptr; llvm::ArrayRef args; - type = TransformType(type); + type = resolve(type); if(type.isNull()) { return lookup_result(); @@ -619,7 +484,7 @@ class PseudoInstantiator : public clang::TreeTransform { /// is substituted (to resolve template params in it) then looked up. /// /// IMPORTANT: when a member is found, stack frames pushed during the lookup - /// are intentionally left intact. The caller (TransformDependentNameType) + /// are intentionally left intact. The caller (resolve_dependent_name) /// needs them to substitute the found decl's underlying type. The caller /// is responsible for popping frames after substitution. lookup_result lookup_in_bases(clang::CXXRecordDecl* CRD, clang::DeclarationName name) { @@ -702,13 +567,8 @@ class PseudoInstantiator : public clang::TreeTransform { for(auto partial: partials) { if(deduce_template_arguments(partial, arguments)) { stack.pop(); - if(!best) { + if(!best || more_specialized(context, partial, best)) { best = partial; - } else if(auto* winner = sema.getMoreSpecializedPartialSpecialization( - partial, - best, - clang::SourceLocation())) { - best = winner; } } } @@ -765,10 +625,10 @@ class PseudoInstantiator : public clang::TreeTransform { /// /// TODO: Replace with a general mechanism for resolving well-known standard /// library patterns, or improve the resolver to handle these chains naturally. - clang::QualType hole(clang::NestedNameSpecifier* NNS, + clang::QualType hole(const clang::NestedNameSpecifier* NNS, const clang::IdentifierInfo* member, TemplateArguments arguments) { - if(NNS->getKind() != clang::NestedNameSpecifier::TypeSpec) { + if(!NNS || NNS->getKind() != clang::NestedNameSpecifier::TypeSpec) { return clang::QualType(); } @@ -798,7 +658,7 @@ class PseudoInstantiator : public clang::TreeTransform { auto prefix = clang::NestedNameSpecifier::Create(context, nullptr, Alloc.getTypePtr()); - auto rebind = sema.getPreprocessor().getIdentifierInfo("rebind"); + auto rebind = &context.Idents.get("rebind"); auto DTST = context.getDependentTemplateSpecializationType( clang::ElaboratedTypeKeyword::None, @@ -807,15 +667,11 @@ class PseudoInstantiator : public clang::TreeTransform { prefix = clang::NestedNameSpecifier::Create(context, prefix, DTST.getTypePtr()); - auto other = sema.getPreprocessor().getIdentifierInfo("other"); - /// Keyword must stay None: the synthesized type is transformed - /// through a trivial TypeSourceInfo whose KeywordLoc is invalid, - /// and Sema::CheckTypenameType asserts (Keyword != None) == - /// KeywordLoc.isValid(). + auto other = &context.Idents.get("other"); auto DNT = context.getDependentNameType(clang::ElaboratedTypeKeyword::None, prefix, other); - auto result = PseudoInstantiator(sema, resolved, indent).TransformType(DNT); + auto result = PseudoInstantiator(context, resolved, indent).resolve(DNT); if(!result.isNull() && !result->isDependentType()) { LOG_DEBUG( "{}" "hole: 'allocator_traits::rebind_alloc' → '{}'", @@ -826,13 +682,7 @@ class PseudoInstantiator : public clang::TreeTransform { if(auto TST = Alloc->getAs()) { llvm::SmallVector replaceArguments = {T}; - llvm::SmallVector canonicalArguments; - for(auto& arg: replaceArguments) { - canonicalArguments.emplace_back(context.getCanonicalTemplateArgument(arg)); - } - auto result = context.getTemplateSpecializationType(TST->getTemplateName(), - replaceArguments, - canonicalArguments); + auto result = make_specialization(TST->getTemplateName(), replaceArguments); LOG_DEBUG( "{}" "hole: 'allocator_traits::rebind_alloc' → '{}'", pad(), @@ -845,82 +695,463 @@ class PseudoInstantiator : public clang::TreeTransform { return clang::QualType(); } -public: - using Base::TransformType; +private: + /// Per-kind dispatch. Whitelist of type classes the resolver understands; + /// anything else passes through unchanged, which downstream treats as + /// unresolved. Local qualifiers are stripped here and reapplied on the + /// rewritten result. + clang::QualType rewrite_type(clang::QualType type, Policy policy) { + auto quals = type.getLocalQualifiers(); + const clang::Type* T = type.getLocalUnqualifiedType().getTypePtr(); - /// Entry point for all type transformations. Guards against: - /// - Null types (return as-is) - /// - Non-dependent types (no transformation needed) - /// - Excessive recursion depth (bail out to prevent stack overflow) - /// - Null results from base transform (return original type instead) - clang::QualType TransformType(clang::QualType type) { - if(type.isNull() || !type->isDependentType()) { - return type; - } - if(depth > 16) { - return type; + clang::QualType result; + switch(T->getTypeClass()) { + case clang::Type::TemplateTypeParm: { + result = rewrite_parameter(llvm::cast(T), policy); + break; + } + + /// Sugar nodes: rewrite what they point at; the wrapper is dropped, + /// which is fine because consumers compare canonically or look + /// through sugar. + case clang::Type::Elaborated: { + result = rewrite(llvm::cast(T)->getNamedType(), policy); + break; + } + case clang::Type::Paren: { + result = rewrite(llvm::cast(T)->getInnerType(), policy); + break; + } + case clang::Type::Using: { + result = rewrite(llvm::cast(T)->getUnderlyingType(), policy); + break; + } + case clang::Type::MacroQualified: { + result = + rewrite(llvm::cast(T)->getUnderlyingType(), policy); + break; + } + case clang::Type::SubstTemplateTypeParm: { + result = + rewrite(llvm::cast(T)->getReplacementType(), + policy); + break; + } + + /// Dependent typedefs expand under Policy::Substitute regardless of + /// the current policy — the invariant that breaks typedef ↔ lookup + /// cycles lives on this single line. + case clang::Type::Typedef: { + auto TND = llvm::cast(T)->getDecl(); + auto underlying = TND->getUnderlyingType(); + if(underlying->isDependentType()) { + result = substitute(underlying); + } + break; + } + + case clang::Type::InjectedClassName: { + auto ICT = llvm::cast(T); + result = rewrite(ICT->getInjectedSpecializationType(), policy); + break; + } + + case clang::Type::TemplateSpecialization: { + result = rewrite_template(llvm::cast(T), policy); + break; + } + + case clang::Type::DependentName: { + auto DNT = llvm::cast(T); + if(policy == Policy::Resolve) { + result = resolve_dependent_name(DNT); + } else { + auto NNS = rewrite_specifier(DNT->getQualifier(), policy); + if(NNS != DNT->getQualifier()) { + result = context.getDependentNameType( + DNT->getKeyword(), + const_cast(NNS), + DNT->getIdentifier()); + } + } + break; + } + + case clang::Type::DependentTemplateSpecialization: { + auto DTST = llvm::cast(T); + if(policy == Policy::Resolve) { + result = resolve_dependent_template(DTST); + } else { + auto& template_name = DTST->getDependentTemplateName(); + auto NNS = rewrite_specifier(template_name.getQualifier(), policy); + llvm::SmallVector arguments; + bool changed = rewrite_arguments(DTST->template_arguments(), arguments, policy); + if(NNS != template_name.getQualifier() || changed) { + result = context.getDependentTemplateSpecializationType( + DTST->getKeyword(), + clang::DependentTemplateStorage( + const_cast(NNS), + template_name.getName(), + template_name.hasTemplateKeyword()), + arguments); + } + } + break; + } + + case clang::Type::Pointer: { + auto pointee = rewrite(llvm::cast(T)->getPointeeType(), policy); + result = context.getPointerType(pointee); + break; + } + + case clang::Type::LValueReference: { + auto pointee = + rewrite(llvm::cast(T)->getPointeeType(), policy); + result = context.getLValueReferenceType(pointee); + break; + } + + case clang::Type::RValueReference: { + auto pointee = + rewrite(llvm::cast(T)->getPointeeType(), policy); + result = context.getRValueReferenceType(pointee); + break; + } + + case clang::Type::PackExpansion: { + auto PET = llvm::cast(T); + auto pattern = rewrite(PET->getPattern(), policy); + if(pattern == PET->getPattern()) { + break; + } + if(pattern->containsUnexpandedParameterPack()) { + result = context.getPackExpansionType(pattern, PET->getNumExpansions()); + } else { + /// The pack was substituted with a concrete (single) + /// argument; the expansion collapses to it. + result = pattern; + } + break; + } + + /// Attempt to resolve decltype expressions that reference variables. + /// Only handles the simple case of `decltype(var)` where `var` is a VarDecl. + /// TODO: Handle more complex decltype expressions (member access, function calls). + case clang::Type::Decltype: { + auto expr = llvm::cast(T)->getUnderlyingExpr(); + if(auto DRE = llvm::dyn_cast(expr)) { + if(auto decl = DRE->getDecl(); llvm::isa(decl)) { + result = rewrite(decl->getType(), policy); + } + } + break; + } + + case clang::Type::DependentSizedArray: { + auto DSAT = llvm::cast(T); + auto element = rewrite(DSAT->getElementType(), policy); + + /// `T[N]` with a known N collapses to a constant array. + if(auto NTTP = referenced_nttp(DSAT->getSizeExpr())) { + if(auto* argument = + stack.find_argument(NTTP, NTTP->getDepth(), NTTP->getIndex()); + argument && argument->getKind() == clang::TemplateArgument::Integral) { + result = context.getConstantArrayType(element, + argument->getAsIntegral(), + nullptr, + DSAT->getSizeModifier(), + DSAT->getIndexTypeCVRQualifiers()); + break; + } + } + + if(element != DSAT->getElementType()) { + result = context.getDependentSizedArrayType(element, + DSAT->getSizeExpr(), + DSAT->getSizeModifier(), + DSAT->getIndexTypeCVRQualifiers()); + } + break; + } + + case clang::Type::ConstantArray: { + auto CAT = llvm::cast(T); + auto element = rewrite(CAT->getElementType(), policy); + if(element != CAT->getElementType()) { + result = context.getConstantArrayType(element, + CAT->getSize(), + CAT->getSizeExpr(), + CAT->getSizeModifier(), + CAT->getIndexTypeCVRQualifiers()); + } + break; + } + + case clang::Type::IncompleteArray: { + auto IAT = llvm::cast(T); + auto element = rewrite(IAT->getElementType(), policy); + if(element != IAT->getElementType()) { + result = context.getIncompleteArrayType(element, + IAT->getSizeModifier(), + IAT->getIndexTypeCVRQualifiers()); + } + break; + } + + case clang::Type::FunctionProto: { + auto FPT = llvm::cast(T); + auto ret = rewrite(FPT->getReturnType(), policy); + llvm::SmallVector params; + bool changed = ret != FPT->getReturnType(); + for(auto param: FPT->getParamTypes()) { + auto rewritten = rewrite(param, policy); + changed |= rewritten != param; + params.push_back(rewritten); + } + if(changed) { + result = context.getFunctionType(ret, params, FPT->getExtProtoInfo()); + } + break; + } + + default: { + break; + } } - ++depth; - auto result = Base::TransformType(type); - --depth; + if(result.isNull()) { - return type; + return clang::QualType(); + } + if(quals.hasQualifiers()) { + result = context.getQualifiedType(result, quals); } return result; } - clang::QualType TransformTemplateTypeParmType(clang::TypeLocBuilder& TLB, - clang::TemplateTypeParmTypeLoc TL, - bool = false) { - auto* T = TL.getTypePtr(); - + clang::QualType rewrite_parameter(const clang::TemplateTypeParmType* TTPT, Policy policy) { // First, try to find a substitution in the instantiation stack. - if(auto* arg = stack.find_argument(T)) { + if(auto* argument = stack.find_argument(TTPT)) { clang::QualType type; - if(arg->getKind() == clang::TemplateArgument::Type) { - type = arg->getAsType(); - } else if(arg->getKind() == clang::TemplateArgument::Pack) { - auto pack = arg->getPackAsArray(); + if(argument->getKind() == clang::TemplateArgument::Type) { + type = argument->getAsType(); + } else if(argument->getKind() == clang::TemplateArgument::Pack) { + auto pack = argument->getPackAsArray(); if(pack.size() == 1 && pack[0].getKind() == clang::TemplateArgument::Type) { type = pack[0].getAsType(); } + /// Multi-element packs are spliced at the template argument + /// list level (rewrite_arguments); a bare parameter cannot + /// stand for several types at once. } - if(!type.isNull()) { - TLB.pushTrivial(context, type, TL.getNameLoc()); - return type; - } - - TLB.push(TL.getType()).setNameLoc(TL.getNameLoc()); - return TL.getType(); + return type; } // No stack substitution available. Fall back to using the parameter's // default argument if one exists. This enables resolution chains like: // template> struct vector; // where Alloc's default depends on T. - if(clang::TemplateTypeParmDecl* TTPD = TL.getDecl()) { - if(TTPD->hasDefaultArgument()) { - const clang::TemplateArgument& argument = TTPD->getDefaultArgument().getArgument(); + if(policy == Policy::Resolve) { + if(clang::TemplateTypeParmDecl* TTPD = TTPT->getDecl(); + TTPD && TTPD->hasDefaultArgument()) { + const auto& argument = TTPD->getDefaultArgument().getArgument(); if(argument.getKind() == clang::TemplateArgument::Type) { - clang::QualType type = TransformType(argument.getAsType()); - if(!type.isNull()) { - TLB.pushTrivial(context, type, getBaseLocation()); - return type; + return rewrite(argument.getAsType(), policy); + } + } + } + + return clang::QualType(); + } + + /// Build a template specialization type from as-written (flat) arguments. + /// + /// The canonical argument list must mirror Sema's argument conversion or + /// the produced type would never compare equal to a parsed `X<...>`: the + /// trailing arguments of a parameter pack are grouped into a single Pack + /// argument, while the specified list stays flat as written. + clang::QualType make_specialization(clang::TemplateName name, TemplateArguments arguments) { + llvm::SmallVector canonical; + + clang::TemplateParameterList* params = nullptr; + if(auto TD = name.getAsTemplateDecl()) { + params = TD->getTemplateParameters(); + } + + unsigned i = 0; + if(params) { + for(auto param: *params) { + if(param->isTemplateParameterPack()) { + if(arguments.size() - i == 1 && + arguments[i].getKind() == clang::TemplateArgument::Pack) { + /// Already grouped by deduction. + canonical.emplace_back(context.getCanonicalTemplateArgument(arguments[i])); + i += 1; + } else { + llvm::SmallVector pack; + for(; i < arguments.size(); ++i) { + pack.emplace_back(context.getCanonicalTemplateArgument(arguments[i])); + } + canonical.emplace_back( + clang::TemplateArgument::CreatePackCopy(context, pack)); } + break; + } + if(i >= arguments.size()) { + break; } + canonical.emplace_back(context.getCanonicalTemplateArgument(arguments[i])); + i += 1; } } + for(; i < arguments.size(); ++i) { + canonical.emplace_back(context.getCanonicalTemplateArgument(arguments[i])); + } - TLB.push(TL.getType()).setNameLoc(TL.getNameLoc()); - return TL.getType(); + return context.getTemplateSpecializationType(name, arguments, canonical); } - clang::QualType TransformDependentNameType(clang::TypeLocBuilder& TLB, - clang::DependentNameTypeLoc TL, - bool DeducedTSTContext = false) { - auto* DNT = TL.getTypePtr(); + clang::QualType rewrite_template(const clang::TemplateSpecializationType* TST, Policy policy) { + /// Alias specializations carry the substituted underlying type as + /// sugar; expanding it is substitution, not lookup, so it is safe + /// under both policies. + if(TST->isTypeAlias()) { + return rewrite(TST->desugar(), policy); + } + + llvm::SmallVector arguments; + if(!rewrite_arguments(TST->template_arguments(), arguments, policy)) { + return clang::QualType(); + } + + return make_specialization(TST->getTemplateName(), arguments); + } + + /// Rewrite a template argument list. Returns true if anything changed. + /// Pack expansions whose pattern is a bound pack parameter are spliced + /// inline, so `type_list` with `Us = {int, float}` becomes + /// `type_list`. + bool rewrite_arguments(TemplateArguments arguments, + llvm::SmallVectorImpl& out, + Policy policy) { + bool changed = false; + + for(auto& argument: arguments) { + switch(argument.getKind()) { + case clang::TemplateArgument::Type: { + auto type = argument.getAsType(); + + if(auto PET = type->getAs()) { + auto pattern = PET->getPattern(); + if(auto TTPT = pattern->getAs()) { + auto* bound = stack.find_argument(TTPT); + if(bound && bound->getKind() == clang::TemplateArgument::Pack) { + out.append(bound->pack_begin(), bound->pack_end()); + changed = true; + continue; + } + } + + auto rewritten = rewrite(pattern, policy); + if(rewritten != pattern) { + changed = true; + if(rewritten->containsUnexpandedParameterPack()) { + rewritten = context.getPackExpansionType(rewritten, + PET->getNumExpansions()); + } + out.emplace_back(rewritten); + } else { + out.push_back(argument); + } + continue; + } + + auto rewritten = rewrite(type, policy); + changed |= rewritten != type; + out.emplace_back(rewritten); + break; + } + + case clang::TemplateArgument::Expression: { + /// Substitute a bound non-type parameter at the argument + /// level; expressions themselves are never rebuilt. + if(auto NTTP = referenced_nttp(argument.getAsExpr())) { + auto* bound = stack.find_argument(NTTP, NTTP->getDepth(), NTTP->getIndex()); + if(bound && !bound->isNull()) { + out.push_back(*bound); + changed = true; + continue; + } + } + out.push_back(argument); + break; + } + + default: { + out.push_back(argument); + break; + } + } + } + + return changed; + } + + const clang::NestedNameSpecifier* rewrite_specifier(const clang::NestedNameSpecifier* NNS, + Policy policy) { + if(!NNS) { + return nullptr; + } + + switch(NNS->getKind()) { + case clang::NestedNameSpecifier::TypeSpec: { + auto prefix = rewrite_specifier(NNS->getPrefix(), policy); + + /// A dependent component written as `prefix::template B` keeps + /// its qualifier in the specifier chain, not in the type node + /// itself; resolve it in the scope of the rewritten prefix. + clang::QualType type; + auto component = clang::QualType(NNS->getAsType(), 0); + auto DTST = + llvm::dyn_cast(NNS->getAsType()); + if(DTST && !DTST->getDependentTemplateName().getQualifier() && + policy == Policy::Resolve) { + type = resolve_dependent_template(DTST, prefix); + } else { + type = rewrite(component, policy); + } + + if(prefix == NNS->getPrefix() && type.getTypePtr() == NNS->getAsType()) { + return NNS; + } + return clang::NestedNameSpecifier::Create( + context, + const_cast(prefix), + type.getTypePtr()); + } + + /// Identifier components are resolved by lookup itself; the prefix + /// may still contain substitutable types. + case clang::NestedNameSpecifier::Identifier: { + auto prefix = rewrite_specifier(NNS->getPrefix(), policy); + if(prefix == NNS->getPrefix()) { + return NNS; + } + return clang::NestedNameSpecifier::Create( + context, + const_cast(prefix), + NNS->getAsIdentifier()); + } + + default: { + return NNS; + } + } + } + + clang::QualType resolve_dependent_name(const clang::DependentNameType* DNT) { LOG_DEBUG("{}" "resolve '{}'", pad(), clang::QualType(DNT, 0).getAsString()); ++indent; @@ -928,7 +1159,6 @@ class PseudoInstantiator : public clang::TreeTransform { if(auto iter = resolved.find(DNT); iter != resolved.end()) { LOG_DEBUG("{}" "→ '{}' (cached)", pad(), iter->second.getAsString()); --indent; - TLB.pushTrivial(context, iter->second, getBaseLocation()); return iter->second; } @@ -936,66 +1166,41 @@ class PseudoInstantiator : public clang::TreeTransform { if(!active_resolutions.insert(DNT).second) { LOG_DEBUG("{}→ ", pad()); --indent; - auto original = clang::QualType(DNT, 0); - auto NewTL = TLB.push(original); - NewTL.setElaboratedKeywordLoc(TL.getElaboratedKeywordLoc()); - NewTL.setQualifierLoc(TL.getQualifierLoc()); - NewTL.setNameLoc(TL.getNameLoc()); - return original; - } - - auto NNSLoc = TransformNestedNameSpecifierLoc(TL.getQualifierLoc()); - if(!NNSLoc) { - active_resolutions.erase(DNT); - LOG_DEBUG("{}→ ", pad()); - --indent; - auto original = clang::QualType(DNT, 0); - auto NewTL = TLB.push(original); - NewTL.setElaboratedKeywordLoc(TL.getElaboratedKeywordLoc()); - NewTL.setQualifierLoc(TL.getQualifierLoc()); - NewTL.setNameLoc(TL.getNameLoc()); - return original; + return clang::QualType(DNT, 0); } - auto* NNS = NNSLoc.getNestedNameSpecifier(); + auto* NNS = rewrite_specifier(DNT->getQualifier(), Policy::Resolve); auto stack_size = stack.data.size(); auto* decl = preferred(lookup(NNS, DNT->getIdentifier())); auto type = get_decl_type(decl); clang::QualType result; if(!type.isNull()) { - if(decl) { - const char* decl_kind = "decl"; - if(llvm::isa(decl)) - decl_kind = "typedef"; - else if(llvm::isa(decl)) - decl_kind = "record"; - auto decl_name = llvm::dyn_cast(decl) - ? llvm::dyn_cast(decl)->getNameAsString() - : "?"; - LOG_DEBUG( - "{}" "found {} '{}' = '{}'", - pad(), - decl_kind, - decl_name, - type.getAsString()); - } + const char* decl_kind = "decl"; + if(llvm::isa(decl)) + decl_kind = "typedef"; + else if(llvm::isa(decl)) + decl_kind = "record"; + auto decl_name = llvm::dyn_cast(decl) + ? llvm::dyn_cast(decl)->getNameAsString() + : "?"; + LOG_DEBUG("{}" "found {} '{}' = '{}'", pad(), decl_kind, decl_name, type.getAsString()); // Step 1: substitute params (expand typedefs, no lookup). result = substitute(type); LOG_DEBUG("{}" "substitute → '{}'", pad(), result.getAsString()); // Pop lookup frames BEFORE further resolution. The substitute step already - // used the full stack for parameter substitution. TransformType should only + // used the full stack for parameter substitution. Resolution should only // see the outer context to avoid polluting free variables (e.g. T) with // mappings from intermediate lookup frames. while(stack.data.size() > stack_size) { stack.pop(); } - // Step 2: if still dependent, do full transform (may trigger more lookups). + // Step 2: if still dependent, do full resolution (may trigger more lookups). if(!result.isNull() && result->isDependentType()) { - result = TransformType(result); + result = rewrite(result, Policy::Resolve); } } else { while(stack.data.size() > stack_size) { @@ -1009,81 +1214,54 @@ class PseudoInstantiator : public clang::TreeTransform { LOG_DEBUG("{}" "→ '{}'", pad(), result.getAsString()); --indent; resolved.try_emplace(DNT, result); - TLB.pushTrivial(context, result, getBaseLocation()); return result; } LOG_DEBUG("{}→ ", pad()); --indent; - auto original = clang::QualType(DNT, 0); - auto NewTL = TLB.push(original); - NewTL.setElaboratedKeywordLoc(TL.getElaboratedKeywordLoc()); - NewTL.setQualifierLoc(TL.getQualifierLoc()); - NewTL.setNameLoc(TL.getNameLoc()); - return original; - } - - using Base::TransformDependentTemplateSpecializationType; - - clang::QualType rebuild_dtst(clang::TypeLocBuilder& TLB, - clang::DependentTemplateSpecializationTypeLoc TL) { - auto* DTST = TL.getTypePtr(); - /// push() returns an uninitialized record; the qualifier slot is a - /// pointer, so leaving it garbage crashes any later getSourceRange - /// (e.g. Sema::CheckTemplateArgument on a transformed argument). - auto NewTL = - TLB.push(clang::QualType(DTST, 0)); - NewTL.initializeLocal(context, getBaseLocation()); - return NewTL.getType(); + return clang::QualType(DNT, 0); } - clang::QualType TransformDependentTemplateSpecializationType( - clang::TypeLocBuilder& TLB, - clang::DependentTemplateSpecializationTypeLoc TL) { - auto* DTST = TL.getTypePtr(); + /// `scope` carries the enclosing specifier prefix for components whose own + /// qualifier is null (see rewrite_specifier). Such resolutions are not + /// cached: the node's identity does not include the scope it was found in. + clang::QualType + resolve_dependent_template(const clang::DependentTemplateSpecializationType* DTST, + const clang::NestedNameSpecifier* scope = nullptr) { LOG_DEBUG("{}" "resolve DTST '{}'", pad(), clang::QualType(DTST, 0).getAsString()); ++indent; - if(auto iter = resolved.find(DTST); iter != resolved.end()) { - --indent; - TLB.pushTrivial(context, iter->second, getBaseLocation()); - return iter->second; - } + auto& template_name = DTST->getDependentTemplateName(); + bool cacheable = template_name.getQualifier() != nullptr || !scope; - auto NNSLoc = TransformNestedNameSpecifierLoc(TL.getQualifierLoc()); - if(!NNSLoc) { - LOG_DEBUG("{}→ ", pad()); - --indent; - return rebuild_dtst(TLB, TL); + if(cacheable) { + if(auto iter = resolved.find(DTST); iter != resolved.end()) { + --indent; + return iter->second; + } } - auto* NNS = NNSLoc.getNestedNameSpecifier(); - clang::TemplateArgumentListInfo info; - using iterator = clang::TemplateArgumentLocContainerIterator< - clang::DependentTemplateSpecializationTypeLoc>; - if(TransformTemplateArguments(iterator(TL, 0), iterator(TL, TL.getNumArgs()), info)) { - LOG_DEBUG("{}→ ", pad()); - --indent; - return rebuild_dtst(TLB, TL); - } + const clang::NestedNameSpecifier* NNS = + template_name.getQualifier() + ? rewrite_specifier(template_name.getQualifier(), Policy::Resolve) + : scope; llvm::SmallVector arguments; - for(auto& arg: info.arguments()) { - arguments.push_back(arg.getArgument()); - } + rewrite_arguments(DTST->template_arguments(), arguments, Policy::Resolve); - auto* name = DTST->getDependentTemplateName().getName().getIdentifier(); + auto* name = template_name.getName().getIdentifier(); if(!name) { LOG_DEBUG("{}→ ", pad()); --indent; - return rebuild_dtst(TLB, TL); + return clang::QualType(DTST, 0); } if(auto result = hole(NNS, name, arguments); !result.isNull()) { LOG_DEBUG("{}" "hole: '{}' → '{}'", pad(), name->getName().str(), result.getAsString()); --indent; - resolved.try_emplace(DTST, result); - TLB.pushTrivial(context, result, getBaseLocation()); + if(cacheable) { + resolved.try_emplace(DTST, result); + } return result; } @@ -1097,13 +1275,14 @@ class PseudoInstantiator : public clang::TreeTransform { stack.pop(); } if(!type.isNull() && type->isDependentType()) { - type = TransformType(type); + type = rewrite(type, Policy::Resolve); } if(!type.isNull()) { LOG_DEBUG("{}" "→ '{}' (alias)", pad(), type.getAsString()); --indent; - resolved.try_emplace(DTST, type); - TLB.pushTrivial(context, type, getBaseLocation()); + if(cacheable) { + resolved.try_emplace(DTST, type); + } return type; } } @@ -1111,18 +1290,14 @@ class PseudoInstantiator : public clang::TreeTransform { // Resolve DTST to a concrete TemplateSpecializationType. // e.g. __alloc_traits>::rebind → rebind (a TST) // This allows subsequent lookup of members (like "other") to work. - // Keep lookup frames on stack — the caller (e.g. TransformNestedNameSpecifierLoc + // Keep lookup frames on stack — the caller (e.g. rewrite_specifier // processing A::B::C) needs them for parameter substitution. - clang::TemplateName TN(CTD); - llvm::SmallVector canonArgs; - for(auto& arg: arguments) { - canonArgs.push_back(context.getCanonicalTemplateArgument(arg)); - } - auto result = context.getTemplateSpecializationType(TN, arguments, canonArgs); + auto result = make_specialization(clang::TemplateName(CTD), arguments); LOG_DEBUG("{}" "→ TST '{}' (class)", pad(), result.getAsString()); --indent; - resolved.try_emplace(DTST, result); - TLB.pushTrivial(context, result, getBaseLocation()); + if(cacheable) { + resolved.try_emplace(DTST, result); + } return result; } } @@ -1132,52 +1307,14 @@ class PseudoInstantiator : public clang::TreeTransform { LOG_DEBUG("{}→ ", pad()); --indent; - auto fallback = rebuild_dtst(TLB, TL); - resolved.try_emplace(DTST, fallback); - return fallback; - } - - /// Desugar dependent typedefs by delegating to SubstituteOnly. - /// This is called by PseudoInstantiator (not by SubstituteOnly itself, which has - /// its own TransformTypedefType). Using substitute() here ensures that typedef - /// expansion does NOT trigger heuristic lookup, preventing the typedef ↔ lookup cycle. - clang::QualType TransformTypedefType(clang::TypeLocBuilder& TLB, clang::TypedefTypeLoc TL) { - if(auto* TND = TL.getTypedefNameDecl()) { - auto underlying = TND->getUnderlyingType(); - if(underlying->isDependentType()) { - auto type = substitute(underlying); - if(!type.isNull()) { - if(auto ET = llvm::dyn_cast(type)) { - type = ET->getNamedType(); - } - TLB.pushTrivial(context, type, getBaseLocation()); - return type; - } - } + auto fallback = clang::QualType(DTST, 0); + if(cacheable) { + resolved.try_emplace(DTST, fallback); } - return Base::TransformTypedefType(TLB, TL); - } - - /// Attempt to resolve decltype expressions that reference variables. - /// Only handles the simple case of `decltype(var)` where `var` is a VarDecl. - /// TODO: Handle more complex decltype expressions (member access, function calls, etc.) - clang::QualType TransformDecltypeType(clang::TypeLocBuilder& TLB, clang::DecltypeTypeLoc TL) { - auto expr = TL.getTypePtr()->getUnderlyingExpr(); - if(auto DRE = llvm::dyn_cast(expr)) { - if(auto decl = DRE->getDecl(); llvm::isa(decl)) { - auto type = TransformType(decl->getType()); - if(!type.isNull()) { - TLB.pushTrivial(context, type, getBaseLocation()); - return type; - } - } - } - - return Base::TransformDecltypeType(TLB, TL); + return fallback; } private: - clang::Sema& sema; clang::ASTContext& context; InstantiationStack stack; llvm::DenseMap& resolved; @@ -1185,65 +1322,22 @@ class PseudoInstantiator : public clang::TreeTransform { llvm::DenseSet> active_ctd_lookups; unsigned depth = 0; unsigned indent = 0; - clang::SourceLocation base_location; std::string pad() const { return std::string(indent * 2, ' '); } }; -/// Pseudo-instantiation drives Sema on speculative inputs, so its failures -/// are expected and must not leak error-level diagnostics into the unit. -/// Only the consumer is swapped out: error counting stays untouched because -/// Sema's error limit is what stops runaway instantiations. -class DiagnosticSilencer { -public: - explicit DiagnosticSilencer(clang::Sema& sema) : engine(sema.getDiagnostics()) { - client = engine.getClient(); - owned = engine.takeClient(); - engine.setClient(&ignoring, false); - } - - ~DiagnosticSilencer() { - /// Deliberate trade-off: error counting is left to accumulate across - /// lookups. Once the engine's error limit trips, later speculative - /// lookups on this unit degrade (Sema bails out of instantiations) — - /// but resetting the counters per lookup hands every pathological - /// instantiation chain a fresh budget, which measures as minutes of - /// resolver time on STL-heavy TUs. Until the resolver carries its - /// own work budget, the shared limit is both the brake and the cap. - if(owned) { - engine.setClient(owned.release(), true); - } else { - engine.setClient(client, false); - } - } - -private: - clang::DiagnosticsEngine& engine; - clang::DiagnosticConsumer* client; - std::unique_ptr owned; - clang::IgnoringDiagConsumer ignoring; -}; - } // namespace clang::QualType TemplateResolver::resolve(clang::QualType type) { - DiagnosticSilencer silencer(sema); - PseudoInstantiator instantiator(sema, resolved); - return instantiator.TransformType(type); -} - -clang::QualType TemplateResolver::resugar(clang::QualType type, clang::Decl* decl) { - DiagnosticSilencer silencer(sema); - ResugarOnly resugar(sema, decl); - return resugar.TransformType(type); + PseudoInstantiator instantiator(context, resolved); + return instantiator.resolve(type); } TemplateResolver::lookup_result TemplateResolver::lookup(const clang::NestedNameSpecifier* NNS, clang::DeclarationName name) { - DiagnosticSilencer silencer(sema); - PseudoInstantiator instantiator(sema, resolved); + PseudoInstantiator instantiator(context, resolved); return instantiator.lookup(NNS, name); } @@ -1254,17 +1348,16 @@ TemplateResolver::lookup_result return {}; } - DiagnosticSilencer silencer(sema); if(expr->isArrow()) { /// Follow overloaded operator-> chains (smart pointers) until a raw /// pointer appears; bounded, cycles just stop resolving. - auto arrow = sema.getASTContext().DeclarationNames.getCXXOperatorName(clang::OO_Arrow); + auto arrow = context.DeclarationNames.getCXXOperatorName(clang::OO_Arrow); for(unsigned hop = 0; hop < 8; hop++) { if(auto* PT = type->getAs()) { type = PT->getPointeeType(); break; } - PseudoInstantiator instantiator(sema, resolved); + PseudoInstantiator instantiator(context, resolved); const clang::CXXMethodDecl* method = nullptr; for(auto* candidate: instantiator.lookup(type, arrow)) { if((method = llvm::dyn_cast(candidate))) { @@ -1288,7 +1381,7 @@ TemplateResolver::lookup_result type = ICNT->getInjectedSpecializationType(); } - PseudoInstantiator instantiator(sema, resolved); + PseudoInstantiator instantiator(context, resolved); return instantiator.lookup(type, expr->getMemberNameInfo().getName()); } diff --git a/src/semantic/resolver.h b/src/semantic/resolver.h index 429f900ac..05fc8cce0 100644 --- a/src/semantic/resolver.h +++ b/src/semantic/resolver.h @@ -3,12 +3,6 @@ #include "clang/AST/ExprCXX.h" #include "clang/AST/Type.h" -namespace clang { - -class Sema; - -} - namespace clice { /// This class is used to resolve dependent names in the unit. @@ -18,26 +12,18 @@ namespace clice { /// some heuristics to simplify the dependent names as normal type/expression. /// For example, `std::vector::value_type` can be simplified as `T`. /// +/// Resolution is pure AST computation: it never enters Sema, so speculative +/// lookups cannot emit diagnostics, register specializations, or otherwise +/// mutate the unit's semantic state. +/// /// Thread safety: NOT thread-safe. Each compilation unit should have its own resolver. /// The `resolved` cache persists across multiple resolve() calls on the same unit. class TemplateResolver { public: - explicit TemplateResolver(clang::Sema& sema) : sema(sema) {} + explicit TemplateResolver(clang::ASTContext& context) : context(context) {} clang::QualType resolve(clang::QualType type); - void resolve(clang::CXXUnresolvedConstructExpr* expr); - - void resolve(clang::UnresolvedLookupExpr* expr); - - // TODO: Use a clearer approach for resolving UnresolvedLookupExpr. - - void resolve(clang::UnresolvedUsingType* type); - - /// Resugar the canonical `TemplateTypeParmType` with given template context. - /// `decl` should be the declaration that the type is in. - clang::QualType resugar(clang::QualType type, clang::Decl* decl); - using lookup_result = clang::DeclContext::lookup_result; /// Look up the name in the given nested name specifier. @@ -91,7 +77,7 @@ class TemplateResolver { } private: - clang::Sema& sema; + clang::ASTContext& context; /// Cache of resolved dependent types, keyed by AST node pointer. /// Shared across resolve() calls within the same TU for performance. diff --git a/src/semantic/unifier.cpp b/src/semantic/unifier.cpp new file mode 100644 index 000000000..8f1a0725a --- /dev/null +++ b/src/semantic/unifier.cpp @@ -0,0 +1,411 @@ +#include "semantic/unifier.h" + +#include "clang/AST/ExprCXX.h" + +namespace clice { + +namespace { + +/// Strip local qualifiers and one-step sugar until a structural node is +/// reached, accumulating qualifiers into `quals`. Child sugar is preserved: +/// only the current level is desugared, so template arguments and pointees +/// keep the form the user wrote. +clang::QualType peel(clang::QualType type, clang::Qualifiers& quals) { + while(true) { + if(type.hasLocalQualifiers()) { + quals.addQualifiers(type.getLocalQualifiers()); + type = type.getLocalUnqualifiedType(); + continue; + } + + const clang::Type* T = type.getTypePtr(); + switch(T->getTypeClass()) { + case clang::Type::Elaborated: { + type = llvm::cast(T)->getNamedType(); + continue; + } + case clang::Type::Paren: { + type = llvm::cast(T)->getInnerType(); + continue; + } + case clang::Type::Using: { + type = llvm::cast(T)->getUnderlyingType(); + continue; + } + case clang::Type::Typedef: { + type = llvm::cast(T)->desugar(); + continue; + } + case clang::Type::SubstTemplateTypeParm: { + type = llvm::cast(T)->getReplacementType(); + continue; + } + case clang::Type::MacroQualified: { + type = llvm::cast(T)->getUnderlyingType(); + continue; + } + case clang::Type::Attributed: { + type = llvm::cast(T)->getEquivalentType(); + continue; + } + case clang::Type::TemplateSpecialization: { + auto TST = llvm::cast(T); + /// Alias specializations are sugar for the substituted + /// underlying type; structural matching sees through them. + if(TST->isTypeAlias()) { + type = TST->desugar(); + continue; + } + return type; + } + default: { + return type; + } + } + } +} + +} // namespace + +const clang::NonTypeTemplateParmDecl* referenced_nttp(const clang::Expr* expr) { + if(!expr) { + return nullptr; + } + if(auto DRE = llvm::dyn_cast(expr->IgnoreParenImpCasts())) { + return llvm::dyn_cast(DRE->getDecl()); + } + return nullptr; +} + +bool TypeUnifier::bind(unsigned index, const clang::TemplateArgument& argument) { + if(index >= bindings.size()) { + return false; + } + + auto& existing = bindings[index]; + if(existing.isNull()) { + existing = argument; + return true; + } + + auto lhs = context.getCanonicalTemplateArgument(existing); + auto rhs = context.getCanonicalTemplateArgument(argument); + if(!lhs.structurallyEquals(rhs)) { + return false; + } + + /// Same argument bound twice; keep the more sugared spelling. + if(existing.getKind() == clang::TemplateArgument::Type && + argument.getKind() == clang::TemplateArgument::Type) { + auto type = existing.getAsType(); + if(type == type.getCanonicalType() && argument.getAsType() != rhs.getAsType()) { + existing = argument; + } + } + return true; +} + +bool TypeUnifier::template_id(clang::QualType type, + clang::TemplateName& name, + TemplateArguments& arguments) const { + clang::Qualifiers quals; + type = peel(type, quals); + + if(auto ICNT = llvm::dyn_cast(type)) { + type = ICNT->getInjectedSpecializationType(); + } + + if(auto TST = llvm::dyn_cast(type)) { + name = TST->getTemplateName(); + arguments = TST->template_arguments(); + return true; + } + + if(auto RT = llvm::dyn_cast(type)) { + if(auto CTSD = llvm::dyn_cast(RT->getDecl())) { + name = clang::TemplateName(CTSD->getSpecializedTemplate()); + arguments = CTSD->getTemplateArgs().asArray(); + return true; + } + } + + return false; +} + +bool TypeUnifier::unify(clang::QualType pattern, clang::QualType argument) { + if(pattern.isNull() || argument.isNull()) { + return false; + } + + clang::Qualifiers pattern_quals; + clang::Qualifiers argument_quals; + pattern = peel(pattern, pattern_quals); + argument = peel(argument, argument_quals); + + /// `cv-list T`: the parameter absorbs the qualifiers the pattern doesn't + /// mention, so its qualifiers must be a subset of the argument's. + if(auto TTPT = llvm::dyn_cast(pattern)) { + if(TTPT->getDepth() != depth) { + return true; + } + if(!argument_quals.isStrictSupersetOf(pattern_quals) && argument_quals != pattern_quals) { + return false; + } + auto remaining = argument_quals; + remaining.removeQualifiers(pattern_quals); + auto bound = context.getQualifiedType(argument, remaining); + return bind(TTPT->getIndex(), clang::TemplateArgument(bound)); + } + + /// Anything else matches structurally: qualifiers must agree exactly. + if(pattern_quals != argument_quals) { + return false; + } + + switch(pattern->getTypeClass()) { + case clang::Type::Pointer: { + auto AP = llvm::dyn_cast(argument); + return AP && unify(llvm::cast(pattern)->getPointeeType(), + AP->getPointeeType()); + } + + case clang::Type::LValueReference: + case clang::Type::RValueReference: { + if(pattern->getTypeClass() != argument->getTypeClass()) { + return false; + } + return unify(llvm::cast(pattern)->getPointeeType(), + llvm::cast(argument)->getPointeeType()); + } + + case clang::Type::TemplateSpecialization: + case clang::Type::InjectedClassName: + case clang::Type::Record: { + clang::TemplateName pattern_name, argument_name; + TemplateArguments pattern_args, argument_args; + if(!template_id(pattern, pattern_name, pattern_args)) { + /// A plain record with no template head matches only itself. + return context.hasSameUnqualifiedType(pattern, argument); + } + if(!template_id(argument, argument_name, argument_args)) { + return false; + } + + /// A template template parameter in the head deduces the + /// argument's template, e.g. matching `X>`. + if(auto TTP = llvm::dyn_cast_or_null( + pattern_name.getAsTemplateDecl()); + TTP && TTP->getDepth() == depth) { + if(!bind(TTP->getIndex(), clang::TemplateArgument(argument_name))) { + return false; + } + } else if(!context.hasSameTemplateName(pattern_name, argument_name)) { + return false; + } + + return unify(pattern_args, argument_args); + } + + case clang::Type::ConstantArray: { + auto PA = llvm::cast(pattern); + auto AA = llvm::dyn_cast(argument); + return AA && PA->getSize() == AA->getSize() && + unify(PA->getElementType(), AA->getElementType()); + } + + case clang::Type::DependentSizedArray: { + auto PA = llvm::cast(pattern); + + /// `T[N]`: deduce N from a constant array bound. + if(auto NTTP = referenced_nttp(PA->getSizeExpr()); NTTP && NTTP->getDepth() == depth) { + if(auto AA = llvm::dyn_cast(argument)) { + llvm::APSInt size(AA->getSize()); + size.setIsUnsigned(NTTP->getType()->isUnsignedIntegerType()); + if(!bind(NTTP->getIndex(), + clang::TemplateArgument(context, size, NTTP->getType()))) { + return false; + } + return unify(PA->getElementType(), AA->getElementType()); + } + } + + if(auto AA = llvm::dyn_cast(argument)) { + return unify(PA->getElementType(), AA->getElementType()); + } + return false; + } + + /// Dependent forms we cannot look into are non-deduced contexts: + /// they constrain nothing. + case clang::Type::DependentName: + case clang::Type::DependentTemplateSpecialization: + case clang::Type::Decltype: + case clang::Type::UnresolvedUsing: + case clang::Type::PackExpansion: { + return true; + } + + default: { + return context.hasSameUnqualifiedType(pattern, argument); + } + } +} + +bool TypeUnifier::unify(const clang::TemplateArgument& pattern, + const clang::TemplateArgument& argument) { + switch(pattern.getKind()) { + case clang::TemplateArgument::Type: { + if(argument.getKind() != clang::TemplateArgument::Type) { + return false; + } + return unify(pattern.getAsType(), argument.getAsType()); + } + + case clang::TemplateArgument::Expression: { + /// A bare reference to an NTTP deduces it; any other expression + /// is a non-deduced context. Constant expression arguments are + /// normalized to Integral so downstream substitution (e.g. array + /// bounds) sees a value, not an expression. + if(auto NTTP = referenced_nttp(pattern.getAsExpr()); + NTTP && NTTP->getDepth() == depth) { + auto bound = argument; + if(argument.getKind() == clang::TemplateArgument::Expression) { + auto expr = argument.getAsExpr(); + if(!expr->isValueDependent()) { + if(auto value = expr->getIntegerConstantExpr(context)) { + bound = clang::TemplateArgument(context, *value, NTTP->getType()); + } + } + } + return bind(NTTP->getIndex(), bound); + } + return true; + } + + case clang::TemplateArgument::Integral: { + if(argument.getKind() == clang::TemplateArgument::Integral) { + return llvm::APSInt::isSameValue(pattern.getAsIntegral(), argument.getAsIntegral()); + } + return false; + } + + case clang::TemplateArgument::Template: { + if(auto TTP = llvm::dyn_cast_or_null( + pattern.getAsTemplate().getAsTemplateDecl()); + TTP && TTP->getDepth() == depth) { + return bind(TTP->getIndex(), argument); + } + if(argument.getKind() != clang::TemplateArgument::Template) { + return false; + } + return context.hasSameTemplateName(pattern.getAsTemplate(), argument.getAsTemplate()); + } + + default: { + auto lhs = context.getCanonicalTemplateArgument(pattern); + auto rhs = context.getCanonicalTemplateArgument(argument); + return lhs.structurallyEquals(rhs); + } + } +} + +bool TypeUnifier::unify(TemplateArguments patterns, TemplateArguments arguments) { + /// Flatten Pack entries on both sides so positional matching lines up: + /// converted argument lists (injected arguments, partial specialization + /// patterns) group a pack's arguments as `Pack{...}`, and substitution + /// may produce Pack entries inline. + llvm::SmallVector flat_patterns; + for(auto& pattern: patterns) { + if(pattern.getKind() == clang::TemplateArgument::Pack) { + flat_patterns.append(pattern.pack_begin(), pattern.pack_end()); + } else { + flat_patterns.push_back(pattern); + } + } + + llvm::SmallVector flat; + for(auto& argument: arguments) { + if(argument.getKind() == clang::TemplateArgument::Pack) { + flat.append(argument.pack_begin(), argument.pack_end()); + } else { + flat.push_back(argument); + } + } + + unsigned i = 0; + for(auto& pattern: flat_patterns) { + if(pattern.isPackExpansion()) { + /// A trailing pack absorbs all remaining arguments. Only a bare + /// pack parameter binds; a structured pattern (`vector...`) + /// is treated as non-deduced. + auto inner = pattern.getPackExpansionPattern(); + if(inner.getKind() == clang::TemplateArgument::Type) { + clang::Qualifiers quals; + auto type = peel(inner.getAsType(), quals); + if(auto TTPT = llvm::dyn_cast(type); + TTPT && TTPT->getDepth() == depth && !quals.hasQualifiers()) { + auto pack = + clang::TemplateArgument::CreatePackCopy(context, + llvm::ArrayRef(flat).drop_front(i)); + if(!bind(TTPT->getIndex(), pack)) { + return false; + } + } + } + return true; + } + + if(i >= flat.size()) { + return false; + } + if(!unify(pattern, flat[i])) { + return false; + } + i += 1; + } + + return i == flat.size(); +} + +bool deduce_arguments(clang::ASTContext& context, + clang::TemplateParameterList* params, + llvm::ArrayRef patterns, + llvm::ArrayRef arguments, + llvm::SmallVectorImpl& deduced) { + TypeUnifier unifier(context, params->getDepth(), params->size()); + if(!unifier.unify(patterns, arguments)) { + return false; + } + + deduced.assign(unifier.results().begin(), unifier.results().end()); + for(auto [i, argument]: llvm::enumerate(deduced)) { + if(!argument.isNull()) { + continue; + } + + /// An unbound pack deduces as empty. + if(params->getParam(i)->isTemplateParameterPack()) { + argument = clang::TemplateArgument::CreatePackCopy(context, {}); + continue; + } + + return false; + } + return true; +} + +bool more_specialized(clang::ASTContext& context, + clang::ClassTemplatePartialSpecializationDecl* left, + clang::ClassTemplatePartialSpecializationDecl* right) { + auto matches = [&](clang::ClassTemplatePartialSpecializationDecl* pattern, + clang::ClassTemplatePartialSpecializationDecl* argument) { + auto params = pattern->getTemplateParameters(); + TypeUnifier unifier(context, params->getDepth(), params->size()); + return unifier.unify(pattern->getTemplateArgs().asArray(), + argument->getTemplateArgs().asArray()); + }; + + return matches(right, left) && !matches(left, right); +} + +} // namespace clice diff --git a/src/semantic/unifier.h b/src/semantic/unifier.h new file mode 100644 index 000000000..fc71185ce --- /dev/null +++ b/src/semantic/unifier.h @@ -0,0 +1,83 @@ +#pragma once + +#include "clang/AST/ASTContext.h" +#include "clang/AST/DeclTemplate.h" + +namespace clice { + +/// Sema-free structural unification of template argument lists, replacing +/// `Sema::DeduceTemplateArguments` and `Sema::getMoreSpecializedPartialSpecialization` +/// for pseudo-instantiation. +/// +/// Differences from clang's deduction, by design: +/// - Works on sugared types: a parameter binds the argument *as written* +/// (e.g. `T = std::string`, not `T = std::basic_string`), so no +/// separate resugar pass is needed downstream. +/// - Binds template parameters to dependent arguments (`T = U`), which real +/// deduction never faces but pseudo-instantiation relies on. +/// - Skips conformance corners irrelevant to lookup (reference collapsing +/// adjustments, array bound promotion, constraint checks). Failure means +/// "this pattern doesn't match", which degrades to an unresolved name. +class TypeUnifier { +public: + using TemplateArguments = llvm::ArrayRef; + + explicit TypeUnifier(clang::ASTContext& context, unsigned depth, unsigned size) : + context(context), depth(depth), bindings(size) {} + + /// Unify `patterns` (a partial specialization's argument pattern or a + /// primary template's injected arguments) against `arguments`. On success, + /// every deduced parameter at `depth` is recorded in `bindings`. + bool unify(TemplateArguments patterns, TemplateArguments arguments); + + /// The deduced arguments, indexed by parameter index. Unbound parameters + /// hold a null TemplateArgument. + llvm::ArrayRef results() const { + return bindings; + } + +private: + bool unify(const clang::TemplateArgument& pattern, const clang::TemplateArgument& argument); + + bool unify(clang::QualType pattern, clang::QualType argument); + + /// Extract a template-id (template + arguments) view of `type`, looking + /// through TST, ClassTemplateSpecializationDecl records and injected class + /// names. Returns false if `type` is not a template-id. + bool template_id(clang::QualType type, + clang::TemplateName& name, + TemplateArguments& arguments) const; + + bool bind(unsigned index, const clang::TemplateArgument& argument); + + clang::ASTContext& context; + unsigned depth; + llvm::SmallVector bindings; +}; + +/// Deduce the arguments of `params` at its own depth by matching `patterns` +/// against `arguments`; then fill remaining parameters from default arguments +/// where representable (type defaults are returned still containing template +/// parameters — the caller substitutes them with its instantiation stack). +/// Returns false if any parameter ends up unbound and defaultless. +/// +/// `patterns` and `params` come in the same pairings the resolver already +/// uses: injected arguments for primary templates and alias templates, +/// `getTemplateArgs()` for partial specializations. +bool deduce_arguments(clang::ASTContext& context, + clang::TemplateParameterList* params, + llvm::ArrayRef patterns, + llvm::ArrayRef arguments, + llvm::SmallVectorImpl& deduced); + +/// Partial ordering via symmetric deduction: `left` is more specialized than +/// `right` iff right's pattern matches left's and not vice versa. +bool more_specialized(clang::ASTContext& context, + clang::ClassTemplatePartialSpecializationDecl* left, + clang::ClassTemplatePartialSpecializationDecl* right); + +/// If `expr` is a (possibly parenthesized/casted) reference to a non-type +/// template parameter, return its declaration. +const clang::NonTypeTemplateParmDecl* referenced_nttp(const clang::Expr* expr); + +} // namespace clice diff --git a/tests/unit/semantic/template_resolver_tests.cpp b/tests/unit/semantic/template_resolver_tests.cpp index 3ca2a4744..45604e363 100644 --- a/tests/unit/semantic/template_resolver_tests.cpp +++ b/tests/unit/semantic/template_resolver_tests.cpp @@ -417,6 +417,54 @@ TEST_CASE(DefaultArgument) { )code"); } +TEST_CASE(NttpDefaultArgument) { + run(R"code( + template + struct S { + using type = T; + }; + + template + struct test { + using input = typename S::type; + using expect = X; + }; + )code"); +} + +TEST_CASE(NttpArraySize) { + run(R"code( + template + struct S { + using type = T[N]; + }; + + template + struct test { + using input = typename S::type; + using expect = X[3]; + }; + )code"); +} + +TEST_CASE(MultiElementPack) { + run(R"code( + template + struct type_list {}; + + template + struct A { + using type = type_list; + }; + + template + struct test { + using input = typename A::type; + using expect = type_list; + }; + )code"); +} + TEST_CASE(PackExpansion) { run(R"code( template From 4a66055f08e72413fc3c114db4361269c83b4a86 Mon Sep 17 00:00:00 2001 From: ykiko Date: Wed, 29 Jul 2026 23:10:33 +0800 Subject: [PATCH 02/32] refactor(semantic): remove allocator rebind hole from resolver --- src/semantic/resolver.cpp | 93 --------------------------------------- 1 file changed, 93 deletions(-) diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 4b2740f3a..8ecd6d6ae 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -611,90 +611,6 @@ class PseudoInstantiator { return lookup_result(); } - /// Short-circuit resolution for `std::allocator_traits::rebind_alloc`. - /// - /// libstdc++'s allocator rebind chain (vector → __alloc_traits → allocator_traits → - /// allocator::rebind) creates deeply nested dependent types that are hard to resolve - /// generically. This function intercepts `allocator_traits::rebind_alloc` - /// and attempts direct resolution. - /// - /// Strategy: - /// 1. Try Alloc::rebind::other (the standard allocator rebind protocol) - /// 2. If that fails (e.g. C++20 removed allocator::rebind), fall back to - /// replacing the first template argument: allocator → allocator - /// - /// TODO: Replace with a general mechanism for resolving well-known standard - /// library patterns, or improve the resolver to handle these chains naturally. - clang::QualType hole(const clang::NestedNameSpecifier* NNS, - const clang::IdentifierInfo* member, - TemplateArguments arguments) { - if(!NNS || NNS->getKind() != clang::NestedNameSpecifier::TypeSpec) { - return clang::QualType(); - } - - auto TST = NNS->getAsType()->getAs(); - if(!TST) { - return clang::QualType(); - } - - auto TD = TST->getTemplateName().getAsTemplateDecl(); - if(!TD) - return clang::QualType(); - if(!TD->getDeclContext()->isStdNamespace()) { - return clang::QualType(); - } - - if(TD->getName() == "allocator_traits") { - if(TST->template_arguments().size() != 1) { - return clang::QualType(); - } - auto Alloc = TST->template_arguments()[0].getAsType(); - - if(member->getName() == "rebind_alloc") { - if(arguments.empty()) - return clang::QualType(); - auto T = arguments[0].getAsType(); - - auto prefix = - clang::NestedNameSpecifier::Create(context, nullptr, Alloc.getTypePtr()); - - auto rebind = &context.Idents.get("rebind"); - - auto DTST = context.getDependentTemplateSpecializationType( - clang::ElaboratedTypeKeyword::None, - clang::DependentTemplateStorage(prefix, rebind, false), - arguments); - - prefix = clang::NestedNameSpecifier::Create(context, prefix, DTST.getTypePtr()); - - auto other = &context.Idents.get("other"); - auto DNT = - context.getDependentNameType(clang::ElaboratedTypeKeyword::None, prefix, other); - - auto result = PseudoInstantiator(context, resolved, indent).resolve(DNT); - if(!result.isNull() && !result->isDependentType()) { - LOG_DEBUG( - "{}" "hole: 'allocator_traits::rebind_alloc' → '{}'", - pad(), - result.getAsString()); - return result; - } - - if(auto TST = Alloc->getAs()) { - llvm::SmallVector replaceArguments = {T}; - auto result = make_specialization(TST->getTemplateName(), replaceArguments); - LOG_DEBUG( - "{}" "hole: 'allocator_traits::rebind_alloc' → '{}'", - pad(), - result.getAsString()); - return result; - } - } - } - - return clang::QualType(); - } - private: /// Per-kind dispatch. Whitelist of type classes the resolver understands; /// anything else passes through unchanged, which downstream treats as @@ -1256,15 +1172,6 @@ class PseudoInstantiator { return clang::QualType(DTST, 0); } - if(auto result = hole(NNS, name, arguments); !result.isNull()) { - LOG_DEBUG("{}" "hole: '{}' → '{}'", pad(), name->getName().str(), result.getAsString()); - --indent; - if(cacheable) { - resolved.try_emplace(DTST, result); - } - return result; - } - auto stack_size = stack.data.size(); if(auto* decl = preferred(lookup(NNS, name))) { if(auto* TATD = llvm::dyn_cast(decl)) { From 62571b7eeb2f1621e4020d05f37923107eddb41b Mon Sep 17 00:00:00 2001 From: ykiko Date: Wed, 29 Jul 2026 23:29:54 +0800 Subject: [PATCH 03/32] feat(semantic): pseudo-SFINAE probe and template template substitution --- src/semantic/resolver.cpp | 228 +++++++++++++++++- .../unit/semantic/template_resolver_tests.cpp | 92 +++++++ 2 files changed, 315 insertions(+), 5 deletions(-) diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 8ecd6d6ae..0ee916a85 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -200,12 +200,14 @@ class PseudoInstantiator { /// - Null types (return as-is) /// - Non-dependent types (no transformation needed) /// - Excessive recursion depth (bail out to prevent runaway recursion) + /// - Exhausted step budget (bounds the total work of one query, including + /// pseudo-SFINAE probes that explore rejected branches) /// - Null results (return original type instead) clang::QualType rewrite(clang::QualType type, Policy policy) { if(type.isNull() || !type->isDependentType()) { return type; } - if(depth > 16) { + if(depth > 16 || ++steps > 4096) { return type; } ++depth; @@ -562,11 +564,21 @@ class PseudoInstantiator { partials.size()); ++indent; /// Deduction alone may match several overlapping partials; pick the - /// most specialized one, as real instantiation would. + /// most specialized one, as real instantiation would — but only among + /// partials whose dependent pattern constraints survive the + /// pseudo-SFINAE probe (see member_absent). clang::ClassTemplatePartialSpecializationDecl* best = nullptr; for(auto partial: partials) { if(deduce_template_arguments(partial, arguments)) { + bool viable = satisfies_pattern(partial); stack.pop(); + if(!viable) { + LOG_DEBUG( + "{}" "pruned partial '{}' (member absent)", + pad(), + partial->getNameAsString()); + continue; + } if(!best || more_specialized(context, partial, best)) { best = partial; } @@ -926,7 +938,26 @@ class PseudoInstantiator { canonical.emplace_back(context.getCanonicalTemplateArgument(arguments[i])); } - return context.getTemplateSpecializationType(name, arguments, canonical); + /// Fully concrete results should compare equal to the same type + /// written in source, whose canonical form is the specialization + /// decl's record type. findSpecialization is a read-only registry + /// query — if the TU never named this specialization, we keep the + /// bare canonical TST rather than fabricating a declaration. + clang::QualType underlying; + bool concrete = std::ranges::none_of(canonical, [](const clang::TemplateArgument& arg) { + return arg.isDependent(); + }); + if(concrete) { + if(auto CTD = + llvm::dyn_cast_or_null(name.getAsTemplateDecl())) { + void* pos = nullptr; + if(auto CTSD = CTD->findSpecialization(canonical, pos)) { + underlying = context.getTypeDeclType(CTSD); + } + } + } + + return context.getTemplateSpecializationType(name, arguments, canonical, underlying); } clang::QualType rewrite_template(const clang::TemplateSpecializationType* TST, Policy policy) { @@ -937,12 +968,27 @@ class PseudoInstantiator { return rewrite(TST->desugar(), policy); } + /// A bound template template parameter in the head is substituted + /// with its deduced template, e.g. `TT` after matching + /// `replace_first, U>` against `box`. + auto name = TST->getTemplateName(); + bool head_changed = false; + if(auto TTP = + llvm::dyn_cast_or_null(name.getAsTemplateDecl())) { + if(auto* bound = stack.find_argument(TTP, TTP->getDepth(), TTP->getIndex()); + bound && bound->getKind() == clang::TemplateArgument::Template) { + name = bound->getAsTemplate(); + head_changed = true; + } + } + llvm::SmallVector arguments; - if(!rewrite_arguments(TST->template_arguments(), arguments, policy)) { + bool args_changed = rewrite_arguments(TST->template_arguments(), arguments, policy); + if(!head_changed && !args_changed) { return clang::QualType(); } - return make_specialization(TST->getTemplateName(), arguments); + return make_specialization(name, arguments); } /// Rewrite a template argument list. Returns true if anything changed. @@ -1067,6 +1113,176 @@ class PseudoInstantiator { } } + /// Pseudo-SFINAE: decide whether a partial specialization's dependent + /// pattern constraints (e.g. the `void_t::other>` + /// idiom) are satisfiable under the current bindings. + /// + /// Real SFINAE substitutes and rejects on ill-formedness. We approximate + /// with a three-way probe on each dependent member access in the pattern: + /// - member resolves → constraint holds, keep the partial + /// - qualifier resolves to a known template/record but the member does + /// not exist there → constraint provably fails, prune the partial + /// - qualifier unknown (bare parameter etc.) → benefit of the doubt, + /// keep the partial; never guess a concrete answer from uncertainty + bool satisfies_pattern(clang::ClassTemplatePartialSpecializationDecl* partial) { + if(probing > 4) { + return true; + } + + /// The probe expression only survives in the as-written arguments: + /// the converted list has already desugared `void_t<...>` to `void`. + auto written = partial->getTemplateArgsAsWritten(); + if(!written) { + return true; + } + + ++probing; + bool viable = true; + for(const clang::TemplateArgumentLoc& loc: written->arguments()) { + auto& argument = loc.getArgument(); + if(argument.getKind() == clang::TemplateArgument::Type && + member_absent(argument.getAsType(), 0)) { + viable = false; + break; + } + } + --probing; + return viable; + } + + /// Walk the written form of `type` (through alias sugar arguments, which + /// is where `void_t` hides its probe expression) and report whether any + /// dependent member access provably names a non-existent member. + bool member_absent(clang::QualType type, unsigned guard) { + /// Note: `void_t` canonically IS `void`, so this must test + /// instantiation dependence, not type dependence. + if(type.isNull() || guard > 16 || !type->isInstantiationDependentType()) { + return false; + } + + const clang::Type* T = type.getLocalUnqualifiedType().getTypePtr(); + switch(T->getTypeClass()) { + case clang::Type::DependentName: { + auto DNT = llvm::cast(T); + return specifier_absent(DNT->getQualifier(), guard) || + scope_lacks(DNT->getQualifier(), DNT->getIdentifier()); + } + + case clang::Type::DependentTemplateSpecialization: { + auto DTST = llvm::cast(T); + auto& template_name = DTST->getDependentTemplateName(); + auto identifier = template_name.getName().getIdentifier(); + auto qualifier = template_name.getQualifier(); + if(specifier_absent(qualifier, guard)) { + return true; + } + return identifier && scope_lacks(qualifier, identifier); + } + + case clang::Type::TemplateSpecialization: { + auto TST = llvm::cast(T); + /// Check the arguments as written: alias sugar (`void_t<...>`) + /// desugars to a type that no longer contains the probe. + for(auto& argument: TST->template_arguments()) { + if(argument.getKind() == clang::TemplateArgument::Type && + member_absent(argument.getAsType(), guard + 1)) { + return true; + } + } + return false; + } + + case clang::Type::Elaborated: { + return member_absent(llvm::cast(T)->getNamedType(), + guard + 1); + } + case clang::Type::Paren: { + return member_absent(llvm::cast(T)->getInnerType(), guard + 1); + } + case clang::Type::Pointer: { + return member_absent(llvm::cast(T)->getPointeeType(), + guard + 1); + } + case clang::Type::LValueReference: + case clang::Type::RValueReference: { + return member_absent(llvm::cast(T)->getPointeeType(), + guard + 1); + } + case clang::Type::PackExpansion: { + return member_absent(llvm::cast(T)->getPattern(), + guard + 1); + } + + default: { + return false; + } + } + } + + /// Does any link of the specifier chain provably name a missing member? + bool specifier_absent(const clang::NestedNameSpecifier* NNS, unsigned guard) { + if(!NNS || guard > 16) { + return false; + } + if(specifier_absent(NNS->getPrefix(), guard + 1)) { + return true; + } + + switch(NNS->getKind()) { + case clang::NestedNameSpecifier::Identifier: { + return scope_lacks(NNS->getPrefix(), NNS->getAsIdentifier()); + } + case clang::NestedNameSpecifier::TypeSpec: { + const clang::Type* T = NNS->getAsType(); + if(auto DTST = llvm::dyn_cast(T)) { + auto& template_name = DTST->getDependentTemplateName(); + auto scope = template_name.getQualifier() ? template_name.getQualifier() + : NNS->getPrefix(); + auto identifier = template_name.getName().getIdentifier(); + return identifier && scope_lacks(scope, identifier); + } + if(auto DNT = llvm::dyn_cast(T)) { + return scope_lacks(DNT->getQualifier(), DNT->getIdentifier()); + } + return false; + } + default: { + return false; + } + } + } + + /// Resolve `scope` and ask whether it is a known template or record that + /// definitely has no member called `name`. Unknown scopes return false. + bool scope_lacks(const clang::NestedNameSpecifier* scope, clang::DeclarationName name) { + if(!scope) { + return false; + } + + auto stack_size = stack.data.size(); + auto resolved_scope = rewrite_specifier(scope, Policy::Resolve); + + bool lacks = false; + if(resolved_scope && resolved_scope->getKind() == clang::NestedNameSpecifier::TypeSpec) { + auto type = resolve(clang::QualType(resolved_scope->getAsType(), 0)); + if(!type.isNull()) { + if(auto TST = type->getAs()) { + if(llvm::isa_and_nonnull( + TST->getTemplateName().getAsTemplateDecl())) { + lacks = lookup(type, name).empty(); + } + } else if(auto RD = type->getAsCXXRecordDecl()) { + lacks = RD->lookup(name).empty() && lookup_in_bases(RD, name).empty(); + } + } + } + + while(stack.data.size() > stack_size) { + stack.pop(); + } + return lacks; + } + clang::QualType resolve_dependent_name(const clang::DependentNameType* DNT) { LOG_DEBUG("{}" "resolve '{}'", pad(), clang::QualType(DNT, 0).getAsString()); ++indent; @@ -1228,6 +1444,8 @@ class PseudoInstantiator { llvm::SmallPtrSet active_resolutions; llvm::DenseSet> active_ctd_lookups; unsigned depth = 0; + unsigned steps = 0; + unsigned probing = 0; unsigned indent = 0; std::string pad() const { diff --git a/tests/unit/semantic/template_resolver_tests.cpp b/tests/unit/semantic/template_resolver_tests.cpp index 45604e363..1dce4ebde 100644 --- a/tests/unit/semantic/template_resolver_tests.cpp +++ b/tests/unit/semantic/template_resolver_tests.cpp @@ -417,6 +417,98 @@ TEST_CASE(DefaultArgument) { )code"); } +TEST_CASE(TemplateTemplateReplace) { + run(R"code( + template + struct box {}; + + template + struct replace_first {}; + + template