diff --git a/crates/biome_js_semantic/src/events.rs b/crates/biome_js_semantic/src/events.rs index cef9cfc99038..3fa6ebbb1583 100644 --- a/crates/biome_js_semantic/src/events.rs +++ b/crates/biome_js_semantic/src/events.rs @@ -13,6 +13,8 @@ use std::collections::VecDeque; use std::mem; use JsSyntaxKind::*; +use crate::ScopeId; + /// Events emitted by the [SemanticEventExtractor]. /// These events are later made into the Semantic Model. #[derive(Debug, Eq, PartialEq)] @@ -25,8 +27,8 @@ pub enum SemanticEvent { /// - Type parameters DeclarationFound { range: TextRange, - scope_id: u32, - hoisted_scope_id: Option, + scope_id: ScopeId, + hoisted_scope_id: Option, }, /// Tracks where a symbol is read, but only if its declaration is before this reference. @@ -35,7 +37,7 @@ pub enum SemanticEvent { Read { range: TextRange, declaration_at: TextSize, - scope_id: u32, + scope_id: ScopeId, }, /// Tracks where a symbol is read, but only if its declaration was hoisted. @@ -44,7 +46,7 @@ pub enum SemanticEvent { HoistedRead { range: TextRange, declaration_at: TextSize, - scope_id: u32, + scope_id: ScopeId, }, /// Tracks where a symbol is written, but only if its declaration is before this reference. @@ -53,7 +55,7 @@ pub enum SemanticEvent { Write { range: TextRange, declaration_at: TextSize, - scope_id: u32, + scope_id: ScopeId, }, /// Tracks where a symbol is written, but only if its declaration was hoisted. @@ -63,7 +65,7 @@ pub enum SemanticEvent { HoistedWrite { range: TextRange, declaration_at: TextSize, - scope_id: u32, + scope_id: ScopeId, }, /// Tracks references that do no have any matching binding @@ -78,8 +80,7 @@ pub enum SemanticEvent { ScopeStarted { /// Scope range range: TextRange, - scope_id: u32, - parent_scope_id: Option, + parent_scope_id: Option, is_closure: bool, }, @@ -90,7 +91,7 @@ pub enum SemanticEvent { ScopeEnded { /// Scope range range: TextRange, - scope_id: u32, + scope_id: ScopeId, }, /// Tracks where a symbol is exported. @@ -156,7 +157,7 @@ pub struct SemanticEventExtractor { scopes: Vec, /// Number of generated scopes /// This allows assigning a unique id to every scope. - scope_count: u32, + scope_count: usize, /// At any point this is the set of available bindings and their range in the current scope bindings: FxHashMap, /// Type parameters bound in a `infer T` clause. @@ -274,7 +275,7 @@ enum ScopeHoisting { #[derive(Debug)] struct Scope { - scope_id: u32, + scope_id: ScopeId, /// All bindings declared inside this scope bindings: Vec, /// References that still needs to be bound and will be solved at the end of the scope @@ -780,11 +781,10 @@ impl SemanticEventExtractor { } fn push_scope(&mut self, range: TextRange, hoisting: ScopeHoisting, is_closure: bool) { - let scope_id = self.scope_count; + let scope_id = ScopeId::new(self.scope_count); self.scope_count += 1; self.stash.push_back(SemanticEvent::ScopeStarted { range, - scope_id, parent_scope_id: self.scopes.iter().last().map(|x| x.scope_id), is_closure, }); @@ -913,13 +913,13 @@ impl SemanticEventExtractor { SemanticEvent::Read { range, declaration_at, - scope_id: 0, + scope_id: ScopeId::new(0), } } else { SemanticEvent::HoistedRead { range, declaration_at, - scope_id: 0, + scope_id: ScopeId::new(0), } }; self.stash.push_back(event); @@ -979,7 +979,7 @@ impl SemanticEventExtractor { /// /// This method when called inside the `f` scope will return /// the `f` scope index. - fn scope_index_to_hoist_declarations(&mut self, skip: u32) -> Option { + fn scope_index_to_hoist_declarations(&mut self, skip: u32) -> Option { debug_assert!(self.scopes.len() > (skip as usize)); // We should at least have the global scope // that do not hoist @@ -999,7 +999,7 @@ impl SemanticEventExtractor { /// Push the binding `binding` into the hoisted scope if it exists, or into the current scope. fn push_binding( &mut self, - hoisted_scope_id: Option, + hoisted_scope_id: Option, binding_name: BindingName, binding_info: BindingInfo, ) { diff --git a/crates/biome_js_semantic/src/semantic_model/builder.rs b/crates/biome_js_semantic/src/semantic_model/builder.rs index 44a00e39f4f4..7725b877f69a 100644 --- a/crates/biome_js_semantic/src/semantic_model/builder.rs +++ b/crates/biome_js_semantic/src/semantic_model/builder.rs @@ -16,8 +16,8 @@ pub struct SemanticModelBuilder { globals: Vec, globals_by_name: FxHashMap>, scopes: Vec, - scope_range_by_start: FxHashMap>>, - scope_hoisted_to_by_range: FxHashMap, + scope_range_by_start: FxHashMap>>, + scope_hoisted_to_by_range: FxHashMap, bindings: Vec, /// maps a binding range start to its index inside SemanticModelBuilder::bindings vec bindings_by_start: FxHashMap, @@ -128,11 +128,10 @@ impl SemanticModelBuilder { ScopeStarted { range, parent_scope_id, - scope_id, is_closure, } => { // Scopes will be raised in order - debug_assert!((scope_id as usize) == self.scopes.len()); + let scope_id = ScopeId::new(self.scopes.len()); self.scopes.push(SemanticModelScopeData { range, @@ -146,9 +145,7 @@ impl SemanticModelBuilder { }); if let Some(parent_scope_id) = parent_scope_id { - self.scopes[parent_scope_id as usize] - .children - .push(scope_id); + self.scopes[parent_scope_id.index()].children.push(scope_id); } let start = range.start(); @@ -171,7 +168,7 @@ impl SemanticModelBuilder { // SAFETY: this scope id is guaranteed to exist because they were generated by the // event extractor - debug_assert!((binding_scope_id as usize) < self.scopes.len()); + debug_assert!((binding_scope_id.index()) < self.scopes.len()); let binding_id = self.bindings.len() as u32; self.bindings.push(SemanticModelBindingData { @@ -181,7 +178,7 @@ impl SemanticModelBuilder { }); self.bindings_by_start.insert(range.start(), binding_id); - let scope = &mut self.scopes[binding_scope_id as usize]; + let scope = &mut self.scopes[binding_scope_id.index()]; scope.bindings.push(binding_id); // Handle bindings with a bogus name @@ -214,7 +211,7 @@ impl SemanticModelBuilder { ty: SemanticModelReferenceType::Read { hoisted: false }, }); - let scope = &mut self.scopes[scope_id as usize]; + let scope = &mut self.scopes[scope_id.index()]; scope.read_references.push(SemanticModelScopeReference { binding_id, reference_id: reference_index, @@ -237,7 +234,7 @@ impl SemanticModelBuilder { ty: SemanticModelReferenceType::Read { hoisted: true }, }); - let scope = &mut self.scopes[scope_id as usize]; + let scope = &mut self.scopes[scope_id.index()]; scope.read_references.push(SemanticModelScopeReference { binding_id, reference_id: reference_index, @@ -260,7 +257,7 @@ impl SemanticModelBuilder { ty: SemanticModelReferenceType::Write { hoisted: false }, }); - let scope = &mut self.scopes[scope_id as usize]; + let scope = &mut self.scopes[scope_id.index()]; scope.read_references.push(SemanticModelScopeReference { binding_id, reference_id: reference_index, @@ -283,7 +280,7 @@ impl SemanticModelBuilder { ty: SemanticModelReferenceType::Write { hoisted: true }, }); - let scope = &mut self.scopes[scope_id as usize]; + let scope = &mut self.scopes[scope_id.index()]; scope.read_references.push(SemanticModelScopeReference { binding_id, reference_id: reference_index, diff --git a/crates/biome_js_semantic/src/semantic_model/closure.rs b/crates/biome_js_semantic/src/semantic_model/closure.rs index 25e8760c24d5..e85ca05c1397 100644 --- a/crates/biome_js_semantic/src/semantic_model/closure.rs +++ b/crates/biome_js_semantic/src/semantic_model/closure.rs @@ -120,7 +120,7 @@ impl Capture { pub struct AllCapturesIter { data: Rc, closure_range: TextRange, - scopes: Vec, + scopes: Vec, references: Vec, } @@ -143,7 +143,7 @@ impl Iterator for AllCapturesIter { } 'scopes: while let Some(scope_id) = self.scopes.pop() { - let scope = &self.data.scopes[scope_id as usize]; + let scope = &self.data.scopes[scope_id.index()]; if scope.is_closure { continue 'scopes; @@ -167,7 +167,7 @@ impl FusedIterator for AllCapturesIter {} /// Iterate all immediate children closures of a specific closure pub struct ChildrenIter { data: Rc, - scopes: Vec, + scopes: Vec, } impl Iterator for ChildrenIter { @@ -175,7 +175,7 @@ impl Iterator for ChildrenIter { fn next(&mut self) -> Option { while let Some(scope_id) = self.scopes.pop() { - let scope = &self.data.scopes[scope_id as usize]; + let scope = &self.data.scopes[scope_id.index()]; if scope.is_closure { return Some(Closure { data: self.data.clone(), @@ -195,7 +195,7 @@ impl FusedIterator for ChildrenIter {} /// Iterate all descendents closures of a specific closure pub struct DescendentsIter { data: Rc, - scopes: Vec, + scopes: Vec, } impl Iterator for DescendentsIter { @@ -203,7 +203,7 @@ impl Iterator for DescendentsIter { fn next(&mut self) -> Option { while let Some(scope_id) = self.scopes.pop() { - let scope = &self.data.scopes[scope_id as usize]; + let scope = &self.data.scopes[scope_id.index()]; self.scopes.extend(scope.children.iter()); if scope.is_closure { return Some(Closure { @@ -223,7 +223,7 @@ impl FusedIterator for DescendentsIter {} #[derive(Clone)] pub struct Closure { data: Rc, - scope_id: u32, + scope_id: ScopeId, } impl Closure { @@ -234,8 +234,8 @@ impl Closure { Closure { data, scope_id } } - pub(super) fn from_scope(data: Rc, scope_id: u32) -> Option { - let node = &data.scope_node_by_range[&data.scopes[scope_id as usize].range]; + pub(super) fn from_scope(data: Rc, scope_id: ScopeId) -> Option { + let node = &data.scope_node_by_range[&data.scopes[scope_id.index()].range]; match node.kind() { JsSyntaxKind::JS_FUNCTION_DECLARATION | JsSyntaxKind::JS_FUNCTION_EXPRESSION @@ -246,7 +246,7 @@ impl Closure { /// Range of this [Closure] pub fn closure_range(&self) -> &TextRange { - &self.data.scopes[self.scope_id as usize].range + &self.data.scopes[self.scope_id.index()].range } /// Return all [Reference] this closure captures, not taking into @@ -263,7 +263,7 @@ impl Closure { /// assert!(model.closure(function_f).all_captures(), &["a"]); /// ``` pub fn all_captures(&self) -> impl Iterator { - let scope = &self.data.scopes[self.scope_id as usize]; + let scope = &self.data.scopes[self.scope_id.index()]; let scopes = scope.children.clone(); @@ -293,7 +293,7 @@ impl Closure { /// assert!(model.closure(function_f).children(), &["g"]); /// ``` pub fn children(&self) -> impl Iterator { - let scope = &self.data.scopes[self.scope_id as usize]; + let scope = &self.data.scopes[self.scope_id.index()]; ChildrenIter { data: self.data.clone(), scopes: scope.children.clone(), diff --git a/crates/biome_js_semantic/src/semantic_model/model.rs b/crates/biome_js_semantic/src/semantic_model/model.rs index c99ffac49b63..32f23a46fdae 100644 --- a/crates/biome_js_semantic/src/semantic_model/model.rs +++ b/crates/biome_js_semantic/src/semantic_model/model.rs @@ -25,6 +25,32 @@ impl From<(BindingIndex, u32)> for ReferenceIndex { } } +// We use `NonZeroU32` to allow niche optimizations. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ScopeId(std::num::NonZeroU32); + +// We don't implement `From for ScopeId` and `From for usize` +// to ensure that the API consumers don't create `ScopeId`. +impl ScopeId { + pub(crate) fn new(index: usize) -> Self { + // SAFETY: We didn't handle files execedding `u32::MAX` bytes. + // Thus, it isn't possible to execedd `u32::MAX` scopes. + // + // Adding 1 ensurtes that the value is never equal to 0. + // Instead of adding 1, we could XOR the value with `u32::MAX`. + // This is what the [nonmax](https://docs.rs/nonmax/latest/nonmax/) crate does. + // However, this doesn't preserve the order. + // It is why we opted for adding 1. + Self(unsafe { std::num::NonZeroU32::new_unchecked(index.unchecked_add(1) as u32) }) + } + + pub(crate) fn index(self) -> usize { + // SAFETY: The internal representation ensures that the value is never equal to 0. + // Thus, it is safe to substract 1. + (unsafe { self.0.get().unchecked_sub(1) }) as usize + } +} + /// Contains all the data of the [SemanticModel] and only lives behind an [Arc]. /// /// That allows any returned struct (like [Scope], [Binding]) @@ -34,9 +60,9 @@ pub(crate) struct SemanticModelData { pub(crate) root: AnyJsRoot, // All scopes of this model pub(crate) scopes: Vec, - pub(crate) scope_by_range: rust_lapper::Lapper, + pub(crate) scope_by_range: rust_lapper::Lapper, // Maps the start of a node range to a scope id - pub(crate) scope_hoisted_to_by_range: FxHashMap, + pub(crate) scope_hoisted_to_by_range: FxHashMap, // Map to each by its range pub(crate) binding_node_by_start: FxHashMap, pub(crate) scope_node_by_range: FxHashMap, @@ -69,7 +95,7 @@ impl SemanticModelData { binding.references.get(index.1 as usize + 1) } - pub(crate) fn scope(&self, range: &TextRange) -> u32 { + pub(crate) fn scope(&self, range: &TextRange) -> ScopeId { let start = range.start().into(); let end = range.end().into(); let scopes = self @@ -85,7 +111,7 @@ impl SemanticModelData { } } - fn scope_hoisted_to(&self, range: &TextRange) -> Option { + fn scope_hoisted_to(&self, range: &TextRange) -> Option { self.scope_hoisted_to_by_range.get(&range.start()).copied() } @@ -123,7 +149,7 @@ impl SemanticModel { pub fn scopes(&self) -> impl Iterator + '_ { self.data.scopes.iter().enumerate().map(|(id, _)| Scope { data: self.data.clone(), - id: id as u32, + id: ScopeId::new(id), }) } @@ -131,7 +157,7 @@ impl SemanticModel { pub fn global_scope(&self) -> Scope { Scope { data: self.data.clone(), - id: 0, + id: ScopeId::new(0), } } diff --git a/crates/biome_js_semantic/src/semantic_model/scope.rs b/crates/biome_js_semantic/src/semantic_model/scope.rs index 96102a4e25e5..ff41014548f1 100644 --- a/crates/biome_js_semantic/src/semantic_model/scope.rs +++ b/crates/biome_js_semantic/src/semantic_model/scope.rs @@ -9,9 +9,9 @@ pub(crate) struct SemanticModelScopeData { // The scope range pub(crate) range: TextRange, // The parent scope of this scope - pub(crate) parent: Option, + pub(crate) parent: Option, // All children scope of this scope - pub(crate) children: Vec, + pub(crate) children: Vec, // All bindings of this scope (points to SemanticModelData::bindings) pub(crate) bindings: Vec, // Map pointing to the [bindings] vec of each bindings by its name @@ -29,7 +29,7 @@ pub(crate) struct SemanticModelScopeData { #[derive(Clone, Debug)] pub struct Scope { pub(crate) data: Rc, - pub(crate) id: u32, + pub(crate) id: ScopeId, } impl PartialEq for Scope { @@ -63,9 +63,9 @@ impl Scope { pub fn parent(&self) -> Option { // id will always be a valid scope because // it was created by [SemanticModel::scope] method. - debug_assert!((self.id as usize) < self.data.scopes.len()); + debug_assert!((self.id.index()) < self.data.scopes.len()); - let parent = self.data.scopes[self.id as usize].parent?; + let parent = self.data.scopes[self.id.index()].parent?; Some(Scope { data: self.data.clone(), id: parent, @@ -85,7 +85,7 @@ impl Scope { /// Returns a binding by its name, like it appears on code. It **does /// not** returns bindings of parent scopes. pub fn get_binding(&self, name: impl AsRef) -> Option { - let data = &self.data.scopes[self.id as usize]; + let data = &self.data.scopes[self.id.index()]; let name = name.as_ref(); let id = data.bindings_by_name.get(name)?; @@ -108,7 +108,7 @@ impl Scope { } pub fn range(&self) -> &TextRange { - &self.data.scopes[self.id as usize].range + &self.data.scopes[self.id.index()].range } pub fn syntax(&self) -> &JsSyntaxNode { @@ -135,7 +135,7 @@ pub(crate) struct SemanticModelScopeReference { /// Iterate all descendents scopes of the specified scope in breadth-first order. pub struct ScopeDescendentsIter { data: Rc, - q: VecDeque, + q: VecDeque, } impl Iterator for ScopeDescendentsIter { @@ -143,7 +143,7 @@ impl Iterator for ScopeDescendentsIter { fn next(&mut self) -> Option { if let Some(id) = self.q.pop_front() { - let scope = &self.data.scopes[id as usize]; + let scope = &self.data.scopes[id.index()]; self.q.extend(scope.children.iter()); Some(Scope { data: self.data.clone(), @@ -162,7 +162,7 @@ impl FusedIterator for ScopeDescendentsIter {} #[derive(Debug)] pub struct ScopeBindingsIter { data: Rc, - scope_id: u32, + scope_id: ScopeId, binding_index: u32, } @@ -172,9 +172,9 @@ impl Iterator for ScopeBindingsIter { fn next(&mut self) -> Option { // scope_id will always be a valid scope because // it was created by [Scope::bindings] method. - debug_assert!((self.scope_id as usize) < self.data.scopes.len()); + debug_assert!((self.scope_id.index()) < self.data.scopes.len()); - let id = self.data.scopes[self.scope_id as usize] + let id = self.data.scopes[self.scope_id.index()] .bindings .get(self.binding_index as usize)?; @@ -191,9 +191,9 @@ impl ExactSizeIterator for ScopeBindingsIter { fn len(&self) -> usize { // scope_id will always be a valid scope because // it was created by [Scope::bindings] method. - debug_assert!((self.scope_id as usize) < self.data.scopes.len()); + debug_assert!((self.scope_id.index()) < self.data.scopes.len()); - self.data.scopes[self.scope_id as usize].bindings.len() + self.data.scopes[self.scope_id.index()].bindings.len() } } diff --git a/crates/biome_js_semantic/src/tests/assertions.rs b/crates/biome_js_semantic/src/tests/assertions.rs index 19d4ed4668ab..b6c9342a6686 100644 --- a/crates/biome_js_semantic/src/tests/assertions.rs +++ b/crates/biome_js_semantic/src/tests/assertions.rs @@ -125,17 +125,15 @@ pub fn assert(code: &str, test_name: &str) { let mut events_by_pos: FxHashMap> = FxHashMap::default(); let mut declaration_range_by_start: FxHashMap = FxHashMap::default(); - let mut scope_range_by_id: FxHashMap = FxHashMap::default(); + let mut scope_ranges: Vec = vec![]; for event in semantic_events(r.syntax()) { let pos = match event { SemanticEvent::DeclarationFound { range, .. } => { declaration_range_by_start.insert(range.start(), range); range.start() } - SemanticEvent::ScopeStarted { - range, scope_id, .. - } => { - scope_range_by_id.insert(scope_id, range); + SemanticEvent::ScopeStarted { range, .. } => { + scope_ranges.push(range); range.start() } SemanticEvent::Read { range, .. } @@ -159,7 +157,7 @@ pub fn assert(code: &str, test_name: &str) { test_name, events_by_pos, declaration_range_by_start, - scope_range_by_id, + scope_ranges, ); } @@ -465,7 +463,7 @@ impl SemanticAssertions { test_name: &str, events_by_pos: FxHashMap>, declaration_range_by_start: FxHashMap, - scope_range_by_id: FxHashMap, + scope_ranges: Vec, ) { // Check every declaration assertion is ok @@ -623,8 +621,9 @@ impl SemanticAssertions { .get(&at_scope_assertion.scope_name) { Some(scope_start_assertion) => { - let scope_started_at = - &scope_range_by_id[&hoisted_scope_id.unwrap_or(*scope_id)].start(); + let scope_started_at = &scope_ranges + [hoisted_scope_id.unwrap_or(*scope_id).index()] + .start(); if scope_start_assertion.range.start() != *scope_started_at { show_all_events(test_name, code, events_by_pos, is_scope_event); show_unmatched_assertion(