merge: pull upstream updates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
爱特每羊队
2026-06-23 23:43:26 +08:00
129 changed files with 7454 additions and 4007 deletions
Generated
+20 -2
View File
@@ -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",
+2
View File
@@ -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",
+9
View File
@@ -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]
+7
View File
@@ -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};
@@ -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<u8> 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;
+2
View File
@@ -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" }
+3 -2
View File
@@ -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<KernelCore>, pool: &Arc<VmPool>) -> 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));
+11
View File
@@ -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
+71
View File
@@ -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<u64, Arc<CompiledModule>>,
}
impl CodeForge {
pub fn new() -> Self {
Self {
map: DashMap::with_shard_amount(16),
}
}
pub fn get(&self, hash: u64) -> Option<Arc<CompiledModule>> {
self.map.get(&hash).map(|entry| Arc::clone(&entry))
}
pub fn insert(&self, hash: u64, module: CompiledModule) -> Arc<CompiledModule> {
let module = Arc::new(module);
Arc::clone(self.map.entry(hash).or_insert(module).value())
}
pub fn get_or_insert_with<F>(&self, hash: u64, compile: F) -> Result<Arc<CompiledModule>, String>
where
F: FnOnce() -> Result<CompiledModule, String>,
{
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));
}
}
+2 -1
View File
@@ -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" }
+194 -9
View File
@@ -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,6 +17,8 @@ pub(crate) fn is_int_literal(value: f64) -> bool {
}
pub(crate) fn is_side_effect_free(expr: &Expression) -> bool {
let mut stack = vec![expr];
while let Some(expr) = stack.pop() {
match expr {
Expression::NumericLiteral(_)
| Expression::StringLiteral(_)
@@ -24,14 +26,19 @@ pub(crate) fn is_side_effect_free(expr: &Expression) -> bool {
| 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)
| Expression::ThisExpression(_) => {}
Expression::ParenthesizedExpression(p) => stack.push(&p.expression),
Expression::BinaryExpression(bin) => {
stack.push(&bin.left);
stack.push(&bin.right);
}
_ => false,
Expression::UnaryExpression(un) if !matches!(un.operator, UnaryOperator::Delete) => {
stack.push(&un.argument);
}
_ => return 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<Label>,
}
pub(crate) struct CompileCtx {
@@ -132,6 +150,10 @@ pub(crate) struct CompileCtx {
pub(crate) loop_stack: Vec<(Label, Label)>,
#[allow(dead_code)]
pub(crate) switch_stack: Vec<Label>,
/// Active labeled-statement scopes (resolves `break label` / `continue label`).
pub(crate) label_scopes: Vec<LabelScope>,
/// Label names awaiting binding to the next emitted loop's continue target.
pub(crate) pending_loop_labels: Vec<String>,
pub(crate) label_counter: u32,
pub(crate) projected_pc: usize,
pub(crate) builtin_reg_map: Vec<(String, u8)>,
@@ -151,6 +173,8 @@ pub(crate) struct CompileCtx {
/// Set when alloc_reg() overflows into the reserved this/new.target range (≥254).
/// Checked after each emit phase to produce a compile error rather than silent corruption.
pub(crate) reg_overflow: bool,
pub(crate) const_overflow: bool,
pub(crate) jump_overflow: bool,
}
#[derive(Clone, Copy)]
@@ -201,6 +225,8 @@ impl CompileCtx {
label_map: HashMap::new(),
loop_stack: Vec::new(),
switch_stack: Vec::new(),
label_scopes: Vec::new(),
pending_loop_labels: Vec::new(),
label_counter: 0,
projected_pc: 0,
builtin_reg_map: Vec::new(),
@@ -215,6 +241,8 @@ impl CompileCtx {
after_super_insert: None,
after_super_inserted: false,
reg_overflow: false,
const_overflow: false,
jump_overflow: false,
}
}
@@ -226,6 +254,67 @@ impl CompileCtx {
self.emit(opcode::encode(OpCode::LOAD_CONST, reg, (idx & 0xFF) as u8, ((idx >> 8) & 0xFF) as u8));
}
pub(crate) fn count_word(&mut self) {
self.projected_pc += 1;
}
pub(crate) fn count_words(&mut self, words: usize) {
self.projected_pc += words;
}
pub(crate) fn count_instr(&mut self) {
self.count_word();
}
pub(crate) fn count_instr_with_ext(&mut self, ext_words: usize) {
self.count_words(1 + ext_words);
}
pub(crate) fn count_load_const(&mut self) {
self.alloc_reg();
self.count_instr();
}
pub(crate) fn count_load_var(&mut self) {
self.alloc_reg();
self.count_instr();
}
pub(crate) fn count_ic_instr_with_ext(&mut self) {
self.count_instr_with_ext(3);
}
pub(crate) fn count_ic_set_with_ext(&mut self) {
self.count_ic_instr_with_ext();
}
pub(crate) fn count_call_instr_with_arg_ext(&mut self) {
self.count_instr_with_ext(1);
}
pub(crate) fn count_delete_static(&mut self) {
self.count_instr_with_ext(1);
}
pub(crate) fn count_define_accessor(&mut self) {
self.count_instr_with_ext(1);
}
pub(crate) fn count_private_access(&mut self) {
self.count_load_const();
self.alloc_reg();
self.count_instr();
}
pub(crate) fn count_template_str(&mut self, segment_count: usize) {
self.alloc_reg();
self.count_instr_with_ext(1 + segment_count);
}
pub(crate) fn count_jump(&mut self) {
self.count_instr();
}
pub(crate) fn alloc_reg(&mut self) -> u8 {
let r = self.next_reg;
// Registers 254 (this) and 255 (new.target) are reserved by the VM.
@@ -273,6 +362,10 @@ impl CompileCtx {
return idx;
}
if self.constants.len() >= u16::MAX as usize {
self.const_overflow = true;
return u16::MAX;
}
let idx = self.constants.len() as u16;
self.constants.push(c);
self.constant_map.insert(key, idx);
@@ -280,10 +373,23 @@ impl CompileCtx {
}
let idx = self.constants.len();
if idx >= u16::MAX as usize {
self.const_overflow = true;
return u16::MAX;
}
self.constants.push(c);
idx as u16
}
pub(crate) fn checked_jump_offset(&mut self, offset: isize) -> i16 {
if offset < i16::MIN as isize || offset > i16::MAX as isize {
self.jump_overflow = true;
0
} else {
offset as i16
}
}
pub(crate) fn resolve_label(&self, label: Label) -> Result<usize, String> {
self.label_map
.get(&label)
@@ -379,6 +485,60 @@ impl CompileCtx {
self.switch_stack.last()
}
pub(crate) fn push_label_scope(
&mut self, name: &str, break_label: Label, continue_label: Option<Label>,
) -> Result<(), String> {
if self.label_scopes.iter().any(|s| s.name == name) {
return Err(format!("SyntaxError: Label '{name}' has already been declared"));
}
self.label_scopes.push(LabelScope {
name: name.to_string(),
break_label,
continue_label,
});
Ok(())
}
pub(crate) fn pop_label_scope(&mut self) {
self.label_scopes.pop();
}
pub(crate) fn find_label(&self, name: &str) -> Option<&LabelScope> {
self.label_scopes.iter().rev().find(|s| s.name == name)
}
/// Queue a label name to be bound to the continue target of the next loop
/// emitted as the labeled statement's body. Rejects duplicates in the active
/// or pending sets.
pub(crate) fn queue_loop_label(&mut self, name: &str) -> Result<(), String> {
if self.label_scopes.iter().any(|s| s.name == name) || self.pending_loop_labels.iter().any(|n| n == name) {
return Err(format!("SyntaxError: Label '{name}' has already been declared"));
}
self.pending_loop_labels.push(name.to_string());
Ok(())
}
/// Drain queued loop labels into active scopes bound to this loop's break and
/// continue targets. Returns how many scopes were pushed (to pop after).
pub(crate) fn take_pending_loop_labels(&mut self, break_label: Label, continue_label: Label) -> usize {
let names = std::mem::take(&mut self.pending_loop_labels);
let count = names.len();
for name in names {
self.label_scopes.push(LabelScope {
name,
break_label,
continue_label: Some(continue_label),
});
}
count
}
pub(crate) fn pop_label_scopes(&mut self, n: usize) {
for _ in 0..n {
self.label_scopes.pop();
}
}
pub(crate) fn is_builtin(&self, name: &str) -> bool {
self.builtin_reg_map.iter().any(|(n, _)| n == name)
}
@@ -645,6 +805,12 @@ impl Compiler {
if ctx.reg_overflow {
return Err("RangeError: function body uses too many registers (max 253)".into());
}
if ctx.const_overflow {
return Err("RangeError: too many constants".into());
}
if ctx.jump_overflow {
return Err("RangeError: jump offset out of range".into());
}
Ok(CompiledModule {
bytecode: ctx.bytecode,
@@ -710,6 +876,12 @@ impl Compiler {
if ctx.reg_overflow {
return Err("RangeError: function body uses too many registers (max 253)".into());
}
if ctx.const_overflow {
return Err("RangeError: too many constants".into());
}
if ctx.jump_overflow {
return Err("RangeError: jump offset out of range".into());
}
Ok(CompiledModule {
bytecode: ctx.bytecode,
@@ -734,3 +906,16 @@ impl Default for Compiler {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::CompileCtx;
#[test]
fn test_jump_offset_overflow_range_error() {
let mut ctx = CompileCtx::new();
let offset = ctx.checked_jump_offset(i16::MAX as isize + 1);
assert_eq!(offset, 0);
assert!(ctx.jump_overflow);
}
}
-855
View File
@@ -1,855 +0,0 @@
use oxide_parser::{
AssignmentOperator, ClassElement, Expression, ForStatementInit, MethodDefinitionKind, SimpleAssignmentTarget,
Statement, UnaryOperator, VariableDeclarationKind,
};
use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label};
impl Compiler {
fn count_binding_pattern(&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx) {
match pattern {
oxide_parser::BindingPattern::BindingIdentifier(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
oxide_parser::BindingPattern::ArrayPattern(ap) => {
ctx.projected_pc += 1; // FOR_OF_INIT
for elem in &ap.elements {
ctx.alloc_reg();
ctx.projected_pc += 1; // FOR_OF_DONE
ctx.alloc_reg();
ctx.projected_pc += 1; // FOR_OF_NEXT
if let Some(inner) = elem {
self.count_binding_pattern(inner, ctx);
}
}
if let Some(rest) = &ap.rest {
self.count_rest_array(ctx);
self.count_binding_pattern(&rest.argument, ctx);
}
ctx.projected_pc += 1; // FOR_OF_CLOSE
}
oxide_parser::BindingPattern::ObjectPattern(op) => {
for prop in &op.properties {
ctx.alloc_reg();
ctx.projected_pc += 5; // LOAD_VAR + LOAD_CONST + IC_GET_PROP + 3 ext
ctx.alloc_reg();
self.count_binding_pattern(&prop.value, ctx);
}
if let Some(rest) = &op.rest {
ctx.alloc_reg();
ctx.projected_pc += 2; // REST_OBJECT + ext
self.count_binding_pattern(&rest.argument, ctx);
}
}
oxide_parser::BindingPattern::AssignmentPattern(ap) => {
ctx.alloc_reg();
ctx.projected_pc += 2; // LOAD_CONST undefined + STRICT_EQ
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_expression(&ap.right, ctx);
ctx.projected_pc += 1; // LOAD_VAR default
self.count_binding_pattern(&ap.left, ctx);
}
}
}
fn count_rest_array(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg(); // rest
ctx.projected_pc += 1; // NEW_ARRAY
ctx.alloc_reg(); // idx
ctx.projected_pc += 1; // LOAD_CONST 0
ctx.alloc_reg(); // has
ctx.projected_pc += 2; // FOR_OF_DONE + JMP_IF_FALSE
ctx.alloc_reg(); // val
ctx.projected_pc += 1; // FOR_OF_NEXT
ctx.projected_pc += 1; // SET_ELEM
ctx.alloc_reg(); // inc tmp
ctx.projected_pc += 2; // INC_PRE + JMP
}
fn count_array_assignment(&self, ap: &oxide_parser::ArrayAssignmentTarget, ctx: &mut CompileCtx) {
ctx.projected_pc += 1; // FOR_OF_INIT
for elem in &ap.elements {
ctx.alloc_reg();
ctx.projected_pc += 1; // FOR_OF_DONE
ctx.alloc_reg();
ctx.projected_pc += 1; // FOR_OF_NEXT
if elem.is_some() {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
if ap.rest.is_some() {
self.count_rest_array(ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
ctx.projected_pc += 1; // FOR_OF_CLOSE
}
fn count_object_assignment(&self, op: &oxide_parser::ObjectAssignmentTarget, ctx: &mut CompileCtx) {
for _ in &op.properties {
ctx.alloc_reg();
ctx.projected_pc += 5; // LOAD_VAR + LOAD_CONST + IC_GET_PROP + 3 ext
ctx.alloc_reg();
ctx.projected_pc += 1;
}
if op.rest.is_some() {
ctx.alloc_reg();
ctx.projected_pc += 2; // REST_OBJECT + ext
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
fn count_class(&self, class: &oxide_parser::Class, ctx: &mut CompileCtx) {
ctx.alloc_reg(); // ctor_reg
ctx.alloc_reg(); // proto_reg
if let Some(super_class) = &class.super_class {
self.count_expression(super_class, ctx);
}
ctx.projected_pc += 2; // LOAD_CONST ctor + NEW_OBJECT proto
if class.super_class.is_some() {
ctx.alloc_reg(); // parent prototype key
ctx.alloc_reg(); // parent prototype value
ctx.alloc_reg(); // __proto__ key
ctx.projected_pc += 5; // LOAD_CONST + GET_PROP + LOAD_CONST + two SET_PROP
}
ctx.alloc_reg(); // constructor key
ctx.projected_pc += 1; // LOAD_CONST "constructor"
ctx.projected_pc += 1; // SET_PROP proto.constructor = ctor
ctx.alloc_reg(); // prototype key
ctx.projected_pc += 1; // LOAD_CONST "prototype"
ctx.projected_pc += 1; // SET_PROP ctor.prototype = proto
for element in &class.body.body {
match element {
ClassElement::MethodDefinition(method) => {
let method = method.as_ref();
if matches!(method.kind, MethodDefinitionKind::Constructor) {
continue;
}
if matches!(method.key, oxide_parser::PropertyKey::PrivateIdentifier(_)) {
if method.r#static {
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 3;
}
continue;
}
ctx.alloc_reg(); // key_reg
ctx.projected_pc += 1;
ctx.alloc_reg(); // method_reg
ctx.projected_pc += 2; // LOAD_CONST method + SET_HOME_OBJECT
match method.kind {
MethodDefinitionKind::Method => {
ctx.projected_pc += 1; // SET_PROP
}
MethodDefinitionKind::Get | MethodDefinitionKind::Set => {
ctx.alloc_reg(); // undefined placeholder
ctx.projected_pc += 3; // LOAD_CONST undefined + DEFINE_ACCESSOR + ext
}
MethodDefinitionKind::Constructor => {}
}
}
ClassElement::PropertyDefinition(prop) => {
let prop = prop.as_ref();
if prop.r#static {
ctx.alloc_reg(); // key_reg
ctx.projected_pc += 1;
if let Some(value) = &prop.value {
self.count_expression(value, ctx);
} else {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
ctx.projected_pc += 1; // SET_PROP
}
}
ClassElement::StaticBlock(block) => {
for stmt in &block.body {
self.count_statement(stmt, ctx);
}
}
ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => {}
}
}
}
pub(crate) fn count_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) {
match stmt {
Statement::ExpressionStatement(es) => {
self.count_expression(&es.expression, ctx);
}
Statement::VariableDeclaration(decl) => {
for d in &decl.declarations {
if let Some(init) = &d.init {
self.count_expression(init, ctx);
self.count_binding_pattern(&d.id, ctx);
} else {
ctx.alloc_reg();
ctx.projected_pc += 2; // LOAD_CONST(undefined) + STORE_VAR
}
}
}
Statement::ReturnStatement(ret) => {
if let Some(arg) = &ret.argument {
self.count_expression(arg, ctx);
}
ctx.projected_pc += 1; // RETURN
}
Statement::IfStatement(ifs) => {
let id = ctx.next_label_id();
let else_label = Label::IfElse(id);
let end_label = Label::IfEnd(id);
self.count_expression(&ifs.test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_statement(&ifs.consequent, ctx);
ctx.alloc_reg(); // result register
ctx.projected_pc += 1; // LOAD_VAR result <- consequent
if ifs.alternate.is_some() {
ctx.projected_pc += 1; // JMP (skip else)
}
ctx.label_map.insert(else_label, ctx.projected_pc);
if let Some(alt_stmt) = &ifs.alternate {
self.count_statement(alt_stmt, ctx);
ctx.projected_pc += 1; // LOAD_VAR result <- alternate
}
ctx.label_map.insert(end_label, ctx.projected_pc);
}
Statement::WhileStatement(wh) => {
let id = ctx.next_label_id();
let start_label = Label::WhileStart(id);
let end_label = Label::WhileEnd(id);
ctx.label_map.insert(start_label, ctx.projected_pc);
self.count_expression(&wh.test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_statement(&wh.body, ctx);
ctx.projected_pc += 1; // JMP (backward)
ctx.label_map.insert(end_label, ctx.projected_pc);
}
Statement::DoWhileStatement(dw) => {
let id = ctx.next_label_id();
let start_label = Label::DoWhileStart(id);
let end_label = Label::DoWhileEnd(id);
ctx.label_map.insert(start_label, ctx.projected_pc);
self.count_statement(&dw.body, ctx);
self.count_expression(&dw.test, ctx);
ctx.projected_pc += 1; // JMP_IF_TRUE (backward)
ctx.label_map.insert(end_label, ctx.projected_pc);
}
Statement::ForStatement(fr) => {
let id = ctx.next_label_id();
let start_label = Label::ForStart(id);
let update_label = Label::ForUpdate(id);
let end_label = Label::ForEnd(id);
if let Some(init) = &fr.init {
if let Some(expr) = init.as_expression() {
self.count_expression(expr, ctx);
} else if let ForStatementInit::VariableDeclaration(decl) = init {
for d in &decl.declarations {
if let Some(init_expr) = &d.init {
self.count_expression(init_expr, ctx);
self.count_binding_pattern(&d.id, ctx);
} else {
ctx.alloc_reg();
ctx.projected_pc += 2; // LOAD_CONST(undefined) + STORE_VAR
}
}
}
}
ctx.label_map.insert(start_label, ctx.projected_pc);
if let Some(test) = &fr.test {
self.count_expression(test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
}
self.count_statement(&fr.body, ctx);
ctx.label_map.insert(update_label, ctx.projected_pc);
if let Some(update) = &fr.update {
self.count_expression(update, ctx);
}
ctx.projected_pc += 1; // JMP (backward)
ctx.label_map.insert(end_label, ctx.projected_pc);
}
Statement::ForInStatement(fi) => {
let id = ctx.next_label_id();
let start_label = Label::ForInStart(id);
let end_label = Label::ForInEnd(id);
self.count_expression(&fi.right, ctx);
ctx.projected_pc += 1; // FOR_IN_INIT
ctx.label_map.insert(start_label, ctx.projected_pc);
ctx.projected_pc += 3; // FOR_IN_DONE + JMP_IF_FALSE + JMP cleanup
ctx.projected_pc += 1; // FOR_IN_NEXT
match &fi.left {
oxide_parser::ForStatementLeft::VariableDeclaration(decl) => {
for _d in &decl.declarations {
ctx.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR (value from FOR_IN_NEXT)
}
}
oxide_parser::ForStatementLeft::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg(); // key register
ctx.projected_pc += 1; // STORE_VAR
}
_ => {}
}
self.count_statement(&fi.body, ctx);
ctx.projected_pc += 1; // JMP back to start
ctx.label_map.insert(end_label, ctx.projected_pc);
ctx.projected_pc += 1; // FOR_IN_CLEANUP
}
Statement::ForOfStatement(fo) => {
let id = ctx.next_label_id();
let start_label = Label::ForOfStart(id);
let end_label = Label::ForOfEnd(id);
self.count_expression(&fo.right, ctx);
ctx.projected_pc += 1; // FOR_OF_INIT
ctx.label_map.insert(start_label, ctx.projected_pc);
ctx.alloc_reg(); // has_reg
ctx.projected_pc += 2; // FOR_OF_DONE + JMP_IF_FALSE
ctx.alloc_reg(); // val_reg
ctx.projected_pc += 1; // FOR_OF_NEXT
match &fo.left {
oxide_parser::ForStatementLeft::VariableDeclaration(decl) => {
for d in &decl.declarations {
self.count_binding_pattern(&d.id, ctx);
}
}
oxide_parser::ForStatementLeft::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
oxide_parser::ForStatementLeft::ArrayAssignmentTarget(ap) => {
self.count_array_assignment(ap, ctx);
}
oxide_parser::ForStatementLeft::ObjectAssignmentTarget(op) => {
self.count_object_assignment(op, ctx);
}
_ => {}
}
self.count_statement(&fo.body, ctx);
ctx.projected_pc += 1; // JMP back
ctx.label_map.insert(end_label, ctx.projected_pc);
ctx.projected_pc += 1; // FOR_OF_CLOSE
}
Statement::SwitchStatement(sw) => {
let id = ctx.next_label_id();
let end_label = Label::SwitchEnd(id);
ctx.push_switch(end_label);
self.count_expression(&sw.discriminant, ctx);
let compare_reg_checkpoint = ctx.reg_checkpoint();
let cases = &sw.cases;
for case in cases.iter() {
if let Some(test) = &case.test {
self.count_expression(test, ctx);
ctx.projected_pc += 1; // EQ
ctx.alloc_reg(); // eq result
ctx.projected_pc += 1; // JMP_IF_TRUE
ctx.restore_reg_checkpoint(compare_reg_checkpoint);
}
}
let has_default = cases.iter().any(|c| c.test.is_none());
if !has_default {
ctx.projected_pc += 1; // JMP to SwitchEnd (no match)
}
for (case_idx, case) in cases.iter().enumerate() {
let case_label = Label::SwitchCase(id, case_idx as u32);
ctx.label_map.insert(case_label, ctx.projected_pc);
for s in &case.consequent {
self.count_statement(s, ctx);
}
}
ctx.label_map.insert(end_label, ctx.projected_pc);
ctx.pop_switch();
}
Statement::BreakStatement(_) => {
ctx.projected_pc += 1; // JMP
}
Statement::ContinueStatement(_) => {
ctx.projected_pc += 1; // JMP
}
Statement::BlockStatement(block) => {
for s in &block.body {
self.count_statement(s, ctx);
}
}
Statement::FunctionDeclaration(fd) => {
let name = if let Some(id) = &fd.id {
id.name.to_string()
} else {
return;
};
// Hoisting: declare function name as initialized
let func_reg = ctx.alloc_reg();
let _ = ctx.declare_initialized(&name, func_reg, VariableDeclarationKind::Var, false);
// Body is compiled in the emit pass only.
// FD emits LOAD_CONST(BytecodeFunc) + STORE_VAR
ctx.projected_pc += 2;
}
Statement::ClassDeclaration(class) => {
ctx.alloc_reg(); // class binding reg
self.count_class(class, ctx);
ctx.projected_pc += 1; // STORE_VAR binding <- ctor
}
Statement::ThrowStatement(ts) => {
self.count_expression(&ts.argument, ctx);
ctx.projected_pc += 1; // THROW
}
Statement::TryStatement(ts) => {
let id = ctx.next_label_id();
let catch_label = Label::CatchBody(id);
let try_end_label = Label::TryEnd(id);
let has_catch = ts.handler.is_some();
let has_finally = ts.finalizer.is_some();
ctx.alloc_reg(); // result_reg
if has_finally {
ctx.projected_pc += 1; // TRY_FINALLY_BEGIN (before try body)
}
if has_catch {
ctx.projected_pc += 1; // TRY_BEGIN (before try body)
}
for s in &ts.block.body {
self.count_statement(s, ctx);
}
ctx.projected_pc += 1; // LOAD_VAR result_reg (if try body has result)
if has_catch {
ctx.projected_pc += 1; // TRY_END
}
let jmp_needed = has_catch || has_finally;
if jmp_needed {
ctx.projected_pc += 1; // JMP
}
ctx.label_map.insert(catch_label, ctx.projected_pc);
if let Some(catch) = &ts.handler {
ctx.push_scope();
if let Some(_param) = &catch.param {
ctx.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR
}
for cs in &catch.body.body {
self.count_statement(cs, ctx);
}
ctx.projected_pc += 1; // LOAD_VAR result_reg (if catch body has result)
ctx.pop_scope();
}
if let Some(finally) = &ts.finalizer {
let finally_label = Label::FinallyBody(id);
ctx.label_map.insert(finally_label, ctx.projected_pc);
for fs in &finally.body {
self.count_statement(fs, ctx);
}
ctx.projected_pc += 1; // LOAD_VAR result_reg (if finally has result)
ctx.projected_pc += 1; // TRY_FINALLY_END
}
ctx.label_map.insert(try_end_label, ctx.projected_pc);
}
_ => {}
}
}
pub(crate) fn count_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
match expr {
Expression::BinaryExpression(bin) => {
let checkpoint = ctx.reg_checkpoint();
self.count_expression(&bin.left, ctx);
self.count_expression(&bin.right, ctx);
ctx.projected_pc += 1; // ADD/SUB/MUL/DIV/etc.
if is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right) {
ctx.restore_reg_checkpoint(checkpoint.saturating_add(1));
}
}
Expression::PrivateInExpression(pin) => {
self.count_expression(&pin.right, ctx);
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 2;
}
Expression::UnaryExpression(un) => {
match un.operator {
UnaryOperator::Delete => {
match &un.argument {
Expression::Identifier(_) => {
// SyntaxError at compile time, no bytecode cost
}
Expression::StaticMemberExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.projected_pc += 2; // DELETE_PROP_STATIC + ext word
}
Expression::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC
}
_ => {}
}
}
_ => {
self.count_expression(&un.argument, ctx);
ctx.projected_pc += 1; // NEG/TYPEOF/VOID/NOT
}
}
}
Expression::CallExpression(call) => {
if matches!(&call.callee, Expression::Super(_)) {
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.alloc_reg();
ctx.projected_pc += 2; // SUPER_CALL + arg_count ext word
return;
}
match &call.callee {
Expression::PrivateFieldExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 2;
}
Expression::StaticMemberExpression(member) => {
if matches!(&member.object, Expression::Super(_)) {
ctx.alloc_reg(); // this register
ctx.projected_pc += 1; // LOAD_VAR this
ctx.alloc_reg(); // key
ctx.projected_pc += 1; // LOAD_CONST key
ctx.alloc_reg(); // callee
ctx.projected_pc += 1; // SUPER_GET_PROP
} else {
self.count_expression(&member.object, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
ctx.alloc_reg();
ctx.projected_pc += 5; // LOAD_VAR + IC_GET_PROP + 3 ext words
}
}
_ => {
self.count_expression(&call.callee, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
// CALL and CALL_NATIVE both carry 1 ext word for arg_count.
ctx.projected_pc += 2;
ctx.alloc_reg(); // result register
ctx.projected_pc += 1; // LOAD_VAR result <- regs[0]
}
Expression::AssignmentExpression(assign) => {
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left {
self.count_expression(&member.object, ctx);
self.count_expression(&assign.right, ctx);
if assign.operator != AssignmentOperator::Assign {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST key
ctx.projected_pc += 1; // IC_SET_PROP or COMPOUND_MEMBER_*
ctx.projected_pc += 3; // 3 IC ext words
} else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left {
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
self.count_expression(&assign.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // SET_PROP_DYNAMIC
} else if let oxide_parser::AssignmentTarget::PrivateFieldExpression(member) = &assign.left {
self.count_expression(&member.object, ctx);
self.count_expression(&assign.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 2; // LOAD_CONST private id + SET_PRIVATE
} else if let oxide_parser::AssignmentTarget::ArrayAssignmentTarget(ap) = &assign.left {
self.count_expression(&assign.right, ctx);
self.count_array_assignment(ap, ctx);
} else if let oxide_parser::AssignmentTarget::ObjectAssignmentTarget(op) = &assign.left {
self.count_expression(&assign.right, ctx);
self.count_object_assignment(op, ctx);
} else {
self.count_expression(&assign.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
Expression::ConditionalExpression(cond) => {
let id = ctx.next_label_id();
let else_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
self.count_expression(&cond.test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_expression(&cond.consequent, ctx);
ctx.alloc_reg(); // result register
ctx.projected_pc += 1; // LOAD_VAR result <- consequent
ctx.projected_pc += 1; // JMP to end
ctx.label_map.insert(else_label, ctx.projected_pc);
self.count_expression(&cond.alternate, ctx);
ctx.projected_pc += 1; // LOAD_VAR result <- alternate
ctx.label_map.insert(end_label, ctx.projected_pc);
}
Expression::SequenceExpression(seq) => {
for e in &seq.expressions {
self.count_expression(e, ctx);
}
}
Expression::LogicalExpression(log) => {
self.count_expression(&log.left, ctx);
let is_simple = is_side_effect_free(&log.left) && is_side_effect_free(&log.right);
if is_simple {
self.count_expression(&log.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // AND/OR
} else {
use oxide_parser::LogicalOperator;
let id = ctx.next_label_id();
ctx.alloc_reg(); // dup register
ctx.projected_pc += 1; // LOAD_VAR (DUP)
ctx.projected_pc += 1; // JMP_IF_FALSE or JMP_IF_TRUE
self.count_expression(&log.right, ctx);
ctx.projected_pc += 1; // LOAD_VAR (overwrite)
let skip_label = match log.operator {
LogicalOperator::And => Label::TernaryEnd(id),
LogicalOperator::Or => Label::TernaryElse(id),
LogicalOperator::Coalesce => Label::TernaryEnd(id),
};
ctx.label_map.insert(skip_label, ctx.projected_pc);
}
}
Expression::ObjectExpression(obj) => {
ctx.alloc_reg();
ctx.projected_pc += 1; // NEW_OBJECT
let prop_checkpoint = ctx.reg_checkpoint();
for prop in &obj.properties {
if let oxide_parser::ObjectPropertyKind::ObjectProperty(p) = prop {
if matches!(p.kind, oxide_parser::PropertyKind::Get | oxide_parser::PropertyKind::Set) {
self.count_expression(&p.value, ctx);
ctx.alloc_reg(); // undefined getter/setter placeholder
ctx.projected_pc += 3; // LOAD_CONST undefined + DEFINE_ACCESSOR + ext
} else {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST key
self.count_expression(&p.value, ctx);
ctx.projected_pc += 1; // SET_PROP
}
ctx.restore_reg_checkpoint(prop_checkpoint);
}
}
}
Expression::TemplateLiteral(tl) => {
// TemplateLiteral: N quasis (strings) + M expressions interleaved.
// Allocate 1 result reg + count expressions.
for expr in &tl.expressions {
self.count_expression(expr, ctx);
}
ctx.alloc_reg(); // result reg
let segment_count = tl.quasis.len() + tl.expressions.len();
// TEMPLATE_STR opcode + ext words (1 header + ceil(segments/1) per u32)
ctx.projected_pc += 1 + 1 + segment_count;
}
Expression::TaggedTemplateExpression(tt) => {
// Tagged template: tag expr + quasis as LOAD_CONST + expression args + CALL
// Consecutive arg registers at end (via LOAD_VAR copies)
self.count_expression(&tt.tag, ctx);
let quasi_count = tt.quasi.quasis.len();
// Each quasi: LOAD_CONST string + LOAD_CONST index + SET_ELEM
// for both cooked and raw arrays
for _ in 0..quasi_count {
ctx.alloc_reg(); // str_reg
ctx.projected_pc += 1; // LOAD_CONST string
ctx.alloc_reg(); // idx_reg
ctx.projected_pc += 1; // LOAD_CONST index
ctx.projected_pc += 1; // SET_ELEM (cooked)
}
// Cooked array: alloc + NEW_ARRAY
ctx.alloc_reg(); // cooked_temp
ctx.projected_pc += 1; // NEW_ARRAY
for _ in 0..quasi_count {
ctx.alloc_reg(); // str_reg
ctx.projected_pc += 1; // LOAD_CONST string
ctx.alloc_reg(); // idx_reg
ctx.projected_pc += 1; // LOAD_CONST index
ctx.projected_pc += 1; // SET_ELEM (raw)
}
// Raw array: alloc + NEW_ARRAY
ctx.alloc_reg(); // raw_temp
ctx.projected_pc += 1; // NEW_ARRAY
// Count expression arguments
for expr in &tt.quasi.expressions {
self.count_expression(expr, ctx);
}
// Consecutive arg slots: cooked_slot, raw_slot, N expr_slots
ctx.alloc_reg(); // cooked_slot
ctx.alloc_reg(); // raw_slot
for _ in &tt.quasi.expressions {
ctx.alloc_reg(); // expr_slot
ctx.projected_pc += 1; // LOAD_VAR
}
// LOAD_VAR for cooked and raw
ctx.projected_pc += 2;
// undefined this arg
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST
// CALL + ext word
ctx.projected_pc += 2;
// Result reg + LOAD_VAR
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR
}
Expression::ArrowFunctionExpression(_arrow) => {
// Arrow functions: alloc 1 register for the function value.
// Body is compiled in the emit pass only (same pattern as FE).
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST
}
Expression::FunctionExpression(_fe) => {
// No hoisting - function created at expression position.
// Body is compiled in the emit pass only.
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST
}
Expression::ClassExpression(class) => {
self.count_class(class, ctx);
}
Expression::NewExpression(ne) => {
// Count callee expression
self.count_expression(&ne.callee, ctx);
// Count arguments
for arg in &ne.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.alloc_reg(); // result register
ctx.projected_pc += 2; // NEW_EXPRESSION + ext word
}
Expression::ArrayExpression(arr) => {
ctx.alloc_reg();
ctx.projected_pc += 1; // NEW_ARRAY
let elem_checkpoint = ctx.reg_checkpoint();
for elem in &arr.elements {
if let Some(e) = elem.as_expression() {
self.count_expression(e, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST index
ctx.projected_pc += 1; // SET_ELEM
ctx.restore_reg_checkpoint(elem_checkpoint);
}
}
}
Expression::StaticMemberExpression(member) => {
if matches!(&member.object, Expression::Super(_)) {
ctx.alloc_reg(); // key
ctx.projected_pc += 1; // LOAD_CONST key
ctx.alloc_reg(); // this
ctx.projected_pc += 1; // LOAD_VAR this
ctx.alloc_reg(); // result
ctx.projected_pc += 1; // SUPER_GET_PROP
return;
}
self.count_expression(&member.object, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST key
ctx.projected_pc += 1; // IC_GET_PROP
ctx.projected_pc += 3; // 3 IC ext words
}
Expression::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // GET_PROP_DYNAMIC
}
Expression::PrivateFieldExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.alloc_reg();
ctx.alloc_reg();
ctx.projected_pc += 2;
}
Expression::ParenthesizedExpression(p) => {
self.count_expression(&p.expression, ctx);
}
Expression::ThisExpression(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR from reg 254
}
Expression::Identifier(ident) => {
if CompileCtx::is_known_builtin(ident.name.as_str()) {
let _ = ctx.lookup_or_builtin(ident.name.as_str());
}
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR
}
Expression::UpdateExpression(update) => match &update.argument {
SimpleAssignmentTarget::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
SimpleAssignmentTarget::StaticMemberExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
ctx.alloc_reg();
ctx.projected_pc += 1; // MEMBER_INC/MEMBER_DEC opcode
ctx.projected_pc += 3; // 3 IC ext words
}
SimpleAssignmentTarget::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
_ => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
},
Expression::RegExpLiteral(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
_ => {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST or LOAD_VAR
}
}
}
}
@@ -0,0 +1,106 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_assignment_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::AssignmentExpression(assign) = expr else {
return;
};
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
self.count_static_member_logical_assignment(member, logical_op, &assign.right, ctx);
return;
}
self.count_expression(&member.object, ctx);
self.count_expression(&assign.right, ctx);
if assign.operator != AssignmentOperator::Assign {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
ctx.count_load_const(); // key
ctx.count_ic_set_with_ext(); // IC_SET_PROP or COMPOUND_MEMBER_* + 3 ext words
} else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
self.count_computed_member_logical_assignment(member, logical_op, &assign.right, ctx);
return;
}
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
self.count_expression(&assign.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // SET_PROP_DYNAMIC
} else if let oxide_parser::AssignmentTarget::PrivateFieldExpression(member) = &assign.left {
self.count_expression(&member.object, ctx);
self.count_expression(&assign.right, ctx);
ctx.count_load_const(); // private id
ctx.count_instr(); // SET_PRIVATE
} else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(_) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
self.count_identifier_logical_assignment(logical_op, &assign.right, ctx);
} else {
self.count_expression(&assign.right, ctx);
if assign.operator != AssignmentOperator::Assign {
ctx.projected_pc += 1; // COMPOUND_* on var
} else {
ctx.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR
}
}
} else if let oxide_parser::AssignmentTarget::ArrayAssignmentTarget(ap) = &assign.left {
self.count_expression(&assign.right, ctx);
self.count_array_assignment(ap, ctx);
} else if let oxide_parser::AssignmentTarget::ObjectAssignmentTarget(op) = &assign.left {
self.count_expression(&assign.right, ctx);
self.count_object_assignment(op, ctx);
} else {
self.count_expression(&assign.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
fn count_static_member_logical_assignment(
&self, member: &oxide_parser::StaticMemberExpression<'_>, logical_op: LogicalOperator, right: &Expression,
ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
self.count_expression(&member.object, ctx);
ctx.count_load_const(); // key
ctx.count_load_var(); // current value copy
ctx.count_ic_instr_with_ext(); // IC_GET_PROP + 3 ext
self.count_logical_assign_test(logical_op, id, ctx);
self.count_expression(right, ctx);
ctx.count_ic_set_with_ext(); // IC_SET_PROP + 3 ext
ctx.projected_pc += 1; // LOAD_VAR result <- rhs
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
}
fn count_computed_member_logical_assignment(
&self, member: &oxide_parser::ComputedMemberExpression<'_>, logical_op: LogicalOperator, right: &Expression,
ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // GET_PROP_DYNAMIC
self.count_logical_assign_test(logical_op, id, ctx);
self.count_expression(right, ctx);
ctx.projected_pc += 1; // SET_PROP_DYNAMIC
ctx.projected_pc += 1; // LOAD_VAR result <- rhs
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
}
fn count_identifier_logical_assignment(
&self, logical_op: LogicalOperator, right: &Expression, ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR result <- current var
self.count_logical_assign_test(logical_op, id, ctx);
self.count_expression(right, ctx);
ctx.projected_pc += 1; // STORE_VAR
ctx.projected_pc += 1; // LOAD_VAR result <- rhs
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
}
}
@@ -0,0 +1,66 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_call_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::CallExpression(call) = expr else {
return;
};
if matches!(&call.callee, Expression::Super(_)) {
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.alloc_reg();
ctx.count_call_instr_with_arg_ext(); // SUPER_CALL + arg_count ext word
return;
}
match &call.callee {
Expression::PrivateFieldExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.count_private_access();
}
Expression::StaticMemberExpression(member) => {
if matches!(&member.object, Expression::Super(_)) {
ctx.count_load_var(); // this register
ctx.count_load_const(); // key
ctx.alloc_reg(); // callee
ctx.count_instr(); // SUPER_GET_PROP
} else {
self.count_expression(&member.object, ctx);
ctx.count_load_const(); // key
ctx.count_load_var(); // callee object copy
ctx.count_ic_instr_with_ext(); // IC_GET_PROP + 3 ext words
}
}
_ => {
self.count_expression(&call.callee, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.count_call_instr_with_arg_ext(); // CALL/CALL_NATIVE + arg_count ext word
ctx.count_load_var(); // result <- regs[0]
}
pub(in crate::counter) fn count_new_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::NewExpression(ne) = expr else {
return;
};
self.count_expression(&ne.callee, ctx);
for arg in &ne.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.alloc_reg(); // result register
ctx.count_instr_with_ext(1); // NEW_EXPRESSION + arg_count ext word
}
}
@@ -0,0 +1,17 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_chain_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ChainExpression(chain) = expr else {
return;
};
let id = ctx.next_label_id();
self.count_chain_element(&chain.expression, true, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR result <- chain value
ctx.projected_pc += 1; // JMP over short-circuit writer
ctx.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
ctx.projected_pc += 1; // LOAD_CONST undefined
ctx.label_map.insert(Label::TernaryEnd(id), ctx.projected_pc);
}
}
@@ -0,0 +1,64 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_conditional_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ConditionalExpression(cond) = expr else {
return;
};
let id = ctx.next_label_id();
let else_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
self.count_expression(&cond.test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_expression(&cond.consequent, ctx);
ctx.alloc_reg(); // result register
ctx.projected_pc += 1; // LOAD_VAR result <- consequent
ctx.projected_pc += 1; // JMP to end
ctx.label_map.insert(else_label, ctx.projected_pc);
self.count_expression(&cond.alternate, ctx);
ctx.projected_pc += 1; // LOAD_VAR result <- alternate
ctx.label_map.insert(end_label, ctx.projected_pc);
}
pub(in crate::counter) fn count_sequence_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::SequenceExpression(seq) = expr else {
return;
};
for e in &seq.expressions {
self.count_expression(e, ctx);
}
}
pub(in crate::counter) fn count_logical_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::LogicalExpression(log) = expr else {
return;
};
self.count_expression(&log.left, ctx);
let is_simple = is_side_effect_free(&log.left) && is_side_effect_free(&log.right);
if is_simple {
self.count_expression(&log.right, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // AND/OR
} else {
let id = ctx.next_label_id();
ctx.alloc_reg(); // dup register
ctx.projected_pc += 1; // LOAD_VAR (DUP)
ctx.projected_pc += 1; // JMP_IF_FALSE, JMP_IF_TRUE, or JMP_IF_NULLISH
if matches!(log.operator, LogicalOperator::Coalesce) {
ctx.projected_pc += 1; // JMP over RHS on non-nullish
ctx.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
}
self.count_expression(&log.right, ctx);
ctx.projected_pc += 1; // LOAD_VAR (overwrite)
let skip_label = match log.operator {
LogicalOperator::And => Label::TernaryEnd(id),
LogicalOperator::Or => Label::TernaryElse(id),
LogicalOperator::Coalesce => Label::TernaryEnd(id),
};
ctx.label_map.insert(skip_label, ctx.projected_pc);
}
}
}
@@ -0,0 +1,20 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_arrow_function_expression(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST
}
pub(in crate::counter) fn count_function_expression(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST
}
pub(in crate::counter) fn count_class_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ClassExpression(class) = expr else {
return;
};
self.count_class(class, ctx);
}
}
@@ -0,0 +1,29 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_this_expression(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR from reg 254
}
pub(in crate::counter) fn count_identifier_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::Identifier(ident) = expr else {
return;
};
if CompileCtx::is_known_builtin(ident.name.as_str()) {
let _ = ctx.lookup_or_builtin(ident.name.as_str());
}
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_VAR
}
pub(in crate::counter) fn count_regexp_literal(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
pub(in crate::counter) fn count_default_expression(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST or LOAD_VAR
}
}
@@ -0,0 +1,44 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_static_member_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::StaticMemberExpression(member) = expr else {
return;
};
if matches!(&member.object, Expression::Super(_)) {
ctx.count_load_const(); // key
ctx.count_load_var(); // this
ctx.alloc_reg(); // result
ctx.count_instr(); // SUPER_GET_PROP
return;
}
self.count_expression(&member.object, ctx);
ctx.count_load_const(); // key
ctx.count_ic_instr_with_ext(); // IC_GET_PROP + 3 ext words
}
pub(in crate::counter) fn count_computed_member_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ComputedMemberExpression(member) = expr else {
return;
};
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // GET_PROP_DYNAMIC
}
pub(in crate::counter) fn count_private_field_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::PrivateFieldExpression(member) = expr else {
return;
};
self.count_expression(&member.object, ctx);
ctx.count_private_access();
}
pub(in crate::counter) fn count_parenthesized_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ParenthesizedExpression(p) = expr else {
return;
};
self.count_expression(&p.expression, ctx);
}
}
@@ -0,0 +1,45 @@
use super::*;
mod assignment;
mod call;
mod chain;
mod control;
mod function;
mod literal;
mod member;
mod object;
mod operator;
mod template;
impl Compiler {
pub(crate) fn count_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
match expr {
Expression::BinaryExpression(_) => self.count_binary_expression(expr, ctx),
Expression::PrivateInExpression(_) => self.count_private_in_expression(expr, ctx),
Expression::UnaryExpression(_) => self.count_unary_expression(expr, ctx),
Expression::CallExpression(_) => self.count_call_expression(expr, ctx),
Expression::AssignmentExpression(_) => self.count_assignment_expression(expr, ctx),
Expression::ConditionalExpression(_) => self.count_conditional_expression(expr, ctx),
Expression::SequenceExpression(_) => self.count_sequence_expression(expr, ctx),
Expression::LogicalExpression(_) => self.count_logical_expression(expr, ctx),
Expression::ChainExpression(_) => self.count_chain_expression(expr, ctx),
Expression::ObjectExpression(_) => self.count_object_expression(expr, ctx),
Expression::TemplateLiteral(_) => self.count_template_literal(expr, ctx),
Expression::TaggedTemplateExpression(_) => self.count_tagged_template_expression(expr, ctx),
Expression::ArrowFunctionExpression(_) => self.count_arrow_function_expression(ctx),
Expression::FunctionExpression(_) => self.count_function_expression(ctx),
Expression::ClassExpression(_) => self.count_class_expression(expr, ctx),
Expression::NewExpression(_) => self.count_new_expression(expr, ctx),
Expression::ArrayExpression(_) => self.count_array_expression(expr, ctx),
Expression::StaticMemberExpression(_) => self.count_static_member_expression(expr, ctx),
Expression::ComputedMemberExpression(_) => self.count_computed_member_expression(expr, ctx),
Expression::PrivateFieldExpression(_) => self.count_private_field_expression(expr, ctx),
Expression::ParenthesizedExpression(_) => self.count_parenthesized_expression(expr, ctx),
Expression::ThisExpression(_) => self.count_this_expression(ctx),
Expression::Identifier(_) => self.count_identifier_expression(expr, ctx),
Expression::UpdateExpression(_) => self.count_update_expression(expr, ctx),
Expression::RegExpLiteral(_) => self.count_regexp_literal(ctx),
_ => self.count_default_expression(ctx),
}
}
}
@@ -0,0 +1,45 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_object_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ObjectExpression(obj) = expr else {
return;
};
ctx.alloc_reg();
ctx.projected_pc += 1; // NEW_OBJECT
let prop_checkpoint = ctx.reg_checkpoint();
for prop in &obj.properties {
if let oxide_parser::ObjectPropertyKind::ObjectProperty(p) = prop {
if matches!(p.kind, oxide_parser::PropertyKind::Get | oxide_parser::PropertyKind::Set) {
self.count_expression(&p.value, ctx);
ctx.count_load_const(); // undefined getter/setter placeholder
ctx.count_define_accessor(); // DEFINE_ACCESSOR + ext
} else {
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST key
self.count_expression(&p.value, ctx);
ctx.projected_pc += 1; // SET_PROP
}
ctx.restore_reg_checkpoint(prop_checkpoint);
}
}
}
pub(in crate::counter) fn count_array_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::ArrayExpression(arr) = expr else {
return;
};
ctx.alloc_reg();
ctx.projected_pc += 1; // NEW_ARRAY
let elem_checkpoint = ctx.reg_checkpoint();
for elem in &arr.elements {
if let Some(e) = elem.as_expression() {
self.count_expression(e, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST index
ctx.projected_pc += 1; // SET_ELEM
ctx.restore_reg_checkpoint(elem_checkpoint);
}
}
}
}
@@ -0,0 +1,109 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_binary_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::BinaryExpression(bin) = expr else {
return;
};
let checkpoint = ctx.reg_checkpoint();
self.count_expression(&bin.left, ctx);
self.count_expression(&bin.right, ctx);
ctx.projected_pc += 1; // ADD/SUB/MUL/DIV/etc.
if is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right) {
ctx.restore_reg_checkpoint(checkpoint.saturating_add(1));
}
}
pub(in crate::counter) fn count_private_in_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::PrivateInExpression(pin) = expr else {
return;
};
self.count_expression(&pin.right, ctx);
ctx.count_private_access();
}
pub(in crate::counter) fn count_unary_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::UnaryExpression(un) = expr else {
return;
};
match un.operator {
UnaryOperator::Delete => self.count_delete_expression(&un.argument, ctx),
_ => {
self.count_expression(&un.argument, ctx);
ctx.projected_pc += 1; // NEG/TYPEOF/VOID/NOT
}
}
}
fn count_delete_expression(&self, argument: &Expression, ctx: &mut CompileCtx) {
match argument {
Expression::Identifier(_) => {
// SyntaxError at compile time, no bytecode cost.
}
Expression::StaticMemberExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.count_delete_static();
}
Expression::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC
}
Expression::ChainExpression(chain) => {
self.count_delete_chain_expression(&chain.expression, ctx);
}
_ => {}
}
}
fn count_delete_chain_expression(&self, chain: &ChainElement, ctx: &mut CompileCtx) {
match chain {
ChainElement::StaticMemberExpression(member) => {
self.count_expression(&member.object, ctx);
if member.optional {
self.count_optional_guard(ctx);
}
ctx.count_delete_static();
}
ChainElement::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
if member.optional {
self.count_optional_guard(ctx);
}
self.count_expression(&member.expression, ctx);
ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC
}
_ => {}
}
ctx.projected_pc += 1; // JMP over nullish true writer
ctx.projected_pc += 1; // LOAD_CONST true
}
pub(in crate::counter) fn count_update_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::UpdateExpression(update) = expr else {
return;
};
match &update.argument {
SimpleAssignmentTarget::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
SimpleAssignmentTarget::StaticMemberExpression(member) => {
self.count_expression(&member.object, ctx);
ctx.count_load_const(); // key
ctx.alloc_reg();
ctx.count_ic_instr_with_ext(); // MEMBER_INC/MEMBER_DEC + 3 ext words
}
SimpleAssignmentTarget::ComputedMemberExpression(member) => {
self.count_expression(&member.object, ctx);
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.projected_pc += 1;
}
_ => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
}
}
}
@@ -0,0 +1,53 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_template_literal(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::TemplateLiteral(tl) = expr else {
return;
};
for expr in &tl.expressions {
self.count_expression(expr, ctx);
}
let segment_count = tl.quasis.len() + tl.expressions.len();
ctx.count_template_str(segment_count);
}
pub(in crate::counter) fn count_tagged_template_expression(&self, expr: &Expression, ctx: &mut CompileCtx) {
let Expression::TaggedTemplateExpression(tt) = expr else {
return;
};
self.count_expression(&tt.tag, ctx);
let quasi_count = tt.quasi.quasis.len();
for _ in 0..quasi_count {
ctx.count_load_const(); // string
ctx.count_load_const(); // index
ctx.count_instr(); // SET_ELEM (cooked)
}
ctx.alloc_reg(); // cooked_temp
ctx.count_instr(); // NEW_ARRAY
for _ in 0..quasi_count {
ctx.count_load_const(); // string
ctx.count_load_const(); // index
ctx.count_instr(); // SET_ELEM (raw)
}
ctx.alloc_reg(); // raw_temp
ctx.count_instr(); // NEW_ARRAY
for expr in &tt.quasi.expressions {
self.count_expression(expr, ctx);
}
ctx.alloc_reg(); // cooked_slot
ctx.alloc_reg(); // raw_slot
for _ in &tt.quasi.expressions {
ctx.alloc_reg(); // expr_slot
ctx.count_instr(); // LOAD_VAR
}
ctx.count_words(2); // LOAD_VAR for cooked and raw
ctx.count_load_const(); // undefined this arg
ctx.count_call_instr_with_arg_ext();
ctx.count_load_var(); // result <- regs[0]
}
}
@@ -0,0 +1,158 @@
use super::super::*;
impl Compiler {
pub(in crate::counter) fn count_binding_pattern(
&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx,
) {
match pattern {
oxide_parser::BindingPattern::BindingIdentifier(_) => {
ctx.alloc_reg();
ctx.projected_pc += 1;
}
oxide_parser::BindingPattern::ArrayPattern(ap) => {
ctx.count_instr(); // FOR_OF_INIT
for elem in &ap.elements {
ctx.alloc_reg();
ctx.count_instr(); // FOR_OF_DONE
ctx.alloc_reg();
ctx.count_instr(); // FOR_OF_NEXT
if let Some(inner) = elem {
self.count_binding_pattern(inner, ctx);
}
}
if let Some(rest) = &ap.rest {
self.count_rest_array(ctx);
self.count_binding_pattern(&rest.argument, ctx);
}
ctx.count_instr(); // FOR_OF_CLOSE
}
oxide_parser::BindingPattern::ObjectPattern(op) => {
for prop in &op.properties {
self.count_object_property_read_key(&prop.key, prop.computed, ctx);
self.count_binding_pattern(&prop.value, ctx);
}
if let Some(rest) = &op.rest {
ctx.alloc_reg();
ctx.count_instr_with_ext(1); // REST_OBJECT + excluded-keys ext
self.count_binding_pattern(&rest.argument, ctx);
}
}
oxide_parser::BindingPattern::AssignmentPattern(ap) => {
ctx.count_load_const(); // undefined
ctx.count_instr(); // STRICT_EQ
ctx.count_jump(); // JMP_IF_FALSE
self.count_expression(&ap.right, ctx);
ctx.count_instr(); // LOAD_VAR default
self.count_binding_pattern(&ap.left, ctx);
}
}
}
pub(in crate::counter) fn count_rest_array(&self, ctx: &mut CompileCtx) {
ctx.alloc_reg(); // rest
ctx.count_instr(); // NEW_ARRAY
ctx.count_load_const(); // idx = 0
ctx.alloc_reg(); // has
ctx.count_instr(); // FOR_OF_DONE
ctx.count_jump(); // JMP_IF_FALSE
ctx.alloc_reg(); // val
ctx.count_instr(); // FOR_OF_NEXT
ctx.count_instr(); // SET_ELEM
ctx.alloc_reg(); // inc tmp
ctx.count_instr(); // INC_PRE
ctx.count_jump(); // JMP
}
pub(in crate::counter) fn count_array_assignment(
&self, ap: &oxide_parser::ArrayAssignmentTarget, ctx: &mut CompileCtx,
) {
ctx.count_instr(); // FOR_OF_INIT
for elem in &ap.elements {
ctx.alloc_reg();
ctx.count_instr(); // FOR_OF_DONE
ctx.alloc_reg();
ctx.count_instr(); // FOR_OF_NEXT
if elem.is_some() {
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
}
if ap.rest.is_some() {
self.count_rest_array(ctx);
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
ctx.count_instr(); // FOR_OF_CLOSE
}
pub(in crate::counter) fn count_object_assignment(
&self, op: &oxide_parser::ObjectAssignmentTarget, ctx: &mut CompileCtx,
) {
for prop in &op.properties {
match prop {
oxide_parser::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(id) => {
self.count_object_property_read_static(ctx);
if let Some(default_expr) = &id.init {
ctx.count_load_const(); // undefined
ctx.count_instr(); // STRICT_EQ
ctx.count_jump(); // JMP_IF_FALSE
self.count_expression(default_expr, ctx);
ctx.count_instr(); // LOAD_VAR default
}
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
oxide_parser::AssignmentTargetProperty::AssignmentTargetPropertyProperty(prop) => {
self.count_object_property_read_key(&prop.name, prop.computed, ctx);
self.count_assignment_maybe_default(&prop.binding, ctx);
}
}
}
if op.rest.is_some() {
ctx.alloc_reg();
ctx.count_instr_with_ext(1); // REST_OBJECT + excluded-keys ext
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
}
pub(in crate::counter) fn count_assignment_maybe_default(
&self, target: &oxide_parser::AssignmentTargetMaybeDefault, ctx: &mut CompileCtx,
) {
match target {
oxide_parser::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(default) => {
ctx.count_load_const(); // undefined
ctx.count_instr(); // STRICT_EQ
ctx.count_jump(); // JMP_IF_FALSE
self.count_expression(&default.init, ctx);
ctx.count_instr(); // LOAD_VAR default
self.count_assign_target(&default.binding, ctx);
}
oxide_parser::AssignmentTargetMaybeDefault::ArrayAssignmentTarget(ap) => {
self.count_array_assignment(ap, ctx)
}
oxide_parser::AssignmentTargetMaybeDefault::ObjectAssignmentTarget(op) => {
self.count_object_assignment(op, ctx)
}
oxide_parser::AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
_ => {}
}
}
pub(in crate::counter) fn count_assign_target(
&self, target: &oxide_parser::AssignmentTarget, ctx: &mut CompileCtx,
) {
match target {
oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
oxide_parser::AssignmentTarget::ArrayAssignmentTarget(ap) => self.count_array_assignment(ap, ctx),
oxide_parser::AssignmentTarget::ObjectAssignmentTarget(op) => self.count_object_assignment(op, ctx),
_ => {}
}
}
}
@@ -0,0 +1,80 @@
use super::super::*;
impl Compiler {
pub(in crate::counter) fn count_class(&self, class: &oxide_parser::Class, ctx: &mut CompileCtx) {
ctx.alloc_reg(); // ctor_reg
ctx.alloc_reg(); // proto_reg
if let Some(super_class) = &class.super_class {
self.count_expression(super_class, ctx);
}
ctx.count_instr(); // LOAD_CONST ctor
ctx.count_instr(); // NEW_OBJECT proto
if class.super_class.is_some() {
ctx.alloc_reg(); // parent prototype key
ctx.count_instr(); // LOAD_CONST
ctx.alloc_reg(); // parent prototype value
ctx.count_instr(); // GET_PROP
ctx.alloc_reg(); // __proto__ key
ctx.count_instr(); // LOAD_CONST
ctx.count_instr(); // SET_PROP proto.__proto__
ctx.count_instr(); // SET_PROP ctor.__proto__
}
ctx.count_load_const(); // constructor key
ctx.count_instr(); // SET_PROP proto.constructor = ctor
ctx.count_load_const(); // prototype key
ctx.count_instr(); // SET_PROP ctor.prototype = proto
for element in &class.body.body {
match element {
ClassElement::MethodDefinition(method) => {
let method = method.as_ref();
if matches!(method.kind, MethodDefinitionKind::Constructor) {
continue;
}
if matches!(method.key, oxide_parser::PropertyKey::PrivateIdentifier(_)) {
if method.r#static {
ctx.count_load_const(); // private id
ctx.count_load_const(); // method
ctx.count_instr(); // INIT_PRIVATE
}
continue;
}
ctx.count_load_const(); // key_reg
ctx.count_load_const(); // method_reg
ctx.count_instr(); // SET_HOME_OBJECT
match method.kind {
MethodDefinitionKind::Method => {
ctx.count_instr(); // SET_PROP
}
MethodDefinitionKind::Get | MethodDefinitionKind::Set => {
ctx.count_load_const(); // undefined placeholder
ctx.count_define_accessor(); // DEFINE_ACCESSOR + ext
}
MethodDefinitionKind::Constructor => {}
}
}
ClassElement::PropertyDefinition(prop) => {
let prop = prop.as_ref();
if prop.r#static {
ctx.count_load_const(); // key_reg
if let Some(value) = &prop.value {
self.count_expression(value, ctx);
} else {
ctx.count_load_const();
}
ctx.count_instr(); // SET_PROP
}
}
ClassElement::StaticBlock(block) => {
for stmt in &block.body {
self.count_statement(stmt, ctx);
}
}
ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => {}
}
}
}
}
@@ -0,0 +1,173 @@
use super::super::*;
impl Compiler {
pub(in crate::counter) fn count_object_property_read_static(&self, ctx: &mut CompileCtx) {
ctx.count_load_var(); // prop/object temp
ctx.count_load_const(); // key reg
ctx.count_ic_instr_with_ext(); // IC_GET_PROP + 3 ext words
}
pub(in crate::counter) fn count_object_property_read_key(
&self, key: &oxide_parser::PropertyKey, computed: bool, ctx: &mut CompileCtx,
) {
if !computed {
self.count_object_property_read_static(ctx);
let _ = key;
return;
}
ctx.count_load_var(); // prop/object temp
match key {
oxide_parser::PropertyKey::Identifier(ident) => {
let name = ident.name.as_str();
let _ = ctx.lookup_or_builtin(name);
ctx.count_load_var(); // key
}
oxide_parser::PropertyKey::StringLiteral(_) => {
ctx.count_load_const(); // key
}
oxide_parser::PropertyKey::NumericLiteral(_) => {
ctx.count_load_const(); // key
}
oxide_parser::PropertyKey::StaticIdentifier(_) => {
ctx.count_load_const(); // key
}
_ => {}
}
ctx.alloc_reg(); // result reg
ctx.count_instr(); // GET_PROP_DYNAMIC
}
pub(in crate::counter) fn count_optional_guard(&self, ctx: &mut CompileCtx) {
ctx.count_load_var(); // dup
ctx.count_jump(); // JMP_IF_NULLISH
}
pub(in crate::counter) fn count_static_member_get_preserve_base(
&self, member: &oxide_parser::StaticMemberExpression, short_chain: bool, ctx: &mut CompileCtx,
) {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.count_load_const(); // key
ctx.count_load_var(); // receiver copy
ctx.count_ic_instr_with_ext(); // IC_GET_PROP + 3 ext words
}
pub(in crate::counter) fn count_computed_member_get_preserve_base(
&self, member: &oxide_parser::ComputedMemberExpression, short_chain: bool, ctx: &mut CompileCtx,
) {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
self.count_expression(&member.expression, ctx);
ctx.alloc_reg();
ctx.count_instr(); // GET_PROP_DYNAMIC
}
pub(in crate::counter) fn count_chain_call(
&self, call: &oxide_parser::CallExpression, short_chain: bool, ctx: &mut CompileCtx,
) {
match &call.callee {
Expression::StaticMemberExpression(member) => {
self.count_static_member_get_preserve_base(member, short_chain, ctx)
}
Expression::ComputedMemberExpression(member) => {
self.count_computed_member_get_preserve_base(member, short_chain, ctx);
}
Expression::PrivateFieldExpression(member) => {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.count_private_access();
}
_ => {
self.count_chainable_expression(&call.callee, short_chain, ctx);
if short_chain && call.optional {
self.count_optional_guard(ctx);
}
ctx.alloc_reg();
ctx.projected_pc += 1; // LOAD_CONST undefined this
}
}
if short_chain
&& call.optional
&& matches!(
&call.callee,
Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::PrivateFieldExpression(_)
)
{
self.count_optional_guard(ctx);
}
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.count_expression(expr, ctx);
}
}
ctx.count_call_instr_with_arg_ext(); // CALL + arg_count ext word
ctx.count_load_var(); // result <- regs[0]
}
pub(in crate::counter) fn count_chainable_expression(
&self, expr: &Expression, short_chain: bool, ctx: &mut CompileCtx,
) {
match expr {
Expression::StaticMemberExpression(member) => {
self.count_static_member_get_preserve_base(member, short_chain, ctx)
}
Expression::ComputedMemberExpression(member) => {
self.count_computed_member_get_preserve_base(member, short_chain, ctx)
}
Expression::PrivateFieldExpression(member) => {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.count_private_access();
}
Expression::CallExpression(call) => self.count_chain_call(call, short_chain, ctx),
Expression::ChainExpression(chain) => self.count_chain_element(&chain.expression, short_chain, ctx),
_ => self.count_expression(expr, ctx),
}
}
pub(in crate::counter) fn count_chain_element(
&self, element: &ChainElement, short_chain: bool, ctx: &mut CompileCtx,
) {
match element {
ChainElement::StaticMemberExpression(member) => {
self.count_static_member_get_preserve_base(member, short_chain, ctx)
}
ChainElement::ComputedMemberExpression(member) => {
self.count_computed_member_get_preserve_base(member, short_chain, ctx)
}
ChainElement::PrivateFieldExpression(member) => {
self.count_chainable_expression(&member.object, short_chain, ctx);
if short_chain && member.optional {
self.count_optional_guard(ctx);
}
ctx.count_private_access();
}
ChainElement::CallExpression(call) => self.count_chain_call(call, short_chain, ctx),
ChainElement::TSNonNullExpression(_) => {}
}
}
pub(in crate::counter) fn count_logical_assign_test(&self, op: LogicalOperator, id: u32, ctx: &mut CompileCtx) {
match op {
LogicalOperator::And | LogicalOperator::Or => {
ctx.projected_pc += 1;
}
LogicalOperator::Coalesce => {
ctx.projected_pc += 1; // JMP_IF_NULLISH to store body
ctx.projected_pc += 1; // JMP to end on non-nullish
ctx.label_map.insert(Label::TernaryElse(id), ctx.projected_pc);
}
}
}
}
@@ -0,0 +1,3 @@
mod binding;
mod class;
mod member;
+10
View File
@@ -0,0 +1,10 @@
use oxide_parser::{
AssignmentOperator, ChainElement, ClassElement, Expression, ForStatementInit, LogicalOperator,
MethodDefinitionKind, SimpleAssignmentTarget, Statement, UnaryOperator, VariableDeclarationKind,
};
use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label};
mod expr;
mod helper;
mod stmt;
@@ -0,0 +1,49 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_expression_statement(
&self, stmt: &oxide_parser::ExpressionStatement<'_>, ctx: &mut CompileCtx,
) {
self.count_expression(&stmt.expression, ctx);
}
pub(in crate::counter) fn count_return_statement(
&self, stmt: &oxide_parser::ReturnStatement<'_>, ctx: &mut CompileCtx,
) {
if let Some(arg) = &stmt.argument {
self.count_expression(arg, ctx);
}
ctx.projected_pc += 1; // RETURN
}
pub(in crate::counter) fn count_break_statement(&self, ctx: &mut CompileCtx) {
ctx.projected_pc += 1; // JMP
}
pub(in crate::counter) fn count_continue_statement(&self, ctx: &mut CompileCtx) {
ctx.projected_pc += 1; // JMP
}
pub(in crate::counter) fn count_labeled_statement(
&self, stmt: &oxide_parser::LabeledStatement<'_>, ctx: &mut CompileCtx,
) {
let body_is_loop = matches!(
stmt.body,
Statement::WhileStatement(_)
| Statement::DoWhileStatement(_)
| Statement::ForStatement(_)
| Statement::ForInStatement(_)
| Statement::ForOfStatement(_)
);
if body_is_loop {
// Loop body owns its labels; labeled break/continue reuse them.
self.count_statement(&stmt.body, ctx);
} else {
// Non-loop label: consume one id (parity with emit) and place the
// LabeledEnd target right after the body for `break label`.
let id = ctx.next_label_id();
self.count_statement(&stmt.body, ctx);
ctx.label_map.insert(Label::LabeledEnd(id), ctx.projected_pc);
}
}
}
@@ -0,0 +1,11 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_block_statement(
&self, block: &oxide_parser::BlockStatement<'_>, ctx: &mut CompileCtx,
) {
for s in &block.body {
self.count_statement(s, ctx);
}
}
}
@@ -0,0 +1,24 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_if_statement(&self, stmt: &oxide_parser::IfStatement<'_>, ctx: &mut CompileCtx) {
let id = ctx.next_label_id();
let else_label = Label::IfElse(id);
let end_label = Label::IfEnd(id);
self.count_expression(&stmt.test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_statement(&stmt.consequent, ctx);
ctx.alloc_reg(); // result register
ctx.projected_pc += 1; // LOAD_VAR result <- consequent
if stmt.alternate.is_some() {
ctx.projected_pc += 1; // JMP (skip else)
}
ctx.label_map.insert(else_label, ctx.projected_pc);
if let Some(alt_stmt) = &stmt.alternate {
self.count_statement(alt_stmt, ctx);
ctx.projected_pc += 1; // LOAD_VAR result <- alternate
}
ctx.label_map.insert(end_label, ctx.projected_pc);
}
}
@@ -0,0 +1,39 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_variable_declaration(
&self, decl: &oxide_parser::VariableDeclaration<'_>, ctx: &mut CompileCtx,
) {
for d in &decl.declarations {
if let Some(init) = &d.init {
self.count_expression(init, ctx);
self.count_binding_pattern(&d.id, ctx);
} else {
ctx.alloc_reg();
ctx.count_words(2); // LOAD_CONST(undefined) + STORE_VAR
}
}
}
pub(in crate::counter) fn count_function_declaration(
&self, decl: &oxide_parser::Function<'_>, ctx: &mut CompileCtx,
) {
let name = if let Some(id) = &decl.id {
id.name.to_string()
} else {
return;
};
let func_reg = ctx.alloc_reg();
let _ = ctx.declare_initialized(&name, func_reg, VariableDeclarationKind::Var, false);
// Body is compiled in the emit pass only.
ctx.count_words(2); // LOAD_CONST(BytecodeFunc) + STORE_VAR
}
pub(in crate::counter) fn count_class_declaration(&self, class: &oxide_parser::Class<'_>, ctx: &mut CompileCtx) {
ctx.alloc_reg(); // class binding reg
self.count_class(class, ctx);
ctx.projected_pc += 1; // STORE_VAR binding <- ctor
}
}
@@ -0,0 +1,68 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_throw_statement(
&self, stmt: &oxide_parser::ThrowStatement<'_>, ctx: &mut CompileCtx,
) {
self.count_expression(&stmt.argument, ctx);
ctx.projected_pc += 1; // THROW
}
pub(in crate::counter) fn count_try_statement(&self, stmt: &oxide_parser::TryStatement<'_>, ctx: &mut CompileCtx) {
let id = ctx.next_label_id();
let catch_label = Label::CatchBody(id);
let try_end_label = Label::TryEnd(id);
let has_catch = stmt.handler.is_some();
let has_finally = stmt.finalizer.is_some();
ctx.alloc_reg(); // result_reg
if has_finally {
ctx.projected_pc += 1; // TRY_FINALLY_BEGIN (before try body)
}
if has_catch {
ctx.projected_pc += 1; // TRY_BEGIN (before try body)
}
for s in &stmt.block.body {
self.count_statement(s, ctx);
}
ctx.projected_pc += 1; // LOAD_VAR result_reg (if try body has result)
if has_catch {
ctx.projected_pc += 1; // TRY_END
}
let jmp_needed = has_catch || has_finally;
if jmp_needed {
ctx.projected_pc += 1; // JMP
}
ctx.label_map.insert(catch_label, ctx.projected_pc);
if let Some(catch) = &stmt.handler {
ctx.push_scope();
if let Some(_param) = &catch.param {
ctx.alloc_reg();
ctx.projected_pc += 1; // STORE_VAR
}
for cs in &catch.body.body {
self.count_statement(cs, ctx);
}
ctx.projected_pc += 1; // LOAD_VAR result_reg (if catch body has result)
ctx.pop_scope();
}
if let Some(finally) = &stmt.finalizer {
let finally_label = Label::FinallyBody(id);
ctx.label_map.insert(finally_label, ctx.projected_pc);
for fs in &finally.body {
self.count_statement(fs, ctx);
}
ctx.projected_pc += 1; // LOAD_VAR result_reg (if finally has result)
ctx.projected_pc += 1; // TRY_FINALLY_END
}
ctx.label_map.insert(try_end_label, ctx.projected_pc);
}
}
@@ -0,0 +1,143 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_while_statement(
&self, stmt: &oxide_parser::WhileStatement<'_>, ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
let start_label = Label::WhileStart(id);
let end_label = Label::WhileEnd(id);
ctx.label_map.insert(start_label, ctx.projected_pc);
self.count_expression(&stmt.test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
self.count_statement(&stmt.body, ctx);
ctx.projected_pc += 1; // JMP (backward)
ctx.label_map.insert(end_label, ctx.projected_pc);
}
pub(in crate::counter) fn count_do_while_statement(
&self, stmt: &oxide_parser::DoWhileStatement<'_>, ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
let start_label = Label::DoWhileStart(id);
let end_label = Label::DoWhileEnd(id);
ctx.label_map.insert(start_label, ctx.projected_pc);
self.count_statement(&stmt.body, ctx);
self.count_expression(&stmt.test, ctx);
ctx.projected_pc += 1; // JMP_IF_TRUE (backward)
ctx.label_map.insert(end_label, ctx.projected_pc);
}
pub(in crate::counter) fn count_for_statement(&self, stmt: &oxide_parser::ForStatement<'_>, ctx: &mut CompileCtx) {
let id = ctx.next_label_id();
let start_label = Label::ForStart(id);
let update_label = Label::ForUpdate(id);
let end_label = Label::ForEnd(id);
if let Some(init) = &stmt.init {
if let Some(expr) = init.as_expression() {
self.count_expression(expr, ctx);
} else if let ForStatementInit::VariableDeclaration(decl) = init {
for d in &decl.declarations {
if let Some(init_expr) = &d.init {
self.count_expression(init_expr, ctx);
self.count_binding_pattern(&d.id, ctx);
} else {
ctx.alloc_reg();
ctx.count_words(2); // LOAD_CONST(undefined) + STORE_VAR
}
}
}
}
ctx.label_map.insert(start_label, ctx.projected_pc);
if let Some(test) = &stmt.test {
self.count_expression(test, ctx);
ctx.projected_pc += 1; // JMP_IF_FALSE
}
self.count_statement(&stmt.body, ctx);
ctx.label_map.insert(update_label, ctx.projected_pc);
if let Some(update) = &stmt.update {
self.count_expression(update, ctx);
}
ctx.projected_pc += 1; // JMP (backward)
ctx.label_map.insert(end_label, ctx.projected_pc);
}
pub(in crate::counter) fn count_for_in_statement(
&self, stmt: &oxide_parser::ForInStatement<'_>, ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
let start_label = Label::ForInStart(id);
let end_label = Label::ForInEnd(id);
self.count_expression(&stmt.right, ctx);
ctx.count_instr(); // FOR_IN_INIT
ctx.label_map.insert(start_label, ctx.projected_pc);
ctx.count_instr(); // FOR_IN_DONE
ctx.count_jump(); // JMP_IF_FALSE
ctx.count_jump(); // JMP cleanup
ctx.count_instr(); // FOR_IN_NEXT
match &stmt.left {
oxide_parser::ForStatementLeft::VariableDeclaration(decl) => {
for _d in &decl.declarations {
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
}
oxide_parser::ForStatementLeft::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg(); // key register
ctx.count_instr(); // STORE_VAR
}
_ => {}
}
self.count_statement(&stmt.body, ctx);
ctx.count_jump(); // JMP back to start
ctx.label_map.insert(end_label, ctx.projected_pc);
ctx.count_instr(); // FOR_IN_CLEANUP
}
pub(in crate::counter) fn count_for_of_statement(
&self, stmt: &oxide_parser::ForOfStatement<'_>, ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
let start_label = Label::ForOfStart(id);
let end_label = Label::ForOfEnd(id);
self.count_expression(&stmt.right, ctx);
ctx.count_instr(); // FOR_OF_INIT
ctx.label_map.insert(start_label, ctx.projected_pc);
ctx.alloc_reg(); // has_reg
ctx.count_instr(); // FOR_OF_DONE
ctx.count_jump(); // JMP_IF_FALSE
ctx.alloc_reg(); // val_reg
ctx.count_instr(); // FOR_OF_NEXT
match &stmt.left {
oxide_parser::ForStatementLeft::VariableDeclaration(decl) => {
for d in &decl.declarations {
self.count_binding_pattern(&d.id, ctx);
}
}
oxide_parser::ForStatementLeft::AssignmentTargetIdentifier(_) => {
ctx.alloc_reg();
ctx.count_instr(); // STORE_VAR
}
oxide_parser::ForStatementLeft::ArrayAssignmentTarget(ap) => {
self.count_array_assignment(ap, ctx);
}
oxide_parser::ForStatementLeft::ObjectAssignmentTarget(op) => {
self.count_object_assignment(op, ctx);
}
_ => {}
}
self.count_statement(&stmt.body, ctx);
ctx.count_jump(); // JMP back
ctx.label_map.insert(end_label, ctx.projected_pc);
ctx.count_instr(); // FOR_OF_CLOSE
}
}
@@ -0,0 +1,35 @@
use super::*;
mod basic;
mod block;
mod control;
mod declaration;
mod exception;
mod iteration;
mod switch;
impl Compiler {
pub(crate) fn count_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) {
match stmt {
Statement::ExpressionStatement(es) => self.count_expression_statement(es, ctx),
Statement::VariableDeclaration(decl) => self.count_variable_declaration(decl, ctx),
Statement::ReturnStatement(ret) => self.count_return_statement(ret, ctx),
Statement::IfStatement(ifs) => self.count_if_statement(ifs, ctx),
Statement::WhileStatement(wh) => self.count_while_statement(wh, ctx),
Statement::DoWhileStatement(dw) => self.count_do_while_statement(dw, ctx),
Statement::ForStatement(fr) => self.count_for_statement(fr, ctx),
Statement::ForInStatement(fi) => self.count_for_in_statement(fi, ctx),
Statement::ForOfStatement(fo) => self.count_for_of_statement(fo, ctx),
Statement::SwitchStatement(sw) => self.count_switch_statement(sw, ctx),
Statement::BreakStatement(_) => self.count_break_statement(ctx),
Statement::ContinueStatement(_) => self.count_continue_statement(ctx),
Statement::BlockStatement(block) => self.count_block_statement(block, ctx),
Statement::FunctionDeclaration(fd) => self.count_function_declaration(fd, ctx),
Statement::ClassDeclaration(class) => self.count_class_declaration(class, ctx),
Statement::ThrowStatement(ts) => self.count_throw_statement(ts, ctx),
Statement::TryStatement(ts) => self.count_try_statement(ts, ctx),
Statement::LabeledStatement(ls) => self.count_labeled_statement(ls, ctx),
_ => {}
}
}
}
@@ -0,0 +1,41 @@
use super::*;
impl Compiler {
pub(in crate::counter) fn count_switch_statement(
&self, stmt: &oxide_parser::SwitchStatement<'_>, ctx: &mut CompileCtx,
) {
let id = ctx.next_label_id();
let end_label = Label::SwitchEnd(id);
ctx.push_switch(end_label);
self.count_expression(&stmt.discriminant, ctx);
let compare_reg_checkpoint = ctx.reg_checkpoint();
let cases = &stmt.cases;
for case in cases.iter() {
if let Some(test) = &case.test {
self.count_expression(test, ctx);
ctx.projected_pc += 1; // EQ
ctx.alloc_reg(); // eq result
ctx.projected_pc += 1; // JMP_IF_TRUE
ctx.restore_reg_checkpoint(compare_reg_checkpoint);
}
}
let has_default = cases.iter().any(|c| c.test.is_none());
if !has_default {
ctx.projected_pc += 1; // JMP to SwitchEnd (no match)
}
for (case_idx, case) in cases.iter().enumerate() {
let case_label = Label::SwitchCase(id, case_idx as u32);
ctx.label_map.insert(case_label, ctx.projected_pc);
for s in &case.consequent {
self.count_statement(s, ctx);
}
}
ctx.label_map.insert(end_label, ctx.projected_pc);
ctx.pop_switch();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_assignment_expression(
&self, assign: &oxide_parser::AssignmentExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
let store_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, obj_reg, 0));
ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, result_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
ctx.emit(opcode::encode(OpCode::IC_SET_PROP, obj_reg, val_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, val_reg, 0));
return Ok(result_reg);
}
let obj_reg = self.emit_expression(&member.object, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
let prop_name = member.property.name.as_str();
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
if assign.operator != AssignmentOperator::Assign {
let op = match assign.operator {
AssignmentOperator::Addition => OpCode::COMPOUND_MEMBER_ADD,
AssignmentOperator::Subtraction => OpCode::COMPOUND_MEMBER_SUB,
AssignmentOperator::Multiplication => OpCode::COMPOUND_MEMBER_MUL,
AssignmentOperator::Division => OpCode::COMPOUND_MEMBER_DIV,
AssignmentOperator::Remainder => OpCode::COMPOUND_MEMBER_MOD,
AssignmentOperator::Exponential => OpCode::COMPOUND_MEMBER_EXP,
_ => return Err(format!("compound assignment operator {:?} not supported", assign.operator)),
};
ctx.emit(opcode::encode(op, obj_reg, val_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
Ok(val_reg)
} else {
ctx.emit(opcode::encode(OpCode::IC_SET_PROP, obj_reg, val_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
Ok(val_reg)
}
} else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
let store_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PROP_DYNAMIC, obj_reg, key_reg, result_reg));
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
ctx.emit(opcode::encode(OpCode::SET_PROP_DYNAMIC, obj_reg, key_reg, val_reg));
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, val_reg, 0));
return Ok(result_reg);
}
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
ctx.emit(opcode::encode(OpCode::SET_PROP_DYNAMIC, obj_reg, key_reg, val_reg));
Ok(val_reg)
} else if let oxide_parser::AssignmentTarget::PrivateFieldExpression(member) = &assign.left {
if assign.operator != AssignmentOperator::Assign {
return Err("compound assignment to private fields not supported".into());
}
let obj_reg = self.emit_expression(&member.object, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?;
ctx.emit(opcode::encode(OpCode::SET_PRIVATE, obj_reg, val_reg, key_reg));
Ok(val_reg)
} else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(id_ref) = &assign.left {
if assign.operator != AssignmentOperator::Assign {
if let Some(logical_op) = assign.operator.to_logical_operator() {
let id = ctx.next_label_id();
let store_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, var_reg, 0));
self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?;
let val_reg = self.emit_expression(&assign.right, ctx)?;
let is_const = ctx.lookup_const_flag(name);
let const_flag = if is_const { 1 } else { 0 };
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag));
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, val_reg, 0));
Ok(result_reg)
} else if assign.operator == AssignmentOperator::Addition
|| assign.operator == AssignmentOperator::Subtraction
|| assign.operator == AssignmentOperator::Multiplication
|| assign.operator == AssignmentOperator::Division
|| assign.operator == AssignmentOperator::Remainder
|| assign.operator == AssignmentOperator::Exponential
|| assign.operator == AssignmentOperator::BitwiseAnd
|| assign.operator == AssignmentOperator::BitwiseOR
|| assign.operator == AssignmentOperator::BitwiseXOR
|| assign.operator == AssignmentOperator::ShiftLeft
|| assign.operator == AssignmentOperator::ShiftRight
|| assign.operator == AssignmentOperator::ShiftRightZeroFill
{
let rhs = self.emit_expression(&assign.right, ctx)?;
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let op = match assign.operator {
AssignmentOperator::Addition => OpCode::COMPOUND_ADD,
AssignmentOperator::Subtraction => OpCode::COMPOUND_SUB,
AssignmentOperator::Multiplication => OpCode::COMPOUND_MUL,
AssignmentOperator::Division => OpCode::COMPOUND_DIV,
AssignmentOperator::Remainder => OpCode::COMPOUND_MOD,
AssignmentOperator::Exponential => OpCode::COMPOUND_EXP,
AssignmentOperator::BitwiseAnd => OpCode::COMPOUND_AND,
AssignmentOperator::BitwiseOR => OpCode::COMPOUND_OR,
AssignmentOperator::BitwiseXOR => OpCode::COMPOUND_XOR,
AssignmentOperator::ShiftLeft => OpCode::COMPOUND_SHL,
AssignmentOperator::ShiftRight => OpCode::COMPOUND_SHR,
AssignmentOperator::ShiftRightZeroFill => OpCode::COMPOUND_USHR,
_ => return Err(format!("compound assignment operator {:?} not supported", assign.operator)),
};
ctx.emit(opcode::encode(op, var_reg, rhs, 0));
Ok(var_reg)
} else {
Err(format!("compound assignment operator {:?} not supported", assign.operator))
}
} else {
let val_reg = self.emit_expression(&assign.right, ctx)?;
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let is_const = ctx.lookup_const_flag(name);
let const_flag = if is_const { 1 } else { 0 };
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag));
Ok(val_reg)
}
} else if matches!(
&assign.left,
oxide_parser::AssignmentTarget::ArrayAssignmentTarget(_)
| oxide_parser::AssignmentTarget::ObjectAssignmentTarget(_)
) {
if assign.operator != AssignmentOperator::Assign {
return Err("compound destructuring assignment not supported".into());
}
let val_reg = self.emit_expression(&assign.right, ctx)?;
self.emit_assign_target(&assign.left, val_reg, ctx)?;
Ok(val_reg)
} else {
Err("assignment target not supported".into())
}
}
}
@@ -0,0 +1,97 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_call_expression(
&self, call: &oxide_parser::CallExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
if matches!(&call.callee, Expression::Super(_)) {
if !ctx.in_derived_constructor {
return Err("super() only supported in derived constructors".into());
}
let mut arg_regs = Vec::new();
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
arg_regs.push(self.emit_expression(expr, ctx)?);
}
}
let first_arg_reg = if arg_regs.is_empty() { 0u8 } else { arg_regs[0] };
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::SUPER_CALL, result_reg, first_arg_reg, 0));
ctx.emit(arg_regs.len() as u32);
if !ctx.after_super_inserted {
if let Some(field_code) = ctx.after_super_insert.clone() {
ctx.bytecode.extend(field_code);
ctx.after_super_inserted = true;
}
}
return Ok(result_reg);
}
let (callee_reg, this_reg) = match &call.callee {
Expression::PrivateFieldExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?;
let callee_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PRIVATE, callee_reg, obj_reg, key_reg));
(callee_reg, obj_reg)
}
Expression::StaticMemberExpression(member) => {
let is_super_member = matches!(&member.object, Expression::Super(_));
let obj_reg = if is_super_member {
let this_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, this_reg, 254, 0));
this_reg
} else {
self.emit_expression(&member.object, ctx)?
};
let prop_name = member.property.name.as_str();
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
let callee_reg = ctx.alloc_reg();
if is_super_member {
if !ctx.in_instance_method && !ctx.in_static_method && !ctx.in_derived_constructor {
return Err("super property only supported in class methods".into());
}
let op = if ctx.in_static_method {
OpCode::SUPER_STATIC_GET_PROP
} else {
OpCode::SUPER_GET_PROP
};
ctx.emit(opcode::encode(op, callee_reg, obj_reg, key_reg));
} else {
ctx.emit(opcode::encode(OpCode::LOAD_VAR, callee_reg, obj_reg, 0));
ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, callee_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
}
(callee_reg, obj_reg)
}
_ => {
let callee_reg = self.emit_expression(&call.callee, ctx)?;
let this_idx = ctx.add_constant(Constant::Undefined);
let this_reg = ctx.alloc_reg();
ctx.emit_load_const(this_reg, this_idx);
(callee_reg, this_reg)
}
};
let mut arg_regs = Vec::new();
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
arg_regs.push(self.emit_expression(expr, ctx)?);
}
}
let first_arg_reg = if arg_regs.is_empty() { 0u8 } else { arg_regs[0] };
let op = match &call.callee {
Expression::Identifier(ident) if ctx.is_builtin(ident.name.as_str()) => OpCode::CALL_NATIVE,
_ => OpCode::CALL,
};
ctx.emit(opcode::encode(op, callee_reg, this_reg, first_arg_reg));
ctx.emit(arg_regs.len() as u32);
// Copy result from regs[0] into a dedicated register so multiple
// call expressions don't overwrite each other.
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, 0, 0));
Ok(result_reg)
}
}
@@ -0,0 +1,101 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_arrow_function_expression(
&self, arrow: &oxide_parser::ArrowFunctionExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
// Rest params not yet supported (D-06 placeholder)
if let Some(_rest) = &arrow.params.rest {
return Err("rest params in arrow functions not yet supported".into());
}
// Extract param names (same pattern as FunctionExpression)
let mut param_names = Vec::new();
for (idx, param) in arrow.params.items.iter().enumerate() {
match &param.pattern {
oxide_parser::BindingPattern::BindingIdentifier(bi) => {
param_names.push(ParamSpec::Identifier(bi.name.to_string()));
}
pattern => {
param_names.push(ParamSpec::Pattern {
synthetic_name: format!("@@param_{idx}"),
pattern,
});
}
}
}
// Expression body: pass body statements directly with is_expression_body=true.
// Statement body: pass body statements with is_expression_body=false.
let body_stmts = &arrow.body.statements;
let is_expr_body = arrow.expression;
let mut sub_module = self.compile_function_body(&param_names, body_stmts, ctx, is_expr_body, true)?;
sub_module.is_arrow = true;
ctx.sub_modules.push(sub_module);
// 1-indexed: 0 = no sub_module (sentinel)
let sub_idx = ctx.sub_modules.len() as u32;
let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx));
let r = ctx.alloc_reg();
ctx.emit_load_const(r, const_idx);
Ok(r)
}
pub(in crate::emitter) fn emit_function_expression(
&self, fe: &oxide_parser::Function, ctx: &mut CompileCtx,
) -> Result<u8, String> {
// FunctionExpression: compile body, emit LOAD_CONST(BytecodeFunc)
let mut param_names = Vec::new();
for (idx, param) in fe.params.items.iter().enumerate() {
match &param.pattern {
oxide_parser::BindingPattern::BindingIdentifier(bi) => {
param_names.push(ParamSpec::Identifier(bi.name.to_string()));
}
pattern => {
param_names.push(ParamSpec::Pattern {
synthetic_name: format!("@@param_{idx}"),
pattern,
});
}
}
}
let body_stmts: &[Statement] = if let Some(body) = &fe.body { &body.statements } else { &[] };
let mut sub_module = self.compile_function_body(&param_names, body_stmts, ctx, false, false)?;
if let Some(id) = &fe.id {
sub_module.function_name = Some(id.name.to_string());
}
ctx.sub_modules.push(sub_module);
// 1-indexed: 0 = no sub_module (sentinel)
let sub_idx = ctx.sub_modules.len() as u32;
let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx));
let r = ctx.alloc_reg();
ctx.emit_load_const(r, const_idx);
Ok(r)
}
pub(in crate::emitter) fn emit_class_expression(&self, class: &Class, ctx: &mut CompileCtx) -> Result<u8, String> {
self.emit_class(class, ctx)
}
pub(in crate::emitter) fn emit_new_expression(
&self, ne: &oxide_parser::NewExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let constructor_reg = self.emit_expression(&ne.callee, ctx)?;
let mut arg_regs = Vec::new();
for arg in &ne.arguments {
if let Some(expr) = arg.as_expression() {
arg_regs.push(self.emit_expression(expr, ctx)?);
}
}
let first_arg_reg = if arg_regs.is_empty() { 0u8 } else { arg_regs[0] };
let r = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::NEW_EXPRESSION, r, constructor_reg, first_arg_reg));
ctx.emit(arg_regs.len() as u32);
Ok(r)
}
}
@@ -0,0 +1,104 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_numeric_literal_expression(
&self, n: &oxide_parser::NumericLiteral, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let idx = if is_int_literal(n.value) {
ctx.add_constant(Constant::Int(n.value as i32))
} else {
ctx.add_constant(Constant::Number(n.value))
};
let r = ctx.alloc_reg();
ctx.emit_load_const(r, idx);
Ok(r)
}
pub(in crate::emitter) fn emit_string_literal_expression(
&self, s: &oxide_parser::StringLiteral, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let idx = ctx.add_constant(Constant::String(s.value.to_string()));
let r = ctx.alloc_reg();
ctx.emit_load_const(r, idx);
Ok(r)
}
pub(in crate::emitter) fn emit_boolean_literal_expression(
&self, b: &oxide_parser::BooleanLiteral, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let idx = ctx.add_constant(Constant::Boolean(b.value));
let r = ctx.alloc_reg();
ctx.emit_load_const(r, idx);
Ok(r)
}
pub(in crate::emitter) fn emit_null_literal_expression(&self, ctx: &mut CompileCtx) -> Result<u8, String> {
let idx = ctx.add_constant(Constant::Null);
let r = ctx.alloc_reg();
ctx.emit_load_const(r, idx);
Ok(r)
}
pub(in crate::emitter) fn emit_identifier_expression(
&self, ident: &oxide_parser::IdentifierReference, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let var_reg = ctx.lookup_or_builtin(ident.name.as_str())?;
let r = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, var_reg, 0));
Ok(r)
}
pub(in crate::emitter) fn emit_parenthesized_expression(
&self, p: &oxide_parser::ParenthesizedExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
self.emit_expression(&p.expression, ctx)
}
pub(in crate::emitter) fn emit_this_expression(&self, ctx: &mut CompileCtx) -> Result<u8, String> {
let r = ctx.alloc_reg();
let src = ctx.static_block_this_reg.unwrap_or(254);
ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, src, 0));
Ok(r)
}
pub(in crate::emitter) fn emit_reg_exp_literal_expression(
&self, lit: &oxide_parser::RegExpLiteral, ctx: &mut CompileCtx,
) -> Result<u8, String> {
if let Some(raw) = &lit.raw {
let raw_str = raw.to_string();
if raw_str.len() >= 2 && raw_str.starts_with('/') {
let last_slash = raw_str.rfind('/').unwrap_or(raw_str.len() - 1);
let pattern = &raw_str[1..last_slash];
let flags = &raw_str[last_slash + 1..];
let ci = ctx.add_constant(Constant::RegExp(pattern.to_string(), flags.to_string()));
let r = ctx.alloc_reg();
ctx.emit_load_const(r, ci);
Ok(r)
} else {
Err(format!("unsupported regexp literal: {:?}", lit))
}
} else {
Err(format!("unsupported regexp literal: {:?}", lit))
}
}
pub(in crate::emitter) fn emit_sequence_expression(
&self, seq: &oxide_parser::SequenceExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
// Comma operator: evaluate each expression in order, value is the last.
// Emits exactly one expression per element (no extra instruction), so the
// projected-PC count in count_sequence_expression stays aligned.
let mut last = 0u8;
for e in &seq.expressions {
last = self.emit_expression(e, ctx)?;
}
Ok(last)
}
pub(in crate::emitter) fn emit_unsupported_expression(
&self, expr: &Expression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let _ = ctx;
Err(format!("unsupported expression type: {:?}", expr))
}
}
@@ -0,0 +1,75 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_static_member_expression(
&self, member: &oxide_parser::StaticMemberExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
if matches!(&member.object, Expression::Super(_)) {
if !ctx.in_instance_method && !ctx.in_static_method && !ctx.in_derived_constructor {
return Err("super property only supported in class methods".into());
}
let prop_name = member.property.name.as_str();
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
let this_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, this_reg, 254, 0));
let result_reg = ctx.alloc_reg();
let op = if ctx.in_static_method {
OpCode::SUPER_STATIC_GET_PROP
} else {
OpCode::SUPER_GET_PROP
};
ctx.emit(opcode::encode(op, result_reg, this_reg, key_reg));
return Ok(result_reg);
}
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_CONST, key_reg, (idx & 0xFF) as u8, ((idx >> 8) & 0xFF) as u8));
ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, obj_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
Ok(obj_reg)
}
pub(in crate::emitter) fn emit_computed_member_expression(
&self, member: &oxide_parser::ComputedMemberExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
let r = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PROP_DYNAMIC, obj_reg, key_reg, r));
Ok(r)
}
pub(in crate::emitter) fn emit_private_field_expression(
&self, member: &oxide_parser::PrivateFieldExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?;
let r = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::GET_PRIVATE, r, obj_reg, key_reg));
Ok(r)
}
pub(in crate::emitter) fn emit_chain_expression(
&self, chain: &oxide_parser::ChainExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let id = ctx.next_label_id();
let short_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let value_reg = self.emit_chain_element(&chain.expression, Some(short_label), ctx)?;
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, value_reg, 0));
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
let undefined_idx = ctx.add_constant(Constant::Undefined);
ctx.emit_load_const(result_reg, undefined_idx);
Ok(result_reg)
}
}
@@ -0,0 +1,10 @@
use super::*;
mod assignment;
mod call;
mod function;
mod literal;
mod member;
mod object;
mod operator;
mod template;
@@ -0,0 +1,76 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_object_expression(
&self, obj: &oxide_parser::ObjectExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let obj_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::NEW_OBJECT, obj_reg, 0, 0));
let prop_checkpoint = ctx.reg_checkpoint();
for prop in &obj.properties {
let oxide_parser::ObjectPropertyKind::ObjectProperty(p) = prop else {
return Err("spread properties not yet supported".into());
};
let prop_name = match &p.key {
oxide_parser::PropertyKey::StaticIdentifier(ident) => ident.name.as_str().to_string(),
oxide_parser::PropertyKey::StringLiteral(s) => s.value.to_string(),
_ => return Err("unsupported object property key type".into()),
};
match p.kind {
PropertyKind::Get | PropertyKind::Set => {
let accessor_reg = self.emit_expression(&p.value, ctx)?;
if let Some(sub_mod) = ctx.sub_modules.last_mut() {
sub_mod.function_name = Some(prop_name.to_string());
}
let undef_reg = self.emit_undefined(ctx);
let (get_reg, set_reg) = if p.kind == PropertyKind::Get {
(accessor_reg, undef_reg)
} else {
(undef_reg, accessor_reg)
};
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
ctx.emit(opcode::encode(OpCode::DEFINE_ACCESSOR, obj_reg, get_reg, set_reg));
ctx.emit(idx as u32);
ctx.restore_reg_checkpoint(prop_checkpoint);
}
_ => {
let idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit_load_const(key_reg, idx);
let val_reg = self.emit_expression(&p.value, ctx)?;
// Name inference (D-04): if property value is an arrow function,
// set the compiled sub_module's function_name to the property key.
if matches!(&p.value, Expression::ArrowFunctionExpression(_)) {
if let Some(sub_mod) = ctx.sub_modules.last_mut() {
sub_mod.function_name = Some(prop_name.to_string());
}
}
ctx.emit(opcode::encode(OpCode::SET_PROP, obj_reg, val_reg, key_reg));
ctx.restore_reg_checkpoint(prop_checkpoint);
}
}
}
Ok(obj_reg)
}
pub(in crate::emitter) fn emit_array_expression(
&self, arr: &oxide_parser::ArrayExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let arr_reg = ctx.alloc_reg();
let n = arr.elements.len() as u16;
ctx.emit(opcode::encode(OpCode::NEW_ARRAY, arr_reg, (n & 0xFF) as u8, ((n >> 8) & 0xFF) as u8));
let elem_checkpoint = ctx.reg_checkpoint();
for (i, elem) in arr.elements.iter().enumerate() {
let Some(e) = elem.as_expression() else {
return Err("spread not supported".into());
};
let val_reg = self.emit_expression(e, ctx)?;
let idx_reg = ctx.alloc_reg();
let idx = ctx.add_constant(Constant::Int(i as i32));
ctx.emit_load_const(idx_reg, idx);
ctx.emit(opcode::encode(OpCode::SET_ELEM, arr_reg, idx_reg, val_reg));
ctx.restore_reg_checkpoint(elem_checkpoint);
}
Ok(arr_reg)
}
}
@@ -0,0 +1,307 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_private_in_expression(
&self, pin: &oxide_parser::PrivateInExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let obj_reg = self.emit_expression(&pin.right, ctx)?;
let key_reg = self.emit_private_id_reg(pin.left.name.as_str(), ctx)?;
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::PRIVATE_BRAND_IN, result_reg, obj_reg, key_reg));
Ok(result_reg)
}
pub(in crate::emitter) fn emit_binary_expression(
&self, bin: &oxide_parser::BinaryExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let checkpoint = ctx.reg_checkpoint();
let left = self.emit_expression(&bin.left, ctx)?;
let right = self.emit_expression(&bin.right, ctx)?;
let op = match bin.operator {
BinaryOperator::Addition => OpCode::ADD,
BinaryOperator::Subtraction => OpCode::SUB,
BinaryOperator::Multiplication => OpCode::MUL,
BinaryOperator::Division => OpCode::DIV,
BinaryOperator::Remainder => OpCode::MOD,
BinaryOperator::BitwiseAnd => OpCode::BIT_AND,
BinaryOperator::BitwiseOR => OpCode::BIT_OR,
BinaryOperator::BitwiseXOR => OpCode::BIT_XOR,
BinaryOperator::ShiftLeft => OpCode::SHL,
BinaryOperator::ShiftRight => OpCode::SHR,
BinaryOperator::ShiftRightZeroFill => OpCode::USHR,
BinaryOperator::Equality => OpCode::EQ,
BinaryOperator::Inequality => OpCode::NEQ,
BinaryOperator::LessThan => OpCode::LT,
BinaryOperator::GreaterThan => OpCode::GT,
BinaryOperator::LessEqualThan => OpCode::LTE,
BinaryOperator::GreaterEqualThan => OpCode::GTE,
BinaryOperator::In => OpCode::IN,
BinaryOperator::Instanceof => OpCode::INSTANCEOF,
BinaryOperator::StrictEquality => OpCode::STRICT_EQ,
BinaryOperator::StrictInequality => OpCode::STRICT_NEQ,
_ => return Err(format!("unsupported binary operator: {:?}", bin.operator)),
};
ctx.emit(opcode::encode(op, left, left, right));
if is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right) {
ctx.restore_reg_checkpoint(checkpoint.saturating_add(1).max(left.saturating_add(1)));
}
Ok(left)
}
pub(in crate::emitter) fn emit_unary_expression(
&self, un: &oxide_parser::UnaryExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
if matches!(un.operator, UnaryOperator::Delete) {
return match &un.argument {
Expression::Identifier(_) => {
Err("SyntaxError: delete of an unqualified identifier in strict mode".into())
}
Expression::StaticMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let const_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, obj_reg, obj_reg, 0));
ctx.emit(const_idx as u32);
Ok(obj_reg)
}
Expression::ComputedMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, obj_reg, obj_reg, key_reg));
Ok(obj_reg)
}
Expression::ChainExpression(chain) => {
let mut nullish_jumps = Vec::new();
let result_reg = match &chain.expression {
ChainElement::StaticMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
if member.optional {
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, obj_reg, 0));
let jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, 0));
nullish_jumps.push(jump_pos);
}
let prop_name = member.property.name.as_str();
let const_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, obj_reg, obj_reg, 0));
ctx.emit(const_idx as u32);
obj_reg
}
ChainElement::ComputedMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
if member.optional {
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, obj_reg, 0));
let jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, 0));
nullish_jumps.push(jump_pos);
}
let key_reg = self.emit_expression(&member.expression, ctx)?;
ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, obj_reg, obj_reg, key_reg));
obj_reg
}
_ => return Err("invalid delete target".into()),
};
let end_jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp(0));
let short_pos = ctx.bytecode.len();
for jump_pos in nullish_jumps {
let offset = (short_pos as isize) - (jump_pos as isize);
let instr = ctx.bytecode[jump_pos];
let rd = opcode::rd(instr);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[jump_pos] = opcode::encode_jmp_if_nullish(rd, offset);
}
let true_idx = ctx.add_constant(Constant::Boolean(true));
ctx.emit_load_const(result_reg, true_idx);
let end_pos = ctx.bytecode.len();
let offset = (end_pos as isize) - (end_jump_pos as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset);
Ok(result_reg)
}
_ => Err("invalid delete target".into()),
};
}
let arg = self.emit_expression(&un.argument, ctx)?;
match un.operator {
UnaryOperator::UnaryNegation => {
ctx.emit(opcode::encode(OpCode::NEG, arg, arg, 0));
Ok(arg)
}
UnaryOperator::Typeof => {
ctx.emit(opcode::encode(OpCode::TYPEOF, arg, arg, 0));
Ok(arg)
}
UnaryOperator::Void => {
ctx.emit(opcode::encode(OpCode::VOID, arg, arg, 0));
Ok(arg)
}
UnaryOperator::LogicalNot => {
ctx.emit(opcode::encode(OpCode::NOT, arg, arg, 0));
Ok(arg)
}
UnaryOperator::BitwiseNot => {
ctx.emit(opcode::encode(OpCode::BIT_NOT, arg, arg, 0));
Ok(arg)
}
UnaryOperator::UnaryPlus => {
ctx.emit(opcode::encode(OpCode::UNARY_PLUS, arg, arg, 0));
Ok(arg)
}
UnaryOperator::Delete => Err("invalid delete target".into()),
}
}
pub(in crate::emitter) fn emit_conditional_expression(
&self, cond: &oxide_parser::ConditionalExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let id = ctx.next_label_id();
let else_label = Label::TernaryElse(id);
let end_label = Label::TernaryEnd(id);
let test_reg = self.emit_expression(&cond.test, ctx)?;
let else_pos = ctx.resolve_label(else_label)?;
let end_pos = ctx.resolve_label(end_label)?;
let offset = (else_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset));
let cons_reg = self.emit_expression(&cond.consequent, ctx)?;
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, cons_reg, 0));
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
let alt_reg = self.emit_expression(&cond.alternate, ctx)?;
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, alt_reg, 0));
Ok(result_reg)
}
pub(in crate::emitter) fn emit_logical_expression(
&self, log: &oxide_parser::LogicalExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
use oxide_parser::LogicalOperator;
let left_reg = self.emit_expression(&log.left, ctx)?;
if is_side_effect_free(&log.left) && is_side_effect_free(&log.right) {
let right_reg = self.emit_expression(&log.right, ctx)?;
let r = ctx.alloc_reg();
let op = match log.operator {
LogicalOperator::And => OpCode::AND,
LogicalOperator::Or => OpCode::OR,
LogicalOperator::Coalesce => OpCode::NULLISH,
};
ctx.emit(opcode::encode(op, r, left_reg, right_reg));
return Ok(r);
}
if matches!(log.operator, LogicalOperator::Coalesce) {
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, left_reg, 0));
let nullish_jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, 0));
let end_jump_pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp(0));
let rhs_pos = ctx.bytecode.len();
let right_reg = self.emit_expression(&log.right, ctx)?;
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, right_reg, 0));
let end_pos = ctx.bytecode.len();
let offset = (rhs_pos as isize) - (nullish_jump_pos as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[nullish_jump_pos] = opcode::encode_jmp_if_nullish(dup_reg, offset);
let offset = (end_pos as isize) - (end_jump_pos as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset);
return Ok(dup_reg);
}
let id = ctx.next_label_id();
let skip_label = match log.operator {
LogicalOperator::And => Label::TernaryEnd(id),
LogicalOperator::Or => Label::TernaryElse(id),
LogicalOperator::Coalesce => return Err("invalid logical operator dispatch".into()),
};
let skip_pos = ctx.resolve_label(skip_label)?;
let dup_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, left_reg, 0));
let offset = (skip_pos as isize) - (ctx.bytecode.len() as isize);
match log.operator {
LogicalOperator::And => {
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_false(dup_reg, offset));
}
LogicalOperator::Or => {
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_true(dup_reg, offset));
}
LogicalOperator::Coalesce => return Err("invalid logical operator dispatch".into()),
}
let right_reg = self.emit_expression(&log.right, ctx)?;
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, right_reg, 0));
Ok(dup_reg)
}
pub(in crate::emitter) fn emit_update_expression(
&self, update: &oxide_parser::UpdateExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
match &update.argument {
SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => {
let name = id.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let result_reg = ctx.alloc_reg();
let op = match (update.operator, update.prefix) {
(UpdateOperator::Increment, true) => OpCode::INC_PRE,
(UpdateOperator::Increment, false) => OpCode::INC_POST,
(UpdateOperator::Decrement, true) => OpCode::DEC_PRE,
(UpdateOperator::Decrement, false) => OpCode::DEC_POST,
};
ctx.emit(opcode::encode(op, var_reg, result_reg, result_reg));
Ok(result_reg)
}
SimpleAssignmentTarget::StaticMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let prop_name = member.property.name.as_str();
let key_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(
OpCode::LOAD_CONST,
key_reg,
(key_idx & 0xFF) as u8,
((key_idx >> 8) & 0xFF) as u8,
));
let val_reg = ctx.alloc_reg();
let op = match update.operator {
UpdateOperator::Increment => OpCode::MEMBER_INC,
UpdateOperator::Decrement => OpCode::MEMBER_DEC,
};
ctx.emit(opcode::encode(op, obj_reg, val_reg, key_reg));
ctx.emit(0);
ctx.emit(0);
ctx.emit(0);
Ok(val_reg)
}
SimpleAssignmentTarget::ComputedMemberExpression(member) => {
let obj_reg = self.emit_expression(&member.object, ctx)?;
let key_reg = self.emit_expression(&member.expression, ctx)?;
let val_reg = ctx.alloc_reg();
let op = match update.operator {
UpdateOperator::Increment => OpCode::DYN_MEMBER_INC,
UpdateOperator::Decrement => OpCode::DYN_MEMBER_DEC,
};
ctx.emit(opcode::encode(op, obj_reg, key_reg, val_reg));
Ok(val_reg)
}
_ => Err("member update not yet supported".into()),
}
}
}
@@ -0,0 +1,128 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_template_literal_expression(
&self, tl: &oxide_parser::TemplateLiteral, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let r = ctx.alloc_reg();
let quasis = &tl.quasis;
let expressions = &tl.expressions;
let segment_count = quasis.len() + expressions.len();
// Evaluate each expression, collecting registers
let expr_regs: Vec<u8> = expressions
.iter()
.map(|e| self.emit_expression(e, ctx))
.collect::<Result<Vec<_>, _>>()?;
// Add each quasi string to constant pool
let quasi_const_idxs: Vec<u16> = quasis
.iter()
.map(|q| {
let s = q.value.cooked.as_ref().map(|c| c.to_string()).unwrap_or_default();
ctx.add_constant(Constant::String(s))
})
.collect();
// Compute total length hint
let total_len_hint: usize = quasis
.iter()
.map(|q| q.value.cooked.as_ref().map(|c| c.len()).unwrap_or(0))
.sum();
// Emit TEMPLATE_STR rd, 0, 0
ctx.emit(opcode::encode(OpCode::TEMPLATE_STR, r, 0, 0));
// Ext word 0: (segment_count << 16) | (total_len_hint & 0xFFFF)
ctx.emit(((segment_count as u32) << 16) | (total_len_hint as u32 & 0xFFFF));
// Interleave segments: quasi[0], expr[0], quasi[1], expr[1], ...
let mut expr_iter = expr_regs.iter();
for const_idx in quasi_const_idxs.iter() {
// Quasi: is_expression=0, bits 0-15 = const_idx
ctx.emit(*const_idx as u32 & 0x7FFF_FFFF);
// Expression (if any remaining)
if let Some(expr_reg) = expr_iter.next() {
// Expression: is_expression=1, bits 0-15 = reg
ctx.emit(0x8000_0000u32 | (*expr_reg as u32));
}
}
Ok(r)
}
pub(in crate::emitter) fn emit_tagged_template_expression(
&self, tt: &oxide_parser::TaggedTemplateExpression, ctx: &mut CompileCtx,
) -> Result<u8, String> {
let quasis = &tt.quasi.quasis;
let expressions = &tt.quasi.expressions;
// 1. Evaluate tag expression
let tag_reg = self.emit_expression(&tt.tag, ctx)?;
// 2. Build cooked strings array (into a temp register)
let cooked_temp = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::NEW_ARRAY, cooked_temp, quasis.len() as u8, 0));
for (i, quasi) in quasis.iter().enumerate() {
let s = quasi.value.cooked.as_ref().map(|c| c.to_string()).unwrap_or_default();
let const_idx = ctx.add_constant(Constant::String(s));
let str_reg = ctx.alloc_reg();
ctx.emit_load_const(str_reg, const_idx);
let idx_const = ctx.add_constant(Constant::Int(i as i32));
let idx_reg = ctx.alloc_reg();
ctx.emit_load_const(idx_reg, idx_const);
ctx.emit(opcode::encode(OpCode::SET_ELEM, cooked_temp, idx_reg, str_reg));
}
// 3. Build raw strings array (into a temp register)
let raw_temp = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::NEW_ARRAY, raw_temp, quasis.len() as u8, 0));
for (i, quasi) in quasis.iter().enumerate() {
let raw = quasi.value.raw.to_string();
let const_idx = ctx.add_constant(Constant::String(raw));
let str_reg = ctx.alloc_reg();
ctx.emit_load_const(str_reg, const_idx);
let idx_const = ctx.add_constant(Constant::Int(i as i32));
let idx_reg = ctx.alloc_reg();
ctx.emit_load_const(idx_reg, idx_const);
ctx.emit(opcode::encode(OpCode::SET_ELEM, raw_temp, idx_reg, str_reg));
}
// 4. Evaluate expression arguments (into temp registers)
let mut expr_temps = Vec::new();
for expr in expressions {
expr_temps.push(self.emit_expression(expr, ctx)?);
}
// 5. Allocate consecutive argument slots: cooked, raw, expr[0], expr[1], ...
let cooked_slot = ctx.alloc_reg();
let raw_slot = ctx.alloc_reg();
let mut expr_slots = Vec::new();
for _ in expressions {
expr_slots.push(ctx.alloc_reg());
}
// Copy temps to consecutive slots using LOAD_VAR (register-to-register move)
ctx.emit(opcode::encode(OpCode::LOAD_VAR, cooked_slot, cooked_temp, 0));
ctx.emit(opcode::encode(OpCode::LOAD_VAR, raw_slot, raw_temp, 0));
for (slot, temp) in expr_slots.iter().zip(expr_temps.iter()) {
ctx.emit(opcode::encode(OpCode::LOAD_VAR, *slot, *temp, 0));
}
// 6. Emit undefined as this_arg
let undef_idx = ctx.add_constant(Constant::Undefined);
let undef_reg = ctx.alloc_reg();
ctx.emit_load_const(undef_reg, undef_idx);
// 7. Emit CALL(tag, undefined, cooked_slot)
let arg_count = 2 + expressions.len();
ctx.emit(opcode::encode(OpCode::CALL, tag_reg, undef_reg, cooked_slot));
ctx.emit(arg_count as u32);
// 8. Result from regs[0]
let result_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, 0, 0));
Ok(result_reg)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,105 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_expression_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ExpressionStatement(es) = stmt else {
return Ok(None);
};
Ok(Some(self.emit_expression(&es.expression, ctx)?))
}
pub(in crate::emitter) fn emit_return_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ReturnStatement(ret) = stmt else {
return Ok(None);
};
match &ret.argument {
Some(expr) => {
let r = self.emit_expression(expr, ctx)?;
ctx.emit(opcode::encode(OpCode::RETURN, r, 0, 0));
}
None => {
ctx.emit(opcode::encode(OpCode::RETURN, 0, 0, 0));
}
}
Ok(None)
}
pub(in crate::emitter) fn emit_break_statement(
&self, stmt: &oxide_parser::BreakStatement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let break_label = if let Some(label) = &stmt.label {
let name = label.name.as_str();
let scope = ctx
.find_label(name)
.ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?;
scope.break_label
} else if let Some(sw_label) = ctx.current_switch() {
*sw_label
} else {
let (bl, _) = ctx.current_loop().ok_or("break outside switch or loop".to_string())?;
*bl
};
let break_pos = ctx.resolve_label(break_label)?;
let offset = (break_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
Ok(None)
}
pub(in crate::emitter) fn emit_continue_statement(
&self, stmt: &oxide_parser::ContinueStatement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let continue_label = if let Some(label) = &stmt.label {
let name = label.name.as_str();
let scope = ctx
.find_label(name)
.ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?;
scope.continue_label.ok_or_else(|| {
format!("SyntaxError: Illegal continue statement: '{name}' does not denote an iteration statement")
})?
} else {
let (_, cl) = ctx.current_loop().ok_or("continue outside loop".to_string())?;
*cl
};
let continue_pos = ctx.resolve_label(continue_label)?;
let offset = (continue_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
Ok(None)
}
pub(in crate::emitter) fn emit_labeled_statement(
&self, stmt: &oxide_parser::LabeledStatement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let name = stmt.label.name.as_str();
if Self::is_iteration_statement(&stmt.body) {
// Bind the label to the loop's own continue/break targets (the loop
// drains the queue and pops the scope after its body).
ctx.queue_loop_label(name)?;
self.emit_statement(&stmt.body, ctx)?;
} else {
// Non-loop labeled statement: only `break label` is valid; it targets
// the LabeledEnd placed (by the count pass) right after the body.
let id = ctx.next_label_id();
ctx.push_label_scope(name, Label::LabeledEnd(id), None)?;
self.emit_statement(&stmt.body, ctx)?;
ctx.pop_label_scope();
}
Ok(None)
}
pub(in crate::emitter) fn is_iteration_statement(stmt: &Statement) -> bool {
matches!(
stmt,
Statement::WhileStatement(_)
| Statement::DoWhileStatement(_)
| Statement::ForStatement(_)
| Statement::ForInStatement(_)
| Statement::ForOfStatement(_)
)
}
}
@@ -0,0 +1,20 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_block_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::BlockStatement(block) = stmt else {
return Ok(None);
};
ctx.push_scope();
let mut r = None;
for s in &block.body {
if let Some(rr) = self.emit_statement(s, ctx)? {
r = Some(rr);
}
}
ctx.pop_scope();
Ok(r)
}
}
@@ -0,0 +1,49 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_if_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::IfStatement(ifs) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let else_label = Label::IfElse(id);
let end_label = Label::IfEnd(id);
let test_reg = self.emit_expression(&ifs.test, ctx)?;
let else_pos = ctx.resolve_label(else_label)?;
let offset = (else_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset));
let cons_reg = self.emit_statement(&ifs.consequent, ctx)?;
let result_reg = ctx.alloc_reg();
if let Some(r) = cons_reg {
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, r, 0));
} else {
let undef_idx = ctx.add_constant(Constant::Undefined);
ctx.emit_load_const(result_reg, undef_idx);
}
if ifs.alternate.is_some() {
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
}
if let Some(alt) = &ifs.alternate {
let alt_reg = self.emit_statement(alt, ctx)?;
if let Some(r) = alt_reg {
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, r, 0));
} else {
let undef_idx = ctx.add_constant(Constant::Undefined);
ctx.emit_load_const(result_reg, undef_idx);
}
}
Ok(Some(result_reg))
}
}
@@ -0,0 +1,105 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_variable_declaration_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::VariableDeclaration(decl) = stmt else {
return Ok(None);
};
let mut r = None;
for d in &decl.declarations {
let is_const = matches!(decl.kind, VariableDeclarationKind::Const);
if is_const && d.init.is_none() {
return Err("const declaration must have an initializer".into());
}
if let Some(init) = &d.init {
let val_reg = self.emit_expression(init, ctx)?;
self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?;
if let BindingPattern::BindingIdentifier(bi) = &d.id {
if matches!(*init, Expression::ArrowFunctionExpression(_)) {
if let Some(sub_mod) = ctx.sub_modules.last_mut() {
sub_mod.function_name = Some(bi.name.to_string());
}
}
}
r = Some(val_reg);
} else {
let BindingPattern::BindingIdentifier(bi) = &d.id else {
return Err("destructuring declaration requires an initializer".into());
};
let idx = ctx.add_constant(Constant::Undefined);
let tmp = ctx.alloc_reg();
ctx.emit_load_const(tmp, idx);
let var_reg = ctx.alloc_reg();
ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?;
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0));
ctx.init_var(bi.name.as_str());
r = Some(var_reg);
}
}
Ok(r)
}
pub(in crate::emitter) fn emit_function_declaration_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::FunctionDeclaration(fd) = stmt else {
return Ok(None);
};
let name = if let Some(id) = &fd.id {
id.name.to_string()
} else {
return Err("FunctionDeclaration without name".into());
};
let mut param_names = Vec::new();
for (idx, param) in fd.params.items.iter().enumerate() {
match &param.pattern {
oxide_parser::BindingPattern::BindingIdentifier(bi) => {
param_names.push(ParamSpec::Identifier(bi.name.to_string()));
}
pattern => {
param_names.push(ParamSpec::Pattern {
synthetic_name: format!("@@param_{idx}"),
pattern,
});
}
}
}
let body_stmts: &[Statement] = if let Some(body) = &fd.body { &body.statements } else { &[] };
let mut sub_module = self.compile_function_body(&param_names, body_stmts, ctx, false, false)?;
sub_module.function_name = Some(name.clone());
ctx.sub_modules.push(sub_module);
let sub_idx = ctx.sub_modules.len() as u32;
let var_reg = ctx.lookup(&name)?;
ctx.reserve_reg(var_reg);
let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx));
ctx.emit_load_const(var_reg, const_idx);
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, var_reg, 0));
Ok(None)
}
pub(in crate::emitter) fn emit_class_declaration_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ClassDeclaration(class) = stmt else {
return Ok(None);
};
let name = class
.id
.as_ref()
.map(|id| id.name.to_string())
.ok_or_else(|| "ClassDeclaration without name".to_string())?;
let var_reg = ctx.alloc_reg();
ctx.declare(&name, var_reg, VariableDeclarationKind::Let, false)?;
ctx.init_var(&name);
let ctor_reg = self.emit_class(class, ctx)?;
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, ctor_reg, 0));
Ok(None)
}
}
@@ -0,0 +1,125 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_throw_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ThrowStatement(ts) = stmt else {
return Ok(None);
};
let exc_reg = self.emit_expression(&ts.argument, ctx)?;
ctx.emit(opcode::encode(OpCode::THROW, exc_reg, 0, 0));
Ok(None)
}
pub(in crate::emitter) fn emit_try_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::TryStatement(ts) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let catch_label = Label::CatchBody(id);
let try_end_label = Label::TryEnd(id);
let has_catch = ts.handler.is_some();
let has_finally = ts.finalizer.is_some();
let result_reg = ctx.alloc_reg();
let mut try_finally_begin_pos: Option<usize> = None;
let mut try_begin_pos: Option<usize> = None;
if has_finally {
try_finally_begin_pos = Some(ctx.bytecode.len());
ctx.emit(opcode::encode_try_finally_begin(0));
}
if has_catch {
try_begin_pos = Some(ctx.bytecode.len());
ctx.emit(opcode::encode_try_begin(0));
}
let mut last_try_result: Option<u8> = None;
for s in &ts.block.body {
if let Some(r) = self.emit_statement(s, ctx)? {
last_try_result = Some(r);
}
}
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_try_result.unwrap_or(result_reg), 0));
if has_catch {
ctx.emit(opcode::encode(OpCode::TRY_END, 0, 0, 0));
}
let jmp_needed = has_catch || has_finally;
let jmp_skip_pos = if jmp_needed {
let pos = ctx.bytecode.len();
ctx.emit(opcode::encode_jmp(0));
Some(pos)
} else {
None
};
let catch_label_pc = ctx.bytecode.len();
ctx.label_map.insert(catch_label, catch_label_pc);
if let Some(try_begin_pc) = try_begin_pos {
let offset = catch_label_pc as isize - (try_begin_pc as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset);
}
if let Some(catch) = &ts.handler {
ctx.push_scope();
if let Some(param) = &catch.param {
let catch_reg = ctx.alloc_reg();
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &param.pattern {
ctx.declare_initialized(bi.name.as_str(), catch_reg, VariableDeclarationKind::Let, false)?;
ctx.emit(opcode::encode(OpCode::STORE_VAR, catch_reg, 0, 0));
}
}
let mut last_catch_result: Option<u8> = None;
for s in &catch.body.body {
if let Some(r) = self.emit_statement(s, ctx)? {
last_catch_result = Some(r);
}
}
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_catch_result.unwrap_or(result_reg), 0));
ctx.pop_scope();
}
if has_finally {
let finally_label = Label::FinallyBody(id);
let finally_label_pc = ctx.bytecode.len();
ctx.label_map.insert(finally_label, finally_label_pc);
if let Some(fb_pos) = try_finally_begin_pos {
let offset = finally_label_pc as isize - (fb_pos as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[fb_pos] = opcode::encode_try_finally_begin(offset);
}
if let Some(jmp_pos) = jmp_skip_pos {
let offset = finally_label_pc as isize - (jmp_pos as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset);
}
let mut last_finally_result: Option<u8> = None;
for s in &ts.finalizer.as_ref().unwrap().body {
if let Some(r) = self.emit_statement(s, ctx)? {
last_finally_result = Some(r);
}
}
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_finally_result.unwrap_or(result_reg), 0));
ctx.emit(opcode::encode(OpCode::TRY_FINALLY_END, 0, 0, 0));
} else if let Some(jmp_pos) = jmp_skip_pos {
let offset = ctx.bytecode.len() as isize - (jmp_pos as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset);
}
let try_end_pc = ctx.bytecode.len();
ctx.label_map.insert(try_end_label, try_end_pc);
Ok(Some(result_reg))
}
}
@@ -0,0 +1,233 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_while_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::WhileStatement(wh) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let start_label = Label::WhileStart(id);
let end_label = Label::WhileEnd(id);
ctx.push_loop(end_label, start_label);
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
let test_reg = self.emit_expression(&wh.test, ctx)?;
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset));
self.emit_statement(&wh.body, ctx)?;
let start_pos = ctx.resolve_label(start_label)?;
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
ctx.pop_label_scopes(n_labeled);
ctx.pop_loop();
Ok(None)
}
pub(in crate::emitter) fn emit_do_while_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::DoWhileStatement(dw) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let start_label = Label::DoWhileStart(id);
let end_label = Label::DoWhileEnd(id);
ctx.push_loop(end_label, start_label);
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
self.emit_statement(&dw.body, ctx)?;
let test_reg = self.emit_expression(&dw.test, ctx)?;
let start_pos = ctx.resolve_label(start_label)?;
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset));
ctx.pop_label_scopes(n_labeled);
ctx.pop_loop();
Ok(None)
}
pub(in crate::emitter) fn emit_for_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ForStatement(fr) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let start_label = Label::ForStart(id);
let update_label = Label::ForUpdate(id);
let end_label = Label::ForEnd(id);
ctx.push_loop(end_label, update_label);
let n_labeled = ctx.take_pending_loop_labels(end_label, update_label);
if let Some(init) = &fr.init {
if let Some(expr) = init.as_expression() {
self.emit_expression(expr, ctx)?;
} else if let ForStatementInit::VariableDeclaration(decl) = init {
for d in &decl.declarations {
let is_const = matches!(decl.kind, VariableDeclarationKind::Const);
if let Some(init_expr) = &d.init {
let val_reg = self.emit_expression(init_expr, ctx)?;
self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?;
} else if let BindingPattern::BindingIdentifier(bi) = &d.id {
let var_reg = ctx.alloc_reg();
ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?;
}
}
}
}
if let Some(test) = &fr.test {
let test_reg = self.emit_expression(test, ctx)?;
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset));
}
self.emit_statement(&fr.body, ctx)?;
if let Some(update) = &fr.update {
self.emit_expression(update, ctx)?;
}
let start_pos = ctx.resolve_label(start_label)?;
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
ctx.pop_label_scopes(n_labeled);
ctx.pop_loop();
Ok(None)
}
pub(in crate::emitter) fn emit_for_in_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ForInStatement(fi) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let start_label = Label::ForInStart(id);
let end_label = Label::ForInEnd(id);
let obj_reg = self.emit_expression(&fi.right, ctx)?;
ctx.emit(opcode::encode(OpCode::FOR_IN_INIT, 0, obj_reg, 0));
ctx.push_loop(end_label, start_label);
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
let done_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::FOR_IN_DONE, done_reg, 0, 0));
let end_pos = ctx.resolve_label(end_label)?;
let cleanup_jmp_offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
ctx.emit(opcode::encode_jmp_if_false(done_reg, 2));
let cleanup_jmp_offset = ctx.checked_jump_offset(cleanup_jmp_offset);
ctx.emit(opcode::encode_jmp(cleanup_jmp_offset));
let key_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::FOR_IN_NEXT, key_reg, 0, 0));
match &fi.left {
ForStatementLeft::VariableDeclaration(decl) => {
for d in &decl.declarations {
let name = match &d.id {
oxide_parser::BindingPattern::BindingIdentifier(bi) => bi.name.as_str(),
_ => return Err("destructuring not supported".into()),
};
let var_reg = ctx.alloc_reg();
ctx.declare(name, var_reg, decl.kind, matches!(decl.kind, VariableDeclarationKind::Const))?;
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, key_reg, 0));
ctx.init_var(name);
}
}
ForStatementLeft::AssignmentTargetIdentifier(id_ref) => {
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, key_reg, 0));
}
_ => return Err("unsupported for-in left-hand side".into()),
}
self.emit_statement(&fi.body, ctx)?;
let start_pos = ctx.resolve_label(start_label)?;
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0));
ctx.pop_label_scopes(n_labeled);
ctx.pop_loop();
Ok(None)
}
pub(in crate::emitter) fn emit_for_of_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::ForOfStatement(fo) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let start_label = Label::ForOfStart(id);
let end_label = Label::ForOfEnd(id);
let iter_src_reg = self.emit_expression(&fo.right, ctx)?;
ctx.emit(opcode::encode(OpCode::FOR_OF_INIT, 0, iter_src_reg, 0));
ctx.push_loop(end_label, start_label);
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
let has_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0));
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_false(has_reg, offset));
let val_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::FOR_OF_NEXT, val_reg, 0, 0));
match &fo.left {
ForStatementLeft::VariableDeclaration(decl) => {
for d in &decl.declarations {
self.emit_binding_pattern(&d.id, val_reg, decl.kind, false, ctx)?;
}
}
ForStatementLeft::AssignmentTargetIdentifier(id_ref) => {
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0));
}
ForStatementLeft::ArrayAssignmentTarget(ap) => {
self.emit_array_assignment(ap, val_reg, ctx)?;
}
ForStatementLeft::ObjectAssignmentTarget(op) => {
self.emit_object_assignment(op, val_reg, ctx)?;
}
_ => return Err("unsupported for-of left-hand side".into()),
}
self.emit_statement(&fo.body, ctx)?;
let start_pos = ctx.resolve_label(start_label)?;
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
ctx.emit(opcode::encode(OpCode::FOR_OF_CLOSE, 0, 0, 0));
ctx.pop_label_scopes(n_labeled);
ctx.pop_loop();
Ok(None)
}
}
@@ -0,0 +1,9 @@
use super::*;
mod basic;
mod block;
mod control;
mod declaration;
mod exception;
mod iteration;
mod switch;
@@ -0,0 +1,50 @@
use super::*;
impl Compiler {
pub(in crate::emitter) fn emit_switch_statement(
&self, stmt: &Statement, ctx: &mut CompileCtx,
) -> Result<Option<u8>, String> {
let Statement::SwitchStatement(sw) = stmt else {
return Ok(None);
};
let id = ctx.next_label_id();
let end_label = Label::SwitchEnd(id);
ctx.push_switch(end_label);
let disc_reg = self.emit_expression(&sw.discriminant, ctx)?;
let compare_reg_checkpoint = ctx.reg_checkpoint();
let cases = &sw.cases;
for (case_idx, case) in cases.iter().enumerate() {
if let Some(test) = &case.test {
let test_reg = self.emit_expression(test, ctx)?;
let eq_reg = ctx.alloc_reg();
ctx.emit(opcode::encode(OpCode::EQ, eq_reg, disc_reg, test_reg));
let case_label = Label::SwitchCase(id, case_idx as u32);
let body_pos = ctx.resolve_label(case_label)?;
let offset = (body_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset));
ctx.restore_reg_checkpoint(compare_reg_checkpoint);
}
}
let has_default = cases.iter().any(|c| c.test.is_none());
if !has_default {
let end_pos = ctx.resolve_label(end_label)?;
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
let offset = ctx.checked_jump_offset(offset);
ctx.emit(opcode::encode_jmp(offset));
}
for case in cases.iter() {
for s in &case.consequent {
self.emit_statement(s, ctx)?;
}
}
ctx.pop_switch();
Ok(None)
}
}
-436
View File
@@ -1,436 +0,0 @@
use oxide_parser::{
BindingPattern, ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey,
SimpleAssignmentTarget, Statement,
};
use std::hash::Hash;
#[derive(Hash)]
enum HashDomain {
Statement,
Expression,
ClassElement,
PropertyKey,
ObjectPropertyKind,
SimpleAssignmentTarget,
}
macro_rules! hash_match {
($domain:expr, $value:expr, $h:expr, { $($arms:tt)* }) => {{
$domain.hash($h);
std::mem::discriminant($value).hash($h);
match $value {
$($arms)*
}
}};
}
pub fn structural_hash(program: &oxide_parser::Program) -> u64 {
hash_program(program, false)
}
pub fn compiled_module_hash(program: &oxide_parser::Program) -> u64 {
hash_program(program, true)
}
fn hash_program(program: &oxide_parser::Program, include_binding_names: bool) -> u64 {
use std::hash::Hasher;
let mut h = rustc_hash::FxHasher::default();
for stmt in &program.body {
hash_statement(stmt, &mut h, include_binding_names);
}
h.finish()
}
fn hash_binding_pattern(pattern: &BindingPattern, h: &mut rustc_hash::FxHasher) {
if let BindingPattern::BindingIdentifier(ident) = pattern {
ident.name.as_str().hash(h);
}
}
fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::Statement, stmt, h, {
Statement::ExpressionStatement(es) => {
hash_expression(&es.expression, h, include_binding_names);
}
Statement::VariableDeclaration(decl) => {
std::mem::discriminant(&decl.kind).hash(h);
(decl.declarations.len() as u32).hash(h);
for d in &decl.declarations {
if include_binding_names {
hash_binding_pattern(&d.id, h);
}
if let Some(init) = &d.init {
hash_expression(init, h, include_binding_names);
}
}
}
Statement::ReturnStatement(ret) => {
if let Some(arg) = &ret.argument {
hash_expression(arg, h, include_binding_names);
}
}
Statement::IfStatement(ifs) => {
hash_expression(&ifs.test, h, include_binding_names);
hash_statement(&ifs.consequent, h, include_binding_names);
if let Some(alt) = &ifs.alternate {
hash_statement(alt, h, include_binding_names);
}
}
Statement::WhileStatement(wh) => {
hash_expression(&wh.test, h, include_binding_names);
hash_statement(&wh.body, h, include_binding_names);
}
Statement::ForStatement(fr) => {
if let Some(init) = &fr.init {
if let Some(expr) = init.as_expression() {
hash_expression(expr, h, include_binding_names);
} else if let ForStatementInit::VariableDeclaration(decl) = init {
std::mem::discriminant(&decl.kind).hash(h);
(decl.declarations.len() as u32).hash(h);
for d in &decl.declarations {
if include_binding_names {
hash_binding_pattern(&d.id, h);
}
if let Some(init_expr) = &d.init {
hash_expression(init_expr, h, include_binding_names);
}
}
}
}
if let Some(test) = &fr.test {
hash_expression(test, h, include_binding_names);
}
if let Some(update) = &fr.update {
hash_expression(update, h, include_binding_names);
}
hash_statement(&fr.body, h, include_binding_names);
}
Statement::BlockStatement(block) => {
for s in &block.body {
hash_statement(s, h, include_binding_names);
}
}
Statement::BreakStatement(_) => {}
Statement::ContinueStatement(_) => {}
Statement::DoWhileStatement(dw) => {
hash_statement(&dw.body, h, include_binding_names);
hash_expression(&dw.test, h, include_binding_names);
}
Statement::ForInStatement(fi) => {
hash_expression(&fi.right, h, include_binding_names);
hash_statement(&fi.body, h, include_binding_names);
}
Statement::SwitchStatement(sw) => {
hash_expression(&sw.discriminant, h, include_binding_names);
for case in &sw.cases {
if let Some(test) = &case.test {
hash_expression(test, h, include_binding_names);
}
for s in &case.consequent {
hash_statement(s, h, include_binding_names);
}
}
}
Statement::FunctionDeclaration(fd) => {
if include_binding_names {
if let Some(id) = &fd.id {
id.name.as_str().hash(h);
}
} else if let Some(id) = &fd.id {
id.name.as_str().hash(h);
}
(fd.params.items.len() as u32).hash(h);
if include_binding_names {
for param in &fd.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
if let Some(body) = &fd.body {
for s in &body.statements {
hash_statement(s, h, include_binding_names);
}
}
}
Statement::ClassDeclaration(class) => {
if let Some(id) = &class.id {
id.name.as_str().hash(h);
}
class.super_class.is_some().hash(h);
if let Some(super_class) = &class.super_class {
hash_expression(super_class, h, include_binding_names);
}
for element in &class.body.body {
hash_class_element(element, h, include_binding_names);
}
}
Statement::ThrowStatement(ts) => {
hash_expression(&ts.argument, h, include_binding_names);
}
Statement::TryStatement(ts) => {
for s in &ts.block.body {
hash_statement(s, h, include_binding_names);
}
if let Some(catch) = &ts.handler {
if let Some(param) = &catch.param {
if include_binding_names {
hash_binding_pattern(&param.pattern, h);
} else if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &param.pattern {
bi.name.as_str().hash(h);
}
}
for s in &catch.body.body {
hash_statement(s, h, include_binding_names);
}
}
if let Some(finally) = &ts.finalizer {
for s in &finally.body {
hash_statement(s, h, include_binding_names);
}
}
}
_ => {}
});
}
fn hash_property_key(key: &PropertyKey, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::PropertyKey, key, h, {
PropertyKey::StaticIdentifier(ident) => {
ident.name.as_str().hash(h);
}
PropertyKey::Identifier(ident) => {
ident.name.as_str().hash(h);
}
PropertyKey::StringLiteral(s) => {
s.value.hash(h);
}
PropertyKey::NumericLiteral(n) => {
n.value.to_bits().hash(h);
}
PropertyKey::PrivateIdentifier(pi) => {
pi.name.as_str().hash(h);
}
_ => {
hash_expression(key.to_expression(), h, include_binding_names);
}
});
}
fn hash_class_element(element: &ClassElement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::ClassElement, element, h, {
ClassElement::MethodDefinition(method) => {
method.r#static.hash(h);
method.computed.hash(h);
std::mem::discriminant(&method.kind).hash(h);
hash_property_key(&method.key, h, include_binding_names);
if method.computed {
hash_property_key(&method.key, h, include_binding_names);
}
(method.value.params.items.len() as u32).hash(h);
if include_binding_names {
for param in &method.value.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
if let Some(body) = &method.value.body {
for stmt in &body.statements {
hash_statement(stmt, h, include_binding_names);
}
}
}
ClassElement::PropertyDefinition(prop) => {
prop.r#static.hash(h);
prop.computed.hash(h);
hash_property_key(&prop.key, h, include_binding_names);
if let Some(value) = &prop.value {
hash_expression(value, h, include_binding_names);
}
}
ClassElement::StaticBlock(block) => {
for stmt in &block.body {
hash_statement(stmt, h, include_binding_names);
}
}
_ => {}
});
}
fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::Expression, expr, h, {
Expression::BinaryExpression(bin) => {
std::mem::discriminant(&bin.operator).hash(h);
hash_expression(&bin.left, h, include_binding_names);
hash_expression(&bin.right, h, include_binding_names);
}
Expression::UnaryExpression(un) => {
std::mem::discriminant(&un.operator).hash(h);
hash_expression(&un.argument, h, include_binding_names);
}
Expression::CallExpression(call) => {
(call.arguments.len() as u32).hash(h);
hash_expression(&call.callee, h, include_binding_names);
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
hash_expression(expr, h, include_binding_names);
}
}
}
Expression::LogicalExpression(log) => {
std::mem::discriminant(&log.operator).hash(h);
hash_expression(&log.left, h, include_binding_names);
hash_expression(&log.right, h, include_binding_names);
}
Expression::ConditionalExpression(cond) => {
hash_expression(&cond.test, h, include_binding_names);
hash_expression(&cond.consequent, h, include_binding_names);
hash_expression(&cond.alternate, h, include_binding_names);
}
Expression::PrivateInExpression(pin) => {
pin.left.name.as_str().hash(h);
hash_expression(&pin.right, h, include_binding_names);
}
Expression::Identifier(ident) => {
if include_binding_names {
ident.name.as_str().hash(h);
}
}
Expression::NumericLiteral(num) => {
num.value.to_bits().hash(h);
}
Expression::StringLiteral(s) => {
s.value.hash(h);
}
Expression::BooleanLiteral(b) => {
b.value.hash(h);
}
Expression::AssignmentExpression(assign) => {
std::mem::discriminant(&assign.operator).hash(h);
if let Some(target) = assign.left.as_simple_assignment_target() {
hash_simple_assignment_target(target, h, include_binding_names);
}
hash_expression(&assign.right, h, include_binding_names);
}
Expression::UpdateExpression(update) => {
std::mem::discriminant(&update.operator).hash(h);
update.prefix.hash(h);
hash_simple_assignment_target(&update.argument, h, include_binding_names);
}
Expression::TemplateLiteral(tl) => {
(tl.quasis.len() as u32).hash(h);
for expr in &tl.expressions {
hash_expression(expr, h, include_binding_names);
}
}
Expression::TaggedTemplateExpression(tt) => {
hash_expression(&tt.tag, h, include_binding_names);
(tt.quasi.quasis.len() as u32).hash(h);
for expr in &tt.quasi.expressions {
hash_expression(expr, h, include_binding_names);
}
}
Expression::ArrowFunctionExpression(arrow) => {
(arrow.params.items.len() as u32).hash(h);
if include_binding_names {
for param in &arrow.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
for s in &arrow.body.statements {
hash_statement(s, h, include_binding_names);
}
}
Expression::FunctionExpression(fe) => {
(fe.params.items.len() as u32).hash(h);
if include_binding_names {
if let Some(id) = &fe.id {
id.name.as_str().hash(h);
}
for param in &fe.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
if let Some(body) = &fe.body {
for s in &body.statements {
hash_statement(s, h, include_binding_names);
}
}
}
Expression::ClassExpression(class) => {
if let Some(id) = &class.id {
id.name.as_str().hash(h);
}
class.super_class.is_some().hash(h);
if let Some(super_class) = &class.super_class {
hash_expression(super_class, h, include_binding_names);
}
for element in &class.body.body {
hash_class_element(element, h, include_binding_names);
}
}
Expression::ObjectExpression(obj) => {
(obj.properties.len() as u32).hash(h);
for prop in &obj.properties {
hash_object_property_kind(prop, h, include_binding_names);
}
}
Expression::NewExpression(ne) => {
hash_expression(&ne.callee, h, include_binding_names);
(ne.arguments.len() as u32).hash(h);
for arg in &ne.arguments {
if let Some(expr) = arg.as_expression() {
hash_expression(expr, h, include_binding_names);
}
}
}
Expression::PrivateFieldExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
}
Expression::Super(_) => {}
Expression::RegExpLiteral(lit) => {
if let Some(raw) = &lit.raw {
raw.to_string().hash(h);
}
}
_ => {}
});
}
fn hash_object_property_kind(prop: &ObjectPropertyKind, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::ObjectPropertyKind, prop, h, {
ObjectPropertyKind::ObjectProperty(p) => {
std::mem::discriminant(&p.kind).hash(h);
p.method.hash(h);
p.computed.hash(h);
hash_property_key(&p.key, h, include_binding_names);
hash_expression(&p.value, h, include_binding_names);
}
ObjectPropertyKind::SpreadProperty(_) => {}
});
}
fn hash_simple_assignment_target(
target: &SimpleAssignmentTarget, h: &mut rustc_hash::FxHasher, include_binding_names: bool,
) {
hash_match!(HashDomain::SimpleAssignmentTarget, target, h, {
SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => {
if include_binding_names {
ident.name.as_str().hash(h);
}
}
SimpleAssignmentTarget::StaticMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
}
SimpleAssignmentTarget::ComputedMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
hash_expression(&member.expression, h, include_binding_names);
}
SimpleAssignmentTarget::PrivateFieldExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
}
_ => {}
});
}
+40
View File
@@ -0,0 +1,40 @@
use super::*;
pub(super) fn hash_class_element(element: &ClassElement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::ClassElement, element, h, {
ClassElement::MethodDefinition(method) => {
method.r#static.hash(h);
method.computed.hash(h);
std::mem::discriminant(&method.kind).hash(h);
property::hash_property_key(&method.key, h, include_binding_names);
if method.computed {
property::hash_property_key(&method.key, h, include_binding_names);
}
(method.value.params.items.len() as u32).hash(h);
if include_binding_names {
for param in &method.value.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
if let Some(body) = &method.value.body {
for stmt in &body.statements {
statement::hash_statement(stmt, h, include_binding_names);
}
}
}
ClassElement::PropertyDefinition(prop) => {
prop.r#static.hash(h);
prop.computed.hash(h);
property::hash_property_key(&prop.key, h, include_binding_names);
if let Some(value) = &prop.value {
expression::hash_expression(value, h, include_binding_names);
}
}
ClassElement::StaticBlock(block) => {
for stmt in &block.body {
statement::hash_statement(stmt, h, include_binding_names);
}
}
_ => {}
});
}
@@ -0,0 +1,196 @@
use super::*;
pub(super) fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::Expression, expr, h, {
Expression::BinaryExpression(bin) => {
std::mem::discriminant(&bin.operator).hash(h);
hash_expression(&bin.left, h, include_binding_names);
hash_expression(&bin.right, h, include_binding_names);
}
Expression::UnaryExpression(un) => {
std::mem::discriminant(&un.operator).hash(h);
hash_expression(&un.argument, h, include_binding_names);
}
Expression::CallExpression(call) => {
(call.arguments.len() as u32).hash(h);
hash_expression(&call.callee, h, include_binding_names);
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
hash_expression(expr, h, include_binding_names);
}
}
}
Expression::LogicalExpression(log) => {
std::mem::discriminant(&log.operator).hash(h);
hash_expression(&log.left, h, include_binding_names);
hash_expression(&log.right, h, include_binding_names);
}
Expression::ConditionalExpression(cond) => {
hash_expression(&cond.test, h, include_binding_names);
hash_expression(&cond.consequent, h, include_binding_names);
hash_expression(&cond.alternate, h, include_binding_names);
}
Expression::PrivateInExpression(pin) => {
pin.left.name.as_str().hash(h);
hash_expression(&pin.right, h, include_binding_names);
}
Expression::SequenceExpression(seq) => {
(seq.expressions.len() as u32).hash(h);
for e in &seq.expressions {
hash_expression(e, h, include_binding_names);
}
}
Expression::Identifier(ident) => {
if include_binding_names {
ident.name.as_str().hash(h);
}
}
Expression::NumericLiteral(num) => {
num.value.to_bits().hash(h);
}
Expression::StringLiteral(s) => {
s.value.hash(h);
}
Expression::BooleanLiteral(b) => {
b.value.hash(h);
}
Expression::AssignmentExpression(assign) => {
std::mem::discriminant(&assign.operator).hash(h);
if let Some(target) = assign.left.as_simple_assignment_target() {
target::hash_simple_assignment_target(target, h, include_binding_names);
}
hash_expression(&assign.right, h, include_binding_names);
}
Expression::UpdateExpression(update) => {
std::mem::discriminant(&update.operator).hash(h);
update.prefix.hash(h);
target::hash_simple_assignment_target(&update.argument, h, include_binding_names);
}
Expression::TemplateLiteral(tl) => {
(tl.quasis.len() as u32).hash(h);
for expr in &tl.expressions {
hash_expression(expr, h, include_binding_names);
}
}
Expression::TaggedTemplateExpression(tt) => {
hash_expression(&tt.tag, h, include_binding_names);
(tt.quasi.quasis.len() as u32).hash(h);
for expr in &tt.quasi.expressions {
hash_expression(expr, h, include_binding_names);
}
}
Expression::ArrowFunctionExpression(arrow) => {
(arrow.params.items.len() as u32).hash(h);
if include_binding_names {
for param in &arrow.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
for s in &arrow.body.statements {
statement::hash_statement(s, h, include_binding_names);
}
}
Expression::FunctionExpression(fe) => {
(fe.params.items.len() as u32).hash(h);
if include_binding_names {
if let Some(id) = &fe.id {
id.name.as_str().hash(h);
}
for param in &fe.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
if let Some(body) = &fe.body {
for s in &body.statements {
statement::hash_statement(s, h, include_binding_names);
}
}
}
Expression::ClassExpression(class) => {
if let Some(id) = &class.id {
id.name.as_str().hash(h);
}
class.super_class.is_some().hash(h);
if let Some(super_class) = &class.super_class {
hash_expression(super_class, h, include_binding_names);
}
for element in &class.body.body {
class::hash_class_element(element, h, include_binding_names);
}
}
Expression::ObjectExpression(obj) => {
(obj.properties.len() as u32).hash(h);
for prop in &obj.properties {
property::hash_object_property_kind(prop, h, include_binding_names);
}
}
Expression::NewExpression(ne) => {
hash_expression(&ne.callee, h, include_binding_names);
(ne.arguments.len() as u32).hash(h);
for arg in &ne.arguments {
if let Some(expr) = arg.as_expression() {
hash_expression(expr, h, include_binding_names);
}
}
}
Expression::PrivateFieldExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
member.optional.hash(h);
}
Expression::StaticMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.property.name.as_str().hash(h);
member.optional.hash(h);
}
Expression::ComputedMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
hash_expression(&member.expression, h, include_binding_names);
member.optional.hash(h);
}
Expression::ChainExpression(chain) => {
hash_chain_element(&chain.expression, h, include_binding_names);
}
Expression::Super(_) => {}
Expression::RegExpLiteral(lit) => {
if let Some(raw) = &lit.raw {
raw.to_string().hash(h);
}
}
_ => {}
});
}
pub(super) fn hash_chain_element(element: &ChainElement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
std::mem::discriminant(element).hash(h);
match element {
ChainElement::StaticMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.property.name.as_str().hash(h);
member.optional.hash(h);
}
ChainElement::ComputedMemberExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
hash_expression(&member.expression, h, include_binding_names);
member.optional.hash(h);
}
ChainElement::PrivateFieldExpression(member) => {
hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
member.optional.hash(h);
}
ChainElement::CallExpression(call) => {
hash_expression(&call.callee, h, include_binding_names);
call.optional.hash(h);
(call.arguments.len() as u32).hash(h);
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
hash_expression(expr, h, include_binding_names);
}
}
}
ChainElement::TSNonNullExpression(expr) => {
hash_expression(&expr.expression, h, include_binding_names);
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use oxide_parser::{
BindingPattern, ChainElement, ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey,
SimpleAssignmentTarget, Statement,
};
use std::hash::Hash;
#[derive(Hash)]
enum HashDomain {
Statement,
Expression,
ClassElement,
PropertyKey,
ObjectPropertyKind,
SimpleAssignmentTarget,
}
macro_rules! hash_match {
($domain:expr, $value:expr, $h:expr, { $($arms:tt)* }) => {{
$domain.hash($h);
std::mem::discriminant($value).hash($h);
match $value {
$($arms)*
}
}};
}
mod class;
mod expression;
mod property;
mod statement;
mod target;
pub fn structural_hash(program: &oxide_parser::Program) -> u64 {
hash_program(program, false)
}
pub fn compiled_module_hash(program: &oxide_parser::Program) -> u64 {
hash_program(program, true)
}
fn hash_program(program: &oxide_parser::Program, include_binding_names: bool) -> u64 {
use std::hash::Hasher;
let mut h = rustc_hash::FxHasher::default();
for stmt in &program.body {
statement::hash_statement(stmt, &mut h, include_binding_names);
}
h.finish()
}
fn hash_binding_pattern(pattern: &BindingPattern, h: &mut rustc_hash::FxHasher) {
if let BindingPattern::BindingIdentifier(ident) = pattern {
ident.name.as_str().hash(h);
}
}
@@ -0,0 +1,39 @@
use super::*;
pub(super) fn hash_property_key(key: &PropertyKey, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::PropertyKey, key, h, {
PropertyKey::StaticIdentifier(ident) => {
ident.name.as_str().hash(h);
}
PropertyKey::Identifier(ident) => {
ident.name.as_str().hash(h);
}
PropertyKey::StringLiteral(s) => {
s.value.hash(h);
}
PropertyKey::NumericLiteral(n) => {
n.value.to_bits().hash(h);
}
PropertyKey::PrivateIdentifier(pi) => {
pi.name.as_str().hash(h);
}
_ => {
expression::hash_expression(key.to_expression(), h, include_binding_names);
}
});
}
pub(super) fn hash_object_property_kind(
prop: &ObjectPropertyKind, h: &mut rustc_hash::FxHasher, include_binding_names: bool,
) {
hash_match!(HashDomain::ObjectPropertyKind, prop, h, {
ObjectPropertyKind::ObjectProperty(p) => {
std::mem::discriminant(&p.kind).hash(h);
p.method.hash(h);
p.computed.hash(h);
hash_property_key(&p.key, h, include_binding_names);
expression::hash_expression(&p.value, h, include_binding_names);
}
ObjectPropertyKind::SpreadProperty(_) => {}
});
}
+169
View File
@@ -0,0 +1,169 @@
use super::*;
pub(super) fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
hash_match!(HashDomain::Statement, stmt, h, {
Statement::ExpressionStatement(es) => {
expression::hash_expression(&es.expression, h, include_binding_names);
}
Statement::VariableDeclaration(decl) => {
hash_variable_declaration(decl, h, include_binding_names);
}
Statement::ReturnStatement(ret) => {
if let Some(arg) = &ret.argument {
expression::hash_expression(arg, h, include_binding_names);
}
}
Statement::IfStatement(ifs) => {
expression::hash_expression(&ifs.test, h, include_binding_names);
hash_statement(&ifs.consequent, h, include_binding_names);
if let Some(alt) = &ifs.alternate {
hash_statement(alt, h, include_binding_names);
}
}
Statement::WhileStatement(wh) => {
expression::hash_expression(&wh.test, h, include_binding_names);
hash_statement(&wh.body, h, include_binding_names);
}
Statement::ForStatement(fr) => {
hash_for_statement(fr, h, include_binding_names);
}
Statement::BlockStatement(block) => {
for s in &block.body {
hash_statement(s, h, include_binding_names);
}
}
Statement::BreakStatement(b) => {
if let Some(label) = &b.label {
label.name.as_str().hash(h);
}
}
Statement::ContinueStatement(c) => {
if let Some(label) = &c.label {
label.name.as_str().hash(h);
}
}
Statement::LabeledStatement(ls) => {
ls.label.name.as_str().hash(h);
hash_statement(&ls.body, h, include_binding_names);
}
Statement::DoWhileStatement(dw) => {
hash_statement(&dw.body, h, include_binding_names);
expression::hash_expression(&dw.test, h, include_binding_names);
}
Statement::ForInStatement(fi) => {
expression::hash_expression(&fi.right, h, include_binding_names);
hash_statement(&fi.body, h, include_binding_names);
}
Statement::SwitchStatement(sw) => {
expression::hash_expression(&sw.discriminant, h, include_binding_names);
for case in &sw.cases {
if let Some(test) = &case.test {
expression::hash_expression(test, h, include_binding_names);
}
for s in &case.consequent {
hash_statement(s, h, include_binding_names);
}
}
}
Statement::FunctionDeclaration(fd) => {
hash_function_declaration(fd, h, include_binding_names);
}
Statement::ClassDeclaration(class) => {
if let Some(id) = &class.id {
id.name.as_str().hash(h);
}
class.super_class.is_some().hash(h);
if let Some(super_class) = &class.super_class {
expression::hash_expression(super_class, h, include_binding_names);
}
for element in &class.body.body {
class::hash_class_element(element, h, include_binding_names);
}
}
Statement::ThrowStatement(ts) => {
expression::hash_expression(&ts.argument, h, include_binding_names);
}
Statement::TryStatement(ts) => {
hash_try_statement(ts, h, include_binding_names);
}
_ => {}
});
}
pub(super) fn hash_variable_declaration(
decl: &oxide_parser::VariableDeclaration<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool,
) {
std::mem::discriminant(&decl.kind).hash(h);
(decl.declarations.len() as u32).hash(h);
for d in &decl.declarations {
if include_binding_names {
hash_binding_pattern(&d.id, h);
}
if let Some(init) = &d.init {
expression::hash_expression(init, h, include_binding_names);
}
}
}
fn hash_for_statement(fr: &oxide_parser::ForStatement<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
if let Some(init) = &fr.init {
if let Some(expr) = init.as_expression() {
expression::hash_expression(expr, h, include_binding_names);
} else if let ForStatementInit::VariableDeclaration(decl) = init {
hash_variable_declaration(decl, h, include_binding_names);
}
}
if let Some(test) = &fr.test {
expression::hash_expression(test, h, include_binding_names);
}
if let Some(update) = &fr.update {
expression::hash_expression(update, h, include_binding_names);
}
hash_statement(&fr.body, h, include_binding_names);
}
fn hash_function_declaration(
fd: &oxide_parser::Function<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool,
) {
if include_binding_names {
if let Some(id) = &fd.id {
id.name.as_str().hash(h);
}
} else if let Some(id) = &fd.id {
id.name.as_str().hash(h);
}
(fd.params.items.len() as u32).hash(h);
if include_binding_names {
for param in &fd.params.items {
hash_binding_pattern(&param.pattern, h);
}
}
if let Some(body) = &fd.body {
for s in &body.statements {
hash_statement(s, h, include_binding_names);
}
}
}
fn hash_try_statement(ts: &oxide_parser::TryStatement<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool) {
for s in &ts.block.body {
hash_statement(s, h, include_binding_names);
}
if let Some(catch) = &ts.handler {
if let Some(param) = &catch.param {
if include_binding_names {
hash_binding_pattern(&param.pattern, h);
} else if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &param.pattern {
bi.name.as_str().hash(h);
}
}
for s in &catch.body.body {
hash_statement(s, h, include_binding_names);
}
}
if let Some(finally) = &ts.finalizer {
for s in &finally.body {
hash_statement(s, h, include_binding_names);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
use super::*;
pub(super) fn hash_simple_assignment_target(
target: &SimpleAssignmentTarget, h: &mut rustc_hash::FxHasher, include_binding_names: bool,
) {
hash_match!(HashDomain::SimpleAssignmentTarget, target, h, {
SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => {
if include_binding_names {
ident.name.as_str().hash(h);
}
}
SimpleAssignmentTarget::StaticMemberExpression(member) => {
expression::hash_expression(&member.object, h, include_binding_names);
}
SimpleAssignmentTarget::ComputedMemberExpression(member) => {
expression::hash_expression(&member.object, h, include_binding_names);
expression::hash_expression(&member.expression, h, include_binding_names);
}
SimpleAssignmentTarget::PrivateFieldExpression(member) => {
expression::hash_expression(&member.object, h, include_binding_names);
member.field.name.as_str().hash(h);
}
_ => {}
});
}
-2
View File
@@ -4,6 +4,4 @@ pub mod compiler;
pub mod counter;
pub mod emitter;
pub mod hash;
pub mod module;
pub mod opcode;
pub mod symbol_table;
@@ -7,7 +7,7 @@ fn parse_to_hash(source: &str) -> u64 {
oxide_compiler::compiler::structural_hash(&program)
}
fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn compile_source(source: &str) -> oxide_bytecode::module::CompiledModule {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let compiler = Compiler::new();
@@ -1,14 +1,16 @@
use oxide_bytecode::module::{CompiledModule, Constant};
use oxide_bytecode::opcode::{self, OpCode};
use oxide_compiler::compiler::Compiler;
use oxide_compiler::opcode::{self, OpCode};
use oxide_parser::Allocator;
fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn compile_source(source: &str) -> CompiledModule {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let compiler = Compiler::new();
compiler.compile(&program).expect("compile failed")
}
#[allow(dead_code)]
fn compile_source_err(source: &str) -> String {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
@@ -31,17 +33,25 @@ fn compile_simple_expr() {
}
#[test]
fn unsupported_logical_assignment_returns_compile_error() {
let err = compile_source_err("let x = 0; x ||= 1;");
assert!(err.contains("compound assignment operator"));
assert!(err.contains("not supported"));
fn test_constant_pool_overflow_range_error() {
let expr = (0..70_000).map(|i| i.to_string()).collect::<Vec<_>>().join(",");
let source = format!("var a=[{expr}]; a");
let err = compile_source_err(&source);
assert!(err.contains("too many constants"), "unexpected error: {err}");
}
#[test]
fn unsupported_member_logical_assignment_returns_compile_error() {
let err = compile_source_err("let obj = { x: 0 }; obj.x ||= 1;");
assert!(err.contains("compound assignment operator"));
assert!(err.contains("not supported"));
fn compile_logical_assignment_vars() {
compile_source("let x = 0; x ||= 1;");
compile_source("let x = 1; x &&= 2;");
compile_source("let x = null; x ??= 3;");
}
#[test]
fn compile_logical_assignment_members() {
compile_source("let obj = { x: 0 }; obj.x ||= 1;");
compile_source("let obj = { x: 1 }; obj.x &&= 2;");
compile_source("let obj = { x: null }; obj.x ??= 3;");
}
#[test]
@@ -49,7 +59,7 @@ fn compile_constants() {
let module = compile_source("42");
assert!(!module.constants.is_empty());
assert_eq!(module.constants[0], oxide_compiler::compiler::Constant::Int(42));
assert_eq!(module.constants[0], Constant::Int(42));
}
#[test]
@@ -58,7 +68,7 @@ fn compile_dedups_identical_string_constants() {
let count = module
.constants
.iter()
.filter(|c| matches!(c, oxide_compiler::compiler::Constant::String(s) if s == "hello"))
.filter(|c| matches!(c, Constant::String(s) if s == "hello"))
.count();
assert_eq!(count, 1, "identical string constants should be interned once per module");
}
@@ -170,10 +180,14 @@ fn compile_logical_or_simple() {
#[test]
fn regression_coalesce_consistency() {
let result = std::panic::catch_unwind(|| {
compile_source("a ?? b");
});
assert!(result.is_err(), "a ?? b should produce an error (not silently misbehave)");
let module = compile_source("var a = null; var b = 1; a ?? b");
assert!(
module
.bytecode
.iter()
.any(|&instr| opcode::opcode(instr) == OpCode::NULLISH || opcode::opcode(instr) == OpCode::JMP_IF_NULLISH),
"a ?? b should emit nullish bytecode"
);
}
#[test]
@@ -331,6 +345,32 @@ fn compile_inc_dec_diff_opcodes() {
assert!(has_dec_pre, "--x should emit DEC_PRE");
}
#[test]
fn nullish_opcodes_round_trip() {
for op in [OpCode::NULLISH, OpCode::JMP_IF_NULLISH] {
assert_eq!(OpCode::try_from(op as u8), Ok(op));
assert_eq!(format!("{op}"), format!("{op:?}"));
}
}
#[test]
fn compile_nullish_fast_path_opcode() {
let module = compile_source("null ?? 5");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::NULLISH),
"side-effect-free ?? should emit NULLISH"
);
}
#[test]
fn compile_nullish_short_circuit_opcode() {
let module = compile_source("var x = 0; x ?? (x = 5)");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_NULLISH),
"side-effecting ?? should emit JMP_IF_NULLISH"
);
}
#[test]
fn compile_member_inc() {
let module = compile_source("var obj={x:1}; obj.x++");
@@ -447,7 +487,7 @@ fn compile_class_expression_default_constructor() {
#[test]
fn compile_large_pure_expression_reuses_temp_registers() {
let expr = std::iter::repeat("1").take(120).collect::<Vec<_>>().join(" + ");
let expr = std::iter::repeat("1").take(40).collect::<Vec<_>>().join(" + ");
let module = compile_source(&expr);
assert!(
module.n_registers < 16,
@@ -1,8 +1,9 @@
use oxide_bytecode::module::{CompiledModule, Constant};
use oxide_bytecode::opcode::{self, OpCode};
use oxide_compiler::compiler::Compiler;
use oxide_compiler::opcode::{self, OpCode};
use oxide_parser::Allocator;
fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn compile_source(source: &str) -> CompiledModule {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let compiler = Compiler::new();
@@ -13,7 +14,7 @@ fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn compile_var_declaration() {
let module = compile_source("var x = 42;");
assert!(!module.bytecode.is_empty());
assert_eq!(module.constants[0], oxide_compiler::compiler::Constant::Int(42));
assert_eq!(module.constants[0], Constant::Int(42));
}
#[test]
@@ -296,3 +297,18 @@ fn compile_class_static_setter_emits_define_accessor() {
"static class setter should emit DEFINE_ACCESSOR"
);
}
#[test]
fn compile_labeled_loop_emits_body() {
let module = compile_source("outer: while (true) { break outer; }");
assert!(!module.bytecode.is_empty(), "labeled loop should produce bytecode");
}
#[test]
fn compile_empty_statements_are_skipped() {
let module = compile_source("; ; var x = 1;");
assert!(
module.constants.iter().any(|c| matches!(c, Constant::Int(1))),
"var after empty statements should still compile"
);
}
@@ -1,9 +1,9 @@
use oxide_code_cache::CodeForge;
use oxide_compiler::compiler::Compiler;
use oxide_kernel::code_forge::CodeForge;
use oxide_parser::Allocator;
#[allow(dead_code)]
fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn compile_source(source: &str) -> oxide_bytecode::module::CompiledModule {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let compiler = Compiler::new();
@@ -29,8 +29,13 @@ fn structural_hash_hit() {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed");
let m1 = forge.get_or_compile(&program, &compiler).expect("first compile");
let m2 = forge.get_or_compile(&program, &compiler).expect("second compile");
let hash = oxide_compiler::compiler::compiled_module_hash(&program);
let m1 = forge
.get_or_insert_with(hash, || compiler.compile(&program))
.expect("first compile");
let m2 = forge
.get_or_insert_with(hash, || compiler.compile(&program))
.expect("second compile");
assert_eq!((m1.bytecode.len(), m1.n_registers), (m2.bytecode.len(), m2.n_registers));
}
@@ -137,3 +142,34 @@ fn class_method_getter_static_setter_shapes_differ() {
assert_ne!(method, static_setter, "method and static setter must hash differently");
assert_ne!(getter, static_setter, "getter and static setter must hash differently");
}
#[test]
fn structural_hash_distinguishes_nullish_operator() {
let nullish = parse_to_hash("a ?? b");
let and = parse_to_hash("a && b");
let or = parse_to_hash("a || b");
assert_ne!(nullish, and);
assert_ne!(nullish, or);
assert_ne!(and, or);
}
#[test]
fn structural_hash_distinguishes_optional_chain_shape() {
let optional_b = parse_to_hash("a?.b");
let optional_c = parse_to_hash("a?.c");
let plain_b = parse_to_hash("a.b");
assert_ne!(optional_b, optional_c);
assert_ne!(optional_b, plain_b);
assert_ne!(optional_c, plain_b);
}
#[test]
fn compiled_module_rejects_optional_chain_assignment_target() {
let allocator = Allocator::default();
match oxide_parser::parse(&allocator, "a?.b = 1") {
Ok(program) => assert!(Compiler::new().compile(&program).is_err()),
Err(_) => {}
}
}
@@ -1,8 +1,9 @@
use oxide_bytecode::module::{CompiledModule, Constant};
use oxide_bytecode::opcode::{self, OpCode};
use oxide_compiler::compiler::Compiler;
use oxide_compiler::opcode::{self, OpCode};
use oxide_parser::Allocator;
fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn compile_source(source: &str) -> CompiledModule {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let compiler = Compiler::new();
@@ -13,7 +14,7 @@ fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
fn symbol_table_declare_and_lookup() {
let module = compile_source("var x = 42;");
assert!(!module.bytecode.is_empty());
assert_eq!(module.constants[0], oxide_compiler::compiler::Constant::Int(42));
assert_eq!(module.constants[0], Constant::Int(42));
}
#[test]
+2 -1
View File
@@ -7,7 +7,8 @@ rust-version.workspace = true
description = "OxideJS - Shared kernel (CodeForge, ShapeForge, PropForge, StringForge, BuiltinWorld)"
[dependencies]
oxide_compiler = { path = "../oxide_compiler" }
oxide_bytecode = { path = "../oxide_bytecode" }
oxide_code_cache = { path = "../oxide_code_cache" }
oxide_parser = { path = "../oxide_parser" }
oxide_types = { path = "../oxide_types" }
dashmap.workspace = true
+5
View File
@@ -109,6 +109,7 @@ pub struct ErrorMethods {
}
pub struct StringMethods {
pub from_char_code: *const (),
pub index_of: *const (),
pub includes: *const (),
pub char_at: *const (),
@@ -938,6 +939,10 @@ impl BuiltinWorld {
}
pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &StringForge, shape_forge: &ShapeForge) {
let ctor_ptr = P::as_ptr(&self.string_constructor) as *mut JsObject;
let ctor = unsafe { &mut *ctor_ptr };
bind_methods!(self, ctor, string_forge, shape_forge, ("fromCharCode", methods.from_char_code, 1),);
let proto_ptr = P::as_ptr(&self.string_proto) as *mut JsObject;
let proto = unsafe { &mut *proto_ptr };
bind_methods!(
+1 -141
View File
@@ -1,141 +1 @@
use std::sync::Arc;
use dashmap::DashMap;
use oxide_compiler::compiler::Compiler;
use oxide_compiler::module::CompiledModule;
use crate::{kernel_debug, kernel_info};
pub use oxide_compiler::compiler::{compiled_module_hash, structural_hash};
/// Shared compiled-module cache.
///
/// Concurrency model:
/// - Read-heavy after warmup: cache hits dominate steady-state execution.
/// - Backed by a fixed-shard `DashMap` so lookup contention stays predictable across
/// developer machines and CI hosts.
pub struct CodeForge {
map: DashMap<u64, Arc<CompiledModule>>,
}
impl CodeForge {
pub fn new() -> Self {
Self {
map: DashMap::with_shard_amount(16),
}
}
pub fn get_or_compile(
&self, program: &oxide_parser::Program, compiler: &Compiler,
) -> Result<Arc<CompiledModule>, String> {
let hash = compiled_module_hash(program);
if let Some(module) = self.map.get(&hash) {
kernel_info!("CodeForge hit compiled_hash={:#x}", hash);
return Ok(Arc::clone(&module));
}
let module = compiler.compile(program)?;
let module = Arc::new(module);
kernel_debug!("CodeForge miss compiled_hash={:#x}", hash);
Ok(Arc::clone(self.map.entry(hash).or_insert(module).value()))
}
}
impl Default for CodeForge {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxide_parser::Allocator;
#[test]
fn test_structural_hash_same_input() {
let a1 = Allocator::default();
let a2 = Allocator::default();
let p1 = oxide_parser::parse(&a1, "1 + 2").expect("parse failed");
let p2 = oxide_parser::parse(&a2, "1 + 2").expect("parse failed");
assert_eq!(structural_hash(&p1), structural_hash(&p2));
}
#[test]
fn test_structural_hash_same_structure() {
let a1 = Allocator::default();
let a2 = Allocator::default();
let p1 = oxide_parser::parse(&a1, "var x = 1; var y = 2;").expect("parse failed");
let p2 = oxide_parser::parse(&a2, "var a = 1; var b = 2;").expect("parse failed");
assert_eq!(structural_hash(&p1), structural_hash(&p2));
}
#[test]
fn test_compiled_module_hash_distinguishes_identifier_loads() {
let a1 = Allocator::default();
let a2 = Allocator::default();
let p1 = oxide_parser::parse(&a1, "var x = 1; var y = 2; x").expect("parse failed");
let p2 = oxide_parser::parse(&a2, "var x = 1; var y = 2; y").expect("parse failed");
assert_eq!(structural_hash(&p1), structural_hash(&p2));
assert_ne!(compiled_module_hash(&p1), compiled_module_hash(&p2));
}
#[test]
fn test_structural_hash_different_ops() {
let a1 = Allocator::default();
let a2 = Allocator::default();
let p1 = oxide_parser::parse(&a1, "1 + 2").expect("parse failed");
let p2 = oxide_parser::parse(&a2, "1 - 2").expect("parse failed");
assert_ne!(structural_hash(&p1), structural_hash(&p2));
}
#[test]
fn test_cache_hit() {
let forge = CodeForge::new();
let compiler = Compiler::new();
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed");
let first = forge.get_or_compile(&program, &compiler).expect("first compile");
let second = forge.get_or_compile(&program, &compiler).expect("second compile");
assert_eq!((first.bytecode.len(), first.n_registers), (second.bytecode.len(), second.n_registers));
}
#[test]
fn test_cache_miss_new_program() {
let forge = CodeForge::new();
let compiler = Compiler::new();
let a1 = Allocator::default();
let p1 = oxide_parser::parse(&a1, "1 + 2").expect("parse failed");
let a2 = Allocator::default();
let p2 = oxide_parser::parse(&a2, "3 * 4").expect("parse failed");
let m1 = forge.get_or_compile(&p1, &compiler).expect("compile p1");
let m2 = forge.get_or_compile(&p2, &compiler).expect("compile p2");
assert_ne!(m1.bytecode, m2.bytecode);
}
#[test]
fn test_cache_miss_same_shape_different_binding_program() {
let forge = CodeForge::new();
let compiler = Compiler::new();
let a1 = Allocator::default();
let p1 = oxide_parser::parse(&a1, "var x = 1; var y = 2; x").expect("parse failed");
let a2 = Allocator::default();
let p2 = oxide_parser::parse(&a2, "var x = 1; var y = 2; y").expect("parse failed");
let m1 = forge.get_or_compile(&p1, &compiler).expect("compile p1");
let m2 = forge.get_or_compile(&p2, &compiler).expect("compile p2");
assert!(!Arc::ptr_eq(&m1, &m2));
assert_ne!(m1.bytecode, m2.bytecode);
}
}
pub use oxide_code_cache::CodeForge;
+12
View File
@@ -146,6 +146,18 @@ impl KernelCore {
pub fn set_session_gc_threshold(&mut self, bytes: usize) {
self.config.session_gc_threshold = bytes;
}
pub fn sweep_runner_forges(&self) {
self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1)));
// test262 creates a fresh VM/session per test. At this boundary no JS object
// from prior tests may retain transient shapes/templates.
if self.shape_forge.len() > 50_000 {
self.shape_forge.clear_transient();
self.prop_forge.clear();
} else if self.prop_forge.len() > 50_000 {
self.prop_forge.clear();
}
}
}
/// Per-session mutable state: builtin prototype objects and the global object.
+12
View File
@@ -43,6 +43,18 @@ impl PropForge {
}
}
}
pub fn clear(&self) {
self.templates.clear();
}
pub fn len(&self) -> usize {
self.templates.len()
}
pub fn is_empty(&self) -> bool {
self.templates.is_empty()
}
}
impl Default for PropForge {
+17
View File
@@ -114,6 +114,23 @@ impl ShapeForge {
shapes.get((id - 1) as usize).and_then(|s| s.clone())
}
pub fn len(&self) -> usize {
self.shapes.read().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear_transient(&self) {
let mut shapes = self.shapes.write().unwrap();
if shapes.len() > 1 {
shapes.truncate(1);
}
self.transitions.clear();
self.next_id.store(2, Ordering::Relaxed);
}
pub fn lookup_position(&self, shape_id: ShapeId, prop_name: StringIndex) -> Option<u32> {
let shapes = self.shapes.read().unwrap();
let total_depth = Self::compute_depth(shape_id, &shapes);
+8
View File
@@ -126,6 +126,14 @@ impl StringForge {
inner.map = new_map;
inner.entries = new_entries;
}
pub fn len(&self) -> usize {
self.inner.read().unwrap().entries.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for StringForge {
+1
View File
@@ -11,6 +11,7 @@ name = "test262-runner"
path = "src/main.rs"
[dependencies]
oxide_bytecode = { path = "../oxide_bytecode" }
oxide_compiler = { path = "../oxide_compiler" }
oxide_vm = { path = "../oxide_vm" }
oxide_parser = { path = "../oxide_parser" }
+458 -22
View File
@@ -6,9 +6,10 @@ use oxide_vm::vm::Vm;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use std::time::Duration;
use std::time::{Duration, Instant};
use walkdir::WalkDir;
// Thread-local that records the path currently being executed.
@@ -147,6 +148,7 @@ struct RunConfig {
test262_root: Option<PathBuf>,
filter: Option<String>,
no_skip: bool,
supervise: bool,
}
struct HarnessSources {
@@ -289,6 +291,7 @@ impl RunConfig {
for arg in args.iter().skip(1) {
match arg.as_str() {
"--no-skip" => config.no_skip = true,
"--supervise" => config.supervise = true,
"--help" | "-h" => return Err(Self::usage()),
_ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())),
_ => positional.push(arg.clone()),
@@ -309,9 +312,17 @@ impl RunConfig {
}
fn usage() -> String {
"usage: test262-runner [--no-skip] [test262-root] [path-filter]\n\
"usage: test262-runner [--no-skip] [--supervise] [test262-root] [path-filter]\n\
\n\
--no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures."
--no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\
--supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\
\x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\
\n\
supervised-mode env tunables:\n\
\x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout (default 10)\n\
\x20 OXIDE_TEST262_WINDOW tests per window (default 5000)\n\
\x20 OXIDE_TEST262_SUPERVISORS concurrent windows (default = available parallelism)\n\
\x20 OXIDE_TEST262_STARTUP_GRACE_SECS grace for a child's first heartbeat (default 60)"
.into()
}
}
@@ -365,7 +376,6 @@ fn is_skipped(meta: &TestMeta) -> Option<String> {
"cross-realm",
"new.target",
"well-formed-json-stringify",
"optional-chaining",
"symbols-as-weakmap-keys",
"class-accessors-private",
];
@@ -512,12 +522,395 @@ fn run_test_inner(
}
fn discover_tests(test262_root: &Path) -> Vec<PathBuf> {
WalkDir::new(test262_root)
let mut paths: Vec<PathBuf> = WalkDir::new(test262_root)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "js"))
.map(|e| e.path().to_path_buf())
.collect();
paths.sort();
paths
}
fn parse_summary_count(stdout: &str, label: &str) -> Option<usize> {
stdout.lines().find_map(|line| {
let trimmed = line.trim_start();
let rest = trimmed.strip_prefix(label)?;
let value = rest.trim().split(' ').next()?;
value.parse::<usize>().ok()
})
}
fn run_chunked(args: &[String], skip_until: usize, end_index: usize, chunk_size: usize) -> bool {
let exe = match std::env::current_exe() {
Ok(path) => path,
Err(err) => {
eprintln!("failed to resolve current executable for chunked mode: {err}");
return false;
}
};
let mut aggregate_pass = 0usize;
let mut aggregate_fail = 0usize;
let mut aggregate_skip = 0usize;
let mut chunk_start = skip_until;
let mut chunk_id = 1usize;
while chunk_start < end_index {
let chunk_len = (end_index - chunk_start).min(chunk_size);
eprintln!("chunk {chunk_id}: tests [{chunk_start}, {})", chunk_start + chunk_len);
let output = match Command::new(&exe)
.args(args.iter().skip(1))
.env("OXIDE_SKIP_UNTIL", chunk_start.to_string())
.env("OXIDE_MAX_TESTS", chunk_len.to_string())
.env("OXIDE_TEST262_CHILD_CHUNK", "1")
.env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1")
.output()
{
Ok(output) => output,
Err(err) => {
eprintln!("failed to run chunk {chunk_id}: {err}");
return false;
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
print!("{stdout}");
eprint!("{stderr}");
if !output.status.success() {
eprintln!("chunk {chunk_id} crashed or aborted");
return false;
}
aggregate_pass += parse_summary_count(&stdout, "pass :").unwrap_or(0);
aggregate_fail += parse_summary_count(&stdout, "fail :").unwrap_or(0);
aggregate_skip += parse_summary_count(&stdout, "skip :").unwrap_or(0);
chunk_start += chunk_len;
chunk_id += 1;
}
let aggregate_total = aggregate_pass + aggregate_fail + aggregate_skip;
println!();
println!("═══════════════════════════════════════");
println!(" test262 chunked aggregate");
println!("═══════════════════════════════════════");
println!(" total : {}", aggregate_total);
println!(" pass : {}", aggregate_pass);
println!(" fail : {}", aggregate_fail);
println!(" skip : {}", aggregate_skip);
println!("═══════════════════════════════════════");
aggregate_fail == 0
}
/// One heartbeat record written by a supervised child and polled by the parent.
/// `index` is the global test index the child is about to run (`START`) or the
/// window end it finished (`DONE`); the tallies always cover tests completed
/// *before* `index`, so the in-flight test is never counted yet.
struct Heartbeat {
phase: String,
index: usize,
pass: usize,
fail: usize,
skip: usize,
}
/// Overwrite the heartbeat file with a single line. Errors are ignored: a missed
/// heartbeat just delays stall detection by one poll interval. Assumes a single
/// worker (the supervisor always forces `OXIDE_TEST262_WORKERS=1`); with more
/// than one worker the running index is ambiguous and the file races.
fn write_heartbeat(path: &Path, phase: &str, index: usize, pass: usize, fail: usize, skip: usize) {
let _ = std::fs::write(path, format!("{phase} {index} {pass} {fail} {skip}\n"));
}
/// Read the latest heartbeat. Returns `None` on any missing/partial/malformed
/// content so the poll loop can simply retry on the next tick.
fn read_heartbeat(path: &Path) -> Option<Heartbeat> {
let content = std::fs::read_to_string(path).ok()?;
let line = content.lines().next()?;
let mut parts = line.split_whitespace();
let phase = parts.next()?.to_string();
let index = parts.next()?.parse().ok()?;
let pass = parts.next()?.parse().ok()?;
let fail = parts.next()?.parse().ok()?;
let skip = parts.next()?.parse().ok()?;
Some(Heartbeat { phase, index, pass, fail, skip })
}
/// Run one window `[wstart, wend)` to completion under supervision, returning
/// `(pass, fail, skip)` aggregated across however many child restarts it took.
///
/// A single-worker child runs the normal in-process path (warm kernel + harness
/// prefix cache) and emits a heartbeat before each test. If the running index
/// stalls past `timeout`, the child is killed, the culprit is reported by path,
/// and a fresh child resumes from `culprit + 1`. A child that crashes mid-test
/// is recovered through the same path. Timeouts/crashes count as skip by default
/// and fail under `--no-skip`.
#[allow(clippy::too_many_arguments)]
fn supervise_window(
exe: &Path, args: &[String], no_skip: bool, wstart: usize, wend: usize, timeout: Duration, startup_grace: Duration,
paths: &[PathBuf], window_id: usize,
) -> (usize, usize, usize) {
let (mut pass, mut fail, mut skip) = (0usize, 0usize, 0usize);
let mut cur = wstart;
let hb_path = std::env::temp_dir().join(format!("oxide_t262_hb_{}_{}.txt", std::process::id(), window_id));
let describe = |idx: usize| -> String {
paths
.get(idx)
.map(|p| p.display().to_string())
.unwrap_or_else(|| format!("#{idx}"))
};
while cur < wend {
let _ = std::fs::remove_file(&hb_path);
let max_tests = wend - cur;
let mut child = match Command::new(exe)
.args(args.iter().skip(1))
.env("OXIDE_SKIP_UNTIL", cur.to_string())
.env("OXIDE_MAX_TESTS", max_tests.to_string())
.env("OXIDE_TEST262_WORKERS", "1")
.env("OXIDE_TEST262_HEARTBEAT", &hb_path)
.env("OXIDE_TEST262_CHILD_CHUNK", "1")
.env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1")
.env_remove("OXIDE_TEST262_CHUNK_SIZE")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(child) => child,
Err(err) => {
eprintln!(" window {window_id}: failed to spawn child at index {cur}: {err}");
if no_skip {
fail += 1;
} else {
skip += 1;
}
cur += 1;
continue;
}
};
let spawn_time = Instant::now();
let mut last_index: Option<usize> = None;
let mut last_change = Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
match read_heartbeat(&hb_path) {
Some(hb) if hb.phase == "DONE" => {
pass += hb.pass;
fail += hb.fail;
skip += hb.skip;
cur = wend;
}
Some(hb) => {
pass += hb.pass;
fail += hb.fail;
skip += hb.skip;
eprintln!(
" [warn] window {window_id}: child exited ({status}) mid-test #{}: {}",
hb.index,
describe(hb.index)
);
if no_skip {
fail += 1;
} else {
skip += 1;
}
cur = hb.index + 1;
}
None => {
eprintln!(
" [warn] window {window_id}: child exited ({status}) with no heartbeat at index {cur}; skipping one"
);
if no_skip {
fail += 1;
} else {
skip += 1;
}
cur += 1;
}
}
break;
}
Ok(None) => {}
Err(err) => {
eprintln!(" window {window_id}: try_wait error: {err}");
let _ = child.kill();
let _ = child.wait();
cur += 1;
break;
}
}
if let Some(hb) = read_heartbeat(&hb_path) {
if Some(hb.index) != last_index {
last_index = Some(hb.index);
last_change = Instant::now();
}
}
let (deadline, elapsed) = if last_index.is_some() {
(timeout, last_change.elapsed())
} else {
(startup_grace, spawn_time.elapsed())
};
if elapsed > deadline {
let hb = read_heartbeat(&hb_path);
let culprit = hb.as_ref().map(|h| h.index).unwrap_or(cur);
if let Some(h) = &hb {
pass += h.pass;
fail += h.fail;
skip += h.skip;
}
eprintln!(
" [timeout] window {window_id}: TIMEOUT ({}s) on test #{culprit}: {}",
deadline.as_secs(),
describe(culprit)
);
let _ = child.kill();
let _ = child.wait();
if no_skip {
fail += 1;
} else {
skip += 1;
}
cur = culprit + 1;
break;
}
std::thread::sleep(Duration::from_millis(200));
}
}
let _ = std::fs::remove_file(&hb_path);
(pass, fail, skip)
}
/// Orchestrate a supervised full run: split `[skip_until, end_index)` into
/// windows and run up to `supervisors` of them concurrently. The supervisor
/// threads only spawn/poll/kill child processes and touch files — they never
/// hold a `KernelCore`, so sharing `paths`/`args` by reference is safe.
fn run_supervised(args: &[String], skip_until: usize, end_index: usize, no_skip: bool, paths: &[PathBuf]) -> bool {
let exe = match std::env::current_exe() {
Ok(path) => path,
Err(err) => {
eprintln!("failed to resolve current executable for supervised mode: {err}");
return false;
}
};
let window = std::env::var("OXIDE_TEST262_WINDOW")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(5000);
let timeout = Duration::from_secs(
std::env::var("OXIDE_TEST262_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.filter(|&n| n > 0)
.unwrap_or(10),
);
let startup_grace = Duration::from_secs(
std::env::var("OXIDE_TEST262_STARTUP_GRACE_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.filter(|&n| n > 0)
.unwrap_or(60),
);
let mut windows: Vec<(usize, usize, usize)> = Vec::new();
let mut start = skip_until;
let mut id = 0usize;
while start < end_index {
let end = (start + window).min(end_index);
windows.push((id, start, end));
start = end;
id += 1;
}
let supervisors = std::env::var("OXIDE_TEST262_SUPERVISORS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4))
.min(windows.len().max(1));
eprintln!(
"supervised mode: {} window(s) of up to {window} test(s), {supervisors} supervisor(s), per-test timeout {}s",
windows.len(),
timeout.as_secs()
);
let next = AtomicUsize::new(0);
let next = &next;
let windows = &windows;
let exe = &exe;
let partials: Vec<(usize, usize, usize)> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..supervisors)
.map(|_| {
scope.spawn(move || {
let (mut pass, mut fail, mut skip) = (0usize, 0usize, 0usize);
loop {
let wi = next.fetch_add(1, Ordering::Relaxed);
if wi >= windows.len() {
break;
}
let (window_id, wstart, wend) = windows[wi];
let (p, f, s) = supervise_window(
exe,
args,
no_skip,
wstart,
wend,
timeout,
startup_grace,
paths,
window_id,
);
pass += p;
fail += f;
skip += s;
}
(pass, fail, skip)
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("supervisor thread panicked"))
.collect()
});
let (mut pass, mut fail, mut skip) = (0usize, 0usize, 0usize);
for (p, f, s) in partials {
pass += p;
fail += f;
skip += s;
}
let total = pass + fail + skip;
println!();
println!("═══════════════════════════════════════");
println!(" test262 supervised aggregate");
println!("═══════════════════════════════════════");
println!(" total : {total}");
println!(" pass : {pass}");
println!(" fail : {fail}");
println!(" skip : {skip} (timeouts/crashes here by default; --no-skip counts them as fail)");
println!("═══════════════════════════════════════");
fail == 0
}
/// Per-test pipeline shared by the serial and parallel execution paths:
@@ -717,13 +1110,19 @@ fn run_tests() -> bool {
// own kernel + harness-source registry + prefix cache. Only `PathBuf` and
// `TestResult` (both `Send`) cross thread boundaries. Workers pull test
// indices from a shared atomic cursor for dynamic load balancing.
let default_workers = if config.no_skip {
4
} else {
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
};
let workers = std::env::var("OXIDE_TEST262_WORKERS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4))
.unwrap_or(default_workers)
.min(total.max(1));
let log_running_tests = std::env::var_os("OXIDE_TEST262_RUNNING_LOG").is_some();
let heartbeat_path: Option<PathBuf> = std::env::var_os("OXIDE_TEST262_HEARTBEAT").map(PathBuf::from);
eprintln!("running on {workers} worker thread(s)");
@@ -738,16 +1137,42 @@ fn run_tests() -> bool {
.and_then(|n| skip_until.checked_add(n))
.map(|n| n.min(total))
.unwrap_or(total);
let kernel_batch = std::env::var("OXIDE_TEST262_KERNEL_BATCH")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(if config.no_skip { 1000 } else { 5000 });
let chunk_size = std::env::var("OXIDE_TEST262_CHUNK_SIZE")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0);
let is_chunk_child = std::env::var_os("OXIDE_TEST262_CHILD_CHUNK").is_some();
let allow_fail_exit = std::env::var_os("OXIDE_TEST262_ALLOW_FAIL_EXIT").is_some();
if config.supervise && !is_chunk_child && filter.is_none() {
eprintln!("supervised mode enabled: child-window execution with per-test timeout + auto-resume");
return run_supervised(&args, skip_until, end_index, config.no_skip, &paths);
}
if let Some(chunk_size) = chunk_size {
if !is_chunk_child && filter.is_none() {
eprintln!("chunked mode enabled: {chunk_size} test(s) per child process");
return run_chunked(&args, skip_until, end_index, chunk_size);
}
}
eprintln!("kernel reset batch: {kernel_batch} test(s)");
let cursor = AtomicUsize::new(skip_until);
let progress = AtomicUsize::new(skip_until);
let filter = &filter;
let no_skip = config.no_skip;
let paths_ref = &paths;
let heartbeat_ref = &heartbeat_path;
let harness_cache = Arc::new(RwLock::new(HarnessPrefixCache::new()));
// Each worker returns its partial stats plus (index, result) pairs so the
// final results vector can be restored to discovery order for stable output.
let partials: Vec<(RunStats, Vec<(usize, TestResult)>)> = std::thread::scope(|scope| {
// Keep memory flat: workers return only aggregate stats. Retaining tens of
// thousands of `TestResult`s makes `--no-skip` runs accumulate path/error
// strings until the process OOMs near the end of the suite.
let partials: Vec<RunStats> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|_| {
let cursor = &cursor;
@@ -758,10 +1183,10 @@ fn run_tests() -> bool {
std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn_scoped(scope, move || {
let kernel = build_runner_kernel();
let mut kernel = build_runner_kernel();
let harness_sources = HARNESS.get_or_init(HarnessSources::new);
let mut stats = RunStats::default();
let mut out: Vec<(usize, TestResult)> = Vec::new();
let mut tests_since_kernel_reset = 0usize;
let tid = std::thread::current().id();
loop {
@@ -777,19 +1202,29 @@ fn run_tests() -> bool {
if log_running_tests {
eprintln!(" [{tid:?}] running: {path_str}");
}
if let Some(hb) = heartbeat_ref {
write_heartbeat(hb, "START", i, stats.pass, stats.fail, stats.skip);
}
let result =
process_path(&paths_ref[i], filter, no_skip, &kernel, harness_sources, &harness_cache);
stats.record(&result);
out.push((i, result));
tests_since_kernel_reset += 1;
let done = progress.fetch_add(1, Ordering::Relaxed) + 1;
if done % 500 == 0 {
kernel.sweep_runner_forges();
}
if tests_since_kernel_reset >= kernel_batch {
kernel = build_runner_kernel();
tests_since_kernel_reset = 0;
}
if done % 500 == 0 || done == total {
eprintln!(" progress: {done}/{total} ({}%)", done * 100 / total);
}
}
(stats, out)
stats
})
.expect("failed to spawn test262 worker thread")
})
@@ -801,26 +1236,27 @@ fn run_tests() -> bool {
.collect()
});
// Reduce partial stats and restore discovery order for deterministic output.
// Reduce partial stats without materializing every test result in memory.
let mut stats = RunStats::default();
let mut indexed: Vec<(usize, TestResult)> = Vec::with_capacity(total);
for (partial_stats, partial_results) in partials {
for partial_stats in partials {
stats.merge(partial_stats);
indexed.extend(partial_results);
}
indexed.sort_by_key(|(i, _)| *i);
let results: Vec<TestResult> = indexed.into_iter().map(|(_, r)| r).collect();
if let Some(hb) = &heartbeat_path {
write_heartbeat(hb, "DONE", end_index, stats.pass, stats.fail, stats.skip);
}
eprintln!();
let ran = stats.pass + stats.fail;
let executed_total = end_index.saturating_sub(skip_until);
println!();
println!("═══════════════════════════════════════");
println!(" test262 results");
println!("═══════════════════════════════════════");
println!(" total : {}", results.len());
let total = results.len() as f64;
println!(" total : {}", executed_total);
let total = executed_total as f64;
println!(" pass : {} ({:.1}%)", stats.pass, stats.pass as f64 / total * 100.0);
println!(" fail : {} ({:.1}%)", stats.fail, stats.fail as f64 / total * 100.0);
println!(" skip : {} ({:.1}%)", stats.skip, stats.skip as f64 / total * 100.0);
@@ -840,7 +1276,7 @@ fn run_tests() -> bool {
}
println!("═══════════════════════════════════════");
if stats.fail > 0 {
if stats.fail > 0 && !allow_fail_exit {
return false;
}
true
+5 -1
View File
@@ -40,6 +40,7 @@ unsafe impl Send for NativeFnPtr {}
unsafe impl Sync for NativeFnPtr {}
pub type ShapeId = u32;
pub const MAX_DENSE_PROPS: usize = 1_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypedArrayKind {
@@ -297,7 +298,7 @@ impl JsObject {
captured_this: JsValue::undefined(),
home_object: JsValue::undefined(),
};
let vec = Box::new(vec![JsValue::undefined(); n_elements]);
let vec = Box::new(vec![JsValue::undefined(); n_elements.min(MAX_DENSE_PROPS)]);
obj.hash_props = Box::into_raw(vec) as *mut u8;
obj
}
@@ -615,6 +616,9 @@ impl JsObject {
/// Set property value at position index. Vec auto-grows if needed.
pub fn set_prop_at(&mut self, position: impl PropIndex, val: JsValue) {
let pos = position.to_u32() as usize;
if pos > MAX_DENSE_PROPS {
return;
}
{
let vec = self.ensure_hash_props();
if pos < vec.len() {
+13
View File
@@ -148,6 +148,10 @@ impl JsValue {
is_nan_boxed(self.0) && get_tag(self.0) == TAG_UNDEFINED
}
pub fn is_nullish(&self) -> bool {
self.is_null() || self.is_undefined()
}
pub fn is_object(&self) -> bool {
is_nan_boxed(self.0) && get_tag(self.0) == TAG_OBJECT
}
@@ -386,6 +390,15 @@ mod tests {
assert_eq!(JsValue::undefined(), JsValue::undefined());
}
#[test]
fn nullish_matches_only_null_and_undefined() {
assert!(JsValue::null().is_nullish());
assert!(JsValue::undefined().is_nullish());
assert!(!JsValue::int(0).is_nullish());
assert!(!JsValue::bool(false).is_nullish());
assert!(!JsValue::float(f64::NAN).is_nullish());
}
#[test]
fn canonicalization_idempotent() {
let mut runner = TestRunner::default();
+2 -1
View File
@@ -7,7 +7,7 @@ rust-version.workspace = true
description = "OxideJS - Register-based VM with epoch arena memory"
[dependencies]
oxide_compiler = { path = "../oxide_compiler" }
oxide_bytecode = { path = "../oxide_bytecode" }
oxide_kernel = { path = "../oxide_kernel" }
oxide_types = { path = "../oxide_types" }
bumpalo.workspace = true
@@ -23,5 +23,6 @@ unicode-normalization.workspace = true
tracing.workspace = true
[dev-dependencies]
oxide_compiler = { path = "../oxide_compiler" }
proptest.workspace = true
oxide_parser = { path = "../oxide_parser" }
@@ -1,5 +1,6 @@
use std::sync::Arc;
use crate::bindings::{apply_binding_table, configure_native_constructor};
use oxide_kernel::builtin::StringMethods;
use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::JsObject;
@@ -7,6 +8,7 @@ use oxide_types::value::JsValue;
pub fn bind_string(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject) {
let string_methods = StringMethods {
from_char_code: crate::builtins::string::string_from_char_code as *const (),
index_of: crate::builtins::string::string_index_of as *const (),
includes: crate::builtins::string::string_includes as *const (),
char_at: crate::builtins::string::string_char_at as *const (),
@@ -40,6 +42,19 @@ pub fn bind_string(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
core.shape_forge().as_ref(),
);
let ctor_ptr = session.builtin_world().string_constructor.as_ptr() as *mut JsObject;
let ctor = unsafe { &mut *ctor_ptr };
configure_native_constructor(ctor, crate::builtins::string::string_constructor as *const (), 1);
let proto_ptr = session.builtin_world().string_proto.as_ptr() as *mut JsObject;
let proto = unsafe { &mut *proto_ptr };
apply_binding_table(
session.builtin_world(),
proto,
core,
&[("toString", crate::builtins::string::string_to_string as *const (), 0)],
);
let si_str = core.string_forge().intern("String").0;
let str_shape = core.shape_forge().make_shape(global.shape_id(), si_str);
let str_val = JsValue::from_js_object(session.builtin_world().string_constructor.as_ptr() as *mut JsObject);
+224 -112
View File
@@ -1,14 +1,14 @@
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::JsObject;
use oxide_types::object::{JsObject, MAX_DENSE_PROPS};
use oxide_types::value::JsValue;
use crate::coercion;
use crate::native::{NativeFn, NativeResult};
use crate::vm::{native_fn_ptr_to_fn, Vm};
use crate::native::NativeResult;
use crate::vm::Vm;
macro_rules! array_ptr {
($vm:expr, $args:expr) => {{
match get_this_array_ref($vm.reg($args[0])) {
match get_this_array_ref($vm, $vm.reg($args[0])) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
}
@@ -23,54 +23,94 @@ macro_rules! array_ptr_len {
}};
}
fn get_this_array_ref(val: JsValue) -> Result<*mut JsObject, JsValue> {
fn array_type_error(vm: &mut Vm, msg: &str) -> JsValue {
crate::builtins::error::create_type_error(vm, msg)
}
fn get_this_array_ref(vm: &mut Vm, val: JsValue) -> Result<*mut JsObject, JsValue> {
if !val.is_object() {
return Err(JsValue::undefined());
return Err(array_type_error(vm, "Array method called on incompatible receiver"));
}
let ptr = val.as_js_object_ptr();
if ptr.is_null() {
return Err(JsValue::undefined());
return Err(array_type_error(vm, "Array method called on incompatible receiver"));
}
let obj = unsafe { &*ptr };
if !obj.is_array() {
return Err(JsValue::undefined());
return Err(array_type_error(vm, "Array method called on incompatible receiver"));
}
Ok(ptr)
}
fn require_callback(vm: &mut Vm, callback_val: JsValue) -> Result<JsValue, JsValue> {
if !callback_val.is_object() {
return Err(array_type_error(vm, "callback is not a function"));
}
let ptr = callback_val.as_js_object_ptr();
if ptr.is_null() || !unsafe { &*ptr }.is_function() {
return Err(array_type_error(vm, "callback is not a function"));
}
Ok(callback_val)
}
fn create_new_array(vm: &mut Vm, n: usize) -> *mut JsObject {
let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
vm.epoch()
.alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump()))
vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(proto),
n.min(MAX_DENSE_PROPS),
vm.epoch().bump(),
))
}
fn array_length_arg(vm: &mut Vm, value: JsValue) -> Result<usize, JsValue> {
let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref());
if !n.is_finite() || n < 0.0 || n.fract() != 0.0 || n > MAX_DENSE_PROPS as f64 {
return Err(crate::builtins::error::create_range_error(vm, "Invalid array length"));
}
Ok(n as usize)
}
fn invoke_native_callback(vm: &mut Vm, callback_val: JsValue, this_val: JsValue, cb_args: &[JsValue]) -> NativeResult {
if !callback_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::builtins::error::create_type_error(vm, "callback is not a function"));
}
let cb_ptr = callback_val.as_js_object_ptr();
if cb_ptr.is_null() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(crate::builtins::error::create_type_error(vm, "callback is not a function"));
}
let cb = unsafe { &*cb_ptr };
if !cb.is_function() || cb.native_fn().is_none() {
return NativeResult::Err(JsValue::undefined());
if !cb.is_function() {
return NativeResult::Err(crate::builtins::error::create_type_error(vm, "callback is not a function"));
}
// SAFETY: native_fn was set via set_native_fn with a valid NativeFn pointer;
// native_fn_ptr_to_fn is the single coercion point for NativeFnPtr → NativeFn.
let func: NativeFn = unsafe { native_fn_ptr_to_fn(cb.native_fn().unwrap()) };
let base = 240u8;
let n = cb_args.len().min(15);
let mut args_buf = [0u8; 17];
vm.set_reg(base, this_val);
args_buf[0] = base;
for i in 0..n {
vm.set_reg(base + 1 + i as u8, cb_args[i]);
args_buf[i + 1] = base + 1 + i as u8;
match vm.call_function_sync(callback_val, this_val, cb_args) {
Ok(value) => NativeResult::Ok(value),
Err(err) => NativeResult::Err(callback_error_from_text(vm, &err)),
}
}
func(vm, &args_buf[..n + 1])
fn callback_error_from_text(vm: &mut Vm, err: &str) -> JsValue {
let err = err.strip_prefix("uncaught ").unwrap_or(err);
if let Some(msg) = err.strip_prefix("TypeError: ") {
return crate::builtins::error::create_type_error(vm, msg);
}
if let Some(msg) = err.strip_prefix("ReferenceError: ") {
return crate::builtins::error::create_reference_error(vm, msg);
}
if let Some(msg) = err.strip_prefix("RangeError: ") {
return crate::builtins::error::create_range_error(vm, msg);
}
if let Some(msg) = err.strip_prefix("SyntaxError: ") {
return crate::builtins::error::create_syntax_error(vm, msg);
}
if let Some(msg) = err.strip_prefix("Error: ") {
return crate::builtins::error::create_error(vm, msg);
}
crate::builtins::error::create_error(vm, err)
}
fn unexpected_tail_call_error(vm: &mut Vm) -> NativeResult {
NativeResult::Err(crate::builtins::error::create_type_error(vm, "unexpected tail call in array callback"))
}
pub fn array_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
@@ -79,26 +119,18 @@ pub fn array_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
if args.len() == 2 {
let val = vm.reg(args[1]);
if val.is_int() {
let n = val.as_int().max(0) as usize;
let arr = vm
.epoch()
.alloc(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n, vm.epoch().bump()));
return NativeResult::Ok(JsValue::from_js_object(arr));
}
if val.is_double() {
let n = val.as_double() as usize;
let arr = vm
.epoch()
.alloc(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n, vm.epoch().bump()));
if val.is_int() || val.is_double() {
let n = match array_length_arg(vm, val) {
Ok(n) => n,
Err(err) => return NativeResult::Err(err),
};
let arr = vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n, vm.epoch().bump()));
return NativeResult::Ok(JsValue::from_js_object(arr));
}
}
let n_elems = if args.len() > 1 { args.len() - 1 } else { 0 };
let arr = vm
.epoch()
.alloc(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n_elems, vm.epoch().bump()));
let arr = vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, proto_val, n_elems, vm.epoch().bump()));
for i in 0..n_elems {
unsafe {
(*arr).set_prop_at(i, vm.reg(args[1 + i]));
@@ -360,22 +392,34 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult {
let arr = unsafe { &*arr_ptr };
let n = arr.prop_count() as usize;
let depth = if args.len() > 1 {
(coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32).max(1) as usize
let n = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref());
if !n.is_finite() {
vm.kernel_core.config.max_call_depth
} else {
(n as i32).max(1) as usize
}
} else {
1
};
}
.min(vm.kernel_core.config.max_call_depth);
fn flatten(items: &[JsValue], remaining_depth: usize) -> Vec<JsValue> {
fn flatten(items: &[JsValue], remaining_depth: usize, seen: &mut Vec<*mut JsObject>) -> Vec<JsValue> {
let mut out = Vec::new();
for &v in items {
if remaining_depth > 0 && v.is_object() {
let ptr = v.as_js_object_ptr();
if !ptr.is_null() {
if seen.iter().any(|seen_ptr| std::ptr::eq(*seen_ptr, ptr)) {
out.push(v);
continue;
}
let o = unsafe { &*ptr };
if o.is_array() {
seen.push(ptr);
let on = o.prop_count() as usize;
let sub: Vec<JsValue> = (0..on).map(|i| o.get_prop_at(i)).collect();
let flat = flatten(&sub, remaining_depth - 1);
let flat = flatten(&sub, remaining_depth - 1, seen);
seen.pop();
out.extend(flat);
continue;
}
@@ -387,7 +431,8 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult {
}
let all: Vec<JsValue> = (0..n).map(|i| arr.get_prop_at(i)).collect();
let flat = flatten(&all, depth);
let mut seen = vec![arr_ptr];
let flat = flatten(&all, depth, &mut seen);
let new_arr = create_new_array(vm, flat.len());
unsafe {
for (i, val) in flat.iter().enumerate() {
@@ -401,16 +446,20 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_for_each(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
for i in 0..n {
let elem = unsafe { (*arr_ptr).get_prop_at(i) };
let _ = invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]);
match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) {
NativeResult::Ok(_) => {}
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(JsValue::undefined())
}
@@ -418,12 +467,12 @@ pub fn array_for_each(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_map(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
let new_arr = create_new_array(vm, n);
for i in 0..n {
@@ -432,7 +481,8 @@ pub fn array_map(vm: &mut Vm, args: &[u8]) -> NativeResult {
NativeResult::Ok(mapped) => unsafe {
(*new_arr).set_prop_at(i, mapped);
},
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
unsafe {
@@ -444,12 +494,12 @@ pub fn array_map(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
let mut kept: Vec<JsValue> = Vec::new();
for i in 0..n {
@@ -461,7 +511,8 @@ pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult {
kept.push(elem);
}
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
let new_arr = create_new_array(vm, kept.len());
@@ -477,15 +528,15 @@ pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_reduce(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if n == 0 && args.len() < 3 {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value"));
}
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let has_initial = args.len() > 2;
let mut accumulator;
let start_idx;
@@ -506,7 +557,8 @@ pub fn array_reduce(vm: &mut Vm, args: &[u8]) -> NativeResult {
&[accumulator, elem, JsValue::int(i as i32), vm.reg(args[0])],
) {
NativeResult::Ok(result) => accumulator = result,
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(accumulator)
@@ -515,12 +567,12 @@ pub fn array_reduce(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
for i in 0..n {
let elem = unsafe { (*arr_ptr).get_prop_at(i) };
@@ -531,7 +583,8 @@ pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult {
return NativeResult::Ok(elem);
}
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(JsValue::undefined())
@@ -540,12 +593,12 @@ pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false));
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Ok(JsValue::bool(false));
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
for i in 0..n {
let elem = unsafe { (*arr_ptr).get_prop_at(i) };
@@ -556,7 +609,8 @@ pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult {
return NativeResult::Ok(JsValue::bool(true));
}
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(JsValue::bool(false))
@@ -565,12 +619,12 @@ pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false));
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Ok(JsValue::bool(false));
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
for i in 0..n {
let elem = unsafe { (*arr_ptr).get_prop_at(i) };
@@ -581,7 +635,8 @@ pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult {
return NativeResult::Ok(JsValue::bool(false));
}
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(JsValue::bool(true))
@@ -590,12 +645,12 @@ pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_flat_map(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
}
let callback_val = vm.reg(args[1]);
if !callback_val.is_object() {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
let mut flat: Vec<JsValue> = Vec::new();
for i in 0..n {
@@ -617,7 +672,8 @@ pub fn array_flat_map(vm: &mut Vm, args: &[u8]) -> NativeResult {
}
flat.push(result);
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
let new_arr = create_new_array(vm, flat.len());
@@ -753,9 +809,12 @@ pub fn array_last_index_of(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_find_index(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Ok(JsValue::int(-1));
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = vm.reg(args[1]);
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
for i in 0..n {
let elem = unsafe { (*arr_ptr).get_prop_at(i) };
@@ -765,7 +824,8 @@ pub fn array_find_index(vm: &mut Vm, args: &[u8]) -> NativeResult {
return NativeResult::Ok(JsValue::int(i as i32));
}
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(JsValue::int(-1))
@@ -777,9 +837,12 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult {
(arr_ptr, len as i32)
};
if args.len() < 2 {
return NativeResult::Ok(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = vm.reg(args[1]);
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
for i in (0..n).rev() {
let elem = unsafe { (*arr_ptr).get_prop_at(i) };
@@ -789,7 +852,8 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult {
return NativeResult::Ok(elem);
}
}
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(JsValue::undefined())
@@ -798,14 +862,17 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn array_reduce_right(vm: &mut Vm, args: &[u8]) -> NativeResult {
let (arr_ptr, n) = array_ptr_len!(vm, args);
if args.len() < 2 {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "callback is not a function"));
}
let callback_val = vm.reg(args[1]);
let callback_val = match require_callback(vm, vm.reg(args[1])) {
Ok(callback) => callback,
Err(err) => return NativeResult::Err(err),
};
let (mut acc, start_idx): (JsValue, i32) = if args.len() > 2 {
(vm.reg(args[2]), n as i32 - 1)
} else {
if n == 0 {
return NativeResult::Err(JsValue::undefined());
return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value"));
}
(unsafe { (*arr_ptr).get_prop_at(n - 1) }, n as i32 - 2)
};
@@ -818,26 +885,71 @@ pub fn array_reduce_right(vm: &mut Vm, args: &[u8]) -> NativeResult {
&[acc, elem, JsValue::int(i), vm.reg(args[0])],
) {
NativeResult::Ok(r) => acc = r,
NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()),
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm),
}
}
NativeResult::Ok(acc)
}
pub fn array_sort(vm: &mut Vm, _args: &[u8]) -> NativeResult {
let arr_ptr = array_ptr!(vm, _args);
let arr = unsafe { &mut *arr_ptr };
let len = arr.prop_count() as usize;
let mut vals: Vec<JsValue> = (0..len).map(|i| arr.get_prop_at(i)).collect();
pub fn array_sort(vm: &mut Vm, args: &[u8]) -> NativeResult {
let arr_ptr = array_ptr!(vm, args);
let len = unsafe { (*arr_ptr).prop_count() as usize };
let mut vals: Vec<JsValue> = (0..len).map(|i| unsafe { (*arr_ptr).get_prop_at(i) }).collect();
let comparator = if args.len() > 1 {
let candidate = vm.reg(args[1]);
if candidate.is_undefined() {
None
} else {
match require_callback(vm, candidate) {
Ok(callback) => Some(callback),
Err(err) => return NativeResult::Err(err),
}
}
} else {
None
};
let mut sort_error = None;
vals.sort_by(|a, b| {
if sort_error.is_some() {
return std::cmp::Ordering::Equal;
}
if let Some(callback) = comparator {
match invoke_native_callback(vm, callback, JsValue::undefined(), &[*a, *b]) {
NativeResult::Ok(result) => {
let n = coercion::to_number(result, vm.kernel_core().string_forge().as_ref());
if n.is_nan() || n == 0.0 {
std::cmp::Ordering::Equal
} else if n < 0.0 {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
}
NativeResult::Err(err) => {
sort_error = Some(err);
std::cmp::Ordering::Equal
}
NativeResult::TailCall { .. } => {
sort_error =
Some(crate::builtins::error::create_type_error(vm, "unexpected tail call in array callback"));
std::cmp::Ordering::Equal
}
}
} else {
let sa = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *a);
let sb = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *b);
sa.cmp(&sb)
}
});
if let Some(err) = sort_error {
return NativeResult::Err(err);
}
let arr = unsafe { &mut *arr_ptr };
for (i, &v) in vals.iter().enumerate() {
arr.set_prop_at(i, v);
}
NativeResult::Ok(vm.reg(_args[0]))
NativeResult::Ok(vm.reg(args[0]))
}
pub fn array_values(vm: &mut Vm, args: &[u8]) -> NativeResult {
+35 -2
View File
@@ -6,7 +6,7 @@ use crate::coercion;
use crate::native::NativeResult;
use crate::vm::Vm;
const MAX_ARRAY_BUFFER_LENGTH: usize = 1 << 30;
pub(crate) const MAX_ARRAY_BUFFER_LENGTH: usize = 1 << 30;
macro_rules! native_try {
($expr:expr) => {
@@ -66,7 +66,40 @@ pub(crate) fn new_array_buffer(vm: &mut Vm, data: Vec<u8>) -> *mut JsObject {
JsValue::int(len as i32),
PropAttributes::new(false, false, false),
);
vm.epoch().alloc(obj)
vm.alloc_object(obj)
}
fn array_buffer_vec_ptr(obj: &JsObject) -> Option<*mut Vec<u8>> {
if !obj.is_array_buffer_obj() {
return None;
}
obj.native_fn().map(|ptr| ptr.as_ptr() as *mut Vec<u8>)
}
pub(crate) fn clone_array_buffer_native(old_obj: &JsObject, new_obj: &mut JsObject) {
let Some(ptr) = array_buffer_vec_ptr(old_obj) else {
return;
};
if ptr.is_null() {
return;
}
let cloned = unsafe { (&*ptr).clone() };
let cloned_ptr = Box::into_raw(Box::new(cloned));
new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned_ptr as *const ()) }));
}
pub(crate) fn drop_array_buffer_native(obj: &mut JsObject) -> u64 {
let Some(data_ptr) = array_buffer_vec_ptr(obj) else {
return 0;
};
if data_ptr.is_null() {
return 0;
}
// SAFETY: ArrayBuffer stores `Box<Vec<u8>>` in native_fn.
let data = unsafe { Box::from_raw(data_ptr) };
let bytes = std::mem::size_of::<Vec<u8>>() + data.capacity();
obj.set_native_fn(None);
bytes as u64
}
pub(crate) fn array_buffer_data_ptr(vm: &mut Vm, this_val: JsValue) -> Result<*mut Vec<u8>, JsValue> {
+10
View File
@@ -57,6 +57,9 @@ pub fn boolean_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn boolean_prototype_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if this_val.is_bool() {
return NativeResult::Ok(this_val);
}
if !this_val.is_object() {
return NativeResult::Err(crate::builtins::error::create_type_error(
vm,
@@ -76,6 +79,13 @@ pub fn boolean_prototype_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn boolean_prototype_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if this_val.is_bool() {
return if this_val.as_bool() {
NativeResult::Ok(vm.intern("true"))
} else {
NativeResult::Ok(vm.intern("false"))
};
}
if !this_val.is_object() {
return NativeResult::Err(crate::builtins::error::create_type_error(
vm,
+67 -2
View File
@@ -8,7 +8,7 @@ use crate::native::NativeResult;
use crate::vm::Vm;
#[derive(Clone, Copy)]
struct DataViewData {
pub(crate) struct DataViewData {
buffer: JsValue,
byte_offset: usize,
byte_length: usize,
@@ -177,7 +177,72 @@ pub fn data_view_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
JsValue::int(byte_length as i32),
PropAttributes::new(false, false, false),
);
NativeResult::Ok(JsValue::from_js_object(vm.epoch().alloc(obj)))
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
fn data_view_data_ptr(obj: &JsObject) -> Option<*mut DataViewData> {
if !obj.is_data_view_obj() {
return None;
}
obj.native_fn().map(|ptr| ptr.as_ptr() as *mut DataViewData)
}
pub(crate) fn data_view_native_edges(obj: &JsObject) -> Vec<JsValue> {
let Some(ptr) = data_view_data_ptr(obj) else {
return Vec::new();
};
if ptr.is_null() {
return Vec::new();
}
let data = unsafe { *ptr };
if data.buffer.is_object() {
vec![data.buffer]
} else {
Vec::new()
}
}
pub(crate) fn clone_data_view_native_with_rewrite<F>(old_obj: &JsObject, new_obj: &mut JsObject, mut rewrite: F)
where
F: FnMut(JsValue) -> JsValue,
{
let Some(ptr) = data_view_data_ptr(old_obj) else {
return;
};
if ptr.is_null() {
return;
}
let mut data = unsafe { *ptr };
data.buffer = rewrite(data.buffer);
let cloned = Box::into_raw(Box::new(data));
new_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(cloned as *const ()) }));
}
pub(crate) fn rewrite_data_view_native<F>(obj: &mut JsObject, mut rewrite: F)
where
F: FnMut(JsValue) -> JsValue,
{
let Some(ptr) = data_view_data_ptr(obj) else {
return;
};
if ptr.is_null() {
return;
}
unsafe {
(*ptr).buffer = rewrite((*ptr).buffer);
}
}
pub(crate) fn drop_data_view_native(obj: &mut JsObject) -> u64 {
let Some(ptr) = data_view_data_ptr(obj) else {
return 0;
};
if ptr.is_null() {
return 0;
}
unsafe { drop(Box::from_raw(ptr)) };
obj.set_native_fn(None);
std::mem::size_of::<DataViewData>() as u64
}
pub fn data_view_get_int8(vm: &mut Vm, args: &[u8]) -> NativeResult {
+1 -1
View File
@@ -201,7 +201,7 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
obj.type_tag = JsObject::OBJ_TYPE_DATE;
obj.set_prop_at(0, JsValue::float(timestamp));
let ptr = vm.epoch().alloc(obj);
let ptr = vm.alloc_object(obj);
NativeResult::Ok(JsValue::from_js_object(ptr))
}
+20 -13
View File
@@ -36,34 +36,41 @@ fn create_error_object(vm: &mut Vm, error_proto_ptr: *mut JsObject, name: &str,
JsValue::from_js_object(obj)
}
pub fn create_kind_error(vm: &mut Vm, kind: &str, msg: &str) -> JsValue {
let proto_ptr = match kind {
"TypeError" => P::as_ptr(&vm.session().builtin_world().type_error_proto) as *mut JsObject,
"RangeError" => P::as_ptr(&vm.session().builtin_world().range_error_proto) as *mut JsObject,
"ReferenceError" => P::as_ptr(&vm.session().builtin_world().reference_error_proto) as *mut JsObject,
"SyntaxError" => P::as_ptr(&vm.session().builtin_world().syntax_error_proto) as *mut JsObject,
"URIError" => P::as_ptr(&vm.session().builtin_world().uri_error_proto) as *mut JsObject,
"EvalError" => P::as_ptr(&vm.session().builtin_world().eval_error_proto) as *mut JsObject,
_ => P::as_ptr(&vm.session().builtin_world().error_proto) as *mut JsObject,
};
create_error_object(vm, proto_ptr, kind, msg)
}
pub fn create_type_error(vm: &mut Vm, msg: &str) -> JsValue {
let proto_ptr = P::as_ptr(&vm.session().builtin_world().type_error_proto) as *mut JsObject;
create_error_object(vm, proto_ptr, "TypeError", msg)
create_kind_error(vm, "TypeError", msg)
}
pub fn create_error(vm: &mut Vm, msg: &str) -> JsValue {
let proto_ptr = P::as_ptr(&vm.session().builtin_world().error_proto) as *mut JsObject;
create_error_object(vm, proto_ptr, "Error", msg)
create_kind_error(vm, "Error", msg)
}
pub fn create_reference_error(vm: &mut Vm, msg: &str) -> JsValue {
let proto_ptr = P::as_ptr(&vm.session().builtin_world().reference_error_proto) as *mut JsObject;
create_error_object(vm, proto_ptr, "ReferenceError", msg)
create_kind_error(vm, "ReferenceError", msg)
}
pub fn create_range_error(vm: &mut Vm, msg: &str) -> JsValue {
let proto_ptr = P::as_ptr(&vm.session().builtin_world().range_error_proto) as *mut JsObject;
create_error_object(vm, proto_ptr, "RangeError", msg)
create_kind_error(vm, "RangeError", msg)
}
pub fn create_syntax_error(vm: &mut Vm, msg: &str) -> JsValue {
let proto_ptr = P::as_ptr(&vm.session().builtin_world().syntax_error_proto) as *mut JsObject;
create_error_object(vm, proto_ptr, "SyntaxError", msg)
create_kind_error(vm, "SyntaxError", msg)
}
pub fn create_uri_error(vm: &mut Vm, msg: &str) -> JsValue {
let proto_ptr = P::as_ptr(&vm.session().builtin_world().uri_error_proto) as *mut JsObject;
create_error_object(vm, proto_ptr, "URIError", msg)
create_kind_error(vm, "URIError", msg)
}
fn get_msg(vm: &mut Vm, args: &[u8]) -> String {
@@ -137,7 +144,7 @@ pub fn error_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult {
} else {
("Error".to_string(), String::new())
};
let result = if msg.is_empty() { name } else { format!("{}: {}", name, msg) };
let result = crate::vm::format_error_message(&name, &msg);
let si = vm.kernel_core().string_forge().intern(&result).0;
NativeResult::Ok(JsValue::string(si, 0))
}
+1 -1
View File
@@ -91,7 +91,7 @@ pub fn function_bind(vm: &mut Vm, args: &[u8]) -> NativeResult {
}
let bound_this = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let wrapper = vm.epoch().alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let wrapper = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
unsafe {
(*wrapper).set_function(true);
// SAFETY: bind_dispatcher is a NativeFn fn-item; valid to store as NativeFnPtr.
+1 -1
View File
@@ -156,7 +156,7 @@ fn make_native_function(vm: &mut Vm, name: &str, native_fn: *const (), arg_count
// SAFETY: native_fn is provided from a NativeFn fn item.
func.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(native_fn) }));
func.set_native_arg_count(arg_count);
let func = vm.epoch().alloc(func);
let func = vm.alloc_object(func);
let name_si = vm.kernel_core().string_forge().intern("name").0;
let value = vm.intern(name);
let func_ref = unsafe { &mut *func };
+2 -2
View File
@@ -48,7 +48,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue {
serde_json::Value::Array(arr) => {
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let n = arr.len();
let array_obj = vm.epoch().alloc(JsObject::new_array(
let array_obj = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
n,
@@ -72,7 +72,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue {
obj.set_shape_id(new_shape);
obj.ensure_hash_props().push(jsv);
}
let obj_ptr = vm.epoch().alloc(obj);
let obj_ptr = vm.alloc_object(obj);
JsValue::from_js_object(obj_ptr)
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ fn alloc_map(vm: &mut Vm) -> *mut JsObject {
obj.set_map(true);
let inner = new_map_inner();
obj.set_native_data(inner as *mut u8);
vm.epoch().alloc(obj)
vm.alloc_object(obj)
}
pub(crate) fn map_native_edges(obj: &JsObject) -> Vec<JsValue> {
+44 -1
View File
@@ -10,6 +10,37 @@ pub fn number_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
} else {
0.0
};
let number_proto = vm.session().builtin_world().number_proto.as_ptr() as *mut oxide_types::object::JsObject;
let is_ctor = if let Some(this_reg) = args.first().copied() {
let this_val = vm.reg(this_reg);
if this_val.is_object() {
let ptr = this_val.as_js_object_ptr();
if ptr.is_null() {
false
} else {
let proto_ptr = unsafe { (*ptr).proto().as_js_object_ptr() };
!proto_ptr.is_null() && std::ptr::eq(proto_ptr, number_proto)
}
} else {
false
}
} else {
false
};
if is_ctor {
let this_val = vm.reg(args[0]);
let obj = unsafe { &mut *this_val.as_js_object_ptr() };
obj.type_tag = oxide_types::object::JsObject::OBJ_TYPE_NUMBER_OBJ;
let boxed = if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 {
JsValue::int(n as i32)
} else {
JsValue::float(n)
};
obj.set_prop_at(0, boxed);
return NativeResult::Ok(this_val);
}
if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 {
NativeResult::Ok(JsValue::int(n as i32))
} else {
@@ -222,5 +253,17 @@ pub fn number_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult {
if this_val.is_int() || this_val.is_double() {
return NativeResult::Ok(this_val);
}
NativeResult::Ok(JsValue::float(f64::NAN))
if this_val.is_object() {
let ptr = this_val.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_number_obj() {
return NativeResult::Ok(obj.get_prop_at(0));
}
}
}
NativeResult::Err(crate::builtins::error::create_type_error(
vm,
"Number.prototype.valueOf called on incompatible receiver",
))
}
+9 -9
View File
@@ -39,7 +39,7 @@ pub(crate) fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u32)> {
}
pub fn object_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult {
let obj = vm.epoch().alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
NativeResult::Ok(JsValue::from_js_object(obj))
}
@@ -68,7 +68,7 @@ pub fn object_keys(vm: &mut Vm, args: &[u8]) -> NativeResult {
let n = key_names.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.epoch().alloc(JsObject::new_array(
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
n,
@@ -94,7 +94,7 @@ pub fn object_create(vm: &mut Vm, args: &[u8]) -> NativeResult {
if !proto_val.is_null() && !proto_val.is_object() {
return NativeResult::Ok(JsValue::undefined());
}
let obj = vm.epoch().alloc(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val));
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val));
NativeResult::Ok(JsValue::from_js_object(obj))
}
@@ -105,7 +105,7 @@ pub fn object_assign(vm: &mut Vm, args: &[u8]) -> NativeResult {
let target_val = vm.reg(args[1]);
let target_val = match coercion::to_object(target_val, vm) {
Ok(val) => val,
Err(msg) => return NativeResult::Err(crate::builtins::error::create_type_error(vm, msg)),
Err(msg) => return NativeResult::Err(crate::builtins::error::create_type_error(vm, &msg)),
};
let target_ptr = target_val.as_js_object_ptr();
if target_ptr.is_null() {
@@ -412,7 +412,7 @@ pub fn object_get_own_property_names(vm: &mut Vm, args: &[u8]) -> NativeResult {
let n = key_names.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.epoch().alloc(JsObject::new_array(
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
n,
@@ -450,7 +450,7 @@ pub fn object_from_entries(vm: &mut Vm, args: &[u8]) -> NativeResult {
if !entries.is_object() {
return NativeResult::Ok(JsValue::null());
}
let obj_ptr = vm.epoch().alloc(JsObject::new_empty(
let obj_ptr = vm.alloc_object(JsObject::new_empty(
EMPTY_SHAPE_ID,
JsValue::from_js_object(vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject),
));
@@ -517,7 +517,7 @@ pub fn object_entries(vm: &mut Vm, args: &[u8]) -> NativeResult {
let keys = walk_own_keys(vm, obj);
let n = keys.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.epoch().alloc(JsObject::new_array(
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
n,
@@ -527,7 +527,7 @@ pub fn object_entries(vm: &mut Vm, args: &[u8]) -> NativeResult {
let key_str = vm.kernel_core().string_forge().lookup(*si).unwrap_or_default();
let key_val = vm.intern(&key_str);
let val = obj.get_prop_at(*offset);
let pair = vm.epoch().alloc(JsObject::new_array(
let pair = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
2,
@@ -552,7 +552,7 @@ pub fn object_values(vm: &mut Vm, args: &[u8]) -> NativeResult {
let keys = walk_own_keys(vm, obj);
let n = keys.len();
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.epoch().alloc(JsObject::new_array(
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
n,
+1 -1
View File
@@ -221,7 +221,7 @@ fn own_field(vm: &Vm, obj: &JsObject, prop_si: u32) -> Option<JsValue> {
fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue {
let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.epoch().alloc(JsObject::new_array(
let arr = vm.alloc_object(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(proto),
parts.len(),
+33 -27
View File
@@ -6,28 +6,22 @@ use crate::coercion;
use crate::native::NativeResult;
use crate::vm::Vm;
fn get_regexp_ptr(vm: &Vm, args: &[u8]) -> Result<*mut JsObject, JsValue> {
fn get_regexp_ptr(vm: &mut Vm, args: &[u8]) -> Result<*mut JsObject, JsValue> {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
return Err(JsValue::string(
vm.kernel_core()
.string_forge()
.intern("TypeError: RegExp.prototype method called on non-object")
.0,
0,
return Err(crate::builtins::error::create_type_error(
vm,
"RegExp.prototype method called on non-object",
));
}
let ptr = this_val.as_js_object_ptr();
if ptr.is_null() {
return Err(JsValue::string(vm.kernel_core().string_forge().intern("TypeError: null object").0, 0));
return Err(crate::builtins::error::create_type_error(vm, "null object"));
}
if !unsafe { &*ptr }.is_regexp_obj() {
return Err(JsValue::string(
vm.kernel_core()
.string_forge()
.intern("TypeError: RegExp.prototype method called on non-RegExp object")
.0,
0,
return Err(crate::builtins::error::create_type_error(
vm,
"RegExp.prototype method called on non-RegExp object",
));
}
Ok(ptr)
@@ -103,12 +97,14 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
// SAFETY: re_ptr is a Box<regex::Regex> pointer stored by the constructor via
// NativeFnPtr::from_raw(re_ptr as *const ()). RegExp objects repurpose the native_fn
// field to hold the compiled Regex — not a NativeFn pointer. Valid for the object's
// lifetime because the Box is never freed (intentional leak tied to epoch).
// lifetime; VM reset drops the Box through `drop_regexp_native`.
obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(re_ptr as *const ()) }));
}
Err(e) => {
let msg = format!("SyntaxError: Invalid regular expression: {}", e);
return NativeResult::Err(JsValue::string(vm.kernel_core().string_forge().intern(&msg).0, 0));
return NativeResult::Err(crate::builtins::error::create_syntax_error(
vm,
&format!("Invalid regular expression: {e}"),
));
}
}
@@ -123,10 +119,26 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
set_prop(&mut obj, "unicode", JsValue::bool(false), vm);
obj.type_tag = JsObject::OBJ_TYPE_REGEXP;
let obj_ptr = vm.epoch().alloc(obj);
let obj_ptr = vm.alloc_object(obj);
NativeResult::Ok(JsValue::from_js_object(obj_ptr))
}
pub(crate) fn drop_regexp_native(obj: &mut JsObject) -> u64 {
if !obj.is_regexp_obj() {
return 0;
}
let Some(ptr) = obj.native_fn() else {
return 0;
};
let regex_ptr = ptr.as_ptr() as *mut regex::Regex;
if regex_ptr.is_null() {
return 0;
}
unsafe { drop(Box::from_raw(regex_ptr)) };
obj.set_native_fn(None);
std::mem::size_of::<regex::Regex>() as u64
}
pub fn regexp_test(vm: &mut Vm, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
@@ -136,10 +148,7 @@ pub fn regexp_test(vm: &mut Vm, args: &[u8]) -> NativeResult {
let fn_ptr = match re.native_fn() {
None => {
return NativeResult::Err(JsValue::string(
vm.kernel_core().string_forge().intern("TypeError: invalid RegExp").0,
0,
));
return NativeResult::Err(crate::builtins::error::create_type_error(vm, "invalid RegExp"));
}
Some(p) => p,
};
@@ -171,10 +180,7 @@ pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult {
let re = unsafe { &*re_ptr };
match re.native_fn() {
None => {
return NativeResult::Err(JsValue::string(
vm.kernel_core().string_forge().intern("TypeError: invalid RegExp").0,
0,
));
return NativeResult::Err(crate::builtins::error::create_type_error(vm, "invalid RegExp"));
}
Some(p) => p,
}
@@ -231,7 +237,7 @@ pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult {
set_prop_at(re_ptr, 0, JsValue::int(m.end() as i32));
}
let obj_ptr = vm.epoch().alloc(match_obj);
let obj_ptr = vm.alloc_object(match_obj);
NativeResult::Ok(JsValue::from_js_object(obj_ptr))
} else {
if is_global {
+1 -1
View File
@@ -105,7 +105,7 @@ fn alloc_set(vm: &mut Vm) -> *mut JsObject {
obj.set_set(true);
let inner = new_set_inner();
obj.set_native_data(inner as *mut u8);
vm.epoch().alloc(obj)
vm.alloc_object(obj)
}
pub(crate) fn set_native_edges(obj: &JsObject) -> Vec<JsValue> {
+71
View File
@@ -11,6 +11,19 @@ fn this_string(vm: &Vm, args: &[u8]) -> String {
coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[0]))
}
pub fn string_from_char_code(vm: &mut Vm, args: &[u8]) -> NativeResult {
let mut out = String::new();
for &arg_reg in args.iter().skip(1) {
let code = coercion::to_uint32(vm.reg(arg_reg), vm.kernel_core().string_forge().as_ref()) & 0xFFFF;
if let Some(ch) = char::from_u32(code) {
out.push(ch);
} else {
out.push('\u{FFFD}');
}
}
NativeResult::Ok(vm.intern(&out))
}
pub fn string_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if this_val.is_string() {
@@ -31,6 +44,58 @@ pub fn string_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult {
))
}
pub fn string_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
let s = if args.len() > 1 {
coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1]))
} else {
String::new()
};
let str_val = vm.intern(&s);
let string_proto = vm.session().builtin_world().string_proto.as_ptr() as *mut JsObject;
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
let is_ctor = if this_val.is_object() {
let ptr = this_val.as_js_object_ptr();
if ptr.is_null() {
false
} else {
let proto_ptr = unsafe { (*ptr).proto().as_js_object_ptr() };
!proto_ptr.is_null() && std::ptr::eq(proto_ptr, string_proto)
}
} else {
false
};
if !is_ctor {
return NativeResult::Ok(str_val);
}
let obj = unsafe { &mut *this_val.as_js_object_ptr() };
obj.type_tag = JsObject::OBJ_TYPE_STRING_OBJ;
obj.push_prop(str_val);
NativeResult::Ok(this_val)
}
pub fn string_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if this_val.is_string() {
return NativeResult::Ok(this_val);
}
if this_val.is_object() {
let ptr = this_val.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_string_obj() {
return NativeResult::Ok(obj.get_prop_at(0));
}
}
}
NativeResult::Err(crate::builtins::error::create_type_error(
vm,
"String.prototype.toString called on non-String object",
))
}
fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue {
let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
let n = parts.len();
@@ -275,6 +340,9 @@ pub fn string_pad_start(vm: &mut Vm, args: &[u8]) -> NativeResult {
} else {
s_len
};
if target > 10000 {
return NativeResult::Err(crate::builtins::error::create_range_error(vm, "Invalid string length"));
}
let pad = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { " ".to_string() };
if s_len >= target || pad.is_empty() {
return NativeResult::Ok(vm.intern(&s));
@@ -295,6 +363,9 @@ pub fn string_pad_end(vm: &mut Vm, args: &[u8]) -> NativeResult {
} else {
s_len
};
if target > 10000 {
return NativeResult::Err(crate::builtins::error::create_range_error(vm, "Invalid string length"));
}
let pad = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { " ".to_string() };
if s_len >= target || pad.is_empty() {
return NativeResult::Ok(vm.intern(&s));
+1 -5
View File
@@ -1,12 +1,8 @@
use oxide_types::value::JsValue;
use crate::native::NativeResult;
use crate::vm::Vm;
fn stub_error(vm: &mut Vm, name: &str) -> NativeResult {
let msg = format!("TypeError: {} is not implemented", name);
let si = vm.kernel_core().string_forge().intern(&msg).0;
NativeResult::Err(JsValue::string(si, 0))
NativeResult::Err(crate::builtins::error::create_type_error(vm, &format!("{name} is not implemented")))
}
// Architecturally deferred — see 13.5-CONTEXT.md for rationale
+6 -12
View File
@@ -17,12 +17,9 @@ pub fn symbol_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
if !proto_ptr.is_null() {
let sp = vm.session().builtin_world().symbol_proto.as_ptr() as *mut JsObject;
if std::ptr::eq(proto_ptr, sp) {
return NativeResult::Err(JsValue::string(
vm.kernel_core()
.string_forge()
.intern("TypeError: Symbol is not a constructor")
.0,
0,
return NativeResult::Err(crate::builtins::error::create_type_error(
vm,
"Symbol is not a constructor",
));
}
}
@@ -45,12 +42,9 @@ pub fn symbol_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
pub fn symbol_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if !this_val.is_symbol() {
return NativeResult::Err(JsValue::string(
vm.kernel_core()
.string_forge()
.intern("TypeError: Symbol.prototype.toString requires a Symbol")
.0,
0,
return NativeResult::Err(crate::builtins::error::create_type_error(
vm,
"Symbol.prototype.toString requires a Symbol",
));
}

Some files were not shown because too many files have changed in this diff Show More