Skip to content
Open
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
10 changes: 7 additions & 3 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5283,11 +5283,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

/// Returns whether a part was appended. Statements that are entirely
/// eliminated while visiting (dead code, `exports.eliminate`) leave `parts`
/// untouched.
pub fn append_part(
&mut self,
parts: &mut ListManaged<'a, js_ast::Part>,
stmts: &'a mut [Stmt],
) -> Result<(), crate::Error> {
) -> Result<bool, crate::Error> {
// Reuse the memory if possible
// This is reusable if the last part turned out to be dead
self.symbol_uses.clear_retaining_capacity();
Expand Down Expand Up @@ -5348,7 +5351,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.relocated_top_level_vars.clear();
}

if !part_stmts.is_empty() {
let appended = !part_stmts.is_empty();
if appended {
// SAFETY: `into_bump_slice_mut` leaks the BumpVec into the arena and
// returns the unique `&'a mut [T]` for that allocation. We compute
// `can_be_removed_if_unused` while the `&mut` is live (reborrowed as
Expand Down Expand Up @@ -5405,7 +5409,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.declared_symbols.clear_retaining_capacity();
self.import_records_for_current_part.clear();
}
Ok(())
Ok(appended)
}

fn binding_can_be_removed_if_unused_without_dce_check(&mut self, binding: Binding) -> bool {
Expand Down
12 changes: 6 additions & 6 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,11 +1058,11 @@ impl<'a> Parser<'a> {
let should_move = !p.options.bundle && class.class.can_be_moved();

let sliced = arena.alloc_slice_copy(&[*stmt]);
p.append_part(&mut parts, sliced)?;
let appended = p.append_part(&mut parts, sliced)?;

if should_move {
if should_move && appended {
// `Part` isn't `Copy`; pop+push instead of last+truncate.
before.push(parts.pop().expect("unreachable"));
before.push(parts.pop().expect("infallible: part was appended"));
}
}
js_ast::StmtData::SExportDefault(value) => {
Expand All @@ -1071,10 +1071,10 @@ impl<'a> Parser<'a> {
// https://github.com/oven-sh/bun/issues/1961
let should_move = !p.options.bundle && value.can_be_moved();
let sliced = arena.alloc_slice_copy(&[*stmt]);
p.append_part(&mut parts, sliced)?;
let appended = p.append_part(&mut parts, sliced)?;

if should_move {
before.push(parts.pop().expect("unreachable"));
if should_move && appended {
before.push(parts.pop().expect("infallible: part was appended"));
}
}
js_ast::StmtData::SEnum(_) => {
Expand Down
6 changes: 3 additions & 3 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// which is not mutated during the visit pass.
let replacer = _ptr.get();
if !self.replace_decl_and_possibly_remove(decl, replacer) {
let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(decl, false, was_const && !is_after, false);
} else {
continue 'outer;
}
// Replace/Inject gave the decl a value; `visit_decl` requires one.
let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(decl, false, was_const && !is_after, false);
}
}
}
Expand Down
128 changes: 75 additions & 53 deletions src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,29 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
};
}

macro_rules! default_replacement_entry {
() => {
p.options
.features
.replace_exports
.get_ptr(b"default")
.cloned()
.expect("infallible: mark_for_replace implies an entry")
};
}

// `is_control_flow_dead` is only set above for a non-replace entry. The original
// value is gone either way, but `inject` still has to emit its renamed export.
macro_rules! inject_default_replacement {
() => {
if mark_for_replace {
let entry = default_replacement_entry!();
let _ =
p.inject_replacement_export(stmts, Ref::NONE, bun_ast::Loc::EMPTY, &entry);
}
};
}

match &mut data.value {
js_ast::StmtOrExpr::Expr(expr) => {
let was_anonymous_named_expr = expr.is_anonymous_named();
Expand Down Expand Up @@ -425,6 +448,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.decorator_class_name = prev_decorator_class_name;

if p.is_control_flow_dead {
inject_default_replacement!();
restore_dead!();
record_on_exit!();
return Ok(());
Expand Down Expand Up @@ -548,13 +572,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

if mark_for_replace {
let entry = p
.options
.features
.replace_exports
.get_ptr(b"default")
.cloned()
.unwrap();
let entry = default_replacement_entry!();
if let crate::parser::Runtime::ReplaceableExport::Replace(replace_expr) = entry
{
*expr = replace_expr;
Expand Down Expand Up @@ -607,6 +625,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.react_compiler_candidate_name = None;

if p.is_control_flow_dead {
inject_default_replacement!();
p.react_refresh.hook_ctx_storage = prev;
restore_dead!();
record_on_exit!();
Expand Down Expand Up @@ -639,13 +658,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

if mark_for_replace {
let entry = p
.options
.features
.replace_exports
.get_ptr(b"default")
.cloned()
.unwrap();
let entry = default_replacement_entry!();
if let crate::parser::Runtime::ReplaceableExport::Replace(
replace_expr,
) = entry
Expand Down Expand Up @@ -777,35 +790,34 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let _ = p.visit_class(s2_loc, &mut class.class, data.default_name.ref_);

if p.is_control_flow_dead {
inject_default_replacement!();
restore_dead!();
record_on_exit!();
return Ok(());
}

if mark_for_replace {
let entry = p
.options
.features
.replace_exports
.get_ptr(b"default")
.cloned()
.unwrap();
let entry = default_replacement_entry!();
if let crate::parser::Runtime::ReplaceableExport::Replace(
replace_expr,
) = entry
{
// The class is discarded, so skip lowering it. Lowering
// reassigns `data.value` and would undo the replacement.
data.value = js_ast::StmtOrExpr::Expr(replace_expr);
stmts.push(*stmt);
} else {
let _ = p.inject_replacement_export(
stmts,
Ref::NONE,
bun_ast::Loc::EMPTY,
&entry,
);
restore_dead!();
record_on_exit!();
return Ok(());
}

restore_dead!();
record_on_exit!();
return Ok(());
}

if !data.default_name.ref_.is_symbol() {
Expand Down Expand Up @@ -884,12 +896,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
stmt: &mut Stmt,
data: &mut S::Function,
) -> Result<(), Error> {
// We mark it as dead, but the value may not actually be dead
// We just want to be sure to not increment the usage counts for anything in the function
let mark_as_dead = p.options.features.dead_code_elimination
&& data.func.flags.contains(flags::Function::IsExport)
// `replace_exports` targets module-level exports. A function exported from a
// namespace becomes a property of the namespace object, not a module export.
let should_replace_export = data.func.flags.contains(flags::Function::IsExport)
&& p.enclosing_namespace_arg_ref.is_none()
&& p.options.features.replace_exports.count() > 0
&& p.is_export_to_eliminate(data.func.name.expect("infallible: name checked").ref_);

// We mark it as dead, but the value may not actually be dead
// We just want to be sure to not increment the usage counts for anything in the function
let mark_as_dead = should_replace_export && p.options.features.dead_code_elimination;
let original_is_dead = p.is_control_flow_dead;

if mark_as_dead {
Expand Down Expand Up @@ -940,13 +956,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
),
Expr::init_identifier(func_name.ref_, func_name.loc),
));
} else if !mark_as_dead {
} else if !should_replace_export {
if remove_overwritten {
// restore on early return.
p.react_refresh.hook_ctx_storage = prev_hook_storage;
if mark_as_dead {
p.is_control_flow_dead = original_is_dead;
}
return Ok(());
}

Expand Down Expand Up @@ -980,21 +993,19 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
} else {
stmts.push(*stmt);
}
} else if mark_as_dead {
if let Some(replacement) = p
.options
.features
.replace_exports
.get_ptr(original_name)
.cloned()
{
let _ = p.inject_replacement_export(
stmts,
name_ref,
data.func.name.expect("infallible: name checked").loc,
&replacement,
);
}
} else if let Some(replacement) = p
.options
.features
.replace_exports
.get_ptr(original_name)
.cloned()
{
let _ = p.inject_replacement_export(
stmts,
name_ref,
data.func.name.expect("infallible: name checked").loc,
&replacement,
);
}

let mut rr: Result<(), Error> = Ok(());
Expand Down Expand Up @@ -1041,15 +1052,21 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
stmt: &mut Stmt,
data: &mut S::Class,
) -> Result<(), Error> {
let mark_as_dead = p.options.features.dead_code_elimination
&& data.is_export
// `replace_exports` targets module-level exports. A class exported from a
// namespace becomes a property of the namespace object, not a module export.
let should_replace_export = data.is_export
&& p.enclosing_namespace_arg_ref.is_none()
&& p.options.features.replace_exports.count() > 0
Comment thread
claude[bot] marked this conversation as resolved.
&& p.is_export_to_eliminate(
data.class
.class_name
.expect("infallible: name checked")
.ref_,
);

// We mark it as dead, but the value may not actually be dead
// We just want to be sure to not increment the usage counts for anything in the class
let mark_as_dead = should_replace_export && p.options.features.dead_code_elimination;
let original_is_dead = p.is_control_flow_dead;

if mark_as_dead {
Expand All @@ -1067,7 +1084,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// Lower class field syntax for browsers that don't support it
let lowered = p.lower_class(js_ast::StmtOrExpr::Stmt(*stmt));

if !mark_as_dead || was_export_inside_namespace {
if !should_replace_export {
// Lower class field syntax for browsers that don't support it
stmts.extend_from_slice(lowered);
} else {
Expand Down Expand Up @@ -1135,10 +1152,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

// visit_decls returns the surviving decl count; truncate `data.decls.len` to it.
let was_const = data.kind == S::Kind::KConst;
let new_len = if !(data.is_export && p.options.features.replace_exports.entries.len() > 0) {
p.visit_decls::<false>(data.decls.slice_mut(), was_const)
} else {
// `replace_exports` targets module-level exports. A declaration exported from a
// namespace becomes a property of the namespace object, not a module export.
let should_replace_export = data.is_export
&& p.enclosing_namespace_arg_ref.is_none()
&& p.options.features.replace_exports.entries.len() > 0;
let new_len = if should_replace_export {
p.visit_decls::<true>(data.decls.slice_mut(), was_const)
} else {
p.visit_decls::<false>(data.decls.slice_mut(), was_const)
Comment thread
robobun marked this conversation as resolved.
};
// Drop the whole statement when every decl was
// eliminated; otherwise we'd emit an empty `var;`/`let;`/`const;`.
Expand Down
Loading
Loading