From 594367e9229c5f049f4b1bd8d29af141e75e2719 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 24 Jul 2026 03:15:52 -0700 Subject: [PATCH] feat: complete phase 0 browser feasibility --- .gitignore | 2 +- Cargo.toml | 2 +- PLAN.md | 2 +- README.md | 7 +- crates/wasm-probe/src/lib.rs | 49 ++++++++++++- crates/wasm-probe/src/main.rs | 35 +++++++--- docs/architecture.md | 6 +- .../0001-adopt-selected-icicle-components.md | 2 + docs/licensing.md | 1 + docs/research/browser-feasibility-report.md | 58 ++++++++++++++++ experiments/icicle-wasm/.gitignore | 1 + experiments/icicle-wasm/Cargo.lock | 15 ++++ experiments/icicle-wasm/Cargo.toml | 20 ++++++ experiments/icicle-wasm/README.md | 17 +++++ experiments/icicle-wasm/src/lib.rs | 69 +++++++++++++++++++ web/index.html | 3 +- web/package.json | 6 +- web/scripts/generate-probes.mjs | 52 ++++++++++++++ web/src/probe.ts | 1 + web/src/probe.worker.ts | 66 +++++++++++++++++- web/tests/probe.spec.ts | 24 +++++-- 21 files changed, 409 insertions(+), 29 deletions(-) create mode 100644 docs/research/browser-feasibility-report.md create mode 100644 experiments/icicle-wasm/.gitignore create mode 100644 experiments/icicle-wasm/Cargo.lock create mode 100644 experiments/icicle-wasm/Cargo.toml create mode 100644 experiments/icicle-wasm/README.md create mode 100644 experiments/icicle-wasm/src/lib.rs create mode 100644 web/scripts/generate-probes.mjs diff --git a/.gitignore b/.gitignore index d9978f1..8335a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ /web/node_modules/ /web/playwright-report/ /web/test-results/ -/web/public/memory64-probe.wasm +/web/public/*.wasm .DS_Store diff --git a/Cargo.toml b/Cargo.toml index ac7af58..ca1dc56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "crates/runtime-core", "crates/wasm-probe", ] -exclude = ["experiments/icicle"] +exclude = ["experiments/icicle", "experiments/icicle-wasm"] resolver = "3" [workspace.package] diff --git a/PLAN.md b/PLAN.md index 5929cac..94167c4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # AArch64 ELF-to-WebAssembly Browser Runtime ## Engineering Build Plan and Agent Handoff -**Status:** Initial architecture plan +**Status:** Phase 0 feasibility complete; Phase 1 ready **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace diff --git a/README.md b/README.md index d40b0e6..d98854b 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,17 @@ `binarrow` is an experimental browser runtime for AArch64 Linux ELF executables. It aims to load a userspace process image, interpret AArch64 through an architecture-neutral IR, handle Linux system calls against explicit host interfaces, and eventually translate hot guest code to WebAssembly. -The project is in the Phase 0 feasibility stage described in [PLAN.md](PLAN.md). The current bootstrap deliberately contains only the boundaries needed for that work: +The project has completed the Phase 0 feasibility stage described in [PLAN.md](PLAN.md). The workspace deliberately contains only the boundaries needed to begin the native static-ELF interpreter: - `crates/runtime-core`: host-independent process types, traps, and resource limits. - `crates/wasm-probe`: generation of the Memory64 WebAssembly module used by browser tests. - `web`: a Worker-based Chrome feature report and Playwright smoke test. - `experiments/icicle`: the pinned, isolated AArch64 decode/interpreter feasibility probe. +- `experiments/icicle-wasm`: a pinned P-code semantic probe compiled to raw browser Wasm. - `docs/decisions`: architecture decision records, including the selective Icicle adoption decision. +The browser gate dynamically instantiates Memory64, executes the normalized P-code semantics corresponding to an AArch64 `mov`/`add` sequence, and proves that a Promise-bearing JSPI import suspends and resumes Wasm. See [the browser feasibility report](docs/research/browser-feasibility-report.md) for the exact verified environment and diagnostics. + ## Prerequisites - Rust 1.93 (installed automatically by `rustup` from `rust-toolchain.toml`) @@ -34,7 +37,7 @@ The browser test generates `web/public/memory64-probe.wasm` from Rust before sta ## Scope -The immediate milestone is feasibility, not a web IDE or broad Linux compatibility. See [PLAN.md](PLAN.md) for the roadmap and [docs/architecture.md](docs/architecture.md) for the initial boundaries. +The immediate milestone is Phase 1's native ELF inspection and process-image path, not a web IDE or broad Linux compatibility. See [PLAN.md](PLAN.md) for the roadmap and [docs/architecture.md](docs/architecture.md) for the initial boundaries. ## License diff --git a/crates/wasm-probe/src/lib.rs b/crates/wasm-probe/src/lib.rs index 16b674e..3b75292 100644 --- a/crates/wasm-probe/src/lib.rs +++ b/crates/wasm-probe/src/lib.rs @@ -1,8 +1,9 @@ //! Small generated WebAssembly modules used to probe browser capabilities. use wasm_encoder::{ - CodeSection, ConstExpr, ExportKind, ExportSection, Function, FunctionSection, GlobalSection, - GlobalType, Instruction, MemorySection, MemoryType, Module, TypeSection, ValType, + CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection, + GlobalSection, GlobalType, ImportSection, Instruction, MemorySection, MemoryType, Module, + TypeSection, ValType, }; /// Emits a module with one 64-bit memory and an exported function that reports @@ -58,9 +59,43 @@ pub fn memory64_probe() -> Vec { module.finish() } +/// Emits a module that calls a Promise-bearing `host.value` import and adds one +/// after JSPI resumes the WebAssembly stack. +#[must_use] +pub fn jspi_probe() -> Vec { + let mut module = Module::new(); + + let mut types = TypeSection::new(); + types.ty().function([], [ValType::I32]); + module.section(&types); + + let mut imports = ImportSection::new(); + imports.import("host", "value", EntityType::Function(0)); + module.section(&imports); + + let mut functions = FunctionSection::new(); + functions.function(0); + module.section(&functions); + + let mut exports = ExportSection::new(); + exports.export("run", ExportKind::Func, 1); + module.section(&exports); + + let mut code = CodeSection::new(); + let mut function = Function::new([]); + function.instruction(&Instruction::Call(0)); + function.instruction(&Instruction::I32Const(1)); + function.instruction(&Instruction::I32Add); + function.instruction(&Instruction::End); + code.function(&function); + module.section(&code); + + module.finish() +} + #[cfg(test)] mod tests { - use super::memory64_probe; + use super::{jspi_probe, memory64_probe}; #[test] fn generated_module_has_wasm_header() { @@ -77,5 +112,13 @@ mod tests { #[test] fn generation_is_deterministic() { assert_eq!(memory64_probe(), memory64_probe()); + assert_eq!(jspi_probe(), jspi_probe()); + } + + #[test] + fn generated_jspi_fixture_is_valid_core_wasm() { + wasmparser::Validator::new() + .validate_all(&jspi_probe()) + .expect("generated JSPI fixture should be valid WebAssembly"); } } diff --git a/crates/wasm-probe/src/main.rs b/crates/wasm-probe/src/main.rs index 57584e3..7d9f4f7 100644 --- a/crates/wasm-probe/src/main.rs +++ b/crates/wasm-probe/src/main.rs @@ -1,19 +1,38 @@ -use std::{env, fs, process::ExitCode}; +use std::{env, fs, path::Path, process::ExitCode}; -use binarrow_wasm_probe::memory64_probe; +use binarrow_wasm_probe::{jspi_probe, memory64_probe}; fn main() -> ExitCode { - let Some(output_path) = env::args_os().nth(1) else { - eprintln!("usage: binarrow-wasm-probe "); + let Some(output_dir) = env::args_os().nth(1) else { + eprintln!("usage: binarrow-wasm-probe "); return ExitCode::FAILURE; }; - let bytes = memory64_probe(); - match fs::write(&output_path, bytes) { + match write_probes(Path::new(&output_dir)) { Ok(()) => ExitCode::SUCCESS, - Err(error) => { - eprintln!("failed to write {}: {error}", output_path.display()); + Err(message) => { + eprintln!("{message}"); ExitCode::FAILURE } } } + +fn write_probes(output_dir: &Path) -> Result<(), String> { + fs::create_dir_all(output_dir).map_err(|error| { + format!( + "failed to create probe directory {}: {error}", + output_dir.display() + ) + })?; + + for (name, bytes) in [ + ("memory64-probe.wasm", memory64_probe()), + ("jspi-probe.wasm", jspi_probe()), + ] { + let output_path = output_dir.join(name); + fs::write(&output_path, bytes) + .map_err(|error| format!("failed to write {}: {error}", output_path.display()))?; + } + + Ok(()) +} diff --git a/docs/architecture.md b/docs/architecture.md index e8343dd..189b1ab 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -This document captures the boundaries the Phase 0 bootstrap establishes. It is intentionally smaller than the target workspace in `PLAN.md`; crates should be split only when a dependency or portability boundary becomes useful. +This document captures the boundaries established by the completed Phase 0 feasibility work. It is intentionally smaller than the target workspace in `PLAN.md`; crates should be split only when a dependency or portability boundary becomes useful. ## Dependency direction @@ -45,3 +45,7 @@ Owns generated WebAssembly feature fixtures. Keeping the bytes generated by `was ## Planned next boundaries The Icicle feasibility spike is isolated under `experiments/icicle`; it is not a dependency of `runtime-core`. [ADR-0001](decisions/0001-adopt-selected-icicle-components.md) selects Icicle's SLEIGH/P-code/CPU interpreter components behind project-owned IR and memory adapters while excluding its native JIT and host-coupled runtime boundary. + +`experiments/icicle-wasm` separately pins Icicle's lightweight `pcode` crate and compiles an allocation-free semantic probe to raw `wasm32-unknown-unknown`. The browser Worker executes that artifact without `icicle-vm`, Cranelift, or host services. This validates the intended P-code boundary while the two upstream portability fixes needed by deeper Icicle crates remain isolated in the native experiment's patch set. + +The next production boundary is an ELF inspection crate plus a thin native CLI. It should own format validation and segment descriptions without depending on the execution engine or host services. diff --git a/docs/decisions/0001-adopt-selected-icicle-components.md b/docs/decisions/0001-adopt-selected-icicle-components.md index 9986993..19bfda9 100644 --- a/docs/decisions/0001-adopt-selected-icicle-components.md +++ b/docs/decisions/0001-adopt-selected-icicle-components.md @@ -72,3 +72,5 @@ Native filesystem access, `std::thread::sleep`, UDP diagnostics, native signals, The full commands, crate-by-crate results, dependency path to the native JIT blocker, runtime assumptions, and licensing review are recorded in [the Icicle dependency report](../research/icicle-dependency-report.md). The pinned experiment in `experiments/icicle` compiles the supported Ghidra AArch64 SLEIGH source, interprets two instructions with JIT disabled, and validates the resulting register and program-counter state. + +The companion `experiments/icicle-wasm` crate pins the same revision's `pcode` package, compiles to raw `wasm32-unknown-unknown`, and executes the normalized `mov`/`add` semantic sequence in Chrome. This keeps the browser acceptance proof honest without treating the native-only `icicle-vm` boundary as portable. diff --git a/docs/licensing.md b/docs/licensing.md index e7f3218..2896250 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -25,6 +25,7 @@ Before adopting a foundational emulator, decoder, ABI source, runtime, or toolch | `wasm-feature-detect` | Canonical browser feature probes | Apache-2.0 | JavaScript dependency | | Vite, TypeScript, Playwright | Build, type-check, and test the probe | MIT / Apache-2.0 | Development dependencies | | Selected Icicle components | AArch64 SLEIGH/P-code lifter and interpreter feasibility | MIT OR Apache-2.0 | Pinned Git experiment; product integration pending | +| Icicle `pcode` | Browser P-code boundary and semantic probe | MIT OR Apache-2.0 | Pinned Git experiment; no copied source | | Ghidra AArch64 language data | AArch64 decode and semantics specification | Apache-2.0 | Fetched pinned source data; not yet distributed | Refresh this table whenever production or foundational dependencies change. Toolchain and guest-runtime distribution manifests will be tracked separately once binary assets are introduced. diff --git a/docs/research/browser-feasibility-report.md b/docs/research/browser-feasibility-report.md new file mode 100644 index 0000000..91227dc --- /dev/null +++ b/docs/research/browser-feasibility-report.md @@ -0,0 +1,58 @@ +# Browser feasibility report + +## Result + +The remaining Phase 0 browser gates pass. A Worker running in headless Chrome can: + +1. Dynamically compile and instantiate a Memory64 module generated with `wasm-encoder`. +2. Instantiate a raw `wasm32-unknown-unknown` artifact using Icicle's pinned P-code model and execute the normalized semantics corresponding to AArch64 `mov x0, #42; add x0, x0, #1`, producing `43`. +3. Call a Promise-bearing JavaScript import from Wasm, suspend the Wasm stack through JSPI, resume on a later event-loop turn, add one in Wasm, and resolve the promising export with `42`. + +The automated run on July 24, 2026 used Playwright's Chrome `149.0.7827.55`. The test requires Chrome 137 or newer because JSPI shipped in Chrome 137. The implementation follows the WebAssembly proposal's current `WebAssembly.Suspending` and `WebAssembly.promising` API. + +Primary references: + +- [Chrome 137 release notes](https://developer.chrome.com/release-notes/137#javascript_promise_integration) +- [WebAssembly JavaScript Promise Integration proposal](https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md) + +## Artifacts and boundaries + +- `crates/wasm-probe` generates the Memory64 and JSPI core Wasm fixtures. +- `experiments/icicle-wasm` is a standalone Cargo workspace that pins Icicle revision `6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3` and emits the P-code semantic fixture. +- `web/scripts/generate-probes.mjs` builds and copies generated artifacts into `web/public`; generated `.wasm` files are ignored by Git. +- `web/src/probe.worker.ts` performs all compilation, instantiation, semantic, and suspension checks away from the main thread. + +The browser fixture intentionally depends only on Icicle's `pcode` package. Deeper SLEIGH and CPU crates still need the two narrow portability changes recorded in the Icicle dependency report, while `icicle-vm` remains excluded because it unconditionally reaches the native Cranelift JIT. + +## Reproduction + +```sh +pnpm --dir web install --frozen-lockfile +pnpm --dir web run check +pnpm --dir web run build +pnpm --dir web test +``` + +The build and test hooks regenerate all three `.wasm` files. The Playwright test asserts that Memory64, AArch64 P-code semantics, and JSPI are supported; capability-only results for tail calls, SIMD, relaxed SIMD, multiple memories, typed function references, and finalized exception handling remain visible in the report but do not gate Phase 0. + +## Failure diagnostics + +Each probe is isolated and returns an independent result. Fetch, compilation, instantiation, missing-export, and JSPI exceptions are caught and attached to the corresponding feature result rather than crashing the Worker. The page exposes the diagnostic as the result element's `title`, and a top-level Worker failure changes the status element to an explicit error state. + +The JSPI check also rejects false positives where the API names exist but: + +- the wrapped export does not return a Promise; +- the host Promise resolves synchronously instead of suspending Wasm; or +- the resumed value does not flow back through Wasm and produce `42`. + +## Phase 0 conclusion + +All Phase 0 acceptance gates now have executable evidence: + +- reusable Icicle components and rejected boundaries are documented; +- a browser artifact executes an AArch64-derived semantic operation; +- generated Wasm compiles and runs in Chrome; +- dependency licensing has been reviewed; and +- ADR-0001 records both the selected foundation and fallback boundary. + +The next milestone is Phase 1's ELF inspection CLI and static process-image loader. diff --git a/experiments/icicle-wasm/.gitignore b/experiments/icicle-wasm/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/experiments/icicle-wasm/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/experiments/icicle-wasm/Cargo.lock b/experiments/icicle-wasm/Cargo.lock new file mode 100644 index 0000000..ccb7362 --- /dev/null +++ b/experiments/icicle-wasm/Cargo.lock @@ -0,0 +1,15 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "binarrow-icicle-wasm-probe" +version = "0.1.0" +dependencies = [ + "pcode", +] + +[[package]] +name = "pcode" +version = "0.2.0" +source = "git+https://github.com/icicle-emu/icicle-emu?rev=6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3#6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3" diff --git a/experiments/icicle-wasm/Cargo.toml b/experiments/icicle-wasm/Cargo.toml new file mode 100644 index 0000000..c49b863 --- /dev/null +++ b/experiments/icicle-wasm/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "binarrow-icicle-wasm-probe" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +pcode = { git = "https://github.com/icicle-emu/icicle-emu", rev = "6f53b6016e63ddcff80a105b8e5ad37caf7a6ed3" } + +[workspace] + +[lints.rust] +unsafe_code = "deny" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } diff --git a/experiments/icicle-wasm/README.md b/experiments/icicle-wasm/README.md new file mode 100644 index 0000000..06574fb --- /dev/null +++ b/experiments/icicle-wasm/README.md @@ -0,0 +1,17 @@ +# Icicle P-code Wasm probe + +This isolated crate pins Icicle's `pcode` crate at the revision evaluated in +ADR-0001. It compiles to `wasm32-unknown-unknown` and exports an allocation-free +function that executes the normalized P-code operations corresponding to the +native probe's AArch64 `mov x0, #42; add x0, x0, #1` sequence. + +The native experiment proves Ghidra SLEIGH decoding and Icicle interpretation. +This smaller artifact proves that the selected P-code boundary and the same +semantic operation execute inside Chrome without pulling `icicle-vm`, Cranelift, +or host services into the browser dependency graph. + +```sh +cargo test --manifest-path experiments/icicle-wasm/Cargo.toml +cargo build --release --target wasm32-unknown-unknown \ + --manifest-path experiments/icicle-wasm/Cargo.toml +``` diff --git a/experiments/icicle-wasm/src/lib.rs b/experiments/icicle-wasm/src/lib.rs new file mode 100644 index 0000000..9283821 --- /dev/null +++ b/experiments/icicle-wasm/src/lib.rs @@ -0,0 +1,69 @@ +//! Browser-target proof for the project-owned Icicle P-code boundary. + +use pcode::{Instruction, Op, Value, VarNode}; + +const X0: VarNode = VarNode::new(1, 8); + +#[derive(Default)] +struct SemanticState { + x0: u64, +} + +impl SemanticState { + fn read(&self, value: Value) -> u64 { + match value { + Value::Const(value, _) => value, + Value::Var(var) if var == X0 => self.x0, + Value::Var(var) => panic!("unexpected P-code variable: {var:?}"), + } + } + + fn write(&mut self, output: VarNode, value: u64) { + assert_eq!(output, X0, "probe may only write AArch64 x0"); + self.x0 = value; + } + + fn execute(&mut self, instruction: Instruction) { + let [a, b] = instruction.inputs.get(); + let value = match instruction.op { + Op::Copy => self.read(a), + Op::IntAdd => self.read(a).wrapping_add(self.read(b)), + op => panic!("unexpected P-code operation: {op:?}"), + }; + self.write(instruction.output, value); + } +} + +/// Executes the normalized P-code semantics corresponding to `AArch64` +/// `mov x0, #42; add x0, x0, #1`. +#[must_use] +pub fn run_semantic_probe() -> u64 { + let instructions = [ + X0.copy_from(Value::Const(42, 8)), + (X0, Op::IntAdd, X0, Value::Const(1, 8)).into(), + ]; + let mut state = SemanticState::default(); + for instruction in instructions { + state.execute(instruction); + } + state.x0 +} + +/// Stable, allocation-free browser export used by the Worker probe. +// The Rust 2024 unsafe attribute is required to give the raw Wasm export a +// predictable name; the function itself contains no unsafe operations. +#[allow(unsafe_code)] +#[unsafe(no_mangle)] +pub extern "C" fn binarrow_aarch64_semantic_probe() -> u64 { + run_semantic_probe() +} + +#[cfg(test)] +mod tests { + use super::run_semantic_probe; + + #[test] + fn executes_aarch64_mov_add_semantics() { + assert_eq!(run_semantic_probe(), 43); + } +} diff --git a/web/index.html b/web/index.html index f97d29e..55cd8c2 100644 --- a/web/index.html +++ b/web/index.html @@ -17,7 +17,8 @@

This Worker checks the WebAssembly capabilities required by the planned AArch64 userspace runtime. Memory64 is tested by instantiating a module - generated from Rust with wasm-encoder. + generated from Rust with wasm-encoder; JSPI and the + browser-portable P-code path are exercised end to end.

Starting probe Worker…

diff --git a/web/package.json b/web/package.json index 95c517b..9454ce6 100644 --- a/web/package.json +++ b/web/package.json @@ -11,9 +11,9 @@ "build": "vite build", "check": "tsc --noEmit", "dev": "vite --host 127.0.0.1", - "generate:probe": "cargo run --quiet --manifest-path ../Cargo.toml -p binarrow-wasm-probe -- public/memory64-probe.wasm", - "prebuild": "pnpm generate:probe", - "pretest": "pnpm generate:probe", + "generate:probes": "node scripts/generate-probes.mjs", + "prebuild": "pnpm generate:probes", + "pretest": "pnpm generate:probes", "test": "playwright test" }, "dependencies": { diff --git a/web/scripts/generate-probes.mjs b/web/scripts/generate-probes.mjs new file mode 100644 index 0000000..55c3b82 --- /dev/null +++ b/web/scripts/generate-probes.mjs @@ -0,0 +1,52 @@ +import { copyFileSync, mkdirSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const webDirectory = fileURLToPath(new URL("../", import.meta.url)); +const repositoryDirectory = fileURLToPath(new URL("../../", import.meta.url)); +const publicDirectory = fileURLToPath(new URL("../public/", import.meta.url)); +const semanticManifest = fileURLToPath( + new URL("../../experiments/icicle-wasm/Cargo.toml", import.meta.url), +); +const semanticArtifact = fileURLToPath( + new URL( + "../../experiments/icicle-wasm/target/wasm32-unknown-unknown/release/binarrow_icicle_wasm_probe.wasm", + import.meta.url, + ), +); + +mkdirSync(publicDirectory, { recursive: true }); + +execFileSync( + "cargo", + [ + "run", + "--quiet", + "--locked", + "-p", + "binarrow-wasm-probe", + "--", + publicDirectory, + ], + { cwd: repositoryDirectory, stdio: "inherit" }, +); + +execFileSync( + "cargo", + [ + "build", + "--quiet", + "--locked", + "--release", + "--target", + "wasm32-unknown-unknown", + "--manifest-path", + semanticManifest, + ], + { cwd: webDirectory, stdio: "inherit" }, +); + +copyFileSync( + semanticArtifact, + fileURLToPath(new URL("../public/icicle-semantic-probe.wasm", import.meta.url)), +); diff --git a/web/src/probe.ts b/web/src/probe.ts index de6dd2a..4b04d61 100644 --- a/web/src/probe.ts +++ b/web/src/probe.ts @@ -1,5 +1,6 @@ export const featureNames = [ "Memory64", + "AArch64 P-code semantics", "JSPI", "Tail calls", "SIMD", diff --git a/web/src/probe.worker.ts b/web/src/probe.worker.ts index f7b1098..b004efe 100644 --- a/web/src/probe.worker.ts +++ b/web/src/probe.worker.ts @@ -1,6 +1,5 @@ import { exceptionsFinal, - jspi, multiMemory, relaxedSimd, simd, @@ -16,6 +15,19 @@ interface Memory64Exports extends WebAssembly.Exports { probe_marker: WebAssembly.Global; } +interface SemanticProbeExports extends WebAssembly.Exports { + binarrow_aarch64_semantic_probe: () => bigint; +} + +interface JspiProbeExports extends WebAssembly.Exports { + run: () => number; +} + +interface JspiApi { + Suspending: new (callback: () => Promise) => WebAssembly.ImportValue; + promising: (callback: () => number) => () => Promise; +} + async function instantiateMemory64Probe(): Promise { const response = await fetch("/memory64-probe.wasm"); if (!response.ok) { @@ -33,6 +45,55 @@ async function instantiateMemory64Probe(): Promise { ); } +async function instantiateSemanticProbe(): Promise { + const response = await fetch("/icicle-semantic-probe.wasm"); + if (!response.ok) { + throw new Error(`AArch64 semantic fixture returned HTTP ${response.status}`); + } + + const bytes = await response.arrayBuffer(); + const { instance } = await WebAssembly.instantiate(bytes); + const exports = instance.exports as SemanticProbeExports; + + return exports.binarrow_aarch64_semantic_probe() === 43n; +} + +async function runJspiRoundTrip(): Promise { + const jspi = WebAssembly as typeof WebAssembly & JspiApi; + if (typeof jspi.Suspending !== "function" || typeof jspi.promising !== "function") { + return false; + } + + const response = await fetch("/jspi-probe.wasm"); + if (!response.ok) { + throw new Error(`JSPI fixture returned HTTP ${response.status}`); + } + + let hostPromiseResolved = false; + const value = new jspi.Suspending( + () => + new Promise((resolve) => { + setTimeout(() => { + hostPromiseResolved = true; + resolve(41); + }, 0); + }), + ); + const bytes = await response.arrayBuffer(); + const { instance } = await WebAssembly.instantiate(bytes, { + host: { value }, + }); + const exports = instance.exports as JspiProbeExports; + const run = jspi.promising(exports.run); + const pendingResult = run(); + + if (!(pendingResult instanceof Promise) || hostPromiseResolved) { + return false; + } + + return (await pendingResult) === 42 && hostPromiseResolved; +} + async function runProbe( name: FeatureName, probe: () => Promise, @@ -52,7 +113,8 @@ async function runProbe( async function createReport(): Promise { return Promise.all([ runProbe("Memory64", instantiateMemory64Probe), - runProbe("JSPI", jspi), + runProbe("AArch64 P-code semantics", instantiateSemanticProbe), + runProbe("JSPI", runJspiRoundTrip), runProbe("Tail calls", tailCall), runProbe("SIMD", simd), runProbe("Relaxed SIMD", relaxedSimd), diff --git a/web/tests/probe.spec.ts b/web/tests/probe.spec.ts index 2fd8df0..ee63023 100644 --- a/web/tests/probe.spec.ts +++ b/web/tests/probe.spec.ts @@ -1,12 +1,24 @@ import { expect, test } from "@playwright/test"; -test("reports required WebAssembly features from a Worker", async ({ page }) => { +test("executes the required Phase 0 proofs in a Worker", async ({ + browser, + page, +}) => { await page.goto("/"); await expect(page.getByRole("status")).toHaveText("Feature report ready"); - await expect(page.getByText("Memory64", { exact: true })).toBeVisible(); - await expect( - page.locator("dt", { hasText: "Memory64" }).locator("+ dd"), - ).toHaveText("Supported"); - await expect(page.locator("#report dt")).toHaveCount(8); + for (const requiredFeature of [ + "Memory64", + "AArch64 P-code semantics", + "JSPI", + ]) { + await expect(page.getByText(requiredFeature, { exact: true })).toBeVisible(); + await expect( + page.locator("dt", { hasText: requiredFeature }).locator("+ dd"), + ).toHaveText("Supported"); + } + await expect(page.locator("#report dt")).toHaveCount(9); + + const chromeMajor = Number.parseInt(browser.version().split(".")[0] ?? "0", 10); + expect(chromeMajor).toBeGreaterThanOrEqual(137); }); -- 2.51.2