diff --git a/Cargo.lock b/Cargo.lock index 984a2ee..96e123d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -889,6 +889,10 @@ dependencies = [ "serde_json", ] +[[package]] +name = "oxide_bytecode" +version = "0.1.0" + [[package]] name = "oxide_cli" version = "0.1.0" @@ -896,6 +900,8 @@ dependencies = [ "ansi_term", "clap", "oxide_api", + "oxide_bytecode", + "oxide_code_cache", "oxide_compiler", "oxide_kernel", "oxide_parser", @@ -904,11 +910,20 @@ dependencies = [ "rustyline", ] +[[package]] +name = "oxide_code_cache" +version = "0.1.0" +dependencies = [ + "dashmap", + "oxide_bytecode", +] + [[package]] name = "oxide_compiler" version = "0.1.0" dependencies = [ - "oxide_kernel", + "oxide_bytecode", + "oxide_code_cache", "oxide_parser", "rustc-hash", "serde", @@ -922,7 +937,8 @@ dependencies = [ "bumpalo", "dashmap", "hashbrown 0.14.5", - "oxide_compiler", + "oxide_bytecode", + "oxide_code_cache", "oxide_parser", "oxide_types", "rustc-hash", @@ -946,6 +962,7 @@ dependencies = [ name = "oxide_test262" version = "0.1.0" dependencies = [ + "oxide_bytecode", "oxide_compiler", "oxide_kernel", "oxide_parser", @@ -974,6 +991,7 @@ dependencies = [ "fast-float", "indexmap", "memchr", + "oxide_bytecode", "oxide_compiler", "oxide_kernel", "oxide_parser", diff --git a/Cargo.toml b/Cargo.toml index 2801c59..0001a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ resolver = "2" members = [ "crates/oxide_parser", + "crates/oxide_bytecode", + "crates/oxide_code_cache", "crates/oxide_compiler", "crates/oxide_types", "crates/oxide_vm", diff --git a/crates/oxide_bytecode/Cargo.toml b/crates/oxide_bytecode/Cargo.toml new file mode 100644 index 0000000..ed73f34 --- /dev/null +++ b/crates/oxide_bytecode/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "oxide_bytecode" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "OxideJS - Shared bytecode protocol and compiled module ABI" + +[dependencies] diff --git a/crates/oxide_bytecode/src/lib.rs b/crates/oxide_bytecode/src/lib.rs new file mode 100644 index 0000000..6214124 --- /dev/null +++ b/crates/oxide_bytecode/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = "OxideJS - Shared bytecode protocol and compiled module ABI"] + +pub mod module; +pub mod opcode; + +pub use module::{CompiledModule, Constant}; +pub use opcode::{Instr, OpCode}; diff --git a/crates/oxide_compiler/src/module.rs b/crates/oxide_bytecode/src/module.rs similarity index 100% rename from crates/oxide_compiler/src/module.rs rename to crates/oxide_bytecode/src/module.rs diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs similarity index 97% rename from crates/oxide_compiler/src/opcode.rs rename to crates/oxide_bytecode/src/opcode.rs index f0fcd88..8fb1fb2 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -142,6 +142,8 @@ pub enum OpCode { COMPOUND_SHL = 0x8B, COMPOUND_SHR = 0x8C, COMPOUND_USHR = 0x8D, + NULLISH = 0x8E, + JMP_IF_NULLISH = 0x8F, // ── Misc (0xF0-0xFF) ── NOP = 0xF0, @@ -290,6 +292,8 @@ impl TryFrom for OpCode { 0x8B => Ok(OpCode::COMPOUND_SHL), 0x8C => Ok(OpCode::COMPOUND_SHR), 0x8D => Ok(OpCode::COMPOUND_USHR), + 0x8E => Ok(OpCode::NULLISH), + 0x8F => Ok(OpCode::JMP_IF_NULLISH), 0xF0 => Ok(OpCode::NOP), 0xF1 => Ok(OpCode::HALT), 0xF2 => Ok(OpCode::TYPEOF), @@ -407,6 +411,8 @@ impl fmt::Display for OpCode { OpCode::COMPOUND_SHL => "COMPOUND_SHL", OpCode::COMPOUND_SHR => "COMPOUND_SHR", OpCode::COMPOUND_USHR => "COMPOUND_USHR", + OpCode::NULLISH => "NULLISH", + OpCode::JMP_IF_NULLISH => "JMP_IF_NULLISH", OpCode::NOP => "NOP", OpCode::HALT => "HALT", OpCode::TYPEOF => "TYPEOF", @@ -470,6 +476,12 @@ pub fn encode_jmp_if_true(rd: u8, offset: i16) -> Instr { encode(OpCode::JMP_IF_TRUE, rd, lo, hi) } +pub fn encode_jmp_if_nullish(rd: u8, offset: i16) -> Instr { + let lo = (offset as u16 & 0xFF) as u8; + let hi = ((offset as u16 >> 8) & 0xFF) as u8; + encode(OpCode::JMP_IF_NULLISH, rd, lo, hi) +} + pub fn encode_try_begin(offset: i16) -> Instr { let lo = (offset as u16 & 0xFF) as u8; let hi = ((offset as u16 >> 8) & 0xFF) as u8; diff --git a/crates/oxide_cli/Cargo.toml b/crates/oxide_cli/Cargo.toml index 19f8ff3..b87ca2f 100644 --- a/crates/oxide_cli/Cargo.toml +++ b/crates/oxide_cli/Cargo.toml @@ -11,6 +11,8 @@ name = "oxide" path = "src/main.rs" [dependencies] +oxide_bytecode = { path = "../oxide_bytecode" } +oxide_code_cache = { path = "../oxide_code_cache" } oxide_parser = { path = "../oxide_parser" } oxide_types = { path = "../oxide_types" } oxide_compiler = { path = "../oxide_compiler" } diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index e836436..ea818ce 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use ansi_term::Colour::Red; use clap::{Parser, Subcommand}; -use oxide_compiler::compiler::Compiler; +use oxide_compiler::compiler::{compiled_module_hash, Compiler}; use oxide_kernel::kernel::{KernelConfig, KernelCore}; use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_kernel::string_forge::StringForge; @@ -91,7 +91,8 @@ fn eval(code: &str, kernel: &Arc, pool: &Arc) -> ExitCode { }; let compiler = Compiler::new(); - let module = match kernel.code_forge().get_or_compile(&program, &compiler) { + let hash = compiled_module_hash(&program); + let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) { Ok(m) => m, Err(err) => { eprintln!("{}", Red.paint(err)); diff --git a/crates/oxide_code_cache/Cargo.toml b/crates/oxide_code_cache/Cargo.toml new file mode 100644 index 0000000..2e7cadf --- /dev/null +++ b/crates/oxide_code_cache/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "oxide_code_cache" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "OxideJS - Compiler-independent compiled module cache" + +[dependencies] +oxide_bytecode = { path = "../oxide_bytecode" } +dashmap.workspace = true diff --git a/crates/oxide_code_cache/src/lib.rs b/crates/oxide_code_cache/src/lib.rs new file mode 100644 index 0000000..06a4aab --- /dev/null +++ b/crates/oxide_code_cache/src/lib.rs @@ -0,0 +1,71 @@ +#![doc = "OxideJS - Compiler-independent compiled module cache"] + +use std::sync::Arc; + +use dashmap::DashMap; +use oxide_bytecode::CompiledModule; + +/// Shared compiled-module cache keyed by a caller-provided safe module hash. +/// +/// The cache intentionally does not know how to parse or compile JavaScript. +/// Compiler-aware callers compute the key and provide the compile callback. +pub struct CodeForge { + map: DashMap>, +} + +impl CodeForge { + pub fn new() -> Self { + Self { + map: DashMap::with_shard_amount(16), + } + } + + pub fn get(&self, hash: u64) -> Option> { + self.map.get(&hash).map(|entry| Arc::clone(&entry)) + } + + pub fn insert(&self, hash: u64, module: CompiledModule) -> Arc { + let module = Arc::new(module); + Arc::clone(self.map.entry(hash).or_insert(module).value()) + } + + pub fn get_or_insert_with(&self, hash: u64, compile: F) -> Result, String> + where + F: FnOnce() -> Result, + { + if let Some(module) = self.get(hash) { + return Ok(module); + } + + Ok(self.insert(hash, compile()?)) + } +} + +impl Default for CodeForge { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cache_returns_same_arc_for_same_hash() { + let forge = CodeForge::new(); + let first = forge.insert(1, CompiledModule::new()); + let second = forge.get_or_insert_with(1, || Ok(CompiledModule::new())).expect("cache hit"); + + assert!(Arc::ptr_eq(&first, &second)); + } + + #[test] + fn cache_misses_for_different_hashes() { + let forge = CodeForge::new(); + let first = forge.insert(1, CompiledModule::new()); + let second = forge.insert(2, CompiledModule::new()); + + assert!(!Arc::ptr_eq(&first, &second)); + } +} diff --git a/crates/oxide_compiler/Cargo.toml b/crates/oxide_compiler/Cargo.toml index edf728e..681882d 100644 --- a/crates/oxide_compiler/Cargo.toml +++ b/crates/oxide_compiler/Cargo.toml @@ -7,10 +7,11 @@ rust-version.workspace = true description = "OxideJS - AST to bytecode compiler" [dependencies] +oxide_bytecode = { path = "../oxide_bytecode" } oxide_parser = { path = "../oxide_parser" } serde.workspace = true serde_json.workspace = true rustc-hash.workspace = true [dev-dependencies] -oxide_kernel = { path = "../oxide_kernel" } +oxide_code_cache = { path = "../oxide_code_cache" } diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 3f8e9b3..936e2e1 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use crate::module::CompiledModule; -use crate::opcode::{self, OpCode}; use crate::symbol_table::{Binding, SymbolTable}; +use oxide_bytecode::module::CompiledModule; +use oxide_bytecode::opcode::{self, OpCode}; pub use crate::hash::{compiled_module_hash, structural_hash}; -pub use crate::module::Constant; use crate::symbol_table::ScopeKind; +pub use oxide_bytecode::module::Constant; pub use oxide_parser::VariableDeclarationKind; pub use oxide_parser::{AssignmentOperator, BinaryOperator, Expression, Statement, UnaryOperator}; @@ -17,21 +17,28 @@ pub(crate) fn is_int_literal(value: f64) -> bool { } pub(crate) fn is_side_effect_free(expr: &Expression) -> bool { - match expr { - Expression::NumericLiteral(_) - | Expression::StringLiteral(_) - | Expression::BooleanLiteral(_) - | Expression::NullLiteral(_) - | Expression::Identifier(_) - | Expression::RegExpLiteral(_) - | Expression::ThisExpression(_) => true, - Expression::ParenthesizedExpression(p) => is_side_effect_free(&p.expression), - Expression::BinaryExpression(bin) => is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right), - Expression::UnaryExpression(un) => { - !matches!(un.operator, UnaryOperator::Delete) && is_side_effect_free(&un.argument) + let mut stack = vec![expr]; + while let Some(expr) = stack.pop() { + match expr { + Expression::NumericLiteral(_) + | Expression::StringLiteral(_) + | Expression::BooleanLiteral(_) + | Expression::NullLiteral(_) + | Expression::Identifier(_) + | Expression::RegExpLiteral(_) + | Expression::ThisExpression(_) => {} + Expression::ParenthesizedExpression(p) => stack.push(&p.expression), + Expression::BinaryExpression(bin) => { + stack.push(&bin.left); + stack.push(&bin.right); + } + Expression::UnaryExpression(un) if !matches!(un.operator, UnaryOperator::Delete) => { + stack.push(&un.argument); + } + _ => return false, } - _ => false, } + true } const BUILTIN_GLOBALS: &[&str] = &[ @@ -118,6 +125,17 @@ pub(crate) enum Label { CatchBody(u32), FinallyBody(u32), TryEnd(u32), + LabeledEnd(u32), +} + +/// A labeled-statement scope active during emission. `break label` targets +/// `break_label`; `continue label` targets `continue_label` (only set when the +/// labeled statement directly wraps an iteration statement). +#[derive(Debug, Clone)] +pub(crate) struct LabelScope { + pub(crate) name: String, + pub(crate) break_label: Label, + pub(crate) continue_label: Option