feat(freestanding): exceptions off graph-wide, and the std subset becomes usable - #459
Merged
Conversation
…omes usable
The freestanding std subset was described in the phase-3 wrap-up as "a separate
piece of work". That was wrong, and the research it contradicted had already
measured it. Reproduced here on the shipped payloads: with a synthesised
`__config_site`, 103 of libc++'s 110 headers compile for riscv64-none-elf, and
all 7 that fail also fail on an x86_64 host with full libc++ and glibc — they
are headers libc++ has not implemented. The freestanding compile-time loss is
zero. `-nostdinc++` is the MECHANISM of that route, not an obstacle: it keeps
libc++'s headers private to the subset package, which exports a module — the
same shape the board package already uses for the target's C library.
What actually blocked it was exceptions, and that is a whole-graph property:
* `std::optional::value()` alone pulls in `__cxa_throw`, two vtables and
`std::bad_optional_access::~bad_optional_access`, none of which can exist
without an unwinder or libc++abi.
* A project's own `[build] cxxflags` cannot fix it: they do not reach a
dependency's module compile, so the BMI and the importer disagree and clang
reports a `.pcm` "configuration mismatch" — naming a module file rather
than the setting that split the graph.
* `is_dialect_flag` deliberately does NOT propagate `-fno-exceptions`, on the
grounds that "dependencies may assume exceptions are available". Right for
a hosted target, exactly backwards here.
So `-fno-exceptions -fno-rtti` join `-ffreestanding` and `-nostdinc++` on the
target spec, which is the one place that reaches every unit in the graph.
⚠️ That change alone would have broken every existing bare-metal user on
upgrade. The dependency cache key carries the target TRIPLE but not the flags
the triple implies — and which flags those are is mcpp's decision, so it moves
between versions while the triple string does not. A project that had built
once got its old BMI back and a hard failure naming a .pcm. Fixed by adding a
`targetImpliedFlags` axis, empty for hosted targets so no existing key moves.
e2e/133 pins the whole chain in the emulator, from both sides: the subset
builds, runs `ranges::sort` with a projection plus optional/atomic/span/
string_view on the target, and `std::mutex` fails to COMPILE — capabilities
turned off in `__config_site` vanish rather than leaving a run-time stub.
The `import std` diagnostic names `std-freestanding` again, now that the
package is published.
…e one
Both bare-metal packages carried `[xlings] deps = ["xim:picolibc-riscv@1.8.12"]`
and the standard-library subset also carried `xim:llvm`. That pinned a board
package and an implementation-neutral library alike to one libc, one ISA, one
toolchain and one version of each — none of which is a property of either.
The cause was a gap in the target model rather than sloppy packaging. A hosted
target has always had its C library resolved for it: musl rides inside its gcc
payload and glibc arrives through PayloadPaths, which is why nobody writes
`xim:glibc` in a manifest. A freestanding target pins a generic clang, which
carries no target libc, and there was no axis for one — so the requirement
leaked outward into every package.
kKnownTargets now names the target's C library beside its compiler pin. It is
installed through the same channel `[xlings] deps` already use, its headers go
on every compile line and its directory on the link search path. Location is a
target fact; selection stays a board fact — a board still chooses
`-lcrt0-semihost` over a UART crt0, and still names its linker script.
Two interfaces let a package ask instead of declare: `mcpp::toolchain_dir()`
for headers that ship with the toolchain (libc++'s, for the freestanding
subset) and `mcpp::sysroot_dir()` for a file inside the target's C library (a
linker script). Both follow whatever `[toolchain]` and `--target` resolved.
Three things this turned up:
* `-L` appended to the ordinary ldflags is discarded — a freestanding link
line is replaced wholesale, so it has to go through LinkInputs. Measured:
the flag was built and then simply was not there.
* `link-script` resolves a bare name against the PACKAGE root, so a board
cannot rely on the linker's search path for it; it asks for the sysroot.
* e2e/131's privacy assertion was testing the wrong thing. It proved a
dependency's `include-dir` stays private by showing `#include <stdio.h>`
fails — but with the libc owned by the target that now correctly SUCCEEDS,
exactly as on a hosted build. Split into two: the target's C headers must
reach the consumer, and a header the BOARD ships must not.
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 19, 2026
…hip answers
Adds a scenario document — what a user types, what they see, and what they no
longer have to write — with the commands and outputs taken from a real run of
the released binary rather than sketched.
Folds two review questions into the analysis:
* `picolibc-riscv` / `qemu-riscv` carrying the target in the name follows
xim's existing rule, which is visible across the index:
aarch64-linux-musl-gcc, riscv64-linux-musl-gcc, mingw-cross-gcc, musl-gcc.
The NAME carries the target; the `archs` axis carries the host, which is
why `llvm` has no target in its name at all.
* picolibc belongs on the xim side, resolved at build time from the target's
row. It is not importable, it is chosen by the target rather than by a
dependency graph, and every other C library in the ecosystem — glibc, musl,
musl-cross-make — is already there; mcpp-index carries zero libc packages.
Moving it would put the libc back into the package graph, undoing #459.
Both answers came with a gap worth recording: `[target.X]` has toolchain,
linkage, runner and cxx_runtime but no `sysroot`, so a project cannot swap
picolibc for newlib today.
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 19, 2026
Written from the discussion that followed the #455-#459 review, and grounded in what is measurable today rather than sketched: every claim marked with a source was verified against the shipped payloads (llvm 22.1.8, gcc 16.1.0, picolibc 1.8.12) while writing. The load-bearing decisions: * Partitioning by RESOURCE KIND, not by which standard-library facility it lights up. The latter couples a kernel ABI to C++ and to today's library, and inverts the dependency — it is the same mistake POSIX made for C and WASIp1 made for POSIX, one generation further on. 'Lights up std::X' is demoted to the upward admission criterion, which is where it belongs. * An opaque one-word handle, because that is what makes openkal indifferent to sitting above or below libc. An int fd forces it below (Windows needs a table); a FILE* forces it above. Measured: four backends store their native thing with zero bridging. * fs and net dissolve. Naming goes to openkal.namespace, and what it hands back is the same stream resource a file, a socket or a UART gives you. Cleaner than 'everything is a file', because naming failure and I/O failure end up in different interfaces. * core is abort + stream + memory. Memory is core because a bump allocator over a static arena is an IMPLEMENTATION, not an emulation — the test being whether a fake would make callers silently wrong, which is true of a clock that does not advance but not of an allocator that can fail. Also records the caps-can-lie problem with four defences ordered by strength, led by making unsupported operations unrepresentable in the type system rather than false in a bool — the same conclusion K1/K2 reached for the MMU.
Sunrisepeak
added a commit
that referenced
this pull request
Aug 19, 2026
…plementation plan (#461) * docs: deep review of #455-#459 and the bare-metal ecosystem Covers what the five PRs did, the four releases they produced, and the ecosystem work alongside them (xim-pkgindex #651/#652/#653, mcpp-index #219/#220, two new mcpplibs repos), assessed on architecture, compatibility, simplicity, stability and cross-platform. Every claim carries its source — a PR number, a file, or the command that measured it — because the point of the document is to be checkable rather than summarised. Three of its assertions were re-verified against the tree while writing it. The uncomfortable half is deliberate: * five defects in this round were self-inflicted, two of them found only AFTER a release; * three test criteria were themselves wrong — green tests that were not testing the thing they named; * the largest architectural error (packages declaring the target's C library) was found by review, not by me, and I had written 'this cannot be done' about the std subset while my own research document had measured that it could. * docs: user-facing bare-metal scenarios, and the package-naming/ownership answers Adds a scenario document — what a user types, what they see, and what they no longer have to write — with the commands and outputs taken from a real run of the released binary rather than sketched. Folds two review questions into the analysis: * `picolibc-riscv` / `qemu-riscv` carrying the target in the name follows xim's existing rule, which is visible across the index: aarch64-linux-musl-gcc, riscv64-linux-musl-gcc, mingw-cross-gcc, musl-gcc. The NAME carries the target; the `archs` axis carries the host, which is why `llvm` has no target in its name at all. * picolibc belongs on the xim side, resolved at build time from the target's row. It is not importable, it is chosen by the target rather than by a dependency graph, and every other C library in the ecosystem — glibc, musl, musl-cross-make — is already there; mcpp-index carries zero libc packages. Moving it would put the libc back into the package graph, undoing #459. Both answers came with a gap worth recording: `[target.X]` has toolchain, linkage, runner and cxx_runtime but no `sysroot`, so a project cannot swap picolibc for newlib today. * docs: openkal design — a two-sided kernel ABI specification Written from the discussion that followed the #455-#459 review, and grounded in what is measurable today rather than sketched: every claim marked with a source was verified against the shipped payloads (llvm 22.1.8, gcc 16.1.0, picolibc 1.8.12) while writing. The load-bearing decisions: * Partitioning by RESOURCE KIND, not by which standard-library facility it lights up. The latter couples a kernel ABI to C++ and to today's library, and inverts the dependency — it is the same mistake POSIX made for C and WASIp1 made for POSIX, one generation further on. 'Lights up std::X' is demoted to the upward admission criterion, which is where it belongs. * An opaque one-word handle, because that is what makes openkal indifferent to sitting above or below libc. An int fd forces it below (Windows needs a table); a FILE* forces it above. Measured: four backends store their native thing with zero bridging. * fs and net dissolve. Naming goes to openkal.namespace, and what it hands back is the same stream resource a file, a socket or a UART gives you. Cleaner than 'everything is a file', because naming failure and I/O failure end up in different interfaces. * core is abort + stream + memory. Memory is core because a bump allocator over a static arena is an IMPLEMENTATION, not an emulation — the test being whether a fake would make callers silently wrong, which is true of a clock that does not advance but not of an allocator that can fail. Also records the caps-can-lie problem with four defences ordered by strength, led by making unsupported operations unrepresentable in the type system rather than false in a bool — the same conclusion K1/K2 reached for the MMU. * docs(openkal): retract two decisions after review, and record six open questions Two things the design got wrong, both retracted with the reasoning that made them look right at the time: * `openkal.namespace` replacing fs and net. It violated this document's own §5.1 rule (a stream whose caps are the union of file and socket operations is precisely the 'present but useless' antipattern), it required every backend to carry a URI parser — which is an emulation layer by the downward admission criterion — and the WASIp2 precedent it cited was a misreading: WASIp2 separates resource KINDS and shares only the stream type. * Extending cfg() with capability predicates. That conclusion was about openarch's AddressSpace; going through openkal interface by interface, core has no semantic axis at all, and the triple already carries most of what cfg(mmu) would have. The design is now a zero-engine-change proposal. Also corrects the module wiring: `reexport` propagates DOWNSTREAM, so an interface package cannot use it to reach a backend the consumer chose. The backend reexports the interface instead, which is what that mechanism is for. Six open questions the draft did not take a position on, led by one that is measured rather than theoretical: picolibc's vfprintf references free, so routing operator new to kal_alloc while printf keeps picolibc's malloc puts two allocators on the same RAM. The spec has to require that kal_alloc be built over a libc allocator where one exists, not beside it. * docs(openkal): capabilities are ADL-probeable — the caps struct and its config file are gone The design's §4 rested on one measurement: `requires { mcpp::runner("x") }` is a hard error when the name is absent. The measurement was right; the quantifier in the conclusion was not. It is QUALIFIED names that cannot be probed — unqualified lookup through ADL is dependent inside a template and evaluates to false, exactly as wanted. Verified on llvm 22.1.8 against real C++20 modules, not headers, with all three behaviours holding at once: * backend present → the concept is true and the call resolves to it * backend absent → the concept is FALSE, so `if constexpr` degrades * backend absent, called anyway → a compile error carrying the spec's own wording, which is what 'build the diagnostic in' was asking for⚠️ One trap worth the record: if the fallback overload returns the same type as the real one, the concept is true even with no backend — a requires-expression does not instantiate the body, so the static_assert never fires. The fallback must return a distinct type. Measured, after writing it the other way first. Consequences: the caps struct, the generated caps module and capabilities.toml are all deleted. A backend's module interface IS its capability declaration, so claim and implementation become the same artifact by construction and the whole 'caps can lie' problem shrinks from structural+behavioural to behavioural only. Nothing leaves mcpp.toml — backend selection stays a conditional dependency. * docs(openkal): the backend owns the interface module name, and consumers declare both Answers two review questions that turned out to be the same question. The draft had the application write `import openkal.uart;`. That is wrong — it pins the source to a backend, which is the one thing openkal exists to avoid. But it papered over a real constraint, now measured on mcpp 2026.8.19.4 with gcc 16.1.0: * a transitive dependency's module IS importable *⚠️ but ADL does NOT reach a module the translation unit did not import ('seek' was not declared in this scope) So the backend's declarations must live in the module the app imports, which forces the backend to own the well-known name `openkal.stream` while the interface package provides `openkal.abi.stream`. Verified end to end: the app writes one import, names no backend, and ADL resolves to the backend's seek.⚠️ And a trap worth the record: the interface module cannot be called `openkal.stream.abi` — the module graph reads the dots as hierarchy and ninja reports a self-cycle on openkal.stream.gcm. `openkal.abi.stream` is fine. On dependencies: two, not one. The backend alone would work, but declaring the contract is what lets the APPLICATION pin the contract version, turning a mismatch into a resolution error instead of a pile of signature errors at compile time. Same shape as embedded-hal plus a board crate. * docs(openkal): openkal IS the ABI, and the fragmentation risk is mechanically bounded Naming: the interface package is `openkal`, not `openkal-abi` — openkal is the specification, so saying it twice is noise. Two module names are still forced by the language (§4.3), but the qualifier now lands only where implementers see it: applications write `import openkal.stream;`, backend authors write `export import openkal.decl.stream;`. The backend owning the application-visible module name is the one real cost of that shape, and it is a fragmentation risk: a backend could put non-standard names into the standard module and applications would not notice. What bounds it is that most of the surface is not the backend's to touch — ⓘ measured, a backend redefining the interface's types is rejected outright: error: redeclaring 'struct kal::io_result@openkal.decl.stream' in module 'openkal.stream' conflicts with import so the only freedom left is adding overloads, and THAT is statically checkable: conformance diffs the module's exported name set — and signatures, since an `unsigned long` offset would still win ADL through a conversion — against the spec list. Vendor extensions must live under a different module name, which makes 'I used an extension' visible in the source. Second review pass adds two findings: the cardinality that matters is one implementation per INTERFACE rather than one backend per program (a program may take stream from one provider and memory from another), and the fallback overload is too greedy — unconstrained, it catches every kal type and tells a socket it is not a seekable stream. * docs(openkal): add a complete Linux reference implementation Two identities: a backend that works today, and the thing other implementers copy. It does not move the D0 gate — that gate is whether a THIRD PARTY writes a third backend — but it turns 'guess the shape and write the implementation' into 'write the implementation'. Writing it out surfaced three things the design document had not: * core operations need no ADL at all. They are declared `extern "C"` by the interface and defined by the backend; missing means a link error. The ADL mechanism serves optional capabilities only, which makes the common path simpler than the draft implied. * short writes are a spec question nobody had asked. ::write(2) may write less than requested, so openkal must choose: write-all-or-error (the loop lives once, in the backend) or allow short writes (every caller writes the loop — which is exactly where POSIX has tripped programs up for decades). * ⭐ it independently confirms the fs/net decomposition. On Linux, seekability is a property of the HANDLE, not of the backend — lseek succeeds on a file and returns ESPIPE on a pipe. If openkal.stream had seek, the Linux backend could not answer honestly: claiming it means always failing on pipes, which is precisely the 'present but useless' antipattern. Because §2.3 puts seek on openkal.fs's descriptor instead, the question does not arise. That last point is the strongest argument for writing a complete reference at all: it is the only way to find a decomposition error, and it finds it earlier than a conformance suite would. * docs(openkal): module naming is normative, and decl is not interchangeable with impl `openkal.impl.*` would be semantically backwards: that module belongs to the interface package and holds declarations — types, the extern "C" surface, the fallback overloads, the concepts. What an implementation provides is `openkal.<interface>` itself. The name has to be in the spec rather than left to taste, for three reasons that are all load-bearing: every backend must `export import` that exact name, so it is part of the contract; the guarantee that a backend cannot redefine the interface's types only holds while all backends import the SAME module; and conformance's exported-name diff needs to know which names came from the shared module.⚠️ The rationale has to ship with the rule. A spec reader will naturally reach for `openkal.stream.decl` — the dotted extension — and that one was measured to produce a ninja self-cycle on openkal.stream.gcm. A rule without its reason sends the first implementer straight into it. Also records a simplification that was considered and rejected: one `openkal` module holding every interface's declarations. It costs a naming level but breaks per-interface independent versioning, and drags task/fs declarations into a backend that only provides streams. * docs: openkal 0.1 implementation plan, and the design document now points at the shipped packages openkal 0.1 exists as two published packages: mcpplibs/openkal carries the specification and the modules that declare it, and mcpplibs/openkal-linux is the reference implementation, maintained as the worked example other implementations follow. Both are mirrored, and the mirrored archives were verified byte-identical. The plan document records the task dependencies, the criteria applied to each decision, and what verification established. Two results are worth separating from the rest. Writing a complete reference implementation confirmed the decomposition independently of the reasoning that produced it: on Linux, whether a stream can be repositioned is a property of the individual descriptor rather than of the implementation, so an openkal.stream that offered positioning could have been neither claimed honestly nor withheld usefully. A decomposition error of that kind is invisible in specification text and would have surfaced later. The exported-surface checker required by clause 9.3 was verified in both directions, and the negative direction mattered: an earlier version of it was vacuous, comparing a set of C++ symbols that inline functions never emit. The design document is now marked as the record of derivation, including withdrawn proposals and their reasons, while the specification records only conclusions. Where they disagree the specification governs. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 20, 2026
Bare-metal support landed across #455-#459 but had no user-facing page. What existed was docs/05 §2.7.2 — the manifest reference for `[target.*]` — and an outlook section in docs/08 written before the work. Adds docs/13 (en + zh): the two commands that produce a booting image, what a freestanding target changes, the engine/target/board layering, worked examples (ISA width, the freestanding std subset, `mcpp test` on the target, the flashing artifact set, runner override), the two diagnostics, and how to write a board support package. ## The measurements are re-taken, not copied Every transcript was measured on 2026-08-20 with mcpp 2026.8.20.1 built from this tree, on x86_64-linux-gnu. That mattered: the recorded scenario notes say `text 8844` and this build measures `text 8572`, so a copied number would have been wrong on arrival. The chapter says so, and gives its component versions. Two claims that could have been repeated on trust were checked instead: * 103 of 110 headers — counted in both trees (`103` distinct `.inc` in the package, `110` `std/*.inc` in the llvm 22.1.8 payload); * five host targets for `xim:qemu-riscv` — read out of the descriptor (linux x64/arm64, darwin x64/arm64, win32 x64), which also carries the comment explaining why win32-arm64 is absent. The one claim not verified here is labelled as such: the 7 omitted headers are reported by the package to fail on a hosted x86_64 too. ## The defect writing it exposed The freestanding `import std` diagnostic ends in a copy-pasteable dependency line, and prepare.cppm carries a comment saying that line is a PROMISE which has to resolve today — because it once named a package that did not exist. It was broken again, in a second form. The line said `"0.1.0"` after `0.2.0` superseded it in the index, and 0.1.0 is not published: E_NOT_FOUND: package 'compat.std-freestanding@0.1.0' not found in the synced index So the version is part of the promise, not decoration. Fixed to `"0.2.0"` and verified end to end: trigger the diagnostic, paste its line, `mcpp run` prints `value 42`. The comment now records this recurrence, since the first note was not enough to prevent it. ## Adjacent documents * README target table gained `riscv64-none-elf` / `riscv32-none-elf` — both tier `verified` in triple.cppm, both executed under qemu by the `baremetal` CI job, and neither was listed. * docs/08 §7.3 stops being an outlook. Three of its predictions held; one was wrong in the way that matters — the C library is NOT inside the toolchain payload, it is a separate payload named by the target's own row, and that is what keeps a bare-metal package from having to name a libc. * docs/05 §2.7.2 stays as the manifest reference and now points at docs/13. `bash .github/tools/check_docs_style.sh` passes; the bilingual heading structure is identical by construction. `mcpp build` succeeds with the prepare.cppm change.
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 20, 2026
⚠️ 这次解除的耦合**在任何 manifest 里都看不见**。`riscv-virt-rt` 自 #459 起既不 声明 LLVM 也不声明 picolibc,却依然服务不了第二种工具链或第二份 C 库 —— 因为它把 `clang_rt.builtins-riscv64`(compiler-rt 的事实,GCC 下是 `libgcc`)与 `rv64gc/lp64d`(picolibc 的 multilib 约定)写进了自己的 build.mcpp。 **声明出来的依赖可见可评审;写死的名字不可见,而且只在换东西时才失败** —— 恰好是 没人在看的时候。 ## 判据用的是既有的那条 位置是目标的事实,选择是板级的事实。据此: * 哪个 builtins 库存在,由**编译器**决定,而且没有板子会选择不要它 (rv64 上的触发者是 picolibc printf 的 128 位移位,ISA 无对应指令); * 某个 ISA 档位的库放在哪个子目录,是 **C 库的约定**,零板级输入。 两者引擎本来就知道 ⇒ `mcpp::target_builtins_lib()` / `mcpp::target_libc_profile()`。 第三个 `mcpp::target_libc()` **不消除耦合,而是让它显形**:crt0 的对象名在 picolibc 与 newlib 之间确实不同,而那确实是板级选择。显式分支可读可扩展, 藏在字面量里的假设两者都不是。 ## 单一读取点(又一次:它原本要变成两处) `bpEnv` 在两处构造(根工程 / 每个依赖)。四个值各推导两遍,会得到「作为根工程拿到 对的、作为依赖拿到旧的」的板级包 —— 而那只在**消费方**的构建里失败,是更难查的方向。 ⇒ 收敛为 `fill_target_build_env()`,两处同一次调用。 ##⚠️ 一处契约不一致,由实测暴露 零 libc 档上 `profile` 仍返回 `rv64gc/lp64d`,而它按名字是 **C 库的**子目录。 没有 C 库时它不是任何东西的约定,发出去等于把一条不存在的路径交给内核,访问器的 名字就成了假话。⇒ 已收紧:**三个 libc 面的答案一起为空**,而 builtins 是编译器 事实,保留。 ## 验证 新增 e2e/134,七步全部**两侧钉**:目标行的 libc 可用 ↔ `sysroot = ""` 后 `<stdio.h>` 确实找不到;零 libc 仍产出镜像;三个查询在 rv64/rv32/零 libc 三种 配置下的值;裸名被解析期拒绝。 ⭐ **做了 revert-A 探针**:把 `effective_sysroot` 的覆盖分支注释掉重建后,e2e 精确 地死在第 4 步那条承重断言(`sysroot = "" did not remove the C library`)。不做这一步 不算写完 —— 这两个特性都属于「在已配置好的机器上,在与不在长得一模一样」那一类。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The freestanding std subset was described in the phase-3 wrap-up as "a separate
piece of work". That was wrong, and the research it contradicted had already
measured it. Reproduced here on the shipped payloads: with a synthesised
__config_site, 103 of libc++'s 110 headers compile for riscv64-none-elf, andall 7 that fail also fail on an x86_64 host with full libc++ and glibc — they
are headers libc++ has not implemented. The freestanding compile-time loss is
zero.
-nostdinc++is the MECHANISM of that route, not an obstacle: it keepslibc++'s headers private to the subset package, which exports a module — the
same shape the board package already uses for the target's C library.
What actually blocked it was exceptions, and that is a whole-graph property:
std::optional::value()alone pulls in__cxa_throw, two vtables andstd::bad_optional_access::~bad_optional_access, none of which can existwithout an unwinder or libc++abi.
[build] cxxflagscannot fix it: they do not reach adependency's module compile, so the BMI and the importer disagree and clang
reports a
.pcm"configuration mismatch" — naming a module file ratherthan the setting that split the graph.
is_dialect_flagdeliberately does NOT propagate-fno-exceptions, on thegrounds that "dependencies may assume exceptions are available". Right for
a hosted target, exactly backwards here.
So
-fno-exceptions -fno-rttijoin-ffreestandingand-nostdinc++on thetarget spec, which is the one place that reaches every unit in the graph.
upgrade. The dependency cache key carries the target TRIPLE but not the flags
the triple implies — and which flags those are is mcpp's decision, so it moves
between versions while the triple string does not. A project that had built
once got its old BMI back and a hard failure naming a .pcm. Fixed by adding a
targetImpliedFlagsaxis, empty for hosted targets so no existing key moves.e2e/133 pins the whole chain in the emulator, from both sides: the subset
builds, runs
ranges::sortwith a projection plus optional/atomic/span/string_view on the target, and
std::mutexfails to COMPILE — capabilitiesturned off in
__config_sitevanish rather than leaving a run-time stub.The
import stddiagnostic namesstd-freestandingagain, now that thepackage is published.
Ships as 2026.8.19.4, alongside
mcpplibs/std-freestanding0.1.0.