fix: 修复全局边界安全与内存稳定性问题
This commit is contained in:
@@ -158,6 +158,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)]
|
||||
@@ -222,6 +224,8 @@ impl CompileCtx {
|
||||
after_super_insert: None,
|
||||
after_super_inserted: false,
|
||||
reg_overflow: false,
|
||||
const_overflow: false,
|
||||
jump_overflow: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +284,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);
|
||||
@@ -287,10 +295,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)
|
||||
@@ -652,6 +673,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,
|
||||
@@ -717,6 +744,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,
|
||||
@@ -741,3 +774,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ impl Compiler {
|
||||
let dup_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(OpCode::LOAD_VAR, dup_reg, reg, 0));
|
||||
let offset = (short_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_nullish(dup_reg, offset));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -189,20 +190,24 @@ impl Compiler {
|
||||
LogicalOperator::And => {
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset));
|
||||
}
|
||||
LogicalOperator::Or => {
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset));
|
||||
}
|
||||
LogicalOperator::Coalesce => {
|
||||
let store_pos = ctx.resolve_label(store_label)?;
|
||||
let offset = (store_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_nullish(test_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_nullish(test_reg, offset));
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -337,7 +342,8 @@ impl Compiler {
|
||||
ctx.emit(opcode::encode(OpCode::LOAD_VAR, val_reg, default_reg, 0));
|
||||
let after = ctx.bytecode.len();
|
||||
let offset = after as isize - jump_pos as isize;
|
||||
ctx.bytecode[jump_pos] = opcode::encode_jmp_if_false(eq_reg, offset as i16);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.bytecode[jump_pos] = opcode::encode_jmp_if_false(eq_reg, offset);
|
||||
Ok(val_reg)
|
||||
}
|
||||
|
||||
@@ -426,9 +432,12 @@ impl Compiler {
|
||||
let tmp_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(OpCode::INC_PRE, idx_reg, tmp_reg, tmp_reg));
|
||||
let back = loop_start as isize - ctx.bytecode.len() as isize;
|
||||
ctx.emit(opcode::encode_jmp(back as i16));
|
||||
let back = ctx.checked_jump_offset(back);
|
||||
ctx.emit(opcode::encode_jmp(back));
|
||||
let after = ctx.bytecode.len();
|
||||
ctx.bytecode[end_jmp] = opcode::encode_jmp_if_false(has_reg, (after as isize - end_jmp as isize) as i16);
|
||||
let end_offset = after as isize - end_jmp as isize;
|
||||
let end_offset = ctx.checked_jump_offset(end_offset);
|
||||
ctx.bytecode[end_jmp] = opcode::encode_jmp_if_false(has_reg, end_offset);
|
||||
Ok(rest_reg)
|
||||
}
|
||||
|
||||
@@ -1013,7 +1022,8 @@ impl Compiler {
|
||||
|
||||
let else_pos = ctx.resolve_label(else_label)?;
|
||||
let offset = (else_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16));
|
||||
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();
|
||||
@@ -1028,7 +1038,8 @@ impl Compiler {
|
||||
if has_alt {
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
}
|
||||
|
||||
if let Some(alt) = &ifs.alternate {
|
||||
@@ -1054,13 +1065,15 @@ impl Compiler {
|
||||
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16));
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
|
||||
ctx.pop_loop();
|
||||
Ok(None)
|
||||
@@ -1078,7 +1091,8 @@ impl Compiler {
|
||||
|
||||
let start_pos = ctx.resolve_label(start_label)?;
|
||||
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset));
|
||||
|
||||
ctx.pop_loop();
|
||||
Ok(None)
|
||||
@@ -1112,7 +1126,8 @@ impl Compiler {
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset));
|
||||
}
|
||||
|
||||
self.emit_statement(&fr.body, ctx)?;
|
||||
@@ -1123,7 +1138,8 @@ impl Compiler {
|
||||
|
||||
let start_pos = ctx.resolve_label(start_label)?;
|
||||
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
|
||||
ctx.pop_loop();
|
||||
|
||||
@@ -1145,7 +1161,8 @@ impl Compiler {
|
||||
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));
|
||||
ctx.emit(opcode::encode_jmp(cleanup_jmp_offset as i16));
|
||||
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));
|
||||
@@ -1175,7 +1192,8 @@ impl Compiler {
|
||||
|
||||
let start_pos = ctx.resolve_label(start_label)?;
|
||||
let offset = (start_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
|
||||
ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0));
|
||||
|
||||
@@ -1195,7 +1213,8 @@ impl Compiler {
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp_if_false(has_reg, offset as i16));
|
||||
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));
|
||||
@@ -1222,7 +1241,8 @@ impl Compiler {
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
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_loop();
|
||||
Ok(None)
|
||||
@@ -1245,7 +1265,8 @@ impl Compiler {
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset));
|
||||
ctx.restore_reg_checkpoint(compare_reg_checkpoint);
|
||||
}
|
||||
}
|
||||
@@ -1254,7 +1275,8 @@ impl Compiler {
|
||||
if !has_default {
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
let offset = (end_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
}
|
||||
|
||||
for case in cases.iter() {
|
||||
@@ -1275,14 +1297,16 @@ impl Compiler {
|
||||
};
|
||||
let break_pos = ctx.resolve_label(break_label)?;
|
||||
let offset = (break_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
Ok(None)
|
||||
}
|
||||
Statement::ContinueStatement(_) => {
|
||||
let (_, continue_label) = ctx.current_loop().ok_or("continue outside loop".to_string())?;
|
||||
let continue_pos = ctx.resolve_label(*continue_label)?;
|
||||
let offset = (continue_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
Ok(None)
|
||||
}
|
||||
Statement::FunctionDeclaration(fd) => {
|
||||
@@ -1395,7 +1419,8 @@ impl Compiler {
|
||||
|
||||
if let Some(try_begin_pc) = try_begin_pos {
|
||||
let offset = catch_label_pc as isize - (try_begin_pc as isize);
|
||||
ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset as i16);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset);
|
||||
}
|
||||
|
||||
if let Some(catch) = &ts.handler {
|
||||
@@ -1423,12 +1448,14 @@ impl Compiler {
|
||||
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);
|
||||
ctx.bytecode[fb_pos] = opcode::encode_try_finally_begin(offset as i16);
|
||||
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);
|
||||
ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset as i16);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset);
|
||||
}
|
||||
|
||||
let mut last_finally_result: Option<u8> = None;
|
||||
@@ -1446,7 +1473,8 @@ impl Compiler {
|
||||
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);
|
||||
ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset as i16);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset);
|
||||
}
|
||||
|
||||
let try_end_pc = ctx.bytecode.len();
|
||||
@@ -1589,13 +1617,15 @@ impl Compiler {
|
||||
let offset = (short_pos as isize) - (jump_pos as isize);
|
||||
let instr = ctx.bytecode[jump_pos];
|
||||
let rd = opcode::rd(instr);
|
||||
ctx.bytecode[jump_pos] = opcode::encode_jmp_if_nullish(rd, offset as i16);
|
||||
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);
|
||||
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset as i16);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset);
|
||||
Ok(result_reg)
|
||||
}
|
||||
_ => Err("invalid delete target".into()),
|
||||
@@ -1640,14 +1670,16 @@ impl Compiler {
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
|
||||
let offset = (else_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
ctx.emit(opcode::encode_jmp_if_false(test_reg, offset as i16));
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
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));
|
||||
@@ -1682,9 +1714,11 @@ impl Compiler {
|
||||
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);
|
||||
ctx.bytecode[nullish_jump_pos] = opcode::encode_jmp_if_nullish(dup_reg, offset as i16);
|
||||
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);
|
||||
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset as i16);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.bytecode[end_jump_pos] = opcode::encode_jmp(offset);
|
||||
return Ok(dup_reg);
|
||||
}
|
||||
let skip_label = match log.operator {
|
||||
@@ -1700,10 +1734,12 @@ impl Compiler {
|
||||
let offset = (skip_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
match log.operator {
|
||||
LogicalOperator::And => {
|
||||
ctx.emit(opcode::encode_jmp_if_false(dup_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_false(dup_reg, offset));
|
||||
}
|
||||
LogicalOperator::Or => {
|
||||
ctx.emit(opcode::encode_jmp_if_true(dup_reg, offset as i16));
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_true(dup_reg, offset));
|
||||
}
|
||||
LogicalOperator::Coalesce => unreachable!(),
|
||||
}
|
||||
@@ -1768,7 +1804,8 @@ impl Compiler {
|
||||
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);
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
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)
|
||||
|
||||
@@ -31,6 +31,14 @@ fn compile_simple_expr() {
|
||||
assert_eq!(last, OpCode::HALT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 compile_logical_assignment_vars() {
|
||||
compile_source("let x = 0; x ||= 1;");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -783,6 +783,9 @@ fn run_tests() -> bool {
|
||||
out.push((i, result));
|
||||
|
||||
let done = progress.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if done % 500 == 0 {
|
||||
kernel.sweep_runner_forges();
|
||||
}
|
||||
if done % 500 == 0 || done == total {
|
||||
eprintln!(" progress: {done}/{total} ({}%)", done * 100 / total);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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;
|
||||
@@ -40,8 +40,20 @@ fn get_this_array_ref(val: JsValue) -> Result<*mut JsObject, JsValue> {
|
||||
|
||||
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 {
|
||||
@@ -92,26 +104,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]));
|
||||
@@ -373,22 +377,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;
|
||||
}
|
||||
@@ -400,7 +416,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() {
|
||||
|
||||
@@ -66,7 +66,25 @@ 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)
|
||||
}
|
||||
|
||||
pub(crate) fn drop_array_buffer_native(obj: &mut JsObject) -> u64 {
|
||||
if !obj.is_array_buffer_obj() {
|
||||
return 0;
|
||||
}
|
||||
let Some(ptr) = obj.native_fn() else {
|
||||
return 0;
|
||||
};
|
||||
let data_ptr = ptr.as_ptr() as *mut Vec<u8>;
|
||||
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> {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -103,7 +103,7 @@ 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) => {
|
||||
@@ -123,10 +123,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,
|
||||
@@ -231,7 +247,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> {
|
||||
|
||||
@@ -288,6 +288,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));
|
||||
@@ -308,6 +311,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));
|
||||
|
||||
@@ -117,7 +117,72 @@ fn create_typed_array(
|
||||
JsValue::int(length as i32),
|
||||
PropAttributes::new(false, false, false),
|
||||
);
|
||||
vm.epoch().alloc(obj)
|
||||
vm.alloc_object(obj)
|
||||
}
|
||||
|
||||
fn typed_array_data_ptr(obj: &JsObject) -> Option<*mut TypedArrayData> {
|
||||
if !obj.is_typed_array_obj() {
|
||||
return None;
|
||||
}
|
||||
obj.native_fn().map(|ptr| ptr.as_ptr() as *mut TypedArrayData)
|
||||
}
|
||||
|
||||
pub(crate) fn typed_array_native_edges(obj: &JsObject) -> Vec<JsValue> {
|
||||
let Some(ptr) = typed_array_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_typed_array_native_with_rewrite<F>(old_obj: &JsObject, new_obj: &mut JsObject, mut rewrite: F)
|
||||
where
|
||||
F: FnMut(JsValue) -> JsValue,
|
||||
{
|
||||
let Some(ptr) = typed_array_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_typed_array_native<F>(obj: &mut JsObject, mut rewrite: F)
|
||||
where
|
||||
F: FnMut(JsValue) -> JsValue,
|
||||
{
|
||||
let Some(ptr) = typed_array_data_ptr(obj) else {
|
||||
return;
|
||||
};
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
(*ptr).buffer = rewrite((*ptr).buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn drop_typed_array_native(obj: &mut JsObject) -> u64 {
|
||||
let Some(ptr) = typed_array_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::<TypedArrayData>() as u64
|
||||
}
|
||||
|
||||
pub(crate) fn get_typed_array_data(vm: &mut Vm, this_val: JsValue) -> Result<TypedArrayData, JsValue> {
|
||||
|
||||
@@ -312,7 +312,7 @@ pub fn to_object(val: JsValue, vm: &mut Vm) -> Result<JsValue, &'static str> {
|
||||
(&*vm.object_prototype as *const JsObject as *mut JsObject, JsObject::OBJ_TYPE_PLAIN)
|
||||
};
|
||||
let proto_val = JsValue::from_js_object(proto_ptr);
|
||||
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));
|
||||
let obj_val = JsValue::from_js_object(obj);
|
||||
let obj_ref = unsafe { &mut *obj };
|
||||
obj_ref.type_tag = type_tag;
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Vm {
|
||||
&mut self, obj: &JsObject, callee: JsValue, this_reg: u8, first_arg_reg: u8, arg_count: usize,
|
||||
) -> Result<(), String> {
|
||||
if self.native_call_depth >= self.kernel_core.config.max_call_depth {
|
||||
return Err("RangeError: Maximum call stack size exceeded".into());
|
||||
return self.raise_error_kind("RangeError", "Maximum call stack size exceeded");
|
||||
}
|
||||
let (args_buf, len) = Self::build_native_args(first_arg_reg, arg_count, this_reg);
|
||||
let args_slice = &args_buf[..len];
|
||||
|
||||
@@ -5,7 +5,7 @@ use oxide_types::object::JsObject;
|
||||
use oxide_types::private_key::make_private_name_id;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use crate::vm::Vm;
|
||||
use crate::vm::{Vm, MAX_PROTO_CHAIN_DEPTH};
|
||||
|
||||
#[inline(always)]
|
||||
fn ic_get_hit(obj: &JsObject, shape_id: u32, slot_index: u32) -> Option<JsValue> {
|
||||
@@ -98,7 +98,9 @@ impl Vm {
|
||||
return Some(obj.get_prop_at(pos));
|
||||
}
|
||||
let mut proto = obj.proto();
|
||||
while proto.is_object() {
|
||||
let mut depth = 0usize;
|
||||
while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH {
|
||||
depth += 1;
|
||||
let proto_obj = unsafe { &*proto.as_js_object_ptr() };
|
||||
if let Some(pos) = self.private_slot(proto_obj, private_key) {
|
||||
return Some(proto_obj.get_prop_at(pos));
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::HashMap;
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use crate::builtins::{map, set};
|
||||
use crate::builtins::{data_view, map, set, typed_array};
|
||||
use crate::vm::Vm;
|
||||
|
||||
impl Vm {
|
||||
@@ -50,6 +50,14 @@ impl Vm {
|
||||
set::clone_set_native_with_rewrite(src_ref, dst_ref, |value| {
|
||||
self.promote_value_if_epoch_object(value, forwarding)
|
||||
});
|
||||
} else if src_ref.is_typed_array_obj() {
|
||||
typed_array::clone_typed_array_native_with_rewrite(src_ref, dst_ref, |value| {
|
||||
self.promote_value_if_epoch_object(value, forwarding)
|
||||
});
|
||||
} else if src_ref.is_data_view_obj() {
|
||||
data_view::clone_data_view_native_with_rewrite(src_ref, dst_ref, |value| {
|
||||
self.promote_value_if_epoch_object(value, forwarding)
|
||||
});
|
||||
}
|
||||
dst
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::Instant;
|
||||
use oxide_types::object::{JsObject, PropMetaEntry};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use crate::builtins::{map, set};
|
||||
use crate::builtins::{array_buffer, data_view, map, regexp, set, typed_array};
|
||||
use crate::vm::Vm;
|
||||
|
||||
pub struct SessionGc {
|
||||
@@ -87,6 +87,12 @@ impl SessionGc {
|
||||
if obj.is_set() {
|
||||
edges.extend(set::set_native_edges(obj));
|
||||
}
|
||||
if obj.is_typed_array_obj() {
|
||||
edges.extend(typed_array::typed_array_native_edges(obj));
|
||||
}
|
||||
if obj.is_data_view_obj() {
|
||||
edges.extend(data_view::data_view_native_edges(obj));
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
@@ -141,7 +147,7 @@ impl SessionGc {
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 {
|
||||
pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject, require_session: bool) -> u64 {
|
||||
if obj_ptr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
@@ -150,7 +156,9 @@ impl SessionGc {
|
||||
// `JsObject::ensure_hash_props`/`ensure_prop_meta` and then drop them once here.
|
||||
unsafe {
|
||||
let obj = &mut *obj_ptr;
|
||||
debug_assert!(obj.is_session_epoch());
|
||||
if require_session {
|
||||
debug_assert!(obj.is_session_epoch());
|
||||
}
|
||||
let mut freed_bytes = 0u64;
|
||||
|
||||
let hash_ptr = obj.hash_props_raw() as *mut Vec<JsValue>;
|
||||
@@ -170,11 +178,19 @@ impl SessionGc {
|
||||
|
||||
freed_bytes += map::drop_map_native(obj);
|
||||
freed_bytes += set::drop_set_native(obj);
|
||||
freed_bytes += array_buffer::drop_array_buffer_native(obj);
|
||||
freed_bytes += regexp::drop_regexp_native(obj);
|
||||
freed_bytes += typed_array::drop_typed_array_native(obj);
|
||||
freed_bytes += data_view::drop_data_view_native(obj);
|
||||
|
||||
freed_bytes
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 {
|
||||
Self::drop_object_heap_data(obj_ptr, true)
|
||||
}
|
||||
|
||||
fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 {
|
||||
Self::drop_session_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64
|
||||
}
|
||||
@@ -204,6 +220,10 @@ impl SessionGc {
|
||||
map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value);
|
||||
} else if old_ref.is_set() {
|
||||
set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value);
|
||||
} else if old_ref.is_typed_array_obj() {
|
||||
typed_array::clone_typed_array_native_with_rewrite(old_ref, new_ref, |value| value);
|
||||
} else if old_ref.is_data_view_obj() {
|
||||
data_view::clone_data_view_native_with_rewrite(old_ref, new_ref, |value| value);
|
||||
}
|
||||
forwarding.insert(old_ptr, new_ptr);
|
||||
freed_bytes += Self::drop_session_object_heap_data(old_ptr);
|
||||
@@ -229,6 +249,10 @@ impl SessionGc {
|
||||
map::rewrite_map_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||||
} else if obj.is_set() {
|
||||
set::rewrite_set_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||||
} else if obj.is_typed_array_obj() {
|
||||
typed_array::rewrite_typed_array_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||||
} else if obj.is_data_view_obj() {
|
||||
data_view::rewrite_data_view_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ use oxide_types::mem::{Epoch, P};
|
||||
use oxide_types::object::{JsObject, NativeFnPtr};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
pub(crate) const MAX_PROTO_CHAIN_DEPTH: usize = 1024;
|
||||
|
||||
/// Convert a `NativeFnPtr` to a callable `NativeFn`.
|
||||
///
|
||||
/// # Safety
|
||||
@@ -145,6 +147,7 @@ pub struct Vm {
|
||||
pub epoch: Epoch,
|
||||
pub(crate) session_epoch: bumpalo::Bump,
|
||||
pub(crate) session_gc: SessionGc,
|
||||
pub(crate) epoch_object_ptrs: Vec<*mut JsObject>,
|
||||
pub(crate) session_object_ptrs: Vec<*mut JsObject>,
|
||||
pub(crate) session_bytes_allocated: usize,
|
||||
pub object_prototype: P<JsObject>,
|
||||
@@ -180,6 +183,12 @@ impl Vm {
|
||||
unsafe { (*obj_ptr).is_session_epoch() }
|
||||
}
|
||||
|
||||
pub(crate) fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject {
|
||||
let ptr = self.epoch.alloc(obj);
|
||||
self.epoch_object_ptrs.push(ptr);
|
||||
ptr
|
||||
}
|
||||
|
||||
pub(crate) fn gc_roots(&self) -> Vec<JsValue> {
|
||||
let mut roots = Vec::new();
|
||||
roots.extend_from_slice(&self.regs);
|
||||
@@ -374,7 +383,9 @@ impl Vm {
|
||||
}
|
||||
}
|
||||
let mut proto = obj.proto();
|
||||
while proto.is_object() {
|
||||
let mut depth = 0usize;
|
||||
while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH {
|
||||
depth += 1;
|
||||
let proto_obj = unsafe { &*proto.as_js_object_ptr() };
|
||||
if let Some(pos) = self
|
||||
.kernel_core
|
||||
@@ -429,7 +440,8 @@ impl Vm {
|
||||
|
||||
if let Some(native_fn) = callee_obj.native_fn() {
|
||||
if self.native_call_depth >= self.kernel_core.config.max_call_depth {
|
||||
return Err("RangeError: Maximum call stack size exceeded".into());
|
||||
self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?;
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
let saved_regs = self.regs;
|
||||
let saved_this = self.regs[254];
|
||||
@@ -488,7 +500,7 @@ impl Vm {
|
||||
));
|
||||
}
|
||||
if self.frames.len() >= self.kernel_core.config.max_call_depth {
|
||||
return Err("RangeError: Maximum call stack size exceeded".into());
|
||||
return self.raise_error_kind("RangeError", "Maximum call stack size exceeded");
|
||||
}
|
||||
|
||||
let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone();
|
||||
@@ -940,7 +952,7 @@ impl Vm {
|
||||
let proto_ptr = self.session.builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||
let n = opcode::imm16(instr) as usize;
|
||||
let bump = self.epoch.bump();
|
||||
let obj = self.epoch.alloc(JsObject::new_array(
|
||||
let obj = self.alloc_object(JsObject::new_array(
|
||||
EMPTY_SHAPE_ID,
|
||||
JsValue::from_js_object(proto_ptr),
|
||||
n,
|
||||
@@ -1226,13 +1238,13 @@ mod tests {
|
||||
obj.set_function(true);
|
||||
// SAFETY: f is a NativeFn fn-item; valid to store as NativeFnPtr.
|
||||
obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(f as *const ()) }));
|
||||
JsValue::object(vm.epoch.alloc(obj) as *mut u8)
|
||||
JsValue::object(vm.alloc_object(obj) as *mut u8)
|
||||
}
|
||||
|
||||
fn plain_object(vm: &mut Vm) -> JsValue {
|
||||
let proto = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||
let obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto));
|
||||
JsValue::object(vm.epoch.alloc(obj) as *mut u8)
|
||||
JsValue::object(vm.alloc_object(obj) as *mut u8)
|
||||
}
|
||||
|
||||
fn add_accessor(vm: &mut Vm, obj_val: JsValue, name: &str, get: JsValue, set: JsValue) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::native::{NativeFn, NativeResult};
|
||||
use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm};
|
||||
use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH};
|
||||
use oxide_types::object::{JsObject, PropAttributes};
|
||||
use oxide_types::private_key::is_private_name_key;
|
||||
use oxide_types::value::JsValue;
|
||||
@@ -37,7 +37,7 @@ impl Vm {
|
||||
let arg_count = (ext & 0xFF) as usize;
|
||||
|
||||
let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject;
|
||||
let new_obj = self.epoch.alloc(JsObject::new_empty(
|
||||
let new_obj = self.alloc_object(JsObject::new_empty(
|
||||
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
|
||||
JsValue::from_js_object(proto_ptr),
|
||||
));
|
||||
@@ -225,11 +225,17 @@ impl Vm {
|
||||
};
|
||||
|
||||
let mut proto = unsafe { &*lhs_val.as_js_object_ptr() }.proto();
|
||||
let mut depth = 0usize;
|
||||
loop {
|
||||
if !proto.is_object() {
|
||||
self.regs[rd] = JsValue::bool(false);
|
||||
break;
|
||||
}
|
||||
if depth >= MAX_PROTO_CHAIN_DEPTH {
|
||||
self.regs[rd] = JsValue::bool(false);
|
||||
break;
|
||||
}
|
||||
depth += 1;
|
||||
let proto_ptr = proto.as_js_object_ptr();
|
||||
if proto_ptr == ctor_proto_ptr {
|
||||
self.regs[rd] = JsValue::bool(true);
|
||||
@@ -262,11 +268,16 @@ impl Vm {
|
||||
let mut keys_vec = bumpalo::collections::Vec::new_in(self.epoch.bump());
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut current = obj_val;
|
||||
let mut depth = 0usize;
|
||||
|
||||
loop {
|
||||
if !current.is_object() {
|
||||
break;
|
||||
}
|
||||
if depth >= MAX_PROTO_CHAIN_DEPTH {
|
||||
break;
|
||||
}
|
||||
depth += 1;
|
||||
let cur = unsafe { &*current.as_js_object_ptr() };
|
||||
let mut cursor = Some(cur.shape_id());
|
||||
while let Some(id) = cursor {
|
||||
@@ -425,7 +436,7 @@ impl Vm {
|
||||
let excluded: std::collections::HashSet<&str> = excluded.split('\0').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
let proto_ptr = self.session.builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||
let rest_ptr = self.epoch.alloc(JsObject::new_empty(
|
||||
let rest_ptr = self.alloc_object(JsObject::new_empty(
|
||||
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
|
||||
JsValue::from_js_object(proto_ptr),
|
||||
));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::arc_with_non_send_sync)]
|
||||
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::vm::Vm;
|
||||
use oxide_kernel::kernel::KernelCore;
|
||||
@@ -91,7 +92,19 @@ impl VmPool {
|
||||
};
|
||||
}
|
||||
|
||||
inner = self.condvar.wait(inner).unwrap();
|
||||
let (guard, wait) = self.condvar.wait_timeout(inner, Duration::from_secs(5)).unwrap();
|
||||
inner = guard;
|
||||
if wait.timed_out() {
|
||||
inner.total_count += 1;
|
||||
drop(inner);
|
||||
let vm = Self::new_vm(&self.kernel_core);
|
||||
return VmGuard {
|
||||
vm: Some(vm),
|
||||
pool: Arc::clone(self),
|
||||
dirty: false,
|
||||
interned_strings: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::coercion;
|
||||
use crate::vm::{FrameContinuation, Vm};
|
||||
use crate::vm::{FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH};
|
||||
use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
@@ -20,39 +20,44 @@ impl Vm {
|
||||
&mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option<u8>,
|
||||
) -> Result<JsValue, String> {
|
||||
let length_si = self.kernel_core.string_forge().intern("length").0;
|
||||
if obj.is_array() && prop_name_si == length_si {
|
||||
return Ok(JsValue::int(obj.prop_count() as i32));
|
||||
}
|
||||
if obj.is_array() {
|
||||
if let Some(index) = self.array_index_from_property_key(prop_name_si) {
|
||||
if index < obj.prop_vec_len() as u32 {
|
||||
return Ok(obj.get_prop_at(index));
|
||||
let mut current = Some(obj);
|
||||
let mut depth = 0usize;
|
||||
while let Some(obj) = current {
|
||||
if obj.is_array() && prop_name_si == length_si {
|
||||
return Ok(JsValue::int(obj.prop_count() as i32));
|
||||
}
|
||||
if obj.is_array() {
|
||||
if let Some(index) = self.array_index_from_property_key(prop_name_si) {
|
||||
if index < obj.prop_vec_len() as u32 {
|
||||
return Ok(obj.get_prop_at(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) {
|
||||
if let Some(meta) = obj.prop_meta_at(pos) {
|
||||
if meta.is_accessor {
|
||||
return if meta.get.is_undefined() {
|
||||
Ok(JsValue::undefined())
|
||||
} else if let Some(tr) = target_reg {
|
||||
let getter = meta.get;
|
||||
let pushed = self.push_bytecode_getter_frame(getter, receiver, tr)?;
|
||||
if pushed {
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
Ok(self.regs[tr as usize])
|
||||
} else {
|
||||
self.call_function_sync(meta.get, receiver, &[])
|
||||
};
|
||||
if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) {
|
||||
if let Some(meta) = obj.prop_meta_at(pos) {
|
||||
if meta.is_accessor {
|
||||
return if meta.get.is_undefined() {
|
||||
Ok(JsValue::undefined())
|
||||
} else if let Some(tr) = target_reg {
|
||||
let getter = meta.get;
|
||||
let pushed = self.push_bytecode_getter_frame(getter, receiver, tr)?;
|
||||
if pushed {
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
Ok(self.regs[tr as usize])
|
||||
} else {
|
||||
self.call_function_sync(meta.get, receiver, &[])
|
||||
};
|
||||
}
|
||||
}
|
||||
return Ok(obj.get_prop_at(pos));
|
||||
}
|
||||
return Ok(obj.get_prop_at(pos));
|
||||
}
|
||||
let proto = obj.proto();
|
||||
if proto.is_object() {
|
||||
let proto_obj = unsafe { &*proto.as_js_object_ptr() };
|
||||
return self.ordinary_get_inner(proto_obj, prop_name_si, receiver, target_reg);
|
||||
if depth >= MAX_PROTO_CHAIN_DEPTH {
|
||||
break;
|
||||
}
|
||||
depth += 1;
|
||||
let proto = obj.proto();
|
||||
current = proto.is_object().then(|| unsafe { &*proto.as_js_object_ptr() });
|
||||
}
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
@@ -89,7 +94,9 @@ impl Vm {
|
||||
|
||||
fn inherited_property_meta(&self, obj: &JsObject, prop_name_si: u32) -> Option<PropMetaEntry> {
|
||||
let mut proto = obj.proto();
|
||||
while proto.is_object() {
|
||||
let mut depth = 0usize;
|
||||
while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH {
|
||||
depth += 1;
|
||||
let proto_obj = unsafe { &*proto.as_js_object_ptr() };
|
||||
if let Some(pos) = self.get_own_property_slot(proto_obj, prop_name_si) {
|
||||
return proto_obj.prop_meta_at(pos);
|
||||
|
||||
@@ -21,8 +21,14 @@ impl Vm {
|
||||
));
|
||||
}
|
||||
if self.frames.len() >= self.kernel_core.config.max_call_depth {
|
||||
return Err("RangeError: Maximum call stack size exceeded".into());
|
||||
self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?;
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
if self.native_call_depth >= self.kernel_core.config.max_call_depth {
|
||||
self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?;
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
self.native_call_depth += 1;
|
||||
|
||||
let sub = self.sub_modules[sub_idx].clone();
|
||||
let converted_constants = self.convert_constants(&sub.constants).map_err(String::from)?;
|
||||
@@ -67,6 +73,7 @@ impl Vm {
|
||||
let _ = callee;
|
||||
|
||||
let result = self.dispatch();
|
||||
self.native_call_depth -= 1;
|
||||
|
||||
self.regs = *saved.regs;
|
||||
self.pc = saved.pc;
|
||||
|
||||
@@ -32,6 +32,7 @@ impl Vm {
|
||||
epoch: Epoch::new(),
|
||||
session_epoch: bumpalo::Bump::new(),
|
||||
session_gc: crate::session_gc::SessionGc::new(),
|
||||
epoch_object_ptrs: Vec::new(),
|
||||
session_object_ptrs: Vec::new(),
|
||||
session_bytes_allocated: 0,
|
||||
object_prototype: obj_proto,
|
||||
@@ -73,6 +74,7 @@ impl Vm {
|
||||
epoch: Epoch::new(),
|
||||
session_epoch: bumpalo::Bump::new(),
|
||||
session_gc: crate::session_gc::SessionGc::new(),
|
||||
epoch_object_ptrs: Vec::new(),
|
||||
session_object_ptrs: Vec::new(),
|
||||
session_bytes_allocated: 0,
|
||||
object_prototype: obj_proto,
|
||||
@@ -124,7 +126,9 @@ impl Vm {
|
||||
self.clear_execution_state();
|
||||
self.bytecode.clear();
|
||||
self.constants.clear();
|
||||
self.free_epoch_object_heap_data();
|
||||
self.epoch.reset();
|
||||
self.epoch_object_ptrs.clear();
|
||||
self.session_epoch.reset();
|
||||
self.session_object_ptrs.clear();
|
||||
self.session_bytes_allocated = 0;
|
||||
@@ -137,6 +141,17 @@ impl Vm {
|
||||
self.active_reg_limit = 0;
|
||||
}
|
||||
|
||||
fn free_epoch_object_heap_data(&mut self) {
|
||||
let mut freed = 0u64;
|
||||
for ptr in self.epoch_object_ptrs.drain(..) {
|
||||
freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, false);
|
||||
}
|
||||
if freed > 0 {
|
||||
self.session_gc.total_bytes_freed = self.session_gc.total_bytes_freed.saturating_add(freed);
|
||||
self.session_gc.last_collection_bytes_freed = freed;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_execution_state(&mut self) {
|
||||
// Reset contract:
|
||||
// - Clears register file, pc, frame/iterator stacks, saved execution stacks,
|
||||
@@ -163,7 +178,9 @@ impl Vm {
|
||||
self.maybe_collect_session_gc();
|
||||
self.bytecode.clear();
|
||||
self.constants.clear();
|
||||
self.free_epoch_object_heap_data();
|
||||
self.epoch.reset();
|
||||
self.epoch_object_ptrs.clear();
|
||||
self.interned_strings.clear();
|
||||
self.root_reg_limit = 0;
|
||||
self.active_reg_limit = 0;
|
||||
@@ -194,7 +211,7 @@ impl Vm {
|
||||
obj.set_arrow(true);
|
||||
obj.set_captured_this(self.regs[254]);
|
||||
}
|
||||
let obj_ptr = self.epoch.alloc(obj);
|
||||
let obj_ptr = self.alloc_object(obj);
|
||||
let func_val = JsValue::object(obj_ptr as *mut u8);
|
||||
|
||||
if !is_arrow {
|
||||
@@ -202,6 +219,7 @@ impl Vm {
|
||||
let prototype_obj = self
|
||||
.epoch
|
||||
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto_ptr)));
|
||||
self.epoch_object_ptrs.push(prototype_obj);
|
||||
let prototype_val = JsValue::from_js_object(prototype_obj);
|
||||
|
||||
let constructor_si = self.kernel_core.string_forge().intern("constructor").0;
|
||||
|
||||
@@ -67,7 +67,12 @@ fn regression_rerun_clears_ic_cache() {
|
||||
|
||||
#[test]
|
||||
fn regression_recursion_depth_limit() {
|
||||
assert_eq!(eval("function f(){f()} f()"), "vm error: RangeError: Maximum call stack size exceeded");
|
||||
let result = eval("function f(){f()} f()");
|
||||
assert!(result.contains("RangeError"), "expected RangeError, got: {result}");
|
||||
assert!(
|
||||
result.contains("Maximum call stack size exceeded"),
|
||||
"expected stack limit message, got: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -164,3 +169,47 @@ fn regression_many_declarations_keep_stable_registers() {
|
||||
fn regression_method_call_receiver_survives_register_reuse() {
|
||||
assert_eq!(eval("var o = { x: 41, f: function() { return this.x + 1; } }; o.f()"), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recursive_getter_throws_range_error() {
|
||||
let result = eval("function f(){ return f(); } f()");
|
||||
assert!(
|
||||
result.contains("Maximum call stack size exceeded"),
|
||||
"expected catchable RangeError, got: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_length_range_error() {
|
||||
let result = eval("new Array(4294967295)");
|
||||
assert!(result.contains("Invalid array length"), "expected invalid length, got: {result}");
|
||||
let result = eval("new Array(-1)");
|
||||
assert!(result.contains("Invalid array length"), "expected invalid length, got: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_huge_array_index_bounded() {
|
||||
let result = eval("var a=[]; a[2147483648]=1; 1");
|
||||
assert_eq!(result, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_pad_bounded() {
|
||||
let result = eval("''.padStart(2147483648)");
|
||||
assert!(result.contains("Invalid string length"), "expected invalid string length, got: {result}");
|
||||
assert_eq!(eval("'x'.padStart(5,'0') == '0000x'"), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_typed_array_survives_promote_reset() {
|
||||
assert_eq!(eval("var a=new Int32Array(4); a.fill(7); a.at(0)"), "7");
|
||||
assert_eq!(
|
||||
eval("var b=new ArrayBuffer(8); var d=new DataView(b); d.setInt32(0, 42); d.getInt32(0)"),
|
||||
"42"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flat_infinity_bounded() {
|
||||
assert_eq!(eval("[1,[2,[3,[4]]]].flat(Infinity).length"), "4");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user