use std::{
collections::{HashMap, HashSet},
rc::Rc,
};
use crate::bytecode::bytecode_array::{
BytecodeArray, ConstantPoolEntry, HandlerTableEntry, SourcePosition,
};
use crate::bytecode::bytecodes::{Instruction, Opcode, Operand, encode};
use crate::bytecode::feedback::{FeedbackMetadata, FeedbackSlotKind};
use crate::bytecode::register::{Register, RegisterAllocator};
use crate::error::{StatorError, StatorResult};
use crate::parser::ast::{
ArrowBody, ArrowExpr, AssignOp, AssignTarget, BinaryOp, BlockStmt, ExportDefaultExpr,
ExportNamedDecl, Expr, FnDecl, FnExpr, ForInOfLeft, ForInit, ForStmt, ImportSpecifier,
LogicalOp, MemberProp, ModuleDecl, ModuleExportName, ObjectPatProp, ObjectProp, Param, Pat,
Program, ProgramItem, PropKey, PropValue, SourceLocation, SourceType, Stmt, UnaryOp, UpdateOp,
VarDecl, VarDeclarator, VarKind,
};
pub(crate) const RUNTIME_DYNAMIC_IMPORT: u32 = 1;
pub(crate) const RUNTIME_IS_GENERATOR_RETURN_COMPLETION: u32 = 2;
pub(crate) const RUNTIME_GET_GENERATOR_RETURN_COMPLETION_VALUE: u32 = 3;
fn to_reg_op(reg: Register) -> Operand {
Operand::Register(reg.0 as u32)
}
fn operand_bytes_needed(op: Operand) -> usize {
match op {
Operand::Register(v)
| Operand::RegisterCount(v)
| Operand::ConstantPoolIdx(v)
| Operand::FeedbackSlot(v)
| Operand::RuntimeId(v) => {
if v <= u8::MAX as u32 {
1
} else if v <= u16::MAX as u32 {
2
} else {
4
}
}
Operand::Immediate(v) | Operand::JumpOffset(v) => {
if (i8::MIN as i32..=i8::MAX as i32).contains(&v) {
1
} else if (i16::MIN as i32..=i16::MAX as i32).contains(&v) {
2
} else {
4
}
}
Operand::Flag(_) => 1,
}
}
fn instr_byte_size(instr: &Instruction) -> usize {
let w = instr
.operands()
.iter()
.map(|&op| operand_bytes_needed(op))
.max()
.unwrap_or(1);
let prefix = usize::from(w > 1);
prefix + 1 + instr.operand_count() * w
}
fn compute_byte_offsets(instructions: &[Instruction]) -> Vec<usize> {
let mut offsets = Vec::with_capacity(instructions.len() + 1);
let mut pos = 0usize;
for instr in instructions {
offsets.push(pos);
pos += instr_byte_size(instr);
}
offsets.push(pos);
offsets
}
struct Label {
bound_at: Option<usize>,
refs: Vec<usize>,
}
impl Label {
fn new() -> Self {
Self {
bound_at: None,
refs: Vec::new(),
}
}
}
#[derive(Clone, Copy)]
struct LocalBinding {
reg: Register,
is_const: bool,
needs_tdz_check: bool,
}
#[derive(Clone)]
struct PrivateNameBinding {
storage_key: String,
brand_key: Option<String>,
}
#[derive(Default)]
struct FunctionCompileOptions {
self_name: Option<String>,
private_names: HashMap<String, PrivateNameBinding>,
source_text: Option<Rc<str>>,
source_span: Option<SourceLocation>,
closure_captures: HashMap<String, u32>,
}
const CLASS_STATIC_INITIALIZER_SLOT_NAME: &str = "__class_static_initializer__";
const CLASS_INSTANCE_FIELD_KEY_PREFIX: &str = "__class_field_key_";
const CLASS_INSTANCE_FIELD_OWNER_NAME: &str = ".class_initializer_class";
struct InstanceFieldInitializer<'a> {
prop: &'a crate::parser::ast::PropertyDef,
computed_key_name: Option<String>,
}
struct FunctionCompiler {
instructions: Vec<Instruction>,
constant_pool: Vec<ConstantPoolEntry>,
allocator: RegisterAllocator,
scopes: Vec<HashMap<String, LocalBinding>>,
source_positions: Vec<SourcePosition>,
pending_positions: Vec<(usize, u32, u32)>,
labels: Vec<Label>,
loop_stack: Vec<(usize, usize)>,
label_map: HashMap<String, (Option<usize>, usize)>,
pending_label_names: Vec<String>,
param_count: u32,
slot_kinds: Vec<FeedbackSlotKind>,
handler_table: Vec<HandlerTableEntry>,
is_generator: bool,
is_async: bool,
yield_suspend_id: u32,
is_program: bool,
is_eval_scope: bool,
is_module: bool,
module_variables: HashMap<String, (u32, i32)>,
next_module_cell: i32,
in_tail_position: bool,
is_strict: bool,
using_vars: Vec<Vec<Register>>,
for_of_iter_regs: HashMap<usize, Register>,
finally_stack: Vec<crate::parser::ast::BlockStmt>,
optional_chain_null_label: Option<usize>,
with_depth: usize,
private_names: HashMap<String, PrivateNameBinding>,
next_private_name_id: u32,
source_text: Option<Rc<str>>,
context_bindings: HashMap<String, u32>,
closure_captures: HashMap<String, u32>,
context_slot_count: u32,
}
impl FunctionCompiler {
fn new(params: &[crate::parser::ast::Param]) -> StatorResult<Self> {
let param_count = params
.iter()
.filter(|p| !matches!(p.pat, Pat::Rest(_)))
.count() as u32;
let mut compiler = Self {
instructions: Vec::new(),
constant_pool: Vec::new(),
allocator: RegisterAllocator::new(param_count),
scopes: vec![HashMap::new()],
source_positions: Vec::new(),
pending_positions: Vec::new(),
labels: Vec::new(),
loop_stack: Vec::new(),
label_map: HashMap::new(),
pending_label_names: Vec::new(),
param_count,
slot_kinds: Vec::new(),
handler_table: Vec::new(),
is_generator: false,
is_async: false,
yield_suspend_id: 0,
is_program: false,
is_eval_scope: false,
is_module: false,
module_variables: HashMap::new(),
next_module_cell: 0,
in_tail_position: false,
is_strict: false,
using_vars: vec![Vec::new()],
for_of_iter_regs: HashMap::new(),
finally_stack: Vec::new(),
optional_chain_null_label: None,
with_depth: 0,
private_names: HashMap::new(),
next_private_name_id: 0,
source_text: None,
context_bindings: HashMap::new(),
closure_captures: HashMap::new(),
context_slot_count: 0,
};
for (i, param) in params.iter().enumerate() {
if let Pat::Ident(ident) = ¶m.pat
&& param.default.is_none()
{
let reg = Register::parameter(i as u32);
compiler.scopes[0].insert(
ident.name.clone(),
LocalBinding {
reg,
is_const: false,
needs_tdz_check: false,
},
);
}
}
Ok(compiler)
}
fn source_text_for_loc(&self, loc: SourceLocation) -> Option<String> {
let source = self.source_text.as_deref()?;
source
.get(loc.start.offset..loc.end.offset)
.map(str::to_owned)
}
fn emit_param_prologue(&mut self, params: &[crate::parser::ast::Param]) -> StatorResult<()> {
for (i, param) in params.iter().enumerate() {
let param_reg = Register::parameter(i as u32);
match ¶m.pat {
Pat::Ident(ident) => {
if let Some(default_expr) = ¶m.default {
let local_reg = self.define_local(&ident.name);
self.emit_ldar(param_reg);
let default_lbl = self.new_label();
let done_lbl = self.new_label();
self.emit_jump(Opcode::JumpIfUndefined, default_lbl);
self.emit_star(local_reg);
self.emit_jump(Opcode::Jump, done_lbl);
self.bind_label(default_lbl);
self.compile_expr(default_expr)?;
self.emit_star(local_reg);
self.bind_label(done_lbl);
}
}
Pat::Object(_) | Pat::Array(_) => {
if let Some(default_expr) = ¶m.default {
let done_lbl = self.new_label();
self.emit_ldar(param_reg);
self.emit_jump(Opcode::JumpIfNotUndefined, done_lbl);
self.compile_expr(default_expr)?;
self.emit_star(param_reg);
self.bind_label(done_lbl);
}
self.compile_binding_pattern(
¶m.pat,
param_reg,
BindingMode::Declare { is_const: false },
)?;
}
Pat::Rest(rest) => {
self.emit(Instruction::new_unchecked(
Opcode::CreateRestParameter,
vec![],
));
if let Pat::Ident(ident) = &*rest.argument {
let local = self.define_local(&ident.name);
self.emit_star(local);
}
}
Pat::Assign(assign) => {
self.compile_binding_pattern(
¶m.pat,
param_reg,
BindingMode::Declare { is_const: false },
)?;
let _ = assign;
}
Pat::Expr(_) => {
self.compile_binding_pattern(
¶m.pat,
param_reg,
BindingMode::Declare { is_const: false },
)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BindingMode {
Declare { is_const: bool },
Assign,
}
impl FunctionCompiler {
fn compile_binding_pattern(
&mut self,
pat: &Pat,
source_reg: Register,
mode: BindingMode,
) -> StatorResult<()> {
match pat {
Pat::Ident(ident) => match mode {
BindingMode::Declare { is_const } => {
let local = if is_const {
self.define_const_local(&ident.name)
} else {
self.define_local(&ident.name)
};
self.emit_ldar(source_reg);
self.emit_star(local);
}
BindingMode::Assign => {
self.emit_ldar(source_reg);
self.compile_ident_store(&ident.name)?;
}
},
Pat::Object(obj_pat) => {
let has_rest = obj_pat
.properties
.iter()
.any(|p| matches!(p, ObjectPatProp::Rest(_)));
let mut excluded_static_keys: Vec<String> = Vec::new();
let mut excluded_computed_regs: Vec<Register> = Vec::new();
for prop in &obj_pat.properties {
match prop {
ObjectPatProp::KeyValue(kv) => {
match &kv.key {
crate::parser::ast::PropKey::Ident(id) => {
if has_rest {
excluded_static_keys.push(id.name.clone());
}
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(source_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::PropKey::Str(s) => {
if has_rest {
excluded_static_keys.push(s.value.clone());
}
let name_idx = self.add_string(&s.value);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(source_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::PropKey::Num(n) => {
if has_rest {
excluded_static_keys.push(n.value.to_string());
}
let name_idx = self.add_string(&n.value.to_string());
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(source_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::PropKey::Computed(expr) => {
self.compile_expr(expr)?;
if has_rest {
let key_reg = self.allocator.new_local();
self.emit_star(key_reg);
excluded_computed_regs.push(key_reg);
}
let slot = self.alloc_slot(FeedbackSlotKind::KeyedLoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaKeyedProperty,
vec![to_reg_op(source_reg), slot],
));
}
crate::parser::ast::PropKey::Private(id) => {
if has_rest {
excluded_static_keys.push(format!("#{}", id.name));
}
let name_idx = self.add_string(&format!("#{}", id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(source_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
}
let scratch = self.allocator.new_local();
self.emit_star(scratch);
self.compile_binding_pattern(&kv.value, scratch, mode)?;
}
ObjectPatProp::Assign(assign_prop) => {
if has_rest {
excluded_static_keys.push(assign_prop.key.name.clone());
}
let name_idx = self.add_string(&assign_prop.key.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(source_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
match mode {
BindingMode::Declare { is_const } => {
let local = if is_const {
self.define_const_local(&assign_prop.key.name)
} else {
self.define_local(&assign_prop.key.name)
};
if let Some(default_expr) = &assign_prop.value {
let done_lbl = self.new_label();
self.emit_star(local);
self.emit_ldar(local);
self.emit_jump(Opcode::JumpIfNotUndefined, done_lbl);
self.compile_expr(default_expr)?;
self.emit_star(local);
self.bind_label(done_lbl);
} else {
self.emit_star(local);
}
}
BindingMode::Assign => {
if let Some(default_expr) = &assign_prop.value {
let done_lbl = self.new_label();
self.emit_jump(Opcode::JumpIfNotUndefined, done_lbl);
self.compile_expr(default_expr)?;
self.bind_label(done_lbl);
}
self.compile_ident_store(&assign_prop.key.name)?;
}
}
}
ObjectPatProp::Rest(rest) => {
self.emit(Instruction::new_unchecked(
Opcode::CreateEmptyObjectLiteral,
vec![],
));
let rest_reg = self.allocator.new_local();
self.emit_star(rest_reg);
self.emit(Instruction::new_unchecked(
Opcode::CopyDataProperties,
vec![to_reg_op(rest_reg), to_reg_op(source_reg)],
));
for key in &excluded_static_keys {
let key_idx = self.add_string(key);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(key_idx)],
));
self.emit(Instruction::new_unchecked(
Opcode::DeletePropertySloppy,
vec![to_reg_op(rest_reg)],
));
}
for &key_reg in &excluded_computed_regs {
self.emit_ldar(key_reg);
self.emit(Instruction::new_unchecked(
Opcode::DeletePropertySloppy,
vec![to_reg_op(rest_reg)],
));
}
self.compile_binding_pattern(&rest.argument, rest_reg, mode)?;
}
}
}
}
Pat::Array(arr_pat) => {
let load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::GetIterator,
vec![to_reg_op(source_reg), load_slot, call_slot],
));
let iter_reg = self.allocator.new_local();
self.emit_star(iter_reg);
let val_reg = self.allocator.new_local();
let total = arr_pat.elements.len();
for (i, element) in arr_pat.elements.iter().enumerate() {
if let Some(Pat::Rest(rest)) = element
&& i == total - 1
{
let arr_slot = self.alloc_slot(FeedbackSlotKind::Literal);
self.emit(Instruction::new_unchecked(
Opcode::CreateEmptyArrayLiteral,
vec![arr_slot],
));
let rest_arr_reg = self.allocator.new_local();
self.emit_star(rest_arr_reg);
let idx_reg = self.allocator.new_local();
self.emit(Instruction::new_unchecked(Opcode::LdaZero, vec![]));
self.emit_star(idx_reg);
let loop_lbl = self.new_label();
let done_lbl = self.new_label();
self.bind_label(loop_lbl);
self.emit(Instruction::new_unchecked(
Opcode::IteratorNext,
vec![to_reg_op(iter_reg), to_reg_op(val_reg)],
));
self.emit_jump_if_true_to(done_lbl);
self.emit_ldar(val_reg);
let elem_slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaInArrayLiteral,
vec![to_reg_op(rest_arr_reg), to_reg_op(idx_reg), elem_slot],
));
self.emit_ldar(idx_reg);
let inc_slot = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(Opcode::Inc, vec![inc_slot]));
self.emit_star(idx_reg);
self.emit_jump_loop_to(loop_lbl);
self.bind_label(done_lbl);
self.compile_binding_pattern(&rest.argument, rest_arr_reg, mode)?;
} else {
self.emit(Instruction::new_unchecked(
Opcode::IteratorNext,
vec![to_reg_op(iter_reg), to_reg_op(val_reg)],
));
if let Some(elem_pat) = element {
self.compile_binding_pattern(elem_pat, val_reg, mode)?;
}
}
}
}
Pat::Assign(assign_pat) => {
let done_lbl = self.new_label();
self.emit_ldar(source_reg);
self.emit_jump(Opcode::JumpIfNotUndefined, done_lbl);
self.compile_expr(&assign_pat.right)?;
self.emit_star(source_reg);
self.bind_label(done_lbl);
self.compile_binding_pattern(&assign_pat.left, source_reg, mode)?;
}
Pat::Rest(rest) => {
self.compile_binding_pattern(&rest.argument, source_reg, mode)?;
}
Pat::Expr(expr) => {
self.emit_ldar(source_reg);
match expr.as_ref() {
Expr::Member(m) => self.compile_member_store(m)?,
Expr::Ident(id) => match mode {
BindingMode::Declare { is_const } => {
let local = if is_const {
self.define_const_local(&id.name)
} else {
self.define_local(&id.name)
};
self.emit_star(local);
}
BindingMode::Assign => {
self.compile_ident_store(&id.name)?;
}
},
other => {
let loc = other.loc();
return Err(StatorError::SyntaxError(format!(
"at {}:{} — Invalid destructuring assignment target",
loc.start.line, loc.start.column
)));
}
}
}
}
Ok(())
}
fn add_number(&mut self, value: f64) -> u32 {
let bits = value.to_bits();
for (i, e) in self.constant_pool.iter().enumerate() {
if let ConstantPoolEntry::Number(v) = e
&& v.to_bits() == bits
{
return i as u32;
}
}
let idx = self.constant_pool.len() as u32;
self.constant_pool.push(ConstantPoolEntry::Number(value));
idx
}
fn add_string(&mut self, s: &str) -> u32 {
for (i, e) in self.constant_pool.iter().enumerate() {
if let ConstantPoolEntry::String(v) = e
&& v == s
{
return i as u32;
}
}
let idx = self.constant_pool.len() as u32;
self.constant_pool
.push(ConstantPoolEntry::String(s.to_owned()));
idx
}
fn add_bigint(&mut self, value: i128) -> u32 {
for (i, e) in self.constant_pool.iter().enumerate() {
if let ConstantPoolEntry::BigInt(v) = e
&& *v == value
{
return i as u32;
}
}
let idx = self.constant_pool.len() as u32;
self.constant_pool.push(ConstantPoolEntry::BigInt(value));
idx
}
fn add_constant_raw(&mut self, entry: ConstantPoolEntry) -> u32 {
let idx = self.constant_pool.len() as u32;
self.constant_pool.push(entry);
idx
}
fn resolve_private_binding(&self, name: &str) -> Option<&PrivateNameBinding> {
self.private_names.get(name)
}
fn resolve_private_storage_key(&self, name: &str) -> String {
self.resolve_private_binding(name)
.map(|binding| binding.storage_key.clone())
.unwrap_or_else(|| format!("#{name}"))
}
fn resolve_private_brand_key(&self, name: &str) -> String {
self.resolve_private_binding(name)
.and_then(|binding| binding.brand_key.clone())
.unwrap_or_else(|| self.resolve_private_storage_key(name))
}
fn resolve_private_kind_key(&self, name: &str) -> String {
self.resolve_private_storage_key(name)
.replacen(".private.", ".private.kind.", 1)
}
fn build_class_private_names(
&mut self,
body: &crate::parser::ast::ClassBody,
) -> HashMap<String, PrivateNameBinding> {
use crate::parser::ast::{ClassMember, PropKey};
let mut private_names = HashMap::new();
for member in &body.body {
let (name, is_static) = match member {
ClassMember::Method(method) => match &method.key {
PropKey::Private(id) => (&id.name, method.is_static),
_ => continue,
},
ClassMember::Property(prop) => match &prop.key {
PropKey::Private(id) => (&id.name, prop.is_static),
_ => continue,
},
ClassMember::StaticBlock(_) => continue,
};
private_names.entry(name.clone()).or_insert_with(|| {
let private_id = self.next_private_name_id;
self.next_private_name_id += 1;
let storage_key = format!(".private.{private_id}.{name}");
let brand_key = if is_static {
None
} else {
Some(format!(".private.brand.{private_id}.{name}"))
};
PrivateNameBinding {
storage_key,
brand_key,
}
});
}
private_names
}
fn inline_pending_finally_blocks(&mut self) -> StatorResult<()> {
let blocks: Vec<_> = self.finally_stack.clone();
for block in blocks.iter().rev() {
self.compile_block(block)?;
}
Ok(())
}
fn alloc_slot(&mut self, kind: FeedbackSlotKind) -> Operand {
let idx = self.slot_kinds.len() as u32;
self.slot_kinds.push(kind);
Operand::FeedbackSlot(idx)
}
fn new_label(&mut self) -> usize {
let id = self.labels.len();
self.labels.push(Label::new());
id
}
fn bind_label(&mut self, label_id: usize) {
self.labels[label_id].bound_at = Some(self.instructions.len());
}
fn patch_pending_labels(&mut self, continue_label: usize) {
for name in self.pending_label_names.drain(..) {
if let Some(entry) = self.label_map.get_mut(&name) {
entry.0 = Some(continue_label);
}
}
}
fn stmt_is_iteration_target(stmt: &Stmt) -> bool {
match stmt {
Stmt::While(_) | Stmt::DoWhile(_) | Stmt::For(_) | Stmt::ForIn(_) | Stmt::ForOf(_) => {
true
}
Stmt::Labeled(s) => Self::stmt_is_iteration_target(&s.body),
_ => false,
}
}
fn emit(&mut self, instr: Instruction) -> usize {
let idx = self.instructions.len();
self.instructions.push(instr);
idx
}
fn emit_jump(&mut self, opcode: Opcode, label_id: usize) -> usize {
let idx = self.instructions.len();
self.emit(Instruction::new_unchecked(
opcode,
vec![Operand::JumpOffset(0)],
));
self.labels[label_id].refs.push(idx);
idx
}
fn emit_star(&mut self, reg: Register) {
self.emit(Instruction::new_unchecked(
Opcode::Star,
vec![to_reg_op(reg)],
));
}
fn emit_ldar(&mut self, reg: Register) {
self.emit(Instruction::new_unchecked(
Opcode::Ldar,
vec![to_reg_op(reg)],
));
}
fn push_scope(&mut self) {
self.scopes.push(HashMap::new());
self.using_vars.push(Vec::new());
}
fn pop_scope(&mut self) {
if let Some(vars) = self.using_vars.pop() {
for reg in vars.into_iter().rev() {
self.emit_using_dispose(reg);
}
}
self.scopes.pop();
}
fn emit_using_dispose(&mut self, reg: Register) {
self.emit_ldar(reg);
let dispose_key = self.add_constant_raw(ConstantPoolEntry::String("@@dispose".into()));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![to_reg_op(reg), Operand::ConstantPoolIdx(dispose_key), slot],
));
let method_reg = self.allocator.new_local();
self.emit_star(method_reg);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(method_reg),
to_reg_op(reg),
Operand::Register(0),
Operand::RegisterCount(0),
call_slot,
],
));
}
fn define_local(&mut self, name: &str) -> Register {
self.define_local_with_kind(name, false)
}
fn define_const_local(&mut self, name: &str) -> Register {
self.define_local_with_kind(name, true)
}
fn define_local_with_kind(&mut self, name: &str, is_const: bool) -> Register {
let reg = self.allocator.new_local();
self.scopes.last_mut().unwrap().insert(
name.to_owned(),
LocalBinding {
reg,
is_const,
needs_tdz_check: false,
},
);
reg
}
fn define_function_scoped_local(&mut self, name: &str) -> Register {
let reg = self.allocator.new_local();
self.scopes[0].insert(
name.to_owned(),
LocalBinding {
reg,
is_const: false,
needs_tdz_check: false,
},
);
reg
}
fn lookup_var(&self, name: &str) -> Option<LocalBinding> {
for scope in self.scopes.iter().rev() {
if let Some(&binding) = scope.get(name) {
return Some(binding);
}
}
None
}
fn mark_initialized(&mut self, name: &str) {
for scope in self.scopes.iter_mut().rev() {
if let Some(binding) = scope.get_mut(name) {
binding.needs_tdz_check = false;
return;
}
}
}
fn collect_var_names(stmts: &[Stmt]) -> Vec<String> {
let mut names = Vec::new();
for stmt in stmts {
Self::collect_var_names_in_stmt(stmt, &mut names);
}
names
}
fn collect_var_names_in_stmt(stmt: &Stmt, names: &mut Vec<String>) {
match stmt {
Stmt::VarDecl(decl) if decl.kind == VarKind::Var => {
for d in &decl.declarators {
Self::collect_pat_names(&d.id, names);
}
}
Stmt::Block(b) => {
for s in &b.body {
Self::collect_var_names_in_stmt(s, names);
}
}
Stmt::If(s) => {
Self::collect_var_names_in_stmt(&s.consequent, names);
if let Some(alt) = &s.alternate {
Self::collect_var_names_in_stmt(alt, names);
}
}
Stmt::While(s) => Self::collect_var_names_in_stmt(&s.body, names),
Stmt::DoWhile(s) => Self::collect_var_names_in_stmt(&s.body, names),
Stmt::For(s) => {
if let Some(ForInit::VarDecl(decl)) = &s.init
&& decl.kind == VarKind::Var
{
for d in &decl.declarators {
Self::collect_pat_names(&d.id, names);
}
}
Self::collect_var_names_in_stmt(&s.body, names);
}
Stmt::ForIn(s) => {
if let crate::parser::ast::ForInOfLeft::VarDecl(decl) = &s.left
&& decl.kind == VarKind::Var
{
for d in &decl.declarators {
Self::collect_pat_names(&d.id, names);
}
}
Self::collect_var_names_in_stmt(&s.body, names);
}
Stmt::ForOf(s) => {
if let crate::parser::ast::ForInOfLeft::VarDecl(decl) = &s.left
&& decl.kind == VarKind::Var
{
for d in &decl.declarators {
Self::collect_pat_names(&d.id, names);
}
}
Self::collect_var_names_in_stmt(&s.body, names);
}
Stmt::Switch(s) => {
for case in &s.cases {
for cs in &case.consequent {
Self::collect_var_names_in_stmt(cs, names);
}
}
}
Stmt::Try(s) => {
for ts in &s.block.body {
Self::collect_var_names_in_stmt(ts, names);
}
if let Some(handler) = &s.handler {
for hs in &handler.body.body {
Self::collect_var_names_in_stmt(hs, names);
}
}
if let Some(fin) = &s.finalizer {
for fs in &fin.body {
Self::collect_var_names_in_stmt(fs, names);
}
}
}
Stmt::Labeled(s) => Self::collect_var_names_in_stmt(&s.body, names),
Stmt::With(s) => Self::collect_var_names_in_stmt(&s.body, names),
_ => {}
}
}
fn collect_pat_names(pat: &Pat, names: &mut Vec<String>) {
match pat {
Pat::Ident(id) => names.push(id.name.clone()),
Pat::Array(arr) => {
for p in arr.elements.iter().flatten() {
Self::collect_pat_names(p, names);
}
}
Pat::Object(obj) => {
for prop in &obj.properties {
match prop {
ObjectPatProp::KeyValue(kv) => Self::collect_pat_names(&kv.value, names),
ObjectPatProp::Assign(a) => names.push(a.key.name.clone()),
ObjectPatProp::Rest(r) => Self::collect_pat_names(&r.argument, names),
}
}
}
Pat::Rest(r) => Self::collect_pat_names(&r.argument, names),
Pat::Assign(a) => Self::collect_pat_names(&a.left, names),
Pat::Expr(_) => {}
}
}
fn collect_lexical_names(stmts: &[Stmt]) -> Vec<(String, bool)> {
let mut names = Vec::new();
for stmt in stmts {
match stmt {
Stmt::VarDecl(decl) => match decl.kind {
VarKind::Let | VarKind::Const => {
let is_const = decl.kind == VarKind::Const;
for d in &decl.declarators {
let mut pat_names = Vec::new();
Self::collect_pat_names(&d.id, &mut pat_names);
for n in pat_names {
names.push((n, is_const));
}
}
}
_ => {}
},
Stmt::ClassDecl(decl) => {
if let Some(id) = &decl.id {
names.push((id.name.clone(), false));
}
}
_ => {}
}
}
names
}
fn collect_block_fn_names(stmts: &[Stmt]) -> Vec<String> {
let mut names = Vec::new();
for stmt in stmts {
Self::collect_block_fn_names_in_stmt(stmt, &mut names);
}
names
}
fn collect_block_fn_names_in_stmt(stmt: &Stmt, names: &mut Vec<String>) {
match stmt {
Stmt::Block(b) => {
for s in &b.body {
if let Stmt::FnDecl(f) = s
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(s, names);
}
}
Stmt::If(s) => {
if let Stmt::FnDecl(f) = s.consequent.as_ref()
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(&s.consequent, names);
if let Some(alt) = &s.alternate {
if let Stmt::FnDecl(f) = alt.as_ref()
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(alt, names);
}
}
Stmt::While(s) => Self::collect_block_fn_names_in_stmt(&s.body, names),
Stmt::DoWhile(s) => Self::collect_block_fn_names_in_stmt(&s.body, names),
Stmt::For(s) => Self::collect_block_fn_names_in_stmt(&s.body, names),
Stmt::ForIn(s) => Self::collect_block_fn_names_in_stmt(&s.body, names),
Stmt::ForOf(s) => Self::collect_block_fn_names_in_stmt(&s.body, names),
Stmt::Switch(s) => {
for case in &s.cases {
for cs in &case.consequent {
if let Stmt::FnDecl(f) = cs
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(cs, names);
}
}
}
Stmt::Try(s) => {
for ts in &s.block.body {
if let Stmt::FnDecl(f) = ts
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(ts, names);
}
if let Some(handler) = &s.handler {
for hs in &handler.body.body {
if let Stmt::FnDecl(f) = hs
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(hs, names);
}
}
if let Some(fin) = &s.finalizer {
for fs in &fin.body {
if let Stmt::FnDecl(f) = fs
&& let Some(id) = &f.id
{
names.push(id.name.clone());
}
Self::collect_block_fn_names_in_stmt(fs, names);
}
}
}
Stmt::Labeled(s) => Self::collect_block_fn_names_in_stmt(&s.body, names),
_ => {}
}
}
fn hoist_annex_b_fn_declarations(&mut self, stmts: &[Stmt]) {
if self.is_strict {
return;
}
let names = Self::collect_block_fn_names(stmts);
for name in names {
if self.scopes[0].contains_key(&name) {
continue;
}
let reg = self.allocator.new_local();
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.emit_star(reg);
self.scopes[0].insert(
name,
LocalBinding {
reg,
is_const: false,
needs_tdz_check: false,
},
);
}
}
fn hoist_annex_b_fn_declarations_global(&mut self, stmts: &[Stmt]) {
if self.is_strict {
return;
}
let names = Self::collect_block_fn_names(stmts);
for name in names {
let name_idx = self.add_string(&name);
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
let slot = self.alloc_slot(FeedbackSlotKind::StoreGlobal);
self.emit(Instruction::new_unchecked(
Opcode::StaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
fn hoist_lexical_decls(&mut self, stmts: &[Stmt]) {
let lex_names = Self::collect_lexical_names(stmts);
for (name, is_const) in lex_names {
let reg = self.allocator.new_local();
self.emit(Instruction::new_unchecked(Opcode::LdaTheHole, vec![]));
self.emit_star(reg);
self.scopes.last_mut().unwrap().insert(
name,
LocalBinding {
reg,
is_const,
needs_tdz_check: true,
},
);
}
}
fn hoist_var_declarations(&mut self, stmts: &[Stmt]) {
let var_names = Self::collect_var_names(stmts);
for name in var_names {
if self.scopes[0].contains_key(&name) {
continue; }
let reg = self.allocator.new_local();
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.emit_star(reg);
self.scopes[0].insert(
name,
LocalBinding {
reg,
is_const: false,
needs_tdz_check: false,
},
);
}
}
fn hoist_var_declarations_global(&mut self, stmts: &[Stmt]) {
let var_names = Self::collect_var_names(stmts);
for name in var_names {
let name_idx = self.add_string(&name);
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
let slot = self.alloc_slot(FeedbackSlotKind::StoreGlobal);
self.emit(Instruction::new_unchecked(
Opcode::StaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
fn compile_stmt(&mut self, stmt: &Stmt) -> StatorResult<()> {
stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
let loc = stmt.loc();
if loc.start.line > 0 {
let instr_idx = self.instructions.len();
self.pending_positions
.push((instr_idx, loc.start.line, loc.start.column));
}
match stmt {
Stmt::Block(s) => self.compile_block(s),
Stmt::VarDecl(s) => self.compile_var_decl(s),
Stmt::FnDecl(s) => self.compile_fn_decl(s),
Stmt::Expr(s) => {
self.compile_expr(&s.expr)?;
Ok(())
}
Stmt::If(s) => self.compile_if(s),
Stmt::While(s) => self.compile_while(s),
Stmt::DoWhile(s) => self.compile_do_while(s),
Stmt::For(s) => self.compile_for(s),
Stmt::Return(s) => self.compile_return(s),
Stmt::Throw(s) => {
self.compile_expr(&s.argument)?;
self.emit(Instruction::new_unchecked(Opcode::Throw, vec![]));
Ok(())
}
Stmt::Try(s) => self.compile_try(s),
Stmt::Break(s) => self.compile_break(s),
Stmt::Continue(s) => self.compile_continue(s),
Stmt::Labeled(s) => self.compile_labeled(s),
Stmt::Debugger(_) => {
self.emit(Instruction::new_unchecked(Opcode::Debugger, vec![]));
Ok(())
}
Stmt::Empty(_) => Ok(()),
Stmt::Switch(s) => self.compile_switch(s),
Stmt::ForIn(s) => self.compile_for_in(s),
Stmt::With(s) => self.compile_with(s),
Stmt::ClassDecl(c) => self.compile_class_decl(c),
Stmt::ForOf(s) => self.compile_for_of(s),
}
}) }
fn compile_block(&mut self, block: &BlockStmt) -> StatorResult<()> {
self.push_scope();
self.hoist_lexical_decls(&block.body);
for stmt in &block.body {
self.compile_stmt(stmt)?;
}
self.pop_scope();
Ok(())
}
fn compile_with(&mut self, s: &crate::parser::ast::WithStmt) -> StatorResult<()> {
self.compile_expr(&s.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit(Instruction::new_unchecked(
Opcode::ToObject,
vec![to_reg_op(obj_reg)],
));
let scope_idx = self.add_string("with");
self.emit(Instruction::new_unchecked(
Opcode::CreateWithContext,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(scope_idx)],
));
let ctx_reg = self.allocator.allocate_temporary();
self.emit(Instruction::new_unchecked(
Opcode::PushContext,
vec![to_reg_op(ctx_reg)],
));
self.push_scope();
self.with_depth += 1;
self.compile_stmt(&s.body)?;
self.with_depth -= 1;
self.pop_scope();
self.emit(Instruction::new_unchecked(
Opcode::PopContext,
vec![to_reg_op(ctx_reg)],
));
self.allocator
.release_temporary(ctx_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_var_decl(&mut self, decl: &VarDecl) -> StatorResult<()> {
let is_using = matches!(decl.kind, VarKind::Using | VarKind::AwaitUsing);
let is_const = matches!(decl.kind, VarKind::Const);
let is_var = matches!(decl.kind, VarKind::Var);
for declarator in &decl.declarators {
let reg = self.compile_var_declarator(declarator, is_const, is_var)?;
if is_using && let Some(r) = reg {
self.using_vars.last_mut().unwrap().push(r);
}
}
Ok(())
}
fn compile_var_declarator(
&mut self,
declarator: &VarDeclarator,
is_const: bool,
is_var: bool,
) -> StatorResult<Option<Register>> {
match &declarator.id {
Pat::Ident(ident) => {
if self.is_eval_scope {
if let Some(init) = &declarator.init {
if !self.compile_named_callable_expr(init, &ident.name)? {
self.compile_expr(init)?;
}
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
self.compile_ident_store(&ident.name)?;
Ok(None)
} else if self.is_program && is_var {
if let Some(init) = &declarator.init {
if !self.compile_named_callable_expr(init, &ident.name)? {
self.compile_expr(init)?;
}
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let name_idx = self.add_string(&ident.name);
let sta_slot = self.alloc_slot(FeedbackSlotKind::StoreGlobal);
self.emit(Instruction::new_unchecked(
Opcode::StaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), sta_slot],
));
Ok(None)
} else if is_var {
let reg = if let Some(binding) = self.scopes[0].get(&ident.name) {
binding.reg
} else {
self.define_function_scoped_local(&ident.name)
};
if let Some(init) = &declarator.init {
if !self.compile_named_callable_expr(init, &ident.name)? {
self.compile_expr(init)?;
}
if let Some(&slot) = self.context_bindings.get(&*ident.name) {
self.emit(Instruction::new_unchecked(
Opcode::StaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
} else {
self.emit_star(reg);
}
}
Ok(Some(reg))
} else {
let reg = if let Some(binding) = self.scopes.last().unwrap().get(&ident.name) {
binding.reg
} else if is_const {
self.define_const_local(&ident.name)
} else {
self.define_local(&ident.name)
};
if let Some(init) = &declarator.init {
if !self.compile_named_callable_expr(init, &ident.name)? {
self.compile_expr(init)?;
}
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
if let Some(&slot) = self.context_bindings.get(&*ident.name) {
self.emit(Instruction::new_unchecked(
Opcode::StaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
} else {
self.emit_star(reg);
}
self.mark_initialized(&ident.name);
Ok(Some(reg))
}
}
pat => {
let source_reg = self.allocator.new_local();
if let Some(init) = &declarator.init {
self.compile_expr(init)?;
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
self.emit_star(source_reg);
self.compile_binding_pattern(pat, source_reg, BindingMode::Declare { is_const })?;
Ok(Some(source_reg))
}
}
}
fn compile_fn_decl(&mut self, decl: &FnDecl) -> StatorResult<()> {
let func_array = compile_function_inner(
&decl.params,
&decl.body,
decl.is_generator,
decl.is_async,
decl.is_strict,
false,
FunctionCompileOptions {
self_name: None,
private_names: self.private_names.clone(),
source_text: self.source_text.clone(),
source_span: Some(decl.loc),
closure_captures: self.context_bindings.clone(),
},
)?;
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(func_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(0)],
));
if let Some(id) = &decl.id {
let reg = if let Some(binding) = self.scopes.last().unwrap().get(&id.name) {
binding.reg
} else {
self.define_local(&id.name)
};
self.emit_star(reg);
if self.is_program {
let name_idx = self.add_string(&id.name);
let sta_slot = self.alloc_slot(FeedbackSlotKind::StoreGlobal);
self.emit_ldar(reg);
self.emit(Instruction::new_unchecked(
Opcode::StaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), sta_slot],
));
}
if !self.is_strict
&& self.scopes.len() > 1
&& let Some(binding) = self.scopes[0].get(&id.name)
{
let outer_reg = binding.reg;
self.emit_ldar(reg);
self.emit_star(outer_reg);
}
}
Ok(())
}
fn compile_class_decl(&mut self, decl: &crate::parser::ast::ClassDecl) -> StatorResult<()> {
self.compile_class(
decl.id.as_ref(),
decl.super_class.as_deref(),
&decl.body,
decl.loc,
)?;
if let Some(id) = &decl.id {
let reg = self
.lookup_var(&id.name)
.map(|binding| binding.reg)
.unwrap_or_else(|| self.define_local(&id.name));
self.emit_star(reg);
self.mark_initialized(&id.name);
if self.is_program {
let name_idx = self.add_string(&id.name);
let sta_slot = self.alloc_slot(FeedbackSlotKind::StoreGlobal);
self.emit_ldar(reg);
self.emit(Instruction::new_unchecked(
Opcode::StaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), sta_slot],
));
}
}
Ok(())
}
fn compile_class_expr(&mut self, expr: &crate::parser::ast::ClassExpr) -> StatorResult<()> {
self.compile_class(
expr.id.as_ref(),
expr.super_class.as_deref(),
&expr.body,
expr.loc,
)
}
fn compile_class(
&mut self,
id: Option<&crate::parser::ast::Ident>,
super_class: Option<&Expr>,
body: &crate::parser::ast::ClassBody,
class_loc: SourceLocation,
) -> StatorResult<()> {
use crate::parser::ast::{ClassMember, MethodKind};
let outer_strict = self.is_strict;
let outer_private_names = self.private_names.clone();
self.is_strict = true;
self.private_names = self.build_class_private_names(body);
let super_reg = self.allocator.allocate_temporary();
if let Some(sc) = super_class {
self.compile_expr(sc)?;
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
self.emit_star(super_reg);
let ctor_method = body.body.iter().find_map(|m| match m {
ClassMember::Method(md) if md.kind == MethodKind::Constructor => Some(md),
_ => None,
});
let ctor_array = if let Some(ctor) = ctor_method {
compile_function_with_private_names(
&ctor.value.params,
&ctor.value.body,
false,
false,
ctor.value.is_strict,
self.private_names.clone(),
)?
} else if super_class.is_some() {
self.compile_default_derived_constructor(body.loc)?
} else {
let empty_body = BlockStmt {
loc: body.loc,
body: vec![],
};
compile_function(&[], &empty_body, false, false, true)?
};
let ctor_array = if let Some(id) = id {
ctor_array.with_function_name(&id.name)
} else {
ctor_array
};
let ctor_array = if let Some(source) = self.source_text_for_loc(class_loc) {
ctor_array.with_source_text(source)
} else {
ctor_array
};
let ctor_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(ctor_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClass,
vec![
Operand::ConstantPoolIdx(ctor_idx),
to_reg_op(super_reg),
slot,
],
));
let class_reg = self.allocator.allocate_temporary();
self.emit_star(class_reg);
if let Some(ident) = id {
let name_val_idx = self.add_string(&ident.name);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(name_val_idx)],
));
let name_key_idx = self.add_string("name");
let name_slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(name_key_idx),
name_slot,
],
));
}
let proto_reg = self.allocator.allocate_temporary();
let proto_name = self.add_string("prototype");
let proto_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(proto_name),
proto_slot,
],
));
self.emit_star(proto_reg);
let mut instance_fields: Vec<InstanceFieldInitializer<'_>> = Vec::new();
let mut next_instance_field_key = 0usize;
for member in &body.body {
match member {
ClassMember::Method(m) if m.kind != MethodKind::Constructor => {
let target = if m.is_static { class_reg } else { proto_reg };
self.compile_class_method(target, m)?;
}
ClassMember::Property(p) if p.is_static => {
self.compile_class_static_property(class_reg, p)?;
}
ClassMember::StaticBlock(sb) => {
self.compile_class_static_block(class_reg, sb)?;
}
ClassMember::Property(p) => {
let computed_key_name = match &p.key {
crate::parser::ast::PropKey::Computed(key_expr) => {
let key_reg = self.compile_computed_property_key(key_expr)?;
let key_name = format!(
"{CLASS_INSTANCE_FIELD_KEY_PREFIX}{next_instance_field_key}"
);
next_instance_field_key += 1;
let key_name_idx = self.add_string(&key_name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit_ldar(key_reg);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(key_name_idx),
slot,
],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Some(key_name)
}
_ => None,
};
instance_fields.push(InstanceFieldInitializer {
prop: p,
computed_key_name,
});
}
_ => {} }
}
let has_instance_private_names = self
.private_names
.values()
.any(|binding| binding.brand_key.is_some());
if !instance_fields.is_empty() || has_instance_private_names {
self.compile_instance_field_initializer(class_reg, &instance_fields)?;
}
self.emit_ldar(class_reg);
self.allocator
.release_temporary(proto_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(class_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(super_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.is_strict = outer_strict;
self.private_names = outer_private_names;
Ok(())
}
fn compile_default_derived_constructor(
&mut self,
loc: crate::parser::ast::SourceLocation,
) -> StatorResult<BytecodeArray> {
use crate::parser::ast::{
BlockStmt, CallExpr, Expr, ExprStmt, Ident, Param, Pat, RestElement, SpreadElement,
Stmt,
};
let args_ident = Ident {
loc,
name: "args".to_string(),
};
let params = vec![Param {
loc,
pat: Pat::Rest(Box::new(RestElement {
loc,
argument: Box::new(Pat::Ident(args_ident.clone())),
})),
default: None,
}];
let body = BlockStmt {
loc,
body: vec![Stmt::Expr(ExprStmt {
loc,
expr: Box::new(Expr::Call(Box::new(CallExpr {
loc,
callee: Box::new(Expr::Ident(Ident {
loc,
name: "super".to_string(),
})),
arguments: vec![Expr::Spread(Box::new(SpreadElement {
loc,
argument: Box::new(Expr::Ident(args_ident)),
}))],
}))),
})],
};
compile_function(¶ms, &body, false, false, true)
}
fn compile_class_method(
&mut self,
target_reg: Register,
method: &crate::parser::ast::MethodDef,
) -> StatorResult<()> {
use crate::parser::ast::{MethodKind, PropKey};
let key_name: Option<String> = match &method.key {
PropKey::Ident(id) => Some(id.name.clone()),
PropKey::Str(s) => Some(s.value.clone()),
PropKey::Num(n) => {
if n.value.fract() == 0.0 && n.value.is_finite() && n.value >= 0.0 {
Some(format!("{}", n.value as u64))
} else {
Some(n.raw.clone())
}
}
PropKey::Private(id) => Some(self.resolve_private_storage_key(&id.name)),
PropKey::Computed(_) => None,
};
match method.kind {
MethodKind::Get if key_name.is_some() => {
self.compile_fn_expr_named_with_source(
&method.value,
&format!("get {}", key_name.as_ref().unwrap()),
method.loc,
)?;
}
MethodKind::Set if key_name.is_some() => {
self.compile_fn_expr_named_with_source(
&method.value,
&format!("set {}", key_name.as_ref().unwrap()),
method.loc,
)?;
}
MethodKind::Method if key_name.is_some() => {
self.compile_fn_expr_named_with_source(
&method.value,
key_name.as_ref().unwrap(),
method.loc,
)?;
}
_ => {
self.compile_fn_expr_with_source(&method.value, method.loc)?;
}
}
match method.kind {
MethodKind::Method => {
if let Some(name) = key_name {
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassNamedOwnProperty,
vec![
to_reg_op(target_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
} else if let PropKey::Computed(key_expr) = &method.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassKeyedOwnProperty,
vec![
to_reg_op(target_reg),
to_reg_op(key_reg),
Operand::Flag(0),
slot,
],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
MethodKind::Get => {
if let Some(name) = key_name {
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassGetterProperty,
vec![
to_reg_op(target_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
} else if let PropKey::Computed(key_expr) = &method.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassKeyedGetterProperty,
vec![to_reg_op(target_reg), to_reg_op(key_reg), slot],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
MethodKind::Set => {
if let Some(name) = key_name {
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassSetterProperty,
vec![
to_reg_op(target_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
} else if let PropKey::Computed(key_expr) = &method.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassKeyedSetterProperty,
vec![to_reg_op(target_reg), to_reg_op(key_reg), slot],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
MethodKind::Constructor => {} }
if let PropKey::Private(id) = &method.key {
let kind_name = match method.kind {
MethodKind::Method => "method",
MethodKind::Get | MethodKind::Set => "accessor",
MethodKind::Constructor => unreachable!("constructors are not private methods"),
};
let kind_idx = self.add_string(kind_name);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(kind_idx)],
));
let kind_key_idx = self.add_string(&self.resolve_private_kind_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineClassNamedOwnProperty,
vec![
to_reg_op(target_reg),
Operand::ConstantPoolIdx(kind_key_idx),
slot,
],
));
}
Ok(())
}
fn compile_class_static_property(
&mut self,
class_reg: Register,
prop: &crate::parser::ast::PropertyDef,
) -> StatorResult<()> {
use crate::parser::ast::PropKey;
let computed_key_reg = if let PropKey::Computed(key_expr) = &prop.key {
Some(self.compile_computed_property_key(key_expr)?)
} else {
None
};
self.compile_class_value_with_receiver(class_reg, prop.value.as_deref(), prop.loc)?;
match &prop.key {
PropKey::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
PropKey::Str(s) => {
let name_idx = self.add_string(&s.value);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
PropKey::Num(n) => {
let name = if n.value.fract() == 0.0 && n.value.is_finite() && n.value >= 0.0 {
format!("{}", n.value as u64)
} else {
n.raw.clone()
};
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
PropKey::Computed(_) => {
let key_reg = computed_key_reg.expect("computed key register missing");
let slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineKeyedOwnProperty,
vec![
to_reg_op(class_reg),
to_reg_op(key_reg),
Operand::Flag(0),
slot,
],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
PropKey::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
let kind_idx = self.add_string("field");
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(kind_idx)],
));
let kind_key_idx = self.add_string(&self.resolve_private_kind_key(&id.name));
let kind_slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(kind_key_idx),
kind_slot,
],
));
}
}
Ok(())
}
fn compile_class_static_block(
&mut self,
class_reg: Register,
block: &crate::parser::ast::StaticBlock,
) -> StatorResult<()> {
let body = BlockStmt {
loc: block.loc,
body: block.body.clone(),
};
let block_array = compile_function_with_private_names(
&[],
&body,
false,
false,
true,
self.private_names.clone(),
)?;
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(block_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(0)],
));
let hidden_name_idx = self.add_string(CLASS_STATIC_INITIALIZER_SLOT_NAME);
let hidden_slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(hidden_name_idx),
hidden_slot,
],
));
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(callee_reg),
to_reg_op(class_reg),
Operand::RegisterCount(0),
call_slot,
],
));
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_class_value_with_receiver(
&mut self,
class_reg: Register,
value: Option<&Expr>,
loc: SourceLocation,
) -> StatorResult<()> {
use crate::parser::ast::{BlockStmt, ReturnStmt, Stmt};
let body = BlockStmt {
loc,
body: vec![Stmt::Return(ReturnStmt {
loc,
argument: value.map(|expr| Box::new(expr.clone())),
})],
};
let value_array = compile_function_with_private_names(
&[],
&body,
false,
false,
true,
self.private_names.clone(),
)?;
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(value_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(0)],
));
let hidden_name_idx = self.add_string(CLASS_STATIC_INITIALIZER_SLOT_NAME);
let hidden_slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(hidden_name_idx),
hidden_slot,
],
));
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(callee_reg),
to_reg_op(class_reg),
Operand::RegisterCount(0),
call_slot,
],
));
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_instance_field_initializer(
&mut self,
class_reg: Register,
fields: &[InstanceFieldInitializer<'_>],
) -> StatorResult<()> {
use crate::parser::ast::PropKey;
let mut ic = FunctionCompiler {
instructions: Vec::new(),
constant_pool: Vec::new(),
allocator: RegisterAllocator::new(1),
scopes: vec![{
let mut s = HashMap::new();
s.insert(
"this".to_owned(),
LocalBinding {
reg: Register::parameter(0),
is_const: false,
needs_tdz_check: false,
},
);
s
}],
source_positions: Vec::new(),
pending_positions: Vec::new(),
labels: Vec::new(),
loop_stack: Vec::new(),
label_map: HashMap::new(),
pending_label_names: Vec::new(),
param_count: 1,
slot_kinds: Vec::new(),
handler_table: Vec::new(),
is_generator: false,
yield_suspend_id: 0,
is_program: false,
is_async: false,
is_eval_scope: false,
is_module: false,
module_variables: HashMap::new(),
next_module_cell: 0,
in_tail_position: false,
is_strict: true,
using_vars: vec![Vec::new()],
for_of_iter_regs: HashMap::new(),
finally_stack: Vec::new(),
optional_chain_null_label: None,
with_depth: 0,
private_names: self.private_names.clone(),
next_private_name_id: 0,
source_text: self.source_text.clone(),
context_bindings: HashMap::new(),
closure_captures: HashMap::new(),
context_slot_count: 0,
};
let this_reg = Register::parameter(0);
let private_brand_keys: Vec<String> = ic
.private_names
.values()
.filter_map(|binding| binding.brand_key.clone())
.collect();
for brand_key in private_brand_keys {
let brand_idx = ic.add_string(&brand_key);
ic.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(brand_idx)],
));
ic.emit(Instruction::new_unchecked(
Opcode::DefinePrivateBrand,
vec![to_reg_op(this_reg)],
));
}
for field in fields {
match &field.prop.key {
PropKey::Ident(id) => {
if let Some(value) = &field.prop.value {
ic.compile_expr(value)?;
} else {
ic.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let name_idx = ic.add_string(&id.name);
let slot = ic.alloc_slot(FeedbackSlotKind::StoreProperty);
ic.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(this_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
PropKey::Str(s) => {
if let Some(value) = &field.prop.value {
ic.compile_expr(value)?;
} else {
ic.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let name_idx = ic.add_string(&s.value);
let slot = ic.alloc_slot(FeedbackSlotKind::StoreProperty);
ic.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(this_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
PropKey::Num(n) => {
if let Some(value) = &field.prop.value {
ic.compile_expr(value)?;
} else {
ic.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let name = if n.value.fract() == 0.0 && n.value.is_finite() && n.value >= 0.0 {
format!("{}", n.value as u64)
} else {
n.raw.clone()
};
let name_idx = ic.add_string(&name);
let slot = ic.alloc_slot(FeedbackSlotKind::StoreProperty);
ic.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(this_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
PropKey::Computed(_) => {
let owner_name_idx = ic.add_string(CLASS_INSTANCE_FIELD_OWNER_NAME);
let owner_slot = ic.alloc_slot(FeedbackSlotKind::LoadGlobal);
ic.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(owner_name_idx), owner_slot],
));
let owner_reg = ic.allocator.allocate_temporary();
ic.emit_star(owner_reg);
let key_name = field
.computed_key_name
.as_ref()
.expect("computed instance field missing cached key name");
let key_name_idx = ic.add_string(key_name);
let key_slot = ic.alloc_slot(FeedbackSlotKind::LoadProperty);
ic.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(owner_reg),
Operand::ConstantPoolIdx(key_name_idx),
key_slot,
],
));
let key_reg = ic.allocator.allocate_temporary();
ic.emit_star(key_reg);
ic.allocator
.release_temporary(owner_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
if let Some(value) = &field.prop.value {
ic.compile_expr(value)?;
} else {
ic.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let slot = ic.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
ic.emit(Instruction::new_unchecked(
Opcode::DefineKeyedOwnProperty,
vec![
to_reg_op(this_reg),
to_reg_op(key_reg),
Operand::Flag(0),
slot,
],
));
ic.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
PropKey::Private(id) => {
if let Some(value) = &field.prop.value {
ic.compile_expr(value)?;
} else {
ic.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let name_idx = ic.add_string(&ic.resolve_private_storage_key(&id.name));
let slot = ic.alloc_slot(FeedbackSlotKind::StoreProperty);
ic.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(this_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
let kind_idx = ic.add_string("field");
ic.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(kind_idx)],
));
let kind_key_idx = ic.add_string(&ic.resolve_private_kind_key(&id.name));
let kind_slot = ic.alloc_slot(FeedbackSlotKind::StoreProperty);
ic.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(this_reg),
Operand::ConstantPoolIdx(kind_key_idx),
kind_slot,
],
));
}
}
}
let init_array = ic.finalize()?;
let init_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(init_array)));
let closure_slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![
Operand::ConstantPoolIdx(init_idx),
closure_slot,
Operand::Flag(0),
],
));
let init_name = self.add_string(".class_field_initializer");
let store_slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![
to_reg_op(class_reg),
Operand::ConstantPoolIdx(init_name),
store_slot,
],
));
Ok(())
}
fn compile_if(&mut self, s: &crate::parser::ast::IfStmt) -> StatorResult<()> {
let else_label = self.new_label();
self.compile_expr(&s.test)?;
self.emit_jump(Opcode::JumpIfToBooleanFalse, else_label);
self.compile_stmt(&s.consequent)?;
if let Some(alt) = &s.alternate {
let end_label = self.new_label();
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(else_label);
self.compile_stmt(alt)?;
self.bind_label(end_label);
} else {
self.bind_label(else_label);
}
Ok(())
}
fn compile_while(&mut self, s: &crate::parser::ast::WhileStmt) -> StatorResult<()> {
let loop_start = self.new_label();
let loop_end = self.new_label();
self.patch_pending_labels(loop_start);
self.loop_stack.push((loop_start, loop_end));
self.bind_label(loop_start);
self.compile_expr(&s.test)?;
self.emit_jump(Opcode::JumpIfToBooleanFalse, loop_end);
self.compile_stmt(&s.body)?;
self.emit_jump_loop_to(loop_start);
self.bind_label(loop_end);
self.loop_stack.pop();
Ok(())
}
fn compile_do_while(&mut self, s: &crate::parser::ast::DoWhileStmt) -> StatorResult<()> {
let loop_start = self.new_label();
let loop_end = self.new_label();
let cond_label = self.new_label();
self.patch_pending_labels(cond_label);
self.loop_stack.push((cond_label, loop_end));
self.bind_label(loop_start);
self.compile_stmt(&s.body)?;
self.bind_label(cond_label);
self.compile_expr(&s.test)?;
self.emit_jump(Opcode::JumpIfToBooleanFalse, loop_end);
self.emit_jump_loop_to(loop_start);
self.bind_label(loop_end);
self.loop_stack.pop();
Ok(())
}
fn compile_for(&mut self, s: &ForStmt) -> StatorResult<()> {
let loop_start = self.new_label();
let loop_end = self.new_label();
let continue_label = self.new_label();
self.push_scope();
if let Some(init) = &s.init {
match init {
ForInit::VarDecl(decl) => self.compile_var_decl(decl)?,
ForInit::Expr(expr) => {
self.compile_expr(expr)?;
}
}
}
self.patch_pending_labels(continue_label);
self.loop_stack.push((continue_label, loop_end));
self.bind_label(loop_start);
if let Some(test) = &s.test {
self.compile_expr(test)?;
self.emit_jump(Opcode::JumpIfToBooleanFalse, loop_end);
}
self.compile_stmt(&s.body)?;
self.bind_label(continue_label);
if let Some(update) = &s.update {
self.compile_expr(update)?;
}
self.emit_jump_loop_to(loop_start);
self.bind_label(loop_end);
self.loop_stack.pop();
self.pop_scope();
Ok(())
}
fn compile_return(&mut self, s: &crate::parser::ast::ReturnStmt) -> StatorResult<()> {
if let Some(arg) = &s.argument {
self.in_tail_position = self.is_strict && self.finally_stack.is_empty();
self.compile_expr(arg)?;
self.in_tail_position = false;
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let iter_regs_to_close: Vec<Register> = self
.loop_stack
.iter()
.rev()
.filter_map(|&(_, bl)| self.for_of_iter_regs.get(&bl).copied())
.collect();
for iter_reg in iter_regs_to_close {
self.emit(Instruction::new_unchecked(
Opcode::IteratorClose,
vec![to_reg_op(iter_reg)],
));
}
if !self.finally_stack.is_empty() {
let save_reg = self.allocator.allocate_temporary();
self.emit_star(save_reg);
self.inline_pending_finally_blocks()?;
self.emit_ldar(save_reg);
self.allocator
.release_temporary(save_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
self.emit(Instruction::new_unchecked(Opcode::Return, vec![]));
Ok(())
}
fn compile_break(&mut self, s: &crate::parser::ast::BreakStmt) -> StatorResult<()> {
if !self.finally_stack.is_empty() {
let save_reg = self.allocator.allocate_temporary();
self.emit_star(save_reg);
self.inline_pending_finally_blocks()?;
self.emit_ldar(save_reg);
self.allocator
.release_temporary(save_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
if let Some(label) = &s.label {
let (_, break_label) = *self.label_map.get(&label.name).ok_or_else(|| {
StatorError::SyntaxError(format!("undefined label '{}'", label.name))
})?;
let iter_regs_to_close: Vec<Register> = self
.loop_stack
.iter()
.rev()
.take_while(|&&(_, bl)| bl != break_label)
.chain(
self.loop_stack
.iter()
.rev()
.find(|&&(_, bl)| bl == break_label),
)
.filter_map(|&(_, bl)| self.for_of_iter_regs.get(&bl).copied())
.collect();
for iter_reg in iter_regs_to_close {
self.emit(Instruction::new_unchecked(
Opcode::IteratorClose,
vec![to_reg_op(iter_reg)],
));
}
self.emit_jump(Opcode::Jump, break_label);
return Ok(());
}
let (_, break_label) = *self
.loop_stack
.last()
.ok_or_else(|| StatorError::SyntaxError("break outside loop".into()))?;
if let Some(&iter_reg) = self.for_of_iter_regs.get(&break_label) {
self.emit(Instruction::new_unchecked(
Opcode::IteratorClose,
vec![to_reg_op(iter_reg)],
));
}
self.emit_jump(Opcode::Jump, break_label);
Ok(())
}
fn compile_continue(&mut self, s: &crate::parser::ast::ContinueStmt) -> StatorResult<()> {
if !self.finally_stack.is_empty() {
let save_reg = self.allocator.allocate_temporary();
self.emit_star(save_reg);
self.inline_pending_finally_blocks()?;
self.emit_ldar(save_reg);
self.allocator
.release_temporary(save_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
if let Some(label) = &s.label {
let (continue_label, break_label) =
*self.label_map.get(&label.name).ok_or_else(|| {
StatorError::SyntaxError(format!("undefined label '{}'", label.name))
})?;
let continue_label = continue_label.ok_or_else(|| {
StatorError::SyntaxError(format!(
"label '{}' is not a loop — continue is invalid",
label.name
))
})?;
let iter_regs_to_close: Vec<Register> = self
.loop_stack
.iter()
.rev()
.take_while(|&&(_, bl)| bl != break_label)
.filter_map(|&(_, bl)| self.for_of_iter_regs.get(&bl).copied())
.collect();
for iter_reg in iter_regs_to_close {
self.emit(Instruction::new_unchecked(
Opcode::IteratorClose,
vec![to_reg_op(iter_reg)],
));
}
self.emit_jump(Opcode::Jump, continue_label);
return Ok(());
}
let &(continue_label, _) = self
.loop_stack
.iter()
.rev()
.find(|&&(cont, brk)| cont != brk)
.ok_or_else(|| StatorError::SyntaxError("continue outside loop".into()))?;
self.emit_jump(Opcode::Jump, continue_label);
Ok(())
}
fn compile_labeled(&mut self, s: &crate::parser::ast::LabeledStmt) -> StatorResult<()> {
let name = s.label.name.clone();
if self.label_map.contains_key(&name) {
return Err(StatorError::SyntaxError(format!(
"duplicate label '{name}'"
)));
}
let break_label = self.new_label();
let is_loop = Self::stmt_is_iteration_target(&s.body);
self.label_map.insert(name.clone(), (None, break_label));
if is_loop {
self.pending_label_names.push(name.clone());
}
self.compile_stmt(&s.body)?;
self.pending_label_names.retain(|pending| pending != &name);
self.label_map.remove(&name);
self.bind_label(break_label);
Ok(())
}
fn compile_switch(&mut self, s: &crate::parser::ast::SwitchStmt) -> StatorResult<()> {
let break_label = self.new_label();
let end_label = break_label;
self.compile_expr(&s.discriminant)?;
let disc_reg = self.allocator.allocate_temporary();
self.emit_star(disc_reg);
let case_labels: Vec<usize> = s.cases.iter().map(|_| self.new_label()).collect();
let default_index = s.cases.iter().position(|case| case.test.is_none());
for (i, case) in s
.cases
.iter()
.enumerate()
.take(default_index.unwrap_or(s.cases.len()))
{
if let Some(test) = &case.test {
self.compile_expr(test)?;
let slot = self.alloc_slot(FeedbackSlotKind::Compare);
self.emit(Instruction::new_unchecked(
Opcode::TestEqualStrict,
vec![to_reg_op(disc_reg), slot],
));
self.emit_jump(Opcode::JumpIfTrue, case_labels[i]);
}
}
if let Some(default_index) = default_index {
for (i, case) in s.cases.iter().enumerate().skip(default_index + 1) {
if let Some(test) = &case.test {
self.compile_expr(test)?;
let slot = self.alloc_slot(FeedbackSlotKind::Compare);
self.emit(Instruction::new_unchecked(
Opcode::TestEqualStrict,
vec![to_reg_op(disc_reg), slot],
));
self.emit_jump(Opcode::JumpIfTrue, case_labels[i]);
}
}
self.emit_jump(Opcode::Jump, case_labels[default_index]);
} else {
self.emit_jump(Opcode::Jump, end_label);
}
self.loop_stack.push((end_label, break_label));
self.push_scope();
for (i, case) in s.cases.iter().enumerate() {
self.bind_label(case_labels[i]);
for stmt in &case.consequent {
self.compile_stmt(stmt)?;
}
}
self.pop_scope();
self.loop_stack.pop();
self.bind_label(end_label);
self.allocator
.release_temporary(disc_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_try(&mut self, s: &crate::parser::ast::TryStmt) -> StatorResult<()> {
if let Some(finalizer) = &s.finalizer {
self.finally_stack.push(finalizer.clone());
}
let try_start = self.instructions.len() as u32;
self.compile_block(&s.block)?;
if s.finalizer.is_some() {
self.finally_stack.pop();
}
let try_end = self.instructions.len() as u32;
if let Some(handler) = &s.handler {
let after_catch_label = self.new_label();
self.emit_jump(Opcode::Jump, after_catch_label);
let catch_start = self.instructions.len() as u32;
self.push_scope();
if let Some(param) = &handler.param {
match param {
Pat::Ident(ident) => {
let reg = self.define_local(&ident.name);
self.emit_star(reg);
}
pat => {
let source_reg = self.allocator.new_local();
self.emit_star(source_reg);
self.compile_binding_pattern(
pat,
source_reg,
BindingMode::Declare { is_const: false },
)?;
}
}
}
self.compile_block(&handler.body)?;
self.pop_scope();
let catch_end = self.instructions.len() as u32;
self.bind_label(after_catch_label);
self.handler_table.push(HandlerTableEntry {
try_start,
try_end,
handler: catch_start,
is_finally: false,
});
if let Some(finalizer) = &s.finalizer {
self.compile_block(finalizer)?;
let after_ex_handler_label = self.new_label();
self.emit_jump(Opcode::Jump, after_ex_handler_label);
let ex_handler_start = self.instructions.len() as u32;
let ex_reg = self.allocator.allocate_temporary();
self.emit_star(ex_reg);
self.compile_block(finalizer)?;
self.emit_ldar(ex_reg);
self.emit(Instruction::new_unchecked(Opcode::ReThrow, vec![]));
self.allocator
.release_temporary(ex_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.bind_label(after_ex_handler_label);
self.handler_table.push(HandlerTableEntry {
try_start: catch_start,
try_end: catch_end,
handler: ex_handler_start,
is_finally: true,
});
}
} else if let Some(finalizer) = &s.finalizer {
self.compile_block(finalizer)?;
let after_ex_handler_label = self.new_label();
self.emit_jump(Opcode::Jump, after_ex_handler_label);
let ex_handler_start = self.instructions.len() as u32;
let ex_reg = self.allocator.allocate_temporary();
self.emit_star(ex_reg); self.compile_block(finalizer)?; self.emit_ldar(ex_reg); self.emit(Instruction::new_unchecked(Opcode::ReThrow, vec![]));
self.allocator
.release_temporary(ex_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.bind_label(after_ex_handler_label);
self.handler_table.push(HandlerTableEntry {
try_start,
try_end,
handler: ex_handler_start,
is_finally: true,
});
}
Ok(())
}
fn compile_expr(&mut self, expr: &Expr) -> StatorResult<()> {
stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
match expr {
Expr::Call(_) | Expr::Conditional(_) | Expr::Logical(_) | Expr::Sequence(_) => {}
_ => {
self.in_tail_position = false;
}
}
match expr {
Expr::Null(_) => {
self.emit(Instruction::new_unchecked(Opcode::LdaNull, vec![]));
Ok(())
}
Expr::Bool(b) => {
let op = if b.value {
Opcode::LdaTrue
} else {
Opcode::LdaFalse
};
self.emit(Instruction::new_unchecked(op, vec![]));
Ok(())
}
Expr::Num(n) => {
self.compile_number(n.value);
Ok(())
}
Expr::Str(s) => {
let idx = self.add_string(&s.value);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(idx)],
));
Ok(())
}
Expr::BigInt(b) => {
let raw = b.value.replace('_', "");
let n: i128 = if let Some(hex) =
raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X"))
{
i128::from_str_radix(hex, 16).map_err(|e| {
StatorError::SyntaxError(format!("invalid BigInt hex literal: {e}"))
})?
} else if let Some(oct) =
raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O"))
{
i128::from_str_radix(oct, 8).map_err(|e| {
StatorError::SyntaxError(format!("invalid BigInt octal literal: {e}"))
})?
} else if let Some(bin) =
raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B"))
{
i128::from_str_radix(bin, 2).map_err(|e| {
StatorError::SyntaxError(format!("invalid BigInt binary literal: {e}"))
})?
} else {
raw.parse::<i128>().map_err(|e| {
StatorError::SyntaxError(format!("invalid BigInt literal: {e}"))
})?
};
let idx = self.add_bigint(n);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(idx)],
));
Ok(())
}
Expr::Regexp(r) => {
let pattern_idx = self.add_string(&r.pattern);
let flags_val: u8 = r.flags.bytes().fold(0u8, |acc, b| {
let bit = match b {
b'g' => 0x01,
b'i' => 0x02,
b'm' => 0x04,
b's' => 0x08,
b'u' => 0x10,
b'v' => 0x20,
b'y' => 0x40,
b'd' => 0x80,
_ => 0,
};
acc | bit
});
let slot = self.alloc_slot(FeedbackSlotKind::Literal);
self.emit(Instruction::new_unchecked(
Opcode::CreateRegExpLiteral,
vec![
Operand::ConstantPoolIdx(pattern_idx),
slot,
Operand::Flag(flags_val),
],
));
Ok(())
}
Expr::Template(t) => self.compile_template(t),
Expr::Ident(id) => {
self.compile_ident_load(&id.name);
Ok(())
}
Expr::This(_) => {
if let Some(binding) = self.lookup_var("this") {
self.emit_ldar(binding.reg);
} else {
let name_idx = self.add_string("this");
let slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), slot],
));
}
Ok(())
}
Expr::Array(a) => self.compile_array(a),
Expr::Object(o) => self.compile_object(o),
Expr::Fn(f) => self.compile_fn_expr(f),
Expr::Arrow(a) => self.compile_arrow_expr(a),
Expr::Class(c) => self.compile_class_expr(c),
Expr::Unary(u) => self.compile_unary(u),
Expr::Update(u) => self.compile_update(u),
Expr::Binary(b) => self.compile_binary(b),
Expr::Logical(l) => self.compile_logical(l),
Expr::Conditional(c) => self.compile_conditional(c),
Expr::Assign(a) => self.compile_assign(a),
Expr::Sequence(s) => {
let saved_tail = self.in_tail_position;
let len = s.expressions.len();
for (i, expr) in s.expressions.iter().enumerate() {
self.in_tail_position = saved_tail && i == len - 1;
self.compile_expr(expr)?;
}
self.in_tail_position = false;
Ok(())
}
Expr::Member(m) => self.compile_member(m),
Expr::OptionalMember(m) => self.compile_optional_member(m),
Expr::OptionalChain(inner) => self.compile_optional_chain(inner),
Expr::Call(c) => self.compile_call(c),
Expr::OptionalCall(c) => self.compile_optional_call(c),
Expr::New(n) => self.compile_new(n),
Expr::Yield(y) => {
if !self.is_generator {
return Err(StatorError::SyntaxError(
"yield expression outside of a generator function".into(),
));
}
self.compile_yield(y)
}
Expr::Await(a) => {
if !self.is_async {
return Err(StatorError::SyntaxError(
"await expression outside of an async function".into(),
));
}
self.compile_await(a)
}
Expr::TaggedTemplate(t) => self.compile_tagged_template(t),
Expr::Spread(s) => {
self.compile_expr(&s.argument)
}
Expr::Import(imp) => self.compile_import_call(imp),
Expr::MetaProp(m) => self.compile_meta_prop(m),
Expr::PrivateName(_) => Err(StatorError::SyntaxError(
"bare private name expression should only appear as LHS of 'in'".into(),
)),
}
}) }
fn compile_number(&mut self, value: f64) {
if value == 0.0 && value.is_sign_positive() {
self.emit(Instruction::new_unchecked(Opcode::LdaZero, vec![]));
} else if value.fract() == 0.0 && value >= i32::MIN as f64 && value <= i32::MAX as f64 {
let smi = value as i32;
self.emit(Instruction::new_unchecked(
Opcode::LdaSmi,
vec![Operand::Immediate(smi)],
));
} else {
let idx = self.add_number(value);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(idx)],
));
}
}
fn compile_ident_load(&mut self, name: &str) {
if let Some(&slot) = self.closure_captures.get(name) {
self.emit(Instruction::new_unchecked(
Opcode::LdaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
return;
}
if let Some(&slot) = self.context_bindings.get(name) {
self.emit(Instruction::new_unchecked(
Opcode::LdaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
return;
}
if let Some(binding) = self.lookup_var(name) {
self.emit_ldar(binding.reg);
if binding.needs_tdz_check {
let name_idx = self.add_string(name);
self.emit(Instruction::new_unchecked(
Opcode::ThrowReferenceErrorIfHole,
vec![Operand::ConstantPoolIdx(name_idx)],
));
}
} else {
let name_idx = self.add_string(name);
if self.with_depth > 0 {
self.emit(Instruction::new_unchecked(
Opcode::LdaLookupSlot,
vec![Operand::ConstantPoolIdx(name_idx)],
));
} else {
let slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
}
fn compile_ident_load_typeof(&mut self, name: &str) {
if let Some(&slot) = self.closure_captures.get(name) {
self.emit(Instruction::new_unchecked(
Opcode::LdaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
return;
}
if let Some(&slot) = self.context_bindings.get(name) {
self.emit(Instruction::new_unchecked(
Opcode::LdaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
return;
}
if let Some(binding) = self.lookup_var(name) {
self.emit_ldar(binding.reg);
if binding.needs_tdz_check {
let name_idx = self.add_string(name);
self.emit(Instruction::new_unchecked(
Opcode::ThrowReferenceErrorIfHole,
vec![Operand::ConstantPoolIdx(name_idx)],
));
}
} else {
let name_idx = self.add_string(name);
if self.with_depth > 0 {
self.emit(Instruction::new_unchecked(
Opcode::LdaLookupSlotInsideTypeof,
vec![Operand::ConstantPoolIdx(name_idx)],
));
} else {
let slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobalInsideTypeof,
vec![Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
}
fn compile_ident_store(&mut self, name: &str) -> StatorResult<()> {
if let Some(&slot) = self.closure_captures.get(name) {
self.emit(Instruction::new_unchecked(
Opcode::StaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
return Ok(());
}
if let Some(&slot) = self.context_bindings.get(name) {
self.emit(Instruction::new_unchecked(
Opcode::StaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
return Ok(());
}
if let Some(binding) = self.lookup_var(name) {
if binding.is_const {
return Err(StatorError::TypeError(format!(
"Assignment to constant variable '{name}'"
)));
}
if binding.needs_tdz_check {
let tmp = self.allocator.allocate_temporary();
self.emit_star(tmp);
self.emit_ldar(binding.reg);
let name_idx = self.add_string(name);
self.emit(Instruction::new_unchecked(
Opcode::ThrowReferenceErrorIfHole,
vec![Operand::ConstantPoolIdx(name_idx)],
));
self.emit_ldar(tmp);
self.emit_star(binding.reg);
self.allocator
.release_temporary(tmp)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
self.emit_star(binding.reg);
}
} else {
let name_idx = self.add_string(name);
if self.with_depth > 0 {
self.emit(Instruction::new_unchecked(
Opcode::StaLookupSlot,
vec![Operand::ConstantPoolIdx(name_idx), Operand::Flag(0)],
));
} else {
let slot = self.alloc_slot(FeedbackSlotKind::StoreGlobal);
self.emit(Instruction::new_unchecked(
Opcode::StaGlobal,
vec![Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
Ok(())
}
fn compile_unary(&mut self, u: &crate::parser::ast::UnaryExpr) -> StatorResult<()> {
match u.op {
UnaryOp::Void => {
self.compile_expr(&u.argument)?;
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
UnaryOp::Typeof => {
if let Expr::Ident(id) = u.argument.as_ref() {
self.compile_ident_load_typeof(&id.name);
} else {
self.compile_expr(&u.argument)?;
}
let slot = self.alloc_slot(FeedbackSlotKind::TypeOf);
self.emit(Instruction::new_unchecked(Opcode::TypeOf, vec![slot]));
}
UnaryOp::Not => {
self.compile_expr(&u.argument)?;
self.emit(Instruction::new_unchecked(
Opcode::ToBooleanLogicalNot,
vec![],
));
}
UnaryOp::Minus => {
self.compile_expr(&u.argument)?;
let slot = self.alloc_slot(FeedbackSlotKind::UnaryOp);
self.emit(Instruction::new_unchecked(Opcode::Negate, vec![slot]));
}
UnaryOp::Plus => {
self.compile_expr(&u.argument)?;
let slot = self.alloc_slot(FeedbackSlotKind::UnaryOp);
self.emit(Instruction::new_unchecked(Opcode::ToNumber, vec![slot]));
}
UnaryOp::BitNot => {
self.compile_expr(&u.argument)?;
let slot = self.alloc_slot(FeedbackSlotKind::UnaryOp);
self.emit(Instruction::new_unchecked(Opcode::BitwiseNot, vec![slot]));
}
UnaryOp::Delete => {
match u.argument.as_ref() {
Expr::Member(m) => {
self.compile_expr(&m.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
self.emit_delete_property(obj_reg, &m.property)?;
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
Expr::OptionalChain(inner) => {
self.compile_delete_optional_chain(inner)?;
}
Expr::Ident(_) if self.is_strict => {
return Err(StatorError::SyntaxError(
"delete of an unqualified identifier is not allowed in strict mode"
.into(),
));
}
Expr::Ident(id) if self.with_depth > 0 => {
let name_idx = self.add_string(&id.name);
self.emit(Instruction::new_unchecked(
Opcode::DeleteLookupSlot,
vec![Operand::ConstantPoolIdx(name_idx)],
));
}
_ => {
self.emit(Instruction::new_unchecked(Opcode::LdaTrue, vec![]));
}
}
}
}
Ok(())
}
fn compile_update(&mut self, u: &crate::parser::ast::UpdateExpr) -> StatorResult<()> {
self.compile_expr(&u.argument)?;
let old_reg = if !u.prefix {
let r = self.allocator.allocate_temporary();
self.emit_star(r);
Some(r)
} else {
None
};
let op = match u.op {
UpdateOp::Increment => Opcode::Inc,
UpdateOp::Decrement => Opcode::Dec,
};
let slot = self.alloc_slot(FeedbackSlotKind::BinaryOpInc);
self.emit(Instruction::new_unchecked(op, vec![slot]));
match u.argument.as_ref() {
Expr::Ident(id) => self.compile_ident_store(&id.name)?,
Expr::Member(m) => self.compile_member_store(m)?,
_ => {
return Err(StatorError::SyntaxError(
"update target must be an identifier or member expression".into(),
));
}
}
if let Some(r) = old_reg {
self.emit_ldar(r);
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
Ok(())
}
fn compile_binary(&mut self, b: &crate::parser::ast::BinaryExpr) -> StatorResult<()> {
if b.op == BinaryOp::In
&& let Expr::PrivateName(ref id) = *b.left
{
self.compile_expr(&b.right)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
let name_idx = self.add_string(&self.resolve_private_brand_key(&id.name));
let brand_reg = self.allocator.allocate_temporary();
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(name_idx)],
));
self.emit_star(brand_reg);
self.emit_ldar(obj_reg);
self.emit(Instruction::new_unchecked(
Opcode::TestPrivateBrand,
vec![to_reg_op(obj_reg), to_reg_op(brand_reg)],
));
self.allocator
.release_temporary(brand_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
return Ok(());
}
self.compile_expr(&b.right)?;
let rhs_reg = self.allocator.allocate_temporary();
self.emit_star(rhs_reg);
self.compile_expr(&b.left)?;
let (op, slot_kind): (Opcode, FeedbackSlotKind) = match b.op {
BinaryOp::Add => (Opcode::Add, FeedbackSlotKind::BinaryOp),
BinaryOp::Sub => (Opcode::Sub, FeedbackSlotKind::BinaryOp),
BinaryOp::Mul => (Opcode::Mul, FeedbackSlotKind::BinaryOp),
BinaryOp::Div => (Opcode::Div, FeedbackSlotKind::BinaryOp),
BinaryOp::Rem => (Opcode::Mod, FeedbackSlotKind::BinaryOp),
BinaryOp::Exp => (Opcode::Exp, FeedbackSlotKind::BinaryOp),
BinaryOp::BitOr => (Opcode::BitwiseOr, FeedbackSlotKind::BinaryOp),
BinaryOp::BitXor => (Opcode::BitwiseXor, FeedbackSlotKind::BinaryOp),
BinaryOp::BitAnd => (Opcode::BitwiseAnd, FeedbackSlotKind::BinaryOp),
BinaryOp::Shl => (Opcode::ShiftLeft, FeedbackSlotKind::BinaryOp),
BinaryOp::Shr => (Opcode::ShiftRight, FeedbackSlotKind::BinaryOp),
BinaryOp::UShr => (Opcode::ShiftRightLogical, FeedbackSlotKind::BinaryOp),
BinaryOp::Eq => (Opcode::TestEqual, FeedbackSlotKind::Compare),
BinaryOp::NotEq => (Opcode::TestNotEqual, FeedbackSlotKind::Compare),
BinaryOp::StrictEq => (Opcode::TestEqualStrict, FeedbackSlotKind::Compare),
BinaryOp::StrictNotEq => {
let slot = self.alloc_slot(FeedbackSlotKind::Compare);
self.emit(Instruction::new_unchecked(
Opcode::TestEqualStrict,
vec![to_reg_op(rhs_reg), slot],
));
self.allocator
.release_temporary(rhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.emit(Instruction::new_unchecked(Opcode::LogicalNot, vec![]));
return Ok(());
}
BinaryOp::Lt => (Opcode::TestLessThan, FeedbackSlotKind::Compare),
BinaryOp::LtEq => (Opcode::TestLessThanOrEqual, FeedbackSlotKind::Compare),
BinaryOp::Gt => (Opcode::TestGreaterThan, FeedbackSlotKind::Compare),
BinaryOp::GtEq => (Opcode::TestGreaterThanOrEqual, FeedbackSlotKind::Compare),
BinaryOp::In => (Opcode::TestIn, FeedbackSlotKind::BinaryOp),
BinaryOp::Instanceof => (Opcode::TestInstanceOf, FeedbackSlotKind::InstanceOf),
};
let slot = self.alloc_slot(slot_kind);
self.emit(Instruction::new_unchecked(
op,
vec![to_reg_op(rhs_reg), slot],
));
self.allocator
.release_temporary(rhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_logical(&mut self, l: &crate::parser::ast::LogicalExpr) -> StatorResult<()> {
let end_label = self.new_label();
let saved_tail = self.in_tail_position;
self.in_tail_position = false;
self.compile_expr(&l.left)?;
self.in_tail_position = saved_tail;
match l.op {
LogicalOp::And => {
self.emit_jump(Opcode::JumpIfToBooleanFalse, end_label);
}
LogicalOp::Or => {
self.emit_jump(Opcode::JumpIfToBooleanTrue, end_label);
}
LogicalOp::NullishCoalesce => {
let eval_right_label = self.new_label();
self.emit_jump(Opcode::JumpIfUndefinedOrNull, eval_right_label);
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(eval_right_label);
self.compile_expr(&l.right)?;
self.in_tail_position = false;
self.bind_label(end_label);
return Ok(());
}
}
self.compile_expr(&l.right)?;
self.in_tail_position = false;
self.bind_label(end_label);
Ok(())
}
fn compile_conditional(&mut self, c: &crate::parser::ast::ConditionalExpr) -> StatorResult<()> {
let else_label = self.new_label();
let end_label = self.new_label();
let saved_tail = self.in_tail_position;
self.in_tail_position = false;
self.compile_expr(&c.test)?;
self.emit_jump(Opcode::JumpIfToBooleanFalse, else_label);
self.in_tail_position = saved_tail;
self.compile_expr(&c.consequent)?;
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(else_label);
self.in_tail_position = saved_tail;
self.compile_expr(&c.alternate)?;
self.in_tail_position = false;
self.bind_label(end_label);
Ok(())
}
fn compile_assign(&mut self, a: &crate::parser::ast::AssignExpr) -> StatorResult<()> {
if a.op == AssignOp::Assign {
let inferred_name = match &a.left {
AssignTarget::Expr(expr) => match &**expr {
Expr::Ident(id) => Some(id.name.as_str()),
_ => None,
},
AssignTarget::Pat(_) => None,
};
if let Some(name) = inferred_name {
if !self.compile_named_callable_expr(&a.right, name)? {
self.compile_expr(&a.right)?;
}
} else {
self.compile_expr(&a.right)?;
}
self.compile_assign_target_store(&a.left)?;
return Ok(());
}
self.compile_assign_target_load(&a.left)?;
let lhs_reg = self.allocator.allocate_temporary();
self.emit_star(lhs_reg);
self.compile_expr(&a.right)?;
let rhs_reg = self.allocator.allocate_temporary();
self.emit_star(rhs_reg);
self.emit_ldar(lhs_reg);
let op: Opcode = match a.op {
AssignOp::AddAssign => Opcode::Add,
AssignOp::SubAssign => Opcode::Sub,
AssignOp::MulAssign => Opcode::Mul,
AssignOp::DivAssign => Opcode::Div,
AssignOp::RemAssign => Opcode::Mod,
AssignOp::ExpAssign => Opcode::Exp,
AssignOp::ShlAssign => Opcode::ShiftLeft,
AssignOp::ShrAssign => Opcode::ShiftRight,
AssignOp::UShrAssign => Opcode::ShiftRightLogical,
AssignOp::BitOrAssign => Opcode::BitwiseOr,
AssignOp::BitXorAssign => Opcode::BitwiseXor,
AssignOp::BitAndAssign => Opcode::BitwiseAnd,
AssignOp::LogicalAndAssign => {
self.allocator
.release_temporary(rhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(lhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.compile_assign_target_load(&a.left)?;
let skip = self.new_label();
self.emit_jump(Opcode::JumpIfToBooleanFalse, skip);
self.compile_expr(&a.right)?;
self.compile_assign_target_store(&a.left)?;
self.bind_label(skip);
return Ok(());
}
AssignOp::LogicalOrAssign => {
self.allocator
.release_temporary(rhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(lhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.compile_assign_target_load(&a.left)?;
let skip = self.new_label();
self.emit_jump(Opcode::JumpIfToBooleanTrue, skip);
self.compile_expr(&a.right)?;
self.compile_assign_target_store(&a.left)?;
self.bind_label(skip);
return Ok(());
}
AssignOp::NullishAssign => {
self.allocator
.release_temporary(rhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(lhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.compile_assign_target_load(&a.left)?;
let do_assign = self.new_label();
let skip = self.new_label();
self.emit_jump(Opcode::JumpIfUndefinedOrNull, do_assign);
self.emit_jump(Opcode::Jump, skip);
self.bind_label(do_assign);
self.compile_expr(&a.right)?;
self.compile_assign_target_store(&a.left)?;
self.bind_label(skip);
return Ok(());
}
AssignOp::Assign => unreachable!("handled above"),
};
let slot = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(
op,
vec![to_reg_op(rhs_reg), slot],
));
self.allocator
.release_temporary(rhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(lhs_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.compile_assign_target_store(&a.left)?;
Ok(())
}
fn compile_assign_target_load(&mut self, target: &AssignTarget) -> StatorResult<()> {
match target {
AssignTarget::Expr(expr) => match expr.as_ref() {
Expr::Ident(id) => {
self.compile_ident_load(&id.name);
Ok(())
}
Expr::Member(m) => self.compile_member(m),
other => {
let loc = other.loc();
Err(StatorError::SyntaxError(format!(
"at {}:{} — Invalid assignment target",
loc.start.line, loc.start.column
)))
}
},
AssignTarget::Pat(_pat) => {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
Ok(())
}
}
}
fn compile_assign_target_store(&mut self, target: &AssignTarget) -> StatorResult<()> {
match target {
AssignTarget::Expr(expr) => match expr.as_ref() {
Expr::Ident(id) => {
self.compile_ident_store(&id.name)?;
Ok(())
}
Expr::Member(m) => self.compile_member_store(m),
other => {
let loc = other.loc();
Err(StatorError::SyntaxError(format!(
"at {}:{} — Invalid assignment target",
loc.start.line, loc.start.column
)))
}
},
AssignTarget::Pat(pat) => {
let source_reg = self.allocator.new_local();
self.emit_star(source_reg);
self.compile_binding_pattern(pat, source_reg, BindingMode::Assign)?;
Ok(())
}
}
}
fn compile_member(&mut self, m: &crate::parser::ast::MemberExpr) -> StatorResult<()> {
if let Expr::Ident(id) = m.object.as_ref()
&& id.name == "super"
{
return self.compile_super_member_load(m);
}
self.compile_expr(&m.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
match &m.property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
crate::parser::ast::MemberProp::Computed(key) => {
self.compile_expr(key)?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedLoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaKeyedProperty,
vec![to_reg_op(obj_reg), slot],
));
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_super_member_load(
&mut self,
m: &crate::parser::ast::MemberExpr,
) -> StatorResult<()> {
let recv_reg = self.allocator.allocate_temporary();
let this_name_idx = self.add_string("this");
let this_slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(this_name_idx), this_slot],
));
self.emit_star(recv_reg);
let super_lookup_idx = self.add_string(".super_lookup_start");
let super_slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(super_lookup_idx), super_slot],
));
match &m.property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedPropertyFromSuper,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedPropertyFromSuper,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::MemberProp::Computed(_) => {
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
return Err(StatorError::SyntaxError(
"computed super property access is not yet supported".into(),
));
}
}
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_optional_member(
&mut self,
m: &crate::parser::ast::OptionalMemberExpr,
) -> StatorResult<()> {
self.compile_expr(&m.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
self.emit_ldar(obj_reg);
if let Some(chain_label) = self.optional_chain_null_label {
self.emit_jump(Opcode::JumpIfUndefinedOrNull, chain_label);
self.emit_optional_member_property(obj_reg, &m.property)?;
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
let null_label = self.new_label();
let end_label = self.new_label();
self.emit_jump(Opcode::JumpIfUndefinedOrNull, null_label);
self.emit_optional_member_property(obj_reg, &m.property)?;
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(null_label);
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.bind_label(end_label);
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
Ok(())
}
fn emit_optional_member_property(
&mut self,
obj_reg: Register,
property: &crate::parser::ast::MemberProp,
) -> StatorResult<()> {
match property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
crate::parser::ast::MemberProp::Computed(key) => {
self.with_suspended_optional_chain(|this| this.compile_expr(key))?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedLoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaKeyedProperty,
vec![to_reg_op(obj_reg), slot],
));
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
Ok(())
}
fn compile_optional_chain(&mut self, inner: &Expr) -> StatorResult<()> {
let null_label = self.new_label();
let end_label = self.new_label();
let saved = self.optional_chain_null_label;
self.optional_chain_null_label = Some(null_label);
self.compile_expr(inner)?;
self.optional_chain_null_label = saved;
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(null_label);
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.bind_label(end_label);
Ok(())
}
fn compile_delete_optional_chain(&mut self, inner: &Expr) -> StatorResult<()> {
let null_label = self.new_label();
let end_label = self.new_label();
let saved = self.optional_chain_null_label;
self.optional_chain_null_label = Some(null_label);
match inner {
Expr::Member(m) => {
self.compile_expr(&m.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
self.emit_delete_property(obj_reg, &m.property)?;
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
Expr::OptionalMember(m) => {
self.compile_expr(&m.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
self.emit_ldar(obj_reg);
self.emit_jump(Opcode::JumpIfUndefinedOrNull, null_label);
self.emit_delete_property(obj_reg, &m.property)?;
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
_ => {
self.compile_expr(inner)?;
self.emit(Instruction::new_unchecked(Opcode::LdaTrue, vec![]));
self.optional_chain_null_label = saved;
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(null_label);
self.emit(Instruction::new_unchecked(Opcode::LdaTrue, vec![]));
self.bind_label(end_label);
return Ok(());
}
}
self.optional_chain_null_label = saved;
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(null_label);
self.emit(Instruction::new_unchecked(Opcode::LdaTrue, vec![]));
self.bind_label(end_label);
Ok(())
}
fn emit_delete_property(
&mut self,
obj_reg: Register,
property: &crate::parser::ast::MemberProp,
) -> StatorResult<()> {
match property {
crate::parser::ast::MemberProp::Computed(key) => {
self.with_suspended_optional_chain(|this| this.compile_expr(key))?;
}
crate::parser::ast::MemberProp::Ident(id) => {
let idx = self.add_string(&id.name);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(idx)],
));
}
crate::parser::ast::MemberProp::Private(name) => {
return Err(StatorError::SyntaxError(format!(
"private fields can not be deleted (field #{})",
name.name
)));
}
}
let delete_op = if self.is_strict {
Opcode::DeletePropertyStrict
} else {
Opcode::DeletePropertySloppy
};
self.emit(Instruction::new_unchecked(
delete_op,
vec![to_reg_op(obj_reg)],
));
Ok(())
}
fn with_suspended_optional_chain<T>(
&mut self,
f: impl FnOnce(&mut Self) -> StatorResult<T>,
) -> StatorResult<T> {
let saved = self.optional_chain_null_label;
self.optional_chain_null_label = None;
let result = f(self);
self.optional_chain_null_label = saved;
result
}
fn compile_member_store(&mut self, m: &crate::parser::ast::MemberExpr) -> StatorResult<()> {
let val_reg = self.allocator.allocate_temporary();
self.emit_star(val_reg);
self.compile_expr(&m.object)?;
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
self.emit_ldar(val_reg);
match &m.property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaNamedProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
crate::parser::ast::MemberProp::Computed(key) => {
self.compile_expr(key)?;
let key_reg = self.allocator.allocate_temporary();
self.emit_star(key_reg);
self.emit_ldar(val_reg);
let slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaKeyedProperty,
vec![to_reg_op(obj_reg), to_reg_op(key_reg), slot],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit_ldar(val_reg);
self.emit(Instruction::new_unchecked(
Opcode::StaNamedProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(val_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_call(&mut self, c: &crate::parser::ast::CallExpr) -> StatorResult<()> {
let is_tail = self.in_tail_position;
self.in_tail_position = false;
if let Expr::Member(m) = c.callee.as_ref() {
self.in_tail_position = is_tail;
return self.compile_method_call(m, &c.arguments);
}
if let Expr::OptionalMember(m) = c.callee.as_ref() {
return self.compile_optional_method_call(m, &c.arguments);
}
let is_direct_eval = matches!(c.callee.as_ref(), Expr::Ident(id) if id.name == "eval");
let has_spread = c.arguments.iter().any(|a| matches!(a, Expr::Spread(_)));
self.compile_expr(&c.callee)?;
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
if has_spread && !is_direct_eval {
let arr_reg = self.compile_arguments_as_array(&c.arguments)?;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallWithSpread,
vec![
to_reg_op(callee_reg),
to_reg_op(arr_reg),
Operand::RegisterCount(1),
slot,
],
));
self.allocator
.release_temporary(arr_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
let arg_regs =
self.with_suspended_optional_chain(|this| this.compile_arguments(&c.arguments))?;
if is_direct_eval {
self.emit_call_direct_eval(callee_reg, &arg_regs)?;
} else if is_tail {
self.emit_tail_call(callee_reg, &arg_regs)?;
} else {
self.emit_call_any_receiver(callee_reg, &arg_regs)?;
}
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_import_call(&mut self, imp: &crate::parser::ast::ImportExpr) -> StatorResult<()> {
self.compile_expr(&imp.source)?;
let source_reg = self.allocator.allocate_temporary();
self.emit_star(source_reg);
self.emit(Instruction::new_unchecked(
Opcode::CallRuntime,
vec![
Operand::RuntimeId(RUNTIME_DYNAMIC_IMPORT),
to_reg_op(source_reg),
Operand::RegisterCount(1),
],
));
self.allocator
.release_temporary(source_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_optional_call(
&mut self,
c: &crate::parser::ast::OptionalCallExpr,
) -> StatorResult<()> {
self.compile_expr(&c.callee)?;
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
self.emit_ldar(callee_reg);
if let Some(chain_label) = self.optional_chain_null_label {
self.emit_jump(Opcode::JumpIfUndefinedOrNull, chain_label);
let arg_regs =
self.with_suspended_optional_chain(|this| this.compile_arguments(&c.arguments))?;
self.emit_call_any_receiver(callee_reg, &arg_regs)?;
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
} else {
let null_label = self.new_label();
let end_label = self.new_label();
self.emit_jump(Opcode::JumpIfUndefinedOrNull, null_label);
let arg_regs = self.compile_arguments(&c.arguments)?;
self.emit_call_any_receiver(callee_reg, &arg_regs)?;
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
self.emit_jump(Opcode::Jump, end_label);
self.bind_label(null_label);
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.bind_label(end_label);
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_method_call(
&mut self,
m: &crate::parser::ast::MemberExpr,
args: &[Expr],
) -> StatorResult<()> {
if let Expr::Ident(id) = m.object.as_ref()
&& id.name == "super"
{
return self.compile_super_method_call(m, args);
}
self.in_tail_position = false;
self.compile_expr(&m.object)?;
let recv_reg = self.allocator.allocate_temporary();
self.emit_star(recv_reg);
match &m.property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::MemberProp::Computed(key) => {
self.compile_expr(key)?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedLoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaKeyedProperty,
vec![to_reg_op(recv_reg), slot],
));
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
}
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let has_spread = args.iter().any(|a| matches!(a, Expr::Spread(_)));
if has_spread {
let arr_reg = self.compile_arguments_as_array(args)?;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallWithSpread,
vec![
to_reg_op(callee_reg),
to_reg_op(arr_reg),
Operand::RegisterCount(1),
slot,
],
));
self.allocator
.release_temporary(arr_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
let arg_regs = self.compile_arguments(args)?;
let arg_count = arg_regs.len() as u32;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(callee_reg),
to_reg_op(recv_reg),
Operand::RegisterCount(arg_count),
slot,
],
));
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_optional_method_call(
&mut self,
m: &crate::parser::ast::OptionalMemberExpr,
args: &[Expr],
) -> StatorResult<()> {
self.in_tail_position = false;
self.compile_expr(&m.object)?;
let recv_reg = self.allocator.allocate_temporary();
self.emit_star(recv_reg);
self.emit_ldar(recv_reg);
let (use_chain, local_null, local_end) =
if let Some(chain_label) = self.optional_chain_null_label {
self.emit_jump(Opcode::JumpIfUndefinedOrNull, chain_label);
(true, 0, 0) } else {
let nl = self.new_label();
let el = self.new_label();
self.emit_jump(Opcode::JumpIfUndefinedOrNull, nl);
(false, nl, el)
};
self.emit_optional_member_property(recv_reg, &m.property)?;
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let arg_regs = self.with_suspended_optional_chain(|this| this.compile_arguments(args))?;
let arg_count = arg_regs.len() as u32;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(callee_reg),
to_reg_op(recv_reg),
Operand::RegisterCount(arg_count),
slot,
],
));
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
if !use_chain {
self.emit_jump(Opcode::Jump, local_end);
self.bind_label(local_null);
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.bind_label(local_end);
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_super_method_call(
&mut self,
m: &crate::parser::ast::MemberExpr,
args: &[Expr],
) -> StatorResult<()> {
self.in_tail_position = false;
let recv_reg = self.allocator.allocate_temporary();
let this_name_idx = self.add_string("this");
let this_slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(this_name_idx), this_slot],
));
self.emit_star(recv_reg);
let super_lookup_idx = self.add_string(".super_lookup_start");
let super_slot = self.alloc_slot(FeedbackSlotKind::LoadGlobal);
self.emit(Instruction::new_unchecked(
Opcode::LdaGlobal,
vec![Operand::ConstantPoolIdx(super_lookup_idx), super_slot],
));
match &m.property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedPropertyFromSuper,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedPropertyFromSuper,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::MemberProp::Computed(_) => {
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
return Err(StatorError::SyntaxError(
"computed super method access is not yet supported".into(),
));
}
}
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let has_spread = args.iter().any(|a| matches!(a, Expr::Spread(_)));
if has_spread {
let arr_reg = self.compile_arguments_as_array(args)?;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallWithSpread,
vec![
to_reg_op(callee_reg),
to_reg_op(arr_reg),
Operand::RegisterCount(1),
slot,
],
));
self.allocator
.release_temporary(arr_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
let arg_regs = self.compile_arguments(args)?;
let arg_count = arg_regs.len() as u32;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(callee_reg),
to_reg_op(recv_reg),
Operand::RegisterCount(arg_count),
slot,
],
));
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_arguments(&mut self, args: &[Expr]) -> StatorResult<Vec<Register>> {
let mut regs = Vec::with_capacity(args.len());
for arg in args {
let r = self.allocator.allocate_temporary();
self.compile_expr(arg)?;
self.emit_star(r);
regs.push(r);
}
Ok(regs)
}
fn compile_arguments_as_array(&mut self, args: &[Expr]) -> StatorResult<Register> {
let arr_slot = self.alloc_slot(FeedbackSlotKind::Literal);
self.emit(Instruction::new_unchecked(
Opcode::CreateEmptyArrayLiteral,
vec![arr_slot],
));
let arr_reg = self.allocator.allocate_temporary();
self.emit_star(arr_reg);
let idx_reg = self.allocator.allocate_temporary();
self.emit(Instruction::new_unchecked(Opcode::LdaZero, vec![]));
self.emit_star(idx_reg);
for arg in args {
if let Expr::Spread(s) = arg {
self.compile_expr(&s.argument)?;
let iterable_reg = self.allocator.allocate_temporary();
self.emit_star(iterable_reg);
let load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::GetIterator,
vec![to_reg_op(iterable_reg), load_slot, call_slot],
));
let iter_reg = self.allocator.allocate_temporary();
self.emit_star(iter_reg);
let val_reg = self.allocator.allocate_temporary();
let loop_lbl = self.new_label();
let done_lbl = self.new_label();
self.bind_label(loop_lbl);
self.emit(Instruction::new_unchecked(
Opcode::IteratorNext,
vec![to_reg_op(iter_reg), to_reg_op(val_reg)],
));
self.emit_jump_if_true_to(done_lbl);
self.emit_ldar(val_reg);
let elem_slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaInArrayLiteral,
vec![to_reg_op(arr_reg), to_reg_op(idx_reg), elem_slot],
));
self.emit_ldar(idx_reg);
let inc_slot = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(Opcode::Inc, vec![inc_slot]));
self.emit_star(idx_reg);
self.emit_jump_loop_to(loop_lbl);
self.bind_label(done_lbl);
self.allocator
.release_temporary(val_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(iter_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(iterable_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
self.compile_expr(arg)?;
let elem_slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaInArrayLiteral,
vec![to_reg_op(arr_reg), to_reg_op(idx_reg), elem_slot],
));
self.emit_ldar(idx_reg);
let inc_slot = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(Opcode::Inc, vec![inc_slot]));
self.emit_star(idx_reg);
}
}
self.allocator
.release_temporary(idx_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(arr_reg)
}
fn emit_call_any_receiver(
&mut self,
callee_reg: Register,
arg_regs: &[Register],
) -> StatorResult<()> {
let arg_count = arg_regs.len() as u32;
let args_start = arg_regs.first().copied().unwrap_or(callee_reg);
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallAnyReceiver,
vec![
to_reg_op(callee_reg),
to_reg_op(args_start),
Operand::RegisterCount(arg_count),
slot,
],
));
Ok(())
}
fn emit_tail_call(&mut self, callee_reg: Register, arg_regs: &[Register]) -> StatorResult<()> {
let arg_count = arg_regs.len() as u32;
let args_start = arg_regs.first().copied().unwrap_or(callee_reg);
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::TailCall,
vec![
to_reg_op(callee_reg),
to_reg_op(args_start),
Operand::RegisterCount(arg_count),
slot,
],
));
Ok(())
}
fn emit_call_direct_eval(
&mut self,
callee_reg: Register,
arg_regs: &[Register],
) -> StatorResult<()> {
let arg_count = arg_regs.len() as u32;
let args_start = arg_regs.first().copied().unwrap_or(callee_reg);
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallDirectEval,
vec![
to_reg_op(callee_reg),
to_reg_op(args_start),
Operand::RegisterCount(arg_count),
slot,
],
));
Ok(())
}
fn compile_new(&mut self, n: &crate::parser::ast::NewExpr) -> StatorResult<()> {
self.compile_expr(&n.callee)?;
let ctor_reg = self.allocator.allocate_temporary();
self.emit_star(ctor_reg);
let has_spread = n.arguments.iter().any(|a| matches!(a, Expr::Spread(_)));
if has_spread {
let arr_reg = self.compile_arguments_as_array(&n.arguments)?;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::ConstructWithSpread,
vec![
to_reg_op(ctor_reg),
to_reg_op(arr_reg),
Operand::RegisterCount(1),
slot,
],
));
self.allocator
.release_temporary(arr_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
} else {
let arg_regs = self.compile_arguments(&n.arguments)?;
let arg_count = arg_regs.len() as u32;
let args_start = arg_regs.first().copied().unwrap_or(ctor_reg);
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::Construct,
vec![
to_reg_op(ctor_reg),
to_reg_op(args_start),
Operand::RegisterCount(arg_count),
slot,
],
));
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
self.allocator
.release_temporary(ctor_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_fn_expr(&mut self, f: &FnExpr) -> StatorResult<()> {
self.compile_fn_expr_with_source(f, f.loc)
}
fn compile_fn_expr_with_source(
&mut self,
f: &FnExpr,
source_span: SourceLocation,
) -> StatorResult<()> {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
let func_array = compile_function_inner(
&f.params,
&f.body,
f.is_generator,
f.is_async,
f.is_strict,
false,
FunctionCompileOptions {
self_name: self_name.map(str::to_owned),
private_names: self.private_names.clone(),
source_text: self.source_text.clone(),
source_span: Some(source_span),
closure_captures: self.context_bindings.clone(),
},
)?;
let func_array = if let Some(name) = self_name {
func_array.with_function_name(name)
} else {
func_array
};
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(func_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(0)],
));
Ok(())
}
fn compile_fn_expr_named(&mut self, f: &FnExpr, name: &str) -> StatorResult<()> {
self.compile_fn_expr_named_with_source(f, name, f.loc)
}
fn compile_fn_expr_named_with_source(
&mut self,
f: &FnExpr,
name: &str,
source_span: SourceLocation,
) -> StatorResult<()> {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
let func_array = compile_function_inner(
&f.params,
&f.body,
f.is_generator,
f.is_async,
f.is_strict,
false,
FunctionCompileOptions {
self_name: self_name.map(str::to_owned),
private_names: self.private_names.clone(),
source_text: self.source_text.clone(),
source_span: Some(source_span),
closure_captures: self.context_bindings.clone(),
},
)?
.with_function_name(name);
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(func_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(0)],
));
Ok(())
}
fn compile_arrow_expr_named(&mut self, a: &ArrowExpr, name: &str) -> StatorResult<()> {
self.compile_arrow_expr_named_with_source(a, name, a.loc)
}
fn compile_arrow_expr_named_with_source(
&mut self,
a: &ArrowExpr,
name: &str,
source_span: SourceLocation,
) -> StatorResult<()> {
let body_block = match &a.body {
ArrowBody::Block(b) => b.clone(),
ArrowBody::Expr(expr) => {
use crate::parser::ast::{BlockStmt, ReturnStmt};
let loc = expr.loc();
BlockStmt {
loc,
body: vec![Stmt::Return(ReturnStmt {
loc,
argument: Some(expr.clone()),
})],
}
}
};
let func_array = compile_function_inner(
&a.params,
&body_block,
false,
a.is_async,
a.is_strict,
true,
FunctionCompileOptions {
self_name: None,
private_names: self.private_names.clone(),
source_text: self.source_text.clone(),
source_span: Some(source_span),
closure_captures: self.context_bindings.clone(),
},
)?
.with_arrow_flag(true)
.with_function_name(name);
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(func_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(1)],
));
Ok(())
}
fn compile_named_callable_expr(&mut self, expr: &Expr, name: &str) -> StatorResult<bool> {
match expr {
Expr::Fn(f) if f.id.is_none() => {
self.compile_fn_expr_named(f, name)?;
Ok(true)
}
Expr::Arrow(a) => {
self.compile_arrow_expr_named(a, name)?;
Ok(true)
}
Expr::Class(c) if c.id.is_none() => {
let synthetic_id = Some(crate::parser::ast::Ident {
loc: c.loc,
name: name.to_string(),
});
self.compile_class(
synthetic_id.as_ref(),
c.super_class.as_deref(),
&c.body,
c.loc,
)?;
Ok(true)
}
_ => Ok(false),
}
}
fn compile_arrow_expr(&mut self, a: &ArrowExpr) -> StatorResult<()> {
self.compile_arrow_expr_with_source(a, a.loc)
}
fn compile_arrow_expr_with_source(
&mut self,
a: &ArrowExpr,
source_span: SourceLocation,
) -> StatorResult<()> {
let body_block = match &a.body {
ArrowBody::Block(b) => b.clone(),
ArrowBody::Expr(expr) => {
use crate::parser::ast::{BlockStmt, ReturnStmt};
let loc = expr.loc();
BlockStmt {
loc,
body: vec![Stmt::Return(ReturnStmt {
loc,
argument: Some(expr.clone()),
})],
}
}
};
let func_array = compile_function_inner(
&a.params,
&body_block,
false,
a.is_async,
a.is_strict,
true,
FunctionCompileOptions {
self_name: None,
private_names: self.private_names.clone(),
source_text: self.source_text.clone(),
source_span: Some(source_span),
closure_captures: self.context_bindings.clone(),
},
)?
.with_arrow_flag(true);
let pool_idx = self.add_constant_raw(ConstantPoolEntry::Function(Rc::new(func_array)));
let slot = self.alloc_slot(FeedbackSlotKind::CreateClosure);
self.emit(Instruction::new_unchecked(
Opcode::CreateClosure,
vec![Operand::ConstantPoolIdx(pool_idx), slot, Operand::Flag(1)],
));
Ok(())
}
fn compile_array(&mut self, a: &crate::parser::ast::ArrayExpr) -> StatorResult<()> {
let has_spread = a
.elements
.iter()
.any(|e| matches!(e, Some(Expr::Spread(_))));
let arr_slot = self.alloc_slot(FeedbackSlotKind::Literal);
self.emit(Instruction::new_unchecked(
Opcode::CreateEmptyArrayLiteral,
vec![arr_slot],
));
let arr_reg = self.allocator.allocate_temporary();
self.emit_star(arr_reg);
if !has_spread {
for (i, elem) in a.elements.iter().enumerate() {
if let Some(elem_expr) = elem {
let idx_reg = self.allocator.allocate_temporary();
let idx_val = i as i32;
self.emit(Instruction::new_unchecked(
Opcode::LdaSmi,
vec![Operand::Immediate(idx_val)],
));
self.emit_star(idx_reg);
self.compile_expr(elem_expr)?;
let elem_slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaInArrayLiteral,
vec![to_reg_op(arr_reg), to_reg_op(idx_reg), elem_slot],
));
self.allocator
.release_temporary(idx_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
} else {
let idx_reg = self.allocator.allocate_temporary();
self.emit(Instruction::new_unchecked(Opcode::LdaZero, vec![]));
self.emit_star(idx_reg);
for elem in &a.elements {
match elem {
None => {
self.emit_ldar(idx_reg);
let inc_slot = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(Opcode::Inc, vec![inc_slot]));
self.emit_star(idx_reg);
}
Some(Expr::Spread(s)) => {
self.compile_expr(&s.argument)?;
let iterable_reg = self.allocator.allocate_temporary();
self.emit_star(iterable_reg);
let load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::GetIterator,
vec![to_reg_op(iterable_reg), load_slot, call_slot],
));
let iter_reg = self.allocator.allocate_temporary();
self.emit_star(iter_reg);
let val_reg = self.allocator.allocate_temporary();
let loop_lbl = self.new_label();
let done_lbl = self.new_label();
self.bind_label(loop_lbl);
self.emit(Instruction::new_unchecked(
Opcode::IteratorNext,
vec![to_reg_op(iter_reg), to_reg_op(val_reg)],
));
self.emit_jump_if_true_to(done_lbl);
self.emit_ldar(val_reg);
let elem_slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaInArrayLiteral,
vec![to_reg_op(arr_reg), to_reg_op(idx_reg), elem_slot],
));
self.emit_ldar(idx_reg);
let inc_slot2 = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(Opcode::Inc, vec![inc_slot2]));
self.emit_star(idx_reg);
self.emit_jump_loop_to(loop_lbl);
self.bind_label(done_lbl);
self.allocator
.release_temporary(val_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(iter_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(iterable_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
Some(expr) => {
self.compile_expr(expr)?;
let elem_slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::StaInArrayLiteral,
vec![to_reg_op(arr_reg), to_reg_op(idx_reg), elem_slot],
));
self.emit_ldar(idx_reg);
let inc_slot3 = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(Opcode::Inc, vec![inc_slot3]));
self.emit_star(idx_reg);
}
}
}
self.allocator
.release_temporary(idx_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
if !has_spread && !a.elements.is_empty() {
let len_slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
let length_name = self.add_string("length");
self.emit(Instruction::new_unchecked(
Opcode::LdaSmi,
vec![Operand::Immediate(a.elements.len() as i32)],
));
self.emit(Instruction::new_unchecked(
Opcode::StaNamedProperty,
vec![
to_reg_op(arr_reg),
Operand::ConstantPoolIdx(length_name),
len_slot,
],
));
}
self.emit_ldar(arr_reg);
self.allocator
.release_temporary(arr_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_object(&mut self, o: &crate::parser::ast::ObjectExpr) -> StatorResult<()> {
self.emit(Instruction::new_unchecked(
Opcode::CreateEmptyObjectLiteral,
vec![],
));
let obj_reg = self.allocator.allocate_temporary();
self.emit_star(obj_reg);
for prop in &o.properties {
match prop {
crate::parser::ast::ObjectProp::Prop(p) => {
self.compile_object_prop(obj_reg, p)?;
}
crate::parser::ast::ObjectProp::Spread(spread) => {
self.compile_expr(&spread.argument)?;
let src_reg = self.allocator.allocate_temporary();
self.emit_star(src_reg);
self.emit(Instruction::new_unchecked(
Opcode::CopyDataProperties,
vec![to_reg_op(obj_reg), to_reg_op(src_reg)],
));
self.allocator
.release_temporary(src_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
}
self.emit_ldar(obj_reg);
self.allocator
.release_temporary(obj_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_computed_property_key(
&mut self,
key_expr: &crate::parser::ast::Expr,
) -> StatorResult<Register> {
self.compile_expr(key_expr)?;
let key_reg = self.allocator.allocate_temporary();
self.emit_star(key_reg);
Ok(key_reg)
}
fn compile_object_prop(
&mut self,
obj_reg: Register,
p: &crate::parser::ast::Prop,
) -> StatorResult<()> {
use crate::parser::ast::{PropKey, PropValue};
let key_name: Option<String> = match &p.key {
PropKey::Ident(id) => Some(id.name.clone()),
PropKey::Str(s) => Some(s.value.clone()),
PropKey::Num(n) => {
let v = n.value;
if v.fract() == 0.0 && v.is_finite() && v >= 0.0 {
Some(format!("{}", v as u64))
} else {
Some(n.raw.clone())
}
}
PropKey::Private(id) => Some(format!("#{}", id.name)),
PropKey::Computed(_) => None,
};
let is_proto_literal_value = matches!(&p.value, PropValue::Value(_))
&& !p.is_computed
&& match &p.key {
PropKey::Ident(id) => id.name == "__proto__",
PropKey::Str(s) => {
s.value == "\"__proto__\"" || s.value == "'__proto__'" || s.value == "__proto__"
}
_ => false,
};
match &p.value {
PropValue::Value(expr) => {
if is_proto_literal_value {
self.compile_expr(expr)?;
self.emit(Instruction::new_unchecked(
Opcode::SetLiteralPrototype,
vec![to_reg_op(obj_reg)],
));
} else if let Some(name) = key_name {
self.compile_expr(expr)?;
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
} else if let PropKey::Computed(key_expr) = &p.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
self.compile_expr(expr)?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineKeyedOwnProperty,
vec![
to_reg_op(obj_reg),
to_reg_op(key_reg),
Operand::Flag(0),
slot,
],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
PropValue::Shorthand => {
if let PropKey::Ident(id) = &p.key {
self.compile_ident_load(&id.name);
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
}
}
PropValue::Method(fn_expr) => {
if let Some(ref name) = key_name {
self.compile_fn_expr_named_with_source(fn_expr, name, p.loc)?;
}
if let Some(name) = key_name {
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::StoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineNamedOwnProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
} else if let PropKey::Computed(key_expr) = &p.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
self.compile_fn_expr_with_source(fn_expr, p.loc)?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedStoreProperty);
self.emit(Instruction::new_unchecked(
Opcode::DefineKeyedOwnProperty,
vec![
to_reg_op(obj_reg),
to_reg_op(key_reg),
Operand::Flag(0),
slot,
],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
PropValue::Get(fn_expr) => {
if let Some(ref name) = key_name {
self.compile_fn_expr_named_with_source(fn_expr, &format!("get {name}"), p.loc)?;
}
if let Some(name) = key_name {
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineGetterProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
} else if let PropKey::Computed(key_expr) = &p.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
self.compile_fn_expr_with_source(fn_expr, p.loc)?;
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineKeyedGetterProperty,
vec![to_reg_op(obj_reg), to_reg_op(key_reg), slot],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
PropValue::Set(fn_expr) => {
if let Some(ref name) = key_name {
self.compile_fn_expr_named_with_source(fn_expr, &format!("set {name}"), p.loc)?;
}
if let Some(name) = key_name {
let name_idx = self.add_string(&name);
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineSetterProperty,
vec![to_reg_op(obj_reg), Operand::ConstantPoolIdx(name_idx), slot],
));
} else if let PropKey::Computed(key_expr) = &p.key {
let key_reg = self.compile_computed_property_key(key_expr)?;
self.compile_fn_expr_with_source(fn_expr, p.loc)?;
let slot = self.alloc_slot(FeedbackSlotKind::DefineAccessor);
self.emit(Instruction::new_unchecked(
Opcode::DefineKeyedSetterProperty,
vec![to_reg_op(obj_reg), to_reg_op(key_reg), slot],
));
self.allocator
.release_temporary(key_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
}
}
Ok(())
}
fn compile_template(&mut self, t: &crate::parser::ast::TemplateLit) -> StatorResult<()> {
if t.expressions.is_empty() {
let s = t
.quasis
.iter()
.filter_map(|q| q.cooked.as_deref())
.collect::<String>();
let idx = self.add_string(&s);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(idx)],
));
return Ok(());
}
let first = t.quasis[0].cooked.as_deref().unwrap_or("").to_owned();
let idx0 = self.add_string(&first);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(idx0)],
));
let acc_reg = self.allocator.allocate_temporary();
self.emit_star(acc_reg);
for (i, expr) in t.expressions.iter().enumerate() {
self.compile_expr(expr)?;
self.emit(Instruction::new_unchecked(Opcode::ToString, vec![]));
let expr_reg = self.allocator.allocate_temporary();
self.emit_star(expr_reg);
self.emit_ldar(acc_reg);
let slot = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(
Opcode::Add,
vec![to_reg_op(expr_reg), slot],
));
self.emit_star(acc_reg);
self.allocator
.release_temporary(expr_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
let quasi = t.quasis[i + 1].cooked.as_deref().unwrap_or("").to_owned();
let qi = self.add_string(&quasi);
self.emit(Instruction::new_unchecked(
Opcode::LdaConstant,
vec![Operand::ConstantPoolIdx(qi)],
));
let q_reg = self.allocator.allocate_temporary();
self.emit_star(q_reg);
self.emit_ldar(acc_reg);
let slot2 = self.alloc_slot(FeedbackSlotKind::BinaryOp);
self.emit(Instruction::new_unchecked(
Opcode::Add,
vec![to_reg_op(q_reg), slot2],
));
self.emit_star(acc_reg);
self.allocator
.release_temporary(q_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
self.emit_ldar(acc_reg);
self.allocator
.release_temporary(acc_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_tagged_template(
&mut self,
t: &crate::parser::ast::TaggedTemplateExpr,
) -> StatorResult<()> {
if let Expr::Member(m) = t.tag.as_ref() {
return self.compile_tagged_template_method(m, &t.quasi);
}
self.compile_expr(&t.tag)?;
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let tpl_idx = self.add_template_object(&t.quasi);
let tpl_slot = self.alloc_slot(FeedbackSlotKind::Literal);
self.emit(Instruction::new_unchecked(
Opcode::GetTemplateObject,
vec![Operand::ConstantPoolIdx(tpl_idx), tpl_slot],
));
let tpl_reg = self.allocator.allocate_temporary();
self.emit_star(tpl_reg);
let mut arg_regs = vec![tpl_reg];
for expr in &t.quasi.expressions {
let r = self.allocator.allocate_temporary();
self.compile_expr(expr)?;
self.emit_star(r);
arg_regs.push(r);
}
self.emit_call_any_receiver(callee_reg, &arg_regs)?;
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_tagged_template_method(
&mut self,
m: &crate::parser::ast::MemberExpr,
quasi: &crate::parser::ast::TemplateLit,
) -> StatorResult<()> {
self.compile_expr(&m.object)?;
let recv_reg = self.allocator.allocate_temporary();
self.emit_star(recv_reg);
match &m.property {
crate::parser::ast::MemberProp::Ident(id) => {
let name_idx = self.add_string(&id.name);
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
crate::parser::ast::MemberProp::Computed(key) => {
self.compile_expr(key)?;
let slot = self.alloc_slot(FeedbackSlotKind::KeyedLoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaKeyedProperty,
vec![to_reg_op(recv_reg), slot],
));
}
crate::parser::ast::MemberProp::Private(id) => {
let name_idx = self.add_string(&self.resolve_private_storage_key(&id.name));
let slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(recv_reg),
Operand::ConstantPoolIdx(name_idx),
slot,
],
));
}
}
let callee_reg = self.allocator.allocate_temporary();
self.emit_star(callee_reg);
let tpl_idx = self.add_template_object(quasi);
let tpl_slot = self.alloc_slot(FeedbackSlotKind::Literal);
self.emit(Instruction::new_unchecked(
Opcode::GetTemplateObject,
vec![Operand::ConstantPoolIdx(tpl_idx), tpl_slot],
));
let mut arg_regs = Vec::with_capacity(1 + quasi.expressions.len());
let tpl_reg = self.allocator.allocate_temporary();
self.emit_star(tpl_reg);
arg_regs.push(tpl_reg);
for expr in &quasi.expressions {
let r = self.allocator.allocate_temporary();
self.compile_expr(expr)?;
self.emit_star(r);
arg_regs.push(r);
}
let arg_count = arg_regs.len() as u32;
let slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty,
vec![
to_reg_op(callee_reg),
to_reg_op(recv_reg),
Operand::RegisterCount(arg_count),
slot,
],
));
for r in arg_regs.into_iter().rev() {
self.allocator
.release_temporary(r)
.map_err(|e| StatorError::Internal(e.to_string()))?;
}
self.allocator
.release_temporary(callee_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(recv_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn add_template_object(&mut self, t: &crate::parser::ast::TemplateLit) -> u32 {
let cooked: Vec<Option<String>> = t.quasis.iter().map(|q| q.cooked.clone()).collect();
let raw: Vec<String> = t.quasis.iter().map(|q| q.raw.clone()).collect();
self.add_constant_raw(ConstantPoolEntry::TemplateObject { cooked, raw })
}
fn compile_yield(&mut self, expr: &crate::parser::ast::YieldExpr) -> StatorResult<()> {
if let Some(arg) = &expr.argument {
if expr.delegate {
return self.compile_yield_star(arg);
}
self.compile_expr(arg)?;
} else {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
let dummy = Operand::Register(0);
let suspend_id = self.yield_suspend_id;
self.yield_suspend_id += 1;
self.emit(Instruction::new_unchecked(
Opcode::SuspendGenerator,
vec![
dummy,
dummy,
Operand::RegisterCount(0),
Operand::Immediate(suspend_id as i32),
],
));
self.emit(Instruction::new_unchecked(
Opcode::ResumeGenerator,
vec![dummy, dummy, Operand::RegisterCount(0)],
));
Ok(())
}
fn compile_await(&mut self, expr: &crate::parser::ast::AwaitExpr) -> StatorResult<()> {
self.compile_expr(&expr.argument)?;
let dummy = Operand::Register(0);
let suspend_id = self.yield_suspend_id;
self.yield_suspend_id += 1;
self.emit(Instruction::new_unchecked(
Opcode::SuspendGenerator,
vec![
dummy,
dummy,
Operand::RegisterCount(0),
Operand::Immediate(suspend_id as i32),
],
));
self.emit(Instruction::new_unchecked(
Opcode::ResumeGenerator,
vec![dummy, dummy, Operand::RegisterCount(0)],
));
Ok(())
}
fn compile_yield_star(&mut self, expr: &crate::parser::ast::Expr) -> StatorResult<()> {
if !self.is_async {
self.compile_expr(expr)?;
let iterable_reg = self.allocator.new_local();
self.emit_star(iterable_reg);
let load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::GetIterator,
vec![to_reg_op(iterable_reg), load_slot, call_slot],
));
let iter_reg = self.allocator.new_local();
self.emit_star(iter_reg);
let sent_reg = self.allocator.new_local();
let result_reg = self.allocator.new_local();
let method_reg = self.allocator.new_local();
let value_reg = self.allocator.new_local();
let exception_reg = self.allocator.new_local();
let completion_value_reg = self.allocator.new_local();
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
self.emit_star(sent_reg);
let next_idx = self.add_string("next");
let return_idx = self.add_string("return");
let throw_idx = self.add_string("throw");
let done_idx = self.add_string("done");
let value_idx = self.add_string("value");
let loop_lbl = self.new_label();
let done_lbl = self.new_label();
let yield_resume_lbl = self.new_label();
let return_forward_lbl = self.new_label();
let missing_throw_lbl = self.new_label();
let rethrow_completion_lbl = self.new_label();
let after_resume_handler_lbl = self.new_label();
let dummy = Operand::Register(0);
self.bind_label(loop_lbl);
let next_load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(iter_reg),
Operand::ConstantPoolIdx(next_idx),
next_load_slot,
],
));
self.emit_star(method_reg);
let next_call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty1,
vec![
to_reg_op(method_reg),
to_reg_op(iter_reg),
to_reg_op(sent_reg),
next_call_slot,
],
));
self.emit_star(result_reg);
let done_load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(done_idx),
done_load_slot,
],
));
self.emit_jump_if_true_to(done_lbl);
let value_load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(value_idx),
value_load_slot,
],
));
self.emit_star(value_reg);
self.emit_ldar(value_reg);
self.bind_label(yield_resume_lbl);
let suspend_id = self.yield_suspend_id;
self.yield_suspend_id += 1;
self.emit(Instruction::new_unchecked(
Opcode::SuspendGenerator,
vec![
dummy,
dummy,
Operand::RegisterCount(0),
Operand::Immediate(suspend_id as i32),
],
));
let resume_try_start = self.instructions.len() as u32;
self.emit(Instruction::new_unchecked(
Opcode::ResumeGenerator,
vec![dummy, dummy, Operand::RegisterCount(0)],
));
self.emit_star(sent_reg);
let resume_try_end = self.instructions.len() as u32;
self.emit_jump_loop_to(loop_lbl);
self.emit_jump(Opcode::Jump, after_resume_handler_lbl);
let resume_handler_start = self.instructions.len() as u32;
self.emit_star(exception_reg);
self.emit(Instruction::new_unchecked(
Opcode::CallRuntime,
vec![
Operand::RuntimeId(RUNTIME_IS_GENERATOR_RETURN_COMPLETION),
to_reg_op(exception_reg),
Operand::RegisterCount(1),
],
));
self.emit_jump_if_true_to(return_forward_lbl);
let throw_load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(iter_reg),
Operand::ConstantPoolIdx(throw_idx),
throw_load_slot,
],
));
self.emit_jump(Opcode::JumpIfUndefinedOrNull, missing_throw_lbl);
self.emit_star(method_reg);
let throw_call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty1,
vec![
to_reg_op(method_reg),
to_reg_op(iter_reg),
to_reg_op(exception_reg),
throw_call_slot,
],
));
self.emit_star(result_reg);
let forwarded_done_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(done_idx),
forwarded_done_slot,
],
));
self.emit_jump_if_true_to(done_lbl);
let forwarded_value_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(value_idx),
forwarded_value_slot,
],
));
self.emit_jump(Opcode::Jump, yield_resume_lbl);
self.bind_label(return_forward_lbl);
self.emit(Instruction::new_unchecked(
Opcode::CallRuntime,
vec![
Operand::RuntimeId(RUNTIME_GET_GENERATOR_RETURN_COMPLETION_VALUE),
to_reg_op(exception_reg),
Operand::RegisterCount(1),
],
));
self.emit_star(completion_value_reg);
let return_load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(iter_reg),
Operand::ConstantPoolIdx(return_idx),
return_load_slot,
],
));
self.emit_jump(Opcode::JumpIfUndefinedOrNull, rethrow_completion_lbl);
self.emit_star(method_reg);
let return_call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
Opcode::CallProperty1,
vec![
to_reg_op(method_reg),
to_reg_op(iter_reg),
to_reg_op(completion_value_reg),
return_call_slot,
],
));
self.emit_star(result_reg);
let return_done_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(done_idx),
return_done_slot,
],
));
self.emit_jump_if_true_to(rethrow_completion_lbl);
let return_value_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(value_idx),
return_value_slot,
],
));
self.emit_jump(Opcode::Jump, yield_resume_lbl);
self.bind_label(missing_throw_lbl);
self.emit_ldar(exception_reg);
self.emit(Instruction::new_unchecked(Opcode::ReThrow, vec![]));
self.bind_label(rethrow_completion_lbl);
self.emit_ldar(exception_reg);
self.emit(Instruction::new_unchecked(Opcode::ReThrow, vec![]));
self.bind_label(after_resume_handler_lbl);
self.handler_table.push(HandlerTableEntry {
try_start: resume_try_start,
try_end: resume_try_end,
handler: resume_handler_start,
is_finally: false,
});
self.bind_label(done_lbl);
let final_value_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
self.emit(Instruction::new_unchecked(
Opcode::LdaNamedProperty,
vec![
to_reg_op(result_reg),
Operand::ConstantPoolIdx(value_idx),
final_value_slot,
],
));
return Ok(());
}
self.compile_expr(expr)?;
let iterable_reg = self.allocator.allocate_temporary();
self.emit_star(iterable_reg);
let load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
let get_iterator = if self.is_async {
Opcode::GetAsyncIterator
} else {
Opcode::GetIterator
};
self.emit(Instruction::new_unchecked(
get_iterator,
vec![to_reg_op(iterable_reg), load_slot, call_slot],
));
self.allocator
.release_temporary(iterable_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
let iter_reg = self.allocator.allocate_temporary();
self.emit_star(iter_reg);
let val_reg = self.allocator.allocate_temporary();
let loop_lbl = self.new_label();
let done_lbl = self.new_label();
self.bind_label(loop_lbl);
self.emit(Instruction::new_unchecked(
Opcode::IteratorNext,
vec![to_reg_op(iter_reg), to_reg_op(val_reg)],
));
self.emit_jump_if_true_to(done_lbl);
let dummy = Operand::Register(0);
self.emit_ldar(val_reg);
let suspend_id = self.yield_suspend_id;
self.yield_suspend_id += 1;
self.emit(Instruction::new_unchecked(
Opcode::SuspendGenerator,
vec![
dummy,
dummy,
Operand::RegisterCount(0),
Operand::Immediate(suspend_id as i32),
],
));
self.emit(Instruction::new_unchecked(
Opcode::ResumeGenerator,
vec![dummy, dummy, Operand::RegisterCount(0)],
));
self.emit_jump_loop_to(loop_lbl);
self.bind_label(done_lbl);
self.emit_ldar(val_reg);
self.allocator
.release_temporary(val_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.allocator
.release_temporary(iter_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
Ok(())
}
fn compile_for_in(&mut self, stmt: &crate::parser::ast::ForInStmt) -> StatorResult<()> {
use crate::parser::ast::ForInOfLeft;
self.push_scope();
let loop_var_reg: Option<Register> = match &stmt.left {
ForInOfLeft::VarDecl(decl) => {
if decl.declarators.len() == 1 {
match &decl.declarators[0].id {
crate::parser::ast::Pat::Ident(id) => {
if decl.kind == VarKind::Var {
Some(self.define_function_scoped_local(&id.name))
} else if decl.kind == VarKind::Const {
Some(self.define_const_local(&id.name))
} else {
Some(self.define_local(&id.name))
}
}
_pat => Some(self.allocator.new_local()),
}
} else if decl.declarators.is_empty() {
None
} else {
return Err(StatorError::SyntaxError(
"for-in loop head must contain a single declaration".into(),
));
}
}
ForInOfLeft::Pat(_) | ForInOfLeft::Expr(_) => None,
};
self.compile_expr(&stmt.right)?;
let r_obj = self.allocator.new_local();
self.emit_star(r_obj);
let enum_slot = self.alloc_slot(FeedbackSlotKind::ForIn);
self.emit(Instruction::new_unchecked(
Opcode::ForInEnumerate,
vec![to_reg_op(r_obj)],
));
let r_keys = self.allocator.new_local();
self.emit_star(r_keys);
let prepare_slot = self.alloc_slot(FeedbackSlotKind::ForIn);
self.emit(Instruction::new_unchecked(
Opcode::ForInPrepare,
vec![to_reg_op(r_keys), prepare_slot],
));
let r_length = self.allocator.new_local();
self.emit_star(r_length);
self.emit(Instruction::new_unchecked(Opcode::LdaZero, vec![]));
let r_index = self.allocator.new_local();
self.emit_star(r_index);
let loop_lbl = self.new_label();
let continue_lbl = self.new_label();
let break_lbl = self.new_label();
self.patch_pending_labels(continue_lbl);
self.loop_stack.push((continue_lbl, break_lbl));
self.bind_label(loop_lbl);
{
let jump_idx = self.instructions.len();
self.labels[break_lbl].refs.push(jump_idx);
self.emit(Instruction::new_unchecked(
Opcode::JumpIfForInDone,
vec![
Operand::JumpOffset(0),
to_reg_op(r_index),
to_reg_op(r_length),
],
));
}
let next_slot = self.alloc_slot(FeedbackSlotKind::ForIn);
self.emit(Instruction::new_unchecked(
Opcode::ForInNext,
vec![
to_reg_op(r_obj),
to_reg_op(r_index),
to_reg_op(r_keys),
next_slot,
],
));
self.emit_jump(Opcode::JumpIfUndefined, continue_lbl);
match &stmt.left {
ForInOfLeft::VarDecl(decl) => {
if let Some(reg) = loop_var_reg {
self.emit_star(reg);
if decl.declarators.len() == 1
&& !matches!(decl.declarators[0].id, crate::parser::ast::Pat::Ident(_))
{
self.compile_binding_pattern(
&decl.declarators[0].id,
reg,
BindingMode::Declare {
is_const: decl.kind == VarKind::Const,
},
)?;
}
}
}
ForInOfLeft::Pat(pat) => match pat {
crate::parser::ast::Pat::Ident(id) => {
let binding = self.lookup_var(&id.name).ok_or_else(|| {
StatorError::SyntaxError(format!(
"for-in: undefined variable '{}'",
id.name
))
})?;
self.emit_star(binding.reg);
}
pat => {
let scratch = self.allocator.new_local();
self.emit_star(scratch);
self.compile_binding_pattern(pat, scratch, BindingMode::Assign)?;
}
},
ForInOfLeft::Expr(expr) => {
let target = AssignTarget::Expr(expr.clone());
self.compile_assign_target_store(&target)?;
}
}
self.compile_stmt(&stmt.body)?;
self.bind_label(continue_lbl);
self.emit(Instruction::new_unchecked(
Opcode::ForInStep,
vec![to_reg_op(r_index)],
));
self.emit_star(r_index);
self.emit_jump_loop_to(loop_lbl);
self.bind_label(break_lbl);
self.loop_stack.pop();
self.pop_scope();
let _ = (enum_slot, prepare_slot, next_slot);
Ok(())
}
fn compile_for_of(&mut self, stmt: &crate::parser::ast::ForOfStmt) -> StatorResult<()> {
use crate::parser::ast::ForInOfLeft;
self.push_scope();
let loop_var_reg: Option<Register> = match &stmt.left {
ForInOfLeft::VarDecl(decl) => {
if decl.declarators.len() == 1 {
match &decl.declarators[0].id {
crate::parser::ast::Pat::Ident(id) => {
if decl.kind == VarKind::Var {
Some(self.define_function_scoped_local(&id.name))
} else if decl.kind == VarKind::Const {
Some(self.define_const_local(&id.name))
} else {
Some(self.define_local(&id.name))
}
}
_pat => Some(self.allocator.new_local()),
}
} else if decl.declarators.is_empty() {
None
} else {
return Err(StatorError::SyntaxError(
"for-of loop head must contain a single declaration".into(),
));
}
}
ForInOfLeft::Pat(_) | ForInOfLeft::Expr(_) => None, };
self.compile_expr(&stmt.right)?;
let iterable_reg = self.allocator.new_local();
self.emit_star(iterable_reg);
let op = if stmt.is_await {
Opcode::GetAsyncIterator
} else {
Opcode::GetIterator
};
let load_slot = self.alloc_slot(FeedbackSlotKind::LoadProperty);
let call_slot = self.alloc_slot(FeedbackSlotKind::Call);
self.emit(Instruction::new_unchecked(
op,
vec![to_reg_op(iterable_reg), load_slot, call_slot],
));
let iter_reg = self.allocator.new_local();
self.emit_star(iter_reg);
let val_reg = self.allocator.new_local();
let loop_lbl = self.new_label();
let break_lbl = self.new_label();
self.patch_pending_labels(loop_lbl);
self.loop_stack.push((loop_lbl, break_lbl));
self.for_of_iter_regs.insert(break_lbl, iter_reg);
self.bind_label(loop_lbl);
self.emit(Instruction::new_unchecked(
Opcode::IteratorNext,
vec![to_reg_op(iter_reg), to_reg_op(val_reg)],
));
self.emit_jump_if_true_to(break_lbl);
let try_start = self.instructions.len() as u32;
match &stmt.left {
ForInOfLeft::VarDecl(decl) => {
if let Some(reg) = loop_var_reg {
self.emit_ldar(val_reg);
self.emit_star(reg);
if decl.declarators.len() == 1
&& !matches!(decl.declarators[0].id, crate::parser::ast::Pat::Ident(_))
{
self.compile_binding_pattern(
&decl.declarators[0].id,
reg,
BindingMode::Declare {
is_const: decl.kind == VarKind::Const,
},
)?;
}
}
}
ForInOfLeft::Pat(pat) => match pat {
crate::parser::ast::Pat::Ident(id) => {
let binding = self.lookup_var(&id.name).ok_or_else(|| {
StatorError::SyntaxError(format!(
"for-of: undefined variable '{}'",
id.name
))
})?;
self.emit_ldar(val_reg);
self.emit_star(binding.reg);
}
pat => {
let scratch = self.allocator.new_local();
self.emit_ldar(val_reg);
self.emit_star(scratch);
self.compile_binding_pattern(pat, scratch, BindingMode::Assign)?;
}
},
ForInOfLeft::Expr(expr) => {
self.emit_ldar(val_reg);
let target = AssignTarget::Expr(expr.clone());
self.compile_assign_target_store(&target)?;
}
}
self.compile_stmt(&stmt.body)?;
let try_end = self.instructions.len() as u32;
self.emit_jump_loop_to(loop_lbl);
self.bind_label(break_lbl);
let after_ex_label = self.new_label();
self.emit_jump(Opcode::Jump, after_ex_label);
let ex_handler_start = self.instructions.len() as u32;
let ex_reg = self.allocator.allocate_temporary();
self.emit_star(ex_reg);
self.emit(Instruction::new_unchecked(
Opcode::IteratorClose,
vec![to_reg_op(iter_reg)],
));
self.emit_ldar(ex_reg);
self.emit(Instruction::new_unchecked(Opcode::ReThrow, vec![]));
self.allocator
.release_temporary(ex_reg)
.map_err(|e| StatorError::Internal(e.to_string()))?;
self.bind_label(after_ex_label);
self.handler_table.push(HandlerTableEntry {
try_start,
try_end,
handler: ex_handler_start,
is_finally: true,
});
self.loop_stack.pop();
self.for_of_iter_regs.remove(&break_lbl);
self.pop_scope();
Ok(())
}
fn emit_jump_if_true_to(&mut self, label_idx: usize) {
let jump_idx = self.instructions.len();
self.labels[label_idx].refs.push(jump_idx);
self.emit(Instruction::new_unchecked(
Opcode::JumpIfToBooleanTrue,
vec![Operand::JumpOffset(0)],
));
}
fn emit_jump_loop_to(&mut self, label_idx: usize) {
let jump_idx = self.instructions.len();
self.labels[label_idx].refs.push(jump_idx);
self.emit(Instruction::new_unchecked(
Opcode::JumpLoop,
vec![
Operand::JumpOffset(0),
Operand::Immediate(0),
Operand::FeedbackSlot(0),
],
));
}
fn get_or_create_module_variable(&mut self, source: &str, binding: &str) -> (u32, i32) {
let key = format!("{source}\0{binding}");
if let Some(&pair) = self.module_variables.get(&key) {
return pair;
}
let module_request_idx = self.add_string(source);
let cell_idx = self.next_module_cell;
self.next_module_cell += 1;
self.module_variables
.insert(key, (module_request_idx, cell_idx));
(module_request_idx, cell_idx)
}
fn compile_import_decl(&mut self, decl: &crate::parser::ast::ImportDecl) -> StatorResult<()> {
let source = &decl.source.value;
for spec in &decl.specifiers {
match spec {
ImportSpecifier::Named(named) => {
let imported_name = match &named.imported {
ModuleExportName::Ident(id) => id.name.as_str(),
ModuleExportName::Str(s) => s.value.as_str(),
};
let (req_idx, cell) = self.get_or_create_module_variable(source, imported_name);
self.emit(Instruction::new_unchecked(
Opcode::LdaModuleVariable,
vec![Operand::ConstantPoolIdx(req_idx), Operand::Immediate(cell)],
));
let reg = self.define_local(&named.local.name);
self.emit_star(reg);
}
ImportSpecifier::Default(def) => {
let (req_idx, cell) = self.get_or_create_module_variable(source, "default");
self.emit(Instruction::new_unchecked(
Opcode::LdaModuleVariable,
vec![Operand::ConstantPoolIdx(req_idx), Operand::Immediate(cell)],
));
let reg = self.define_local(&def.local.name);
self.emit_star(reg);
}
ImportSpecifier::Namespace(ns) => {
let req_idx = self.add_string(source);
self.emit(Instruction::new_unchecked(
Opcode::GetModuleNamespace,
vec![Operand::ConstantPoolIdx(req_idx)],
));
let reg = self.define_local(&ns.local.name);
self.emit_star(reg);
}
}
}
Ok(())
}
fn compile_export_named(&mut self, decl: &ExportNamedDecl) -> StatorResult<()> {
if let Some(ref inner) = decl.declaration {
self.compile_stmt(inner)?;
let names = Self::declared_names(inner);
for name in names {
if let Some(binding) = self.lookup_var(&name) {
self.emit_ldar(binding.reg);
} else {
self.compile_ident_load(&name);
}
let (req_idx, cell) = self.get_or_create_module_variable("", &name);
self.emit(Instruction::new_unchecked(
Opcode::StaModuleVariable,
vec![Operand::ConstantPoolIdx(req_idx), Operand::Immediate(cell)],
));
}
return Ok(());
}
if let Some(ref source) = decl.source {
for spec in &decl.specifiers {
let imported_name = match &spec.local {
ModuleExportName::Ident(id) => id.name.as_str(),
ModuleExportName::Str(s) => s.value.as_str(),
};
let (req_idx, cell) =
self.get_or_create_module_variable(&source.value, imported_name);
self.emit(Instruction::new_unchecked(
Opcode::LdaModuleVariable,
vec![Operand::ConstantPoolIdx(req_idx), Operand::Immediate(cell)],
));
let exported_name = match &spec.exported {
ModuleExportName::Ident(id) => id.name.as_str(),
ModuleExportName::Str(s) => s.value.as_str(),
};
let (out_req, out_cell) = self.get_or_create_module_variable("", exported_name);
self.emit(Instruction::new_unchecked(
Opcode::StaModuleVariable,
vec![
Operand::ConstantPoolIdx(out_req),
Operand::Immediate(out_cell),
],
));
}
return Ok(());
}
for spec in &decl.specifiers {
let local_name = match &spec.local {
ModuleExportName::Ident(id) => id.name.as_str(),
ModuleExportName::Str(s) => s.value.as_str(),
};
if let Some(binding) = self.lookup_var(local_name) {
self.emit_ldar(binding.reg);
} else {
self.compile_ident_load(local_name);
}
let exported_name = match &spec.exported {
ModuleExportName::Ident(id) => id.name.as_str(),
ModuleExportName::Str(s) => s.value.as_str(),
};
let (req_idx, cell) = self.get_or_create_module_variable("", exported_name);
self.emit(Instruction::new_unchecked(
Opcode::StaModuleVariable,
vec![Operand::ConstantPoolIdx(req_idx), Operand::Immediate(cell)],
));
}
Ok(())
}
fn compile_export_default(
&mut self,
decl: &crate::parser::ast::ExportDefaultDecl,
) -> StatorResult<()> {
match &decl.declaration {
ExportDefaultExpr::Fn(f) => self.compile_fn_decl(f)?,
ExportDefaultExpr::Class(c) => self.compile_class_decl(c)?,
ExportDefaultExpr::Expr(e) => self.compile_expr(e)?,
}
let (req_idx, cell) = self.get_or_create_module_variable("", "default");
self.emit(Instruction::new_unchecked(
Opcode::StaModuleVariable,
vec![Operand::ConstantPoolIdx(req_idx), Operand::Immediate(cell)],
));
Ok(())
}
fn compile_export_all(&mut self, decl: &crate::parser::ast::ExportAllDecl) -> StatorResult<()> {
let req_idx = self.add_string(&decl.source.value);
self.emit(Instruction::new_unchecked(
Opcode::GetModuleNamespace,
vec![Operand::ConstantPoolIdx(req_idx)],
));
if let Some(ref exported) = decl.exported {
let name = match exported {
ModuleExportName::Ident(id) => id.name.as_str(),
ModuleExportName::Str(s) => s.value.as_str(),
};
let (out_req, out_cell) = self.get_or_create_module_variable("", name);
self.emit(Instruction::new_unchecked(
Opcode::StaModuleVariable,
vec![
Operand::ConstantPoolIdx(out_req),
Operand::Immediate(out_cell),
],
));
}
Ok(())
}
fn compile_meta_prop(&mut self, m: &crate::parser::ast::MetaPropExpr) -> StatorResult<()> {
if m.meta.name == "import" && m.property.name == "meta" {
self.emit(Instruction::new_unchecked(Opcode::LdaImportMeta, vec![]));
Ok(())
} else if m.meta.name == "new" && m.property.name == "target" {
self.emit(Instruction::new_unchecked(Opcode::LdaNewTarget, vec![]));
Ok(())
} else {
Err(StatorError::Internal(format!(
"{}.{} meta property is not yet supported",
m.meta.name, m.property.name,
)))
}
}
fn compile_module_decl(&mut self, decl: &ModuleDecl) -> StatorResult<()> {
match decl {
ModuleDecl::Import(d) => self.compile_import_decl(d),
ModuleDecl::ExportNamed(d) => self.compile_export_named(d),
ModuleDecl::ExportDefault(d) => self.compile_export_default(d),
ModuleDecl::ExportAll(d) => self.compile_export_all(d),
}
}
fn declared_names(stmt: &Stmt) -> Vec<String> {
match stmt {
Stmt::VarDecl(decl) => decl
.declarators
.iter()
.filter_map(|d| {
if let Pat::Ident(id) = &d.id {
Some(id.name.clone())
} else {
None
}
})
.collect(),
Stmt::FnDecl(decl) => decl.id.iter().map(|id| id.name.clone()).collect(),
Stmt::ClassDecl(decl) => decl.id.iter().map(|id| id.name.clone()).collect(),
_ => vec![],
}
}
fn finalize(mut self) -> StatorResult<BytecodeArray> {
let needs_implicit_return = self
.instructions
.last()
.map(|i| i.opcode != Opcode::Return)
.unwrap_or(true);
if needs_implicit_return {
if !self.is_program {
self.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
}
self.emit(Instruction::new_unchecked(Opcode::Return, vec![]));
}
resolve_jumps(&mut self.instructions, &self.labels)?;
let byte_offsets = compute_byte_offsets(&self.instructions);
let mut source_positions =
Vec::with_capacity(self.source_positions.len() + self.pending_positions.len());
source_positions.extend(self.source_positions);
for (instr_idx, line, column) in self.pending_positions {
if let Some(&byte_off) = byte_offsets.get(instr_idx) {
source_positions.push(SourcePosition::new(byte_off as u32, line, column));
}
}
source_positions.sort_by_key(|p| p.bytecode_offset);
let frame_size = self.allocator.frame_size();
let bytes = encode(&self.instructions);
let feedback_metadata = FeedbackMetadata::new(self.slot_kinds);
let binding_registers = self
.scopes
.iter()
.fold(HashMap::new(), |mut registers, scope| {
for (name, binding) in scope {
registers.insert(name.clone(), binding.reg.0);
}
registers
});
let ba = BytecodeArray::new(
bytes,
self.constant_pool,
frame_size,
self.param_count,
source_positions,
feedback_metadata,
self.handler_table,
);
Ok(ba
.with_generator_flag(self.is_generator)
.with_async_flag(self.is_async)
.with_module_flag(self.is_module)
.with_strict_flag(self.is_strict)
.with_top_level_flag(self.is_program)
.with_binding_registers(binding_registers))
}
}
fn resolve_jumps(instructions: &mut [Instruction], labels: &[Label]) -> StatorResult<()> {
const MAX_ITERS: usize = 20;
for _ in 0..MAX_ITERS {
let offsets = compute_byte_offsets(instructions);
let mut changed = false;
for label in labels {
if label.refs.is_empty() {
continue;
}
let target_instr = label.bound_at.ok_or_else(|| {
StatorError::Internal("unbound label during jump resolution".into())
})?;
let target_byte = offsets[target_instr];
for &jump_instr_idx in &label.refs {
let instr_end_byte = offsets[jump_instr_idx + 1];
let delta = target_byte as i64 - instr_end_byte as i64;
let new_op = Operand::JumpOffset(delta as i32);
if *instructions[jump_instr_idx].operand(0) != new_op {
*instructions[jump_instr_idx].operand_mut(0) = new_op;
changed = true;
}
}
}
if !changed {
return Ok(());
}
}
Err(StatorError::Internal(
"jump resolution failed to converge".into(),
))
}
fn compile_function(
params: &[crate::parser::ast::Param],
body: &BlockStmt,
is_generator: bool,
is_async: bool,
is_strict: bool,
) -> StatorResult<BytecodeArray> {
compile_function_inner(
params,
body,
is_generator,
is_async,
is_strict,
false,
FunctionCompileOptions::default(),
)
}
fn compile_function_with_private_names(
params: &[crate::parser::ast::Param],
body: &BlockStmt,
is_generator: bool,
is_async: bool,
is_strict: bool,
private_names: HashMap<String, PrivateNameBinding>,
) -> StatorResult<BytecodeArray> {
compile_function_inner(
params,
body,
is_generator,
is_async,
is_strict,
false,
FunctionCompileOptions {
self_name: None,
private_names,
source_text: None,
source_span: None,
closure_captures: HashMap::new(),
},
)
}
fn collect_function_scope_names(
params: &[Param],
stmts: &[Stmt],
self_name: Option<&str>,
) -> HashSet<String> {
let mut names = HashSet::new();
if let Some(name) = self_name {
names.insert(name.to_string());
}
for p in params {
collect_pat_binding_names(&p.pat, &mut names);
}
collect_function_scope_decl_names_in_stmts(stmts, &mut names);
names
}
fn collect_function_scope_decl_names_in_stmts(stmts: &[Stmt], names: &mut HashSet<String>) {
for stmt in stmts {
collect_function_scope_decl_names_in_stmt(stmt, names);
}
}
fn collect_function_scope_decl_names_in_stmt(stmt: &Stmt, names: &mut HashSet<String>) {
match stmt {
Stmt::VarDecl(v) => {
if v.kind == VarKind::Var {
collect_var_decl_binding_names(v, names);
}
}
Stmt::FnDecl(f) => {
if let Some(id) = &f.id {
names.insert(id.name.clone());
}
}
Stmt::Block(b) => collect_function_scope_decl_names_in_stmts(&b.body, names),
Stmt::If(i) => {
collect_function_scope_decl_names_in_stmt(&i.consequent, names);
if let Some(alt) = &i.alternate {
collect_function_scope_decl_names_in_stmt(alt, names);
}
}
Stmt::While(w) => collect_function_scope_decl_names_in_stmt(&w.body, names),
Stmt::DoWhile(d) => collect_function_scope_decl_names_in_stmt(&d.body, names),
Stmt::For(f) => {
if let Some(ForInit::VarDecl(v)) = &f.init
&& v.kind == VarKind::Var
{
collect_var_decl_binding_names(v, names);
}
collect_function_scope_decl_names_in_stmt(&f.body, names);
}
Stmt::ForIn(fi) => {
if let ForInOfLeft::VarDecl(v) = &fi.left
&& v.kind == VarKind::Var
{
collect_var_decl_binding_names(v, names);
}
collect_function_scope_decl_names_in_stmt(&fi.body, names);
}
Stmt::ForOf(fo) => {
if let ForInOfLeft::VarDecl(v) = &fo.left
&& v.kind == VarKind::Var
{
collect_var_decl_binding_names(v, names);
}
collect_function_scope_decl_names_in_stmt(&fo.body, names);
}
Stmt::Switch(s) => {
for case in &s.cases {
collect_function_scope_decl_names_in_stmts(&case.consequent, names);
}
}
Stmt::Try(t) => {
collect_function_scope_decl_names_in_stmts(&t.block.body, names);
if let Some(handler) = &t.handler {
collect_function_scope_decl_names_in_stmts(&handler.body.body, names);
}
if let Some(fin) = &t.finalizer {
collect_function_scope_decl_names_in_stmts(&fin.body, names);
}
}
Stmt::Labeled(l) => collect_function_scope_decl_names_in_stmt(&l.body, names),
_ => {}
}
}
fn collect_block_lexical_decl_names(stmts: &[Stmt]) -> HashSet<String> {
let mut names = HashSet::new();
for stmt in stmts {
match stmt {
Stmt::VarDecl(v) => {
if v.kind != VarKind::Var {
collect_var_decl_binding_names(v, &mut names);
}
}
Stmt::FnDecl(f) => {
if let Some(id) = &f.id {
names.insert(id.name.clone());
}
}
_ => {}
}
}
names
}
fn collect_var_decl_binding_names(decl: &VarDecl, names: &mut HashSet<String>) {
for d in &decl.declarators {
collect_pat_binding_names(&d.id, names);
}
}
fn collect_for_left_binding_names(left: &ForInOfLeft, names: &mut HashSet<String>) {
if let ForInOfLeft::VarDecl(v) = left {
collect_var_decl_binding_names(v, names);
}
}
fn collect_pat_binding_names(pat: &Pat, names: &mut HashSet<String>) {
match pat {
Pat::Ident(id) => {
names.insert(id.name.clone());
}
Pat::Array(a) => {
for p in a.elements.iter().flatten() {
collect_pat_binding_names(p, names);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => collect_pat_binding_names(&kv.value, names),
ObjectPatProp::Assign(a) => {
names.insert(a.key.name.clone());
}
ObjectPatProp::Rest(r) => collect_pat_binding_names(&r.argument, names),
}
}
}
Pat::Assign(a) => collect_pat_binding_names(&a.left, names),
Pat::Rest(r) => collect_pat_binding_names(&r.argument, names),
Pat::Expr(_) => {}
}
}
fn is_bound_in_scopes(name: &str, scopes: &[HashSet<String>]) -> bool {
scopes.iter().rev().any(|scope| scope.contains(name))
}
fn record_free_ref(name: &str, scopes: &[HashSet<String>], free_refs: &mut HashSet<String>) {
if !is_bound_in_scopes(name, scopes) {
free_refs.insert(name.to_string());
}
}
fn capture_visible_free_refs(
free_refs: HashSet<String>,
scopes: &[HashSet<String>],
captured: &mut HashSet<String>,
) {
for name in free_refs {
if is_bound_in_scopes(&name, scopes) {
captured.insert(name);
}
}
}
fn collect_function_free_refs(
params: &[Param],
stmts: &[Stmt],
self_name: Option<&str>,
) -> HashSet<String> {
let function_scope = collect_function_scope_names(params, stmts, self_name);
let mut scopes = vec![function_scope];
let mut free_refs = HashSet::new();
collect_free_refs_in_params(params, &scopes, &mut free_refs);
collect_free_refs_in_block(stmts, &mut scopes, &mut free_refs);
free_refs
}
fn collect_arrow_free_refs(arrow: &ArrowExpr) -> HashSet<String> {
let mut scopes = vec![collect_function_scope_names(&arrow.params, &[], None)];
let mut free_refs = HashSet::new();
collect_free_refs_in_params(&arrow.params, &scopes, &mut free_refs);
match &arrow.body {
ArrowBody::Block(b) => collect_free_refs_in_block(&b.body, &mut scopes, &mut free_refs),
ArrowBody::Expr(e) => collect_free_refs_in_expr(e, &scopes, &mut free_refs),
}
free_refs
}
fn collect_free_refs_in_params(
params: &[Param],
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
for param in params {
collect_free_refs_in_pat_initializers(¶m.pat, scopes, free_refs);
if let Some(default) = ¶m.default {
collect_free_refs_in_expr(default, scopes, free_refs);
}
}
}
fn collect_free_refs_in_block(
stmts: &[Stmt],
scopes: &mut Vec<HashSet<String>>,
free_refs: &mut HashSet<String>,
) {
scopes.push(collect_block_lexical_decl_names(stmts));
for stmt in stmts {
collect_free_refs_in_stmt(stmt, scopes, free_refs);
}
scopes.pop();
}
fn collect_free_refs_in_stmt(
stmt: &Stmt,
scopes: &mut Vec<HashSet<String>>,
free_refs: &mut HashSet<String>,
) {
match stmt {
Stmt::Expr(es) => collect_free_refs_in_expr(&es.expr, scopes, free_refs),
Stmt::Return(r) => {
if let Some(e) = &r.argument {
collect_free_refs_in_expr(e, scopes, free_refs);
}
}
Stmt::Block(b) => collect_free_refs_in_block(&b.body, scopes, free_refs),
Stmt::If(i) => {
collect_free_refs_in_expr(&i.test, scopes, free_refs);
collect_free_refs_in_stmt(&i.consequent, scopes, free_refs);
if let Some(alt) = &i.alternate {
collect_free_refs_in_stmt(alt, scopes, free_refs);
}
}
Stmt::While(w) => {
collect_free_refs_in_expr(&w.test, scopes, free_refs);
collect_free_refs_in_stmt(&w.body, scopes, free_refs);
}
Stmt::DoWhile(d) => {
collect_free_refs_in_stmt(&d.body, scopes, free_refs);
collect_free_refs_in_expr(&d.test, scopes, free_refs);
}
Stmt::For(f) => {
let pushed_loop_scope = push_for_init_scope(&f.init, scopes);
if let Some(init) = &f.init {
match init {
ForInit::Expr(e) => collect_free_refs_in_expr(e, scopes, free_refs),
ForInit::VarDecl(v) => {
collect_free_refs_in_var_decl_initializers(v, scopes, free_refs);
}
}
}
if let Some(test) = &f.test {
collect_free_refs_in_expr(test, scopes, free_refs);
}
if let Some(update) = &f.update {
collect_free_refs_in_expr(update, scopes, free_refs);
}
collect_free_refs_in_stmt(&f.body, scopes, free_refs);
if pushed_loop_scope {
scopes.pop();
}
}
Stmt::ForIn(fi) => {
let pushed_loop_scope = push_for_in_of_scope(&fi.left, scopes);
collect_free_refs_in_for_left(&fi.left, scopes, free_refs);
collect_free_refs_in_expr(&fi.right, scopes, free_refs);
collect_free_refs_in_stmt(&fi.body, scopes, free_refs);
if pushed_loop_scope {
scopes.pop();
}
}
Stmt::ForOf(fo) => {
let pushed_loop_scope = push_for_in_of_scope(&fo.left, scopes);
collect_free_refs_in_for_left(&fo.left, scopes, free_refs);
collect_free_refs_in_expr(&fo.right, scopes, free_refs);
collect_free_refs_in_stmt(&fo.body, scopes, free_refs);
if pushed_loop_scope {
scopes.pop();
}
}
Stmt::VarDecl(v) => collect_free_refs_in_var_decl_initializers(v, scopes, free_refs),
Stmt::Switch(s) => {
collect_free_refs_in_expr(&s.discriminant, scopes, free_refs);
scopes.push(collect_switch_lexical_decl_names(&s.cases));
for case in &s.cases {
if let Some(test) = &case.test {
collect_free_refs_in_expr(test, scopes, free_refs);
}
for stmt in &case.consequent {
collect_free_refs_in_stmt(stmt, scopes, free_refs);
}
}
scopes.pop();
}
Stmt::Throw(t) => collect_free_refs_in_expr(&t.argument, scopes, free_refs),
Stmt::Try(t) => {
collect_free_refs_in_block(&t.block.body, scopes, free_refs);
if let Some(handler) = &t.handler {
let mut catch_scope = HashSet::new();
if let Some(param) = &handler.param {
collect_pat_binding_names(param, &mut catch_scope);
collect_free_refs_in_pat_initializers(param, scopes, free_refs);
}
scopes.push(catch_scope);
collect_free_refs_in_block(&handler.body.body, scopes, free_refs);
scopes.pop();
}
if let Some(fin) = &t.finalizer {
collect_free_refs_in_block(&fin.body, scopes, free_refs);
}
}
Stmt::Labeled(l) => collect_free_refs_in_stmt(&l.body, scopes, free_refs),
Stmt::With(w) => {
collect_free_refs_in_expr(&w.object, scopes, free_refs);
collect_free_refs_in_stmt(&w.body, scopes, free_refs);
}
Stmt::FnDecl(f) => {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
let nested_free_refs = collect_function_free_refs(&f.params, &f.body.body, self_name);
for name in nested_free_refs {
record_free_ref(&name, scopes, free_refs);
}
}
_ => {}
}
}
fn collect_free_refs_in_expr(
expr: &Expr,
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
match expr {
Expr::Ident(id) => record_free_ref(&id.name, scopes, free_refs),
Expr::Fn(f) => {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
for name in collect_function_free_refs(&f.params, &f.body.body, self_name) {
record_free_ref(&name, scopes, free_refs);
}
}
Expr::Arrow(a) => {
for name in collect_arrow_free_refs(a) {
record_free_ref(&name, scopes, free_refs);
}
}
Expr::Class(_) => {}
Expr::Unary(u) => collect_free_refs_in_expr(&u.argument, scopes, free_refs),
Expr::Update(u) => collect_free_refs_in_expr(&u.argument, scopes, free_refs),
Expr::Binary(b) => {
collect_free_refs_in_expr(&b.left, scopes, free_refs);
collect_free_refs_in_expr(&b.right, scopes, free_refs);
}
Expr::Logical(l) => {
collect_free_refs_in_expr(&l.left, scopes, free_refs);
collect_free_refs_in_expr(&l.right, scopes, free_refs);
}
Expr::Assign(a) => {
match &a.left {
AssignTarget::Expr(e) => collect_free_refs_in_expr(e, scopes, free_refs),
AssignTarget::Pat(p) => collect_free_refs_in_pat_target(p, scopes, free_refs),
}
collect_free_refs_in_expr(&a.right, scopes, free_refs);
}
Expr::Conditional(c) => {
collect_free_refs_in_expr(&c.test, scopes, free_refs);
collect_free_refs_in_expr(&c.consequent, scopes, free_refs);
collect_free_refs_in_expr(&c.alternate, scopes, free_refs);
}
Expr::Call(c) => {
collect_free_refs_in_expr(&c.callee, scopes, free_refs);
for arg in &c.arguments {
collect_free_refs_in_expr(arg, scopes, free_refs);
}
}
Expr::New(n) => {
collect_free_refs_in_expr(&n.callee, scopes, free_refs);
for arg in &n.arguments {
collect_free_refs_in_expr(arg, scopes, free_refs);
}
}
Expr::Member(m) => {
collect_free_refs_in_expr(&m.object, scopes, free_refs);
if let MemberProp::Computed(e) = &m.property {
collect_free_refs_in_expr(e, scopes, free_refs);
}
}
Expr::OptionalMember(m) => {
collect_free_refs_in_expr(&m.object, scopes, free_refs);
if let MemberProp::Computed(e) = &m.property {
collect_free_refs_in_expr(e, scopes, free_refs);
}
}
Expr::OptionalCall(c) => {
collect_free_refs_in_expr(&c.callee, scopes, free_refs);
for arg in &c.arguments {
collect_free_refs_in_expr(arg, scopes, free_refs);
}
}
Expr::Sequence(s) => {
for e in &s.expressions {
collect_free_refs_in_expr(e, scopes, free_refs);
}
}
Expr::Array(a) => {
for elem in a.elements.iter().flatten() {
collect_free_refs_in_expr(elem, scopes, free_refs);
}
}
Expr::Object(o) => {
for prop in &o.properties {
match prop {
ObjectProp::Prop(p) => {
collect_free_refs_in_prop_key(&p.key, scopes, free_refs);
match &p.value {
PropValue::Value(e) => {
collect_free_refs_in_expr(e, scopes, free_refs);
}
PropValue::Shorthand => {
if let PropKey::Ident(id) = &p.key {
record_free_ref(&id.name, scopes, free_refs);
}
}
PropValue::Get(f) | PropValue::Set(f) | PropValue::Method(f) => {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
for name in
collect_function_free_refs(&f.params, &f.body.body, self_name)
{
record_free_ref(&name, scopes, free_refs);
}
}
}
}
ObjectProp::Spread(s) => {
collect_free_refs_in_expr(&s.argument, scopes, free_refs);
}
}
}
}
Expr::Spread(s) => collect_free_refs_in_expr(&s.argument, scopes, free_refs),
Expr::Template(t) => {
for e in &t.expressions {
collect_free_refs_in_expr(e, scopes, free_refs);
}
}
Expr::TaggedTemplate(t) => {
collect_free_refs_in_expr(&t.tag, scopes, free_refs);
for e in &t.quasi.expressions {
collect_free_refs_in_expr(e, scopes, free_refs);
}
}
Expr::Yield(y) => {
if let Some(arg) = &y.argument {
collect_free_refs_in_expr(arg, scopes, free_refs);
}
}
Expr::Await(a) => collect_free_refs_in_expr(&a.argument, scopes, free_refs),
_ => {}
}
}
fn collect_free_refs_in_prop_key(
key: &PropKey,
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
if let PropKey::Computed(expr) = key {
collect_free_refs_in_expr(expr, scopes, free_refs);
}
}
fn collect_free_refs_in_var_decl_initializers(
decl: &VarDecl,
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
for d in &decl.declarators {
collect_free_refs_in_pat_initializers(&d.id, scopes, free_refs);
if let Some(init) = &d.init {
collect_free_refs_in_expr(init, scopes, free_refs);
}
}
}
fn collect_free_refs_in_pat_initializers(
pat: &Pat,
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
match pat {
Pat::Array(a) => {
for p in a.elements.iter().flatten() {
collect_free_refs_in_pat_initializers(p, scopes, free_refs);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => {
collect_free_refs_in_prop_key(&kv.key, scopes, free_refs);
collect_free_refs_in_pat_initializers(&kv.value, scopes, free_refs);
}
ObjectPatProp::Assign(a) => {
if let Some(default) = &a.value {
collect_free_refs_in_expr(default, scopes, free_refs);
}
}
ObjectPatProp::Rest(r) => {
collect_free_refs_in_pat_initializers(&r.argument, scopes, free_refs);
}
}
}
}
Pat::Assign(a) => {
collect_free_refs_in_pat_initializers(&a.left, scopes, free_refs);
collect_free_refs_in_expr(&a.right, scopes, free_refs);
}
Pat::Rest(r) => collect_free_refs_in_pat_initializers(&r.argument, scopes, free_refs),
Pat::Expr(e) => collect_free_refs_in_expr(e, scopes, free_refs),
Pat::Ident(_) => {}
}
}
fn collect_free_refs_in_pat_target(
pat: &Pat,
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
match pat {
Pat::Ident(id) => record_free_ref(&id.name, scopes, free_refs),
Pat::Array(a) => {
for p in a.elements.iter().flatten() {
collect_free_refs_in_pat_target(p, scopes, free_refs);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => {
collect_free_refs_in_prop_key(&kv.key, scopes, free_refs);
collect_free_refs_in_pat_target(&kv.value, scopes, free_refs);
}
ObjectPatProp::Assign(a) => {
record_free_ref(&a.key.name, scopes, free_refs);
if let Some(default) = &a.value {
collect_free_refs_in_expr(default, scopes, free_refs);
}
}
ObjectPatProp::Rest(r) => {
collect_free_refs_in_pat_target(&r.argument, scopes, free_refs)
}
}
}
}
Pat::Assign(a) => {
collect_free_refs_in_pat_target(&a.left, scopes, free_refs);
collect_free_refs_in_expr(&a.right, scopes, free_refs);
}
Pat::Rest(r) => collect_free_refs_in_pat_target(&r.argument, scopes, free_refs),
Pat::Expr(e) => collect_free_refs_in_expr(e, scopes, free_refs),
}
}
fn collect_free_refs_in_for_left(
left: &ForInOfLeft,
scopes: &[HashSet<String>],
free_refs: &mut HashSet<String>,
) {
match left {
ForInOfLeft::VarDecl(v) => collect_free_refs_in_var_decl_initializers(v, scopes, free_refs),
ForInOfLeft::Pat(p) => collect_free_refs_in_pat_target(p, scopes, free_refs),
ForInOfLeft::Expr(e) => collect_free_refs_in_expr(e, scopes, free_refs),
}
}
fn push_for_init_scope(init: &Option<ForInit>, scopes: &mut Vec<HashSet<String>>) -> bool {
if let Some(ForInit::VarDecl(v)) = init
&& v.kind != VarKind::Var
{
let mut names = HashSet::new();
collect_var_decl_binding_names(v, &mut names);
scopes.push(names);
return true;
}
false
}
fn push_for_in_of_scope(left: &ForInOfLeft, scopes: &mut Vec<HashSet<String>>) -> bool {
if let ForInOfLeft::VarDecl(v) = left
&& v.kind != VarKind::Var
{
let mut names = HashSet::new();
collect_for_left_binding_names(left, &mut names);
scopes.push(names);
return true;
}
false
}
fn collect_switch_lexical_decl_names(cases: &[crate::parser::ast::SwitchCase]) -> HashSet<String> {
let mut names = HashSet::new();
for case in cases {
names.extend(collect_block_lexical_decl_names(&case.consequent));
}
names
}
fn find_captured_vars(outer_params: &[Param], body: &BlockStmt) -> HashSet<String> {
let function_scope = collect_function_scope_names(outer_params, &body.body, None);
let mut scopes = vec![function_scope];
let mut captured = HashSet::new();
find_captures_in_params(outer_params, &mut scopes, &mut captured);
find_captures_in_block(&body.body, &mut scopes, &mut captured);
captured
}
fn find_captures_in_params(
params: &[Param],
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
for param in params {
find_captures_in_pat_exprs(¶m.pat, scopes, captured);
if let Some(default) = ¶m.default {
find_captures_in_expr(default, scopes, captured);
}
}
}
fn find_captures_in_block(
stmts: &[Stmt],
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
scopes.push(collect_block_lexical_decl_names(stmts));
for stmt in stmts {
find_captures_in_stmt(stmt, scopes, captured);
}
scopes.pop();
}
fn find_captures_in_stmt(
stmt: &Stmt,
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
match stmt {
Stmt::Expr(es) => find_captures_in_expr(&es.expr, scopes, captured),
Stmt::Return(r) => {
if let Some(e) = &r.argument {
find_captures_in_expr(e, scopes, captured);
}
}
Stmt::Block(b) => find_captures_in_block(&b.body, scopes, captured),
Stmt::If(i) => {
find_captures_in_expr(&i.test, scopes, captured);
find_captures_in_stmt(&i.consequent, scopes, captured);
if let Some(alt) = &i.alternate {
find_captures_in_stmt(alt, scopes, captured);
}
}
Stmt::While(w) => {
find_captures_in_expr(&w.test, scopes, captured);
find_captures_in_stmt(&w.body, scopes, captured);
}
Stmt::DoWhile(d) => {
find_captures_in_stmt(&d.body, scopes, captured);
find_captures_in_expr(&d.test, scopes, captured);
}
Stmt::For(f) => {
let pushed_loop_scope = push_for_init_scope(&f.init, scopes);
if let Some(init) = &f.init {
match init {
ForInit::Expr(e) => find_captures_in_expr(e, scopes, captured),
ForInit::VarDecl(v) => find_captures_in_var_decl(v, scopes, captured),
}
}
if let Some(test) = &f.test {
find_captures_in_expr(test, scopes, captured);
}
if let Some(update) = &f.update {
find_captures_in_expr(update, scopes, captured);
}
find_captures_in_stmt(&f.body, scopes, captured);
if pushed_loop_scope {
scopes.pop();
}
}
Stmt::ForIn(fi) => {
let pushed_loop_scope = push_for_in_of_scope(&fi.left, scopes);
find_captures_in_for_left(&fi.left, scopes, captured);
find_captures_in_expr(&fi.right, scopes, captured);
find_captures_in_stmt(&fi.body, scopes, captured);
if pushed_loop_scope {
scopes.pop();
}
}
Stmt::ForOf(fo) => {
let pushed_loop_scope = push_for_in_of_scope(&fo.left, scopes);
find_captures_in_for_left(&fo.left, scopes, captured);
find_captures_in_expr(&fo.right, scopes, captured);
find_captures_in_stmt(&fo.body, scopes, captured);
if pushed_loop_scope {
scopes.pop();
}
}
Stmt::VarDecl(v) => find_captures_in_var_decl(v, scopes, captured),
Stmt::Switch(s) => {
find_captures_in_expr(&s.discriminant, scopes, captured);
scopes.push(collect_switch_lexical_decl_names(&s.cases));
for case in &s.cases {
if let Some(test) = &case.test {
find_captures_in_expr(test, scopes, captured);
}
for stmt in &case.consequent {
find_captures_in_stmt(stmt, scopes, captured);
}
}
scopes.pop();
}
Stmt::Throw(t) => find_captures_in_expr(&t.argument, scopes, captured),
Stmt::Try(t) => {
find_captures_in_block(&t.block.body, scopes, captured);
if let Some(handler) = &t.handler {
let mut catch_scope = HashSet::new();
if let Some(param) = &handler.param {
collect_pat_binding_names(param, &mut catch_scope);
find_captures_in_pat_exprs(param, scopes, captured);
}
scopes.push(catch_scope);
find_captures_in_block(&handler.body.body, scopes, captured);
scopes.pop();
}
if let Some(fin) = &t.finalizer {
find_captures_in_block(&fin.body, scopes, captured);
}
}
Stmt::Labeled(l) => find_captures_in_stmt(&l.body, scopes, captured),
Stmt::With(w) => {
find_captures_in_expr(&w.object, scopes, captured);
find_captures_in_stmt(&w.body, scopes, captured);
}
Stmt::FnDecl(f) => {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
capture_visible_free_refs(
collect_function_free_refs(&f.params, &f.body.body, self_name),
scopes,
captured,
);
}
_ => {}
}
}
fn find_captures_in_expr(
expr: &Expr,
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
match expr {
Expr::Fn(f) => {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
capture_visible_free_refs(
collect_function_free_refs(&f.params, &f.body.body, self_name),
scopes,
captured,
);
}
Expr::Arrow(a) => {
capture_visible_free_refs(collect_arrow_free_refs(a), scopes, captured);
}
Expr::Class(_) => {}
Expr::Unary(u) => find_captures_in_expr(&u.argument, scopes, captured),
Expr::Update(u) => find_captures_in_expr(&u.argument, scopes, captured),
Expr::Binary(b) => {
find_captures_in_expr(&b.left, scopes, captured);
find_captures_in_expr(&b.right, scopes, captured);
}
Expr::Logical(l) => {
find_captures_in_expr(&l.left, scopes, captured);
find_captures_in_expr(&l.right, scopes, captured);
}
Expr::Assign(a) => {
match &a.left {
AssignTarget::Expr(e) => find_captures_in_expr(e, scopes, captured),
AssignTarget::Pat(p) => find_captures_in_pat_exprs(p, scopes, captured),
}
find_captures_in_expr(&a.right, scopes, captured);
}
Expr::Conditional(c) => {
find_captures_in_expr(&c.test, scopes, captured);
find_captures_in_expr(&c.consequent, scopes, captured);
find_captures_in_expr(&c.alternate, scopes, captured);
}
Expr::Call(c) => {
find_captures_in_expr(&c.callee, scopes, captured);
for arg in &c.arguments {
find_captures_in_expr(arg, scopes, captured);
}
}
Expr::New(n) => {
find_captures_in_expr(&n.callee, scopes, captured);
for arg in &n.arguments {
find_captures_in_expr(arg, scopes, captured);
}
}
Expr::Member(m) => {
find_captures_in_expr(&m.object, scopes, captured);
if let MemberProp::Computed(e) = &m.property {
find_captures_in_expr(e, scopes, captured);
}
}
Expr::OptionalMember(m) => {
find_captures_in_expr(&m.object, scopes, captured);
if let MemberProp::Computed(e) = &m.property {
find_captures_in_expr(e, scopes, captured);
}
}
Expr::OptionalCall(c) => {
find_captures_in_expr(&c.callee, scopes, captured);
for arg in &c.arguments {
find_captures_in_expr(arg, scopes, captured);
}
}
Expr::Sequence(s) => {
for e in &s.expressions {
find_captures_in_expr(e, scopes, captured);
}
}
Expr::Array(a) => {
for elem in a.elements.iter().flatten() {
find_captures_in_expr(elem, scopes, captured);
}
}
Expr::Object(o) => {
for prop in &o.properties {
match prop {
ObjectProp::Prop(p) => {
find_captures_in_prop_key(&p.key, scopes, captured);
match &p.value {
PropValue::Value(e) => find_captures_in_expr(e, scopes, captured),
PropValue::Get(f) | PropValue::Set(f) | PropValue::Method(f) => {
let self_name = f.id.as_ref().map(|id| id.name.as_str());
capture_visible_free_refs(
collect_function_free_refs(&f.params, &f.body.body, self_name),
scopes,
captured,
);
}
PropValue::Shorthand => {}
}
}
ObjectProp::Spread(s) => {
find_captures_in_expr(&s.argument, scopes, captured);
}
}
}
}
Expr::Spread(s) => find_captures_in_expr(&s.argument, scopes, captured),
Expr::Template(t) => {
for e in &t.expressions {
find_captures_in_expr(e, scopes, captured);
}
}
Expr::TaggedTemplate(t) => {
find_captures_in_expr(&t.tag, scopes, captured);
for e in &t.quasi.expressions {
find_captures_in_expr(e, scopes, captured);
}
}
Expr::Yield(y) => {
if let Some(arg) = &y.argument {
find_captures_in_expr(arg, scopes, captured);
}
}
Expr::Await(a) => find_captures_in_expr(&a.argument, scopes, captured),
_ => {}
}
}
fn find_captures_in_var_decl(
decl: &VarDecl,
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
for d in &decl.declarators {
find_captures_in_pat_exprs(&d.id, scopes, captured);
if let Some(init) = &d.init {
find_captures_in_expr(init, scopes, captured);
}
}
}
fn find_captures_in_for_left(
left: &ForInOfLeft,
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
match left {
ForInOfLeft::VarDecl(v) => find_captures_in_var_decl(v, scopes, captured),
ForInOfLeft::Pat(p) => find_captures_in_pat_exprs(p, scopes, captured),
ForInOfLeft::Expr(e) => find_captures_in_expr(e, scopes, captured),
}
}
fn find_captures_in_prop_key(
key: &PropKey,
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
if let PropKey::Computed(expr) = key {
find_captures_in_expr(expr, scopes, captured);
}
}
fn find_captures_in_pat_exprs(
pat: &Pat,
scopes: &mut Vec<HashSet<String>>,
captured: &mut HashSet<String>,
) {
match pat {
Pat::Array(a) => {
for p in a.elements.iter().flatten() {
find_captures_in_pat_exprs(p, scopes, captured);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => {
find_captures_in_prop_key(&kv.key, scopes, captured);
find_captures_in_pat_exprs(&kv.value, scopes, captured);
}
ObjectPatProp::Assign(a) => {
if let Some(default) = &a.value {
find_captures_in_expr(default, scopes, captured);
}
}
ObjectPatProp::Rest(r) => {
find_captures_in_pat_exprs(&r.argument, scopes, captured)
}
}
}
}
Pat::Assign(a) => {
find_captures_in_pat_exprs(&a.left, scopes, captured);
find_captures_in_expr(&a.right, scopes, captured);
}
Pat::Rest(r) => find_captures_in_pat_exprs(&r.argument, scopes, captured),
Pat::Expr(e) => find_captures_in_expr(e, scopes, captured),
Pat::Ident(_) => {}
}
}
fn compile_function_inner(
params: &[crate::parser::ast::Param],
body: &BlockStmt,
is_generator: bool,
is_async: bool,
is_strict: bool,
is_arrow: bool,
options: FunctionCompileOptions,
) -> StatorResult<BytecodeArray> {
let mut compiler = FunctionCompiler::new(params)?;
compiler.is_generator = is_generator;
compiler.is_async = is_async;
compiler.is_strict = is_strict;
let FunctionCompileOptions {
self_name,
private_names,
source_text,
source_span,
closure_captures,
} = options;
compiler.private_names = private_names;
compiler.source_text = source_text;
compiler.closure_captures = closure_captures;
if is_generator || is_async {
compiler.emit(Instruction::new_unchecked(
Opcode::SwitchOnGeneratorState,
vec![Operand::Register(0)],
));
}
compiler.emit_param_prologue(params)?;
if !is_arrow {
compiler.emit(Instruction::new_unchecked(
if is_strict {
Opcode::CreateUnmappedArguments
} else {
Opcode::CreateMappedArguments
},
vec![],
));
let args_reg = compiler.define_local("arguments");
compiler.emit_star(args_reg);
}
let self_name_reg = if let Some(name) = self_name.as_deref() {
let reg = compiler.define_local(name);
compiler.emit(Instruction::new_unchecked(Opcode::LdaUndefined, vec![]));
compiler.emit_star(reg);
Some(reg)
} else {
None
};
compiler.hoist_var_declarations(&body.body);
compiler.hoist_annex_b_fn_declarations(&body.body);
let captured = find_captured_vars(params, body);
if !captured.is_empty() {
let mut slot_idx = 0u32;
let mut sorted: Vec<_> = captured.into_iter().collect();
sorted.sort();
for name in sorted {
compiler.context_bindings.insert(name, slot_idx);
slot_idx += 1;
}
compiler.context_slot_count = slot_idx;
let scope_idx = compiler.add_string("<closure>");
compiler.emit(Instruction::new_unchecked(
Opcode::CreateFunctionContext,
vec![
Operand::ConstantPoolIdx(scope_idx),
Operand::Immediate(compiler.context_slot_count as i32),
],
));
let saved_ctx_reg = compiler.allocator.new_local();
compiler.emit(Instruction::new_unchecked(
Opcode::PushContext,
vec![to_reg_op(saved_ctx_reg)],
));
for (name, &slot) in &compiler.context_bindings.clone() {
if let Some(binding) = compiler.scopes[0].get(name) {
compiler.emit_ldar(binding.reg);
compiler.emit(Instruction::new_unchecked(
Opcode::StaCurrentContextSlot,
vec![Operand::ConstantPoolIdx(slot)],
));
}
}
}
for stmt in &body.body {
if let Stmt::FnDecl(decl) = stmt {
compiler.compile_fn_decl(decl)?;
}
}
for stmt in &body.body {
if !matches!(stmt, Stmt::FnDecl(_)) {
compiler.compile_stmt(stmt)?;
}
}
let source_text = source_span.and_then(|loc| compiler.source_text_for_loc(loc));
let mut ba = compiler.finalize()?.with_function_length(
params
.iter()
.take_while(|param| param.default.is_none() && !matches!(param.pat, Pat::Rest(_)))
.count() as u32,
);
if let Some(reg) = self_name_reg {
ba = ba.with_self_name_register(reg.0);
}
if let Some(source) = source_text {
ba = ba.with_source_text(source);
}
let writes_closure = ba
.instructions()
.map(|instrs| {
instrs.iter().any(|i| {
matches!(
i.opcode,
Opcode::StaContextSlot | Opcode::StaCurrentContextSlot
)
})
})
.unwrap_or(false);
ba = ba.with_writes_closure_vars(writes_closure);
Ok(ba)
}
pub struct BytecodeGenerator;
impl BytecodeGenerator {
pub fn compile_program(program: &Program) -> StatorResult<BytecodeArray> {
Self::compile_program_with_source(program, None)
}
pub fn compile_program_with_source(
program: &Program,
source: Option<&str>,
) -> StatorResult<BytecodeArray> {
let mut compiler = FunctionCompiler::new(&[])?;
compiler.source_text = source.map(Rc::<str>::from);
compiler.is_program = true;
let is_module = program.source_type == SourceType::Module;
compiler.is_module = is_module;
compiler.is_strict = program.is_strict || is_module;
if is_module {
compiler.is_async = true;
}
let top_stmts: Vec<&Stmt> = program
.body
.iter()
.filter_map(|item| {
if let ProgramItem::Stmt(s) = item {
Some(s)
} else {
None
}
})
.collect();
let stmts_owned: Vec<Stmt> = top_stmts.iter().map(|s| (*s).clone()).collect();
compiler.hoist_var_declarations_global(&stmts_owned);
compiler.hoist_annex_b_fn_declarations_global(&stmts_owned);
compiler.hoist_lexical_decls(&stmts_owned);
for item in &program.body {
if let ProgramItem::Stmt(Stmt::FnDecl(decl)) = item {
compiler.compile_fn_decl(decl)?;
}
}
for item in &program.body {
match item {
ProgramItem::Stmt(Stmt::FnDecl(_)) => {} ProgramItem::Stmt(stmt) => compiler.compile_stmt(stmt)?,
ProgramItem::ModuleDecl(decl) => {
if !is_module {
return Err(StatorError::SyntaxError(
"module declarations are not allowed in scripts".into(),
));
}
compiler.compile_module_decl(decl)?;
}
}
}
compiler.finalize()
}
pub fn compile_eval_program(program: &Program) -> StatorResult<BytecodeArray> {
Self::compile_eval_program_with_source(program, None)
}
pub fn compile_eval_program_with_source(
program: &Program,
source: Option<&str>,
) -> StatorResult<BytecodeArray> {
let mut compiler = FunctionCompiler::new(&[])?;
compiler.source_text = source.map(Rc::<str>::from);
compiler.is_program = true;
compiler.is_eval_scope = true;
compiler.is_strict = program.is_strict;
let top_stmts: Vec<Stmt> = program
.body
.iter()
.filter_map(|item| {
if let ProgramItem::Stmt(s) = item {
Some(s.clone())
} else {
None
}
})
.collect();
compiler.hoist_lexical_decls(&top_stmts);
for item in &program.body {
if let ProgramItem::Stmt(Stmt::FnDecl(decl)) = item {
compiler.compile_fn_decl(decl)?;
}
}
for item in &program.body {
match item {
ProgramItem::Stmt(Stmt::FnDecl(_)) => {}
ProgramItem::Stmt(stmt) => compiler.compile_stmt(stmt)?,
ProgramItem::ModuleDecl(_) => {
return Err(StatorError::SyntaxError(
"module declarations are not allowed in eval".into(),
));
}
}
}
compiler.finalize()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::bytecodes::{Opcode, decode};
use crate::parser::ast::{
ArrayPat, AssignExpr, AssignOp, AssignPatProp, AssignTarget, BinaryExpr, BinaryOp,
BlockStmt, BoolLit, BreakStmt, CatchClause, ClassBody, ClassDecl, ClassExpr, ClassMember,
ContinueStmt, DoWhileStmt, Expr, ExprStmt, FnDecl, FnExpr, ForStmt, Ident, IfStmt,
KeyValuePatProp, LabeledStmt, LogicalExpr, LogicalOp, MethodDef, MethodKind, NullLit,
NumLit, ObjectExpr, ObjectPat, ObjectPatProp, ObjectProp, Param, Pat, Program, ProgramItem,
Prop, PropKey, PropValue, PropertyDef, ReturnStmt, SourceType, StaticBlock, Stmt,
StringLit, ThrowStmt, TryStmt, VarDecl, VarDeclarator, VarKind, WhileStmt,
};
use crate::parser::scanner::{Position, Span};
fn span() -> Span {
let p = Position {
offset: 0,
line: 1,
column: 1,
};
Span { start: p, end: p }
}
fn ident(name: &str) -> Ident {
Ident {
loc: span(),
name: name.to_owned(),
}
}
fn num_expr(v: f64) -> Expr {
Expr::Num(NumLit {
loc: span(),
value: v,
raw: v.to_string(),
})
}
fn bool_expr(v: bool) -> Expr {
Expr::Bool(BoolLit {
loc: span(),
value: v,
})
}
fn str_expr(s: &str) -> Expr {
Expr::Str(StringLit {
loc: span(),
value: s.to_owned(),
})
}
fn null_expr() -> Expr {
Expr::Null(NullLit { loc: span() })
}
fn ident_expr(name: &str) -> Expr {
Expr::Ident(ident(name))
}
fn var_decl_stmt(kind: VarKind, name: &str, init: Option<Expr>) -> Stmt {
Stmt::VarDecl(VarDecl {
loc: span(),
kind,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident(name)),
init: init.map(Box::new),
}],
})
}
fn return_stmt(arg: Option<Expr>) -> Stmt {
Stmt::Return(ReturnStmt {
loc: span(),
argument: arg.map(Box::new),
})
}
fn make_program(stmts: Vec<Stmt>) -> Program {
Program {
loc: span(),
source_type: SourceType::Script,
body: stmts.into_iter().map(ProgramItem::Stmt).collect(),
is_strict: false,
}
}
#[test]
fn test_empty_program() {
let prog = make_program(vec![]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs.last().unwrap().opcode, Opcode::Return);
}
#[test]
fn test_return_null() {
let prog = make_program(vec![return_stmt(Some(null_expr()))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaNull);
assert_eq!(instrs[1].opcode, Opcode::Return);
}
#[test]
fn test_return_true_false() {
let prog = make_program(vec![return_stmt(Some(bool_expr(true)))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaTrue);
let prog2 = make_program(vec![return_stmt(Some(bool_expr(false)))]);
let arr2 = BytecodeGenerator::compile_program(&prog2).unwrap();
let instrs2 = arr2.instructions().unwrap();
assert_eq!(instrs2[0].opcode, Opcode::LdaFalse);
}
#[test]
fn test_return_zero() {
let prog = make_program(vec![return_stmt(Some(num_expr(0.0)))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaZero);
assert_eq!(instrs[1].opcode, Opcode::Return);
}
#[test]
fn test_return_smi() {
let prog = make_program(vec![return_stmt(Some(num_expr(42.0)))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaSmi);
assert_eq!(*instrs[0].operand(0), Operand::Immediate(42));
assert_eq!(instrs[1].opcode, Opcode::Return);
}
#[test]
fn test_return_float() {
let prog = make_program(vec![return_stmt(Some(num_expr(3.14)))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaConstant);
assert_eq!(arr.get_constant(0), Some(&ConstantPoolEntry::Number(3.14)));
}
#[test]
fn test_return_string() {
let prog = make_program(vec![return_stmt(Some(str_expr("hello")))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaConstant);
assert_eq!(
arr.get_constant(0),
Some(&ConstantPoolEntry::String("hello".to_owned()))
);
}
#[test]
fn test_return_without_value() {
let prog = make_program(vec![return_stmt(None)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert_eq!(instrs[0].opcode, Opcode::LdaUndefined);
assert_eq!(instrs[1].opcode, Opcode::Return);
}
#[test]
fn test_arithmetic_add() {
let expr = Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(num_expr(1.0)),
right: Box::new(num_expr(2.0)),
}));
let prog = make_program(vec![return_stmt(Some(expr))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::Add || i.opcode == Opcode::AddSmi)
);
assert_eq!(instrs.last().unwrap().opcode, Opcode::Return);
}
#[test]
fn test_comparison_lt() {
let expr = Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Lt,
left: Box::new(num_expr(1.0)),
right: Box::new(num_expr(2.0)),
}));
let prog = make_program(vec![return_stmt(Some(expr))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::TestLessThan));
}
#[test]
fn test_var_decl_and_return() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(num_expr(5.0))),
return_stmt(Some(ident_expr("x"))),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::Star));
assert!(instrs.iter().any(|i| i.opcode == Opcode::Ldar));
assert_eq!(instrs.last().unwrap().opcode, Opcode::Return);
}
#[test]
fn test_frame_size() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "a", Some(num_expr(1.0))),
var_decl_stmt(VarKind::Let, "b", Some(num_expr(2.0))),
var_decl_stmt(VarKind::Let, "c", Some(num_expr(3.0))),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
assert!(arr.frame_size() >= 3);
}
#[test]
fn test_if_statement() {
let prog = make_program(vec![Stmt::If(IfStmt {
loc: span(),
test: Box::new(bool_expr(true)),
consequent: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![return_stmt(Some(num_expr(1.0)))],
})),
alternate: Some(Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![return_stmt(Some(num_expr(2.0)))],
}))),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::JumpIfToBooleanFalse)
);
assert!(instrs.iter().any(|i| i.opcode == Opcode::Jump));
assert!(instrs.iter().any(|i| i.opcode == Opcode::Return));
}
#[test]
fn test_while_loop() {
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget};
let init = var_decl_stmt(VarKind::Let, "i", Some(num_expr(0.0)));
let test_expr = Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Lt,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(10.0)),
}));
let incr = Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Expr(Box::new(ident_expr("i"))),
right: Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(1.0)),
}))),
}));
let prog = make_program(vec![
init,
Stmt::While(WhileStmt {
loc: span(),
test: Box::new(test_expr),
body: Box::new(Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(incr),
})),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let has_back_jump = instrs.iter().any(|i| {
i.opcode == Opcode::JumpLoop
&& i.operands()
.first()
.map(|o| matches!(o, Operand::JumpOffset(v) if *v < 0))
.unwrap_or(false)
});
assert!(has_back_jump, "while loop must have a back-edge jump");
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("bytecode must be valid");
assert!(!decoded.is_empty());
}
#[test]
fn test_for_loop_break_continue() {
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget};
let prog = make_program(vec![Stmt::For(ForStmt {
loc: span(),
init: Some(ForInit::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Let,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("i")),
init: Some(Box::new(num_expr(0.0))),
}],
})),
test: Some(Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Lt,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(3.0)),
})))),
update: Some(Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Expr(Box::new(ident_expr("i"))),
right: Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(1.0)),
}))),
})))),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![
Stmt::If(IfStmt {
loc: span(),
test: Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Eq,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(1.0)),
}))),
consequent: Box::new(Stmt::Continue(ContinueStmt {
loc: span(),
label: None,
})),
alternate: None,
}),
Stmt::If(IfStmt {
loc: span(),
test: Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Eq,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(2.0)),
}))),
consequent: Box::new(Stmt::Break(BreakStmt {
loc: span(),
label: None,
})),
alternate: None,
}),
],
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("bytecode must decode");
assert!(!decoded.is_empty());
}
#[test]
fn test_do_while_loop() {
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget};
let incr = Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Expr(Box::new(ident_expr("i"))),
right: Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(1.0)),
}))),
}));
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "i", Some(num_expr(0.0))),
Stmt::DoWhile(DoWhileStmt {
loc: span(),
body: Box::new(Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(incr),
})),
test: Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Lt,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(3.0)),
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::JumpLoop));
}
#[test]
fn test_fn_decl_creates_closure() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("a")),
right: Box::new(ident_expr("b")),
}))))],
};
let prog = make_program(vec![Stmt::FnDecl(Box::new(FnDecl {
loc: span(),
id: Some(ident("add")),
is_async: false,
is_generator: false,
params: vec![
Param {
loc: span(),
pat: Pat::Ident(ident("a")),
default: None,
},
Param {
loc: span(),
pat: Pat::Ident(ident("b")),
default: None,
},
],
body,
is_strict: false,
}))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::CreateClosure));
assert!(matches!(
arr.get_constant(0),
Some(ConstantPoolEntry::Function(_))
));
}
#[test]
fn test_nested_function_compiles() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("a")),
right: Box::new(ident_expr("b")),
}))))],
};
let inner = compile_function(
&[
Param {
loc: span(),
pat: Pat::Ident(ident("a")),
default: None,
},
Param {
loc: span(),
pat: Pat::Ident(ident("b")),
default: None,
},
],
&body,
false,
false,
false,
)
.unwrap();
assert_eq!(inner.parameter_count(), 2);
let instrs = inner.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::Add));
assert_eq!(instrs.last().unwrap().opcode, Opcode::Return);
}
#[test]
fn test_call_expr() {
use crate::parser::ast::CallExpr;
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Call(Box::new(CallExpr {
loc: span(),
callee: Box::new(ident_expr("f")),
arguments: vec![num_expr(1.0), num_expr(2.0)],
}))),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::CallAnyReceiver));
}
#[test]
fn test_throw_statement() {
use crate::parser::ast::{NewExpr, StringLit};
let prog = make_program(vec![Stmt::Throw(ThrowStmt {
loc: span(),
argument: Box::new(Expr::New(Box::new(NewExpr {
loc: span(),
callee: Box::new(ident_expr("Error")),
arguments: vec![Expr::Str(StringLit {
loc: span(),
value: "oops".to_owned(),
})],
}))),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::Construct));
assert!(instrs.iter().any(|i| i.opcode == Opcode::Throw));
}
#[test]
fn test_try_catch() {
let prog = make_program(vec![Stmt::Try(TryStmt {
loc: span(),
block: BlockStmt {
loc: span(),
body: vec![var_decl_stmt(VarKind::Let, "x", Some(num_expr(1.0)))],
},
handler: Some(CatchClause {
loc: span(),
param: Some(Pat::Ident(ident("e"))),
body: BlockStmt {
loc: span(),
body: vec![],
},
}),
finalizer: None,
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(!instrs.is_empty());
let bytes = arr.bytecodes();
decode(bytes).expect("try/catch bytecode must decode");
}
#[test]
fn test_bytecodes_round_trip() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(num_expr(42.0))),
var_decl_stmt(VarKind::Let, "y", Some(num_expr(58.0))),
return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("x")),
right: Box::new(ident_expr("y")),
})))),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("must decode");
assert!(!decoded.is_empty());
assert_eq!(decoded.last().unwrap().opcode, Opcode::Return);
}
fn slot_kinds_for(prog: &Program) -> Vec<crate::bytecode::feedback::FeedbackSlotKind> {
let arr = BytecodeGenerator::compile_program(prog).unwrap();
arr.feedback_metadata().slot_kinds().to_vec()
}
#[test]
fn test_feedback_slots_binary_add() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(num_expr(1.0))),
var_decl_stmt(VarKind::Let, "y", Some(num_expr(2.0))),
return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("x")),
right: Box::new(ident_expr("y")),
})))),
]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::BinaryOp),
"expected BinaryOp slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_comparison_lt() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(num_expr(1.0))),
var_decl_stmt(VarKind::Let, "y", Some(num_expr(2.0))),
return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Lt,
left: Box::new(ident_expr("x")),
right: Box::new(ident_expr("y")),
})))),
]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::Compare),
"expected Compare slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_global_load() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![return_stmt(Some(ident_expr("console")))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::LoadGlobal),
"expected LoadGlobal slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_global_store() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget};
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Expr(Box::new(ident_expr("x"))),
right: Box::new(num_expr(1.0)),
}))),
})]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::StoreGlobal),
"expected StoreGlobal slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_function_call() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::CallExpr;
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Call(Box::new(CallExpr {
loc: span(),
callee: Box::new(ident_expr("f")),
arguments: vec![num_expr(1.0), num_expr(2.0)],
}))),
})]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::Call),
"expected Call slot, got {kinds:?}"
);
assert!(
kinds.contains(&FeedbackSlotKind::LoadGlobal),
"expected LoadGlobal slot for callee, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_named_property_load() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::MemberExpr;
let prog = make_program(vec![return_stmt(Some(Expr::Member(Box::new(
MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: crate::parser::ast::MemberProp::Ident(ident("name")),
is_computed: false,
},
))))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::LoadProperty),
"expected LoadProperty slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_named_property_store() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget, MemberExpr};
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Expr(Box::new(Expr::Member(Box::new(MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: crate::parser::ast::MemberProp::Ident(ident("x")),
is_computed: false,
})))),
right: Box::new(num_expr(1.0)),
}))),
})]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::StoreProperty),
"expected StoreProperty slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_keyed_property_load() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::MemberExpr;
let prog = make_program(vec![return_stmt(Some(Expr::Member(Box::new(
MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: crate::parser::ast::MemberProp::Computed(Box::new(ident_expr("key"))),
is_computed: true,
},
))))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::KeyedLoadProperty),
"expected KeyedLoadProperty slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_keyed_property_store() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget, MemberExpr};
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Expr(Box::new(Expr::Member(Box::new(MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: crate::parser::ast::MemberProp::Computed(Box::new(ident_expr("key"))),
is_computed: true,
})))),
right: Box::new(num_expr(1.0)),
}))),
})]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::KeyedStoreProperty),
"expected KeyedStoreProperty slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_fn_decl() {
use crate::bytecode::feedback::FeedbackSlotKind;
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("a")),
right: Box::new(ident_expr("b")),
}))))],
};
let prog = make_program(vec![Stmt::FnDecl(Box::new(FnDecl {
loc: span(),
id: Some(ident("add")),
is_async: false,
is_generator: false,
params: vec![
Param {
loc: span(),
pat: Pat::Ident(ident("a")),
default: None,
},
Param {
loc: span(),
pat: Pat::Ident(ident("b")),
default: None,
},
],
body,
is_strict: false,
}))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::CreateClosure),
"expected CreateClosure slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_typeof() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::UnaryExpr;
let prog = make_program(vec![return_stmt(Some(Expr::Unary(Box::new(UnaryExpr {
loc: span(),
op: crate::parser::ast::UnaryOp::Typeof,
argument: Box::new(ident_expr("x")),
}))))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::TypeOf),
"expected TypeOf slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_unary_negate() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::UnaryExpr;
let prog = make_program(vec![return_stmt(Some(Expr::Unary(Box::new(UnaryExpr {
loc: span(),
op: crate::parser::ast::UnaryOp::Minus,
argument: Box::new(ident_expr("x")),
}))))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::UnaryOp),
"expected UnaryOp slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_increment() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::UpdateExpr;
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "i", Some(num_expr(0.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Update(Box::new(UpdateExpr {
loc: span(),
op: crate::parser::ast::UpdateOp::Increment,
prefix: false,
argument: Box::new(ident_expr("i")),
}))),
}),
]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::BinaryOpInc),
"expected BinaryOpInc slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_decrement() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::UpdateExpr;
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "i", Some(num_expr(5.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Update(Box::new(UpdateExpr {
loc: span(),
op: crate::parser::ast::UpdateOp::Decrement,
prefix: true,
argument: Box::new(ident_expr("i")),
}))),
}),
]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::BinaryOpInc),
"expected BinaryOpInc slot for decrement, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_array_literal() {
use crate::bytecode::feedback::FeedbackSlotKind;
use crate::parser::ast::ArrayExpr;
let prog = make_program(vec![return_stmt(Some(Expr::Array(Box::new(ArrayExpr {
loc: span(),
elements: vec![
Some(num_expr(1.0)),
Some(num_expr(2.0)),
Some(num_expr(3.0)),
],
}))))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::Literal),
"expected Literal slot for array creation, got {kinds:?}"
);
assert!(
kinds.contains(&FeedbackSlotKind::KeyedStoreProperty),
"expected KeyedStoreProperty slot for StaInArrayLiteral, got {kinds:?}"
);
}
#[test]
fn test_feedback_slots_strict_not_equal() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "a", Some(num_expr(1.0))),
var_decl_stmt(VarKind::Let, "b", Some(num_expr(2.0))),
return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::StrictNotEq,
left: Box::new(ident_expr("a")),
right: Box::new(ident_expr("b")),
})))),
]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::Compare),
"expected Compare slot for !==, got {kinds:?}"
);
}
#[test]
fn test_feedback_slot_indices_embedded_in_bytecode() {
use crate::bytecode::bytecodes::Operand;
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(num_expr(1.0))),
var_decl_stmt(VarKind::Let, "y", Some(num_expr(2.0))),
var_decl_stmt(
VarKind::Let,
"a",
Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("x")),
right: Box::new(ident_expr("y")),
}))),
),
return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Mul,
left: Box::new(ident_expr("x")),
right: Box::new(ident_expr("y")),
})))),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instructions = arr.instructions().expect("valid bytecode");
let mut slot_indices: Vec<u32> = instructions
.iter()
.flat_map(Instruction::operands)
.filter_map(|op| {
if let Operand::FeedbackSlot(idx) = op {
Some(*idx)
} else {
None
}
})
.collect();
slot_indices.sort_unstable();
slot_indices.dedup();
let slot_count = arr.feedback_metadata().slot_count();
for &idx in &slot_indices {
assert!(
idx < slot_count,
"slot index {idx} >= metadata slot_count {slot_count}"
);
}
}
#[test]
fn test_feedback_metadata_no_slots_for_constant_only_program() {
let prog = make_program(vec![return_stmt(Some(num_expr(42.0)))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
assert_eq!(arr.feedback_metadata().slot_count(), 0);
}
#[test]
fn test_feedback_metadata_instanceof() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![return_stmt(Some(Expr::Binary(Box::new(
BinaryExpr {
loc: span(),
op: BinaryOp::Instanceof,
left: Box::new(ident_expr("x")),
right: Box::new(ident_expr("Array")),
},
))))]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::InstanceOf),
"expected InstanceOf slot, got {kinds:?}"
);
}
#[test]
fn test_feedback_vector_from_compiled_array() {
use crate::bytecode::feedback::{FeedbackVector, InlineCacheState};
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(num_expr(1.0))),
return_stmt(Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("x")),
right: Box::new(num_expr(2.0)),
})))),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let metadata = arr.feedback_metadata();
let vector = FeedbackVector::new(metadata);
assert_eq!(vector.slot_count(), metadata.slot_count());
for i in 0..vector.slot_count() {
assert_eq!(vector.get_state(i), Some(InlineCacheState::Uninitialized));
}
}
fn make_generator_fn_decl(name: &str, body: Vec<Stmt>) -> FnDecl {
use crate::parser::ast::FnDecl;
FnDecl {
loc: span(),
id: Some(ident(name)),
params: vec![],
body: BlockStmt { loc: span(), body },
is_generator: true,
is_async: false,
is_strict: false,
}
}
fn yield_stmt(arg: Option<Expr>, delegate: bool) -> Stmt {
use crate::parser::ast::{ExprStmt, YieldExpr};
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Yield(Box::new(YieldExpr {
loc: span(),
delegate,
argument: arg.map(Box::new),
}))),
})
}
#[test]
fn test_generator_function_compiles() {
let decl = make_generator_fn_decl(
"gen",
vec![
yield_stmt(Some(num_expr(1.0)), false),
yield_stmt(Some(num_expr(2.0)), false),
],
);
let prog = make_program(vec![Stmt::FnDecl(Box::new(decl))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::CreateClosure));
let pool = arr.constant_pool();
assert!(!pool.is_empty());
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(
ba.is_generator(),
"nested function must be marked as generator"
);
let inner_instrs = ba.instructions().unwrap();
assert_eq!(
inner_instrs[0].opcode,
Opcode::SwitchOnGeneratorState,
"generator body must begin with SwitchOnGeneratorState"
);
assert!(
inner_instrs
.iter()
.any(|i| i.opcode == Opcode::SuspendGenerator),
"generator body must contain SuspendGenerator"
);
assert!(
inner_instrs
.iter()
.any(|i| i.opcode == Opcode::ResumeGenerator),
"generator body must contain ResumeGenerator"
);
} else {
panic!("constant pool[0] should be a Function (generator body)");
}
}
fn run(prog: &Program) -> crate::objects::value::JsValue {
use crate::interpreter::{Interpreter, InterpreterFrame};
let arr = BytecodeGenerator::compile_program(prog).unwrap();
let mut frame = InterpreterFrame::new(Rc::new(arr), vec![]);
Interpreter::run(&mut frame).unwrap()
}
#[test]
fn test_generator_call_returns_generator_value() {
use crate::objects::value::JsValue;
let decl = make_generator_fn_decl("gen", vec![yield_stmt(Some(num_expr(1.0)), false)]);
let prog = make_program(vec![
Stmt::FnDecl(Box::new(decl)),
Stmt::Return(ReturnStmt {
loc: span(),
argument: Some(Box::new(Expr::Call(Box::new(
crate::parser::ast::CallExpr {
loc: span(),
callee: Box::new(ident_expr("gen")),
arguments: vec![],
},
)))),
}),
]);
let result = run(&prog);
assert!(
matches!(result, JsValue::Generator(_)),
"calling a generator function must return JsValue::Generator, got {result:?}"
);
}
#[test]
fn test_for_of_array_sum() {
use crate::bytecode::bytecode_array::BytecodeArray;
use crate::bytecode::bytecodes::{Instruction, Operand, decode_with_byte_offsets, encode};
use crate::bytecode::feedback::FeedbackMetadata;
use crate::interpreter::{Interpreter, InterpreterFrame};
use crate::objects::value::{JsValue, NativeIterator};
let param0 = Operand::Register((-1i32) as u32);
let sum_reg = Operand::Register(0); let val_reg = Operand::Register(1); let slot0 = Operand::FeedbackSlot(0);
let instrs = vec![
Instruction::new_unchecked(Opcode::LdaZero, vec![]),
Instruction::new_unchecked(Opcode::Star, vec![sum_reg]),
Instruction::new_unchecked(Opcode::IteratorNext, vec![param0, val_reg]),
Instruction::new_unchecked(Opcode::JumpIfToBooleanTrue, vec![Operand::JumpOffset(0)]),
Instruction::new_unchecked(Opcode::Ldar, vec![val_reg]),
Instruction::new_unchecked(Opcode::Add, vec![sum_reg, slot0]),
Instruction::new_unchecked(Opcode::Star, vec![sum_reg]),
Instruction::new_unchecked(
Opcode::JumpLoop,
vec![
Operand::JumpOffset(0),
Operand::Immediate(0),
Operand::FeedbackSlot(0),
],
),
Instruction::new_unchecked(Opcode::Ldar, vec![sum_reg]),
Instruction::new_unchecked(Opcode::Return, vec![]),
];
let initial_bytes = encode(&instrs);
let (_, offsets) = decode_with_byte_offsets(&initial_bytes).unwrap();
let mut resolved = instrs.clone();
let end3 = offsets[4];
let tgt8 = offsets[8];
*resolved[3].operand_mut(0) = Operand::JumpOffset(tgt8 as i32 - end3 as i32);
let end7 = offsets[8];
let tgt2 = offsets[2];
*resolved[7].operand_mut(0) = Operand::JumpOffset(tgt2 as i32 - end7 as i32);
let bytes = encode(&resolved);
let ba = BytecodeArray::new(
bytes,
vec![],
2, 1, vec![],
FeedbackMetadata::new(vec![crate::bytecode::feedback::FeedbackSlotKind::BinaryOp]),
vec![],
);
let iter = JsValue::Iterator(NativeIterator::from_items(vec![
JsValue::Smi(1),
JsValue::Smi(2),
JsValue::Smi(3),
]));
let mut frame = InterpreterFrame::new(Rc::new(ba), vec![iter]);
let result = Interpreter::run(&mut frame).unwrap();
assert_eq!(
result,
JsValue::Smi(6),
"for-of sum over [1,2,3] should be 6"
);
}
#[test]
fn test_for_of_compiles_with_get_iterator_opcode() {
use crate::parser::ast::{ForInOfLeft, ForOfStmt, VarDecl, VarDeclarator, VarKind};
let prog = make_program(vec![Stmt::ForOf(ForOfStmt {
loc: span(),
is_await: false,
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Const,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("x")),
init: None,
}],
}),
right: Box::new(ident_expr("someArr")),
body: Box::new(Stmt::Empty(crate::parser::ast::EmptyStmt { loc: span() })),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::GetIterator),
"for-of must emit GetIterator"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::IteratorNext),
"for-of must emit IteratorNext"
);
}
#[test]
fn test_for_of_emits_iterator_close_handler() {
use crate::parser::ast::{ForInOfLeft, ForOfStmt, VarDecl, VarDeclarator, VarKind};
let prog = make_program(vec![Stmt::ForOf(ForOfStmt {
loc: span(),
is_await: false,
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Const,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("x")),
init: None,
}],
}),
right: Box::new(ident_expr("arr")),
body: Box::new(Stmt::Empty(crate::parser::ast::EmptyStmt { loc: span() })),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::IteratorClose),
"for-of must emit IteratorClose for exception handler"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::ReThrow),
"for-of exception handler must re-throw"
);
assert!(
!arr.handler_table().is_empty(),
"for-of must register an exception handler table entry"
);
}
#[test]
#[ignore] fn test_yield_star_compiles() {
use crate::parser::ast::{FnDecl, YieldExpr};
let decl = FnDecl {
loc: span(),
id: Some(ident("outer")),
params: vec![],
body: BlockStmt {
loc: span(),
body: vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Yield(Box::new(YieldExpr {
loc: span(),
delegate: true,
argument: Some(Box::new(ident_expr("inner"))),
}))),
})],
},
is_generator: true,
is_async: false,
is_strict: false,
};
let prog = make_program(vec![Stmt::FnDecl(Box::new(decl))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
assert!(
!pool.is_empty(),
"generator function should be in constant pool"
);
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_generator());
let inner_instrs = ba.instructions().unwrap();
assert!(
inner_instrs.iter().any(|i| i.opcode == Opcode::GetIterator),
"yield* must emit GetIterator"
);
assert!(
inner_instrs
.iter()
.any(|i| i.opcode == Opcode::IteratorNext),
"yield* must emit IteratorNext"
);
assert!(
inner_instrs
.iter()
.any(|i| i.opcode == Opcode::SuspendGenerator),
"yield* must emit SuspendGenerator to re-yield values"
);
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
fn test_labeled_break_non_loop() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![Stmt::Break(BreakStmt {
loc: span(),
label: Some(ident("outer")),
})],
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("bytecode must decode");
assert!(!decoded.is_empty());
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::Jump),
"labeled break must emit a Jump"
);
}
#[test]
fn test_labeled_break_while_loop() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::While(WhileStmt {
loc: span(),
test: Box::new(bool_expr(true)),
body: Box::new(Stmt::Break(BreakStmt {
loc: span(),
label: Some(ident("outer")),
})),
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("bytecode must decode");
assert!(!decoded.is_empty());
}
#[test]
fn test_labeled_continue_while_loop() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::While(WhileStmt {
loc: span(),
test: Box::new(bool_expr(true)),
body: Box::new(Stmt::Continue(ContinueStmt {
loc: span(),
label: Some(ident("outer")),
})),
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("bytecode must decode");
assert!(!decoded.is_empty());
}
#[test]
fn test_nested_labeled_loops() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::While(WhileStmt {
loc: span(),
test: Box::new(bool_expr(true)),
body: Box::new(Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("inner"),
body: Box::new(Stmt::While(WhileStmt {
loc: span(),
test: Box::new(bool_expr(true)),
body: Box::new(Stmt::Break(BreakStmt {
loc: span(),
label: Some(ident("outer")),
})),
})),
})),
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let bytes = arr.bytecodes();
let decoded = decode(bytes).expect("bytecode must decode");
assert!(!decoded.is_empty());
}
#[test]
fn test_labeled_continue_non_loop_errors() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![Stmt::Continue(ContinueStmt {
loc: span(),
label: Some(ident("outer")),
})],
})),
})]);
let result = BytecodeGenerator::compile_program(&prog);
assert!(result.is_err(), "continue on non-loop label must error");
}
#[test]
fn test_duplicate_label_errors() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("outer"),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![],
})),
})),
})]);
let result = BytecodeGenerator::compile_program(&prog);
assert!(result.is_err(), "duplicate labels must error");
}
fn object_expr(props: Vec<ObjectProp>) -> Expr {
Expr::Object(Box::new(ObjectExpr {
loc: span(),
properties: props,
}))
}
fn accessor_prop(name: &str, kind: PropValue) -> ObjectProp {
ObjectProp::Prop(Box::new(Prop {
loc: span(),
key: PropKey::Ident(ident(name)),
is_computed: false,
value: kind,
}))
}
fn empty_fn_expr(param_names: &[&str]) -> FnExpr {
FnExpr {
loc: span(),
id: None,
is_async: false,
is_generator: false,
params: param_names
.iter()
.map(|n| Param {
loc: span(),
pat: Pat::Ident(ident(n)),
default: None,
})
.collect(),
body: BlockStmt {
loc: span(),
body: vec![],
},
is_strict: false,
}
}
#[test]
fn test_object_getter_emits_define_getter_property() {
let prog = make_program(vec![var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![accessor_prop(
"x",
PropValue::Get(empty_fn_expr(&[])),
)])),
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineGetterProperty),
"expected DefineGetterProperty opcode, got {instrs:?}"
);
}
#[test]
fn test_object_setter_emits_define_setter_property() {
let prog = make_program(vec![var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![accessor_prop(
"x",
PropValue::Set(empty_fn_expr(&["v"])),
)])),
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineSetterProperty),
"expected DefineSetterProperty opcode, got {instrs:?}"
);
}
#[test]
fn test_object_computed_getter_emits_define_keyed_getter() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "k", Some(str_expr("x"))),
var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![ObjectProp::Prop(Box::new(Prop {
loc: span(),
key: PropKey::Computed(Box::new(ident_expr("k"))),
is_computed: true,
value: PropValue::Get(empty_fn_expr(&[])),
}))])),
),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineKeyedGetterProperty),
"expected DefineKeyedGetterProperty opcode, got {instrs:?}"
);
}
#[test]
fn test_object_computed_setter_emits_define_keyed_setter() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "k", Some(str_expr("x"))),
var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![ObjectProp::Prop(Box::new(Prop {
loc: span(),
key: PropKey::Computed(Box::new(ident_expr("k"))),
is_computed: true,
value: PropValue::Set(empty_fn_expr(&["v"])),
}))])),
),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineKeyedSetterProperty),
"expected DefineKeyedSetterProperty opcode, got {instrs:?}"
);
}
#[test]
fn test_feedback_slots_getter_setter() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![
accessor_prop("x", PropValue::Get(empty_fn_expr(&[]))),
accessor_prop("x", PropValue::Set(empty_fn_expr(&["v"]))),
])),
)]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::DefineAccessor),
"expected DefineAccessor slot, got {kinds:?}"
);
}
#[test]
fn test_with_emits_push_pop_context() {
use crate::parser::ast::{EmptyStmt, WithStmt};
let prog = make_program(vec![Stmt::With(WithStmt {
loc: span(),
object: Box::new(ident_expr("obj")),
body: Box::new(Stmt::Empty(EmptyStmt { loc: span() })),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let opcodes: Vec<Opcode> = instrs.iter().map(|i| i.opcode).collect();
assert!(
opcodes.contains(&Opcode::ToObject),
"expected ToObject, got {opcodes:?}"
);
assert!(
opcodes.contains(&Opcode::PushContext),
"expected PushContext, got {opcodes:?}"
);
assert!(
opcodes.contains(&Opcode::PopContext),
"expected PopContext, got {opcodes:?}"
);
}
fn make_async_generator_fn_decl(name: &str, body: Vec<Stmt>) -> FnDecl {
FnDecl {
loc: span(),
id: Some(ident(name)),
params: vec![],
body: BlockStmt { loc: span(), body },
is_generator: true,
is_async: true,
is_strict: false,
}
}
fn await_stmt(arg: Expr) -> Stmt {
use crate::parser::ast::{AwaitExpr, ExprStmt};
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Await(Box::new(AwaitExpr {
loc: span(),
argument: Box::new(arg),
}))),
})
}
#[test]
fn test_async_generator_function_compiles() {
let decl = make_async_generator_fn_decl(
"gen",
vec![
yield_stmt(Some(num_expr(1.0)), false),
yield_stmt(Some(num_expr(2.0)), false),
],
);
let prog = make_program(vec![Stmt::FnDecl(Box::new(decl))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
assert!(!pool.is_empty());
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_generator(), "must be marked as generator");
assert!(ba.is_async(), "must be marked as async");
let inner_instrs = ba.instructions().unwrap();
assert_eq!(
inner_instrs[0].opcode,
Opcode::SwitchOnGeneratorState,
"async generator body must begin with SwitchOnGeneratorState"
);
assert!(
inner_instrs
.iter()
.any(|i| i.opcode == Opcode::SuspendGenerator),
"async generator body must contain SuspendGenerator"
);
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
fn test_async_generator_with_await_compiles() {
let decl = make_async_generator_fn_decl(
"gen",
vec![
await_stmt(ident_expr("x")),
yield_stmt(Some(num_expr(1.0)), false),
],
);
let prog = make_program(vec![Stmt::FnDecl(Box::new(decl))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_async(), "must be marked as async");
let inner_instrs = ba.instructions().unwrap();
let suspend_count = inner_instrs
.iter()
.filter(|i| i.opcode == Opcode::SuspendGenerator)
.count();
assert!(
suspend_count >= 2,
"expected >= 2 SuspendGenerator (1 await + 1 yield), got {suspend_count}"
);
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
#[ignore] fn test_async_generator_expr_compiles() {
use crate::parser::ast::FnExpr;
let fn_expr = Expr::Fn(Box::new(FnExpr {
loc: span(),
id: None,
params: vec![],
body: BlockStmt {
loc: span(),
body: vec![yield_stmt(Some(num_expr(1.0)), false)],
},
is_generator: true,
is_async: true,
is_strict: false,
}));
let prog = make_program(vec![var_decl_stmt(VarKind::Var, "f", Some(fn_expr))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_generator(), "must be marked as generator");
assert!(ba.is_async(), "must be marked as async");
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
fn test_for_await_of_compiles_with_get_async_iterator() {
use crate::parser::ast::{ForInOfLeft, ForOfStmt, VarDecl, VarDeclarator, VarKind};
let prog = make_program(vec![Stmt::ForOf(ForOfStmt {
loc: span(),
is_await: true,
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Const,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("x")),
init: None,
}],
}),
right: Box::new(ident_expr("someArr")),
body: Box::new(Stmt::Empty(crate::parser::ast::EmptyStmt { loc: span() })),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::GetAsyncIterator),
"for-await-of must emit GetAsyncIterator, got {instrs:?}"
);
}
fn class_decl_stmt(name: &str, super_class: Option<Expr>, body: Vec<ClassMember>) -> Stmt {
Stmt::ClassDecl(Box::new(ClassDecl {
loc: span(),
id: Some(ident(name)),
super_class: super_class.map(Box::new),
body: ClassBody { loc: span(), body },
}))
}
fn method_member(
name: &str,
params: &[&str],
is_static: bool,
kind: MethodKind,
) -> ClassMember {
ClassMember::Method(MethodDef {
loc: span(),
is_static,
kind,
key: PropKey::Ident(ident(name)),
is_computed: false,
value: empty_fn_expr(params),
})
}
fn field_member(name: &str, value: Option<Expr>, is_static: bool) -> ClassMember {
ClassMember::Property(PropertyDef {
loc: span(),
is_static,
key: PropKey::Ident(ident(name)),
is_computed: false,
value: value.map(Box::new),
})
}
fn static_block_member(body: Vec<Stmt>) -> ClassMember {
ClassMember::StaticBlock(StaticBlock { loc: span(), body })
}
#[test]
fn test_class_decl_emits_create_class() {
let prog = make_program(vec![class_decl_stmt("Foo", None, vec![])]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::CreateClass),
"expected CreateClass opcode, got {instrs:?}"
);
}
#[test]
fn test_async_generator_call_returns_generator_value() {
use crate::objects::value::JsValue;
let decl =
make_async_generator_fn_decl("gen", vec![yield_stmt(Some(num_expr(1.0)), false)]);
let prog = make_program(vec![
Stmt::FnDecl(Box::new(decl)),
Stmt::Return(ReturnStmt {
loc: span(),
argument: Some(Box::new(Expr::Call(Box::new(
crate::parser::ast::CallExpr {
loc: span(),
callee: Box::new(ident_expr("gen")),
arguments: vec![],
},
)))),
}),
]);
let result = run(&prog);
assert!(
matches!(result, JsValue::Generator(_)),
"calling an async generator must return JsValue::Generator, got {result:?}"
);
}
#[test]
fn test_class_with_constructor() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![method_member(
"constructor",
&[],
false,
MethodKind::Constructor,
)],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::CreateClass));
}
#[test]
#[ignore] fn test_class_method_emits_define_named_own() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![method_member("bar", &[], false, MethodKind::Method)],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineNamedOwnProperty),
"expected DefineNamedOwnProperty for method"
);
}
#[test]
#[ignore] fn test_class_static_method() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![method_member("bar", &[], true, MethodKind::Method)],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineNamedOwnProperty)
);
}
#[test]
#[ignore] fn test_class_getter_setter() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![
method_member("x", &[], false, MethodKind::Get),
method_member("x", &["v"], false, MethodKind::Set),
],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineGetterProperty)
);
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineSetterProperty)
);
}
#[test]
fn test_class_extends() {
let prog = make_program(vec![
class_decl_stmt("Foo", None, vec![]),
class_decl_stmt("Bar", Some(ident_expr("Foo")), vec![]),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let count = instrs
.iter()
.filter(|i| i.opcode == Opcode::CreateClass)
.count();
assert_eq!(count, 2, "expected two CreateClass opcodes");
}
#[test]
fn test_class_static_field() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![field_member("count", Some(num_expr(0.0)), true)],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineNamedOwnProperty)
);
}
#[test]
fn test_class_instance_field_emits_initializer() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![field_member("x", Some(num_expr(1.0)), false)],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::CreateClosure),
"expected CreateClosure for field initializer"
);
}
#[test]
#[ignore] fn test_class_expression() {
let prog = make_program(vec![var_decl_stmt(
VarKind::Var,
"C",
Some(Expr::Class(Box::new(ClassExpr {
loc: span(),
id: None,
super_class: None,
body: ClassBody {
loc: span(),
body: vec![],
},
}))),
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::CreateClass),
"expected CreateClass for class expression"
);
}
#[test]
fn test_class_static_block() {
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![static_block_member(vec![])],
)]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(instrs.iter().any(|i| i.opcode == Opcode::CreateClass));
}
#[test]
#[ignore] fn test_class_computed_method() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "k", Some(str_expr("bar"))),
class_decl_stmt(
"Foo",
None,
vec![ClassMember::Method(MethodDef {
loc: span(),
is_static: false,
kind: MethodKind::Method,
key: PropKey::Computed(Box::new(ident_expr("k"))),
is_computed: true,
value: empty_fn_expr(&[]),
})],
),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineKeyedOwnProperty),
"expected DefineKeyedOwnProperty for computed method"
);
}
#[test]
fn test_feedback_slots_class_method() {
use crate::bytecode::feedback::FeedbackSlotKind;
let prog = make_program(vec![class_decl_stmt(
"Foo",
None,
vec![method_member("bar", &[], false, MethodKind::Method)],
)]);
let kinds = slot_kinds_for(&prog);
assert!(
kinds.contains(&FeedbackSlotKind::CreateClosure),
"expected CreateClosure slot for class, got {kinds:?}"
);
assert!(
kinds.contains(&FeedbackSlotKind::StoreProperty),
"expected StoreProperty slot for method, got {kinds:?}"
);
}
#[test]
fn test_async_function_decl_compiles() {
let decl = FnDecl {
loc: span(),
id: Some(ident("f")),
params: vec![],
body: BlockStmt {
loc: span(),
body: vec![
await_stmt(num_expr(1.0)),
Stmt::Return(ReturnStmt {
loc: span(),
argument: Some(Box::new(num_expr(2.0))),
}),
],
},
is_generator: false,
is_async: true,
is_strict: false,
};
let prog = make_program(vec![Stmt::FnDecl(Box::new(decl))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
assert!(!pool.is_empty());
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_async(), "must be marked as async");
assert!(!ba.is_generator(), "must NOT be marked as generator");
let inner_instrs = ba.instructions().unwrap();
assert_eq!(
inner_instrs[0].opcode,
Opcode::SwitchOnGeneratorState,
"async function body must begin with SwitchOnGeneratorState"
);
assert!(
inner_instrs
.iter()
.any(|i| i.opcode == Opcode::SuspendGenerator),
"async function body must contain SuspendGenerator for await"
);
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
#[ignore] fn test_async_function_expr_compiles() {
use crate::parser::ast::FnExpr;
let fn_expr = Expr::Fn(Box::new(FnExpr {
loc: span(),
id: None,
params: vec![],
body: BlockStmt {
loc: span(),
body: vec![await_stmt(num_expr(1.0))],
},
is_generator: false,
is_async: true,
is_strict: false,
}));
let prog = make_program(vec![var_decl_stmt(VarKind::Var, "f", Some(fn_expr))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_async(), "must be marked as async");
assert!(!ba.is_generator(), "must NOT be marked as generator");
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
#[ignore] fn test_async_arrow_compiles() {
use crate::parser::ast::ArrowExpr;
let arrow = Expr::Arrow(Box::new(ArrowExpr {
loc: span(),
params: vec![],
body: ArrowBody::Block(BlockStmt {
loc: span(),
body: vec![await_stmt(num_expr(1.0))],
}),
is_async: true,
is_strict: false,
}));
let prog = make_program(vec![var_decl_stmt(VarKind::Var, "f", Some(arrow))]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let pool = arr.constant_pool();
if let crate::bytecode::bytecode_array::ConstantPoolEntry::Function(ba) = &pool[0] {
assert!(ba.is_async(), "must be marked as async");
} else {
panic!("constant pool[0] should be a Function");
}
}
#[test]
fn test_optional_catch_binding() {
let prog = make_program(vec![Stmt::Try(TryStmt {
loc: span(),
block: BlockStmt {
loc: span(),
body: vec![var_decl_stmt(VarKind::Let, "x", Some(num_expr(1.0)))],
},
handler: Some(CatchClause {
loc: span(),
param: None,
body: BlockStmt {
loc: span(),
body: vec![],
},
}),
finalizer: None,
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(!instrs.is_empty());
let bytes = arr.bytecodes();
decode(bytes).expect("optional catch binding bytecode must decode");
}
#[test]
fn test_nullish_coalesce_bytecode() {
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Logical(Box::new(LogicalExpr {
loc: span(),
op: LogicalOp::NullishCoalesce,
left: Box::new(ident_expr("a")),
right: Box::new(ident_expr("b")),
}))),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(!instrs.is_empty());
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
assert!(
opcodes.contains(&Opcode::JumpIfUndefinedOrNull),
"expected JumpIfUndefinedOrNull, got {opcodes:?}"
);
}
#[test]
fn test_logical_and_assign_bytecode() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(bool_expr(true))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::LogicalAndAssign,
left: AssignTarget::Expr(Box::new(ident_expr("x"))),
right: Box::new(num_expr(1.0)),
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
assert!(
opcodes.contains(&Opcode::JumpIfToBooleanFalse),
"expected JumpIfToBooleanFalse for &&=, got {opcodes:?}"
);
}
#[test]
fn test_logical_or_assign_bytecode() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(bool_expr(false))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::LogicalOrAssign,
left: AssignTarget::Expr(Box::new(ident_expr("x"))),
right: Box::new(num_expr(1.0)),
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
assert!(
opcodes.contains(&Opcode::JumpIfToBooleanTrue),
"expected JumpIfToBooleanTrue for ||=, got {opcodes:?}"
);
}
#[test]
fn test_nullish_assign_bytecode() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "x", Some(null_expr())),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::NullishAssign,
left: AssignTarget::Expr(Box::new(ident_expr("x"))),
right: Box::new(num_expr(1.0)),
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
assert!(
opcodes.contains(&Opcode::JumpIfUndefinedOrNull),
"expected JumpIfUndefinedOrNull for ??=, got {opcodes:?}"
);
}
#[test]
fn test_default_param_emits_jump_if_undefined() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("a")))],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Ident(ident("a")),
default: Some(num_expr(1.0)),
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::JumpIfUndefined),
"default param must emit JumpIfUndefined"
);
assert_eq!(inner.parameter_count(), 1);
}
#[test]
fn test_object_destructuring_param() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("a")))],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Object(Box::new(ObjectPat {
loc: span(),
properties: vec![
ObjectPatProp::Assign(AssignPatProp {
loc: span(),
key: ident("a"),
value: None,
}),
ObjectPatProp::Assign(AssignPatProp {
loc: span(),
key: ident("b"),
value: None,
}),
],
})),
default: None,
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs
.iter()
.filter(|i| i.opcode == Opcode::LdaNamedProperty)
.count()
>= 2,
"object destructuring param must emit LdaNamedProperty for each property"
);
}
#[test]
fn test_array_destructuring_param() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("x")))],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Array(Box::new(ArrayPat {
loc: span(),
elements: vec![Some(Pat::Ident(ident("x"))), Some(Pat::Ident(ident("y")))],
})),
default: None,
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::GetIterator),
"array destructuring param must emit GetIterator"
);
assert!(
instrs
.iter()
.filter(|i| i.opcode == Opcode::IteratorNext)
.count()
>= 2,
"array destructuring param must emit IteratorNext for each element"
);
}
#[test]
fn test_object_destructuring_with_rename() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("x")))],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Object(Box::new(ObjectPat {
loc: span(),
properties: vec![ObjectPatProp::KeyValue(KeyValuePatProp {
loc: span(),
key: PropKey::Ident(ident("a")),
is_computed: false,
value: Pat::Ident(ident("x")),
})],
})),
default: None,
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaNamedProperty),
"renamed object destructuring must emit LdaNamedProperty"
);
}
#[test]
fn test_default_param_with_later_param_ref() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("b")))],
};
let inner = compile_function(
&[
Param {
loc: span(),
pat: Pat::Ident(ident("a")),
default: Some(num_expr(1.0)),
},
Param {
loc: span(),
pat: Pat::Ident(ident("b")),
default: Some(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Add,
left: Box::new(ident_expr("a")),
right: Box::new(num_expr(1.0)),
}))),
},
],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert_eq!(
instrs
.iter()
.filter(|i| i.opcode == Opcode::JumpIfUndefined)
.count(),
2,
"each default param must emit its own JumpIfUndefined"
);
}
#[test]
fn test_object_destructuring_param_with_default() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("b")))],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Object(Box::new(ObjectPat {
loc: span(),
properties: vec![
ObjectPatProp::Assign(AssignPatProp {
loc: span(),
key: ident("a"),
value: None,
}),
ObjectPatProp::Assign(AssignPatProp {
loc: span(),
key: ident("b"),
value: Some(Box::new(num_expr(42.0))),
}),
],
})),
default: None,
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::JumpIfNotUndefined),
"object destructuring with default must emit JumpIfNotUndefined"
);
}
#[test]
fn test_array_destructuring_with_elision() {
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(ident_expr("x")))],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Array(Box::new(ArrayPat {
loc: span(),
elements: vec![None, Some(Pat::Ident(ident("x")))],
})),
default: None,
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert_eq!(
instrs
.iter()
.filter(|i| i.opcode == Opcode::IteratorNext)
.count(),
2,
"elision must still advance the iterator"
);
}
#[test]
fn test_dynamic_import_emits_call_runtime() {
use crate::parser::ast::ImportExpr;
let import_expr = Expr::Import(Box::new(ImportExpr {
loc: span(),
source: Box::new(str_expr("./mod.js")),
}));
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(import_expr),
})]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::CallRuntime),
"expected CallRuntime opcode for dynamic import"
);
}
fn module_program(items: Vec<ProgramItem>) -> Program {
Program {
loc: span(),
source_type: SourceType::Module,
body: items,
is_strict: true,
}
}
fn string_lit(s: &str) -> StringLit {
StringLit {
loc: span(),
value: s.to_owned(),
}
}
#[test]
fn test_import_named_emits_lda_module_variable() {
use crate::parser::ast::{
ImportDecl, ImportNamedSpecifier, ImportSpecifier, ModuleDecl, ModuleExportName,
};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::Import(
ImportDecl {
loc: span(),
specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
loc: span(),
imported: ModuleExportName::Ident(ident("foo")),
local: ident("foo"),
})],
source: string_lit("./mod.js"),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
assert!(ba.is_module());
assert!(ba.is_async());
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaModuleVariable),
"import should emit LdaModuleVariable, got {instrs:?}"
);
}
#[test]
fn test_module_top_level_await_compiles() {
use crate::parser::ast::{
AwaitExpr, ExprStmt, ImportDecl, ImportDefaultSpecifier, ImportSpecifier, ModuleDecl,
};
let await_stmt = ProgramItem::Stmt(Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Await(Box::new(AwaitExpr {
loc: span(),
argument: Box::new(num_expr(42.0)),
}))),
}));
let import = ProgramItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
loc: span(),
specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier {
loc: span(),
local: ident("m"),
})],
source: string_lit("./mod.js"),
attributes: vec![],
}));
let prog = module_program(vec![import, await_stmt]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
assert!(ba.is_module());
assert!(ba.is_async());
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::SuspendGenerator),
"top-level await should emit SuspendGenerator, got {instrs:?}"
);
}
#[test]
fn test_import_default_emits_lda_module_variable() {
use crate::parser::ast::{ImportDecl, ImportDefaultSpecifier, ImportSpecifier, ModuleDecl};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::Import(
ImportDecl {
loc: span(),
specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier {
loc: span(),
local: ident("def"),
})],
source: string_lit("./mod.js"),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaModuleVariable),
"default import should emit LdaModuleVariable"
);
}
#[test]
fn test_dynamic_import_emits_single_argument_runtime_call() {
use crate::parser::ast::ImportExpr;
let import_expr = Expr::Import(Box::new(ImportExpr {
loc: span(),
source: Box::new(str_expr("./mod.json")),
}));
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(import_expr),
})]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
let rt_call = instrs
.iter()
.find(|i| i.opcode == Opcode::CallRuntime)
.expect("expected CallRuntime");
assert_eq!(
*rt_call.operand(2),
Operand::RegisterCount(1),
"dynamic import should pass exactly one argument"
);
}
#[test]
fn test_import_namespace_emits_get_module_namespace() {
use crate::parser::ast::{
ImportDecl, ImportNamespaceSpecifier, ImportSpecifier, ModuleDecl,
};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::Import(
ImportDecl {
loc: span(),
specifiers: vec![ImportSpecifier::Namespace(ImportNamespaceSpecifier {
loc: span(),
local: ident("ns"),
})],
source: string_lit("./mod.js"),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::GetModuleNamespace),
"namespace import should emit GetModuleNamespace"
);
}
#[test]
fn test_export_named_decl_emits_sta_module_variable() {
use crate::parser::ast::{ExportNamedDecl, ModuleDecl};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::ExportNamed(
ExportNamedDecl {
loc: span(),
specifiers: vec![],
source: None,
declaration: Some(Box::new(var_decl_stmt(
VarKind::Let,
"x",
Some(num_expr(42.0)),
))),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::StaModuleVariable),
"export let should emit StaModuleVariable"
);
}
#[test]
fn test_export_default_expr_emits_sta_module_variable() {
use crate::parser::ast::{ExportDefaultDecl, ExportDefaultExpr, ModuleDecl};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::ExportDefault(
ExportDefaultDecl {
loc: span(),
declaration: ExportDefaultExpr::Expr(Box::new(num_expr(99.0))),
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::StaModuleVariable),
"export default should emit StaModuleVariable"
);
}
#[test]
fn test_export_all_emits_get_module_namespace() {
use crate::parser::ast::{ExportAllDecl, ModuleDecl};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::ExportAll(
ExportAllDecl {
loc: span(),
exported: None,
source: string_lit("./other.js"),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::GetModuleNamespace),
"export * should emit GetModuleNamespace"
);
}
#[test]
fn test_export_all_as_name_emits_sta_module_variable() {
use crate::parser::ast::{ExportAllDecl, ModuleDecl, ModuleExportName};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::ExportAll(
ExportAllDecl {
loc: span(),
exported: Some(ModuleExportName::Ident(ident("ns"))),
source: string_lit("./other.js"),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::GetModuleNamespace),
"export * as ns should emit GetModuleNamespace"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::StaModuleVariable),
"export * as ns should emit StaModuleVariable"
);
}
#[test]
fn test_re_export_named_from_source() {
use crate::parser::ast::{ExportNamedDecl, ExportSpecifier, ModuleDecl, ModuleExportName};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::ExportNamed(
ExportNamedDecl {
loc: span(),
specifiers: vec![ExportSpecifier {
loc: span(),
local: ModuleExportName::Ident(ident("x")),
exported: ModuleExportName::Ident(ident("y")),
}],
source: Some(string_lit("./mod.js")),
declaration: None,
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaModuleVariable),
"re-export should load from source via LdaModuleVariable"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::StaModuleVariable),
"re-export should store via StaModuleVariable"
);
}
#[test]
fn test_import_meta_emits_lda_import_meta() {
use crate::parser::ast::MetaPropExpr;
let prog = module_program(vec![ProgramItem::Stmt(Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::MetaProp(MetaPropExpr {
loc: span(),
meta: ident("import"),
property: ident("meta"),
})),
}))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaImportMeta),
"import.meta should emit LdaImportMeta"
);
}
#[test]
fn test_module_decl_in_script_errors() {
use crate::parser::ast::{ImportDecl, ModuleDecl};
let prog = Program {
loc: span(),
source_type: SourceType::Script,
body: vec![ProgramItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
loc: span(),
specifiers: vec![],
source: string_lit("./mod.js"),
attributes: vec![],
}))],
is_strict: false,
};
let result = BytecodeGenerator::compile_program(&prog);
assert!(result.is_err(), "module decl in script should error");
}
#[test]
fn test_live_binding_export_let_stores_module_variable() {
use crate::parser::ast::{ExportNamedDecl, ModuleDecl};
let prog = module_program(vec![ProgramItem::ModuleDecl(ModuleDecl::ExportNamed(
ExportNamedDecl {
loc: span(),
specifiers: vec![],
source: None,
declaration: Some(Box::new(var_decl_stmt(
VarKind::Let,
"counter",
Some(num_expr(0.0)),
))),
attributes: vec![],
},
))]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = decode(&ba.bytecodes()).unwrap();
let sta_count = instrs
.iter()
.filter(|i| i.opcode == Opcode::StaModuleVariable)
.count();
assert!(
sta_count >= 1,
"export let must emit at least one StaModuleVariable for live binding"
);
}
#[test]
fn test_array_destructuring_assign() {
let result =
crate::builtins::global::global_eval("var a, b; [a, b] = [10, 20]; a + b").unwrap();
assert_eq!(result, crate::objects::value::JsValue::Smi(30));
}
#[test]
fn test_object_destructuring_assign() {
let result =
crate::builtins::global::global_eval("var x, y; ({x, y} = {x: 10, y: 20}); x + y")
.unwrap();
assert_eq!(result, crate::objects::value::JsValue::Smi(30));
}
#[test]
fn test_new_target_emits_lda_new_target() {
use crate::parser::ast::MetaPropExpr;
let body = BlockStmt {
loc: span(),
body: vec![return_stmt(Some(Expr::MetaProp(MetaPropExpr {
loc: span(),
meta: ident("new"),
property: ident("target"),
})))],
};
let inner = compile_function(&[], &body, false, false, false).unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaNewTarget),
"new.target must emit LdaNewTarget, got {instrs:?}"
);
}
#[test]
fn test_new_target_in_program_compiles() {
use crate::parser::ast::MetaPropExpr;
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::MetaProp(MetaPropExpr {
loc: span(),
meta: ident("new"),
property: ident("target"),
})),
})]);
let ba = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = ba.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaNewTarget),
"program-level new.target must emit LdaNewTarget"
);
}
#[test]
fn test_labeled_continue_for_loop() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("L"),
body: Box::new(Stmt::For(ForStmt {
loc: span(),
init: Some(ForInit::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Var,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("i")),
init: Some(Box::new(num_expr(0.0))),
}],
})),
test: Some(Box::new(Expr::Binary(Box::new(BinaryExpr {
loc: span(),
op: BinaryOp::Lt,
left: Box::new(ident_expr("i")),
right: Box::new(num_expr(10.0)),
})))),
update: Some(Box::new(Expr::Update(Box::new(
crate::parser::ast::UpdateExpr {
loc: span(),
op: UpdateOp::Increment,
prefix: false,
argument: Box::new(ident_expr("i")),
},
)))),
body: Box::new(Stmt::Continue(ContinueStmt {
loc: span(),
label: Some(ident("L")),
})),
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::Jump),
"labeled continue L in for loop must emit a Jump"
);
}
#[test]
fn test_labeled_continue_do_while_loop() {
let prog = make_program(vec![Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("L"),
body: Box::new(Stmt::DoWhile(DoWhileStmt {
loc: span(),
test: Box::new(bool_expr(true)),
body: Box::new(Stmt::Continue(ContinueStmt {
loc: span(),
label: Some(ident("L")),
})),
})),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::Jump),
"labeled continue L in do-while must emit a Jump"
);
}
#[test]
fn test_labeled_continue_for_in_loop() {
use crate::parser::ast::{ForInOfLeft, ForInStmt};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "obj", Some(num_expr(0.0))),
Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("L"),
body: Box::new(Stmt::ForIn(ForInStmt {
loc: span(),
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Var,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("x")),
init: None,
}],
}),
right: Box::new(ident_expr("obj")),
body: Box::new(Stmt::Continue(ContinueStmt {
loc: span(),
label: Some(ident("L")),
})),
})),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::Jump),
"labeled continue L in for-in must emit a Jump"
);
}
#[test]
fn test_for_in_var_hoists_to_function_scope() {
use crate::parser::ast::{ForInOfLeft, ForInStmt};
let body = BlockStmt {
loc: span(),
body: vec![
Stmt::ForIn(ForInStmt {
loc: span(),
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Var,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("x")),
init: None,
}],
}),
right: Box::new(ident_expr("o")),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![],
})),
}),
return_stmt(Some(ident_expr("x"))),
],
};
let inner = compile_function(
&[Param {
loc: span(),
pat: Pat::Ident(ident("o")),
default: None,
}],
&body,
false,
false,
false,
)
.unwrap();
let instrs = inner.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::Return),
"function must compile successfully with var x visible after for-in"
);
}
#[test]
fn test_for_in_var_visible_in_nested_scope() {
use crate::parser::ast::{ForInOfLeft, ForInStmt};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "obj", Some(num_expr(0.0))),
Stmt::ForIn(ForInStmt {
loc: span(),
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Var,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("k")),
init: None,
}],
}),
right: Box::new(ident_expr("obj")),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![],
})),
}),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(ident_expr("k")),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
!instrs.is_empty(),
"for-in var k should be accessible after the loop"
);
}
#[test]
fn test_object_computed_method_emits_define_keyed() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "k", Some(str_expr("foo"))),
var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![ObjectProp::Prop(Box::new(Prop {
loc: span(),
key: PropKey::Computed(Box::new(ident_expr("k"))),
is_computed: true,
value: PropValue::Method(empty_fn_expr(&[])),
}))])),
),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineKeyedOwnProperty),
"computed method must emit DefineKeyedOwnProperty, got {instrs:?}"
);
}
#[test]
fn test_object_computed_value_emits_define_keyed() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "k", Some(str_expr("bar"))),
var_decl_stmt(
VarKind::Var,
"o",
Some(object_expr(vec![ObjectProp::Prop(Box::new(Prop {
loc: span(),
key: PropKey::Computed(Box::new(ident_expr("k"))),
is_computed: true,
value: PropValue::Value(Box::new(num_expr(42.0))),
}))])),
),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineKeyedOwnProperty),
"computed value property must emit DefineKeyedOwnProperty, got {instrs:?}"
);
}
#[test]
fn test_template_with_expressions_emits_add() {
use crate::parser::ast::{TemplateElement, TemplateLit};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "x", Some(num_expr(1.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Template(Box::new(TemplateLit {
loc: span(),
quasis: vec![
TemplateElement {
loc: span(),
raw: "a".to_owned(),
cooked: Some("a".to_owned()),
tail: false,
},
TemplateElement {
loc: span(),
raw: "b".to_owned(),
cooked: Some("b".to_owned()),
tail: true,
},
],
expressions: vec![ident_expr("x")],
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
let add_count = instrs.iter().filter(|i| i.opcode == Opcode::Add).count();
assert!(
add_count >= 2,
"template with expression needs at least 2 Add opcodes (expr+quasi), got {add_count}"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::ToString),
"template must emit ToString for interpolated expression"
);
}
#[test]
fn test_tagged_template_emits_get_template_object() {
use crate::parser::ast::{TaggedTemplateExpr, TemplateElement, TemplateLit};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "tag", Some(num_expr(0.0))),
var_decl_stmt(VarKind::Var, "expr", Some(num_expr(1.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::TaggedTemplate(Box::new(TaggedTemplateExpr {
loc: span(),
tag: Box::new(ident_expr("tag")),
quasi: TemplateLit {
loc: span(),
quasis: vec![
TemplateElement {
loc: span(),
raw: "str".to_owned(),
cooked: Some("str".to_owned()),
tail: false,
},
TemplateElement {
loc: span(),
raw: "str".to_owned(),
cooked: Some("str".to_owned()),
tail: true,
},
],
expressions: vec![ident_expr("expr")],
},
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::GetTemplateObject),
"tagged template must emit GetTemplateObject, got {instrs:?}"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::CallAnyReceiver),
"tagged template must emit a call instruction, got {instrs:?}"
);
}
#[test]
fn test_tagged_template_method_uses_call_property() {
use crate::parser::ast::{
MemberExpr, MemberProp, TaggedTemplateExpr, TemplateElement, TemplateLit,
};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "obj", Some(num_expr(0.0))),
var_decl_stmt(VarKind::Var, "x", Some(num_expr(1.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::TaggedTemplate(Box::new(TaggedTemplateExpr {
loc: span(),
tag: Box::new(Expr::Member(Box::new(MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: MemberProp::Ident(ident("tag")),
is_computed: false,
}))),
quasi: TemplateLit {
loc: span(),
quasis: vec![
TemplateElement {
loc: span(),
raw: "a".to_owned(),
cooked: Some("a".to_owned()),
tail: false,
},
TemplateElement {
loc: span(),
raw: "b".to_owned(),
cooked: Some("b".to_owned()),
tail: true,
},
],
expressions: vec![ident_expr("x")],
},
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::GetTemplateObject),
"tagged template method must emit GetTemplateObject"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::CallProperty),
"tagged template on method must use CallProperty for correct `this`"
);
}
#[test]
fn test_member_expr_in_object_destructuring_assignment() {
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget, MemberExpr};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "obj", Some(num_expr(0.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Pat(Pat::Object(Box::new(ObjectPat {
loc: span(),
properties: vec![ObjectPatProp::KeyValue(KeyValuePatProp {
loc: span(),
key: PropKey::Ident(ident("a")),
is_computed: false,
value: Pat::Expr(Box::new(Expr::Member(Box::new(MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: crate::parser::ast::MemberProp::Ident(ident("x")),
is_computed: false,
})))),
})],
}))),
right: Box::new(num_expr(42.0)),
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::LdaNamedProperty),
"destructuring must load property from source, got {instrs:?}"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::StaNamedProperty),
"member target must emit StaNamedProperty, got {instrs:?}"
);
}
#[test]
fn test_member_expr_in_array_destructuring_assignment() {
use crate::parser::ast::{AssignExpr, AssignOp, AssignTarget, MemberExpr};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "obj", Some(num_expr(0.0))),
Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Assign(Box::new(AssignExpr {
loc: span(),
op: AssignOp::Assign,
left: AssignTarget::Pat(Pat::Array(Box::new(crate::parser::ast::ArrayPat {
loc: span(),
elements: vec![Some(Pat::Expr(Box::new(Expr::Member(Box::new(
MemberExpr {
loc: span(),
object: Box::new(ident_expr("obj")),
property: crate::parser::ast::MemberProp::Ident(ident("x")),
is_computed: false,
},
)))))],
}))),
right: Box::new(num_expr(1.0)),
}))),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::GetIterator),
"array destructuring must use iterator, got {instrs:?}"
);
assert!(
instrs.iter().any(|i| i.opcode == Opcode::StaNamedProperty),
"member target must emit StaNamedProperty, got {instrs:?}"
);
}
#[test]
fn test_for_of_pat_uses_assign_mode() {
use crate::parser::ast::{ForInOfLeft, ForOfStmt};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "arr", Some(num_expr(0.0))),
var_decl_stmt(VarKind::Var, "a", Some(num_expr(0.0))),
Stmt::ForOf(ForOfStmt {
loc: span(),
is_await: false,
left: ForInOfLeft::Pat(Pat::Object(Box::new(ObjectPat {
loc: span(),
properties: vec![ObjectPatProp::Assign(AssignPatProp {
loc: span(),
key: ident("a"),
value: None,
})],
}))),
right: Box::new(ident_expr("arr")),
body: Box::new(Stmt::Block(BlockStmt {
loc: span(),
body: vec![],
})),
}),
]);
let _arr = BytecodeGenerator::compile_program(&prog).unwrap();
}
fn eval_to_value(source: &str) -> crate::objects::value::JsValue {
crate::builtins::global::global_eval(source).unwrap()
}
#[test]
fn test_comma_operator() {
let result = eval_to_value("var x = (1, 2, 3); x");
assert_eq!(result, crate::objects::value::JsValue::Smi(3));
}
#[test]
fn test_comma_operator_side_effects() {
let result = eval_to_value("var a = 0; var x = (a = 10, a + 5); x");
assert_eq!(result, crate::objects::value::JsValue::Smi(15));
}
#[test]
fn test_comma_operator_bytecode() {
use crate::parser::ast::SequenceExpr;
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Sequence(Box::new(SequenceExpr {
loc: span(),
expressions: vec![num_expr(1.0), num_expr(2.0), num_expr(3.0)],
}))),
})]);
let _arr = BytecodeGenerator::compile_program(&prog).unwrap();
}
#[test]
fn test_conditional_truthy() {
let result = eval_to_value("var x = true ? 42 : 99; x");
assert_eq!(result, crate::objects::value::JsValue::Smi(42));
}
#[test]
fn test_conditional_falsy() {
let result = eval_to_value("var x = false ? 42 : 99; x");
assert_eq!(result, crate::objects::value::JsValue::Smi(99));
}
#[test]
fn test_conditional_complex_expressions() {
let result = eval_to_value("var a = 5; var x = (a > 3) ? a * 2 : a + 1; x");
assert_eq!(result, crate::objects::value::JsValue::Smi(10));
}
#[test]
fn test_conditional_nested() {
let result = eval_to_value("var x = true ? (false ? 1 : 2) : 3; x");
assert_eq!(result, crate::objects::value::JsValue::Smi(2));
}
#[test]
fn test_conditional_bytecode() {
use crate::parser::ast::ConditionalExpr;
let prog = make_program(vec![Stmt::Expr(ExprStmt {
loc: span(),
expr: Box::new(Expr::Conditional(Box::new(ConditionalExpr {
loc: span(),
test: Box::new(bool_expr(true)),
consequent: Box::new(num_expr(1.0)),
alternate: Box::new(num_expr(2.0)),
}))),
})]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::JumpIfToBooleanFalse),
"conditional must emit JumpIfToBooleanFalse, got {instrs:?}"
);
}
#[test]
fn test_logical_and_assign() {
let result = eval_to_value("var a = 1; a &&= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(42));
}
#[test]
fn test_logical_and_assign_short_circuit() {
let result = eval_to_value("var a = 0; a &&= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(0));
}
#[test]
fn test_logical_or_assign() {
let result = eval_to_value("var a = 0; a ||= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(42));
}
#[test]
fn test_logical_or_assign_short_circuit() {
let result = eval_to_value("var a = 1; a ||= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(1));
}
#[test]
fn test_nullish_assign_null() {
let result = eval_to_value("var a = null; a ??= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(42));
}
#[test]
fn test_nullish_assign_undefined() {
let result = eval_to_value("var a = undefined; a ??= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(42));
}
#[test]
fn test_nullish_assign_non_nullish() {
let result = eval_to_value("var a = 0; a ??= 42; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(0));
}
#[test]
fn test_tagged_template_basic() {
let result = eval_to_value(
"function tag(strings, val) { return val; } var x = tag`hello ${42} world`; x",
);
assert_eq!(result, crate::objects::value::JsValue::Smi(42));
}
#[test]
fn test_tagged_template_strings_array() {
let result = eval_to_value(
"function tag(strings) { return strings.length; } var x = tag`a${1}b${2}c`; x",
);
assert_eq!(result, crate::objects::value::JsValue::Smi(3));
}
#[test]
fn test_object_destructuring_default_used() {
let result = eval_to_value("var {a, b = 2} = {a: 10}; b");
assert_eq!(result, crate::objects::value::JsValue::Smi(2));
}
#[test]
fn test_object_destructuring_default_overridden() {
let result = eval_to_value("var {a = 1, b = 2} = {a: 10}; a");
assert_eq!(result, crate::objects::value::JsValue::Smi(10));
}
#[test]
fn test_array_destructuring_default() {
let result = eval_to_value("var [a = 5, b = 10] = [1]; a + b");
assert_eq!(result, crate::objects::value::JsValue::Smi(11));
}
#[test]
#[ignore] fn test_computed_class_method_bytecode() {
let prog = make_program(vec![
var_decl_stmt(VarKind::Let, "k", Some(str_expr("myMethod"))),
class_decl_stmt(
"C",
None,
vec![ClassMember::Method(MethodDef {
loc: span(),
is_static: false,
kind: MethodKind::Method,
key: PropKey::Computed(Box::new(ident_expr("k"))),
is_computed: true,
value: empty_fn_expr(&[]),
})],
),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs
.iter()
.any(|i| i.opcode == Opcode::DefineKeyedOwnProperty),
"computed class method must emit DefineKeyedOwnProperty"
);
}
#[test]
fn test_labeled_continue_for_of_loop() {
use crate::parser::ast::{ForInOfLeft, ForOfStmt};
let prog = make_program(vec![
var_decl_stmt(VarKind::Var, "iter", Some(num_expr(0.0))),
Stmt::Labeled(LabeledStmt {
loc: span(),
label: ident("L"),
body: Box::new(Stmt::ForOf(ForOfStmt {
loc: span(),
is_await: false,
left: ForInOfLeft::VarDecl(VarDecl {
loc: span(),
kind: VarKind::Var,
declarators: vec![VarDeclarator {
loc: span(),
id: Pat::Ident(ident("x")),
init: None,
}],
}),
right: Box::new(ident_expr("iter")),
body: Box::new(Stmt::Continue(ContinueStmt {
loc: span(),
label: Some(ident("L")),
})),
})),
}),
]);
let arr = BytecodeGenerator::compile_program(&prog).unwrap();
let instrs = arr.instructions().unwrap();
assert!(
instrs.iter().any(|i| i.opcode == Opcode::Jump),
"labeled continue L in for-of must emit a Jump"
);
}
use crate::objects::value::JsValue;
#[test]
fn test_switch_fallthrough() {
let result = crate::builtins::global::global_eval(
"var x = 0; switch(2) { case 1: x += 1; case 2: x += 2; case 3: x += 4; } x",
)
.unwrap();
assert_eq!(result, JsValue::Smi(6));
}
#[test]
fn test_switch_default() {
let result = crate::builtins::global::global_eval(
"var x = 0; switch(99) { case 1: x = 1; break; default: x = 42; } x",
)
.unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_nested_ternary() {
let result = crate::builtins::global::global_eval(
"var x = 5; x > 10 ? 'big' : x > 3 ? 'medium' : 'small'",
)
.unwrap();
assert_eq!(result, JsValue::String("medium".into()));
}
#[test]
fn test_try_catch_finally() {
let result = crate::builtins::global::global_eval(
"var x = 0; try { throw 'err'; } catch(e) { x = 1; } finally { x += 10; } x",
)
.unwrap();
assert_eq!(result, JsValue::Smi(11));
}
#[test]
fn test_labeled_break_nested() {
let result = crate::builtins::global::global_eval(
"var sum = 0; outer: for (var i = 0; i < 5; i++) { for (var j = 0; j < 5; j++) { if (j === 2) break outer; sum++; } } sum",
)
.unwrap();
assert_eq!(result, JsValue::Smi(2));
}
#[test]
fn test_comma_operator_direct_eval() {
let result = crate::builtins::global::global_eval("(1, 2, 3)").unwrap();
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_void_operator() {
let result = crate::builtins::global::global_eval("void 42").unwrap();
assert_eq!(result, JsValue::Undefined);
}
#[test]
fn test_delete_property() {
let result =
crate::builtins::global::global_eval("var obj = {x: 1, y: 2}; delete obj.x; obj.x")
.unwrap();
assert_eq!(result, JsValue::Undefined);
}
#[test]
fn test_getter_in_object() {
let result =
crate::builtins::global::global_eval("var obj = { get val() { return 42; } }; obj.val")
.unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_setter_in_object() {
let result =
crate::builtins::global::global_eval("var obj = { get x() { return 42; } }; obj.x")
.unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_symbol_typeof() {
let result = crate::builtins::global::global_eval("typeof Symbol('test')").unwrap();
assert_eq!(result, JsValue::String("symbol".into()));
}
#[test]
fn test_for_in_object_keys() {
let result = crate::builtins::global::global_eval(
"var keys = ''; var obj = {a:1, b:2, c:3}; for (var k in obj) keys += k; keys",
)
.unwrap();
assert_eq!(result, JsValue::String("abc".into()));
}
#[test]
fn test_bitwise_and() {
let result = crate::builtins::global::global_eval("0xFF & 0x0F").unwrap();
assert_eq!(result, JsValue::Smi(15));
}
#[test]
fn test_bitwise_left_shift() {
let result = crate::builtins::global::global_eval("1 << 10").unwrap();
assert_eq!(result, JsValue::Smi(1024));
}
#[test]
fn test_unsigned_right_shift() {
let result = crate::builtins::global::global_eval("-1 >>> 0").unwrap();
assert_eq!(result, JsValue::HeapNumber(4294967295.0));
}
#[test]
fn test_const_reassignment_throws() {
let result = crate::builtins::global::global_eval("const x = 1; x = 2;");
assert!(result.is_err(), "const reassignment should throw");
let err = result.unwrap_err();
assert!(
err.to_string().contains("constant variable"),
"error should mention constant variable, got: {err}"
);
}
#[test]
fn test_const_compound_assignment_throws() {
let result = crate::builtins::global::global_eval("const x = 1; x += 2;");
assert!(result.is_err(), "const compound assignment should throw");
}
#[test]
fn test_const_increment_throws() {
let result = crate::builtins::global::global_eval("const x = 1; x++;");
assert!(result.is_err(), "const increment should throw");
}
#[test]
fn test_const_decrement_throws() {
let result = crate::builtins::global::global_eval("const x = 1; --x;");
assert!(result.is_err(), "const decrement should throw");
}
#[test]
fn test_let_reassignment_works() {
let result = crate::builtins::global::global_eval("let x = 1; x = 2; x").unwrap();
assert_eq!(result, JsValue::Smi(2));
}
#[test]
fn test_const_declaration_works() {
let result = crate::builtins::global::global_eval("const x = 42; x").unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_let_tdz_throws_reference_error() {
let result = crate::builtins::global::global_eval("x; let x = 1;");
assert!(
result.is_err(),
"accessing let before declaration should throw"
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("before initialization")
|| err.to_string().contains("ReferenceError"),
"should be ReferenceError, got: {err}"
);
}
#[test]
fn test_const_tdz_throws_reference_error() {
let result = crate::builtins::global::global_eval("x; const x = 1;");
assert!(
result.is_err(),
"accessing const before declaration should throw"
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("before initialization")
|| err.to_string().contains("ReferenceError"),
"should be ReferenceError, got: {err}"
);
}
#[test]
fn test_typeof_let_tdz_throws() {
let result = crate::builtins::global::global_eval("typeof x; let x = 1;");
assert!(result.is_err(), "typeof on TDZ let should throw");
}
#[test]
fn test_typeof_undeclared_returns_undefined() {
let result = crate::builtins::global::global_eval("typeof undeclaredGlobalVar").unwrap();
assert_eq!(
result,
JsValue::String("undefined".into()),
"typeof undeclared should be 'undefined'"
);
}
#[test]
fn test_let_tdz_block_scoping_shadows() {
let result = crate::builtins::global::global_eval(
"let x = 10; var result; { try { result = x; } catch(e) { result = 'error'; } let x = 20; } result",
).unwrap();
assert_eq!(
result,
JsValue::String("error".into()),
"should throw ReferenceError in block TDZ"
);
}
#[test]
fn test_var_hoisting_returns_undefined() {
let result = crate::builtins::global::global_eval("var r = x; var x = 5; r").unwrap();
assert_eq!(
result,
JsValue::Undefined,
"hoisted var before assignment should be undefined"
);
}
#[test]
fn test_var_hoisting_in_function() {
let result = crate::builtins::global::global_eval(
"function f() { var r = x; var x = 5; return r; } f()",
)
.unwrap();
assert_eq!(
result,
JsValue::Undefined,
"hoisted var in function before assignment should be undefined"
);
}
#[test]
fn test_var_in_block_is_function_scoped() {
let result =
crate::builtins::global::global_eval("function f() { { var x = 42; } return x; } f()")
.unwrap();
assert_eq!(
result,
JsValue::Smi(42),
"var in block should be visible outside"
);
}
#[test]
fn test_var_in_if_is_function_scoped() {
let result = crate::builtins::global::global_eval(
"function f() { if (true) { var x = 99; } return x; } f()",
)
.unwrap();
assert_eq!(
result,
JsValue::Smi(99),
"var in if-block should be function scoped"
);
}
#[test]
fn test_var_in_for_is_function_scoped() {
let result = crate::builtins::global::global_eval(
"function f() { for (var i = 0; i < 3; i++) {} return i; } f()",
)
.unwrap();
assert_eq!(
result,
JsValue::Smi(3),
"var in for-loop should be function scoped"
);
}
#[test]
fn test_function_declaration_hoisted() {
let result =
crate::builtins::global::global_eval("var r = f(); function f() { return 42; } r")
.unwrap();
assert_eq!(
result,
JsValue::Smi(42),
"function declaration should be hoisted"
);
}
#[test]
fn test_function_declaration_hoisted_in_function() {
let result = crate::builtins::global::global_eval(
"function outer() { var r = inner(); function inner() { return 7; } return r; } outer()",
)
.unwrap();
assert_eq!(result, JsValue::Smi(7));
}
#[test]
fn test_const_reassignment_simple() {
let result = crate::builtins::global::global_eval("const x = 1; x = 2;");
assert!(result.is_err(), "const reassignment should throw");
}
#[test]
#[ignore] fn test_let_block_scoped() {
let result = crate::builtins::global::global_eval("{ let x = 1; } x");
assert!(result.is_err(), "let should be block scoped");
}
#[test]
#[ignore] fn test_const_block_scoped() {
let result = crate::builtins::global::global_eval("{ const x = 1; } x");
assert!(result.is_err(), "const should be block scoped");
}
#[test]
#[ignore] fn test_let_in_for_block_scoped() {
let result = crate::builtins::global::global_eval("for (let i = 0; i < 3; i++) {} i");
assert!(result.is_err(), "let in for-loop should be block scoped");
}
#[test]
fn test_let_separate_blocks() {
let result = crate::builtins::global::global_eval(
"var r; { let x = 10; r = x; } { let x = 20; r = r + x; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(30));
}
#[test]
fn test_strict_mode_delete_variable_throws() {
let result = crate::builtins::global::global_eval("'use strict'; var x = 1; delete x;");
assert!(
result.is_err(),
"delete on variable in strict mode should throw SyntaxError"
);
}
#[test]
fn test_var_and_let_coexist() {
let result = crate::builtins::global::global_eval(
"function f() { var a = 1; let b = 2; { var c = 3; let d = 4; } return a + b + c; } f()",
)
.unwrap();
assert_eq!(
result,
JsValue::Smi(6),
"var c should be function-scoped, let d block-scoped"
);
}
#[test]
fn test_let_initialization_order() {
let result = crate::builtins::global::global_eval("let x = 5; x + 1").unwrap();
assert_eq!(result, JsValue::Smi(6));
}
#[test]
fn test_const_in_block_works() {
let result =
crate::builtins::global::global_eval("var r; { const c = 100; r = c; } r").unwrap();
assert_eq!(result, JsValue::Smi(100));
}
#[test]
fn test_switch_strict_comparison() {
let result = crate::builtins::global::global_eval(
"var r = 'no'; switch(1) { case '1': r = 'loose'; break; case 1: r = 'strict'; break; } r",
).unwrap();
assert_eq!(result, JsValue::String("strict".into()));
}
#[test]
fn test_switch_fallthrough_chain() {
let result = crate::builtins::global::global_eval(
"var x = 0; switch(2) { case 1: x += 1; case 2: x += 10; case 3: x += 100; } x",
)
.unwrap();
assert_eq!(result, JsValue::Smi(110));
}
#[test]
fn test_switch_default_in_middle() {
let result = crate::builtins::global::global_eval(
"var x = 0; switch(99) { case 1: x = 1; break; default: x = 50; case 2: x += 10; } x",
)
.unwrap();
assert_eq!(result, JsValue::Smi(60));
}
#[test]
fn test_switch_first_match_wins() {
let result = crate::builtins::global::global_eval(
"var r = 'none'; switch(1) { case 1: r = 'first'; break; case 1: r = 'second'; break; } r",
).unwrap();
assert_eq!(result, JsValue::String("first".into()));
}
#[test]
#[ignore] fn test_switch_block_scoping_let() {
let result = crate::builtins::global::global_eval(
"var r; switch(1) { case 1: let v = 42; r = v; break; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_switch_on_string() {
let result = crate::builtins::global::global_eval(
"var r = 0; switch('hello') { case 'hello': r = 1; break; case 'world': r = 2; break; } r",
).unwrap();
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_switch_no_match_no_default() {
let result = crate::builtins::global::global_eval(
"var r = 99; switch(5) { case 1: r = 1; break; case 2: r = 2; break; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(99));
}
#[test]
fn test_label_break_block() {
let result = crate::builtins::global::global_eval(
"var r = 1; myLabel: { r = 2; break myLabel; r = 3; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(2));
}
#[test]
fn test_labeled_continue_for_skips() {
let result = crate::builtins::global::global_eval(
"var s = 0; outer: for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { if (j === 1) continue outer; s++; } } s",
).unwrap();
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_nested_labels_break_outer() {
let result = crate::builtins::global::global_eval(
"var r = 0; a: { b: { r = 1; break a; r = 2; } r = 3; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_labeled_for_break() {
let result = crate::builtins::global::global_eval(
"var s = 0; loop: for (var i = 0; i < 10; i++) { if (i === 3) break loop; s += i; } s",
)
.unwrap();
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_finally_always_runs_normal() {
let result = crate::builtins::global::global_eval(
"var r = 0; try { r = 1; } finally { r += 10; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(11));
}
#[test]
fn test_finally_runs_on_return() {
let result = crate::builtins::global::global_eval(
"var r = 0; (function() { try { r = 1; return; } finally { r = 99; } })(); r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(99));
}
#[test]
fn test_catch_binding_optional() {
let result = crate::builtins::global::global_eval(
"var r = 0; try { throw 'oops'; } catch { r = 1; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(1));
}
#[test]
#[ignore] fn test_finally_overrides_return() {
let result = crate::builtins::global::global_eval(
"(function() { try { return 1; } finally { return 2; } })()",
)
.unwrap();
assert_eq!(result, JsValue::Smi(2));
}
#[test]
fn test_nested_try_catch_inner_outer() {
let result = crate::builtins::global::global_eval(
"var r = 0; try { try { throw 'inner'; } catch(e) { r = 1; throw 'outer'; } } catch(e2) { r += 10; } r",
).unwrap();
assert_eq!(result, JsValue::Smi(11));
}
#[test]
fn test_try_finally_exception_propagates() {
let result = crate::builtins::global::global_eval(
"var r = 0; try { try { throw 'err'; } finally { r = 5; } } catch(e) { r += 10; } r",
)
.unwrap();
assert_eq!(result, JsValue::Smi(15));
}
#[test]
fn test_try_in_loop_with_break() {
let result = crate::builtins::global::global_eval(
"var r = 0; for (var i = 0; i < 5; i++) { try { if (i === 2) break; r += 1; } finally { r += 10; } } r",
).unwrap();
assert_eq!(result, JsValue::Smi(32));
}
#[test]
fn test_try_in_loop_with_continue() {
let result = crate::builtins::global::global_eval(
"var r = 0; for (var i = 0; i < 3; i++) { try { if (i === 1) continue; r += 1; } finally { r += 10; } } r",
).unwrap();
assert_eq!(result, JsValue::Smi(32));
}
#[test]
fn test_comma_evaluates_all_returns_last() {
let result =
crate::builtins::global::global_eval("var a = 0; var b = (a = 1, a = 2, a + 10); b")
.unwrap();
assert_eq!(result, JsValue::Smi(12));
}
#[test]
fn test_comma_side_effects() {
let result = crate::builtins::global::global_eval("var x = 0; (x++, x++, x++); x").unwrap();
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_ternary_true_branch() {
let result = crate::builtins::global::global_eval("true ? 1 : 2").unwrap();
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_ternary_false_branch() {
let result = crate::builtins::global::global_eval("false ? 1 : 2").unwrap();
assert_eq!(result, JsValue::Smi(2));
}
#[test]
fn test_ternary_deeply_nested() {
let result = crate::builtins::global::global_eval(
"var x = 7; x > 10 ? 'big' : x > 5 ? 'mid' : 'small'",
)
.unwrap();
assert_eq!(result, JsValue::String("mid".into()));
}
#[test]
fn test_ternary_short_circuit() {
let result =
crate::builtins::global::global_eval("var x = 0; true ? (x = 1) : (x = 2); x").unwrap();
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_void_returns_undefined() {
let result = crate::builtins::global::global_eval("void 0").unwrap();
assert_eq!(result, JsValue::Undefined);
}
#[test]
fn test_void_evaluates_argument() {
let result = crate::builtins::global::global_eval("var x = 0; void (x = 5); x").unwrap();
assert_eq!(result, JsValue::Smi(5));
}
#[test]
fn test_void_expression() {
let result = crate::builtins::global::global_eval("void (1 + 2)").unwrap();
assert_eq!(result, JsValue::Undefined);
}
#[test]
fn test_delete_property_returns_true() {
let result =
crate::builtins::global::global_eval("var obj = {a: 1}; delete obj.a").unwrap();
assert_eq!(result, JsValue::Boolean(true));
}
#[test]
fn test_delete_removes_property() {
let result =
crate::builtins::global::global_eval("var obj = {a: 1, b: 2}; delete obj.a; obj.a")
.unwrap();
assert_eq!(result, JsValue::Undefined);
}
#[test]
fn test_delete_non_existent_returns_true() {
let result = crate::builtins::global::global_eval("var obj = {}; delete obj.nope").unwrap();
assert_eq!(result, JsValue::Boolean(true));
}
#[test]
fn test_delete_non_reference_returns_true() {
let result = crate::builtins::global::global_eval("delete 42").unwrap();
assert_eq!(result, JsValue::Boolean(true));
}
#[test]
fn test_nested_object_destructuring() {
let result = eval_to_value("var {a: {b}} = {a: {b: 1}}; b");
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_nested_object_destructuring_deep() {
let result = eval_to_value("var {a: {b: {c}}} = {a: {b: {c: 42}}}; c");
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_nested_array_destructuring() {
let result = eval_to_value("var [[a, b], [c]] = [[1, 2], [3]]; a + b + c");
assert_eq!(result, JsValue::Smi(6));
}
#[test]
fn test_nested_array_destructuring_deep() {
let result = eval_to_value("var [[[x]]] = [[[99]]]; x");
assert_eq!(result, JsValue::Smi(99));
}
#[test]
fn test_mixed_destructuring_obj_array() {
let result = eval_to_value("var {a: [b, c]} = {a: [1, 2]}; b + c");
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_mixed_destructuring_array_obj() {
let result = eval_to_value("var [{x}, {y}] = [{x: 10}, {y: 20}]; x + y");
assert_eq!(result, JsValue::Smi(30));
}
#[test]
fn test_computed_property_destructuring() {
let result = eval_to_value(r#"var key = "x"; var {[key]: val} = {x: 42}; val"#);
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_computed_property_destructuring_number() {
let result = eval_to_value("var {[0]: first} = {0: 'hello'}; first");
assert_eq!(result, JsValue::String("hello".into()));
}
#[test]
fn test_default_value_object_missing_key() {
let result = eval_to_value("var {a = 1} = {}; a");
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_default_value_object_key_present() {
let result = eval_to_value("var {a = 1} = {a: 5}; a");
assert_eq!(result, JsValue::Smi(5));
}
#[test]
fn test_default_value_array_short() {
let result = eval_to_value("var [a = 1] = []; a");
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_default_value_array_present() {
let result = eval_to_value("var [a = 1] = [9]; a");
assert_eq!(result, JsValue::Smi(9));
}
#[test]
fn test_default_triggers_on_undefined() {
let result = eval_to_value("var {a = 1} = {a: undefined}; a");
assert_eq!(result, JsValue::Smi(1));
}
#[test]
fn test_default_does_not_trigger_on_null() {
let result = eval_to_value("var {a = 1} = {a: null}; a");
assert_eq!(result, JsValue::Null);
}
#[test]
fn test_default_array_undefined_element() {
let result = eval_to_value("var [a = 10] = [undefined]; a");
assert_eq!(result, JsValue::Smi(10));
}
#[test]
fn test_object_rest_destructuring() {
let result = eval_to_value("var {a, ...rest} = {a: 1, b: 2, c: 3}; rest.b + rest.c");
assert_eq!(result, JsValue::Smi(5));
}
#[test]
fn test_object_rest_excludes_extracted() {
let result = eval_to_value("var {a, ...rest} = {a: 1, b: 2, c: 3}; rest.a === undefined");
assert_eq!(result, JsValue::Boolean(true));
}
#[test]
fn test_array_rest_destructuring() {
let result = eval_to_value("var [a, ...rest] = [1, 2, 3]; rest[0] + rest[1]");
assert_eq!(result, JsValue::Smi(5));
}
#[test]
fn test_for_of_object_destructuring_e2e() {
let result = eval_to_value(
"var sum = 0; for (var {x, y} of [{x:1,y:2},{x:3,y:4}]) sum += x + y; sum",
);
assert_eq!(result, JsValue::Smi(10));
}
#[test]
fn test_for_of_array_destructuring_e2e() {
let result =
eval_to_value("var sum = 0; for (var [a, b] of [[1,2],[3,4]]) sum += a + b; sum");
assert_eq!(result, JsValue::Smi(10));
}
#[test]
fn test_for_of_nested_destructuring() {
let result =
eval_to_value("var r = 0; for (var {a: {b}} of [{a:{b:10}},{a:{b:20}}]) r += b; r");
assert_eq!(result, JsValue::Smi(30));
}
#[test]
fn test_function_param_object_destructuring_e2e() {
let result = eval_to_value("function f({a, b}) { return a + b; } f({a: 3, b: 7})");
assert_eq!(result, JsValue::Smi(10));
}
#[test]
fn test_function_param_array_destructuring_e2e() {
let result = eval_to_value("function f([a, b]) { return a * b; } f([3, 4])");
assert_eq!(result, JsValue::Smi(12));
}
#[test]
fn test_function_param_nested_destructuring() {
let result = eval_to_value("function f({a: [x, y]}) { return x + y; } f({a: [5, 6]})");
assert_eq!(result, JsValue::Smi(11));
}
#[test]
fn test_function_param_default_value() {
let result = eval_to_value("function f({a = 10}) { return a; } f({})");
assert_eq!(result, JsValue::Smi(10));
}
#[test]
fn test_assignment_destructuring_object() {
let result = eval_to_value("var a, b; ({a, b} = {a: 10, b: 20}); a + b");
assert_eq!(result, JsValue::Smi(30));
}
#[test]
fn test_assignment_destructuring_array() {
let result = eval_to_value("var x, y; [x, y] = [100, 200]; x + y");
assert_eq!(result, JsValue::Smi(300));
}
#[test]
fn test_assignment_destructuring_nested() {
let result = eval_to_value("var a, b; ({a, b} = {a: {x: 1}, b: 2}); a.x + b");
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_destructuring_with_rename_and_default() {
let result = eval_to_value("var {a: x = 5} = {}; x");
assert_eq!(result, JsValue::Smi(5));
}
#[test]
fn test_destructuring_with_rename_and_default_present() {
let result = eval_to_value("var {a: x = 5} = {a: 42}; x");
assert_eq!(result, JsValue::Smi(42));
}
#[test]
fn test_destructuring_let_binding() {
let result = eval_to_value("let {a, b} = {a: 1, b: 2}; a + b");
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_destructuring_const_binding() {
let result = eval_to_value("const [a, b] = [10, 20]; a + b");
assert_eq!(result, JsValue::Smi(30));
}
#[test]
fn test_destructuring_swap() {
let result = eval_to_value("var a = 1, b = 2; [a, b] = [b, a]; a * 10 + b");
assert_eq!(result, JsValue::Smi(21));
}
#[test]
fn test_destructuring_skip_elements() {
let result = eval_to_value("var [,, third] = [1, 2, 3]; third");
assert_eq!(result, JsValue::Smi(3));
}
#[test]
fn test_destructuring_string_iterable() {
let result = eval_to_value(r#"var [a, b, c] = "xyz"; a + b + c"#);
assert_eq!(result, JsValue::String("xyz".into()));
}
#[test]
fn test_for_of_destructuring_with_let() {
let result =
eval_to_value("var sum = 0; for (let {v} of [{v:1},{v:2},{v:3}]) sum += v; sum");
assert_eq!(result, JsValue::Smi(6));
}
}