diff --git a/.isu/issues.json b/.isu/issues.json index c0a6d78..8e1a73d 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -4045,7 +4045,7 @@ "labels": [], "assigned": [], "author": "piefev", - "state": "open", + "state": "closed", "created_at": "2026-06-04T16:54:22Z" }, { diff --git a/crates/browser/src/module_loader.rs b/crates/browser/src/module_loader.rs index 8dfd17e..eff148c 100644 --- a/crates/browser/src/module_loader.rs +++ b/crates/browser/src/module_loader.rs @@ -24,6 +24,7 @@ use crate::import_map::ImportMap; use crate::loader::{Resource, ResourceLoader, ResourceRequestType}; use std::collections::HashMap; +use std::time::Instant; use we_js::ast::Program; use we_js::module; use we_js::parser::Parser; @@ -50,6 +51,12 @@ struct ModuleRecord { resolve: HashMap, /// Resolved keys of this module's dependencies, in source order. deps: Vec, + /// Source byte length, used only for diagnostics. + source_len: usize, +} + +fn trace_modules_enabled() -> bool { + std::env::var_os("WE_TRACE_MODULES").is_some() } /// Classify and resolve a module specifier against a base URL, consulting the @@ -95,6 +102,7 @@ fn build_graph( import_map: &ImportMap, ) -> HashMap { let mut graph: HashMap = HashMap::new(); + let trace = trace_modules_enabled(); // Worklist of (key, base, source) modules still to be parsed and scanned. let mut pending: Vec<(String, Url, String)> = entries @@ -106,6 +114,8 @@ fn build_graph( if graph.contains_key(&key) { continue; } + let source_len = source.len(); + let parse_started = Instant::now(); let program = match Parser::parse_module(&source) { Ok(p) => p, Err(e) => { @@ -113,6 +123,12 @@ fn build_graph( continue; } }; + if trace { + eprintln!( + "[module-trace] parsed {key} bytes={source_len} parse_ms={}", + parse_started.elapsed().as_millis() + ); + } let mut resolve = HashMap::new(); let mut deps = Vec::new(); @@ -128,6 +144,12 @@ fn build_graph( } if !graph.contains_key(&dep_key) { if let Some(dep_source) = fetch_module_source(loader, &dep_url, origin) { + if trace { + eprintln!( + "[module-trace] fetched dep {dep_key} bytes={}", + dep_source.len() + ); + } pending.push((dep_key, dep_url, dep_source)); } } @@ -152,6 +174,12 @@ fn build_graph( } if !graph.contains_key(&dep_key) { if let Some(dep_source) = fetch_module_source(loader, &dep_url, origin) { + if trace { + eprintln!( + "[module-trace] fetched dynamic dep {dep_key} bytes={}", + dep_source.len() + ); + } pending.push((dep_key, dep_url, dep_source)); } } @@ -164,6 +192,7 @@ fn build_graph( program, resolve, deps, + source_len, }, ); } @@ -226,16 +255,41 @@ pub fn load_and_execute_modules( let graph = build_graph(&entries, loader, origin, import_map); let order = evaluation_order(&entries, &graph); + let trace = trace_modules_enabled(); + + if trace { + eprintln!( + "[module-trace] graph modules={} eval_order={}", + graph.len(), + order.len() + ); + } for key in order { let Some(record) = graph.get(&key) else { continue; }; + let compile_started = Instant::now(); match module::compile_module(&record.program, &key, &record.resolve) { Ok(func) => { + if trace { + eprintln!( + "[module-trace] execute start {key} bytes={} deps={} compile_ms={}", + record.source_len, + record.deps.len(), + compile_started.elapsed().as_millis() + ); + } + let execute_started = Instant::now(); if let Err(e) = vm.execute(&func) { eprintln!("[module] runtime error in {key}: {e}"); } + if trace { + eprintln!( + "[module-trace] execute end {key} execute_ms={}", + execute_started.elapsed().as_millis() + ); + } } Err(e) => { eprintln!("[module] compile error in {key}: {e}"); @@ -382,6 +436,7 @@ mod tests { program: Parser::parse_module("export const x = 1;").unwrap(), resolve: HashMap::new(), deps: Vec::new(), + source_len: "export const x = 1;".len(), }, ); let order = evaluation_order(&entries, &graph); diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index b897103..10a91d8 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -220,7 +220,7 @@ fn array_set(gc: &mut Gc, shapes: &mut ShapeTable, obj: GcRef, idx: pub fn init_builtins(vm: &mut Vm) { // Create Object.prototype first (root of the prototype chain). let obj_proto = vm.gc.alloc(HeapObject::Object(ObjectData::new())); - init_object_prototype(&mut vm.gc, &mut vm.shapes, obj_proto); + vm.object_has_own_property = Some(init_object_prototype(&mut vm.gc, &mut vm.shapes, obj_proto)); // Create Array.prototype (inherits from Object.prototype). let mut arr_proto_data = ObjectData::new(); @@ -396,7 +396,7 @@ pub fn init_builtins(vm: &mut Vm) { // ── Object.prototype ───────────────────────────────────────── -fn init_object_prototype(gc: &mut Gc, shapes: &mut ShapeTable, proto: GcRef) { +fn init_object_prototype(gc: &mut Gc, shapes: &mut ShapeTable, proto: GcRef) -> GcRef { let has_own = make_native(gc, "hasOwnProperty", object_proto_has_own_property); set_builtin_prop( gc, @@ -411,6 +411,8 @@ fn init_object_prototype(gc: &mut Gc, shapes: &mut ShapeTable, proto let value_of = make_native(gc, "valueOf", object_proto_value_of); set_builtin_prop(gc, shapes, proto, "valueOf", Value::Function(value_of)); + + has_own } fn object_proto_has_own_property( diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index 3f6f705..38bf342 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -1487,6 +1487,7 @@ fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Resu compile_stmt(fc, body, result_reg)?; // Increment index: idx = idx + 1. + let continue_target = fc.builder.offset(); // Use a temp register for the constant 1. Since we allocate it // after the loop body, we can't free it with LIFO either — the // saved_next restoration handles cleanup. @@ -1504,7 +1505,7 @@ fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Resu fc.builder.patch_jump(patch); } for patch in ctx.continue_patches { - fc.builder.patch_jump_to(patch, loop_start); + fc.builder.patch_jump_to(patch, continue_target); } // Restore locals/regs — frees all temporaries at once. diff --git a/crates/js/src/jit/compiler.rs b/crates/js/src/jit/compiler.rs index f8bcce2..dff47e0 100644 --- a/crates/js/src/jit/compiler.rs +++ b/crates/js/src/jit/compiler.rs @@ -70,6 +70,7 @@ impl BaselineJit { if func.is_generator || func.is_async { return Err(CompileError::UnsupportedOpcode(0)); } + reject_unsupported_opcodes(func)?; let mut jit = Self { asm: Assembler::new(), @@ -753,6 +754,53 @@ impl BaselineJit { } } +fn reject_unsupported_opcodes(func: &Function) -> Result<(), CompileError> { + let code = &func.code; + let mut ip = 0; + while ip < code.len() { + let opcode = code[ip]; + ip += 1; + let Some(op) = Op::from_byte(opcode) else { + return Err(CompileError::UnsupportedOpcode(opcode)); + }; + if !jit_supports_op(op) { + return Err(CompileError::UnsupportedOpcode(op as u8)); + } + ip += instruction_operand_size(op); + } + Ok(()) +} + +fn jit_supports_op(op: Op) -> bool { + !matches!( + op, + Op::Exp + | Op::InstanceOf + | Op::In + | Op::NewCall + | Op::CallSpread + | Op::NewCallSpread + | Op::Delete + | Op::DefineMethod + | Op::DefineAccessor + | Op::GetPrivate + | Op::SetPrivate + | Op::HasPrivate + | Op::CopyDataProperties + | Op::SetHomeObject + | Op::LoadSuperBase + | Op::ForInInit + | Op::ForInNext + | Op::SetPrototype + | Op::GetPrototype + | Op::Yield + | Op::Spread + | Op::Await + | Op::BuildArguments + | Op::BuildRestArguments + ) +} + // ── Bytecode reading helpers ───────────────────────────────────────────────── fn read_u8(code: &[u8], ip: &mut usize) -> u8 { @@ -953,4 +1001,17 @@ mod tests { let result = compile_func(&func); assert!(matches!(result, Err(CompileError::UnsupportedOpcode(_)))); } + + #[test] + fn for_in_function_not_compiled() { + let mut builder = BytecodeBuilder::new("for_in".to_string(), 0); + builder.func.register_count = 2; + builder.emit_reg(Op::CreateObject, 0); + builder.emit_reg_reg(Op::ForInInit, 1, 0); + builder.emit_reg(Op::Return, 1); + let func = builder.finish(); + + let result = compile_func(&func); + assert!(matches!(result, Err(CompileError::UnsupportedOpcode(_)))); + } } diff --git a/crates/js/src/parser.rs b/crates/js/src/parser.rs index 0b70df3..466dbdf 100644 --- a/crates/js/src/parser.rs +++ b/crates/js/src/parser.rs @@ -164,6 +164,29 @@ impl Parser { } } + fn expect_module_export_name(&mut self) -> Result<(String, bool), ParseError> { + match self.peek_kind().clone() { + TokenKind::Identifier(name) => { + self.advance(); + Ok((name, true)) + } + TokenKind::String(name) => { + self.advance(); + Ok((name, false)) + } + kind if is_keyword(&kind) => { + let binding_ok = is_contextual_identifier(&kind); + let name = format!("{kind}"); + self.advance(); + Ok((name, binding_ok)) + } + _ => Err(self.error(format!( + "expected module export name, found {:?}", + self.peek_kind() + ))), + } + } + fn start_span(&self) -> SourcePos { self.peek().span.start } @@ -1210,10 +1233,15 @@ impl Parser { else if self.at(&TokenKind::LBrace) { self.advance(); while !self.at(&TokenKind::RBrace) && !self.at_eof() { - let imported = self.expect_identifier()?; + let (imported, imported_can_bind) = self.expect_module_export_name()?; let local = if self.eat_identifier_matching("as") { self.expect_identifier()? } else { + if !imported_can_bind { + return Err( + self.error(format!("imported name {imported:?} requires `as` binding")) + ); + } imported.clone() }; specifiers.push(ImportSpecifier::Named { imported, local }); @@ -1236,10 +1264,15 @@ impl Parser { } else { self.expect(&TokenKind::LBrace)?; while !self.at(&TokenKind::RBrace) && !self.at_eof() { - let imported = self.expect_identifier()?; + let (imported, imported_can_bind) = self.expect_module_export_name()?; let local = if self.eat_identifier_matching("as") { self.expect_identifier()? } else { + if !imported_can_bind { + return Err(self.error(format!( + "imported name {imported:?} requires `as` binding" + ))); + } imported.clone() }; specifiers.push(ImportSpecifier::Named { imported, local }); @@ -1336,9 +1369,9 @@ impl Parser { self.advance(); let mut specifiers = Vec::new(); while !self.at(&TokenKind::RBrace) && !self.at_eof() { - let local = self.expect_identifier()?; + let (local, _) = self.expect_module_export_name()?; let exported = if self.eat_identifier_matching("as") { - self.expect_identifier()? + self.expect_module_export_name()?.0 } else { local.clone() }; @@ -3913,6 +3946,19 @@ mod tests { } } + #[test] + fn test_import_default_as_named_export() { + let prog = Parser::parse_module("import { default as React } from \"react\";").unwrap(); + match &prog.body[0].kind { + StmtKind::Import { specifiers, .. } => { + assert!( + matches!(&specifiers[0], ImportSpecifier::Named { imported, local } if imported == "default" && local == "React") + ); + } + _ => panic!("expected import"), + } + } + #[test] fn test_import_namespace() { let prog = Parser::parse_module("import * as ns from \"mod\";").unwrap(); @@ -3946,6 +3992,19 @@ mod tests { } } + #[test] + fn test_export_named_as_default() { + let prog = Parser::parse_module("export { value as default };").unwrap(); + match &prog.body[0].kind { + StmtKind::Export(ExportDecl::Named { specifiers, .. }) => { + assert_eq!(specifiers.len(), 1); + assert_eq!(specifiers[0].local, "value"); + assert_eq!(specifiers[0].exported, "default"); + } + _ => panic!("expected export named"), + } + } + #[test] fn test_export_declaration() { let prog = Parser::parse_module("export const x = 1;").unwrap(); diff --git a/crates/js/src/shape.rs b/crates/js/src/shape.rs index 51ebc31..227121e 100644 --- a/crates/js/src/shape.rs +++ b/crates/js/src/shape.rs @@ -76,7 +76,7 @@ pub struct Shape { /// Arena of all shapes, shared by every object in the VM. pub struct ShapeTable { shapes: Vec, - lookup_cache: RefCell>>, + lookup_cache: RefCell>>>, } impl Default for ShapeTable { @@ -105,9 +105,11 @@ impl ShapeTable { pub fn lookup(&self, shape_id: ShapeId, name: &str) -> Option { let cacheable = cacheable_lookup_name(name); if cacheable { - let key = (shape_id, name.to_string()); - if let Some(cached) = self.lookup_cache.borrow().get(&key).copied() { - return cached; + let cache = self.lookup_cache.borrow(); + if let Some(shape_cache) = cache.get(&shape_id) { + if let Some(cached) = shape_cache.get(name).copied() { + return cached; + } } } @@ -124,7 +126,9 @@ impl ShapeTable { if cacheable { self.lookup_cache .borrow_mut() - .insert((shape_id, name.to_string()), result); + .entry(shape_id) + .or_default() + .insert(name.to_string(), result); } result } diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index 3d4f1ba..00ceaf2 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -233,6 +233,8 @@ pub struct ObjectData { pub private_fields: HashMap, } +const SHAPED_PROPERTY_LIMIT: usize = 64; + impl ObjectData { pub fn new() -> Self { Self { @@ -286,6 +288,12 @@ impl ObjectData { return; } + if let ObjectStorage::Shaped { shape, slots } = &self.storage { + if slots.len() >= SHAPED_PROPERTY_LIMIT && shapes.lookup(*shape, &key).is_none() { + self.to_dictionary(shapes); + } + } + let attrs = PropertyAttrs { writable: prop.writable, enumerable: prop.enumerable, @@ -339,6 +347,12 @@ impl ObjectData { /// Set a property: update existing value if writable, or insert new. pub fn set_property(&mut self, key: String, val: Value, shapes: &mut ShapeTable) { + if let ObjectStorage::Shaped { shape, slots } = &self.storage { + if slots.len() >= SHAPED_PROPERTY_LIMIT && shapes.lookup(*shape, &key).is_none() { + self.to_dictionary(shapes); + } + } + match &mut self.storage { ObjectStorage::Shaped { shape, slots } => { if let Some(desc) = shapes.lookup(*shape, &key) { @@ -1475,6 +1489,8 @@ pub struct Vm { pub regexp_prototype: Option, /// Built-in Promise.prototype (for Promise objects). pub promise_prototype: Option, + /// Built-in Object.prototype.hasOwnProperty implementation. + pub object_has_own_property: Option, /// Shared Function.prototype.apply implementation returned for function /// property reads. function_apply: Option, @@ -1503,6 +1519,9 @@ pub struct Vm { /// stack unwinding so an unhandled exception can be reported with the /// location it was raised from. Cleared at the start of each `execute()`. last_throw_location: Option<(u32, u32)>, + /// Env-gated VM progress tracing for long-running real-web scripts. + trace_js_progress: bool, + trace_js_progress_interval: u64, } /// Maximum total register file size across all live call frames. @@ -1531,6 +1550,12 @@ impl Vm { // See isu issue 263. crate::timers::reset_timers(); crate::builtins::reset_microtask_queues(); + let trace_js_progress_interval = std::env::var("WE_TRACE_JS_PROGRESS") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|interval| *interval > 0) + .unwrap_or(1_000_000); + let trace_js_progress = std::env::var("WE_TRACE_JS_PROGRESS").is_ok(); let mut vm = Self { registers: vec![Value::Undefined; 256], frames: Vec::new(), @@ -1547,6 +1572,7 @@ impl Vm { date_prototype: None, regexp_prototype: None, promise_prototype: None, + object_has_own_property: None, function_apply: None, function_call: None, function_bind: None, @@ -1558,6 +1584,8 @@ impl Vm { jit_compiled: HashMap::new(), jit_depth: 0, last_throw_location: None, + trace_js_progress, + trace_js_progress_interval, }; crate::builtins::init_builtins(&mut vm); vm @@ -2052,6 +2080,10 @@ impl Vm { func_ref: GcRef, args: &[Value], ) -> Result { + if let Some(value) = self.try_call_function_call_apply_builtin(func_ref, args)? { + return Ok(value); + } + let (kind, upvalues) = match self.gc.get(func_ref) { Some(HeapObject::Function(f)) => (f.kind.clone(), f.upvalues.clone()), _ => return Err(RuntimeError::type_error("not a function")), @@ -3715,6 +3747,11 @@ impl Vm { roots.push(home); } } + // Compiled native code is tied to the bytecode metadata of its source + // function. Keep those functions alive while their code remains cached, + // otherwise a reused GcRef could run stale machine code against a new + // function's frame. + roots.extend(self.jit_compiled.keys().copied()); // Built-in prototype roots. if let Some(r) = self.object_prototype { roots.push(r); @@ -3734,6 +3771,9 @@ impl Vm { if let Some(r) = self.promise_prototype { roots.push(r); } + if let Some(r) = self.object_has_own_property { + roots.push(r); + } if let Some(r) = self.function_apply { roots.push(r); } @@ -3820,15 +3860,17 @@ impl Vm { continue; } - // Instruction limit check (for test harnesses). - if let Some(limit) = self.instruction_limit { + // Instruction limit/progress checks (for test harnesses and diagnostics). + if self.instruction_limit.is_some() || self.trace_js_progress { self.instructions_executed += 1; - if self.instructions_executed > limit { - return Err(RuntimeError { - kind: ErrorKind::Error, - message: "instruction limit exceeded".into(), - source_location: None, - }); + if let Some(limit) = self.instruction_limit { + if self.instructions_executed > limit { + return Err(RuntimeError { + kind: ErrorKind::Error, + message: "instruction limit exceeded".into(), + source_location: None, + }); + } } } @@ -3842,6 +3884,18 @@ impl Vm { source_location: None, }); }; + if self.trace_js_progress + && self + .instructions_executed + .is_multiple_of(self.trace_js_progress_interval) + { + let pc = self.frames[fi].ip.saturating_sub(1); + let loc = self.frames[fi].func.source_location_for(pc); + eprintln!( + "[js-trace] instr={} frame={} func={} pc={} op={:?} loc={:?}", + self.instructions_executed, fi, self.frames[fi].func.name, pc, op, loc + ); + } match op { // ── Register loads ────────────────────────────── @@ -4199,6 +4253,23 @@ impl Vm { } }; + if !is_construct { + match self.try_call_function_call_apply_builtin(func_gc_ref, &args) { + Ok(Some(value)) => { + self.registers[base + dst as usize] = value; + continue; + } + Ok(None) => {} + Err(err) => { + let err_val = err.to_value(&mut self.gc, &mut self.shapes); + if !self.handle_exception(err_val) { + return Err(err); + } + continue; + } + } + } + // Read function data from GC (scoped borrow). let call_info = { match self.gc.get(func_gc_ref) { @@ -6163,6 +6234,72 @@ impl Vm { args } + fn try_call_function_call_apply_builtin( + &mut self, + func_ref: GcRef, + args: &[Value], + ) -> Result, RuntimeError> { + let is_apply = self.function_apply == Some(func_ref); + let is_call = self.function_call == Some(func_ref); + if !is_apply && !is_call { + return Ok(None); + } + + let target = match self + .globals + .get("this") + .cloned() + .unwrap_or(Value::Undefined) + { + Value::Function(func_ref) => func_ref, + _ if is_apply => { + return Err(RuntimeError::type_error( + "Function.prototype.apply target is not callable", + )); + } + _ => { + return Err(RuntimeError::type_error( + "Function.prototype.call target is not callable", + )); + } + }; + + if is_call && self.object_has_own_property == Some(target) { + let this_arg = args.first().cloned().unwrap_or(Value::Undefined); + return Ok(Some( + self.object_has_own_property_value(&this_arg, args.get(1)), + )); + } + + if is_apply { + let this_arg = args.first().cloned().unwrap_or(Value::Undefined); + let arg_array = args.get(1).cloned().unwrap_or(Value::Undefined); + let call_args = self.function_apply_args_from_value(&arg_array); + return self + .call_function_with_this(target, this_arg, &call_args) + .map(Some); + } + + let this_arg = args.first().cloned().unwrap_or(Value::Undefined); + self.call_function_with_this(target, this_arg, args.get(1..).unwrap_or(&[])) + .map(Some) + } + + fn object_has_own_property_value(&self, receiver: &Value, key_value: Option<&Value>) -> Value { + let key = key_value + .map(|value| value.to_js_string(&self.gc)) + .unwrap_or_default(); + let has_key = match receiver.gc_ref() { + Some(obj_ref) => match self.gc.get(obj_ref) { + Some(HeapObject::Object(data)) => data.contains_key(&key, &self.shapes), + Some(HeapObject::Function(fdata)) => fdata.properties.contains_key(&key), + _ => false, + }, + None => false, + }; + Value::Boolean(has_key) + } + fn try_call_function_apply_marker( &mut self, value: &Value, @@ -6198,12 +6335,11 @@ impl Vm { this_arg: Value, args: &[Value], ) -> Result { - let old_this = self.globals.get("this").cloned(); - self.globals.insert("this".to_string(), this_arg); + let old_this = self.replace_global_value("this", this_arg); let result = self.call_function(func_ref, args); match old_this { Some(value) => { - self.globals.insert("this".to_string(), value); + self.replace_global_value("this", value); } None => { self.globals.remove("this"); @@ -6214,7 +6350,7 @@ impl Vm { /// Set a global variable. pub fn set_global(&mut self, name: &str, val: Value) { - self.globals.insert(name.to_string(), val.clone()); + self.replace_global_value(name, val.clone()); if name == "window" { if let Value::Object(window_ref) | Value::Function(window_ref) = val { self.mirror_existing_globals_to_window(window_ref); @@ -6224,6 +6360,14 @@ impl Vm { } } + fn replace_global_value(&mut self, name: &str, val: Value) -> Option { + if let Some(existing) = self.globals.get_mut(name) { + Some(std::mem::replace(existing, val)) + } else { + self.globals.insert(name.to_string(), val) + } + } + /// Remove a global variable. pub fn remove_global(&mut self, name: &str) { self.globals.remove(name); @@ -6338,8 +6482,12 @@ impl Vm { /// Returns `Some(result)` if JIT execution completed, `None` if we need to /// fall back to the interpreter (compilation failed or bail-out). fn try_jit_execute(&mut self, func_gc_ref: GcRef) -> Option> { + let trace_jit = std::env::var_os("WE_TRACE_JIT").is_some(); // Check if we have cached compiled code. if let Some(&code_ptr) = self.jit_compiled.get(&func_gc_ref) { + if trace_jit { + eprintln!("[jit-trace] execute cached func_ref={func_gc_ref:?}"); + } return Some(self.run_jit_code(code_ptr)); } @@ -6351,6 +6499,16 @@ impl Vm { }, _ => return None, }; + if trace_jit { + eprintln!( + "[jit-trace] compile attempt func_ref={func_gc_ref:?} name={} code_len={} regs={} consts={} names={}", + func.name, + func.code.len(), + func.register_count, + func.constants.len(), + func.names.len() + ); + } // Initialize JIT buffer lazily. if self.jit_buffer.is_none() { @@ -6360,9 +6518,23 @@ impl Vm { let code_ptr = match BaselineJit::compile(&func, buffer) { Ok(ptr) => ptr, - Err(_) => return None, // Compilation failed — fall back to interpreter + Err(err) => { + if trace_jit { + eprintln!( + "[jit-trace] compile rejected func_ref={func_gc_ref:?} name={} err={err:?}", + func.name + ); + } + return None; // Compilation failed — fall back to interpreter + } }; + if trace_jit { + eprintln!( + "[jit-trace] compile ok func_ref={func_gc_ref:?} name={}", + func.name + ); + } self.jit_compiled.insert(func_gc_ref, code_ptr); Some(self.run_jit_code(code_ptr)) } @@ -6682,6 +6854,11 @@ impl Vm { args.push(self.registers[base + args_start as usize + i as usize].clone()); } + if let Some(value) = self.try_call_function_call_apply_builtin(func_gc_ref, &args)? { + self.registers[base + dst as usize] = value; + return Ok(()); + } + let call_info = { match self.gc.get(func_gc_ref) { Some(HeapObject::Function(fdata)) => match &fdata.kind { @@ -7509,6 +7686,55 @@ mod tests { } } + #[test] + fn function_call_loop_does_not_allocate_apply_markers() { + let program = crate::parser::Parser::parse( + "function add(a, b) { return a + b; } \ + var sum = 0; \ + for (var i = 0; i < 1000; i++) { \ + sum = add.call(null, sum, 1); \ + } \ + sum", + ) + .expect("parse"); + let func = crate::compiler::compile(&program).expect("compile"); + let mut vm = Vm::new(); + + match vm.execute(&func).expect("execute") { + Value::Number(n) => assert_eq!(n, 1000.0), + v => panic!("expected 1000, got {v:?}"), + } + } + + #[test] + fn function_call_specializes_has_own_property_helpers() { + let program = crate::parser::Parser::parse( + "var source = {}; \ + for (var i = 0; i < 200; i++) { \ + Object.defineProperty(source, 'p' + i, { value: i, enumerable: true }); \ + } \ + var target = {}; \ + var names = Object.getOwnPropertyNames(source); \ + var copied = 0; \ + for (var j = 0; j < names.length; j++) { \ + var key = names[j]; \ + if (!Object.prototype.hasOwnProperty.call(target, key)) { \ + copied++; \ + Object.defineProperty(target, key, { value: source[key], enumerable: true }); \ + } \ + } \ + copied + ':' + Object.prototype.hasOwnProperty.call(target, 'p199') + ':' + target.p42", + ) + .expect("parse"); + let func = crate::compiler::compile(&program).expect("compile"); + let mut vm = Vm::new(); + + match vm.execute(&func).expect("execute") { + Value::String(s) => assert_eq!(s, "200:true:42"), + v => panic!("expected helper result, got {v:?}"), + } + } + #[test] fn rest_parameter_collects_trailing_arguments() { let src = r#" @@ -9297,6 +9523,28 @@ mod tests { } } + #[test] + fn large_dynamic_objects_promote_to_dictionary_storage() { + let mut shapes = ShapeTable::new(); + let mut obj = ObjectData::new(); + + for i in 0..=SHAPED_PROPERTY_LIMIT { + obj.insert_property( + format!("p{i}"), + Property::data(Value::Number(i as f64)), + &mut shapes, + ); + } + + assert!(matches!(obj.storage, ObjectStorage::Dictionary(_))); + assert!(obj.contains_key("p0", &shapes)); + assert!(obj.contains_key(&format!("p{SHAPED_PROPERTY_LIMIT}"), &shapes)); + match obj.get_property("p64", &shapes).map(|prop| prop.value) { + Some(Value::Number(n)) => assert_eq!(n, 64.0), + v => panic!("expected p64 value, got {v:?}"), + } + } + #[test] fn test_property_writable_flag() { // Setting a non-writable property should silently fail. @@ -9368,6 +9616,27 @@ mod tests { } } + #[test] + fn test_for_in_continue_advances_iterator() { + let src = r#" + var o = { a: 1, b: 2, c: 3 }; + var keys = ""; + for (var k in o) { + if (k === "b") continue; + keys = keys + k; + } + keys + "#; + match eval(src).unwrap() { + Value::String(s) => { + assert!(s.contains("a")); + assert!(s.contains("c")); + assert!(!s.contains("b")); + } + v => panic!("expected string, got {v:?}"), + } + } + #[test] fn test_for_in_empty_object() { let src = r#" @@ -13237,6 +13506,47 @@ mod tests { } } + #[test] + fn jit_compiled_functions_are_gc_roots() { + let program = Parser::parse( + "function hot(x) { return x + 1; } + var r = 0; + for (var i = 0; i < 200; i++) { + r = hot(r); + } + r", + ) + .expect("parse"); + let func = compiler::compile(&program).expect("compile"); + let mut vm = Vm::new(); + vm.execute(&func).expect("execute"); + + let compiled: Vec = vm.jit_compiled.keys().copied().collect(); + assert!( + !compiled.is_empty(), + "hot function should have been JIT-compiled" + ); + + vm.registers.clear(); + vm.globals.clear(); + + let roots = vm.collect_roots(); + for func_ref in &compiled { + assert!( + roots.contains(func_ref), + "compiled function {func_ref:?} should be a GC root" + ); + } + + vm.gc.collect(&roots); + for func_ref in compiled { + assert!( + vm.gc.get(func_ref).is_some(), + "compiled function {func_ref:?} should survive GC" + ); + } + } + #[test] fn test_jit_hot_function_comparison() { // JIT with comparison and conditional logic.