feat: 添加 do while 与 for-in 语法编译与执行支持

This commit is contained in:
2026-06-06 18:20:14 +08:00
parent a2762fe3c9
commit bf3913e183
6 changed files with 507 additions and 5 deletions
+76
View File
@@ -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
}
+120 -4
View File
@@ -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)
@@ -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");
}