refactor(semantic): rewrite template resolver — eliminate SubstType, fix crashes (#388)
## Summary Rewrites the template resolver to eliminate `SubstType`/`CodeSynthesisContexts` dependency, fixing widespread crashes on real-world C++ code. ### What changed **Architecture**: replaced double-TreeTransform (PseudoInstantiator + SubstType) with single-layer design: - **`SubstituteOnly`** — new lightweight TreeTransform for Phase 2 (typedef expansion + parameter substitution). Does NOT override `TransformDependentNameType`, breaking the typedef ↔ lookup infinite cycle. - **`PseudoInstantiator`** — retains heuristic lookup (the unique value clang doesn't provide), delegates substitution to `SubstituteOnly`. **Deleted**: - `DesugarOnly` class - `instantiate()` method and all `CodeSynthesisContexts` / `SubstType` usage - `state()` / `rewind()` stack management - `std::abort()` on valid NNS kinds - `#ifndef NDEBUG` debug flag + `std::print` logging **Added**: - `SubstituteOnly` class with depth guard - `InstantiationStack::findArgument()` — depth/index based parameter lookup - CTD→TST resolution for `DependentTemplateSpecializationType` (enables `__alloc_traits::rebind<T>::other` resolution) - `active_resolutions` (DNT cycle detection) + `active_ctd_lookups` (CTD cycle detection via RAII guard) - Stack frame pollution fix: pop lookup frames before further `TransformType` - Pack argument support (single-element forwarding) - Null safety on all Transform return paths - Structured `LOG_DEBUG` trace logging with indentation - `--log-level` / `--test-filter` CLI options for unit test runner - Bounds checks in `hole()`, `ResugarOnly`, `visitTemplateDeclContexts` **Tests**: 20 → 36 passing test cases (+5 documented TODOs for known limitations). New coverage: recursive base classes, multiple inheritance, typedef chains, CRTP, `remove_reference` partial specs, `std::map`, `std::basic_string`, pack forwarding. ### Stress test result ``` CDB: llvm-project build (4669 C++ files) Types resolved: 3,690,190 Types unchanged: 75,907,298 Crashes: 0 ``` Before this PR, the same test produced ~52% crash rate (413 crashes in 800 files). ### Known limitations (documented as TODOs) - NTTP partial specialization matching (`enable_if<true, X>`, `A<X, 0>`) - Template template parameter deduction - Non-dependent qualifier nested class templates (`Outer<int>::Inner<X>`) - Multi-element pack expansion - `CXXDependentScopeMemberExpr` lookup (unimplemented) - Operator-name lookup in dependent contexts 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Enhanced template resolution with improved cycle detection to prevent infinite loops * Better type substitution handling for complex dependent types * **Bug Fixes** * Fixed edge cases in template specialization resolution * Improved null-safety in type transformations * Enhanced handling of standard library template traits * **Tests** * Expanded test coverage for recursive and complex template patterns * Added validation for standard library type resolution <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -331,7 +331,7 @@ TEST_CASE(InnerDependentMemberClass) {
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(InnerDependentPartialMemberClass) {
|
||||
TEST_CASE(InnerPartialMember) {
|
||||
run(R"code(
|
||||
template <typename... Ts>
|
||||
struct type_list {};
|
||||
@@ -456,6 +456,476 @@ TEST_CASE(BasePackExpansion) {
|
||||
)code");
|
||||
}
|
||||
|
||||
// --- Robustness tests for edge cases found during stress testing ---
|
||||
|
||||
TEST_CASE(RecursiveBaseClass) {
|
||||
// Regression test: callback_traits<F> inherits callback_traits<decltype(&F::operator())>,
|
||||
// creating infinite recursion through lookupInBases. CTD cycle detection must bail out.
|
||||
// We set input = expect because the resolver cannot fully resolve this pattern;
|
||||
// the test verifies it doesn't crash or hang.
|
||||
run(R"code(
|
||||
template <typename F>
|
||||
struct callback_traits : callback_traits<decltype(&F::operator())> {};
|
||||
|
||||
template <typename R, typename C, typename... Args>
|
||||
struct callback_traits<R (C::*)(Args...) const> {
|
||||
using result_type = R;
|
||||
};
|
||||
|
||||
template <typename F>
|
||||
struct test {
|
||||
using input = typename callback_traits<F>::result_type;
|
||||
using expect = typename callback_traits<F>::result_type;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(PointerType) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct A {
|
||||
using type = T*;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::type;
|
||||
using expect = X*;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(ReferenceType) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct A {
|
||||
using type = T&;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::type;
|
||||
using expect = X&;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(ConstQualified) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct A {
|
||||
using type = const T;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::type;
|
||||
using expect = const X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
// TODO: Outer<int> is non-dependent, TransformNestedNameSpecifierLoc
|
||||
// doesn't trigger our heuristic lookup for non-dependent qualifiers.
|
||||
// TEST_CASE(NestedClassTemplate) { ... }
|
||||
|
||||
TEST_CASE(MultipleInheritance) {
|
||||
run(R"code(
|
||||
template <typename... Ts>
|
||||
struct type_list {};
|
||||
|
||||
template <typename T>
|
||||
struct Base1 {
|
||||
using type1 = type_list<T>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Base2 {
|
||||
using type2 = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Derived : Base1<T>, Base2<T> {};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename Derived<X>::type1;
|
||||
using expect = type_list<X>;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(SecondBaseInheritance) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct Base1 {
|
||||
using type1 = int;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Base2 {
|
||||
using type2 = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Derived : Base1<T>, Base2<T> {};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename Derived<X>::type2;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(TypedefChain) {
|
||||
// Deep typedef chain that SubstituteOnly must expand
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct A {
|
||||
using step1 = T;
|
||||
using step2 = step1;
|
||||
using step3 = step2;
|
||||
using type = step3;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(DependentBaseTypedef) {
|
||||
// Base class type depends on template parameter through alias
|
||||
run(R"code(
|
||||
template <typename... Ts>
|
||||
struct type_list {};
|
||||
|
||||
template <typename T>
|
||||
struct Base {
|
||||
using value_type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Derived {
|
||||
using base = Base<T>;
|
||||
using type = typename base::value_type;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename Derived<X>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(CRTPPattern) {
|
||||
// Common CRTP pattern
|
||||
run(R"code(
|
||||
template <typename Derived>
|
||||
struct Base {
|
||||
using derived_type = Derived;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Impl : Base<Impl<T>> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename Impl<X>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
// TODO: NTTP partial specialization matching not yet supported.
|
||||
// checkTemplateArguments only fills default TemplateTypeParmDecl args.
|
||||
// TEST_CASE(NonTypeTemplateParam) { ... }
|
||||
|
||||
TEST_CASE(IdentityAlias) {
|
||||
// Alias template that forwards type unchanged
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
using identity = T;
|
||||
|
||||
template <typename T>
|
||||
struct A {
|
||||
using type = identity<T>;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(ConditionalType) {
|
||||
// Partial specialization as conditional
|
||||
run(R"code(
|
||||
template <bool B, typename T, typename F>
|
||||
struct conditional {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T, typename F>
|
||||
struct conditional<false, T, F> {
|
||||
using type = F;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename conditional<true, X, int>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
// TODO: Same as NonTypeTemplateParam — partial specialization on `false`
|
||||
// requires NTTP matching which is not yet supported.
|
||||
// TEST_CASE(ConditionalTypeFalse) { ... }
|
||||
|
||||
// TODO: Template template parameter deduction not yet supported.
|
||||
// TEST_CASE(TemplateTemplateParam) { ... }
|
||||
|
||||
TEST_CASE(DependentReturnType) {
|
||||
// Resolve through a struct that wraps a function return type pattern
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct remove_reference {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct remove_reference<T&> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct remove_reference<T&&> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename remove_reference<X&>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(RvalueRefRemoval) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct remove_reference {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct remove_reference<T&> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct remove_reference<T&&> {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename remove_reference<X&&>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(AddPointer) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct add_pointer {
|
||||
using type = T*;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct add_pointer<T&> {
|
||||
using type = T*;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename add_pointer<X&>::type;
|
||||
using expect = X*;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
// TODO: enable_if<true, X> requires NTTP partial specialization matching.
|
||||
// TEST_CASE(EnableIfLike) { ... }
|
||||
|
||||
TEST_CASE(NestedLookup) {
|
||||
// Two levels of dependent lookup: A<T>::B<T>::type
|
||||
run(R"code(
|
||||
template <typename... Ts>
|
||||
struct type_list {};
|
||||
|
||||
template <typename T>
|
||||
struct A {
|
||||
template <typename U>
|
||||
struct B {
|
||||
using type = type_list<T, U>;
|
||||
};
|
||||
};
|
||||
|
||||
template <typename X, typename Y>
|
||||
struct test {
|
||||
using input = typename A<X>::template B<Y>::type;
|
||||
using expect = type_list<X, Y>;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(IndirectBaseClass) {
|
||||
// Member found through two levels of inheritance
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct GrandBase {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Middle : GrandBase<T> {};
|
||||
|
||||
template <typename T>
|
||||
struct Top : Middle<T> {};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename Top<X>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(SelfReferentialAlias) {
|
||||
// Type alias that refers back to the same class (like iterator::self)
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct Wrapper {
|
||||
using self = Wrapper<T>;
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename Wrapper<X>::self::self::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(VoidSpecialization) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct A {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct A<void> {
|
||||
using type = int;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::type;
|
||||
using expect = X;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(DependentSizedArray) {
|
||||
run(R"code(
|
||||
template <typename T>
|
||||
struct A {
|
||||
using type = T;
|
||||
using pointer = type*;
|
||||
};
|
||||
|
||||
template <typename X>
|
||||
struct test {
|
||||
using input = typename A<X>::pointer;
|
||||
using expect = X*;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(MultiplePacks) {
|
||||
// Two separate pack parameters
|
||||
run(R"code(
|
||||
template <typename... Ts>
|
||||
struct type_list {};
|
||||
|
||||
template <typename T, typename... Us>
|
||||
struct A {
|
||||
using type = type_list<T, Us...>;
|
||||
};
|
||||
|
||||
template <typename X, typename... Ys>
|
||||
struct test {
|
||||
using input = typename A<X, Ys...>::type;
|
||||
using expect = type_list<X, Ys...>;
|
||||
};
|
||||
)code");
|
||||
}
|
||||
|
||||
TEST_CASE(StandardMap) {
|
||||
add_main("main.cpp", R"code(
|
||||
#include <map>
|
||||
|
||||
template <typename K, typename V>
|
||||
struct test {
|
||||
using input = typename std::map<K, V>::mapped_type;
|
||||
using expect = V;
|
||||
};
|
||||
)code");
|
||||
ASSERT_TRUE(compile_driver());
|
||||
|
||||
InputFinder finder(*unit);
|
||||
finder.TraverseAST(unit->context());
|
||||
|
||||
auto input = unit->resolver().resolve(finder.input);
|
||||
auto target = finder.expect;
|
||||
ASSERT_FALSE(input.isNull() || target.isNull());
|
||||
EXPECT_EQ(input.getCanonicalType(), target.getCanonicalType());
|
||||
}
|
||||
|
||||
TEST_CASE(StandardString) {
|
||||
add_main("main.cpp", R"code(
|
||||
#include <string>
|
||||
|
||||
template <typename T>
|
||||
struct test {
|
||||
using input = typename std::basic_string<T>::value_type;
|
||||
using expect = T;
|
||||
};
|
||||
)code");
|
||||
ASSERT_TRUE(compile_driver());
|
||||
|
||||
InputFinder finder(*unit);
|
||||
finder.TraverseAST(unit->context());
|
||||
|
||||
auto input = unit->resolver().resolve(finder.input);
|
||||
auto target = finder.expect;
|
||||
ASSERT_FALSE(input.isNull() || target.isNull());
|
||||
EXPECT_EQ(input.getCanonicalType(), target.getCanonicalType());
|
||||
}
|
||||
|
||||
TEST_CASE(Standard) {
|
||||
add_main("main.cpp", R"code(
|
||||
#include <vector>
|
||||
|
||||
Reference in New Issue
Block a user