use super::MAX_LOCAL_VARIABLES;
use crate::engine::api::error::{Error, ErrorKind};
use crate::engine::code::bytecode::Instruction;
use crate::engine::code::function::metadata::EvalKind;
use crate::engine::compiler::lexer::Span;
use crate::engine::compiler::model::bindings::ResolvedBinding;
use crate::engine::compiler::model::bindings::{BindingKind, BindingStorage};
use crate::engine::compiler::model::ir::function::FunctionKind;
use crate::engine::compiler::model::ir::function::FunctionTree;
use crate::engine::compiler::model::ir::{FunctionId, IrOp, SpannedIrOp};
use crate::engine::compiler::model::scope::ScopeId;
use crate::engine::compiler::relocation::prepend_hoist_prefix;
use crate::engine::compiler::resolution::find_or_create_own_binding;
use crate::engine::compiler::resolution::lookup_interned_name;
pub(super) const THIS_LOCAL_NAME: &str = "<this>";
pub(super) const NEW_TARGET_LOCAL_NAME: &str = "<new.target>";
pub(super) const HOME_OBJECT_LOCAL_NAME: &str = "<home_object>";
pub(super) const ACTIVE_FUNCTION_LOCAL_NAME: &str = "<this_active_func>";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum PseudoBinding {
HomeObject,
ActiveFunction,
This,
NewTarget,
}
impl PseudoBinding {
pub(super) const fn name(self) -> &'static str {
match self {
Self::HomeObject => HOME_OBJECT_LOCAL_NAME,
Self::ActiveFunction => ACTIVE_FUNCTION_LOCAL_NAME,
Self::This => THIS_LOCAL_NAME,
Self::NewTarget => NEW_TARGET_LOCAL_NAME,
}
}
pub(super) fn from_name(name: &str) -> Option<Self> {
match name {
HOME_OBJECT_LOCAL_NAME => Some(Self::HomeObject),
ACTIVE_FUNCTION_LOCAL_NAME => Some(Self::ActiveFunction),
THIS_LOCAL_NAME => Some(Self::This),
NEW_TARGET_LOCAL_NAME => Some(Self::NewTarget),
_ => None,
}
}
}
pub(super) fn ensure_eval_visible_pseudo_bindings(
tree: &mut FunctionTree,
consuming_function: FunctionId,
) -> Result<(), Error> {
let span = tree.functions[consuming_function].source.span;
if !ensure_pseudo_binding_path(tree, consuming_function, PseudoBinding::This, span)? {
return Err(Error::internal(
"direct eval environment has no authenticated this binding",
));
}
if function_allows_new_target(tree, consuming_function)
&& !ensure_pseudo_binding_path(tree, consuming_function, PseudoBinding::NewTarget, span)?
{
return Err(Error::internal(
"direct eval environment lost its new.target capability",
));
}
if tree.functions[consuming_function].super_allowed
&& !ensure_pseudo_binding_path(tree, consuming_function, PseudoBinding::HomeObject, span)?
{
return Err(Error::internal(
"direct eval environment lost its HomeObject capability",
));
}
if tree.functions[consuming_function].super_call_allowed
&& !ensure_pseudo_binding_path(
tree,
consuming_function,
PseudoBinding::ActiveFunction,
span,
)?
{
return Err(Error::internal(
"direct eval environment lost its active constructor binding",
));
}
let arguments_name = lookup_interned_name(tree, "arguments")?;
let mut arguments_owner = Some(consuming_function);
let mut definition_scope = None;
while let Some(function_id) = arguments_owner {
let function = &tree.functions[function_id];
if matches!(function.kind, FunctionKind::Ordinary | FunctionKind::Method) {
if function.arguments_forbidden {
definition_scope = function.parent.map(|parent| parent.definition_scope);
arguments_owner = function.parent.map(|parent| parent.function);
continue;
}
if definition_scope.is_some_and(|scope| {
function
.parameter_scope
.is_some_and(|parameter| function.scope_is_within(scope, parameter))
}) {
break;
}
let span = function.source.span;
find_or_create_own_binding(tree, function_id, ScopeId(0), arguments_name, span)?;
break;
}
if matches!(function.kind, FunctionKind::Eval(EvalKind::Direct))
&& function
.binding_from_scope(function.var_scope, arguments_name)
.is_some()
{
break;
}
definition_scope = function.parent.map(|parent| parent.definition_scope);
arguments_owner = function.parent.map(|parent| parent.function);
}
let mut cursor = Some(consuming_function);
while let Some(function_id) = cursor {
let (name, span, parent) = {
let function = &tree.functions[function_id];
(
if function.private_name_binding {
function.function_name
} else {
None
},
function.source.span,
function.parent,
)
};
if let Some(name) = name {
find_or_create_own_binding(tree, function_id, ScopeId(0), name, span)?;
}
cursor = parent.map(|parent| parent.function);
}
Ok(())
}
fn function_allows_new_target(tree: &FunctionTree, mut function_id: FunctionId) -> bool {
let Some(new_target_name) = tree.names.lookup(NEW_TARGET_LOCAL_NAME) else {
return false;
};
loop {
let function = &tree.functions[function_id];
match function.kind {
FunctionKind::Ordinary | FunctionKind::Method => return true,
FunctionKind::Script
| FunctionKind::Module
| FunctionKind::Eval(EvalKind::Indirect) => return false,
FunctionKind::Eval(EvalKind::Direct) => {
return function
.binding_from_scope(function.var_scope, new_target_name)
.is_some();
}
FunctionKind::Eval(EvalKind::None) => return false,
FunctionKind::Arrow => {
let Some(parent) = function.parent else {
return false;
};
function_id = parent.function;
}
}
}
}
fn ensure_pseudo_binding_path(
tree: &mut FunctionTree,
mut function_id: FunctionId,
pseudo: PseudoBinding,
span: Span,
) -> Result<bool, Error> {
loop {
if find_or_create_own_pseudo_binding(tree, function_id, pseudo, span)?.is_some() {
return Ok(true);
}
let Some(parent) = tree.functions[function_id].parent else {
return Ok(false);
};
function_id = parent.function;
}
}
pub(super) fn install_pseudo_binding_prologues(tree: &mut FunctionTree) -> Result<(), Error> {
for function in &mut tree.functions {
if function.pseudo_binding_prologues_installed {
return Err(Error::internal(
"pseudo-binding prologues were installed more than once",
));
}
let mut prefix = Vec::with_capacity(
usize::from(function.home_object_local.is_some()) * 2
+ usize::from(function.active_function_local.is_some()) * 2
+ usize::from(function.new_target_local.is_some()) * 2
+ usize::from(function.this_local.is_some() && !function.derived_class_constructor)
* 2,
);
if let Some(local) = function.home_object_local {
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PushHomeObject),
pc_site: None,
});
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PutLocal(local)),
pc_site: None,
});
}
if let Some(local) = function.active_function_local {
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PushActiveFunction),
pc_site: None,
});
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PutLocal(local)),
pc_site: None,
});
}
if let Some(local) = function.new_target_local {
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PushNewTarget),
pc_site: None,
});
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PutLocal(local)),
pc_site: None,
});
}
if let Some(local) = function
.this_local
.filter(|_| !function.derived_class_constructor)
{
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PushThis),
pc_site: None,
});
prefix.push(SpannedIrOp {
op: IrOp::Bytecode(Instruction::PutLocal(local)),
pc_site: None,
});
}
if !prefix.is_empty() {
prepend_hoist_prefix(function, prefix)?;
}
function.pseudo_binding_prologues_installed = true;
}
Ok(())
}
pub(super) const fn function_owns_pseudo_binding(
kind: FunctionKind,
pseudo: PseudoBinding,
) -> bool {
match pseudo {
PseudoBinding::HomeObject | PseudoBinding::ActiveFunction => {
matches!(kind, FunctionKind::Method)
}
PseudoBinding::This => matches!(
kind,
FunctionKind::Script
| FunctionKind::Module
| FunctionKind::Ordinary
| FunctionKind::Method
| FunctionKind::Eval(EvalKind::Indirect)
),
PseudoBinding::NewTarget => {
matches!(kind, FunctionKind::Ordinary | FunctionKind::Method)
}
}
}
pub(super) fn find_or_create_own_pseudo_binding(
tree: &mut FunctionTree,
function_id: FunctionId,
pseudo: PseudoBinding,
span: Span,
) -> Result<Option<ResolvedBinding>, Error> {
let name = lookup_interned_name(tree, pseudo.name())?;
let function = tree
.functions
.get(function_id)
.ok_or_else(|| Error::internal("pseudo-binding owner is out of bounds"))?;
if let Some(binding) = function.binding_from_scope(function.var_scope, name) {
let derived_this = pseudo == PseudoBinding::This
&& function.super_call_allowed
&& binding.kind == (BindingKind::Lexical { is_const: false });
if binding.kind != BindingKind::Normal && !derived_this {
return Err(Error::internal(
"pseudo binding has non-ordinary binding metadata",
));
}
return Ok(Some(binding));
}
if !function_owns_pseudo_binding(function.kind, pseudo) {
return Ok(None);
}
let function = &mut tree.functions[function_id];
if function.locals.len() >= MAX_LOCAL_VARIABLES {
return Err(Error::new(ErrorKind::JsInternal, "too many local variables").with_span(span));
}
let index = u16::try_from(function.locals.len())
.map_err(|_| Error::new(ErrorKind::JsInternal, "too many local variables"))?;
let slot = match pseudo {
PseudoBinding::HomeObject => {
function.needs_home_object = true;
&mut function.home_object_local
}
PseudoBinding::ActiveFunction => &mut function.active_function_local,
PseudoBinding::This => &mut function.this_local,
PseudoBinding::NewTarget => &mut function.new_target_local,
};
if slot.replace(index).is_some() {
return Err(Error::internal(
"pseudo local metadata was allocated more than once",
));
}
function.locals.push(name);
let kind = if pseudo == PseudoBinding::This && function.derived_class_constructor {
BindingKind::Lexical { is_const: false }
} else {
BindingKind::Normal
};
function.add_binding(
function.var_scope,
function.var_scope,
name,
BindingStorage::Local(index),
kind,
None,
);
Ok(Some(ResolvedBinding {
storage: BindingStorage::Local(index),
kind,
}))
}