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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
use crate::generated::ancestor::Ancestor;
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue};
use oxc_span::GetSpan;
use oxc_syntax::symbol::SymbolId;
use crate::TraverseCtx;
use crate::symbol_value::FreshValueKind;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut TraverseCtx<'a>) {
let BindingPattern::BindingIdentifier(ident) = &decl.id else { return };
let Some(symbol_id) = ident.symbol_id.get() else { return };
// Evaluate the initializer's constant once; reuse it for the value-context
// constant and the boolean-falsy fact below. `None` for a non-constant or
// absent initializer.
let init_constant = decl.init.as_ref().and_then(|e| e.evaluate_value(ctx));
// Whether the initializer is an explicit falsy constant (not the implicit
// `undefined` of `var x;`, which `init_constant` leaves `None`). Fed to
// `init_value`, which turns it into the `boolean_falsy` fact (see
// `SymbolValue::boolean_falsy`).
let falsy_init = init_constant.as_ref().is_some_and(Self::is_falsy_constant);
let value = if Self::is_for_statement_init(ctx) {
// for-statement initializers have their value set by the for statement itself.
None
} else if decl.kind.is_var() && !Self::is_hoisted_var_inlineable(decl, symbol_id, ctx) {
// `var` is hoisted: reads before the initializer line see `undefined`.
// Skip unless the safety predicate proves no such read exists.
None
} else {
// No initializer hoists to `undefined`; otherwise reuse the constant.
decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |_| init_constant)
};
let kind = decl.init.as_ref().map_or(FreshValueKind::None, Self::fresh_value_kind);
ctx.init_value(symbol_id, value, kind, falsy_init, decl.init.is_none());
}
/// A `ConstantValue` that coerces to `false` (`false`, `0`/`-0`/`NaN`, `""`,
/// `null`, `undefined`). BigInt is skipped conservatively.
fn is_falsy_constant(cv: &ConstantValue<'a>) -> bool {
match cv {
ConstantValue::Boolean(b) => !b,
ConstantValue::Number(n) => n.is_nan() || *n == 0.0,
ConstantValue::String(s) => s.as_ref().is_empty(),
ConstantValue::Null | ConstantValue::Undefined => true,
ConstantValue::BigInt(_) => false,
}
}
/// Predicate for inlining a hoisted `var x = <literal>;`. True when no read
/// can observe `x` as its hoisted `undefined`:
/// - the declarator sits at the current body's top scope and that body is
/// still in its declarative prelude;
/// - it has an initializer (uninitialized `var foo;` would inline to
/// `undefined`, which churns existing tests for marginal benefit);
/// - script-mode top-level vars are excluded (they alias the global object);
/// - at program scope, if the module loads any other module (`import`,
/// `export … from`, `export * from`), skip: a cyclic importer can call
/// into our exports and observe any var our exported functions/classes
/// close over, regardless of export status;
/// - every read sits inside a nested function/arrow body (the gap
/// `substitute_single_use_symbol` can't reach). Multiple such reads are
/// fine: the prelude check proves none observes the hoisted `undefined`,
/// so the value is constant at every read, and the small-value rule /
/// write-count guard in `inline_identifier_reference` decide whether each
/// read actually folds (e.g. a write-once falsy flag read by `if (flag)`
/// throughout — the Svelte/Vue `hydrating` shape, #14001).
///
/// Limitation: the constant is recorded here at the declarator's exit, so a
/// reader in a function declared *before* the var in source order has
/// already been visited and won't be inlined. Safe but suboptimal; the
/// common "flag declared at the top" pattern is unaffected.
fn is_hoisted_var_inlineable(
decl: &VariableDeclarator<'a>,
symbol_id: SymbolId,
ctx: &TraverseCtx<'a>,
) -> bool {
if decl.init.is_none() || Self::keep_top_level_var_in_script_mode(ctx) {
return false;
}
// `body_unsafe` is set by a preceding non-declarative statement, and the
// program root additionally starts unsafe when the module has loaders
// (see `enter_program`) — so this one check covers the cyclic-import gate.
let &(body_scope, body_unsafe) = ctx.state.body_unsafe_stack.last();
if body_unsafe || ctx.current_scope_id() != body_scope {
return false;
}
// At least one read, and every read crosses a function boundary.
let mut reads = ctx.scoping().get_resolved_references(symbol_id).filter(|r| r.is_read());
let Some(first) = reads.next() else { return false };
if !Self::read_crosses_function_boundary(first.scope_id(), body_scope, ctx) {
return false;
}
reads.all(|read| Self::read_crosses_function_boundary(read.scope_id(), body_scope, ctx))
}
/// Classify the fresh value an expression creates (a value that cannot alias
/// another binding and has no setters/getters that could trigger side effects
/// on property writes), or `FreshValueKind::None` when it is not fresh.
fn fresh_value_kind(expr: &Expression<'a>) -> FreshValueKind {
match expr {
Expression::ArrayExpression(_) => FreshValueKind::Array,
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => {
FreshValueKind::Function
}
Expression::ObjectExpression(obj) => {
// Object literals with setter/getter properties are not safe to treat as fresh.
// Setters trigger side effects on property writes.
// Getter-only properties throw TypeError in strict mode on write.
// Also check property values for nested setters/getters.
let has_side_effects = obj.properties.iter().any(|prop| {
matches!(
prop,
ObjectPropertyKind::ObjectProperty(p)
if matches!(p.kind, PropertyKind::Set | PropertyKind::Get)
|| Self::expression_has_setter_or_getter(&p.value)
// `{ __proto__: ... }` sets the prototype chain and could
// install setters that make property writes side-effectful.
|| (p.kind == PropertyKind::Init
&& !p.computed
&& p.key.is_specific_static_name("__proto__"))
)
});
if has_side_effects { FreshValueKind::None } else { FreshValueKind::Object }
}
Expression::ClassExpression(class) => {
if Self::class_may_have_property_side_effects(class) {
FreshValueKind::None
} else {
FreshValueKind::Class
}
}
_ => FreshValueKind::None,
}
}
/// Check if a class may have side effects on property writes.
/// Returns `true` if the class has static setters, static accessor properties,
/// static property definitions with values, or an `extends` clause.
/// Following SWC's approach: any class with static property definitions
/// is not considered fresh, because the static initializer runs during
/// class creation and defines the property via `[[DefineOwnProperty]]`.
fn class_may_have_property_side_effects(class: &Class<'a>) -> bool {
// Classes with `extends` may inherit static setters from the parent.
// We can't statically determine the parent's static setters,
// so conservatively mark as non-fresh.
if class.super_class.is_some() {
return true;
}
// Class-level decorators run arbitrary code during class creation and can
// replace the constructor or install setters — never fresh.
if !class.decorators.is_empty() {
return true;
}
class.body.body.iter().any(|element| match element {
ClassElement::MethodDefinition(method) => {
// Any decorator can install a setter or replace the member.
!method.decorators.is_empty()
// A static getter OR setter makes a write to that key throw in
// strict mode (a get-only accessor has no [[Set]]; a setter runs
// on write), so neither is a droppable fresh-value write.
|| (method.r#static
&& matches!(
method.kind,
MethodDefinitionKind::Set | MethodDefinitionKind::Get
))
}
// `static accessor foo` auto-generates a getter+setter pair.
ClassElement::AccessorProperty(prop) => prop.r#static || !prop.decorators.is_empty(),
// Any static property definition with a value prevents fresh marking.
// The value is evaluated during class creation and could interact with
// property writes in unexpected ways (e.g. nested setters, proxies).
ClassElement::PropertyDefinition(prop) => {
!prop.decorators.is_empty() || (prop.r#static && prop.value.is_some())
}
// A static block runs arbitrary code during class creation and can
// install setters or define static properties.
ClassElement::StaticBlock(_) => true,
ClassElement::TSIndexSignature(_) => false,
})
}
/// Check if an expression contains setter or getter definitions (recursively).
fn expression_has_setter_or_getter(expr: &Expression<'a>) -> bool {
match expr {
Expression::ObjectExpression(obj) => obj.properties.iter().any(|prop| {
matches!(
prop,
ObjectPropertyKind::ObjectProperty(p)
if matches!(p.kind, PropertyKind::Set | PropertyKind::Get)
|| Self::expression_has_setter_or_getter(&p.value)
)
}),
Expression::ClassExpression(class) => Self::class_may_have_property_side_effects(class),
_ => false,
}
}
/// Initialize symbol value for function declarations.
/// Function declarations always create fresh values (cannot alias another binding).
pub fn init_function_declaration_symbol_value(
id: Option<&BindingIdentifier<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
let Some(id) = id else { return };
let Some(symbol_id) = id.symbol_id.get() else { return };
ctx.init_value(symbol_id, None, FreshValueKind::Function, false, false);
}
/// Initialize symbol value for class declarations.
/// Class declarations create fresh values, but classes with static setters
/// are not considered fresh because property writes trigger setter side effects.
pub fn init_class_declaration_symbol_value(class: &Class<'a>, ctx: &mut TraverseCtx<'a>) {
let Some(id) = &class.id else { return };
let Some(symbol_id) = id.symbol_id.get() else { return };
let kind = if Self::class_may_have_property_side_effects(class) {
FreshValueKind::None
} else {
FreshValueKind::Class
};
ctx.init_value(symbol_id, None, kind, false, false);
}
fn is_for_statement_init(ctx: &TraverseCtx<'a>) -> bool {
ctx.ancestors().nth(1).is_some_and(Ancestor::is_parent_of_for_statement_left)
}
pub fn inline_identifier_reference(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::Identifier(ident) = expr else { return };
let reference_id = ident.reference_id();
let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else { return };
let Some(symbol_value) = ctx.state.symbol_values.get_symbol_value(symbol_id) else {
return;
};
// Skip if there are write references.
if symbol_value.write_references_count > 0 {
return;
}
let Some(cv) = &symbol_value.initialized_constant else { return };
// Textually inlining the implicit `undefined` of `let x;` can only grow
// the output; see `SymbolValue::implicit_undefined` (rolldown#10174).
if symbol_value.implicit_undefined {
return;
}
if symbol_value.read_references_count == 1
|| match cv {
ConstantValue::Number(n) => n.fract() == 0.0 && *n >= -99.0 && *n <= 999.0,
ConstantValue::BigInt(_) => false,
ConstantValue::String(s) => s.len() <= 3,
ConstantValue::Boolean(_) | ConstantValue::Undefined | ConstantValue::Null => true,
}
{
let new_expr = ctx.value_to_expr(expr.span(), cv.clone());
ctx.replace_expression(expr, new_expr);
}
}
}