Skip to content

Commit 65bf675

Browse files
committed
the token was accepted in silence and changed nothing
`[c-abi] builtins = "iso"` emitted `-fno-builtin-memset_pattern16` from this mechanism's first revision. A/B on a real compile command, varying only that flag, reads as built (flag present) 1 reference to memset_pattern16 flag REMOVED 1 reference -fno-builtin 0 -mllvm -disable-loop-idiom-memset 0 and clang accepts `-fno-builtin-totally_not_a_function` just as quietly: the `-fno-builtin-<fn>` family is matched against clang's builtin table, while `memset_pattern16` is an LLVM TargetLibraryInfo libfunc. The call is emitted by LoopIdiomRecognize, which consults TLI, and the per-function attribute does not reach it. `-mllvm` is not chosen because it passes an internal LLVM option, which can be renamed or removed between releases; when it is, the mechanism returns to failing silently, which is the defect being repaired. The cost of the blunt flag is measured rather than argued: on the translation unit that surfaced this the object grows 38200 to 38888 bytes, 1.8 per cent. THE NO-OP SURVIVED BECAUSE IT HAD NO CRITERION. `cenv` verifies its tokens against a `-dM` dump, and a code-generation property is not visible there. The criterion now lives in `openkal-cross.yml` and has three legs, on all three hosts: no flag (the symbol MUST appear, or the probe measures nothing), the per-function flag (it must still appear, pinning the defect), and an `aarch64-macos` build over the openkal stack by the mcpp under test (zero references). Run against the previous binary the step fails, at the link: ld64.lld: error: undefined symbol: memset_pattern16
1 parent 93f5bfc commit 65bf675

7 files changed

Lines changed: 205 additions & 6 deletions

File tree

.github/workflows/openkal-cross.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,117 @@ jobs:
264264
echo "${{ matrix.host }} → $t : $(ls -l "$a" | awk '{print $5}') bytes"
265265
done
266266
267+
# THE ONLY CRITERION THE `builtins` TOKEN HAS, AND WHY IT IS HERE.
268+
#
269+
# `[c-abi] builtins = "iso"` states that the C library supplies the ISO
270+
# functions and no vendor extensions. On Darwin targets clang's loop
271+
# idiom recogniser rewrites a constant-pattern fill into a call to
272+
# `memset_pattern16`, an Apple libc extension no such library carries.
273+
# That call is produced by the code generator, so it appears in no `-D`
274+
# and in no preprocessor dump --- and `mcpp.toolchain.cenv` verifies its
275+
# tokens by comparing a `-dM` dump. The token this mechanism emitted was
276+
# therefore a silent no-op for the whole of its first life, and this
277+
# step exists because no other kind of check could have reported it.
278+
#
279+
# LEG 1 IS THE DENOMINATOR, AND IT IS NOT DECORATION. At `-O0` the pass
280+
# does not run, and `-ffreestanding` implies `-fno-builtin`; an
281+
# assertion that only reads "the symbol is absent" therefore passes in
282+
# several worlds where nothing was measured. Leg 1 compiles with no flag
283+
# at all and fails if the symbol does NOT appear.
284+
#
285+
# LEG 2 PINS THE DEFECT ITSELF. `-fno-builtin-memset_pattern16` is
286+
# accepted in silence and changes nothing: `-fno-builtin-<fn>` is matched
287+
# against clang's builtin table, and `memset_pattern16` is an LLVM
288+
# TargetLibraryInfo libfunc rather than a clang builtin. The toolchain is
289+
# pinned in this job, so the reading is stable. Should a later pin make
290+
# leg 2 fail, clang has gained the narrower behaviour, and `cenv` can
291+
# emit the narrower flag and recover the 1.8 per cent `-fno-builtin`
292+
# costs.
293+
#
294+
# LEG 3 IS THE ENGINE: the same idiom, over the openkal stack, for
295+
# `aarch64-macos`, built by the mcpp under test. Measured on the token
296+
# this step was written for, the two readings are
297+
#
298+
# -fno-builtin-memset_pattern16 1 reference, and the link fails
299+
# -fno-builtin 0 references, and it links
300+
#
301+
# so a regression here reports itself at the link before the assertion
302+
# is reached. The assertion covers the remaining case, in which some
303+
# layer happens to supply the symbol and the link succeeds anyway.
304+
#
305+
# `-O2` IS PER PACKAGE RATHER THAN `--release`. The idiom pass does not
306+
# run at the dev profile's `-O0`, and a release build would compile the
307+
# runtime a second time in a second profile for no reading.
308+
- name: builtins = "iso" withdraws the Apple pattern fill
309+
run: |
310+
set -euo pipefail
311+
BIN="${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-llvm/22.1.8/bin"
312+
# Not `ls ... | head -1`: with `pipefail` the absent candidate's
313+
# exit status ends the step before the guard below is reached.
314+
CLANG=""
315+
for c in "$BIN/clang" "$BIN/clang.exe"; do
316+
if [ -x "$c" ]; then CLANG="$c"; break; fi
317+
done
318+
[ -n "$CLANG" ] || { echo "::error::no clang under $BIN"; exit 1; }
319+
320+
# Inside the clone, so the dependency is named by a relative path.
321+
# `$RUNNER_TEMP` is a backslash path on the Windows host and a TOML
322+
# string would read its separators as escapes.
323+
W="$RUNNER_TEMP/okl/examples/builtins-probe"
324+
rm -rf "$W"; mkdir -p "$W/src"
325+
326+
# The one shape the idiom recogniser rewrites. The element type is
327+
# `int` and not `char` because a byte-repeating value becomes
328+
# `memset`, which every C library has.
329+
cat > "$W/src/main.cpp" <<'PROBE'
330+
extern "C" void fill(int* a, long n) {
331+
for (long i = 0; i < n; ++i) a[i] = 0x01020304;
332+
}
333+
334+
int main() {
335+
static int buf[64];
336+
fill(buf, 64);
337+
return buf[0] == 0x01020304 ? 0 : 1;
338+
}
339+
PROBE
340+
341+
cat > "$W/mcpp.toml" <<'PROJECT'
342+
[package]
343+
name = "openkal-builtins-probe"
344+
version = "0.1.0"
345+
346+
[build]
347+
cxxflags = ["-O2"]
348+
349+
[dependencies]
350+
openkal-llvm-runtime = { path = "../.." }
351+
352+
[toolchain]
353+
default = "llvm@22.1.8"
354+
PROJECT
355+
356+
# The symbol name is in the object's string table, so a byte match
357+
# reads it without naming an `llvm-nm` whose path differs per host.
358+
refs() { grep -ac memset_pattern16 "$1" 2>/dev/null || true; }
359+
T=--target=arm64-apple-macos14.0
360+
361+
"$CLANG" $T -O2 -c "$W/src/main.cpp" -o "$W/bare.o"
362+
"$CLANG" $T -O2 -fno-builtin-memset_pattern16 -c "$W/src/main.cpp" -o "$W/narrow.o"
363+
"$CLANG" $T -O2 -fno-builtin -c "$W/src/main.cpp" -o "$W/blunt.o"
364+
echo "no flag : $(refs "$W/bare.o")"
365+
echo "-fno-builtin-memset_pattern16 : $(refs "$W/narrow.o")"
366+
echo "-fno-builtin : $(refs "$W/blunt.o")"
367+
368+
[ "$(refs "$W/bare.o")" = 1 ] || { echo "::error::leg 1: the probe no longer triggers the idiom, so legs 2 and 3 measure nothing"; exit 1; }
369+
[ "$(refs "$W/narrow.o")" = 1 ] || { echo "::error::leg 2: clang now honours -fno-builtin-memset_pattern16, and mcpp.toolchain.cenv can emit the narrower token"; exit 1; }
370+
[ "$(refs "$W/blunt.o")" = 0 ] || { echo "::error::leg 2: -fno-builtin no longer withdraws the pattern fill"; exit 1; }
371+
372+
(cd "$W" && "$MCPP_UNDER_TEST" build --target aarch64-macos)
373+
obj=$(find "$W/target" -name 'main.o' | head -1)
374+
[ -n "$obj" ] || { echo "::error::leg 3 produced no object to read"; exit 1; }
375+
echo "engine, aarch64-macos over openkal: $(refs "$obj")"
376+
[ "$(refs "$obj")" = 0 ] || { echo "::error::leg 3: builtins = \"iso\" did not withdraw memset_pattern16"; exit 1; }
377+
267378
- uses: actions/upload-artifact@v4
268379
with:
269380
name: openkal-built-on-${{ matrix.host }}

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,45 @@
55

66
## [Unreleased]
77

8+
### `builtins = "iso"` 发的那个 token 是静默空操作,已换成 `-fno-builtin`
9+
10+
`[c-abi] builtins = "iso"` 声明 C 库只提供 ISO 函数、没有厂商扩展。Apple 目标上
11+
clang 的循环惯用法识别会把常量模式填充改写成 `memset_pattern16` 调用——那是一个
12+
Apple libc 扩展,这样的库没有它。此前这里发的是 `-fno-builtin-memset_pattern16`。
13+
在一份真实的 `build.ninja` 编译命令上做 A/B,只改这一个 flag:
14+
15+
| flag | `memset_pattern16` 引用数 |
16+
|---|---|
17+
| 照原样(flag 在) | 1 |
18+
| flag **删掉** | 1 |
19+
| `-fno-builtin` | 0 |
20+
| `-mllvm -disable-loop-idiom-memset` | 0 |
21+
22+
**而它报不出自己什么都没做。** clang 静默接受 `-fno-builtin-totally_not_a_function`:
23+
`-fno-builtin-X` 这一族按 clang 的 builtin 表校验,而 `memset_pattern16` 是 LLVM
24+
TargetLibraryInfo 的 libfunc,不在那张表里;发出调用的是 LoopIdiomRecognize,它查
25+
TLI,按函数名的属性到不了它。
26+
27+
不选 `-mllvm` 的理由是它传的是 LLVM 内部选项,不是受支持的接口,改名或删除之后这套
28+
机制会再次静默失效——那正是这次要修的缺陷本身。代价是量出来的:在暴露此事的那个翻译
29+
单元(libarchive 的 7zip reader,`-O2`,aarch64-macos)上,目标文件从 38200 涨到
30+
38888 字节,1.8%,因为 `-fno-builtin` 同时撤走了 C 库确实提供的那些 ISO 函数。这比
31+
`builtins = "iso"` 声明的范围宽,而它宽在安全的方向:代码生成器不合成的调用不会变成
32+
链接错误。
33+
34+
**这个空操作能活下来,是因为它没有判据。** `cenv` 的探针用 `-dM` dump 校验自己发的
35+
token,而代码生成阶段的性质在预处理器 dump 里不可见。判据现在在
36+
`.github/workflows/openkal-cross.yml`,三条腿,跑在三台宿主上:
37+
38+
| 腿 | 内容 | 判据 |
39+
|---|---|---|
40+
| 1 | 不带任何 flag 编译探针 | 符号**必须出现**——否则探针已经触发不了惯用法,腿 2、3 什么都没测 |
41+
| 2 | `-fno-builtin-memset_pattern16` | 符号**仍必须出现**(钉住这个缺陷;若哪天红了,说明 clang 认了窄拼法,`cenv` 可以改回去把这 1.8% 拿回来) |
42+
| 3 | mcpp 为 `aarch64-macos` 走 openkal 栈构建同一份源码 | 目标文件里**零引用** |
43+
44+
腿 3 的对照:换回旧 token,同一个工程链接失败于
45+
`ld64.lld: error: undefined symbol: memset_pattern16`。
46+
847
### 更正:那个「四个成员」是二,而分组用错了依据
948

1049
2026.9.21.2 的条目、`cenv.cppm` 与 `predefines.cppm` 的注释、`docs/21` 与 `docs/22`

docs/22-target-side.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ that names no C library:
378378
| macOS | `posix` / `arch-default` | one token, `-D__unix__` — Apple's clang predefines `__APPLE__`/`__MACH__` on its default triple, never `__unix__` |
379379
| freestanding | `posix` / `arch-default` | the same one token, `-D__unix__`, for the same reason: nothing here defines it either |
380380
| Windows | `posix` / `arch-default` | Cygwin-flavoured: `--target=x86_64-pc-cygwin` on the compile line only; plus `-D__MCPP_TARGET_WINDOWS__` (see the note below); `data-model` becomes LP64 as a consequence of the triple, not a separate flag |
381-
| any | `builtins = "iso"` | turns off code-generation idioms that assume a platform C library — `-fno-builtin-memset_pattern16` on Apple targets is the one this survey measured; see `src/toolchain/cenv.cppm` for what else was checked and found not to apply |
381+
| any | `builtins = "iso"` | turns off code-generation idioms that assume a platform C library — `-fno-builtin` on Apple targets, because the per-function spelling was measured to be a silent no-op for the one idiom that matters (`memset_pattern16` is an LLVM TargetLibraryInfo libfunc, not a clang builtin); see `src/toolchain/cenv.cppm` for the A/B and the measured cost |
382382
| anything else | | refused, naming the target, the request and what is missing — never a silent downgrade |
383383

384384
**The macOS and freestanding rows are a correction, not the design's original

docs/zh/22-target-side.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ libunwind 的 `assembly.h`,正是按这个宏来选寄存器保存集的)。mcpp
310310
| macOS | `posix` / `arch-default` | 一个令牌,`-D__unix__`——Apple 的 clang 默认三元组预定义的是 `__APPLE__`/`__MACH__`,从来不是 `__unix__` |
311311
| 裸机(freestanding) | `posix` / `arch-default` | 同样一个令牌,`-D__unix__`,原因相同:这里同样没有任何东西定义它 |
312312
| Windows | `posix` / `arch-default` | 采用 Cygwin 式语义:仅在编译行加 `--target=x86_64-pc-cygwin`;并加 `-D__MCPP_TARGET_WINDOWS__`(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 |
313-
| 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——本轮实测到的唯一一例是 Apple 目标上的 `-fno-builtin-memset_pattern16`;`src/toolchain/cenv.cppm` 记录了还核实过哪些、结论是不适用 |
313+
| 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——Apple 目标上发 `-fno-builtin`,因为按函数名的那个拼法对唯一重要的那个惯用法实测是**静默空操作**(`memset_pattern16` 是 LLVM TargetLibraryInfo 的 libfunc,不是 clang 的 builtin);A/B 与实测代价见 `src/toolchain/cenv.cppm` |
314314
| 其余情况 | | 明确拒绝,点名目标、请求与缺什么——不静默降级 |
315315

316316
**macOS 与裸机这两行是一次修正,不是设计原文(协调者修订,2026.9.18.1 发布不到一天就被真实

src/toolchain/cenv.cppm

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,9 +558,51 @@ inline std::expected<Realisation, std::string> realise(
558558
// So `iso` realises to one flag, on Apple targets only, and does nothing
559559
// measurable elsewhere today. That is reported rather than silently
560560
// accepted: a caller that wants to know what changed reads `builtinsTokens`.
561+
// `-fno-builtin`, NOT `-fno-builtin-memset_pattern16`, AND THE NARROWER
562+
// SPELLING WAS A SILENT NO-OP.
563+
//
564+
// The per-function form was emitted here from this mechanism's first
565+
// revision, because `memset_pattern16` is the one platform idiom measured
566+
// to matter and a targeted flag looks like the smaller instrument. A/B on
567+
// the real compile command from a build.ninja, varying only this flag,
568+
// says it does nothing:
569+
//
570+
// as built (flag present) 1 reference to memset_pattern16
571+
// flag REMOVED 1 reference
572+
// -fno-builtin 0
573+
// -mllvm -disable-loop-idiom-memset 0
574+
//
575+
// AND IT CANNOT REPORT THAT IT DOES NOTHING. clang accepts
576+
// `-fno-builtin-totally_not_a_function` in silence: the `-fno-builtin-X`
577+
// family is checked against clang's builtin table, and
578+
// `memset_pattern16` is an LLVM TargetLibraryInfo libfunc rather than a
579+
// clang builtin. The call is produced by LoopIdiomRecognize, which
580+
// consults TLI, and the per-function attribute does not reach it.
581+
//
582+
// WHY THE BLUNT ONE AND NOT `-mllvm`. `-mllvm` passes an internal LLVM
583+
// option; it is not a supported interface and can be renamed or removed
584+
// between releases, and when it is, this mechanism goes back to failing
585+
// silently --- which is exactly the defect being repaired.
586+
//
587+
// THE COST IS MEASURED RATHER THAN ARGUED: on the translation unit that
588+
// surfaced this (libarchive's 7zip reader, `-O2`, aarch64-macos) the
589+
// object grows 38200 to 38888 bytes, 1.8 per cent, because `-fno-builtin`
590+
// also withdraws the ISO functions the C library does supply. That is
591+
// broader than `builtins = "iso"` declares, and it errs in the safe
592+
// direction: a call the generator does not synthesise is never a link
593+
// error.
594+
//
595+
// NOTHING HERE IS VERIFIED BY THE PROBE, WHICH IS HOW THE NO-OP SURVIVED.
596+
// `expectDefined`/`expectUndefined` are compared against the probe's `-dM`
597+
// dump, under this module's own rule that a `-D` which did not take effect
598+
// is a verification failure rather than a silent one. A code-generation
599+
// property is not visible in a preprocessor dump, so the criterion for
600+
// this token lives in `openkal-cross.yml`, which compiles an
601+
// idiom-triggering unit for `aarch64-macos` over the openkal stack and
602+
// asserts the symbol is absent from the object.
561603
if (decl.builtins == mcpp::targetside::CAbiBuiltins::Iso) {
562604
if (!freestanding && (os == "macos" || os == "ios"))
563-
r.builtinsTokens.push_back("-fno-builtin-memset_pattern16");
605+
r.builtinsTokens.push_back("-fno-builtin");
564606
}
565607

566608
return r;

tests/unit/test_cenv.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,14 @@ TEST(CEnv, BuiltinsIsoOnMacosDisablesMemsetPattern16) {
147147
ts::CAbiBuiltins::Iso);
148148
auto r = cenv::realise(d, "macos", "x86_64", false);
149149
ASSERT_TRUE(r.has_value()) << r.error();
150-
EXPECT_TRUE(has(r->builtinsTokens, "-fno-builtin-memset_pattern16"));
150+
// `-fno-builtin`, NOT the per-function spelling. The narrower form was
151+
// emitted here and measured to do nothing: clang checks `-fno-builtin-X`
152+
// against its builtin table, `memset_pattern16` is an LLVM TLI libfunc,
153+
// and the call is produced by a pass that consults TLI. clang accepts an
154+
// unknown name in that family in silence, so the flag looked right and
155+
// was not.
156+
EXPECT_TRUE(has(r->builtinsTokens, "-fno-builtin"));
157+
EXPECT_FALSE(has(r->builtinsTokens, "-fno-builtin-memset_pattern16"));
151158
}
152159

153160
TEST(CEnv, BuiltinsIsoOnLinuxAddsNothingMeasurable) {

tests/unit/test_cenv_probe.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,11 @@ TEST(CenvProbeArgv, AHostedCrossTargetCarriesItsCrossFlag) {
352352

353353
TEST(CenvProbeArgv, BuiltinsTokensAreCarriedLast) {
354354
auto r = cp::assemble_argv("", {}, {"-D__unix__"},
355-
{"-fno-builtin-memset_pattern16"},
355+
{"-fno-builtin"},
356356
false, "x86_64-apple-macos");
357357
ASSERT_TRUE(r.has_value()) << r.error();
358358
ASSERT_EQ(r->size(), 2u);
359-
EXPECT_EQ((*r)[1], "-fno-builtin-memset_pattern16");
359+
EXPECT_EQ((*r)[1], "-fno-builtin");
360360
}
361361

362362
// ── The argv must select the target (mcpp#674 review, 2026-09-20) ──────────

0 commit comments

Comments
 (0)