diff --git a/PLAN.md b/PLAN.md index 447787e..368bb15 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1861,9 +1861,9 @@ Phase 3 is complete. `guest-tests/cpython/build.sh` checksum-pins CPython 3.12.1 The first Phase 4 checkpoint is implemented. The interpreter identifies basic-block boundaries from normalized external-branch and supervisor-call operations, counts successful block entries in a deterministic bounded profile, and exposes the ordered results through the Linux process boundary. Browser reports include the tracked-block count, hottest guest address, and its execution count. Focused regressions prove a tight self-loop reaches exactly the configured hotness while instruction exhaustion, syscall behavior, and ordinary fixture outputs remain unchanged. Basic-block Wasm lowering is next. -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. +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 AArch64 scalar-loop fixture gives Chromium a real decoded block to compile dynamically and compare against interpreter state. Hot-dispatch integration, broader operation coverage, and the translation cache followed from this checkpoint. -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. +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 across a 17-instruction block covering normal and exceptional signed/unsigned division, shifts, and bitwise operations. Structured forward internal branches lower to Wasm blocks while malformed control retains interpreter fallback. 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/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index 629d70d..4bce34d 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -1406,16 +1406,20 @@ mod tests { let mut memory = image.memory; let mut state = binarrow_aarch64::Aarch64State::new(entry, image.initial_state.sp); let mut interpreter = Interpreter::new().unwrap(); - for _ in 0..3 { + for _ in 0..17 { interpreter.step(&mut state, &mut memory).unwrap(); } - assert_eq!(state.x(0), Some(42)); + assert_eq!(state.x(2), Some(42)); + assert_eq!(state.x(5), Some(4)); + assert_eq!(state.x(8), Some(0)); + assert_eq!(state.x(11), Some(1_u64 << 63)); + assert_eq!(state.x(14), Some(42)); assert_eq!(state.pc(), entry); - let translation = translate_fixture_entry_inner("translated-loop", 4).unwrap(); + let translation = translate_fixture_entry_inner("translated-loop", 32).unwrap(); assert_eq!(&translation.wasm_module[..8], b"\0asm\x01\0\0\0"); - assert_eq!(translation.guest_instructions, 3); + assert_eq!(translation.guest_instructions, 17); assert!(translation.normalized_operations > 0); assert_ne!(translation.initial_state, translation.interpreted_state); assert_eq!(translation.interpreted_next_pc, entry.get()); diff --git a/crates/wasm-backend/src/lib.rs b/crates/wasm-backend/src/lib.rs index ea396d9..ba7520d 100644 --- a/crates/wasm-backend/src/lib.rs +++ b/crates/wasm-backend/src/lib.rs @@ -85,6 +85,7 @@ pub fn compile(block: &BasicBlock) -> Result { let mut scratch_locals = BTreeMap::new(); let mut operation_count = 0_u32; for (instruction_index, instruction) in block.instructions().iter().enumerate() { + validate_internal_control(&instruction.operations, instruction.pc)?; for (operation_index, operation) in instruction.operations.iter().enumerate() { operation_count = operation_count.saturating_add(1); collect_operation_storage( @@ -219,6 +220,16 @@ fn collect_operation_storage( left, right, } + | Operation::Divide { + destination, + left, + right, + } + | Operation::SignedDivide { + destination, + left, + right, + } | Operation::BitwiseAnd { destination, left, @@ -302,6 +313,10 @@ fn collect_operation_storage( collect_value_storage(condition, instruction_index, locals); collect_value_storage(target, instruction_index, locals); } + Operation::InternalBranch { condition, .. } => { + collect_value_storage(condition, instruction_index, locals); + } + Operation::Label(_) => {} _ => { return Err(UnsupportedBlock { pc, @@ -313,6 +328,36 @@ fn collect_operation_storage( Ok(()) } +fn validate_internal_control( + operations: &[Operation], + pc: GuestAddress, +) -> Result<(), UnsupportedBlock> { + let mut labels = Vec::new(); + for (operation_index, operation) in operations.iter().copied().enumerate() { + match operation { + Operation::InternalBranch { label, .. } => labels.push(label), + Operation::Label(label) if labels.pop() == Some(label) => {} + Operation::Label(_) => { + return Err(UnsupportedBlock { + pc, + operation_index: u32::try_from(operation_index).unwrap_or(u32::MAX), + reason: "internal control flow is not properly nested", + }); + } + _ => {} + } + } + if labels.is_empty() { + Ok(()) + } else { + Err(UnsupportedBlock { + pc, + operation_index: u32::try_from(operations.len()).unwrap_or(u32::MAX), + reason: "internal branch has no matching forward label", + }) + } +} + fn collect_value_storage(value: Value, instruction_index: usize, locals: &mut ScratchLocals) { if let ValueSource::Storage(storage) = value.source { collect_storage(storage, instruction_index, locals); @@ -427,6 +472,14 @@ fn emit_operation( temporary_local, unsupported, )?, + operation @ (Operation::Divide { .. } | Operation::SignedDivide { .. }) => emit_divide( + function, + operation, + instruction_index, + locals, + temporary_local, + unsupported, + )?, operation @ (Operation::BooleanAnd { .. } | Operation::BooleanOr { .. }) => { emit_boolean_binary( function, @@ -477,6 +530,16 @@ fn emit_operation( function.instruction(&Instruction::End); return Ok(true); } + Operation::InternalBranch { condition, .. } => { + function.instruction(&Instruction::Block(BlockType::Empty)); + emit_value(function, condition, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64Const(0)); + function.instruction(&Instruction::I64Ne); + function.instruction(&Instruction::BrIf(0)); + } + Operation::Label(_) => { + function.instruction(&Instruction::End); + } _ => return Err(unsupported("operation is not in the scalar Tier-1 subset")), } Ok(false) @@ -561,6 +624,7 @@ fn emit_boolean_binary( ) } +#[allow(clippy::too_many_lines)] fn binary_parts(operation: Operation) -> Option<(Place, Value, Value)> { match operation { Operation::Add { @@ -578,6 +642,16 @@ fn binary_parts(operation: Operation) -> Option<(Place, Value, Value)> { left, right, } + | Operation::Divide { + destination, + left, + right, + } + | Operation::SignedDivide { + destination, + left, + right, + } | Operation::BitwiseAnd { destination, left, @@ -657,6 +731,75 @@ fn binary_parts(operation: Operation) -> Option<(Place, Value, Value)> { } } +fn emit_divide( + function: &mut Function, + operation: Operation, + instruction_index: usize, + locals: &ScratchLocals, + temporary_local: u32, + unsupported: impl Fn(&'static str) -> UnsupportedBlock + Copy, +) -> Result<(), UnsupportedBlock> { + let (destination, left, right) = + binary_parts(operation).ok_or_else(|| unsupported("operation has no divide form"))?; + if left.size != right.size { + return Err(unsupported("divide operands have different widths")); + } + match operation { + Operation::Divide { .. } => { + emit_value(function, right, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64Eqz); + function.instruction(&Instruction::If(BlockType::Result(ValType::I64))); + function.instruction(&Instruction::I64Const(0)); + function.instruction(&Instruction::Else); + emit_value(function, left, instruction_index, locals, unsupported)?; + emit_value(function, right, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64DivU); + function.instruction(&Instruction::End); + } + Operation::SignedDivide { .. } => { + emit_signed_value(function, right, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64Eqz); + function.instruction(&Instruction::If(BlockType::Result(ValType::I64))); + function.instruction(&Instruction::I64Const(0)); + function.instruction(&Instruction::Else); + emit_signed_value(function, left, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64Const(signed_minimum(left.size)?)); + function.instruction(&Instruction::I64Eq); + emit_signed_value(function, right, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64Const(-1)); + function.instruction(&Instruction::I64Eq); + function.instruction(&Instruction::I32And); + function.instruction(&Instruction::If(BlockType::Result(ValType::I64))); + emit_signed_value(function, left, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::Else); + emit_signed_value(function, left, instruction_index, locals, unsupported)?; + emit_signed_value(function, right, instruction_index, locals, unsupported)?; + function.instruction(&Instruction::I64DivS); + function.instruction(&Instruction::End); + function.instruction(&Instruction::End); + } + _ => return Err(unsupported("operation is not scalar division")), + } + emit_write( + function, + destination, + instruction_index, + locals, + temporary_local, + unsupported, + ) +} + +fn signed_minimum(size: u8) -> Result { + check_scalar_size(size).map_err(|reason| UnsupportedBlock { + pc: GuestAddress::NULL, + operation_index: 0, + reason, + })?; + let shift = 64_u32 - u32::from(size) * 8; + Ok(i64::MIN >> shift) +} + fn emit_flag_operation( function: &mut Function, operation: Operation, @@ -1071,4 +1214,96 @@ mod tests { assert_eq!(error.pc, GuestAddress::new(0x2000)); assert_eq!(error.operation_index, 0); } + + #[test] + fn emits_guarded_division_and_forward_internal_control() { + let quotient = Place { + storage: Storage::Scratch { id: 0, offset: 0 }, + size: 8, + }; + let divisor_is_zero = Place { + storage: Storage::Scratch { id: 1, offset: 0 }, + size: 1, + }; + let divisor_zero = Value { + source: ValueSource::Storage(divisor_is_zero.storage), + size: 1, + }; + let block = BasicBlock::new(vec![LiftedInstruction { + pc: GuestAddress::new(0x3000), + fallthrough: GuestAddress::new(0x3004), + encoding: 0, + operations: vec![ + Operation::Copy { + destination: quotient, + source: constant(0), + }, + Operation::Equal { + destination: divisor_is_zero, + left: constant(0), + right: constant(0), + }, + Operation::InternalBranch { + condition: divisor_zero, + label: 0, + }, + Operation::Divide { + destination: quotient, + left: constant(84), + right: constant(0), + }, + Operation::Label(0), + Operation::Copy { + destination: Place { + storage: Storage::X { + index: 0, + offset: 0, + }, + size: 8, + }, + source: Value { + source: ValueSource::Storage(quotient.storage), + size: 8, + }, + }, + Operation::Branch { + condition: constant(1), + target: constant(0x3000), + }, + ], + }]) + .unwrap(); + + let compiled = compile(&block).unwrap(); + wasmparser::Validator::new() + .validate_all(compiled.bytes()) + .expect("guarded division should produce valid structured WebAssembly"); + } + + #[test] + fn rejects_unmatched_internal_control_for_interpreter_fallback() { + let block = BasicBlock::new(vec![LiftedInstruction { + pc: GuestAddress::new(0x4000), + fallthrough: GuestAddress::new(0x4004), + encoding: 0, + operations: vec![ + Operation::InternalBranch { + condition: constant(1), + label: 7, + }, + Operation::Branch { + condition: constant(1), + target: constant(0x4000), + }, + ], + }]) + .unwrap(); + + let error = compile(&block).unwrap_err(); + assert_eq!(error.pc, GuestAddress::new(0x4000)); + assert_eq!( + error.reason, + "internal branch has no matching forward label" + ); + } } diff --git a/docs/architecture.md b/docs/architecture.md index ecca1e5..44dd062 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -80,7 +80,7 @@ Provides the narrow wasm-bindgen boundary used by the Worker. It embeds determin ### `binarrow-wasm-backend` -Consumes validated project-owned basic blocks and emits deterministic core WebAssembly modules. The initial Tier-1 state ABI imports a bounded Memory32 state page, stores `x0..x30` followed by SP, and exports `run(i32 state_base) -> i64 next_pc`. Scalar register, scratch, arithmetic, comparison, flag-helper, and external-branch operations lower directly; unsupported memory, vector, floating-point, supervisor, or internal-control operations return a structured fallback reason. Chromium dynamically compiles a real three-instruction AArch64 block, verifies its `x0 = 42` state mutation, and compares its next PC with the loop entry. The normal browser execution path also dispatches a hot self-loop through one cached generated module while preserving instruction-budget and profiling accounting. +Consumes validated project-owned basic blocks and emits deterministic core WebAssembly modules. The initial Tier-1 state ABI imports a bounded Memory32 state page, stores `x0..x30` followed by SP, and exports `run(i32 state_base) -> i64 next_pc`. Scalar register, scratch, arithmetic, guarded signed/unsigned division, comparison, flag-helper, structured forward internal-control, and external-branch operations lower directly; unsupported memory, vector, floating-point, supervisor, or malformed internal-control operations return a structured fallback reason. Chromium dynamically compiles a real 17-instruction AArch64 block and compares its complete scalar state ABI plus next PC with the interpreter, including AArch64's nontrapping divide-by-zero and signed-overflow behavior. The normal browser execution path also dispatches a hot self-loop through one cached generated module while preserving instruction-budget and profiling accounting. ### `binarrow-cli` @@ -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, 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. +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 differential probe compares the complete scalar state ABI and next PC after interpreting and translating the same 17-instruction arithmetic/control block. Broader lowering and differential coverage, function-table dispatch, and performance evidence remain. diff --git a/guest-tests/translated-loop/README.md b/guest-tests/translated-loop/README.md index 08725a1..221b3dd 100644 --- a/guest-tests/translated-loop/README.md +++ b/guest-tests/translated-loop/README.md @@ -1,9 +1,11 @@ # Translated-loop AArch64 fixture -This static AArch64 ELF sets `x0` to 40, adds two, and branches back to its -entry. It is the deterministic Phase 4 differential fixture: the interpreter -and dynamically compiled WebAssembly block must both produce `x0 == 42` and -the entry address as the next program counter. +This static AArch64 ELF exercises unsigned and signed division, architectural +divide-by-zero and signed-overflow behavior, shifts, and bitwise operations +before branching back to its entry. It is the deterministic Phase 4 +differential fixture: the interpreter and dynamically compiled WebAssembly +block must produce identical scalar register/SP state and the same next program +counter. Build from any working directory with Zig 0.16.0: @@ -17,5 +19,5 @@ bytes without requiring Zig at test time. Expected SHA-256: ```text -48f14f982f26207850112022d7d9daa6af05d1727bd1f6a632b74dba30127c2e +fc9be9d9ea613ee23c361a27929d8d4b3685b28aed6ac9b1162d73b56eb5b1b2 ``` diff --git a/guest-tests/translated-loop/main.S b/guest-tests/translated-loop/main.S index 551d8f5..8f6c6ce 100644 --- a/guest-tests/translated-loop/main.S +++ b/guest-tests/translated-loop/main.S @@ -3,6 +3,24 @@ .type _start, %function _start: - mov x0, #40 - add x0, x0, #2 + mov x0, #84 + mov x1, #2 + udiv x2, x0, x1 + + mov x3, #-8 + mov x4, #-2 + sdiv x5, x3, x4 + + mov x6, #123 + mov x7, #0 + udiv x8, x6, x7 + + mov x9, #-1 + mov x10, #1 + lsl x10, x10, #63 + sdiv x11, x10, x9 + + eor x12, x2, x5 + lsl x13, x5, #3 + orr x14, x13, x2 b _start diff --git a/guest-tests/translated-loop/translated-loop.aarch64.elf b/guest-tests/translated-loop/translated-loop.aarch64.elf index 4c71de9d05bed549efdc32ab259cd7052a47b52e..fa7b357ff77e9205fb2ef8ca43d4cccd3db7dd8b 100644 GIT binary patch delta 112 zcmaFCvVe7h2IGQ>n#(y|7$Cr9;!StQ2CjxnjtmW#m^cp3dd$!;Nrj { } async function runTranslatedBlockProbe(): Promise { - const translation = translate_fixture_entry("translated-loop", 4); + const translation = translate_fixture_entry("translated-loop", 32); try { const state = new WebAssembly.Memory({ initial: 1, maximum: 1 }); const instantiated = await WebAssembly.instantiate(translation.wasm_module, { @@ -166,7 +166,7 @@ async function runTranslatedBlockProbe(): Promise { return ( nextPc === translation.interpreted_next_pc && stateBytes.every((byte, index) => byte === expectedState[index]) && - translation.guest_instructions === 3 && + translation.guest_instructions === 17 && translation.wasm_bytes === translation.wasm_module.byteLength ); } finally {