mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
6 files changed
+524
-80
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user