open-standards/.github/workflows/wai-vector.yml
Transaction Science 888cc3ba20 CI: a check holds every workflow to the one pinned wasmtime and both its settings
The install steps' comment said every WAI workflow that runs wasmtime pins one
version and passes both determinism settings, but nothing enforced it: a step
that ran a stock or differently pinned wasmtime would have merged green.
scripts/check-wasmtime-pin.py now fails, in the repository checks, any workflow
that

1. downloads a wasmtime release other than 50.0.1, or checks it against any
   SHA-256 but that archive's;
2. installs wasmtime another way (cargo install, the install script, an action);
3. runs wasmtime (run, serve, compile, or a module given to it) or names it as
   a CARGO_TARGET_*_RUNNER without -W nan-canonicalization=y and
   -W relaxed-simd-deterministic=y.

It also fails when it finds no download or no run, so a check that looked in
the wrong place cannot pass. On this tree it finds the downloads and runs it
should. Its self-test (scripts/check-wasmtime-pin-selftest.sh) passes a scratch
workflow shaped like these and fails each way of breaking it that the
self-test lists, matching the reason in the output.

The comment's relaxed_madd claim was measured on arm64 only. It now rests on
both architectures, measured on an arm64 macOS host and an x86_64 Linux host
with a small probe (wasmtime 50.0.1's release for the host, checked against
the digest the release lists):

    host Darwin arm64; wasmtime 50.0.1 (c307ec81d 2026-10-29)
    f32.add(bits 0x7fa00001, 1.0)                     [no flags]: 0x7fe00001
    f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [no flags]: 0x33800000
    f32.add(bits 0x7fa00001, 1.0)                     [both flags]: 0x7fc00000
    f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [both flags]: 0x33800000
    an unknown -W option is refused: error: failed to parse -W / --wasm option:
      unknown option: no-such-option

    host Linux x86_64; wasmtime 50.0.1 (c307ec81d 2026-10-29)
    (the same five lines, bit for bit)

0x33800000 is 2^-24, the fused result; the unfused fadd(fmul) gives 0. So the
NaN setting takes effect on both, and relaxed_madd is fused on both with and
without the relaxed setting. That matches 50.0.1's source (the
wasmtime-internal-cranelift crate, read from the registry): the translator
lowers F32x4/F64x2RelaxedMadd through fma_f32x4/fma_f64x2
(src/translate/code_translator.rs L2530-2539), which emit a native fma on a
host that has one, a fused library call under relaxed-simd-deterministic on a
host that has none, and the unfused fmul and fadd only on a host with no FMA
and the setting off (src/func_environ.rs L5907-5925). The setting is what
makes the result fused on every host. And since an unknown -W option stops
wasmtime, a run that exits 0 with the two flags has parsed them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 12:28:47 -05:00

409 lines
18 KiB
YAML

name: wai-vector vector graphics
# The vector graphics capability (wai/extensions/graphics-vector.md), checked on every
# fabric leg the corpus has today. Every leg recomputes every expected file of
# wai/vector-conformance/ from its vector and the committed sets, and requires the
# committed bytes:
#
# - Linux x86_64, Linux arm64 and macOS arm64, natively, debug and release: the unit
# tests, the refusal-order, canonical, clock, track, geometry, cost, held-set and
# raster tests, the corpus tests (every frame of the rendering groups recomputed
# to its SHA-256, and every text record of the a11y group recomputed byte for
# byte), the in-house sets' tests (the default stroke set's SHA-256 is Annex B's,
# Annex A's table is the expansion's, and both sets re-derive from their WSF1
# sources), the text layer's tests (the projection and the transcript of section
# 5.4 against JSON written out by hand, and the worked example read from the
# extension), and `wai-vector vectors verify`, which also regenerates the whole
# corpus and converts every payload's authoring form back to its bytes; and the
# sink's build, without the feature `author`, so with no stroker;
# - WASI (wasm32-wasip2) under wasmtime, a 32-bit target: the same tests and the same
# `vectors verify`;
# - a browser's engine (wasm32-unknown-unknown) under Node: the recomputation of
# every expected file and every authoring form, in a sink's build (an a11y
# payload's expected file states the SHA-256 of its text record, so the projection
# and the transcript are recomputed here too), and the default stroke set's
# SHA-256 against Annex B;
# - three browser engines (chromium, webkit and firefox, as Playwright 1.60.0 names
# the engines it drives), in that version's container image, pinned by digest. The
# package a site serves (the sink's exports alone, built for wasm32v1-none and read
# back as WebAssembly 1.0 by wasm/check-mvp.mjs) loads every payload to its code,
# states each listed size's cost, and renders the frames through the page's hook:
# each is hashed by the module and by the platform, and the sampled ones are sent
# to Node and hashed there. For each a11y payload the sink's transcript and its
# projection at every frame of every listed rate, written out as the text
# record's lines, must be the committed record byte for byte. The recomputation
# module then recomputes every expected file and authoring form in each engine.
# A missing engine fails the job;
# - the fuzz leg: the corpus-seeded fuzz of the WOS1 and WVG1 parsers at depth, each
# accepted payload of small cost rendered and each accepted payload's text layer
# held (its transcript is JSON, and its projections list the blocks the frame
# procedure draws), the peak-allocation test and the duplicate-detection
# comparison count;
# - the independent checker, tools/wai_vector_check.py: Python standard library only,
# written from the extension text alone and sharing no code with wai-vector, which
# recomputes every expected file byte for byte (every frame by the closed-form
# crossings in big integers), rewrites every payload and set to its own bytes, and
# recomputes every digest of sha256.json.
#
# There is no Windows leg: the Windows runner has been offline since 2026-08-28.
# One run per branch at a time. A push supersedes the run before it.
#
# There is one `macos` runner and it is the dev Mac, so every macOS job in this repo
# queues on it; only the newest commit's verdict is wanted, so older runs are
# cancelled. `cancel-in-progress: true` also applies to `main`, which advances by
# merge commits whose tree contains the commits before them.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
paths:
- 'wai/wai-vector/**'
# SHA-256 is wai-ops's, included by #[path].
- 'wai/wai-ops/src/sha256.rs'
- 'wai/vector-conformance/**'
- 'wai/tools/wai_vector_*.py'
# The text the crate and the corpus are held to.
- 'wai/extensions/graphics-vector.md'
# The licence text the in-house sets carry in their notes.
- 'LICENSE'
- '.github/workflows/wai-vector.yml'
push:
branches:
- main
paths:
- 'wai/wai-vector/**'
- 'wai/wai-ops/src/sha256.rs'
- 'wai/vector-conformance/**'
- 'wai/tools/wai_vector_*.py'
- 'wai/extensions/graphics-vector.md'
- 'LICENSE'
- '.github/workflows/wai-vector.yml'
jobs:
# Does this pull request's own diff touch the paths below? This forge matches a
# workflow's `paths:` against the push, so a merge of main into a branch fires the
# workflow whatever the pull request changes; every job here waits on this answer.
# A push to main, a schedule, or anything the script cannot determine answers
# `true` (scripts/pr-touches-paths.py).
relevant:
name: does this change touch it
runs-on: linux
outputs:
run: ${{ steps.gate.outputs.run }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- id: gate
run: |
set -euo pipefail
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-vector.yml \
"${{ github.event.pull_request.base.sha }}" \
"${{ github.event.pull_request.head.sha }}")
echo "this change touches wai-vector: $r"
echo "run=$r" >> "$GITHUB_OUTPUT"
native:
needs: relevant
if: needs.relevant.outputs.run == 'true'
name: ${{ matrix.label }}
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- { os: linux, label: linux-x86_64 }
- { os: linux-arm64, label: linux-aarch64 }
- { os: macos, label: macos-arm64 }
steps:
- uses: actions/checkout@v4
# The docker images ship no Rust; the macOS host has it, but launchd's PATH omits
# ~/.cargo/bin.
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Debug builds check every arithmetic operation for overflow, so a bound the
# text states that the code exceeds panics here.
- name: Every test, debug
working-directory: wai/wai-vector
shell: bash
run: cargo test --locked
- name: Every test, release
working-directory: wai/wai-vector
shell: bash
run: cargo test --locked --release
- name: The corpus regenerated, every expected file recomputed
working-directory: wai/wai-vector
shell: bash
run: cargo run --locked --release --features std --bin wai-vector -- vectors verify ..
# A sink has no stroker (graphics-vector.md §3.14): the library builds without
# the authoring code.
- name: The sink's build, without the feature author
working-directory: wai/wai-vector
shell: bash
run: cargo build --locked --no-default-features --lib
wasip2:
needs: relevant
if: needs.relevant.outputs.run == 'true'
name: wasm32-wasip2 (wasmtime)
runs-on: linux
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install Rust 1.98.0 with the wasm32-wasip2 target
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
rustup target add wasm32-wasip2
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# The release binary, pinned by version and SHA-256, rather than a build from source.
# Every WAI workflow that runs wasmtime pins this one version, and every run passes
# -W nan-canonicalization=y -W relaxed-simd-deterministic=y: every NaN a float
# instruction makes is the canonical one, and each relaxed SIMD instruction has one
# behaviour on every host. A stock wasmtime leaves both off. The fixed relaxed
# behaviour is the engine's own: on 50.0.1 relaxed_madd is fused (measured on arm64 and
# x86_64), where WebAssembly's deterministic profile prescribes the unfused result.
# The repository checks (scripts/check-wasmtime-pin.py) fail a workflow that pins another
# release or runs wasmtime without both settings.
- name: Install wasmtime 50.0.1
shell: bash
run: |
set -euo pipefail
[ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; }
if ! command -v xz >/dev/null 2>&1; then
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq xz-utils
fi
v=50.0.1
f="wasmtime-v$v-x86_64-linux.tar.xz"
curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \
"https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f"
echo "685466455dd42a14774a86c36c8f2e45cbb9bd6d92a850139315df60a75498a5 $RUNNER_TEMP/$f" | sha256sum -c -
tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH"
# The tests read committed files through CARGO_MANIFEST_DIR/.., so the wai
# directory is preopened at its own absolute path.
- name: Every test, under WASI
working-directory: wai/wai-vector
shell: bash
run: |
set -euo pipefail
W=$(cd .. && pwd)
CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y --dir $W::$W" \
cargo test --locked --release --target wasm32-wasip2
- name: Build the tool for wasm32-wasip2
working-directory: wai/wai-vector
shell: bash
run: cargo build --locked --release --features std --target wasm32-wasip2 --bin wai-vector
- name: The corpus regenerated, every expected file recomputed, under WASI
working-directory: wai
shell: bash
run: |
set -euo pipefail
wasmtime --version
wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y --dir .::/wai wai-vector/target/wasm32-wasip2/release/wai-vector.wasm vectors verify /wai
node:
needs: relevant
if: needs.relevant.outputs.run == 'true'
name: wasm32 under Node
runs-on: linux
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install Rust 1.98.0 with the wasm32 target
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
rustup target add wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build the recomputation for wasm32-unknown-unknown
working-directory: wai/wai-vector/wasm
shell: bash
run: cargo build --locked --release --target wasm32-unknown-unknown
- name: Every expected file and authoring form recomputed under Node
working-directory: wai/wai-vector/wasm
shell: bash
run: node verify.mjs target/wasm32-unknown-unknown/release/wai_vector_wasm.wasm ../../vector-conformance
browser:
needs: relevant
if: needs.relevant.outputs.run == 'true'
name: wasm32 in three browser engines
runs-on: linux
timeout-minutes: 45
# Playwright 1.60.0's own image: Node and the three engines that version drives,
# already installed. Pinned by the digest of its linux/amd64 manifest (the linux
# runner is x86_64), so the engines' builds cannot move under the tag.
container:
image: mcr.microsoft.com/playwright:v1.60.0-noble@sha256:83192064c7510f7ee73dd63dc5f22a5e01a92c81a2e6a9c715d9e3fe55471fd9
env:
# The image runs as root, and an engine that finds HOME owned by another user
# refuses to start.
HOME: /root
steps:
- uses: actions/checkout@v4
# The image ships no Rust. wasm32v1-none is the package's target: its
# precompiled core and alloc are WebAssembly 1.0, which those of
# wasm32-unknown-unknown are not (wasm/build-pkg.sh says why that decides it).
- name: Install Rust 1.98.0 with the wasm32v1-none and wasm32-unknown-unknown targets
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
rustup target add wasm32v1-none wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# The sink's exports alone, built and then read back: every section, type,
# segment and instruction WebAssembly 1.0's, no imports, no 64-bit integer at an
# exported function, no floating point, and the exported functions the script
# lists. The build fails otherwise.
- name: The package, built for wasm32v1-none and read back as WebAssembly 1.0
shell: bash
run: bash wai/wai-vector/wasm/build-pkg.sh
- name: Build the recomputation for wasm32-unknown-unknown
working-directory: wai/wai-vector/wasm
shell: bash
run: cargo build --locked --release --target wasm32-unknown-unknown
# The check must be able to fail. The recomputation module is the default
# target's build, whose precompiled core and alloc use bulk memory: the reader
# must refuse it, and for that reason.
- name: The reader refuses a module that is not WebAssembly 1.0
working-directory: wai/wai-vector/wasm
shell: bash
run: |
set -euo pipefail
if node check-mvp.mjs target/wasm32-unknown-unknown/release/wai_vector_wasm.wasm > "$RUNNER_TEMP/check-default.log" 2>&1; then
cat "$RUNNER_TEMP/check-default.log"
echo "::error::check-mvp.mjs passed the default target's build"
exit 1
fi
cat "$RUNNER_TEMP/check-default.log"
grep -q 'NOT 1.0: bulk-memory (memory.copy)' "$RUNNER_TEMP/check-default.log" \
|| { echo "::error::check-mvp.mjs did not report bulk memory in the default target's build"; exit 1; }
# The image's engines are the ones this Playwright drives; the package comes
# from the lockfile, with its integrity hash.
- name: Playwright 1.60.0, from the lockfile
working-directory: wai/wai-vector/browser
shell: bash
run: |
set -euo pipefail
node --version
npm ci --no-audit --no-fund
# A missing engine is a failure: run.mjs passes only when all three ran.
- name: Every payload, frame, text record and vector in chromium, webkit and firefox
working-directory: wai/wai-vector/browser
shell: bash
run: |
set -euo pipefail
node run.mjs --engines chromium,webkit,firefox \
--pkg ../wasm/dist/pkg-vector \
--recompute ../wasm/target/wasm32-unknown-unknown/release/wai_vector_wasm.wasm \
../../vector-conformance
fuzz:
needs: relevant
if: needs.relevant.outputs.run == 'true'
name: fuzz, peak allocation, comparison count
runs-on: linux
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Every corpus vector and every file under fuzz/seeds/ as a seed, 2,000
# mutations each: no input panics, and every accepted input writes back to
# its own bytes.
- name: Corpus-seeded fuzz at depth
working-directory: wai/wai-vector
shell: bash
env:
WAI_VECTOR_FUZZ_ITERS: '2000'
run: cargo test --locked --release --test fuzz_smoke -- --nocapture
- name: Peak allocation and the duplicate-detection comparison count
working-directory: wai/wai-vector
shell: bash
run: cargo test --locked --release --test peak_alloc --test canonical -- --nocapture
- name: Every workflow declares a concurrency group
shell: bash
run: bash scripts/check-workflow-concurrency.sh
checker:
needs: relevant
if: needs.relevant.outputs.run == 'true'
name: independent checker (Python)
runs-on: linux
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# Standard library only, so it needs nothing installed. Every frame of every
# rendering vector is rendered, the clock vector's 3840 x 2160 frames included.
- name: wai_vector_check.py recomputes every expected file of the corpus
working-directory: wai
shell: bash
run: python3 -I tools/wai_vector_check.py corpus vector-conformance --quiet
- name: Every workflow declares a concurrency group
shell: bash
run: bash scripts/check-workflow-concurrency.sh