Skip to content

v0.1.606: f_pow, exp and log on RISC-V answer libm's bits; an extern'… #917

v0.1.606: f_pow, exp and log on RISC-V answer libm's bits; an extern'…

v0.1.606: f_pow, exp and log on RISC-V answer libm's bits; an extern'… #917

Workflow file for this run

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