mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 实现 SequenceExpression + 带标签 break/continue + EmptyStatement
This commit is contained in:
@@ -125,6 +125,17 @@ pub(crate) enum Label {
|
||||
CatchBody(u32),
|
||||
FinallyBody(u32),
|
||||
TryEnd(u32),
|
||||
LabeledEnd(u32),
|
||||
}
|
||||
|
||||
/// A labeled-statement scope active during emission. `break label` targets
|
||||
/// `break_label`; `continue label` targets `continue_label` (only set when the
|
||||
/// labeled statement directly wraps an iteration statement).
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LabelScope {
|
||||
pub(crate) name: String,
|
||||
pub(crate) break_label: Label,
|
||||
pub(crate) continue_label: Option<Label>,
|
||||
}
|
||||
|
||||
pub(crate) struct CompileCtx {
|
||||
@@ -139,6 +150,10 @@ pub(crate) struct CompileCtx {
|
||||
pub(crate) loop_stack: Vec<(Label, Label)>,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) switch_stack: Vec<Label>,
|
||||
/// Active labeled-statement scopes (resolves `break label` / `continue label`).
|
||||
pub(crate) label_scopes: Vec<LabelScope>,
|
||||
/// Label names awaiting binding to the next emitted loop's continue target.
|
||||
pub(crate) pending_loop_labels: Vec<String>,
|
||||
pub(crate) label_counter: u32,
|
||||
pub(crate) projected_pc: usize,
|
||||
pub(crate) builtin_reg_map: Vec<(String, u8)>,
|
||||
@@ -210,6 +225,8 @@ impl CompileCtx {
|
||||
label_map: HashMap::new(),
|
||||
loop_stack: Vec::new(),
|
||||
switch_stack: Vec::new(),
|
||||
label_scopes: Vec::new(),
|
||||
pending_loop_labels: Vec::new(),
|
||||
label_counter: 0,
|
||||
projected_pc: 0,
|
||||
builtin_reg_map: Vec::new(),
|
||||
@@ -468,6 +485,60 @@ impl CompileCtx {
|
||||
self.switch_stack.last()
|
||||
}
|
||||
|
||||
pub(crate) fn push_label_scope(
|
||||
&mut self, name: &str, break_label: Label, continue_label: Option<Label>,
|
||||
) -> Result<(), String> {
|
||||
if self.label_scopes.iter().any(|s| s.name == name) {
|
||||
return Err(format!("SyntaxError: Label '{name}' has already been declared"));
|
||||
}
|
||||
self.label_scopes.push(LabelScope {
|
||||
name: name.to_string(),
|
||||
break_label,
|
||||
continue_label,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn pop_label_scope(&mut self) {
|
||||
self.label_scopes.pop();
|
||||
}
|
||||
|
||||
pub(crate) fn find_label(&self, name: &str) -> Option<&LabelScope> {
|
||||
self.label_scopes.iter().rev().find(|s| s.name == name)
|
||||
}
|
||||
|
||||
/// Queue a label name to be bound to the continue target of the next loop
|
||||
/// emitted as the labeled statement's body. Rejects duplicates in the active
|
||||
/// or pending sets.
|
||||
pub(crate) fn queue_loop_label(&mut self, name: &str) -> Result<(), String> {
|
||||
if self.label_scopes.iter().any(|s| s.name == name) || self.pending_loop_labels.iter().any(|n| n == name) {
|
||||
return Err(format!("SyntaxError: Label '{name}' has already been declared"));
|
||||
}
|
||||
self.pending_loop_labels.push(name.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drain queued loop labels into active scopes bound to this loop's break and
|
||||
/// continue targets. Returns how many scopes were pushed (to pop after).
|
||||
pub(crate) fn take_pending_loop_labels(&mut self, break_label: Label, continue_label: Label) -> usize {
|
||||
let names = std::mem::take(&mut self.pending_loop_labels);
|
||||
let count = names.len();
|
||||
for name in names {
|
||||
self.label_scopes.push(LabelScope {
|
||||
name,
|
||||
break_label,
|
||||
continue_label: Some(continue_label),
|
||||
});
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
pub(crate) fn pop_label_scopes(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
self.label_scopes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_builtin(&self, name: &str) -> bool {
|
||||
self.builtin_reg_map.iter().any(|(n, _)| n == name)
|
||||
}
|
||||
|
||||
@@ -23,4 +23,27 @@ impl Compiler {
|
||||
pub(in crate::counter) fn count_continue_statement(&self, ctx: &mut CompileCtx) {
|
||||
ctx.projected_pc += 1; // JMP
|
||||
}
|
||||
|
||||
pub(in crate::counter) fn count_labeled_statement(
|
||||
&self, stmt: &oxide_parser::LabeledStatement<'_>, ctx: &mut CompileCtx,
|
||||
) {
|
||||
let body_is_loop = matches!(
|
||||
stmt.body,
|
||||
Statement::WhileStatement(_)
|
||||
| Statement::DoWhileStatement(_)
|
||||
| Statement::ForStatement(_)
|
||||
| Statement::ForInStatement(_)
|
||||
| Statement::ForOfStatement(_)
|
||||
);
|
||||
if body_is_loop {
|
||||
// Loop body owns its labels; labeled break/continue reuse them.
|
||||
self.count_statement(&stmt.body, ctx);
|
||||
} else {
|
||||
// Non-loop label: consume one id (parity with emit) and place the
|
||||
// LabeledEnd target right after the body for `break label`.
|
||||
let id = ctx.next_label_id();
|
||||
self.count_statement(&stmt.body, ctx);
|
||||
ctx.label_map.insert(Label::LabeledEnd(id), ctx.projected_pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ impl Compiler {
|
||||
Statement::ClassDeclaration(class) => self.count_class_declaration(class, ctx),
|
||||
Statement::ThrowStatement(ts) => self.count_throw_statement(ts, ctx),
|
||||
Statement::TryStatement(ts) => self.count_try_statement(ts, ctx),
|
||||
Statement::LabeledStatement(ls) => self.count_labeled_statement(ls, ctx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,19 @@ impl Compiler {
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::emitter) fn emit_sequence_expression(
|
||||
&self, seq: &oxide_parser::SequenceExpression, ctx: &mut CompileCtx,
|
||||
) -> Result<u8, String> {
|
||||
// Comma operator: evaluate each expression in order, value is the last.
|
||||
// Emits exactly one expression per element (no extra instruction), so the
|
||||
// projected-PC count in count_sequence_expression stays aligned.
|
||||
let mut last = 0u8;
|
||||
for e in &seq.expressions {
|
||||
last = self.emit_expression(e, ctx)?;
|
||||
}
|
||||
Ok(last)
|
||||
}
|
||||
|
||||
pub(in crate::emitter) fn emit_unsupported_expression(
|
||||
&self, expr: &Expression, ctx: &mut CompileCtx,
|
||||
) -> Result<u8, String> {
|
||||
|
||||
@@ -967,12 +967,14 @@ impl Compiler {
|
||||
Statement::ForInStatement(_) => self.emit_for_in_statement(stmt, ctx),
|
||||
Statement::ForOfStatement(_) => self.emit_for_of_statement(stmt, ctx),
|
||||
Statement::SwitchStatement(_) => self.emit_switch_statement(stmt, ctx),
|
||||
Statement::BreakStatement(_) => self.emit_break_statement(ctx),
|
||||
Statement::ContinueStatement(_) => self.emit_continue_statement(ctx),
|
||||
Statement::BreakStatement(b) => self.emit_break_statement(b, ctx),
|
||||
Statement::ContinueStatement(c) => self.emit_continue_statement(c, ctx),
|
||||
Statement::FunctionDeclaration(_) => self.emit_function_declaration_statement(stmt, ctx),
|
||||
Statement::ClassDeclaration(_) => self.emit_class_declaration_statement(stmt, ctx),
|
||||
Statement::ThrowStatement(_) => self.emit_throw_statement(stmt, ctx),
|
||||
Statement::TryStatement(_) => self.emit_try_statement(stmt, ctx),
|
||||
Statement::LabeledStatement(ls) => self.emit_labeled_statement(ls, ctx),
|
||||
Statement::EmptyStatement(_) => Ok(None),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -1006,6 +1008,7 @@ impl Compiler {
|
||||
Expression::CallExpression(call) => self.emit_call_expression(call, ctx),
|
||||
Expression::ThisExpression(_) => self.emit_this_expression(ctx),
|
||||
Expression::RegExpLiteral(lit) => self.emit_reg_exp_literal_expression(lit, ctx),
|
||||
Expression::SequenceExpression(seq) => self.emit_sequence_expression(seq, ctx),
|
||||
_ => self.emit_unsupported_expression(expr, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,16 @@ impl Compiler {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(in crate::emitter) fn emit_break_statement(&self, ctx: &mut CompileCtx) -> Result<Option<u8>, String> {
|
||||
let break_label = if let Some(sw_label) = ctx.current_switch() {
|
||||
pub(in crate::emitter) fn emit_break_statement(
|
||||
&self, stmt: &oxide_parser::BreakStatement, ctx: &mut CompileCtx,
|
||||
) -> Result<Option<u8>, String> {
|
||||
let break_label = if let Some(label) = &stmt.label {
|
||||
let name = label.name.as_str();
|
||||
let scope = ctx
|
||||
.find_label(name)
|
||||
.ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?;
|
||||
scope.break_label
|
||||
} else if let Some(sw_label) = ctx.current_switch() {
|
||||
*sw_label
|
||||
} else {
|
||||
let (bl, _) = ctx.current_loop().ok_or("break outside switch or loop".to_string())?;
|
||||
@@ -42,12 +50,56 @@ impl Compiler {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(in crate::emitter) fn emit_continue_statement(&self, ctx: &mut CompileCtx) -> Result<Option<u8>, String> {
|
||||
let (_, continue_label) = ctx.current_loop().ok_or("continue outside loop".to_string())?;
|
||||
let continue_pos = ctx.resolve_label(*continue_label)?;
|
||||
pub(in crate::emitter) fn emit_continue_statement(
|
||||
&self, stmt: &oxide_parser::ContinueStatement, ctx: &mut CompileCtx,
|
||||
) -> Result<Option<u8>, String> {
|
||||
let continue_label = if let Some(label) = &stmt.label {
|
||||
let name = label.name.as_str();
|
||||
let scope = ctx
|
||||
.find_label(name)
|
||||
.ok_or_else(|| format!("SyntaxError: Undefined label '{name}'"))?;
|
||||
scope.continue_label.ok_or_else(|| {
|
||||
format!("SyntaxError: Illegal continue statement: '{name}' does not denote an iteration statement")
|
||||
})?
|
||||
} else {
|
||||
let (_, cl) = ctx.current_loop().ok_or("continue outside loop".to_string())?;
|
||||
*cl
|
||||
};
|
||||
let continue_pos = ctx.resolve_label(continue_label)?;
|
||||
let offset = (continue_pos as isize) - (ctx.bytecode.len() as isize);
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(in crate::emitter) fn emit_labeled_statement(
|
||||
&self, stmt: &oxide_parser::LabeledStatement, ctx: &mut CompileCtx,
|
||||
) -> Result<Option<u8>, String> {
|
||||
let name = stmt.label.name.as_str();
|
||||
if Self::is_iteration_statement(&stmt.body) {
|
||||
// Bind the label to the loop's own continue/break targets (the loop
|
||||
// drains the queue and pops the scope after its body).
|
||||
ctx.queue_loop_label(name)?;
|
||||
self.emit_statement(&stmt.body, ctx)?;
|
||||
} else {
|
||||
// Non-loop labeled statement: only `break label` is valid; it targets
|
||||
// the LabeledEnd placed (by the count pass) right after the body.
|
||||
let id = ctx.next_label_id();
|
||||
ctx.push_label_scope(name, Label::LabeledEnd(id), None)?;
|
||||
self.emit_statement(&stmt.body, ctx)?;
|
||||
ctx.pop_label_scope();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(in crate::emitter) fn is_iteration_statement(stmt: &Statement) -> bool {
|
||||
matches!(
|
||||
stmt,
|
||||
Statement::WhileStatement(_)
|
||||
| Statement::DoWhileStatement(_)
|
||||
| Statement::ForStatement(_)
|
||||
| Statement::ForInStatement(_)
|
||||
| Statement::ForOfStatement(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ impl Compiler {
|
||||
let end_label = Label::WhileEnd(id);
|
||||
|
||||
ctx.push_loop(end_label, start_label);
|
||||
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
|
||||
|
||||
let test_reg = self.emit_expression(&wh.test, ctx)?;
|
||||
let end_pos = ctx.resolve_label(end_label)?;
|
||||
@@ -26,6 +27,7 @@ impl Compiler {
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
|
||||
ctx.pop_label_scopes(n_labeled);
|
||||
ctx.pop_loop();
|
||||
Ok(None)
|
||||
}
|
||||
@@ -41,6 +43,7 @@ impl Compiler {
|
||||
let end_label = Label::DoWhileEnd(id);
|
||||
|
||||
ctx.push_loop(end_label, start_label);
|
||||
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
|
||||
self.emit_statement(&dw.body, ctx)?;
|
||||
let test_reg = self.emit_expression(&dw.test, ctx)?;
|
||||
|
||||
@@ -49,6 +52,7 @@ impl Compiler {
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp_if_true(test_reg, offset));
|
||||
|
||||
ctx.pop_label_scopes(n_labeled);
|
||||
ctx.pop_loop();
|
||||
Ok(None)
|
||||
}
|
||||
@@ -65,6 +69,7 @@ impl Compiler {
|
||||
let end_label = Label::ForEnd(id);
|
||||
|
||||
ctx.push_loop(end_label, update_label);
|
||||
let n_labeled = ctx.take_pending_loop_labels(end_label, update_label);
|
||||
|
||||
if let Some(init) = &fr.init {
|
||||
if let Some(expr) = init.as_expression() {
|
||||
@@ -102,6 +107,7 @@ impl Compiler {
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
|
||||
ctx.pop_label_scopes(n_labeled);
|
||||
ctx.pop_loop();
|
||||
|
||||
Ok(None)
|
||||
@@ -121,6 +127,7 @@ impl Compiler {
|
||||
ctx.emit(opcode::encode(OpCode::FOR_IN_INIT, 0, obj_reg, 0));
|
||||
|
||||
ctx.push_loop(end_label, start_label);
|
||||
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
|
||||
|
||||
let done_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(OpCode::FOR_IN_DONE, done_reg, 0, 0));
|
||||
@@ -164,6 +171,7 @@ impl Compiler {
|
||||
|
||||
ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0));
|
||||
|
||||
ctx.pop_label_scopes(n_labeled);
|
||||
ctx.pop_loop();
|
||||
Ok(None)
|
||||
}
|
||||
@@ -181,6 +189,7 @@ impl Compiler {
|
||||
let iter_src_reg = self.emit_expression(&fo.right, ctx)?;
|
||||
ctx.emit(opcode::encode(OpCode::FOR_OF_INIT, 0, iter_src_reg, 0));
|
||||
ctx.push_loop(end_label, start_label);
|
||||
let n_labeled = ctx.take_pending_loop_labels(end_label, start_label);
|
||||
|
||||
let has_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0));
|
||||
@@ -217,6 +226,7 @@ impl Compiler {
|
||||
let offset = ctx.checked_jump_offset(offset);
|
||||
ctx.emit(opcode::encode_jmp(offset));
|
||||
ctx.emit(opcode::encode(OpCode::FOR_OF_CLOSE, 0, 0, 0));
|
||||
ctx.pop_label_scopes(n_labeled);
|
||||
ctx.pop_loop();
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ pub(super) fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, i
|
||||
pin.left.name.as_str().hash(h);
|
||||
hash_expression(&pin.right, h, include_binding_names);
|
||||
}
|
||||
Expression::SequenceExpression(seq) => {
|
||||
(seq.expressions.len() as u32).hash(h);
|
||||
for e in &seq.expressions {
|
||||
hash_expression(e, h, include_binding_names);
|
||||
}
|
||||
}
|
||||
Expression::Identifier(ident) => {
|
||||
if include_binding_names {
|
||||
ident.name.as_str().hash(h);
|
||||
|
||||
@@ -32,8 +32,20 @@ pub(super) fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, inc
|
||||
hash_statement(s, h, include_binding_names);
|
||||
}
|
||||
}
|
||||
Statement::BreakStatement(_) => {}
|
||||
Statement::ContinueStatement(_) => {}
|
||||
Statement::BreakStatement(b) => {
|
||||
if let Some(label) = &b.label {
|
||||
label.name.as_str().hash(h);
|
||||
}
|
||||
}
|
||||
Statement::ContinueStatement(c) => {
|
||||
if let Some(label) = &c.label {
|
||||
label.name.as_str().hash(h);
|
||||
}
|
||||
}
|
||||
Statement::LabeledStatement(ls) => {
|
||||
ls.label.name.as_str().hash(h);
|
||||
hash_statement(&ls.body, h, include_binding_names);
|
||||
}
|
||||
Statement::DoWhileStatement(dw) => {
|
||||
hash_statement(&dw.body, h, include_binding_names);
|
||||
expression::hash_expression(&dw.test, h, include_binding_names);
|
||||
|
||||
@@ -297,3 +297,18 @@ fn compile_class_static_setter_emits_define_accessor() {
|
||||
"static class setter should emit DEFINE_ACCESSOR"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_labeled_loop_emits_body() {
|
||||
let module = compile_source("outer: while (true) { break outer; }");
|
||||
assert!(!module.bytecode.is_empty(), "labeled loop should produce bytecode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_empty_statements_are_skipped() {
|
||||
let module = compile_source("; ; var x = 1;");
|
||||
assert!(
|
||||
module.constants.iter().any(|c| matches!(c, Constant::Int(1))),
|
||||
"var after empty statements should still compile"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,3 +236,72 @@ fn eval_large_adjacent_switches_do_not_collide_labels() {
|
||||
source.push_str("}switch(0){case 0:r=r+1;break;}r");
|
||||
assert_eq!(eval(&source), "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_sequence_expression_returns_last() {
|
||||
assert_eq!(eval("var x = 0; (x = 1, x = x + 2, x)"), "3");
|
||||
assert_eq!(eval("(1, 2, 3)"), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_sequence_expression_in_for_clauses() {
|
||||
assert_eq!(eval("for (var i = (0, 1); i < 3; i = (i + 1, i + 1)) {} i"), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_empty_statement_is_noop() {
|
||||
assert_eq!(eval("while (false); 1"), "1");
|
||||
assert_eq!(eval("; ; var x = 1; x"), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_labeled_break_exits_loop() {
|
||||
assert_eq!(eval("outer: while (true) { break outer; } 7"), "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_labeled_break_exits_block() {
|
||||
assert_eq!(eval("label: { break label; } 5"), "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_labeled_continue_loop() {
|
||||
assert_eq!(eval("var i = 0; outer: while (i < 3) { i = i + 1; continue outer; } i"), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_labeled_continue_outer_from_inner_loop() {
|
||||
assert_eq!(
|
||||
eval("var c = 0; outer: for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { if (j == 1) continue outer; c = c + 1; } } c"),
|
||||
"3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_labeled_break_outer_from_inner_loop() {
|
||||
assert_eq!(
|
||||
eval("var c = 0; outer: for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { if (i == 1 && j == 1) break outer; c = c + 1; } } c"),
|
||||
"4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_duplicate_label_is_compile_error() {
|
||||
let r = eval("a: a: while (false) {}");
|
||||
assert!(r.contains("compile error") && r.contains("already been declared"), "got: {r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_continue_to_non_loop_label_is_compile_error() {
|
||||
let r = eval("label: { continue label; }");
|
||||
assert!(
|
||||
r.contains("compile error") && r.contains("does not denote an iteration statement"),
|
||||
"got: {r}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_break_to_undefined_label_is_compile_error() {
|
||||
let r = eval("foo: while (false) {} break bar;");
|
||||
assert!(r.contains("compile error") && r.contains("Undefined label"), "got: {r}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user