Skip to content

Commit 0364eb4

Browse files
committed
test(e2e): #256 clang module operator-template canary + module-package hazard docs
mcpp has no defect here: --precompile succeeds and the crash is inside the Clang frontend on the import side. But mcpp bundles LLVM, so the hazard ships with the toolchain, and it lands squarely on the module-package pattern this project recommends — wrapping an upstream header whose operators are static inline templates and mirroring their signatures with a trivially-true constraint is exactly how you hit it. Two things it needs, both of which mcpp owns: Documentation, so the next person does not repeat the bisect: every template parameter of an exported operator template should be pinned by the FIRST argument; otherwise deduce whole operand types and constrain them. The reporter's shipped workaround is included, along with the property that makes this so hard to attribute — the crash is name-keyed, so one poisoned operator* makes every `x * y` in every importer crash on unrelated types. A canary, because the alternative is finding out by accident. It is a STATE canary rather than a pass/fail contract: the expectation table records crash for LLVM 20-22 and ok for 18-19, and the test fails when reality stops matching — a Clang bump that FIXES this is just as much a signal as one that re-breaks it, and neither should be able to change what packages can express without anyone noticing. A control build without the unpinned overload guards the canary itself. Verified against both bundled toolchains (20.1.7 and 22.1.8 crash as recorded; system clang 18.1.3 and gcc 16.1.0 compile the same repro fine). An upstream LLVM report is filed separately; this does not wait on it.
1 parent f886901 commit 0364eb4

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

‎docs/03-toolchains.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,56 @@ When `MCPP_HOME` is not set explicitly, mcpp locates the sandbox automatically b
267267
## ABI Capability Enforcement
268268

269269
A dependency can declare an `abi:<name>` capability (for example, `compat.glfw` declares `abi:glibc`). When the resolved toolchain's ABI does not satisfy any dependency's abi requirement, the build **fails early** with a suggested fix (for example, a musl-static toolchain encountering an abi:glibc dependency), replacing deeper link/header errors. Inspect with: `mcpp why toolchain`.
270+
271+
## Known Toolchain Hazard: Operator Templates in Module Interfaces (Clang 20+)
272+
273+
A module that exports **replacement operator templates** can poison name
274+
lookup for that operator in every importer when compiled by Clang 20 or 22:
275+
any translation unit that `import`s the module and uses that operator — **on
276+
any type at all** — crashes the frontend (SIGSEGV). GCC 16 and Clang 18 are
277+
unaffected, so this is a regression somewhere between Clang 18 and 20.
278+
279+
This bites the module-package pattern directly. Wrapping an upstream header
280+
whose operators are `static inline` templates, and mirroring their signatures
281+
with a trivially-true constraint (the standard mixed-TU subsumption recipe),
282+
is exactly how you hit it.
283+
284+
**The rule of thumb:** every template parameter should be pinned by the
285+
**first** function argument. Shapes that break this are the poisonous ones:
286+
287+
```cpp
288+
// Poisonous — `n` and `l` are not determined by argument 1
289+
template<typename T, int m, int n, int l>
290+
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b);
291+
292+
// Poisonous — second typename appears only in argument 2
293+
template<typename T1, typename T2, int n>
294+
Vec<T1, n>& operator+=(Vec<T1, n>& a, const Vec<T2, n>& b);
295+
296+
// Fine — every parameter is pinned by argument 1
297+
template<typename T, int m, int n>
298+
Matx<T, m, n> operator+(const Matx<T, m, n>& a, const Matx<T, m, n>& b);
299+
```
300+
301+
The crash is **name-keyed**: one poisoned `operator*` declaration makes every
302+
`x * y` in every importer crash, for entirely unrelated types. The function
303+
body is irrelevant.
304+
305+
**The workaround** is to deduce whole operand types and constrain them,
306+
rather than destructuring them in the parameter list. It stays
307+
call-compatible, and mixed-TU semantics survive because the upstream
308+
exact-pattern `static inline` remains more specialized and still wins there:
309+
310+
```cpp
311+
template<typename MA, typename MB>
312+
requires pick<typename MA::value_type>
313+
&& __is_same(MA, typename MA::mat_type)
314+
&& __is_same(MB, Matx<typename MB::value_type,
315+
(int)MA::rows, (int)MA::cols>)
316+
inline MA& operator+=(MA& a, const MB& b);
317+
```
318+
319+
Tracked as [mcpp#256](https://github.com/mcpp-community/mcpp/issues/256).
320+
`tests/e2e/150_clang_module_operator_template.sh` is a canary over the
321+
bundled LLVM toolchains, so a future Clang bump that fixes — or re-breaks —
322+
this becomes visible instead of silently changing what packages can express.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env bash
2+
# #256: known-issue canary for a Clang regression that mcpp cannot fix but
3+
# that silently limits what a module package may export.
4+
#
5+
# A module exporting replacement operator templates whose parameters are NOT
6+
# all pinned by the first argument poisons name lookup for that operator in
7+
# every importer: the frontend SIGSEGVs on any use of the name, on any type.
8+
# Clang 18 and GCC 16 are fine; Clang 20 and 22 crash. Since mcpp bundles
9+
# LLVM, the hazard ships with the toolchain.
10+
#
11+
# This is a STATE canary, not a pass/fail contract: today the expected result
12+
# for the bundled toolchains is "crashes". The test fails when reality stops
13+
# matching the recorded expectation — i.e. when a Clang bump fixes it (time
14+
# to update the docs and the table) or when a version previously known-good
15+
# regresses. Silence here would mean a future toolchain bump quietly changed
16+
# what packages can express.
17+
set -e
18+
19+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20+
source "$SCRIPT_DIR/_llvm_env.sh"
21+
22+
if [[ ! -d "$LLVM_ROOT" ]]; then
23+
echo "SKIP: no llvm payload installed"
24+
exit 0
25+
fi
26+
27+
CLANGXX="$LLVM_ROOT/bin/clang++"
28+
[[ -x "$CLANGXX" ]] || { echo "SKIP: no clang++ in $LLVM_ROOT"; exit 0; }
29+
30+
TMP=$(mktemp -d)
31+
trap "rm -rf $TMP" EXIT
32+
cd "$TMP"
33+
34+
cat > matx.h <<'EOF'
35+
#pragma once
36+
namespace cv {
37+
template<typename T, int m, int n> struct Matx {
38+
T val[m*n];
39+
typedef Matx<T, m, n> mat_type;
40+
enum { rows = m, cols = n };
41+
};
42+
template<typename T> struct Point_ { T x, y; };
43+
using Point = Point_<int>;
44+
template<typename T, int m, int n, int l> static inline
45+
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b) { return Matx<T,m,n>{}; }
46+
template<typename T> static inline
47+
Point_<T> operator*(const Point_<T>& a, int b) { return Point_<T>{a.x*b, a.y*b}; }
48+
}
49+
EOF
50+
51+
# The module layer: upstream shape mirrored with a trivially-true constraint.
52+
# The 4-parameter Matx*Matx overload is the poisonous one — `n` and `l` are
53+
# not determined by the first argument.
54+
cat > m.cppm <<'EOF'
55+
module;
56+
#include "matx.h"
57+
export module m;
58+
export namespace cv {
59+
using cv::Matx;
60+
using cv::Point_;
61+
using cv::Point;
62+
inline namespace repl {
63+
template<typename T> inline constexpr bool pick = true;
64+
template<typename T, int m, int n, int l> requires pick<T> inline
65+
Matx<T, m, n> operator*(const Matx<T, m, l>& a, const Matx<T, l, n>& b) { return Matx<T,m,n>{}; }
66+
template<typename T> requires pick<T> inline
67+
Point_<T> operator*(const Point_<T>& a, int b) { return Point_<T>{a.x*b, a.y*b}; }
68+
}
69+
}
70+
EOF
71+
72+
# Note: only a Point is multiplied. The crash is name-keyed, so an unrelated
73+
# type is enough — that is what makes the hazard so hard to attribute.
74+
cat > use.cpp <<'EOF'
75+
import m;
76+
int main() { cv::Point p{1,2}; auto q = p * 2; return q.x; }
77+
EOF
78+
79+
"$CLANGXX" -std=c++23 --precompile m.cppm -o m.pcm > precompile.log 2>&1 || {
80+
cat precompile.log
81+
echo "FAIL: producing the BMI should always succeed — the defect is on the import side"
82+
exit 1
83+
}
84+
85+
set +e
86+
"$CLANGXX" -std=c++23 -fprebuilt-module-path=. -c use.cpp -o use.o > use.log 2>&1
87+
IMPORT_RC=$?
88+
set -e
89+
90+
# Control: the same module WITHOUT the unpinned overload must always compile.
91+
# If this ever fails, the canary itself is broken rather than the toolchain.
92+
grep -v 'Matx<T, m, n> operator\*(const Matx<T, m, l>& a' m.cppm \
93+
| grep -v 'template<typename T, int m, int n, int l> requires pick<T> inline' > ctl.cppm
94+
"$CLANGXX" -std=c++23 --precompile ctl.cppm -o ctl.pcm > ctl_pre.log 2>&1 || {
95+
cat ctl_pre.log; echo "FAIL: control module failed to precompile"; exit 1; }
96+
97+
# Recorded expectations, by LLVM major version.
98+
case "${LLVM_VERSION%%.*}" in
99+
18|19) EXPECTED=ok ;;
100+
20|21|22) EXPECTED=crash ;;
101+
*) EXPECTED=unknown ;;
102+
esac
103+
104+
if [[ $IMPORT_RC -eq 0 ]]; then ACTUAL=ok; else ACTUAL=crash; fi
105+
106+
echo " llvm@${LLVM_VERSION}: expected=${EXPECTED} actual=${ACTUAL}"
107+
108+
if [[ "$EXPECTED" == "unknown" ]]; then
109+
echo " no recorded expectation for LLVM ${LLVM_VERSION%%.*} — record one in this test"
110+
echo "OK"
111+
exit 0
112+
fi
113+
114+
if [[ "$ACTUAL" != "$EXPECTED" ]]; then
115+
echo
116+
echo "FAIL: #256 canary state changed for llvm@${LLVM_VERSION}."
117+
if [[ "$ACTUAL" == "ok" ]]; then
118+
echo " The importer now compiles — the Clang regression appears FIXED."
119+
echo " Update the expectation table here and the hazard section in"
120+
echo " docs/03-toolchains.md, and tell mcpp-community/mcpp#256."
121+
else
122+
echo " The importer now crashes on a version previously known good."
123+
echo " A toolchain bump has re-broken module operator templates."
124+
sed -n '1,25p' use.log
125+
fi
126+
exit 1
127+
fi
128+
129+
echo "OK"

0 commit comments

Comments
 (0)