October 6, 2026·4 min read·engineeringcppllvmmoduleslookup

LLVM 23.1.3 repairs module lookup for range adaptors

LLVM 23.1.3 includes a fix for missing friend declarations during module argument-dependent lookup. The reproducer shows why import history could break a valid pipe expression.

Using a range-adaptor closure in one module should not make the same adaptor unusable in a module that imports it. Yet that is the failure reported in LLVM issue 218228. A later pipe expression could fail with invalid operands even though removing the earlier use made compilation succeed. The repair is among the changes included in LLVM 23.1.3, released on October 6.

This is a useful maintenance-release story because the symptom points in the wrong direction. A diagnostic about the operands of operator| suggests an incompatible range or adaptor type. The reported cause instead concerns declarations the compiler had not loaded from a module. No new source syntax was needed. The compiler needed to make the existing friend declarations available at the point where lookup required them.

After a decade with C++, I find the coexistence of several language eras harder to manage than any individual feature. A library abstraction expressed through templates can interact with a module import and produce diagnostics far from the missing declaration. Fixes that restore predictable behavior at those boundaries matter to the practical cost of using the language.

An earlier adaptor use changes the outcome

The original report uses std::views::split in two module interfaces. One imports the standard library module and forms a split closure. The second imports that module and applies a split closure through the pipe operator to a string view. Compilation of the second interface fails, while removing the earlier closure use allows it to compile.

That dependency is the informative part of the reproducer. The failing expression is not isolated from the compiler state created while loading imports. A source reduction that kept only the last expression would lose the condition that triggers the bug. The report therefore includes the module files and the commands that build them in sequence.

The diagnostic lists unrelated operator| candidates, such as overloads for byte and stream flags, before rejecting the expression. Those candidates do not explain why the intended overload disappeared. The compiler can perform an overload search and issue an ordinary-looking diagnostic even when its candidate set is incomplete because an earlier compiler step failed to load required declarations.

The reduced case removes the standard library

Chuanqi Xu asks for a minimal reproducer without the standard library. The reporter supplies a small adaptor implementation with a friend function template for the pipe operator. A class template derives from the class introducing that friend and forwards invocation to an adaptor object. Separate modules recreate the earlier use and the later failed expression.

Removing the standard library narrows the problem. It shows that the failure does not require the complete implementation of views::split or a particular string-view operation. The smaller program retains the relationship between an associated class, a friend operator and module loading. That is enough to investigate the compiler behavior directly.

The resulting release-branch patch identifies the cause as a friend declaration not being loaded when argument-dependent lookup runs. Its presence in the 23.1.3 commit history connects the specific repair to this maintenance release.

Lookup now loads the associated friends

The patch adds a query for lazy friend declarations and a function that loads them. Argument-dependent lookup first identifies the associated namespaces and classes. Before continuing lookup, the new code examines associated class definitions and loads friends when their stored representation is still lazy.

The loading function follows serialized friend-list offsets through the external syntax-tree source. This is a compiler representation detail with a direct language consequence. A declaration stored in a module must participate when lookup requires it, even if the compiler deferred materializing that declaration earlier. The fix makes that transition explicit at the lookup boundary.

The patch comments acknowledge a possible efficiency cost for classes with many friends. Loading them as a list is the immediate solution, while a name lookup table is mentioned as a possible future response if this becomes a bottleneck. The repair does not claim a measured compile-time improvement. Its purpose is to restore the missing candidates for correct lookup.

The regression test preserves module history

The added test builds the reduced adaptor module, then a module containing the earlier adaptor use, then consumers. It repeats the sequence with reduced module interfaces as well as full interfaces. This matters because serialization and lazy loading are part of the trigger, so one compilation strategy alone would leave a relevant path unexamined.

The consumers include a module interface and translation units mixing imports with the adaptor header. Header inclusion appears both before and after the imports. Each case expects no diagnostics. The test therefore checks the source arrangements involved in the failure instead of merely asserting that a helper function can traverse a friend list.

For users of modules and adaptor-style libraries, the concrete benefit is a previously rejected lookup case becoming compilable. The report and the reduced test identify the required trigger precisely enough to compare with an affected build. LLVM 23.1.3 supplies the repair within the existing compiler series, with a regression test that retains the import sequence needed to reproduce the original failure.