diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter/mod.rs similarity index 79% rename from crates/oxide_compiler/src/emitter.rs rename to crates/oxide_compiler/src/emitter/mod.rs index d18b652..2b3c377 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter/mod.rs @@ -11,6 +11,9 @@ use oxide_parser::{ use crate::compiler::{ is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler, FunctionBodyContext, ParamSpec, }; + +mod stmt; + impl Compiler { fn emit_optional_guard(&self, reg: u8, short_label: Label, ctx: &mut CompileCtx) -> Result<(), String> { let short_pos = ctx.resolve_label(short_label)?; @@ -952,539 +955,26 @@ impl Compiler { pub(crate) fn emit_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) -> Result, String> { match stmt { - Statement::ExpressionStatement(es) => Ok(Some(self.emit_expression(&es.expression, ctx)?)), - Statement::VariableDeclaration(decl) => { - let mut r = None; - for d in &decl.declarations { - let is_const = matches!(decl.kind, VariableDeclarationKind::Const); - if is_const && d.init.is_none() { - return Err("const declaration must have an initializer".into()); - } - if let Some(init) = &d.init { - let val_reg = self.emit_expression(init, ctx)?; - self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; - // Name inference (D-04): if the initializer is an arrow function, - // set the compiled sub_module's function_name. - if let BindingPattern::BindingIdentifier(bi) = &d.id { - if matches!(*init, Expression::ArrowFunctionExpression(_)) { - if let Some(sub_mod) = ctx.sub_modules.last_mut() { - sub_mod.function_name = Some(bi.name.to_string()); - } - } - } - r = Some(val_reg); - } else { - let BindingPattern::BindingIdentifier(bi) = &d.id else { - return Err("destructuring declaration requires an initializer".into()); - }; - let idx = ctx.add_constant(Constant::Undefined); - let tmp = ctx.alloc_reg(); - ctx.emit_load_const(tmp, idx); - let var_reg = ctx.alloc_reg(); - ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); - ctx.init_var(bi.name.as_str()); - r = Some(var_reg); - } - } - Ok(r) - } - Statement::ReturnStatement(ret) => { - match &ret.argument { - Some(expr) => { - let r = self.emit_expression(expr, ctx)?; - ctx.emit(opcode::encode(OpCode::RETURN, r, 0, 0)); - } - None => { - ctx.emit(opcode::encode(OpCode::RETURN, 0, 0, 0)); - } - } - Ok(None) - } - Statement::BlockStatement(block) => { - ctx.push_scope(); - let mut r = None; - for s in &block.body { - if let Some(rr) = self.emit_statement(s, ctx)? { - r = Some(rr); - } - } - ctx.pop_scope(); - Ok(r) - } - Statement::IfStatement(ifs) => { - let id = ctx.next_label_id(); - let else_label = Label::IfElse(id); - let end_label = Label::IfEnd(id); - - let test_reg = self.emit_expression(&ifs.test, ctx)?; - - let else_pos = ctx.resolve_label(else_label)?; - let offset = (else_pos as isize) - (ctx.bytecode.len() as isize); - 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(); - if let Some(r) = cons_reg { - ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, r, 0)); - } else { - let undef_idx = ctx.add_constant(Constant::Undefined); - ctx.emit_load_const(result_reg, undef_idx); - } - - let has_alt = ifs.alternate.is_some(); - if has_alt { - let end_pos = ctx.resolve_label(end_label)?; - let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp(offset)); - } - - if let Some(alt) = &ifs.alternate { - let alt_reg = self.emit_statement(alt, ctx)?; - if let Some(r) = alt_reg { - ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, r, 0)); - } else { - let undef_idx = ctx.add_constant(Constant::Undefined); - ctx.emit_load_const(result_reg, undef_idx); - } - } - - Ok(Some(result_reg)) - } - Statement::WhileStatement(wh) => { - let id = ctx.next_label_id(); - let start_label = Label::WhileStart(id); - let end_label = Label::WhileEnd(id); - - ctx.push_loop(end_label, start_label); - - let test_reg = self.emit_expression(&wh.test, ctx)?; - - let end_pos = ctx.resolve_label(end_label)?; - let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp(offset)); - - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp_if_true(test_reg, offset)); - - ctx.pop_loop(); - Ok(None) - } - Statement::ForStatement(fr) => { - let id = ctx.next_label_id(); - let start_label = Label::ForStart(id); - let update_label = Label::ForUpdate(id); - let end_label = Label::ForEnd(id); - - ctx.push_loop(end_label, update_label); - - if let Some(init) = &fr.init { - if let Some(expr) = init.as_expression() { - self.emit_expression(expr, ctx)?; - } else if let ForStatementInit::VariableDeclaration(decl) = init { - for d in &decl.declarations { - let is_const = matches!(decl.kind, VariableDeclarationKind::Const); - if let Some(init_expr) = &d.init { - let val_reg = self.emit_expression(init_expr, ctx)?; - self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; - } else if let BindingPattern::BindingIdentifier(bi) = &d.id { - let var_reg = ctx.alloc_reg(); - ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; - } - } - } - } - - if let Some(test) = &fr.test { - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); - } - - self.emit_statement(&fr.body, ctx)?; - - if let Some(update) = &fr.update { - self.emit_expression(update, ctx)?; - } - - let start_pos = ctx.resolve_label(start_label)?; - let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp(offset)); - - ctx.pop_loop(); - - 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)); - 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)); - - 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, decl.kind, matches!(decl.kind, VariableDeclarationKind::Const))?; - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp(offset)); - - ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0)); - - ctx.pop_loop(); - Ok(None) - } - Statement::ForOfStatement(fo) => { - let id = ctx.next_label_id(); - let start_label = Label::ForOfStart(id); - let end_label = Label::ForOfEnd(id); - - 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 has_reg = ctx.alloc_reg(); - 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); - 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)); - match &fo.left { - ForStatementLeft::VariableDeclaration(decl) => { - for d in &decl.declarations { - self.emit_binding_pattern(&d.id, val_reg, decl.kind, false, ctx)?; - } - } - 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, val_reg, 0)); - } - ForStatementLeft::ArrayAssignmentTarget(ap) => { - self.emit_array_assignment(ap, val_reg, ctx)?; - } - ForStatementLeft::ObjectAssignmentTarget(op) => { - self.emit_object_assignment(op, val_reg, ctx)?; - } - _ => return Err("unsupported for-of left-hand side".into()), - } - - 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); - 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) - } - 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 compare_reg_checkpoint = ctx.reg_checkpoint(); - 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, case_idx as u32); - let body_pos = ctx.resolve_label(case_label)?; - let offset = (body_pos as isize) - (ctx.bytecode.len() as isize); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset)); - ctx.restore_reg_checkpoint(compare_reg_checkpoint); - } - } - - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp(offset)); - } - - 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 = 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); - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.emit(opcode::encode_jmp(offset)); - Ok(None) - } - Statement::FunctionDeclaration(fd) => { - // FunctionDeclaration: emit LOAD_CONST(BytecodeFunc) + STORE_VAR - let name = if let Some(id) = &fd.id { - id.name.to_string() - } else { - return Err("FunctionDeclaration without name".into()); - }; - - // Extract params - let mut param_names = Vec::new(); - for (idx, param) in fd.params.items.iter().enumerate() { - match ¶m.pattern { - oxide_parser::BindingPattern::BindingIdentifier(bi) => { - param_names.push(ParamSpec::Identifier(bi.name.to_string())); - } - pattern => { - param_names.push(ParamSpec::Pattern { - synthetic_name: format!("@@param_{idx}"), - pattern, - }); - } - } - } - - // Extract body statements (pass by reference) - let body_stmts: &[Statement] = if let Some(body) = &fd.body { &body.statements } else { &[] }; - - // Compile body into sub-module - let mut sub_module = self.compile_function_body(¶m_names, body_stmts, ctx, false, false)?; - sub_module.function_name = Some(name.clone()); - ctx.sub_modules.push(sub_module); - // 1-indexed: 0 = no sub_module (sentinel) - let sub_idx = ctx.sub_modules.len() as u32; - - // Use the register allocated during the count/hoist pass. Calling - // lookup_or_global here would allocate an extra register during - // emission and desynchronize later function declaration bindings. - let var_reg = ctx.lookup(&name)?; - ctx.reserve_reg(var_reg); - let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); - ctx.emit_load_const(var_reg, const_idx); - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, var_reg, 0)); - - Ok(None) - } - Statement::ClassDeclaration(class) => { - let name = class - .id - .as_ref() - .map(|id| id.name.to_string()) - .ok_or_else(|| "ClassDeclaration without name".to_string())?; - let var_reg = ctx.alloc_reg(); - ctx.declare(&name, var_reg, VariableDeclarationKind::Let, false)?; - ctx.init_var(&name); - let ctor_reg = self.emit_class(class, ctx)?; - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, ctor_reg, 0)); - Ok(None) - } - Statement::ThrowStatement(ts) => { - let exc_reg = self.emit_expression(&ts.argument, ctx)?; - ctx.emit(opcode::encode(OpCode::THROW, exc_reg, 0, 0)); - Ok(None) - } - Statement::TryStatement(ts) => { - let id = ctx.next_label_id(); - let catch_label = Label::CatchBody(id); - let try_end_label = Label::TryEnd(id); - let has_catch = ts.handler.is_some(); - let has_finally = ts.finalizer.is_some(); - - let result_reg = ctx.alloc_reg(); - let mut try_finally_begin_pos: Option = None; - let mut try_begin_pos: Option = None; - - if has_finally { - try_finally_begin_pos = Some(ctx.bytecode.len()); - ctx.emit(opcode::encode_try_finally_begin(0)); // placeholder - } - - if has_catch { - try_begin_pos = Some(ctx.bytecode.len()); - ctx.emit(opcode::encode_try_begin(0)); // placeholder - } - - let mut last_try_result: Option = None; - for s in &ts.block.body { - if let Some(r) = self.emit_statement(s, ctx)? { - last_try_result = Some(r); - } - } - ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_try_result.unwrap_or(result_reg), 0)); - - if has_catch { - ctx.emit(opcode::encode(OpCode::TRY_END, 0, 0, 0)); - } - - let jmp_needed = has_catch || has_finally; - let jmp_skip_pos = if jmp_needed { - let pos = ctx.bytecode.len(); - ctx.emit(opcode::encode_jmp(0)); // placeholder - Some(pos) - } else { - None - }; - - let catch_label_pc = ctx.bytecode.len(); - ctx.label_map.insert(catch_label, catch_label_pc); - - if let Some(try_begin_pc) = try_begin_pos { - let offset = catch_label_pc as isize - (try_begin_pc as isize); - let offset = ctx.checked_jump_offset(offset); - ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset); - } - - if let Some(catch) = &ts.handler { - ctx.push_scope(); - if let Some(param) = &catch.param { - let catch_reg = ctx.alloc_reg(); - if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { - ctx.declare_initialized(bi.name.as_str(), catch_reg, VariableDeclarationKind::Let, false)?; - ctx.emit(opcode::encode(OpCode::STORE_VAR, catch_reg, 0, 0)); - } - } - let mut last_catch_result: Option = None; - for s in &catch.body.body { - if let Some(r) = self.emit_statement(s, ctx)? { - last_catch_result = Some(r); - } - } - ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_catch_result.unwrap_or(result_reg), 0)); - ctx.pop_scope(); - } - - if has_finally { - let finally_label = Label::FinallyBody(id); - let finally_label_pc = ctx.bytecode.len(); - 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); - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset); - } - - let mut last_finally_result: Option = None; - for s in &ts.finalizer.as_ref().unwrap().body { - if let Some(r) = self.emit_statement(s, ctx)? { - last_finally_result = Some(r); - } - } - ctx.emit(opcode::encode( - OpCode::LOAD_VAR, - result_reg, - last_finally_result.unwrap_or(result_reg), - 0, - )); - 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); - let offset = ctx.checked_jump_offset(offset); - ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset); - } - - let try_end_pc = ctx.bytecode.len(); - ctx.label_map.insert(try_end_label, try_end_pc); - - Ok(Some(result_reg)) - } + Statement::ExpressionStatement(_) => self.emit_expression_statement(stmt, ctx), + Statement::VariableDeclaration(_) => self.emit_variable_declaration_statement(stmt, ctx), + Statement::ReturnStatement(_) => self.emit_return_statement(stmt, ctx), + Statement::BlockStatement(_) => self.emit_block_statement(stmt, ctx), + Statement::IfStatement(_) => self.emit_if_statement(stmt, ctx), + Statement::WhileStatement(_) => self.emit_while_statement(stmt, ctx), + Statement::DoWhileStatement(_) => self.emit_do_while_statement(stmt, ctx), + Statement::ForStatement(_) => self.emit_for_statement(stmt, ctx), + 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::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), _ => Ok(None), } } - pub(crate) fn emit_expression(&self, expr: &Expression, ctx: &mut CompileCtx) -> Result { match expr { Expression::NumericLiteral(n) => { diff --git a/crates/oxide_compiler/src/emitter/stmt/basic.rs b/crates/oxide_compiler/src/emitter/stmt/basic.rs new file mode 100644 index 0000000..042720c --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/basic.rs @@ -0,0 +1,53 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_expression_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ExpressionStatement(es) = stmt else { + return Ok(None); + }; + Ok(Some(self.emit_expression(&es.expression, ctx)?)) + } + + pub(in crate::emitter) fn emit_return_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ReturnStatement(ret) = stmt else { + return Ok(None); + }; + match &ret.argument { + Some(expr) => { + let r = self.emit_expression(expr, ctx)?; + ctx.emit(opcode::encode(OpCode::RETURN, r, 0, 0)); + } + None => { + ctx.emit(opcode::encode(OpCode::RETURN, 0, 0, 0)); + } + } + Ok(None) + } + + pub(in crate::emitter) fn emit_break_statement(&self, ctx: &mut CompileCtx) -> Result, String> { + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + Ok(None) + } + + pub(in crate::emitter) fn emit_continue_statement(&self, ctx: &mut CompileCtx) -> Result, String> { + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + Ok(None) + } +} diff --git a/crates/oxide_compiler/src/emitter/stmt/block.rs b/crates/oxide_compiler/src/emitter/stmt/block.rs new file mode 100644 index 0000000..8d0cb7e --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/block.rs @@ -0,0 +1,20 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_block_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::BlockStatement(block) = stmt else { + return Ok(None); + }; + ctx.push_scope(); + let mut r = None; + for s in &block.body { + if let Some(rr) = self.emit_statement(s, ctx)? { + r = Some(rr); + } + } + ctx.pop_scope(); + Ok(r) + } +} diff --git a/crates/oxide_compiler/src/emitter/stmt/control.rs b/crates/oxide_compiler/src/emitter/stmt/control.rs new file mode 100644 index 0000000..d1edb97 --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/control.rs @@ -0,0 +1,49 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_if_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::IfStatement(ifs) = stmt else { + return Ok(None); + }; + let id = ctx.next_label_id(); + let else_label = Label::IfElse(id); + let end_label = Label::IfEnd(id); + + let test_reg = self.emit_expression(&ifs.test, ctx)?; + + let else_pos = ctx.resolve_label(else_label)?; + let offset = (else_pos as isize) - (ctx.bytecode.len() as isize); + 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(); + if let Some(r) = cons_reg { + ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, r, 0)); + } else { + let undef_idx = ctx.add_constant(Constant::Undefined); + ctx.emit_load_const(result_reg, undef_idx); + } + + if ifs.alternate.is_some() { + let end_pos = ctx.resolve_label(end_label)?; + let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + } + + if let Some(alt) = &ifs.alternate { + let alt_reg = self.emit_statement(alt, ctx)?; + if let Some(r) = alt_reg { + ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, r, 0)); + } else { + let undef_idx = ctx.add_constant(Constant::Undefined); + ctx.emit_load_const(result_reg, undef_idx); + } + } + + Ok(Some(result_reg)) + } +} diff --git a/crates/oxide_compiler/src/emitter/stmt/declaration.rs b/crates/oxide_compiler/src/emitter/stmt/declaration.rs new file mode 100644 index 0000000..67ae72f --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/declaration.rs @@ -0,0 +1,105 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_variable_declaration_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::VariableDeclaration(decl) = stmt else { + return Ok(None); + }; + let mut r = None; + for d in &decl.declarations { + let is_const = matches!(decl.kind, VariableDeclarationKind::Const); + if is_const && d.init.is_none() { + return Err("const declaration must have an initializer".into()); + } + if let Some(init) = &d.init { + let val_reg = self.emit_expression(init, ctx)?; + self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; + if let BindingPattern::BindingIdentifier(bi) = &d.id { + if matches!(*init, Expression::ArrowFunctionExpression(_)) { + if let Some(sub_mod) = ctx.sub_modules.last_mut() { + sub_mod.function_name = Some(bi.name.to_string()); + } + } + } + r = Some(val_reg); + } else { + let BindingPattern::BindingIdentifier(bi) = &d.id else { + return Err("destructuring declaration requires an initializer".into()); + }; + let idx = ctx.add_constant(Constant::Undefined); + let tmp = ctx.alloc_reg(); + ctx.emit_load_const(tmp, idx); + let var_reg = ctx.alloc_reg(); + ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); + ctx.init_var(bi.name.as_str()); + r = Some(var_reg); + } + } + Ok(r) + } + + pub(in crate::emitter) fn emit_function_declaration_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::FunctionDeclaration(fd) = stmt else { + return Ok(None); + }; + let name = if let Some(id) = &fd.id { + id.name.to_string() + } else { + return Err("FunctionDeclaration without name".into()); + }; + + let mut param_names = Vec::new(); + for (idx, param) in fd.params.items.iter().enumerate() { + match ¶m.pattern { + oxide_parser::BindingPattern::BindingIdentifier(bi) => { + param_names.push(ParamSpec::Identifier(bi.name.to_string())); + } + pattern => { + param_names.push(ParamSpec::Pattern { + synthetic_name: format!("@@param_{idx}"), + pattern, + }); + } + } + } + + let body_stmts: &[Statement] = if let Some(body) = &fd.body { &body.statements } else { &[] }; + + let mut sub_module = self.compile_function_body(¶m_names, body_stmts, ctx, false, false)?; + sub_module.function_name = Some(name.clone()); + ctx.sub_modules.push(sub_module); + let sub_idx = ctx.sub_modules.len() as u32; + + let var_reg = ctx.lookup(&name)?; + ctx.reserve_reg(var_reg); + let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); + ctx.emit_load_const(var_reg, const_idx); + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, var_reg, 0)); + + Ok(None) + } + + pub(in crate::emitter) fn emit_class_declaration_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ClassDeclaration(class) = stmt else { + return Ok(None); + }; + let name = class + .id + .as_ref() + .map(|id| id.name.to_string()) + .ok_or_else(|| "ClassDeclaration without name".to_string())?; + let var_reg = ctx.alloc_reg(); + ctx.declare(&name, var_reg, VariableDeclarationKind::Let, false)?; + ctx.init_var(&name); + let ctor_reg = self.emit_class(class, ctx)?; + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, ctor_reg, 0)); + Ok(None) + } +} diff --git a/crates/oxide_compiler/src/emitter/stmt/exception.rs b/crates/oxide_compiler/src/emitter/stmt/exception.rs new file mode 100644 index 0000000..22012c5 --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/exception.rs @@ -0,0 +1,125 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_throw_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ThrowStatement(ts) = stmt else { + return Ok(None); + }; + let exc_reg = self.emit_expression(&ts.argument, ctx)?; + ctx.emit(opcode::encode(OpCode::THROW, exc_reg, 0, 0)); + Ok(None) + } + + pub(in crate::emitter) fn emit_try_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::TryStatement(ts) = stmt else { + return Ok(None); + }; + let id = ctx.next_label_id(); + let catch_label = Label::CatchBody(id); + let try_end_label = Label::TryEnd(id); + let has_catch = ts.handler.is_some(); + let has_finally = ts.finalizer.is_some(); + + let result_reg = ctx.alloc_reg(); + let mut try_finally_begin_pos: Option = None; + let mut try_begin_pos: Option = None; + + if has_finally { + try_finally_begin_pos = Some(ctx.bytecode.len()); + ctx.emit(opcode::encode_try_finally_begin(0)); + } + + if has_catch { + try_begin_pos = Some(ctx.bytecode.len()); + ctx.emit(opcode::encode_try_begin(0)); + } + + let mut last_try_result: Option = None; + for s in &ts.block.body { + if let Some(r) = self.emit_statement(s, ctx)? { + last_try_result = Some(r); + } + } + ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_try_result.unwrap_or(result_reg), 0)); + + if has_catch { + ctx.emit(opcode::encode(OpCode::TRY_END, 0, 0, 0)); + } + + let jmp_needed = has_catch || has_finally; + let jmp_skip_pos = if jmp_needed { + let pos = ctx.bytecode.len(); + ctx.emit(opcode::encode_jmp(0)); + Some(pos) + } else { + None + }; + + let catch_label_pc = ctx.bytecode.len(); + ctx.label_map.insert(catch_label, catch_label_pc); + + if let Some(try_begin_pc) = try_begin_pos { + let offset = catch_label_pc as isize - (try_begin_pc as isize); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[try_begin_pc] = opcode::encode_try_begin(offset); + } + + if let Some(catch) = &ts.handler { + ctx.push_scope(); + if let Some(param) = &catch.param { + let catch_reg = ctx.alloc_reg(); + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { + ctx.declare_initialized(bi.name.as_str(), catch_reg, VariableDeclarationKind::Let, false)?; + ctx.emit(opcode::encode(OpCode::STORE_VAR, catch_reg, 0, 0)); + } + } + let mut last_catch_result: Option = None; + for s in &catch.body.body { + if let Some(r) = self.emit_statement(s, ctx)? { + last_catch_result = Some(r); + } + } + ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_catch_result.unwrap_or(result_reg), 0)); + ctx.pop_scope(); + } + + if has_finally { + let finally_label = Label::FinallyBody(id); + let finally_label_pc = ctx.bytecode.len(); + 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); + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset); + } + + let mut last_finally_result: Option = None; + for s in &ts.finalizer.as_ref().unwrap().body { + if let Some(r) = self.emit_statement(s, ctx)? { + last_finally_result = Some(r); + } + } + ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, last_finally_result.unwrap_or(result_reg), 0)); + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.bytecode[jmp_pos] = opcode::encode_jmp(offset); + } + + let try_end_pc = ctx.bytecode.len(); + ctx.label_map.insert(try_end_label, try_end_pc); + + Ok(Some(result_reg)) + } +} diff --git a/crates/oxide_compiler/src/emitter/stmt/iteration.rs b/crates/oxide_compiler/src/emitter/stmt/iteration.rs new file mode 100644 index 0000000..b574205 --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/iteration.rs @@ -0,0 +1,223 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_while_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::WhileStatement(wh) = stmt else { + return Ok(None); + }; + let id = ctx.next_label_id(); + let start_label = Label::WhileStart(id); + let end_label = Label::WhileEnd(id); + + ctx.push_loop(end_label, start_label); + + let test_reg = self.emit_expression(&wh.test, ctx)?; + let end_pos = ctx.resolve_label(end_label)?; + let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + + ctx.pop_loop(); + Ok(None) + } + + pub(in crate::emitter) fn emit_do_while_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::DoWhileStatement(dw) = stmt else { + return Ok(None); + }; + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_true(test_reg, offset)); + + ctx.pop_loop(); + Ok(None) + } + + pub(in crate::emitter) fn emit_for_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ForStatement(fr) = stmt else { + return Ok(None); + }; + let id = ctx.next_label_id(); + let start_label = Label::ForStart(id); + let update_label = Label::ForUpdate(id); + let end_label = Label::ForEnd(id); + + ctx.push_loop(end_label, update_label); + + if let Some(init) = &fr.init { + if let Some(expr) = init.as_expression() { + self.emit_expression(expr, ctx)?; + } else if let ForStatementInit::VariableDeclaration(decl) = init { + for d in &decl.declarations { + let is_const = matches!(decl.kind, VariableDeclarationKind::Const); + if let Some(init_expr) = &d.init { + let val_reg = self.emit_expression(init_expr, ctx)?; + self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; + } else if let BindingPattern::BindingIdentifier(bi) = &d.id { + let var_reg = ctx.alloc_reg(); + ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; + } + } + } + } + + if let Some(test) = &fr.test { + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_false(test_reg, offset)); + } + + self.emit_statement(&fr.body, ctx)?; + + if let Some(update) = &fr.update { + self.emit_expression(update, ctx)?; + } + + let start_pos = ctx.resolve_label(start_label)?; + let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + + ctx.pop_loop(); + + Ok(None) + } + + pub(in crate::emitter) fn emit_for_in_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ForInStatement(fi) = stmt else { + return Ok(None); + }; + 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)); + 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)); + + 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, decl.kind, matches!(decl.kind, VariableDeclarationKind::Const))?; + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + + ctx.emit(opcode::encode(OpCode::FOR_IN_CLEANUP, 0, 0, 0)); + + ctx.pop_loop(); + Ok(None) + } + + pub(in crate::emitter) fn emit_for_of_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::ForOfStatement(fo) = stmt else { + return Ok(None); + }; + let id = ctx.next_label_id(); + let start_label = Label::ForOfStart(id); + let end_label = Label::ForOfEnd(id); + + 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 has_reg = ctx.alloc_reg(); + 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); + 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)); + match &fo.left { + ForStatementLeft::VariableDeclaration(decl) => { + for d in &decl.declarations { + self.emit_binding_pattern(&d.id, val_reg, decl.kind, false, ctx)?; + } + } + 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, val_reg, 0)); + } + ForStatementLeft::ArrayAssignmentTarget(ap) => { + self.emit_array_assignment(ap, val_reg, ctx)?; + } + ForStatementLeft::ObjectAssignmentTarget(op) => { + self.emit_object_assignment(op, val_reg, ctx)?; + } + _ => return Err("unsupported for-of left-hand side".into()), + } + + 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); + 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) + } +} diff --git a/crates/oxide_compiler/src/emitter/stmt/mod.rs b/crates/oxide_compiler/src/emitter/stmt/mod.rs new file mode 100644 index 0000000..a0650df --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/mod.rs @@ -0,0 +1,9 @@ +use super::*; + +mod basic; +mod block; +mod control; +mod declaration; +mod exception; +mod iteration; +mod switch; diff --git a/crates/oxide_compiler/src/emitter/stmt/switch.rs b/crates/oxide_compiler/src/emitter/stmt/switch.rs new file mode 100644 index 0000000..a6cd47d --- /dev/null +++ b/crates/oxide_compiler/src/emitter/stmt/switch.rs @@ -0,0 +1,50 @@ +use super::*; + +impl Compiler { + pub(in crate::emitter) fn emit_switch_statement( + &self, stmt: &Statement, ctx: &mut CompileCtx, + ) -> Result, String> { + let Statement::SwitchStatement(sw) = stmt else { + return Ok(None); + }; + 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 compare_reg_checkpoint = ctx.reg_checkpoint(); + 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, case_idx as u32); + let body_pos = ctx.resolve_label(case_label)?; + let offset = (body_pos as isize) - (ctx.bytecode.len() as isize); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp_if_true(eq_reg, offset)); + ctx.restore_reg_checkpoint(compare_reg_checkpoint); + } + } + + 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); + let offset = ctx.checked_jump_offset(offset); + ctx.emit(opcode::encode_jmp(offset)); + } + + for case in cases.iter() { + for s in &case.consequent { + self.emit_statement(s, ctx)?; + } + } + + ctx.pop_switch(); + Ok(None) + } +}