Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,122 @@ impl Expr {
// `bun_js_parser_jsc::macro_::expr_from_blob`.
}

/// A copied object or array whose children still point at nodes of the source.
enum LiteralContainer {
Object(StoreRef<E::Object>),
Array(StoreRef<E::Array>),
}

impl Expr {
/// Copies a tree of object, array and string literals. The copy shares no node with `self`.
#[cold]
pub fn clone_literal_tree(self) -> Expr {
let mut root = self;
// A work list, not recursion: a define can nest deeper than the stack of a pool thread allows.
let mut work: Vec<LiteralContainer> = Vec::new();
let mut next = root.own_literal_node();
while let Some(container) = next {
match container {
LiteralContainer::Object(mut object) => {
for property in object.properties.slice_mut() {
let children = [
&mut property.key,
&mut property.value,
&mut property.initializer,
];
for child in children.into_iter().flatten() {
work.extend(child.own_literal_node());
}
}
}
LiteralContainer::Array(mut array) => {
for item in array.items.slice_mut() {
work.extend(item.own_literal_node());
}
}
}
next = work.pop();
}
root
}

/// Points `self` at a field-wise copy of its string, object or array node.
fn own_literal_node(&mut self) -> Option<LiteralContainer> {
match self.data {
Data::EString(string) => {
self.data = Data::EString(data::Store::append(string.shallow_clone()));
None
}
Data::EObject(object) => {
let mut properties: G::PropertyList =
Vec::with_capacity_in(object.properties.len(), bun_alloc::AstAlloc);
properties.extend(object.properties.iter().map(G::Property::shallow_clone));
let copy = data::Store::append(E::Object {
properties,
comma_after_spread: object.comma_after_spread,
is_single_line: object.is_single_line,
is_parenthesized: object.is_parenthesized,
was_originally_macro: object.was_originally_macro,
close_brace_loc: object.close_brace_loc,
});
self.data = Data::EObject(copy);
Some(LiteralContainer::Object(copy))
}
Data::EArray(array) => {
let copy = data::Store::append(E::Array {
items: bun_alloc::AstAlloc::vec_from_slice(array.items.slice()),
comma_after_spread: array.comma_after_spread,
is_single_line: array.is_single_line,
is_parenthesized: array.is_parenthesized,
was_originally_macro: array.was_originally_macro,
close_bracket_loc: array.close_bracket_loc,
});
self.data = Data::EArray(copy);
Some(LiteralContainer::Array(copy))
}
_ => None,
}
}

/// Whether `self` and `other` share a string, object or array node, or a list, at the same place.
pub fn shares_literal_node_with(&self, other: &Expr) -> bool {
let mut work: Vec<(Expr, Expr)> = vec![(*self, *other)];
while let Some((a, b)) = work.pop() {
match (a.data, b.data) {
(Data::EString(a), Data::EString(b)) => {
if a == b {
return true;
}
}
(Data::EObject(a), Data::EObject(b)) => {
if a == b
|| (!a.properties.is_empty()
&& a.properties.as_ptr() == b.properties.as_ptr())
{
return true;
}
for (a, b) in a.properties.iter().zip(b.properties.iter()) {
let children = [
(a.key, b.key),
(a.value, b.value),
(a.initializer, b.initializer),
];
work.extend(children.into_iter().filter_map(|(a, b)| a.zip(b)));
}
}
(Data::EArray(a), Data::EArray(b)) => {
if a == b || (!a.items.is_empty() && a.items.as_ptr() == b.items.as_ptr()) {
return true;
}
work.extend(a.items.iter().copied().zip(b.items.iter().copied()));
}
_ => {}
}
}
false
}
}

#[derive(Clone, Copy)]
pub struct Query {
pub expr: Expr,
Expand Down
14 changes: 14 additions & 0 deletions src/ast/g.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,20 @@ impl Property {
self.class_static_block.as_deref_mut()
}

/// Field-wise copy: the expressions and the static block still point at the nodes of `self`.
pub fn shallow_clone(&self) -> Property {
Property {
initializer: self.initializer,
kind: self.kind,
flags: self.flags,
class_static_block: self.class_static_block,
ts_decorators: bun_alloc::AstAlloc::vec_from_slice(self.ts_decorators.slice()),
key: self.key,
value: self.value,
ts_metadata: self.ts_metadata.clone(),
}
}

pub(crate) fn deep_clone(
&self,
bump: &bun_alloc::Arena,
Expand Down
72 changes: 21 additions & 51 deletions src/bundler/defines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::defines_table::{
};

// ══════════════════════════════════════════════════════════════════════════
// B-3 UNIFIED: `Define` / `DefineData` / `DotDefine` / `Flags` / `Options` /
// B-3 UNIFIED: `Define` / `DefineData` / `DotDefine` / `Options` /
// `RawDefines` / `UserDefines` / `UserDefinesArray` are canonical in
// `bun_js_parser::defines` (lower tier) so the parser's `P.define: &'a Define`
// and `BundleOptions.define: Box<Define>` are the *same* nominal type. This
Expand All @@ -21,7 +21,7 @@ use crate::defines_table::{
// directly with no cross-crate hook.
// ══════════════════════════════════════════════════════════════════════════
pub use bun_js_parser::defines::{
Define, DefineData, DotDefine, Flags, IdentifierDefine, Options, RawDefines, UserDefines,
Define, DefineData, DotDefine, IdentifierDefine, Options, RawDefines, UserDefines,
UserDefinesArray, are_parts_equal,
};

Expand Down Expand Up @@ -356,6 +356,22 @@ impl DefineDataExt for DefineData {
}
}

let define = |value: ExprData, can_be_removed_if_unused: bool| {
DefineData::init(Options {
value,
original_name: if !value_str.is_empty() {
Some(value_str)
} else {
None
},
valueless: value_is_undefined,
can_be_removed_if_unused,
call_can_be_unwrapped_if_unused: bun_ast::E::CallUnwrap::Never,
method_call_must_be_replaced_with_undefined:
method_call_must_be_replaced_with_undefined_,
})
};

if is_ident {
// Special-case undefined. it's not an identifier here
// https://github.com/evanw/esbuild/issues/1407
Expand All @@ -367,25 +383,7 @@ impl DefineDataExt for DefineData {
)
};

return Ok(DefineData {
value,
// upstream `DefineData` now owns `original_name:
// Option<Box<[u8]>>` (js_parser/lib.rs:1369) instead of a
// borrowed `ptr`/`len` pair. Dupe
// the value bytes — these are tiny startup-time copies.
original_name: if !value_str.is_empty() {
Some(Box::<[u8]>::from(value_str))
} else {
None
},
flags: Flags::new(
/* valueless: */ value_is_undefined,
/* can_be_removed_if_unused: */ true,
/* call_can_be_unwrapped_if_unused: */ bun_ast::E::CallUnwrap::Never,
/* method_call_must_be_replaced_with_undefined: */
method_call_must_be_replaced_with_undefined_,
),
});
return Ok(define(value, true));
}

// Fast path for the compile-time-constant literal define *values* that
Expand All @@ -399,21 +397,7 @@ impl DefineDataExt for DefineData {
// (created lazily below only when a value really needs JSON parsing).
if let Some(value) = const_default_define_value(value_str) {
let can_be_removed_if_unused = bun_ast::expr::Tag::is_primitive_literal(value.tag());
return Ok(DefineData {
value,
original_name: if !value_str.is_empty() {
Some(Box::<[u8]>::from(value_str))
} else {
None
},
flags: Flags::new(
/* valueless: */ value_is_undefined,
/* can_be_removed_if_unused: */ can_be_removed_if_unused,
/* call_can_be_unwrapped_if_unused: */ bun_ast::E::CallUnwrap::Never,
/* method_call_must_be_replaced_with_undefined: */
method_call_must_be_replaced_with_undefined_,
),
});
return Ok(define(value, can_be_removed_if_unused));
}

// `parse_env_json` builds `E::String`/`E::Object` nodes in the
Expand Down Expand Up @@ -446,21 +430,7 @@ impl DefineDataExt for DefineData {
let data: ExprData =
bun_parsers::json_parser::clone_out_of_store(&expr, &source, log, bump)?.data;
let can_be_removed_if_unused = bun_ast::expr::Tag::is_primitive_literal(data.tag());
Ok(DefineData {
value: data,
original_name: if !value_str.is_empty() {
Some(Box::<[u8]>::from(value_str))
} else {
None
},
flags: Flags::new(
/* valueless: */ value_is_undefined,
/* can_be_removed_if_unused: */ can_be_removed_if_unused,
/* call_can_be_unwrapped_if_unused: */ bun_ast::E::CallUnwrap::Never,
/* method_call_must_be_replaced_with_undefined: */
method_call_must_be_replaced_with_undefined_,
),
})
Ok(define(data, can_be_removed_if_unused))
}

fn from_input(
Expand Down
4 changes: 2 additions & 2 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,8 +943,8 @@ pub(crate) fn defines_from_transform_options(
.keys()
.iter()
.zip(environment_defines.values().iter())
.filter_map(|(k, v)| match &v.value {
defines::DefineValue::EString(s) if s.is_utf8() => Some((k.as_ref(), s.slice8())),
.filter_map(|(k, v)| match v.string() {
Some(s) if s.is_utf8() => Some((k.as_ref(), s.slice8())),
_ => None,
}),
drop.iter().copied(),
Expand Down
2 changes: 1 addition & 1 deletion src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ impl<'a> Transpiler<'a> {
if had_explicit_node_env {
if let Some(node_env) = self.options.define.dots.get(b"NODE_ENV".as_slice()) {
if !node_env.is_empty() {
if let Some(s) = node_env[0].data.value.e_string() {
if let Some(s) = node_env[0].data.string() {
if s.eql_comptime(b"production") {
is_production = true;
} else if s.eql_comptime(b"development") {
Expand Down
80 changes: 28 additions & 52 deletions src/js_parser/defines_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,76 +192,52 @@ pub static GLOBAL_NO_SIDE_EFFECT_FUNCTION_CALLS_SAFE_FOR_TO_STRING: &[&[&[u8]]]
];

// `OnceLock` for lazily-initialised statics (`DefineData` is not const-constructible).
//
// `DefineData` is not `Send`/`Sync` in general (it carries `ExprData`, whose
// boxed variants hold `NonNull<_>`). The four instances stored here only ever
// use the inline `EUndefined` / `ENumber` variants and `original_name = None`,
// so they contain no raw pointers and are trivially shareable. Wrap in a local
// newtype to satisfy `OnceLock<T: Sync + Send>` without globally relaxing
// `DefineData`'s auto-traits.
#[repr(transparent)]
struct SyncDefineData(defines::IdentifierDefine);
// SAFETY: only constructed below with pointer-free `ExprData` payloads
// (`EUndefined`/`ENumber`) and `original_name = None`; never mutated after
// `OnceLock` init. See note above.
unsafe impl Sync for SyncDefineData {}
// SAFETY: see `Sync` impl above.
unsafe impl Send for SyncDefineData {}

fn pure_global_identifier_define() -> &'static defines::IdentifierDefine {
static CELL: OnceLock<SyncDefineData> = OnceLock::new();
&CELL
.get_or_init(|| {
SyncDefineData(defines::DefineData::init(defines::Options {
value: ExprData::EUndefined(js_ast::E::Undefined),
valueless: true,
can_be_removed_if_unused: true,
..Default::default()
}))
static CELL: OnceLock<defines::IdentifierDefine> = OnceLock::new();
CELL.get_or_init(|| {
defines::DefineData::init(defines::Options {
value: ExprData::EUndefined(js_ast::E::Undefined),
valueless: true,
can_be_removed_if_unused: true,
..Default::default()
})
.0
})
}

mod identifiers {
use super::{ExprData, OnceLock, SyncDefineData, defines, js_ast};
use super::{ExprData, OnceLock, defines, js_ast};

const NAN_VAL: js_ast::E::Number = js_ast::E::Number::new(f64::NAN);
const INF_VAL: js_ast::E::Number = js_ast::E::Number::new(f64::INFINITY);

// Step 2. Swap in certain literal values because those can be constant folded
pub(super) fn undefined() -> &'static defines::IdentifierDefine {
static CELL: OnceLock<SyncDefineData> = OnceLock::new();
&CELL
.get_or_init(|| {
SyncDefineData(defines::DefineData::init(defines::Options {
value: ExprData::EUndefined(js_ast::E::Undefined),
can_be_removed_if_unused: true,
..Default::default()
}))
static CELL: OnceLock<defines::IdentifierDefine> = OnceLock::new();
CELL.get_or_init(|| {
defines::DefineData::init(defines::Options {
value: ExprData::EUndefined(js_ast::E::Undefined),
can_be_removed_if_unused: true,
..Default::default()
})
.0
})
}
pub(super) fn nan() -> &'static defines::IdentifierDefine {
static CELL: OnceLock<SyncDefineData> = OnceLock::new();
&CELL
.get_or_init(|| {
SyncDefineData(defines::DefineData::init(defines::Options {
value: ExprData::ENumber(NAN_VAL),
..Default::default()
}))
static CELL: OnceLock<defines::IdentifierDefine> = OnceLock::new();
CELL.get_or_init(|| {
defines::DefineData::init(defines::Options {
value: ExprData::ENumber(NAN_VAL),
..Default::default()
})
.0
})
}
pub(super) fn infinity() -> &'static defines::IdentifierDefine {
static CELL: OnceLock<SyncDefineData> = OnceLock::new();
&CELL
.get_or_init(|| {
SyncDefineData(defines::DefineData::init(defines::Options {
value: ExprData::ENumber(INF_VAL),
..Default::default()
}))
static CELL: OnceLock<defines::IdentifierDefine> = OnceLock::new();
CELL.get_or_init(|| {
defines::DefineData::init(defines::Options {
value: ExprData::ENumber(INF_VAL),
..Default::default()
})
.0
})
}
}

Expand Down
Loading
Loading