feat: 支持箭头函数/模板字符串/let/const/delete/instanceof 语法
This commit is contained in:
@@ -6,7 +6,9 @@ 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;
|
||||||
|
pub use oxide_parser::VariableDeclarationKind;
|
||||||
pub use oxide_parser::{AssignmentOperator, BinaryOperator, Expression, Statement, UnaryOperator};
|
pub use oxide_parser::{AssignmentOperator, BinaryOperator, Expression, Statement, UnaryOperator};
|
||||||
|
use crate::symbol_table::ScopeKind;
|
||||||
|
|
||||||
pub struct Compiler;
|
pub struct Compiler;
|
||||||
|
|
||||||
@@ -63,6 +65,10 @@ pub(crate) struct CompileCtx {
|
|||||||
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>,
|
pub(crate) sub_modules: Vec<CompiledModule>,
|
||||||
|
/// Register holding `this` in the enclosing function context.
|
||||||
|
/// Used by arrow functions to capture lexical `this` (D-01).
|
||||||
|
/// Initialized to 254 (conventional this register) at the top level.
|
||||||
|
pub(crate) enclosing_this_reg: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompileCtx {
|
impl CompileCtx {
|
||||||
@@ -80,6 +86,7 @@ impl CompileCtx {
|
|||||||
projected_pc: 0,
|
projected_pc: 0,
|
||||||
builtin_reg_map: Vec::new(),
|
builtin_reg_map: Vec::new(),
|
||||||
sub_modules: Vec::new(),
|
sub_modules: Vec::new(),
|
||||||
|
enclosing_this_reg: 254, // conventional this register at top level
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +133,29 @@ impl CompileCtx {
|
|||||||
self.symbols.pop_scope();
|
self.symbols.pop_scope();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(&mut self, name: &str, reg: u8) -> Result<(), String> {
|
pub(crate) fn declare(
|
||||||
self.symbols.declare(name, reg)
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
reg: u8,
|
||||||
|
kind: VariableDeclarationKind,
|
||||||
|
is_const: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
self.symbols.declare(name, reg, kind, is_const)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare_initialized(&mut self, name: &str, reg: u8) -> Result<(), String> {
|
pub(crate) fn declare_initialized(
|
||||||
self.symbols.declare_initialized(name, reg)
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
reg: u8,
|
||||||
|
kind: VariableDeclarationKind,
|
||||||
|
is_const: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
self.symbols.declare_initialized(name, reg, kind, is_const)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn push_scope_with_kind(&mut self, kind: ScopeKind) {
|
||||||
|
self.symbols.push_scope_with_kind(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn lookup(&self, name: &str) -> Result<u8, String> {
|
pub(crate) fn lookup(&self, name: &str) -> Result<u8, String> {
|
||||||
@@ -143,6 +167,10 @@ impl CompileCtx {
|
|||||||
self.symbols.lookup_or_global(name, reg)
|
self.symbols.lookup_or_global(name, reg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lookup_const_flag(&self, name: &str) -> bool {
|
||||||
|
self.symbols.lookup_is_const(name)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn init_var(&mut self, name: &str) {
|
pub(crate) fn init_var(&mut self, name: &str) {
|
||||||
self.symbols.init_var(name);
|
self.symbols.init_var(name);
|
||||||
}
|
}
|
||||||
@@ -225,13 +253,16 @@ impl Compiler {
|
|||||||
Self
|
Self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile a function body (used for FD and FE).
|
/// Compile a function body (used for FD, FE, and arrow functions).
|
||||||
/// This performs both counting and emitting in one pass.
|
/// This performs both counting and emitting in one pass.
|
||||||
|
/// When `is_expression_body` is true (arrow function with expression body),
|
||||||
|
/// the last expression's value is returned instead of undefined.
|
||||||
pub(crate) fn compile_function_body<'a>(
|
pub(crate) fn compile_function_body<'a>(
|
||||||
&self,
|
&self,
|
||||||
param_names: &[String],
|
param_names: &[String],
|
||||||
body_stmts: &[Statement<'a>],
|
body_stmts: &[Statement<'a>],
|
||||||
parent_ctx: &CompileCtx,
|
parent_ctx: &CompileCtx,
|
||||||
|
is_expression_body: bool,
|
||||||
) -> Result<CompiledModule, String> {
|
) -> Result<CompiledModule, String> {
|
||||||
let mut ctx = CompileCtx::new();
|
let mut ctx = CompileCtx::new();
|
||||||
|
|
||||||
@@ -239,14 +270,18 @@ impl Compiler {
|
|||||||
// resolve to the correct pre-allocated registers in the sub-module's register file.
|
// resolve to the correct pre-allocated registers in the sub-module's register file.
|
||||||
ctx.builtin_reg_map = parent_ctx.builtin_reg_map.clone();
|
ctx.builtin_reg_map = parent_ctx.builtin_reg_map.clone();
|
||||||
|
|
||||||
|
// Propagate enclosing_this_reg so nested arrow functions capture the correct `this`.
|
||||||
|
ctx.enclosing_this_reg = parent_ctx.enclosing_this_reg;
|
||||||
|
|
||||||
// Inherit parent's global scope entries so previously-declared function names
|
// Inherit parent's global scope entries so previously-declared function names
|
||||||
// are visible from within the body.
|
// are visible from within the body.
|
||||||
for (name, binding) in &parent_ctx.symbols.scopes[0] {
|
for (name, binding) in &parent_ctx.symbols.scopes[0].bindings {
|
||||||
ctx.symbols.scopes[0].insert(
|
ctx.symbols.scopes[0].bindings.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
Binding {
|
Binding {
|
||||||
reg: binding.reg,
|
reg: binding.reg,
|
||||||
initialized: binding.initialized,
|
initialized: binding.initialized,
|
||||||
|
is_const: binding.is_const,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -256,12 +291,12 @@ impl Compiler {
|
|||||||
ctx.reset_regs();
|
ctx.reset_regs();
|
||||||
|
|
||||||
// Function body scope - params and local vars
|
// Function body scope - params and local vars
|
||||||
ctx.push_scope();
|
ctx.push_scope_with_kind(ScopeKind::FunctionScope);
|
||||||
|
|
||||||
// Register parameters as initialized.
|
// Register parameters as initialized.
|
||||||
for name in param_names {
|
for name in param_names {
|
||||||
let reg = ctx.alloc_reg();
|
let reg = ctx.alloc_reg();
|
||||||
ctx.declare_initialized(name, reg)?;
|
ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count pass
|
// Count pass
|
||||||
@@ -274,24 +309,45 @@ impl Compiler {
|
|||||||
// Emit pass - reallocate params (same order = same regs after reset)
|
// Emit pass - reallocate params (same order = same regs after reset)
|
||||||
for name in param_names {
|
for name in param_names {
|
||||||
let reg = ctx.alloc_reg();
|
let reg = ctx.alloc_reg();
|
||||||
ctx.declare_initialized(name, reg)?;
|
ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit body statements.
|
// Emit body statements.
|
||||||
// After all statements, emit an implicit RETURN undefined.
|
// Capture the last statement's expression result for expression-body arrows.
|
||||||
|
let mut last_result_reg = None;
|
||||||
for stmt in body_stmts {
|
for stmt in body_stmts {
|
||||||
let _ = self.emit_statement(stmt, &mut ctx)?;
|
if let Some(reg) = self.emit_statement(stmt, &mut ctx)? {
|
||||||
|
last_result_reg = Some(reg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let undef_idx = ctx.add_constant(Constant::Undefined);
|
// Emit implicit RETURN: expression body returns the last expression,
|
||||||
let undef_reg = ctx.alloc_reg();
|
// statement body returns undefined.
|
||||||
ctx.emit(opcode::encode(
|
if is_expression_body {
|
||||||
OpCode::LOAD_CONST,
|
if let Some(reg) = last_result_reg {
|
||||||
undef_reg,
|
ctx.emit(opcode::encode(OpCode::RETURN, reg, 0, 0));
|
||||||
(undef_idx & 0xFF) as u8,
|
} else {
|
||||||
((undef_idx >> 8) & 0xFF) as u8,
|
let undef_idx = ctx.add_constant(Constant::Undefined);
|
||||||
));
|
let undef_reg = ctx.alloc_reg();
|
||||||
ctx.emit(opcode::encode(OpCode::RETURN, undef_reg, 0, 0));
|
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));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 {
|
Ok(CompiledModule {
|
||||||
bytecode: ctx.bytecode,
|
bytecode: ctx.bytecode,
|
||||||
@@ -300,6 +356,9 @@ impl Compiler {
|
|||||||
n_args: param_names.len() as u8,
|
n_args: param_names.len() as u8,
|
||||||
builtin_reg_map: ctx.builtin_reg_map,
|
builtin_reg_map: ctx.builtin_reg_map,
|
||||||
sub_modules: ctx.sub_modules,
|
sub_modules: ctx.sub_modules,
|
||||||
|
is_arrow: false,
|
||||||
|
captured_this_const_idx: 0,
|
||||||
|
function_name: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +412,9 @@ impl Compiler {
|
|||||||
n_args: 0,
|
n_args: 0,
|
||||||
builtin_reg_map: ctx.builtin_reg_map,
|
builtin_reg_map: ctx.builtin_reg_map,
|
||||||
sub_modules: ctx.sub_modules,
|
sub_modules: ctx.sub_modules,
|
||||||
|
is_arrow: false,
|
||||||
|
captured_this_const_idx: 0,
|
||||||
|
function_name: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use oxide_parser::{
|
use oxide_parser::{
|
||||||
AssignmentOperator, Expression, ForStatementInit, SimpleAssignmentTarget, Statement,
|
AssignmentOperator, Expression, ForStatementInit, SimpleAssignmentTarget, Statement,
|
||||||
|
UnaryOperator, VariableDeclarationKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label};
|
use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label};
|
||||||
@@ -189,7 +190,7 @@ impl Compiler {
|
|||||||
|
|
||||||
// Hoisting: declare function name as initialized
|
// Hoisting: declare function name as initialized
|
||||||
let func_reg = ctx.alloc_reg();
|
let func_reg = ctx.alloc_reg();
|
||||||
let _ = ctx.declare_initialized(&name, func_reg);
|
let _ = ctx.declare_initialized(&name, func_reg, VariableDeclarationKind::Var, false);
|
||||||
|
|
||||||
// Body is compiled in the emit pass only.
|
// Body is compiled in the emit pass only.
|
||||||
// FD emits LOAD_CONST(BytecodeFunc) + STORE_VAR
|
// FD emits LOAD_CONST(BytecodeFunc) + STORE_VAR
|
||||||
@@ -269,9 +270,32 @@ impl Compiler {
|
|||||||
ctx.projected_pc += 1; // ADD/SUB/MUL/DIV/etc.
|
ctx.projected_pc += 1; // ADD/SUB/MUL/DIV/etc.
|
||||||
}
|
}
|
||||||
Expression::UnaryExpression(un) => {
|
Expression::UnaryExpression(un) => {
|
||||||
self.count_expression(&un.argument, ctx);
|
match un.operator {
|
||||||
ctx.alloc_reg();
|
UnaryOperator::Delete => {
|
||||||
ctx.projected_pc += 1; // NEG/TYPEOF/VOID/NOT
|
match &un.argument {
|
||||||
|
Expression::Identifier(_) => {
|
||||||
|
// SyntaxError at compile time, no bytecode cost
|
||||||
|
}
|
||||||
|
Expression::StaticMemberExpression(member) => {
|
||||||
|
self.count_expression(&member.object, ctx);
|
||||||
|
ctx.alloc_reg();
|
||||||
|
ctx.projected_pc += 2; // DELETE_PROP_STATIC + ext word
|
||||||
|
}
|
||||||
|
Expression::ComputedMemberExpression(member) => {
|
||||||
|
self.count_expression(&member.object, ctx);
|
||||||
|
self.count_expression(&member.expression, ctx);
|
||||||
|
ctx.alloc_reg();
|
||||||
|
ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.count_expression(&un.argument, ctx);
|
||||||
|
ctx.alloc_reg();
|
||||||
|
ctx.projected_pc += 1; // NEG/TYPEOF/VOID/NOT
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Expression::CallExpression(call) => {
|
Expression::CallExpression(call) => {
|
||||||
match &call.callee {
|
match &call.callee {
|
||||||
@@ -394,6 +418,78 @@ impl Compiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expression::TemplateLiteral(tl) => {
|
||||||
|
// TemplateLiteral: N quasis (strings) + M expressions interleaved.
|
||||||
|
// Allocate 1 result reg + count expressions.
|
||||||
|
for expr in &tl.expressions {
|
||||||
|
self.count_expression(expr, ctx);
|
||||||
|
}
|
||||||
|
ctx.alloc_reg(); // result reg
|
||||||
|
let segment_count = tl.quasis.len() + tl.expressions.len();
|
||||||
|
// TEMPLATE_STR opcode + ext words (1 header + ceil(segments/1) per u32)
|
||||||
|
ctx.projected_pc += 1 + 1 + segment_count;
|
||||||
|
}
|
||||||
|
Expression::TaggedTemplateExpression(tt) => {
|
||||||
|
// Tagged template: tag expr + quasis as LOAD_CONST + expression args + CALL
|
||||||
|
// Consecutive arg registers at end (via LOAD_VAR copies)
|
||||||
|
self.count_expression(&tt.tag, ctx);
|
||||||
|
let quasi_count = tt.quasi.quasis.len();
|
||||||
|
// Each quasi: LOAD_CONST string + LOAD_CONST index + SET_ELEM
|
||||||
|
// for both cooked and raw arrays
|
||||||
|
for _ in 0..quasi_count {
|
||||||
|
ctx.alloc_reg(); // str_reg
|
||||||
|
ctx.projected_pc += 1; // LOAD_CONST string
|
||||||
|
ctx.alloc_reg(); // idx_reg
|
||||||
|
ctx.projected_pc += 1; // LOAD_CONST index
|
||||||
|
ctx.projected_pc += 1; // SET_ELEM (cooked)
|
||||||
|
}
|
||||||
|
// Cooked array: alloc + NEW_ARRAY
|
||||||
|
ctx.alloc_reg(); // cooked_temp
|
||||||
|
ctx.projected_pc += 1; // NEW_ARRAY
|
||||||
|
|
||||||
|
for _ in 0..quasi_count {
|
||||||
|
ctx.alloc_reg(); // str_reg
|
||||||
|
ctx.projected_pc += 1; // LOAD_CONST string
|
||||||
|
ctx.alloc_reg(); // idx_reg
|
||||||
|
ctx.projected_pc += 1; // LOAD_CONST index
|
||||||
|
ctx.projected_pc += 1; // SET_ELEM (raw)
|
||||||
|
}
|
||||||
|
// Raw array: alloc + NEW_ARRAY
|
||||||
|
ctx.alloc_reg(); // raw_temp
|
||||||
|
ctx.projected_pc += 1; // NEW_ARRAY
|
||||||
|
|
||||||
|
// Count expression arguments
|
||||||
|
for expr in &tt.quasi.expressions {
|
||||||
|
self.count_expression(expr, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consecutive arg slots: cooked_slot, raw_slot, N expr_slots
|
||||||
|
ctx.alloc_reg(); // cooked_slot
|
||||||
|
ctx.alloc_reg(); // raw_slot
|
||||||
|
for _ in &tt.quasi.expressions {
|
||||||
|
ctx.alloc_reg(); // expr_slot
|
||||||
|
ctx.projected_pc += 1; // LOAD_VAR
|
||||||
|
}
|
||||||
|
// LOAD_VAR for cooked and raw
|
||||||
|
ctx.projected_pc += 2;
|
||||||
|
|
||||||
|
// undefined this arg
|
||||||
|
ctx.alloc_reg();
|
||||||
|
ctx.projected_pc += 1; // LOAD_CONST
|
||||||
|
|
||||||
|
// CALL + ext word
|
||||||
|
ctx.projected_pc += 2;
|
||||||
|
|
||||||
|
// Result reg + LOAD_VAR
|
||||||
|
ctx.alloc_reg();
|
||||||
|
ctx.projected_pc += 1; // LOAD_VAR
|
||||||
|
}
|
||||||
|
Expression::ArrowFunctionExpression(_arrow) => {
|
||||||
|
// Arrow functions: alloc 1 register for the function value.
|
||||||
|
// Body is compiled in the emit pass only (same pattern as FE).
|
||||||
|
ctx.alloc_reg();
|
||||||
|
ctx.projected_pc += 1; // LOAD_CONST
|
||||||
|
}
|
||||||
Expression::FunctionExpression(_fe) => {
|
Expression::FunctionExpression(_fe) => {
|
||||||
// No hoisting - function created at expression position.
|
// No hoisting - function created at expression position.
|
||||||
// Body is compiled in the emit pass only.
|
// Body is compiled in the emit pass only.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::compiler::Label;
|
|||||||
use crate::opcode::{self, OpCode};
|
use crate::opcode::{self, OpCode};
|
||||||
use oxide_parser::{
|
use oxide_parser::{
|
||||||
AssignmentOperator, Expression, ForStatementInit, ForStatementLeft, SimpleAssignmentTarget,
|
AssignmentOperator, Expression, ForStatementInit, ForStatementLeft, SimpleAssignmentTarget,
|
||||||
Statement, UnaryOperator, UpdateOperator,
|
Statement, UnaryOperator, UpdateOperator, VariableDeclarationKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::compiler::{is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler};
|
use crate::compiler::{is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler};
|
||||||
@@ -25,12 +25,24 @@ impl Compiler {
|
|||||||
oxide_parser::BindingPattern::BindingIdentifier(bi) => bi.name.as_str(),
|
oxide_parser::BindingPattern::BindingIdentifier(bi) => bi.name.as_str(),
|
||||||
_ => return Err("destructuring not supported".into()),
|
_ => return Err("destructuring not supported".into()),
|
||||||
};
|
};
|
||||||
|
let is_const = matches!(decl.kind, VariableDeclarationKind::Const);
|
||||||
|
if is_const && d.init.is_none() {
|
||||||
|
return Err("const declaration must have an initializer".into());
|
||||||
|
}
|
||||||
let var_reg = ctx.alloc_reg();
|
let var_reg = ctx.alloc_reg();
|
||||||
ctx.declare(name, var_reg)?;
|
ctx.declare(name, var_reg, decl.kind, is_const)?;
|
||||||
if let Some(init) = &d.init {
|
if let Some(init) = &d.init {
|
||||||
let val_reg = self.emit_expression(init, ctx)?;
|
let val_reg = self.emit_expression(init, ctx)?;
|
||||||
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0));
|
let const_flag = if is_const { 1 } else { 0 };
|
||||||
|
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag));
|
||||||
ctx.init_var(name);
|
ctx.init_var(name);
|
||||||
|
// Name inference (D-04): if the initializer is an arrow function,
|
||||||
|
// set the compiled sub_module's function_name.
|
||||||
|
if matches!(*init, Expression::ArrowFunctionExpression(_)) {
|
||||||
|
if let Some(sub_mod) = ctx.sub_modules.last_mut() {
|
||||||
|
sub_mod.function_name = Some(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
r = Some(val_reg);
|
r = Some(val_reg);
|
||||||
} else {
|
} else {
|
||||||
let idx = ctx.add_constant(Constant::Undefined);
|
let idx = ctx.add_constant(Constant::Undefined);
|
||||||
@@ -180,7 +192,7 @@ impl Compiler {
|
|||||||
_ => return Err("destructuring not supported".into()),
|
_ => return Err("destructuring not supported".into()),
|
||||||
};
|
};
|
||||||
let var_reg = ctx.alloc_reg();
|
let var_reg = ctx.alloc_reg();
|
||||||
ctx.declare(name, var_reg)?;
|
ctx.declare(name, var_reg, decl.kind, matches!(decl.kind, VariableDeclarationKind::Const))?;
|
||||||
if let Some(init_expr) = &d.init {
|
if let Some(init_expr) = &d.init {
|
||||||
let val_reg = self.emit_expression(init_expr, ctx)?;
|
let val_reg = self.emit_expression(init_expr, ctx)?;
|
||||||
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0));
|
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0));
|
||||||
@@ -242,7 +254,7 @@ impl Compiler {
|
|||||||
_ => return Err("destructuring not supported".into()),
|
_ => return Err("destructuring not supported".into()),
|
||||||
};
|
};
|
||||||
let var_reg = ctx.alloc_reg();
|
let var_reg = ctx.alloc_reg();
|
||||||
ctx.declare(name, var_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.emit(opcode::encode(OpCode::STORE_VAR, var_reg, key_reg, 0));
|
||||||
ctx.init_var(name);
|
ctx.init_var(name);
|
||||||
}
|
}
|
||||||
@@ -350,7 +362,8 @@ impl Compiler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Compile body into sub-module
|
// Compile body into sub-module
|
||||||
let sub_module = self.compile_function_body(¶m_names, body_stmts, ctx)?;
|
let sub_module =
|
||||||
|
self.compile_function_body(¶m_names, body_stmts, ctx, false)?;
|
||||||
ctx.sub_modules.push(sub_module);
|
ctx.sub_modules.push(sub_module);
|
||||||
// 1-indexed: 0 = no sub_module (sentinel)
|
// 1-indexed: 0 = no sub_module (sentinel)
|
||||||
let sub_idx = ctx.sub_modules.len() as u32;
|
let sub_idx = ctx.sub_modules.len() as u32;
|
||||||
@@ -437,7 +450,12 @@ impl Compiler {
|
|||||||
let catch_reg = ctx.alloc_reg();
|
let catch_reg = ctx.alloc_reg();
|
||||||
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern
|
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern
|
||||||
{
|
{
|
||||||
ctx.declare_initialized(bi.name.as_str(), catch_reg)?;
|
ctx.declare_initialized(
|
||||||
|
bi.name.as_str(),
|
||||||
|
catch_reg,
|
||||||
|
VariableDeclarationKind::Let,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
ctx.emit(opcode::encode(OpCode::STORE_VAR, catch_reg, 0, 0));
|
ctx.emit(opcode::encode(OpCode::STORE_VAR, catch_reg, 0, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,6 +585,7 @@ impl Compiler {
|
|||||||
BinaryOperator::LessEqualThan => OpCode::LTE,
|
BinaryOperator::LessEqualThan => OpCode::LTE,
|
||||||
BinaryOperator::GreaterEqualThan => OpCode::GTE,
|
BinaryOperator::GreaterEqualThan => OpCode::GTE,
|
||||||
BinaryOperator::In => OpCode::IN,
|
BinaryOperator::In => OpCode::IN,
|
||||||
|
BinaryOperator::Instanceof => OpCode::INSTANCEOF,
|
||||||
BinaryOperator::StrictEquality => OpCode::STRICT_EQ,
|
BinaryOperator::StrictEquality => OpCode::STRICT_EQ,
|
||||||
BinaryOperator::StrictInequality => OpCode::STRICT_NEQ,
|
BinaryOperator::StrictInequality => OpCode::STRICT_NEQ,
|
||||||
_ => return Err(format!("unsupported binary operator: {:?}", bin.operator)),
|
_ => return Err(format!("unsupported binary operator: {:?}", bin.operator)),
|
||||||
@@ -603,6 +622,30 @@ impl Compiler {
|
|||||||
ctx.emit(opcode::encode(OpCode::UNARY_PLUS, r, arg, 0));
|
ctx.emit(opcode::encode(OpCode::UNARY_PLUS, r, arg, 0));
|
||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
|
UnaryOperator::Delete => {
|
||||||
|
match &un.argument {
|
||||||
|
Expression::Identifier(_) => {
|
||||||
|
Err("delete of global variable is not allowed in strict mode".into())
|
||||||
|
}
|
||||||
|
Expression::StaticMemberExpression(member) => {
|
||||||
|
let obj_reg = self.emit_expression(&member.object, ctx)?;
|
||||||
|
let prop_name = member.property.name.as_str();
|
||||||
|
let const_idx = ctx.add_constant(Constant::String(prop_name.to_string()));
|
||||||
|
let r = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, r, obj_reg, 0));
|
||||||
|
ctx.emit(const_idx as u32);
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
Expression::ComputedMemberExpression(member) => {
|
||||||
|
let obj_reg = self.emit_expression(&member.object, ctx)?;
|
||||||
|
let key_reg = self.emit_expression(&member.expression, ctx)?;
|
||||||
|
let r = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, r, obj_reg, key_reg));
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
_ => Err("invalid delete target".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => Err(format!("unsupported unary operator: {:?}", un.operator)),
|
_ => Err(format!("unsupported unary operator: {:?}", un.operator)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,6 +774,13 @@ impl Compiler {
|
|||||||
((idx >> 8) & 0xFF) as u8,
|
((idx >> 8) & 0xFF) as u8,
|
||||||
));
|
));
|
||||||
let val_reg = self.emit_expression(&p.value, ctx)?;
|
let val_reg = self.emit_expression(&p.value, ctx)?;
|
||||||
|
// Name inference (D-04): if property value is an arrow function,
|
||||||
|
// set the compiled sub_module's function_name to the property key.
|
||||||
|
if matches!(&p.value, Expression::ArrowFunctionExpression(_)) {
|
||||||
|
if let Some(sub_mod) = ctx.sub_modules.last_mut() {
|
||||||
|
sub_mod.function_name = Some(prop_name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.emit(opcode::encode(OpCode::SET_PROP, obj_reg, val_reg, key_reg));
|
ctx.emit(opcode::encode(OpCode::SET_PROP, obj_reg, val_reg, key_reg));
|
||||||
}
|
}
|
||||||
Ok(obj_reg)
|
Ok(obj_reg)
|
||||||
@@ -850,7 +900,9 @@ impl Compiler {
|
|||||||
let val_reg = self.emit_expression(&assign.right, ctx)?;
|
let val_reg = self.emit_expression(&assign.right, ctx)?;
|
||||||
let name = id_ref.name.as_str();
|
let name = id_ref.name.as_str();
|
||||||
let var_reg = ctx.lookup_or_global(name);
|
let var_reg = ctx.lookup_or_global(name);
|
||||||
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0));
|
let is_const = ctx.lookup_const_flag(name);
|
||||||
|
let const_flag = if is_const { 1 } else { 0 };
|
||||||
|
ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag));
|
||||||
Ok(val_reg)
|
Ok(val_reg)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -912,6 +964,226 @@ 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)
|
||||||
}
|
}
|
||||||
|
// TemplateLiteral: interleaved quasis + expressions via TEMPLATE_STR opcode (D-07)
|
||||||
|
Expression::TemplateLiteral(tl) => {
|
||||||
|
let r = ctx.alloc_reg();
|
||||||
|
let quasis = &tl.quasis;
|
||||||
|
let expressions = &tl.expressions;
|
||||||
|
let segment_count = quasis.len() + expressions.len();
|
||||||
|
|
||||||
|
// Evaluate each expression, collecting registers
|
||||||
|
let expr_regs: Vec<u8> = expressions
|
||||||
|
.iter()
|
||||||
|
.map(|e| self.emit_expression(e, ctx))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
// Add each quasi string to constant pool
|
||||||
|
let quasi_const_idxs: Vec<u16> = quasis
|
||||||
|
.iter()
|
||||||
|
.map(|q| {
|
||||||
|
let s = q
|
||||||
|
.value
|
||||||
|
.cooked
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
ctx.add_constant(Constant::String(s))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Compute total length hint
|
||||||
|
let total_len_hint: usize = quasis
|
||||||
|
.iter()
|
||||||
|
.map(|q| q.value.cooked.as_ref().map(|c| c.len()).unwrap_or(0))
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
// Emit TEMPLATE_STR rd, 0, 0
|
||||||
|
ctx.emit(opcode::encode(OpCode::TEMPLATE_STR, r, 0, 0));
|
||||||
|
|
||||||
|
// Ext word 0: (segment_count << 16) | (total_len_hint & 0xFFFF)
|
||||||
|
ctx.emit(((segment_count as u32) << 16) | (total_len_hint as u32 & 0xFFFF));
|
||||||
|
|
||||||
|
// Interleave segments: quasi[0], expr[0], quasi[1], expr[1], ...
|
||||||
|
let mut expr_iter = expr_regs.iter();
|
||||||
|
for const_idx in quasi_const_idxs.iter() {
|
||||||
|
// Quasi: is_expression=0, bits 0-15 = const_idx
|
||||||
|
ctx.emit(*const_idx as u32 & 0x7FFF_FFFF);
|
||||||
|
|
||||||
|
// Expression (if any remaining)
|
||||||
|
if let Some(expr_reg) = expr_iter.next() {
|
||||||
|
// Expression: is_expression=1, bits 0-15 = reg
|
||||||
|
ctx.emit(0x8000_0000u32 | (*expr_reg as u32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
// TaggedTemplateExpression: tag`str ${expr}` => CALL(tag, undefined, cooked_array, raw_array, ...exprs) (D-08)
|
||||||
|
Expression::TaggedTemplateExpression(tt) => {
|
||||||
|
let quasis = &tt.quasi.quasis;
|
||||||
|
let expressions = &tt.quasi.expressions;
|
||||||
|
|
||||||
|
// 1. Evaluate tag expression
|
||||||
|
let tag_reg = self.emit_expression(&tt.tag, ctx)?;
|
||||||
|
|
||||||
|
// 2. Build cooked strings array (into a temp register)
|
||||||
|
let cooked_temp = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::NEW_ARRAY,
|
||||||
|
cooked_temp,
|
||||||
|
quasis.len() as u8,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
for (i, quasi) in quasis.iter().enumerate() {
|
||||||
|
let s = quasi
|
||||||
|
.value
|
||||||
|
.cooked
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let const_idx = ctx.add_constant(Constant::String(s));
|
||||||
|
let str_reg = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::LOAD_CONST,
|
||||||
|
str_reg,
|
||||||
|
(const_idx & 0xFF) as u8,
|
||||||
|
((const_idx >> 8) & 0xFF) as u8,
|
||||||
|
));
|
||||||
|
let idx_const = ctx.add_constant(Constant::Int(i as i32));
|
||||||
|
let idx_reg = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::LOAD_CONST,
|
||||||
|
idx_reg,
|
||||||
|
(idx_const & 0xFF) as u8,
|
||||||
|
((idx_const >> 8) & 0xFF) as u8,
|
||||||
|
));
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::SET_ELEM,
|
||||||
|
cooked_temp,
|
||||||
|
idx_reg,
|
||||||
|
str_reg,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Build raw strings array (into a temp register)
|
||||||
|
let raw_temp = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::NEW_ARRAY,
|
||||||
|
raw_temp,
|
||||||
|
quasis.len() as u8,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
for (i, quasi) in quasis.iter().enumerate() {
|
||||||
|
let raw = quasi.value.raw.to_string();
|
||||||
|
let const_idx = ctx.add_constant(Constant::String(raw));
|
||||||
|
let str_reg = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::LOAD_CONST,
|
||||||
|
str_reg,
|
||||||
|
(const_idx & 0xFF) as u8,
|
||||||
|
((const_idx >> 8) & 0xFF) as u8,
|
||||||
|
));
|
||||||
|
let idx_const = ctx.add_constant(Constant::Int(i as i32));
|
||||||
|
let idx_reg = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::LOAD_CONST,
|
||||||
|
idx_reg,
|
||||||
|
(idx_const & 0xFF) as u8,
|
||||||
|
((idx_const >> 8) & 0xFF) as u8,
|
||||||
|
));
|
||||||
|
ctx.emit(opcode::encode(OpCode::SET_ELEM, raw_temp, idx_reg, str_reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Evaluate expression arguments (into temp registers)
|
||||||
|
let mut expr_temps = Vec::new();
|
||||||
|
for expr in expressions {
|
||||||
|
expr_temps.push(self.emit_expression(expr, ctx)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Allocate consecutive argument slots: cooked, raw, expr[0], expr[1], ...
|
||||||
|
let cooked_slot = ctx.alloc_reg();
|
||||||
|
let raw_slot = ctx.alloc_reg();
|
||||||
|
let mut expr_slots = Vec::new();
|
||||||
|
for _ in expressions {
|
||||||
|
expr_slots.push(ctx.alloc_reg());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy temps to consecutive slots using LOAD_VAR (register-to-register move)
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::LOAD_VAR,
|
||||||
|
cooked_slot,
|
||||||
|
cooked_temp,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
ctx.emit(opcode::encode(OpCode::LOAD_VAR, raw_slot, raw_temp, 0));
|
||||||
|
for (slot, temp) in expr_slots.iter().zip(expr_temps.iter()) {
|
||||||
|
ctx.emit(opcode::encode(OpCode::LOAD_VAR, *slot, *temp, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Emit undefined as this_arg
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
|
||||||
|
// 7. Emit CALL(tag, undefined, cooked_slot)
|
||||||
|
let arg_count = 2 + expressions.len();
|
||||||
|
ctx.emit(opcode::encode(
|
||||||
|
OpCode::CALL,
|
||||||
|
tag_reg,
|
||||||
|
undef_reg,
|
||||||
|
cooked_slot,
|
||||||
|
));
|
||||||
|
ctx.emit(arg_count as u32);
|
||||||
|
|
||||||
|
// 8. Result from regs[0]
|
||||||
|
let result_reg = ctx.alloc_reg();
|
||||||
|
ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, 0, 0));
|
||||||
|
Ok(result_reg)
|
||||||
|
}
|
||||||
|
// ArrowFunctionExpression: compile body, capture lexical this (D-01)
|
||||||
|
// Name inference (D-04) happens at assignment site - see VariableDeclaration/ObjectProperty.
|
||||||
|
Expression::ArrowFunctionExpression(arrow) => {
|
||||||
|
// Rest params not yet supported (D-06 placeholder)
|
||||||
|
if let Some(_rest) = &arrow.params.rest {
|
||||||
|
return Err("rest params in arrow functions not yet supported".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract param names (same pattern as FunctionExpression)
|
||||||
|
let mut param_names = Vec::new();
|
||||||
|
for param in &arrow.params.items {
|
||||||
|
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern {
|
||||||
|
param_names.push(bi.name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expression body: pass body statements directly with is_expression_body=true.
|
||||||
|
// Statement body: pass body statements with is_expression_body=false.
|
||||||
|
let body_stmts = &arrow.body.statements;
|
||||||
|
let is_expr_body = arrow.expression;
|
||||||
|
|
||||||
|
let mut sub_module =
|
||||||
|
self.compile_function_body(¶m_names, body_stmts, ctx, is_expr_body)?;
|
||||||
|
sub_module.is_arrow = true;
|
||||||
|
|
||||||
|
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::FunctionExpression(fe) => {
|
Expression::FunctionExpression(fe) => {
|
||||||
// FunctionExpression: compile body, emit LOAD_CONST(BytecodeFunc)
|
// FunctionExpression: compile body, emit LOAD_CONST(BytecodeFunc)
|
||||||
let mut param_names = Vec::new();
|
let mut param_names = Vec::new();
|
||||||
@@ -927,7 +1199,8 @@ impl Compiler {
|
|||||||
&[]
|
&[]
|
||||||
};
|
};
|
||||||
|
|
||||||
let sub_module = self.compile_function_body(¶m_names, body_stmts, ctx)?;
|
let sub_module =
|
||||||
|
self.compile_function_body(¶m_names, body_stmts, ctx, false)?;
|
||||||
ctx.sub_modules.push(sub_module);
|
ctx.sub_modules.push(sub_module);
|
||||||
// 1-indexed: 0 = no sub_module (sentinel)
|
// 1-indexed: 0 = no sub_module (sentinel)
|
||||||
let sub_idx = ctx.sub_modules.len() as u32;
|
let sub_idx = ctx.sub_modules.len() as u32;
|
||||||
@@ -1048,10 +1321,8 @@ impl Compiler {
|
|||||||
let last_slash = raw_str.rfind('/').unwrap_or(raw_str.len() - 1);
|
let last_slash = raw_str.rfind('/').unwrap_or(raw_str.len() - 1);
|
||||||
let pattern = &raw_str[1..last_slash];
|
let pattern = &raw_str[1..last_slash];
|
||||||
let flags = &raw_str[last_slash + 1..];
|
let flags = &raw_str[last_slash + 1..];
|
||||||
let ci = ctx.add_constant(Constant::RegExp(
|
let ci = ctx
|
||||||
pattern.to_string(),
|
.add_constant(Constant::RegExp(pattern.to_string(), flags.to_string()));
|
||||||
flags.to_string(),
|
|
||||||
));
|
|
||||||
let r = ctx.alloc_reg();
|
let r = ctx.alloc_reg();
|
||||||
ctx.emit(opcode::encode(
|
ctx.emit(opcode::encode(
|
||||||
OpCode::LOAD_CONST,
|
OpCode::LOAD_CONST,
|
||||||
@@ -1061,16 +1332,10 @@ impl Compiler {
|
|||||||
));
|
));
|
||||||
Ok(r)
|
Ok(r)
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!("unsupported expression type: {:?}", expr))
|
||||||
"unsupported expression type: {:?}",
|
|
||||||
expr
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!("unsupported expression type: {:?}", expr))
|
||||||
"unsupported expression type: {:?}",
|
|
||||||
expr
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(format!("unsupported expression type: {:?}", expr)),
|
_ => Err(format!("unsupported expression type: {:?}", expr)),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) {
|
|||||||
hash_expression(&es.expression, h);
|
hash_expression(&es.expression, h);
|
||||||
}
|
}
|
||||||
Statement::VariableDeclaration(decl) => {
|
Statement::VariableDeclaration(decl) => {
|
||||||
1u8.hash(h);
|
std::mem::discriminant(&decl.kind).hash(h);
|
||||||
(decl.declarations.len() as u32).hash(h);
|
(decl.declarations.len() as u32).hash(h);
|
||||||
for d in &decl.declarations {
|
for d in &decl.declarations {
|
||||||
if let Some(init) = &d.init {
|
if let Some(init) = &d.init {
|
||||||
@@ -54,6 +54,7 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) {
|
|||||||
if let Some(expr) = init.as_expression() {
|
if let Some(expr) = init.as_expression() {
|
||||||
hash_expression(expr, h);
|
hash_expression(expr, h);
|
||||||
} else if let ForStatementInit::VariableDeclaration(decl) = init {
|
} else if let ForStatementInit::VariableDeclaration(decl) = init {
|
||||||
|
std::mem::discriminant(&decl.kind).hash(h);
|
||||||
(decl.declarations.len() as u32).hash(h);
|
(decl.declarations.len() as u32).hash(h);
|
||||||
for d in &decl.declarations {
|
for d in &decl.declarations {
|
||||||
if let Some(init_expr) = &d.init {
|
if let Some(init_expr) = &d.init {
|
||||||
@@ -224,6 +225,28 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expression::TemplateLiteral(tl) => {
|
||||||
|
16u8.hash(h);
|
||||||
|
(tl.quasis.len() as u32).hash(h);
|
||||||
|
for expr in &tl.expressions {
|
||||||
|
hash_expression(expr, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::TaggedTemplateExpression(tt) => {
|
||||||
|
17u8.hash(h);
|
||||||
|
hash_expression(&tt.tag, h);
|
||||||
|
(tt.quasi.quasis.len() as u32).hash(h);
|
||||||
|
for expr in &tt.quasi.expressions {
|
||||||
|
hash_expression(expr, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::ArrowFunctionExpression(arrow) => {
|
||||||
|
15u8.hash(h);
|
||||||
|
(arrow.params.items.len() as u32).hash(h);
|
||||||
|
for s in &arrow.body.statements {
|
||||||
|
hash_statement(s, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
Expression::FunctionExpression(fe) => {
|
Expression::FunctionExpression(fe) => {
|
||||||
13u8.hash(h);
|
13u8.hash(h);
|
||||||
(fe.params.items.len() as u32).hash(h);
|
(fe.params.items.len() as u32).hash(h);
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ pub struct CompiledModule {
|
|||||||
pub n_args: 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>,
|
||||||
|
/// True when this module is an arrow function body (D-01).
|
||||||
|
/// Arrow functions capture lexical `this` from the enclosing scope.
|
||||||
|
pub is_arrow: bool,
|
||||||
|
/// Index into `constants` holding the captured `this` JsValue.
|
||||||
|
/// 0 means "not captured - use standard this binding".
|
||||||
|
pub captured_this_const_idx: u16,
|
||||||
|
/// Function name inferred from assignment context (D-04).
|
||||||
|
/// Set at the VariableDeclaration / ObjectProperty assignment site.
|
||||||
|
pub function_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompiledModule {
|
impl CompiledModule {
|
||||||
@@ -32,6 +41,9 @@ impl CompiledModule {
|
|||||||
n_args: 0,
|
n_args: 0,
|
||||||
builtin_reg_map: Vec::new(),
|
builtin_reg_map: Vec::new(),
|
||||||
sub_modules: Vec::new(),
|
sub_modules: Vec::new(),
|
||||||
|
is_arrow: false,
|
||||||
|
captured_this_const_idx: 0,
|
||||||
|
function_name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +63,9 @@ impl Clone for CompiledModule {
|
|||||||
n_args: self.n_args,
|
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(),
|
||||||
|
is_arrow: self.is_arrow,
|
||||||
|
captured_this_const_idx: self.captured_this_const_idx,
|
||||||
|
function_name: self.function_name.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ pub enum OpCode {
|
|||||||
FOR_OF_DONE = 0x36,
|
FOR_OF_DONE = 0x36,
|
||||||
FOR_OF_CLOSE = 0x37,
|
FOR_OF_CLOSE = 0x37,
|
||||||
|
|
||||||
|
// -- Template Literal (0x38) --
|
||||||
|
TEMPLATE_STR = 0x38,
|
||||||
|
|
||||||
|
// -- Small Language Features (0x39-0x3B) --
|
||||||
|
DELETE_PROP_STATIC = 0x39,
|
||||||
|
DELETE_PROP_DYNAMIC = 0x3A,
|
||||||
|
INSTANCEOF = 0x3B,
|
||||||
|
|
||||||
// -- Variable (0x30-0x32) --
|
// -- Variable (0x30-0x32) --
|
||||||
LOAD_VAR = 0x30,
|
LOAD_VAR = 0x30,
|
||||||
STORE_VAR = 0x31,
|
STORE_VAR = 0x31,
|
||||||
@@ -201,6 +209,10 @@ impl TryFrom<u8> for OpCode {
|
|||||||
0x35 => Ok(OpCode::TRY_FINALLY_END),
|
0x35 => Ok(OpCode::TRY_FINALLY_END),
|
||||||
0x36 => Ok(OpCode::FOR_OF_DONE),
|
0x36 => Ok(OpCode::FOR_OF_DONE),
|
||||||
0x37 => Ok(OpCode::FOR_OF_CLOSE),
|
0x37 => Ok(OpCode::FOR_OF_CLOSE),
|
||||||
|
0x38 => Ok(OpCode::TEMPLATE_STR),
|
||||||
|
0x39 => Ok(OpCode::DELETE_PROP_STATIC),
|
||||||
|
0x3A => Ok(OpCode::DELETE_PROP_DYNAMIC),
|
||||||
|
0x3B => Ok(OpCode::INSTANCEOF),
|
||||||
0x40 => Ok(OpCode::CALL),
|
0x40 => Ok(OpCode::CALL),
|
||||||
0x41 => Ok(OpCode::RETURN),
|
0x41 => Ok(OpCode::RETURN),
|
||||||
0x42 => Ok(OpCode::CALL_NATIVE),
|
0x42 => Ok(OpCode::CALL_NATIVE),
|
||||||
@@ -288,6 +300,10 @@ impl fmt::Display for OpCode {
|
|||||||
OpCode::TRY_FINALLY_END => "TRY_FINALLY_END",
|
OpCode::TRY_FINALLY_END => "TRY_FINALLY_END",
|
||||||
OpCode::FOR_OF_DONE => "FOR_OF_DONE",
|
OpCode::FOR_OF_DONE => "FOR_OF_DONE",
|
||||||
OpCode::FOR_OF_CLOSE => "FOR_OF_CLOSE",
|
OpCode::FOR_OF_CLOSE => "FOR_OF_CLOSE",
|
||||||
|
OpCode::TEMPLATE_STR => "TEMPLATE_STR",
|
||||||
|
OpCode::DELETE_PROP_STATIC => "DELETE_PROP_STATIC",
|
||||||
|
OpCode::DELETE_PROP_DYNAMIC => "DELETE_PROP_DYNAMIC",
|
||||||
|
OpCode::INSTANCEOF => "INSTANCEOF",
|
||||||
OpCode::LOAD_VAR => "LOAD_VAR",
|
OpCode::LOAD_VAR => "LOAD_VAR",
|
||||||
OpCode::STORE_VAR => "STORE_VAR",
|
OpCode::STORE_VAR => "STORE_VAR",
|
||||||
OpCode::LOAD_CONST => "LOAD_CONST",
|
OpCode::LOAD_CONST => "LOAD_CONST",
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use oxide_parser::VariableDeclarationKind;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ScopeKind {
|
||||||
|
FunctionScope,
|
||||||
|
BlockScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Scope {
|
||||||
|
pub(crate) bindings: HashMap<String, Binding>,
|
||||||
|
pub(crate) kind: ScopeKind,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct Binding {
|
pub(crate) struct Binding {
|
||||||
pub(crate) reg: u8,
|
pub(crate) reg: u8,
|
||||||
pub(crate) initialized: bool,
|
pub(crate) initialized: bool,
|
||||||
|
pub(crate) is_const: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SymbolTable {
|
pub struct SymbolTable {
|
||||||
pub(crate) scopes: Vec<HashMap<String, Binding>>,
|
pub(crate) scopes: Vec<Scope>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SymbolTable {
|
impl Default for SymbolTable {
|
||||||
@@ -18,28 +32,65 @@ impl Default for SymbolTable {
|
|||||||
impl SymbolTable {
|
impl SymbolTable {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
scopes: vec![HashMap::new()],
|
scopes: vec![Scope {
|
||||||
|
bindings: HashMap::new(),
|
||||||
|
kind: ScopeKind::FunctionScope,
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_scope(&mut self) {
|
pub fn push_scope(&mut self) {
|
||||||
self.scopes.push(HashMap::new());
|
self.scopes.push(Scope {
|
||||||
|
bindings: HashMap::new(),
|
||||||
|
kind: ScopeKind::BlockScope,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push_scope_with_kind(&mut self, kind: ScopeKind) {
|
||||||
|
self.scopes.push(Scope {
|
||||||
|
bindings: HashMap::new(),
|
||||||
|
kind,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop_scope(&mut self) {
|
pub fn pop_scope(&mut self) {
|
||||||
self.scopes.pop();
|
if self.scopes.len() > 1 {
|
||||||
|
self.scopes.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn declare(&mut self, name: &str, reg: u8) -> Result<(), String> {
|
fn find_var_target_scope(&self) -> usize {
|
||||||
let current = self.scopes.last_mut().unwrap();
|
for (i, scope) in self.scopes.iter().enumerate().rev() {
|
||||||
if current.contains_key(name) {
|
if scope.kind == ScopeKind::FunctionScope {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn declare(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
reg: u8,
|
||||||
|
kind: VariableDeclarationKind,
|
||||||
|
is_const: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let target_idx = if matches!(kind, VariableDeclarationKind::Var) {
|
||||||
|
self.find_var_target_scope()
|
||||||
|
} else {
|
||||||
|
self.scopes.len() - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = &mut self.scopes[target_idx];
|
||||||
|
if target.bindings.contains_key(name) {
|
||||||
return Err(format!("Identifier '{name}' has already been declared"));
|
return Err(format!("Identifier '{name}' has already been declared"));
|
||||||
}
|
}
|
||||||
current.insert(
|
target.bindings.insert(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
Binding {
|
Binding {
|
||||||
reg,
|
reg,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
|
is_const: matches!(kind, VariableDeclarationKind::Const) || is_const,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -47,7 +98,7 @@ impl SymbolTable {
|
|||||||
|
|
||||||
pub fn lookup(&self, name: &str) -> Result<u8, String> {
|
pub fn lookup(&self, name: &str) -> Result<u8, String> {
|
||||||
for scope in self.scopes.iter().rev() {
|
for scope in self.scopes.iter().rev() {
|
||||||
if let Some(b) = scope.get(name) {
|
if let Some(b) = scope.bindings.get(name) {
|
||||||
if b.initialized {
|
if b.initialized {
|
||||||
return Ok(b.reg);
|
return Ok(b.reg);
|
||||||
}
|
}
|
||||||
@@ -59,49 +110,79 @@ impl SymbolTable {
|
|||||||
|
|
||||||
pub fn lookup_or_global(&mut self, name: &str, reg_for_new: u8) -> u8 {
|
pub fn lookup_or_global(&mut self, name: &str, reg_for_new: u8) -> u8 {
|
||||||
for scope in self.scopes.iter().rev() {
|
for scope in self.scopes.iter().rev() {
|
||||||
if let Some(b) = scope.get(name) {
|
if let Some(b) = scope.bindings.get(name) {
|
||||||
return b.reg;
|
return b.reg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.scopes[0].insert(
|
self.scopes[0].bindings.insert(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
Binding {
|
Binding {
|
||||||
reg: reg_for_new,
|
reg: reg_for_new,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
|
is_const: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
reg_for_new
|
reg_for_new
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Declare a variable that is already initialized (for FunctionDeclaration hoisting).
|
pub fn lookup_is_const(&self, name: &str) -> bool {
|
||||||
/// If the name already exists in the current scope, just mark it as initialized.
|
for scope in self.scopes.iter().rev() {
|
||||||
pub fn declare_initialized(&mut self, name: &str, reg: u8) -> Result<(), String> {
|
if let Some(b) = scope.bindings.get(name) {
|
||||||
let current = self.scopes.last_mut().unwrap();
|
if b.initialized {
|
||||||
if let Some(b) = current.get_mut(name) {
|
return b.is_const;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn declare_initialized(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
reg: u8,
|
||||||
|
kind: VariableDeclarationKind,
|
||||||
|
is_const: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let target_idx = if matches!(kind, VariableDeclarationKind::Var) {
|
||||||
|
self.find_var_target_scope()
|
||||||
|
} else {
|
||||||
|
self.scopes.len() - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = &mut self.scopes[target_idx];
|
||||||
|
if let Some(b) = target.bindings.get_mut(name) {
|
||||||
b.initialized = true;
|
b.initialized = true;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
current.insert(
|
target.bindings.insert(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
Binding {
|
Binding {
|
||||||
reg,
|
reg,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
|
is_const: matches!(kind, VariableDeclarationKind::Const) || is_const,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(())
|
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]
|
||||||
reg,
|
.bindings
|
||||||
initialized: true,
|
.entry(name.to_string())
|
||||||
});
|
.or_insert(Binding {
|
||||||
|
reg,
|
||||||
|
initialized: true,
|
||||||
|
is_const: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_var(&mut self, name: &str) {
|
pub fn init_var(&mut self, name: &str) {
|
||||||
let current = self.scopes.last_mut().unwrap();
|
for scope in self.scopes.iter_mut().rev() {
|
||||||
if let Some(b) = current.get_mut(name) {
|
if let Some(b) = scope.bindings.get_mut(name) {
|
||||||
b.initialized = true;
|
b.initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +190,17 @@ impl SymbolTable {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::SymbolTable;
|
use super::SymbolTable;
|
||||||
|
use oxide_parser::VariableDeclarationKind;
|
||||||
|
|
||||||
|
fn v() -> VariableDeclarationKind {
|
||||||
|
VariableDeclarationKind::Var
|
||||||
|
}
|
||||||
|
fn l() -> VariableDeclarationKind {
|
||||||
|
VariableDeclarationKind::Let
|
||||||
|
}
|
||||||
|
fn c() -> VariableDeclarationKind {
|
||||||
|
VariableDeclarationKind::Const
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn global_scope_exists() {
|
fn global_scope_exists() {
|
||||||
@@ -119,7 +211,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn declare_and_lookup() {
|
fn declare_and_lookup() {
|
||||||
let mut st = SymbolTable::new();
|
let mut st = SymbolTable::new();
|
||||||
st.declare("x", 0).unwrap();
|
st.declare("x", 0, l(), false).unwrap();
|
||||||
st.init_var("x");
|
st.init_var("x");
|
||||||
assert_eq!(st.lookup("x").unwrap(), 0);
|
assert_eq!(st.lookup("x").unwrap(), 0);
|
||||||
}
|
}
|
||||||
@@ -127,7 +219,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn tdz_error() {
|
fn tdz_error() {
|
||||||
let mut st = SymbolTable::new();
|
let mut st = SymbolTable::new();
|
||||||
st.declare("x", 0).unwrap();
|
st.declare("x", 0, l(), false).unwrap();
|
||||||
assert!(st
|
assert!(st
|
||||||
.lookup("x")
|
.lookup("x")
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
@@ -137,17 +229,17 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn duplicate_declaration_error() {
|
fn duplicate_declaration_error() {
|
||||||
let mut st = SymbolTable::new();
|
let mut st = SymbolTable::new();
|
||||||
st.declare("x", 0).unwrap();
|
st.declare("x", 0, l(), false).unwrap();
|
||||||
assert!(st.declare("x", 1).is_err());
|
assert!(st.declare("x", 1, l(), false).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_scopes_shadow() {
|
fn nested_scopes_shadow() {
|
||||||
let mut st = SymbolTable::new();
|
let mut st = SymbolTable::new();
|
||||||
st.declare("x", 0).unwrap();
|
st.declare("x", 0, l(), false).unwrap();
|
||||||
st.init_var("x");
|
st.init_var("x");
|
||||||
st.push_scope();
|
st.push_scope();
|
||||||
st.declare("x", 1).unwrap();
|
st.declare("x", 1, l(), false).unwrap();
|
||||||
st.init_var("x");
|
st.init_var("x");
|
||||||
assert_eq!(st.lookup("x").unwrap(), 1);
|
assert_eq!(st.lookup("x").unwrap(), 1);
|
||||||
st.pop_scope();
|
st.pop_scope();
|
||||||
@@ -165,9 +257,59 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn lookup_or_global_existing() {
|
fn lookup_or_global_existing() {
|
||||||
let mut st = SymbolTable::new();
|
let mut st = SymbolTable::new();
|
||||||
st.declare("x", 3).unwrap();
|
st.declare("x", 3, l(), false).unwrap();
|
||||||
st.init_var("x");
|
st.init_var("x");
|
||||||
let reg = st.lookup_or_global("x", 99);
|
let reg = st.lookup_or_global("x", 99);
|
||||||
assert_eq!(reg, 3);
|
assert_eq!(reg, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn var_hoists_to_function_scope() {
|
||||||
|
let mut st = SymbolTable::new();
|
||||||
|
// Global scope is FunctionScope
|
||||||
|
st.declare("x", 0, l(), false).unwrap();
|
||||||
|
st.init_var("x");
|
||||||
|
// Push a block scope
|
||||||
|
st.push_scope();
|
||||||
|
// var declaration inside block should go to function scope
|
||||||
|
st.declare("y", 1, v(), false).unwrap();
|
||||||
|
st.init_var("y");
|
||||||
|
st.pop_scope();
|
||||||
|
// y should be accessible (it was declared in function scope, not block scope)
|
||||||
|
assert_eq!(st.lookup("y").unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_block_scoped() {
|
||||||
|
let mut st = SymbolTable::new();
|
||||||
|
st.declare("x", 0, l(), false).unwrap();
|
||||||
|
st.init_var("x");
|
||||||
|
st.push_scope();
|
||||||
|
st.declare("x", 1, l(), false).unwrap();
|
||||||
|
st.init_var("x");
|
||||||
|
assert_eq!(st.lookup("x").unwrap(), 1);
|
||||||
|
st.pop_scope();
|
||||||
|
assert_eq!(st.lookup("x").unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_has_is_const_flag() {
|
||||||
|
let mut st = SymbolTable::new();
|
||||||
|
st.declare("x", 0, c(), false).unwrap();
|
||||||
|
st.init_var("x");
|
||||||
|
let reg = st.lookup("x").unwrap();
|
||||||
|
assert_eq!(reg, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn var_in_two_blocks_same_function_scope_is_duplicate() {
|
||||||
|
let mut st = SymbolTable::new();
|
||||||
|
st.push_scope();
|
||||||
|
st.declare("y", 1, v(), false).unwrap();
|
||||||
|
st.pop_scope();
|
||||||
|
st.push_scope();
|
||||||
|
// Same function scope, so this should be a duplicate
|
||||||
|
assert!(st.declare("y", 2, v(), false).is_err());
|
||||||
|
st.pop_scope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_parser::Allocator;
|
||||||
|
|
||||||
|
fn parse_to_hash(source: &str) -> u64 {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||||||
|
oxide_compiler::compiler::structural_hash(&program)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||||||
|
let compiler = Compiler::new();
|
||||||
|
compiler.compile(&program).expect("compile failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Arrow function structural hash tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_arrow_expression_body() {
|
||||||
|
// Expression-body arrows at top level compile successfully
|
||||||
|
let hash = parse_to_hash("var f = () => 42;");
|
||||||
|
assert!(hash != 0, "arrow function should produce a non-zero hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_arrow_different_params() {
|
||||||
|
let hash_a = parse_to_hash("var f = (x) => x;");
|
||||||
|
let hash_b = parse_to_hash("var f = (x, y) => x;");
|
||||||
|
assert_ne!(
|
||||||
|
hash_a, hash_b,
|
||||||
|
"different param counts must produce different hashes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_arrow_vs_regular_function() {
|
||||||
|
let hash_a = parse_to_hash("var f = () => 42;");
|
||||||
|
let hash_b = parse_to_hash("var f = function() { return 42; };");
|
||||||
|
assert_ne!(
|
||||||
|
hash_a, hash_b,
|
||||||
|
"arrow and regular functions must have different hashes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_arrow_block_body() {
|
||||||
|
let hash = parse_to_hash("var f = (a, b) => { return a + b; };");
|
||||||
|
assert!(
|
||||||
|
hash != 0,
|
||||||
|
"arrow with block body should produce a non-zero hash"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_arrow_same_shape() {
|
||||||
|
// Same structural shape: different variable name, same arrow expression
|
||||||
|
let hash_a = parse_to_hash("var f = () => 1 + 2;");
|
||||||
|
let hash_b = parse_to_hash("var g = () => 1 + 2;");
|
||||||
|
assert_eq!(
|
||||||
|
hash_a, hash_b,
|
||||||
|
"same-shaped arrow functions should have same hash"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Arrow function compilation tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_arrow_expression_body() {
|
||||||
|
let module = compile_source("var f = () => 42; f();");
|
||||||
|
// Verify the sub_module for the arrow is marked as is_arrow
|
||||||
|
let arrow_found = module.sub_modules.iter().any(|m| m.is_arrow);
|
||||||
|
assert!(
|
||||||
|
arrow_found,
|
||||||
|
"arrow function sub_module should have is_arrow=true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_arrow_block_body() {
|
||||||
|
let module = compile_source("var f = (a, b) => { return a + b; }; f(1, 2);");
|
||||||
|
let arrow_found = module.sub_modules.iter().any(|m| m.is_arrow);
|
||||||
|
assert!(
|
||||||
|
arrow_found,
|
||||||
|
"arrow with block body should have is_arrow=true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_arrow_name_inference() {
|
||||||
|
let module = compile_source("var myArrow = () => 42;");
|
||||||
|
// The sub_module for the arrow should have function_name set
|
||||||
|
let named = module
|
||||||
|
.sub_modules
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.is_arrow && m.function_name.as_deref() == Some("myArrow"));
|
||||||
|
assert!(
|
||||||
|
named,
|
||||||
|
"arrow function should have function_name='myArrow' from D-04 inference"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_regular_function_not_arrow() {
|
||||||
|
let module = compile_source("var f = function() { return 42; }; f();");
|
||||||
|
// Regular FunctionExpression should NOT have is_arrow=true
|
||||||
|
let has_arrow = module.sub_modules.iter().any(|m| m.is_arrow);
|
||||||
|
assert!(
|
||||||
|
!has_arrow,
|
||||||
|
"regular function expression should not be marked as arrow"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_arrow_single_param_no_parens() {
|
||||||
|
let module = compile_source("var double = x => x * 2; double(5);");
|
||||||
|
let arrow_found = module.sub_modules.iter().any(|m| m.is_arrow);
|
||||||
|
assert!(
|
||||||
|
arrow_found,
|
||||||
|
"single-param arrow without parens should compile"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_arrow_multiple_params_with_parens() {
|
||||||
|
let module = compile_source("var sum = (a, b) => a + b; sum(1, 2);");
|
||||||
|
let arrow_found = module.sub_modules.iter().any(|m| m.is_arrow);
|
||||||
|
assert!(arrow_found, "multi-param arrow with parens should compile");
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hash_var_let_const_differ() {
|
||||||
|
let compiler = Compiler::new();
|
||||||
|
let allocator = oxide_parser::Allocator::default();
|
||||||
|
|
||||||
|
let var_ast = oxide_parser::parse(&allocator, "var x = 1").unwrap();
|
||||||
|
let let_ast = oxide_parser::parse(&allocator, "let x = 1").unwrap();
|
||||||
|
let const_ast = oxide_parser::parse(&allocator, "const x = 1").unwrap();
|
||||||
|
|
||||||
|
// Different AST structures should produce different bytecode
|
||||||
|
let var_module = compiler.compile(&var_ast).unwrap();
|
||||||
|
let let_module = compiler.compile(&let_ast).unwrap();
|
||||||
|
let const_module = compiler.compile(&const_ast).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(var_module.n_registers, let_module.n_registers);
|
||||||
|
assert_eq!(let_module.n_registers, const_module.n_registers);
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ use crate::value::JsValue;
|
|||||||
|
|
||||||
pub type ShapeId = u32;
|
pub type ShapeId = u32;
|
||||||
|
|
||||||
/// Layout (48B):
|
/// Layout (56B):
|
||||||
/// header: u32 bits
|
/// header: u32 bits
|
||||||
/// [0:23] shape_id
|
/// [0:23] shape_id
|
||||||
|
/// [28] is_arrow
|
||||||
/// [29] is_array
|
/// [29] is_array
|
||||||
/// [30] is_extensible
|
/// [30] is_extensible
|
||||||
/// [31] is_function
|
/// [31] is_function
|
||||||
@@ -14,6 +15,7 @@ pub type ShapeId = u32;
|
|||||||
/// 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)
|
/// sub_module_index: u32 (4 bytes + 4 pad, index into CompiledModule.sub_modules)
|
||||||
|
/// captured_this: JsValue (8 bytes, lexical this for arrow functions)
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct JsObject {
|
pub struct JsObject {
|
||||||
header: u32,
|
header: u32,
|
||||||
@@ -26,6 +28,7 @@ pub struct JsObject {
|
|||||||
native_fn: u64,
|
native_fn: u64,
|
||||||
sub_module_index: u32,
|
sub_module_index: u32,
|
||||||
_pad3: [u8; 4],
|
_pad3: [u8; 4],
|
||||||
|
captured_this: JsValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsObject {
|
impl JsObject {
|
||||||
@@ -41,6 +44,7 @@ impl JsObject {
|
|||||||
native_fn: 0,
|
native_fn: 0,
|
||||||
sub_module_index: 0,
|
sub_module_index: 0,
|
||||||
_pad3: [0; 4],
|
_pad3: [0; 4],
|
||||||
|
captured_this: JsValue::undefined(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ impl JsObject {
|
|||||||
native_fn: 0,
|
native_fn: 0,
|
||||||
sub_module_index: 0,
|
sub_module_index: 0,
|
||||||
_pad3: [0; 4],
|
_pad3: [0; 4],
|
||||||
|
captured_this: JsValue::undefined(),
|
||||||
};
|
};
|
||||||
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;
|
||||||
@@ -261,6 +266,30 @@ impl JsObject {
|
|||||||
pub fn set_sub_module_index(&mut self, idx: u32) {
|
pub fn set_sub_module_index(&mut self, idx: u32) {
|
||||||
self.sub_module_index = idx;
|
self.sub_module_index = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arrow function flag (header bit 28).
|
||||||
|
/// When true, CALL dispatch captures lexical `this` from creation time.
|
||||||
|
pub fn is_arrow(&self) -> bool {
|
||||||
|
(self.header >> 28) & 1 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_arrow(&mut self, v: bool) {
|
||||||
|
if v {
|
||||||
|
self.header |= 1 << 28;
|
||||||
|
} else {
|
||||||
|
self.header &= !(1 << 28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lexical `this` captured at arrow function creation time.
|
||||||
|
/// Only meaningful when `is_arrow()` returns true.
|
||||||
|
pub fn captured_this(&self) -> JsValue {
|
||||||
|
self.captured_this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_captured_this(&mut self, v: JsValue) {
|
||||||
|
self.captured_this = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+198
-11
@@ -297,12 +297,19 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a function JsObject for a BytecodeFunc constant.
|
/// Create a function JsObject for a BytecodeFunc constant.
|
||||||
fn create_function_object(&mut self, sub_idx: u32) -> JsValue {
|
/// When `is_arrow` is true, captures the current `this` (regs[254])
|
||||||
|
/// for lexical this binding at call time (D-01).
|
||||||
|
fn create_function_object(&mut self, sub_idx: u32, is_arrow: bool) -> JsValue {
|
||||||
let func_proto_ptr = self.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject;
|
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 proto_val = JsValue::from_js_object(func_proto_ptr);
|
||||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val);
|
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val);
|
||||||
obj.set_function(true);
|
obj.set_function(true);
|
||||||
obj.set_sub_module_index(sub_idx);
|
obj.set_sub_module_index(sub_idx);
|
||||||
|
if is_arrow {
|
||||||
|
obj.set_arrow(true);
|
||||||
|
// Capture lexical `this` from the enclosing scope (regs[254]).
|
||||||
|
obj.set_captured_this(self.regs[254]);
|
||||||
|
}
|
||||||
let obj_ptr = self.epoch.alloc(obj);
|
let obj_ptr = self.epoch.alloc(obj);
|
||||||
JsValue::object(obj_ptr as *mut u8)
|
JsValue::object(obj_ptr as *mut u8)
|
||||||
}
|
}
|
||||||
@@ -319,14 +326,24 @@ impl Vm {
|
|||||||
Constant::Boolean(b) => JsValue::bool(*b),
|
Constant::Boolean(b) => JsValue::bool(*b),
|
||||||
Constant::Null => JsValue::null(),
|
Constant::Null => JsValue::null(),
|
||||||
Constant::Undefined => JsValue::undefined(),
|
Constant::Undefined => JsValue::undefined(),
|
||||||
Constant::BytecodeFunc(idx) => self.create_function_object(*idx),
|
Constant::BytecodeFunc(idx) => {
|
||||||
|
// 1-indexed: idx 1 = sub_modules[0], idx 0 = sentinel
|
||||||
|
let sub_idx = *idx as usize;
|
||||||
|
let is_arrow = if sub_idx > 0 && sub_idx <= module.sub_modules.len() {
|
||||||
|
module.sub_modules[sub_idx - 1].is_arrow
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.create_function_object(*idx, is_arrow)
|
||||||
|
}
|
||||||
Constant::RegExp(pattern, flags) => {
|
Constant::RegExp(pattern, flags) => {
|
||||||
let pat_si = self.kernel.string_forge().intern(pattern).0;
|
let pat_si = self.kernel.string_forge().intern(pattern).0;
|
||||||
let flags_si = self.kernel.string_forge().intern(flags).0;
|
let flags_si = self.kernel.string_forge().intern(flags).0;
|
||||||
let pat_val = JsValue::string(pat_si, 0);
|
let pat_val = JsValue::string(pat_si, 0);
|
||||||
let flags_val = JsValue::string(flags_si, 0);
|
let flags_val = JsValue::string(flags_si, 0);
|
||||||
|
|
||||||
let native_fn = self.kernel.builtin_world().regexp_constructor.as_ptr() as *mut JsObject;
|
let native_fn =
|
||||||
|
self.kernel.builtin_world().regexp_constructor.as_ptr() as *mut JsObject;
|
||||||
let ctor = unsafe { &*native_fn };
|
let ctor = unsafe { &*native_fn };
|
||||||
let ctor_fn = ctor.native_fn();
|
let ctor_fn = ctor.native_fn();
|
||||||
if ctor_fn.is_none() {
|
if ctor_fn.is_none() {
|
||||||
@@ -338,7 +355,8 @@ impl Vm {
|
|||||||
self.regs[0] = JsValue::undefined();
|
self.regs[0] = JsValue::undefined();
|
||||||
self.regs[1] = pat_val;
|
self.regs[1] = pat_val;
|
||||||
self.regs[2] = flags_val;
|
self.regs[2] = flags_val;
|
||||||
let func: crate::native::NativeFn = unsafe { std::mem::transmute(ctor_fn.unwrap()) };
|
let func: crate::native::NativeFn =
|
||||||
|
unsafe { std::mem::transmute(ctor_fn.unwrap()) };
|
||||||
let result = func(self, &[0, 1, 2]);
|
let result = func(self, &[0, 1, 2]);
|
||||||
self.regs[0] = saved_0;
|
self.regs[0] = saved_0;
|
||||||
self.regs[1] = saved_1;
|
self.regs[1] = saved_1;
|
||||||
@@ -347,7 +365,7 @@ impl Vm {
|
|||||||
Ok(val) => val,
|
Ok(val) => val,
|
||||||
Err(_) => JsValue::undefined(),
|
Err(_) => JsValue::undefined(),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -654,6 +672,16 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OpCode::STORE_VAR => {
|
OpCode::STORE_VAR => {
|
||||||
|
if b != 0 {
|
||||||
|
// const guard: check if already initialized
|
||||||
|
if !self.regs[rd].is_undefined() {
|
||||||
|
throw_err!(
|
||||||
|
self,
|
||||||
|
TypeError,
|
||||||
|
"Assignment to constant variable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.regs[rd] = self.regs[a];
|
self.regs[rd] = self.regs[a];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,6 +735,7 @@ impl Vm {
|
|||||||
let sub_constants = self.sub_modules[sub_idx].constants.clone();
|
let sub_constants = self.sub_modules[sub_idx].constants.clone();
|
||||||
let sub_builtin_count =
|
let sub_builtin_count =
|
||||||
self.sub_modules[sub_idx].builtin_reg_map.len();
|
self.sub_modules[sub_idx].builtin_reg_map.len();
|
||||||
|
let sub_is_arrow = self.sub_modules[sub_idx].is_arrow;
|
||||||
|
|
||||||
// Copy args to regs[sub_builtin_count..sub_builtin_count+n_args]
|
// Copy args to regs[sub_builtin_count..sub_builtin_count+n_args]
|
||||||
// (builtins occupy regs[0..sub_builtin_count-1])
|
// (builtins occupy regs[0..sub_builtin_count-1])
|
||||||
@@ -714,8 +743,13 @@ impl Vm {
|
|||||||
let src_reg = first_arg_reg.wrapping_add(i as u8) as usize;
|
let src_reg = first_arg_reg.wrapping_add(i as u8) as usize;
|
||||||
self.regs[sub_builtin_count + i] = self.regs[src_reg];
|
self.regs[sub_builtin_count + i] = self.regs[src_reg];
|
||||||
}
|
}
|
||||||
// Set this (reg 254 convention)
|
// Set this (reg 254 convention).
|
||||||
self.regs[254] = callee;
|
// Arrow functions use captured lexical `this` (D-01).
|
||||||
|
self.regs[254] = if sub_is_arrow {
|
||||||
|
obj.captured_this()
|
||||||
|
} else {
|
||||||
|
callee
|
||||||
|
};
|
||||||
|
|
||||||
// Save current bytecode/constants
|
// Save current bytecode/constants
|
||||||
self.saved_bytecode_stack
|
self.saved_bytecode_stack
|
||||||
@@ -745,7 +779,15 @@ impl Vm {
|
|||||||
Constant::Null => JsValue::null(),
|
Constant::Null => JsValue::null(),
|
||||||
Constant::Undefined => JsValue::undefined(),
|
Constant::Undefined => JsValue::undefined(),
|
||||||
Constant::BytecodeFunc(idx) => {
|
Constant::BytecodeFunc(idx) => {
|
||||||
self.create_function_object(*idx)
|
let idx_usize = *idx as usize;
|
||||||
|
let nested_is_arrow = if idx_usize > 0
|
||||||
|
&& idx_usize <= self.sub_modules.len()
|
||||||
|
{
|
||||||
|
self.sub_modules[idx_usize - 1].is_arrow
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.create_function_object(*idx, nested_is_arrow)
|
||||||
}
|
}
|
||||||
Constant::RegExp(_, _) => JsValue::undefined(),
|
Constant::RegExp(_, _) => JsValue::undefined(),
|
||||||
})
|
})
|
||||||
@@ -829,6 +871,14 @@ impl Vm {
|
|||||||
"NEW_EXPRESSION: constructor is not a function"
|
"NEW_EXPRESSION: constructor is not a function"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Arrow functions cannot be used as constructors (D-03)
|
||||||
|
if ctor_obj.is_arrow() {
|
||||||
|
throw_err!(
|
||||||
|
self,
|
||||||
|
TypeError,
|
||||||
|
"arrow functions cannot be used as constructors"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Read extension word for arg_count
|
// Read extension word for arg_count
|
||||||
let ext = self.bytecode[self.pc];
|
let ext = self.bytecode[self.pc];
|
||||||
@@ -1196,6 +1246,143 @@ impl Vm {
|
|||||||
self.regs[rd] = JsValue::undefined();
|
self.regs[rd] = JsValue::undefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OpCode::TEMPLATE_STR => {
|
||||||
|
// Read header ext word: (segment_count << 16) | (total_len_hint & 0xFFFF)
|
||||||
|
let header = self.bytecode[self.pc];
|
||||||
|
self.pc += 1;
|
||||||
|
let segment_count = (header >> 16) as usize;
|
||||||
|
let len_hint = (header & 0xFFFF) as usize;
|
||||||
|
|
||||||
|
// Build result string
|
||||||
|
let mut result = String::with_capacity(len_hint.max(16));
|
||||||
|
for _ in 0..segment_count {
|
||||||
|
let seg = self.bytecode[self.pc];
|
||||||
|
self.pc += 1;
|
||||||
|
if (seg >> 31) == 1 {
|
||||||
|
// Expression: register value
|
||||||
|
let reg = (seg & 0x7F) as u8;
|
||||||
|
let val = self.regs[reg as usize];
|
||||||
|
let s = if val.is_string() {
|
||||||
|
self.kernel()
|
||||||
|
.string_forge()
|
||||||
|
.lookup(val.as_string_index())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
format!("{}", val)
|
||||||
|
};
|
||||||
|
result.push_str(&s);
|
||||||
|
} else {
|
||||||
|
// Quasi: constant string
|
||||||
|
let const_idx = (seg & 0x7FFF_FFFF) as usize;
|
||||||
|
if const_idx < self.constants.len() {
|
||||||
|
let val = self.constants[const_idx];
|
||||||
|
if val.is_string() {
|
||||||
|
let s = self
|
||||||
|
.kernel()
|
||||||
|
.string_forge()
|
||||||
|
.lookup(val.as_string_index())
|
||||||
|
.unwrap_or_default();
|
||||||
|
result.push_str(&s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let si = self.kernel.string_forge().intern(&result).0;
|
||||||
|
self.regs[rd] = JsValue::string(si, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpCode::DELETE_PROP_STATIC => {
|
||||||
|
let obj_val = self.regs[a];
|
||||||
|
if !obj_val.is_object() {
|
||||||
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
} else {
|
||||||
|
let obj_ptr = obj_val.as_js_object_ptr();
|
||||||
|
let obj = unsafe { &mut *obj_ptr };
|
||||||
|
let ext = self.bytecode[self.pc];
|
||||||
|
self.pc += 1;
|
||||||
|
let const_idx = ext as usize;
|
||||||
|
let prop_name_val = self.constants[const_idx];
|
||||||
|
if !prop_name_val.is_string() {
|
||||||
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prop_name_si = prop_name_val.as_string_index();
|
||||||
|
if let Some(pos) = self.kernel.shape_forge().lookup_position(
|
||||||
|
obj.shape_id(),
|
||||||
|
prop_name_si,
|
||||||
|
) {
|
||||||
|
obj.set_prop_at(pos, JsValue::undefined());
|
||||||
|
}
|
||||||
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpCode::DELETE_PROP_DYNAMIC => {
|
||||||
|
let obj_val = self.regs[a];
|
||||||
|
if !obj_val.is_object() {
|
||||||
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
} else {
|
||||||
|
let key_val = self.regs[b];
|
||||||
|
let prop_name_si = if key_val.is_string() {
|
||||||
|
key_val.as_string_index()
|
||||||
|
} else {
|
||||||
|
let s = crate::coercion::to_string(
|
||||||
|
self.kernel.string_forge().as_ref(),
|
||||||
|
key_val,
|
||||||
|
);
|
||||||
|
self.kernel.string_forge().intern(&s).0
|
||||||
|
};
|
||||||
|
let obj_ptr = obj_val.as_js_object_ptr();
|
||||||
|
let obj = unsafe { &mut *obj_ptr };
|
||||||
|
if let Some(pos) = self.kernel.shape_forge().lookup_position(
|
||||||
|
obj.shape_id(),
|
||||||
|
prop_name_si,
|
||||||
|
) {
|
||||||
|
obj.set_prop_at(pos, JsValue::undefined());
|
||||||
|
}
|
||||||
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpCode::INSTANCEOF => {
|
||||||
|
let lhs_val = self.regs[a];
|
||||||
|
let rhs_val = self.regs[b];
|
||||||
|
|
||||||
|
if !rhs_val.is_object() {
|
||||||
|
throw_err!(self, TypeError, "INSTANCEOF right-hand side is not callable");
|
||||||
|
}
|
||||||
|
if !lhs_val.is_object() {
|
||||||
|
self.regs[rd] = JsValue::bool(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rhs_obj = unsafe { &*rhs_val.as_js_object_ptr() };
|
||||||
|
let proto_si = self.kernel.string_forge().intern("prototype").0;
|
||||||
|
let ctor_proto = self.resolve_property(rhs_obj, proto_si);
|
||||||
|
|
||||||
|
let ctor_proto_ptr = match ctor_proto {
|
||||||
|
Some(v) if v.is_object() => v.as_js_object_ptr(),
|
||||||
|
_ => {
|
||||||
|
self.regs[rd] = JsValue::bool(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut proto = unsafe { &*lhs_val.as_js_object_ptr() }.proto();
|
||||||
|
loop {
|
||||||
|
if !proto.is_object() {
|
||||||
|
self.regs[rd] = JsValue::bool(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let proto_ptr = proto.as_js_object_ptr();
|
||||||
|
if proto_ptr == ctor_proto_ptr {
|
||||||
|
self.regs[rd] = JsValue::bool(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
proto = unsafe { &*proto_ptr }.proto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OpCode::IN => {
|
OpCode::IN => {
|
||||||
let key_val = self.regs[a];
|
let key_val = self.regs[a];
|
||||||
let obj_ptr = self.regs[b].as_object_ptr() as *mut JsObject;
|
let obj_ptr = self.regs[b].as_object_ptr() as *mut JsObject;
|
||||||
@@ -1581,11 +1768,11 @@ impl Vm {
|
|||||||
|
|
||||||
OpCode::FOR_OF_INIT => {}
|
OpCode::FOR_OF_INIT => {}
|
||||||
|
|
||||||
OpCode::FOR_OF_NEXT => {},
|
OpCode::FOR_OF_NEXT => {}
|
||||||
|
|
||||||
OpCode::FOR_OF_DONE => {},
|
OpCode::FOR_OF_DONE => {}
|
||||||
|
|
||||||
OpCode::FOR_OF_CLOSE => {},
|
OpCode::FOR_OF_CLOSE => {}
|
||||||
|
|
||||||
OpCode::THROW => {
|
OpCode::THROW => {
|
||||||
let exc_value = self.regs[rd];
|
let exc_value = self.regs[rd];
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_parser::Allocator;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(source: &str) -> String {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = match oxide_parser::parse(&allocator, source) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return format!("parse error: {}", e[0].message),
|
||||||
|
};
|
||||||
|
let module = match Compiler::new().compile(&program) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return format!("compile error: {}", e),
|
||||||
|
};
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
match vm.run(&module) {
|
||||||
|
Ok(result) => format!("{}", result),
|
||||||
|
Err(e) => format!("vm error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_val(source: &str) -> (Vm, Result<JsValue, String>) {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = match oxide_parser::parse(&allocator, source) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return (Vm::new(), Err(format!("parse error: {}", e[0].message))),
|
||||||
|
};
|
||||||
|
let module = match Compiler::new().compile(&program) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return (Vm::new(), Err(format!("compile error: {}", e))),
|
||||||
|
};
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = vm.run(&module);
|
||||||
|
(vm, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_str(vm: &Vm, val: JsValue) -> String {
|
||||||
|
if val.is_string() {
|
||||||
|
vm.kernel()
|
||||||
|
.string_forge()
|
||||||
|
.lookup(val.as_string_index())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
format!("{}", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Arrow function integration tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_expression_body_returns_value() {
|
||||||
|
assert_eq!(eval("(() => 42)()"), "42");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_block_body_returns_value() {
|
||||||
|
assert_eq!(eval("((a,b) => { return a+b; })(3,4)"), "7");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_single_param_no_parens() {
|
||||||
|
assert_eq!(eval("(x => x * 2)(5)"), "10");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_multiple_params_with_parens() {
|
||||||
|
assert_eq!(eval("((x, y) => x + y)(3, 7)"), "10");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_stored_in_variable_callable() {
|
||||||
|
assert_eq!(eval("var f = () => 99; f()"), "99");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_stored_in_variable_with_params() {
|
||||||
|
assert_eq!(eval("var add = (a,b) => a+b; add(10,20)"), "30");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_not_constructable_throws() {
|
||||||
|
let result = eval("new (() => {})");
|
||||||
|
assert!(
|
||||||
|
result.contains("TypeError") || result.contains("arrow"),
|
||||||
|
"Expected arrow constructor TypeError, got: {}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_no_params_string_result() {
|
||||||
|
let (vm, result) = eval_val("(() => 'hello')()");
|
||||||
|
let s = to_str(&vm, result.unwrap());
|
||||||
|
assert_eq!(s, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_expression_body_implicit_return() {
|
||||||
|
assert_eq!(eval("(() => 1 + 2)()"), "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_block_body_needs_explicit_return() {
|
||||||
|
assert_eq!(eval("((a,b) => { a+b; })(3,4)"), "undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arrow_at_global_scope_this_is_undefined() {
|
||||||
|
assert_eq!(eval("(() => this)()"), "undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_arrows_in_same_scope() {
|
||||||
|
assert_eq!(eval("var a = () => 1, b = () => 2; a() + b()"), "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Arrow functions capturing lexical `this` from enclosing functions
|
||||||
|
// require nested function call support (sub_module flattening), which is
|
||||||
|
// a pre-existing limitation. These tests will be enabled in a future phase:
|
||||||
|
//
|
||||||
|
// fn arrow_captures_enclosing_this() {
|
||||||
|
// // const o = {x:10, f:function(){ return (() => this.x)(); }}; o.f()
|
||||||
|
// // Expected: 10
|
||||||
|
// }
|
||||||
@@ -9,7 +9,7 @@ fn make_vm() -> Vm {
|
|||||||
|
|
||||||
fn eval(source: &str) -> Result<JsValue, String> {
|
fn eval(source: &str) -> Result<JsValue, String> {
|
||||||
let allocator = oxide_parser::Allocator::default();
|
let allocator = oxide_parser::Allocator::default();
|
||||||
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {}", e))?;
|
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
|
||||||
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
|
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
|
||||||
let mut vm = make_vm();
|
let mut vm = make_vm();
|
||||||
vm.run(&module)
|
vm.run(&module)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(source: &str) -> Result<JsValue, String> {
|
||||||
|
let allocator = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
|
||||||
|
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
vm.run(&module)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_block_scoping_outer_unchanged() {
|
||||||
|
let result = eval("let x = 1; { let x = 2; } x").unwrap();
|
||||||
|
assert_eq!(result.as_int(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_reassignment_throws() {
|
||||||
|
let result = eval("const x = 1; x = 2");
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(err.contains("TypeError") || err.contains("constant"), "got: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn var_block_not_isolated() {
|
||||||
|
let result = eval("var x = 5; { x = 10; } x").unwrap();
|
||||||
|
assert_eq!(result.as_int(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_existing_property() {
|
||||||
|
let result = eval("var o = {a: 1}; delete o.a; o.a").unwrap();
|
||||||
|
assert!(result.is_undefined());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_returns_true() {
|
||||||
|
let result = eval("var o = {a: 1}; delete o.a").unwrap();
|
||||||
|
assert!(result.as_bool());
|
||||||
|
assert!(result.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instanceof_array() {
|
||||||
|
let result = eval("[] instanceof Array").unwrap();
|
||||||
|
assert!(result.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_not_array() {
|
||||||
|
let result = eval("({}) instanceof Array").unwrap();
|
||||||
|
assert!(!result.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn void_returns_undefined() {
|
||||||
|
let result = eval("void 0").unwrap();
|
||||||
|
assert!(result.is_undefined());
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_parser::Allocator;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(source: &str) -> String {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = match oxide_parser::parse(&allocator, source) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return format!("parse error: {}", e[0].message),
|
||||||
|
};
|
||||||
|
let module = match Compiler::new().compile(&program) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return format!("compile error: {}", e),
|
||||||
|
};
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
match vm.run(&module) {
|
||||||
|
Ok(result) => format!("{}", result),
|
||||||
|
Err(e) => format!("vm error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_val(source: &str) -> (Vm, Result<JsValue, String>) {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = match oxide_parser::parse(&allocator, source) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return (Vm::new(), Err(format!("parse error: {}", e[0].message))),
|
||||||
|
};
|
||||||
|
let module = match Compiler::new().compile(&program) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return (Vm::new(), Err(format!("compile error: {}", e))),
|
||||||
|
};
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = vm.run(&module);
|
||||||
|
(vm, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_str(vm: &Vm, val: JsValue) -> String {
|
||||||
|
if val.is_string() {
|
||||||
|
vm.kernel()
|
||||||
|
.string_forge()
|
||||||
|
.lookup(val.as_string_index())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
format!("{}", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Template literal tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_no_expressions() {
|
||||||
|
let (vm, result) = eval_val("`hello`");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_single_expression() {
|
||||||
|
let (vm, result) = eval_val("const name = 'world'; `hello ${name}`");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_multiple_expressions() {
|
||||||
|
let (vm, result) = eval_val("`a${1}b${2}c`");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "a1b2c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_expression_only() {
|
||||||
|
let (vm, result) = eval_val("const x = 'foo', y = 'bar'; `${x}${y}`");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "foobar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_empty() {
|
||||||
|
let (vm, result) = eval_val("``");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_with_numbers() {
|
||||||
|
let (vm, result) = eval_val("`value is ${42}`");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "value is 42");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_numeric_expression() {
|
||||||
|
let (vm, result) = eval_val("`${1 + 1}`");
|
||||||
|
assert_eq!(to_str(&vm, result.unwrap()), "2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Template tagging tests (basic) --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tagged_template_basic() {
|
||||||
|
// Use Math.max as a simple native tag function to verify the CALL dispatch works.
|
||||||
|
// Math.max(cooked_array, raw_array, 42) should return 42 (the max of the args).
|
||||||
|
// This avoids bytecode function call complexity.
|
||||||
|
let result = eval("Math.max`hello ${42} world`");
|
||||||
|
// Math.max on the args should work - just verify it doesn't crash
|
||||||
|
assert!(
|
||||||
|
!result.starts_with("vm error:"),
|
||||||
|
"Tagged template should not crash, got: {}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tagged_template_compiles_no_error() {
|
||||||
|
// Verify that tagged templates compile without error (even if the tag
|
||||||
|
// function behavior isn't fully tested)
|
||||||
|
let result = eval("function t(s,v){ return s[0]+v; } t`x${1}`");
|
||||||
|
assert!(
|
||||||
|
!result.starts_with("compile error:"),
|
||||||
|
"Tagged template should compile"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user