diff --git a/Cargo.lock b/Cargo.lock index 2054095..f22e86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,7 @@ name = "binarrow-browser-runtime" version = "0.1.0" dependencies = [ "binarrow-aarch64", + "binarrow-execution-ir", "binarrow-host-api", "binarrow-linux-runtime", "binarrow-loader", diff --git a/PLAN.md b/PLAN.md index 8827fe6..447787e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1863,7 +1863,7 @@ The first Phase 4 checkpoint is implemented. The interpreter identifies basic-bl The initial basic-block lowering checkpoint is also implemented. Decoder output now crosses into the project-owned `binarrow-execution-ir` crate, where blocks are validated as non-empty, contiguous single-entry sequences with a final terminator and scratch storage no longer exposes Icicle types. `binarrow-wasm-backend` lowers its scalar Tier-1 subset against an explicit imported state-memory ABI, emits deterministic modules and translation metrics, and returns structured unsupported-operation results for interpreter fallback. A checked-in three-instruction AArch64 fixture sets `x0` to 40, adds two, and loops; Chromium lifts and dynamically compiles that real block, verifies the translated state contains `x0 == 42`, and verifies its next PC is the block entry. Hot-dispatch integration, broader operation coverage, and the translation cache are next. -Hot-dispatch integration is now implemented for the initial scalar subset. The interpreter offers blocks to a backend after a deterministic execution threshold, charges translated instructions against the same process budget, applies replacement architectural state atomically, and remembers unsupported block identities for interpreter fallback. The browser backend compiles and caches core Wasm modules by guest address plus instruction encodings, reuses a module-local imported state memory, and exposes translated-block, translated-instruction, fallback, compilation, cache-hit, and emitted-byte counters. The Chromium infinite-loop regression executes 64 cold iterations in the interpreter and the next 64 from one generated Wasm module with 63 cache hits. Broader operation coverage, explicit cache-invalidation regressions, a larger differential suite, function-table dispatch, and benchmark evidence remain before Phase 4 is complete. +Hot-dispatch integration is now implemented for the initial scalar subset. The interpreter offers blocks to a backend after a deterministic execution threshold, charges translated instructions against the same process budget, applies replacement architectural state atomically, and remembers unsupported block identities for interpreter fallback. The browser backend compiles and caches core Wasm modules by guest address plus instruction encodings, reuses a module-local imported state memory, and exposes translated-block, translated-instruction, fallback, compilation, cache-hit, and emitted-byte counters. The Chromium infinite-loop regression executes 64 cold iterations in the interpreter and the next 64 from one generated Wasm module with 63 cache hits. Code-identity regressions replace executable bytes at the same guest address and prove that both the fallback set and compiled-module cache select a new identity. The dynamic translation probe initializes interpreter and Wasm execution from identical state, then compares the complete scalar state ABI and next PC. Broader operation coverage, a larger differential corpus, function-table dispatch, and benchmark evidence remain before Phase 4 is complete. Do not begin the full web IDE before item 30 passes. diff --git a/crates/aarch64/src/lib.rs b/crates/aarch64/src/lib.rs index 634b824..6a6aa5e 100644 --- a/crates/aarch64/src/lib.rs +++ b/crates/aarch64/src/lib.rs @@ -2881,6 +2881,9 @@ mod tests { const TIGHT_LOOP_CODE: &[u8] = &[ 0x00, 0x00, 0x00, 0x14, // b . ]; + const CHANGED_TIGHT_LOOP_CODE: &[u8] = &[ + 0x00, 0x00, 0x00, 0x94, // bl . + ]; #[test] fn state_models_integer_vector_and_userspace_control_registers() { @@ -3021,6 +3024,60 @@ mod tests { ); } + #[test] + fn retries_translation_when_executable_bytes_change_at_the_same_address() { + let mut memory = executable_memory(TIGHT_LOOP_CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + let mut interpreter = Interpreter::new().unwrap(); + let mut executor = LoopBlockExecutor { + executions: 0, + supported: false, + }; + + for _ in 0..2 { + assert_eq!( + interpreter.run_until_supervisor_call_tiered( + &mut state, + &mut memory, + 1, + 1, + 4, + &mut executor, + ), + Err(Trap::ResourceLimit(ResourceLimit::Instructions)) + ); + } + assert_eq!(executor.executions, 1); + + memory.unmap(CODE_ADDRESS, 4).unwrap(); + memory + .map_zeroed( + CODE_ADDRESS, + 4, + Permissions::READ_EXECUTE, + RegionKind::Anonymous, + ) + .unwrap(); + memory + .initialize(CODE_ADDRESS, CHANGED_TIGHT_LOOP_CODE) + .unwrap(); + + assert_eq!( + interpreter.run_until_supervisor_call_tiered( + &mut state, + &mut memory, + 1, + 1, + 4, + &mut executor, + ), + Err(Trap::ResourceLimit(ResourceLimit::Instructions)) + ); + assert_eq!(executor.executions, 2); + assert_eq!(state.x(30), Some(CODE_ADDRESS.get() + 4)); + assert_eq!(interpreter.tiered_metrics().interpreter_fallback_blocks, 2); + } + #[test] fn runs_until_svc_and_reports_the_linux_abi_register() { let mut memory = executable_memory(SYSCALL_CODE); diff --git a/crates/browser-runtime/Cargo.toml b/crates/browser-runtime/Cargo.toml index 8731113..3d1a277 100644 --- a/crates/browser-runtime/Cargo.toml +++ b/crates/browser-runtime/Cargo.toml @@ -20,5 +20,8 @@ wasm-bindgen.workspace = true [lib] crate-type = ["cdylib", "rlib"] +[dev-dependencies] +binarrow-execution-ir = { path = "../execution-ir", version = "0.1.0" } + [lints] workspace = true diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index 7410af0..629d70d 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -1,7 +1,7 @@ //! WebAssembly entry point for Worker-hosted guest execution. use core::convert::Infallible; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", test))] use std::collections::BTreeMap; use binarrow_aarch64::{Aarch64State, BasicBlock, BlockExecutor, Interpreter}; @@ -12,9 +12,9 @@ use binarrow_linux_runtime::{ExecutionError, ExecutionEvent, Process}; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; use binarrow_memory_fs::MemoryFileSystem; use binarrow_runtime_core::{MemoryAccess, ResourceLimit, Trap}; -use binarrow_wasm_backend::compile as compile_wasm_block; -#[cfg(target_arch = "wasm32")] -use binarrow_wasm_backend::{GENERAL_REGISTERS_OFFSET, STACK_POINTER_OFFSET, STATE_BYTES}; +use binarrow_wasm_backend::{ + GENERAL_REGISTERS_OFFSET, STACK_POINTER_OFFSET, STATE_BYTES, compile as compile_wasm_block, +}; use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] @@ -61,7 +61,7 @@ extern "C" { const HOT_BLOCK_THRESHOLD: u64 = 64; const MAX_TRANSLATED_BLOCK_INSTRUCTIONS: u32 = 64; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", test))] const MAX_TRANSLATED_BLOCKS: usize = 4096; const COMPILER_HELLO_ELF: &[u8] = @@ -119,6 +119,9 @@ pub struct BrowserExecution { pub struct BrowserTranslation { wasm_module: Vec, entry: u64, + initial_state: Vec, + interpreted_state: Vec, + interpreted_next_pc: u64, guest_instructions: u32, normalized_operations: u32, wasm_bytes: u32, @@ -133,12 +136,12 @@ struct BrowserTranslationMetrics { #[derive(Default)] struct BrowserBlockExecutor { - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", test))] cache: BTreeMap, CachedBrowserBlock>, metrics: BrowserTranslationMetrics, } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", test))] struct CachedBrowserBlock { cache_key: String, wasm_module: Vec, @@ -165,6 +168,18 @@ impl BrowserBlockExecutor { block: &BasicBlock, state: &Aarch64State, ) -> Option { + let cached = self.get_or_compile(block)?; + let mut state_bytes = encode_translation_state(state); + let next_pc = + binarrow_execute_translated(&cached.cache_key, &cached.wasm_module, &mut state_bytes) + .ok()?; + decode_translation_state(state, &state_bytes, next_pc) + } +} + +#[cfg(any(target_arch = "wasm32", test))] +impl BrowserBlockExecutor { + fn get_or_compile(&mut self, block: &BasicBlock) -> Option<&CachedBrowserBlock> { let identity = block .instructions() .iter() @@ -190,16 +205,11 @@ impl BrowserBlockExecutor { }, ); } - let cached = self.cache.get(&identity)?; - let mut state_bytes = encode_translation_state(state); - let next_pc = - binarrow_execute_translated(&cached.cache_key, &cached.wasm_module, &mut state_bytes) - .ok()?; - decode_translation_state(state, &state_bytes, next_pc) + self.cache.get(&identity) } } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", test))] fn translation_cache_key(identity: &[(u64, u32)]) -> String { identity .iter() @@ -208,7 +218,6 @@ fn translation_cache_key(identity: &[(u64, u32)]) -> String { .join(",") } -#[cfg(target_arch = "wasm32")] fn encode_translation_state(state: &Aarch64State) -> Vec { let mut bytes = vec![0; usize::try_from(STATE_BYTES).expect("state ABI fits host usize")]; for index in 0_u8..31 { @@ -264,6 +273,24 @@ impl BrowserTranslation { self.entry } + #[wasm_bindgen(getter)] + #[must_use] + pub fn initial_state(&self) -> Vec { + self.initial_state.clone() + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn interpreted_state(&self) -> Vec { + self.interpreted_state.clone() + } + + #[wasm_bindgen(getter)] + #[must_use] + pub fn interpreted_next_pc(&self) -> u64 { + self.interpreted_next_pc + } + #[wasm_bindgen(getter)] #[must_use] pub fn guest_instructions(&self) -> u32 { @@ -316,15 +343,26 @@ fn translate_fixture_entry_inner( ) .map_err(|error| error.to_string())?; let entry = image.initial_state.pc; + let initial_state = Aarch64State::new(entry, image.initial_state.sp); let mut interpreter = Interpreter::new().map_err(|error| error.to_string())?; let block = interpreter .lift_basic_block(&image.memory, entry, max_instructions) .map_err(|error| error.to_string())?; let compiled = compile_wasm_block(&block).map_err(|error| error.to_string())?; let metrics = compiled.metrics(); + let mut interpreted_state = initial_state.clone(); + let mut memory = image.memory; + for _ in block.instructions() { + interpreter + .step(&mut interpreted_state, &mut memory) + .map_err(|error| format!("{error:?}"))?; + } Ok(BrowserTranslation { wasm_module: compiled.bytes().to_vec(), entry: entry.get(), + initial_state: encode_translation_state(&initial_state), + interpreted_state: encode_translation_state(&interpreted_state), + interpreted_next_pc: interpreted_state.pc().get(), guest_instructions: metrics.guest_instructions, normalized_operations: metrics.normalized_operations, wasm_bytes: metrics.wasm_bytes, @@ -1110,9 +1148,12 @@ impl HostTerminal for CapturedTerminal { #[cfg(test)] mod tests { + use binarrow_execution_ir::{BasicBlock, LiftedInstruction, Operation, Value, ValueSource}; + use binarrow_runtime_core::GuestAddress; + use super::{ - COMPILER_HELLO_ELF, Credentials, Interpreter, ProcessConfig, ProcessParameters, - execute_fixture, fixture, load_process, start_fixture, start_program, + BrowserBlockExecutor, COMPILER_HELLO_ELF, Credentials, Interpreter, ProcessConfig, + ProcessParameters, execute_fixture, fixture, load_process, start_fixture, start_program, translate_fixture_entry_inner, }; @@ -1122,6 +1163,25 @@ mod tests { const DEFAULT_MEMORY: u64 = 256 * 1024 * 1024; const DEFAULT_FILESYSTEM: u64 = 16 * 1024 * 1024; + fn cached_loop_block(encoding: u32) -> BasicBlock { + BasicBlock::new(vec![LiftedInstruction { + pc: GuestAddress::new(0x1000), + fallthrough: GuestAddress::new(0x1004), + encoding, + operations: vec![Operation::Branch { + condition: Value { + source: ValueSource::Constant(1), + size: 1, + }, + target: Value { + source: ValueSource::Constant(0x1000), + size: 8, + }, + }], + }]) + .unwrap() + } + #[test] fn executes_the_c_and_rust_browser_fixtures_on_the_native_test_host() { for (fixture, message, instructions, syscalls) in [ @@ -1357,9 +1417,35 @@ mod tests { assert_eq!(&translation.wasm_module[..8], b"\0asm\x01\0\0\0"); assert_eq!(translation.guest_instructions, 3); assert!(translation.normalized_operations > 0); + assert_ne!(translation.initial_state, translation.interpreted_state); + assert_eq!(translation.interpreted_next_pc, entry.get()); assert_eq!( usize::try_from(translation.wasm_bytes).unwrap(), translation.wasm_module.len() ); } + + #[test] + fn translation_cache_invalidates_when_code_changes_at_the_same_address() { + let original = cached_loop_block(0x1400_0000); + let changed = cached_loop_block(0x9400_0000); + let mut executor = BrowserBlockExecutor::default(); + + let original_cached = executor.get_or_compile(&original).unwrap(); + let original_key = original_cached.cache_key.clone(); + let original_wasm_bytes = original_cached.wasm_module.len(); + let repeated_key = executor + .get_or_compile(&original) + .unwrap() + .cache_key + .clone(); + let changed_key = executor.get_or_compile(&changed).unwrap().cache_key.clone(); + + assert_eq!(original_key, repeated_key); + assert_ne!(original_key, changed_key); + assert_ne!(original_wasm_bytes, 0); + assert_eq!(executor.cache.len(), 2); + assert_eq!(executor.metrics.compiled_blocks, 2); + assert_eq!(executor.metrics.cache_hits, 1); + } } diff --git a/docs/architecture.md b/docs/architecture.md index 934ebf8..ecca1e5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -100,4 +100,4 @@ The Icicle feasibility spike is isolated under `experiments/icicle`; it is not a `experiments/icicle-wasm` separately validates Icicle's lightweight `pcode` crate in raw `wasm32-unknown-unknown`. The production `binarrow-aarch64` crate pins `pcode`, `sleigh-runtime`, and the filesystem-free portion of `sleigh-compile` at that same revision and keeps their types private. The required AHash/getrandom browser backend is selected in the workspace's target configuration; `icicle-cpu` and its native VM/JIT remain outside the production graph. -Phase 3's VFS, OPFS persistence, host services, image packaging, and native/browser CPython checkpoints are complete. Phase 4 now has bounded profiling, scalar basic-block lowering, browser-side dynamic compilation, hot dispatch, interpreter fallback, a session-local module cache, and observable translation metrics. Broader lowering, cache invalidation tests, function-table dispatch, differential coverage, and performance evidence remain. +Phase 3's VFS, OPFS persistence, host services, image packaging, and native/browser CPython checkpoints are complete. Phase 4 now has bounded profiling, scalar basic-block lowering, browser-side dynamic compilation, hot dispatch, interpreter fallback, a session-local module cache, code-identity invalidation tests, and observable translation metrics. The first differential probe compares the complete scalar state ABI and next PC after interpreting and translating the same block. Broader lowering and differential coverage, function-table dispatch, and performance evidence remain. diff --git a/web/src/probe.worker.ts b/web/src/probe.worker.ts index 107dc8e..6736b75 100644 --- a/web/src/probe.worker.ts +++ b/web/src/probe.worker.ts @@ -158,11 +158,14 @@ async function runTranslatedBlockProbe(): Promise { .instance ?? (instantiated as WebAssembly.Instance); const exports = instance.exports as TranslatedBlockExports; + const initialState = translation.initial_state; + const stateBytes = new Uint8Array(state.buffer, 0, initialState.length); + stateBytes.set(initialState); const nextPc = exports.run(0); - const translatedX0 = new DataView(state.buffer).getBigUint64(0, true); + const expectedState = translation.interpreted_state; return ( - nextPc === translation.entry && - translatedX0 === 42n && + nextPc === translation.interpreted_next_pc && + stateBytes.every((byte, index) => byte === expectedState[index]) && translation.guest_instructions === 3 && translation.wasm_bytes === translation.wasm_module.byteLength );