perf(semantic, allocator): branchless clone_in for semantic IDs (#24564)

Follow-on after #24422.

Make cloning semantic IDs (`NodeId`, `ScopeId` etc) use branchless arithmetic.

* Cloning `NodeId` now just uses a single OR operation to take into account whether to preserve the existing ID (`clone_in_with_semantic_ids`) or substitute a dummy ID (`clone_in`).
* Cloning `Cell<Option<ScopeId>>` is 2 instructions (NOT + AND) on x86_64, or just 1 on aarch64 (NOT-AND).

https://godbolt.org/z/1KWdc5hnb

The mechanism is a type `CloneInSemanticIds` which is used as `with_semantic_ids` instead of a `bool`:

```rs
#[repr(u32)]
pub enum CloneInSemanticIds {
    With = 0,
    Without = u32::MAX,
}
```

This is 4 bytes, rather than `bool`'s 1 byte, but that makes no difference - either way it takes 1 register to pass between functions. The choice of discriminants is what enables the cheap arithmetic.

This is even smaller in terms of binary size than the previous solution of marking `Cell::clone_in_impl` as `#[inline(never)]` (OR instruction is smaller than function call), and it's more performant as it loses the function call overhead. It also keeps `Cell`'s blanket implementation of `CloneIn` generalized.

Between #24422 and this PR, merging `CloneIn` with/without semantic IDs into a single path has gained us a large binary size reduction, and lost us nothing - or may even have gained us a marginal perf improvement.

No visible change on our benchmarks, as we don't use `CloneIn` much, but it may well be a measurable improvement for Rolldown, which clones whole ASTs.
This commit is contained in:
overlookmotel committed 2026-07-16 04:14:55 +00:00
1 parent 3acf4c1789
commit ba65790029
22 files changed
+749 -437

No files matched your search

+2 -2
View File
@@ -6,7 +6,7 @@ use std::{
slice,
};
use crate::{Allocator, Box, CloneIn};
use crate::{Allocator, Box, CloneIn, CloneInSemanticIds};
const USIZE_BITS: usize = usize::BITS as usize;
@@ -177,7 +177,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for BitSet<'_> {
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> BitSet<'new_alloc> {
let slice = self.entries.as_ref();
+58 -38
View File
@@ -8,20 +8,36 @@ use std::{
use crate::{Allocator, Box, HashMap, Vec};
/// Option to pass to [`CloneIn::clone_in_impl`] to determine how to clone semantic IDs.
///
/// * [`CloneInSemanticIds::With`] to clone by copying current value.
/// * [`CloneInSemanticIds::Without`] to clone by substituting a dummy value -
/// `NonMaxU32(0)` for a plain ID, `None` for an optional ID.
///
/// This is designed so that cloning semantic IDs is branchless and cheap.
/// See `SemanticId` trait in `oxc_syntax` crate.
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum CloneInSemanticIds {
With = 0,
Without = u32::MAX,
}
/// A trait to explicitly clone an object into an arena allocator.
///
/// As a convention `Cloned` associated type should always be the same as `Self`,
/// It'd only differ in the lifetime, Here's an example:
///
/// ```
/// # use oxc_allocator::{Allocator, CloneIn, Vec};
/// # use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds, Vec};
/// # struct Struct<'a> {a: Vec<'a, u8>, b: u8}
///
/// impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for Struct<'old_alloc> {
/// type Cloned = Struct<'new_alloc>;
///
/// fn clone_in_impl(
/// &self,
/// with_semantic_ids: bool,
/// with_semantic_ids: CloneInSemanticIds,
/// allocator: &'new_alloc Allocator,
/// ) -> Self::Cloned {
/// Struct {
@@ -32,9 +48,8 @@ use crate::{Allocator, Box, HashMap, Vec};
/// }
/// ```
///
/// Implementations of this trait on non-allocated items usually short-circuit to `Clone::clone`;
/// Implementations of this trait on non-allocated items usually delegate to `Clone::clone`.
/// However, it **isn't** guaranteed.
///
pub trait CloneIn<'new_alloc>: Sized {
/// The type of the cloned object.
///
@@ -47,7 +62,7 @@ pub trait CloneIn<'new_alloc>: Sized {
#[expect(clippy::inline_always)]
#[inline(always)]
fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {
self.clone_in_impl(false, allocator)
self.clone_in_impl(CloneInSemanticIds::Without, allocator)
}
/// Almost identical as `clone_in`, but for some special type, it will also clone the semantic ids.
@@ -56,25 +71,28 @@ pub trait CloneIn<'new_alloc>: Sized {
#[expect(clippy::inline_always)]
#[inline(always)]
fn clone_in_with_semantic_ids(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {
self.clone_in_impl(true, allocator)
self.clone_in_impl(CloneInSemanticIds::With, allocator)
}
/// Clone `self` into `allocator`, threading whether semantic ids should be preserved as a
/// runtime `with_semantic_ids` flag rather than as two separate methods. This is the method
/// that implementors provide; `clone_in` and `clone_in_with_semantic_ids` are thin wrappers
/// around it.
/// runtime `with_semantic_ids` flag rather than as two separate methods.
///
/// It exists so the `CloneIn` derive can emit a *single* recursive traversal that serves both
/// `clone_in` (`with_semantic_ids == false`) and `clone_in_with_semantic_ids`
/// (`with_semantic_ids == true`), instead of monomorphizing two near-identical traversals over
/// the whole AST. Id fields recurse when `with_semantic_ids` is `true` and reset to their
/// default when it is `false`.
/// This is the method that implementors provide.
/// `clone_in` and `clone_in_with_semantic_ids` are thin wrappers around it.
///
/// Prefer calling `clone_in` or `clone_in_with_semantic_ids` at call sites — they name the
/// intent and delegate here.
/// It exists so the `CloneIn` derive can emit a *single* recursive traversal that serves both:
/// * `clone_in`: `with_semantic_ids == CloneInSemanticIds::Without`
/// * `clone_in_with_semantic_ids`: `with_semantic_ids == CloneInSemanticIds::With`
///
/// This is instead of monomorphizing two near-identical traversals over the whole AST.
/// ID fields copy existing values when `with_semantic_ids == CloneInSemanticIds::With`,
/// and reset to their default when `CloneInSemanticIds::Without`.
///
/// Prefer calling `clone_in` or `clone_in_with_semantic_ids` at call sites —
/// they name the intent and delegate here.
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned;
}
@@ -86,7 +104,11 @@ where
type Cloned = Option<C>;
#[inline]
fn clone_in_impl(&self, with_semantic_ids: bool, allocator: &'alloc Allocator) -> Self::Cloned {
fn clone_in_impl(
&self,
with_semantic_ids: CloneInSemanticIds,
allocator: &'alloc Allocator,
) -> Self::Cloned {
self.as_ref().map(|it| it.clone_in_impl(with_semantic_ids, allocator))
}
}
@@ -100,7 +122,7 @@ where
#[inline]
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Box::new_in(self.as_ref().clone_in_impl(with_semantic_ids, allocator), &allocator)
@@ -115,7 +137,7 @@ where
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
let slice = self.as_ref();
@@ -165,7 +187,7 @@ where
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
// The implementation below is equivalent to:
@@ -206,7 +228,7 @@ where
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
// Keys in original hash map are guaranteed to be unique.
@@ -227,21 +249,19 @@ where
}
}
impl<'alloc, T: Copy + Default> CloneIn<'alloc> for Cell<T> {
type Cloned = Cell<T>;
impl<'alloc, T, C> CloneIn<'alloc> for Cell<T>
where
T: Copy + CloneIn<'alloc, Cloned = C>,
{
type Cloned = Cell<C>;
// `#[inline(never)]` so the `with_semantic_ids` branch below is *not* inlined at every id-field
// clone site. Consumers that only clone *without* semantic ids (e.g. the napi transform/parse
// bindings, via `oxc_transformer`) would otherwise carry the dead preserve branch at every site
// — ~16 KiB on the transform binary. Outlining it to one function per `Cell<T>` keeps those
// binaries at their pre-`clone_in_impl` size, while consumers that clone *with* ids (rolldown,
// oxlint's react compiler) are unaffected — their win comes from the shared traversal, not this leaf.
#[inline(never)]
fn clone_in_impl(&self, with_semantic_ids: bool, _: &'alloc Allocator) -> Self::Cloned {
// `Cell` in the AST only ever holds semantic ids (`Cell<NodeId>`, `Cell<Option<ScopeId>>`,
// etc.). Preserve them when cloning *with* semantic ids, otherwise reset to the default
// (`NodeId::DUMMY`, `None`, ...) so ids aren't carried into a fresh clone.
if with_semantic_ids { Cell::new(self.get()) } else { Cell::new(T::default()) }
#[inline]
fn clone_in_impl(
&self,
with_semantic_ids: CloneInSemanticIds,
allocator: &'alloc Allocator,
) -> Self::Cloned {
Cell::new(self.get().clone_in_impl(with_semantic_ids, allocator))
}
}
@@ -250,7 +270,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for &str {
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
allocator.alloc_str(self)
@@ -263,7 +283,7 @@ macro_rules! impl_clone_in {
impl<'alloc> CloneIn<'alloc> for $t {
type Cloned = Self;
#[inline(always)]
fn clone_in_impl(&self, _with_semantic_ids: bool, _: &'alloc Allocator) -> Self {
fn clone_in_impl(&self, _with_semantic_ids: CloneInSemanticIds, _: &'alloc Allocator) -> Self {
*self
}
}
+1 -1
View File
@@ -75,7 +75,7 @@ pub use allocator::Allocator;
#[cfg(feature = "bitset")]
pub use bitset::BitSet;
pub use boxed::{Box, Box as ArenaBox};
pub use clone_in::CloneIn;
pub use clone_in::{CloneIn, CloneInSemanticIds};
pub use convert::{FromIn, IntoIn};
pub use hash_map::{HashMap, HashMap as ArenaHashMap};
pub use hash_set::{HashSet, HashSet as ArenaHashSet};
+6 -2
View File
@@ -1,6 +1,6 @@
use bitflags::bitflags;
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use oxc_ast_macros::ast;
use oxc_estree::ESTree;
use oxc_span::{ContentEq, Span};
@@ -126,7 +126,11 @@ impl ContentEq for CommentNewlines {
impl<'alloc> CloneIn<'alloc> for CommentNewlines {
type Cloned = Self;
fn clone_in_impl(&self, _with_semantic_ids: bool, _: &'alloc Allocator) -> Self::Cloned {
fn clone_in_impl(
&self,
_with_semantic_ids: CloneInSemanticIds,
_: &'alloc Allocator,
) -> Self::Cloned {
*self
}
}
+6 -2
View File
@@ -5,7 +5,7 @@ use std::{
fmt::{self, Display},
};
use oxc_allocator::{Allocator, CloneIn, Dummy};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds, Dummy};
use oxc_data_structures::inline_string::InlineString;
use oxc_span::ContentEq;
@@ -172,7 +172,11 @@ impl ContentEq for RegExpFlags {
impl<'alloc> CloneIn<'alloc> for RegExpFlags {
type Cloned = Self;
fn clone_in_impl(&self, _with_semantic_ids: bool, _: &'alloc Allocator) -> Self::Cloned {
fn clone_in_impl(
&self,
_with_semantic_ids: CloneInSemanticIds,
_: &'alloc Allocator,
) -> Self::Cloned {
*self
}
}
File diff suppressed because it is too large. Load diff
@@ -1,11 +1,15 @@
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use crate::ast::Modifier;
impl<'alloc> CloneIn<'alloc> for Modifier {
type Cloned = Self;
fn clone_in_impl(&self, _with_semantic_ids: bool, _: &'alloc Allocator) -> Self::Cloned {
fn clone_in_impl(
&self,
_with_semantic_ids: CloneInSemanticIds,
_: &'alloc Allocator,
) -> Self::Cloned {
*self
}
}
@@ -5,7 +5,7 @@
use std::cell::Cell;
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use crate::ast::*;
@@ -14,7 +14,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Pattern<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Pattern {
@@ -29,7 +29,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Disjunction<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Disjunction {
@@ -44,7 +44,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Alternative<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Alternative {
@@ -59,7 +59,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Term<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
match self {
@@ -108,7 +108,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for BoundaryAssertion {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
BoundaryAssertion {
@@ -124,7 +124,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for BoundaryAssertionKind {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -136,7 +136,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for LookAroundAssertion<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
LookAroundAssertion {
@@ -153,7 +153,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for LookAroundAssertionKind {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -165,7 +165,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Quantifier<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Quantifier {
@@ -183,7 +183,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Character {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Character {
@@ -200,7 +200,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterKind {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -212,7 +212,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterClassEscape {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
CharacterClassEscape {
@@ -228,7 +228,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterClassEscapeKind {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -240,7 +240,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for UnicodePropertyEscape<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
UnicodePropertyEscape {
@@ -258,7 +258,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Dot {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Dot { span: CloneIn::clone_in_impl(&self.span, with_semantic_ids, allocator) }
@@ -270,7 +270,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterClass<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
CharacterClass {
@@ -289,7 +289,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterClassContentsKind {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -301,7 +301,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterClassContents<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
match self {
@@ -334,7 +334,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CharacterClassRange {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
CharacterClassRange {
@@ -350,7 +350,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for ClassStringDisjunction<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
ClassStringDisjunction {
@@ -366,7 +366,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for ClassString<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
ClassString {
@@ -382,7 +382,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for CapturingGroup<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
CapturingGroup {
@@ -398,7 +398,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for IgnoreGroup<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
IgnoreGroup {
@@ -414,7 +414,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Modifiers {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Modifiers {
@@ -430,7 +430,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for IndexedReference {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
IndexedReference {
@@ -445,7 +445,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for NamedReference<'_> {
fn clone_in_impl(
&self,
with_semantic_ids: bool,
with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
NamedReference {
+10 -6
View File
@@ -3,7 +3,7 @@ use std::{collections::hash_map::Entry, fmt, mem};
use rustc_hash::{FxHashMap, FxHashSet};
use self_cell::self_cell;
use oxc_allocator::{Allocator, ArenaVec, BitSet, CloneIn};
use oxc_allocator::{Allocator, ArenaVec, BitSet, CloneIn, CloneInSemanticIds};
use oxc_index::IndexVec;
use oxc_span::Span;
use oxc_str::{ArenaIdentHashMap, Ident};
@@ -31,11 +31,15 @@ impl CloneIn<'_> for Redeclaration {
type Cloned = Self;
#[inline]
fn clone_in_impl(&self, with_semantic_ids: bool, _allocator: &Allocator) -> Self::Cloned {
if with_semantic_ids {
self.clone()
} else {
Self { span: self.span, declaration: NodeId::DUMMY, flags: self.flags }
fn clone_in_impl(
&self,
with_semantic_ids: CloneInSemanticIds,
allocator: &Allocator,
) -> Self::Cloned {
Self {
span: self.span,
declaration: self.declaration.clone_in_impl(with_semantic_ids, allocator),
flags: self.flags,
}
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ use std::{
str::FromStr,
};
use oxc_allocator::{Allocator, CloneIn, Dummy};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds, Dummy};
use oxc_ast_macros::ast;
use oxc_estree::ESTree;
@@ -103,7 +103,7 @@ impl<'a> CloneIn<'a> for SourceType {
type Cloned = Self;
#[inline]
fn clone_in_impl(&self, _with_semantic_ids: bool, _: &'a Allocator) -> Self {
fn clone_in_impl(&self, _with_semantic_ids: CloneInSemanticIds, _: &'a Allocator) -> Self {
*self
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ use miette::{LabeledSpan, SourceOffset, SourceSpan};
#[cfg(feature = "serialize")]
use serde::{Serialize, Serializer as SerdeSerializer, ser::SerializeMap};
use oxc_allocator::{Allocator, CloneIn, Dummy};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds, Dummy};
use oxc_ast_macros::ast;
use oxc_estree::ESTree;
@@ -635,7 +635,7 @@ impl<'a> CloneIn<'a> for Span {
type Cloned = Self;
#[inline]
fn clone_in_impl(&self, _with_semantic_ids: bool, _: &'a Allocator) -> Self {
fn clone_in_impl(&self, _with_semantic_ids: CloneInSemanticIds, _: &'a Allocator) -> Self {
*self
}
}
+3 -3
View File
@@ -11,8 +11,8 @@ use std::{
};
use oxc_allocator::{
Allocator, ArenaStringBuilder, CloneIn, Dummy, FromIn, GetAllocator, IdentBuildHasher,
ident_hash,
Allocator, ArenaStringBuilder, CloneIn, CloneInSemanticIds, Dummy, FromIn, GetAllocator,
IdentBuildHasher, ident_hash,
};
#[cfg(feature = "serialize")]
use oxc_estree::{ESTree, JsonSafeString, Serializer as ESTreeSerializer};
@@ -454,7 +454,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Ident<'_> {
#[inline]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
let s = allocator.alloc_str(self.as_str());
+4 -2
View File
@@ -4,7 +4,9 @@ use std::{
ops::Deref,
};
use oxc_allocator::{Allocator, ArenaStringBuilder, CloneIn, Dummy, FromIn, GetAllocator};
use oxc_allocator::{
Allocator, ArenaStringBuilder, CloneIn, CloneInSemanticIds, Dummy, FromIn, GetAllocator,
};
#[cfg(feature = "serialize")]
use oxc_estree::{ESTree, Serializer as ESTreeSerializer};
#[cfg(feature = "serialize")]
@@ -119,7 +121,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for Str<'_> {
#[inline]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
Str::from_in(self.as_str(), allocator)
@@ -5,7 +5,7 @@
use std::cell::Cell;
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use crate::number::*;
use crate::operator::*;
@@ -16,7 +16,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for NumberBase {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -29,7 +29,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for BigintBase {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -42,7 +42,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for AssignmentOperator {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -55,7 +55,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for BinaryOperator {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -68,7 +68,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for LogicalOperator {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -81,7 +81,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for UnaryOperator {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
@@ -94,7 +94,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for UpdateOperator {
#[inline(always)]
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
*self
+1
View File
@@ -17,6 +17,7 @@ pub mod operator;
pub mod precedence;
pub mod reference;
pub mod scope;
pub mod semantic_id;
#[cfg(feature = "serialize")]
mod serialize;
pub mod symbol;
+10 -10
View File
@@ -2,13 +2,16 @@
use bitflags::bitflags;
use oxc_allocator::{Allocator, CloneIn, Dummy};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds, Dummy};
use oxc_ast_macros::ast;
use oxc_index::define_nonmax_u32_index_type;
use crate::semantic_id::SemanticId;
define_nonmax_u32_index_type! {
/// AST Node ID
#[ast]
#[clone_in(semantic_id)]
#[content_eq(skip)]
#[estree(skip)]
pub struct NodeId;
@@ -41,18 +44,15 @@ impl<'a> Dummy<'a> for NodeId {
impl<'alloc> CloneIn<'alloc> for NodeId {
type Cloned = Self;
#[inline]
fn clone_in_impl(&self, with_semantic_ids: bool, _: &'alloc Allocator) -> Self {
if with_semantic_ids {
*self
} else {
// `with_semantic_ids == false` should never reach this, because `CloneIn` skips
// `node_id` field, filling it with its default instead.
unreachable!();
}
#[expect(clippy::inline_always)]
#[inline(always)] // Because this method only delegates
fn clone_in_impl(&self, with_semantic_ids: CloneInSemanticIds, _: &'alloc Allocator) -> Self {
self.clone_id(with_semantic_ids)
}
}
impl SemanticId for NodeId {}
bitflags! {
/// Contains additional information about an AST node.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+9 -12
View File
@@ -3,15 +3,16 @@ use oxc_index::define_nonmax_u32_index_type;
#[cfg(feature = "serialize")]
use serde::Serialize;
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use crate::{node::NodeId, scope::ScopeId, symbol::SymbolId};
use crate::{node::NodeId, scope::ScopeId, semantic_id::SemanticId, symbol::SymbolId};
use oxc_ast_macros::ast;
define_nonmax_u32_index_type! {
#[ast]
#[builder(default)]
#[clone_in(semantic_id)]
#[content_eq(skip)]
#[estree(skip)]
pub struct ReferenceId;
@@ -21,18 +22,14 @@ impl<'alloc> CloneIn<'alloc> for ReferenceId {
type Cloned = Self;
#[expect(clippy::inline_always)]
#[inline(always)]
fn clone_in_impl(&self, with_semantic_ids: bool, _: &'alloc Allocator) -> Self {
if with_semantic_ids {
*self
} else {
// `with_semantic_ids == false` should never reach this, because `CloneIn` skips
// `reference_id` field, filling it with its default instead.
unreachable!();
}
#[inline(always)] // Because this method only delegates
fn clone_in_impl(&self, with_semantic_ids: CloneInSemanticIds, _: &'alloc Allocator) -> Self {
self.clone_id(with_semantic_ids)
}
}
impl SemanticId for ReferenceId {}
bitflags! {
/// Describes how a symbol is being referenced in the AST.
///
@@ -226,7 +223,7 @@ impl<'alloc> CloneIn<'alloc> for ReferenceFlags {
fn clone_in_impl(
&self,
_with_semantic_ids: bool,
_with_semantic_ids: CloneInSemanticIds,
_: &'alloc oxc_allocator::Allocator,
) -> Self::Cloned {
*self
+9 -10
View File
@@ -1,12 +1,15 @@
use bitflags::bitflags;
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use oxc_index::define_nonmax_u32_index_type;
use oxc_ast_macros::ast;
use crate::semantic_id::SemanticId;
define_nonmax_u32_index_type! {
#[ast]
#[builder(default)]
#[clone_in(semantic_id)]
#[content_eq(skip)]
#[estree(skip)]
pub struct ScopeId;
@@ -16,18 +19,14 @@ impl<'alloc> CloneIn<'alloc> for ScopeId {
type Cloned = Self;
#[expect(clippy::inline_always)]
#[inline(always)]
fn clone_in_impl(&self, with_semantic_ids: bool, _: &'alloc Allocator) -> Self {
if with_semantic_ids {
*self
} else {
// `with_semantic_ids == false` should never reach this, because `CloneIn` skips
// `scope_id` field, filling it with its default instead.
unreachable!();
}
#[inline(always)] // Because this method only delegates
fn clone_in_impl(&self, with_semantic_ids: CloneInSemanticIds, _: &'alloc Allocator) -> Self {
self.clone_id(with_semantic_ids)
}
}
impl SemanticId for ScopeId {}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScopeFlags: u16 {
+173
View File
@@ -0,0 +1,173 @@
//! [`SemanticId`] trait for branchless cloning of semantic ID types.
use std::cell::Cell;
use nonmax::NonMaxU32;
use oxc_allocator::CloneInSemanticIds;
use oxc_index::Idx;
// The branchless bit-tricks in [`SemanticId`]'s methods rely on the exact discriminant values of
// [`CloneInSemanticIds`]. This crate is separate from the one that defines [`CloneInSemanticIds`],
// so assert the discriminants here rather than relying on them by proximity.
const _: () = {
assert!(CloneInSemanticIds::With as u32 == 0);
assert!(CloneInSemanticIds::Without as u32 == u32::MAX);
};
/// A semantic ID type - a [`NonMaxU32`]-backed index (`NodeId`, `ScopeId`, `SymbolId`, `ReferenceId`),
/// created by [`define_nonmax_u32_index_type!`].
///
/// Provides methods to clone a semantic ID, keeping it when cloning with semantic IDs
/// and resetting it when cloning without - branchless in both cases:
///
/// * [`clone_id`] - `CloneIn::clone_in_impl` for ID types delegates to it.
/// * [`clone_cell_option_id`] - the `CloneIn` derive calls it to clone `Cell<Option<Id>>` fields
/// (`ScopeId`, `SymbolId`, `ReferenceId`).
///
/// The [`Idx`] bound provides [`index`] and [`from_usize`], the two operations these methods need.
///
/// [`define_nonmax_u32_index_type!`]: oxc_index::define_nonmax_u32_index_type
/// [`clone_id`]: SemanticId::clone_id
/// [`clone_cell_option_id`]: SemanticId::clone_cell_option_id
/// [`index`]: Idx::index
/// [`from_usize`]: Idx::from_usize
pub trait SemanticId: Idx {
/// Clone a semantic ID.
///
/// Behavior depends on value of `with_semantic_ids`:
///
/// * [`CloneInSemanticIds::With`]: Copy the current value.
/// * [`CloneInSemanticIds::Without`]: Reset to the dummy ID `Id(0)`.
///
/// Branchless - a single OR instruction.
///
/// # How this works
///
/// `index & mask`, where `mask` is all-ones for [`With`] (discriminant `0`)
/// and all-zeros for [`Without`] (discriminant `u32::MAX`), then rebuilt.
/// `from_usize`'s bounds check is elided: `index & mask <= index <= MAX`.
///
/// <https://godbolt.org/z/1KWdc5hnb>
///
/// [`With`]: CloneInSemanticIds::With
/// [`Without`]: CloneInSemanticIds::Without
#[must_use]
#[expect(clippy::inline_always)]
#[inline(always)] // Because this method is only a single instruction
fn clone_id(&self, with_semantic_ids: CloneInSemanticIds) -> Self {
let mask = !(with_semantic_ids as u32);
// `index()` of a `NonMaxU32`-backed type always fits in `u32`, so `as u32` can't truncate
#[expect(clippy::cast_possible_truncation)]
let index = self.index() as u32;
let masked = index & mask;
Self::from_usize(masked as usize)
}
/// Clone a `Cell<Option<Id>>` semantic ID field.
///
/// Behavior depends on value of `with_semantic_ids`:
///
/// * [`CloneInSemanticIds::With`]: Copy the current value.
/// * [`CloneInSemanticIds::Without`]: Reset to `None`.
///
/// Branchless - a load plus an AND-NOT operation
/// (a single `bic` instruction on aarch64, `not` + `and` on x86_64).
///
/// <https://godbolt.org/z/1KWdc5hnb>
///
/// # How this works
///
/// The in-memory representation of `Option<Id>` (equivalently `Option<NonMaxU32>`) is:
///
/// * `None` = `0` (niche optimization - `NonMaxU32` wraps a `NonZeroU32` whose niche is 0).
/// * `Some(v)` = `!v` (`NonMaxU32` stores its value bitwise-inverted, and is never 0).
///
/// On that representation, "keep, or replace with `None`" is `bits & mask`,
/// where `mask` is all-ones for [`With`] and all-zeros for [`Without`].
///
/// The code below computes exactly that, but via safe APIs.
/// `bits` rebuilds the stored form as a plain logical value (`!id.index()` for `Some`, `0` for `None`),
/// and `NonMaxU32::new(!x)` inverts the encoding (`new` returns `None` exactly when `x == 0`).
/// Correctness is plain logic and doesn't depend on the representation - only the codegen does.
/// The compiler folds it all to the same instructions as an unsafe `transmute`-based version.
///
/// [`With`]: CloneInSemanticIds::With
/// [`Without`]: CloneInSemanticIds::Without
#[expect(clippy::inline_always)]
#[inline(always)] // Because this method is only a few instructions
fn clone_cell_option_id(
cell: &Cell<Option<Self>>,
with_semantic_ids: CloneInSemanticIds,
) -> Cell<Option<Self>> {
let mask = !(with_semantic_ids as u32);
// `index()` of a `NonMaxU32`-backed type always fits in `u32`, so `as u32` can't truncate
#[expect(clippy::cast_possible_truncation)]
let bits = match cell.get() {
Some(id) => !(id.index() as u32),
None => 0,
};
let masked = bits & mask;
let raw = NonMaxU32::new(!masked);
Cell::new(raw.map(|raw| Self::from_usize(raw.get() as usize)))
}
}
#[cfg(test)]
mod test {
use std::cell::Cell;
use oxc_allocator::CloneInSemanticIds;
use crate::node::NodeId;
use super::SemanticId;
// `NonMaxU32` cannot represent `u32::MAX`, so `u32::MAX - 1` is the largest ID we can test
const INPUTS: [u32; 11] = [
0,
1,
2,
42,
1000,
u32::MAX / 2 - 1,
u32::MAX / 2,
u32::MAX / 2 + 1,
u32::MAX - 1000,
u32::MAX - 2,
u32::MAX - 1,
];
#[test]
fn clone_id() {
for n in INPUTS {
let id = NodeId::from_usize(n as usize);
// `With` copies the current value
assert_eq!(id.clone_id(CloneInSemanticIds::With), id);
// `Without` resets to the dummy ID `NodeId(0)`
assert_eq!(id.clone_id(CloneInSemanticIds::Without), NodeId::from_usize(0));
}
}
#[test]
fn clone_cell_option_id() {
// `None` is returned unchanged by both variants
let cell = Cell::new(None);
assert_eq!(NodeId::clone_cell_option_id(&cell, CloneInSemanticIds::With).get(), None);
assert_eq!(NodeId::clone_cell_option_id(&cell, CloneInSemanticIds::Without).get(), None);
for n in INPUTS {
let id = NodeId::from_usize(n as usize);
let cell = Cell::new(Some(id));
// `With` copies the current value
assert_eq!(
NodeId::clone_cell_option_id(&cell, CloneInSemanticIds::With).get(),
Some(id)
);
// `Without` resets to `None`
assert_eq!(
NodeId::clone_cell_option_id(&cell, CloneInSemanticIds::Without).get(),
None
);
}
}
}
+9 -10
View File
@@ -1,14 +1,17 @@
use bitflags::bitflags;
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
use oxc_index::define_nonmax_u32_index_type;
#[cfg(feature = "serialize")]
use serde::Serialize;
use oxc_ast_macros::ast;
use crate::semantic_id::SemanticId;
define_nonmax_u32_index_type! {
#[ast]
#[builder(default)]
#[clone_in(semantic_id)]
#[content_eq(skip)]
#[estree(skip)]
pub struct SymbolId;
@@ -18,18 +21,14 @@ impl<'alloc> CloneIn<'alloc> for SymbolId {
type Cloned = Self;
#[expect(clippy::inline_always)]
#[inline(always)]
fn clone_in_impl(&self, with_semantic_ids: bool, _: &'alloc Allocator) -> Self {
if with_semantic_ids {
*self
} else {
// `with_semantic_ids == false` should never reach this, because `CloneIn` skips
// `symbol_id` field, filling it with its default instead.
unreachable!();
}
#[inline(always)] // Because this method only delegates
fn clone_in_impl(&self, with_semantic_ids: CloneInSemanticIds, _: &'alloc Allocator) -> Self {
self.clone_id(with_semantic_ids)
}
}
impl SemanticId for SymbolId {}
define_nonmax_u32_index_type! {
pub struct RedeclarationId;
}
+40 -16
View File
@@ -38,19 +38,23 @@ impl Derive for DeriveCloneIn {
&[("clone_in", attr_positions!(StructMaybeDerived | EnumMaybeDerived | StructField))]
}
/// Parse `#[clone_in(default)]` on struct, enum, or struct field.
/// Parse `#[clone_in(default)]` on struct, enum, or struct field,
/// or `#[clone_in(semantic_id)]` on struct.
fn parse_attr(&self, _attr_name: &str, location: AttrLocation, part: AttrPart) -> Result<()> {
// No need to check attr name is `clone_in`, because that's the only attribute this derive handles.
if !matches!(part, AttrPart::Tag("default")) {
return Err(());
}
match location {
AttrLocation::Struct(struct_def) => struct_def.clone_in.is_default = true,
AttrLocation::Enum(enum_def) => enum_def.clone_in.is_default = true,
AttrLocation::StructField(struct_def, field_index) => {
struct_def.fields[field_index].clone_in.is_default = true;
}
match part {
AttrPart::Tag("default") => match location {
AttrLocation::Struct(struct_def) => struct_def.clone_in.is_default = true,
AttrLocation::Enum(enum_def) => enum_def.clone_in.is_default = true,
AttrLocation::StructField(struct_def, field_index) => {
struct_def.fields[field_index].clone_in.is_default = true;
}
_ => return Err(()),
},
AttrPart::Tag("semantic_id") => match location {
AttrLocation::Struct(struct_def) => struct_def.clone_in.is_semantic_id = true,
_ => return Err(()),
},
_ => return Err(()),
}
@@ -65,7 +69,7 @@ impl Derive for DeriveCloneIn {
use std::cell::Cell;
///@@line_break
use oxc_allocator::{Allocator, CloneIn};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds};
}
}
@@ -88,10 +92,19 @@ fn derive_struct(struct_def: &StructDef, schema: &Schema) -> TokenStream {
let fields = struct_def.fields.iter().map(|field| {
let field_ident = field.ident();
if struct_field_is_default(field, schema) {
// Fields (or fields whose type is) marked `#[clone_in(default)]` always reset to
// their default. Semantic id `Cell`s are *not* marked — they thread the
// `with_semantic_ids` flag through their `CloneIn` impl instead (see `oxc_allocator`).
// Fields (or fields whose type is) marked `#[clone_in(default)]` always reset to their default
quote!( #field_ident: Default::default() )
} else if struct_field_is_semantic_id_cell_option(field, schema) {
// `Cell<Option<Id>>` semantic ID fields (`scope_id`, `symbol_id`, `reference_id`)
// clone via `SemanticId::clone_cell_option_id`, which keeps the ID or resets it to `None`,
// depending on `with_semantic_ids`.
//
// The generic `Option<T>` / `Cell<T>` `CloneIn` impls can't produce that reset -
// `Option<T>`'s impl maps over `Some`, which would reset `Some(id)` to `Some(dummy)`, not `None`.
//
// `SemanticId` lives in `oxc_syntax`, so it's named by its full path here rather than
// imported in the prelude (which is shared with crates that don't depend on `oxc_syntax`).
quote!( #field_ident: oxc_syntax::semantic_id::SemanticId::clone_cell_option_id(&self.#field_ident, with_semantic_ids) )
} else {
quote!( #field_ident: CloneIn::clone_in_impl(&self.#field_ident, with_semantic_ids, allocator) )
}
@@ -120,6 +133,17 @@ fn struct_field_is_default(field: &FieldDef, schema: &Schema) -> bool {
}
}
/// Get if a struct field is a `Cell<Option<Id>>`, where `Id` is a semantic ID type
/// (marked `#[clone_in(semantic_id)]`).
///
/// Such fields are cloned with `SemanticId::clone_cell_option_id`.
fn struct_field_is_semantic_id_cell_option(field: &FieldDef, schema: &Schema) -> bool {
let TypeDef::Cell(cell_def) = field.type_def(schema) else { return false };
let TypeDef::Option(option_def) = cell_def.inner_type(schema) else { return false };
let TypeDef::Struct(struct_def) = option_def.inner_type(schema) else { return false };
struct_def.clone_in.is_semantic_id
}
fn derive_enum(enum_def: &EnumDef, schema: &Schema) -> TokenStream {
let type_ident = enum_def.ident();
@@ -209,7 +233,7 @@ fn generate_impl(
#inline
fn clone_in_impl(
&self,
#flag_param: bool,
#flag_param: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
#clone_in_impl_body
@@ -3,6 +3,9 @@
pub struct CloneInType {
/// `true` if should be replaced with default value when cloning
pub is_default: bool,
/// `true` if type is a semantic ID (`NodeId`, `ScopeId`, `SymbolId`, `ReferenceId`).
/// Fields of these types are cloned using `SemanticId` trait.
pub is_semantic_id: bool,
}
/// Details for `CloneIn` derive on a struct field.