wai-web: reach builds — L1, L2, J and an ES5 loader (CI verdict pending) #205

Merged
dcharlot merged 10 commits from wai/reach-builds into main 2026-10-10 23:00:19 -04:00
26 changed files with 15747 additions and 13 deletions

220
.github/workflows/wai-web-reach.yml vendored Normal file
View file

@ -0,0 +1,220 @@
name: wai-web reach builds
# The reach builds of wai-web (wai/wai-web/reach/): the shipped build, L1 (legalised at
# the JS interface), L2 (MVP-lowered and legalised) and J (the wasm2js translation),
# each run on an engine line it serves and compared with the shipped build output for
# output.
#
# On the linux runner, after a gate that reads the pull request's own diff, one leg per
# package:
#
# record the package of record (reach/record/: the two files the site serves under
# /wai/pkg/, built from the commit reach/manifest.json names). Both files must
# have the manifest's SHA-256, and build.sh runs with WAI_REACH_REQUIRE_RECORD=1,
# so every output must have the manifest's SHA-256 and size: the bytes the site
# serves under /wai/pkg/reach/. The package is kept, not rebuilt here: the same
# commit and recipe give different wasm on a Linux x86_64 host than on the
# macOS arm64 host that built it (the glue is the same).
# tree wai-web built from this tree, so a change to wai-web meets the reach builds
# before the package of record moves to it.
#
# Each leg then:
#
# 1. derives every reach build with reach/build.sh (pinned binaryen and JS tools; J's
# wasm2js is binaryen's WebAssembly build, the same on every host), which checks that
# the translation and the glue parse as ECMAScript 5 and writes each output's SHA-256
# to a manifest;
# 2. on current Node (reach/test/run-modern.mjs): the loader must choose the shipped
# build; every rung and profile, forced, must give the shipped build's outputs over
# the corpora; an engine stripped to ES5 must be given J and the same outputs; and
# the demo verify scripts must pass against shipped, L1, L2 and J;
# 3. on older engine lines, each in its own Node image (reach/test/conformance.js,
# which is ECMAScript 5): the loader must choose the rung that line needs, by
# feature probes alone, and every workload must equal the shipped build's output
# from step 2:
# node:0.12 no WebAssembly -> J
# node:8.0.0 WebAssembly without the RegExp dotAll flag -> J (full), L2 (receipt)
# node:8 MVP WebAssembly, no BigInt -> L2
# node:10 MVP WebAssembly, BigInt -> L2
# node:12 post-MVP features, no BigInt at the boundary -> L1
# node:14 post-MVP features, no BigInt at the boundary -> L1
# These are proxies: engine lines that real devices also ship (the README says
# which device classes each stands for). No real old device runs here.
#
# The images run as `docker://` steps: each mounts this job's workspace, so the build
# and the reference of steps 1-2 are read where they were written. They run no
# JavaScript action, since their Node is too old to.
#
# Push filters match the push's delta, so every path a step reads is listed.
# One run per branch at a time. A push supersedes the run before it; on main a newer
# merge's tree contains the older one's.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths:
- 'wai/wai-web/**'
- 'wai/wai-rs/**'
- 'wai/wai-quantum/**'
- 'wai/neural-conformance/**'
- 'wai/staged-conformance/**'
- 'wai/staged-measure-conformance/**'
- 'wai/registry/**'
- '.github/workflows/wai-web-reach.yml'
pull_request:
paths:
- 'wai/wai-web/**'
- 'wai/wai-rs/**'
- 'wai/wai-quantum/**'
- 'wai/neural-conformance/**'
- 'wai/staged-conformance/**'
- 'wai/staged-measure-conformance/**'
- 'wai/registry/**'
- '.github/workflows/wai-web-reach.yml'
env:
REACH: wai/wai-web/reach
jobs:
# Forgejo matches `paths:` against the push delta, so a rebase fires this workflow
# whatever the pull request changes. This job reads the pull request's own diff
# against the paths above (scripts/pr-touches-paths.py, which fails open) and the
# build runs only when it touches them. On a push to main it answers `true`.
relevant:
name: wai-web reach · 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-web-reach.yml \
"${{ github.event.pull_request.base.sha }}" \
"${{ github.event.pull_request.head.sha }}")
echo "this change touches the reach builds: $r"
echo "run=$r" >> "$GITHUB_OUTPUT"
reach:
name: wai-web reach builds · ${{ matrix.name }}
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- leg: record
name: the package of record, every output as recorded
- leg: tree
name: this tree's wai-web, every rung on its engine line
env:
DIST: wai/wai-web/reach/ci-dist-${{ matrix.leg }}
steps:
- uses: actions/checkout@v5
# The docker runner image ships no Rust.
- name: Install Rust 1.98.0 with the wasm32 target
if: matrix.leg == 'tree'
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 --toolchain 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Pinned to the wasm-bindgen version in wai-web/Cargo.lock: the CLI and the crate
# must match exactly.
- name: wasm-bindgen CLI 0.2.127
if: matrix.leg == 'tree'
shell: bash
run: cargo install --locked wasm-bindgen-cli --version 0.2.127
- uses: actions/setup-node@v4
with:
node-version: '26'
# The site's package is wasm-pack's web target with wasm-opt off
# ([package.metadata.wasm-pack.profile.release]): cargo, then wasm-bindgen. It goes
# outside the tree, where no package.json decides how Node reads the glue.
- name: Build wai-web for wasm32 from this tree (web target)
if: matrix.leg == 'tree'
shell: bash
working-directory: wai/wai-web
run: |
set -euo pipefail
cargo build --locked --release --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir "$RUNNER_TEMP/pkg" --out-name wai_web \
target/wasm32-unknown-unknown/release/wai_web.wasm
# The package of record leaves the tree for the same reason, after its check.
- name: The package of record (reach/record/), held to the manifest
if: matrix.leg == 'record'
shell: bash
run: |
set -euo pipefail
node -e '
const fs = require("fs"), path = require("path"), crypto = require("crypto");
const [man, pkg] = process.argv.slice(1);
const m = JSON.parse(fs.readFileSync(man, "utf8"));
console.log(`the package of record: ${m.input.source}`);
let bad = 0;
for (const [n, e] of Object.entries(m.input.files)) {
const b = fs.readFileSync(path.join(pkg, n));
const got = crypto.createHash("sha256").update(b).digest("hex");
console.log(`${got} ${n} (${b.length} B); the manifest records ${e.sha256} (${e.bytes} B)`);
if (got !== e.sha256 || b.length !== e.bytes) bad++;
}
if (bad) { console.error(`reach: reach/record/ is not the package the manifest names (${bad} file(s) differ)`); process.exit(1); }' \
"$REACH/manifest.json" "$REACH/record"
mkdir -p "$RUNNER_TEMP/pkg"
cp "$REACH/record/wai_web_bg.wasm" "$REACH/record/wai_web.js" "$RUNNER_TEMP/pkg/"
# The build and the reference stay in the workspace: the engine images below
# mount it, and nothing else.
- name: Reach builds (reach/build.sh, pinned binaryen and JS tools)
shell: bash
run: |
set -euo pipefail
if [ "${{ matrix.leg }}" = record ]; then export WAI_REACH_REQUIRE_RECORD=1; fi
WAI_REACH_TOOLS="$RUNNER_TEMP/reach-tools" bash "$REACH/build.sh" "$RUNNER_TEMP/pkg" "$DIST"
cat "$DIST/manifest.json"
- name: Current Node — every rung forced, the demo verify scripts on every full build
shell: bash
run: node "$REACH/test/run-modern.mjs" --dist "$DIST" --shipped "$RUNNER_TEMP/pkg" --out "$REACH/ci-modern-${{ matrix.leg }}"
- name: node:0.12 (no WebAssembly) — J chosen and equal to the shipped build
uses: docker://node:0.12
with:
entrypoint: /bin/sh
args: -c "set -e; node --version; for p in full verify_decode verify receipt; do node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile $p --expect-rung J --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; done"
- name: node:8.0.0 (WebAssembly, no RegExp dotAll) — J for the full build, the L2 receipt build
uses: docker://node:8.0.0
with:
entrypoint: /bin/sh
args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung J --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile receipt --expect-rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json"
- name: node:8 (MVP WebAssembly, no BigInt) — L2 chosen and equal to the shipped build
uses: docker://node:8
with:
entrypoint: /bin/sh
args: -c "set -e; node --version; for p in full verify_decode verify receipt; do node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile $p --expect-rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; done; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --rung J --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json"
- name: node:10 (MVP WebAssembly, BigInt) — L2 chosen and equal to the shipped build
uses: docker://node:10
with:
entrypoint: /bin/sh
args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json"
- name: node:12 (post-MVP WebAssembly, no BigInt at the boundary) — L1 chosen and equal to the shipped build
uses: docker://node:12
with:
entrypoint: /bin/sh
args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --rung L2 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json"
- name: node:14 (post-MVP WebAssembly, no BigInt at the boundary) — L1 chosen and equal to the shipped build
uses: docker://node:14
with:
entrypoint: /bin/sh
args: -c "set -e; node --version; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist-${{ matrix.leg }} --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern-${{ matrix.leg }}/reference.json"

5
wai/.gitignore vendored
View file

@ -57,3 +57,8 @@ wai-web/demo/int_hyper/
# Reference codec tools built by tools/build_reference_decoders.sh (sink decoders, fetched out-of-band).
/.reference-tools/
# The reach build's pinned tools and its output (wai-web/reach/build.sh).
wai-web/reach/.tools/
wai-web/reach/dist/
wai-web/reach/ci-dist*/
wai-web/reach/ci-modern*/

View file

@ -6,6 +6,62 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### wai-web on older engines: the reach builds
- **Three builds from the shipped package, and a loader** ([wai-web/reach](wai-web/reach/README.md)).
`reach/build.sh` derives them from `wai_web_bg.wasm` and `wai_web.js` with no change to
the Rust:
- **L1** legalises the binary's JS interface (`wasm-opt --legalize-js-interface`), so
engines where i64 does not cross as `BigInt` run the full module;
- **L2** lowers it to MVP WebAssembly and legalises it;
- **J** translates the MVP module to ES5 JavaScript with `wasm2js`.
L2 and J come in profiles, kept by `wasm-metadce` from the exports each needs: `full`,
`verify_decode`, `verify` and `receipt`. An ES5 runtime supplies the missing builtins
and joins and splits i64 values at the boundary: a `BigInt` where the engine has one, a
`Number` (exact below 2^53) where it does not. The glue is the shipped glue compiled to
ES5, with counted changes. One of them rebuilds a memory view whenever the memory's
buffer changes, not only when it is detached.
- **`wai-load.js` chooses by feature probes alone.** Tiny modules passed to
`WebAssembly.validate` (MVP, sign-extension, non-trapping float-to-int, bulk memory), one
instantiated module that returns its i64 argument, and checks for typed arrays decide
between shipped, L1, L2 and J. The probes never throw. An early WebAssembly engine line
(WebAssembly and the stack-trace capture API, but no RegExp `dotAll` flag: the feature
`noDotAll`) gets J, or the L2 receipt build, because the full MVP module has crashed an
engine of that line on arm64. A rung can be forced, and the loader says why it chose.
- **Pinned and recorded.** binaryen `version_123` is checked against each platform
archive's SHA-256, and TypeScript, esbuild, acorn and binaryen's WebAssembly build are
pinned by `package-lock.json`; J's `wasm2js` is that WebAssembly build, so J comes out
the same on every host.
`reach/manifest.json` records the SHA-256 and sizes of every output built from the
package the site serves, and the commit that package was built from; `reach/record/`
keeps that package. Given it, the build refuses any output that differs.
- **Checked output for output.**
- `reach/test/conformance.js` (ES5) runs workloads over the repository's corpora on any
rung, and every output must equal the shipped build's. The workloads cover receipts,
the staged corpus, the integer payloads, int_synth, the menu, the output tree and the
i64 boundary.
- `reach/test/run-modern.mjs` forces every rung and profile on a current Node, and runs
the demo verify scripts against each full build.
- CI (`wai-web-reach.yml`) runs a leg per package. On the package of record, kept in
`reach/record/` and held to the manifest's SHA-256, every output must equal the
manifest's; on the tree's own wai-web, the reach builds must still build and pass.
Each leg runs the conformance on the Node images of the engine lines each rung serves
(J on `node:0.12`, the early-engine routing on `node:8.0.0`, L2 on `node:8` and
`node:10`, L1 on `node:12` and `node:14`) and the shipped build on a current Node.
The images stand in for device classes, which the README names by capability. No real
old device has been run.
- **The receipt vector's signer id is a label.** The receipt workloads verify an asset and
its provenance receipt in `reach/test/vectors/provenance/`, signed with `wai_prov`'s
published test seed under its label `did:example:wai-prov`. `wai_prov --carry
<receipt.json>` measures nothing: it carries each step's figure, and its label, from an
earlier receipt for the same lineage and signs them under this revision's id, and it
refuses a receipt that does not verify or whose steps are not this run's. The vector
carries the figures `wai_prov` measured when it first wrote the asset, and the
documented command, carrying the receipt onto itself, writes both files again byte for
byte.
### Generated pictures and scores, made natively and checked by SHA-256
- **The payload of `wai.generate.spectral`** (SPEC §5, *Payload of

View file

@ -80,6 +80,31 @@ into:
- `wai_envelope_{pack,unpack}` — wrap/unwrap the WAI container
- `wai_buffer_free`, `wai_last_error` — memory + error handling
## In the browser, on older engines: the reach builds
`wai-web` is the same Rust compiled to wasm32 for a page. Its shipped package needs
WebAssembly with sign-extension, non-trapping float-to-int and bulk memory, and i64
values crossing the JS boundary as `BigInt`. The reach builds
([`wai-web/reach/`](wai-web/reach/README.md)) run that code where those are missing, and
give the same outputs byte for byte:
| rung | build | for engines with |
|---|---|---|
| shipped | the package | every feature above |
| L1 | the shipped binary, legalised at the JS interface | the same WebAssembly, no `BigInt` at the boundary |
| L2 | MVP-lowered and legalised | MVP WebAssembly |
| J | the wasm2js translation, ES5 | ES5 and typed arrays, no WebAssembly |
L2 and J also come in reduced profiles: `receipt`, `verify` and `verify_decode`.
`wai-load.js`, an ES5 loader, picks the rung from feature probes alone and says why. A
rung can be forced for testing. `reach/build.sh` makes every build with pinned tools
and records each output's SHA-256 in `reach/manifest.json`.
CI runs each rung on a Node image of the engine line it serves, and every output must
equal the shipped build's; the reach builds made from the package of record (kept in
`reach/record/`) must equal `reach/manifest.json`. The images stand in for device classes;
no real old device has been run.
## Registered capabilities (the zeroth menu)
| Capability | Codec | Lossless? | Mandatory? |

View file

@ -85,7 +85,10 @@ and its figure is unlabelled. `wai_prov` labels each encode it measures
`OnChipCounter`, with its declared uncertainty, and records an encode whose
measurement fails the §4 ratio or the power reader's liveness (§3.1) as
unmetered, and writes a measurement report for each step it measured
(energy-measurement §6). `total_joules_micro` carries no class of its own, and a
(energy-measurement §6). With `--carry` it measures nothing: it carries each
figure, and its label, from an earlier receipt for the same lineage, which must
verify and whose steps must have the same operations, hashes and work, and writes
no report. `total_joules_micro` carries no class of its own, and a
total over steps that include an unmetered one covers only the measured steps
(`Partial`).

View file

@ -9,7 +9,7 @@
//! unmetered: the step's `joules_micro` carries the unmetered marker `0`, with
//! no label, never a figure of zero joules.
//!
//! wai_prov <out_dir> [--no-report] [--previous-report <file>]
//! wai_prov <out_dir> [--no-report] [--previous-report <file>] [--carry <receipt.json>]
//!
//! Writes `<out_dir>/captured.jpeg.wai` (the content-addressed asset),
//! `<out_dir>/captured.receipt.json` (the signed provenance), and for each
@ -17,7 +17,18 @@
//! report (energy-measurement §6), a `measurement` claim signed with the
//! receipt's key naming the step by its index, the first chained to
//! `--previous-report` when it is given and the second to the first.
//! `--no-report` turns the reports off, and says so. The asset is
//! `--no-report` turns the reports off, and says so.
//!
//! `--carry <receipt.json>` measures nothing: each step's figure, and its label
//! if it has one, is carried as an earlier receipt for this same lineage states
//! it, and signed under this revision's signer id. That receipt must verify, and
//! its steps must be this run's — the same operations, input and output hashes
//! and work — so a figure is only ever carried onto the bytes it was measured
//! on. No report is written: a report restates a bracket, and this run took
//! none. Nothing in such a run varies, so a committed asset and receipt
//! (`wai-web/reach/test/vectors/provenance/`) are written again byte for byte
//! when the receipt is carried onto itself; a host whose synthesis or encoders
//! give other bytes has another lineage, and is refused. The asset is
//! synthesized deterministically here — WAI is asset-agnostic; the lineage
//! mechanism is identical whether the origin is a camera or a synthesizer,
//! and that is stated plainly. The point is provenance for bytes WAI
@ -38,9 +49,12 @@ const W: u32 = 512;
const H: u32 = 512;
fn main() {
let report_args = meter_report::parse_args_or_exit(&std::env::args().collect::<Vec<_>>());
let mut argv: Vec<String> = std::env::args().collect();
let carry = take_carry(&mut argv);
let report_args = meter_report::parse_args_or_exit(&argv);
let args = &report_args.positional;
let dir = Path::new(args.get(1).map(String::as_str).unwrap_or("."));
let carried = carry.as_deref().map(read_carried);
std::fs::create_dir_all(dir).expect("mkdir");
// A deterministic synthesized frame (radial rings + gradient). Real
@ -49,17 +63,23 @@ fn main() {
let pixels = (W * H) as u64;
let raw_hash = blake3::hash(&rgb).into();
// step 1: lossless PNG master — measured.
let (png_m, png_v) = measure("encode.png", &rgb, |r| {
let _ = std::hint::black_box(png_encode(r, H, W));
});
// step 1: lossless PNG master — measured, unless the figures are carried.
let (png_m, png_v) = match carried {
Some(_) => (None, None),
None => measure("encode.png", &rgb, |r| {
let _ = std::hint::black_box(png_encode(r, H, W));
}),
};
let png = png_encode(&rgb, H, W).expect("png");
let png_hash: [u8; 32] = blake3::hash(&png).into();
// step 2: published JPEG (from the master pixels) — measured.
let (jpeg_m, jpeg_v) = measure("encode.jpeg", &rgb, |r| {
let _ = std::hint::black_box(jpeg_encode(r, H, W, 90));
});
// step 2: published JPEG (from the master pixels) — measured, unless carried.
let (jpeg_m, jpeg_v) = match carried {
Some(_) => (None, None),
None => measure("encode.jpeg", &rgb, |r| {
let _ = std::hint::black_box(jpeg_encode(r, H, W, 90));
}),
};
let jpeg = jpeg_encode(&rgb, H, W, 90).expect("jpeg");
// Wrap the JPEG as a content-addressed wai.image.jpeg envelope.
@ -77,11 +97,14 @@ fn main() {
None => s,
}
};
let steps = vec![
let mut steps = vec![
ProvStep::new("origin.synthesize", [0u8; 32], raw_hash, pixels, 0),
step("encode.png", raw_hash, png_hash, png_m.clone()),
step("encode.jpeg", png_hash, asset_hash, jpeg_m.clone()),
];
if let (Some(from), Some(path)) = (&carried, &carry) {
steps = carry_figures(path, from, steps, asset_hash);
}
let signer = SigningKey::from_bytes(&[0x70u8; 32]); // 'p' for prov
// A demo signer whose seed is published: a label, never origin-bound (jwp-receipts §7.1, §7.8).
@ -106,6 +129,16 @@ fn main() {
// The origin step is never metered, so the total covers the measured steps
// only, and an all-unmetered lineage is UNMETERED, never 0 µJ.
println!("captured.receipt.json : {} steps, total {}, receipt {}…", receipt.steps.len(), receipt.total_label(), &hex(&receipt.receipt_hash())[..16]);
if let Some(path) = &carry {
let shown = |s: &ProvStep| match (s.joules_micro, &s.energy_provenance) {
(0, _) => "UNMETERED".to_string(),
(uj, Some(c)) => format!("{uj} µJ ({})", c.label()),
(uj, None) => format!("{uj} µJ (unlabelled)"),
};
println!(" encode.png : {} encode.jpeg : {} (carried from {path})", shown(&receipt.steps[1]), shown(&receipt.steps[2]));
println!("report : not written — the figures are carried, and this run took no bracket for a report to restate");
return;
}
let shown = |m: &Option<(u64, EnergyProvenance)>| match m {
Some((uj, c)) => format!("{uj} µJ ({})", c.label()),
None => "UNMETERED".to_string(),
@ -127,6 +160,54 @@ fn main() {
}
}
/// Take `--carry <receipt.json>` out of `argv`. `--carry` with no file after it, or
/// given twice, is a usage error (exit status 2), never a measured run.
fn take_carry(argv: &mut Vec<String>) -> Option<String> {
let mut found = None;
while let Some(i) = argv.iter().position(|a| a == "--carry") {
if found.is_some() || !argv.get(i + 1).is_some_and(|p| !p.starts_with("--")) {
eprintln!("usage error: --carry takes one earlier receipt's file, once");
std::process::exit(2);
}
found = Some(argv.remove(i + 1));
argv.remove(i);
}
found
}
/// The receipt `--carry` names. It must parse and verify.
fn read_carried(path: &str) -> AssetReceipt {
let json = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let r = AssetReceipt::from_json(&json).unwrap_or_else(|| panic!("{path}: not an asset-provenance receipt"));
assert!(r.verify(), "{path}: the receipt to carry from does not verify");
r
}
/// This run's steps, each with the figure and the label `from` states for it.
/// Refused (exit status 1) unless `from` is a receipt for this lineage: the same
/// asset, media, capability and group, no parent, and step for step the same
/// operation, input and output hashes and work.
fn carry_figures(path: &str, from: &AssetReceipt, steps: Vec<ProvStep>, asset_hash: [u8; 32]) -> Vec<ProvStep> {
let same_asset = from.asset_hash == asset_hash
&& from.media == "image"
&& from.capability == "wai.image.jpeg"
&& from.group_id == 0
&& from.parent_receipt_hash.is_none();
let same_steps = from.steps.len() == steps.len()
&& from.steps.iter().zip(&steps).all(|(c, s)| {
c.op == s.op && c.input_hash == s.input_hash && c.output_hash == s.output_hash && c.work == s.work
});
if !same_asset || !same_steps {
eprintln!("{path}: not a receipt for this lineage (its asset, or its steps' operations, hashes or work, differ from this run's); nothing carried");
std::process::exit(1);
}
steps
.into_iter()
.zip(&from.steps)
.map(|(s, c)| ProvStep { joules_micro: c.joules_micro, energy_provenance: c.energy_provenance.clone(), ..s })
.collect()
}
/// Deterministic 512×512 RGB: radial rings over a diagonal gradient.
fn synth_rgb() -> Vec<u8> {
let mut out = Vec::with_capacity((W * H * 3) as usize);

222
wai/wai-web/reach/README.md Normal file
View file

@ -0,0 +1,222 @@
# wai-web reach builds
The shipped wai-web package (`wai_web.js` and `wai_web_bg.wasm`, the wasm-bindgen web
target) runs where an engine has WebAssembly with sign-extension, non-trapping
float-to-int conversion and bulk memory, where a 64-bit integer crosses the JS boundary as
a `BigInt`, and where ES modules load. The reach builds run the same code on engines
without those features, and give the same outputs byte for byte. Nothing in the Rust
changes: every reach build is derived from the shipped package itself.
| rung | files | the engine needs | the CI proxies it runs on |
|---|---|---|---|
| shipped | `../wai_web.js`, `../wai_web_bg.wasm` (the package) | WebAssembly with sign-extension, non-trapping float-to-int and bulk memory; i64 as `BigInt`; ES modules | a current Node |
| **L1** | `wai_web_bg.l1.wasm` + `wai_web.reach.js` | the same WebAssembly features; no `BigInt` at the boundary | `node:12`, `node:14` |
| **L2** | `wai_web_bg.l2.<profile>.wasm` + `wai_web.reach.js` | MVP WebAssembly | `node:8.0.0` (receipt), `node:8`, `node:10` |
| **J** | `wai_web.j.<profile>.js` | ES5 and typed arrays; no WebAssembly | `node:0.12`, `node:8.0.0` (full), and a current Node stripped to ES5 |
- **L1** is the shipped binary with its JS interface legalised (`wasm-opt
--legalize-js-interface`): an i64 parameter becomes a pair of i32 halves, and an i64
result returns its low half and passes the high half through `env.setTempRet0`. Nothing
else changes, so the code runs at the shipped build's speed.
- **L2** is the shipped binary lowered to the MVP (`--signext-lowering`,
`--llvm-nontrapping-fptoint-lowering`, `--llvm-memory-copy-fill-lowering`) and then
legalised. Lowered without legalisation it would still fail every export with an i64 in
its signature, so it is never shipped alone.
- **J** is the MVP-lowered module translated to JavaScript by `wasm2js`, in one ES5
script per profile: the runtime, the i64 table, the translation and the glue.
`wai_web.reach.js` and every J script carry the same ES5 parts:
- **The runtime** (`src/wai-reach-runtime.js`) installs what an old engine lacks and the
glue or the translation uses (`Math.imul` / `fround` / `clz32` / `trunc`,
`TypedArray#slice`, `TextEncoder` / `TextDecoder`, `Array#fill`, a small `Map`, a
`Symbol.iterator` stand-in, and a few more), each only where it is missing. It also joins
and splits i64 values at the boundary, from the table the build extracts from the binary.
With `BigInt` a 64-bit value is a `BigInt`, as the shipped build gives it. Without
`BigInt` it is a `Number`, exact below 2^53.
- **The glue** is the shipped `wai_web.js`, changed in a fixed, counted set of places
(`tools/cli.mjs`, `glue`; `manifest.json` records the counts) and compiled to ES5:
- its WebAssembly loading is replaced by two hooks the loader calls;
- a cached memory view is rebuilt whenever `wasm.memory.buffer` is a different buffer.
The shipped glue rebuilt one only when its buffer was detached, which a WebAssembly
memory does when it grows and the translation's memory does not;
- `Symbol.dispose` is read only where `Symbol` exists;
- the 64-bit helpers take the `Number` path without `BigInt`;
- copies out of wasm memory no longer use `TypedArray#slice`, which early engines
implement as a view.
## Profiles
L2 and J come in profiles. `profiles.json` lists the exports each one keeps, and
`wasm-metadce` drops what they cannot reach:
| profile | what it keeps |
|---|---|
| `full` | everything |
| `verify_decode` | `verify`, plus the integer decoders (WIV1, WIH1, WIA1, WIS1, int_synth, the JSON integer forms) and the staged-delivery sink |
| `verify` | `receipt`, plus multi-rendition envelopes, pins, the capability registry, stage identities, session claims and the output tree |
| `receipt` | envelope parsing, asset provenance (content hash, Ed25519 signature, Merkle chain) and the hashes |
A page that calls a function outside its profile gets a `TypeError`. The shipped build and
L1 are always the full module.
## The loader
`wai-load.js` is ES5. It chooses a rung from feature probes alone, never from the user
agent, and its detection never throws:
1. no typed arrays: none, and the reason says to serve the classical floor renditions;
2. no WebAssembly, or a WebAssembly that refuses a minimal MVP module: **J**;
3. an early WebAssembly engine line (WebAssembly and `Error.captureStackTrace` present, no
RegExp `dotAll` flag; the feature `noDotAll`): **J**, or the L2 receipt build when the
page asks for the receipt profile. The full MVP module has crashed an engine of this
line on arm64;
4. a probe module for sign-extension, non-trapping float-to-int or bulk memory (with a
data-count section) fails `WebAssembly.validate`: **L2**;
5. a one-function module returning its i64 argument does not take and give a `BigInt`:
**L1**;
6. otherwise: **shipped**.
```html
<script src="/wai/pkg/reach/wai-load.js"></script>
<script>
WaiLoad.load({ profile: 'verify' }, function (err, r) {
if (err) { /* r.reason says why; serve the classical floor */ return; }
// r.api is wai-web's API; r.rung is 'shipped' | 'L1' | 'L2' | 'J'; r.reason says why.
var p = new r.api.AssetProvenance(envelopeBytes, receiptJson);
});
</script>
```
| option | meaning |
|---|---|
| `base` | the folder of the reach files, ending in `/`; by default the folder `wai-load.js` came from |
| `profile` | `full` (default), `verify_decode`, `verify` or `receipt`; used by L2 and J |
| `force` | `shipped`, `L1`, `L2` or `J`: load that rung whatever the probes say (testing). The global `WAI_REACH_FORCE` does the same |
| `shipped` | `{ glue, wasm }` URLs of the shipped package; by default `base + '../wai_web.js'` and `base + '../wai_web_bg.wasm'` |
| `host` | `{ loadScript, fetchBytes, importModule, streams }`, to load from somewhere other than the page. With `streams` (a browser's default), the shipped glue gets the wasm URL and compiles it while it downloads |
`WaiLoad.detect(opts)` returns `{ rung, profile, reason, features }` without loading
anything. After a load, `WaiLoad.state` holds what the callback received.
## Building
```
bash wai/wai-web/reach/build.sh <package-dir> [<dist-dir>]
```
`<package-dir>` holds `wai_web.js` and `wai_web_bg.wasm`. The output goes to
`<dist-dir>` (default `reach/dist/`, which git ignores) with a `manifest.json`: every
file's SHA-256 and its raw, gzip and brotli sizes, the input's SHA-256, the tool pins, and
the counts of the glue changes.
The tools are pinned:
- binaryen at release `version_123`, by the SHA-256 of each platform's archive, checked
before use. The platforms are macOS arm64 and x86_64, and Linux x86_64 and aarch64;
- TypeScript, esbuild, acorn and binaryen's WebAssembly build of the same release by
`package-lock.json`. `npm ci` checks their integrity hashes. J's `wasm2js` is that
WebAssembly build, run on Node: the x86_64 Linux archive's native `wasm2js` allocates the
translation's locals differently from the macOS archive's, while the WebAssembly build
gives the same output on every host (and the macOS archive's output). Every other step
gives the same bytes from every archive.
The build refuses:
- an input that uses any feature beyond the shipped build's;
- a reduced module that exports anything other than its profile;
- an L2 module whose i64 boundary differs from the shipped binary's;
- a glue whose layout it does not recognise;
- a script that does not parse as ECMAScript 5.
`manifest.json` in this folder is the build of the shipped package of record. Its `input`
says which package: the SHA-256 of both files, the commit they were built from, and how.
Given that package, `build.sh` refuses any output whose SHA-256 differs from the
manifest's; with `WAI_REACH_REQUIRE_RECORD=1` it also refuses any other package.
`WAI_REACH_RECORD=1` writes a new manifest.
## Conformance
Every rung must give the shipped build's outputs. The runners below check this:
- **`test/conformance.js`** (ES5, Node 0.12 onward) loads a rung through `wai-load.js` and
runs `test/workloads.js` over the repository's corpora. Each workload is checked against
an independent reference (a corpus's hashes, a sidecar's SHA-256, the receipt's signed
fields, the registry), and its output must equal the shipped build's byte for byte. The
workloads, by profile:
- receipt: the hashes; envelope parsing; asset provenance over the vector in
`test/vectors/provenance/` (its README says how it is made), including a tampered
copy and a forged joule total;
- verify: the capability menu against the registry; the staged corpus's content hashes;
multi-rendition envelopes; the output tree of the staged-measurement goldens;
- verify_decode: every committed WIV1, WIH1, WIA1 and WIS1 payload against its sidecar;
the JSON integer forms; the committed int_synth vectors; all of `staged-conformance/`
replayed through `StageSink`;
- full: i64 parameters and results on both sides of 2^32.
Under J, `WebAssembly` is deleted before anything loads. `--strip es5` also deletes
`BigInt`, `Map`, `Symbol`, `Promise` and the other post-ES5 builtins, and makes
`TypedArray#slice` return a view.
- **`test/run-modern.mjs`** runs on a current Node:
- the loader must choose the shipped build, and that run is the reference;
- every rung and profile is forced and must equal it;
- an engine stripped to ES5 must be given J, and the same outputs;
- the demo verify scripts (`demo/verify_*.mjs`: integer payloads, int_synth, the staged
corpus with its guards and fuzz, the menu, the writers, the staged measurement) run
against shipped, L1, L2 and J through a generated adapter module. The measurement
script's written files must equal the shipped build's.
```
node wai/wai-web/reach/test/run-modern.mjs --dist <dist> --shipped <package-dir>
node wai/wai-web/reach/test/conformance.js --dist <dist> --wai wai --rung J --profile receipt --reference <reference.json>
```
CI (`.github/workflows/wai-web-reach.yml`, on the `linux` runner) runs a leg per package:
- **The package of record.** `record/` keeps the two files the site serves under
`/wai/pkg/`, built from the commit `manifest.json` names (`input.commit`; `input.source`
says how). Both files must have the SHA-256 the manifest records, and `build.sh` then
runs with `WAI_REACH_REQUIRE_RECORD=1`, so every output must have the manifest's SHA-256
and size: these are the bytes the site serves under `/wai/pkg/reach/`. The package is
kept rather than rebuilt in CI: the same commit and recipe give a different wasm on a
Linux x86_64 host than on the macOS arm64 host that built it (the glue is the same), so
the commit's build is checked on that host (the site's `build-wasm-pkg.sh recorded wai`).
- **The tree.** wai-web is built from the pull request's own tree, so a change to wai-web
meets the reach builds before the package of record moves to it. These outputs are not
the recorded ones.
On each leg:
1. `run-modern.mjs` runs on the outputs;
2. `conformance.js` runs in one Node image per engine line. On each line the loader must
choose that line's rung by itself, and every output must equal the shipped build's
from step 1.
### What each proxy stands for
The images are proxies. Each runs one JavaScript engine line on x86_64 Linux, with the
engine features below; real devices ship engines of the same lines. **No real old device
has been run.** Real devices also differ in ways no proxy reproduces: 32-bit ARM code
generation, engines with the JIT switched off, small memories, and engine families other
than the proxied one.
| proxy | engine features | rung the loader must choose | device classes it stands for |
|---|---|---|---|
| `node:0.12` | ES5 and typed arrays; no WebAssembly | J | TV sets and boxes whose web engine, or system web view, is frozen at an engine with no WebAssembly, smart TVs of model years 2014–2019 among them; phones whose system web view predates WebAssembly; broadcast-application receivers (HbbTV 2.0.x, ATSC 3.0 A/344). Console browsers that run with the JIT off, and engine families other than the proxied one, are J devices that this proxy does not cover |
| `node:8.0.0` | MVP WebAssembly; no RegExp `dotAll` flag | J, or L2 for the receipt profile | TV sets and boxes whose system web view is frozen at an engine with WebAssembly but no RegExp `dotAll` flag. The full MVP module has crashed an engine of this line on arm64; this x86_64 proxy does not crash, so it checks the choice and the outputs, not the crash |
| `node:8` | MVP WebAssembly and the `dotAll` flag; no `BigInt` | L2 | TV sets and boxes whose system web view is frozen at MVP WebAssembly with no `BigInt`, and TV platforms of model years 2019–2020 |
| `node:10` | MVP WebAssembly and `BigInt`; no sign-extension or bulk memory | L2 | TV sets and boxes whose system web view has `BigInt` but not the post-MVP features the shipped build uses |
| `node:12` | the shipped build's WebAssembly features; i64 does not cross the JS boundary as `BigInt` | L1 | TV platforms of model year 2021 |
| `node:14` | the same | L1 | TV platforms and streaming sticks whose engine has the shipped build's WebAssembly features but passes no i64 as `BigInt`, and phones' system web views of the same line |
| a current Node | every feature, with i64 as `BigInt` | shipped | current engines. Engine families other than the proxied one are not proxied here |
## Limits
- J is slower than WebAssembly, and far slower on engines without a JIT. The `receipt`
and `verify` profiles are the ones for small or slow devices.
- Without `BigInt`, a 64-bit value is a `Number`, exact below 2^53. `IntPayloadCost.macs`
(a 128-bit value) and the functions that return a `BigInt64Array` still need `BigInt`.
- A trap in the WebAssembly builds is an `Error('abort')` in J. The translation does not
check memory accesses out of bounds, which trap in WebAssembly. The Rust does not make
such accesses, and the conformance runs compare every output.
- Reduced profiles throw a `TypeError` for any function outside them.

207
wai/wai-web/reach/build.sh Normal file
View file

@ -0,0 +1,207 @@
#!/usr/bin/env bash
# The reach builds of wai-web, from one wai-web package (wai_web.js + wai_web_bg.wasm,
# the wasm-bindgen web target).
#
# bash wai/wai-web/reach/build.sh <package-dir> [<dist-dir>]
#
# Writes into <dist-dir> (default wai/wai-web/reach/dist, which git ignores):
#
# wai_web_bg.l1.wasm L1 the shipped binary legalised at the JS interface
# wai_web_bg.l2.<profile>.wasm L2 MVP-lowered and legalised; full, verify_decode,
# verify and receipt
# wai_web.reach.js ES5 runtime + i64 table + glue, for L1 and L2
# wai_web.j.<profile>.js J wasm2js translation in one ES5 script per profile
# wai-load.js the loader
# manifest.json every file's SHA-256 and sizes, the input's SHA-256,
# the tool pins and the glue changes
#
# Reproducibility. The tools are pinned: binaryen by release and by the SHA-256 of each
# platform's archive (checked before use), and TypeScript, esbuild, acorn and binaryen's
# own WebAssembly build by package-lock.json (npm ci checks their integrity hashes). J's
# wasm2js is that WebAssembly build, run on Node: the native wasm2js of the x86_64 Linux
# archive gives the translation's locals a different allocation than the macOS archive
# does, while the WebAssembly build gives the same output on every host (and equals the
# macOS archive's). The same package gives the same bytes on every platform below. When the package is the one reach/manifest.json
# names as its input, the build is refused unless every output's SHA-256 equals the
# manifest's; WAI_REACH_RECORD=1 writes the new manifest there instead.
#
# Environment:
# WAI_REACH_TOOLS where binaryen and the JS tools are installed (default
# wai/wai-web/reach/.tools, which git ignores)
# WAI_REACH_RECORD 1: write <dist-dir>/manifest.json to reach/manifest.json
# WAI_REACH_REQUIRE_RECORD
# 1: refuse unless the package is the input reach/manifest.json names,
# so the outputs are always checked against the manifest (CI's leg on
# the package of record)
# WAI_REACH_SOURCE a line saying where the package came from, kept in the manifest
# WAI_REACH_INPUT_COMMIT
# the open-standards commit the package was built from, kept in the
# manifest as input.commit
#
# Needs bash, curl, tar, node >= 18 and npm. Platforms: macOS arm64 and x86_64, Linux
# x86_64 and aarch64.
set -euo pipefail
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
echo "usage: bash build.sh <package-dir> [<dist-dir>]" >&2
exit 2
fi
REACH="$(cd "$(dirname "$0")" && pwd -P)"
PKG="$(cd "$1" && pwd -P)"
DIST="${2:-$REACH/dist}"
mkdir -p "$DIST"
DIST="$(cd "$DIST" && pwd -P)"
TOOLS="${WAI_REACH_TOOLS:-$REACH/.tools}"
mkdir -p "$TOOLS"
TOOLS="$(cd "$TOOLS" && pwd -P)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
for f in wai_web_bg.wasm wai_web.js; do
[ -f "$PKG/$f" ] || { echo "reach: $PKG has no $f" >&2; exit 1; }
done
for t in curl tar node npm; do
command -v "$t" >/dev/null || { echo "reach: needs $t" >&2; exit 1; }
done
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
[ "$NODE_MAJOR" -ge 18 ] || { echo "reach: the build needs node >= 18 (this is $(node --version))" >&2; exit 1; }
sha256() { if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi; }
# ---- binaryen, pinned -------------------------------------------------------------
BINARYEN=version_123
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) PLAT=arm64-macos; BIN_SHA=74428be348c1a09863e7b642a1fa948cabf8ec9561052233d8288e941951725b ;;
Darwin-x86_64) PLAT=x86_64-macos; BIN_SHA=cc18b14d2b673d9c66bf54f31ff2b0ceb23ba5132455b893965ae2792f9e00dd ;;
Linux-x86_64) PLAT=x86_64-linux; BIN_SHA=e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe ;;
Linux-aarch64) PLAT=aarch64-linux; BIN_SHA=4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376 ;;
*) echo "reach: no pinned binaryen archive for $(uname -s)-$(uname -m)" >&2; exit 1 ;;
esac
ARCHIVE="binaryen-$BINARYEN-$PLAT.tar.gz"
B="$TOOLS/binaryen-$BINARYEN-$PLAT/bin"
if [ ! -x "$B/wasm-opt" ] || [ "$(cat "$TOOLS/binaryen-$BINARYEN-$PLAT/.sha256" 2>/dev/null)" != "$BIN_SHA" ]; then
rm -rf "$TOOLS/binaryen-$BINARYEN-$PLAT" "$TOOLS/$ARCHIVE"
curl -fsSL --retry 3 -o "$TOOLS/$ARCHIVE" \
"https://github.com/WebAssembly/binaryen/releases/download/$BINARYEN/$ARCHIVE" # allow-name: the binaryen project's release download
got="$(sha256 "$TOOLS/$ARCHIVE")"
if [ "$got" != "$BIN_SHA" ]; then
echo "reach: $ARCHIVE has SHA-256 $got, not the pinned $BIN_SHA" >&2
rm -f "$TOOLS/$ARCHIVE"
exit 1
fi
mkdir -p "$TOOLS/binaryen-$BINARYEN-$PLAT"
tar -xzf "$TOOLS/$ARCHIVE" -C "$TOOLS/binaryen-$BINARYEN-$PLAT" --strip-components=1
rm -f "$TOOLS/$ARCHIVE"
echo "$BIN_SHA" > "$TOOLS/binaryen-$BINARYEN-$PLAT/.sha256"
fi
"$B/wasm-opt" --version | grep -q "($BINARYEN)" || { echo "reach: $B/wasm-opt is not binaryen $BINARYEN" >&2; exit 1; }
# ---- TypeScript, esbuild, acorn, pinned by package-lock.json ------------------------
LOCK_SHA="$(sha256 "$REACH/package-lock.json")"
if [ "$(cat "$TOOLS/js/.lock-sha256" 2>/dev/null)" != "$LOCK_SHA" ] || [ ! -d "$TOOLS/js/node_modules" ]; then
rm -rf "$TOOLS/js"
mkdir -p "$TOOLS/js"
cp "$REACH/package.json" "$REACH/package-lock.json" "$TOOLS/js/"
(cd "$TOOLS/js" && npm ci --ignore-scripts --no-audit --no-fund --loglevel=error)
echo "$LOCK_SHA" > "$TOOLS/js/.lock-sha256"
fi
export WAI_REACH_NODE_MODULES="$TOOLS/js/node_modules"
CLI=(node "$REACH/tools/cli.mjs")
# binaryen's wasm2js compiled to WebAssembly, pinned by package-lock.json: host-independent.
W2J=(node "$TOOLS/js/node_modules/binaryen/bin/wasm2js")
"${W2J[@]}" --version | grep -q "($BINARYEN)" || { echo "reach: the pinned WebAssembly wasm2js is not binaryen $BINARYEN" >&2; exit 1; }
IN="$PKG/wai_web_bg.wasm"
FEATURES=(--enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext)
LOWER=(--signext-lowering --llvm-nontrapping-fptoint-lowering --llvm-memory-copy-fill-lowering)
PROFILES=(verify_decode verify receipt)
rm -f "$DIST"/wai_web_bg.l1.wasm "$DIST"/wai_web_bg.l2.*.wasm "$DIST"/wai_web.reach.js "$DIST"/wai_web.j.*.js "$DIST"/wai-load.js "$DIST"/manifest.json
# The input must be exactly the shipped build's feature set: MVP + sign-extension,
# non-trapping float-to-int and bulk memory. A module using anything more (multi-value,
# reference types, SIMD, ...) fails here, before any rung is built from it.
if ! "$B/wasm-opt" "$IN" --mvp-features "${FEATURES[@]}" -o "$WORK/shipped.check.wasm" 2> "$WORK/check.err"; then
cat "$WORK/check.err" >&2
echo "reach: $IN uses a feature beyond MVP, sign-extension, non-trapping float-to-int and bulk memory" >&2
exit 1
fi
# ---- L1: legalised at the JS interface, nothing lowered ------------------------------
"$B/wasm-opt" "$IN" --mvp-features "${FEATURES[@]}" --legalize-js-interface -o "$DIST/wai_web_bg.l1.wasm"
# ---- L2: MVP-lowered, then legalised -------------------------------------------------
# The lowering runs with the post-MVP features enabled for reading; reading the result
# back with MVP features only is the check that nothing post-MVP is left.
"$B/wasm-opt" "$IN" --mvp-features "${FEATURES[@]}" "${LOWER[@]}" -o "$WORK/mvp.full.wasm"
"$B/wasm-opt" "$WORK/mvp.full.wasm" --mvp-features --legalize-js-interface -o "$DIST/wai_web_bg.l2.full.wasm"
for p in "${PROFILES[@]}"; do
"${CLI[@]}" graph "$WORK/mvp.full.wasm" "$REACH/profiles.json" "$p" "$WORK/graph.$p.json"
"$B/wasm-metadce" "$WORK/mvp.full.wasm" --mvp-features --graph-file "$WORK/graph.$p.json" -o "$WORK/mvp.$p.dce.wasm" > /dev/null
"$B/wasm-opt" "$WORK/mvp.$p.dce.wasm" --mvp-features -Oz -ocimfs=200 -o "$WORK/mvp.$p.wasm"
"${CLI[@]}" exports "$WORK/mvp.$p.wasm" "$WORK/graph.$p.json"
"$B/wasm-opt" "$WORK/mvp.$p.wasm" --mvp-features --legalize-js-interface -o "$DIST/wai_web_bg.l2.$p.wasm"
done
# ---- the i64 table, the runtime and the glue ----------------------------------------
"${CLI[@]}" same-sigs "$IN" "$WORK/mvp.full.wasm"
"${CLI[@]}" sigs "$IN" "$WORK/sigs.js"
"${CLI[@]}" minify "$REACH/src/wai-reach-runtime.js" "$WORK/runtime.min.js"
"${CLI[@]}" glue "$PKG/wai_web.js" "$WORK/glue.js" "$WORK/glue-notes.json"
"${CLI[@]}" minify "$WORK/glue.js" "$WORK/glue.min.js"
cat "$WORK/runtime.min.js" "$WORK/sigs.js" "$WORK/glue.min.js" > "$DIST/wai_web.reach.js"
# ---- J: wasm2js on the MVP-lowered modules -------------------------------------------
# The translations run side by side (each is single-threaded), then in order.
pids=()
for p in full "${PROFILES[@]}"; do
"${W2J[@]}" "$WORK/mvp.$p.wasm" -Oz -ocimfs=200 -o "$WORK/j.$p.mjs" & pids+=($!)
done
for pid in "${pids[@]}"; do wait "$pid" || { echo "reach: wasm2js failed" >&2; exit 1; }; done
for p in full "${PROFILES[@]}"; do
"${CLI[@]}" factory "$WORK/j.$p.mjs" "$WORK/j.$p.factory.js"
"${CLI[@]}" minify "$WORK/j.$p.factory.js" "$WORK/j.$p.factory.min.js"
cat "$WORK/runtime.min.js" "$WORK/sigs.js" "$WORK/j.$p.factory.min.js" "$WORK/glue.min.js" > "$DIST/wai_web.j.$p.js"
done
cp "$REACH/src/wai-load.js" "$DIST/wai-load.js"
"${CLI[@]}" es5 "$DIST/wai-load.js" "$DIST/wai_web.reach.js" "$DIST"/wai_web.j.*.js
# ---- manifest --------------------------------------------------------------------------
cat > "$WORK/tools.json" <<EOF
{
"binaryen": { "release": "$BINARYEN", "archives_sha256": {
"arm64-macos": "74428be348c1a09863e7b642a1fa948cabf8ec9561052233d8288e941951725b",
"x86_64-macos": "cc18b14d2b673d9c66bf54f31ff2b0ceb23ba5132455b893965ae2792f9e00dd",
"x86_64-linux": "e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe",
"aarch64-linux": "4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376" } },
"package_lock_sha256": "$LOCK_SHA",
"l1": "wasm-opt --mvp-features ${FEATURES[*]} --legalize-js-interface",
"l2": "wasm-opt --mvp-features ${FEATURES[*]} ${LOWER[*]}, then wasm-opt --mvp-features --legalize-js-interface; reduced profiles: wasm-metadce with the profile's exports, wasm-opt -Oz -ocimfs=200, then legalised",
"j": "wasm2js -Oz -ocimfs=200 on the MVP-lowered module of each profile, run from binaryen's WebAssembly build of the same release (npm binaryen, pinned by package-lock.json)"
}
EOF
"${CLI[@]}" manifest "$DIST" "$PKG" "$WORK/glue-notes.json" "$WORK/tools.json" "$DIST/manifest.json"
# ---- the recorded build --------------------------------------------------------------
REC="$REACH/manifest.json"
if [ "${WAI_REACH_RECORD:-}" = 1 ]; then
cp "$DIST/manifest.json" "$REC"
echo "reach: recorded $REC"
elif [ -f "$REC" ] && node -e '
const [rec, out] = process.argv.slice(1).map((f) => JSON.parse(require("fs").readFileSync(f, "utf8")));
process.exit(JSON.stringify(rec.input.files) === JSON.stringify(out.input.files) ? 0 : 1);' "$REC" "$DIST/manifest.json"; then
node -e '
const [rec, out] = process.argv.slice(1).map((f) => JSON.parse(require("fs").readFileSync(f, "utf8")));
const names = [...new Set([...Object.keys(rec.outputs), ...Object.keys(out.outputs)])].sort();
let bad = 0;
for (const n of names) {
const a = rec.outputs[n], b = out.outputs[n];
if (!a || !b || a.sha256 !== b.sha256 || a.bytes !== b.bytes) { bad++; console.error(`reach: ${n}: ${b ? b.sha256 : "missing"}, recorded ${a ? a.sha256 : "nothing"}`); }
}
if (bad) { console.error(`reach: ${bad} outputs differ from reach/manifest.json`); process.exit(1); }
console.log(`reach: all ${names.length} outputs equal reach/manifest.json`);' "$REC" "$DIST/manifest.json"
elif [ "${WAI_REACH_REQUIRE_RECORD:-}" = 1 ]; then
echo "reach: WAI_REACH_REQUIRE_RECORD=1, but the package is not the input reach/manifest.json names" >&2
exit 1
else
echo "reach: the package is not the recorded input; outputs are in $DIST/manifest.json only"
fi

View file

@ -0,0 +1,140 @@
{
"format": "wai-reach-manifest/1",
"input": {
"files": {
"wai_web_bg.wasm": {
"sha256": "c2b63f747c8b55b3eb74fd9dd53109bb4031a578622e07c62e26d1a27eb2050e",
"bytes": 2586670
},
"wai_web.js": {
"sha256": "f63605f012b44b935af11ea7285b0ca57ef9a760158353b1d826304d54930f6a",
"bytes": 448554
}
},
"source": "the package of the WAI site's /wai/pkg/, kept in reach/record/: wai/wai-web at commit 248846f13f2da2db32914cff1e716064eaf3d039, built by wasm-pack 0.15.0 (web target, release, wasm-opt off) with rustc 1.98.0 on macOS arm64, the commit's wai/ exported to /private/tmp/wai-wasm-pkg/open-standards, and the export, the Cargo home, the toolchain and the standard library's sources remapped to /open-standards, /cargo, /rustup and /rustc/88d9e12ae178fab0fb5cc050a94da85685d449ea",
"commit": "248846f13f2da2db32914cff1e716064eaf3d039"
},
"tools": {
"binaryen": {
"release": "version_123",
"archives_sha256": {
"arm64-macos": "74428be348c1a09863e7b642a1fa948cabf8ec9561052233d8288e941951725b",
"x86_64-macos": "cc18b14d2b673d9c66bf54f31ff2b0ceb23ba5132455b893965ae2792f9e00dd",
"x86_64-linux": "e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe",
"aarch64-linux": "4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376"
}
},
"package_lock_sha256": "59c8234c2e5dcc0e75a4e6270fb455be3a9281e800ca935432f4a03d2862ef8a",
"l1": "wasm-opt --mvp-features --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --legalize-js-interface",
"l2": "wasm-opt --mvp-features --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --signext-lowering --llvm-nontrapping-fptoint-lowering --llvm-memory-copy-fill-lowering, then wasm-opt --mvp-features --legalize-js-interface; reduced profiles: wasm-metadce with the profile's exports, wasm-opt -Oz -ocimfs=200, then legalised",
"j": "wasm2js -Oz -ocimfs=200 on the MVP-lowered module of each profile, run from binaryen's WebAssembly build of the same release (npm binaryen, pinned by package-lock.json)"
},
"glue": {
"memory_view_checks": 10,
"symbol_dispose_guards": 62,
"bigint_sites_made_optional": 44,
"bigint_required_by": 2,
"typed_array_copies": 119
},
"outputs": {
"wai-load.js": {
"rung": "all",
"profile": null,
"what": "the loader",
"sha256": "c3dc79392e6bf75d6cc190cd38eabab94f449d0d3687585cae6c3e03fe6775a2",
"bytes": 16621,
"gzip": 5105,
"brotli": 4474
},
"wai_web.j.full.js": {
"rung": "J",
"profile": "full",
"what": "wasm2js bundle: runtime + i64 table + translation + glue",
"sha256": "1af5b5917ecb3097e8eb3c79342dd087bd3febeef58bd2222b623e0099b17848",
"bytes": 5969835,
"gzip": 1561026,
"brotli": 1053831
},
"wai_web.j.receipt.js": {
"rung": "J",
"profile": "receipt",
"what": "wasm2js bundle: runtime + i64 table + translation + glue",
"sha256": "b6c52b020c3daef6f0dc5a7ccbbd8e7134a951012987ceaaf66fdfaad9e2d77b",
"bytes": 996473,
"gzip": 309932,
"brotli": 251134
},
"wai_web.j.verify.js": {
"rung": "J",
"profile": "verify",
"what": "wasm2js bundle: runtime + i64 table + translation + glue",
"sha256": "4862fb576c72bb12bffe4799684450c81a3181a34db134118a44e12b9204ab76",
"bytes": 3086139,
"gzip": 844913,
"brotli": 606845
},
"wai_web.j.verify_decode.js": {
"rung": "J",
"profile": "verify_decode",
"what": "wasm2js bundle: runtime + i64 table + translation + glue",
"sha256": "d2bb946fbab8dbcc5fb7ca101b6c4615881d3f1f1de47813a0b3c9e080f1b1bf",
"bytes": 3310622,
"gzip": 903681,
"brotli": 642273
},
"wai_web.reach.js": {
"rung": "L1, L2",
"profile": "full",
"what": "runtime + i64 table + glue, for the wasm rungs",
"sha256": "747db637684c8ffed24aab175a3dc1a399901c1d9699dd99c86a090b4b78bf0c",
"bytes": 198994,
"gzip": 22998,
"brotli": 18502
},
"wai_web_bg.l1.wasm": {
"rung": "L1",
"profile": "full",
"what": "the shipped binary, legalised at the JS interface",
"sha256": "25e728714bcc67ccb133042012860a53c881ea6c26c3dc7538bf378c0598b3b8",
"bytes": 2591481,
"gzip": 918302,
"brotli": 683525
},
"wai_web_bg.l2.full.wasm": {
"rung": "L2",
"profile": "full",
"what": "MVP-lowered and legalised",
"sha256": "5af77d42fbd2e2e6023789aca42dcb62c6bcf1ccb29e7303b31de5e6251fa88e",
"bytes": 2591161,
"gzip": 919464,
"brotli": 684042
},
"wai_web_bg.l2.receipt.wasm": {
"rung": "L2",
"profile": "receipt",
"what": "MVP-lowered and legalised",
"sha256": "89ad094a90fbc31aa3399d0e4db41728c742c110a90480952f3c2527310fb821",
"bytes": 393027,
"gzip": 192859,
"brotli": 163989
},
"wai_web_bg.l2.verify.wasm": {
"rung": "L2",
"profile": "verify",
"what": "MVP-lowered and legalised",
"sha256": "cbf4d3705f566b76ea1aab6d4513a51632df0373338e0e206cceff9d4b7c6a00",
"bytes": 1222037,
"gzip": 521773,
"brotli": 407801
},
"wai_web_bg.l2.verify_decode.wasm": {
"rung": "L2",
"profile": "verify_decode",
"what": "MVP-lowered and legalised",
"sha256": "efe0033b1cfc151054baf6282bd6cd9b2a264cf5df8321586cdbcf1f62928b87",
"bytes": 1316304,
"gzip": 559206,
"brotli": 433878
}
}
}

546
wai/wai-web/reach/package-lock.json generated Normal file
View file

@ -0,0 +1,546 @@
{
"name": "wai-web-reach-tools",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wai-web-reach-tools",
"license": "Apache-2.0",
"devDependencies": {
"acorn": "8.15.0",
"binaryen": "123.0.0",
"esbuild": "0.27.4",
"typescript": "5.9.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/binaryen": {
"version": "123.0.0",
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-123.0.0.tgz",
"integrity": "sha512-/hls/a309aZCc0itqP6uhoR+5DsKSlJVfB8Opd2BY9Ndghs84IScTunlyidyF4r2Xe3lQttnfBNIDjaNpj6mTw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"wasm-as": "bin/wasm-as",
"wasm-ctor-eval": "bin/wasm-ctor-eval",
"wasm-dis": "bin/wasm-dis",
"wasm-merge": "bin/wasm-merge",
"wasm-metadce": "bin/wasm-metadce",
"wasm-opt": "bin/wasm-opt",
"wasm-reduce": "bin/wasm-reduce",
"wasm-shell": "bin/wasm-shell",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.4",
"@esbuild/android-arm": "0.27.4",
"@esbuild/android-arm64": "0.27.4",
"@esbuild/android-x64": "0.27.4",
"@esbuild/darwin-arm64": "0.27.4",
"@esbuild/darwin-x64": "0.27.4",
"@esbuild/freebsd-arm64": "0.27.4",
"@esbuild/freebsd-x64": "0.27.4",
"@esbuild/linux-arm": "0.27.4",
"@esbuild/linux-arm64": "0.27.4",
"@esbuild/linux-ia32": "0.27.4",
"@esbuild/linux-loong64": "0.27.4",
"@esbuild/linux-mips64el": "0.27.4",
"@esbuild/linux-ppc64": "0.27.4",
"@esbuild/linux-riscv64": "0.27.4",
"@esbuild/linux-s390x": "0.27.4",
"@esbuild/linux-x64": "0.27.4",
"@esbuild/netbsd-arm64": "0.27.4",
"@esbuild/netbsd-x64": "0.27.4",
"@esbuild/openbsd-arm64": "0.27.4",
"@esbuild/openbsd-x64": "0.27.4",
"@esbuild/openharmony-arm64": "0.27.4",
"@esbuild/sunos-x64": "0.27.4",
"@esbuild/win32-arm64": "0.27.4",
"@esbuild/win32-ia32": "0.27.4",
"@esbuild/win32-x64": "0.27.4"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View file

@ -0,0 +1,12 @@
{
"name": "wai-web-reach-tools",
"private": true,
"description": "Build-time tools of the WAI reach builds, pinned by package-lock.json. build.sh installs them with npm ci; nothing here is shipped. binaryen here is the release build.sh pins, compiled to WebAssembly: its wasm2js gives the same output on every host.",
"license": "Apache-2.0",
"devDependencies": {
"acorn": "8.15.0",
"binaryen": "123.0.0",
"esbuild": "0.27.4",
"typescript": "5.9.3"
}
}

View file

@ -0,0 +1,32 @@
{
"about": "The reduced profiles of the MVP-lowered builds (L2 and J): which exports of wai_web_bg.wasm each keeps. wasm-metadce keeps these and whatever they reach, and drops the rest. A prefix keeps every export that starts with it, and keeps the class's __wbg_<prefix>free as well. 'always' is in every profile. The full profile keeps every export and is not listed. A page that calls a function outside its profile gets a TypeError from the glue.",
"always": {
"names": ["memory", "wasmMemoryBytes"],
"prefixes": ["__wbindgen_"]
},
"profiles": {
"receipt": {
"about": "Envelope parse, asset provenance (content hash, Ed25519 signature, Merkle chain), and the hashes.",
"names": ["parse", "detectVersion", "sha256Hex", "blake3Hex", "fnv1aHex",
"waienvelope_capability", "waienvelope_kind", "waienvelope_media", "waienvelope_intent",
"waienvelope_fallback", "waienvelope_payload_len", "__wbg_waienvelope_free"],
"prefixes": ["assetprovenance_"]
},
"verify": {
"about": "receipt, plus multi-rendition envelopes, pins, capability checks, stage identities, session claims and the output tree.",
"includes": ["receipt"],
"names": ["parseMulti", "stageContentHash", "stageLayerInfo", "verifyClaim", "verifyPin", "outputRoot",
"isCandidate", "isRegistered", "known_capabilities", "checkRefusal"],
"prefixes": ["waienvelope_", "waimulti_", "rendition_"]
},
"verify_decode": {
"about": "verify, plus the integer decoders (WIV1, WIH1, WIA1, WIS1, int_synth, the JSON integer forms) and the staged-delivery sink.",
"includes": ["verify"],
"names": ["decodeWiv1", "decodeWih1", "decodeWia1", "decodeWis1", "decodeIntAudio", "decodeIntVideo",
"decodeIntVideoPframes", "decodeIntHyper", "decodeIntHyperBin", "decodeIntSynth", "decodeIntSplat",
"intPayloadCost"],
"prefixes": ["decodedaudio_", "decodedvideo_", "decodedimage_", "decodedsplat_", "intpayloadcost_",
"stagebase_", "stagesink_"]
}
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,310 @@
/* wai-load.js — the WAI reach loader (ECMAScript 5).
*
* Picks the build of wai-web an engine can run and loads it:
*
* shipped the shipped package (wai_web.js + wai_web_bg.wasm): WebAssembly with
* sign-extension, non-trapping float-to-int and bulk memory, and i64 values
* crossing the JS boundary as BigInt.
* L1 the shipped binary legalised at the JS interface (wai_web_bg.l1.wasm):
* the same features, for engines where i64 does not cross as BigInt.
* L2 the MVP-lowered, legalised binary (wai_web_bg.l2.<profile>.wasm): MVP
* WebAssembly only.
* J the wasm2js translation (wai_web.j.<profile>.js): no WebAssembly, ES5 and
* typed arrays.
*
* Detection uses only feature probes: tiny modules passed to WebAssembly.validate, one
* tiny module instantiated to see whether i64 crosses as BigInt, and checks for BigInt
* and typed arrays. It never reads the user agent and never throws.
*
* WaiLoad.detect([opts]) -> { rung, profile, reason, features }
* WaiLoad.load(opts, callback) -> callback(error, { api, rung, profile, reason, features, forced })
*
* opts:
* base URL (or, in Node, a directory) of the reach artifacts, ending in '/'.
* In a browser it defaults to the folder this script was loaded from.
* profile 'full' (default) | 'verify_decode' | 'verify' | 'receipt'. The L2 and J
* rungs load the reduced build of that profile; shipped and L1 always load
* the full module.
* force 'shipped' | 'L1' | 'L2' | 'J': load that rung whatever the probes say
* (for testing). Also read from WAI_REACH_FORCE on the global object.
* shipped { glue: URL, wasm: URL } of the shipped package; default base + '../'.
* host overrides for loading: { loadScript(url, cb), fetchBytes(url, cb),
* importModule(url, cb), streams }. In a browser and in Node there are
* defaults; with streams set (a browser's default), the shipped glue is
* given the wasm URL to fetch and compile itself.
*
* After a load, WaiLoad.state holds the same object the callback received.
*/
(function (root) {
'use strict';
var RUNGS = ['shipped', 'L1', 'L2', 'J'];
var PROFILES = ['full', 'verify_decode', 'verify', 'receipt'];
var HEAD = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
// Each probe is the smallest module that uses one feature.
var PROBES = {
// (func (result i32) i32.const 0)
mvp: [0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00,
0x0a, 0x06, 0x01, 0x04, 0x00, 0x41, 0x00, 0x0b],
// (func (param i32) (result i32) local.get 0 i32.extend8_s)
signExt: [0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00,
0x0a, 0x07, 0x01, 0x05, 0x00, 0x20, 0x00, 0xc0, 0x0b],
// (func (param f32) (result i32) local.get 0 i32.trunc_sat_f32_s)
satFloatToInt: [0x01, 0x06, 0x01, 0x60, 0x01, 0x7d, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00,
0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, 0x00, 0xfc, 0x00, 0x0b],
// (memory 1) (func i32.const 0 i32.const 0 i32.const 0 memory.copy), with a data count section
bulkMemory: [0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, 0x00, 0x01,
0x0c, 0x01, 0x00,
0x0a, 0x0e, 0x01, 0x0c, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x0a, 0x00, 0x00, 0x0b]
};
// (func (export "f") (param i64) (result i64) local.get 0)
var I64_IDENTITY = [0x01, 0x06, 0x01, 0x60, 0x01, 0x7e, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00,
0x07, 0x05, 0x01, 0x01, 0x66, 0x00, 0x00,
0x0a, 0x06, 0x01, 0x04, 0x00, 0x20, 0x00, 0x0b];
function moduleBytes(body) {
var a = HEAD.concat(body), u = new Uint8Array(a.length);
for (var i = 0; i < a.length; i++) u[i] = a[i];
return u;
}
function attempt(fn) { try { return !!fn(); } catch (e) { return false; } }
function probe() {
var f = {}, W;
f.typedArrays = attempt(function () {
var names = ['ArrayBuffer', 'DataView', 'Uint8Array', 'Int8Array', 'Uint16Array', 'Int16Array',
'Uint32Array', 'Int32Array', 'Float32Array', 'Float64Array'];
for (var i = 0; i < names.length; i++) {
var t = typeof root[names[i]];
if (t !== 'function' && t !== 'object') return false;
}
var b = new ArrayBuffer(8), d = new DataView(b), u = new Uint8Array(b);
d.setUint32(0, 0x01020304, true);
return u[0] === 4 && typeof u.subarray === 'function' && typeof u.set === 'function';
});
f.bigInt = attempt(function () { return typeof root.BigInt === 'function' && typeof root.BigInt(1) === 'bigint'; });
W = attempt(function () { return typeof root.WebAssembly === 'object' && root.WebAssembly !== null; }) ? root.WebAssembly : null;
f.webAssembly = attempt(function () {
return W && typeof W.validate === 'function' && typeof W.Module === 'function' && typeof W.Instance === 'function';
});
f.wasmMvp = f.webAssembly && f.typedArrays && attempt(function () { return W.validate(moduleBytes(PROBES.mvp)); });
f.signExt = f.wasmMvp && attempt(function () { return W.validate(moduleBytes(PROBES.signExt)); });
f.satFloatToInt = f.wasmMvp && attempt(function () { return W.validate(moduleBytes(PROBES.satFloatToInt)); });
f.bulkMemory = f.wasmMvp && attempt(function () { return W.validate(moduleBytes(PROBES.bulkMemory)); });
f.bigIntWasm = f.wasmMvp && f.bigInt && attempt(function () {
var inst = new W.Instance(new W.Module(moduleBytes(I64_IDENTITY)), {});
var r = inst.exports.f(root.BigInt(-2));
return typeof r === 'bigint' && r === root.BigInt(-2);
});
// An early WebAssembly engine line: WebAssembly and the stack-trace capture API
// (Error.captureStackTrace), but no RegExp dotAll flag. The full MVP module has crashed
// an engine of this line on arm64, so it gets the receipt build or J. Both checks read
// engine features; neither reads the user agent.
f.noDotAll = f.webAssembly && attempt(function () { return typeof Error.captureStackTrace === 'function'; }) &&
!attempt(function () { return new RegExp('.', 's').dotAll === true; });
return f;
}
function choose(f, profile) {
if (!f.typedArrays) {
return { rung: null, reason: 'no typed arrays: no build of WAI runs here; serve the classical floor renditions' };
}
if (!f.webAssembly) return { rung: 'J', reason: 'no WebAssembly: the wasm2js translation' };
if (!f.wasmMvp) return { rung: 'J', reason: 'WebAssembly refuses an MVP module: the wasm2js translation' };
if (f.noDotAll) {
return profile === 'receipt'
? { rung: 'L2', reason: 'WebAssembly without the RegExp dotAll flag, where the full MVP module can crash: the reduced receipt build' }
: { rung: 'J', reason: 'WebAssembly without the RegExp dotAll flag, where the full MVP module can crash: the wasm2js translation' };
}
var missing = [];
if (!f.signExt) missing.push('sign-extension');
if (!f.satFloatToInt) missing.push('non-trapping float-to-int');
if (!f.bulkMemory) missing.push('bulk memory');
if (missing.length) return { rung: 'L2', reason: 'MVP WebAssembly without ' + missing.join(', ') + ': the MVP-lowered, legalised build' };
if (!f.bigIntWasm) {
return { rung: 'L1', reason: 'post-MVP WebAssembly, but i64 does not cross the JS boundary as BigInt: the legalised build' };
}
return { rung: 'shipped', reason: 'WebAssembly with every feature the shipped build uses, and i64 as BigInt: the shipped build' };
}
function normProfile(p) {
if (p === undefined || p === null) return 'full';
for (var i = 0; i < PROFILES.length; i++) if (PROFILES[i] === p) return p;
return null;
}
function detect(opts) {
opts = opts || {};
var f, c, profile = normProfile(opts.profile);
try { f = probe(); } catch (e) { f = { error: String(e && e.message || e) }; }
try { c = choose(f, profile || 'full'); } catch (e2) { c = { rung: null, reason: 'detection failed: ' + String(e2 && e2.message || e2) }; }
var rung = c.rung;
var force = opts.force !== undefined ? opts.force : root.WAI_REACH_FORCE;
var forced = false, reason = c.reason;
if (force !== undefined && force !== null && force !== '') {
forced = true;
reason = 'forced: ' + force + ' (detection gives ' + (rung || 'none') + ': ' + c.reason + ')';
rung = force;
}
var used = rung === 'shipped' || rung === 'L1' ? 'full' : profile;
return { rung: rung, profile: used, reason: reason, features: f, forced: forced };
}
// ---------- hosts ----------
function browserHost() {
var doc = root.document;
return {
streams: true,
loadScript: function (url, cb) {
var s = doc.createElement('script'), done = false;
function fin(err) { if (done) return; done = true; s.onload = s.onerror = s.onreadystatechange = null; cb(err || null); }
s.onload = function () { fin(null); };
s.onerror = function () { fin(new Error('could not load ' + url)); };
s.onreadystatechange = function () { if (s.readyState === 'loaded' || s.readyState === 'complete') fin(null); };
s.src = url;
(doc.head || doc.getElementsByTagName('head')[0] || doc.documentElement).appendChild(s);
},
fetchBytes: function (url, cb) {
var x = new XMLHttpRequest();
x.open('GET', url, true);
x.responseType = 'arraybuffer';
x.onload = function () {
if (x.status !== 200 && x.status !== 0) { cb(new Error('HTTP ' + x.status + ' fetching ' + url)); return; }
cb(null, new Uint8Array(x.response));
};
x.onerror = function () { cb(new Error('could not fetch ' + url)); };
x.send();
},
importModule: function (url, cb) {
var dyn = null;
try { dyn = new Function('u', 'return import(u)'); } catch (e) { dyn = null; }
if (dyn) {
var p;
try { p = dyn(url); } catch (e3) { cb(e3); return; }
p.then(function (m) { cb(null, m); }, function (e4) { cb(e4); });
return;
}
// Content-Security-Policy without 'unsafe-eval': an inline module script.
var key = '__waiLoadModule' + Math.floor(Math.random() * 1e9), s = doc.createElement('script'), settled = false;
var timer = setTimeout(function () { finish(new Error('the shipped module did not load: ' + url)); }, 30000);
function finish(err, m) { if (settled) return; settled = true; clearTimeout(timer); try { delete root[key]; } catch (e5) { root[key] = undefined; } cb(err, m); }
root[key] = finish;
s.type = 'module';
s.text = 'import(' + JSON.stringify(url) + ').then(function (m) { window[' + JSON.stringify(key) + '](null, m); }, ' +
'function (e) { window[' + JSON.stringify(key) + '](e); });';
(doc.head || doc.documentElement).appendChild(s);
}
};
}
function nodeHost() {
// eslint-disable-next-line no-undef
var fs = require('fs'), vm = require('vm'), path = require('path');
return {
loadScript: function (file, cb) {
try { vm.runInThisContext(fs.readFileSync(file, 'utf8'), file); } catch (e) { cb(e); return; }
cb(null);
},
fetchBytes: function (file, cb) {
var b;
try { b = fs.readFileSync(file); } catch (e) { cb(e); return; }
var u = new Uint8Array(b.length);
for (var i = 0; i < b.length; i++) u[i] = b[i];
cb(null, u);
},
importModule: function (file, cb) {
var dyn = null;
try { dyn = new Function('u', 'return import(u)'); } catch (e) { cb(new Error('this engine has no dynamic import: ' + e.message)); return; }
var url = 'file://' + path.resolve(file).split(path.sep).join('/');
dyn(url).then(function (m) { cb(null, m); }, function (e2) { cb(e2); });
}
};
}
function defaultHost() {
if (root.document && typeof root.document.createElement === 'function') return browserHost();
// eslint-disable-next-line no-undef
if (typeof require === 'function' && typeof process === 'object') return nodeHost();
return null;
}
var scriptBase = (function () {
try {
var d = root.document, s = d && (d.currentScript || (function () { var a = d.getElementsByTagName('script'); return a[a.length - 1]; })());
return s && s.src ? s.src.replace(/[^\/]*$/, '') : '';
} catch (e) { return ''; }
})();
// ---------- load ----------
function load(opts, callback) {
opts = opts || {};
var cb = typeof callback === 'function' ? callback : function () {};
var once = false;
function finish(err, res) {
if (once) return;
once = true;
if (res) WaiLoad.state = res;
cb(err, res);
}
var d;
try { d = detect(opts); } catch (e0) { finish(new Error('detection failed: ' + e0.message)); return; }
var state = { api: null, rung: d.rung, profile: d.profile, reason: d.reason, features: d.features, forced: d.forced };
if (normProfile(opts.profile) === null) { finish(new Error('unknown profile ' + opts.profile + '; one of ' + PROFILES.join(', ')), state); return; }
if (d.rung === null) { finish(new Error(d.reason), state); return; }
var known = false;
for (var i = 0; i < RUNGS.length; i++) if (RUNGS[i] === d.rung) known = true;
if (!known) { finish(new Error('unknown rung ' + d.rung + '; one of ' + RUNGS.join(', ')), state); return; }
var host = {}, dflt = defaultHost() || {}, k;
for (k in dflt) if (Object.prototype.hasOwnProperty.call(dflt, k)) host[k] = dflt[k];
if (opts.host) for (k in opts.host) if (Object.prototype.hasOwnProperty.call(opts.host, k)) host[k] = opts.host[k];
var base = opts.base !== undefined ? opts.base : scriptBase;
var shipped = opts.shipped || {};
function fail(err) { finish(err instanceof Error ? err : new Error(String(err)), state); }
function ok(api) { state.api = api; finish(null, state); }
function guard(fn) { return function () { try { fn.apply(null, arguments); } catch (e) { fail(e); } }; }
try {
if (d.rung === 'shipped') {
var glueUrl = shipped.glue || base + '../wai_web.js', wasmUrl = shipped.wasm || base + '../wai_web_bg.wasm';
if (!host.importModule || !host.fetchBytes) { fail(new Error('no host to load the shipped module')); return; }
host.importModule(glueUrl, guard(function (err, m) {
if (err) { fail(err); return; }
// A browser hands the glue the URL, so it compiles while it downloads; a host that
// reads files hands it the bytes.
if (host.streams) { m['default']({ module_or_path: wasmUrl }).then(function () { ok(m); }, fail); return; }
host.fetchBytes(wasmUrl, guard(function (err2, bytes) {
if (err2) { fail(err2); return; }
m['default']({ module_or_path: bytes }).then(function () { ok(m); }, fail);
}));
}));
} else if (d.rung === 'L1' || d.rung === 'L2') {
var wasmFile = d.rung === 'L1' ? 'wai_web_bg.l1.wasm' : 'wai_web_bg.l2.' + d.profile + '.wasm';
host.loadScript(base + 'wai_web.reach.js', guard(function (err) {
if (err) { fail(err); return; }
host.fetchBytes(base + wasmFile, guard(function (err2, bytes) {
if (err2) { fail(err2); return; }
var R = root.WaiReachRuntime, S = root.WaiReachSigs, G = root.WaiWeb;
var imports = R.legalImports(G.__reachImports(), S);
root.WebAssembly.instantiate(bytes, imports).then(guard(function (res) {
G.__reachFinalize(R.wrapExports(res.instance.exports, S));
ok(G);
}), fail);
}));
}));
} else {
host.loadScript(base + 'wai_web.j.' + d.profile + '.js', guard(function (err) {
if (err) { fail(err); return; }
var R = root.WaiReachRuntime, S = root.WaiReachSigs, G = root.WaiWeb;
G.__reachFinalize(R.instantiate(root.WaiReachFactory, S, G.__reachImports()));
ok(G);
}));
}
} catch (e) {
fail(e);
}
}
var WaiLoad = { detect: detect, load: load, rungs: RUNGS.slice(), profiles: PROFILES.slice(), state: null };
root.WaiLoad = WaiLoad;
if (typeof module !== 'undefined' && module.exports) module.exports = WaiLoad;
})(typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this);

View file

@ -0,0 +1,281 @@
/* WAI reach runtime (ECMAScript 5). Loaded first by every reach rung (L1, L2 and J).
*
* 1. Polyfills, each installed only where the engine lacks it, for the post-ES5
* builtins the glue and the wasm2js translation touch: Math.imul / fround / clz32 /
* trunc, TypedArray.prototype.slice, Number.isSafeInteger, Object.entries,
* Array.from (array-likes and iterators), Reflect.get / apply, TextEncoder /
* TextDecoder (UTF-8), Array.prototype.fill, a small insertion-ordered Map, and a
* string-keyed Symbol.iterator stand-in where there is no Symbol.
* 2. The i64 boundary. A legalised module (L1, L2) and the wasm2js translation (J)
* take an i64 parameter as a (low, high) pair of i32 values and return an i64
* result's low half, passing the high half through env.setTempRet0. The glue
* speaks in single values. legalImports() and wrapExports() join and split them
* at the boundary, from the table WaiReachSigs. With BigInt a 64-bit value is a
* BigInt, as the shipped build gives it; without BigInt it is a Number, exact
* below 2^53.
*
* Written in ES5 on purpose: no let/const, arrow functions, classes, template
* literals or BigInt literals, so that it parses on every engine the J rung serves.
* Global: WaiReachRuntime.
*/
(function (root) {
'use strict';
var TWO32 = 4294967296;
// ---------- 1. polyfills ----------
var poly = [];
function def(obj, name, fn) {
if (typeof obj[name] !== 'function') {
try { Object.defineProperty(obj, name, { value: fn, writable: true, configurable: true }); }
catch (e) { obj[name] = fn; }
poly.push(name);
}
}
def(Math, 'imul', function (a, b) {
var ah = (a >>> 16) & 0xffff, al = a & 0xffff, bh = (b >>> 16) & 0xffff, bl = b & 0xffff;
return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)) | 0;
});
if (typeof Float32Array !== 'undefined') {
var f32 = new Float32Array(1);
def(Math, 'fround', function (x) { f32[0] = x; return f32[0]; });
}
def(Math, 'clz32', function (x) {
x = x >>> 0;
if (x === 0) return 32;
var n = 0;
if ((x & 0xffff0000) === 0) { n += 16; x = x << 16; }
if ((x & 0xff000000) === 0) { n += 8; x = x << 8; }
if ((x & 0xf0000000) === 0) { n += 4; x = x << 4; }
if ((x & 0xc0000000) === 0) { n += 2; x = x << 2; }
if ((x & 0x80000000) === 0) { n += 1; }
return n;
});
def(Math, 'trunc', function (x) { return x < 0 ? Math.ceil(x) : Math.floor(x); });
def(Number, 'isSafeInteger', function (v) {
return typeof v === 'number' && isFinite(v) && Math.floor(v) === v && Math.abs(v) <= 9007199254740991;
});
def(Object, 'entries', function (o) {
var out = [], k;
for (k in o) { if (Object.prototype.hasOwnProperty.call(o, k)) out.push([k, o[k]]); }
return out;
});
def(Array, 'from', function (a) {
var out = [], i, r;
if (a == null) throw new TypeError('Array.from: null');
if (typeof a.length === 'number') { for (i = 0; i < a.length; i++) out.push(a[i]); return out; }
if (typeof a.next === 'function') { for (r = a.next(); !r.done; r = a.next()) out.push(r.value); return out; }
return out;
});
if (typeof root.Reflect === 'undefined') { root.Reflect = {}; poly.push('Reflect'); }
def(root.Reflect, 'get', function (o, k) { return o[k]; });
def(root.Reflect, 'apply', function (f, t, a) { return Function.prototype.apply.call(f, t, a); });
var TA = ['Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array',
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array'];
for (var t = 0; t < TA.length; t++) {
var C = root[TA[t]];
if (!C) continue;
(function (C, name) {
if (typeof C.prototype.slice !== 'function') {
C.prototype.slice = function (b, e) {
var len = this.length;
b = b === undefined ? 0 : (b < 0 ? Math.max(len + b, 0) : Math.min(b, len));
e = e === undefined ? len : (e < 0 ? Math.max(len + e, 0) : Math.min(e, len));
var out = new C(Math.max(e - b, 0));
out.set(this.subarray(b, e));
return out;
};
poly.push(name + '.prototype.slice');
}
})(C, TA[t]);
}
if (typeof root.TextEncoder === 'undefined') {
var TE = function TextEncoder() {};
TE.prototype.encode = function (s) {
s = String(s === undefined ? '' : s);
var out = [], i, c, c2;
for (i = 0; i < s.length; i++) {
c = s.charCodeAt(i);
if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) {
c2 = s.charCodeAt(i + 1);
if (c2 >= 0xdc00 && c2 <= 0xdfff) { c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); i++; }
else c = 0xfffd;
} else if (c >= 0xd800 && c <= 0xdfff) c = 0xfffd;
if (c < 0x80) out.push(c);
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
}
return new Uint8Array(out);
};
TE.prototype.encodeInto = function (s, view) {
var b = this.encode(s), n = Math.min(b.length, view.length);
view.set(b.subarray(0, n));
return { read: s.length, written: n };
};
root.TextEncoder = TE; poly.push('TextEncoder');
}
if (typeof root.TextDecoder === 'undefined') {
var TD = function TextDecoder(label, opts) { this.fatal = !!(opts && opts.fatal); };
TD.prototype.decode = function (u8) {
if (!u8) return '';
var s = '', i = 0, n = u8.length, c, cp, need, chunk = [];
while (i < n) {
c = u8[i++];
if (c < 0x80) cp = c;
else {
need = c >= 0xf0 ? 3 : c >= 0xe0 ? 2 : c >= 0xc0 ? 1 : -1;
if (need < 0 || i + need > n) { if (this.fatal) throw new TypeError('invalid utf-8'); cp = 0xfffd; }
else {
cp = c & (need === 1 ? 0x1f : need === 2 ? 0x0f : 0x07);
while (need-- > 0) cp = (cp << 6) | (u8[i++] & 63);
}
}
if (cp >= 0x10000) { cp -= 0x10000; chunk.push(0xd800 + (cp >> 10), 0xdc00 + (cp & 1023)); }
else chunk.push(cp);
if (chunk.length >= 8192) { s += String.fromCharCode.apply(null, chunk); chunk = []; }
}
return s + String.fromCharCode.apply(null, chunk);
};
root.TextDecoder = TD; poly.push('TextDecoder');
}
// The glue builds its object heap at load time with `new Array(1024).fill(undefined)`.
if (typeof Array.prototype.fill !== 'function') {
try {
Object.defineProperty(Array.prototype, 'fill', { writable: true, configurable: true, value: function (v, s, e) {
var len = this.length >>> 0, i = s === undefined ? 0 : (s < 0 ? Math.max(len + s, 0) : Math.min(s, len));
var end = e === undefined ? len : (e < 0 ? Math.max(len + e, 0) : Math.min(e, len));
for (; i < end; i++) this[i] = v;
return this;
} });
poly.push('Array.prototype.fill');
} catch (e) { /* an engine that refuses to extend Array.prototype: the glue cannot load there */ }
}
// The glue builds a Map when Rust serialises one and tests `instanceof Map` when it
// reads one. Engines before ES2015 collections lack it.
if (typeof root.Map === 'undefined') {
var MapP = function Map() { this._k = []; this._v = []; this.size = 0; };
MapP.prototype._i = function (k) {
for (var i = 0; i < this._k.length; i++) { var x = this._k[i]; if (x === k || (x !== x && k !== k)) return i; }
return -1;
};
MapP.prototype.get = function (k) { var i = this._i(k); return i < 0 ? undefined : this._v[i]; };
MapP.prototype.has = function (k) { return this._i(k) >= 0; };
MapP.prototype.set = function (k, v) {
var i = this._i(k);
if (i < 0) { this._k.push(k); this._v.push(v); this.size = this._k.length; } else this._v[i] = v;
return this;
};
MapP.prototype['delete'] = function (k) {
var i = this._i(k); if (i < 0) return false;
this._k.splice(i, 1); this._v.splice(i, 1); this.size = this._k.length; return true;
};
MapP.prototype.clear = function () { this._k = []; this._v = []; this.size = 0; };
MapP.prototype.forEach = function (cb, th) { for (var i = 0; i < this._k.length; i++) cb.call(th, this._v[i], this._k[i], this); };
var MapIt = function (m, kind) { this.m = m; this.i = 0; this.kind = kind; };
MapIt.prototype.next = function () {
if (this.i >= this.m._k.length) return { done: true, value: undefined };
var i = this.i++, k = this.m._k[i], v = this.m._v[i];
return { done: false, value: this.kind === 0 ? [k, v] : this.kind === 1 ? k : v };
};
MapP.prototype.entries = function () { return new MapIt(this, 0); };
MapP.prototype.keys = function () { return new MapIt(this, 1); };
MapP.prototype.values = function () { return new MapIt(this, 2); };
root.Map = MapP; poly.push('Map');
}
// serde-wasm-bindgen reads Symbol.iterator through the glue to ask whether a value is
// iterable. Where there is no Symbol, a string-keyed stand-in makes that answer "not
// iterable" and serde takes its plain-object path. It is an object, not a function, so
// `typeof Symbol === 'function'` checks stay false.
if (typeof root.Symbol === 'undefined') {
root.Symbol = { iterator: '@@iterator' };
if (root.Map && root.Map.prototype && !root.Map.prototype['@@iterator'] && root.Map.prototype.entries) {
root.Map.prototype['@@iterator'] = root.Map.prototype.entries;
}
poly.push('Symbol.iterator (string-keyed stand-in)');
}
// ---------- 2. the i64 boundary ----------
var HAS_BIGINT = typeof root.BigInt === 'function';
var tempRet0 = 0;
function splitI64(v) {
if (HAS_BIGINT && typeof v === 'bigint') {
var B = root.BigInt;
return [Number(B.asUintN(32, v)) | 0, Number(B.asIntN(32, v >> B(32))) | 0];
}
v = Number(v);
if (!(Math.abs(v) <= 9007199254740991) || Math.floor(v) !== v) {
throw new RangeError('an i64 argument must be an integer of magnitude below 2^53 where there is no BigInt');
}
var lo = v % TWO32; if (lo < 0) lo += TWO32;
return [lo | 0, Math.floor(v / TWO32) | 0];
}
function joinI64(lo, hi) {
if (HAS_BIGINT) {
var B = root.BigInt;
return B.asIntN(64, (B(hi | 0) << B(32)) | B(lo >>> 0));
}
return (hi | 0) * TWO32 + (lo >>> 0);
}
function wrapExport(fn, s) {
var ps = s.params, res = s.result;
return function () {
var a = [], i, p;
for (i = 0; i < ps.length; i++) {
if (ps[i] === 'i64') { p = splitI64(arguments[i]); a.push(p[0], p[1]); }
else a.push(arguments[i]);
}
var r = fn.apply(null, a);
return res === 'i64' ? joinI64(r, tempRet0) : r;
};
}
function wrapImport(fn, s) {
var ps = s.params, res = s.result;
return function () {
var a = [], i, j = 0, r, p;
for (i = 0; i < ps.length; i++) {
if (ps[i] === 'i64') { a.push(joinI64(arguments[j], arguments[j + 1])); j += 2; }
else { a.push(arguments[j]); j += 1; }
}
r = fn.apply(null, a);
if (res !== 'i64') return r;
p = splitI64(r);
tempRet0 = p[1];
return p[0];
};
}
// The imports a legalised module or the translation is instantiated with: the glue's
// imports with every i64 import joined, plus env.setTempRet0 / getTempRet0.
function legalImports(imports, sigs) {
var legal = {}, mod, name, f, s;
for (mod in imports) {
if (!Object.prototype.hasOwnProperty.call(imports, mod)) continue;
legal[mod] = {};
for (name in imports[mod]) {
if (!Object.prototype.hasOwnProperty.call(imports[mod], name)) continue;
f = imports[mod][name]; s = sigs.imports[name];
legal[mod][name] = (s && s.module === mod) ? wrapImport(f, s) : f;
}
}
legal.env = { setTempRet0: function (v) { tempRet0 = v | 0; }, getTempRet0: function () { return tempRet0; } };
return legal;
}
// The exports as the glue expects them: every i64 export split and joined, the rest
// (functions, memory, tables) as they are.
function wrapExports(raw, sigs) {
var ex = {}, k;
for (k in raw) {
if (!Object.prototype.hasOwnProperty.call(raw, k)) continue;
ex[k] = sigs.exports[k] ? wrapExport(raw[k], sigs.exports[k]) : raw[k];
}
return { exports: ex };
}
// J: instantiate the translation synchronously. factory is WaiReachFactory.
function instantiate(factory, sigs, imports) {
return wrapExports(factory(legalImports(imports, sigs)), sigs);
}
root.WaiReachRuntime = {
polyfilled: poly, hasBigInt: HAS_BIGINT,
legalImports: legalImports, wrapExports: wrapExports, instantiate: instantiate,
splitI64: splitI64, joinI64: joinI64
};
})(typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this);

View file

@ -0,0 +1,122 @@
/* Reach conformance runner (ECMAScript 5, Node 0.12 onward).
*
* node conformance.js --dist <dist> --wai <wai-dir> [options]
*
* --rung auto|shipped|L1|L2|J auto (default) lets the loader choose; the others force it
* --profile full|verify_decode|verify|receipt (default full)
* --expect-rung R fail unless the loader chose R
* --shipped <package-dir> the shipped package (wai_web.js, wai_web_bg.wasm), for the shipped rung
* --reference <file> fail unless every workload's output equals the file's (the shipped build's)
* --write-reference <file> write this run's outputs there
* --strip es5 delete the post-ES5 builtins first (WebAssembly, BigInt, Map, Symbol, ...),
* so a modern engine behaves like the oldest engines J serves
* --only a,b run only these workloads
*
* Under the J rung WebAssembly is deleted before anything loads, so the run proves the
* translation does not use it. Prints one line per workload (PASS/FAIL, named
* "<rung>/<profile> <workload>") and a JSON summary line; exits 0 only when every
* workload passes its own check and, with --reference, equals the shipped build's output.
*/
'use strict';
var fs = require('fs'), path = require('path'), crypto = require('crypto');
var H = require('./node-host.js');
var a = process.argv.slice(2), opt = { rung: 'auto', profile: 'full' };
for (var i = 0; i < a.length; i++) {
var k = a[i];
if (k.slice(0, 2) !== '--' || i + 1 >= a.length) { console.log('usage: see the header of conformance.js'); process.exit(2); }
opt[k.slice(2)] = a[++i];
}
if (!opt.dist || !opt.wai) { console.log('conformance: --dist and --wai are required'); process.exit(2); }
var DIST = path.resolve(opt.dist), WAI = path.resolve(opt.wai);
var engine = 'node ' + process.version + ' (V8 ' + process.versions.v8 + ')';
function sha256(u8) {
var b = typeof Buffer.from === 'function' && Buffer.from !== Uint8Array.from
? Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength) : new Buffer(u8);
return crypto.createHash('sha256').update(b).digest('hex');
}
function now() { var t = process.hrtime(); return t[0] * 1e3 + t[1] / 1e6; }
var ctx = {
bytes: function (rel) { return H.bytes(path.join(WAI, rel)); },
text: function (rel) { return fs.readFileSync(path.join(WAI, rel), 'utf8'); },
json: function (rel) { return JSON.parse(fs.readFileSync(path.join(WAI, rel), 'utf8')); },
dirs: function (rel) {
var d = path.join(WAI, rel);
return fs.readdirSync(d).filter(function (n) { return fs.statSync(path.join(d, n)).isDirectory(); }).sort();
},
sha256: sha256,
now: now
};
// Node's own modules are loaded above; what is deleted now is what the code under test sees.
var stripped = [];
if (opt.strip === 'es5') {
require('./es5-strip.js');
stripped = global.__WAI_ES5_STRIPPED || [];
}
if (opt.rung === 'J' && typeof global.WebAssembly !== 'undefined') {
try { delete global.WebAssembly; } catch (e) { /* not configurable */ }
if (typeof global.WebAssembly !== 'undefined') global.WebAssembly = undefined;
stripped.push('WebAssembly');
}
H.runScript(path.join(__dirname, 'workloads.js'));
var Wk = global.WaiReachWorkloads;
var WaiLoad = H.loadLoader(DIST);
var summary = { engine: engine, requested: opt.rung, profile: opt.profile, stripped: stripped };
var loadOpts = { base: DIST + path.sep, profile: opt.profile, host: H.host };
if (opt.rung !== 'auto') loadOpts.force = opt.rung;
if (opt.shipped) {
loadOpts.shipped = { glue: path.join(path.resolve(opt.shipped), 'wai_web.js'), wasm: path.join(path.resolve(opt.shipped), 'wai_web_bg.wasm') };
}
var t0 = now();
WaiLoad.load(loadOpts, function (err, st) {
summary.rung = st ? st.rung : null;
summary.reason = st ? st.reason : null;
summary.features = st ? st.features : null;
summary.load_ms = Math.round(now() - t0);
var label = (st && st.rung ? st.rung : opt.rung) + '/' + (st && st.profile ? st.profile : opt.profile);
var pass = true;
if (err) {
summary.load_error = String(err && err.message || err);
console.log('FAIL ' + label + ' load: ' + summary.load_error);
finish(false);
return;
}
console.log(' ' + engine + ': ' + label + ' (' + st.reason + ')');
if (opt['expect-rung'] && st.rung !== opt['expect-rung']) {
console.log('FAIL ' + label + ' rung: the loader chose ' + st.rung + ', expected ' + opt['expect-rung']);
pass = false;
}
var names = Wk.forProfile(st.profile);
if (opt.only) names = names.filter(function (n) { return opt.only.split(',').indexOf(n) >= 0; });
var ref = opt.reference ? JSON.parse(fs.readFileSync(opt.reference, 'utf8')).outputs : null;
var R = Wk.run(st.api, ctx, names), outs = {};
for (var j = 0; j < names.length; j++) {
var n = names[j], r = R[n], ok = r.ok && !r.err, why = r.err ? r.err : (r.ok ? '' : 'its own check failed');
if (ok && ref) {
if (!Object.prototype.hasOwnProperty.call(ref, n)) { ok = false; why = 'no shipped output to compare with'; }
else if (JSON.stringify(ref[n]) !== JSON.stringify(r.out)) { ok = false; why = 'output differs from the shipped build'; }
}
outs[n] = r.out;
pass = pass && ok;
console.log((ok ? 'PASS ' : 'FAIL ') + label + ' ' + n + ' ' + r.ms + ' ms' + (why ? ' ' + why : ''));
}
try { summary.memory_bytes = st.api.wasmMemoryBytes(); } catch (e) { summary.memory_bytes = null; }
summary.results = R;
if (opt['write-reference']) {
fs.writeFileSync(opt['write-reference'], JSON.stringify({ engine: engine, rung: st.rung, profile: st.profile, outputs: outs }, null, 1) + '\n');
}
finish(pass);
});
function finish(pass) {
summary.pass = pass;
console.log('SUMMARY ' + JSON.stringify({ engine: summary.engine, rung: summary.rung, profile: summary.profile, pass: pass,
load_ms: summary.load_ms, memory_bytes: summary.memory_bytes, reason: summary.reason, stripped: summary.stripped.length,
ms: (function () { var o = {}; for (var n in summary.results || {}) o[n] = summary.results[n].ms; return o; })() }));
// exitCode, not exit(): a pipe on some platforms is written asynchronously.
process.exitCode = pass ? 0 : 1;
}

View file

@ -0,0 +1,36 @@
/* Strip a current engine down to ECMAScript 5 and typed arrays, so that one fast engine can
* stand in for the oldest engines the J rung serves. Loaded by conformance.js with
* `--strip es5`, after Node's own modules (which keep the builtins they captured at
* start-up). The deleted names are listed in global.__WAI_ES5_STRIPPED. Modern syntax
* is fine here: this file runs only on a modern Node. */
'use strict';
const g = globalThis, out = [];
const SYM_IT = Symbol.iterator;
function del(obj, name, label) {
if (obj && Object.prototype.hasOwnProperty.call(obj, name)) {
try { delete obj[name]; out.push(label || String(name)); } catch (e) { /* not configurable */ }
}
}
for (const k of ['WebAssembly', 'BigInt', 'BigInt64Array', 'BigUint64Array', 'Map', 'Set', 'WeakMap', 'WeakSet', 'WeakRef',
'FinalizationRegistry', 'Reflect', 'Proxy', 'TextEncoder', 'TextDecoder', 'Symbol', 'Promise']) del(g, k);
for (const k of ['imul', 'fround', 'clz32', 'trunc', 'sign', 'log2', 'log10', 'cbrt', 'hypot', 'expm1', 'log1p']) del(Math, k, 'Math.' + k);
for (const k of ['isSafeInteger', 'isInteger', 'isFinite', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'MAX_SAFE_INTEGER', 'MIN_SAFE_INTEGER']) del(Number, k, 'Number.' + k);
for (const k of ['entries', 'values', 'assign', 'is', 'getOwnPropertySymbols', 'setPrototypeOf', 'fromEntries']) del(Object, k, 'Object.' + k);
for (const k of ['from', 'of']) del(Array, k, 'Array.' + k);
for (const k of ['fill', 'find', 'findIndex', 'findLast', 'findLastIndex', 'includes', 'keys', 'values', 'entries', 'copyWithin', 'flat', 'flatMap', 'at'])
del(Array.prototype, k, 'Array.prototype.' + k);
for (const k of ['startsWith', 'endsWith', 'includes', 'repeat', 'padStart', 'padEnd', 'codePointAt', 'normalize', 'at', 'trimStart', 'trimEnd', 'replaceAll', 'matchAll'])
del(String.prototype, k, 'String.prototype.' + k);
del(String, 'fromCodePoint', 'String.fromCodePoint');
const TA = Object.getPrototypeOf(Uint8Array.prototype);
for (const k of ['slice', 'fill', 'find', 'findIndex', 'includes', 'indexOf', 'lastIndexOf', 'join', 'forEach', 'map', 'filter', 'reduce', 'reduceRight',
'some', 'every', 'sort', 'reverse', 'keys', 'values', 'entries', 'copyWithin', 'at', 'toString'])
del(TA, k, 'TypedArray.prototype.' + k);
del(TA, SYM_IT, 'TypedArray.prototype[Symbol.iterator]');
del(Array.prototype, SYM_IT, 'Array.prototype[Symbol.iterator]');
del(DataView.prototype, 'getBigInt64', 'DataView.prototype.getBigInt64');
del(DataView.prototype, 'setBigInt64', 'DataView.prototype.setBigInt64');
// An early typed-array quirk: slice() returns a view, not a copy.
Object.defineProperty(TA, 'slice', { configurable: true, writable: true, value: function (b, e) { return this.subarray(b, e); } });
out.push('TypedArray.prototype.slice := subarray (a view, as on early engines)');
g.__WAI_ES5_STRIPPED = out;

View file

@ -0,0 +1,49 @@
/* The reach loader's host under Node, in ECMAScript 5 so that it runs on every Node line
* the conformance proxies use (0.12 onward).
*
* var H = require('./node-host.js');
* H.host { loadScript(file, cb), fetchBytes(file, cb), importModule(file, cb) }
* H.loadLoader(dist) evaluates dist/wai-load.js as a classic script; returns WaiLoad
* H.bytes(file) the file as a Uint8Array
*/
'use strict';
var fs = require('fs'), vm = require('vm'), path = require('path');
function toBytes(buf) {
var u;
try { u = new Uint8Array(buf); if (u.length === buf.length) return u; } catch (e) { /* engines without array-like construction */ }
u = new Uint8Array(buf.length);
for (var i = 0; i < buf.length; i++) u[i] = buf[i];
return u;
}
function bytes(file) { return toBytes(fs.readFileSync(file)); }
function runScript(file) {
vm.runInThisContext(fs.readFileSync(file, 'utf8'), file);
}
var host = {
loadScript: function (file, cb) {
try { runScript(file); } catch (e) { cb(e); return; }
cb(null);
},
fetchBytes: function (file, cb) {
var u;
try { u = bytes(file); } catch (e) { cb(e); return; }
cb(null, u);
},
importModule: function (file, cb) {
var dyn;
try { dyn = new Function('u', 'return import(u)'); } catch (e) { cb(new Error('no dynamic import on this engine: ' + e.message)); return; }
var url = 'file://' + path.resolve(file).split(path.sep).join('/');
dyn(url).then(function (m) { cb(null, m); }, function (e) { cb(e); });
}
};
function loadLoader(dist) {
runScript(path.join(dist, 'wai-load.js'));
return global.WaiLoad;
}
module.exports = { host: host, loadLoader: loadLoader, bytes: bytes, toBytes: toBytes, runScript: runScript };

View file

@ -0,0 +1,120 @@
// Every rung forced, on a modern Node: the reach conformance at every profile, and the
// existing wai-web verify scripts (demo/verify_*.mjs) on every full build.
//
// node run-modern.mjs --dist <dist> --shipped <package-dir> [--out <dir>]
//
// 1. The shipped package, chosen by the loader itself (it must choose `shipped` here),
// runs the workloads; its outputs are the reference.
// 2. Every rung and profile, forced, runs the same workloads, and each output must
// equal the reference byte for byte: L1; L2 at full, verify_decode, verify and
// receipt; J at the same four profiles (with WebAssembly deleted). Then the engine
// is stripped to ES5 (no WebAssembly, BigInt, Map, Symbol, ...), and the loader must
// choose J on its own and give the same outputs, at full and receipt.
// 3. The verify scripts of demo/ (integer payloads, int_synth, the staged corpus with
// its guards and fuzz, the menu, the writers, the staged measurement) run against
// shipped, L1, L2 and J, each loaded through the loader by a generated adapter
// module; the measurement script's written outputs must equal the shipped build's
// file for file.
//
// Exit 0 only if every run passes. --out keeps the reference, the logs and a JSON
// summary there (default: a temporary folder, removed).
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const web = resolve(here, '../..');
const wai = resolve(web, '..');
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
if (!args.dist || !args.shipped) { console.log('usage: node run-modern.mjs --dist <dist> --shipped <package-dir> [--out <dir>]'); process.exit(2); }
const dist = resolve(args.dist), shipped = resolve(args.shipped);
const keep = !!args.out;
const out = keep ? resolve(args.out) : mkdtempSync(join(tmpdir(), 'wai-reach-'));
mkdirSync(out, { recursive: true });
const ref = join(out, 'reference.json');
const runs = [];
function conformance(label, extra) {
const r = spawnSync(process.execPath, [join(here, 'conformance.js'), '--dist', dist, '--wai', wai, '--shipped', shipped, ...extra], { encoding: 'utf8', maxBuffer: 1 << 26 });
const text = (r.stdout || '') + (r.stderr || '');
writeFileSync(join(out, `conformance.${label.replace(/[^\w.-]+/g, '_')}.log`), text);
const lines = text.split('\n').filter((l) => /^(PASS|FAIL) /.test(l));
const summary = (text.match(/^SUMMARY (.*)$/m) || [])[1];
const ok = r.status === 0;
runs.push({ kind: 'workloads', label, ok, passed: lines.filter((l) => l.startsWith('PASS')).length, failed: lines.filter((l) => l.startsWith('FAIL')), summary: summary ? JSON.parse(summary) : null });
console.log(`${ok ? 'PASS' : 'FAIL'} ${label}: ${lines.length} checks${ok ? '' : '\n ' + lines.filter((l) => l.startsWith('FAIL')).join('\n ')}`);
return ok;
}
// 1. the reference
conformance('shipped/full auto (reference)', ['--expect-rung', 'shipped', '--write-reference', ref]);
if (!existsSync(ref)) { console.log('FAIL no reference was written'); process.exit(1); }
// 2. every rung and profile, forced
conformance('shipped/full forced', ['--rung', 'shipped', '--reference', ref]);
conformance('L1/full forced', ['--rung', 'L1', '--reference', ref]);
for (const p of ['full', 'verify_decode', 'verify', 'receipt']) conformance(`L2/${p} forced`, ['--rung', 'L2', '--profile', p, '--reference', ref]);
for (const p of ['full', 'verify_decode', 'verify', 'receipt']) conformance(`J/${p} forced, no WebAssembly`, ['--rung', 'J', '--profile', p, '--reference', ref]);
for (const p of ['full', 'receipt']) conformance(`J/${p} chosen on an ES5-stripped engine`, ['--profile', p, '--strip', 'es5', '--expect-rung', 'J', '--reference', ref]);
// 3. the demo verify scripts through an adapter per rung
const glue = await import(pathToFileURL(join(shipped, 'wai_web.js')).href);
const names = Object.keys(glue).filter((n) => n !== 'default' && n !== 'initSync').sort();
const adapters = join(out, 'adapters');
mkdirSync(adapters, { recursive: true });
function adapter(rung) {
const file = join(adapters, `wai_web.${rung}.mjs`);
writeFileSync(file, `// Generated by run-modern.mjs: the ${rung} rung of wai-web, loaded through wai-load.js, as an ES module.
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const H = require(${JSON.stringify(join(here, 'node-host.js'))});
${rung === 'J' ? `// J must not use WebAssembly: every entry point throws. RuntimeError stays, and counts a
// wasm2js trap (an Error('abort')) as one, so the scripts' "nothing traps" checks still hold.
const trap = class RuntimeError extends Error { static [Symbol.hasInstance](e) { return e instanceof Error && e.message === 'abort'; } };
const refuse = () => { throw new Error('the J rung called WebAssembly'); };
globalThis.WebAssembly = { RuntimeError: trap, validate: refuse, compile: refuse, instantiate: refuse, Module: refuse, Instance: refuse, Memory: refuse, Table: refuse };
` : ''}const WaiLoad = H.loadLoader(${JSON.stringify(dist)});
const st = await new Promise((res, rej) => WaiLoad.load({
base: ${JSON.stringify(dist + '/')}, profile: 'full', force: ${JSON.stringify(rung)}, host: H.host,
shipped: { glue: ${JSON.stringify(join(shipped, 'wai_web.js'))}, wasm: ${JSON.stringify(join(shipped, 'wai_web_bg.wasm'))} },
}, (e, s) => (e ? rej(e) : res(s))));
if (st.rung !== ${JSON.stringify(rung)}) throw new Error('the loader gave ' + st.rung);
export const { ${names.join(', ')} } = st.api;
`);
return file;
}
const SCRIPTS = [
['verify_int_payload_wasm', (p) => [p, join(wai, 'wai-rs/tests/vectors')]],
['verify_int_synth_wasm', (p) => [p, join(wai, 'neural-conformance')]],
['verify_staged_wasm', (p) => [p, join(wai, 'staged-conformance')]],
['verify_menu', (p) => [p, join(wai, 'registry/capabilities.json')]],
['verify_pack', (p) => [p, join(wai, 'registry/capabilities.json')]],
['verify_measure_wasm', (p, rung) => [p, join(wai, 'staged-measure-conformance'), '--out', join(out, `measure.${rung}`)]],
];
for (const rung of ['shipped', 'L1', 'L2', 'J']) {
const a = adapter(rung);
for (const [script, argv] of SCRIPTS) {
const r = spawnSync(process.execPath, [join(web, 'demo', `${script}.mjs`), ...argv(a, rung)], { encoding: 'utf8', cwd: web, maxBuffer: 1 << 26 });
const text = (r.stdout || '') + (r.stderr || '');
writeFileSync(join(out, `${script}.${rung}.log`), text);
const ok = r.status === 0;
runs.push({ kind: 'demo', label: `${rung} ${script}`, ok, failed: ok ? [] : text.split('\n').filter((l) => /FAIL|Error/.test(l)).slice(0, 8) });
console.log(`${ok ? 'PASS' : 'FAIL'} ${rung} demo/${script}.mjs${ok ? '' : '\n ' + text.trim().split('\n').slice(-6).join('\n ')}`);
}
if (rung !== 'shipped') {
const A = join(out, 'measure.shipped'), B = join(out, `measure.${rung}`);
const files = existsSync(A) ? readdirSync(A).sort() : [];
const differ = files.filter((f) => !existsSync(join(B, f)) || !readFileSync(join(A, f)).equals(readFileSync(join(B, f))));
const ok = files.length > 0 && differ.length === 0 && readdirSync(B).length === files.length;
runs.push({ kind: 'demo', label: `${rung} measurement outputs equal the shipped build's`, ok, failed: differ });
console.log(`${ok ? 'PASS' : 'FAIL'} ${rung}: the measurement script's ${files.length} written files equal the shipped build's${ok ? '' : ': ' + differ.join(', ')}`);
}
}
const failed = runs.filter((r) => !r.ok);
writeFileSync(join(out, 'summary.json'), JSON.stringify({ engine: `node ${process.version}`, runs }, null, 1) + '\n');
console.log(`\n${runs.length - failed.length} of ${runs.length} runs passed on node ${process.version}${keep ? `; logs in ${out}` : ''}`);
if (!keep) rmSync(out, { recursive: true, force: true });
process.exit(failed.length ? 1 : 0);

View file

@ -0,0 +1,46 @@
# A provenance vector for the reach conformance runs
`captured.jpeg.wai` is a `wai.image.jpeg` envelope and `captured.receipt.json` its signed
asset-provenance receipt (`wai::provenance::AssetReceipt`): three steps
(`origin.synthesize`, `encode.png`, `encode.jpeg`), a Merkle root over them, the total
joules, and an Ed25519 signature. Its pixels are a synthesized pattern (rings and a
gradient), not a photograph.
**Provenance.** Both files are written by `wai_prov` (`wai-rs/src/bin/wai_prov.rs`), which
synthesizes the pixels, encodes them to PNG and then to JPEG, wraps the JPEG and signs the
lineage. The key is the tool's published test seed, the byte `0x70` repeated 32 times
(public key `19204c8c…ef067791`), and the `signer_id` is the tool's label,
`did:example:wai-prov`: a key whose seed is published attests nothing, so its id names no
origin (jwp-receipts §7.1, §7.8). The figures, 426844 µJ for `encode.png` and 140425 µJ
for `encode.jpeg` (unlabelled; `origin.synthesize` is unmetered), are the ones `wai_prov`
measured on the host's on-chip energy counters when it first wrote this asset, at an earlier
revision that signed under an origin-bound id. `--carry` carries them onto the same bytes
and signs them again under the label; nothing is measured again. `wai_prov` carries a
figure only from a receipt that verifies and whose steps have this run's operations, input
and output hashes and work.
To regenerate, from `wai/wai-rs` (the `codecs` feature links the system codec libraries):
```
cargo run --release --no-default-features --features provenance,codecs,receipt_claims --bin wai_prov -- \
../wai-web/reach/test/vectors/provenance \
--carry ../wai-web/reach/test/vectors/provenance/captured.receipt.json
```
The receipt is carried onto itself, so the command writes both files again byte for byte;
on a host whose synthesis or encoders give other bytes, the lineage differs and the
command refuses. Without `--carry`, `wai_prov` measures each encode afresh (a usable
figure is labelled `OnChipCounter`), and the receipt and its hash below change.
The receipt profile of the reach builds exists to check receipts like this one, so the
conformance runs (`../../conformance.js`) verify it on every rung: the content hash and
the signature must verify, a copy with one bit changed must fail the content check, and
a copy with its joule total raised by one must fail the signature check. The receipt's
hash, signer id, step count and joule total must read as the receipt states them; the
total crosses the JavaScript boundary as a 64-bit integer, a `BigInt` where the engine has
one and a `Number` where it does not.
| file | SHA-256 |
|---|---|
| `captured.jpeg.wai` | `4382082d2bbd1ef5358315afd578fc533d74bf973a57d4c45033c89c847a22a3` |
| `captured.receipt.json` | `97c5c24a03eae1c6a2d0e5374ad13c5ca00de00d696f40116f41c66b7097ff64` |

View file

@ -0,0 +1 @@
{"kind":"asset-provenance","asset_hash":"166da2294bfb78cfa93935663249bdb577e701117d9522caa0516fffdf0f5168","capability":"wai.image.jpeg","group_id":0,"media":"image","parent_receipt_hash":null,"receipt_hash":"a48046e0497be1b6efe25fac237a992b8f76fb71583295d7016f17c7f1175762","root_hash":"f9b58b1f0c525c382ff2c02113b1ec2481722ac70bc5cb5497b20aacc0edfce0","sig":"cddfd290ca4c5354226188be098fb45dee597fb1a12d3c10d0d31f57c5cf4480203085b76b8127fd472a4560139557f1e91bb502e5ad1c2841ed1f26047e5800","signer_id":"did:example:wai-prov","signer_pubkey":"19204c8c3ba85ba059cd18ac9017bd02a18da5d10c68fb779a34be43ef067791","steps":[{"input_hash":"0000000000000000000000000000000000000000000000000000000000000000","joules_micro":0,"op":"origin.synthesize","output_hash":"7d263abb93419a8d34f901674ac4297ee1ca47a6f6ea51170955a0de6acc647b","work":262144},{"input_hash":"7d263abb93419a8d34f901674ac4297ee1ca47a6f6ea51170955a0de6acc647b","joules_micro":426844,"op":"encode.png","output_hash":"514689be3f3515a8f2ddcb046603ea77de5f2f5def7768c8e8f08ea79a113f9b","work":262144},{"input_hash":"514689be3f3515a8f2ddcb046603ea77de5f2f5def7768c8e8f08ea79a113f9b","joules_micro":140425,"op":"encode.jpeg","output_hash":"166da2294bfb78cfa93935663249bdb577e701117d9522caa0516fffdf0f5168","work":262144}],"total_joules_micro":567269}

View file

@ -0,0 +1,426 @@
/* The reach conformance workloads, in ECMAScript 5 so that one file runs on every engine
* the proxies stand for.
*
* WaiReachWorkloads.forProfile(profile) -> [name, ...]
* WaiReachWorkloads.run(W, ctx, names) -> { name: { ok, out, err, ms } }
*
* W is the wai-web API a rung loaded (the shipped module or the reach glue: the same
* surface). ctx reads the repository's corpora: ctx.bytes(rel), ctx.text(rel),
* ctx.json(rel), ctx.dirs(rel) (sub-folders, sorted), ctx.sha256(u8) (hex, computed by
* the host, not by the build under test), ctx.now().
*
* Every workload checks its result against an independent reference: a corpus's own
* hashes, a sidecar's SHA-256, the signed receipt's fields, or the registry. Its `out` is
* a canonical summary that the runner compares byte for byte with the shipped build's.
* 64-bit values are turned into strings before they enter `out`, so a BigInt and a
* Number of the same value compare equal.
*/
(function (root) {
'use strict';
var PROFILE_WORKLOADS = {
receipt: ['hashes', 'envelope', 'receipt_verify'],
verify: ['menu', 'stage_ids', 'multi', 'output_tree'],
verify_decode: ['int_payloads', 'int_json', 'int_synth', 'staged_sink'],
full: ['i64_boundary']
};
var INCLUDES = { receipt: [], verify: ['receipt'], verify_decode: ['verify'], full: ['verify_decode'] };
function forProfile(p) {
if (!PROFILE_WORKLOADS[p]) throw new Error('unknown profile ' + p);
var out = [], inc = INCLUDES[p];
for (var i = 0; i < inc.length; i++) out = out.concat(forProfile(inc[i]));
return out.concat(PROFILE_WORKLOADS[p]);
}
function S(v) { return v === null || v === undefined ? String(v) : String(v); }
function keys(o) { var k, a = []; for (k in o) if (Object.prototype.hasOwnProperty.call(o, k)) a.push(k); return a; }
function canon(v, depth) {
depth = depth || 0;
if (depth > 12) return '<deep>';
if (v === undefined) return '<undefined>';
if (v === null) return null;
var t = typeof v;
if (t === 'bigint') return 'BIGINT:' + String(v);
if (t === 'number' || t === 'boolean' || t === 'string') return v;
if (typeof Map !== 'undefined' && v instanceof Map) {
var es = [];
v.forEach(function (val, key) { es.push([canon(key, depth + 1), canon(val, depth + 1)]); });
return { '<map>': es };
}
if (typeof v.length === 'number' && typeof v.BYTES_PER_ELEMENT === 'number') return '<' + v.length + ' x ' + v.BYTES_PER_ELEMENT + ' bytes>';
if (Object.prototype.toString.call(v) === '[object Array]') {
var a = [];
for (var i = 0; i < v.length; i++) a.push(canon(v[i], depth + 1));
return a;
}
var o = {}, ks = keys(v).sort();
for (var j = 0; j < ks.length; j++) o[ks[j]] = canon(v[ks[j]], depth + 1);
return o;
}
function same(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
function errText(e) { return String(e && e.message !== undefined ? e.message : e); }
function errCode(e) { return errText(e).split(':')[0]; }
function concat(list) {
var n = 0, i, o = 0;
for (i = 0; i < list.length; i++) n += list[i].length;
var u = new Uint8Array(n);
for (i = 0; i < list.length; i++) { u.set(list[i], o); o += list[i].length; }
return u;
}
function hexToBytes(h) {
var u = new Uint8Array(h.length / 2);
for (var i = 0; i < u.length; i++) u[i] = parseInt(h.substr(2 * i, 2), 16);
return u;
}
function ascii(s) { var u = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) u[i] = s.charCodeAt(i) & 0xff; return u; }
function u8view(ta) { return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength); }
function copy(u) { var c = new Uint8Array(u.length); c.set(u); return c; }
var PROV = 'wai-web/reach/test/vectors/provenance/';
var V = 'wai-rs/tests/vectors/';
var WORK = {};
// receipt -------------------------------------------------------------------------
WORK.hashes = function (W, ctx) {
var files = [PROV + 'captured.jpeg.wai', V + 'int_video/int_video_bitstream.wiv1', V + 'int_audio/audio_model.bin'];
var out = {}, ok = true;
for (var i = 0; i < files.length; i++) {
var b = ctx.bytes(files[i]);
var r = { sha256: W.sha256Hex(b), blake3: W.blake3Hex(b), fnv1a: W.fnv1aHex(b) };
if (r.sha256 !== ctx.sha256(b)) ok = false;
out[files[i]] = r;
}
return { ok: ok, out: out };
};
WORK.envelope = function (W, ctx) {
var cap = ctx.bytes(PROV + 'captured.jpeg.wai'), receipt = ctx.json(PROV + 'captured.receipt.json');
var e = W.parse(cap);
var out = { capability: S(e.capability), kind: S(e.kind), media: S(e.media), intent: S(e.intent),
fallback: canon(e.fallback), payload_len: S(e.payload_len), version: S(W.detectVersion(cap)) };
e.free();
var refused;
try { W.parse(ascii('not an envelope')); refused = 'parsed'; } catch (x) { refused = errCode(x); }
out.garbage = refused;
return { ok: out.capability === receipt.capability && out.media === receipt.media && out.version === '1' && refused !== 'parsed', out: out };
};
WORK.receipt_verify = function (W, ctx) {
var cap = ctx.bytes(PROV + 'captured.jpeg.wai'), json = ctx.text(PROV + 'captured.receipt.json'), r = JSON.parse(json);
var p = new W.AssetProvenance(cap, json);
var out = { content_ok: p.content_ok(), signature_ok: p.signature_ok(), capability: p.capability(), media: p.media(),
asset: p.asset_hash_hex(), computed: p.computed_hash_hex(), receipt_hash: p.receipt_hash_hex(),
signer: p.signer_id(), steps: p.step_count(), joules_micro: S(p.total_joules_micro()),
payload_sha256: ctx.sha256(p.payload()) };
p.free();
var t = copy(cap);
t[Math.floor(t.length * 0.7)] ^= 0x40;
try { var q = new W.AssetProvenance(t, json); out.tampered = { content_ok: q.content_ok(), signature_ok: q.signature_ok() }; q.free(); }
catch (x) { out.tampered = { refused: errCode(x) }; }
var forged = JSON.parse(json);
forged.total_joules_micro = forged.total_joules_micro + 1;
var f = new W.AssetProvenance(cap, JSON.stringify(forged));
out.forged = { content_ok: f.content_ok(), signature_ok: f.signature_ok() };
f.free();
var ok = out.content_ok === true && out.signature_ok === true && out.asset === r.asset_hash && out.computed === r.asset_hash &&
out.receipt_hash === r.receipt_hash && out.joules_micro === String(r.total_joules_micro) && out.steps === r.steps.length &&
out.signer === r.signer_id && (out.tampered.refused !== undefined || out.tampered.content_ok === false) &&
out.forged.signature_ok === false;
return { ok: ok, out: out };
};
// verify --------------------------------------------------------------------------
WORK.menu = function (W, ctx) {
var reg = ctx.json('registry/capabilities.json').capabilities, byName = {}, i;
for (i = 0; i < reg.length; i++) byName[reg[i].capability] = reg[i];
var menu = Array.prototype.slice.call(W.known_capabilities()), bad = [];
for (i = 0; i < menu.length; i++) {
var c = menu[i], r = byName[c];
if (!r || (r.kind !== 'payload' && r.kind !== 'companion') || r.status !== 'registered' || W.isCandidate(c) || !W.isRegistered(c)) bad.push(c);
}
var disagree = [];
for (i = 0; i < reg.length; i++) {
if ((reg[i].kind === 'payload' || reg[i].kind === 'companion') && W.isCandidate(reg[i].capability) !== (reg[i].status === 'candidate')) disagree.push(reg[i].capability);
}
return { ok: menu.length > 0 && bad.length === 0 && disagree.length === 0, out: { menu: menu, bad: bad, disagree: disagree } };
};
WORK.stage_ids = function (W, ctx) {
var names = ctx.dirs('staged-conformance/cases'), n = 0, bad = [], info = {};
for (var i = 0; i < names.length; i++) {
var dir = 'staged-conformance/cases/' + names[i] + '/', c = ctx.json(dir + 'case.json');
for (var j = 0; j < c.bases.length; j++) {
n++;
if (W.stageContentHash(ctx.bytes(dir + c.bases[j].file)) !== c.bases[j].object) bad.push(names[i] + '/' + c.bases[j].file);
}
if (i % 8 === 0 && c.files.length) info[names[i] + '/' + c.files[0].file] = canon(W.stageLayerInfo(ctx.bytes(dir + c.files[0].file)));
}
return { ok: n > 0 && bad.length === 0, out: { bases: n, bad: bad, layer_info: info } };
};
WORK.multi = function (W, ctx) {
var idx = ctx.json('staged-conformance/wai2/index.json'), out = {}, parsed = 0;
for (var i = 0; i < idx.files.length; i++) {
var f = idx.files[i].file, b = ctx.bytes('staged-conformance/wai2/' + f), r;
try {
var m = W.parseMulti(b);
r = { n: m.n_renditions, media: S(m.media), intent: S(m.intent), meta: canon(m.renditionsMeta()), pins: canon(m.checkPins()) };
m.free();
parsed++;
} catch (x) { r = { refused: errText(x) }; }
out[f] = r;
}
return { ok: parsed > 0, out: out };
};
WORK.output_tree = function (W, ctx) {
var g = ctx.json('staged-measure-conformance/goldens.json').fixtures, out = {}, ok = g.length > 0;
for (var i = 0; i < g.length; i++) {
var outputs = [];
for (var j = 0; j < g[i].presentations.length; j++) {
var p = g[i].presentations[j];
outputs.push({ unit: p.unit, layers: p.layers, decoder: p.decoder, out_digest: p.out_sha256 });
}
var root = W.outputRoot(JSON.stringify(outputs));
if (root !== g[i].output_root) ok = false;
out[g[i].title] = root;
}
return { ok: ok, out: out };
};
// verify_decode --------------------------------------------------------------------
function clipDigest(v, ctx) {
var n = v.width * v.height * 3, rgb = v.rgb, ds = [];
for (var f = 0; f < v.nFrames; f++) {
var one = v.frame(f);
if (one.length !== n) throw new Error('frame ' + f + ' has ' + one.length + ' bytes');
for (var k = 0; k < n; k++) if (one[k] !== rgb[f * n + k]) throw new Error('frame ' + f + ' differs from rgb');
ds.push(hexToBytes(ctx.sha256(one)));
}
return ctx.sha256(concat(ds));
}
WORK.int_payloads = function (W, ctx) {
var side = function (rel, key) { return ctx.json(V + rel)[key]; };
var g = ctx.bytes(V + 'int_hyper/tiny_g_s.bin'), h = ctx.bytes(V + 'int_hyper/tiny_h_s.bin'), e = ctx.bytes(V + 'int_hyper/tiny_entropy.json');
var checks = [
['int_video', function () { var v = W.decodeWiv1('wai.video.int_motion', ctx.bytes(V + 'int_video/int_video_bitstream.wiv1')); var d = clipDigest(v, ctx); v.free(); return d; },
side('int_video/sha256.json', 'int_video').clip_sha256],
['video_real', function () { var v = W.decodeWiv1('wai.video.int_motion', ctx.bytes(V + 'int_video/video_real_bitstream.wiv1')); var d = clipDigest(v, ctx); v.free(); return d; },
side('int_video/sha256.json', 'video_real').clip_sha256],
['video_nk', function () { var v = W.decodeWiv1('wai.video.int_hyper', ctx.bytes(V + 'int_video/video_nk_bitstream.wiv1'), g, h, e); var d = clipDigest(v, ctx); v.free(); return d; },
side('int_video/sha256.json', 'video_nk').clip_sha256],
['tiny', function () { var im = W.decodeWih1(ctx.bytes(V + 'int_hyper/tiny_bitstream.wih1'), g, h, e); var d = ctx.sha256(im.rgb); im.free(); return d; },
side('int_hyper/sha256.json', 'tiny').sha256],
['audio', function () {
var a = W.decodeWia1(ctx.bytes(V + 'int_audio/audio_bitstream.wia1'), ctx.bytes(V + 'int_audio/audio_model.bin'));
if (a.sampleRate !== 16000) return 'sample rate ' + a.sampleRate;
var d = ctx.sha256(u8view(a.pcm)); a.free(); return d;
}, side('int_audio/sha256.json', 'audio').sha256],
['splat', function () {
var s = W.decodeWis1(ctx.bytes(V + 'int_splat/splat.wis1'), ctx.bytes(V + 'int_splat/splat_gs.bin'));
if (s.ranges.length !== 2 * s.nAttr || s.nGaussians !== 1024) return 'ranges or Gaussian count';
var d = ctx.sha256(u8view(s.attr)); s.free(); return d;
}, side('int_splat/sha256.json', 'splat').sha256],
['json_refused', function () {
try { W.decodeWiv1('wai.video.int_motion', ctx.bytes(V + 'int_video/int_video_bitstream.json')); return 'decoded'; }
catch (x) { return errCode(x); }
}, 'bad_magic']
];
var out = {}, ok = true;
for (var i = 0; i < checks.length; i++) {
var got = checks[i][1]();
out[checks[i][0]] = got;
if (got !== checks[i][2]) ok = false;
}
return { ok: ok, out: out };
};
WORK.int_json = function (W, ctx) {
var v = W.decodeIntVideo(ctx.bytes(V + 'int_video/int_video_bitstream.json'));
var vd = clipDigest(v, ctx); v.free();
var a = W.decodeIntAudio(ctx.bytes(V + 'int_audio/audio_model.bin'), ctx.bytes(V + 'int_audio/audio_bitstream.json'));
var conf = ctx.json(V + 'int_audio/audio.json');
var out = { video: vd, audio_samples: a.nSamples, audio_fnv1a: W.fnv1aHex(u8view(a.pcm)), audio_sha256: ctx.sha256(u8view(a.pcm)) };
a.free();
var ok = vd === ctx.json(V + 'int_video/sha256.json').int_video.clip_sha256 && out.audio_samples === conf.n_samples && out.audio_fnv1a === conf.pcm_fnv1a;
return { ok: ok, out: out };
};
WORK.int_synth = function (W, ctx) {
var base = 'neural-conformance/', expected = ctx.json(base + 'expected_hashes.json'), names = ['gradient', 'bloom', 'facade', 'photo'];
var dom = ascii('wai:int-synth-rgb'), out = {}, ok = true;
for (var i = 0; i < names.length; i++) {
var img = W.decodeIntSynth(ctx.bytes(base + 'vectors/' + names[i] + '/latent.bin'), ctx.bytes(base + 'vectors/' + names[i] + '/int_synth.json'));
var rgb = img.rgb, buf = new Uint8Array(dom.length + 1 + rgb.length);
buf.set(dom, 0); buf[dom.length] = 1; buf.set(rgb, dom.length + 1);
var got = W.blake3Hex(buf), want = expected[names[i]];
out[names[i]] = { w: img.width, h: img.height, rgb_hash: got };
if (got !== want.rgb_hash || img.width !== want.width || img.height !== want.height || rgb.length !== want.width * want.height * 3) ok = false;
img.free();
}
return { ok: ok, out: out };
};
// The staged-delivery corpus replayed through StageSink (the replay of
// demo/verify_staged_wasm.mjs; its guards and fuzz run there, on the full builds).
WORK.staged_sink = function (W, ctx) {
var LIMIT = { output_bytes: 64 << 20, symbols: 64 << 20, working_bytes: 256 << 20 };
var names = ctx.dirs('staged-conformance/cases'), fails = [], replayed = 0, stageHashes = 0, digest = [];
function fail(m) { if (fails.length < 12) fails.push(m); else if (fails.length === 12) fails.push('...'); }
function modelSet(set) {
var ks = keys(set || {});
if (!ks.length) return undefined;
var m = new Map();
for (var i = 0; i < ks.length; i++) m.set(ks[i], ctx.bytes(set[ks[i]]));
return m;
}
function withoutClaimed(o) {
var r = {}, ks = keys(o);
for (var i = 0; i < ks.length; i++) if (ks[i] !== 'claimed') r[ks[i]] = o[ks[i]];
return r;
}
for (var ci = 0; ci < names.length; ci++) {
var name = names[ci], dir = 'staged-conformance/cases/' + name + '/', c = ctx.json(dir + 'case.json');
var bases = [], okCase = true, b, i;
for (i = 0; i < c.bases.length; i++) {
b = c.bases[i];
var env = ctx.bytes(dir + b.file), stage0 = ctx.bytes(dir + b.stage0), elem = b.units[0].elem, shapeList = [], base, k;
for (k = 0; k < b.units.length; k++) {
if (b.units[k].elem !== elem) { fail(name + ': mixed element types'); okCase = false; }
shapeList = shapeList.concat(b.units[k].shape);
}
var shapes = new Uint32Array(shapeList.length);
for (k = 0; k < shapeList.length; k++) shapes[k] = shapeList[k];
var make = (function (b, env, stage0, elem, shapes) {
return function () {
var x = b.decode.kind === 'int'
? W.StageBase.decode(env, modelSet(b.decode.set), b.decoded_via, LIMIT)
: W.StageBase.fromStage0(env, b.decoded_via, b.tier, shapes, elem, stage0);
x.attested = b.attested;
x.setFirstUnit(b.first_unit);
return x;
};
})(b, env, stage0, elem, shapes);
try { base = make(); } catch (x) { fail(name + ': ' + b.file + ': ' + errText(x)); okCase = false; continue; }
if (b.decode.kind === 'int') {
if (ctx.sha256(base.stage0()) !== b.stage0_sha256) { fail(name + ': the decode of ' + b.file + ' is not ' + b.stage0); okCase = false; }
if (base.tier !== b.tier || base.viaFallback !== b.via_fallback || base.capability !== b.decoded_via) { fail(name + ': base facts'); okCase = false; }
} else if (!base.claimed || base.viaFallback !== b.via_fallback || base.tier !== b.tier) { fail(name + ': claimed base facts'); okCase = false; }
bases.push({ base: base, hash: base.contentHash, claimed: base.claimed, remake: make, spent: false });
}
if (!okCase) continue;
var files = {};
for (i = 0; i < c.files.length; i++) files[c.files[i].file] = ctx.bytes(dir + c.files[i].file);
var sink = new W.StageSink(c.config), reports = [], s, got;
for (i = 0; i < c.steps.length; i++) {
s = c.steps[i];
if (s.op === 'offer') {
try { got = sink.offer(files[s.file], s.at_ms, s.evidence); } catch (x) { got = { error: errCode(x) }; }
} else if (s.op === 'base') {
var bb = bases[s.index];
if (bb.spent) bb.base = bb.remake();
got = sink.addBase(bb.base);
bb.spent = true;
} else if (s.op === 'expire') got = sink.expire(s.at_ms);
else if (s.op === 'remove') { got = sink.removeBase(bases[s.index].hash); if (got === undefined || got === null) got = []; }
else if (s.op === 'attest') { sink.markAttested(bases[s.index].hash); got = []; }
else { fail(name + ': unknown op ' + s.op); got = null; }
if (Object.prototype.toString.call(got) === '[object Array]') {
var un = [];
for (var r = 0; r < got.length; r++) {
if (got[r].claimed !== undefined && got[r].claimed !== true) fail(name + ': claimed is ' + got[r].claimed);
un.push(withoutClaimed(got[r]));
}
got = un;
}
reports.push(got);
}
if (reports.length !== c.expect.reports.length) fail(name + ': ' + reports.length + ' steps');
for (i = 0; i < c.expect.reports.length; i++) {
if (!same(reports[i], c.expect.reports[i])) fail(name + ': step ' + i + ' (' + c.steps[i].op + ') reports ' + JSON.stringify(reports[i]).slice(0, 120));
}
for (i = 0; i < c.expect.bases.length; i++) {
var want = c.expect.bases[i], h = bases[i].hash, raw = sink.units(h), fin = null;
if (raw !== null) {
fin = withoutClaimed(raw);
var us = [];
for (var u = 0; u < raw.units.length; u++) us.push(withoutClaimed(raw.units[u]));
fin.units = us;
}
if (!same(fin, want)) { fail(name + ': base ' + i + ' final units differ'); continue; }
if (fin === null) continue;
for (var kk = 0; kk < fin.units.length; kk++) {
var unit = fin.units[kk], sh = sink.unitSha256(h, kk);
if ((sh ? sh.sha256 : null) !== unit.stage_sha256) fail(name + ': base ' + i + ' unit ' + kk + ': unitSha256');
if (unit.stage_sha256 !== null) {
stageHashes++;
var buf;
try { buf = sink.unitBytes(h, kk); } catch (x) { buf = u8view(sink.unitPcm(h, kk)); }
if (ctx.sha256(buf) !== unit.stage_sha256) fail(name + ': base ' + i + ' unit ' + kk + ': the output does not hash to its stage hash');
}
}
var clip = sink.clipSha256(h);
if ((clip ? clip.sha256 : null) !== want.clip_sha256) fail(name + ': base ' + i + ': clipSha256');
}
digest.push(JSON.stringify(reports));
sink.free();
for (i = 0; i < bases.length; i++) if (!bases[i].spent) bases[i].base.free();
replayed++;
}
return { ok: replayed === names.length && replayed > 0 && fails.length === 0,
out: { cases: names.length, replayed: replayed, stage_hashes: stageHashes, fails: fails, reports_sha256: ctx.sha256(ascii(digest.join('\n'))) } };
};
// full ------------------------------------------------------------------------------
// The 64-bit boundary: i64 parameters and results through the glue, with values on
// both sides of 2^32.
WORK.i64_boundary = function (W, ctx) {
var I64 = function (n) { return typeof root.BigInt === 'function' ? root.BigInt(n) : n; };
var seed = new Uint8Array(32), node = new Uint8Array(16), i;
for (i = 0; i < 32; i++) seed[i] = 0x2a;
for (i = 0; i < 16; i++) node[i] = i + 1;
var actor = new W.Actor(seed);
var op1 = actor.sign_truss_impulse(I64(7), node, 0.25, -0.5, 1.0);
actor.observe_lamport(I64(100));
var op2 = actor.sign_truss_impulse(I64(9), node, 0.25, -0.5, 1.0);
actor.observe_lamport(I64(5000000000));
var op3 = actor.sign_truss_impulse(I64(4294967297), node, 0.25, -0.5, 1.0);
// 2^32 + 2^31: a low half with its top bit set, in both directions.
actor.observe_lamport(I64(6442450944));
var op4 = actor.sign_truss_impulse(I64(6442450944), node, 0.25, -0.5, 1.0);
var out = { pub: actor.pubkey_hex(), op1: W.fnv1aHex(op1), lamport1: S(W.op_lamport(op1)), op2: W.fnv1aHex(op2),
lamport2: S(W.op_lamport(op2)), op3: W.fnv1aHex(op3), lamport3: S(W.op_lamport(op3)),
op4: W.fnv1aHex(op4), lamport4: S(W.op_lamport(op4)), quantize: S(W.quantizeScale(0.5, 16)),
// i64 results returned directly (op_lamport returns through memory): a low half with its
// top bit set, with a zero and a non-zero high half.
quantize32_low: S(W.quantizeScale(0.75, 32)), quantize32_high: S(W.quantizeScale(1.5, 32)),
session: W.worldSessionHash('0707070707070707070707070707070707070707070707070707070707070707', I64(0), 64, 64, 24,
'2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a',
ascii('[{"lamport":1,"tick":1,"dx":1,"dy":0,"buttons":0},{"lamport":2,"tick":2,"dx":0,"dy":1,"buttons":0}]')) };
actor.free();
var ok = out.lamport1 === '1' && out.lamport2 === '101' && out.lamport3 === '5000000001' && out.lamport4 === '6442450945' &&
out.quantize === '32768' && out.quantize32_low === '3221225472' && out.quantize32_high === '6442450944' && out.session.length === 64;
return { ok: ok, out: out };
};
function run(W, ctx, names) {
var R = {};
for (var i = 0; i < names.length; i++) {
var name = names[i], rec = { ok: false, out: null, err: null, ms: 0 }, t0 = ctx.now();
try {
if (!WORK[name]) throw new Error('no workload ' + name);
var res = WORK[name](W, ctx);
rec.ok = res.ok === true;
rec.out = canon(res.out);
} catch (e) {
rec.err = errText(e) + (e && e.name ? ' [' + e.name + ']' : '');
}
rec.ms = Math.round(ctx.now() - t0);
R[name] = rec;
}
return R;
}
root.WaiReachWorkloads = { forProfile: forProfile, run: run, canon: canon };
})(typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this);

View file

@ -0,0 +1,311 @@
// The reach build's steps that are not binaryen. build.sh calls each one; every step
// refuses input it does not recognise rather than produce a quietly different build.
//
// node cli.mjs sigs <module.wasm> <out.js>
// node cli.mjs graph <module.wasm> <profiles.json> <profile> <out.json>
// node cli.mjs exports <module.wasm> <graph.json> the module exports exactly the graph's names
// node cli.mjs same-sigs <a.wasm> <b.wasm> the two modules have one i64 boundary
// node cli.mjs factory <wasm2js.mjs> <out.js>
// node cli.mjs glue <wai_web.js> <out.js> <notes.json>
// node cli.mjs minify <in.js> <out.js>
// node cli.mjs es5 <file>... each file parses as ECMAScript 5
// node cli.mjs manifest <dist> <input-dir> <notes.json> <tools.json> <out.json>
//
// The JavaScript tools (TypeScript, esbuild, acorn) are the versions package-lock.json
// pins, installed by build.sh into $WAI_REACH_NODE_MODULES.
import fs from 'node:fs';
import path from 'node:path';
import zlib from 'node:zlib';
import crypto from 'node:crypto';
import { createRequire } from 'node:module';
import { readModule, i64Boundary } from './wasm.mjs';
const NM = process.env.WAI_REACH_NODE_MODULES;
const req = NM ? createRequire(path.join(NM, 'noop.js')) : null;
const tool = (name) => {
if (!req) throw new Error('WAI_REACH_NODE_MODULES is not set (build.sh sets it)');
return req(name);
};
const read = (p) => fs.readFileSync(p);
const readText = (p) => fs.readFileSync(p, 'utf8');
const die = (msg) => { console.error(`reach: ${msg}`); process.exit(1); };
const ROOT_EXPR = "typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this";
// ---- sigs: the i64 boundary table the runtime needs -------------------------------
function sigs(wasmPath, outPath) {
const s = i64Boundary(readModule(read(wasmPath)));
const js = '/* The i64 boundary of wai_web_bg.wasm: every import and export with an i64 parameter or result (generated by the reach build). */\n' +
`(function (root) { root.WaiReachSigs = ${JSON.stringify(s)}; })(${ROOT_EXPR});\n`;
fs.writeFileSync(outPath, js);
console.log(`sigs: ${Object.keys(s.exports).length} exports and ${Object.keys(s.imports).length} imports cross the i64 boundary`);
}
function sameSigs(a, b) {
const x = JSON.stringify(i64Boundary(readModule(read(a))));
const y = JSON.stringify(i64Boundary(readModule(read(b))));
if (x !== y) throw new Error(`${path.basename(a)} and ${path.basename(b)} have different i64 boundaries`);
console.log(`same-sigs: ${path.basename(a)} and ${path.basename(b)} agree`);
}
// ---- graph: the wasm-metadce graph of one profile ---------------------------------
function profileRules(profilesPath, profile) {
const P = JSON.parse(readText(profilesPath));
const names = new Set(P.always.names), prefixes = [...P.always.prefixes];
const seen = new Set();
const add = (p) => {
if (seen.has(p)) return;
seen.add(p);
const r = P.profiles[p];
if (!r) throw new Error(`no profile '${p}' in ${profilesPath}`);
for (const inc of r.includes ?? []) add(inc);
for (const n of r.names ?? []) names.add(n);
for (const x of r.prefixes ?? []) prefixes.push(x);
};
add(profile);
return { names, prefixes };
}
function keptExports(wasmPath, profilesPath, profile) {
const all = readModule(read(wasmPath)).exports.map((e) => e.name);
const { names, prefixes } = profileRules(profilesPath, profile);
const frees = new Set(prefixes.filter((x) => !x.startsWith('__')).map((x) => `__wbg_${x}free`));
const keep = all.filter((n) => names.has(n) || frees.has(n) || prefixes.some((x) => n.startsWith(x)));
const missing = [...names].filter((n) => !all.includes(n));
if (missing.length) throw new Error(`profile ${profile} names exports the module lacks: ${missing.join(', ')}`);
for (const x of prefixes) if (!all.some((n) => n.startsWith(x))) throw new Error(`profile ${profile}: no export starts with ${x}`);
return { all, keep };
}
function graph(wasmPath, profilesPath, profile, outPath) {
const { all, keep } = keptExports(wasmPath, profilesPath, profile);
const g = [{ name: 'root', root: true, reaches: keep.map((_, i) => `e${i}`) }, ...keep.map((n, i) => ({ name: `e${i}`, export: n }))];
fs.writeFileSync(outPath, JSON.stringify(g));
console.log(`graph ${profile}: keeps ${keep.length} of ${all.length} exports`);
}
function checkExports(wasmPath, graphPath) {
const want = JSON.parse(readText(graphPath)).filter((n) => n.export).map((n) => n.export).sort();
const got = readModule(read(wasmPath)).exports.map((e) => e.name).sort();
if (JSON.stringify(want) !== JSON.stringify(got)) {
const extra = got.filter((n) => !want.includes(n)), lost = want.filter((n) => !got.includes(n));
throw new Error(`${path.basename(wasmPath)}: exports differ from the profile (extra: ${extra.slice(0, 5).join(', ') || 'none'}; lost: ${lost.slice(0, 5).join(', ') || 'none'})`);
}
console.log(`exports: ${path.basename(wasmPath)} exports exactly its profile's ${got.length}`);
}
// ---- factory: the wasm2js ES module as a classic script ----------------------------
// wasm2js writes `import * as X from '...'` lines, the translation, then
// `var retasmFunc = asmFunc({...});` and one `export var` line per export. The imports
// and everything from the instantiation on are dropped; the rest is wrapped in
// WaiReachFactory(imports), which returns the module's exports.
function factory(inPath, outPath) {
const lines = readText(inPath).split('\n');
const body = [];
let imports = 0, cut = false;
for (const l of lines) {
if (cut) {
if (/^export var \S+ = retasmFunc\.\S+;$/.test(l) || /^\s*("[^"]+": \S+,?|\}\);)?\s*$/.test(l)) continue;
throw new Error(`unexpected line after the instantiation: ${l.slice(0, 80)}`);
}
if (/^import \* as \S+ from '[^']+';\s*$/.test(l)) { imports++; continue; }
if (/^var retasmFunc = asmFunc\(/.test(l)) { cut = true; continue; }
body.push(l);
}
if (!cut || imports === 0) throw new Error('the wasm2js layout changed (no imports or no instantiation found)');
const text = body.join('\n');
if (!/^function asmFunc\(imports\) \{$/m.test(text)) throw new Error('no asmFunc(imports)');
if (/^\s*(import|export)\s/m.test(text)) throw new Error('an import or export statement is left in the translation');
if (/\bWebAssembly\b/.test(text)) throw new Error('the translation refers to WebAssembly');
const out = '/* WAI wasm2js factory: binaryen wasm2js translation of the MVP-lowered wai_web_bg.wasm. ES5 and typed arrays; no WebAssembly. */\n' +
'(function (root) {\nfunction WaiReachFactory(imports) {\n' + text + '\nreturn asmFunc(imports);\n}\n' +
`root.WaiReachFactory = WaiReachFactory;\n})(${ROOT_EXPR});\n`;
fs.writeFileSync(outPath, out);
console.log(`factory: ${path.basename(outPath)} (${imports} import lines and the instantiation dropped)`);
}
// ---- glue: the wasm-bindgen glue, made loadable by the reach rungs -----------------
// The shipped glue is an ES module for engines with WebAssembly, BigInt-to-i64 and ES
// modules. The reach glue is the same code with five changes, each counted and checked:
// 1. init: the WebAssembly loading at the end is replaced by two hooks the loader
// calls, __reachImports() and __reachFinalize(instance);
// 2. memory views: the glue rebuilt a cached view only when its buffer was detached,
// which a real WebAssembly.Memory does on growth and wasm2js's memory does not; it
// now rebuilds whenever wasm.memory.buffer is a different buffer;
// 3. Symbol.dispose is read only where Symbol exists;
// 4. 64-bit values: with BigInt the helpers do what the glue did; without it they
// carry the value as a Number (exact below 2^53);
// 5. typed-array copies: `view.slice()` on wasm memory becomes an explicit copy, as
// early typed-array engines return a view from slice().
// It is then compiled to ES5 by TypeScript and wrapped as a classic script that
// defines the global WaiWeb.
function glue(inPath, outPath, notesPath) {
let g = readText(inPath);
const notes = {};
const count = (re) => (g.match(re) || []).length;
if (/\bimport\.meta\b/.test(g.slice(0, g.indexOf('async function __wbg_load(')))) throw new Error('import.meta before the init code');
const cutAt = g.indexOf('async function __wbg_load(');
if (cutAt < 0) throw new Error('no __wbg_load: the glue layout changed');
const tail = g.slice(cutAt);
if (!tail.includes('__wbg_finalize_init(instance, module)') || !/export \{ initSync, __wbg_init as default \};\s*$/.test(tail)) {
throw new Error('the init code changed');
}
if (!/^function __wbg_get_imports\(\)/m.test(g.slice(0, cutAt)) || !/^function __wbg_finalize_init\(instance, module\)/m.test(g.slice(0, cutAt))) {
throw new Error('no __wbg_get_imports or __wbg_finalize_init before the init code');
}
g = g.slice(0, cutAt) + [
'// reach init: the reach loader instantiates the module (a legalised wasm, or the wasm2js',
'// translation) and hands the instance here; the glue never fetches or compiles.',
'function __reachImports() { return __wbg_get_imports(); }',
'function __reachFinalize(instance) {',
' if (wasm !== undefined) return wasm;',
' return __wbg_finalize_init(instance, null);',
'}',
'export { __reachImports, __reachFinalize };',
'',
].join('\n');
if (/\bWebAssembly\b/.test(g)) throw new Error('the glue still refers to WebAssembly outside the init code');
// 2. memory views keyed on buffer identity
const caches = count(/^let cached\w+Memory0 = null;$/gm);
let n = 0;
g = g.replace(/(cached\w+Memory0) === null \|\| \1\.byteLength === 0/g, (m, v) => { n++; return `${v} === null || ${v}.buffer !== wasm.memory.buffer`; });
const dv = 'cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)';
if (g.includes(dv)) { g = g.replace(dv, 'cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer !== wasm.memory.buffer'); n++; }
if (n !== caches || caches === 0) throw new Error(`memory views: ${caches} caches, ${n} checks rewritten`);
if (/Memory0\.byteLength === 0|\.detached\b/.test(g)) throw new Error('a memory-view check was left unrewritten');
notes.memory_view_checks = n;
// 3. Symbol.dispose guarded
notes.symbol_dispose_guards = 0;
g = g.replace(/^if \(Symbol\.dispose\)/gm, () => { notes.symbol_dispose_guards++; return "if (typeof Symbol !== 'undefined' && Symbol.dispose)"; });
if (/^(?!if \(typeof Symbol !== 'undefined' && Symbol\.dispose\)).*\bSymbol\.dispose\b/m.test(g)) throw new Error('an unguarded Symbol.dispose is left');
// 4. BigInt made optional
let b = 0;
g = g.replace(/BigInt\.asUintN\(64, (\w+)\)/g, (m, v) => { b++; return `__wai_u64(${v})`; });
g = g.replace(/getDataViewMemory0\(\)\.getBigInt64\(([^,()]+), true\)/g, (m, p) => { b++; return `__wai_get_i64(${p})`; });
g = g.replace(/getDataViewMemory0\(\)\.setBigInt64\(([^,()]+), isLikeNone\(ret\) \? BigInt\(0\) : ret, true\)/g, (m, p) => { b++; return `__wai_set_i64(${p}, isLikeNone(ret) ? 0 : ret)`; });
notes.bigint_sites_made_optional = b;
// What may still need BigInt, by design: the u128 value IntPayloadCost.macs, and the
// BigInt64Array / BigUint64Array views (built only when a function returns one).
const stillBigInt = g.split('\n').filter((l) => /\bBigInt/.test(l) && !/^\s*(\*|\/\/)/.test(l));
const allowed = [/<< BigInt\(64\)\)\);$/, /new BigInt64Array\(wasm\.memory\.buffer\)/, /new BigUint64Array\(wasm\.memory\.buffer\)/,
/cachedBig(Int|Uint)64ArrayMemory0/, /getBig(Int|Uint)64ArrayMemory0\(\)/, /^function getArrayBig(I|U)\w*FromWasm0/];
const unexpected = stillBigInt.filter((l) => !allowed.some((re) => re.test(l)));
if (unexpected.length) throw new Error(`BigInt left where a Number path was expected: ${unexpected[0].trim().slice(0, 100)}`);
notes.bigint_required_by = stillBigInt.length;
// 5. typed-array copies that do not rely on TypedArray#slice
let c = 0;
g = g.replace(/getArray(\w+)FromWasm0\(([^()]*)\)\.slice\(\)/g, (m, t, a) => { c++; return `__wai_ta_copy(getArray${t}FromWasm0(${a}))`; });
if (/FromWasm0\([^()]*\)\.slice\(/.test(g)) throw new Error('a typed-array slice() on wasm memory is left');
notes.typed_array_copies = c;
g = [
'// reach helpers (see tools/cli.mjs, glue)',
"const __WAI_HAS_BIGINT = typeof BigInt === 'function';",
'function __wai_u64(v) { return __WAI_HAS_BIGINT ? BigInt.asUintN(64, v) : (v < 0 ? v + 18446744073709551616 : v); }',
'function __wai_get_i64(p) {',
' const dv = getDataViewMemory0();',
' return __WAI_HAS_BIGINT ? dv.getBigInt64(p, true) : dv.getInt32(p + 4, true) * 4294967296 + dv.getUint32(p, true);',
'}',
'function __wai_set_i64(p, v) {',
' const dv = getDataViewMemory0();',
" if (__WAI_HAS_BIGINT) { dv.setBigInt64(p, typeof v === 'bigint' ? v : BigInt(v), true); return; }",
' const lo = v % 4294967296;',
' dv.setUint32(p, lo < 0 ? lo + 4294967296 : lo, true);',
' dv.setInt32(p + 4, Math.floor(v / 4294967296), true);',
'}',
'function __wai_ta_copy(v) { const o = new v.constructor(v.length); o.set(v); return o; }',
g,
].join('\n');
const ts = tool('typescript');
const r = ts.transpileModule(g, {
compilerOptions: {
target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS, allowJs: true,
downlevelIteration: true, importHelpers: false, removeComments: true,
},
reportDiagnostics: true,
});
if (r.diagnostics && r.diagnostics.length) {
throw new Error(`TypeScript: ${ts.flattenDiagnosticMessageText(r.diagnostics[0].messageText, ' ')}`);
}
const out = '/* WAI wasm-bindgen glue, reach build: ES5 classic script. Global: WaiWeb. */\n' +
'(function (root) {\nvar exports = {}; var module = { exports: exports };\n' + r.outputText +
`\nroot.WaiWeb = module.exports;\n})(${ROOT_EXPR});\n`;
fs.writeFileSync(outPath, out);
fs.writeFileSync(notesPath, JSON.stringify(notes, null, 2) + '\n');
console.log(`glue: ${JSON.stringify(notes)}`);
}
// ---- minify: esbuild, ES5 output ---------------------------------------------------
function minify(inPath, outPath) {
const esbuild = tool('esbuild');
const r = esbuild.transformSync(readText(inPath), { minify: true, target: 'es5', charset: 'ascii', legalComments: 'none', logLevel: 'error' });
fs.writeFileSync(outPath, r.code);
}
// ---- es5: each file parses as ECMAScript 5 -----------------------------------------
function es5(...files) {
const acorn = tool('acorn');
for (const f of files) {
try {
acorn.parse(readText(f), { ecmaVersion: 5, sourceType: 'script', allowReserved: true });
} catch (e) {
throw new Error(`${path.basename(f)} is not ECMAScript 5: ${e.message}`);
}
}
console.log(`es5: ${files.length} files parse as ECMAScript 5`);
}
// ---- manifest ------------------------------------------------------------------------
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
const sizes = (buf) => ({
sha256: sha256(buf),
bytes: buf.length,
gzip: zlib.gzipSync(buf, { level: 9 }).length,
brotli: zlib.brotliCompressSync(buf, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, [zlib.constants.BROTLI_PARAM_LGWIN]: 22 } }).length,
});
function manifest(dist, inputDir, notesPath, toolsPath, outPath) {
const RUNG = [
[/^wai_web_bg\.l1\.wasm$/, 'L1', 'full', 'the shipped binary, legalised at the JS interface'],
[/^wai_web_bg\.l2\.(\w+)\.wasm$/, 'L2', null, 'MVP-lowered and legalised'],
[/^wai_web\.reach\.js$/, 'L1, L2', 'full', 'runtime + i64 table + glue, for the wasm rungs'],
[/^wai_web\.j\.(\w+)\.js$/, 'J', null, 'wasm2js bundle: runtime + i64 table + translation + glue'],
[/^wai-load\.js$/, 'all', null, 'the loader'],
];
const files = {};
for (const f of fs.readdirSync(dist).sort()) {
const st = fs.statSync(path.join(dist, f));
if (!st.isFile() || f === 'manifest.json') continue;
const row = RUNG.find(([re]) => re.test(f));
if (!row) throw new Error(`${f} is not a reach artifact`);
const [re, rung, profile, what] = row;
files[f] = { rung, profile: profile ?? (f.match(re)[1] ?? null), what, ...sizes(read(path.join(dist, f))) };
}
const input = { files: {}, source: process.env.WAI_REACH_SOURCE || null, commit: process.env.WAI_REACH_INPUT_COMMIT || null };
for (const f of ['wai_web_bg.wasm', 'wai_web.js']) input.files[f] = { sha256: sha256(read(path.join(inputDir, f))), bytes: read(path.join(inputDir, f)).length };
const m = {
format: 'wai-reach-manifest/1',
input,
tools: JSON.parse(readText(toolsPath)),
glue: JSON.parse(readText(notesPath)),
outputs: files,
};
fs.writeFileSync(outPath, JSON.stringify(m, null, 2) + '\n');
console.log(`manifest: ${Object.keys(files).length} artifacts`);
}
// ---- dispatch (last, so every step above is initialised) ----------------------------
const [cmd, ...args] = process.argv.slice(2);
const commands = { sigs, graph, exports: checkExports, 'same-sigs': sameSigs, factory, glue, minify, es5, manifest };
if (!commands[cmd]) die(`unknown step '${cmd}'`);
try {
commands[cmd](...args);
} catch (e) {
die(`${cmd}: ${e.message}`);
}

View file

@ -0,0 +1,119 @@
// A small reader for the parts of a WebAssembly binary the reach build needs: the
// function signatures of every import and export. It reads the type, import,
// function and export sections and skips the rest, so the build does not depend on
// any tool's text format.
//
// import { readModule } from './wasm.mjs';
// const m = readModule(bytes);
// m.imports [{ module, name, kind, params?, results? }]
// m.exports [{ name, kind, params?, results? }] kind: func | table | memory | global | tag
const VALTYPE = { 0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64', 0x7b: 'v128', 0x70: 'funcref', 0x6f: 'externref' };
const KIND = ['func', 'table', 'memory', 'global', 'tag'];
export function readModule(bytes) {
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
let p = 0;
const fail = (msg) => { throw new Error(`wasm: ${msg} at byte ${p}`); };
const u8 = () => (p < b.length ? b[p++] : fail('unexpected end'));
const leb = () => {
let result = 0, shift = 0, byte;
do {
byte = u8();
result += (byte & 0x7f) * 2 ** shift;
shift += 7;
if (shift > 35) fail('LEB128 too long');
} while (byte & 0x80);
return result;
};
const name = () => {
const n = leb();
if (p + n > b.length) fail('name runs past the end');
const s = new TextDecoder('utf-8', { fatal: true }).decode(b.subarray(p, p + n));
p += n;
return s;
};
const valtype = () => {
const t = u8();
return VALTYPE[t] ?? fail(`value type 0x${t.toString(16)} not read here`);
};
const limits = () => {
const flag = u8();
if (flag > 7) fail(`limits flag 0x${flag.toString(16)}`);
leb();
if (flag & 1) leb();
};
if (b.length < 8 || b[0] !== 0 || b[1] !== 0x61 || b[2] !== 0x73 || b[3] !== 0x6d) fail('not a WebAssembly binary');
if (b[4] !== 1 || b[5] !== 0 || b[6] !== 0 || b[7] !== 0) fail('not version 1');
p = 8;
const types = [];
const imports = [];
const funcTypes = []; // type index of every function, imported ones first
const exports = [];
while (p < b.length) {
const id = u8();
const size = leb();
const end = p + size;
if (end > b.length) fail(`section ${id} runs past the end`);
if (id === 1) {
const n = leb();
for (let i = 0; i < n; i++) {
const form = u8();
if (form !== 0x60) fail(`type form 0x${form.toString(16)} not read here`);
const params = Array.from({ length: leb() }, valtype);
const results = Array.from({ length: leb() }, valtype);
types.push({ params, results });
}
} else if (id === 2) {
const n = leb();
for (let i = 0; i < n; i++) {
const module = name(), field = name(), kind = u8();
if (kind === 0) {
const t = leb();
funcTypes.push(t);
imports.push({ module, name: field, kind: 'func', ...types[t] });
} else if (kind === 1) { u8(); limits(); imports.push({ module, name: field, kind: 'table' }); }
else if (kind === 2) { limits(); imports.push({ module, name: field, kind: 'memory' }); }
else if (kind === 3) { valtype(); u8(); imports.push({ module, name: field, kind: 'global' }); }
else if (kind === 4) { u8(); leb(); imports.push({ module, name: field, kind: 'tag' }); }
else fail(`import kind ${kind}`);
}
} else if (id === 3) {
const n = leb();
for (let i = 0; i < n; i++) funcTypes.push(leb());
} else if (id === 7) {
const n = leb();
for (let i = 0; i < n; i++) {
const field = name(), kind = u8(), index = leb();
const e = { name: field, kind: KIND[kind] ?? fail(`export kind ${kind}`), index };
if (kind === 0) Object.assign(e, types[funcTypes[index]] ?? fail(`export ${field}: no type for function ${index}`));
exports.push(e);
}
}
p = end;
}
return { types, imports, exports };
}
// The i64 boundary of a module: every function import and export with an i64
// parameter or result. The reach runtime splits and joins these values at the JS
// boundary (an i64 crosses as a (low, high) pair of i32 values, and a result's high
// half travels through env.setTempRet0). A multi-value result is refused: neither
// legalisation nor the runtime handles one.
export function i64Boundary(m) {
const sig = (f) => {
if (f.results.length > 1) throw new Error(`${f.name}: a multi-value result is not supported`);
return { params: f.params, result: f.results[0] ?? null };
};
const has64 = (f) => f.params.includes('i64') || f.results.includes('i64');
const exports = {}, imports = {};
for (const e of m.exports) if (e.kind === 'func' && has64(e)) exports[e.name] = sig(e);
for (const i of m.imports) {
if (i.kind !== 'func' || !has64(i)) continue;
if (imports[i.name]) throw new Error(`import ${i.name} appears under two modules`);
imports[i.name] = { module: i.module, ...sig(i) };
}
return { exports, imports };
}