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
2 changes: 1 addition & 1 deletion src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ impl IntoExprData for &E::EString {
impl Expr {
/// When the lifetime of an Expr.Data's pointer must exist longer than reset() is called, use this function.
/// Be careful to free the memory (or use an arena that does it for you)
/// Also, prefer Expr.init or Expr.alloc when possible. This will be slower.
/// Prefer `Expr::init` for nodes that only need to live until the current parse's reset().
#[inline]
pub fn allocate<T: IntoExprData>(bump: &Bump, st: T, loc: Loc) -> Expr {
data::Store::assert();
Expand Down
8 changes: 5 additions & 3 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5586,11 +5586,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

/// Returns `false` when visiting eliminated every statement, so no part was pushed.
pub(crate) fn append_part(
&mut self,
parts: &mut ListManaged<'a, js_ast::Part>,
stmts: &'a mut [Stmt],
) -> Result<(), crate::Error> {
) -> Result<bool, crate::Error> {
// Uses recorded outside a part's visit (the parse pass resolving
// decorator-metadata types) belong to no part.
self.part_uses.clear();
Expand Down Expand Up @@ -5652,7 +5653,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 @@ -5711,7 +5713,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)
}

/// A pattern runs getters or the iterator on its value, so only a literal value is side-effect free.
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 @@ -1157,11 +1157,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 @@ -1170,10 +1170,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
28 changes: 15 additions & 13 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
is_export: bool,
) -> usize {
let was_const = kind == LocalKind::KConst;
// A member of a namespace is not an export of the module.
let applies_entries =
IS_POSSIBLY_DECL_TO_REMOVE && self.enclosing_namespace_arg_ref.is_none();
let mut j: usize = 0;
// Iterate by index so kept entries can be written back through `decls[j]`
// while scanning ahead.
Expand All @@ -305,7 +308,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut replacement: Option<
bun_ptr::BackRef<crate::parser::Runtime::ReplaceableExport>,
> = None;
if IS_POSSIBLY_DECL_TO_REMOVE {
if applies_entries {
if let BData::BIdentifier(id) = decl.binding.data {
let id_ref = id.r#ref;
let name = self.load_name_from_ref(id_ref);
Expand Down Expand Up @@ -571,7 +574,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

if IS_POSSIBLY_DECL_TO_REMOVE {
if applies_entries {
self.is_control_flow_dead = orig_dead;
if let BData::BIdentifier(_) = decl.binding.data {
if let Some(_ptr) = replacement {
Expand All @@ -598,7 +601,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
},
},
);
} else if IS_POSSIBLY_DECL_TO_REMOVE {
} else if applies_entries {
if let BData::BIdentifier(id) = decl.binding.data {
let id_ref = id.r#ref;
let name = self.load_name_from_ref(id_ref);
Expand All @@ -613,18 +616,17 @@ 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,
VisitDeclOpts {
was_anonymous_named_expr: false,
could_be_const_value: was_const && !is_after,
could_be_macro: false,
},
);
} else {
continue 'outer;
}
let is_after = self.vis_scope().is_after_const_local_prefix;
self.visit_decl(
decl,
VisitDeclOpts {
was_anonymous_named_expr: false,
could_be_const_value: was_const && !is_after,
could_be_macro: false,
},
);
}
}
}
Expand Down
101 changes: 90 additions & 11 deletions src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use crate::Error;
use crate::lexer as js_lexer;
use crate::p::{P, ReactRefreshExportKind};
use crate::parser::Runtime::ReplaceableExport;
use crate::parser::{
PrependTempRefsOpts, ReactRefresh, Ref, RelocateVarsMode, SideEffects, StmtsKind,
statement_cares_about_scope,
Expand Down Expand Up @@ -265,6 +266,64 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Ok(())
}

/// Puts the export of `entry` in the place of `alias`, a name that a re-export exports and the module does not bind.
#[cold]
#[inline(never)]
fn replace_reexport(
p: &mut Self,
stmts: &mut StmtList<'a>,
alias: &'a [u8],
entry: &ReplaceableExport,
) {
let exported_name: &[u8] = match entry {
ReplaceableExport::Inject { name, .. } => name,
ReplaceableExport::Replace(_) | ReplaceableExport::Delete => alias,
};
match entry {
ReplaceableExport::Replace(value) | ReplaceableExport::Inject { value, .. }
if exported_name == js_ast::ClauseItem::DEFAULT_ALIAS =>
{
let mut export_default = p.s(
S::ExportDefault {
default_name: js_ast::LocRef::default(),
value: js_ast::StmtOrExpr::Expr(*value),
},
bun_ast::Loc::EMPTY,
);
// No entry applies to the export that an entry made, so the visitor sees no entries.
let replace_exports = core::mem::take(&mut p.options.features.replace_exports);
p.visit_and_append_stmt(stmts, &mut export_default)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
.expect("unreachable");
p.options.features.replace_exports = replace_exports;
}
ReplaceableExport::Replace(_) => {
// TypeScript lets a declaration take the place of an import of its name. This export is not one.
if TYPESCRIPT
&& let Some(import) = p
.current_scope()
.get_member_with_hash(alias, js_ast::Scope::get_member_hash(alias))
&& p.symbols[import.ref_.inner_index() as usize].kind
== js_ast::symbol::Kind::Import
{
p.log().add_symbol_already_declared_error(
p.source,
alias,
bun_ast::Loc::EMPTY,
import.loc,
);
return;
}
let declared = p
.declare_symbol(js_ast::symbol::Kind::Other, bun_ast::Loc::EMPTY, alias)
.expect("unreachable");
let _ = p.inject_replacement_export(stmts, declared, bun_ast::Loc::EMPTY, entry);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
ReplaceableExport::Delete | ReplaceableExport::Inject { .. } => {
let _ = p.inject_replacement_export(stmts, Ref::NONE, bun_ast::Loc::EMPTY, entry);
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}
}

fn s_export_from(
p: &mut Self,
stmts: &mut StmtList<'a>,
Expand All @@ -289,8 +348,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// alias is arena-owned (`ArenaStr`), valid for 'a.
let alias = items[i].alias.slice();
if let Some(entry) = p.options.features.replace_exports.get_ptr(alias).cloned() {
let _ =
p.inject_replacement_export(stmts, old_ref, bun_ast::Loc::EMPTY, &entry);
Self::replace_reexport(p, stmts, alias, &entry);
continue;
}

Comment thread
robobun marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -345,15 +403,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
.get_ptr(alias_name)
.cloned()
{
let declared = p
.declare_symbol(
js_ast::symbol::Kind::Other,
bun_ast::Loc::EMPTY,
alias_name,
)
.expect("unreachable");
let _ =
p.inject_replacement_export(stmts, declared, bun_ast::Loc::EMPTY, &entry);
Self::replace_reexport(p, stmts, alias_name, &entry);
return Ok(());
}
}
Expand Down Expand Up @@ -398,6 +448,29 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
};
}

// The value that an entry takes away is dead code. The export that the entry injects is not.
macro_rules! inject_for_dead_value {
() => {
if mark_for_replace && !orig_dead {
if let Some(entry) = p
.options
.features
.replace_exports
.get_ptr(b"default")
.cloned()
&& !entry.is_replace()
{
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 @@ -427,6 +500,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

if p.is_control_flow_dead {
restore_dead!();
inject_for_dead_value!();
record_on_exit!();
return Ok(());
}
Expand All @@ -450,6 +524,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// this symbol, drop the statement.
data.default_name.ref_ = Ref::NONE;
restore_dead!();
inject_for_dead_value!();
record_on_exit!();
return Ok(());
}
Comment thread
robobun marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -611,6 +686,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if p.is_control_flow_dead {
p.react_refresh.hook_ctx_storage = prev;
restore_dead!();
inject_for_dead_value!();
record_on_exit!();
return Ok(());
}
Expand Down Expand Up @@ -780,6 +856,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

if p.is_control_flow_dead {
restore_dead!();
inject_for_dead_value!();
record_on_exit!();
return Ok(());
}
Expand Down Expand Up @@ -890,6 +967,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mark_as_dead = p.options.features.dead_code_elimination
&& data.func.flags.contains(flags::Function::IsExport)
&& p.options.features.replace_exports.count() > 0
&& p.enclosing_namespace_arg_ref.is_none()
&& p.is_export_to_eliminate(data.func.name.expect("infallible: name checked").ref_);
Comment thread
robobun marked this conversation as resolved.
Resolved
let original_is_dead = p.is_control_flow_dead;

Expand Down Expand Up @@ -1048,6 +1126,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mark_as_dead = p.options.features.dead_code_elimination
&& data.is_export
&& p.options.features.replace_exports.count() > 0
&& p.enclosing_namespace_arg_ref.is_none()
&& p.is_export_to_eliminate(
data.class
.class_name
Expand Down
6 changes: 5 additions & 1 deletion src/runtime/api/JSTranspiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,9 @@ fn export_replacement_value(
global: &JSGlobalObject,
arena: &Arena,
) -> JsResult<Option<bun_ast::Expr>> {
// The result outlives every parse, so it must not be built in the thread-local store.
let _guard = bun_ast::expr::Disabler::scope();

if value.is_boolean() {
return Ok(Some(Expr {
data: bun_ast::ExprData::EBoolean(bun_ast::E::Boolean {
Expand Down Expand Up @@ -916,7 +919,8 @@ fn export_replacement_value(
// `E::EString::init` erases the borrow to `'static` per the AST
// crate's `Str` convention (see ast/E.rs).
let data = arena.alloc_slice_copy(utf8.slice());
return Ok(Some(Expr::init(
return Ok(Some(Expr::allocate(
arena,
bun_ast::E::EString::init(data),
bun_ast::Loc::EMPTY,
)));
Expand Down
Loading
Loading