From c4e592cfa4ed2095b662837c3959e743e3f3a10f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 18:36:04 -0400 Subject: [PATCH 1/9] =?UTF-8?q?wai-web:=20reach=20builds=20=E2=80=94=20L1?= =?UTF-8?q?=20legalised,=20L2=20MVP-lowered,=20J=20wasm2js,=20and=20an=20E?= =?UTF-8?q?S5=20loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shipped wai-web package needs WebAssembly with sign-extension, non-trapping float-to-int and bulk memory, and i64 crossing the JS boundary as BigInt. reach/build.sh derives three more builds from that package with pinned tools (binaryen version_123 by archive SHA-256; TypeScript, esbuild and acorn by package-lock.json): - L1: the shipped binary legalised at the JS interface, for engines where i64 does not cross as BigInt; - L2: MVP-lowered and legalised, at four profiles (full, verify_decode, verify, receipt), for MVP-only WebAssembly; - J: the wasm2js translation, one ES5 script per profile, with an ES5-floor runtime and glue: memory views refreshed when the buffer changes, i64 values carried as Numbers where there is no BigInt. wai-load.js (ES5) picks shipped, L1, L2 or J from feature probes alone and says why; a rung can be forced for testing. test/conformance.js (ES5) runs the workloads over the repository's corpora on any rung and compares every output with the shipped build's; run-modern.mjs forces every rung and profile on a modern Node and runs the demo verify scripts against each full build. Co-Authored-By: Claude Opus 5.5 --- wai/.gitignore | 3 + wai/wai-web/reach/build.sh | 185 ++++++ wai/wai-web/reach/package-lock.json | 527 ++++++++++++++++++ wai/wai-web/reach/package.json | 11 + wai/wai-web/reach/profiles.json | 32 ++ wai/wai-web/reach/src/wai-load.js | 303 ++++++++++ wai/wai-web/reach/src/wai-reach-runtime.js | 281 ++++++++++ wai/wai-web/reach/test/conformance.js | 122 ++++ wai/wai-web/reach/test/es5-strip.js | 36 ++ wai/wai-web/reach/test/node-host.js | 49 ++ wai/wai-web/reach/test/run-modern.mjs | 120 ++++ .../reach/test/vectors/provenance/README.md | 18 + .../test/vectors/provenance/captured.jpeg.wai | Bin 0 -> 35142 bytes .../vectors/provenance/captured.receipt.json | 1 + wai/wai-web/reach/test/workloads.js | 418 ++++++++++++++ wai/wai-web/reach/tools/cli.mjs | 311 +++++++++++ wai/wai-web/reach/tools/wasm.mjs | 119 ++++ 17 files changed, 2536 insertions(+) create mode 100644 wai/wai-web/reach/build.sh create mode 100644 wai/wai-web/reach/package-lock.json create mode 100644 wai/wai-web/reach/package.json create mode 100644 wai/wai-web/reach/profiles.json create mode 100644 wai/wai-web/reach/src/wai-load.js create mode 100644 wai/wai-web/reach/src/wai-reach-runtime.js create mode 100644 wai/wai-web/reach/test/conformance.js create mode 100644 wai/wai-web/reach/test/es5-strip.js create mode 100644 wai/wai-web/reach/test/node-host.js create mode 100644 wai/wai-web/reach/test/run-modern.mjs create mode 100644 wai/wai-web/reach/test/vectors/provenance/README.md create mode 100644 wai/wai-web/reach/test/vectors/provenance/captured.jpeg.wai create mode 100644 wai/wai-web/reach/test/vectors/provenance/captured.receipt.json create mode 100644 wai/wai-web/reach/test/workloads.js create mode 100644 wai/wai-web/reach/tools/cli.mjs create mode 100644 wai/wai-web/reach/tools/wasm.mjs diff --git a/wai/.gitignore b/wai/.gitignore index 2b99657fc..f215e4179 100644 --- a/wai/.gitignore +++ b/wai/.gitignore @@ -57,3 +57,6 @@ 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/ diff --git a/wai/wai-web/reach/build.sh b/wai/wai-web/reach/build.sh new file mode 100644 index 000000000..5c62fed36 --- /dev/null +++ b/wai/wai-web/reach/build.sh @@ -0,0 +1,185 @@ +#!/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 [] +# +# Writes into (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..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..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 and acorn by +# package-lock.json (npm ci checks their integrity hashes). 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 /manifest.json to reach/manifest.json +# +# 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 []" >&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") + +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 ------------------------------------------- +for p in full "${PROFILES[@]}"; do + "$B/wasm2js" "$WORK/mvp.$p.wasm" -Oz -ocimfs=200 -o "$WORK/j.$p.mjs" + "${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" < JSON.parse(require("fs").readFileSync(f, "utf8"))); + process.exit(JSON.stringify(rec.input) === JSON.stringify(out.input) ? 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" +else + echo "reach: the package is not the recorded input; outputs are in $DIST/manifest.json only" +fi diff --git a/wai/wai-web/reach/package-lock.json b/wai/wai-web/reach/package-lock.json new file mode 100644 index 000000000..70085f415 --- /dev/null +++ b/wai/wai-web/reach/package-lock.json @@ -0,0 +1,527 @@ +{ + "name": "wai-web-reach-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wai-web-reach-tools", + "license": "Apache-2.0", + "devDependencies": { + "acorn": "8.15.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/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" + } + } + } +} diff --git a/wai/wai-web/reach/package.json b/wai/wai-web/reach/package.json new file mode 100644 index 000000000..50badda73 --- /dev/null +++ b/wai/wai-web/reach/package.json @@ -0,0 +1,11 @@ +{ + "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.", + "license": "Apache-2.0", + "devDependencies": { + "acorn": "8.15.0", + "esbuild": "0.27.4", + "typescript": "5.9.3" + } +} diff --git a/wai/wai-web/reach/profiles.json b/wai/wai-web/reach/profiles.json new file mode 100644 index 000000000..04ccc6ace --- /dev/null +++ b/wai/wai-web/reach/profiles.json @@ -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_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_"] + } + } +} diff --git a/wai/wai-web/reach/src/wai-load.js b/wai/wai-web/reach/src/wai-load.js new file mode 100644 index 000000000..056c16a0e --- /dev/null +++ b/wai/wai-web/reach/src/wai-load.js @@ -0,0 +1,303 @@ +/* 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..wasm): MVP + * WebAssembly only. + * J the wasm2js translation (wai_web.j..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) }. In a browser and in Node there are defaults. + * + * 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); + }); + // V8 5.7 to 6.1 (WebAssembly without the RegExp dotAll flag): the research measured + // the full MVP module crashing these engines on arm64, so they get the receipt build + // or J. Error.captureStackTrace and the dotAll flag are both engine features. + f.v8Before62 = 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.v8Before62) { + return profile === 'receipt' + ? { rung: 'L2', reason: 'V8 before 6.2 (WebAssembly without RegExp dotAll), where the full MVP module can crash: the reduced receipt build' } + : { rung: 'J', reason: 'V8 before 6.2 (WebAssembly without RegExp dotAll), 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 { + 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; } + 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); diff --git a/wai/wai-web/reach/src/wai-reach-runtime.js b/wai/wai-web/reach/src/wai-reach-runtime.js new file mode 100644 index 000000000..efaca2504 --- /dev/null +++ b/wai/wai-web/reach/src/wai-reach-runtime.js @@ -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); diff --git a/wai/wai-web/reach/test/conformance.js b/wai/wai-web/reach/test/conformance.js new file mode 100644 index 000000000..6bcaa45c4 --- /dev/null +++ b/wai/wai-web/reach/test/conformance.js @@ -0,0 +1,122 @@ +/* Reach conformance runner (ECMAScript 5, Node 0.12 onward). + * + * node conformance.js --dist --wai [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 the shipped package (wai_web.js, wai_web_bg.wasm), for the shipped rung + * --reference fail unless every workload's output equals the file's (the shipped build's) + * --write-reference 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 + * "/ ") 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; +} diff --git a/wai/wai-web/reach/test/es5-strip.js b/wai/wai-web/reach/test/es5-strip.js new file mode 100644 index 000000000..55aac719d --- /dev/null +++ b/wai/wai-web/reach/test/es5-strip.js @@ -0,0 +1,36 @@ +/* Strip a modern V8 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'); +// The early typed-array quirk of V8 3.14: 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 V8 3.14)'); +g.__WAI_ES5_STRIPPED = out; diff --git a/wai/wai-web/reach/test/node-host.js b/wai/wai-web/reach/test/node-host.js new file mode 100644 index 000000000..d36a04922 --- /dev/null +++ b/wai/wai-web/reach/test/node-host.js @@ -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 }; diff --git a/wai/wai-web/reach/test/run-modern.mjs b/wai/wai-web/reach/test/run-modern.mjs new file mode 100644 index 000000000..200d1e39d --- /dev/null +++ b/wai/wai-web/reach/test/run-modern.mjs @@ -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 --shipped [--out ] +// +// 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 --shipped [--out ]'); 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); diff --git a/wai/wai-web/reach/test/vectors/provenance/README.md b/wai/wai-web/reach/test/vectors/provenance/README.md new file mode 100644 index 000000000..04c708ad4 --- /dev/null +++ b/wai/wai-web/reach/test/vectors/provenance/README.md @@ -0,0 +1,18 @@ +# 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. Both were written by `wai_prov` +(`wai-rs/src/bin/wai_prov.rs`) at an earlier revision of that tool: its pixels are a +synthesized pattern (rings and a gradient), not a photograph. + +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. + +| file | SHA-256 | +|---|---| +| `captured.jpeg.wai` | `4382082d2bbd1ef5358315afd578fc533d74bf973a57d4c45033c89c847a22a3` | +| `captured.receipt.json` | `8bc52251fe6169f881a7662d285bf0d1531347a936d43a63d7d63059744c3014` | diff --git a/wai/wai-web/reach/test/vectors/provenance/captured.jpeg.wai b/wai/wai-web/reach/test/vectors/provenance/captured.jpeg.wai new file mode 100644 index 0000000000000000000000000000000000000000..18bf56761883fffe070ec62bc82f95283fa831fa GIT binary patch literal 35142 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 Thi@D`G3sGm;?U8Q-y{DA1HoyY literal 0 HcmV?d00001 diff --git a/wai/wai-web/reach/test/vectors/provenance/captured.receipt.json b/wai/wai-web/reach/test/vectors/provenance/captured.receipt.json new file mode 100644 index 000000000..c817cd372 --- /dev/null +++ b/wai/wai-web/reach/test/vectors/provenance/captured.receipt.json @@ -0,0 +1 @@ +{"kind":"asset-provenance","asset_hash":"166da2294bfb78cfa93935663249bdb577e701117d9522caa0516fffdf0f5168","capability":"wai.image.jpeg","group_id":0,"media":"image","parent_receipt_hash":null,"receipt_hash":"de23956033baf7bfb285bf12eff2f130d13230e09bec89172bfea7a1a23edd62","root_hash":"f9b58b1f0c525c382ff2c02113b1ec2481722ac70bc5cb5497b20aacc0edfce0","sig":"f9ae708d10d8af4720983c7b8cb010da7d2c64ed3e88b86d46587349806e096a052ff23424e39cd167c745b0a21dc61a22db08babacc71e9cc937acd543cdd01","signer_id":"did:web:wai.transaction.science#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} \ No newline at end of file diff --git a/wai/wai-web/reach/test/workloads.js b/wai/wai-web/reach/test/workloads.js new file mode 100644 index 000000000..57b1637ba --- /dev/null +++ b/wai/wai-web/reach/test/workloads.js @@ -0,0 +1,418 @@ +/* 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 ''; + if (v === undefined) return ''; + 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 { '': 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); + 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)), quantize: S(W.quantizeScale(0.5, 16)), + 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.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); diff --git a/wai/wai-web/reach/tools/cli.mjs b/wai/wai-web/reach/tools/cli.mjs new file mode 100644 index 000000000..9f884d6a1 --- /dev/null +++ b/wai/wai-web/reach/tools/cli.mjs @@ -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 +// node cli.mjs graph +// node cli.mjs exports the module exports exactly the graph's names +// node cli.mjs same-sigs the two modules have one i64 boundary +// node cli.mjs factory +// node cli.mjs glue +// node cli.mjs minify +// node cli.mjs es5 ... each file parses as ECMAScript 5 +// node cli.mjs manifest +// +// 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 = {}; + for (const f of ['wai_web_bg.wasm', 'wai_web.js']) input[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}`); +} diff --git a/wai/wai-web/reach/tools/wasm.mjs b/wai/wai-web/reach/tools/wasm.mjs new file mode 100644 index 000000000..9970ec0dc --- /dev/null +++ b/wai/wai-web/reach/tools/wasm.mjs @@ -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 }; +} -- 2.54.0 (Apple Git-157) From bf7c61ff0bd30b784ae87a8af984e21c3f38a01b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 18:46:28 -0400 Subject: [PATCH 2/9] wai-web reach: the recorded manifest, the README, the CHANGELOG, and CI on the engine lines reach/manifest.json is the build of the package the site serves (wai_web_bg.wasm 0063129a..., wai_web.js f63605f0...); given that package, build.sh refuses any output whose SHA-256 differs. The manifest records where the package came from. wai-web-reach.yml (linux runner) builds wai-web from the tree with the web target, runs build.sh and run-modern.mjs on it, then runs conformance.js in one Node image per engine line: node:0.12 (J), node:8.0.0 (J, and the L2 receipt build), node:8 and node:10 (L2), node:12 and node:14 (L1). Each must be given its rung by the loader's probes, and every output must equal the shipped build's. The README says which device classes each image stands for, and that no real old device has been run. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-web-reach.yml | 148 ++++++++++++++++++++ wai/.gitignore | 3 + wai/CHANGELOG.md | 41 ++++++ wai/README.md | 24 ++++ wai/wai-web/reach/README.md | 200 ++++++++++++++++++++++++++++ wai/wai-web/reach/build.sh | 3 +- wai/wai-web/reach/manifest.json | 139 +++++++++++++++++++ wai/wai-web/reach/tools/cli.mjs | 4 +- 8 files changed, 559 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/wai-web-reach.yml create mode 100644 wai/wai-web/reach/README.md create mode 100644 wai/wai-web/reach/manifest.json diff --git a/.github/workflows/wai-web-reach.yml b/.github/workflows/wai-web-reach.yml new file mode 100644 index 000000000..0d0c6ee52 --- /dev/null +++ b/.github/workflows/wai-web-reach.yml @@ -0,0 +1,148 @@ +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. +# +# One job on the linux runner: +# +# 1. wai-web is built for wasm32 from this tree with the web target, as the site's +# package is built (wasm-bindgen at the version Cargo.lock pins, no wasm-opt); +# 2. reach/build.sh derives every reach build from it with the pinned binaryen and +# JS tools, checks that the translation and the glue parse as ECMAScript 5, and +# writes each output's SHA-256 to a manifest; +# 3. 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; +# 4. 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 3: +# node:0.12 V8 3.28 no WebAssembly -> J +# node:8.0.0 V8 5.8 WebAssembly without RegExp dotAll -> J (full), L2 (receipt) +# node:8 V8 6.2 MVP WebAssembly, no BigInt -> L2 +# node:10 V8 6.8 MVP WebAssembly, BigInt -> L2 +# node:12 V8 7.8 post-MVP features, no BigInt-i64 -> L1 +# node:14 V8 8.4 post-MVP features, no BigInt-i64 -> L1 +# These are proxies: the same JavaScript engine lines that real devices 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-3 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 + PKG: wai/wai-web/reach/ci-pkg + DIST: wai/wai-web/reach/ci-dist + REF: wai/wai-web/reach/ci-modern/reference.json + +jobs: + reach: + name: wai-web reach builds · every rung on its engine line + runs-on: linux + timeout-minutes: 90 + steps: + - uses: actions/checkout@v5 + # The docker runner image ships no Rust. + - name: Install Rust 1.98.0 with the wasm32 target + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add wasm32-unknown-unknown + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # 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 + 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. + - name: Build wai-web for wasm32 (web target) + 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 reach/ci-pkg --out-name wai_web \ + target/wasm32-unknown-unknown/release/wai_web.wasm + - name: Reach builds (reach/build.sh, pinned binaryen and JS tools) + shell: bash + run: | + set -euo pipefail + WAI_REACH_TOOLS="$RUNNER_TEMP/reach-tools" bash "$REACH/build.sh" "$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 "$PKG" --out "$REACH/ci-modern" + + - name: node:0.12 (V8 3.28) — 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 --wai wai --profile $p --expect-rung J --reference wai/wai-web/reach/ci-modern/reference.json; done" + - name: node:8.0.0 (V8 5.8) — 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 --wai wai --profile full --expect-rung J --reference wai/wai-web/reach/ci-modern/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist --wai wai --profile receipt --expect-rung L2 --reference wai/wai-web/reach/ci-modern/reference.json" + - name: node:8 (V8 6.2) — 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 --wai wai --profile $p --expect-rung L2 --reference wai/wai-web/reach/ci-modern/reference.json; done; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist --wai wai --rung J --reference wai/wai-web/reach/ci-modern/reference.json" + - name: node:10 (V8 6.8) — 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 --wai wai --profile full --expect-rung L2 --reference wai/wai-web/reach/ci-modern/reference.json" + - name: node:12 (V8 7.8) — 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 --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist --wai wai --rung L2 --reference wai/wai-web/reach/ci-modern/reference.json" + - name: node:14 (V8 8.4) — 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 --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern/reference.json" diff --git a/wai/.gitignore b/wai/.gitignore index f215e4179..219ab261c 100644 --- a/wai/.gitignore +++ b/wai/.gitignore @@ -60,3 +60,6 @@ wai-web/demo/int_hyper/ # 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-pkg/ +wai-web/reach/ci-dist/ +wai-web/reach/ci-modern/ diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 112c348c5..36bbc7a03 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,47 @@ 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 at four 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 five 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. V8 before 6.2 is recognised by + its missing RegExp `dotAll` flag and gets J, or the L2 receipt build, because the full + MVP module crashed those engines 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 and acorn are pinned by `package-lock.json`. + `reach/manifest.json` records the SHA-256 and sizes of every output built from the + package the site serves. Given that package, the build refuses any output that differs. +- **Proved 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 the conformance on Node images of the engine lines each + rung serves: J on V8 3.28, the V8 5.8 routing, L2 on V8 6.2 and 6.8, L1 on V8 7.8 and + 8.4, and the shipped build on a current Node. + + The images stand in for device classes, which the README names. No real old device has + been run. + ### Staged measurement and the session claim in the browser - **The measurement core, bound for JS** ([staged-measurement](extensions/staged-measurement.md)). diff --git a/wai/README.md b/wai/README.md index aa0d59a36..8b035bee2 100644 --- a/wai/README.md +++ b/wai/README.md @@ -80,6 +80,30 @@ 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 images stand in for device classes; no real old device +has been run. + ## Registered capabilities (the zeroth menu) | Capability | Codec | Lossless? | Mandatory? | diff --git a/wai/wai-web/reach/README.md b/wai/wai-web/reach/README.md new file mode 100644 index 000000000..9a4a0853f --- /dev/null +++ b/wai/wai-web/reach/README.md @@ -0,0 +1,200 @@ +# 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 | engine lines tested | +|---|---|---|---| +| 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 | V8 14.6 | +| **L1** | `wai_web_bg.l1.wasm` + `wai_web.reach.js` | the same WebAssembly features; no `BigInt` at the boundary | V8 7.8, 8.4 | +| **L2** | `wai_web_bg.l2..wasm` + `wai_web.reach.js` | MVP WebAssembly | V8 5.8 (receipt), 6.2, 6.7, 6.8, 7.4 | +| **J** | `wai_web.j..js` | ES5 and typed arrays; no WebAssembly | V8 3.28, 5.8, and V8 14.6 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 two 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 five counted places + (`tools/cli.mjs`, `glue`) 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 four 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. V8 before 6.2 (WebAssembly present, `Error.captureStackTrace` present, no RegExp + `dotAll` flag): **J**, or the L2 receipt build when the page asks for the receipt + profile. On arm64, these engines crashed running the full MVP module; +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 + + +``` + +| 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 }`, to load from somewhere other than the page | + +`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 [] +``` + +`` holds `wai_web.js` and `wai_web_bg.wasm`. The output goes to +`` (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 and acorn by `package-lock.json`. `npm ci` checks their integrity + hashes. + +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). Given that package, `build.sh` refuses any output whose SHA-256 differs from +the manifest's. `WAI_REACH_RECORD=1` writes a new one. + +## Conformance + +Every rung must give the shipped build's outputs. Two runners 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, 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 four 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 --shipped +node wai/wai-web/reach/test/conformance.js --dist --wai wai --rung J --profile receipt --reference +``` + +CI (`.github/workflows/wai-web-reach.yml`, on the `linux` runner): + +1. it builds wai-web from the tree as the site's package is built, and runs `build.sh` + and `run-modern.mjs` on it; +2. it runs `conformance.js` 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 the same JavaScript engine line that a class of real +devices ships, on x86_64 Linux. **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 V8. + +| proxy | engine | rung the loader must choose | real device classes it stands for | +|---|---|---|---| +| `node:0.12` | V8 3.28 (the Chromium 38 line) | J | smart TVs of model years 2014–2019 whose browser engine predates WebAssembly, Chromium 38 being one such line; Android-based TV sets and boxes on Android 7 or earlier, keeping their stock WebView; Android 4.4 WebViews; broadcast-application receivers (HbbTV 2.0.x, ATSC 3.0 A/344). Console browsers that run with the JIT off, and phones whose engine is Gecko 48 or JavaScriptCore, are J devices that this V8 proxy does not cover | +| `node:8.0.0` | V8 5.8 (the Chromium 58 line) | J, or L2 for the receipt profile | devices frozen at Chromium 57–61, with WebAssembly but no RegExp `dotAll`: among them Android 8 TVs and boxes keeping their stock WebView (Chromium 58). The full MVP module crashed V8 5.8–6.1 on arm64; this x86_64 proxy does not crash, so it checks the choice and the outputs, not the crash | +| `node:8` | V8 6.2 (the Chromium 62 line) | L2 | engines at Chromium 62–66, with MVP WebAssembly and no `BigInt`: Android 9 TVs and boxes keeping their stock WebView (Chromium 66), and TV platforms of model years 2019–2020 | +| `node:10` | V8 6.8 (the Chromium 68 line) | L2 | engines at Chromium 67–74, with `BigInt` but without bulk memory: Android 10 TVs and boxes keeping their stock WebView (Chromium 74) | +| `node:12` | V8 7.8 (the Chromium 78 line) | L1 | TV platforms of model year 2021 with Chromium 76–79 engines | +| `node:14` | V8 8.4 (the Chromium 84 line) | L1 | TV platforms and streaming sticks on Chromium 79–84, and the Android 11 stock WebView (Chromium 83) | +| current Node | V8 14.6 | shipped | Chromium 85 and later. Other engine families with i64-as-`BigInt` 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. diff --git a/wai/wai-web/reach/build.sh b/wai/wai-web/reach/build.sh index 5c62fed36..5078db8f6 100644 --- a/wai/wai-web/reach/build.sh +++ b/wai/wai-web/reach/build.sh @@ -26,6 +26,7 @@ # 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 /manifest.json to reach/manifest.json +# WAI_REACH_SOURCE a line saying where the package came from, kept in the manifest # # Needs bash, curl, tar, node >= 18 and npm. Platforms: macOS arm64 and x86_64, Linux # x86_64 and aarch64. @@ -169,7 +170,7 @@ if [ "${WAI_REACH_RECORD:-}" = 1 ]; then 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) === JSON.stringify(out.input) ? 0 : 1);' "$REC" "$DIST/manifest.json"; then + 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(); diff --git a/wai/wai-web/reach/manifest.json b/wai/wai-web/reach/manifest.json new file mode 100644 index 000000000..5c05c4f9f --- /dev/null +++ b/wai/wai-web/reach/manifest.json @@ -0,0 +1,139 @@ +{ + "format": "wai-reach-manifest/1", + "input": { + "files": { + "wai_web_bg.wasm": { + "sha256": "0063129a424d7eea2421f6641b75eecdf147d4d853484bd709d6cf11b06e5c38", + "bytes": 2587342 + }, + "wai_web.js": { + "sha256": "f63605f012b44b935af11ea7285b0ca57ef9a760158353b1d826304d54930f6a", + "bytes": 448554 + } + }, + "source": "the package wai.transaction.science serves at /wai/pkg/: wai/wai-web at commit 248846f13f2da2db32914cff1e716064eaf3d039, built by wasm-pack 0.15.0 (web target, release, wasm-opt off) with rustc 1.98.0" + }, + "tools": { + "binaryen": { + "release": "version_123", + "archives_sha256": { + "arm64-macos": "74428be348c1a09863e7b642a1fa948cabf8ec9561052233d8288e941951725b", + "x86_64-macos": "cc18b14d2b673d9c66bf54f31ff2b0ceb23ba5132455b893965ae2792f9e00dd", + "x86_64-linux": "e959f2170af4c20c552e9de3a0253704d6a9d2766e8fdb88e4d6ac4bae9388fe", + "aarch64-linux": "4b6bd61ba6cd3b18c993b4657d93426c782f9b91b74be0d38018cd8be1319376" + } + }, + "package_lock_sha256": "f4409209b38f0b5612ec6dbb552ea0cba40c1868b3a0e36fdd39939b6a852eba", + "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" + }, + "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": "5d54e0da3287cd1ad5c4c73e93334ff201ccbcdcfc27d912a5f0a9a472d95229", + "bytes": 16154, + "gzip": 4958, + "brotli": 4339 + }, + "wai_web.j.full.js": { + "rung": "J", + "profile": "full", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "331c6cef2fd3b7be85773f23dec89ee5c2fbc09b2898460c823468497a04fdf5", + "bytes": 5970825, + "gzip": 1561203, + "brotli": 1055608 + }, + "wai_web.j.receipt.js": { + "rung": "J", + "profile": "receipt", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "04635995dad681d3e0d57b1864f1e423ee3bb9c4bc77a9e97c8f32ee503f8b88", + "bytes": 997368, + "gzip": 310118, + "brotli": 251328 + }, + "wai_web.j.verify.js": { + "rung": "J", + "profile": "verify", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "a68b2c693363e4a54b0bd1cfc59ef899fef437a2e055392546d5710ae531ddbf", + "bytes": 3087053, + "gzip": 845184, + "brotli": 607640 + }, + "wai_web.j.verify_decode.js": { + "rung": "J", + "profile": "verify_decode", + "what": "wasm2js bundle: runtime + i64 table + translation + glue", + "sha256": "0a2e8b5231cd129991aa897f1fa80956d5052bfda5550cb9a0374acf58a7c9b3", + "bytes": 3311544, + "gzip": 903949, + "brotli": 642580 + }, + "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": "dee5424ce7fac4a0750a99315566e83a33f9beb2b16f7c66675493d13a5a7851", + "bytes": 2592153, + "gzip": 918301, + "brotli": 683618 + }, + "wai_web_bg.l2.full.wasm": { + "rung": "L2", + "profile": "full", + "what": "MVP-lowered and legalised", + "sha256": "23fb9865235e8d958e56f427425d530d5706990acb4e572c3899975f197db410", + "bytes": 2591833, + "gzip": 919479, + "brotli": 683794 + }, + "wai_web_bg.l2.receipt.wasm": { + "rung": "L2", + "profile": "receipt", + "what": "MVP-lowered and legalised", + "sha256": "a8a4bbff72a5af319533a466dc8c6dff39d5775f5ed41ba331105c6aa6ff5845", + "bytes": 393699, + "gzip": 192922, + "brotli": 164196 + }, + "wai_web_bg.l2.verify.wasm": { + "rung": "L2", + "profile": "verify", + "what": "MVP-lowered and legalised", + "sha256": "1cbf873e82528966e0f7cd0185cd161d8600a3eb17ba64b0ef7c21d83ff9e213", + "bytes": 1222709, + "gzip": 521808, + "brotli": 407854 + }, + "wai_web_bg.l2.verify_decode.wasm": { + "rung": "L2", + "profile": "verify_decode", + "what": "MVP-lowered and legalised", + "sha256": "fe6210c75d9b8226e2cfff4ec199a0c41a9151b21015bfba1662045ad53e1e20", + "bytes": 1316976, + "gzip": 559259, + "brotli": 433933 + } + } +} diff --git a/wai/wai-web/reach/tools/cli.mjs b/wai/wai-web/reach/tools/cli.mjs index 9f884d6a1..560f791f9 100644 --- a/wai/wai-web/reach/tools/cli.mjs +++ b/wai/wai-web/reach/tools/cli.mjs @@ -287,8 +287,8 @@ function manifest(dist, inputDir, notesPath, toolsPath, outPath) { 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 = {}; - for (const f of ['wai_web_bg.wasm', 'wai_web.js']) input[f] = { sha256: sha256(read(path.join(inputDir, f))), bytes: read(path.join(inputDir, f)).length }; + const input = { files: {}, source: process.env.WAI_REACH_SOURCE || 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, -- 2.54.0 (Apple Git-157) From 2c5f382492221c24d6952a8c97cc050079d72944 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 18:49:16 -0400 Subject: [PATCH 3/9] wai-web reach: the i64 workload crosses a low half with its top bit set 2^32 + 2^31 goes in as a parameter and comes back as a result, so the Number path (no BigInt) is tested where the low half's sign bit matters. Co-Authored-By: Claude Opus 5.5 --- wai/wai-web/reach/test/workloads.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wai/wai-web/reach/test/workloads.js b/wai/wai-web/reach/test/workloads.js index 57b1637ba..f475d22e9 100644 --- a/wai/wai-web/reach/test/workloads.js +++ b/wai/wai-web/reach/test/workloads.js @@ -386,13 +386,18 @@ 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)), quantize: S(W.quantizeScale(0.5, 16)), + 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)), 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.session.length === 64; + var ok = out.lamport1 === '1' && out.lamport2 === '101' && out.lamport3 === '5000000001' && out.lamport4 === '6442450945' && + out.session.length === 64; return { ok: ok, out: out }; }; -- 2.54.0 (Apple Git-157) From 3f26fab1f4e0728d0d5a1aec884e2dfa146ebe41 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 19:22:41 -0400 Subject: [PATCH 4/9] wai-web reach: direct i64 results with a high low half; the shipped wasm streams in browsers The runtime's falsification (an i64 result's low half read as signed where there is no BigInt) passed every workload: op_lamport returns its u64 through memory, not as an i64 result, so no workload returned an i64 whose low half has its top bit set. quantizeScale(0.75, 32) and (1.5, 32) now do, with a zero and a non-zero high half. For the shipped rung a browser now hands the glue the wasm URL, so the module compiles while it downloads; a host that reads files still hands it the bytes. The recorded manifest changes for wai-load.js only. Co-Authored-By: Claude Opus 5.5 --- wai/wai-web/reach/README.md | 2 +- wai/wai-web/reach/manifest.json | 8 ++++---- wai/wai-web/reach/src/wai-load.js | 8 +++++++- wai/wai-web/reach/test/workloads.js | 5 ++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/wai/wai-web/reach/README.md b/wai/wai-web/reach/README.md index 9a4a0853f..5b833a430 100644 --- a/wai/wai-web/reach/README.md +++ b/wai/wai-web/reach/README.md @@ -93,7 +93,7 @@ agent, and its detection never throws: | `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 }`, to load from somewhere other than the page | +| `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. diff --git a/wai/wai-web/reach/manifest.json b/wai/wai-web/reach/manifest.json index 5c05c4f9f..437a1063f 100644 --- a/wai/wai-web/reach/manifest.json +++ b/wai/wai-web/reach/manifest.json @@ -40,10 +40,10 @@ "rung": "all", "profile": null, "what": "the loader", - "sha256": "5d54e0da3287cd1ad5c4c73e93334ff201ccbcdcfc27d912a5f0a9a472d95229", - "bytes": 16154, - "gzip": 4958, - "brotli": 4339 + "sha256": "2307c49803b2b44ee9a2b4d7b188cd25b078468e1dd05098b47b4ca3a1b0839d", + "bytes": 16577, + "gzip": 5101, + "brotli": 4464 }, "wai_web.j.full.js": { "rung": "J", diff --git a/wai/wai-web/reach/src/wai-load.js b/wai/wai-web/reach/src/wai-load.js index 056c16a0e..47e11bcc6 100644 --- a/wai/wai-web/reach/src/wai-load.js +++ b/wai/wai-web/reach/src/wai-load.js @@ -29,7 +29,9 @@ * (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) }. In a browser and in Node there are defaults. + * 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. */ @@ -151,6 +153,7 @@ 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); } @@ -265,6 +268,9 @@ 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); diff --git a/wai/wai-web/reach/test/workloads.js b/wai/wai-web/reach/test/workloads.js index f475d22e9..d7abd19c6 100644 --- a/wai/wai-web/reach/test/workloads.js +++ b/wai/wai-web/reach/test/workloads.js @@ -392,12 +392,15 @@ 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.session.length === 64; + out.quantize === '32768' && out.quantize32_low === '3221225472' && out.quantize32_high === '6442450944' && out.session.length === 64; return { ok: ok, out: out }; }; -- 2.54.0 (Apple Git-157) From 07ee78d414bd79d3e2baa434867c4dd0d9eb3e7a Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 19:24:45 -0400 Subject: [PATCH 5/9] ci(wai-web-reach): build the package outside the tree The wasm-bindgen glue is an ES module in a .js file. Under wai/wai-web/reach/ the nearest package.json (the build tools' pins) has no "type", so Node would read the glue as CommonJS first. In $RUNNER_TEMP no package.json applies; only the reach build and the reference, which the engine images read, stay in the workspace. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-web-reach.yml | 12 +++++++----- wai/.gitignore | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wai-web-reach.yml b/.github/workflows/wai-web-reach.yml index 0d0c6ee52..749c355d9 100644 --- a/.github/workflows/wai-web-reach.yml +++ b/.github/workflows/wai-web-reach.yml @@ -66,7 +66,6 @@ on: env: REACH: wai/wai-web/reach - PKG: wai/wai-web/reach/ci-pkg DIST: wai/wai-web/reach/ci-dist REF: wai/wai-web/reach/ci-modern/reference.json @@ -97,24 +96,27 @@ jobs: 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. + # ([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 (web target) 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 reach/ci-pkg --out-name wai_web \ + wasm-bindgen --target web --out-dir "$RUNNER_TEMP/pkg" --out-name wai_web \ target/wasm32-unknown-unknown/release/wai_web.wasm + # 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 - WAI_REACH_TOOLS="$RUNNER_TEMP/reach-tools" bash "$REACH/build.sh" "$PKG" "$DIST" + 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 "$PKG" --out "$REACH/ci-modern" + run: node "$REACH/test/run-modern.mjs" --dist "$DIST" --shipped "$RUNNER_TEMP/pkg" --out "$REACH/ci-modern" - name: node:0.12 (V8 3.28) — J chosen and equal to the shipped build uses: docker://node:0.12 diff --git a/wai/.gitignore b/wai/.gitignore index 219ab261c..9a65c6549 100644 --- a/wai/.gitignore +++ b/wai/.gitignore @@ -60,6 +60,5 @@ wai-web/demo/int_hyper/ # 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-pkg/ wai-web/reach/ci-dist/ wai-web/reach/ci-modern/ -- 2.54.0 (Apple Git-157) From b77b332d3db7a90544f014e60a67a0c9807fd876 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 19:26:20 -0400 Subject: [PATCH 6/9] ci(wai-web-reach): gate the build on the pull request's own diff A rebase's push delta fires every path-filtered workflow; the reach job builds wai-web and runs six engine images, so it now runs only when scripts/pr-touches-paths.py says the pull request touches its paths (failing open, and always on main), as wai-pack does. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-web-reach.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wai-web-reach.yml b/.github/workflows/wai-web-reach.yml index 749c355d9..6c97a6d9f 100644 --- a/.github/workflows/wai-web-reach.yml +++ b/.github/workflows/wai-web-reach.yml @@ -5,7 +5,7 @@ name: wai-web reach builds # each run on an engine line it serves and compared with the shipped build output for # output. # -# One job on the linux runner: +# On the linux runner, after a gate that reads the pull request's own diff: # # 1. wai-web is built for wasm32 from this tree with the web target, as the site's # package is built (wasm-bindgen at the version Cargo.lock pins, no wasm-opt); @@ -70,8 +70,32 @@ env: REF: wai/wai-web/reach/ci-modern/reference.json 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 · every rung on its engine line + needs: relevant + if: needs.relevant.outputs.run == 'true' runs-on: linux timeout-minutes: 90 steps: -- 2.54.0 (Apple Git-157) From df9d349d928625faef44682ecfa7a06c6d2bdd43 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 9 Oct 2026 21:46:01 -0400 Subject: [PATCH 7/9] wai-web reach: CI holds the served bytes to the manifest; the package of record kept in-tree The reach builds' byte-identity rested on local runs: CI built wai-web from the tree, which can never be the package the site serves, so build.sh took its "not the recorded input" branch and the recorded outputs were never checked there. - reach/record/ keeps the package of record: the two files the site serves under /wai/pkg/. It is rebuilt from wai/wai-web at 248846f1 with the site's recipe, which now also remaps the standard library's sources in the toolchain to /rustc/: the old remap kept the toolchain's folder, named after the build host, in every panic location of generic std code (and so in every reach binary and J bundle). manifest.json is re-recorded from it, with input.commit. - The package is kept rather than rebuilt in CI: the same commit and recipe give a different wasm on the linux runner's host than on the macOS arm64 host that built it (measured twice in the runner's image, with the export at /tmp and at /private/tmp; the glue is the same, the code section is not). - wai-web-reach.yml runs a leg per package. The record leg checks reach/record/ against the manifest's SHA-256 and runs build.sh with the new WAI_REACH_REQUIRE_RECORD=1, which refuses any other package, so every output must equal the manifest; then run-modern and the engine images run on those outputs. The tree leg is the earlier job. Step names describe each image by its engine features. - wai-load.js: the early-engine probe is the feature `noDotAll` (was `v8Before62`), and its reasons and comment say what the engine lacks, not which engine it is. - README, CHANGELOG and wai/README: the proxies are named by Node image and engine features, and the device classes they stand for by capability; no engine, browser or OS family names. Counts that drift ("five counted places", "four profiles", "the four int_synth vectors") describe the shape instead. The CHANGELOG describes what CI runs and claims no CI result. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-web-reach.yml | 117 +- wai/.gitignore | 4 +- wai/CHANGELOG.md | 38 +- wai/README.md | 5 +- wai/wai-web/reach/README.md | 87 +- wai/wai-web/reach/build.sh | 10 + wai/wai-web/reach/manifest.json | 87 +- wai/wai-web/reach/record/wai_web.js | 12364 +++++++++++++++++++++ wai/wai-web/reach/record/wai_web_bg.wasm | Bin 0 -> 2586670 bytes wai/wai-web/reach/src/wai-load.js | 15 +- wai/wai-web/reach/test/es5-strip.js | 6 +- wai/wai-web/reach/tools/cli.mjs | 2 +- 12 files changed, 12589 insertions(+), 146 deletions(-) create mode 100644 wai/wai-web/reach/record/wai_web.js create mode 100644 wai/wai-web/reach/record/wai_web_bg.wasm diff --git a/.github/workflows/wai-web-reach.yml b/.github/workflows/wai-web-reach.yml index 6c97a6d9f..4070053bb 100644 --- a/.github/workflows/wai-web-reach.yml +++ b/.github/workflows/wai-web-reach.yml @@ -5,32 +5,43 @@ name: wai-web reach builds # 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: +# On the linux runner, after a gate that reads the pull request's own diff, one leg per +# package: # -# 1. wai-web is built for wasm32 from this tree with the web target, as the site's -# package is built (wasm-bindgen at the version Cargo.lock pins, no wasm-opt); -# 2. reach/build.sh derives every reach build from it with the pinned binaryen and -# JS tools, checks that the translation and the glue parse as ECMAScript 5, and -# writes each output's SHA-256 to a manifest; -# 3. on current Node (reach/test/run-modern.mjs): the loader must choose the shipped +# 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), +# 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; -# 4. on older engine lines, each in its own Node image (reach/test/conformance.js, +# 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 3: -# node:0.12 V8 3.28 no WebAssembly -> J -# node:8.0.0 V8 5.8 WebAssembly without RegExp dotAll -> J (full), L2 (receipt) -# node:8 V8 6.2 MVP WebAssembly, no BigInt -> L2 -# node:10 V8 6.8 MVP WebAssembly, BigInt -> L2 -# node:12 V8 7.8 post-MVP features, no BigInt-i64 -> L1 -# node:14 V8 8.4 post-MVP features, no BigInt-i64 -> L1 -# These are proxies: the same JavaScript engine lines that real devices ship (the -# README says which device classes each stands for). No real old device runs here. +# 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-3 are read where they were written. They run no +# 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. @@ -66,8 +77,6 @@ on: env: REACH: wai/wai-web/reach - DIST: wai/wai-web/reach/ci-dist - REF: wai/wai-web/reach/ci-modern/reference.json jobs: # Forgejo matches `paths:` against the push delta, so a rebase fires this workflow @@ -93,15 +102,26 @@ jobs: echo "run=$r" >> "$GITHUB_OUTPUT" reach: - name: wai-web reach builds · every rung on its engine line + 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" @@ -109,11 +129,12 @@ jobs: 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 + 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 @@ -122,7 +143,8 @@ jobs: # 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 (web target) + - name: Build wai-web for wasm32 from this tree (web target) + if: matrix.leg == 'tree' shell: bash working-directory: wai/wai-web run: | @@ -130,45 +152,68 @@ jobs: 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" + run: node "$REACH/test/run-modern.mjs" --dist "$DIST" --shipped "$RUNNER_TEMP/pkg" --out "$REACH/ci-modern-${{ matrix.leg }}" - - name: node:0.12 (V8 3.28) — J chosen and equal to the shipped build + - 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 --wai wai --profile $p --expect-rung J --reference wai/wai-web/reach/ci-modern/reference.json; done" - - name: node:8.0.0 (V8 5.8) — J for the full build, the L2 receipt build + 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 --wai wai --profile full --expect-rung J --reference wai/wai-web/reach/ci-modern/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist --wai wai --profile receipt --expect-rung L2 --reference wai/wai-web/reach/ci-modern/reference.json" - - name: node:8 (V8 6.2) — L2 chosen and equal to the shipped build + 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 --wai wai --profile $p --expect-rung L2 --reference wai/wai-web/reach/ci-modern/reference.json; done; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist --wai wai --rung J --reference wai/wai-web/reach/ci-modern/reference.json" - - name: node:10 (V8 6.8) — L2 chosen and equal to the shipped build + 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 --wai wai --profile full --expect-rung L2 --reference wai/wai-web/reach/ci-modern/reference.json" - - name: node:12 (V8 7.8) — L1 chosen and equal to the shipped build + 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 --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern/reference.json; node wai/wai-web/reach/test/conformance.js --dist wai/wai-web/reach/ci-dist --wai wai --rung L2 --reference wai/wai-web/reach/ci-modern/reference.json" - - name: node:14 (V8 8.4) — L1 chosen and equal to the shipped build + 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 --wai wai --profile full --expect-rung L1 --reference wai/wai-web/reach/ci-modern/reference.json" + 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" diff --git a/wai/.gitignore b/wai/.gitignore index 9a65c6549..225b95c31 100644 --- a/wai/.gitignore +++ b/wai/.gitignore @@ -60,5 +60,5 @@ wai-web/demo/int_hyper/ # 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/ +wai-web/reach/ci-dist*/ +wai-web/reach/ci-modern*/ diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 36bbc7a03..af999f3c2 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -16,36 +16,40 @@ wire format may change and such changes are recorded here. - **L2** lowers it to MVP WebAssembly and legalises it; - **J** translates the MVP module to ES5 JavaScript with `wasm2js`. - L2 and J come at four 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 five counted changes. One of them rebuilds a memory view whenever - the memory's buffer changes, not only when it is detached. + 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. V8 before 6.2 is recognised by - its missing RegExp `dotAll` flag and gets J, or the L2 receipt build, because the full - MVP module crashed those engines on arm64. A rung can be forced, and the loader says - why it chose. + 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 and acorn are pinned by `package-lock.json`. `reach/manifest.json` records the SHA-256 and sizes of every output built from the - package the site serves. Given that package, the build refuses any output that differs. -- **Proved output for output.** + 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 the conformance on Node images of the engine lines each - rung serves: J on V8 3.28, the V8 5.8 routing, L2 on V8 6.2 and 6.8, L1 on V8 7.8 and - 8.4, and the shipped build on a current Node. + - 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. No real old device has - been run. + The images stand in for device classes, which the README names by capability. No real + old device has been run. ### Staged measurement and the session claim in the browser diff --git a/wai/README.md b/wai/README.md index 8b035bee2..62c39dc14 100644 --- a/wai/README.md +++ b/wai/README.md @@ -101,8 +101,9 @@ rung can be forced for testing. `reach/build.sh` makes every build with pinned t 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 images stand in for device classes; no real old device -has been run. +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) diff --git a/wai/wai-web/reach/README.md b/wai/wai-web/reach/README.md index 5b833a430..979f48d9d 100644 --- a/wai/wai-web/reach/README.md +++ b/wai/wai-web/reach/README.md @@ -7,12 +7,12 @@ a `BigInt`, and where ES modules load. The reach builds run the same code on eng 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 | engine lines tested | +| 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 | V8 14.6 | -| **L1** | `wai_web_bg.l1.wasm` + `wai_web.reach.js` | the same WebAssembly features; no `BigInt` at the boundary | V8 7.8, 8.4 | -| **L2** | `wai_web_bg.l2..wasm` + `wai_web.reach.js` | MVP WebAssembly | V8 5.8 (receipt), 6.2, 6.7, 6.8, 7.4 | -| **J** | `wai_web.j..js` | ES5 and typed arrays; no WebAssembly | V8 3.28, 5.8, and V8 14.6 stripped to ES5 | +| 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..wasm` + `wai_web.reach.js` | MVP WebAssembly | `node:8.0.0` (receipt), `node:8`, `node:10` | +| **J** | `wai_web.j..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 @@ -25,7 +25,7 @@ changes: every reach build is derived from the shipped package itself. - **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 two ES5 parts: +`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`, @@ -34,8 +34,8 @@ changes: every reach build is derived from the shipped package itself. 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 five counted places - (`tools/cli.mjs`, `glue`) and compiled to ES5: +- **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 @@ -47,7 +47,7 @@ changes: every reach build is derived from the shipped package itself. ## Profiles -L2 and J come in four profiles. `profiles.json` lists the exports each one keeps, and +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 | @@ -67,9 +67,10 @@ 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. V8 before 6.2 (WebAssembly present, `Error.captureStackTrace` present, no RegExp - `dotAll` flag): **J**, or the L2 receipt build when the page asks for the receipt - profile. On arm64, these engines crashed running the full MVP module; +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`: @@ -124,13 +125,15 @@ The build refuses: - 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). Given that package, `build.sh` refuses any output whose SHA-256 differs from -the manifest's. `WAI_REACH_RECORD=1` writes a new one. +`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. Two runners check this: +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 @@ -142,7 +145,7 @@ Every rung must give the shipped build's outputs. Two runners check this: - 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 four int_synth vectors; all of `staged-conformance/` + 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. @@ -163,30 +166,44 @@ node wai/wai-web/reach/test/run-modern.mjs --dist --shipped node wai/wai-web/reach/test/conformance.js --dist --wai wai --rung J --profile receipt --reference ``` -CI (`.github/workflows/wai-web-reach.yml`, on the `linux` runner): +CI (`.github/workflows/wai-web-reach.yml`, on the `linux` runner) runs a leg per package: -1. it builds wai-web from the tree as the site's package is built, and runs `build.sh` - and `run-modern.mjs` on it; -2. it runs `conformance.js` 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. +- **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 the same JavaScript engine line that a class of real -devices ships, on x86_64 Linux. **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 V8. +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 | rung the loader must choose | real device classes it stands for | +| proxy | engine features | rung the loader must choose | device classes it stands for | |---|---|---|---| -| `node:0.12` | V8 3.28 (the Chromium 38 line) | J | smart TVs of model years 2014–2019 whose browser engine predates WebAssembly, Chromium 38 being one such line; Android-based TV sets and boxes on Android 7 or earlier, keeping their stock WebView; Android 4.4 WebViews; broadcast-application receivers (HbbTV 2.0.x, ATSC 3.0 A/344). Console browsers that run with the JIT off, and phones whose engine is Gecko 48 or JavaScriptCore, are J devices that this V8 proxy does not cover | -| `node:8.0.0` | V8 5.8 (the Chromium 58 line) | J, or L2 for the receipt profile | devices frozen at Chromium 57–61, with WebAssembly but no RegExp `dotAll`: among them Android 8 TVs and boxes keeping their stock WebView (Chromium 58). The full MVP module crashed V8 5.8–6.1 on arm64; this x86_64 proxy does not crash, so it checks the choice and the outputs, not the crash | -| `node:8` | V8 6.2 (the Chromium 62 line) | L2 | engines at Chromium 62–66, with MVP WebAssembly and no `BigInt`: Android 9 TVs and boxes keeping their stock WebView (Chromium 66), and TV platforms of model years 2019–2020 | -| `node:10` | V8 6.8 (the Chromium 68 line) | L2 | engines at Chromium 67–74, with `BigInt` but without bulk memory: Android 10 TVs and boxes keeping their stock WebView (Chromium 74) | -| `node:12` | V8 7.8 (the Chromium 78 line) | L1 | TV platforms of model year 2021 with Chromium 76–79 engines | -| `node:14` | V8 8.4 (the Chromium 84 line) | L1 | TV platforms and streaming sticks on Chromium 79–84, and the Android 11 stock WebView (Chromium 83) | -| current Node | V8 14.6 | shipped | Chromium 85 and later. Other engine families with i64-as-`BigInt` are not proxied here | +| `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 diff --git a/wai/wai-web/reach/build.sh b/wai/wai-web/reach/build.sh index 5078db8f6..78bc26194 100644 --- a/wai/wai-web/reach/build.sh +++ b/wai/wai-web/reach/build.sh @@ -26,7 +26,14 @@ # 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 /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. @@ -181,6 +188,9 @@ elif [ -f "$REC" ] && node -e ' } 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 diff --git a/wai/wai-web/reach/manifest.json b/wai/wai-web/reach/manifest.json index 437a1063f..d4e7bffad 100644 --- a/wai/wai-web/reach/manifest.json +++ b/wai/wai-web/reach/manifest.json @@ -3,15 +3,16 @@ "input": { "files": { "wai_web_bg.wasm": { - "sha256": "0063129a424d7eea2421f6641b75eecdf147d4d853484bd709d6cf11b06e5c38", - "bytes": 2587342 + "sha256": "c2b63f747c8b55b3eb74fd9dd53109bb4031a578622e07c62e26d1a27eb2050e", + "bytes": 2586670 }, "wai_web.js": { "sha256": "f63605f012b44b935af11ea7285b0ca57ef9a760158353b1d826304d54930f6a", "bytes": 448554 } }, - "source": "the package wai.transaction.science serves at /wai/pkg/: wai/wai-web at commit 248846f13f2da2db32914cff1e716064eaf3d039, built by wasm-pack 0.15.0 (web target, release, wasm-opt off) with rustc 1.98.0" + "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": { @@ -40,46 +41,46 @@ "rung": "all", "profile": null, "what": "the loader", - "sha256": "2307c49803b2b44ee9a2b4d7b188cd25b078468e1dd05098b47b4ca3a1b0839d", - "bytes": 16577, - "gzip": 5101, - "brotli": 4464 + "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": "331c6cef2fd3b7be85773f23dec89ee5c2fbc09b2898460c823468497a04fdf5", - "bytes": 5970825, - "gzip": 1561203, - "brotli": 1055608 + "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": "04635995dad681d3e0d57b1864f1e423ee3bb9c4bc77a9e97c8f32ee503f8b88", - "bytes": 997368, - "gzip": 310118, - "brotli": 251328 + "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": "a68b2c693363e4a54b0bd1cfc59ef899fef437a2e055392546d5710ae531ddbf", - "bytes": 3087053, - "gzip": 845184, - "brotli": 607640 + "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": "0a2e8b5231cd129991aa897f1fa80956d5052bfda5550cb9a0374acf58a7c9b3", - "bytes": 3311544, - "gzip": 903949, - "brotli": 642580 + "sha256": "d2bb946fbab8dbcc5fb7ca101b6c4615881d3f1f1de47813a0b3c9e080f1b1bf", + "bytes": 3310622, + "gzip": 903681, + "brotli": 642273 }, "wai_web.reach.js": { "rung": "L1, L2", @@ -94,46 +95,46 @@ "rung": "L1", "profile": "full", "what": "the shipped binary, legalised at the JS interface", - "sha256": "dee5424ce7fac4a0750a99315566e83a33f9beb2b16f7c66675493d13a5a7851", - "bytes": 2592153, - "gzip": 918301, - "brotli": 683618 + "sha256": "25e728714bcc67ccb133042012860a53c881ea6c26c3dc7538bf378c0598b3b8", + "bytes": 2591481, + "gzip": 918302, + "brotli": 683525 }, "wai_web_bg.l2.full.wasm": { "rung": "L2", "profile": "full", "what": "MVP-lowered and legalised", - "sha256": "23fb9865235e8d958e56f427425d530d5706990acb4e572c3899975f197db410", - "bytes": 2591833, - "gzip": 919479, - "brotli": 683794 + "sha256": "5af77d42fbd2e2e6023789aca42dcb62c6bcf1ccb29e7303b31de5e6251fa88e", + "bytes": 2591161, + "gzip": 919464, + "brotli": 684042 }, "wai_web_bg.l2.receipt.wasm": { "rung": "L2", "profile": "receipt", "what": "MVP-lowered and legalised", - "sha256": "a8a4bbff72a5af319533a466dc8c6dff39d5775f5ed41ba331105c6aa6ff5845", - "bytes": 393699, - "gzip": 192922, - "brotli": 164196 + "sha256": "89ad094a90fbc31aa3399d0e4db41728c742c110a90480952f3c2527310fb821", + "bytes": 393027, + "gzip": 192859, + "brotli": 163989 }, "wai_web_bg.l2.verify.wasm": { "rung": "L2", "profile": "verify", "what": "MVP-lowered and legalised", - "sha256": "1cbf873e82528966e0f7cd0185cd161d8600a3eb17ba64b0ef7c21d83ff9e213", - "bytes": 1222709, - "gzip": 521808, - "brotli": 407854 + "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": "fe6210c75d9b8226e2cfff4ec199a0c41a9151b21015bfba1662045ad53e1e20", - "bytes": 1316976, - "gzip": 559259, - "brotli": 433933 + "sha256": "efe0033b1cfc151054baf6282bd6cd9b2a264cf5df8321586cdbcf1f62928b87", + "bytes": 1316304, + "gzip": 559206, + "brotli": 433878 } } } diff --git a/wai/wai-web/reach/record/wai_web.js b/wai/wai-web/reach/record/wai_web.js new file mode 100644 index 000000000..7f78f433c --- /dev/null +++ b/wai/wai-web/reach/record/wai_web.js @@ -0,0 +1,12364 @@ +/* @ts-self-types="./wai_web.d.ts" */ + +/** + * A signing actor in the browser. The page supplies 32 random bytes + * (crypto.getRandomValues); the key never leaves the tab — only + * signed ops do. An op is the unit of play: what travels between + * tabs (by any transport — transport is out of the extension's + * scope) is exactly what a native sink would ingest. + */ +export class Actor { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + ActorFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_actor_free(ptr, 0); + } + /** + * @param {Uint8Array} seed + */ + constructor(seed) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + wasm.actor_new(retptr, ptr0, len0); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + this.__wbg_ptr = r0; + ActorFinalization.register(this, this.__wbg_ptr, this); + return this; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Lamport discipline: an actor's counter MUST exceed every + * lamport it has observed. The page calls this with the lamports + * of ops it receives. + * @param {bigint} lamport + */ + observe_lamport(lamport) { + wasm.actor_observe_lamport(this.__wbg_ptr, lamport); + } + /** + * @returns {string} + */ + pubkey_hex() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.actor_pubkey_hex(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * Sign a truss IMPULSE op (the SUM-resolved shove) targeting + * `node_id` (16 bytes) at `effect_tick`. `dx/dy/dz` are authoring + * floats converted to Q32.32 *here, once* — the canonical artifact + * is the returned op bytes, so every sink derives identical state + * from them regardless of who authored or how. + * @param {bigint} effect_tick + * @param {Uint8Array} node_id + * @param {number} dx + * @param {number} dy + * @param {number} dz + * @returns {Uint8Array} + */ + sign_truss_impulse(effect_tick, node_id, dx, dy, dz) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(node_id, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + wasm.actor_sign_truss_impulse(retptr, this.__wbg_ptr, effect_tick, ptr0, len0, dx, dy, dz); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_export4(r0, r1 * 1, 1); + return v2; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} +if (Symbol.dispose) Actor.prototype[Symbol.dispose] = Actor.prototype.free; + +export class AssetProvenance { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + AssetProvenanceFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_assetprovenance_free(ptr, 0); + } + /** + * @returns {string} + */ + asset_hash_hex() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_asset_hash_hex(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * @returns {string} + */ + capability() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_capability(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * The hash this sink actually computed over the carried bytes (shown + * next to `asset_hash` so a mismatch is visible). + * @returns {string} + */ + computed_hash_hex() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_computed_hash_hex(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * The carried bytes' content hash equals the receipt's `asset_hash` + * — the asset is exactly what the receipt vouches for. A 1-bit change + * to the envelope flips this to false. + * @returns {boolean} + */ + content_ok() { + const ret = wasm.assetprovenance_content_ok(this.__wbg_ptr); + return ret !== 0; + } + /** + * @returns {string} + */ + media() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.assetprovenance_media(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); + } + } + /** + * Open an asset envelope + its provenance receipt JSON. Constructing + * with deliberately-mutated `envelope` bytes is how the page shows + * tamper-evidence: `content_ok` then returns false. + * @param {Uint8Array} envelope + * @param {string} receipt_json + */ + constructor(envelope, receipt_json) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(envelope, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(receipt_json, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + wasm.assetprovenance_new(retptr, ptr0, len0, ptr1, len1); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + this.__wbg_ptr = r0; + AssetProvenanceFinalization.register(this, this.__wbg_ptr, this); + return this; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * The carried payload bytes — the codec-specific bytes a native + * element (``, `