Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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/js_parser/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

// rewrite `module.exports` to `exports`
p.module_exports_rewrite_count += 1;
p.module_exports_rewrites.note(p.part_generation);
return Some(Expr {
data: js_ast::ExprData::ESpecial(E::Special::ModuleExports),
loc: name_loc,
Expand Down
147 changes: 102 additions & 45 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,40 @@ pub enum ReactRefreshExportKind {
Default,
}

/// Uses of one symbol that stay in its `use_count_estimate` while the visit
/// answers for them another way. A reader compares `total` with that count.
/// A part the visit leaves empty takes its uses back from the count, so it
/// takes its notes back from `total` too (`revert_dead_part`).
Comment thread
robobun marked this conversation as resolved.
Resolved
#[derive(Default, Clone, Copy)]
pub(crate) struct AccountedUses {
pub(crate) total: u32,
/// The notes in `total` from the part `part_generation` names.
in_part: u32,
part_generation: u32,
}

impl AccountedUses {
pub(crate) fn note(&mut self, part_generation: u32) {
if self.part_generation != part_generation {
self.part_generation = part_generation;
self.in_part = 0;
}
self.in_part += 1;
self.total = self.total.saturating_add(1);
}

fn revert(&mut self, part_generation: u32) {
if self.part_generation != part_generation {
return;
}
// `u32::MAX` marks "more uses than accounted for"; it is not a count.
if self.total != u32::MAX {
self.total = self.total.saturating_sub(self.in_part);
}
self.in_part = 0;
}
}

// ─────────────────────────────────────────────────────────────────────────────
// P — the parser struct.
// `'a` covers borrowed init() params (log/define/source) AND the arena (`bump`).
Expand Down Expand Up @@ -360,15 +394,16 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
/// Per namespace ref, how many of its `use_count_estimate` uses were an
/// accounted-for read (`ns.a`, a destructure, a truthiness test). Kept
/// beside the real count rather than decremented from it so the minifier's
/// single-use substitution still sees every use.
pub(crate) namespace_tracked_uses: HashMap<Ref, u32>,
/// single-use substitution still sees every use. The synthetic ref of an
/// `import()` has no use count: a note there says its consumer was tracked.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) namespace_tracked_uses: HashMap<Ref, AccountedUses>,
pub(crate) unwrap_all_requires: bool,

pub(crate) commonjs_named_exports: bun_ast::ast_result::CommonJSNamedExports,
pub(crate) commonjs_named_exports_deoptimized: bool,
pub(crate) commonjs_module_exports_assigned_deoptimized: bool,
/// Uses of `module` that became `module.exports` and stay in its use count.
pub(crate) module_exports_rewrite_count: u32,
pub(crate) module_exports_rewrites: AccountedUses,
pub(crate) commonjs_named_exports_needs_conversion: u32,
pub(crate) had_commonjs_named_exports_this_visit: bool,
pub(crate) commonjs_replacement_stmts: StmtNodeList,
Expand Down Expand Up @@ -1287,8 +1322,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if self.is_revisit_for_substitution || self.is_control_flow_dead {
return;
}
let n = self.namespace_tracked_uses.entry(ns).or_default();
*n = n.saturating_add(1);
// `revert_dead_part` finds a local's notes through its use in the part.
debug_assert!(
!self.dynamic_import_namespace_locals.contains_key(&ns)
|| self.existing_part_use(ns).is_some()
);
self.namespace_tracked_uses
.entry(ns)
.or_default()
.note(self.part_generation);
}

/// `ns` in a position that only tests it for null/truthiness (`if (ns)`,
Expand Down Expand Up @@ -1804,22 +1846,38 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.deoptimize_common_js_named_exports();
}

fn clear_symbol_usages_from_dead_part(&mut self, part: &js_ast::Part) {
let symbol_use_refs = part.symbol_uses.keys();
let symbol_use_values = part.symbol_uses.values();
let symbols = self.symbols.as_mut_slice();

debug_assert_eq!(symbol_use_refs.len(), symbol_use_values.len());
for (r#ref, prev) in symbol_use_refs.iter().zip(symbol_use_values) {
symbols[r#ref.inner_index() as usize].use_count_estimate = symbols
[r#ref.inner_index() as usize]
.use_count_estimate
.saturating_sub(prev.count_estimate());
}
let declared_refs = part.declared_symbols.refs();
for declared in declared_refs {
symbols[declared.inner_index() as usize].use_count_estimate = 0;
/// The visit left the current part without a statement. Take back what it
/// recorded, so the file's counts read as if the part was never there:
/// its uses from `use_count_estimate`, and its notes from the counts that
/// are compared with it. A symbol the part declared keeps the uses other
/// parts have of it: an unwrapped `const x = require()` leaves its part
/// empty and `x` is still the import's namespace.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn revert_dead_part(&mut self) {
let part_generation = self.part_generation;
let live = u64::from(part_generation) << 32;
for (at, &(ref_, use_)) in self.part_uses.iter().enumerate() {
let i = ref_.inner_index() as usize;
if self.part_use_slot[i] == live | at as u64 {
let symbol = &mut self.symbols[i];
symbol.use_count_estimate = symbol
.use_count_estimate
.saturating_sub(use_.count_estimate());
}
// A note on a namespace local follows a use of it in the same
// part, so the local is listed here, also once `ignore_usage`
// forgot the entry. An `import()`'s synthetic ref has no use, and
// its notes stay.
Comment thread
robobun marked this conversation as resolved.
Resolved
if !self.namespace_tracked_uses.is_empty()
&& let Some(tracked) = self.namespace_tracked_uses.get_mut(&ref_)
{
tracked.revert(part_generation);
}
}
self.module_exports_rewrites.revert(part_generation);
self.part_uses.clear();
self.part_generation += 1;
self.declared_symbols.clear_retaining_capacity();
self.import_records_for_current_part.clear();
}

// s() lives in the impl block above (deduped).
Expand Down Expand Up @@ -5735,15 +5793,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// to empty by `core::mem::take` above; no second assignment needed.
self.had_commonjs_named_exports_this_visit = false;
} else if self.declared_symbols.len() > 0 || !self.part_uses.is_empty() {
// if the part is dead, invalidate all the usage counts
let symbol_uses = self.take_symbol_uses()?;
self.clear_symbol_usages_from_dead_part(&js_ast::Part {
declared_symbols: self.declared_symbols.clone()?,
symbol_uses,
..Default::default()
});
self.declared_symbols.clear_retaining_capacity();
self.import_records_for_current_part.clear();
self.revert_dead_part();
}
Ok(())
}
Expand Down Expand Up @@ -6159,10 +6209,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
debug_assert!(!self.runtime_imports.__require.is_empty());
let r = self.runtime_identifier_ref(b"__require");
self.ignore_usage(r);
self.symbols[self.require_ref.inner_index() as usize].use_count_estimate = self.symbols
[self.require_ref.inner_index() as usize]
.use_count_estimate
.saturating_sub(1);
// `const r = require; typeof r` folds on the revisit that follows
// the substitution. The first visit recorded that use of `require`.
Comment thread
robobun marked this conversation as resolved.
Resolved
if !self.is_control_flow_dead {
self.ignore_recorded_usage(self.require_ref);
}
}
}

Expand Down Expand Up @@ -6764,23 +6815,29 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

pub(crate) fn ignore_usage(&mut self, r#ref: Ref) {
if !self.is_control_flow_dead && !self.is_revisit_for_substitution {
debug_assert!((r#ref.inner_index() as usize) < self.symbols.len());
self.symbols[r#ref.inner_index() as usize].use_count_estimate = self.symbols
[r#ref.inner_index() as usize]
.use_count_estimate
.saturating_sub(1);
if let Some(use_) = self.existing_part_use(r#ref) {
use_.subtract(1);
if use_.count_estimate() == 0 {
self.forget_part_use(r#ref);
}
}
self.ignore_recorded_usage(r#ref);
}

// Don't roll back the "tsUseCounts" increment. This must be counted even if
// the value is ignored because that's what the TypeScript compiler does.
}

/// Takes one use of `ref` back from its count and from the current part.
#[inline]
fn ignore_recorded_usage(&mut self, r#ref: Ref) {
debug_assert!((r#ref.inner_index() as usize) < self.symbols.len());
self.symbols[r#ref.inner_index() as usize].use_count_estimate = self.symbols
[r#ref.inner_index() as usize]
.use_count_estimate
.saturating_sub(1);
if let Some(use_) = self.existing_part_use(r#ref) {
use_.subtract(1);
if use_.count_estimate() == 0 {
self.forget_part_use(r#ref);
}
}
}

pub(crate) fn ignore_usage_of_identifier_in_dot_chain(&mut self, expr: Expr) {
let mut current = expr;
loop {
Expand Down Expand Up @@ -9883,7 +9940,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
unwrap_all_requires,
commonjs_named_exports: Default::default(),
commonjs_module_exports_assigned_deoptimized: false,
module_exports_rewrite_count: 0,
module_exports_rewrites: AccountedUses::default(),
commonjs_named_exports_needs_conversion: u32::MAX,
had_commonjs_named_exports_this_visit: false,
commonjs_replacement_stmts: js_ast::StmtNodeList::EMPTY,
Expand Down
9 changes: 6 additions & 3 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,7 +1312,7 @@ impl<'a> Parser<'a> {
// record (the synthetic ref from `transpose_import` plus a
// `const ns = …` / `.then(ns => …)` / `{...rest}` local). A record is
// fully tracked only when *every* such ref had all its uses accounted
// for (`use_count_estimate == 0`); the alias set is the union across
// for (`namespace_tracked_uses`); the alias set is the union across
// them. Untracked records get no entry and keep every export.
if !p
.imports_to_convert_from_dynamic_import
Expand Down Expand Up @@ -1347,7 +1347,10 @@ impl<'a> Parser<'a> {
// linked symbol was merged with another declaration (a
// hoisted `var`, a parameter), so its own use count says
// nothing.
let tracked = p.namespace_tracked_uses.get(&ns_ref).copied().unwrap_or(0);
let tracked = p
.namespace_tracked_uses
.get(&ns_ref)
.map_or(0, |uses| uses.total);
// A source-visible local escapes when it has uses nobody
// accounted for; the synthetic per-`import()` ref (never
// referenced in source) escapes when its one consumer did not.
Expand Down Expand Up @@ -1552,7 +1555,7 @@ impl<'a> Parser<'a> {
p.deoptimize_commonjs_named_exports();
} else if p.symbols.as_slice()[p.module_ref.inner_index() as usize]
.use_count_estimate
> p.module_exports_rewrite_count
> p.module_exports_rewrites.total
{
// `module.constructor` and other uses of `module` need the wrapper.
p.deoptimize_commonjs_named_exports();
Expand Down
7 changes: 5 additions & 2 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1966,10 +1966,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// moved into `replacement`; unless it was an accounted-for read
// (`f(ns.a)`), the namespace escapes there.
if p.dynamic_import_namespace_locals.contains_key(&id)
&& p.namespace_tracked_uses.get(&id).copied().unwrap_or(0) == 0
&& p.namespace_tracked_uses
.get(&id)
.map_or(0, |uses| uses.total)
== 0
{
// Read as "more uses than accounted for" when finalizing.
p.namespace_tracked_uses.insert(id, u32::MAX);
p.namespace_tracked_uses.entry(id).or_default().total = u32::MAX;
}
match local.decls.len_u32() {
1 => {
Expand Down
Loading
Loading