Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mangler): reuse variable names #8562

Merged
merged 8 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ encoding_rs = "0.8.35"
encoding_rs_io = "0.1.7"
env_logger = { version = "0.11.5", default-features = false }
fast-glob = "0.4.0"
fixedbitset = "0.5.7"
flate2 = "1.0.35"
futures = "0.3.31"
globset = "0.4.15"
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_mangler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ oxc_index = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true }

fixedbitset = { workspace = true }
itertools = { workspace = true }
rustc-hash = { workspace = true }
80 changes: 55 additions & 25 deletions crates/oxc_mangler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::iter;
use std::ops::Deref;

use fixedbitset::FixedBitSet;
use itertools::Itertools;
use rustc_hash::FxHashSet;

Expand Down Expand Up @@ -88,11 +90,15 @@ impl Mangler {

#[must_use]
pub fn build(self, program: &Program<'_>) -> Mangler {
let semantic = SemanticBuilder::new().build(program).semantic;
let semantic =
SemanticBuilder::new().with_scope_tree_child_ids(true).build(program).semantic;
let (symbol_table, scope_tree) = semantic.into_symbol_table_and_scope_tree();
self.build_with_symbols_and_scopes(symbol_table, &scope_tree, program)
}

/// # Panics
///
/// Panics if the child_ids does not exist in scope_tree.
#[must_use]
pub fn build_with_symbols_and_scopes(
self,
Expand All @@ -117,6 +123,8 @@ impl Mangler {
program: &Program<'_>,
generate_name: G,
) -> Mangler {
assert!(scope_tree.has_child_ids(), "child_id needs to be generated");

let (exported_names, exported_symbols) = if self.options.top_level {
Mangler::collect_exported_symbols(program)
} else {
Expand All @@ -129,51 +137,73 @@ impl Mangler {
// A slot is the occurrence index of a binding identifier inside a scope.
let mut symbol_table = symbol_table;

// Total number of slots for all scopes
let mut total_number_of_slots: Slot = 0;

// All symbols with their assigned slots. Keyed by symbol id.
let mut slots: Vec<'_, Slot> = Vec::with_capacity_in(symbol_table.len(), &allocator);
for _ in 0..symbol_table.len() {
slots.push(0);
}

// Keep track of the maximum slot number for each scope
let mut max_slot_for_scope = Vec::with_capacity_in(scope_tree.len(), &allocator);
for _ in 0..scope_tree.len() {
max_slot_for_scope.push(0);
}
let mut slot_liveness: std::vec::Vec<FixedBitSet> = vec![];

// Walk the scope tree and compute the slot number for each scope
let mut tmp_bindings = std::vec::Vec::with_capacity(100);
for scope_id in scope_tree.descendants_from_root() {
for scope_id in iter::once(scope_tree.root_scope_id())
.chain(scope_tree.iter_all_child_ids(scope_tree.root_scope_id()))
{
let nearest_var_scope_id = scope_tree
.ancestors(scope_id)
.find(|s_id| scope_tree.get_flags(*s_id).is_var())
.unwrap_or(scope_tree.root_scope_id());
let bindings = scope_tree.get_bindings(scope_id);

// The current slot number is continued by the maximum slot from the parent scope
let parent_max_slot = scope_tree
.get_parent_id(scope_id)
.map_or(0, |parent_scope_id| max_slot_for_scope[parent_scope_id.index()]);

let mut slot = parent_max_slot;
let mut slot = slot_liveness.len();

if !bindings.is_empty() {
let reusable_slots = Vec::from_iter_in(
slot_liveness
.iter()
.enumerate()
.filter(|(_, slot_liveness)| !slot_liveness.contains(scope_id.index()))
.map(|(slot, _)| slot)
.take(bindings.len()),
&allocator,
);
let remaining_count = bindings.len() - reusable_slots.len();

let assignable_slots =
reusable_slots.into_iter().chain(slot..slot + remaining_count);
slot += remaining_count;
if slot_liveness.len() < slot {
slot_liveness
.resize_with(slot, || FixedBitSet::with_capacity(scope_tree.len()));
}

// Sort `bindings` in declaration order.
tmp_bindings.clear();
tmp_bindings.extend(bindings.values().copied());
tmp_bindings.sort_unstable();
for symbol_id in &tmp_bindings {
slots[symbol_id.index()] = slot;
slot += 1;
for (symbol_id, assigned_slot) in tmp_bindings.iter().zip(assignable_slots) {
slots[symbol_id.index()] = assigned_slot;

let lived_scope_ids = symbol_table
.get_resolved_references(*symbol_id)
.flat_map(|reference| {
// treat all symbols as var for now
// the reusability can be improved by reducing the lived scope ids for const / let / class
scope_tree
.ancestors(reference.scope_id())
.take_while(|s_id| *s_id != nearest_var_scope_id)
})
.chain(iter::once(nearest_var_scope_id));
for scope_id in lived_scope_ids {
slot_liveness[assigned_slot].insert(scope_id.index());
}
}
}

max_slot_for_scope[scope_id.index()] = slot;

if slot > total_number_of_slots {
total_number_of_slots = slot;
}
}

let total_number_of_slots = slot_liveness.len();

let frequencies = self.tally_slot_frequencies(
&symbol_table,
&exported_symbols,
Expand Down
6 changes: 5 additions & 1 deletion crates/oxc_minifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ impl Minifier {
Stats::default()
};
let mangler = self.options.mangle.map(|options| {
let semantic = SemanticBuilder::new().with_stats(stats).build(program).semantic;
let semantic = SemanticBuilder::new()
.with_stats(stats)
.with_scope_tree_child_ids(true)
.build(program)
.semantic;
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();
Mangler::default()
.with_options(options)
Expand Down
18 changes: 9 additions & 9 deletions crates/oxc_minifier/tests/mangler/snapshots/mangler.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
source: crates/oxc_minifier/tests/mangler/mod.rs
---
function foo(a) {a}
function foo(b) {
b;
function foo(a) {
a;
}

function foo(a) { let _ = { x } }
function foo(b) {
let c = { x };
function foo(a) {
let b = { x };
}

function foo(a) { let { x } = y }
function foo(b) {
let { x: c } = y;
function foo(a) {
let { x: b } = y;
}

var x; function foo(a) { ({ x } = y) }
var x;
function foo(c) {
function foo(b) {
({x} = y);
}

Expand All @@ -32,8 +32,8 @@ function _(exports) {
}

function foo(a) {a}
function a(b) {
b;
function a(a) {
a;
}

export function foo() {}; foo()
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_semantic/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2119,7 +2119,7 @@ impl<'a> SemanticBuilder<'a> {

fn reference_identifier(&mut self, ident: &IdentifierReference<'a>) {
let flags = self.resolve_reference_usages();
let reference = Reference::new(self.current_node_id, flags);
let reference = Reference::new(self.current_node_id, self.current_scope_id, flags);
let reference_id = self.declare_reference(ident.name, reference);
ident.reference_id.set(Some(reference_id));
}
Expand Down
23 changes: 18 additions & 5 deletions crates/oxc_syntax/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use oxc_index::Idx;
#[cfg(feature = "serialize")]
use serde::{Serialize, Serializer};

use crate::{node::NodeId, symbol::SymbolId};
use crate::{node::NodeId, scope::ScopeId, symbol::SymbolId};

#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ReferenceId(NonMaxU32);
Expand Down Expand Up @@ -234,6 +234,8 @@ impl<'alloc> CloneIn<'alloc> for ReferenceFlags {
pub struct Reference {
/// The AST node making the reference.
node_id: NodeId,
/// The scope of the AST node making the reference.
scope_id: ScopeId,
/// The symbol being referenced.
///
/// This will be [`None`] if no symbol could be found within
Expand All @@ -248,14 +250,19 @@ pub struct Reference {
impl Reference {
/// Create a new unresolved reference.
#[inline]
pub fn new(node_id: NodeId, flags: ReferenceFlags) -> Self {
Self { node_id, symbol_id: None, flags }
pub fn new(node_id: NodeId, scope_id: ScopeId, flags: ReferenceFlags) -> Self {
Self { node_id, scope_id, symbol_id: None, flags }
}

/// Create a new resolved reference on a symbol.
#[inline]
pub fn new_with_symbol_id(node_id: NodeId, symbol_id: SymbolId, flags: ReferenceFlags) -> Self {
Self { node_id, symbol_id: Some(symbol_id), flags }
pub fn new_with_symbol_id(
node_id: NodeId,
scope_id: ScopeId,
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> Self {
Self { node_id, scope_id, symbol_id: Some(symbol_id), flags }
}

/// Get the id of the node that is referencing the symbol.
Expand All @@ -264,6 +271,12 @@ impl Reference {
self.node_id
}

/// Get the id of the scope that is referencing the symbol.
#[inline]
pub fn scope_id(&self) -> ScopeId {
self.scope_id
}

/// Get the id of the symbol being referenced.
///
/// Will return [`None`] if the symbol could not be resolved.
Expand Down
5 changes: 3 additions & 2 deletions crates/oxc_traverse/src/context/scoping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,16 @@ impl TraverseScoping {
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> ReferenceId {
let reference = Reference::new_with_symbol_id(NodeId::DUMMY, symbol_id, flags);
let reference =
Reference::new_with_symbol_id(NodeId::DUMMY, self.current_scope_id, symbol_id, flags);
let reference_id = self.symbols.create_reference(reference);
self.symbols.add_resolved_reference(symbol_id, reference_id);
reference_id
}

/// Create an unbound reference
pub fn create_unbound_reference(&mut self, name: &str, flags: ReferenceFlags) -> ReferenceId {
let reference = Reference::new(NodeId::DUMMY, flags);
let reference = Reference::new(NodeId::DUMMY, self.current_scope_id, flags);
let reference_id = self.symbols.create_reference(reference);
self.scopes.add_root_unresolved_reference(name, reference_id);
reference_id
Expand Down
2 changes: 1 addition & 1 deletion napi/minify/test/minify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('simple', () => {
it('matches output', () => {
const ret = minify('test.js', code, { sourcemap: true });
expect(ret).toStrictEqual({
'code': 'function foo(){var b;b(void 0)}foo();',
'code': 'function foo(){var a;a(void 0)}foo();',
'map': {
'mappings': 'AAAA,SAAS,KAAM,CAAE,IAAIA,EAAK,SAAc,AAAE,CAAC,KAAK',
'names': [
Expand Down
1 change: 1 addition & 0 deletions tasks/benchmark/benches/minifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn bench_mangler(criterion: &mut Criterion) {
allocator.reset();
let program = Parser::new(&allocator, source_text, source_type).parse().program;
let (symbols, scopes) = SemanticBuilder::new()
.with_scope_tree_child_ids(true)
.build(&program)
.semantic
.into_symbol_table_and_scope_tree();
Expand Down
24 changes: 12 additions & 12 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
| Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.67 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js
72.14 kB | 23.61 kB | 23.70 kB | 8.55 kB | 8.54 kB | react.development.js

173.90 kB | 59.79 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js
173.90 kB | 59.71 kB | 59.82 kB | 19.26 kB | 19.33 kB | moment.js

287.63 kB | 90.08 kB | 90.07 kB | 32.03 kB | 31.95 kB | jquery.js
287.63 kB | 89.58 kB | 90.07 kB | 31.08 kB | 31.95 kB | jquery.js

342.15 kB | 118.19 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js
342.15 kB | 117.76 kB | 118.14 kB | 43.67 kB | 44.37 kB | vue.js

544.10 kB | 71.75 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
544.10 kB | 71.50 kB | 72.48 kB | 25.92 kB | 26.20 kB | lodash.js

555.77 kB | 272.89 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
555.77 kB | 272.35 kB | 270.13 kB | 88.60 kB | 90.80 kB | d3.js

1.01 MB | 460.16 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js
1.01 MB | 458.28 kB | 458.89 kB | 123.94 kB | 126.71 kB | bundle.min.js

1.25 MB | 652.68 kB | 646.76 kB | 163.48 kB | 163.73 kB | three.js
1.25 MB | 650.82 kB | 646.76 kB | 161.51 kB | 163.73 kB | three.js

2.14 MB | 723.85 kB | 724.14 kB | 179.88 kB | 181.07 kB | victory.js
2.14 MB | 719.54 kB | 724.14 kB | 162.47 kB | 181.07 kB | victory.js

3.20 MB | 1.01 MB | 1.01 MB | 331.98 kB | 331.56 kB | echarts.js
3.20 MB | 1.01 MB | 1.01 MB | 325.40 kB | 331.56 kB | echarts.js

6.69 MB | 2.31 MB | 2.31 MB | 491.91 kB | 488.28 kB | antd.js
6.69 MB | 2.30 MB | 2.31 MB | 470.00 kB | 488.28 kB | antd.js

10.95 MB | 3.48 MB | 3.49 MB | 905.29 kB | 915.50 kB | typescript.js
10.95 MB | 3.37 MB | 3.49 MB | 866.68 kB | 915.50 kB | typescript.js