wai-web: reach builds — L1, L2, J and an ES5 loader (CI verdict pending) #205
26 changed files with 15747 additions and 13 deletions
220
.github/workflows/wai-web-reach.yml
vendored
Normal file
220
.github/workflows/wai-web-reach.yml
vendored
Normal 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
5
wai/.gitignore
vendored
|
|
@ -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*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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? |
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
||||
|
|
|
|||
|
|
@ -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
222
wai/wai-web/reach/README.md
Normal 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
207
wai/wai-web/reach/build.sh
Normal 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
|
||||
140
wai/wai-web/reach/manifest.json
Normal file
140
wai/wai-web/reach/manifest.json
Normal 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
546
wai/wai-web/reach/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
wai/wai-web/reach/package.json
Normal file
12
wai/wai-web/reach/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
32
wai/wai-web/reach/profiles.json
Normal file
32
wai/wai-web/reach/profiles.json
Normal 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_"]
|
||||
}
|
||||
}
|
||||
}
|
||||
12364
wai/wai-web/reach/record/wai_web.js
Normal file
12364
wai/wai-web/reach/record/wai_web.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
wai/wai-web/reach/record/wai_web_bg.wasm
Normal file
BIN
wai/wai-web/reach/record/wai_web_bg.wasm
Normal file
Binary file not shown.
310
wai/wai-web/reach/src/wai-load.js
Normal file
310
wai/wai-web/reach/src/wai-load.js
Normal 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);
|
||||
281
wai/wai-web/reach/src/wai-reach-runtime.js
Normal file
281
wai/wai-web/reach/src/wai-reach-runtime.js
Normal 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);
|
||||
122
wai/wai-web/reach/test/conformance.js
Normal file
122
wai/wai-web/reach/test/conformance.js
Normal 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;
|
||||
}
|
||||
36
wai/wai-web/reach/test/es5-strip.js
Normal file
36
wai/wai-web/reach/test/es5-strip.js
Normal 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;
|
||||
49
wai/wai-web/reach/test/node-host.js
Normal file
49
wai/wai-web/reach/test/node-host.js
Normal 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 };
|
||||
120
wai/wai-web/reach/test/run-modern.mjs
Normal file
120
wai/wai-web/reach/test/run-modern.mjs
Normal 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);
|
||||
46
wai/wai-web/reach/test/vectors/provenance/README.md
Normal file
46
wai/wai-web/reach/test/vectors/provenance/README.md
Normal 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` |
|
||||
BIN
wai/wai-web/reach/test/vectors/provenance/captured.jpeg.wai
Normal file
BIN
wai/wai-web/reach/test/vectors/provenance/captured.jpeg.wai
Normal file
Binary file not shown.
|
|
@ -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}
|
||||
426
wai/wai-web/reach/test/workloads.js
Normal file
426
wai/wai-web/reach/test/workloads.js
Normal 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);
|
||||
311
wai/wai-web/reach/tools/cli.mjs
Normal file
311
wai/wai-web/reach/tools/cli.mjs
Normal 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}`);
|
||||
}
|
||||
119
wai/wai-web/reach/tools/wasm.mjs
Normal file
119
wai/wai-web/reach/tools/wasm.mjs
Normal 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 };
|
||||
}
|
||||
Loading…
Reference in a new issue