merge: pull upstream updates
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+20
-2
@@ -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,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",
|
||||
|
||||
@@ -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]
|
||||
@@ -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;
|
||||
@@ -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" }
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 ¶m.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(¶m_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 ¶m.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(¶m_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 ¶m.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(¶m_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) = ¶m.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)
|
||||
}
|
||||
}
|
||||
@@ -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(¶m.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(¶m.pattern, h);
|
||||
} else if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.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(¶m.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(¶m.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(¶m.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);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
@@ -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(¶m.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(¶m.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(¶m.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(_) => {}
|
||||
});
|
||||
}
|
||||
@@ -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(¶m.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(¶m.pattern, h);
|
||||
} else if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user