fix(mangler): exclude non-manglable symbols from slot assignment (#25539)

Slots were assigned to non-manglable symbols as well. This made the slot re-use non-optimal.

This PR fixes that.

I used AI to find out this bug and to generate the code and then reviewed the code.
This commit is contained in:
sapphi-red committed 2026-08-13 06:39:16 +00:00
1 parent 40f7f9ba68
commit 771d79a9e3
3 files changed
+39 -29

No files matched your search

+22 -23
View File
@@ -557,6 +557,19 @@ impl<'a, 's> Constraints<'a, 's> {
fn is_kept_name(&self, name: &str) -> bool {
is_special_name(name) || (!self.reserved.is_empty() && self.reserved.contains(name))
}
/// Whether `symbol_id` will receive a mangled name.
#[inline]
fn is_mangle_candidate(&self, symbol_id: SymbolId, scoping: &Scoping) -> bool {
let scope_id = scoping.symbol_scope_id(symbol_id);
!(scope_id == scoping.root_scope_id()
&& (!self.top_level
|| self.exported_symbols.as_ref().is_some_and(|e| e.has_bit(symbol_id.index())))
|| scoping.scope_flags(scope_id).contains_direct_eval()
|| self.is_kept_name(scoping.symbol_name(symbol_id))
|| self.keep_name_symbols.as_ref().is_some_and(|keep| keep.has_bit(symbol_id.index())))
}
}
impl<'a, 's> SlotAssignment<'a, 's> {
@@ -574,7 +587,6 @@ impl<'a, 's> SlotAssignment<'a, 's> {
ast_nodes: &AstNodes,
constraints: &Constraints,
) -> Self {
let keep_name_symbols = constraints.keep_name_symbols.as_ref();
// Names of bindings in direct-`eval` scopes — collected here, reserved in Phase 4.
// TODO: eval reservation is conservative — ideally we'd reserve names per-slot.
let mut eval_reserved_names: FxHashSet<&'s str> = FxHashSet::default();
@@ -610,9 +622,12 @@ impl<'a, 's> SlotAssignment<'a, 's> {
// Sort `bindings` in declaration order.
tmp_bindings.clear();
tmp_bindings.extend(bindings.values().copied().filter(|binding| {
!keep_name_symbols.is_some_and(|keep| keep.has_bit(binding.index()))
}));
tmp_bindings.extend(
bindings
.values()
.copied()
.filter(|&binding| constraints.is_mangle_candidate(binding, scoping)),
);
if tmp_bindings.is_empty() {
continue;
}
@@ -715,6 +730,7 @@ impl<'a, 's> SlotAssignment<'a, 's> {
&& let Some(id) = &func.id
&& let Some(&shadower) = bindings.get(&id.name)
&& shadower != id.symbol_id()
&& constraints.is_mangle_candidate(id.symbol_id(), scoping)
&& slots[shadower.index()] != SLOT_UNASSIGNED
{
slots[id.symbol_id().index()] = slots[shadower.index()];
@@ -727,17 +743,13 @@ impl<'a, 's> SlotAssignment<'a, 's> {
}
impl<'a> SlotRanking<'a> {
/// Phase 3: count references per slot and sort hottest-first, skipping slots whose only
/// symbols are kept, exported (at top level), eval-visible, or special (`arguments`).
/// Phase 3: count references per candidate slot and sort hottest-first.
fn tally(
allocator: &'a Allocator,
scoping: &Scoping,
constraints: &Constraints,
slots: &SlotAssignment,
) -> Self {
let exported_symbols = constraints.exported_symbols.as_ref();
let keep_name_symbols = constraints.keep_name_symbols.as_ref();
let root_scope_id = scoping.root_scope_id();
let mut frequencies = ArenaVec::from_iter_in(
repeat_with(|| SlotFrequency::new(allocator)).take(slots.total_slots),
&allocator,
@@ -748,20 +760,7 @@ impl<'a> SlotRanking<'a> {
continue;
}
let symbol_id = SymbolId::from_usize(symbol_id);
let symbol_scope_id = scoping.symbol_scope_id(symbol_id);
if symbol_scope_id == root_scope_id
&& (!constraints.top_level
|| exported_symbols.is_some_and(|e| e.has_bit(symbol_id.index())))
{
continue;
}
if scoping.scope_flags(symbol_scope_id).contains_direct_eval() {
continue;
}
if constraints.is_kept_name(scoping.symbol_name(symbol_id)) {
continue;
}
if keep_name_symbols.is_some_and(|keep| keep.has_bit(symbol_id.index())) {
if !constraints.is_mangle_candidate(symbol_id, scoping) {
continue;
}
let index = slot as usize;
+16 -5
View File
@@ -339,8 +339,8 @@ fn reserved_names() {
? t(exports)
: t((e.lib = {}));
})(this, function (exports) {
function t(e) { return e; }
exports.queue = t;
function e(e) { return e; }
exports.queue = e;
});",
&options,
);
@@ -353,9 +353,9 @@ fn reserved_names() {
exports.other = helper;
})(m, m.exports);",
"(function (module, exports) {
function t(e) { return e; }
module.exports.helper = t;
exports.other = t;
function e(e) { return e; }
module.exports.helper = e;
exports.other = e;
})(m, m.exports);",
&options,
);
@@ -375,6 +375,17 @@ fn reserved_names() {
);
}
#[test]
fn non_mangleable_symbols_do_not_consume_slots() {
let options = MangleOptions { debug: true, ..MangleOptions::default() };
test(
"var A, B; { use(A); let x; } { use(B); let y; }",
"var A, B; { use(A); let slot_0; } { use(B); let slot_0; }",
&options,
);
}
/// Annex B.3.2.1: In sloppy mode, function declarations inside blocks have var-like hoisting.
/// The mangler must not assign the same name to such a function and an outer `var` binding.
#[test]
+1 -1
View File
@@ -23,5 +23,5 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
6.69 MB | 2.12 MB | 2.31 MB | 453.79 kB | 488.28 kB | 5 | antd.js
10.95 MB | 3.33 MB | 3.49 MB | 852.64 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.33 MB | 3.49 MB | 852.09 kB | 915.50 kB | 4 | typescript.js