Skip to content

Commit

Permalink
feat(mangler): reuse variable names
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Jan 17, 2025
1 parent 007e8c0 commit 1b9026f
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 56 deletions.
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: Vec<FixedBitSet> = Vec::new_in(&allocator);

// 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, _)| !slot_liveness[*slot].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
scope_tree
.ancestors(reference.scope_id())
.take_while(|s_id| *s_id != nearest_var_scope_id)
})
.chain(iter::once(nearest_var_scope_id))
.dedup();
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.clone(), 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.70 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js
72.14 kB | 23.64 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.59 kB | 90.07 kB | 31.07 kB | 31.95 kB | jquery.js

342.15 kB | 118.14 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js
342.15 kB | 117.72 kB | 118.14 kB | 43.67 kB | 44.37 kB | vue.js

544.10 kB | 71.76 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
544.10 kB | 71.51 kB | 72.48 kB | 25.92 kB | 26.20 kB | lodash.js

555.77 kB | 272.91 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
555.77 kB | 272.37 kB | 270.13 kB | 88.60 kB | 90.80 kB | d3.js

1.01 MB | 460.17 kB | 458.89 kB | 126.76 kB | 126.71 kB | bundle.min.js
1.01 MB | 458.29 kB | 458.89 kB | 123.94 kB | 126.71 kB | bundle.min.js

1.25 MB | 652.88 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js
1.25 MB | 651.02 kB | 646.76 kB | 161.58 kB | 163.73 kB | three.js

2.14 MB | 724.06 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js
2.14 MB | 719.76 kB | 724.14 kB | 162.53 kB | 181.07 kB | victory.js

3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js
3.20 MB | 1.01 MB | 1.01 MB | 325.44 kB | 331.56 kB | echarts.js

6.69 MB | 2.32 MB | 2.31 MB | 492.44 kB | 488.28 kB | antd.js
6.69 MB | 2.30 MB | 2.31 MB | 470.44 kB | 488.28 kB | antd.js

10.95 MB | 3.49 MB | 3.49 MB | 907.09 kB | 915.50 kB | typescript.js
10.95 MB | 3.38 MB | 3.49 MB | 868.10 kB | 915.50 kB | typescript.js

0 comments on commit 1b9026f

Please sign in to comment.