You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(freestanding): Cortex-M as seven target rows, and dead-section elimination (2026.9.4.1)
The bare-metal target table grows from four rows to eleven. M-profile is seven
rows rather than one: an object built for `thumbv7em` uses instructions a
Cortex-M0 does not have, so the two spellings produce incompatible objects
rather than expressing a preference. The table exists so that `--target
<triple>` alone suffices to produce a correct object file, and a single
`arm-none-eabi` row plus an `-mcpu` each project remembered would move a
correctness decision out of the table and into every manifest.
⚠️ The float ABI does not settle whether the FPU is used. clang derives
`-mfloat-abi` from the `eabi`/`eabihf` suffix without help, but that governs how
floating-point values cross a function boundary, not what the compiler may emit
inside one — and `thumbv7em` implies FPv4-SP. Measured on llvm 22.1.8: under the
soft-float ABI clang still emits `vmul.f32` for a float multiply, which faults
at run time on a Cortex-M4 with no FPU after a clean compile and a clean link.
Every soft-float row therefore carries `-mfpu=none`, including the rows whose
architecture has no FPU at all — a row states the property it guarantees rather
than inheriting it from a default that is free to change. The uniform form was
not the first draft: the flag was initially applied only where a non-zero
instruction count had been observed, which made the table record a measurement
rather than a guarantee, and the unit test quantified over the rows caught it.
Freestanding builds now compile with `-ffunction-sections -fdata-sections` and
link with `--gc-sections`, on both the driver path and the direct `ld.lld` path.
Both halves belong to the engine because a dependency's translation units must
carry them and a project cannot reach those. The flags became necessary rather
than economical when a C library began arriving from the dependency graph: a
dependency's object files enter a link unconditionally, unlike an archive member
pulled only while its symbol is undefined, and a Cortex-M part has kilobytes
where the existing bare-metal rows had megabytes.
⚠️ A linker script becomes load-bearing in a new way. An interrupt vector table
is referenced by nothing — the hardware reads it by address — so `--gc-sections`
collects it, and a board's script must say `KEEP(*(.vectors))`.
The tier column records what was run. Four rows booted under
`xim:qemu-arm@9.2.4-1` and printed over semihosting: thumbv6m on `microbit`,
thumbv7m on `mps2-an385`, thumbv7em-eabihf on `mps2-an386`, thumbv8m.main-eabi
on `mps2-an505`. The remaining three build and link and are marked `preview`.
`lldEmulation` is empty on every M-profile row: clang has a BareMetal toolchain
for arm, so these triples reach `ld.lld` through the driver as the RISC-V and
aarch64 rows do, and the x86_64 row's workaround does not recur. `mcmodel` is
empty because 32-bit ARM has no such axis.
Coverage:
* `tests/e2e/332_cortex_m_builds_and_boots.sh` boots the four verified rows,
asserts a function nothing calls is absent while the vector table survives,
and measures the float ABI from both sides — the hard row's FPU instruction
count and the soft row's link failure naming `__aeabi_fmul`, which states
that the multiply did not become an FPU instruction. Both guards were
confirmed to fail independently against a reverted engine.
* Three unit tests state the rules the e2e cannot: every soft row disables the
FPU and no hard row does (with both halves counted, so neither is vacuous),
M-profile rows need no code model and no direct lld driving, and every
freestanding row compiles with per-function sections.
* The script declares `# requires: qemu-arm`, which no sharded runner has, so
it is invoked directly from the `baremetal` job where its PASS line and a
count of four booted rows can both be demanded.
Also backfills two claims in docs/13 that 2026.8.28.2 had already overturned:
exceptions, RTTI and `import std` are available on a freestanding target when a
package provides `hosted-standard-library`. The document had continued to state
them as unconditional limitations, which would lead a reader to abandon a
capability that already works.
* docs: sync the target tables in README and docs/16 with the seven new rows
Both mirror kKnownTargets and would otherwise describe a four-row bare-metal
table that no longer exists. The README's legend for a verified row is that CI
builds AND executes the artifact, which the baremetal job now does for the four
rows marked so.
* test(e2e): register qemu-arm in KNOWN_CAPS
The harness validates every `# requires:` token against a master list before
running anything, so a capability that is only added to the Linux detection
branch aborts every suite on every platform. CI caught it: the msvc job failed
with "declares unknown capability 'qemu-arm'" while the bare-metal job — the
one that has the emulator — passed.
That is the guard working. A token absent from this list would otherwise match
nothing and the test would skip everywhere while looking supported.
* docs(plan): 商业级裸机/嵌入式总体方案,并回填 P0 的实施状态
方案文档此前只存在于工作副本里,没有进入 PR —— 而它是本次改动的依据,读者要靠
它才能知道七个目标行与 --gc-sections 为什么是同一批。
§10 记下实施推翻方案的两处:extra 列承载的是 -mfpu=none 而不是方案写的
-mfloat-abi(triple 已经承载了浮点 ABI),以及行数是七不是六。
* revert(freestanding): drop dead-section elimination from this change
⚠️ MEASURED TO BREAK A JOB THAT IS GREEN ON main, BY A MECHANISM THAT
CONTRADICTS THE CODE, WHICH IS REASON ENOUGH NOT TO SHIP IT.
`--gc-sections` was correct on this machine and in the bare-metal CI job. It
broke the openkal cross-build on a WINDOWS host:
lld: error: unable to find library -lntdll (and four more)
Three-way control, not reasoning:
main, via workflow_dispatch build 3 targets on windows ✅
this PR, with gc-sections (three runs) ❌
bisect/no-gc-sections, that change alone reverted ✅
And the result contradicts the model. Both call sites sit inside
`isFreestandingTarget`, which `x86_64-windows-gnu` does not satisfy; the same
cross-build succeeds on a Linux host; and the `-Wl,--gc-sections` visible in the
failing link line was traced to openkal-windows' own manifest rather than to the
engine. The actual symptom is a MISSING `-L` for the directory holding the
import libraries that package generates.
So the mechanism is not understood, and an unexplained change does not belong in
a release. The flags are removed here and the finding is recorded in
`.agents/docs/2026-09-04-…-plan.md` §10.1 with the control that establishes it,
because dead-section elimination remains a prerequisite for a C library supplied
from the dependency graph.
The Cortex-M rows do not depend on it: they build, link and boot without it, and
the e2e still asserts the vector table is present and the float ABI is honoured
on both sides. Only the two dead-code assertions are withdrawn.
* test(matrix): the support matrix declares the seven M-profile rows
⭐ AND THE SCAN'S OWN PREDICATE WAS A SPELLING RATHER THAN A PROPERTY, IN TWO
PLACES.
`scan.sh` decided "is this a bare-metal target" with `grep -- '-none-elf$'`.
That is the environment field of four particular rows, not the question being
asked. Cortex-M spells the same OS `-none-eabi` and `-none-eabihf`, so both
copies of the rule fell through:
* the probe source picked the hosted branch and emitted `#include <cstdio>`
for a target with no C library, producing a column of `build-failed` that
described the probe rather than the target — precisely the defect the
comment three lines above it already records;
* the graph-mode skip did not skip, so fourteen cells were measured for a
combination the same file declares out of scope.
Both now ask the OS field. This is the shape recorded as "one rule, two copies:
change one and you will miss the other", and it is why the rows are added by
MEASURING with the fixed scan rather than by writing them out.
The 42 payload rows are the live scan's output, verified by `compare.sh` for
both modes. Graph mode contributes none, which is the declaration the file
already made for every other bare-metal row.
* Revert "revert(freestanding): drop dead-section elimination"
The removal rested on a false signal. Reverting gc-sections made one CI run
green, and that was taken as cause; the full control matrix says otherwise:
main ×2 ✅
main + the seven rows ✅
main + the version bump ✅
this PR minus the version bump ✅
this PR, with AND without gc-sections, ×5 ❌
a branch holding BYTE-IDENTICAL code to the PR ✅ ← the decisive one
The last row rules out every code-level explanation. The cause is GitHub
Actions cache scoping: a branch reads its own cache before falling back to the
default branch's, and this branch's first run — aborted by the unregistered
`qemu-arm` capability — saved a half-populated sandbox that every later run on
it then restored. Fresh bisect branches had no cache of their own and fell back
to main's, which is why each of them passed and none of them exonerated
anything.
⭐ Recorded in the plan document: when every single factor passes in isolation
and the combination still fails, the next question is whether the criterion is
being decided by the environment — not which factor to split next.
* docs(triple): drop a sentence the table had outgrown
Self-review: the paragraph above the M-profile rows still said "two rows are
verified", written when two were. Four boot under QEMU and the block directly
below already names all four, so the sentence contradicted the table two lines
under it — the kind of stale count a reader trusts because it reads like a
summary.
---------
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
|`thumbv8m.main-none-eabihf`| preview | none by default — Cortex-M33F/M55F, hard float |
24
31
25
32
`verified` means an image has been built **and run** for the row. `preview`
26
-
means it builds and has been observed to run, but is not yet covered by the
27
-
engine's own emulator jobs.
33
+
means it builds and links, and no emulator run has been recorded.
34
+
35
+
### M-profile is seven rows rather than one
36
+
37
+
Every other bare-metal family above is one row per architecture. Cortex-M is
38
+
not. An object built for `thumbv7em` uses instructions a Cortex-M0 does not
39
+
have, and the two spellings produce incompatible objects rather than expressing
40
+
a preference. The table exists so that `--target <triple>` alone suffices to
41
+
produce a correct object file; a single `arm-none-eabi` row plus an `-mcpu` that
42
+
each project remembered would move a correctness decision out of the table and
43
+
into every manifest.
44
+
45
+
The `eabi`/`eabihf` suffix is the float ABI, and clang derives it from the
46
+
triple without help: measured on llvm 22.1.8, `thumbv7em-none-eabi` yields
47
+
`-mfloat-abi soft` and `thumbv7em-none-eabihf` yields `hard`.
48
+
49
+
⚠️ **The float ABI does not settle whether the FPU is used.** It governs how
50
+
floating-point values cross a function boundary, not what the compiler may emit
51
+
inside one, and the `thumbv7em` architecture implies FPv4-SP. Measured: under
52
+
the soft-float ABI clang still emits `vmul.f32` for a float multiply. On a
53
+
Cortex-M4 without an FPU that instruction faults at run time, after a clean
54
+
compile and a clean link. Every soft-float row therefore carries `-mfpu=none`,
55
+
including the rows describing architectures that have no FPU at all — a row
56
+
states the property it guarantees rather than inheriting it from a default.
57
+
58
+
Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain
59
+
for arm, so these triples reach `ld.lld` through the driver as the RISC-V and
60
+
aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too.
61
+
62
+
### Dead-section elimination
63
+
64
+
Freestanding builds compile with `-ffunction-sections -fdata-sections` and link
65
+
with `--gc-sections`. Both halves belong to the engine rather than to a project
66
+
because a dependency's translation units must carry them, and a project cannot
67
+
reach those.
68
+
69
+
The flags became necessary rather than merely economical when a C library began
70
+
arriving from the dependency graph. A dependency's object files enter the link
71
+
unconditionally, unlike an archive member, which is pulled only while its symbol
72
+
is undefined. That costs nothing when the C library is a prebuilt archive and
73
+
the target has megabytes; a Cortex-M part has kilobytes, and without dead-section
74
+
elimination every image would carry the whole of the C library.
75
+
76
+
⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector
77
+
table is referenced by nothing — the hardware reads it by address — so
78
+
`--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`.
79
+
Measured: with the `KEEP` present, a function nothing calls is dropped, the
80
+
table survives, and the image boots.
28
81
29
82
⚠️ The last two rows default to no C library, and that is a statement rather
30
83
than an omission: the first consumer of both rows — the `openarch` layer of
@@ -190,8 +243,8 @@ by pointing `main` at the source file that carries `_start`.
190
243
| Linker selection | `ld.lld` is addressed by **absolute path**, derived from the driver's own directory. `-fuse-ld=lld` resolves by name and finds GNU ld on any machine with binutils earlier on `PATH`, which then fails with `unrecognised emulation mode: elf64lriscv`. |
191
244
| ISA flags | `-march`, `-mabi` and `-mcmodel` come from one row per target in `src/freestanding/target.cppm`, so `--target <triple>` alone is sufficient to produce a correct object file. |
192
245
| C library | The **target's**, resolved by mcpp from the target's own table row exactly as the compiler is. A bare-metal project declares no libc, just as a hosted project declares no glibc. The engine places the sysroot's library directory on the link search path, so a board-support package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). |
193
-
| Exceptions and RTTI | Off on every translation unit in the graph, including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise reference `__cxa_throw` and three further undefined symbols. The setting belongs to the target rather than to a project's `cxxflags` because a BMI records it, and a dependency compiled with exceptions cannot be imported by a unit without them. |
194
-
| `import std` | Unavailable, and rejected at configure time with a diagnostic rather than at link time. |
246
+
| Exceptions and RTTI | Off on every translation unit in the graph, including a dependency's, **unless a package supplies a C++ runtime built for this target**. There is otherwise no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would reference `__cxa_throw` and three further undefined symbols. The setting belongs to the target rather than to a project's `cxxflags` because a BMI records it, and a dependency compiled with exceptions cannot be imported by a unit without them. A package declaring `provides = ["hosted-standard-library"]` reverses the default: exceptions and RTTI are enabled, `-ffreestanding` is dropped, and `-fasynchronous-unwind-tables` is added. |
247
+
| `import std` | Available when a package in the graph provides `hosted-standard-library` and names its own `std` module source; otherwise rejected at configure time with a diagnostic rather than at link time. |
195
248
| Entry point | `int main()` is available whenever something supplies a `crt0`. A board-support package normally does. |
196
249
| Default linkage | Static, and not as a preference: there is no loader, so there is no other option. |
197
250
@@ -716,7 +769,7 @@ targets, but that expectation is **not** covered by a test.
716
769
| Limitation | Observed behaviour |
717
770
|---|---|
718
771
|`std::format`, `std::sort` over builtin scalar types, and a complete `std::string`| Fail at **link** time naming the undefined symbol. libc++ places these entities in the compiled library — the scalar `__sort` instantiations are `extern template`, with no macro that disables them — so a target-built `libc++.a` is required. No such payload is published. |
719
-
| Exceptions and RTTI | Disabled across the whole graph. `try`/`catch` is unavailable at compile time. A board shipping a target-built`libc++abi` and unwinder has a genuine case for re-enabling them; that is the point at which this becomes a manifest key. |
772
+
| Exceptions and RTTI | Disabled across the whole graph**unless a package provides `hosted-standard-library`**, which `mcpplibs/openkal-llvm-runtime` does by carrying `libc++`,`libc++abi` and `libunwind` configured for the target. Without such a package `try`/`catch` remains unavailable at compile time. |
720
773
| Board coverage | One board family. `riscv32-none-elf` demonstrates that the ISA table is data, not that a second machine has been ported. ARM Cortex-M has not been attempted. |
721
774
| C library substitution | Expressible since 2026.8.20.2 through `[target.<triple>].sysroot`, and **verified only for the empty value** (the zero-libc tier). Pointing it at a different C library is accepted and installed through the same channel, but no second bare-metal C library is published, so that path is untested. |
722
775
|`qemu-riscv` on `win32-arm64`| The upstream package publishes no asset for that host, so installation fails on it. The failure is correct rather than silent, but the host cannot run a bare-metal image. |
0 commit comments