perf(semantic): improve SoA with multi index vec (#19138)

## Summary

- Introduce `multi_index_vec!` macro (modeled on Zig's `MultiArrayList`) that generates a struct-of-arrays container backed by a single allocation
- Replace 3 separate `IndexVec` fields (`scope_parent_ids`, `scope_node_ids`, `scope_flags`) with a single `ScopeTable`
- Single `len`/`cap` (u32) instead of 3× `ptr+len+cap` (72 bytes → 24 bytes of metadata)
- Single capacity check on `push`, single `reserve` call for scopes
- Follow-up: extend to symbol fields, reference SOA split, arena allocation

Ref: https://github.com/oxc-project/backlog/issues/11

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-02-12 07:24:16 +00:00
1 parent 41c50a5a29
commit 5b90d46391
6 files changed
+524 -80

No files matched your search

+1 -1
View File
@@ -76,7 +76,7 @@ impl<'a> Binder<'a> for VariableDeclarator<'a> {
// avoid same symbols appear in multi-scopes
builder.scoping.remove_binding(scope_id, name);
builder.scoping.add_binding(target_scope_id, name, symbol_id);
builder.scoping.symbol_scope_ids[symbol_id] = target_scope_id;
builder.scoping.set_symbol_scope_id(symbol_id, target_scope_id);
}
break;
}
+1
View File
@@ -36,6 +36,7 @@ mod is_global_reference;
#[cfg(feature = "linter")]
mod jsdoc;
mod label;
mod multi_index_vec;
mod node;
mod scoping;
mod stats;
+444
View File
@@ -0,0 +1,444 @@
/// A macro that generates a struct-of-arrays (SoA) container backed by a single allocation.
///
/// Instead of N separate `IndexVec`s each with their own `ptr + len + capacity`,
/// this generates a struct with:
/// - A single allocation containing all field arrays contiguously
/// - A single `len` and `cap` (stored as `u32`)
/// - Per-field typed accessors with a single bounds check
/// - A single capacity check on `push`
///
/// This is modeled on Zig's `MultiArrayList`.
///
/// # Usage
///
/// ```ignore
/// multi_index_vec! {
/// pub struct ScopeTable<ScopeId> {
/// pub parent_ids => parent_ids_mut: Option<ScopeId>,
/// pub node_ids => node_ids_mut: NodeId,
/// pub flags => flags_mut: ScopeFlags,
/// }
/// }
/// ```
///
/// The `=> name_mut` part specifies the mutable accessor name (needed because
/// declarative macros cannot concatenate identifiers).
macro_rules! multi_index_vec {
(
$(#[$meta:meta])*
$vis:vis struct $name:ident<$idx:ty> {
$(
$fvis:vis $fname:ident => $fname_mut:ident : $fty:ty
),* $(,)?
}
) => {
$(#[$meta])*
$vis struct $name {
/// Pointer to the base of the single allocation.
/// Layout: `[Field1 × cap][Field2 × cap]...` with alignment padding between fields.
base: ::core::ptr::NonNull<u8>,
/// Cached pointers to the start of each field's array within the allocation.
$( $fname: ::core::ptr::NonNull<$fty>, )*
/// Number of elements currently stored.
len: u32,
/// Total capacity of each field array.
cap: u32,
}
// SAFETY: All data is fully owned by this struct. The raw pointers do not
// alias with any other pointers. This is equivalent to Vec's Send/Sync impls
// which are safe because Vec owns its data.
unsafe impl Send for $name
where
$( $fty: Send, )*
{}
// SAFETY: Immutable access to the data through `&self` does not allow mutation.
// Mutable access requires `&mut self` which guarantees exclusive access.
unsafe impl Sync for $name
where
$( $fty: Sync, )*
{}
#[expect(clippy::allow_attributes, reason = "macro-generated methods may not all be used")]
impl $name {
/// Maximum number of elements this table can hold while still being
/// representable by the index type.
#[inline(always)]
fn max_len() -> usize {
<$idx as ::oxc_index::Idx>::MAX.saturating_add(1)
}
/// Compute the memory layout for a given capacity.
/// Returns `None` if `cap == 0`.
fn compute_layout(cap: usize) -> Option<::core::alloc::Layout> {
if cap == 0 {
return None;
}
let mut size = 0usize;
let mut max_align = 1usize;
$(
let align = ::core::mem::align_of::<$fty>();
if align > max_align { max_align = align; }
size = (size + align - 1) & !(align - 1);
size += ::core::mem::size_of::<$fty>().checked_mul(cap).expect("capacity overflow");
)*
Some(::core::alloc::Layout::from_size_align(size, max_align).expect("invalid layout"))
}
/// Set cached field pointers based on the base allocation pointer and capacity.
///
/// # Safety
/// `base` must point to a valid allocation of the layout returned by
/// `compute_layout(cap)`. The allocation must be at least as aligned as
/// the maximum field alignment.
unsafe fn set_pointers(&mut self, base: *mut u8, cap: usize) {
let mut _offset = 0usize;
$(
let align = ::core::mem::align_of::<$fty>();
_offset = (_offset + align - 1) & !(align - 1);
#[expect(clippy::cast_ptr_alignment)]
{
// SAFETY: `_offset` is within the allocation, and properly aligned
// for `$fty` because `compute_layout` aligns each field's offset,
// and the base pointer has at least `max_align` alignment.
self.$fname = unsafe {
::core::ptr::NonNull::new_unchecked(
base.add(_offset).cast::<$fty>()
)
};
}
_offset += ::core::mem::size_of::<$fty>() * cap;
)*
}
/// Create a new empty container.
#[inline]
$vis fn new() -> Self {
Self {
base: ::core::ptr::NonNull::dangling(),
$( $fname: ::core::ptr::NonNull::dangling(), )*
len: 0,
cap: 0,
}
}
/// Returns the number of elements.
#[inline]
$vis fn len(&self) -> usize {
self.len as usize
}
/// Returns `true` if there are no elements.
#[inline]
$vis fn is_empty(&self) -> bool {
self.len == 0
}
/// Reserve capacity for at least `additional` more elements.
$vis fn reserve(&mut self, additional: usize) {
let required = (self.len as usize).checked_add(additional).expect("capacity overflow");
assert!(
required <= Self::max_len(),
"capacity exceeds index type range"
);
if required <= self.cap as usize {
return;
}
self.grow_to(required);
}
/// Grow the allocation to hold at least `min_cap` elements.
fn grow_to(&mut self, min_cap: usize) {
let max_len = Self::max_len();
assert!(min_cap <= max_len, "capacity exceeds index type range");
let new_cap = if self.cap == 0 {
min_cap.max(4)
} else {
(self.cap as usize).checked_mul(2).expect("capacity overflow").max(min_cap)
}
.min(max_len);
let old_cap = self.cap as usize;
let old_len = self.len as usize;
let new_layout = Self::compute_layout(new_cap)
.expect("new capacity must be > 0");
// SAFETY: All pointer arithmetic is within bounds of the allocations.
// Old data is copied field-by-field from the old allocation to the new one.
// The old allocation is freed after the copy.
unsafe {
let new_base = ::std::alloc::alloc(new_layout);
if new_base.is_null() {
::std::alloc::handle_alloc_error(new_layout);
}
// Copy old data to new allocation.
if old_cap > 0 {
let old_base = self.base.as_ptr();
let mut _old_offset = 0usize;
let mut _new_offset = 0usize;
$(
let align = ::core::mem::align_of::<$fty>();
let elem_size = ::core::mem::size_of::<$fty>();
_old_offset = (_old_offset + align - 1) & !(align - 1);
_new_offset = (_new_offset + align - 1) & !(align - 1);
::core::ptr::copy_nonoverlapping(
old_base.add(_old_offset),
new_base.add(_new_offset),
elem_size * old_len,
);
_old_offset += elem_size * old_cap;
_new_offset += elem_size * new_cap;
)*
let old_layout = Self::compute_layout(old_cap).unwrap();
::std::alloc::dealloc(old_base, old_layout);
}
self.base = ::core::ptr::NonNull::new_unchecked(new_base);
self.set_pointers(new_base, new_cap);
self.cap = u32::try_from(new_cap).expect("capacity exceeds u32");
}
}
/// Push a new element to all field arrays. Returns the index of the new element.
$vis fn push(&mut self, $( $fname: $fty ),*) -> $idx {
if self.len == self.cap {
self.grow_to(self.len as usize + 1);
}
let idx = self.len as usize;
// SAFETY: `idx < cap` is guaranteed because we just grew if needed.
// The field pointers are valid and properly aligned for their types.
unsafe {
$(
self.$fname.as_ptr().add(idx).write($fname);
)*
}
self.len += 1;
// SAFETY: `idx < len <= cap <= max_len = Idx::MAX + 1`, so `idx <= Idx::MAX`.
unsafe { <$idx as ::oxc_index::Idx>::from_usize_unchecked(idx) }
}
/// Iterate over all valid indices.
$vis fn iter_ids(&self) -> impl Iterator<Item = $idx> {
let len = self.len as usize;
// SAFETY: Valid indices are `0..len`, and by invariant `len <= Idx::MAX + 1`.
(0..len).map(|i| unsafe { <$idx as ::oxc_index::Idx>::from_usize_unchecked(i) })
}
#[inline(always)]
fn checked_idx(&self, id: $idx) -> usize {
let idx = ::oxc_index::Idx::index(id);
assert!(idx < self.len as usize, "index out of bounds");
idx
}
// Per-field accessors.
$(
#[inline]
$fvis fn $fname(&self, id: $idx) -> &$fty {
let idx = self.checked_idx(id);
// SAFETY: `idx` is validated by `checked_idx`.
// The pointer is aligned and valid for reads.
unsafe { &*self.$fname.as_ptr().add(idx) }
}
#[inline]
#[allow(dead_code)]
$fvis fn $fname_mut(&mut self, id: $idx) -> &mut $fty {
let idx = self.checked_idx(id);
// SAFETY: `idx` is validated by `checked_idx`.
// The pointer is aligned and valid. `&mut self` guarantees no aliasing.
unsafe { &mut *self.$fname.as_ptr().add(idx) }
}
)*
}
impl Clone for $name
where
$( $fty: Clone, )*
{
fn clone(&self) -> Self {
let len = self.len as usize;
if len == 0 {
return Self::new();
}
// Clone with exact capacity (no excess).
let layout = Self::compute_layout(len)
.expect("len > 0 so layout must exist");
// SAFETY: We allocate a fresh buffer and copy `len` elements per field.
// All source pointers are valid for `len` reads, and destination pointers
// are valid for `len` writes.
unsafe {
let new_base = ::std::alloc::alloc(layout);
if new_base.is_null() {
::std::alloc::handle_alloc_error(layout);
}
let mut result = Self {
base: ::core::ptr::NonNull::new_unchecked(new_base),
$( $fname: ::core::ptr::NonNull::dangling(), )*
len: self.len,
cap: self.len, // exact capacity
};
result.set_pointers(new_base, len);
$(
if ::core::mem::needs_drop::<$fty>() {
for i in 0..len {
result.$fname.as_ptr().add(i).write(
(*self.$fname.as_ptr().add(i)).clone(),
);
}
} else {
::core::ptr::copy_nonoverlapping(
self.$fname.as_ptr(),
result.$fname.as_ptr(),
len,
);
}
)*
result
}
}
}
impl Drop for $name {
fn drop(&mut self) {
if self.cap == 0 {
return;
}
let len = self.len as usize;
$(
if ::core::mem::needs_drop::<$fty>() {
for i in 0..len {
// SAFETY: `i < len <= cap`, so the pointer is within the allocation.
// Each element is only dropped once.
unsafe {
::core::ptr::drop_in_place(self.$fname.as_ptr().add(i));
}
}
}
)*
let layout = Self::compute_layout(self.cap as usize).unwrap();
// SAFETY: `self.base` was allocated with this layout.
unsafe {
::std::alloc::dealloc(self.base.as_ptr(), layout);
}
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl ::core::fmt::Debug for $name {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_struct(stringify!($name))
.field("len", &self.len)
.field("cap", &self.cap)
.finish()
}
}
};
}
pub(crate) use multi_index_vec;
#[cfg(test)]
mod tests {
oxc_index::define_index_type! {
struct TestId = usize;
}
multi_index_vec! {
struct TestTable<TestId> {
values => values_mut: u32,
}
}
oxc_index::define_index_type! {
struct SmallId = u8;
MAX_INDEX = 3;
}
multi_index_vec! {
struct SmallTable<SmallId> {
values => values_mut: u32,
}
}
oxc_index::define_index_type! {
struct StringId = usize;
}
multi_index_vec! {
struct StringTable<StringId> {
values => values_mut: String,
}
}
#[test]
fn iter_ids_are_valid() {
let mut table = TestTable::new();
assert!(table.is_empty());
table.reserve(3);
table.push(1);
table.push(2);
table.push(3);
assert_eq!(table.len(), 3);
let ids = table.iter_ids().collect::<Vec<_>>();
assert_eq!(ids.len(), 3);
assert_eq!(usize::from(ids[0]), 0);
assert_eq!(usize::from(ids[1]), 1);
assert_eq!(usize::from(ids[2]), 2);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn accessor_panics_on_invalid_index() {
let mut table = TestTable::new();
table.push(1);
let _ = table.values(TestId::from(1usize));
}
#[test]
#[should_panic(expected = "capacity exceeds index type range")]
fn reserve_panics_when_exceeding_index_range() {
let mut table = SmallTable::new();
assert!(table.is_empty());
assert_eq!(table.len(), 0);
let _ = table.iter_ids().count();
table.reserve(5);
}
#[test]
#[should_panic(expected = "capacity exceeds index type range")]
fn push_panics_when_exceeding_index_range() {
let mut table = SmallTable::new();
let id = table.push(1);
assert_eq!(*table.values(id), 1);
table.push(2);
table.push(3);
table.push(4);
table.push(5);
}
#[test]
fn clone_clones_non_copy_fields() {
let mut table = StringTable::new();
assert!(table.is_empty());
table.reserve(1);
let id = table.push(String::from("a"));
assert_eq!(table.len(), 1);
let _ = table.iter_ids().count();
let mut clone = table.clone();
table.values_mut(id).push('1');
clone.values_mut(id).push('2');
assert_eq!(table.values(id), "a1");
assert_eq!(clone.values(id), "a2");
}
}
+66 -67
View File
@@ -13,6 +13,8 @@ use oxc_syntax::{
symbol::{SymbolFlags, SymbolId},
};
use crate::multi_index_vec::multi_index_vec;
pub type Bindings<'a> = ArenaIdentHashMap<'a, SymbolId>;
pub type UnresolvedReferences<'a> = ArenaIdentHashMap<'a, ArenaVec<'a, ReferenceId>>;
@@ -37,6 +39,34 @@ impl CloneIn<'_> for Redeclaration {
}
}
multi_index_vec! {
/// Scope tree stored as struct-of-arrays in a single allocation.
///
/// Contains parent IDs, node IDs, and flags for all scopes. Using a single
/// allocation with one `len`/`cap` instead of 3 separate `IndexVec`s saves
/// memory (no redundant len/cap) and CPU (one bounds check, one capacity
/// check on push).
struct ScopeTable<ScopeId> {
parent_ids => parent_ids_mut: Option<ScopeId>,
node_ids => node_ids_mut: NodeId,
flags => flags_mut: ScopeFlags,
}
}
multi_index_vec! {
/// Symbol table stored as struct-of-arrays in a single allocation.
///
/// Contains spans, flags, scope IDs, and declaration node IDs for all symbols.
/// Using a single allocation with one `len`/`cap` instead of 4 separate `IndexVec`s
/// saves memory and CPU.
struct SymbolTable<SymbolId> {
symbol_spans => symbol_spans_mut: Span,
symbol_flags => symbol_flags_mut: SymbolFlags,
symbol_scope_ids => symbol_scope_ids_mut: ScopeId,
symbol_declarations => symbol_declarations_mut: NodeId,
}
}
/// # Symbol Table and Scope Tree
///
/// ## Symbol Table
@@ -56,26 +86,16 @@ impl CloneIn<'_> for Redeclaration {
/// - Scopes can have 0 or more child scopes.
/// - Nodes that create a scope store the [`ScopeId`] of the scope they create.
pub struct Scoping {
/* Symbol Table Fields */
pub(crate) symbol_spans: IndexVec<SymbolId, Span>,
pub(crate) symbol_flags: IndexVec<SymbolId, SymbolFlags>,
pub(crate) symbol_scope_ids: IndexVec<SymbolId, ScopeId>,
/// Pointer to the AST Node where this symbol is declared
pub(crate) symbol_declarations: IndexVec<SymbolId, NodeId>,
/* Symbol Table - single allocation for all symbol-indexed flat fields */
symbol_table: SymbolTable,
pub(crate) references: IndexVec<ReferenceId, Reference>,
/// Function or Variable Symbol IDs that are marked with `@__NO_SIDE_EFFECTS__`.
pub(crate) no_side_effects: FxHashSet<SymbolId>,
/* Scope Tree Fields */
/// Maps a scope to the parent scope it belongs in.
scope_parent_ids: IndexVec<ScopeId, Option<ScopeId>>,
/// Maps a scope to its node id.
scope_node_ids: IndexVec<ScopeId, NodeId>,
scope_flags: IndexVec<ScopeId, ScopeFlags>,
/* Scope Tree - single allocation for all scope-indexed flat fields */
scope_table: ScopeTable,
pub(crate) cell: ScopingCell,
}
@@ -89,15 +109,10 @@ impl fmt::Debug for Scoping {
impl Default for Scoping {
fn default() -> Self {
Self {
symbol_spans: IndexVec::new(),
symbol_flags: IndexVec::new(),
symbol_scope_ids: IndexVec::new(),
symbol_declarations: IndexVec::new(),
symbol_table: SymbolTable::new(),
references: IndexVec::new(),
no_side_effects: FxHashSet::default(),
scope_parent_ids: IndexVec::new(),
scope_node_ids: IndexVec::new(),
scope_flags: IndexVec::new(),
scope_table: ScopeTable::new(),
cell: ScopingCell::new(Allocator::default(), |allocator| ScopingInner {
symbol_names: ArenaVec::new_in(allocator),
resolved_references: ArenaVec::new_in(allocator),
@@ -272,13 +287,13 @@ impl Scoping {
/// Returns the number of symbols in this table.
#[inline]
pub fn symbols_len(&self) -> usize {
self.symbol_spans.len()
self.symbol_table.len()
}
/// Returns `true` if this table contains no symbols.
#[inline]
pub fn symbols_is_empty(&self) -> bool {
self.symbol_spans.is_empty()
self.symbol_table.is_empty()
}
pub fn symbol_names(&self) -> impl Iterator<Item = &str> + '_ {
@@ -296,7 +311,7 @@ impl Scoping {
///
/// [`ScopeTree::iter_bindings_in`]: crate::scoping::Scoping::iter_bindings_in
pub fn symbol_ids(&self) -> impl Iterator<Item = SymbolId> + '_ {
self.symbol_spans.iter_enumerated().map(|(symbol_id, _)| symbol_id)
self.symbol_table.iter_ids()
}
/// Get the [`Span`] of the [`AstNode`] declaring a symbol.
@@ -304,7 +319,7 @@ impl Scoping {
/// [`AstNode`]: crate::node::AstNode
#[inline]
pub fn symbol_span(&self, symbol_id: SymbolId) -> Span {
self.symbol_spans[symbol_id]
*self.symbol_table.symbol_spans(symbol_id)
}
/// Get the identifier name a symbol is bound to.
@@ -334,13 +349,13 @@ impl Scoping {
/// To find how a symbol is used, use [`Scoping::get_resolved_references`].
#[inline]
pub fn symbol_flags(&self, symbol_id: SymbolId) -> SymbolFlags {
self.symbol_flags[symbol_id]
*self.symbol_table.symbol_flags(symbol_id)
}
/// Get a mutable reference to a symbol's [flags](SymbolFlags).
#[inline]
pub fn symbol_flags_mut(&mut self, symbol_id: SymbolId) -> &mut SymbolFlags {
&mut self.symbol_flags[symbol_id]
self.symbol_table.symbol_flags_mut(symbol_id)
}
#[inline]
@@ -356,17 +371,17 @@ impl Scoping {
#[inline]
pub fn union_symbol_flag(&mut self, symbol_id: SymbolId, includes: SymbolFlags) {
self.symbol_flags[symbol_id] |= includes;
*self.symbol_table.symbol_flags_mut(symbol_id) |= includes;
}
#[inline]
pub fn set_symbol_scope_id(&mut self, symbol_id: SymbolId, scope_id: ScopeId) {
self.symbol_scope_ids[symbol_id] = scope_id;
*self.symbol_table.symbol_scope_ids_mut(symbol_id) = scope_id;
}
#[inline]
pub fn symbol_scope_id(&self, symbol_id: SymbolId) -> ScopeId {
self.symbol_scope_ids[symbol_id]
*self.symbol_table.symbol_scope_ids(symbol_id)
}
/// Get the ID of the AST node declaring a symbol.
@@ -381,7 +396,7 @@ impl Scoping {
/// [`BindingPattern`]: oxc_ast::ast::BindingPattern
#[inline]
pub fn symbol_declaration(&self, symbol_id: SymbolId) -> NodeId {
self.symbol_declarations[symbol_id]
*self.symbol_table.symbol_declarations(symbol_id)
}
pub fn create_symbol(
@@ -396,10 +411,7 @@ impl Scoping {
cell.symbol_names.push(name.clone_in(allocator));
cell.resolved_references.push(ArenaVec::new_in(allocator));
});
self.symbol_spans.push(span);
self.symbol_flags.push(flags);
self.symbol_scope_ids.push(scope_id);
self.symbol_declarations.push(node_id)
self.symbol_table.push(span, flags, scope_id, node_id)
}
pub fn add_symbol_redeclaration(
@@ -495,7 +507,7 @@ impl Scoping {
/// If symbol is `const`, always returns `false`.
/// Otherwise, returns `true` if the symbol is assigned to somewhere in AST.
pub fn symbol_is_mutated(&self, symbol_id: SymbolId) -> bool {
if self.symbol_flags[symbol_id].contains(SymbolFlags::ConstVariable) {
if self.symbol_table.symbol_flags(symbol_id).contains(SymbolFlags::ConstVariable) {
false
} else {
self.get_resolved_references(symbol_id).any(Reference::is_write)
@@ -538,22 +550,17 @@ impl Scoping {
additional_references: usize,
additional_scopes: usize,
) {
self.symbol_spans.reserve(additional_symbols);
self.symbol_flags.reserve(additional_symbols);
self.symbol_scope_ids.reserve(additional_symbols);
self.symbol_declarations.reserve(additional_symbols);
self.symbol_table.reserve(additional_symbols);
self.cell.with_dependent_mut(|_allocator, cell| {
cell.symbol_names.reserve_exact(additional_symbols);
cell.resolved_references.reserve_exact(additional_symbols);
});
self.references.reserve(additional_references);
self.scope_parent_ids.reserve(additional_scopes);
self.scope_flags.reserve(additional_scopes);
self.scope_table.reserve(additional_scopes);
self.cell.with_dependent_mut(|_allocator, cell| {
cell.bindings.reserve(additional_scopes);
});
self.scope_node_ids.reserve(additional_scopes);
}
pub fn no_side_effects(&self) -> &FxHashSet<SymbolId> {
@@ -569,7 +576,7 @@ impl Scoping {
/// program scope.
#[inline]
pub fn scopes_len(&self) -> usize {
self.scope_parent_ids.len()
self.scope_table.len()
}
/// Returns `true` if there are no scopes in the program.
@@ -578,7 +585,7 @@ impl Scoping {
/// since there is a root scope.
#[inline]
pub fn scopes_is_empty(&self) -> bool {
self.scope_parent_ids.is_empty()
self.scope_table.is_empty()
}
/// Iterate over the scopes that contain a scope.
@@ -586,11 +593,11 @@ impl Scoping {
/// The first element of this iterator will be the scope itself. This
/// guarantees the iterator will have at least 1 element.
pub fn scope_ancestors(&self, scope_id: ScopeId) -> impl Iterator<Item = ScopeId> + '_ {
std::iter::successors(Some(scope_id), |&scope_id| self.scope_parent_ids[scope_id])
std::iter::successors(Some(scope_id), |&scope_id| *self.scope_table.parent_ids(scope_id))
}
pub fn scope_descendants_from_root(&self) -> impl Iterator<Item = ScopeId> + '_ {
self.scope_parent_ids.iter_enumerated().map(|(scope_id, _)| scope_id)
self.scope_table.iter_ids()
}
/// Get the root [`Program`] scope id.
@@ -607,7 +614,7 @@ impl Scoping {
/// This is a shorthand for `scope.get_flags(scope.root_scope_id())`.
#[inline]
pub fn root_scope_flags(&self) -> ScopeFlags {
self.scope_flags[self.root_scope_id()]
*self.scope_table.flags(self.root_scope_id())
}
#[inline]
@@ -657,12 +664,12 @@ impl Scoping {
#[inline]
pub fn scope_flags(&self, scope_id: ScopeId) -> ScopeFlags {
self.scope_flags[scope_id]
*self.scope_table.flags(scope_id)
}
#[inline]
pub fn scope_flags_mut(&mut self, scope_id: ScopeId) -> &mut ScopeFlags {
&mut self.scope_flags[scope_id]
self.scope_table.flags_mut(scope_id)
}
/// Get [`ScopeFlags`] for a new child scope under `parent_scope_id`.
@@ -673,18 +680,18 @@ impl Scoping {
#[inline]
pub fn scope_parent_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
self.scope_parent_ids[scope_id]
*self.scope_table.parent_ids(scope_id)
}
pub fn set_scope_parent_id(&mut self, scope_id: ScopeId, parent_id: Option<ScopeId>) {
self.scope_parent_ids[scope_id] = parent_id;
*self.scope_table.parent_ids_mut(scope_id) = parent_id;
}
/// Change the parent scope of a scope.
///
/// This will also remove the scope from the child list of the old parent and add it to the new parent.
pub fn change_scope_parent_id(&mut self, scope_id: ScopeId, new_parent_id: Option<ScopeId>) {
self.scope_parent_ids[scope_id] = new_parent_id;
*self.scope_table.parent_ids_mut(scope_id) = new_parent_id;
}
/// Get a variable binding by name that was declared in the top-level scope
@@ -730,7 +737,7 @@ impl Scoping {
if let Some(symbol_id) = cell.bindings[scope_id].get(&name) {
return Some(*symbol_id);
}
let parent_scope_id = self.scope_parent_ids[scope_id]?;
let parent_scope_id = (*self.scope_table.parent_ids(scope_id))?;
scope_id = parent_scope_id;
}
}
@@ -746,7 +753,7 @@ impl Scoping {
/// [`AstNode`]: crate::AstNode
#[inline]
pub fn get_node_id(&self, scope_id: ScopeId) -> NodeId {
self.scope_node_ids[scope_id]
*self.scope_table.node_ids(scope_id)
}
/// Iterate over all bindings declared in the entire program.
@@ -785,13 +792,10 @@ impl Scoping {
node_id: NodeId,
flags: ScopeFlags,
) -> ScopeId {
let scope_id = self.scope_parent_ids.push(parent_id);
self.scope_flags.push(flags);
let scope_id = self.scope_table.push(parent_id, node_id, flags);
self.cell.with_dependent_mut(|allocator, cell| {
cell.bindings.push(Bindings::new_in(allocator));
});
self.scope_node_ids.push(node_id);
scope_id
}
@@ -872,7 +876,7 @@ impl Scoping {
self.cell.with_dependent_mut(|_allocator, cell| {
for bindings in &mut cell.bindings {
bindings.retain(|_name, symbol_id| {
let flags = self.symbol_flags[*symbol_id];
let flags = *self.symbol_table.symbol_flags(*symbol_id);
!flags.intersects(
SymbolFlags::TypeAlias
| SymbolFlags::Interface
@@ -892,15 +896,10 @@ impl Scoping {
let cell = self.cell.borrow_dependent();
Self {
symbol_spans: self.symbol_spans.clone(),
symbol_flags: self.symbol_flags.clone(),
symbol_scope_ids: self.symbol_scope_ids.clone(),
symbol_declarations: self.symbol_declarations.clone(),
symbol_table: self.symbol_table.clone(),
references: self.references.clone(),
no_side_effects: self.no_side_effects.clone(),
scope_parent_ids: self.scope_parent_ids.clone(),
scope_node_ids: self.scope_node_ids.clone(),
scope_flags: self.scope_flags.clone(),
scope_table: self.scope_table.clone(),
cell: {
let allocator = Allocator::with_capacity(used_bytes);
ScopingCell::new(allocator, |allocator| ScopingInner {
@@ -1,14 +1,14 @@
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs | Arena bytes
-------------------------------------------------------------------------------------------------------------------------------------------
checker.ts | 2.92 MB || 3990 | 1675 || 152600 | 28244
checker.ts | 2.92 MB || 3981 | 1672 || 152600 | 28244
cal.com.tsx | 1.06 MB || 21108 | 474 || 37138 | 4586
cal.com.tsx | 1.06 MB || 21099 | 471 || 37138 | 4586
RadixUIAdoptionSection.jsx | 2.52 kB || 52 | 0 || 30 | 6
RadixUIAdoptionSection.jsx | 2.52 kB || 42 | 0 || 30 | 6
pdf.mjs | 567.30 kB || 4680 | 572 || 47462 | 7734
pdf.mjs | 567.30 kB || 4671 | 569 || 47462 | 7734
antd.js | 6.69 MB || 10703 | 2517 || 331644 | 69358
antd.js | 6.69 MB || 10694 | 2514 || 331644 | 69358
binder.ts | 193.08 kB || 416 | 123 || 7075 | 824
binder.ts | 193.08 kB || 407 | 120 || 7075 | 824
@@ -1,14 +1,14 @@
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs | Arena bytes
-------------------------------------------------------------------------------------------------------------------------------------------
checker.ts | 2.92 MB || 2094 | 1017 || 0 | 0
checker.ts | 2.92 MB || 2089 | 1017 || 0 | 0
cal.com.tsx | 1.06 MB || 14994 | 208 || 0 | 0
cal.com.tsx | 1.06 MB || 14989 | 208 || 0 | 0
RadixUIAdoptionSection.jsx | 2.52 kB || 18 | 0 || 0 | 0
RadixUIAdoptionSection.jsx | 2.52 kB || 13 | 0 || 0 | 0
pdf.mjs | 567.30 kB || 1752 | 286 || 0 | 0
pdf.mjs | 567.30 kB || 1747 | 286 || 0 | 0
antd.js | 6.69 MB || 4129 | 503 || 0 | 0
antd.js | 6.69 MB || 4124 | 503 || 0 | 0
binder.ts | 193.08 kB || 211 | 67 || 0 | 0
binder.ts | 193.08 kB || 206 | 67 || 0 | 0