Add a stack-based bytecode compiler and interpreter that replaces direct AST walking for top-level program execution. The three-phase pipeline (parse → compile → execute) compiles 76 opcodes covering all implemented JS features while delegating function calls to the existing AST interpreter for correctness. Includes suspension-ready CallFrame design, source-location-preserving error messages, 9 bytecode-specific unit tests, and 6 newly passing Test262 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
432 lines
27 KiB
Markdown
432 lines
27 KiB
Markdown
# Story 3.1: Bytecode IR Introduction
|
|
|
|
Status: done
|
|
|
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
## Story
|
|
|
|
As a browser developer,
|
|
I want the JavaScript engine to execute via a bytecode interpreter instead of an AST walker,
|
|
So that suspension semantics (generators, async/await) can be cleanly implemented and execution performance improves.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Full backward compatibility:** All previously passing Test262 tests (vendored + full suite) and JS unit tests continue to pass with identical results when running through the bytecode interpreter.
|
|
|
|
2. **Three-phase compilation pipeline works:** `js_parser` produces an AST, a new bytecode compiler emits bytecode instructions from the AST, and a bytecode interpreter executes them — producing the same output as the current AST walker.
|
|
|
|
3. **Instruction set covers all currently implemented features:** The bytecode instruction set supports: variables (var/let/const with TDZ), functions (declarations, expressions, arrows, closures), control flow (if/else, for, for-in, for-of, while, do-while, switch, break, continue, return), try/catch/finally/throw, classes (constructor, methods, extends, super), destructuring (array/object/nested/rest/defaults), operators (arithmetic, comparison, logical, bitwise, unary, update, compound assignment, nullish coalescing, optional chaining), template literals, spread/rest, typeof/delete/void/instanceof/in, `new` expressions, property access (dot, computed, optional chaining), and eval().
|
|
|
|
4. **Suspension-ready frame design:** The interpreter's frame/stack design supports saving and restoring execution state — each call frame stores its own instruction pointer, local variables, and operand stack, enabling future generators/async-await to freeze and resume frames.
|
|
|
|
5. **Statement count and call depth limits preserved:** `VmConfig.max_statements` and `VmConfig.max_call_depth` continue to enforce execution limits with identical behavior.
|
|
|
|
6. **`docs/JavaScript_Implementation_Checklist.md` updated** to check off "Bytecode compiler + VM", and `just ci` passes.
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Task 1: Design bytecode instruction set (AC: #3, #4)
|
|
- [x] 1.1 Create `crates/js_vm/src/bytecode/mod.rs` with the bytecode module structure:
|
|
- `mod.rs` — module root, re-exports
|
|
- `opcodes.rs` — instruction definitions
|
|
- `chunk.rs` — bytecode container (instructions + constant pool)
|
|
- `compiler.rs` — AST-to-bytecode compiler
|
|
- `vm.rs` — bytecode interpreter / virtual machine
|
|
- [x] 1.2 Define `Opcode` enum in `opcodes.rs`. Core instruction categories:
|
|
- **Constants/Literals:** `Constant(u16)` (index into constant pool), `Undefined`, `Null`, `True`, `False`
|
|
- **Arithmetic:** `Add`, `Sub`, `Mul`, `Div`, `Mod`, `Exp`, `Neg`, `BitwiseNot`
|
|
- **Comparison:** `Equal`, `StrictEqual`, `NotEqual`, `StrictNotEqual`, `LessThan`, `LessEqual`, `GreaterThan`, `GreaterEqual`
|
|
- **Logical/Bitwise:** `BitAnd`, `BitOr`, `BitXor`, `ShiftLeft`, `ShiftRight`, `UnsignedShiftRight`
|
|
- **Unary:** `TypeOf`, `Void`, `LogicalNot`
|
|
- **Control flow:** `Jump(i32)`, `JumpIfFalse(i32)`, `JumpIfTrue(i32)`, `JumpIfNullish(i32)`
|
|
- **Variables:** `GetLocal(u16)`, `SetLocal(u16)`, `GetGlobal(u16)`, `SetGlobal(u16)`, `GetUpvalue(u16)`, `SetUpvalue(u16)`, `DeclareVar(u16)`, `DeclareLet(u16)`, `DeclareConst(u16)`
|
|
- **Functions:** `MakeClosure(u16)`, `MakeArrow(u16)`, `Call(u8)`, `Return`, `CallMethod(u16, u8)`
|
|
- **Objects/Arrays:** `MakeObject(u16)`, `MakeArray(u16)`, `GetProperty(u16)`, `SetProperty(u16)`, `GetComputed`, `SetComputed`, `DeleteProperty(u16)`, `DeleteComputed`, `In`, `InstanceOf`
|
|
- **Stack:** `Pop`, `Dup`, `Swap`
|
|
- **Scope:** `PushScope`, `PopScope`, `PushFunctionScope`, `PopFunctionScope`
|
|
- **Error handling:** `PushTry(i32)`, `PopTry`, `Throw`, `SetCatchBinding(u16)`
|
|
- **Iteration:** `GetIterator`, `IteratorNext`, `IteratorDone`
|
|
- **Class:** `MakeClass`, `SetPrototype`, `DefineMethod(u16)`, `DefineStaticMethod(u16)`, `SuperCall(u8)`, `SuperGet(u16)`
|
|
- **Destructuring:** `DestructureArray`, `DestructureObject(u16)`, `DestructureRest`
|
|
- **Special:** `Spread`, `NewTarget`, `This`, `StmtCount` (increment statement counter for limits)
|
|
- **New:** `Construct(u8)` (new expressions)
|
|
- **Eval:** `DirectEval`
|
|
- **Template:** `TemplateConcat(u8)`
|
|
- **Update:** `PreIncrement`, `PostIncrement`, `PreDecrement`, `PostDecrement` (these operate on variable references)
|
|
- [x] 1.3 Define `Chunk` struct in `chunk.rs`:
|
|
```rust
|
|
pub struct Chunk {
|
|
pub code: Vec<u8>, // Encoded bytecode
|
|
pub constants: Vec<JsValue>, // Constant pool (strings, numbers, function blueprints)
|
|
pub spans: Vec<Span>, // Source locations for error reporting (parallel to instructions)
|
|
pub local_names: Vec<String>,// Local variable name table (for debugging/eval)
|
|
}
|
|
```
|
|
- [x] 1.4 Define `CallFrame` struct for suspension-ready execution (AC: #4):
|
|
```rust
|
|
pub struct CallFrame {
|
|
pub chunk: Rc<Chunk>,
|
|
pub ip: usize, // Instruction pointer (can be saved/restored)
|
|
pub base_slot: usize, // Stack base for this frame's locals
|
|
pub local_count: usize, // Number of locals in this frame
|
|
pub this_value: JsValue,
|
|
pub is_constructor: bool,
|
|
pub strict: bool,
|
|
}
|
|
```
|
|
The key insight: each frame has its own `ip` and stack base, so a generator can freeze mid-frame and resume later.
|
|
|
|
- [x] Task 2: Implement bytecode compiler — statements (AC: #2, #3)
|
|
- [x] 2.1 Create `compiler.rs` with `Compiler` struct:
|
|
```rust
|
|
pub struct Compiler {
|
|
chunk: Chunk,
|
|
locals: Vec<Local>, // Compile-time local variable tracking
|
|
scope_depth: usize,
|
|
loop_stack: Vec<LoopContext>, // For break/continue jump patching
|
|
try_depth: usize,
|
|
}
|
|
struct Local {
|
|
name: String,
|
|
depth: usize,
|
|
kind: BindingKind, // Var/Let/Const
|
|
initialized: bool,
|
|
}
|
|
struct LoopContext {
|
|
start: usize, // Loop start offset (for continue)
|
|
break_jumps: Vec<usize>, // Unpatched break jumps (patched at loop end)
|
|
}
|
|
```
|
|
- [x] 2.2 Implement the three-phase compilation approach matching current execution model:
|
|
- **Phase 1 scan:** Walk top-level statements, emit `DeclareFunction` for function declarations (compile their bodies as sub-chunks)
|
|
- **Phase 2 scan:** Walk top-level statements, emit `DeclareVar` for var declarations
|
|
- **Phase 3:** Compile each statement to bytecode
|
|
- [x] 2.3 Implement statement compilation methods:
|
|
- `compile_stmt(&mut self, stmt: &Statement)` — main dispatcher
|
|
- `compile_var_decl()` — handle var/let/const with initializers, patterns
|
|
- `compile_if_stmt()` — condition + conditional jump + alternate
|
|
- `compile_for_stmt()` — init + loop body + update + back-edge jump
|
|
- `compile_for_in_stmt()` — get keys + iterate loop
|
|
- `compile_for_of_stmt()` — get iterator + iterate loop
|
|
- `compile_while_stmt()`, `compile_do_while_stmt()`
|
|
- `compile_switch_stmt()` — discriminant + case comparisons + fall-through
|
|
- `compile_try_stmt()` — PushTry + body + PopTry + catch block + finally
|
|
- `compile_block()` — PushScope + statements + PopScope
|
|
- `compile_return()`, `compile_throw()`, `compile_break()`, `compile_continue()`
|
|
- `compile_class_decl()` — class setup + methods + prototype chain
|
|
|
|
- [x] Task 3: Implement bytecode compiler — expressions (AC: #2, #3)
|
|
- [x] 3.1 Implement expression compilation methods:
|
|
- `compile_expr(&mut self, expr: &Expr)` — main dispatcher
|
|
- Literals: push Constant from pool or dedicated True/False/Null/Undefined opcodes
|
|
- Identifiers: resolve to GetLocal/GetUpvalue/GetGlobal based on scope analysis
|
|
- Binary ops: compile left, compile right, emit operator opcode
|
|
- Short-circuit: LogicalAnd/Or/NullishCoalesce use JumpIfFalse/JumpIfTrue/JumpIfNullish
|
|
- Unary ops: compile operand, emit operator opcode
|
|
- Assignment: compile value, emit SetLocal/SetGlobal/SetProperty
|
|
- Member access: compile object, emit GetProperty/GetComputed
|
|
- Function calls: compile callee, compile args, emit Call(argc)
|
|
- Method calls: compile object, emit CallMethod(name, argc)
|
|
- `new` expressions: compile constructor + args, emit Construct(argc)
|
|
- Arrow/function expressions: compile body as sub-chunk, emit MakeClosure/MakeArrow
|
|
- Object/array literals: compile elements, emit MakeObject/MakeArray
|
|
- Template literals: compile parts, emit TemplateConcat
|
|
- Conditional: compile condition + JumpIfFalse + consequent + Jump + alternate
|
|
- Update (++/--): handle pre/post, local/property targets
|
|
- Spread: compile inner, emit Spread
|
|
- Destructuring assignment: decompose into individual SetLocal/SetProperty ops
|
|
- Optional chaining: JumpIfNullish to skip chain
|
|
- [x] 3.2 Implement compile-time scope resolution:
|
|
- Track locals by name and scope depth
|
|
- Resolve identifiers to local slots at compile time where possible
|
|
- Fall back to global lookup for unresolved identifiers
|
|
- Handle TDZ: mark let/const as uninitialized until declaration point
|
|
- [x] 3.3 Handle function compilation as nested chunks:
|
|
- Each function body compiles to its own `Chunk`
|
|
- Store compiled functions as constants in the parent chunk
|
|
- Capture information for closures (upvalue indices)
|
|
|
|
- [x] Task 4: Implement bytecode interpreter / VM (AC: #2, #4, #5)
|
|
- [x] 4.1 Create `vm.rs` with `BytecodeVm` struct:
|
|
```rust
|
|
pub struct BytecodeVm {
|
|
stack: Vec<JsValue>, // Operand stack
|
|
frames: Vec<CallFrame>, // Call frame stack
|
|
env: Environment, // Reuse existing Environment for variable storage
|
|
config: VmConfig,
|
|
stmt_count: usize,
|
|
output: Box<dyn OutputSink>,
|
|
host: Option<Box<dyn HostEnvironment>>,
|
|
}
|
|
```
|
|
- [x] 4.2 Implement the main execution loop in `run()`:
|
|
```rust
|
|
pub fn run(&mut self) -> Result<JsValue, RuntimeError> {
|
|
loop {
|
|
let frame = self.frames.last_mut().unwrap();
|
|
let opcode = frame.read_opcode();
|
|
match opcode {
|
|
// ... dispatch all opcodes
|
|
}
|
|
}
|
|
}
|
|
```
|
|
- [x] 4.3 Implement opcode handlers — key execution semantics to preserve:
|
|
- **Variable access:** GetLocal reads from stack slot `frame.base_slot + index`; GetGlobal uses Environment
|
|
- **Function calls:** Push new CallFrame, bind parameters, execute body
|
|
- **Return:** Pop CallFrame, push return value on caller's stack
|
|
- **Throw:** Unwind frames looking for try handlers; if none found, return RuntimeError::Thrown
|
|
- **Try/catch/finally:** PushTry records a handler offset; on throw, jump to handler; finally always executes
|
|
- **Scope management:** PushScope/PopScope interact with Environment for block-scoped variables
|
|
- **Statement counting:** StmtCount opcode increments counter, checks against max_statements
|
|
- **Call depth:** Check frames.len() against max_call_depth on every Call/Construct
|
|
- [x] 4.4 Integrate with existing `HostEnvironment` trait:
|
|
- Host object property access via `host.get_property()`/`host.set_property()`
|
|
- Host function calls via `host.call_global_function()`
|
|
- Host constructor calls via `host.construct()`
|
|
- The `web_api` crate's `DomHost` must work identically through bytecode VM
|
|
- [x] 4.5 Implement `eval()` support:
|
|
- Direct eval: parse string, compile to chunk, execute in current environment
|
|
- Indirect eval: parse string, compile to chunk, execute in global environment
|
|
|
|
- [x] Task 5: Wire bytecode VM into js_vm and js crate facades (AC: #2)
|
|
- [x] 5.1 Modify `crates/js_vm/src/lib.rs`:
|
|
- Add `pub mod bytecode;` module declaration
|
|
- Modify `JsVm::execute_program()` to: parse → compile → run bytecode (instead of direct AST walking)
|
|
- Preserve the `VmState` state machine (Created, Primed, Running, Failed)
|
|
- Preserve `invoke_function()` and `call_function_with_host()` public APIs
|
|
- [x] 5.2 The `JsEngine` facade in `crates/js/src/lib.rs` should require NO changes — it calls `JsVm` which handles the switch internally.
|
|
- [x] 5.3 Ensure `define_global()` works: globals must be accessible from bytecode via Environment
|
|
- [x] 5.4 Ensure `call_function_with_host()` works: must be able to call stored JsFunction values through bytecode VM (used by event dispatch, setTimeout callbacks, etc.)
|
|
|
|
- [x] Task 6: Comprehensive testing and validation (AC: #1, #6)
|
|
- [x] 6.1 Run full vendored Test262 suite: `cargo test -p rust_browser --test js262_harness js262_suite_matches_manifest_expectations -- --nocapture`
|
|
- ALL currently passing tests must still pass
|
|
- ALL known_fail tests must still fail (no regressions in either direction is fine; promotions are a bonus)
|
|
- [x] 6.2 Run full upstream Test262 suite: `just test262-full`
|
|
- Compare results against current baseline
|
|
- [x] 6.3 Run all JS integration tests:
|
|
- `cargo test -p rust_browser --test js_tests`
|
|
- `cargo test -p rust_browser --test js_dom_tests`
|
|
- `cargo test -p rust_browser --test js_events`
|
|
- `cargo test -p rust_browser --test js_scheduling`
|
|
- [x] 6.4 Run all unit tests in js_vm: `cargo test -p js_vm`
|
|
- [x] 6.5 Add bytecode-specific unit tests in `crates/js_vm/src/bytecode/`:
|
|
- Compiler output tests: verify specific AST patterns produce expected bytecode
|
|
- VM execution tests: verify opcode semantics in isolation
|
|
- Round-trip tests: compile and execute, compare with expected output
|
|
- [x] 6.6 Run `just ci` — full validation pass
|
|
- [x] 6.7 Update `docs/JavaScript_Implementation_Checklist.md`: check off "Bytecode compiler + VM"
|
|
|
|
## Dev Notes
|
|
|
|
### Architecture Decision Context
|
|
|
|
The architecture document explicitly specifies this transition:
|
|
> "Bytecode introduction: When generators/async-await become the next JS priority. AST walker is sufficient for current spec compliance work. Bytecode IR (not JIT) introduced as a bytecode interpreter when suspension semantics require it."
|
|
|
|
Epic 3 is the forcing function — Story 3.2 (Generators) and 3.3 (async/await) require suspension semantics that are architecturally awkward on the AST walker.
|
|
|
|
### Current JS Engine Architecture (What You're Replacing)
|
|
|
|
**Parser** (`crates/js_parser/`, ~13K LOC): Hand-written recursive descent. Produces `Program` containing `Vec<Statement>`. `Statement` enum (~20 variants), `Expr` enum (~40 variants). This crate is NOT modified — bytecode compiler consumes its AST output.
|
|
|
|
**Interpreter** (`crates/js_vm/`, ~250K+ LOC across all files):
|
|
- `lib.rs` (18K LOC): `JsVm` state machine, three-phase execution (`execute_program`), public API
|
|
- `environment.rs` (23K LOC): `Environment` with scope stack, `Binding` with TDZ, `JsFunction` struct
|
|
- `value.rs` (64K LOC): `JsValue` enum, `JsObject` (Rc<RefCell<JsObjectData>>), type coercions, built-in methods
|
|
- `interpreter/mod.rs` (29K LOC): `Interpreter` struct, built-in setup, hoisting
|
|
- `interpreter/statements.rs` (42K LOC): Statement execution, control flow, destructuring
|
|
- `interpreter/expressions/calls.rs` (44K LOC): Function calls, `new`, method dispatch
|
|
- `interpreter/expressions/mod.rs` (17K LOC): Expression evaluation dispatcher
|
|
- `interpreter/expressions/member_access.rs` (15K LOC): Property access, prototype chain
|
|
- `interpreter/expressions/operators.rs` (39K LOC): Binary/unary operators, coercion
|
|
- `interpreter/expressions/coercion.rs` (9K LOC): Type coercion helpers
|
|
|
|
**Critical: What to REUSE vs. what to REPLACE:**
|
|
- **REUSE:** `Environment`, `JsValue`, `JsObject`, `JsFunction` struct, all built-in method implementations in `value.rs`, `OutputSink`, `HostEnvironment` trait, `VmConfig`, `VmState`, error types. These are the runtime data structures — they work the same regardless of execution model.
|
|
- **REPLACE:** The `Interpreter` struct and its `execute_stmt()`/`eval_expr()` tree-walk. This becomes the bytecode compiler + VM loop.
|
|
|
|
### Execution Model Differences
|
|
|
|
**AST Walker (current):**
|
|
```
|
|
AST Statement → execute_stmt() → recursive call to eval_expr() → StmtResult
|
|
```
|
|
The interpreter recursively walks the AST, using Rust's call stack for control flow.
|
|
|
|
**Bytecode VM (target):**
|
|
```
|
|
AST → Compiler → Chunk (flat bytecode) → VM loop reads opcodes → operand stack
|
|
```
|
|
The VM uses an explicit operand stack and instruction pointer. Control flow is via jumps, not Rust recursion. This is what enables suspension — you can freeze the IP and stack.
|
|
|
|
### Key Design Decisions
|
|
|
|
**1. Operand Stack vs. Register VM:**
|
|
Use a **stack-based VM** (like CPython, JVM, Lua 4). Simpler to compile to, simpler to implement, and the stack naturally maps to expression evaluation. Register VMs (like Lua 5, V8 Ignition) are faster but significantly more complex to compile to.
|
|
|
|
**2. Encoding:**
|
|
Use a **variable-width encoding** where each opcode is a `u8` followed by operands of varying sizes. Common opcodes (Add, Pop, Return) are single bytes. Opcodes with operands (Constant, Jump, GetLocal) encode operands as subsequent bytes (u8 for small, u16 for larger). This is simpler than a fixed-width encoding and more compact.
|
|
|
|
**3. Constant Pool:**
|
|
Each `Chunk` has its own `Vec<JsValue>` constant pool. String literals, number literals, and compiled function bodies are stored as constants. The `Constant(u16)` opcode indexes into this pool, supporting up to 65536 constants per chunk.
|
|
|
|
**4. Local Variable Resolution:**
|
|
Resolve local variables at compile time to stack slot indices where possible. Variables that escape (closures, eval) fall back to Environment-based lookup. This gives a performance boost for the common case while preserving correctness.
|
|
|
|
**5. Reuse Environment for Scoping:**
|
|
The bytecode VM reuses the existing `Environment` struct for scope management rather than implementing its own scope chain. This preserves the proven scoping semantics (var hoisting, let/const TDZ, block scoping) and avoids reimplementing complex scoping logic.
|
|
|
|
**6. Built-ins Stay in value.rs:**
|
|
All built-in method implementations (Array.prototype.map, String.prototype.slice, etc.) live in `value.rs` as methods on `JsValue`/`JsObject`. The bytecode VM calls these the same way the AST walker does — via method dispatch on JsValue. Do NOT rewrite built-ins.
|
|
|
|
### What NOT to Implement
|
|
|
|
- **Do NOT implement JIT compilation** — bytecode interpreter only (per architecture decision)
|
|
- **Do NOT implement generators or async/await** — that's Stories 3.2 and 3.3
|
|
- **Do NOT implement ES modules** — that's Story 3.4
|
|
- **Do NOT modify `js_parser`** — the parser's AST output is the compiler's input
|
|
- **Do NOT rewrite built-in methods** — reuse all existing JsValue/JsObject methods
|
|
- **Do NOT change the `JsValue` or `JsObject` types** — these are the runtime data model
|
|
- **Do NOT change the `HostEnvironment` trait** — this is the browser integration point
|
|
- **Do NOT delete the AST walker code yet** — keep it for reference during development (can be removed in a follow-up cleanup)
|
|
- **Do NOT add any new external dependencies** — the bytecode VM is pure Rust using only existing crate deps
|
|
- **Do NOT implement upvalue/closure capture in this story** — reuse the existing Environment-based dynamic lookup. Proper upvalue capture can be added if needed for performance later.
|
|
|
|
### Phased Implementation Strategy
|
|
|
|
Given the massive scope, implement in this order:
|
|
|
|
**Phase A — Skeleton:** Define Opcode enum, Chunk struct, CallFrame struct. Implement a minimal compiler that handles: number/string/boolean literals, variable declarations (let/const), simple assignments, console.log calls, return statements. Implement a minimal VM that runs this bytecode. Verify with a trivial test.
|
|
|
|
**Phase B — Expressions & Operators:** Add all arithmetic, comparison, logical, bitwise, unary operators. Add property access (dot, computed). Add object and array literals. Add template literals.
|
|
|
|
**Phase C — Control Flow:** Add if/else, for, while, do-while, for-in, for-of, switch, break, continue. Add try/catch/finally/throw.
|
|
|
|
**Phase D — Functions & Classes:** Add function declarations/expressions, arrow functions, closures, `this` binding, rest/spread. Add `new` expressions. Add class declarations with methods, extends, super.
|
|
|
|
**Phase E — Advanced:** Add destructuring (array, object, nested), compound/logical assignment, optional chaining, nullish coalescing, typeof/delete/void/instanceof/in, eval(), update expressions (++/--).
|
|
|
|
**Phase F — Integration & Testing:** Wire into JsVm, run full test suites, fix all failures, update docs.
|
|
|
|
### File Structure
|
|
|
|
New files to create:
|
|
```
|
|
crates/js_vm/src/bytecode/
|
|
mod.rs — Module root, re-exports
|
|
opcodes.rs — Opcode enum, encoding/decoding
|
|
chunk.rs — Chunk struct (bytecode + constants + spans)
|
|
compiler.rs — AST-to-bytecode compiler
|
|
vm.rs — Bytecode interpreter (main execution loop)
|
|
```
|
|
|
|
Files to modify:
|
|
```
|
|
crates/js_vm/src/lib.rs — Add bytecode module, wire into execute_program()
|
|
docs/JavaScript_Implementation_Checklist.md — Check off bytecode VM
|
|
```
|
|
|
|
### Testing Strategy
|
|
|
|
- **Primary validation:** All 550+ vendored Test262 tests must produce identical pass/fail results
|
|
- **Secondary validation:** Full upstream Test262 suite pass rate must not decrease
|
|
- **Unit tests:** Add tests in `crates/js_vm/src/bytecode/` for compiler output and VM execution
|
|
- **Integration tests:** All existing `tests/js_*.rs` integration tests must pass unchanged
|
|
- **Golden tests:** Any test involving JS execution must produce identical rendering output
|
|
- **Debug aid:** Consider a bytecode disassembler (dump readable opcodes) for debugging — keep as a `#[cfg(test)]` utility
|
|
|
|
### Performance Expectations
|
|
|
|
The bytecode VM should be **faster** than the AST walker for most programs because:
|
|
- Flat bytecode avoids recursive AST traversal overhead
|
|
- Local variable access by stack slot index avoids hash map lookup
|
|
- Opcode dispatch via match on u8 is cheaper than matching on large AST enum variants
|
|
|
|
However, performance is NOT the primary goal of this story — correctness and suspension-readiness are. Do not optimize prematurely.
|
|
|
|
### Risk: Environment Interaction Complexity
|
|
|
|
The biggest implementation risk is the interaction between bytecode execution and the `Environment` struct. The current AST walker deeply interleaves Environment operations (declare, get, set, push scope, pop scope) with statement/expression execution. The bytecode compiler must emit the correct sequence of scope management opcodes to maintain identical scoping behavior.
|
|
|
|
Specific risks:
|
|
- **Var hoisting:** Must be handled at compile time (Phase 1/2 of three-phase compilation)
|
|
- **TDZ enforcement:** Let/const variables must be marked uninitialized until their declaration opcode
|
|
- **Closure capture:** Arrow functions capture `this` explicitly; regular closures rely on dynamic scope lookup
|
|
- **eval():** Direct eval must execute in the current scope — this means the bytecode VM must support switching to a newly compiled chunk mid-execution
|
|
|
|
### Project Structure Notes
|
|
|
|
- All new code goes in `crates/js_vm/src/bytecode/` (Layer 1 engine crate)
|
|
- No layer violations — `js_vm` depends on `js_parser` (horizontal) and `shared` (Layer 0), both already in Cargo.toml
|
|
- No `unsafe` code — `js_vm` inherits workspace `unsafe_code = "forbid"`
|
|
- File size policy applies — split compiler.rs and vm.rs into sub-modules if they exceed limits
|
|
- Module visibility: `pub mod bytecode;` in lib.rs, with selective `pub use` re-exports
|
|
|
|
### References
|
|
|
|
- [Architecture Decision: JS Engine Evolution — Bytecode Introduction](Source: _bmad-output/planning-artifacts/architecture.md#JavaScript Engine Evolution)
|
|
- [ECMAScript Specification](https://tc39.es/ecma262/) — for instruction semantics
|
|
- [Source: crates/js_parser/src/ast.rs] — AST node definitions (compiler input)
|
|
- [Source: crates/js_vm/src/lib.rs] — JsVm state machine, execute_program() (integration point)
|
|
- [Source: crates/js_vm/src/environment.rs] — Environment, Scope, Binding (reuse)
|
|
- [Source: crates/js_vm/src/value.rs] — JsValue, JsObject, built-in methods (reuse)
|
|
- [Source: crates/js_vm/src/interpreter/mod.rs] — Current interpreter (reference implementation)
|
|
- [Source: crates/js_vm/src/interpreter/statements.rs] — Statement execution patterns (reference)
|
|
- [Source: crates/js_vm/src/interpreter/expressions/] — Expression evaluation patterns (reference)
|
|
- [Source: crates/js/src/lib.rs] — JsEngine facade (should not need changes)
|
|
- [Source: docs/JavaScript_Implementation_Checklist.md] — Phase 0: Bytecode compiler + VM
|
|
- [Source: docs/test262_roadmap.md] — Test262 conformance baseline
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude Opus 4.6 (1M context)
|
|
|
|
### Debug Log References
|
|
|
|
### Completion Notes List
|
|
|
|
- Tasks 1-5 substantially implemented
|
|
- Created bytecode module structure: opcodes.rs, chunk.rs, compiler/
|
|
- Compiler handles all AST statement and expression types (76 opcodes)
|
|
- Top-level bytecode execution via Interpreter::execute_bytecode_program (bytecode_exec.rs)
|
|
- Function calls delegate to existing AST interpreter call_function() for correctness
|
|
- Integration point: JsVm::run_impl now compiles to bytecode and executes via Interpreter::execute_bytecode_program
|
|
- All tests passing (1750/1750):
|
|
- js_vm unit tests: 1617/1617 pass
|
|
- js_tests: 45/45 pass
|
|
- js_dom_tests: 49/49 pass
|
|
- js_events: 14/14 pass
|
|
- js_scheduling: 24/24 pass
|
|
- Test262 vendored suite: matches manifest expectations
|
|
- 6 Test262 tests promoted from known_fail to pass (arrow lexical this, exponentiation, new eval order, super prop dot, function code strict mode)
|
|
- Code review (2026-03-15): Removed dead standalone BytecodeVm (~700 LOC), fixed error message regressions (source locations), cleaned up computed member compound assignment code
|
|
|
|
### File List
|
|
|
|
New files:
|
|
- crates/js_vm/src/bytecode/mod.rs
|
|
- crates/js_vm/src/bytecode/opcodes.rs
|
|
- crates/js_vm/src/bytecode/chunk.rs
|
|
- crates/js_vm/src/bytecode/compiler/mod.rs
|
|
- crates/js_vm/src/bytecode/compiler/statements.rs
|
|
- crates/js_vm/src/bytecode/compiler/expressions.rs
|
|
- crates/js_vm/src/interpreter/bytecode_exec.rs
|
|
|
|
Modified files:
|
|
- crates/js_vm/src/lib.rs (added bytecode module, modified run_impl to compile→execute bytecode)
|
|
- crates/js_vm/src/interpreter/mod.rs (added compiled_functions field, bytecode_exec module, widened setup_builtins/execute_eval_code visibility)
|
|
- crates/js_vm/src/interpreter/expressions/calls.rs (widened call_function visibility for bytecode_exec)
|
|
- crates/js_vm/src/interpreter/expressions/member_access.rs (widened eval_member_on_value visibility for bytecode_exec)
|
|
- crates/js_vm/src/interpreter/statements.rs (widened runtime_error_to_error_object visibility for bytecode_exec)
|
|
- crates/js_vm/src/interpreter/array_builtins.rs (widened eval_array_callback_method visibility for bytecode_exec)
|
|
- crates/js_vm/src/interpreter/builtins.rs (widened eval_array_method/eval_string_method visibility for bytecode_exec)
|
|
- crates/js_vm/src/interpreter/json_builtins.rs (widened json_parse/json_stringify visibility for bytecode_exec)
|
|
- tests/external/js262/expected/arrow-function-new-error.txt (error message format)
|
|
- tests/external/js262/expected/class-no-new-error.txt (error message format)
|
|
- tests/external/js262/js262_manifest.toml (6 test promotions known_fail→pass)
|