Files
clice/tests/unit/semantic/template_resolver_tests.cpp
ykiko a1b6c0632d 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>
2026-04-05 00:18:07 +08:00

955 lines
22 KiB
C++

#include "test/test.h"
#include "test/tester.h"
#include "clang/AST/RecursiveASTVisitor.h"
namespace clice::testing {
namespace {
struct InputFinder : clang::RecursiveASTVisitor<InputFinder> {
CompilationUnitRef unit;
clang::QualType input;
clang::QualType expect;
using Base = clang::RecursiveASTVisitor<InputFinder>;
InputFinder(CompilationUnitRef unit) : unit(unit) {}
bool TraverseDecl(clang::Decl* decl) {
if(decl && (llvm::isa<clang::TranslationUnitDecl>(decl) ||
unit.file_id(decl->getLocation()) == unit.interested_file())) {
Base::TraverseDecl(decl);
}
return true;
}
bool VisitTypedefNameDecl(const clang::TypedefNameDecl* decl) {
if(decl->getName() == "input") {
input = decl->getUnderlyingType();
}
if(decl->getName() == "expect") {
expect = decl->getUnderlyingType();
}
return true;
}
};
TEST_SUITE(TemplateResolver, Tester) {
void run(llvm::StringRef code) {
add_main("main.cpp", code);
ASSERT_TRUE(compile());
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(TypeParameterType) {
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(SingleLevel) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T>
struct A {
using type = type_list<T>;
};
template <typename X>
struct test {
using input = typename A<X>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(SingleLevelNotDependent) {
run(R"code(
template <typename T>
struct A {
using type = int;
};
template <typename X>
struct test {
using input = typename A<X>::type;
using expect = int;
};
)code");
}
TEST_CASE(MultiLevel) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using type = type_list<T1>;
};
template <typename T2>
struct B {
using type = typename A<T2>::type;
};
template <typename T3>
struct C {
using type = typename B<T3>::type;
};
template <typename X>
struct test {
using input = typename C<X>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(MultiLevelNotDependent) {
run(R"code(
template <typename T1>
struct A {
using type = int;
};
template <typename T2>
struct B {
using type = typename A<T2>::type;
};
template <typename T3>
struct C {
using type = typename B<T3>::type;
};
template <typename X>
struct test {
using input = typename C<X>::type;
using expect = int;
};
)code");
}
TEST_CASE(ArgumentDependent) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using type = T1;
};
template <typename T2>
struct B {
using type = type_list<T2>;
};
template <typename X>
struct test {
using input = typename B<typename A<X>::type>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(AliasArgument) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using type = T1;
};
template <typename T2>
struct B {
using base = A<T2>;
using type = type_list<typename base::type>;
};
template <typename X>
struct test {
using input = typename B<X>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(AliasDependent) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using type = type_list<T1>;
};
template <typename T2>
struct B {
using base = A<T2>;
using type = typename base::type;
};
template <typename X>
struct test {
using input = typename B<X>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(AliasTemplate) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1, typename U1>
struct A {
using type = type_list<T1, U1>;
};
template <typename T2>
struct B {
template <typename U2>
using type = typename A<T2, U2>::type;
};
template <typename X, typename Y>
struct test {
using input = typename B<X>::template type<Y>;
using expect = type_list<X, Y>;
};
)code");
}
TEST_CASE(BaseDependent) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using type = type_list<T1>;
};
template <typename U2>
struct B : A<U2> {};
template <typename X>
struct test {
using input = typename B<X>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(MultiNested) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using self = A<T1>;
using type = type_list<T1>;
};
template <typename X>
struct test {
using input = typename A<X>::self::self::self::self::self::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(OuterDependentMemberClass) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
template <typename T2>
struct B {
template <typename T3>
struct C {
using type = type_list<T1, T2, T3>;
};
};
};
template <typename X, typename Y, typename Z>
struct test {
using input = typename A<X>::template B<Y>::template C<Z>::type;
using expect = type_list<X, Y, Z>;
};
)code");
}
TEST_CASE(InnerDependentMemberClass) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T>
struct test {
template <int N, typename U>
struct B {
using type = type_list<U, T>;
};
using input = typename B<1, T>::type;
using expect = type_list<T, T>;
};
)code");
}
TEST_CASE(InnerPartialMember) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T, typename U>
struct test {};
template <typename T>
struct test<T, T> {
template <int N, typename U>
struct A {
using type = type_list<U, T>;
};
using input = typename A<1, T>::type;
using expect = type_list<T, T>;
};
)code");
}
TEST_CASE(PartialSpecialization) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {};
template <typename U2>
struct B {};
template <typename U2, template <typename...> typename HKT>
struct B<HKT<U2>> {
using type = type_list<U2>;
};
template <typename X>
struct test {
using input = typename B<A<X>>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(PartialDefaultArgument) {
run(R"code(
template <typename T, typename U = T>
struct X {};
template <typename T>
struct X<T, T> {
using type = T;
};
template <typename T>
struct test {
using input = typename X<T>::type;
using expect = T;
};
)code");
}
TEST_CASE(DefaultArgument) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename T1>
struct A {
using type = type_list<T1>;
};
template <typename U1, typename U2 = A<U1>>
struct B {
using type = typename U2::type;
};
template <typename X>
struct test {
using input = typename B<X>::type;
using expect = type_list<X>;
};
)code");
}
TEST_CASE(PackExpansion) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename U, typename... Us>
struct X {
using type = type_list<Us...>;
};
template <typename... Ts>
struct test {
using input = typename X<int, Ts...>::type;
using expect = type_list<Ts...>;
};
)code");
}
TEST_CASE(BasePackExpansion) {
run(R"code(
template <typename... Ts>
struct type_list {};
template <typename U, typename... Us>
struct X {
using type = type_list<Us...>;
};
template <typename... Us>
struct Y : X<int, Us...> {};
template <typename... Ts>
struct test {
using input = typename Y<Ts...>::type;
using expect = type_list<Ts...>;
};
)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>
template <typename T>
struct test {
using input = typename std::vector<T>::reference;
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_SUITE(TemplateResolver)
} // namespace
} // namespace clice::testing