feat: 支持字节码函数声明与调用
This commit is contained in:
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::module::CompiledModule;
|
||||
use crate::opcode::{self, OpCode};
|
||||
use crate::symbol_table::SymbolTable;
|
||||
use crate::symbol_table::{Binding, SymbolTable};
|
||||
|
||||
pub use crate::hash::structural_hash;
|
||||
pub use crate::module::Constant;
|
||||
@@ -58,6 +58,7 @@ pub(crate) struct CompileCtx {
|
||||
pub(crate) label_counter: u32,
|
||||
pub(crate) projected_pc: usize,
|
||||
pub(crate) builtin_reg_map: Vec<(String, u8)>,
|
||||
pub(crate) sub_modules: Vec<CompiledModule>,
|
||||
}
|
||||
|
||||
impl CompileCtx {
|
||||
@@ -74,6 +75,7 @@ impl CompileCtx {
|
||||
label_counter: 0,
|
||||
projected_pc: 0,
|
||||
builtin_reg_map: Vec::new(),
|
||||
sub_modules: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +126,10 @@ impl CompileCtx {
|
||||
self.symbols.declare(name, reg)
|
||||
}
|
||||
|
||||
pub(crate) fn declare_initialized(&mut self, name: &str, reg: u8) -> Result<(), String> {
|
||||
self.symbols.declare_initialized(name, reg)
|
||||
}
|
||||
|
||||
pub(crate) fn lookup(&self, name: &str) -> Result<u8, String> {
|
||||
self.symbols.lookup(name)
|
||||
}
|
||||
@@ -203,6 +209,84 @@ impl Compiler {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Compile a function body (used for FD and FE).
|
||||
/// This performs both counting and emitting in one pass.
|
||||
pub(crate) fn compile_function_body<'a>(
|
||||
&self,
|
||||
param_names: &[String],
|
||||
body_stmts: &[Statement<'a>],
|
||||
parent_ctx: &CompileCtx,
|
||||
) -> Result<CompiledModule, String> {
|
||||
let mut ctx = CompileCtx::new();
|
||||
|
||||
// Inherit parent's builtin_reg_map so builtin identifiers (Math, Object, etc.)
|
||||
// resolve to the correct pre-allocated registers in the sub-module's register file.
|
||||
ctx.builtin_reg_map = parent_ctx.builtin_reg_map.clone();
|
||||
|
||||
// Inherit parent's global scope entries so previously-declared function names
|
||||
// are visible from within the body.
|
||||
for (name, binding) in &parent_ctx.symbols.scopes[0] {
|
||||
ctx.symbols.scopes[0].insert(
|
||||
name.clone(),
|
||||
Binding {
|
||||
reg: binding.reg,
|
||||
initialized: binding.initialized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Align next_reg with builtin count so both count and emit passes start at the
|
||||
// same register offset (params go after builtin slots).
|
||||
ctx.reset_regs();
|
||||
|
||||
// Function body scope - params and local vars
|
||||
ctx.push_scope();
|
||||
|
||||
// Register parameters as initialized.
|
||||
for name in param_names {
|
||||
let reg = ctx.alloc_reg();
|
||||
ctx.declare_initialized(name, reg)?;
|
||||
}
|
||||
|
||||
// Count pass
|
||||
for stmt in body_stmts {
|
||||
self.count_statement(stmt, &mut ctx);
|
||||
}
|
||||
ctx.max_regs = ctx.max_regs.max(1);
|
||||
ctx.reset_regs();
|
||||
|
||||
// Emit pass - reallocate params (same order = same regs after reset)
|
||||
for name in param_names {
|
||||
let reg = ctx.alloc_reg();
|
||||
ctx.declare_initialized(name, reg)?;
|
||||
}
|
||||
|
||||
// Emit body statements.
|
||||
// After all statements, emit an implicit RETURN undefined.
|
||||
for stmt in body_stmts {
|
||||
let _ = self.emit_statement(stmt, &mut ctx)?;
|
||||
}
|
||||
|
||||
let undef_idx = ctx.add_constant(Constant::Undefined);
|
||||
let undef_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(
|
||||
OpCode::LOAD_CONST,
|
||||
undef_reg,
|
||||
(undef_idx & 0xFF) as u8,
|
||||
((undef_idx >> 8) & 0xFF) as u8,
|
||||
));
|
||||
ctx.emit(opcode::encode(OpCode::RETURN, undef_reg, 0, 0));
|
||||
|
||||
Ok(CompiledModule {
|
||||
bytecode: ctx.bytecode,
|
||||
constants: ctx.constants,
|
||||
n_registers: ctx.max_regs,
|
||||
n_args: param_names.len() as u8,
|
||||
builtin_reg_map: ctx.builtin_reg_map,
|
||||
sub_modules: ctx.sub_modules,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compile(&self, program: &oxide_parser::Program) -> Result<CompiledModule, String> {
|
||||
let mut ctx = CompileCtx::new();
|
||||
ctx.pre_register_builtins();
|
||||
@@ -213,8 +297,20 @@ impl Compiler {
|
||||
ctx.max_regs = ctx.max_regs.max(1);
|
||||
ctx.reset_regs();
|
||||
|
||||
// First sub-pass: emit FunctionDeclarations (hoisting)
|
||||
// This ensures function objects are available before any code runs.
|
||||
for stmt in &program.body {
|
||||
if matches!(stmt, Statement::FunctionDeclaration(_)) {
|
||||
self.emit_statement(stmt, &mut ctx)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Second sub-pass: emit all other statements
|
||||
let mut last_result: Option<u8> = None;
|
||||
for stmt in &program.body {
|
||||
if matches!(stmt, Statement::FunctionDeclaration(_)) {
|
||||
continue; // Already emitted above
|
||||
}
|
||||
match self.emit_statement(stmt, &mut ctx)? {
|
||||
Some(r) => last_result = Some(r),
|
||||
None => last_result = None,
|
||||
@@ -238,8 +334,9 @@ impl Compiler {
|
||||
bytecode: ctx.bytecode,
|
||||
constants: ctx.constants,
|
||||
n_registers: ctx.max_regs,
|
||||
n_args: 0,
|
||||
builtin_reg_map: ctx.builtin_reg_map,
|
||||
sub_modules: Vec::new(),
|
||||
sub_modules: ctx.sub_modules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,21 @@ impl Compiler {
|
||||
self.count_statement(s, ctx);
|
||||
}
|
||||
}
|
||||
Statement::FunctionDeclaration(fd) => {
|
||||
let name = if let Some(id) = &fd.id {
|
||||
id.name.to_string()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Hoisting: declare function name as initialized
|
||||
let func_reg = ctx.alloc_reg();
|
||||
let _ = ctx.declare_initialized(&name, func_reg);
|
||||
|
||||
// Body is compiled in the emit pass only.
|
||||
// FD emits LOAD_CONST(BytecodeFunc) + STORE_VAR
|
||||
ctx.projected_pc += 2;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -217,7 +232,21 @@ impl Compiler {
|
||||
self.count_expression(expr, ctx);
|
||||
}
|
||||
}
|
||||
ctx.projected_pc += 2;
|
||||
// CALL_NATIVE: 1 opcode + 1 ext word; CALL(0x40): 1 opcode
|
||||
let is_builtin = match &call.callee {
|
||||
Expression::Identifier(ident) => ctx.is_builtin(ident.name.as_str()),
|
||||
Expression::StaticMemberExpression(m) => {
|
||||
if let Expression::Identifier(ident) = &m.object {
|
||||
ctx.is_builtin(ident.name.as_str())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
ctx.projected_pc += if is_builtin { 2 } else { 1 };
|
||||
ctx.alloc_reg(); // result register
|
||||
ctx.projected_pc += 1; // LOAD_VAR result <- regs[0]
|
||||
}
|
||||
Expression::AssignmentExpression(assign) => {
|
||||
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left
|
||||
@@ -304,6 +333,24 @@ impl Compiler {
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression::FunctionExpression(_fe) => {
|
||||
// No hoisting - function created at expression position.
|
||||
// Body is compiled in the emit pass only.
|
||||
ctx.alloc_reg();
|
||||
ctx.projected_pc += 1; // LOAD_CONST
|
||||
}
|
||||
Expression::NewExpression(ne) => {
|
||||
// Count callee expression
|
||||
self.count_expression(&ne.callee, ctx);
|
||||
// Count arguments
|
||||
for arg in &ne.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
self.count_expression(expr, ctx);
|
||||
}
|
||||
}
|
||||
ctx.alloc_reg(); // result register
|
||||
ctx.projected_pc += 2; // NEW_EXPRESSION + ext word
|
||||
}
|
||||
Expression::ArrayExpression(arr) => {
|
||||
ctx.alloc_reg();
|
||||
ctx.projected_pc += 1; // NEW_ARRAY
|
||||
|
||||
@@ -326,6 +326,51 @@ impl Compiler {
|
||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||
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 param in &fd.params.items {
|
||||
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern {
|
||||
param_names.push(bi.name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 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 sub_module = self.compile_function_body(¶m_names, body_stmts, ctx)?;
|
||||
ctx.sub_modules.push(sub_module);
|
||||
// 1-indexed: 0 = no sub_module (sentinel)
|
||||
let sub_idx = ctx.sub_modules.len() as u32;
|
||||
|
||||
// Add BytecodeFunc constant and emit LOAD_CONST + STORE_VAR
|
||||
let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx));
|
||||
let func_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(
|
||||
OpCode::LOAD_CONST,
|
||||
func_reg,
|
||||
(const_idx & 0xFF) as u8,
|
||||
((const_idx >> 8) & 0xFF) as u8,
|
||||
));
|
||||
|
||||
// Get the register that was allocated for this FD in the counter pass
|
||||
let var_reg = ctx.lookup_or_global(&name);
|
||||
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, func_reg, 0));
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -745,6 +790,59 @@ impl Compiler {
|
||||
ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, var_reg, 0));
|
||||
Ok(r)
|
||||
}
|
||||
Expression::FunctionExpression(fe) => {
|
||||
// FunctionExpression: compile body, emit LOAD_CONST(BytecodeFunc)
|
||||
let mut param_names = Vec::new();
|
||||
for param in &fe.params.items {
|
||||
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern {
|
||||
param_names.push(bi.name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let body_stmts: &[Statement] = if let Some(body) = &fe.body {
|
||||
&body.statements
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
let sub_module = self.compile_function_body(¶m_names, body_stmts, ctx)?;
|
||||
ctx.sub_modules.push(sub_module);
|
||||
// 1-indexed: 0 = no sub_module (sentinel)
|
||||
let sub_idx = ctx.sub_modules.len() as u32;
|
||||
|
||||
let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx));
|
||||
let r = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(
|
||||
OpCode::LOAD_CONST,
|
||||
r,
|
||||
(const_idx & 0xFF) as u8,
|
||||
((const_idx >> 8) & 0xFF) as u8,
|
||||
));
|
||||
Ok(r)
|
||||
}
|
||||
Expression::NewExpression(ne) => {
|
||||
let constructor_reg = self.emit_expression(&ne.callee, ctx)?;
|
||||
let mut arg_regs = Vec::new();
|
||||
for arg in &ne.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
arg_regs.push(self.emit_expression(expr, ctx)?);
|
||||
}
|
||||
}
|
||||
let first_arg_reg = if arg_regs.is_empty() {
|
||||
0u8
|
||||
} else {
|
||||
arg_regs[0]
|
||||
};
|
||||
let r = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(
|
||||
OpCode::NEW_EXPRESSION,
|
||||
r,
|
||||
constructor_reg,
|
||||
first_arg_reg,
|
||||
));
|
||||
ctx.emit(arg_regs.len() as u32);
|
||||
Ok(r)
|
||||
}
|
||||
Expression::ParenthesizedExpression(p) => self.emit_expression(&p.expression, ctx),
|
||||
Expression::CallExpression(call) => {
|
||||
let (callee_reg, this_reg) = match &call.callee {
|
||||
@@ -795,11 +893,28 @@ impl Compiler {
|
||||
Expression::Identifier(ident) if ctx.is_builtin(ident.name.as_str()) => {
|
||||
OpCode::CALL_NATIVE
|
||||
}
|
||||
Expression::StaticMemberExpression(m) => {
|
||||
if let Expression::Identifier(ident) = &m.object {
|
||||
if ctx.is_builtin(ident.name.as_str()) {
|
||||
OpCode::CALL_NATIVE
|
||||
} else {
|
||||
OpCode::CALL
|
||||
}
|
||||
} else {
|
||||
OpCode::CALL
|
||||
}
|
||||
}
|
||||
_ => OpCode::CALL,
|
||||
};
|
||||
ctx.emit(opcode::encode(op, callee_reg, this_reg, first_arg_reg));
|
||||
ctx.emit(arg_regs.len() as u32);
|
||||
Ok(0u8)
|
||||
if matches!(op, OpCode::CALL_NATIVE) {
|
||||
ctx.emit(arg_regs.len() as u32);
|
||||
}
|
||||
// Copy result from regs[0] into a dedicated register so multiple
|
||||
// call expressions don't overwrite each other.
|
||||
let result_reg = ctx.alloc_reg();
|
||||
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, 0, 0));
|
||||
Ok(result_reg)
|
||||
}
|
||||
_ => Err(format!("unsupported expression type: {:?}", expr)),
|
||||
}
|
||||
|
||||
@@ -104,6 +104,18 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Statement::FunctionDeclaration(fd) => {
|
||||
12u8.hash(h);
|
||||
if let Some(id) = &fd.id {
|
||||
id.name.as_str().hash(h);
|
||||
}
|
||||
(fd.params.items.len() as u32).hash(h);
|
||||
if let Some(body) = &fd.body {
|
||||
for s in &body.statements {
|
||||
hash_statement(s, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
std::mem::discriminant(stmt).hash(h);
|
||||
}
|
||||
@@ -187,6 +199,25 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Expression::FunctionExpression(fe) => {
|
||||
13u8.hash(h);
|
||||
(fe.params.items.len() as u32).hash(h);
|
||||
if let Some(body) = &fe.body {
|
||||
for s in &body.statements {
|
||||
hash_statement(s, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression::NewExpression(ne) => {
|
||||
14u8.hash(h);
|
||||
hash_expression(&ne.callee, h);
|
||||
(ne.arguments.len() as u32).hash(h);
|
||||
for arg in &ne.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
hash_expression(expr, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
std::mem::discriminant(expr).hash(h);
|
||||
}
|
||||
|
||||
@@ -17,16 +17,37 @@ pub struct CompiledModule {
|
||||
pub bytecode: Vec<opcode::Instr>,
|
||||
pub constants: Vec<Constant>,
|
||||
pub n_registers: u8,
|
||||
pub n_args: u8,
|
||||
pub builtin_reg_map: Vec<(String, u8)>,
|
||||
pub sub_modules: Vec<CompiledModule>,
|
||||
}
|
||||
|
||||
impl CompiledModule {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bytecode: Vec::new(),
|
||||
constants: Vec::new(),
|
||||
n_registers: 0,
|
||||
n_args: 0,
|
||||
builtin_reg_map: Vec::new(),
|
||||
sub_modules: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CompiledModule {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for CompiledModule {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
bytecode: self.bytecode.clone(),
|
||||
constants: self.constants.clone(),
|
||||
n_registers: self.n_registers,
|
||||
n_args: self.n_args,
|
||||
builtin_reg_map: self.builtin_reg_map.clone(),
|
||||
sub_modules: self.sub_modules.clone(),
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct Binding {
|
||||
reg: u8,
|
||||
initialized: bool,
|
||||
pub(crate) struct Binding {
|
||||
pub(crate) reg: u8,
|
||||
pub(crate) initialized: bool,
|
||||
}
|
||||
|
||||
pub struct SymbolTable {
|
||||
scopes: Vec<HashMap<String, Binding>>,
|
||||
pub(crate) scopes: Vec<HashMap<String, Binding>>,
|
||||
}
|
||||
|
||||
impl Default for SymbolTable {
|
||||
@@ -73,6 +73,24 @@ impl SymbolTable {
|
||||
reg_for_new
|
||||
}
|
||||
|
||||
/// Declare a variable that is already initialized (for FunctionDeclaration hoisting).
|
||||
/// If the name already exists in the current scope, just mark it as initialized.
|
||||
pub fn declare_initialized(&mut self, name: &str, reg: u8) -> Result<(), String> {
|
||||
let current = self.scopes.last_mut().unwrap();
|
||||
if let Some(b) = current.get_mut(name) {
|
||||
b.initialized = true;
|
||||
return Ok(());
|
||||
}
|
||||
current.insert(
|
||||
name.to_string(),
|
||||
Binding {
|
||||
reg,
|
||||
initialized: true,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pre_register_global(&mut self, name: &str, reg: u8) {
|
||||
self.scopes[0].entry(name.to_string()).or_insert(Binding {
|
||||
reg,
|
||||
|
||||
Reference in New Issue
Block a user