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
use crate::generated::ancestor::Ancestor;
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue};
use oxc_span::GetSpan;
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
use crate::TraverseCtx;
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 };
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 {
decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx))
};
let is_fresh_value = decl.init.as_ref().is_some_and(Self::is_fresh_value_expression);
ctx.init_value(symbol_id, value, is_fresh_value);
}
/// 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;
/// - exactly one read, and it sits inside a nested function/arrow body
/// (multi-use and same-call-frame reads are handled by
/// `inline_identifier_reference`'s small-value rule or by
/// `substitute_single_use_symbol`).
///
/// 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;
}
// Exactly one read, and it crosses a function boundary.
let mut reads = ctx.scoping().get_resolved_references(symbol_id).filter(|r| r.is_read());
let Some(read) = reads.next() else { return false };
reads.next().is_none()
&& Self::read_crosses_function_boundary(read.scope_id(), body_scope, ctx)
}
/// True if the scope chain from `read_scope` to `body_scope` (exclusive of
/// `body_scope`) crosses any `Function` scope. `substitute_single_use_symbol`
/// only walks adjacent statements within the same call frame, so this is
/// the gap our path fills.
fn read_crosses_function_boundary(
read_scope: ScopeId,
body_scope: ScopeId,
ctx: &TraverseCtx<'a>,
) -> bool {
let scoping = ctx.scoping();
scoping
.scope_ancestors(read_scope)
.take_while(|&s| s != body_scope)
.any(|s| scoping.scope_flags(s).is_function())
}
/// Check if an expression creates a fresh value that cannot alias another binding
/// and has no setters/getters that could trigger side effects on property writes.
fn is_fresh_value_expression(expr: &Expression<'a>) -> bool {
match expr {
Expression::ArrayExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::FunctionExpression(_) => true,
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.
!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__"))
)
})
}
Expression::ClassExpression(class) => {
!Self::class_may_have_property_side_effects(class)
}
_ => false,
}
}
/// 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.body.body.iter().any(|element| match element {
ClassElement::MethodDefinition(method) => {
method.r#static && method.kind == MethodDefinitionKind::Set
}
// `static accessor foo` auto-generates a getter+setter pair
ClassElement::AccessorProperty(prop) => prop.r#static,
// 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.r#static && prop.value.is_some(),
_ => 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, true);
}
/// 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 is_fresh = !Self::class_may_have_property_side_effects(class);
ctx.init_value(symbol_id, None, is_fresh);
}
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 };
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);
}
}
}