1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
use oxc_ecmascript::constant_evaluation::ConstantValue;
use rustc_hash::{FxHashMap, FxHashSet};
use oxc_allocator::{Allocator, BitSet};
use oxc_data_structures::stack::NonEmptyStack;
use oxc_semantic::Scoping;
use oxc_span::SourceType;
use oxc_str::Str;
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
use crate::{CompressOptions, symbol_value::SymbolValues};
/// Dirty data accumulated by the `replace_*` / `drop_*` helper calls between
/// two consumption points. Live from `MinifierState::new` so the pre-loop
/// `Normalize` pass records drops through the same typed helpers as the
/// peephole loop; consumed and re-initialized by `flush_pass_dirty` in the
/// `Compressor` driver after `Normalize` and after every peephole pass.
pub struct PassDirty<'a> {
/// `ReferenceId`s whose AST node has been removed and not re-installed
/// in any later mutation this pass.
///
/// Arena-allocated bitset sized to the program's `references_len()` at
/// construction / the previous flush. A `BitSet` (rather than an
/// `FxHashSet`) keeps the per-ident cost on the `DropDiff` hot path to
/// a direct array store instead of a hash + heap insert.
///
/// INVARIANT (the "capacity guard", relied on by `DropDiff`,
/// `Scoping::retain_resolved_references_excluding`, and the over-prune
/// debug assert): references minted MID-pass have indices beyond the
/// bitset's capacity and are treated as live everywhere — never marked,
/// never excluded. Conservative: such a reference stays in its symbol's
/// list until callers rebuild scoping (a missed optimization, never a
/// correctness issue). `Normalize` mints no references, so a capacity
/// taken at construction is exact for the first pass.
pub(crate) dead_refs: BitSet<'a>,
/// At least one direct `eval(...)` call was dropped this pass. Gates
/// the small `LiveDirectEvalCollector` walk at flush time.
pub(crate) eval_dropped: bool,
}
impl<'a> PassDirty<'a> {
pub fn new(references_len: usize, allocator: &'a Allocator) -> Self {
Self { dead_refs: BitSet::new_in(references_len, allocator), eval_dropped: false }
}
}
pub struct MinifierState<'a> {
pub source_type: SourceType,
pub options: CompressOptions,
/// Two modes: tree-shaking only (`true`), or full minify (`false`).
/// `Compressor::dead_code_elimination` uses `true`; `Compressor::build`
/// uses `false`.
///
/// "DCE" here does not mean "removes dead code" (full minify does that
/// too). It means tree-shaking: remove code that nothing imports, without
/// making the rest smaller. Rolldown runs this on every tree-shaking build,
/// with or without `minify`, so users can see this output directly.
///
/// In this mode `exit_*` only runs the passes that remove code, plus the
/// constant folds those removals need. For example, `fold_binary_expr`
/// folds `'production' === 'production'` to `true` so the dead `else`
/// branch can be dropped (this is how `define` values remove branches). The
/// passes that only shrink code (`substitute_*`, `minimize_*`) are left
/// out. See the `if ctx.state.dce` branch in `peephole/mod.rs`.
pub dce: bool,
/// The return value of function declarations that are pure
pub pure_functions: FxHashMap<SymbolId, Option<ConstantValue<'a>>>,
pub symbol_values: SymbolValues<'a>,
/// Private member usage for classes
pub class_symbols_stack: ClassSymbolsStack<'a>,
/// Symbols that have `__proto__` member writes.
/// Writing to `__proto__` changes the prototype chain, potentially installing
/// setters that make subsequent property writes side-effectful.
pub proto_write_symbols: FxHashSet<SymbolId>,
/// One frame per enclosing function body (program root at the bottom).
/// `(body_scope, body_unsafe)`. While `body_unsafe` is false, the next
/// `var x = <literal>;` whose declarator sits at `body_scope` is safe to
/// inline despite hoisting. A preceding non-declarative statement sets it;
/// the program root additionally starts unsafe when the module has any
/// loader (`import` / `export … from` / `export * from`), since a cyclic
/// importer could observe a not-yet-assigned binding our exports close over.
/// Pushed by `enter_function_body`, popped by `exit_function_body`. See
/// `init_symbol_value`.
pub body_unsafe_stack: NonEmptyStack<(ScopeId, bool)>,
/// Set when a typed helper mutates the AST. Private by design: the only
/// writers are the helpers on `MinifierTraverseCtx`; the only reader is
/// the fixed-point loop driver via `take_mutated()`.
mutated: bool,
/// Per-pass dirty accumulator populated by `replace_*` / `drop_*` helpers
/// as subtrees are removed. Consumed by `flush_pass_dirty` in the
/// `Compressor` driver (pre-loop and after each mutated pass) to drive
/// the incremental scoping refresh.
pub(crate) dirty: PassDirty<'a>,
/// Scratch buffer reused by `try_fold_concat` to build template literal
/// quasis without allocating a fresh `String` per call.
pub concat_scratch: String,
}
impl<'a> MinifierState<'a> {
pub fn new(
source_type: SourceType,
options: CompressOptions,
dce: bool,
scoping: &Scoping,
allocator: &'a Allocator,
) -> Self {
Self {
source_type,
options,
dce,
pure_functions: FxHashMap::default(),
symbol_values: SymbolValues::new(scoping.symbols_len()),
class_symbols_stack: ClassSymbolsStack::new(),
proto_write_symbols: FxHashSet::default(),
body_unsafe_stack: NonEmptyStack::new((scoping.root_scope_id(), false)),
mutated: false,
dirty: PassDirty::new(scoping.references_len(), allocator),
concat_scratch: String::new(),
}
}
/// Returns whether the AST was mutated since the last call, and resets.
/// Read and reset are one operation so the signal cannot be cleared
/// anywhere except at its single consumption point.
pub(crate) fn take_mutated(&mut self) -> bool {
std::mem::take(&mut self.mutated)
}
/// Record that a typed helper mutated the AST.
pub(crate) fn record_mutation(&mut self) {
self.mutated = true;
}
}
/// Stack to track class symbol information
pub struct ClassSymbolsStack<'a> {
stack: NonEmptyStack<FxHashSet<Str<'a>>>,
}
impl<'a> ClassSymbolsStack<'a> {
pub fn new() -> Self {
Self { stack: NonEmptyStack::new(FxHashSet::default()) }
}
/// Check if the stack is exhausted
pub fn is_exhausted(&self) -> bool {
self.stack.is_exhausted()
}
/// Enter a new class scope
pub fn push_class_scope(&mut self) {
self.stack.push(FxHashSet::default());
}
/// Exit the current class scope
pub fn pop_class_scope(&mut self, declared_private_symbols: impl Iterator<Item = Str<'a>>) {
let mut used_private_symbols = self.stack.pop();
declared_private_symbols.for_each(|name| {
used_private_symbols.remove(&name);
});
// if the symbol was not declared in this class, that is declared in the class outside the current class
self.stack.last_mut().extend(used_private_symbols);
}
/// Add a private member to the current class scope
pub fn push_private_member_to_current_class(&mut self, name: Str<'a>) {
self.stack.last_mut().insert(name);
}
/// Check if a private member is used in the current class scope
pub fn is_private_member_used_in_current_class(&self, name: &Str<'a>) -> bool {
self.stack.last().contains(name)
}
}