mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(minifier): avoid stale pure function summaries (#24636)
Pure-function summaries trusted resolved read and write references, but redeclarations, direct `eval`, and classic Script globals can replace a binding without producing such a reference. A later call could therefore be folded according to an earlier empty declaration and lose runtime side effects.
## Example
```js
function foo() {}
eval("foo = () => side_effect()")
foo()
```
Bindings with redeclarations, bindings visible to direct eval, and classic Script-root bindings do not receive a reusable summary; the eval restriction is intentionally non-sticky so removing the final eval can enable the optimization in a later pass. Module and CommonJS bindings retain the existing fold.
The PR tip passed the full `oxc_minifier` suite independently. The completed stack also passed clippy with warnings denied, formatting, minsize and allocation regeneration, and `just ready`.
## Stack
1. **#24636 — function-summary correctness**
2. #24637 — explicit symbol effect types
3. #24638 — grouped reference counts
4. #24639 — merged persistent metadata
5. #24640 — SymbolState facade
Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
This commit is contained in:
1 parent
54cc4415af
commit
061af1f9c6
4 files changed
+85
-4
No files matched your search
@@ -496,6 +496,20 @@ impl<'a> PeepholeOptimizations {
|
||||
return;
|
||||
}
|
||||
let Some(symbol_id) = id.and_then(|id| id.symbol_id.get()) else { return };
|
||||
let binding_scope_id = ctx.scoping().symbol_scope_id(symbol_id);
|
||||
let binding_scope_flags = ctx.scoping().scope_flags(binding_scope_id);
|
||||
// These bindings can be replaced without producing a resolved write
|
||||
// reference: redeclarations are span-only in semantic, direct eval can
|
||||
// assign through the scope chain, and Script globals alias properties
|
||||
// of the global object. A different function may therefore win at
|
||||
// runtime even when this declaration is pure.
|
||||
if !ctx.scoping().symbol_redeclarations(symbol_id).is_empty()
|
||||
|| binding_scope_flags.contains_direct_eval()
|
||||
|| (ctx.source_type().is_script() && binding_scope_id == ctx.scoping().root_scope_id())
|
||||
{
|
||||
ctx.state.pure_functions.remove(&symbol_id);
|
||||
return;
|
||||
}
|
||||
if ctx.scoping().get_resolved_references(symbol_id).all(|r| r.flags().is_read_only()) {
|
||||
ctx.state.pure_functions.insert(
|
||||
symbol_id,
|
||||
|
||||
@@ -893,7 +893,9 @@ fn dce_keeps_sloppy_duplicate_block_functions() {
|
||||
#[test]
|
||||
fn dce_keeps_script_root_var_in_nested_statement_after_cycle_removed() {
|
||||
let source = "function outer() { function d1() { return x + d2() } function d2() { return d1() } return 1 } outer(); switch (1) { case 1: var x = 42; }";
|
||||
let expected = "function outer() { return 1 } switch (1) { case 1: var x = 42; }";
|
||||
// Script globals can be rebound through global-object properties without a
|
||||
// resolved write reference, so the call cannot reuse a pure summary.
|
||||
let expected = "function outer() { return 1 } outer(); switch (1) { case 1: var x = 42; }";
|
||||
test_source_type(source, expected, SourceType::script());
|
||||
|
||||
// CommonJS top-level vars are wrapper-local, so ordinary counts may remove them.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::{
|
||||
CompressOptions, CompressOptionsUnused, default_options, test, test_options, test_same,
|
||||
test_same_options,
|
||||
CompressOptions, CompressOptionsUnused, default_options, test, test_options,
|
||||
test_options_source_type, test_same, test_same_options, test_same_options_source_type,
|
||||
};
|
||||
|
||||
#[track_caller]
|
||||
@@ -287,3 +289,64 @@ fn remove_empty_function() {
|
||||
test_same_options("function* foo({}) {} foo()", &options);
|
||||
test_options("var foo = function*({}) {}; foo()", "(function*({}) {})()", &options);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redeclared_pure_function_is_not_folded_var() {
|
||||
test_same("var foo = (u) => {}; if (g) var foo = (a) => { console.log(a); }; foo('x');");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redeclared_pure_function_is_not_folded_function_declaration() {
|
||||
test_same_options_source_type(
|
||||
"function foo(u) {} function foo(a) { console.log(a); } foo('x');",
|
||||
SourceType::cjs().with_script(true),
|
||||
&default_options(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_eval_rebound_function_is_not_folded() {
|
||||
test(
|
||||
"function foo() {} eval(\"foo = () => side_effect()\"); foo();",
|
||||
"function foo() {} eval(\"foo = () => side_effect()\"), foo();",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_direct_eval_rebound_function_is_not_folded() {
|
||||
test(
|
||||
"function foo() {} function g() { eval(\"foo = () => side_effect()\") } g(); foo();",
|
||||
"function foo() {} function g() { eval(\"foo = () => side_effect()\") } g(), foo();",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_direct_eval_reenables_pure_function_folding() {
|
||||
test_options_source_type(
|
||||
"function foo() {} if (0) eval(\"foo = () => side_effect()\"); foo();",
|
||||
"",
|
||||
SourceType::cjs(),
|
||||
&CompressOptions::smallest(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_root_rebound_function_is_not_folded() {
|
||||
test_options_source_type(
|
||||
"function foo() {} globalThis.foo = () => side_effect(); foo();",
|
||||
"function foo() {} globalThis.foo = () => side_effect(), foo();",
|
||||
SourceType::cjs().with_script(true),
|
||||
&default_options(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_redeclared_pure_function_still_folds() {
|
||||
test("const foo = (u) => {}; foo(1)", "const foo = (u) => {};");
|
||||
test_options_source_type(
|
||||
"function foo() {} foo()",
|
||||
"function foo() {}",
|
||||
SourceType::cjs(),
|
||||
&default_options(),
|
||||
);
|
||||
}
|
||||
@@ -856,7 +856,9 @@ fn keep_sloppy_annex_b_alias_member_write_after_cycle_removed() {
|
||||
fn keep_script_root_var_in_nested_statement_after_cycle_removed() {
|
||||
let options = CompressOptions::smallest();
|
||||
let source = "function outer() { function d1() { return x + d2() } function d2() { return d1() } return 1 } outer(); switch (1) { case 1: var x = 42; }";
|
||||
let expected = "function outer() { return 1 } switch (1) { case 1: var x = 42; }";
|
||||
// Script globals can be rebound through global-object properties without a
|
||||
// resolved write reference, so the call cannot reuse a pure summary.
|
||||
let expected = "function outer() { return 1 } switch (outer(), 1) { case 1: var x = 42; }";
|
||||
test_options_source_type(source, expected, SourceType::script(), &options);
|
||||
|
||||
// CommonJS top-level vars are wrapper-local, so ordinary counts may remove them.
|
||||
|
||||
Reference in new issue
Block a user