Repository navigation
v0.1.606: f_pow, exp and log on RISC-V answer libm's bits; an extern'… #917
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
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| # EVERY GATE STEP RUNS, EVEN AFTER ONE FAILS (`if: ${{ !cancelled() }}`). | |
| # | |
| # Without it a red step ends the job and every step after it is reported as | |
| # "skipped", which reads like "nothing else was wrong" and means "nothing else | |
| # was asked". That is not hypothetical: `component_parity` invoked a | |
| # bash-shebang script with `sh`, which is bash on macOS and dash on Ubuntu, so | |
| # it was green here and red in CI -- and the TWENTY-ONE gates after it in this | |
| # file (proto, graphql, http2, grpc, downstream_check, qemu_virt, ...) had not | |
| # run for any commit since. A red CI is one known failure plus everything | |
| # downstream unknown; this makes it one known failure plus everything else | |
| # actually measured. | |
| # | |
| # Setup steps are deliberately NOT marked: if the toolchain fails to install, | |
| # the gates below it fail loudly rather than silently not existing. | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| ocaml-version: ["5.1", "5.4"] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up OCaml | |
| uses: ocaml/setup-ocaml@v3 | |
| with: | |
| ocaml-compiler: ${{ matrix.ocaml-version }} | |
| - name: Install dependencies | |
| run: opam install . --deps-only --with-test | |
| - name: Build | |
| run: opam exec -- dune build | |
| - name: Run tests | |
| run: opam exec -- dune runtest | |
| # The gates that are not `dune runtest`: cross-backend parity, the differential | |
| # checks against other people's implementations (node's URL and TextDecoder, | |
| # QEMU), self-hosting, and the language server. | |
| # | |
| # These live in scripts/ and were run by hand until now, which is the same | |
| # exposure as a measurement that only exists in /tmp — a gate nobody runs is a | |
| # gate that is not protecting anything. str_replace was returning a buffer with | |
| # no length header on the C backend for as long as it had existed, and what | |
| # found it was scripts/parity.sh: exactly one of these. | |
| # | |
| # One step per harness, so a red build names the gate rather than a log line. | |
| # A gate's exit status is its class (v0.1.554): 0 passed, 1 failed, 2 could | |
| # not answer (a tool it needs is missing -- tool_preflight says the runner has | |
| # them all, so here that is red), 3 optional and not run. The optional gates | |
| # run through scripts/gate.sh, which turns 3 into 0 and says so, so a green | |
| # job means each step passed or was optional and said it did not run. | |
| # | |
| # Linux only, and that is deliberate rather than a shortcut: development | |
| # happens on macOS, where these are run before every commit, so the two | |
| # platforms end up covered between CI and the working copy. | |
| gates: | |
| name: Gates (cross-backend and differential) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up OCaml | |
| uses: ocaml/setup-ocaml@v3 | |
| with: | |
| ocaml-compiler: "5.4" | |
| # The oracle for url_parity and encoding_parity is node's URL and | |
| # TextDecoder, so node is a pinned dependency and not whatever the image | |
| # happens to ship. The runner's default was v22, which predates two | |
| # changes to the URL Standard — `^` in the path percent-encode set, and | |
| # `..` resolved against an empty path — and reported our spec-correct | |
| # answers as failures. An unpinned oracle makes the result depend on the | |
| # machine, which is the thing a differential gate exists to avoid. | |
| - name: Set up Node (the differential oracle) | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| # clang, because the LLVM backend's output is LLVM IR and gcc cannot | |
| # compile it — parity.sh would report MISCOMPILE for every case rather | |
| # than skipping. wabt for the wasm backend. lldb for debug_info, which | |
| # only asks where a breakpoint resolves and never runs the process, so | |
| # none of this touches ptrace. | |
| - name: Install harness tools | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang wabt lldb | |
| # SDL2 for the window capability. window_check.sh skips without it, and | |
| # a skipped gate is a gate that is not running — so install it here. | |
| sudo apt-get install -y libsdl2-dev | |
| # OpenSSL headers for downstream_cc_check (mere-blog and mbrowse include them) | |
| sudo apt-get install -y libssl-dev | |
| # socket_parity builds a Wasm component and compares its socket behaviour | |
| # against the C build, so it needs the component toolchain: wasm-tools, | |
| # wasmtime, and the WASI command adapter that ships inside jco. Pinned to | |
| # the versions development runs against, for the reason the node pin | |
| # exists — a tool that decides a gate's answer is a versioned dependency. | |
| - name: Install the Wasm component toolchain | |
| run: | | |
| set -e | |
| WASM_TOOLS=1.255.0 | |
| WASMTIME=46.0.1 | |
| # binaryen, for the wasm-opt -Oz that build_full.sh runs on every | |
| # playground module. NOT apt: ubuntu 24.04 ships version 108, which | |
| # refuses these modules outright -- without --enable-tail-call on the | |
| # return_call sites, and with it on "Exported global cannot be | |
| # mutable". Pinned for the stronger reason too: scripts/wasm_size_ | |
| # budget.txt records byte counts, so the optimizer version is part of | |
| # the measurement. 132 emits the recorded bytes exactly, checked on | |
| # this image against the same modules built on macOS. | |
| BINARYEN=132 | |
| mkdir -p "$HOME/.local/bin" | |
| curl -sSfL "https://github.com/bytecodealliance/wasm-tools/releases/download/v${WASM_TOOLS}/wasm-tools-${WASM_TOOLS}-x86_64-linux.tar.gz" \ | |
| | tar -xz -C /tmp | |
| install "/tmp/wasm-tools-${WASM_TOOLS}-x86_64-linux/wasm-tools" "$HOME/.local/bin/" | |
| curl -sSfL "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME}/wasmtime-v${WASMTIME}-x86_64-linux.tar.xz" \ | |
| | tar -xJ -C /tmp | |
| install "/tmp/wasmtime-v${WASMTIME}-x86_64-linux/wasmtime" "$HOME/.local/bin/" | |
| curl -sSfL "https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN}/binaryen-version_${BINARYEN}-x86_64-linux.tar.gz" \ | |
| | tar -xz -C /tmp | |
| install "/tmp/binaryen-version_${BINARYEN}/bin/wasm-opt" "$HOME/.local/bin/" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| npm install -g @bytecodealliance/jco@1.27.0 | |
| # graphql-js is the oracle for graphql_parity.sh. Pinned for the same | |
| # reason as everything else here: the gate's answer comes from it. | |
| npm install -g graphql@17.0.2 | |
| # protoc is the oracle for proto_parity.sh, and it is pinned for the same | |
| # reason node is: a gate whose answer comes from a tool inherits that | |
| # tool's version. The wire format itself is frozen — protobuf's | |
| # compatibility guarantee is that these bytes never change — so unlike | |
| # node's URL, a version bump here is not expected to move any answer. The | |
| # pin is so that if one ever does, the diff names a version change instead | |
| # of looking like a regression in contrib/proto. | |
| - name: Install protoc (oracle for proto_parity) | |
| run: | | |
| set -e | |
| PROTOC=27.3 | |
| mkdir -p "$HOME/.local/bin" | |
| curl -sSfL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC}/protoc-${PROTOC}-linux-x86_64.zip" \ | |
| -o /tmp/protoc.zip | |
| unzip -q -o /tmp/protoc.zip -d /tmp/protoc | |
| install /tmp/protoc/bin/protoc "$HOME/.local/bin/" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| # Best effort: qemu_virt.sh is the differential check against an emulator | |
| # nobody here wrote, and it skips cleanly when the binary is absent. It is | |
| # installed separately and allowed to fail so that a package rename | |
| # degrades this gate to a skip instead of turning the build red. | |
| - name: Install qemu-system-riscv32 (optional gate) | |
| continue-on-error: true | |
| run: sudo apt-get install -y qemu-system-misc | |
| # Best effort, same argument: rvd_oracle_check.sh holds `mere -rvd` to a | |
| # disassembler nobody here wrote, and skips cleanly when it is absent. | |
| # The gate exists because the disassembler had none, and fell so far | |
| # behind the backend it explains that 41% of a 64-bit listing was | |
| # question marks. | |
| - name: Install binutils-riscv64 (optional gate — oracle for mere -rvd) | |
| continue-on-error: true | |
| run: sudo apt-get install -y binutils-riscv64-linux-gnu | |
| # grpcurl is one of the two clients grpc_parity drives. Pinned, like every | |
| # other oracle here — and it is an ORACLE in the useful sense: it was | |
| # written by people who never saw this implementation. | |
| - name: Install grpcurl (client for grpc_parity) | |
| run: | | |
| set -e | |
| GRPCURL=1.8.8 | |
| mkdir -p "$HOME/.local/bin" | |
| curl -sSfL "https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL}/grpcurl_${GRPCURL}_linux_x86_64.tar.gz" \ | |
| | tar -xz -C /tmp grpcurl | |
| install /tmp/grpcurl "$HOME/.local/bin/" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| # hyperframe is the oracle for http2_parity.sh — an independent RFC 9113 | |
| # implementation and the frame layer of h2. Pinned for the same reason as | |
| # node, protoc and graphql-js: the gate's answer comes from it. | |
| - name: Install hyperframe / hpack (oracles for http2_parity, hpack_parity) | |
| run: python3 -m pip install --break-system-packages hyperframe==6.1.0 h2==4.4.1 hpack==4.2.0 | |
| # PostgreSQL as BINARIES, not as a service, the way mere-blog's CI does it: | |
| # live_soundness_check.sh runs its own throwaway cluster on a port it picks, | |
| # so nothing is shared between runs and there is nothing to collide over. | |
| # Without them that gate SKIPS, and a gate that skips is a gate that does | |
| # not run. | |
| # v0.1.535: installed HERE, with the other tools, and not beside the gates | |
| # that use it. It used to sit between two gates, after tool_preflight had | |
| # already run -- so the preflight looked for initdb and pg_ctl before they | |
| # were on the PATH, reported them absent, and was red from the day it | |
| # became a required step (v0.1.531) while the gates that use them passed. | |
| - name: PostgreSQL (oracle for live_soundness, migration, cascade_catalog) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y postgresql postgresql-client | |
| echo "$(pg_config --bindir)" >> "$GITHUB_PATH" | |
| - name: Install dependencies | |
| run: opam install . --deps-only --with-test | |
| - name: Build | |
| run: opam exec -- dune build | |
| # Asserted rather than assumed: a missing tool would otherwise turn into | |
| # either a silent skip (a gate that passes without running) or a storm of | |
| # MISCOMPILE rows. wat2wasm's flags are checked because parity.sh treats a | |
| # rejected flag as a failure, not as an absent toolchain. | |
| - name: Toolchain preflight | |
| run: | | |
| set -e | |
| for t in clang lldb node python3 protoc wat2wasm wasm-opt wasm-objdump wasm-tools wasmtime; do | |
| command -v "$t" >/dev/null 2>&1 || { echo "required tool missing: $t"; exit 1; } | |
| done | |
| # socket_parity finds the WASI command adapter inside the global jco | |
| # install, and skips without it — so its absence is asserted here too. | |
| adapter="$(npm root -g)/@bytecodealliance/jco/lib/wasi_snapshot_preview1.command.wasm" | |
| [ -f "$adapter" ] || { echo "WASI command adapter missing: $adapter"; exit 1; } | |
| # graphql_parity skips without this, and a gate that skips in CI is a | |
| # gate that passes without running. | |
| [ -f "$(npm root -g)/graphql/package.json" ] || { echo "graphql-js missing"; exit 1; } | |
| python3 -c "import hyperframe" || { echo "hyperframe missing"; exit 1; } | |
| python3 -c "import hpack" || { echo "hpack missing"; exit 1; } | |
| # width_check compares the generated table against Reline, and skips | |
| # without it -- so its absence is asserted rather than tolerated. The | |
| # comparison is only evidence if a second implementation actually ran. | |
| command -v ruby >/dev/null || { echo "ruby missing (width_check)"; exit 1; } | |
| ruby -e 'require "reline"' || { echo "reline missing (width_check)"; exit 1; } | |
| # tty_raw_check opens a pty from python3, which is asserted above -- | |
| # named here so the dependency is not invisible at the gate. | |
| command -v grpcurl >/dev/null || { echo "grpcurl missing"; exit 1; } | |
| for f in --enable-tail-call --enable-threads; do | |
| wat2wasm --help 2>&1 | grep -q -- "$f" \ | |
| || { echo "wat2wasm does not support $f"; exit 1; } | |
| done | |
| # wasm_size_check's behaviour half runs the shipped modules under | |
| # node, and every playground module uses return_call. node accepts it | |
| # unflagged from 22 on; below that the gate skips itself, so a node | |
| # downgrade here would quietly stop checking that wasm-opt preserved | |
| # behaviour. Asserted as a capability, not a version string. | |
| printf '(module (func $a (result i32) (i32.const 1))\n (func (export "m") (result i32) (return_call $a)))\n' > /tmp/tc.wat | |
| wat2wasm --enable-tail-call /tmp/tc.wat -o /tmp/tc.wasm | |
| node -e 'new WebAssembly.Module(require("fs").readFileSync("/tmp/tc.wasm"))' \ | |
| || { echo "node cannot load a tail-call module (needs 22+): $(node --version)"; exit 1; } | |
| # The two node-oracle harnesses skip below this floor rather than | |
| # reporting phantom failures, so the floor is asserted here — a gate | |
| # that silently skips in CI is a gate that passes without running. | |
| node -e 'if (+process.versions.node.split(".")[0] < 24) | |
| { console.error("node " + process.version + " is below the v24 oracle floor"); process.exit(1); }' | |
| clang --version | head -1 | |
| node --version | |
| wat2wasm --version | |
| lldb --version | head -1 | |
| wasm-tools --version | |
| wasmtime --version | |
| command -v qemu-system-riscv32 >/dev/null 2>&1 \ | |
| && qemu-system-riscv32 --version | head -1 \ | |
| || echo "qemu-system-riscv32 absent: qemu_virt will skip" | |
| command -v riscv64-linux-gnu-objdump >/dev/null 2>&1 \ | |
| && riscv64-linux-gnu-objdump --version | head -1 \ | |
| || echo "riscv64-linux-gnu-objdump absent: rvd_oracle will skip" | |
| # ⚠ "WILL SKIP" IS THE PROBLEM, NOT THE REPORT. 65 gates in this repo exit | |
| # 0 when a tool they need is absent, and 46 of them are named in this | |
| # file. That is right for a laptop without psql and wrong for a build | |
| # whose only output is red or green: the day a package is renamed, the | |
| # install above fails (it is allowed to), the gate skips, and this job | |
| # stays green with its strongest differential gone. | |
| # | |
| # tool_preflight derives the tool list from the gates themselves and | |
| # fails HERE, naming the gate that would have disappeared, before any of | |
| # them has a chance to pass by not running. | |
| # `opam exec --` because dune is a tool some gates need (first_run_check | |
| # runs `dune test` to derive the README's test count), and it is only on | |
| # the PATH inside the opam environment. Without it this step reported | |
| # dune absent -- correctly, as it turned out: first_run_check ran without | |
| # opam and failed on exactly that (v0.1.535). | |
| - name: tool_preflight (a gate that did not run is not a gate that passed) | |
| run: opam exec -- sh scripts/tool_preflight_check.sh --required | |
| - name: tool_preflight poison | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/tool_preflight_check.sh --poison | |
| # The build matrix runs this too, but without wat2wasm and node it skips | |
| # the self-host codegen cross-validation — 2,225 cases there against 2,430 | |
| # here. Those 205 only run in this job, which has the toolchain. | |
| - name: Run tests (full, with the wasm toolchain present) | |
| run: opam exec -- dune runtest | |
| # A program that overflows, and what it says when it does. The suite checks this | |
| # feature by looking for the message in the EMITTED TEXT, which stayed green while | |
| # neither backend built on this runner at all. See v0.1.285. | |
| - name: stack_overflow (the fault names itself, per backend) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/stack_overflow.sh | |
| # And WHERE it happened, plus the calls that got there. Builtins raise with | |
| # no position of their own, so this is the application node lending them | |
| # one; the frames come from the interpreter's call stack. The poison runs | |
| # here too, because a poison nobody executes is a claim, not a check — it | |
| # switches the frames off and moves one expected position, and requires | |
| # the gate to go red for each. | |
| - name: "runtime_loc (a runtime failure names its line and its callers)" | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/runtime_loc_check.sh | |
| sh scripts/runtime_loc_check.sh --poison | |
| # The other side of the exhaustiveness question: which arms no value | |
| # reaches. The four cases that must stay SILENT are the point -- a check | |
| # that fires on a working match gets a live arm deleted. The sweep keeps | |
| # the shipped tree clean, and the poison moves an expected LINE as well as | |
| # a count, because warning about the wrong arm is the expensive failure. | |
| - name: "unreachable_arm (a dead match arm is reported, a live one is not)" | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/unreachable_arm_check.sh | |
| sh scripts/unreachable_arm_check.sh --poison | |
| # Q-138. Three backends counting allocation in three different places and | |
| # agreeing to within 18 bytes on 262 KB. A meter checked only against | |
| # itself is checking nothing, which is why the agreement is the gate and | |
| # not a note in the changelog. | |
| - name: "alloc_meter (C, LLVM and Wasm agree on how much was allocated)" | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/alloc_meter_check.sh | |
| sh scripts/alloc_meter_check.sh --poison | |
| # No harness may drop a line of its subject's output to compensate for | |
| # what the language used to print. Q-136 (v0.1.494) made a unit main | |
| # silent; the `sed '$d'` and `grep -v '^()$'` written against the old | |
| # behaviour stayed, and what they dropped stopped being noise -- eleven | |
| # CI steps across seven commits, including protoc-agreeing bytes that | |
| # came out as an empty string. Cheap, and it goes first because it reads | |
| # files rather than running anything. | |
| - name: "trailing_trim (no harness eats its subject's last line)" | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/trailing_trim_check.sh | |
| sh scripts/trailing_trim_check.sh --poison | |
| # What a program prints WHEN IT ENDS, on all four. parity.sh cannot see | |
| # this: every program it runs ends with `print`, so the path that | |
| # displays the program's own value is one almost nothing exercises -- and | |
| # it had drifted into four different answers for the same program. | |
| - name: "main_value (the four backends display a value the same way)" | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/main_value_check.sh | |
| sh scripts/main_value_check.sh --poison | |
| # The examples corpus, run on the interpreter and on C and required to | |
| # print the same bytes. 291 programs written for people rather than for a | |
| # harness, which is why they cover shapes a hand-written suite does not: | |
| # its first run found `show` of a Vec answering `<unknown>` on C. | |
| - name: "examples_parity (the corpus prints the same on interp and C)" | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/examples_parity.sh | |
| sh scripts/examples_parity.sh --poison | |
| - name: "determinism (parity precondition — stdout is a function of the program)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/determinism_check.sh | |
| # A double's bits, held as integers narrow enough for the one backend | |
| # that has no float. The gate checks the round trip bit for bit, that | |
| # interp and C agree, and that every exported name compiles for RV32I. | |
| - name: "softfloat (a double's bits, on a target with no float)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/softfloat_check.sh | |
| # The -rv prelude is injected by the driver, so the backend's own tests | |
| # cannot see it. Without this a name could be added there and never | |
| # compiled by anything. | |
| - name: "rv prelude (every name it defines compiles for -rv)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/rv_prelude_check.sh | |
| # `run` is the only channel a program has for the fate of a child, and the | |
| # interpreter reported a signalled child with OCaml's signal encoding: 121 | |
| # for SIGKILL where the shell and the C backend say 137. Below 128, where | |
| # nobody looks. | |
| - name: "run status (how a child died, same answer on both backends)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/run_status_check.sh | |
| # The interpreter is a capability boundary by construction; this asks the | |
| # filesystem whether it held, and asks whether a host can come back with a | |
| # verdict for a plugin that loops, allocates, or does not parse. | |
| - name: "plugin host (survives somebody else's program)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/plugin_host_check.sh | |
| # What a spawned thread's failure does to the program: one answer on four | |
| # backends since v0.1.586 (join raises it again, detach reports it, an | |
| # unclaimed one is a line at exit, the exit status is main's). Until then | |
| # three answers, and on C and LLVM a coin flip weighted by machine load. | |
| - name: "thread fail (what a dead thread does to the program)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/thread_fail_check.sh --poison | |
| # host_matrix asks which backend has each builtin; this asks whether the | |
| # documentation has heard of it. 25 of 221 had not been written about | |
| # anywhere by hand when it was added. | |
| - name: "doc coverage (every builtin is named in a hand-written doc)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/doc_coverage_check.sh | |
| # Every reserved word the lexer has, in language-reference.md's Keywords | |
| # block and in reserved-names.md (v0.1.538, Q-084): `view` was in neither | |
| # list, and the reference's block was missing trait / impl / dyn / derive. | |
| - name: "keywords doc (every reserved word is listed where people look)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/keywords_doc_check.sh && sh scripts/keywords_doc_check.sh --poison | |
| - name: "decls round-trip (--decls output, pasted back, changes nothing)" | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/decls_roundtrip.sh | |
| - name: live_query (which reads a write affects, on every backend) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/live_query_check.sh | |
| - name: migration_check (a migration consumes every existing row) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/migration_check.sh | |
| - name: live_soundness (a write never leaves a read stale — judged by the database) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/live_soundness_check.sh | |
| - name: render_purity (no view module reads the clock or the environment) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/render_purity_check.sh | |
| - name: cascade_catalog (the DDL parser vs pg_constraint) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/cascade_catalog_check.sh | |
| - name: extern_host (which contrib modules a native binary can link) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/extern_host_check.sh | |
| - name: wasm_stub (which builtins answer without reaching the host) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/wasm_stub_check.sh | |
| - name: wasm_stub poison (the gate can still go red) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/wasm_stub_check.sh --poison | |
| - name: type_query_imports (-t resolves an import like the build does) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/type_query_imports_check.sh | |
| - name: lsp_binding_position (definition/rename point at the name) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/lsp_binding_position_check.sh | |
| - name: lsp_binding_position poison | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/lsp_binding_position_check.sh --poison | |
| - name: mount (the router and the manifest come from one list) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/mount_check.sh | |
| - name: htmlbuild (the writing half escapes, on every backend) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/htmlbuild_check.sh | |
| - name: budget (sizes inside their bands, and what a page owes its reader) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/budget_check.sh | |
| # Separate from budget above because it costs a full site build (most of | |
| # a minute, nearly all of it compiling selfhost-compile.mere) and covers | |
| # a different subject: the .wasm the public site hands a browser. | |
| # `opam exec --`, like pages.yml, because this gate runs the real | |
| # contrib/site/build_full.sh and that starts with `dune exec mere -- | |
| # install`. The neighbouring gates here need no such thing -- they call | |
| # _build/default/bin/mere.exe directly -- so copying their step form is | |
| # what broke it: dune is on PATH here only inside opam's environment. | |
| - name: wasm_size (what the playground ships, inside its bands) | |
| if: ${{ !cancelled() }} | |
| run: opam exec -- sh scripts/wasm_size_check.sh | |
| - name: nojs (the primary flow completes with no JavaScript) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/nojs_check.sh | |
| - name: authz_coverage (no effect runs as nobody) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/authz_coverage_check.sh | |
| - name: live_e2e (the live-read loop over a real socket) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/live_e2e_check.sh | |
| - name: sse_native (live push from a native binary, no runtime under it) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/sse_native_check.sh | |
| - name: boundary_compat (every released version of the wire, crossed) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/boundary_compat_check.sh | |
| - name: render_agreement (server walk vs client DOM, same tree) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/render_agreement_check.sh | |
| - name: dom_canvas (a frame put on a canvas arrives pixel for pixel) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/dom_canvas_check.sh | |
| - name: parity (interp / C / LLVM / wasm agree) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/parity.sh | |
| - name: ctest (C backend end to end) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/ctest.sh | |
| # contrib's own self-tests ran under the interpreter and nowhere else, | |
| # which is how `contrib/toml` spent the project's whole history unable | |
| # to produce C that compiles. Four minutes, 87 programs. | |
| - name: contrib_ctest (contrib compiles and agrees with the interpreter) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/contrib_ctest.sh | |
| # Q-134 pinned in both directions: an allocation visible in a function's | |
| # type takes the caller's region, one that is not falls back to the | |
| # default. The second is conservative and open; the first is a | |
| # regression if it ever flips. | |
| - name: alloc_region_pin (where an invisible allocation goes) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/alloc_region_pin.sh | |
| # v0.1.481-482. Two bounds, and the floor is the one that matters: a gate | |
| # that only checked that saturated calls are free would stay green on the | |
| # day the two-step fallback was deleted, and a callback with no fn2 would | |
| # then call through a null pointer. Measured rather than grepped because | |
| # v0.1.324 records a fix to this same cost that built, kept every gate | |
| # green, and moved the allocation by zero bytes. | |
| - name: closure_alloc_pin (the uncurried entry, both directions) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/closure_alloc_pin.sh | |
| - name: range_version_check (the checked loop is the oracle for Q-108) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/range_version_check.sh | |
| - name: vectorize_check (clang vectorizes the emitted C) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/vectorize_check.sh | |
| - name: url_parity (vs node's URL) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/url_parity.sh | |
| - name: encoding_parity (vs node's TextDecoder) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/encoding_parity.sh | |
| - name: unicode_parity (vs node's Intl.Segmenter) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/unicode_parity.sh | |
| # No oracle exists for UAX #14, so this one runs the Unicode Consortium's | |
| # own conformance file, vendored under test/data so it needs no network. | |
| - name: linebreak_conformance (UCD test file) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/linebreak_conformance.sh | |
| # Normalization is the one algorithm here with both kinds of gate: an | |
| # independent implementation above, and the UCD's exhaustive file here. | |
| - name: normalize_conformance (UCD test file) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/normalize_conformance.sh | |
| - name: host_matrix (every builtin on every backend) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/host_matrix.sh | |
| # These two landed with v0.1.304/305 but were never wired in here — a | |
| # gate that exists and does not run is a claim, not a check. | |
| - name: thread_leak_check (a leak is reported, a join is not) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/thread_leak_check.sh | |
| - name: virtual_clock_check (parked threads advance the clock) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/virtual_clock_check.sh | |
| # Arena capacity stays proportional to allocation: one giant allocation | |
| # must neither strand the bump block's tail nor become the doubling base | |
| # (v0.1.307). Deterministic — the meter is MERE_REGION_STATS, not RSS. | |
| - name: region_slack_check (capacity tracks allocation) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/region_slack_check.sh | |
| # Does `region R { }` actually return memory, per backend? Same program, | |
| # same answer, three footprints -- which is why parity cannot see this. | |
| # The LLVM leg is a pin on a KNOWN gap and fails if the gap closes, so | |
| # the recorded numbers cannot go stale quietly. | |
| - name: region_reclaim_check (a region block returns memory, or is recorded as not doing so) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/region_reclaim_check.sh | |
| # Does a key a program asked for actually ARRIVE? A pipe has no line | |
| # discipline, so every tty test in this project passed while medit's | |
| # documented save and quit keys were being eaten as XOFF/XON. This drives | |
| # a Mere program through a real pty. The Ctrl-C leg pins the OTHER | |
| # direction: plain tty_raw must still let the signal through, so folding | |
| # ISIG into it cannot happen quietly. | |
| - name: tty_raw_check (raw mode delivers the keys, and still interrupts) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/tty_raw_check.sh | |
| # contrib/unicode/width against a NAMED second implementation (Reline), | |
| # not against the UCD it is generated from -- that would compare the | |
| # generator with itself. Every difference has to land in a category with | |
| # a reason attached; anything else is a DIFF and fails. Two of the | |
| # categories are ones where the ORACLE is the one to correct, which is | |
| # why the script is written to report rather than to waive. | |
| - name: width_check (display width agrees with a second implementation) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/width_check.sh | |
| # How big Q-127's remaining half is, measured on the corpus rather than | |
| # guessed: which functions would take a hidden region argument, and which | |
| # cannot take one at all. | |
| - name: region_params_check (the size of the change Q-127 still needs) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/region_params_check.sh | |
| # Progress that survives SIGKILL. The gate counts the attempts that were | |
| # killed WHILE STILL WORKING and fails if there were none (a kill that | |
| # lands after the program finished proves nothing), and runs the same | |
| # kill schedule against the same program with its log turned off, which | |
| # must never finish. | |
| - name: durable_check (a killed run resumes, and the kills are real) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/durable_check.sh | |
| # Recomputing only what changed: two consumers against the free oracle | |
| # (recompute everything), the exact node count rather than a bound, and | |
| # two negative controls that run as part of the gate -- an engine with | |
| # invalidation dropped (must differ from the oracle) and one that | |
| # recomputes everything every time (must MATCH it, which is why the | |
| # count is checked at all). | |
| - name: inc_check (contrib/inc recomputes the right nodes, and only those) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/inc_check.sh | |
| # A program's exit status is part of its answer. The Wasm backend used to | |
| # drop the code and trap, so `exit 0` reported failure on every host that | |
| # runs these modules -- and no parity program called `exit`, so the whole | |
| # builtin was outside the differential suite (v0.1.434, Q-114). | |
| - name: exit_status_check (exit n ends the program with n, on every backend) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/exit_status_check.sh | |
| # `mere check` must accept exactly what the compile path accepts. The | |
| # hazard is that it becomes a second `mere -t` -- a fast answer to a | |
| # different question, which is what -t already is (it exits 0 on | |
| # examples/borrow_conflict.mere and every backend refuses that file). So | |
| # the check is a differential over the whole examples tree rather than a | |
| # handful of picked cases, plus the named row that only the backend flags | |
| # can see. | |
| - name: check_cmd_check (`mere check` agrees with the compile path) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/check_cmd_check.sh | |
| # A `match` with no arm for a case was a warning printed from inside the | |
| # interpreter's path -- so `-c`, `-ll`, `-w` and `-rv` said nothing and | |
| # exited 0. (This comment used to say each backend invented a value for the | |
| # fallthrough; measured, three of the four failed LATE AND MUTE instead -- | |
| # see v0.1.470.) The refusal is | |
| # held on all five paths here, and the arm the error prints is pasted | |
| # back into the program and run, so a hint that stops being valid Mere | |
| # goes red rather than being merely wrong. | |
| - name: exhaustive_check (a missing case is refused on every path) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/exhaustive_check.sh | |
| # The region report names the one expression that took binarytrees from | |
| # 169 MiB to 5.5 MiB, and stays quiet where the region is already written. | |
| # Nothing is inferred: the report is where the language says "here", and | |
| # the source is where the decision becomes visible (v0.1.415). | |
| - name: suggest_check (the region report names the expression that mattered) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/suggest_check.sh | |
| # The enumerated escape table. Every way a region-bound value can reach | |
| # something that outlives its block, with what each entry point answers | |
| # -- and the four compiling backends must agree, because two backends | |
| # disagreeing with nothing watching is what Q-053 was. Open holes are | |
| # rows, not absences: if one closes, this FAILS until the table says so. | |
| - name: escape_check (region escape routes, enumerated) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/escape_check.sh | |
| # The SAFE rows above are accepted; this asks whether an accepted program | |
| # then reads the right bytes once the block's memory has been reused. Four | |
| # routes were safe by every verdict and use-after-free at run time | |
| # (Q-190, Q-191, Q-192 and LLVM channels, v0.1.563-564). | |
| - name: region_uaf_check (what escaped a block by a route the types do not see is still there) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/region_uaf_check.sh --poison | |
| # v0.1.605: a ten-million-element list ran the copy out of C stack, one | |
| # frame per element. The copier now walks a list's spine in a loop. | |
| - name: deep_list_check (a long list is copied without a C frame per element) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/deep_list_check.sh --poison | |
| # `mere -c --region-sites` names the source lines whose containers filled | |
| # the default region (v0.1.570), and must change nothing else: the same | |
| # output, the same default-region total. | |
| - name: region_sites_check (the meter names the line that filled the default region) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/region_sites_check.sh --poison | |
| # Two host-facing corners of the LLVM backend that nothing built and ran | |
| # until downstream_cc_check compiled mwasm's IR (v0.1.571): a program | |
| # whose only file builtins are file_openrw / file_size / file_close, and | |
| # `args ()` strings, which were argv's raw pointers with no length header. | |
| - name: llvm_host_args_check (file-only programs build on LLVM; an argument is a str) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/llvm_host_args_check.sh --poison | |
| # A container written by a thread other than the one that made it fails | |
| # by name on the interpreter, C and LLVM (v0.1.575): the case the type | |
| # check cannot see is a closure that arrives as a parameter. | |
| # v0.1.582: and a container another thread has read is read-only, an | |
| # OwnedVec belongs to the thread spawn moved it into. | |
| - name: owner_check (a container is written only by the thread that made it) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/owner_check.sh --poison | |
| # A saturated call of a curried inner function allocates no closures on | |
| # C, LLVM and Wasm (Q-142, v0.1.583): mandelbrot's inner loop was 104 | |
| # B/iter on Wasm and 235 MB on LLVM before. | |
| - name: inner_direct_check (an inner function called with every argument builds no closures) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/inner_direct_check.sh && sh scripts/inner_direct_check.sh --poison | |
| # The language server (v0.1.576): an unbroken run of didChanges that has | |
| # already arrived is checked once, and foldingRange is answered. | |
| - name: lsp_coalesce_check (keystrokes already out of date are not each re-checked) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/lsp_coalesce_check.sh | |
| - name: lsp_folding_check (declarations and comment runs fold) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/lsp_folding_check.sh | |
| # The self-hosted lexer reads every word lib/lexer.ml reserves as a keyword | |
| # (v0.1.578); the operators are selfhost_lexer_ops_check's. | |
| - name: selfhost_lexer_keywords_check (no reserved word is read as a name) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/selfhost_lexer_keywords_check.sh --poison | |
| # The README, executed. Its Quick-examples transcript is cut from the | |
| # file at run time and run, the counts it states are re-derived from the | |
| # repo, and a newcomer's first program is built and run outside this | |
| # directory on two backends. `install.sh` once served a binary 260 | |
| # versions old while every gate here was green: nothing was pointed | |
| # outward. This is. | |
| - name: first_run_check (the README, executed) | |
| if: ${{ !cancelled() }} | |
| run: opam exec -- sh scripts/first_run_check.sh | |
| # Parts of the Wasm runtime are literal WAT text gated behind a flag only | |
| # a program using that feature sets, so text nobody reaches is never | |
| # validated and a representation change walks past it. Q-068 and Q-069 | |
| # were both that, in sections wrong since values widened. Every gated | |
| # section needs a program, or a written reason it cannot have one. | |
| - name: section_coverage (every hand-written runtime section has a program) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/section_coverage.sh | |
| # The cross-language benchmark suite, deterministic half only: every | |
| # implementation of a workload must print the same bytes, Mere's | |
| # programs must build, and the default region's cumulative allocation | |
| # must stay in its recorded band. No wall clock and no peak RSS -- both | |
| # measure the runner. Reference toolchains absent from the image are | |
| # skipped with a printed reason rather than silently. | |
| - name: bench_check (the implementations agree; allocation is in band) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/bench_check.sh | |
| # The --lib boundary, checked from the outside: the .so's exported | |
| # symbol set is exactly the boundary, a C host linking it gets the | |
| # right values, and module init runs once (v0.1.308). | |
| - name: lib_check (shared-library boundary) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/lib_check.sh | |
| # Readiness I/O (v0.1.313): the transcript checks both directions -- | |
| # events arrive when they should, none are invented when nothing | |
| # happened -- and pins the coded would-block returns on nonblocking | |
| # read / accept / write. | |
| - name: io_poll_check (readiness answers what it claims) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/io_poll_check.sh | |
| # Audio output (v0.1.314): the queue's contract under the SDL dummy | |
| # driver — visible pending bytes, bounded drain to exactly 0, -1 after | |
| # close. Sample correctness is the consumer's oracle, not this gate's. | |
| - name: audio_check (the audio queue's contract, headless) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/audio_check.sh | |
| # v0.1.509: the file_* metadata runtime. Every expectation is a value the | |
| # probe sets itself -- it chmods and reads the mode back, truncates and | |
| # reads the size -- so the gate is the same on any POSIX host and needs | |
| # no library. It caught a stale-cache design on its first run. | |
| - name: filestat_check (stat, chmod, truncate, umask, readlink, mkfifo) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/filestat_check.sh | |
| # v0.1.522: the fd_* runtime -- an open file. open(2) is in <fcntl.h>, | |
| # which an emitted program does not include, and its flags are platform | |
| # constants; the mode string and the whence numbering are contracts the | |
| # runtime defines and this gate exercises. Every expectation is a value | |
| # the probe sets. It caught two DEGENERATE CHECKS of its own before it | |
| # caught anything in the runtime: a seek test where CUR and END happened | |
| # to answer the same number, and a truncation test that was reading the | |
| # previous run's leftovers. | |
| - name: fdio_check (open, read, write, seek, dup, pipe, close) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/fdio_check.sh | |
| # v0.1.550: resource limits, scheduling priority and flock(2). The | |
| # platform's numbers differ exactly here (RLIMIT_NOFILE 8 vs 7, | |
| # RLIM_INFINITY 2^63-1 vs 2^64-1, EWOULDBLOCK 35 vs 11), so the runtime | |
| # takes a resource by name and defines its own selector and lock bits; | |
| # every row is a limit the probe set and read back, or a refusal it | |
| # provoked. Same transcript as root and as an unprivileged user. | |
| - name: proclimit_check (getrlimit, setrlimit, getpriority, setpriority, flock) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/proclimit_check.sh | |
| # v0.1.589: a signal's disposition (catch and do nothing, default, raise) | |
| # and the errno of a refused write to stdout, each in its scene: an | |
| # inherited SIG_IGN, a pipe whose reader has exited, a run that must end | |
| # of SIGPIPE (141). | |
| - name: procsig_check (SIGPIPE caught and passed on as the default, a write to a closed pipe is EPIPE) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/procsig_check.sh | |
| # v0.1.555: a listener on a chosen address, bind(2) on a socket, the | |
| # address a socket has, and the errno the fd_* family keeps. The errno | |
| # numbers differ exactly here (EADDRINUSE 48 vs 98, ECONNRESET 54 vs | |
| # 104), so the expected ones are read from the host's <errno.h>; every | |
| # other row is a port the kernel gave the probe or a refusal it provoked. | |
| - name: sockaddr_check (tcp_listen_at, sock_bind, getsockname, getpeername, fd_last_errno) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/sockaddr_check.sh | |
| # Float results must not depend on the C optimizer (v0.1.315): the | |
| # same dot product at -O0 and -O2 must produce the same bits, and both | |
| # must match the interpreter. Without the emitted FP_CONTRACT OFF | |
| # pragma, clang's default fma contraction breaks both claims. | |
| - name: fp_contract_check (the optimizer must not change the answer) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/fp_contract_check.sh | |
| # The one rounding a program asks for by name (Q-176, v0.1.534): `fma` | |
| # and `f64x2_fma` against the C library's fma(3), bit for bit, on all | |
| # four backends. The Wasm leg is software ($__lang_fma) and is the reason | |
| # this gate exists; it fails rather than skips without wat2wasm and node. | |
| - name: fma_check (fma against fma(3), bit for bit, four backends) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/fma_check.sh | |
| # The emitted C must fit inside clang's nesting limit and must evaluate | |
| # operands in the interpreter's order (v0.1.450). Both used to be left to | |
| # the C compiler: Apple's clang allows nesting that Ubuntu's refuses, and | |
| # gcc evaluates `a ++ b` right to left where clang goes left to right. | |
| # This runner has BOTH compilers, which is the point -- one compiler | |
| # cannot disagree with itself. | |
| - name: bracket_depth_check (nesting limit + operand order) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/bracket_depth_check.sh | |
| # A `musttail` the target cannot honour is a build that DIES, and only at -O0 | |
| # (v0.1.451). The width where that starts is ABI-dependent -- x86-64 stops at | |
| # half of what arm64 forwards -- so this runner is the one that sees the | |
| # binding number, and the gate fails if the pinned table drifts either way. | |
| - name: musttail_budget_check (a musttail the target can keep) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/musttail_budget_check.sh | |
| - name: selfhost_check | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/selfhost_check.sh | |
| # The self-hosted lexer reads every operator lib/lexer.ml reads, as one | |
| # token (v0.1.539, Q-153): `|>` came out as Pipe then Gt and `@@` / `?` | |
| # did not lex, while selfhost_check stayed green on a corpus using none. | |
| - name: selfhost_lexer_ops (the second lexer keeps up with the first) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/selfhost_lexer_ops_check.sh && sh scripts/selfhost_lexer_ops_check.sh --poison | |
| - name: lsp_smoke | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/lsp_smoke.sh | |
| sh scripts/lsp_smoke.sh --poison | |
| # v0.1.502. Three checks that came in with the Gleam-derived slice, each | |
| # with its poison run: a gate that cannot go red is a line in a log. | |
| - name: unused_check (bindings nothing reads) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/unused_check.sh | |
| sh scripts/unused_check.sh --poison | |
| - name: version_floor_check (the feature/version table, re-derived) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/version_floor_check.sh | |
| sh scripts/version_floor_check.sh --poison | |
| - name: echo_check (echo says the same thing on every backend) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/echo_check.sh | |
| sh scripts/echo_check.sh --poison | |
| # v0.1.504. The second Gleam-derived slice, same rule: each with its | |
| # poison run. | |
| - name: warnings_as_errors_check (a warning can fail a build) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/warnings_as_errors_check.sh | |
| sh scripts/warnings_as_errors_check.sh --poison | |
| - name: decls_json_check (the two exits of --decls agree) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/decls_json_check.sh | |
| sh scripts/decls_json_check.sh --poison | |
| - name: module_privacy_check (pub hides what it does not mark) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/module_privacy_check.sh | |
| sh scripts/module_privacy_check.sh --poison | |
| # v0.1.505. | |
| - name: refutable_let_check (a let that can fail is refused) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/refutable_let_check.sh | |
| sh scripts/refutable_let_check.sh --poison | |
| - name: fmt_comments_check (the formatter keeps column-1 comments) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/fmt_comments_check.sh | |
| sh scripts/fmt_comments_check.sh --poison | |
| # v0.1.506. | |
| - name: diagnostic_position_check (a diagnostic points where you can act) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/diagnostic_position_check.sh | |
| sh scripts/diagnostic_position_check.sh --poison | |
| # v0.1.507. | |
| - name: syntax_hint_check (a syntax error says what to write instead) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/syntax_hint_check.sh | |
| sh scripts/syntax_hint_check.sh --poison | |
| # doc_coverage asks whether a builtin's NAME is spelled in a doc; this asks | |
| # whether a doc's CLAIM is still true. Three sentences described a compiler | |
| # that had not existed for hundreds of releases, each one next to a green | |
| # gate that proved the opposite. | |
| - name: doc_claims_check (a documented limitation is still a limitation) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/doc_claims_check.sh | |
| sh scripts/doc_claims_check.sh --poison | |
| # Q-165. The parity suite holds the backends to one answer; this asks | |
| # whether that answer is the same one the uncaught failure prints. | |
| - name: fail_reason_check (a caught failure and an uncaught one agree) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/fail_reason_check.sh | |
| sh scripts/fail_reason_check.sh --poison | |
| # Q-166. The module gate one level out: `import` splices, so the boundary | |
| # `pub` needs is the file each token came from. | |
| - name: file_privacy_check (`pub` at the top of a file survives the splice) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/file_privacy_check.sh | |
| sh scripts/file_privacy_check.sh --poison | |
| # Q-171. A `fail` caught outside a `region R { }` longjmps past the | |
| # block's release; the scale is the point, because one catch passed on the | |
| # backend that segfaulted at a hundred. | |
| - name: region_unwind_check (a catch releases the block it jumped over) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/region_unwind_check.sh | |
| sh scripts/region_unwind_check.sh --poison | |
| # Q-172. `mere fmt -i` rewrites in place, so the output being a program is | |
| # not a nicety -- 52 of 293 examples came back as something the compiler | |
| # refused, and the formatter's own tests were all small single files. | |
| - name: fmt_roundtrip_check (what fmt writes is still the same program) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/fmt_roundtrip_check.sh | |
| sh scripts/fmt_roundtrip_check.sh --poison | |
| # Q-120. The C side of an `extern fn` should include a header the compiler | |
| # wrote rather than copying the `mu_` prefix and the field order by hand. | |
| # The poison is the hand-copied struct, giving a different answer. | |
| - name: ffi_header_check (the shim includes what the compiler wrote) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/ffi_header_check.sh | |
| sh scripts/ffi_header_check.sh --poison | |
| # Q-125. A record declared inside a module could not be named in an | |
| # annotation -- not even inside the module that declared it -- while | |
| # variants could. The twins are asked the same questions side by side, | |
| # because the bug was that they disagreed. | |
| - name: module_type_check (a module's type can be written down) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/module_type_check.sh | |
| sh scripts/module_type_check.sh --poison | |
| # Q-133. The substring assertions must be capable of failing. 41 of the 97 | |
| # wasm ones were not, and 11 were outright false about the user's code. | |
| # All three backends (wasm / C / LLVM, 352 assertions) compare against a | |
| # control inside the test now; this pins each population and the size of | |
| # each runtime exemption. | |
| - name: wasm_assert_strength (the backends' assertions can fail) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/wasm_assert_strength.sh | |
| sh scripts/wasm_assert_strength.sh --poison | |
| # Q-052. A local `let` must not write a top-level binding that shares its | |
| # name. The same bug was fixed on Wasm first and shipped with NO gate, | |
| # which is exactly how the LLVM side stayed broken; this asks both. | |
| - name: toplevel_shadow_check (a callee's local does not write your global) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/toplevel_shadow_check.sh | |
| sh scripts/toplevel_shadow_check.sh --poison | |
| # Q-169. `mere doc` joins two answers the compiler already had (`--decls` | |
| # and hover's `doc_above`). The undocumented case is the one checked | |
| # hardest: a tool that prints only documented names merges "not exported" | |
| # with "nobody wrote a comment". | |
| - name: doc_cmd_check (mere doc says what a file exports) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/doc_cmd_check.sh | |
| sh scripts/doc_cmd_check.sh --poison | |
| # Q-169, the other half. `mere test` runs the [test] list in mere.toml | |
| # (else verify.sh) and classes each exit status. The poison removes the | |
| # list: the declared list must win over the convention. | |
| # v0.1.554. No gate says it skipped and then exits 0: "passed" and "did | |
| # not run" are different statuses (2 could not answer, 3 optional). | |
| - name: skip_exit_check (no gate says it skipped and exits 0) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/skip_exit_check.sh | |
| sh scripts/skip_exit_check.sh --poison | |
| - name: test_cmd_check (mere test runs what a package declares, in order) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/test_cmd_check.sh | |
| sh scripts/test_cmd_check.sh --poison | |
| # Q-168. A program could be DIAGNOSED for running out of stack since | |
| # v0.1.271 but could not ask for more; the answer lived outside the build | |
| # in three different spellings. Both directions on both native backends, | |
| # because a fixture that passed either way would prove nothing. | |
| - name: stack_request_check (a program can say how much stack it needs) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/stack_request_check.sh | |
| sh scripts/stack_request_check.sh --poison | |
| # Q-178. `spawn` ignored the stack the program asked for, a spawned | |
| # thread's overflow died unnamed, and on LLVM a spawned thread's fail | |
| # jumped into main's try_or. Both native backends, with a poison. | |
| # Q-180. LLVM's allocator took no lock on the default region and kept | |
| # its current region per process: two allocating threads ran out of | |
| # memory. Both native backends, with a poison. | |
| - name: threads_alloc_check (two threads allocating at once) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/threads_alloc_check.sh | |
| sh scripts/threads_alloc_check.sh --poison | |
| # Same-thread coroutines. A switch carries the four pieces of state that | |
| # belong to a stack (region, open blocks, try_or, stack bounds); the | |
| # poison drops each, and the env copy and two checks, from the emitted C | |
| # and from the emitted IR. --poison runs the fixtures and refusals first. | |
| - name: coro_check (same-thread coroutines, interpreter, C and LLVM) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/coro_check.sh --poison | |
| - name: spawn_stack_check (a spawned thread gets the requested stack) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/spawn_stack_check.sh | |
| sh scripts/spawn_stack_check.sh --poison | |
| # v0.1.508. | |
| - name: binding_form_check (every spelling gets the same verdict) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| sh scripts/binding_form_check.sh | |
| sh scripts/binding_form_check.sh --poison | |
| - name: socket_parity | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/socket_parity.sh | |
| # Three runtime sections exist only under `-w --component`, so the parity | |
| # harness -- which compiles with plain `-w` -- cannot reach them at all. | |
| # This builds and RUNS them against the interpreter. It shares the | |
| # component toolchain installed for socket_parity above, and skips loudly | |
| # rather than silently if that toolchain is absent. | |
| - name: component_parity (the component-only sections, run) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/component_parity.sh | |
| - name: tcp_read_codes | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/tcp_read_codes.sh | |
| # A Mere program ANSWERING a TLS connection, not dialling one. The client | |
| # half has existed since v0.1.x; the server half did not, which is why | |
| # every web dogfood in this project serves plaintext behind a proxy. The | |
| # oracles are curl and openssl s_client -- two TLS implementations that | |
| # are not ours and cannot be made lenient from this repository. | |
| - name: tls_server_check (a Mere program terminates TLS) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/tls_server_check.sh | |
| # Whether the server answers requests AT THE SAME TIME. A handler that | |
| # sleeps on purpose is what makes the two answers different, and the same | |
| # binary run with one worker is the sequential control -- without it a | |
| # fast machine passes for the wrong reason. | |
| # A bare `wait` waits for every child, including a gate's own background | |
| # server, which does not exit. It cost mere-blog a sixty-minute CI job on | |
| # 2026-08-28 -- hours after the identical line was fixed in | |
| # scripts/http_concurrency_check.sh here. Fixing an instance is not | |
| # fixing a pattern. | |
| - name: no bare `wait` in a gate that starts a server | |
| if: ${{ !cancelled() }} | |
| run: | | |
| if grep -rn '^[[:space:]]*wait[[:space:]]*$' scripts/*.sh; then | |
| echo "a bare wait: name the client PIDs instead" | |
| exit 1 | |
| fi | |
| echo "ok: no bare wait in scripts/" | |
| # A file upload, checked by CONTENT. contrib/http/multipart.mere had been | |
| # written, documented and never run; running it found that request bodies | |
| # stopped at their first zero byte, so every JSON test passed while every | |
| # binary upload would have been truncated. | |
| - name: http_upload_check (a binary upload survives its zero bytes) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/http_upload_check.sh | |
| - name: http_concurrency_check (the server serves more than one at once) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/http_concurrency_check.sh | |
| # The Protocol Buffers wire format against protoc's BYTES. Runs the | |
| # interpreter AND the C backend, because two of its four sections are about | |
| # the int-width difference between them: above 2^62 the interpreter is | |
| # 63-bit and the compiled backends are 64-bit, and that divergence is | |
| # PINNED here rather than dropped, so a fix breaks this gate and says so. | |
| - name: proto_parity (vs protoc's bytes) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/proto_parity.sh | |
| # The .proto parser and descriptor encoder, byte-identical to | |
| # `protoc --descriptor_set_out`. This is where the bootstrap closes: a | |
| # descriptor set is itself a protobuf message, so the code that reads a | |
| # schema is serialised by the code that reads wire bytes and one oracle | |
| # checks both layers at once. | |
| - name: proto_desc_parity (vs protoc's descriptor set) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/proto_desc_parity.sh | |
| # The GENERATED codec against protoc's bytes: decode protoc's output and | |
| # re-encode it, and require the two byte strings to be identical. Also diffs | |
| # the committed examples/hello_pb.mere against a fresh run, because a | |
| # generator whose output nobody reads is a generator nobody can review. | |
| - name: proto_gen_parity (generated codec vs protoc's bytes) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/proto_gen_parity.sh | |
| # The GraphQL parser and printer against graphql-js, by sending our output | |
| # back through their parser rather than by comparing serialised ASTs — so | |
| # nothing here transcribes their tree, and a shared misreading has nowhere | |
| # to hide. A deliberately broken lexer proved the round-trip alone is blind | |
| # to the Int/Float distinction, so the harness asks for that kind directly. | |
| - name: graphql_parity (vs graphql-js) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/graphql_parity.sh | |
| # The executor against graphql-js's execute(), with the resolvers being the | |
| # DATA on both sides so nothing about resolution is transcribed. What it | |
| # really exercises is null propagation: a non-null field that resolves to | |
| # null destroys the nearest nullable ancestor, and an item error in [Int!] | |
| # nulls the whole list. | |
| - name: graphql_exec_parity (vs graphql-js execute) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/graphql_exec_parity.sh | |
| - name: graphql_intro_parity (vs graphql-js introspection) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/graphql_intro_parity.sh | |
| - name: graphql_validate_parity (vs graphql-js validate) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/graphql_validate_parity.sh | |
| - name: graphql_server_parity (a native GraphQL endpoint, real clients) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/graphql_server_parity.sh | |
| # HTTP/2 framing against hyperframe, in both directions, plus the reserved | |
| # bit of the stream identifier — the one field that behaves correctly | |
| # against every well-behaved peer while being wrong. | |
| - name: http2_parity (vs hyperframe) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/http2_parity.sh | |
| # HPACK against the Python hpack library, per CONNECTION rather than per | |
| # block — a desynchronised dynamic table does not error, it reports a | |
| # different header name. Includes a block captured from grpc-go, which is | |
| # what checks the two tables the oracle and the implementation share. | |
| - name: hpack_parity (vs hpack) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/hpack_parity.sh | |
| # The whole stack, driven by clients that know nothing about it: grpcurl | |
| # (Go) over four connections, and a python h2 client making three requests | |
| # on ONE connection with one of them split across TCP segments. The split is | |
| # not decoration — removing the server's frame reassembly passed every other | |
| # section, because on loopback a request arrives in a single read. | |
| - name: grpc_parity (grpcurl and python h2 against a Mere server) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/grpc_parity.sh | |
| # The window capability, checked by reading the window's own pixels back. | |
| # SDL's `dummy` video driver gives a software renderer and an event queue | |
| # with no display, so this needs no X server on the runner. | |
| - name: window_check | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/window_check.sh | |
| # The HTML tokenizer against html5lib-tests, the suite maintained alongside | |
| # the specification. Vendored, so this needs no network. | |
| - name: html_tokenizer_conformance | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/html_tokenizer_conformance.sh | |
| - name: infer_scaling (type inference stays linear) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/infer_scaling.sh | |
| - name: wasm_sourcemap | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/wasm_sourcemap.sh | |
| - name: debug_info (lldb agrees where each line is) | |
| if: ${{ !cancelled() }} | |
| run: sh scripts/gate.sh scripts/debug_info.sh | |
| # The RISC-V arc's headline claim is that the same bytes behave | |
| # identically on QEMU and on a CPU written in Mere. That emulator lives | |
| # in another repository, so without this checkout qemu_virt ran the QEMU | |
| # half only -- and reported the same pass line either way, which is how a | |
| # gate stops meaning what it says. It says which half it ran now, and | |
| # this makes the answer "both". | |
| # The programs written IN this language. Around three dozen dogfood | |
| # repositories exist, none of them with CI, and this repository could not | |
| # see any of them -- so a language change broke them silently and the | |
| # news arrived whenever somebody next opened one. test/downstream/REPOS | |
| # names one per surface; the check is `mere -c` from inside each, which | |
| # asks the question this repo can answer (is their code still a program) | |
| # rather than the one they own (do their tests pass). | |
| # A clone that fails leaves that repo absent, which the gate counts and | |
| # names rather than dropping. | |
| # `if: !cancelled()` like the gates below it: without it this step is | |
| # skipped the moment any earlier gate fails, downstream_check then finds | |
| # nothing to check, reports "13 absent, 0 checked" -- and exited 0. That | |
| # is how v0.1.410's downstream row was green while mere-ruby was never | |
| # compiled at all. The script now refuses to pass on zero, and this step | |
| # now runs whenever the gates do. | |
| - name: Check out the dogfood repositories | |
| if: ${{ !cancelled() }} | |
| continue-on-error: true | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/downstream" | |
| cd "$RUNNER_TEMP/downstream" | |
| for r in $(awk '$1 !~ /^#/ && NF && $3 == "compile" {print $1}' \ | |
| "$GITHUB_WORKSPACE/test/downstream/REPOS"); do | |
| # --recurse-submodules because mere-ruby carries contrib/mgz as a | |
| # submodule; a plain --depth 1 clone leaves .mere_modules/mgz an | |
| # empty directory and the gate blames the compiler for it. | |
| git clone --depth 1 --recurse-submodules --shallow-submodules -q \ | |
| "https://github.com/284km/$r.git" "$r" \ | |
| || { echo "clone failed: $r (the gate will report it absent)"; continue; } | |
| done | |
| # Dependency resolution is NOT here: downstream_check.sh runs | |
| # `mere install` itself when a repo has a mere.toml and no | |
| # .mere_modules, so the gate asks the same question on a fresh clone | |
| # and on a development checkout. Splitting it between this file and | |
| # the script is what let the two drift apart in the first place. | |
| - name: downstream_check (their code is still a program) | |
| if: ${{ !cancelled() }} | |
| run: MERE_DOWNSTREAM="$RUNNER_TEMP/downstream" sh scripts/gate.sh scripts/downstream_check.sh | |
| # downstream_check stops at emission; nothing compiled what came out, so a | |
| # runtime change reaches every downstream with no gate looking (v0.1.566-567 | |
| # were found by a mere-ruby build). This runs each repository's own compile | |
| # line on its emitted C (and the LLVM IR of the four the LLVM backend | |
| # emits whole), no linking. A row whose code, compiler and flags hash to an | |
| # earlier pass is reused, which is what the cache below is for: mere-ruby | |
| # and mbrowse are minutes each and peak at 6-7 GB. The poison leg first: | |
| # each kind of failure must say which kind it is. | |
| - name: Restore the downstream compile cache | |
| if: ${{ !cancelled() }} | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/mere-downstream-cc | |
| key: downstream-cc-${{ github.run_id }} | |
| restore-keys: downstream-cc- | |
| - name: downstream_cc_check --poison (a failure says whether it was an error, a timeout or a kill) | |
| if: ${{ !cancelled() }} | |
| run: MERE_DOWNSTREAM="$RUNNER_TEMP/downstream" sh scripts/gate.sh scripts/downstream_cc_check.sh --poison | |
| - name: downstream_cc_check (their emitted code still compiles) | |
| if: ${{ !cancelled() }} | |
| run: MERE_DOWNSTREAM="$RUNNER_TEMP/downstream" sh scripts/gate.sh scripts/downstream_cc_check.sh | |
| # memu is already among the dogfood clones above, so the RISC-V | |
| # differential points at that checkout rather than fetching it twice. | |
| # Without it, qemu_virt runs the QEMU half only -- and used to report | |
| # the same pass line either way, which is how a gate stops meaning what | |
| # it says. It names which halves ran now. | |
| - name: qemu_virt (vs an emulator nobody here wrote, and one we did) | |
| if: ${{ !cancelled() }} | |
| run: MEMU="$RUNNER_TEMP/downstream/memu" sh scripts/gate.sh scripts/qemu_virt.sh | |
| # The operating system on that CPU: a two-task kernel with a shell, and a | |
| # kernel that runs a separately compiled user program and answers its | |
| # syscalls. These are the programs behind "an OS on a self-made CPU", and | |
| # nothing ran them -- when a literal check arrived that refused their | |
| # 0x80000007 interrupt cause, all three stopped compiling and no gate said | |
| # so. This builds them and runs two on the same memu checkout. | |
| - name: os_check (the kernel, the shell and a user process, on memu) | |
| if: ${{ !cancelled() }} | |
| run: MEMU="$RUNNER_TEMP/downstream/memu" sh scripts/os_check.sh | |
| # ⚠ THIS GATE WAS IN NO WORKFLOW. rv_exec_check is the differential | |
| # between the RISC-V backend and the C backend on the hosted side -- the | |
| # half qemu_virt does not cover -- and nothing ran it, so it had never | |
| # had to pass. When it was finally run on 2026-09-24 it was RED: | |
| # int_width_boundary, added to the parity suite for a different question, | |
| # differs by construction on a 32-bit target. | |
| - name: rv_exec (the RISC-V backend against the C backend, hosted side) | |
| if: ${{ !cancelled() }} | |
| run: MEMU="$RUNNER_TEMP/downstream/memu" sh scripts/rv_exec_check.sh | |
| # The other half of the same argument: qemu_virt checks that the bytes we | |
| # emit MEAN what we think, and this checks that the listing we print of | |
| # those bytes SAYS what they mean. A disassembler is an instrument, and an | |
| # instrument with no gate goes quietly wrong -- this one was 41% | |
| # unreadable on the wide target and confidently mis-named srli as srai. | |
| - name: rvd_oracle (mere -rvd vs a disassembler nobody here wrote) | |
| if: ${{ !cancelled() }} | |
| run: OBJDUMP=riscv64-linux-gnu-objdump sh scripts/rvd_oracle_check.sh |