quickjs-oxide 0.4.0

A safe Rust JavaScript engine targeting semantic parity with QuickJS
Documentation
//! Lexical scope identities and declaration-order indexes shared by compilation stages.

use super::bindings::BindingId;
use crate::engine::compiler::model::bindings::IrBinding;
use crate::engine::compiler::names::NameId;
use std::collections::HashMap;

/// Function-local lexical scope identity. QuickJS carries the corresponding
/// `scope_level` beside every unresolved scope opcode; keeping it typed avoids
/// accidentally resolving a child use from the parent's final parse scope.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(in crate::engine::compiler) struct ScopeId(pub(in crate::engine::compiler) usize);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::engine::compiler) enum ScopeKind {
    FunctionRoot,
    /// Declarative environment used while evaluating non-simple formal
    /// parameters. It is deliberately parentless: parameter initializers may
    /// see parameter cells and selected function pseudo-bindings, but never
    /// declarations from the authored body variable environment.
    Parameter,
    FunctionBody,
    ProgramBody,
    Block,
    /// QuickJS's class-body scope. It owns private-name declarations plus the
    /// compiler-only lexical cells used to retain computed field keys. It is
    /// separate from the class-name scope so heritage evaluation cannot observe
    /// names declared later in the body.
    ClassPrivate,
    If,
    For,
    Switch,
    Catch,
    With,
}

#[derive(Debug)]
pub(in crate::engine::compiler) struct IrScope {
    pub(in crate::engine::compiler) parent: Option<ScopeId>,
    pub(in crate::engine::compiler) kind: ScopeKind,
    /// This nested lexical scope is entered and left while formal-parameter
    /// initialization is still running.  It is distinct from the Parameter
    /// scope itself: its bindings may be captured by closures created in an
    /// initializer, but must never be mistaken for authored body lexicals.
    pub(in crate::engine::compiler) is_parameter_initializer: bool,
    pub(in crate::engine::compiler) bindings: Vec<BindingId>,
    /// Last binding in declaration order for each name. The ordered list remains
    /// authoritative for validation, lowering and observable declaration order.
    /// Boxing keeps every small scope's header compact; only large scopes allocate it.
    #[expect(clippy::box_collection)]
    pub(in crate::engine::compiler) bindings_by_name: Option<Box<HashMap<NameId, BindingId>>>,
}

impl IrScope {
    pub(in crate::engine::compiler) fn binding_named(
        &self,
        name: NameId,
        bindings: &[IrBinding],
    ) -> Option<BindingId> {
        if let Some(index) = &self.bindings_by_name {
            return index.get(&name).copied();
        }
        self.bindings.iter().rev().copied().find(|id| {
            bindings
                .get(id.0)
                .is_some_and(|binding| binding.name == name)
        })
    }

    pub(in crate::engine::compiler) fn index_binding(
        &mut self,
        name: NameId,
        binding: BindingId,
        bindings: &[IrBinding],
    ) {
        if let Some(index) = &mut self.bindings_by_name {
            index.insert(name, binding);
        } else if self.bindings.len() > 8 {
            self.rebuild_name_index(bindings);
        }
    }

    pub(in crate::engine::compiler) fn rebuild_name_index(&mut self, bindings: &[IrBinding]) {
        if self.bindings.len() <= 8 {
            self.bindings_by_name = None;
            return;
        }
        let index = self.bindings_by_name.get_or_insert_with(Box::default);
        index.clear();
        for &binding in &self.bindings {
            index.insert(bindings[binding.0].name, binding);
        }
    }
}