feat: 支持字节码函数声明与调用
This commit is contained in:
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use crate::module::CompiledModule;
|
use crate::module::CompiledModule;
|
||||||
use crate::opcode::{self, OpCode};
|
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::hash::structural_hash;
|
||||||
pub use crate::module::Constant;
|
pub use crate::module::Constant;
|
||||||
@@ -58,6 +58,7 @@ pub(crate) struct CompileCtx {
|
|||||||
pub(crate) label_counter: u32,
|
pub(crate) label_counter: u32,
|
||||||
pub(crate) projected_pc: usize,
|
pub(crate) projected_pc: usize,
|
||||||
pub(crate) builtin_reg_map: Vec<(String, u8)>,
|
pub(crate) builtin_reg_map: Vec<(String, u8)>,
|
||||||
|
pub(crate) sub_modules: Vec<CompiledModule>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompileCtx {
|
impl CompileCtx {
|
||||||
@@ -74,6 +75,7 @@ impl CompileCtx {
|
|||||||
label_counter: 0,
|
label_counter: 0,
|
||||||
projected_pc: 0,
|
projected_pc: 0,
|
||||||
builtin_reg_map: Vec::new(),
|
builtin_reg_map: Vec::new(),
|
||||||
|
sub_modules: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +126,10 @@ impl CompileCtx {
|
|||||||
self.symbols.declare(name, reg)
|
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> {
|
pub(crate) fn lookup(&self, name: &str) -> Result<u8, String> {
|
||||||
self.symbols.lookup(name)
|
self.symbols.lookup(name)
|
||||||
}
|
}
|
||||||
@@ -203,6 +209,84 @@ impl Compiler {
|
|||||||
Self
|
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> {
|
pub fn compile(&self, program: &oxide_parser::Program) -> Result<CompiledModule, String> {
|
||||||
let mut ctx = CompileCtx::new();
|
let mut ctx = CompileCtx::new();
|
||||||
ctx.pre_register_builtins();
|
ctx.pre_register_builtins();
|
||||||
@@ -213,8 +297,20 @@ impl Compiler {
|
|||||||
ctx.max_regs = ctx.max_regs.max(1);
|
ctx.max_regs = ctx.max_regs.max(1);
|
||||||
ctx.reset_regs();
|
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;
|
let mut last_result: Option<u8> = None;
|
||||||
for stmt in &program.body {
|
for stmt in &program.body {
|
||||||
|
if matches!(stmt, Statement::FunctionDeclaration(_)) {
|
||||||
|
continue; // Already emitted above
|
||||||
|
}
|
||||||
match self.emit_statement(stmt, &mut ctx)? {
|
match self.emit_statement(stmt, &mut ctx)? {
|
||||||
Some(r) => last_result = Some(r),
|
Some(r) => last_result = Some(r),
|
||||||
None => last_result = None,
|
None => last_result = None,
|
||||||
@@ -238,8 +334,9 @@ impl Compiler {
|
|||||||
bytecode: ctx.bytecode,
|
bytecode: ctx.bytecode,
|
||||||
constants: ctx.constants,
|
constants: ctx.constants,
|
||||||
n_registers: ctx.max_regs,
|
n_registers: ctx.max_regs,
|
||||||
|
n_args: 0,
|
||||||
builtin_reg_map: ctx.builtin_reg_map,
|
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);
|
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);
|
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) => {
|
Expression::AssignmentExpression(assign) => {
|
||||||
if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left
|
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) => {
|
Expression::ArrayExpression(arr) => {
|
||||||
ctx.alloc_reg();
|
ctx.alloc_reg();
|
||||||
ctx.projected_pc += 1; // NEW_ARRAY
|
ctx.projected_pc += 1; // NEW_ARRAY
|
||||||
|
|||||||
@@ -326,6 +326,51 @@ impl Compiler {
|
|||||||
ctx.emit(opcode::encode_jmp(offset as i16));
|
ctx.emit(opcode::encode_jmp(offset as i16));
|
||||||
Ok(None)
|
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),
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,6 +790,59 @@ impl Compiler {
|
|||||||
ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, var_reg, 0));
|
ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, var_reg, 0));
|
||||||
Ok(r)
|
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::ParenthesizedExpression(p) => self.emit_expression(&p.expression, ctx),
|
||||||
Expression::CallExpression(call) => {
|
Expression::CallExpression(call) => {
|
||||||
let (callee_reg, this_reg) = match &call.callee {
|
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()) => {
|
Expression::Identifier(ident) if ctx.is_builtin(ident.name.as_str()) => {
|
||||||
OpCode::CALL_NATIVE
|
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,
|
_ => OpCode::CALL,
|
||||||
};
|
};
|
||||||
ctx.emit(opcode::encode(op, callee_reg, this_reg, first_arg_reg));
|
ctx.emit(opcode::encode(op, callee_reg, this_reg, first_arg_reg));
|
||||||
ctx.emit(arg_regs.len() as u32);
|
if matches!(op, OpCode::CALL_NATIVE) {
|
||||||
Ok(0u8)
|
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)),
|
_ => 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);
|
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);
|
std::mem::discriminant(expr).hash(h);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,37 @@ pub struct CompiledModule {
|
|||||||
pub bytecode: Vec<opcode::Instr>,
|
pub bytecode: Vec<opcode::Instr>,
|
||||||
pub constants: Vec<Constant>,
|
pub constants: Vec<Constant>,
|
||||||
pub n_registers: u8,
|
pub n_registers: u8,
|
||||||
|
pub n_args: u8,
|
||||||
pub builtin_reg_map: Vec<(String, u8)>,
|
pub builtin_reg_map: Vec<(String, u8)>,
|
||||||
pub sub_modules: Vec<CompiledModule>,
|
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 {
|
impl Clone for CompiledModule {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
bytecode: self.bytecode.clone(),
|
bytecode: self.bytecode.clone(),
|
||||||
constants: self.constants.clone(),
|
constants: self.constants.clone(),
|
||||||
n_registers: self.n_registers,
|
n_registers: self.n_registers,
|
||||||
|
n_args: self.n_args,
|
||||||
builtin_reg_map: self.builtin_reg_map.clone(),
|
builtin_reg_map: self.builtin_reg_map.clone(),
|
||||||
sub_modules: self.sub_modules.clone(),
|
sub_modules: self.sub_modules.clone(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
struct Binding {
|
pub(crate) struct Binding {
|
||||||
reg: u8,
|
pub(crate) reg: u8,
|
||||||
initialized: bool,
|
pub(crate) initialized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SymbolTable {
|
pub struct SymbolTable {
|
||||||
scopes: Vec<HashMap<String, Binding>>,
|
pub(crate) scopes: Vec<HashMap<String, Binding>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SymbolTable {
|
impl Default for SymbolTable {
|
||||||
@@ -73,6 +73,24 @@ impl SymbolTable {
|
|||||||
reg_for_new
|
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) {
|
pub fn pre_register_global(&mut self, name: &str, reg: u8) {
|
||||||
self.scopes[0].entry(name.to_string()).or_insert(Binding {
|
self.scopes[0].entry(name.to_string()).or_insert(Binding {
|
||||||
reg,
|
reg,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::value::JsValue;
|
|||||||
|
|
||||||
pub type ShapeId = u32;
|
pub type ShapeId = u32;
|
||||||
|
|
||||||
/// Layout (40B):
|
/// Layout (48B):
|
||||||
/// header: u32 bits
|
/// header: u32 bits
|
||||||
/// [0:23] shape_id
|
/// [0:23] shape_id
|
||||||
/// [29] is_array
|
/// [29] is_array
|
||||||
@@ -13,6 +13,7 @@ pub type ShapeId = u32;
|
|||||||
/// proto: JsValue (8 bytes)
|
/// proto: JsValue (8 bytes)
|
||||||
/// generation: u32 (4 bytes + 4 pad)
|
/// generation: u32 (4 bytes + 4 pad)
|
||||||
/// native_fn: u64 (8 bytes, 0 = None sentinel)
|
/// native_fn: u64 (8 bytes, 0 = None sentinel)
|
||||||
|
/// sub_module_index: u32 (4 bytes + 4 pad, index into CompiledModule.sub_modules)
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct JsObject {
|
pub struct JsObject {
|
||||||
header: u32,
|
header: u32,
|
||||||
@@ -23,6 +24,8 @@ pub struct JsObject {
|
|||||||
generation: u32,
|
generation: u32,
|
||||||
_pad2: [u8; 4],
|
_pad2: [u8; 4],
|
||||||
native_fn: u64,
|
native_fn: u64,
|
||||||
|
sub_module_index: u32,
|
||||||
|
_pad3: [u8; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsObject {
|
impl JsObject {
|
||||||
@@ -36,6 +39,8 @@ impl JsObject {
|
|||||||
generation: 1,
|
generation: 1,
|
||||||
_pad2: [0; 4],
|
_pad2: [0; 4],
|
||||||
native_fn: 0,
|
native_fn: 0,
|
||||||
|
sub_module_index: 0,
|
||||||
|
_pad3: [0; 4],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +59,8 @@ impl JsObject {
|
|||||||
generation: 1,
|
generation: 1,
|
||||||
_pad2: [0; 4],
|
_pad2: [0; 4],
|
||||||
native_fn: 0,
|
native_fn: 0,
|
||||||
|
sub_module_index: 0,
|
||||||
|
_pad3: [0; 4],
|
||||||
};
|
};
|
||||||
let vec = Box::new(vec![Box::new(JsValue::undefined()); n_elements]);
|
let vec = Box::new(vec![Box::new(JsValue::undefined()); n_elements]);
|
||||||
obj.hash_props = Box::into_raw(vec) as *mut u8;
|
obj.hash_props = Box::into_raw(vec) as *mut u8;
|
||||||
@@ -246,6 +253,14 @@ impl JsObject {
|
|||||||
pub fn set_native_arg_count(&mut self, n: u8) {
|
pub fn set_native_arg_count(&mut self, n: u8) {
|
||||||
self.native_arg_count = n;
|
self.native_arg_count = n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sub_module_index(&self) -> u32 {
|
||||||
|
self.sub_module_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sub_module_index(&mut self, idx: u32) {
|
||||||
|
self.sub_module_index = idx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -257,8 +272,8 @@ mod tests {
|
|||||||
fn object_size_bounds() {
|
fn object_size_bounds() {
|
||||||
let sz = std::mem::size_of::<JsObject>();
|
let sz = std::mem::size_of::<JsObject>();
|
||||||
assert!(
|
assert!(
|
||||||
sz >= 32 && sz <= 64,
|
sz >= 40 && sz <= 56,
|
||||||
"JsObject layout mismatch - expected 32-64B, got {sz}B"
|
"JsObject layout mismatch - expected 40-56B, got {sz}B"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+165
-43
@@ -63,6 +63,10 @@ pub struct Vm {
|
|||||||
pub math_rng_state: u64,
|
pub math_rng_state: u64,
|
||||||
sub_modules: Vec<CompiledModule>,
|
sub_modules: Vec<CompiledModule>,
|
||||||
sub_module_constants: Vec<Vec<JsValue>>,
|
sub_module_constants: Vec<Vec<JsValue>>,
|
||||||
|
/// Stack of saved bytecode for nested bytecode calls
|
||||||
|
saved_bytecode_stack: Vec<Vec<opcode::Instr>>,
|
||||||
|
/// Stack of saved constants for nested bytecode calls
|
||||||
|
saved_constants_stack: Vec<Vec<JsValue>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_object(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
fn bind_object(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
||||||
@@ -87,6 +91,16 @@ fn bind_object(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
|||||||
);
|
);
|
||||||
global.set_shape_id(obj_shape);
|
global.set_shape_id(obj_shape);
|
||||||
global.push_prop(obj_val);
|
global.push_prop(obj_val);
|
||||||
|
|
||||||
|
// Set native constructor on Object
|
||||||
|
{
|
||||||
|
let obj_ctor_ptr = kernel.builtin_world().object_constructor.as_ptr() as *mut JsObject;
|
||||||
|
let obj_ctor = unsafe { &mut *obj_ctor_ptr };
|
||||||
|
obj_ctor.set_native_fn(Some(
|
||||||
|
crate::builtins::object::object_constructor as *const (),
|
||||||
|
));
|
||||||
|
obj_ctor.set_native_arg_count(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_array(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
fn bind_array(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
||||||
@@ -124,6 +138,14 @@ fn bind_array(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
|||||||
global.set_shape_id(arr_shape);
|
global.set_shape_id(arr_shape);
|
||||||
global.ensure_hash_props().push(Box::new(arr_val));
|
global.ensure_hash_props().push(Box::new(arr_val));
|
||||||
global.bump_generation();
|
global.bump_generation();
|
||||||
|
|
||||||
|
// Set native constructor on Array
|
||||||
|
{
|
||||||
|
let arr_ctor_ptr = kernel.builtin_world().array_constructor.as_ptr() as *mut JsObject;
|
||||||
|
let arr_ctor = unsafe { &mut *arr_ctor_ptr };
|
||||||
|
arr_ctor.set_native_fn(Some(crate::builtins::array::array_constructor as *const ()));
|
||||||
|
arr_ctor.set_native_arg_count(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_error(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
fn bind_error(kernel: &Arc<OxideKernel>, global: &mut JsObject) {
|
||||||
@@ -636,6 +658,8 @@ impl Vm {
|
|||||||
math_rng_state: 0,
|
math_rng_state: 0,
|
||||||
sub_modules: Vec::new(),
|
sub_modules: Vec::new(),
|
||||||
sub_module_constants: Vec::new(),
|
sub_module_constants: Vec::new(),
|
||||||
|
saved_bytecode_stack: Vec::new(),
|
||||||
|
saved_constants_stack: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,6 +678,8 @@ impl Vm {
|
|||||||
math_rng_state: 0,
|
math_rng_state: 0,
|
||||||
sub_modules: Vec::new(),
|
sub_modules: Vec::new(),
|
||||||
sub_module_constants: Vec::new(),
|
sub_module_constants: Vec::new(),
|
||||||
|
saved_bytecode_stack: Vec::new(),
|
||||||
|
saved_constants_stack: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,6 +741,34 @@ impl Vm {
|
|||||||
JsValue::string(idx, hash)
|
JsValue::string(idx, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a function JsObject for a BytecodeFunc constant.
|
||||||
|
fn create_function_object(&mut self, sub_idx: u32) -> JsValue {
|
||||||
|
let func_proto_ptr = self.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject;
|
||||||
|
let proto_val = JsValue::from_js_object(func_proto_ptr);
|
||||||
|
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val);
|
||||||
|
obj.set_function(true);
|
||||||
|
obj.set_sub_module_index(sub_idx);
|
||||||
|
let obj_ptr = self.epoch.alloc(obj);
|
||||||
|
JsValue::object(obj_ptr as *mut u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a CompiledModule's constants into JsValue vec.
|
||||||
|
fn convert_constants(&mut self, module: &CompiledModule) -> Vec<JsValue> {
|
||||||
|
module
|
||||||
|
.constants
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
Constant::Number(v) => JsValue::float(*v),
|
||||||
|
Constant::Int(v) => JsValue::int(*v),
|
||||||
|
Constant::String(s) => self.intern(s),
|
||||||
|
Constant::Boolean(b) => JsValue::bool(*b),
|
||||||
|
Constant::Null => JsValue::null(),
|
||||||
|
Constant::Undefined => JsValue::undefined(),
|
||||||
|
Constant::BytecodeFunc(idx) => self.create_function_object(*idx),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lookup_str(&self, val: JsValue) -> Option<String> {
|
pub fn lookup_str(&self, val: JsValue) -> Option<String> {
|
||||||
if !val.is_string() {
|
if !val.is_string() {
|
||||||
return None;
|
return None;
|
||||||
@@ -814,25 +868,15 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&mut self, module: &CompiledModule) -> Result<JsValue, String> {
|
pub fn run(&mut self, module: &CompiledModule) -> Result<JsValue, String> {
|
||||||
self.constants = module
|
self.constants = self.convert_constants(module);
|
||||||
.constants
|
|
||||||
.iter()
|
|
||||||
.map(|c| match c {
|
|
||||||
Constant::Number(v) => JsValue::float(*v),
|
|
||||||
Constant::Int(v) => JsValue::int(*v),
|
|
||||||
Constant::String(s) => self.intern(s),
|
|
||||||
Constant::Boolean(b) => JsValue::bool(*b),
|
|
||||||
Constant::Null => JsValue::null(),
|
|
||||||
Constant::Undefined => JsValue::undefined(),
|
|
||||||
Constant::BytecodeFunc(_idx) => JsValue::undefined(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
self.sub_modules = module.sub_modules.clone();
|
self.sub_modules = module.sub_modules.clone();
|
||||||
self.sub_module_constants = vec![Vec::new(); self.sub_modules.len()];
|
self.sub_module_constants = vec![Vec::new(); self.sub_modules.len()];
|
||||||
self.bytecode = module.bytecode.clone();
|
self.bytecode = module.bytecode.clone();
|
||||||
self.pc = 0;
|
self.pc = 0;
|
||||||
self.regs = [JsValue::undefined(); 256];
|
self.regs = [JsValue::undefined(); 256];
|
||||||
self.frames.clear();
|
self.frames.clear();
|
||||||
|
self.saved_bytecode_stack.clear();
|
||||||
|
self.saved_constants_stack.clear();
|
||||||
|
|
||||||
for (name, reg) in &module.builtin_reg_map {
|
for (name, reg) in &module.builtin_reg_map {
|
||||||
let si = self.kernel.string_forge().intern(name.as_str()).0;
|
let si = self.kernel.string_forge().intern(name.as_str()).0;
|
||||||
@@ -1048,35 +1092,111 @@ impl Vm {
|
|||||||
let obj_ptr = callee.as_js_object_ptr();
|
let obj_ptr = callee.as_js_object_ptr();
|
||||||
if !obj_ptr.is_null() {
|
if !obj_ptr.is_null() {
|
||||||
let obj = unsafe { &*obj_ptr };
|
let obj = unsafe { &*obj_ptr };
|
||||||
if obj.is_function() && obj.native_fn().is_some() {
|
if obj.is_function() {
|
||||||
let ext = self.bytecode[self.pc];
|
if obj.native_fn().is_some() {
|
||||||
self.pc += 1;
|
// Native call - use extension word for arg_count
|
||||||
let arg_count = (ext & 0xFF) as usize;
|
let ext = self.bytecode[self.pc];
|
||||||
|
self.pc += 1;
|
||||||
|
let arg_count = (ext & 0xFF) as usize;
|
||||||
|
|
||||||
let mut args_buf = [0u8; 257];
|
let mut args_buf = [0u8; 257];
|
||||||
args_buf[0] = this_reg;
|
args_buf[0] = this_reg;
|
||||||
for i in 0..arg_count.min(256) {
|
for i in 0..arg_count.min(256) {
|
||||||
args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8);
|
args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8);
|
||||||
}
|
|
||||||
let args_slice = &args_buf[..arg_count + 1];
|
|
||||||
|
|
||||||
let func: NativeFn =
|
|
||||||
unsafe { std::mem::transmute(obj.native_fn().unwrap()) };
|
|
||||||
self.regs[254] = callee;
|
|
||||||
match func(self, args_slice) {
|
|
||||||
Ok(val) => self.regs[0] = val,
|
|
||||||
Err(err_val) => {
|
|
||||||
return Err(format!("Native error: {:?}", err_val));
|
|
||||||
}
|
}
|
||||||
|
let args_slice = &args_buf[..arg_count + 1];
|
||||||
|
|
||||||
|
let func: NativeFn =
|
||||||
|
unsafe { std::mem::transmute(obj.native_fn().unwrap()) };
|
||||||
|
self.regs[254] = callee;
|
||||||
|
match func(self, args_slice) {
|
||||||
|
Ok(val) => self.regs[0] = val,
|
||||||
|
Err(err_val) => {
|
||||||
|
return Err(format!("Native error: {:?}", err_val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else if obj.sub_module_index() > 0 {
|
||||||
|
// 1-indexed sub_module_index (0 = not a bytecode function)
|
||||||
|
let sub_idx = obj.sub_module_index() as usize - 1;
|
||||||
|
if sub_idx >= self.sub_modules.len() {
|
||||||
|
return Err(format!(
|
||||||
|
"CALL: sub_module_index {} out of bounds (max {})",
|
||||||
|
sub_idx,
|
||||||
|
self.sub_modules.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone sub_module data before mutably borrowing self
|
||||||
|
let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone();
|
||||||
|
let sub_n_args = self.sub_modules[sub_idx].n_args as usize;
|
||||||
|
let sub_constants = self.sub_modules[sub_idx].constants.clone();
|
||||||
|
let sub_builtin_count =
|
||||||
|
self.sub_modules[sub_idx].builtin_reg_map.len();
|
||||||
|
|
||||||
|
// Copy args to regs[sub_builtin_count..sub_builtin_count+n_args]
|
||||||
|
// (builtins occupy regs[0..sub_builtin_count-1])
|
||||||
|
for i in 0..sub_n_args {
|
||||||
|
let src_reg = first_arg_reg.wrapping_add(i as u8) as usize;
|
||||||
|
self.regs[sub_builtin_count + i] = self.regs[src_reg];
|
||||||
|
}
|
||||||
|
// Set this (reg 254 convention)
|
||||||
|
self.regs[254] = callee;
|
||||||
|
|
||||||
|
// Save current bytecode/constants
|
||||||
|
self.saved_bytecode_stack
|
||||||
|
.push(std::mem::take(&mut self.bytecode));
|
||||||
|
self.saved_constants_stack
|
||||||
|
.push(std::mem::take(&mut self.constants));
|
||||||
|
|
||||||
|
// Push call frame
|
||||||
|
self.frames.push(CallFrame {
|
||||||
|
return_addr: self.pc,
|
||||||
|
n_locals: sub_n_args as u8,
|
||||||
|
n_args: sub_n_args as u8,
|
||||||
|
function_obj_reg: callee_reg as u8,
|
||||||
|
frame_base: sub_builtin_count as u8,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert sub_module constants
|
||||||
|
self.bytecode = sub_bytecode;
|
||||||
|
self.constants = sub_constants
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
Constant::Number(v) => JsValue::float(*v),
|
||||||
|
Constant::Int(v) => JsValue::int(*v),
|
||||||
|
Constant::String(s) => self.intern(s),
|
||||||
|
Constant::Boolean(b) => JsValue::bool(*b),
|
||||||
|
Constant::Null => JsValue::null(),
|
||||||
|
Constant::Undefined => JsValue::undefined(),
|
||||||
|
Constant::BytecodeFunc(idx) => {
|
||||||
|
self.create_function_object(*idx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Pre-fill builtin registers from the global object
|
||||||
|
// (same as Vm::run does for the main module).
|
||||||
|
for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map {
|
||||||
|
let si = self.kernel.string_forge().intern(name.as_str()).0;
|
||||||
|
let global = self.kernel.global_object();
|
||||||
|
if let Some(pos) = self
|
||||||
|
.kernel
|
||||||
|
.shape_forge()
|
||||||
|
.lookup_position(global.shape_id(), si)
|
||||||
|
{
|
||||||
|
self.regs[*reg as usize] = global.get_prop_at(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pc = 0;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
// bytecode path skeleton - wired in plan 12.1-03
|
|
||||||
return Err("bytecode function calls not yet supported".into());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err("CALL target is not a native function".into());
|
return Err("CALL target is not callable".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
OpCode::CALL_NATIVE => {
|
OpCode::CALL_NATIVE => {
|
||||||
@@ -1161,11 +1281,10 @@ impl Vm {
|
|||||||
// If constructor has native_fn, call it with this=new_obj
|
// If constructor has native_fn, call it with this=new_obj
|
||||||
if ctor_obj.native_fn().is_some() {
|
if ctor_obj.native_fn().is_some() {
|
||||||
let new_obj_val = JsValue::object(new_obj as *mut u8);
|
let new_obj_val = JsValue::object(new_obj as *mut u8);
|
||||||
// this_reg = register holding new_obj; we use regs[255] as scratch
|
|
||||||
self.regs[255] = new_obj_val;
|
self.regs[255] = new_obj_val;
|
||||||
|
|
||||||
let mut args_buf = [0u8; 257];
|
let mut args_buf = [0u8; 257];
|
||||||
args_buf[0] = 255u8; // this = new_obj
|
args_buf[0] = 255u8;
|
||||||
for i in 0..arg_count.min(256) {
|
for i in 0..arg_count.min(256) {
|
||||||
args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8);
|
args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8);
|
||||||
}
|
}
|
||||||
@@ -1176,11 +1295,10 @@ impl Vm {
|
|||||||
self.regs[254] = constructor;
|
self.regs[254] = constructor;
|
||||||
match func(self, args_slice) {
|
match func(self, args_slice) {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
// If constructor returns an object, use that; else use new_obj
|
|
||||||
if val.is_object() {
|
if val.is_object() {
|
||||||
self.regs[0] = val;
|
self.regs[rd] = val;
|
||||||
} else {
|
} else {
|
||||||
self.regs[0] = new_obj_val;
|
self.regs[rd] = new_obj_val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err_val) => {
|
Err(err_val) => {
|
||||||
@@ -1198,10 +1316,14 @@ impl Vm {
|
|||||||
OpCode::RETURN => {
|
OpCode::RETURN => {
|
||||||
let result = self.regs[rd];
|
let result = self.regs[rd];
|
||||||
if let Some(frame) = self.frames.pop() {
|
if let Some(frame) = self.frames.pop() {
|
||||||
|
// Restore caller's saved bytecode and constants
|
||||||
|
if let Some(saved_bc) = self.saved_bytecode_stack.pop() {
|
||||||
|
self.bytecode = saved_bc;
|
||||||
|
}
|
||||||
|
if let Some(saved_consts) = self.saved_constants_stack.pop() {
|
||||||
|
self.constants = saved_consts;
|
||||||
|
}
|
||||||
// Restore caller's pc and merge result into caller's regs[0].
|
// Restore caller's pc and merge result into caller's regs[0].
|
||||||
// frame.frame_base marks the first register of the caller frame;
|
|
||||||
// when bytecode calls are wired in 12.1-03, registers above
|
|
||||||
// frame_base will be cleared on RETURN.
|
|
||||||
self.pc = frame.return_addr;
|
self.pc = frame.return_addr;
|
||||||
self.regs[0] = result;
|
self.regs[0] = result;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(vm: &mut Vm, source: &str) -> Result<JsValue, String> {
|
||||||
|
let allocator = oxide_parser::Allocator::default();
|
||||||
|
let program =
|
||||||
|
oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||||
|
let module = Compiler::new()
|
||||||
|
.compile(&program)
|
||||||
|
.map_err(|e| format!("Compile error: {}", e))?;
|
||||||
|
vm.run(&module)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Function Declaration Basics ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_return_literal() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f() { return 42; } f()").unwrap();
|
||||||
|
assert_eq!(result.as_int(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_return_void() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f() { 1; } f()").unwrap();
|
||||||
|
assert!(result.is_undefined());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_hoisting() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "foo(); function foo() { return 1; }").unwrap();
|
||||||
|
assert_eq!(result.as_int(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_with_params() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function add(a,b) { return a + b; } add(2, 3)").unwrap();
|
||||||
|
assert!((result.as_double() - 5.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_single_param() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function echo(x) { return x; } echo(99)").unwrap();
|
||||||
|
assert_eq!(result.as_int(), 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Function Expression ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fe_basic() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "var f = function() { return 99; }; f()").unwrap();
|
||||||
|
assert_eq!(result.as_int(), 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fe_with_params() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(
|
||||||
|
&mut vm,
|
||||||
|
"var mul = function(x,y) { return x * y; }; mul(6, 7)",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!((result.as_double() - 42.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cross-function calls ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_func_call() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(
|
||||||
|
&mut vm,
|
||||||
|
"function a() { return 1; } function b() { return a() + 2; } b()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!((result.as_double() - 3.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_funcs_called_from_global() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(
|
||||||
|
&mut vm,
|
||||||
|
"function a() { return 1; } function b() { return 2; } a() + b()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(result.is_double(), "expected double, got: {:?}", result);
|
||||||
|
assert!((result.as_double() - 3.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Builtins inside functions ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_calls_builtin_return() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f() { return Math.abs(-5); } f()").unwrap();
|
||||||
|
assert!(result.is_double(), "expected double, got: {:?}", result);
|
||||||
|
assert!((result.as_double() - 5.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_calls_builtin_with_param() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f(x) { return Math.abs(x); } f(-10)").unwrap();
|
||||||
|
assert!(result.is_double(), "expected double, got: {:?}", result);
|
||||||
|
assert!((result.as_double() - 10.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_calls_builtin_two_args() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f(x) { return Math.pow(x, 2); } f(3)").unwrap();
|
||||||
|
assert!(result.is_double(), "expected double, got: {:?}", result);
|
||||||
|
assert!((result.as_double() - 9.0).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Multiple FDs, first FD calls second ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_chain_call() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(
|
||||||
|
&mut vm,
|
||||||
|
"function a() { return 10; } function b() { return a(); } b()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.as_int(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- new Xxx() in function ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_returns_new_object() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f() { return new Object(); } f()").unwrap();
|
||||||
|
assert!(result.is_object(), "expected object, got: {:?}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fd_returns_new_array() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = eval(&mut vm, "function f() { return new Array(3); } f()").unwrap();
|
||||||
|
assert!(result.is_object());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user