Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# (#1255). This workflow used to run on push-to-main only, so the real wasm32
# build (emcc, `web/build.sh`) was first attempted AFTER a merge — and it sat
# red on main for five commits (517cf08..0ac8a9b) while the local stand-in
# (tools/ilp32_syntax_check.sh, suite [99i3], clang -m32) was green: i386
# (a clang -m32 approximation, deleted in #1274) was green: i386
# aligns `double` to 4 inside a struct and wasm32 aligns it to 8, so a layout
# `_Static_assert` can hold under the stand-in and fail the real target.
#
Expand Down
259 changes: 7 additions & 252 deletions CHANGELOG.md

Large diffs are not rendered by default.

35 changes: 12 additions & 23 deletions docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ the only place that ran was `.github/workflows/pages.yml` on **push to
(517cf08 back to 0ac8a9b) before anyone looked, because nothing on a pull
request compiled for the real target.

The local gate did not catch it either, and could not. Suite `[99i3]`
(`tools/ilp32_syntax_check.sh`) compiles every translation unit the recipe
hands the compiler with host `clang -m32` — the **i386** ABI. i386 aligns
`double` to 4 inside a struct; wasm32 aligns it to 8. The Value union was 36
bytes under `-m32` and 40 under emcc, so a layout `_Static_assert` held under
the gate and failed the real build. `[99i3]` was labelled "the playground's
wasm32 build cannot break unnoticed"; that was false as stated.
The local gate did not catch it either, and could not. The suite's ILP32
gate compiled every translation unit the recipe hands the compiler with host
`clang -m32` — the **i386** ABI. i386 aligns `double` to 4 inside a struct;
wasm32 aligns it to 8. The Value union was 36 bytes under `-m32` and 40 under
emcc, so a layout `_Static_assert` held under the gate and failed the real
build. That approximation was deleted in #1274: the real build below runs on
every change it could have guarded, and an approximation of a required target
is a second, weaker answer to the same question.

What is true now:

Expand Down Expand Up @@ -105,22 +106,10 @@ What is true now:
concurrency group, so a PR push can never cancel a `main` deploy. Measured
on `main`, the build job is about 72 s with the emsdk cache warm, and every
PR pays it.
- **`[99i3]` says what it is.** With emcc on `PATH` the gate REPLAYS each
recorded emcc call — the recipe's own argv, plus `-fsyntax-only`, minus the
`-o` operand and the call's other translation units, and nothing else — and
prints `verdict: AUTHORITATIVE`. (Round 1 compiled with an injected `-Isrc`
and `-DEIGENSCRIPT_VERSION`, so a recipe emcc rejects could print
AUTHORITATIVE; self-test plants `10i`/`10d` are that class, under both arms.)
Without it (every CI suite leg, and the dev box) it runs the `-m32` arm and
prints `verdict: APPROXIMATION`, and its OK line names pages.yml as the
authority. Its self-test pins the limit from both sides: control `1w`
requires the `-m32` arm to pass a layout assert that is 12 bytes at i386
and 16 at wasm32, and `1wt` requires clang's wasm32 frontend to refuse it.

**The check to require on `main` is `playground (real emcc wasm32 build)`**
— the aggregator, never the worker alone (a skipped worker reads as
passing). Until it is in the ruleset, a red wasm build is visible on the PR
but does not block the merge.

**The required check is `playground (real emcc wasm32 build)`** — the
aggregator, never the worker alone (a skipped worker reads as passing). It is
listed in `.github/required-checks.txt`, so a red wasm build blocks the merge.

## The doc gates — where they run, and why they are cheap

Expand Down
106 changes: 12 additions & 94 deletions docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,100 +54,18 @@ The compile-time pin of "caching the length cost zero bytes" was written as
`sizeof(data) == sizeof(data.fn)`. That equality is a 64-bit accident — at
32-bit pointers `data` is 36 bytes and `fn` is 28 — and it broke the Docs-site
wasm32 build (pages.yml) from #1185. The claim that holds on every pointer
width is `sizeof(data.strv) <= sizeof(data.fn)`. Suite `[99i3]` compiles every
translation unit `web/build.sh` hands to emcc — the runtime `src/*.c` units and
`web/eigs_wasm.c`, the playground entry point — at `-m32`. That catches the
pointer-width class before a push; it is an APPROXIMATION, not the target, and
says so on its own output: i386 aligns `double` to 4 inside a struct and wasm32
to 8, so the union measured 36 bytes there and 40 under emcc, and a layout
assert can pass `[99i3]` and fail the real build (#1255 — the lane stayed red on
`main` for five commits with `[99i3]` green). The authoritative check is
`.github/workflows/pages.yml`, whose build job now runs the real `web/build.sh`
under emcc on every pull request; with emcc on `PATH`, `[99i3]` itself compiles
with it instead. The gate prints the population it examined rather than
asserting a typed-in number, and floors it so a shrinking SOURCES array is a
deliberate re-pin.

That population is not a reading of the script. The gate runs the real
`web/build.sh` in a scratch sandbox with a stand-in `emcc` first on `PATH`
that records, **for every invocation**, its argv, its cwd, the files that call
created and any unit handed to it on stdin; bash has already resolved quoting,
`$(...)`, variables and array shape by then. Every call is kept and the
population is their union, printed as `N call(s) recorded`: a stand-in that
recorded only the last call made compile-then-link — the canonical build
shape — invisible, and a unit compiled by a first `emcc -c` call was outside
the population while the gate printed `OK`. The recorded argv is classified by
the **filesystem**, not by a model of emcc's option grammar: `@response-files`
are expanded first (as emcc expands them), and an input is any token naming an
existing regular file that call's compiler did not itself write whose suffix
is a C-family translation unit. That rule is position-independent, so a unit
after a flag that takes no operand — `--emrun`, `--proxy-to-worker` — is
counted, where a typed operand table dropped it. The two shapes a suffix
cannot see, a unit on stdin (`-x c -`) and a unit with a non-TU suffix
(`-x c web/unit.inc`), are decided by asking the real clang driver for its own
`-x c` inputs; the gate examines the **union** of the two derivations and goes
red by name when they disagree. That driver is fed only operands it can open:
emcc's spaced setting form `-s TOTAL_MEMORY=64MB` is an input clang cannot
open, so the driver's own "no such file or directory" diagnostic names it, the
gate drops it and prints it on `classifier: dropped=` — while a refused
operand whose suffix is a `.c` stays red by name, because that is a recipe
naming a unit that does not exist.

Recording a call's argv is not enough on its own: the call's own FLAGS shape
what the real compiler does too, and round 6/7 discarded them after
classification (#1232). The live recipe compiles at `-O2`, which defines
`__OPTIMIZE__` on the real target — a gate that derived the macro world and
compiled every TU without it took a DIFFERENT arm than the lane it stands in
for, silently. Each recorded call's accepted option tokens — optimisation
level, every `-D`/`-U`, `-std=`, `-f*`, `-W*`, filtered through the same
accepted-option set the driver cross-check already measures — are tracked per
call and printed as `classifier: flags=N per call`; they reach both the
macro-world derivation (so `__OPTIMIZE__` agrees with the real build) and the
compile of the TU that call recorded, so a TU compiled once under one call's
own `-D` and again — unqualified — inside a later call's SOURCES is examined
under the call that actually shaped it. Both `emcc` and `em++` are shimmed,
not only `emcc`; empty stdin (`-x c -` with nothing piped) is a valid empty
TU, not a rejected recipe. Two residuals are stated: the recorded input is
read after the WHOLE recipe finishes rather than snapshotted at its own call
(a `#error` compiled early and overwritten with valid C before a later call
would be examined on the later bytes), and an INVALID option the driver
rejects is indistinguishable from a legitimate emcc-only one and is silently
dropped rather than failed — the recipe's real build is the oracle for the
recipe's own validity, not this gate.

It compiles with the wasm32-emscripten target's macro world **derived**, never
typed: both worlds' predefines are read with `-E -dM`
(`clang --target=wasm32-unknown-emscripten` and `clang -m32`), every difference
in NAME is reconciled with a `-U` or a `-D`, every difference in VALUE —
however many the two toolchains disagree on, printed by the gate itself as
`macro_parity: ... values=N/M` (32 on this dev box, 33 on the `linux / gcc`
runner, measured 2026-09-21; the toolchain decides the number, not this page)
and including `__SIZEOF_LONG_DOUBLE__` at 16 on the target and 12 on the
host — gets the target's own value, and which of those glibc's `-m32` headers
refuse is measured rather than assumed. Three hand-typed predefines were not
enough: under them the gate took the `#elif defined(__linux__)` arm at
`src/fsutil.c:69` on a lane that has no `__linux__` at all, i.e. it stood in
for the lane while compiling the other branch. Availability is probed by
EXECUTION and the probe asks for the capability the gate uses — it compiles a
32-bit TU **that includes the C library**, because a probe with no includes
passes on an arm64 mac that has no 32-bit SDK and lets the gate go red on
every TU instead of skipping. **Exactly one outcome may skip**, and the suite
counts it: a C library with no 32-bit target for its own headers, which says
so in its own words (`#error Unsupported architecture` on the macOS SDK) — the
gate matches that diagnostic, at either of the two stages that can hit it, and
prints it. macos-latest is the measured case, and it passes the availability
probe: the SDK refuses only once the reconciliation has replaced `__i386__`
and `__APPLE__` with the target's world, which is the reconciliation doing its
job. Everything else fails by name, because everything else is the gate's own
apparatus: no compiler on `PATH`, the gate's own `<gnu/stubs-32.h>` stub
missing, a system include directory that does not exist, and a derived macro
world this C library refuses for a reason that is *not* the SDK's — a broken
derivation, which round 5 reported as a skip. The control for that verdict —
the same headers *without* the reconciliation — runs in the live path before
the verdict is taken either way. The suite's RESULTS line prints
`passed, failed, skipped` on every lane, `skipped=0` included, so a lane that
examined 23 translation units last week and 0 this week is visible in the
verdict and not only in the log.
width is `sizeof(data.strv) <= sizeof(data.fn)`. The check that
holds it is the real target: `.github/workflows/pages.yml` runs `web/build.sh`
under emcc on every pull request as a required check. A host `clang -m32`
approximation used to stand in for it and was deleted (#1274): i386 aligns
`double` to 4 inside a struct and wasm32 to 8, so the union measured 36 bytes
there and 40 under emcc, and a layout assert passed the approximation while
the real build stayed red on `main` for five commits (#1255).

The suite's RESULTS line prints
`passed, failed, skipped` on every lane, `skipped=0` included, so a section that
measured something last week and nothing this week is visible in the verdict
and not only in the log.

`skipped` counts SECTION-LEVEL skips: sections whose verdict is the skip,
which contributed no PASS and no FAIL on that lane. Round 6 shipped the counter
Expand Down
Loading
Loading