#!/bin/sh set -e cd "$(dirname "$0")/.." ZENA=${ZENA:-$(command -v zena-cli || true)} if [ ! -x "$ZENA" ]; then echo "build: no Zena compiler. Put zena-cli on PATH or set ZENA to its path (see CONTRIBUTING.md)." >&2 exit 1 fi BIN=./node_modules/.bin # Only the features zena already emits, so wasm-opt can't introduce ones engines don't ship yet # (--all-features yields custom-descriptor `exact` types that Node 24 rejects). SIMD is for engine/rt.wat's v128 # scan loops (fixed-width SIMD ships in every engine that ships WasmGC). FEATURES="--enable-simd --enable-gc --enable-reference-types --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --enable-nontrapping-float-to-int" # Both artifacts export only i32 functions plus memory, so no caller outside this # module graph can reach an internal function or type: --closed-world is valid, and # unlocks --gufa-optimizing's whole-program type/value inference. # --one-caller-inline-max-function-size caps -Oz's single-caller inlining, which # otherwise builds one function too big for V8 to keep in Liftoff: TurboFan then # optimizes it, which is most of the RSS a short-lived call pays (7.9MiB at the # default cap, 1.8MiB at 150; raw grows by about 150B, gzip drops by about 40B). # --low-memory-unused lets optimize-added-constants fold `p + C` into a load or store's offset, which is # exact unless p + C wraps past 4GiB into the low 1KiB; the engine's addresses are all small positive # sums, and layout.mjs keeps the static data above 1KiB so nothing lives in that range anyway. CLOSED="--low-memory-unused --closed-world --gufa-optimizing --one-caller-inline-max-function-size=150" mkdir -p build # Static tables (engine/*.wat, written by scripts/gen) are active data segments in modules that import the # engine's memory; zena cannot declare a data segment itself, so wasm-merge links them in after compiling. # engine/rt.wat, merged into every build, supplies the instructions zena has no intrinsic for; its exports # are only there to satisfy the zena module's "rt" imports, so they are removed after the merge. # merge ... merge() { src=$1 out=$2 shift 2 args="" for w in "$@"; do b=build/$(basename "$w" .wat).data.wasm "$BIN/wasm-as" "$w" -o "$b" args="$args $b $(basename "$w" .wat)" done # shellcheck disable=SC2086 "$BIN/wasm-as" engine/rt.wat --enable-bulk-memory --enable-simd -o build/rt.wasm "$BIN/wasm-merge" "$src" engine build/rt.wasm rt $args $FEATURES -o "$out" "$BIN/wasm-opt" "$out" $FEATURES --remove-exports --pass-arg=remove-exports@move -o "$out" } # engine/hooks.zena is swapped in before each compile: strip.zena imports it, never # engine/downlevel.zena directly, so the erase build's import graph never reaches # engine/downlevel.zena and wasm-opt has no dead downlevel code to chase. cp engine/hooks-erase.zena engine/hooks.zena "$ZENA" -O s build engine/main-erase.zena -o build/erase.zena.wasm --target js # -O2 rather than -Oz: about 1% more gzip for 8-20% more throughput (docs/architecture.md). The one-caller # inline cap in $CLOSED, not the -O level, is what keeps post-tier-up RSS flat. merge build/erase.zena.wasm build/erase.merged.wasm engine/keywords.wat engine/chartable.wat "$BIN/wasm-opt" build/erase.merged.wasm $FEATURES $CLOSED -O2 --converge --precompute-propagate --simplify-locals-nostructure -O2 --converge -o build/erase.wasm cp engine/hooks-downlevel.zena engine/hooks.zena "$ZENA" -O s build engine/main-downlevel.zena -o build/downlevel.zena.wasm --target js merge build/downlevel.zena.wasm build/downlevel.merged.wasm engine/keywords.wat engine/chartable.wat engine/downlevel-text.wat "$BIN/wasm-opt" build/downlevel.merged.wasm $FEATURES $CLOSED -O2 --converge --precompute-propagate --simplify-locals-nostructure -O2 --converge -o build/downlevel.wasm "$ZENA" -O s build engine/debug.zena -o build/downlevel-debug.zena.wasm --target js merge build/downlevel-debug.zena.wasm build/downlevel-debug.wasm engine/keywords.wat engine/chartable.wat engine/downlevel-text.wat rm -f engine/hooks.zena wc -c build/erase.zena.wasm | awk '{print "erase.zena.wasm " $1 " bytes (before wasm-opt)"}' wc -c build/erase.wasm | awk '{print "erase.wasm " $1 " bytes"}' gzip -9c build/erase.wasm | wc -c | awk '{print "erase.wasm.gz " $1 " bytes"}' wc -c build/downlevel.wasm | awk '{print "downlevel.wasm " $1 " bytes"}' gzip -9c build/downlevel.wasm | wc -c | awk '{print "downlevel.wasm.gz " $1 " bytes"}'