From bf3913e18391cc92416e6b367198a2c0ea9ad1ab Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 6 Jun 2026 18:20:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20do=20while=20?= =?UTF-8?q?=E4=B8=8E=20for-in=20=E8=AF=AD=E6=B3=95=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E4=B8=8E=E6=89=A7=E8=A1=8C=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/counter.rs | 76 +++++++++++ crates/oxide_compiler/src/emitter.rs | 124 ++++++++++++++++- .../tests/compile_stmt_tests.rs | 99 ++++++++++++++ crates/oxide_vm/Cargo.toml | 1 + crates/oxide_vm/src/vm.rs | 86 +++++++++++- crates/oxide_vm/tests/control_flow_tests.rs | 126 ++++++++++++++++++ 6 files changed, 507 insertions(+), 5 deletions(-) diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index b32d00fa..f4a6e904 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -57,6 +57,17 @@ impl Compiler { ctx.projected_pc += 1; // JMP (backward) ctx.label_map.insert(end_label, ctx.projected_pc); } + Statement::DoWhileStatement(dw) => { + let id = ctx.next_label_id(); + let start_label = Label::DoWhileStart(id); + let end_label = Label::DoWhileEnd(id); + + ctx.label_map.insert(start_label, ctx.projected_pc); + self.count_statement(&dw.body, ctx); + self.count_expression(&dw.test, ctx); + ctx.projected_pc += 1; // JMP_IF_TRUE (backward) + ctx.label_map.insert(end_label, ctx.projected_pc); + } Statement::ForStatement(fr) => { let id = ctx.next_label_id(); let start_label = Label::ForStart(id); @@ -91,6 +102,71 @@ impl Compiler { ctx.projected_pc += 1; // JMP (backward) ctx.label_map.insert(end_label, ctx.projected_pc); } + Statement::ForInStatement(fi) => { + let id = ctx.next_label_id(); + let start_label = Label::ForInStart(id); + let end_label = Label::ForInEnd(id); + + self.count_expression(&fi.right, ctx); + ctx.projected_pc += 1; // FOR_IN_INIT + + ctx.label_map.insert(start_label, ctx.projected_pc); + ctx.projected_pc += 3; // FOR_IN_DONE + JMP_IF_FALSE + JMP cleanup + + ctx.projected_pc += 1; // FOR_IN_NEXT + match &fi.left { + oxide_parser::ForStatementLeft::VariableDeclaration(decl) => { + for _d in &decl.declarations { + ctx.alloc_reg(); + ctx.projected_pc += 1; // STORE_VAR (value from FOR_IN_NEXT) + } + } + oxide_parser::ForStatementLeft::AssignmentTargetIdentifier(_) => { + ctx.alloc_reg(); // key register + ctx.projected_pc += 1; // STORE_VAR + } + _ => {} + } + + self.count_statement(&fi.body, ctx); + ctx.projected_pc += 1; // JMP back to start + + ctx.label_map.insert(end_label, ctx.projected_pc); + ctx.projected_pc += 1; // FOR_IN_CLEANUP + } + Statement::SwitchStatement(sw) => { + let id = ctx.next_label_id(); + let end_label = Label::SwitchEnd(id); + ctx.push_switch(end_label); + + self.count_expression(&sw.discriminant, ctx); + + let cases = &sw.cases; + for case in cases.iter() { + if let Some(test) = &case.test { + self.count_expression(test, ctx); + ctx.projected_pc += 1; // EQ + ctx.alloc_reg(); // eq result + ctx.projected_pc += 1; // JMP_IF_TRUE + } + } + + let has_default = cases.iter().any(|c| c.test.is_none()); + if !has_default { + ctx.projected_pc += 1; // JMP to SwitchEnd (no match) + } + + for (case_idx, case) in cases.iter().enumerate() { + let case_label = Label::SwitchCase(id * 256 + case_idx as u32); + ctx.label_map.insert(case_label, ctx.projected_pc); + for s in &case.consequent { + self.count_statement(s, ctx); + } + } + + ctx.label_map.insert(end_label, ctx.projected_pc); + ctx.pop_switch(); + } Statement::BreakStatement(_) => { ctx.projected_pc += 1; // JMP } diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index e454ac95..61fc8af1 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -1,6 +1,8 @@ use crate::compiler::Label; use crate::opcode::{self, OpCode}; -use oxide_parser::{AssignmentOperator, Expression, ForStatementInit, Statement, UnaryOperator}; +use oxide_parser::{ + AssignmentOperator, Expression, ForStatementInit, ForStatementLeft, Statement, UnaryOperator, +}; use crate::compiler::{is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler}; use crate::module::Constant; @@ -139,6 +141,24 @@ impl Compiler { ctx.pop_loop(); Ok(None) } + Statement::DoWhileStatement(dw) => { + let id = ctx.next_label_id(); + let start_label = Label::DoWhileStart(id); + let end_label = Label::DoWhileEnd(id); + + ctx.push_loop(end_label, start_label); + + self.emit_statement(&dw.body, ctx)?; + + let test_reg = self.emit_expression(&dw.test, ctx)?; + + let start_pos = ctx.resolve_label(start_label)?; + let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); + ctx.emit(opcode::encode_jmp_if_true(test_reg, offset as i16)); + + ctx.pop_loop(); + Ok(None) + } Statement::ForStatement(fr) => { let id = ctx.next_label_id(); let start_label = Label::ForStart(id); @@ -190,10 +210,106 @@ impl Compiler { Ok(None) } + Statement::ForInStatement(fi) => { + let id = ctx.next_label_id(); + let start_label = Label::ForInStart(id); + let end_label = Label::ForInEnd(id); + + let obj_reg = self.emit_expression(&fi.right, ctx)?; + ctx.emit(opcode::encode(OpCode::FOR_IN_INIT, 0, obj_reg, 0)); + + ctx.push_loop(end_label, start_label); + + let done_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_IN_DONE, done_reg, 0, 0)); + + let end_pos = ctx.resolve_label(end_label)?; + let cleanup_jmp_offset = (end_pos as isize) - (ctx.bytecode.len() as isize); + ctx.emit(opcode::encode_jmp_if_false(done_reg, 2)); + ctx.emit(opcode::encode_jmp(cleanup_jmp_offset as i16)); + + let key_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_IN_NEXT, key_reg, 0, 0)); + + match &fi.left { + ForStatementLeft::VariableDeclaration(decl) => { + for d in &decl.declarations { + let name = match &d.id { + oxide_parser::BindingPattern::BindingIdentifier(bi) => { + bi.name.as_str() + } + _ => return Err("destructuring not supported".into()), + }; + let var_reg = ctx.alloc_reg(); + ctx.declare(name, var_reg)?; + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, key_reg, 0)); + ctx.init_var(name); + } + } + ForStatementLeft::AssignmentTargetIdentifier(id_ref) => { + let name = id_ref.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, key_reg, 0)); + } + _ => return Err("unsupported for-in left-hand side".into()), + } + + self.emit_statement(&fi.body, ctx)?; + + let start_pos = ctx.resolve_label(start_label)?; + let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); + ctx.emit(opcode::encode_jmp(offset as i16)); + + ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0)); + + ctx.pop_loop(); + Ok(None) + } + Statement::SwitchStatement(sw) => { + let id = ctx.next_label_id(); + let end_label = Label::SwitchEnd(id); + ctx.push_switch(end_label); + + let disc_reg = self.emit_expression(&sw.discriminant, ctx)?; + let cases = &sw.cases; + + for (case_idx, case) in cases.iter().enumerate() { + if let Some(test) = &case.test { + let test_reg = self.emit_expression(test, ctx)?; + let eq_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::EQ, eq_reg, disc_reg, test_reg)); + + let case_label = Label::SwitchCase(id * 256 + 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 has_default = cases.iter().any(|c| c.test.is_none()); + if !has_default { + let end_pos = ctx.resolve_label(end_label)?; + let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); + ctx.emit(opcode::encode_jmp(offset as i16)); + } + + for case in cases.iter() { + for s in &case.consequent { + self.emit_statement(s, ctx)?; + } + } + + ctx.pop_switch(); + Ok(None) + } Statement::BreakStatement(_) => { - let (break_label, _) = - ctx.current_loop().ok_or("break outside loop".to_string())?; - let break_pos = ctx.resolve_label(*break_label)?; + let break_label = if let Some(sw_label) = ctx.current_switch() { + *sw_label + } else { + let (bl, _) = ctx.current_loop().ok_or("break outside switch or loop".to_string())?; + *bl + }; + let break_pos = ctx.resolve_label(break_label)?; let offset = (break_pos as isize) - (ctx.bytecode.len() as isize); ctx.emit(opcode::encode_jmp(offset as i16)); Ok(None) diff --git a/crates/oxide_compiler/tests/compile_stmt_tests.rs b/crates/oxide_compiler/tests/compile_stmt_tests.rs index da76e2c1..f6f73f55 100644 --- a/crates/oxide_compiler/tests/compile_stmt_tests.rs +++ b/crates/oxide_compiler/tests/compile_stmt_tests.rs @@ -176,3 +176,102 @@ fn regression_redundant_jmp_if_without_else() { "final opcode should be HALT, not dangling JMP" ); } + +#[test] +fn compile_do_while_emits_jmp_if_true() { + let module = compile_source("do { 1 } while (true)"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::JMP_IF_TRUE), + "do-while should contain JMP_IF_TRUE" + ); +} + +#[test] +fn compile_do_while_emits_body_before_test() { + let module = compile_source("var x = true; do { 1 } while (x)"); + let has_opcodes_before_jmp = module + .bytecode + .iter() + .take_while(|&&i| opcode::opcode(i) != OpCode::JMP_IF_TRUE) + .count() + > 1; + assert!(has_opcodes_before_jmp, "body should emit before test"); +} + +#[test] +fn compile_do_while_break() { + let module = compile_source("do { break; } while (true)"); + assert!(!module.bytecode.is_empty(), "do-while with break should compile"); +} + +#[test] +fn compile_do_while_continue() { + let module = compile_source("var x = 0; do { continue; } while (x < 5)"); + assert!( + !module.bytecode.is_empty(), + "do-while with continue should compile" + ); +} + +#[test] +fn compile_for_in_emits_opcodes() { + let module = compile_source("var obj={a:1}; for (k in obj) {}"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::FOR_IN_INIT), + "for-in should emit FOR_IN_INIT" + ); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::FOR_IN_NEXT), + "for-in should emit FOR_IN_NEXT" + ); +} + +#[test] +fn compile_for_in_var_decl() { + let module = compile_source("var obj={a:1}; for (var k in obj) { k }"); + assert!(!module.bytecode.is_empty()); +} + +#[test] +fn compile_for_in_let_decl() { + let module = compile_source("var obj={a:1}; for (let k in obj) { k }"); + assert!(!module.bytecode.is_empty()); +} + +#[test] +fn compile_switch_emits_jmp_if_true() { + let module = compile_source("var x=0; switch(x){case 1:1;case 2:2;}"); + assert!( + module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_TRUE), + "switch should emit JMP_IF_TRUE" + ); +} + +#[test] +fn compile_switch_default() { + let module = compile_source("var x=0; switch(x){default:0;}"); + assert!(!module.bytecode.is_empty()); +} + +#[test] +fn compile_switch_break() { + let module = compile_source("var x=0; switch(x){case 1:break;}"); + assert!(!module.bytecode.is_empty()); +} + +#[test] +fn compile_continue_in_switch_errors() { + let result = std::panic::catch_unwind(|| { + compile_source("var x=0; switch(x){case 1:continue;}"); + }); + assert!(result.is_err(), "continue inside switch should error"); +} diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index cd95160b..aadea3b0 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -10,6 +10,7 @@ description = "OxideJS - Register-based VM with epoch arena memory" oxide_compiler = { path = "../oxide_compiler" } oxide_kernel = { path = "../oxide_kernel" } oxide_types = { path = "../oxide_types" } +bumpalo.workspace = true fast-float.workspace = true ryu.workspace = true diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index f132a903..00eb39c7 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -9,9 +9,9 @@ use oxide_compiler::opcode::{self, OpCode}; use crate::coercion; use oxide_kernel::kernel::{KernelConfig, OxideKernel}; use oxide_kernel::prop_forge::PropTemplate; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::mem::{Epoch, P}; use oxide_types::object::JsObject; -use oxide_types::shape::EMPTY_SHAPE_ID; use oxide_types::value::JsValue; pub struct CallFrame { @@ -20,12 +20,18 @@ pub struct CallFrame { pub n_args: u8, } +pub struct ForInIter<'bump> { + pub keys: bumpalo::collections::Vec<'bump, JsValue>, + pub index: usize, +} + pub struct Vm { regs: [JsValue; 256], pc: usize, bytecode: Vec, constants: Vec, frames: Vec, + pub for_in_iters: Vec<*mut u8>, kernel: Arc, interned_strings: Vec, pub epoch: Epoch, @@ -40,6 +46,7 @@ impl Vm { bytecode: Vec::new(), constants: Vec::new(), frames: Vec::with_capacity(128), + for_in_iters: Vec::new(), kernel: Arc::new(OxideKernel::new(KernelConfig::minimal())), interned_strings: Vec::new(), epoch: Epoch::new(), @@ -60,6 +67,7 @@ impl Vm { self.bytecode.clear(); self.constants.clear(); self.frames.clear(); + self.for_in_iters.clear(); self.epoch.reset(); self.interned_strings.clear(); } @@ -104,6 +112,7 @@ impl Vm { self.pc = 0; self.regs = [JsValue::undefined(); 256]; self.frames.clear(); + self.for_in_iters.clear(); self.dispatch() } @@ -566,6 +575,81 @@ impl Vm { self.regs[rd] = if cond { self.regs[a] } else { self.regs[b] }; } + OpCode::FOR_IN_INIT => { + let obj_val = self.regs[a]; + if !obj_val.is_object() { + return Err("TypeError: for-in right-hand side is not an object".into()); + } + let obj = unsafe { &*obj_val.as_js_object_ptr() }; + let shape_id = obj.shape_id(); + + let mut keys_vec = + bumpalo::collections::Vec::new_in(self.epoch.bump()); + let mut cursor = Some(shape_id); + while let Some(id) = cursor { + if id == oxide_kernel::shape_forge::EMPTY_SHAPE_ID { + break; + } + if let Some(shape) = self.kernel.shape_forge().get_shape(id) { + if shape.property_name != u32::MAX { + let hash = self + .kernel + .string_forge() + .get_hash(shape.property_name) + .unwrap_or(0); + keys_vec.push(JsValue::string(shape.property_name, hash)); + } + cursor = shape.parent; + } else { + break; + } + } + + let iter = self.epoch.alloc(ForInIter { + keys: keys_vec, + index: 0, + }); + self.for_in_iters.push(iter as *mut u8); + } + + OpCode::FOR_IN_NEXT => { + let iter_ptr = self + .for_in_iters + .last() + .copied() + .map(|p| p as *mut ForInIter) + .unwrap_or(std::ptr::null_mut()); + if iter_ptr.is_null() { + return Err("FOR_IN_NEXT without active iterator".into()); + } + let iter = unsafe { &mut *iter_ptr }; + if iter.index < iter.keys.len() { + self.regs[rd] = iter.keys[iter.index]; + iter.index += 1; + } else { + self.regs[rd] = JsValue::undefined(); + } + } + + OpCode::FOR_IN_DONE => { + let iter_ptr = self + .for_in_iters + .last() + .copied() + .map(|p| p as *mut ForInIter) + .unwrap_or(std::ptr::null_mut()); + if iter_ptr.is_null() { + self.regs[rd] = JsValue::bool(true); + } else { + let iter = unsafe { &*iter_ptr }; + self.regs[rd] = JsValue::bool(iter.index >= iter.keys.len()); + } + } + + OpCode::FOR_IN_CLEANUP => { + self.for_in_iters.pop(); + } + _ => { if !op.is_implemented() { return Ok(JsValue::undefined()); diff --git a/crates/oxide_vm/tests/control_flow_tests.rs b/crates/oxide_vm/tests/control_flow_tests.rs index ba843158..7132a642 100644 --- a/crates/oxide_vm/tests/control_flow_tests.rs +++ b/crates/oxide_vm/tests/control_flow_tests.rs @@ -142,3 +142,129 @@ fn eval_control_flow_complex() { "8" ); } + +#[test] +fn eval_do_while_body_executes_once() { + assert_eq!(eval("var x = 0; do { x = 1; } while (false); x"), "1"); +} + +#[test] +fn eval_do_while_multi_iteration() { + assert_eq!(eval("var i = 0; do { i = i + 1; } while (i < 3); i"), "3"); +} + +#[test] +fn eval_do_while_break() { + assert_eq!( + eval("var i = 0; do { i = i + 1; if (i >= 3) break; } while (true); i"), + "3" + ); +} + +#[test] +fn eval_do_while_continue() { + assert_eq!( + eval("var i = 0; var r = 0; do { i = i + 1; if (i == 3) continue; r = r + 1; } while (i < 5); r"), + "4" + ); +} + +#[test] +fn eval_for_in_enumerates_keys() { + assert_eq!( + eval("var n=0; for (k in {a:1,b:2}) { n=n+1; } n"), + "2" + ); +} + +#[test] +fn eval_for_in_empty_object() { + assert_eq!(eval("var r=0; for (k in {}) { r=1; } r"), "0"); +} + +#[test] +fn eval_for_in_non_object_throws() { + let result = eval("for (k in 42) {}"); + assert!( + result.contains("TypeError"), + "expected TypeError, got: {result}" + ); +} + +#[test] +fn eval_for_in_nested() { + assert_eq!( + eval("var r=0; for (k in {a:{x:1}}) { for (j in {y:2}) { r=r+1; } } r"), + "1" + ); +} + +#[test] +fn eval_for_in_break() { + assert_eq!( + eval("var r=0; for (k in {a:1,b:2,c:3}) { if (k=='b') break; r=r+1; } r"), + "1" + ); +} + +#[test] +fn eval_for_in_continue() { + assert_eq!( + eval("var r=0; for (k in {a:1,b:2}) { if (k=='a') continue; r=r+1; } r"), + "1" + ); +} + +#[test] +fn eval_for_in_let_scoping() { + let result = eval("var r=0; for (let k in {a:1}) { r=1; } r"); + assert_eq!(result, "1"); +} + +#[test] +fn eval_switch_basic_match() { + assert_eq!( + eval("var x=0;switch(2){case 1:x=10;case 2:x=20;break;default:x=30;}x"), + "20" + ); +} + +#[test] +fn eval_switch_fallthrough() { + assert_eq!( + eval("var x=0;switch(1){case 1:case 2:x=42;}x"), + "42" + ); +} + +#[test] +fn eval_switch_default() { + assert_eq!( + eval("var x=0;switch(99){case 1:x=10;default:x=30;}x"), + "30" + ); +} + +#[test] +fn eval_switch_no_match_no_default() { + assert_eq!( + eval("var x=0;switch(99){case 1:x=10;}x"), + "0" + ); +} + +#[test] +fn eval_switch_break() { + assert_eq!( + eval("var x=0;switch(1){case 1:x=10;break;case 2:x=20;}x"), + "10" + ); +} + +#[test] +fn eval_break_in_switch_in_loop() { + assert_eq!( + eval("var r=0;while(true){switch(1){case 1:r=1;break;}r=2;break;}r"), + "2" + ); +}