feat(es/minifier): Implement more rules (#1871)

swc_ecma_codegen:
 - Fix codegen of `U+0028` and `U+0029`. 

swc_ecma_minifier:
 - Implement MVP.

swc_ecma_transforms_base:
 - `fixer`: Handle seq/await in rhs of for-of.
This commit is contained in:
강동윤 2021-07-30 10:11:27 +09:00 committed by GitHub
parent d64aa6f80d
commit b02e189d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
366 changed files with 238188 additions and 1364 deletions

View File

@ -160,7 +160,7 @@ jobs:
# Source map format # Source map format
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: "14" node-version: "16"
# We explicitly do this to cache properly. # We explicitly do this to cache properly.
- name: Install Rust - name: Install Rust
@ -170,7 +170,7 @@ jobs:
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 14 node-version: 16
- uses: denoland/setup-deno@v1 - uses: denoland/setup-deno@v1
with: with:

15
Cargo.lock generated
View File

@ -2430,7 +2430,7 @@ dependencies = [
[[package]] [[package]]
name = "swc_ecma_codegen" name = "swc_ecma_codegen"
version = "0.64.0" version = "0.64.2"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"num-bigint", "num-bigint",
@ -2500,11 +2500,12 @@ dependencies = [
[[package]] [[package]]
name = "swc_ecma_minifier" name = "swc_ecma_minifier"
version = "0.14.0" version = "0.15.0"
dependencies = [ dependencies = [
"ansi_term 0.12.1", "ansi_term 0.12.1",
"anyhow", "anyhow",
"fxhash", "fxhash",
"indexmap",
"log", "log",
"once_cell", "once_cell",
"pretty_assertions 0.6.1", "pretty_assertions 0.6.1",
@ -2522,7 +2523,9 @@ dependencies = [
"swc_ecma_transforms_base", "swc_ecma_transforms_base",
"swc_ecma_utils", "swc_ecma_utils",
"swc_ecma_visit", "swc_ecma_visit",
"swc_node_base",
"testing", "testing",
"unicode-xid",
"walkdir", "walkdir",
] ]
@ -2605,7 +2608,7 @@ dependencies = [
[[package]] [[package]]
name = "swc_ecma_transforms_base" name = "swc_ecma_transforms_base"
version = "0.24.0" version = "0.24.1"
dependencies = [ dependencies = [
"fxhash", "fxhash",
"once_cell", "once_cell",
@ -2788,7 +2791,7 @@ dependencies = [
[[package]] [[package]]
name = "swc_ecma_transforms_typescript" name = "swc_ecma_transforms_typescript"
version = "0.30.0" version = "0.30.1"
dependencies = [ dependencies = [
"fxhash", "fxhash",
"serde", "serde",
@ -2835,7 +2838,7 @@ dependencies = [
[[package]] [[package]]
name = "swc_ecmascript" name = "swc_ecmascript"
version = "0.48.0" version = "0.49.0"
dependencies = [ dependencies = [
"swc_ecma_ast", "swc_ecma_ast",
"swc_ecma_codegen", "swc_ecma_codegen",
@ -2940,7 +2943,7 @@ dependencies = [
"env_logger 0.7.1", "env_logger 0.7.1",
"log", "log",
"once_cell", "once_cell",
"pretty_assertions 0.6.1", "pretty_assertions 0.7.2",
"regex", "regex",
"swc_common", "swc_common",
"testing_macros", "testing_macros",

View File

@ -6,7 +6,7 @@ edition = "2018"
license = "Apache-2.0/MIT" license = "Apache-2.0/MIT"
name = "swc_ecmascript" name = "swc_ecmascript"
repository = "https://github.com/swc-project/swc.git" repository = "https://github.com/swc-project/swc.git"
version = "0.48.0" version = "0.49.0"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
@ -34,7 +34,7 @@ typescript = ["typescript-parser", "swc_ecma_transforms/typescript"]
swc_ecma_ast = {version = "0.49.0", path = "./ast"} swc_ecma_ast = {version = "0.49.0", path = "./ast"}
swc_ecma_codegen = {version = "0.64.0", path = "./codegen", optional = true} swc_ecma_codegen = {version = "0.64.0", path = "./codegen", optional = true}
swc_ecma_dep_graph = {version = "0.33.0", path = "./dep-graph", optional = true} swc_ecma_dep_graph = {version = "0.33.0", path = "./dep-graph", optional = true}
swc_ecma_minifier = {version = "0.14.0", path = "./minifier", optional = true} swc_ecma_minifier = {version = "0.15.0", path = "./minifier", optional = true}
swc_ecma_parser = {version = "0.65.0", path = "./parser", optional = true, default-features = false} swc_ecma_parser = {version = "0.65.0", path = "./parser", optional = true, default-features = false}
swc_ecma_transforms = {version = "0.61.0", path = "./transforms", optional = true} swc_ecma_transforms = {version = "0.61.0", path = "./transforms", optional = true}
swc_ecma_utils = {version = "0.41.0", path = "./utils", optional = true} swc_ecma_utils = {version = "0.41.0", path = "./utils", optional = true}

View File

@ -7,7 +7,7 @@ include = ["Cargo.toml", "src/**/*.rs"]
license = "Apache-2.0/MIT" license = "Apache-2.0/MIT"
name = "swc_ecma_codegen" name = "swc_ecma_codegen"
repository = "https://github.com/swc-project/swc.git" repository = "https://github.com/swc-project/swc.git"
version = "0.64.1" version = "0.64.2"
[dependencies] [dependencies]
bitflags = "1" bitflags = "1"

View File

@ -2543,6 +2543,13 @@ fn escape_without_source(v: &str, target: JscTarget, single_quote: bool) -> Stri
let _ = write!(buf, "\\x{:x}", c as u8); let _ = write!(buf, "\\x{:x}", c as u8);
} }
'\u{2028}' => {
buf.push_str("\\u2028");
}
'\u{2029}' => {
buf.push_str("\\u2029");
}
_ => { _ => {
buf.push(c); buf.push(c);
} }

View File

@ -560,7 +560,7 @@ fn test_escape_without_source() {
es2020("abcde", "abcde"); es2020("abcde", "abcde");
es2020( es2020(
"\x00\r\n\u{85}\u{2028}\u{2029};", "\x00\r\n\u{85}\u{2028}\u{2029};",
"\\x00\\r\\n\\x85\u{2028}\u{2029};", "\\x00\\r\\n\\x85\\u2028\\u2029;",
); );
es2020("\n", "\\n"); es2020("\n", "\\n");

View File

@ -0,0 +1,47 @@
{
"parser": "babel-eslint",
"rules": {
"indent": [
"error",
4
],
"semi": [
"error",
"always"
],
"space-unary-ops": 2,
"no-undef": "off",
"consistent-return": "off",
"no-prototype-builtins": "off",
"no-cond-assign": "off",
"multiline-ternary": [
"error",
"always-multiline"
],
"object-property-newline": [
"error",
{
"allowAllPropertiesOnSameLine": false
}
],
"function-call-argument-newline": [
"error",
"always"
],
"object-curly-newline": [
"error",
"always"
],
"comma-dangle": [
"error",
"always"
],
"function-paren-newline": [
"error",
"always"
],
"no-trailing-spaces": [
"error"
]
}
}

2
ecmascript/minifier/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Used to see ideal output using terser
lab.js

View File

@ -7,13 +7,14 @@ include = ["Cargo.toml", "src/**/*.rs", "src/lists/*.json"]
license = "Apache-2.0/MIT" license = "Apache-2.0/MIT"
name = "swc_ecma_minifier" name = "swc_ecma_minifier"
repository = "https://github.com/swc-project/swc.git" repository = "https://github.com/swc-project/swc.git"
version = "0.14.0" version = "0.15.0"
[features] [features]
debug = [] debug = []
[dependencies] [dependencies]
fxhash = "0.2.1" fxhash = "0.2.1"
indexmap = "1.7.0"
log = "0.4" log = "0.4"
once_cell = "1.5.2" once_cell = "1.5.2"
pretty_assertions = {version = "0.6.1", optional = true} pretty_assertions = {version = "0.6.1", optional = true}
@ -31,10 +32,12 @@ swc_ecma_transforms = {version = "0.61.0", path = "../transforms/", features = [
swc_ecma_transforms_base = {version = "0.24.0", path = "../transforms/base"} swc_ecma_transforms_base = {version = "0.24.0", path = "../transforms/base"}
swc_ecma_utils = {version = "0.41.0", path = "../utils"} swc_ecma_utils = {version = "0.41.0", path = "../utils"}
swc_ecma_visit = {version = "0.35.0", path = "../visit"} swc_ecma_visit = {version = "0.35.0", path = "../visit"}
unicode-xid = "0.2.2"
[dev-dependencies] [dev-dependencies]
ansi_term = "0.12.1" ansi_term = "0.12.1"
anyhow = "1" anyhow = "1"
pretty_assertions = "0.6.1" pretty_assertions = "0.6.1"
swc_node_base = {version = "0.2.0", path = "../../node/base"}
testing = {version = "0.12.0", path = "../../testing"} testing = {version = "0.12.0", path = "../../testing"}
walkdir = "2.3.1" walkdir = "2.3.1"

View File

@ -8,6 +8,26 @@ Currently name mangler is very simple. To focus on creating a MVP, I (kdy1) will
## Testing ## Testing
### Real-library tests
You can use vscode to see diffs. Select a file from `tests/projects/output` and select the corresponding file from `tests/projects/refs`. Then you can right click to see an option to open diff.
#### Splitting a library test
Extracting only subset from a library test makes development much easier.
Split status:
- [ ] angular-1.2.5
- [x] backbone-1.1.0
- [ ] jquery-1.9.1
- [ ] jquery.mobile-1.4.2
- [ ] mootools-1.4.5
- [x] underscore-1.5.2
- [ ] yui-3.12.0
### Tests from terser
Tests ported from terser has Tests ported from terser has
- input.js - input.js
@ -16,6 +36,6 @@ Tests ported from terser has
- output.js (if exists and can be modified if our one is better or due to lack of frequency-aware base54) - output.js (if exists and can be modified if our one is better or due to lack of frequency-aware base54)
- output.terser.js (if exists) - output.terser.js (if exists)
## Copying tests #### Copying tests
Replace the content of `terser/test/compress.js` with it of `scripts/compress.js` and run `npm run test:compress` Replace the content of `terser/test/compress.js` with it of `scripts/compress.js` and run `npm run test:compress`

View File

@ -0,0 +1,20 @@
{
"name": "minifier",
"private": true,
"version": "1.0.0",
"description": "EcmaScript minifier for the swc project. This is basically a port of terser.",
"main": "lab.js",
"directories": {
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.29.0"
}
}

View File

@ -6,10 +6,10 @@ set -eu
export SWC_RUN=0 export SWC_RUN=0
cargo test --test compress --all-features \ cargo test --test compress --all-features fixture_tests__terser__compress__ \
| grep 'terser__compress' \ | grep 'fixture_tests__terser__compress__' \
| grep 'js .\.\. ok$' \ | grep 'js .\.\. ok$' \
| sed -e 's!test fixture_terser__compress__!!' \ | sed -e 's!test fixture_tests__terser__compress__!!' \
| sed -e 's! ... ok!!' \ | sed -e 's! ... ok!!' \
| sed -e 's!__!/!g' \ | sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \ | sed -e 's!_js!.js!' \

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -eux
# Prevent regression
./scripts/run.sh
export RUST_BACKTRACE=1
export RUST_LOG=swc_ecma_minifier=trace
touch tests/compress.rs
UPDATE=1 cargo test --test compress files__projects --all-features || true
# Make it easier to compare
prettier --write tests/projects/output
yarn run eslint --fix ./tests/projects/output/
ls -al ./tests/projects/output
ls -al ./tests/projects/refs

View File

@ -5,4 +5,4 @@ export RUST_LOG=swc_ecma_minifier=trace
# To prevent regression, we run base test before real tests. # To prevent regression, we run base test before real tests.
touch tests/compress.rs touch tests/compress.rs
cargo test --test compress ${1-base_exec} --all-features cargo test --test compress ${1-base_exec} -q --all-features

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
#
# This scripts print the ideal output of lab.js
#
set -eux
terser --compress --format beautify -- lab.js
node lab.js

View File

@ -3,10 +3,10 @@ set -eu
export SWC_RUN=0 export SWC_RUN=0
cargo test --test compress --all-features ${1-''} \ cargo test --test compress --all-features ${1-'fixture_tests__terser__compress__'} \
| grep 'terser__compress' \ | grep 'fixture_tests__terser__compress__' \
| grep 'js .\.\. FAILED$' \ | grep 'js .\.\. FAILED$' \
| sed -e 's!test fixture_terser__compress__!!' \ | sed -e 's!test fixture_tests__terser__compress__!!' \
| sed -e 's! ... FAILED!!' \ | sed -e 's! ... FAILED!!' \
| sed -e 's!__!/!g' \ | sed -e 's!__!/!g' \
| sed -e 's!_js!.js!' \ | sed -e 's!_js!.js!' \

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eu
run() {
terser --compress -- ./tests/projects/files/$1 > ./tests/projects/refs/$1
}
mkdir -p ./tests/projects/refs
run angular-1.2.5.js
run backbone-1.1.0.js
run jquery-1.9.1.js
run jquery.mobile-1.4.2.js
run mootools-1.4.5.js
run underscore-1.5.2.js
run yui-3.12.0.js
run react-17.0.2.js
run react-dom-17.0.2.js
prettier --write tests/projects/refs/
yarn run eslint --fix ./tests/projects/refs/

View File

@ -13,6 +13,9 @@ set -eu
export RUST_LOG=swc_ecma_minifier=trace export RUST_LOG=swc_ecma_minifier=trace
# Run unit tests.
cargo test --all-features --lib
# To prevent regression, we run base test before real tests. # To prevent regression, we run base test before real tests.
touch tests/compress.rs touch tests/compress.rs
UPDATE=1 ./scripts/base.sh base_fixture UPDATE=1 ./scripts/base.sh base_fixture
@ -21,7 +24,7 @@ UPDATE=1 ./scripts/base.sh base_fixture
if [ -z "$@" ]; then if [ -z "$@" ]; then
./scripts/sort.sh ./scripts/sort.sh
SWC_RUN=0 GOLDEN_ONLY=1 cargo test --test compress --all-features SWC_RUN=0 GOLDEN_ONLY=1 cargo test -q --test compress --all-features fixture_tests__terser__compress__
fi fi
SKIP_GOLDEN=1 cargo test --test compress --all-features $@ SKIP_GOLDEN=1 cargo test --test compress --all-features $@

View File

@ -12,4 +12,4 @@ set -eu
echo '' > tests/ignored.txt echo '' > tests/ignored.txt
scripts/add-golden.sh scripts/add-golden.sh
scripts/ignore.sh '' scripts/ignore.sh 'fixture_tests__terser__compress__'

View File

@ -17,6 +17,9 @@ impl UsageAnalyzer {
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub(super) struct Ctx { pub(super) struct Ctx {
pub var_decl_kind_of_pat: Option<VarDeclKind>, pub var_decl_kind_of_pat: Option<VarDeclKind>,
pub in_var_decl_with_no_side_effect_for_member_access: bool,
pub in_pat_of_var_decl: bool, pub in_pat_of_var_decl: bool,
pub in_pat_of_var_decl_with_init: bool, pub in_pat_of_var_decl_with_init: bool,
pub in_pat_of_param: bool, pub in_pat_of_param: bool,
@ -38,6 +41,8 @@ pub(super) struct Ctx {
pub in_update_arg: bool, pub in_update_arg: bool,
pub in_assign_lhs: bool, pub in_assign_lhs: bool,
pub in_cond: bool, pub in_cond: bool,
pub inline_prevented: bool,
} }
pub(super) struct WithCtx<'a> { pub(super) struct WithCtx<'a> {

View File

@ -4,6 +4,7 @@ use crate::util::idents_used_by;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use fxhash::FxHashSet; use fxhash::FxHashSet;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::time::Instant;
use swc_atoms::JsWord; use swc_atoms::JsWord;
use swc_common::SyntaxContext; use swc_common::SyntaxContext;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
@ -24,6 +25,8 @@ pub(crate) fn analyze<N>(n: &N) -> ProgramData
where where
N: VisitWith<UsageAnalyzer>, N: VisitWith<UsageAnalyzer>,
{ {
let start_time = Instant::now();
let mut v = UsageAnalyzer { let mut v = UsageAnalyzer {
data: Default::default(), data: Default::default(),
scope: Default::default(), scope: Default::default(),
@ -33,11 +36,17 @@ where
let top_scope = v.scope; let top_scope = v.scope;
v.data.top.merge(top_scope, false); v.data.top.merge(top_scope, false);
let end_time = Instant::now();
log::debug!("Scope analysis took {:?}", end_time - start_time);
v.data v.data
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct VarUsageInfo { pub(crate) struct VarUsageInfo {
pub inline_prevented: bool,
/// The number of reference to this identifier. /// The number of reference to this identifier.
pub ref_count: usize, pub ref_count: usize,
@ -51,6 +60,8 @@ pub(crate) struct VarUsageInfo {
/// `true` if the enclosing function defines this variable as a parameter. /// `true` if the enclosing function defines this variable as a parameter.
pub declared_as_fn_param: bool, pub declared_as_fn_param: bool,
pub declared_as_fn_expr: bool,
pub assign_count: usize, pub assign_count: usize,
pub mutation_by_call_count: usize, pub mutation_by_call_count: usize,
pub usage_count: usize, pub usage_count: usize,
@ -71,6 +82,8 @@ pub(crate) struct VarUsageInfo {
/// functions. /// functions.
pub is_fn_local: bool, pub is_fn_local: bool,
used_by_nested_fn: bool,
pub used_in_loop: bool, pub used_in_loop: bool,
pub var_kind: Option<VarDeclKind>, pub var_kind: Option<VarDeclKind>,
@ -83,6 +96,8 @@ pub(crate) struct VarUsageInfo {
/// Indicates a variable or function is overrided without using it. /// Indicates a variable or function is overrided without using it.
pub overriden_without_used: bool, pub overriden_without_used: bool,
pub no_side_effect_for_member_access: bool,
/// In `c = b`, `b` inffects `c`. /// In `c = b`, `b` inffects `c`.
infects: Vec<Id>, infects: Vec<Id>,
} }
@ -143,9 +158,12 @@ impl ProgramData {
to.merge(scope, false); to.merge(scope, false);
} }
for (id, var_info) in child.vars { for (id, mut var_info) in child.vars {
// log::trace!("merge({:?},{}{:?})", kind, id.0, id.1);
match self.vars.entry(id) { match self.vars.entry(id) {
Entry::Occupied(mut e) => { Entry::Occupied(mut e) => {
e.get_mut().inline_prevented |= var_info.inline_prevented;
e.get_mut().ref_count += var_info.ref_count; e.get_mut().ref_count += var_info.ref_count;
e.get_mut().cond_init |= var_info.cond_init; e.get_mut().cond_init |= var_info.cond_init;
@ -158,6 +176,7 @@ impl ProgramData {
e.get_mut().declared |= var_info.declared; e.get_mut().declared |= var_info.declared;
e.get_mut().declared_count += var_info.declared_count; e.get_mut().declared_count += var_info.declared_count;
e.get_mut().declared_as_fn_param |= var_info.declared_as_fn_param; e.get_mut().declared_as_fn_param |= var_info.declared_as_fn_param;
e.get_mut().declared_as_fn_expr |= var_info.declared_as_fn_expr;
// If a var is registered at a parent scope, it means that it's delcared before // If a var is registered at a parent scope, it means that it's delcared before
// usages. // usages.
@ -172,14 +191,30 @@ impl ProgramData {
e.get_mut().infects.extend(var_info.infects); e.get_mut().infects.extend(var_info.infects);
e.get_mut().no_side_effect_for_member_access =
e.get_mut().no_side_effect_for_member_access
&& var_info.no_side_effect_for_member_access;
match kind { match kind {
ScopeKind::Fn => { ScopeKind::Fn => {
e.get_mut().is_fn_local = false; e.get_mut().is_fn_local = false;
e.get_mut().used_by_nested_fn = true;
}
ScopeKind::Block => {
if var_info.used_by_nested_fn {
e.get_mut().is_fn_local = false;
e.get_mut().used_by_nested_fn = true;
}
} }
ScopeKind::Block => {}
} }
} }
Entry::Vacant(e) => { Entry::Vacant(e) => {
match kind {
ScopeKind::Fn => {
var_info.used_by_nested_fn = true;
}
ScopeKind::Block => {}
}
e.insert(var_info); e.insert(var_info);
} }
} }
@ -225,17 +260,25 @@ impl UsageAnalyzer {
} }
fn report(&mut self, i: Id, is_modify: bool, dejavu: &mut FxHashSet<Id>) { fn report(&mut self, i: Id, is_modify: bool, dejavu: &mut FxHashSet<Id>) {
// log::trace!("report({}{:?})", i.0, i.1);
let is_first = dejavu.is_empty(); let is_first = dejavu.is_empty();
if !dejavu.insert(i.clone()) { if !dejavu.insert(i.clone()) {
return; return;
} }
let e = self.data.vars.entry(i).or_insert_with(|| VarUsageInfo { let e = self.data.vars.entry(i.clone()).or_insert_with(|| {
// log::trace!("insert({}{:?})", i.0, i.1);
VarUsageInfo {
used_above_decl: true, used_above_decl: true,
..Default::default() ..Default::default()
}
}); });
e.inline_prevented |= self.ctx.inline_prevented;
e.ref_count += 1; e.ref_count += 1;
e.reassigned |= is_first && is_modify && self.ctx.is_exact_reassignment; e.reassigned |= is_first && is_modify && self.ctx.is_exact_reassignment;
// Passing object as a argument is possibly modification. // Passing object as a argument is possibly modification.
@ -258,7 +301,7 @@ impl UsageAnalyzer {
} }
fn report_usage(&mut self, i: &Ident, is_assign: bool) { fn report_usage(&mut self, i: &Ident, is_assign: bool) {
self.report(i.to_id(), is_assign, &mut Default::default()) self.report(i.to_id(), is_assign, &mut Default::default());
} }
fn declare_decl( fn declare_decl(
@ -266,7 +309,12 @@ impl UsageAnalyzer {
i: &Ident, i: &Ident,
has_init: bool, has_init: bool,
kind: Option<VarDeclKind>, kind: Option<VarDeclKind>,
is_fn_decl: bool,
) -> &mut VarUsageInfo { ) -> &mut VarUsageInfo {
// log::trace!("declare_decl({}{:?})", i.sym, i.span.ctxt);
let ctx = self.ctx;
let v = self let v = self
.data .data
.vars .vars
@ -282,6 +330,9 @@ impl UsageAnalyzer {
is_fn_local: true, is_fn_local: true,
var_kind: kind, var_kind: kind,
var_initialized: has_init, var_initialized: has_init,
no_side_effect_for_member_access: ctx
.in_var_decl_with_no_side_effect_for_member_access,
..Default::default() ..Default::default()
}); });
self.scope self.scope
@ -297,6 +348,8 @@ impl UsageAnalyzer {
} }
v.declared_as_catch_param |= self.ctx.in_catch_param; v.declared_as_catch_param |= self.ctx.in_catch_param;
v.declared_as_fn_expr |= is_fn_decl;
v v
} }
} }
@ -385,8 +438,22 @@ impl Visit for UsageAnalyzer {
} }
} }
fn visit_class(&mut self, n: &Class, _: &dyn Node) {
n.decorators.visit_with(n, self);
{
let ctx = Ctx {
inline_prevented: true,
..self.ctx
};
n.super_class.visit_with(n, &mut *self.with_ctx(ctx));
}
n.body.visit_with(n, self);
}
fn visit_class_decl(&mut self, n: &ClassDecl, _: &dyn Node) { fn visit_class_decl(&mut self, n: &ClassDecl, _: &dyn Node) {
self.declare_decl(&n.ident, true, None); self.declare_decl(&n.ident, true, None, false);
n.visit_children_with(self); n.visit_children_with(self);
} }
@ -415,8 +482,20 @@ impl Visit for UsageAnalyzer {
} }
} }
fn visit_fn_expr(&mut self, n: &FnExpr, _: &dyn Node) {
n.visit_children_with(self);
if let Some(id) = &n.ident {
self.data
.vars
.entry(id.to_id())
.or_default()
.declared_as_fn_expr = true;
}
}
fn visit_fn_decl(&mut self, n: &FnDecl, _: &dyn Node) { fn visit_fn_decl(&mut self, n: &FnDecl, _: &dyn Node) {
self.declare_decl(&n.ident, true, None); self.declare_decl(&n.ident, true, None, true);
n.visit_children_with(self); n.visit_children_with(self);
} }
@ -432,12 +511,14 @@ impl Visit for UsageAnalyzer {
}; };
n.left.visit_with(n, &mut *child.with_ctx(ctx)); n.left.visit_with(n, &mut *child.with_ctx(ctx));
n.right.visit_with(n, child);
let ctx = Ctx { let ctx = Ctx {
in_loop: true, in_loop: true,
in_cond: true, in_cond: true,
..child.ctx ..child.ctx
}; };
n.body.visit_with(n, &mut *child.with_ctx(ctx)) n.body.visit_with(n, &mut *child.with_ctx(ctx));
}); });
} }
@ -597,6 +678,7 @@ impl Visit for UsageAnalyzer {
&i.id, &i.id,
self.ctx.in_pat_of_var_decl_with_init, self.ctx.in_pat_of_var_decl_with_init,
self.ctx.var_decl_kind_of_pat, self.ctx.var_decl_kind_of_pat,
false,
); );
if in_pat_of_param { if in_pat_of_param {
@ -735,6 +817,10 @@ impl Visit for UsageAnalyzer {
let ctx = Ctx { let ctx = Ctx {
in_pat_of_var_decl: true, in_pat_of_var_decl: true,
in_pat_of_var_decl_with_init: e.init.is_some(), in_pat_of_var_decl_with_init: e.init.is_some(),
in_var_decl_with_no_side_effect_for_member_access: match e.init.as_deref() {
Some(Expr::Array(..) | Expr::Lit(..)) => true,
_ => false,
},
..self.ctx ..self.ctx
}; };
e.name.visit_with(e, &mut *self.with_ctx(ctx)); e.name.visit_with(e, &mut *self.with_ctx(ctx));

View File

@ -1,4 +1,3 @@
use crate::analyzer::analyze;
use crate::analyzer::ProgramData; use crate::analyzer::ProgramData;
use crate::analyzer::UsageAnalyzer; use crate::analyzer::UsageAnalyzer;
use crate::util::is_hoisted_var_decl_without_init; use crate::util::is_hoisted_var_decl_without_init;
@ -24,21 +23,21 @@ pub(super) struct DeclHoisterConfig {
pub top_level: bool, pub top_level: bool,
} }
pub(super) fn decl_hoister(config: DeclHoisterConfig) -> Hoister { pub(super) fn decl_hoister(config: DeclHoisterConfig, data: &ProgramData) -> Hoister {
Hoister { Hoister {
config, config,
changed: false, changed: false,
data: None, data,
} }
} }
pub(super) struct Hoister { pub(super) struct Hoister<'a> {
config: DeclHoisterConfig, config: DeclHoisterConfig,
changed: bool, changed: bool,
data: Option<ProgramData>, data: &'a ProgramData,
} }
impl Repeated for Hoister { impl Repeated for Hoister<'_> {
fn changed(&self) -> bool { fn changed(&self) -> bool {
self.changed self.changed
} }
@ -48,30 +47,23 @@ impl Repeated for Hoister {
} }
} }
impl Hoister { impl Hoister<'_> {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>) fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where where
T: StmtLike + IsModuleItem, T: StmtLike + IsModuleItem,
Vec<T>: VisitMutWith<Self> + VisitWith<UsageAnalyzer>, Vec<T>: for<'aa> VisitMutWith<Hoister<'aa>> + VisitWith<UsageAnalyzer>,
{ {
match self.data {
None => {
self.data = Some(analyze(stmts));
}
_ => {}
}
stmts.visit_mut_children_with(self); stmts.visit_mut_children_with(self);
let should_hoist = !is_sorted_by_key(stmts.iter(), |stmt| match stmt.as_stmt() { let should_hoist = !is_sorted_by_key(stmts.iter(), |stmt| match stmt.as_stmt() {
Some(stmt) => match stmt { Some(stmt) => match stmt {
Stmt::Decl(Decl::Fn(..)) if self.config.hoist_fns => 1, Stmt::Decl(Decl::Fn(..)) if self.config.hoist_fns => 1,
Stmt::Decl(Decl::Var(var)) if self.config.hoist_vars => { Stmt::Decl(Decl::Var(var)) if self.config.hoist_vars => {
if let Some(data) = &self.data {
let ids: Vec<Id> = find_ids(&var.decls); let ids: Vec<Id> = find_ids(&var.decls);
if ids.iter().any(|id| { if ids.iter().any(|id| {
data.vars self.data
.vars
.get(id) .get(id)
.map(|v| !v.used_above_decl) .map(|v| !v.used_above_decl)
.unwrap_or(false) .unwrap_or(false)
@ -80,9 +72,6 @@ impl Hoister {
} else { } else {
3 3
} }
} else {
2
}
} }
_ => 3, _ => 3,
}, },
@ -133,8 +122,8 @@ impl Hoister {
if decl.init.is_none() if decl.init.is_none()
&& self && self
.data .data
.as_ref() .vars
.and_then(|v| v.vars.get(&id.to_id())) .get(&id.to_id())
.map(|v| v.declared_as_fn_param) .map(|v| v.declared_as_fn_param)
.unwrap_or(false) .unwrap_or(false)
{ {
@ -207,8 +196,8 @@ impl Hoister {
if decl.init.is_none() if decl.init.is_none()
&& self && self
.data .data
.as_ref() .vars
.and_then(|v| v.vars.get(&name.to_id())) .get(&name.to_id())
.map(|v| v.declared_as_fn_param) .map(|v| v.declared_as_fn_param)
.unwrap_or(false) .unwrap_or(false)
{ {
@ -258,7 +247,7 @@ impl Hoister {
} }
} }
impl VisitMut for Hoister { impl VisitMut for Hoister<'_> {
noop_visit_mut_type!(); noop_visit_mut_type!();
fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) { fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {

View File

@ -1,9 +1,12 @@
use self::drop_console::drop_console; use self::drop_console::drop_console;
use self::hoist_decls::DeclHoisterConfig; use self::hoist_decls::DeclHoisterConfig;
use self::optimize::optimizer; use self::optimize::optimizer;
use crate::analyzer::analyze;
use crate::analyzer::ProgramData;
use crate::compress::hoist_decls::decl_hoister; use crate::compress::hoist_decls::decl_hoister;
use crate::debug::dump; use crate::debug::dump;
use crate::debug::invoke; use crate::debug::invoke;
use crate::marks::Marks;
use crate::option::CompressOptions; use crate::option::CompressOptions;
use crate::util::Optional; use crate::util::Optional;
#[cfg(feature = "pretty_assertions")] #[cfg(feature = "pretty_assertions")]
@ -13,12 +16,15 @@ use std::fmt;
use std::fmt::Debug; use std::fmt::Debug;
use std::fmt::Display; use std::fmt::Display;
use std::fmt::Formatter; use std::fmt::Formatter;
use swc_common::chain; use std::time::Instant;
use swc_common::comments::Comments; use swc_common::comments::Comments;
use swc_common::pass::CompilerPass; use swc_common::pass::CompilerPass;
use swc_common::pass::Repeat; use swc_common::pass::Repeat;
use swc_common::pass::Repeated; use swc_common::pass::Repeated;
use swc_common::sync::Lrc;
use swc_common::{chain, SourceMap};
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms::fixer;
use swc_ecma_transforms::optimization::simplify::dead_branch_remover; use swc_ecma_transforms::optimization::simplify::dead_branch_remover;
use swc_ecma_transforms::optimization::simplify::expr_simplifier; use swc_ecma_transforms::optimization::simplify::expr_simplifier;
use swc_ecma_transforms::pass::JsPass; use swc_ecma_transforms::pass::JsPass;
@ -34,7 +40,9 @@ mod drop_console;
mod hoist_decls; mod hoist_decls;
mod optimize; mod optimize;
pub fn compressor<'a>( pub(crate) fn compressor<'a>(
cm: Lrc<SourceMap>,
marks: Marks,
options: &'a CompressOptions, options: &'a CompressOptions,
comments: Option<&'a dyn Comments>, comments: Option<&'a dyn Comments>,
) -> impl 'a + JsPass { ) -> impl 'a + JsPass {
@ -43,10 +51,13 @@ pub fn compressor<'a>(
visitor: drop_console(), visitor: drop_console(),
}; };
let compressor = Compressor { let compressor = Compressor {
comments, cm,
marks,
options, options,
pass: 0, comments,
changed: false, changed: false,
pass: 0,
data: None,
}; };
chain!( chain!(
@ -57,10 +68,13 @@ pub fn compressor<'a>(
} }
struct Compressor<'a> { struct Compressor<'a> {
cm: Lrc<SourceMap>,
marks: Marks,
options: &'a CompressOptions, options: &'a CompressOptions,
comments: Option<&'a dyn Comments>, comments: Option<&'a dyn Comments>,
changed: bool, changed: bool,
pass: usize, pass: usize,
data: Option<ProgramData>,
} }
impl CompilerPass for Compressor<'_> { impl CompilerPass for Compressor<'_> {
@ -77,6 +91,7 @@ impl Repeated for Compressor<'_> {
fn reset(&mut self) { fn reset(&mut self) {
self.changed = false; self.changed = false;
self.pass += 1; self.pass += 1;
self.data = None;
} }
} }
@ -84,7 +99,7 @@ impl Compressor<'_> {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>) fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where where
T: StmtLike, T: StmtLike,
Vec<T>: VisitMutWith<Self> + VisitMutWith<hoist_decls::Hoister>, Vec<T>: VisitMutWith<Self> + for<'aa> VisitMutWith<hoist_decls::Hoister<'aa>>,
{ {
// Skip if `use asm` exists. // Skip if `use asm` exists.
if stmts.iter().any(|stmt| match stmt.as_stmt() { if stmts.iter().any(|stmt| match stmt.as_stmt() {
@ -104,17 +119,6 @@ impl Compressor<'_> {
return; return;
} }
{
let mut v = decl_hoister(DeclHoisterConfig {
hoist_fns: self.options.hoist_fns,
hoist_vars: self.options.hoist_vars,
top_level: self.options.top_level(),
});
stmts.visit_mut_with(&mut v);
self.changed |= v.changed();
}
// TODO: Hoist decls
stmts.visit_mut_children_with(self); stmts.visit_mut_children_with(self);
// TODO: drop unused // TODO: drop unused
@ -125,38 +129,56 @@ impl VisitMut for Compressor<'_> {
noop_visit_mut_type!(); noop_visit_mut_type!();
fn visit_mut_module(&mut self, n: &mut Module) { fn visit_mut_module(&mut self, n: &mut Module) {
debug_assert!(self.data.is_none());
self.data = Some(analyze(&*n));
if self.options.passes != 0 && self.options.passes + 1 <= self.pass { if self.options.passes != 0 && self.options.passes + 1 <= self.pass {
let done = dump(&*n); let done = dump(&*n);
log::trace!("===== Done =====\n{}", done); log::debug!("===== Done =====\n{}", done);
return; return;
} }
// Temporary // Temporary
if self.pass > 10 { if self.pass > 30 {
panic!("Infinite loop detected") panic!("Infinite loop detected (current pass = {})", self.pass)
} }
let start = if cfg!(feature = "debug") { let start = if cfg!(feature = "debug") {
let start = dump(&*n); let start = dump(&n.clone().fold_with(&mut fixer(None)));
log::trace!("===== Start =====\n{}", start); log::debug!("===== Start =====\n{}", start);
start start
} else { } else {
String::new() String::new()
}; };
{ {
log::info!(
"compress: Running expression simplifier (pass = {})",
self.pass
);
let start_time = Instant::now();
let mut visitor = expr_simplifier(); let mut visitor = expr_simplifier();
n.map_with_mut(|m| m.fold_with(&mut visitor)); n.visit_mut_with(&mut visitor);
self.changed |= visitor.changed(); self.changed |= visitor.changed();
if visitor.changed() { if visitor.changed() {
log::trace!("compressor: Simplified expressions"); log::debug!("compressor: Simplified expressions");
if cfg!(feature = "debug") { if cfg!(feature = "debug") {
log::trace!("===== Simplified =====\n{}", dump(&*n)); log::debug!("===== Simplified =====\n{}", dump(&*n));
} }
} }
let end_time = Instant::now();
log::info!(
"compress: expr_simplifier took {:?} (pass = {})",
end_time - start_time,
self.pass
);
if cfg!(feature = "debug") && !visitor.changed() { if cfg!(feature = "debug") && !visitor.changed() {
let simplified = dump(&*n); let simplified = dump(&n.clone().fold_with(&mut fixer(None)));
if start != simplified { if start != simplified {
assert_eq!( assert_eq!(
DebugUsingDisplay(&start), DebugUsingDisplay(&start),
@ -169,12 +191,31 @@ impl VisitMut for Compressor<'_> {
} }
{ {
log::info!("compress: Running optimizer (pass = {})", self.pass);
let start_time = Instant::now();
// TODO: reset_opt_flags // TODO: reset_opt_flags
// //
// This is swc version of `node.optimize(this);`. // This is swc version of `node.optimize(this);`.
let mut visitor = optimizer(self.options.clone(), self.comments); let mut visitor = optimizer(
self.cm.clone(),
self.marks,
self.options,
self.comments,
self.data.as_ref().unwrap(),
);
n.visit_mut_with(&mut visitor); n.visit_mut_with(&mut visitor);
self.changed |= visitor.changed(); self.changed |= visitor.changed();
let end_time = Instant::now();
log::info!(
"compress: Optimizer took {:?} (pass = {})",
end_time - start_time,
self.pass
);
// let done = dump(&*n);
// log::debug!("===== Result =====\n{}", done);
} }
if self.options.conditionals || self.options.dead_code { if self.options.conditionals || self.options.dead_code {
@ -184,14 +225,18 @@ impl VisitMut for Compressor<'_> {
"".into() "".into()
}; };
let start_time = Instant::now();
let mut v = dead_branch_remover(); let mut v = dead_branch_remover();
n.map_with_mut(|n| n.fold_with(&mut v)); n.map_with_mut(|n| n.fold_with(&mut v));
let end_time = Instant::now();
if cfg!(feature = "debug") { if cfg!(feature = "debug") {
let simplified = dump(&*n); let simplified = dump(&*n);
if start != simplified { if start != simplified {
log::trace!( log::debug!(
"===== Removed dead branches =====\n{}\n==== ===== ===== ===== ======\n{}", "===== Removed dead branches =====\n{}\n==== ===== ===== ===== ======\n{}",
start, start,
simplified simplified
@ -199,6 +244,12 @@ impl VisitMut for Compressor<'_> {
} }
} }
log::info!(
"compress: dead_branch_remover took {:?} (pass = {})",
end_time - start_time,
self.pass
);
self.changed |= v.changed(); self.changed |= v.changed();
} }
@ -207,14 +258,20 @@ impl VisitMut for Compressor<'_> {
invoke(&*n); invoke(&*n);
} }
fn visit_mut_stmt(&mut self, n: &mut Stmt) { fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
// TODO: Skip if node is already optimized. {
// if (has_flag(node, SQUEEZED)) return node; let mut v = decl_hoister(
DeclHoisterConfig {
n.visit_mut_children_with(self); hoist_fns: self.options.hoist_fns,
hoist_vars: self.options.hoist_vars,
top_level: self.options.top_level(),
},
self.data.as_ref().unwrap(),
);
stmts.visit_mut_with(&mut v);
self.changed |= v.changed();
} }
fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
self.handle_stmt_likes(stmts); self.handle_stmt_likes(stmts);
stmts.retain(|stmt| match stmt { stmts.retain(|stmt| match stmt {
@ -232,6 +289,26 @@ impl VisitMut for Compressor<'_> {
}); });
} }
fn visit_mut_script(&mut self, n: &mut Script) {
debug_assert!(self.data.is_none());
self.data = Some(analyze(&*n));
{
let mut v = decl_hoister(
DeclHoisterConfig {
hoist_fns: self.options.hoist_fns,
hoist_vars: self.options.hoist_vars,
top_level: self.options.top_level(),
},
self.data.as_ref().unwrap(),
);
n.body.visit_mut_with(&mut v);
self.changed |= v.changed();
}
n.visit_mut_children_with(self);
}
fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) { fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
self.handle_stmt_likes(stmts); self.handle_stmt_likes(stmts);

View File

@ -1,12 +1,13 @@
use super::Optimizer; use super::Optimizer;
use crate::analyzer::analyze;
use crate::compress::optimize::is_left_access_to_arguments; use crate::compress::optimize::is_left_access_to_arguments;
use std::iter::repeat_with; use std::iter::repeat_with;
use swc_atoms::js_word; use swc_atoms::js_word;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_utils::find_ids;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::private_ident; use swc_ecma_utils::private_ident;
use swc_ecma_utils::Id;
use swc_ecma_visit::noop_visit_mut_type; use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut; use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith; use swc_ecma_visit::VisitMutWith;
@ -36,7 +37,7 @@ impl Optimizer<'_> {
} }
self.changed = true; self.changed = true;
log::trace!("arguments: Optimizing computed access to arguments"); log::debug!("arguments: Optimizing computed access to arguments");
member.computed = false; member.computed = false;
member.prop = Box::new(Expr::Ident(Ident { member.prop = Box::new(Expr::Ident(Ident {
span: prop.span, span: prop.span,
@ -75,9 +76,9 @@ impl Optimizer<'_> {
{ {
// If a function has a variable named `arguments`, we abort. // If a function has a variable named `arguments`, we abort.
let data = analyze(&f.body); let data: Vec<Id> = find_ids(&f.body);
for (id, var) in &data.vars { for id in &data {
if id.0 == js_word!("arguments") && var.declared { if id.0 == js_word!("arguments") {
return; return;
} }
} }
@ -113,7 +114,7 @@ impl ArgReplacer<'_> {
let new_args = idx + 1 - self.params.len(); let new_args = idx + 1 - self.params.len();
self.changed = true; self.changed = true;
log::trace!("arguments: Injecting {} parameters", new_args); log::debug!("arguments: Injecting {} parameters", new_args);
let mut start = self.params.len(); let mut start = self.params.len();
self.params.extend( self.params.extend(
repeat_with(|| { repeat_with(|| {
@ -191,7 +192,7 @@ impl VisitMut for ArgReplacer<'_> {
if let Some(param) = self.params.get(idx) { if let Some(param) = self.params.get(idx) {
match &param.pat { match &param.pat {
Pat::Ident(i) => { Pat::Ident(i) => {
log::trace!( log::debug!(
"arguments: Replacing access to arguments to \ "arguments: Replacing access to arguments to \
normal reference", normal reference",
); );

View File

@ -20,7 +20,7 @@ impl Optimizer<'_> {
if s.stmts.len() == 1 { if s.stmts.len() == 1 {
if let Stmt::Return(s) = &mut s.stmts[0] { if let Stmt::Return(s) = &mut s.stmts[0] {
if let Some(arg) = &mut s.arg { if let Some(arg) = &mut s.arg {
log::trace!("arrows: Optimizing the body of an arrow"); log::debug!("arrows: Optimizing the body of an arrow");
*b = BlockStmtOrExpr::Expr(arg.take()); *b = BlockStmtOrExpr::Expr(arg.take());
return; return;
} }

View File

@ -1,71 +1,20 @@
use super::Optimizer; use super::Optimizer;
use crate::compress::optimize::is_pure_undefined; use crate::compress::optimize::{is_pure_undefined, Ctx};
use crate::debug::dump;
use crate::util::make_bool; use crate::util::make_bool;
use swc_atoms::js_word; use swc_atoms::js_word;
use swc_common::Spanned; use swc_common::Spanned;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ExprExt; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Type; use swc_ecma_utils::Type;
use swc_ecma_utils::Value; use swc_ecma_utils::Value;
use swc_ecma_utils::Value::Known; use swc_ecma_utils::Value::Known;
use swc_ecma_utils::Value::Unknown; use swc_ecma_utils::Value::Unknown;
use swc_ecma_utils::{undefined, ExprExt};
/// Methods related to the options `bools` and `bool_as_ints`. /// Methods related to the options `bools` and `bool_as_ints`.
impl Optimizer<'_> { impl Optimizer<'_> {
/// Disabled because it can change semantics.
///
/// - `!foo || bar();` => `foo && bar();`
/// - `!foo && bar();` => `foo || bar();`
pub(super) fn compress_logical_exprs_with_negated_lhs(&mut self, e: &mut Expr) {
if !self.options.bools || true {
return;
}
match e {
Expr::Bin(BinExpr {
span,
op: op @ op!("||"),
left,
right,
..
})
| Expr::Bin(BinExpr {
span,
op: op @ op!("&&"),
left,
right,
..
}) => match &mut **left {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => {
if *op == op!("&&") {
log::trace!("booleans: Compressing `!foo && bar` as `foo || bar`");
} else {
log::trace!("booleans: Compressing `!foo || bar` as `foo && bar`");
}
self.changed = true;
*e = Expr::Bin(BinExpr {
span: *span,
left: arg.take(),
op: if *op == op!("&&") {
op!("||")
} else {
op!("&&")
},
right: right.take(),
});
return;
}
_ => {}
},
_ => {}
}
}
/// ///
/// - `!condition() || !-3.5` => `!condition()` /// - `!condition() || !-3.5` => `!condition()`
/// ///
@ -104,9 +53,9 @@ impl Optimizer<'_> {
if can_remove { if can_remove {
if *op == op!("&&") { if *op == op!("&&") {
log::trace!("booleans: Compressing `!foo && true` as `!foo`"); log::debug!("booleans: Compressing `!foo && true` as `!foo`");
} else { } else {
log::trace!("booleans: Compressing `!foo || false` as `!foo`"); log::debug!("booleans: Compressing `!foo || false` as `!foo`");
} }
self.changed = true; self.changed = true;
*e = *left.take(); *e = *left.take();
@ -128,10 +77,7 @@ impl Optimizer<'_> {
match e { match e {
Expr::Unary(UnaryExpr { Expr::Unary(UnaryExpr {
span, op: op!("!"), arg, ..
op: op!("!"),
arg,
..
}) => match &mut **arg { }) => match &mut **arg {
Expr::Bin(BinExpr { Expr::Bin(BinExpr {
op: op!("&&"), op: op!("&&"),
@ -139,30 +85,123 @@ impl Optimizer<'_> {
right, right,
.. ..
}) => { }) => {
log::trace!("Optimizing ``!(a && b)` as `!a || !b`"); if negate_cost(&left, self.ctx.in_bool_ctx, false).unwrap_or(isize::MAX) >= 0
self.changed = true; || negate_cost(&right, self.ctx.in_bool_ctx, false).unwrap_or(isize::MAX)
*e = Expr::Bin(BinExpr { >= 0
span: *span, {
op: op!("||"),
left: Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg: left.take(),
})),
right: Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: op!("!"),
arg: right.take(),
})),
});
return; return;
} }
log::debug!("Optimizing `!(a && b)` as `!a || !b`");
self.changed = true;
self.negate(arg);
*e = *arg.take();
return;
}
Expr::Unary(UnaryExpr {
op: op!("!"),
arg: arg_of_arg,
..
}) => match &mut **arg_of_arg {
Expr::Bin(BinExpr {
op: op!("||"),
left,
right,
..
}) => {
if negate_cost(&left, self.ctx.in_bool_ctx, false).unwrap_or(isize::MAX) > 0
&& negate_cost(&right, self.ctx.in_bool_ctx, false)
.unwrap_or(isize::MAX)
> 0
{
return;
}
log::debug!("Optimizing `!!(a || b)` as `!a && !b`");
self.changed = true;
self.negate(arg_of_arg);
*e = *arg.take();
return;
}
_ => {}
},
_ => {} _ => {}
}, },
_ => {} _ => {}
} }
} }
/// **This negates bool**.
///
/// Returns true if it's negated.
pub(super) fn optimize_bang_within_logical_ops(
&mut self,
expr: &mut Expr,
is_ret_val_ignored: bool,
) -> bool {
if negate_cost(&expr, is_ret_val_ignored, is_ret_val_ignored).unwrap_or(isize::MAX) >= 0 {
return false;
}
let e = match expr {
Expr::Bin(b) => b,
_ => return false,
};
match e.op {
op!("&&") | op!("||") => {}
_ => return false,
}
if !is_ret_val_ignored {
if let Known(Type::Bool) = e.left.get_type() {
} else {
// Don't change type.
return false;
}
if let Known(Type::Bool) = e.right.get_type() {
} else {
// Don't change type.
return false;
}
}
// `!_ && 'undefined' !== typeof require`
//
// =>
//
// `_ || 'undefined' == typeof require`
log::debug!(
"bools({}): Negating: (!a && !b) => !(a || b) (because both expression are good for \
negation)",
self.line_col(e.span)
);
let start = dump(&*e);
e.op = if e.op == op!("&&") {
op!("||")
} else {
op!("&&")
};
let ctx = Ctx {
in_bool_ctx: true,
..self.ctx
};
self.changed = true;
self.with_ctx(ctx).negate(&mut e.left);
self.with_ctx(ctx).negate(&mut e.right);
if cfg!(feature = "debug") {
log::trace!("[Change] {} => {}", start, dump(&*e));
}
true
}
pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) { pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) {
if !self.options.bools { if !self.options.bools {
return; return;
@ -226,7 +265,7 @@ impl Optimizer<'_> {
} else { } else {
self.changed = true; self.changed = true;
let span = delete.arg.span(); let span = delete.arg.span();
log::trace!("booleans: Compressing `delete` as sequence expression"); log::debug!("booleans: Compressing `delete` as sequence expression");
*e = Expr::Seq(SeqExpr { *e = Expr::Seq(SeqExpr {
span, span,
exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))], exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))],
@ -241,26 +280,84 @@ impl Optimizer<'_> {
if convert_to_true { if convert_to_true {
self.changed = true; self.changed = true;
let span = delete.arg.span(); let span = delete.arg.span();
log::trace!("booleans: Compressing `delete` => true"); log::debug!("booleans: Compressing `delete` => true");
*e = make_bool(span, true); *e = make_bool(span, true);
return; return;
} }
} }
pub(super) fn compress_comparsion_of_typeof(&mut self, e: &mut BinExpr) {
fn should_optimize(l: &Expr, r: &Expr) -> bool {
match (l, r) {
(
Expr::Unary(UnaryExpr {
op: op!("typeof"), ..
}),
Expr::Lit(..),
) => true,
_ => false,
}
}
match e.op {
op!("===") | op!("!==") => {}
_ => return,
}
if should_optimize(&e.left, &e.right) || should_optimize(&e.right, &e.left) {
log::debug!("bools: Compressing comparison of `typeof` with literal");
self.changed = true;
e.op = match e.op {
op!("===") => {
op!("==")
}
op!("!==") => {
op!("!=")
}
_ => {
unreachable!()
}
}
}
}
/// This method converts `!1` to `0`. /// This method converts `!1` to `0`.
pub(super) fn optimize_expr_in_bool_ctx(&mut self, n: &mut Expr) { pub(super) fn optimize_expr_in_bool_ctx(&mut self, n: &mut Expr) {
if !self.options.bools { if !self.options.bools {
return; return;
} }
match n {
Expr::Bin(BinExpr {
op: op!("&&") | op!("||"),
left,
right,
..
}) => {
// Regardless if it's truthy or falsy, we can optimize it because it will be
// casted as bool anyway.
self.optimize_expr_in_bool_ctx(&mut **left);
self.optimize_expr_in_bool_ctx(&mut **right);
return;
}
Expr::Seq(e) => {
if let Some(last) = e.exprs.last_mut() {
self.optimize_expr_in_bool_ctx(&mut **last);
}
}
_ => {}
}
match n { match n {
Expr::Unary(UnaryExpr { Expr::Unary(UnaryExpr {
span, span,
op: op!("!"), op: op!("!"),
arg, arg,
}) => match &**arg { }) => match &mut **arg {
Expr::Lit(Lit::Num(Number { value, .. })) => { Expr::Lit(Lit::Num(Number { value, .. })) => {
log::trace!("Optimizing: number => number (in bool context)"); log::debug!("Optimizing: number => number (in bool context)");
self.changed = true; self.changed = true;
*n = Expr::Lit(Lit::Num(Number { *n = Expr::Lit(Lit::Num(Number {
@ -268,6 +365,15 @@ impl Optimizer<'_> {
value: if *value == 0.0 { 1.0 } else { 0.0 }, value: if *value == 0.0 { 1.0 } else { 0.0 },
})) }))
} }
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => {
log::debug!("bools: !!expr => expr (in bool ctx)");
self.changed = true;
*n = *arg.take();
return;
}
_ => {} _ => {}
}, },
@ -276,7 +382,7 @@ impl Optimizer<'_> {
op: op!("typeof"), op: op!("typeof"),
arg, arg,
}) => { }) => {
log::trace!("Optimizing: typeof => true (in bool context)"); log::debug!("Optimizing: typeof => true (in bool context)");
self.changed = true; self.changed = true;
match &**arg { match &**arg {
@ -301,7 +407,7 @@ impl Optimizer<'_> {
} }
Expr::Lit(Lit::Str(s)) => { Expr::Lit(Lit::Str(s)) => {
log::trace!("Converting string as boolean expressions"); log::debug!("Converting string as boolean expressions");
self.changed = true; self.changed = true;
*n = Expr::Lit(Lit::Num(Number { *n = Expr::Lit(Lit::Num(Number {
span: s.span, span: s.span,
@ -314,7 +420,7 @@ impl Optimizer<'_> {
return; return;
} }
if self.options.bools { if self.options.bools {
log::trace!("booleans: Converting number as boolean expressions"); log::debug!("booleans: Converting number as boolean expressions");
self.changed = true; self.changed = true;
*n = Expr::Lit(Lit::Num(Number { *n = Expr::Lit(Lit::Num(Number {
span: num.span, span: num.span,
@ -331,7 +437,7 @@ impl Optimizer<'_> {
}) => { }) => {
// Optimize if (a ?? false); as if (a); // Optimize if (a ?? false); as if (a);
if let Value::Known(false) = right.as_pure_bool() { if let Value::Known(false) = right.as_pure_bool() {
log::trace!( log::debug!(
"Dropping right operand of `??` as it's always false (in bool context)" "Dropping right operand of `??` as it's always false (in bool context)"
); );
self.changed = true; self.changed = true;
@ -348,7 +454,7 @@ impl Optimizer<'_> {
// `a || false` => `a` (as it will be casted to boolean anyway) // `a || false` => `a` (as it will be casted to boolean anyway)
if let Known(false) = right.as_pure_bool() { if let Known(false) = right.as_pure_bool() {
log::trace!("bools: `expr || false` => `expr` (in bool context)"); log::debug!("bools: `expr || false` => `expr` (in bool context)");
self.changed = true; self.changed = true;
*n = *left.take(); *n = *left.take();
return; return;
@ -359,8 +465,9 @@ impl Optimizer<'_> {
let span = n.span(); let span = n.span();
let v = n.as_pure_bool(); let v = n.as_pure_bool();
if let Known(v) = v { if let Known(v) = v {
log::trace!("Optimizing expr as {} (in bool context)", v); log::debug!("Optimizing expr as {} (in bool context)", v);
*n = make_bool(span, v); *n = make_bool(span, v);
return;
} }
} }
} }
@ -381,7 +488,7 @@ impl Optimizer<'_> {
None => match &mut *stmt.cons { None => match &mut *stmt.cons {
Stmt::Expr(cons) => { Stmt::Expr(cons) => {
self.changed = true; self.changed = true;
log::trace!("conditionals: `if (foo) bar;` => `foo && bar`"); log::debug!("conditionals: `if (foo) bar;` => `foo && bar`");
*s = Stmt::Expr(ExprStmt { *s = Stmt::Expr(ExprStmt {
span: stmt.span, span: stmt.span,
expr: Box::new(Expr::Bin(BinExpr { expr: Box::new(Expr::Bin(BinExpr {
@ -397,4 +504,286 @@ impl Optimizer<'_> {
}, },
} }
} }
///
/// - `"undefined" == typeof value;` => `void 0 === value`
pub(super) fn compress_typeof_undefined(&mut self, e: &mut BinExpr) {
fn opt(o: &mut Optimizer, l: &mut Expr, r: &mut Expr) -> bool {
match (&mut *l, &mut *r) {
(
Expr::Lit(Lit::Str(Str {
value: js_word!("undefined"),
..
})),
Expr::Unary(UnaryExpr {
op: op!("typeof"),
arg,
..
}),
) => {
// TODO?
match &**arg {
Expr::Ident(arg) => {
if let Some(usage) =
o.data.as_ref().and_then(|data| data.vars.get(&arg.to_id()))
{
if !usage.declared {
return false;
}
}
}
_ => {}
}
*l = *undefined(l.span());
*r = *arg.take();
true
}
_ => false,
}
}
match e.op {
op!("==") | op!("!=") | op!("===") | op!("!==") => {}
_ => return,
}
if opt(self, &mut e.left, &mut e.right) || opt(self, &mut e.right, &mut e.left) {
e.op = match e.op {
op!("==") => {
op!("===")
}
op!("!=") => {
op!("!==")
}
_ => e.op,
};
}
}
}
pub(crate) fn is_ok_to_negate_for_cond(e: &Expr) -> bool {
match e {
Expr::Update(..) => false,
_ => true,
}
}
pub(crate) fn is_ok_to_negate_rhs(rhs: &Expr) -> bool {
match rhs {
Expr::Member(..) => true,
Expr::Bin(BinExpr {
op: op!("===") | op!("!==") | op!("==") | op!("!="),
..
}) => true,
Expr::Call(..) | Expr::New(..) => false,
Expr::Update(..) => false,
Expr::Bin(BinExpr {
op: op!("&&") | op!("||"),
left,
right,
..
}) => is_ok_to_negate_rhs(&left) && is_ok_to_negate_rhs(&right),
Expr::Bin(BinExpr { left, right, .. }) => {
is_ok_to_negate_rhs(&left) && is_ok_to_negate_rhs(&right)
}
Expr::Assign(e) => is_ok_to_negate_rhs(&e.right),
Expr::Unary(UnaryExpr {
op: op!("!") | op!("delete"),
..
}) => true,
Expr::Seq(e) => {
if let Some(last) = e.exprs.last() {
is_ok_to_negate_rhs(&last)
} else {
true
}
}
Expr::Cond(e) => is_ok_to_negate_rhs(&e.cons) && is_ok_to_negate_rhs(&e.alt),
_ => {
if !rhs.may_have_side_effects() {
return true;
}
if cfg!(feature = "debug") {
log::warn!("unimplemented: is_ok_to_negate_rhs: `{}`", dump(&*rhs));
}
false
}
}
}
/// A negative value means that it's efficient to negate the expression.
pub(crate) fn negate_cost(e: &Expr, in_bool_ctx: bool, is_ret_val_ignored: bool) -> Option<isize> {
fn cost(
e: &Expr,
in_bool_ctx: bool,
bin_op: Option<BinaryOp>,
is_ret_val_ignored: bool,
) -> isize {
match e {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => {
// TODO: Check if this argument is actually start of line.
match &**arg {
Expr::Call(CallExpr {
callee: ExprOrSuper::Expr(callee),
..
}) => match &**callee {
Expr::Fn(..) => return 0,
_ => {}
},
_ => {}
}
if in_bool_ctx {
let c = -cost(arg, true, None, is_ret_val_ignored);
return c.min(-1);
}
match &**arg {
Expr::Unary(UnaryExpr { op: op!("!"), .. }) => -1,
_ => 1,
}
}
Expr::Bin(BinExpr {
op: op!("===") | op!("!==") | op!("==") | op!("!="),
..
}) => 0,
Expr::Bin(BinExpr {
op: op @ op!("||") | op @ op!("&&"),
left,
right,
..
}) => {
let l_cost = cost(&left, in_bool_ctx, Some(*op), false);
if !is_ret_val_ignored && !is_ok_to_negate_rhs(&right) {
return l_cost + 3;
}
l_cost + cost(&right, in_bool_ctx, Some(*op), is_ret_val_ignored)
}
Expr::Cond(CondExpr { cons, alt, .. })
if is_ok_to_negate_for_cond(&cons) && is_ok_to_negate_for_cond(&alt) =>
{
cost(&cons, in_bool_ctx, bin_op, is_ret_val_ignored)
+ cost(&alt, in_bool_ctx, bin_op, is_ret_val_ignored)
}
Expr::Cond(..)
| Expr::Update(..)
| Expr::Bin(BinExpr {
op: op!("in") | op!("instanceof"),
..
}) => 3,
Expr::Assign(..) => {
if is_ret_val_ignored {
0
} else {
3
}
}
Expr::Seq(e) => {
if let Some(last) = e.exprs.last() {
return cost(&last, in_bool_ctx, bin_op, is_ret_val_ignored);
}
if is_ret_val_ignored {
0
} else {
1
}
}
_ => {
if is_ret_val_ignored {
0
} else {
1
}
}
}
}
let cost = cost(e, in_bool_ctx, None, is_ret_val_ignored);
if cfg!(feature = "debug") {
log::trace!("negation cost of `{}` = {}", dump(&*e), cost);
}
Some(cost)
}
#[cfg(test)]
mod tests {
use super::negate_cost;
use swc_common::{input::SourceFileInput, FileName};
use swc_ecma_parser::{lexer::Lexer, Parser};
fn assert_negate_cost(s: &str, in_bool_ctx: bool, is_ret_val_ignored: bool, expected: isize) {
testing::run_test2(false, |cm, _| {
let fm = cm.new_source_file(FileName::Anon, s.to_string());
let lexer = Lexer::new(
Default::default(),
swc_ecma_ast::EsVersion::latest(),
SourceFileInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let e = parser
.parse_expr()
.expect("failed to parse input as an expression");
let actual = negate_cost(&e, in_bool_ctx, is_ret_val_ignored).unwrap();
assert_eq!(
actual, expected,
"Expected negation cost of {} to be {}, but got {}",
s, expected, actual,
);
Ok(())
})
.unwrap();
}
#[test]
fn logical_1() {
assert_negate_cost(
"this[key] && !this.hasOwnProperty(key) || (this[key] = value)",
false,
true,
2,
);
}
#[test]
#[ignore]
fn logical_2() {
assert_negate_cost(
"(!this[key] || this.hasOwnProperty(key)) && (this[key] = value)",
false,
true,
-2,
);
}
} }

View File

@ -4,6 +4,7 @@ use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::prepend;
use swc_ecma_utils::Id; use swc_ecma_utils::Id;
use swc_ecma_utils::StmtLike; use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_mut_type; use swc_ecma_visit::noop_visit_mut_type;
@ -57,7 +58,7 @@ impl Optimizer<'_> {
return; return;
} }
if self.ctx.in_try_block || self.ctx.executed_multiple_time { if self.ctx.in_try_block || self.ctx.executed_multiple_time || self.ctx.in_cond {
return; return;
} }
@ -84,6 +85,20 @@ impl Optimizer<'_> {
{ {
return; return;
} }
if usage.used_in_loop {
match &*assign.right {
Expr::Lit(..) | Expr::Ident(..) => {}
_ => return,
}
}
if usage.usage_count >= 2 {
match &*assign.right {
Expr::Lit(..) => {}
_ => return,
}
}
} }
let value = match &*assign.right { let value = match &*assign.right {
@ -94,6 +109,12 @@ impl Optimizer<'_> {
_ => return, _ => return,
}; };
log::debug!(
"collpase_vars: Decided to inline {}{:?}",
left.id.sym,
left.id.span.ctxt
);
self.lits.insert(left.to_id(), value); self.lits.insert(left.to_id(), value);
} }
_ => {} _ => {}
@ -101,139 +122,284 @@ impl Optimizer<'_> {
} }
/// Collapse single-use non-constant variables, side effects permitting. /// Collapse single-use non-constant variables, side effects permitting.
pub(super) fn collapse_consequtive_vars<T>(&mut self, stmts: &mut Vec<T>) ///
/// This merges all variables to first variable declartion with an
/// initializer. If such variable declaration is not found, variables are
/// prepended to `stmts`.
pub(super) fn collapse_vars_without_init<T>(&mut self, stmts: &mut Vec<T>)
where where
T: StmtLike, T: StmtLike,
Vec<T>:
VisitWith<VarWithOutInitCounter> + VisitMutWith<VarMover> + VisitMutWith<VarPrepender>,
{ {
if !self.options.collapse_vars { if !self.options.collapse_vars {
return; return;
} }
let indexes = stmts
.windows(2)
.enumerate()
.filter_map(|(i, stmts)| {
match (stmts[0].as_stmt(), stmts[1].as_stmt()) {
(Some(Stmt::Decl(Decl::Var(v))), Some(r)) => {
// Should be handled by other passes.
if v.kind == VarDeclKind::Const {
return None;
}
// We only inline for some subset of statements.
match r {
Stmt::Expr(..) | Stmt::Return(..) => {}
_ => return None,
}
// We only touch last variable delcarator.
if let Some(last) = v.decls.last() {
match last.init.as_deref()? {
Expr::Object(..) => {
// Objects are handled by other passes.
return None;
}
Expr::Arrow(..) | Expr::Fn(..) => {
// Handled by other passes
return None;
}
_ => {
let mut chekcer = InlinabiltyChecker { can_inline: true };
last.init
.visit_with(&Invalid { span: DUMMY_SP }, &mut chekcer);
if !chekcer.can_inline {
return None;
}
}
}
match &last.name {
Pat::Ident(name) => {
//
if let Some(var) = self
.data
.as_ref()
.and_then(|data| data.vars.get(&name.to_id()))
{ {
if var.usage_count != 1 // let mut found_other = false;
|| var.mutated // let mut need_work = false;
|| !SimpleUsageFinder::find(&name.id, r)
{
return None;
}
return Some(i); // for stmt in &*stmts {
} // match stmt.as_stmt() {
// Some(Stmt::Decl(Decl::Var(
// v
// @
// VarDecl {
// kind: VarDeclKind::Var,
// ..
// },
// ))) => {
// if v.decls.iter().any(|v| v.init.is_none()) {
// if found_other {
// need_work = true;
// }
// } else {
// found_other = true;
// }
// }
None // _ => {
} // found_other = true;
_ => None, // }
} // }
} else { // }
None
}
}
_ => None,
}
})
.collect::<Vec<_>>();
if indexes.is_empty() { // Check for nested variable declartions.
let mut v = VarWithOutInitCounter::default();
stmts.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
if !v.need_work {
return; return;
} }
}
self.changed = true; self.changed = true;
log::trace!("collapse_vars: Collapsing {:?}", indexes); log::debug!("collapse_vars: Collapsing variables without an initializer");
let mut new = Vec::with_capacity(stmts.len()); let vars = {
let mut values = FxHashMap::default(); let mut v = VarMover {
for (idx, stmt) in stmts.take().into_iter().enumerate() { vars: Default::default(),
match stmt.try_into_stmt() { var_decl_kind: Default::default(),
Ok(mut stmt) => match stmt { };
Stmt::Decl(Decl::Var(mut v)) if indexes.contains(&idx) => { stmts.visit_mut_with(&mut v);
if let Some(last) = v.decls.pop() {
match last.name {
Pat::Ident(name) => {
values.insert(name.to_id(), last.init);
}
_ => unreachable!(),
}
} else {
unreachable!()
}
}
_ => {
stmt.visit_mut_with(&mut Inliner {
values: &mut values,
});
new.push(T::from_stmt(stmt)); v.vars
} };
},
Err(item) => {
new.push(item);
}
}
}
*stmts = new; // Prepend vars
// let mut prepender = VarPrepender { vars };
stmts.visit_mut_with(&mut prepender);
if !prepender.vars.is_empty() {
prepend(
stmts,
T::from_stmt(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: prepender.vars,
}))),
);
}
} }
} }
/// Checks inlinabilty of variable initializer. /// See if there's two [VarDecl] which has [VarDeclarator] without the
struct InlinabiltyChecker { /// initializer.
can_inline: bool, #[derive(Default)]
pub(super) struct VarWithOutInitCounter {
need_work: bool,
found_var_without_init: bool,
found_var_with_init: bool,
} }
impl Visit for InlinabiltyChecker { impl Visit for VarWithOutInitCounter {
noop_visit_type!(); noop_visit_type!();
fn visit_update_expr(&mut self, _: &UpdateExpr, _: &dyn Node) { fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {}
self.can_inline = false;
fn visit_constructor(&mut self, _: &Constructor, _: &dyn Node) {}
fn visit_function(&mut self, _: &Function, _: &dyn Node) {}
fn visit_var_decl(&mut self, v: &VarDecl, _: &dyn Node) {
v.visit_children_with(self);
if v.kind != VarDeclKind::Var {
return;
} }
let mut found_init = false;
for d in &v.decls {
if d.init.is_some() {
found_init = true;
} else {
if found_init {
self.need_work = true;
return;
}
}
}
if v.decls.iter().all(|v| v.init.is_none()) {
if self.found_var_without_init || self.found_var_with_init {
self.need_work = true;
}
self.found_var_without_init = true;
} else {
self.found_var_with_init = true;
}
}
fn visit_var_decl_or_pat(&mut self, _: &VarDeclOrPat, _: &dyn Node) {}
}
/// Moves all varaible without initializer.
pub(super) struct VarMover {
vars: Vec<VarDeclarator>,
var_decl_kind: Option<VarDeclKind>,
}
impl VisitMut for VarMover {
noop_visit_mut_type!();
/// Noop
fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {}
/// Noop
fn visit_mut_constructor(&mut self, _: &mut Constructor) {}
/// Noop
fn visit_mut_function(&mut self, _: &mut Function) {}
fn visit_mut_module_item(&mut self, s: &mut ModuleItem) {
s.visit_mut_children_with(self);
match s {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Var(d),
..
})) if d.decls.is_empty() => {
s.take();
return;
}
_ => {}
}
}
fn visit_mut_opt_var_decl_or_expr(&mut self, n: &mut Option<VarDeclOrExpr>) {
n.visit_mut_children_with(self);
match n {
Some(VarDeclOrExpr::VarDecl(var)) => {
if var.decls.is_empty() {
*n = None;
}
}
_ => {}
}
}
fn visit_mut_stmt(&mut self, s: &mut Stmt) {
s.visit_mut_children_with(self);
match s {
Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
s.take();
return;
}
_ => {}
}
}
fn visit_mut_var_decl(&mut self, v: &mut VarDecl) {
let old = self.var_decl_kind.take();
self.var_decl_kind = Some(v.kind);
v.visit_mut_children_with(self);
self.var_decl_kind = old;
}
fn visit_mut_var_decl_or_pat(&mut self, _: &mut VarDeclOrPat) {}
fn visit_mut_var_declarators(&mut self, d: &mut Vec<VarDeclarator>) {
d.visit_mut_children_with(self);
if self.var_decl_kind.unwrap() != VarDeclKind::Var {
return;
}
if d.iter().all(|v| v.init.is_some()) {
return;
}
let has_init = d.iter().any(|v| v.init.is_some());
if has_init {
let mut new = vec![];
for v in d.take() {
if v.init.is_some() {
new.push(v)
} else {
self.vars.push(v)
}
}
*d = new;
}
let mut new = vec![];
if has_init {
new.extend(self.vars.drain(..));
}
for v in d.take() {
if v.init.is_some() {
new.push(v)
} else {
self.vars.push(v)
}
}
*d = new;
}
}
pub(super) struct VarPrepender {
vars: Vec<VarDeclarator>,
}
impl VisitMut for VarPrepender {
noop_visit_mut_type!();
fn visit_mut_var_decl(&mut self, v: &mut VarDecl) {
if self.vars.is_empty() {
return;
}
if v.kind != VarDeclKind::Var {
return;
}
if v.decls.iter().any(|d| d.init.is_some()) {
let mut decls = self.vars.take();
decls.extend(v.decls.take());
v.decls = decls;
}
}
/// Noop
fn visit_mut_function(&mut self, _: &mut Function) {}
/// Noop
fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {}
/// Noop
fn visit_mut_constructor(&mut self, _: &mut Constructor) {}
} }
struct Inliner<'a> { struct Inliner<'a> {
@ -249,6 +415,8 @@ impl VisitMut for Inliner<'_> {
match e { match e {
Expr::Ident(i) => { Expr::Ident(i) => {
if let Some(value) = self.values.remove(&i.to_id()) { if let Some(value) = self.values.remove(&i.to_id()) {
log::debug!("collapse_vars: Inlining {}{:?}", i.sym, i.span.ctxt);
*e = *value.expect("should be used only once"); *e = *value.expect("should be used only once");
} }
} }
@ -256,63 +424,3 @@ impl VisitMut for Inliner<'_> {
} }
} }
} }
/// Finds usage of `ident`, but except assignment to it.
struct SimpleUsageFinder<'a> {
ident: &'a Ident,
found: bool,
}
impl<'a> Visit for SimpleUsageFinder<'a> {
noop_visit_type!();
fn visit_pat(&mut self, n: &Pat, _: &dyn Node) {
match n {
Pat::Ident(..) => {}
_ => {
n.visit_children_with(self);
}
}
}
fn visit_update_expr(&mut self, _: &UpdateExpr, _: &dyn Node) {}
fn visit_expr_or_spread(&mut self, n: &ExprOrSpread, _: &dyn Node) {
if n.spread.is_some() {
return;
}
n.visit_children_with(self);
}
fn visit_assign_expr(&mut self, e: &AssignExpr, _: &dyn Node) {
e.right.visit_with(e, self);
}
fn visit_ident(&mut self, i: &Ident, _: &dyn Node) {
if i.span.ctxt() == self.ident.span.ctxt() && i.sym == self.ident.sym {
self.found = true;
}
}
fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) {
e.obj.visit_with(e as _, self);
if e.computed {
e.prop.visit_with(e as _, self);
}
}
}
impl<'a> SimpleUsageFinder<'a> {
pub fn find<N>(ident: &'a Ident, node: &N) -> bool
where
N: VisitWith<Self>,
{
let mut v = SimpleUsageFinder {
ident,
found: false,
};
node.visit_with(&Invalid { span: DUMMY_SP } as _, &mut v);
v.found
}
}

View File

@ -1,6 +1,11 @@
use super::Optimizer; use super::Optimizer;
use crate::compress::optimize::bools::negate_cost;
use crate::compress::optimize::Ctx;
use crate::compress::optimize::DISABLE_BUGGY_PASSES;
use crate::debug::dump;
use crate::util::make_bool; use crate::util::make_bool;
use crate::util::SpanExt; use crate::util::SpanExt;
use std::mem::swap;
use swc_common::EqIgnoreSpan; use swc_common::EqIgnoreSpan;
use swc_common::Spanned; use swc_common::Spanned;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
@ -18,6 +23,104 @@ use swc_ecma_utils::Value::Known;
/// Methods related to the option `conditionals`. All methods are noop if /// Methods related to the option `conditionals`. All methods are noop if
/// `conditionals` is false. /// `conditionals` is false.
impl Optimizer<'_> { impl Optimizer<'_> {
pub(super) fn negate_cond_expr(&mut self, cond: &mut CondExpr) {
if negate_cost(&cond.test, true, false).unwrap_or(isize::MAX) >= 0 {
return;
}
self.changed = true;
log::debug!("conditionals: `a ? foo : bar` => `!a ? bar : foo` (considered cost)");
let start_str = dump(&*cond);
{
let ctx = Ctx {
in_bool_ctx: true,
..self.ctx
};
self.with_ctx(ctx).negate(&mut cond.test);
}
swap(&mut cond.cons, &mut cond.alt);
if cfg!(feature = "debug") {
log::trace!(
"[Change] Negated cond: `{}` => `{}`",
start_str,
dump(&*cond)
)
}
}
/// Negates the condition of a `if` statement to reduce body size.
pub(super) fn negate_if_stmt(&mut self, stmt: &mut IfStmt) {
let alt = match stmt.alt.as_deref_mut() {
Some(v) => v,
_ => return,
};
match &*stmt.cons {
Stmt::Return(..) | Stmt::Continue(ContinueStmt { label: None, .. }) => return,
_ => {}
}
if negate_cost(&stmt.test, true, false).unwrap_or(isize::MAX) < 0 {
self.changed = true;
log::debug!("if_return: Negating `cond` of an if statement which has cons and alt");
let ctx = Ctx {
in_bool_ctx: true,
..self.ctx
};
self.with_ctx(ctx).negate(&mut stmt.test);
swap(alt, &mut *stmt.cons);
return;
}
match &*alt {
Stmt::Return(..) | Stmt::Continue(ContinueStmt { label: None, .. }) => {
self.changed = true;
log::debug!(
"if_return: Negating an if statement because the alt is return / continue"
);
self.negate(&mut stmt.test);
swap(alt, &mut *stmt.cons);
}
_ => return,
}
}
/// This method may change return value.
///
/// - `a ? b : false` => `a && b`
pub(super) fn compress_cond_to_logical_ignoring_return_value(&mut self, e: &mut Expr) {
let cond = match e {
Expr::Cond(cond) => cond,
_ => return,
};
if !cond.cons.may_have_side_effects() {
self.changed = true;
log::debug!("conditionals: `cond ? useless : alt` => `cond || alt`");
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("||"),
left: cond.test.take(),
right: cond.alt.take(),
});
return;
}
if !cond.alt.may_have_side_effects() {
self.changed = true;
log::debug!("conditionals: `cond ? cons : useless` => `cond && cons`");
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("&&"),
left: cond.test.take(),
right: cond.cons.take(),
});
return;
}
}
/// Removes useless operands of an logical expressions. /// Removes useless operands of an logical expressions.
pub(super) fn drop_logical_operands(&mut self, e: &mut Expr) { pub(super) fn drop_logical_operands(&mut self, e: &mut Expr) {
if !self.options.conditionals { if !self.options.conditionals {
@ -47,7 +150,7 @@ impl Optimizer<'_> {
// `!!b || true` => true // `!!b || true` => true
if let Known(true) = rb { if let Known(true) = rb {
self.changed = true; self.changed = true;
log::trace!("conditionals: `!!foo || true` => `true`"); log::debug!("conditionals: `!!foo || true` => `true`");
*e = make_bool(bin.span, true); *e = make_bool(bin.span, true);
return; return;
} }
@ -70,7 +173,7 @@ impl Optimizer<'_> {
(Expr::Bin(cons @ BinExpr { op: op!("||"), .. }), alt) (Expr::Bin(cons @ BinExpr { op: op!("||"), .. }), alt)
if (*cons.right).eq_ignore_span(&*alt) => if (*cons.right).eq_ignore_span(&*alt) =>
{ {
log::trace!("conditionals: `x ? y || z : z` => `x || y && z`"); log::debug!("conditionals: `x ? y || z : z` => `x || y && z`");
self.changed = true; self.changed = true;
*e = Expr::Bin(BinExpr { *e = Expr::Bin(BinExpr {
@ -91,13 +194,10 @@ impl Optimizer<'_> {
} }
/// ///
/// - `foo ? 1 : false` => `!!foo && 1` /// - `foo ? bar : false` => `!!foo && bar`
/// - `!foo ? true : 0` => `!foo || 0` /// - `!foo ? true : bar` => `!foo || bar`
/// - `foo ? false : bar` => `!foo && bar`
pub(super) fn compress_conds_as_logical(&mut self, e: &mut Expr) { pub(super) fn compress_conds_as_logical(&mut self, e: &mut Expr) {
if !self.options.conditionals {
return;
}
let cond = match e { let cond = match e {
Expr::Cond(cond) => cond, Expr::Cond(cond) => cond,
_ => return, _ => return,
@ -107,7 +207,7 @@ impl Optimizer<'_> {
if let Known(Type::Bool) = lt { if let Known(Type::Bool) = lt {
let lb = cond.cons.as_pure_bool(); let lb = cond.cons.as_pure_bool();
if let Known(true) = lb { if let Known(true) = lb {
log::trace!("conditionals: `foo ? true : bar` => `!!foo || bar`"); log::debug!("conditionals: `foo ? true : bar` => `!!foo || bar`");
// Negate twice to convert `test` to boolean. // Negate twice to convert `test` to boolean.
self.negate_twice(&mut cond.test); self.negate_twice(&mut cond.test);
@ -123,9 +223,8 @@ impl Optimizer<'_> {
} }
// TODO: Verify this rule. // TODO: Verify this rule.
if false {
if let Known(false) = lb { if let Known(false) = lb {
log::trace!("conditionals: `foo ? false : bar` => `!foo && bar`"); log::debug!("conditionals: `foo ? false : bar` => `!foo && bar`");
self.changed = true; self.changed = true;
self.negate(&mut cond.test); self.negate(&mut cond.test);
@ -139,13 +238,12 @@ impl Optimizer<'_> {
return; return;
} }
} }
}
let rt = cond.alt.get_type(); let rt = cond.alt.get_type();
if let Known(Type::Bool) = rt { if let Known(Type::Bool) = rt {
let rb = cond.alt.as_pure_bool(); let rb = cond.alt.as_pure_bool();
if let Known(false) = rb { if let Known(false) = rb {
log::trace!("conditionals: `foo ? 1 : false` => `!!foo && 1`"); log::debug!("conditionals: `foo ? bar : false` => `!!foo && bar`");
self.changed = true; self.changed = true;
// Negate twice to convert `test` to boolean. // Negate twice to convert `test` to boolean.
@ -159,6 +257,22 @@ impl Optimizer<'_> {
}); });
return; return;
} }
if let Known(true) = rb {
log::debug!("conditionals: `foo ? bar : true` => `!foo || bar");
self.changed = true;
// Negate twice to convert `test` to boolean.
self.negate(&mut cond.test);
*e = Expr::Bin(BinExpr {
span: cond.span,
op: op!("||"),
left: cond.test.take(),
right: cond.cons.take(),
});
return;
}
} }
} }
@ -199,7 +313,7 @@ impl Optimizer<'_> {
} }
self.changed = true; self.changed = true;
log::trace!("conditionals: Merging if statements with same `cons`"); log::debug!("conditionals: Merging if statements with same `cons`");
let mut cur: Option<IfStmt> = None; let mut cur: Option<IfStmt> = None;
let mut new = Vec::with_capacity(stmts.len()); let mut new = Vec::with_capacity(stmts.len());
@ -272,15 +386,28 @@ impl Optimizer<'_> {
/// } /// }
/// ``` /// ```
pub(super) fn compress_if_stmt_as_cond(&mut self, s: &mut Stmt) { pub(super) fn compress_if_stmt_as_cond(&mut self, s: &mut Stmt) {
if !self.options.conditionals {
return;
}
let stmt = match s { let stmt = match s {
Stmt::If(v) => v, Stmt::If(v) => v,
_ => return, _ => return,
}; };
match &*stmt.cons {
Stmt::Empty(..) => {
if self.options.conditionals || self.options.unused {
if stmt.alt.is_none() {
*s = Stmt::Expr(ExprStmt {
span: stmt.span,
expr: stmt.test.take(),
});
self.changed = true;
log::debug!("conditionals: `if (foo);` => `foo` ");
return;
}
}
}
_ => {}
}
// If alt does not exist, an if statement is better than a conditional // If alt does not exist, an if statement is better than a conditional
// expression. // expression.
let alt = match &mut stmt.alt { let alt = match &mut stmt.alt {
@ -303,7 +430,7 @@ impl Optimizer<'_> {
Expr::Unary(UnaryExpr { Expr::Unary(UnaryExpr {
op: op!("!"), arg, .. op: op!("!"), arg, ..
}) => { }) => {
log::trace!("Optimizing `if (!foo); else bar();` as `foo && bar();`"); log::debug!("Optimizing `if (!foo); else bar();` as `foo && bar();`");
let mut expr = Box::new(Expr::Bin(BinExpr { let mut expr = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP, span: DUMMY_SP,
@ -318,7 +445,7 @@ impl Optimizer<'_> {
}); });
} }
_ => { _ => {
log::trace!("Optimizing `if (foo); else bar();` as `foo || bar();`"); log::debug!("Optimizing `if (foo); else bar();` as `foo || bar();`");
let mut expr = Box::new(Expr::Bin(BinExpr { let mut expr = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP, span: DUMMY_SP,
@ -356,10 +483,11 @@ impl Optimizer<'_> {
None => {} None => {}
} }
if self.options.conditionals || self.options.bools {
// if (a) b(); else c(); => a ? b() : c() // if (a) b(); else c(); => a ? b() : c()
log::trace!( log::debug!(
"Compressing if statement as conditional expression (even though cons and alt is not \ "Compressing if statement as conditional expression (even though cons and alt is \
compressable)" not compressable)"
); );
self.changed = true; self.changed = true;
*s = Stmt::Expr(ExprStmt { *s = Stmt::Expr(ExprStmt {
@ -372,13 +500,10 @@ impl Optimizer<'_> {
})), })),
}) })
} }
}
/// Compress a conditional expression if cons and alt is simillar /// Compress a conditional expression if cons and alt is simillar
pub(super) fn compress_cond_expr_if_simillar(&mut self, e: &mut Expr) { pub(super) fn compress_cond_expr_if_simillar(&mut self, e: &mut Expr) {
if !self.options.conditionals {
return;
}
let cond = match e { let cond = match e {
Expr::Cond(expr) => expr, Expr::Cond(expr) => expr,
_ => return, _ => return,
@ -398,7 +523,7 @@ impl Optimizer<'_> {
// x ? x : y => x || y // x ? x : y => x || y
if cond.test.is_ident() && cond.test.eq_ignore_span(&cond.cons) { if cond.test.is_ident() && cond.test.eq_ignore_span(&cond.cons) {
log::trace!("Compressing `x ? x : y` as `x || y`"); log::debug!("Compressing `x ? x : y` as `x || y`");
self.changed = true; self.changed = true;
*e = Expr::Bin(BinExpr { *e = Expr::Bin(BinExpr {
span: cond.span, span: cond.span,
@ -417,43 +542,24 @@ impl Optimizer<'_> {
alt: &mut Expr, alt: &mut Expr,
is_for_if_stmt: bool, is_for_if_stmt: bool,
) -> Option<Expr> { ) -> Option<Expr> {
if cons.eq_ignore_span(alt) { if cons.eq_ignore_span(alt)
&& match &*cons {
Expr::Yield(..) | Expr::Fn(..) => false,
_ => true,
}
{
log::debug!("conditionals: cons is same as alt");
return Some(Expr::Seq(SeqExpr { return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP, span: DUMMY_SP,
exprs: vec![test.take(), Box::new(cons.take())], exprs: vec![test.take(), Box::new(cons.take())],
})); }));
} }
match &**test {
Expr::Call(CallExpr {
callee: ExprOrSuper::Expr(callee),
..
}) => match &**callee {
Expr::Ident(callee) => {
let side_effect_free = self
.data
.as_ref()
.and_then(|data| data.vars.get(&callee.to_id()))
.map(|v| v.is_fn_local)
.unwrap_or(false);
if !side_effect_free {
match cons {
Expr::Call(..) => {
return None;
}
_ => {}
}
}
}
_ => return None,
},
_ => {}
}
match (cons, alt) { match (cons, alt) {
(Expr::Call(cons), Expr::Call(alt)) => { (Expr::Call(cons), Expr::Call(alt)) => {
let cons_callee = cons.callee.as_expr().and_then(|e| e.as_ident())?; let cons_callee = cons.callee.as_expr().and_then(|e| e.as_ident())?;
// //
if !cons.callee.eq_ignore_span(&alt.callee) { if !cons.callee.eq_ignore_span(&alt.callee) {
return None; return None;
} }
@ -462,7 +568,7 @@ impl Optimizer<'_> {
.data .data
.as_ref() .as_ref()
.and_then(|data| data.vars.get(&cons_callee.to_id())) .and_then(|data| data.vars.get(&cons_callee.to_id()))
.map(|v| v.is_fn_local) .map(|v| v.is_fn_local && v.declared)
.unwrap_or(false); .unwrap_or(false);
if side_effect_free if side_effect_free
@ -476,8 +582,9 @@ impl Optimizer<'_> {
.zip(alt.args.iter()) .zip(alt.args.iter())
.filter(|(cons, alt)| !cons.eq_ignore_span(alt)) .filter(|(cons, alt)| !cons.eq_ignore_span(alt))
.count(); .count();
if diff_count == 1 { if diff_count == 1 {
log::trace!( log::debug!(
"conditionals: Merging cons and alt as only one argument differs" "conditionals: Merging cons and alt as only one argument differs"
); );
self.changed = true; self.changed = true;
@ -543,7 +650,7 @@ impl Optimizer<'_> {
})), })),
}); });
log::trace!( log::debug!(
"Compreessing if into cond as there's no side effect and the number of \ "Compreessing if into cond as there's no side effect and the number of \
arguments is 1" arguments is 1"
); );
@ -556,7 +663,7 @@ impl Optimizer<'_> {
} }
if !side_effect_free && is_for_if_stmt { if !side_effect_free && is_for_if_stmt {
log::trace!("Compreessing if into cond while preserving side effects"); log::debug!("Compreessing if into cond while preserving side effects");
return Some(Expr::Cond(CondExpr { return Some(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt), span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: test.take(), test: test.take(),
@ -601,7 +708,7 @@ impl Optimizer<'_> {
})), })),
}); });
log::trace!( log::debug!(
"Compreessing if statement into a condiotnal expression of `new` as \ "Compreessing if statement into a condiotnal expression of `new` as \
there's no side effect and the number of arguments is 1" there's no side effect and the number of arguments is 1"
); );
@ -623,7 +730,7 @@ impl Optimizer<'_> {
&& cons.left.eq_ignore_span(&alt.left) && cons.left.eq_ignore_span(&alt.left)
&& is_simple_lhs(&cons.left) => && is_simple_lhs(&cons.left) =>
{ {
log::trace!("Merging assignments in cons and alt of if statement"); log::debug!("Merging assignments in cons and alt of if statement");
Some(Expr::Assign(AssignExpr { Some(Expr::Assign(AssignExpr {
span: DUMMY_SP, span: DUMMY_SP,
op: cons.op, op: cons.op,
@ -639,7 +746,7 @@ impl Optimizer<'_> {
// a ? b ? c() : d() : d() => a && b ? c() : d() // a ? b ? c() : d() : d() => a && b ? c() : d()
(Expr::Cond(cons), alt) if (*cons.alt).eq_ignore_span(&*alt) => { (Expr::Cond(cons), alt) if (*cons.alt).eq_ignore_span(&*alt) => {
log::trace!("conditionals: a ? b ? c() : d() : d() => a && b ? c() : d()"); log::debug!("conditionals: a ? b ? c() : d() : d() => a && b ? c() : d()");
return Some(Expr::Cond(CondExpr { return Some(Expr::Cond(CondExpr {
span: DUMMY_SP.with_ctxt(self.done_ctxt), span: DUMMY_SP.with_ctxt(self.done_ctxt),
test: Box::new(Expr::Bin(BinExpr { test: Box::new(Expr::Bin(BinExpr {
@ -656,16 +763,19 @@ impl Optimizer<'_> {
// z ? "fuji" : (condition(), "fuji"); // z ? "fuji" : (condition(), "fuji");
// => // =>
// (z || condition(), "fuji"); // (z || condition(), "fuji");
(cons, Expr::Seq(alt)) (cons, Expr::Seq(alt)) if (**alt.exprs.last().unwrap()).eq_ignore_span(&*cons) => {
if alt.exprs.len() == 2 && (**alt.exprs.last().unwrap()).eq_ignore_span(&*cons) => self.changed = true;
{ log::debug!("conditionals: Reducing seq expr in alt");
log::trace!("conditionals: Reducing seq expr in alt");
// //
alt.exprs.pop();
let first = Box::new(Expr::Bin(BinExpr { let first = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP, span: DUMMY_SP,
left: test.take(), left: test.take(),
op: op!("||"), op: op!("||"),
right: alt.exprs[0].take(), right: Box::new(Expr::Seq(SeqExpr {
span: alt.span,
exprs: alt.exprs.take(),
})),
})); }));
return Some(Expr::Seq(SeqExpr { return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP, span: DUMMY_SP,
@ -673,9 +783,164 @@ impl Optimizer<'_> {
})); }));
} }
// z ? (condition(), "fuji") : "fuji"
// =>
// (z && condition(), "fuji");
(Expr::Seq(cons), alt) if (**cons.exprs.last().unwrap()).eq_ignore_span(&*alt) => {
self.changed = true;
log::debug!("conditionals: Reducing seq expr in cons");
//
cons.exprs.pop();
let first = Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
left: test.take(),
op: op!("&&"),
right: Box::new(Expr::Seq(SeqExpr {
span: cons.span,
exprs: cons.exprs.take(),
})),
}));
return Some(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: vec![first, Box::new(alt.take())],
}));
}
_ => None, _ => None,
} }
} }
/// Currently disabled.
pub(super) fn inject_else(&mut self, stmts: &mut Vec<Stmt>) {
if DISABLE_BUGGY_PASSES {
return;
}
let len = stmts.len();
let pos_of_if = stmts.iter().enumerate().rposition(|(idx, s)| {
idx != len - 1
&& match s {
Stmt::If(IfStmt {
cons, alt: None, ..
}) => match &**cons {
Stmt::Block(b) => {
b.stmts.len() == 2
&& match b.stmts.first() {
Some(Stmt::If(..) | Stmt::Expr(..)) => false,
_ => true,
}
&& match b.stmts.last() {
Some(Stmt::Return(ReturnStmt { arg: None, .. })) => true,
_ => false,
}
}
_ => false,
},
_ => false,
}
});
let pos_of_if = match pos_of_if {
Some(v) => v,
_ => return,
};
self.changed = true;
log::debug!("if_return: Injecting else because it's shorter");
let mut new = vec![];
new.reserve(pos_of_if + 1);
new.extend(stmts.drain(..pos_of_if));
let alt = stmts.drain(1..).collect::<Vec<_>>();
let if_stmt = stmts.take().into_iter().next().unwrap();
match if_stmt {
Stmt::If(mut s) => {
match &mut *s.cons {
Stmt::Block(cons) => {
cons.stmts.pop();
}
_ => {
unreachable!()
}
}
assert_eq!(s.alt, None);
s.alt = Some(if alt.len() == 1 {
Box::new(alt.into_iter().next().unwrap())
} else {
Box::new(Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: alt,
}))
});
new.push(Stmt::If(s))
}
_ => {
unreachable!()
}
}
*stmts = new;
}
/// if (foo) return bar()
/// else baz()
///
/// `else` token can be removed from the code above.
pub(super) fn drop_else_token<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
// Find an if statement with else token.
let need_work = stmts.iter().any(|stmt| match stmt.as_stmt() {
Some(Stmt::If(IfStmt {
cons,
alt: Some(..),
..
})) => always_terminates(cons),
_ => false,
});
if !need_work {
return;
}
//
let mut new_stmts = vec![];
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(stmt) => match stmt {
Stmt::If(IfStmt {
span,
test,
cons,
alt: Some(alt),
..
}) if always_terminates(&cons) => {
new_stmts.push(T::from_stmt(Stmt::If(IfStmt {
span,
test,
cons,
alt: None,
})));
new_stmts.push(T::from_stmt(*alt));
}
_ => {
new_stmts.push(T::from_stmt(stmt));
}
},
Err(stmt) => new_stmts.push(stmt),
}
}
self.changed = true;
log::debug!("conditionals: Dropped useless `else` token");
*stmts = new_stmts;
}
} }
fn extract_expr_stmt(s: &mut Stmt) -> Option<&mut Expr> { fn extract_expr_stmt(s: &mut Stmt) -> Option<&mut Expr> {
@ -693,7 +958,23 @@ fn is_simple_lhs(l: &PatOrExpr) -> bool {
}, },
PatOrExpr::Pat(l) => match &**l { PatOrExpr::Pat(l) => match &**l {
Pat::Ident(_) => return true, Pat::Ident(_) => return true,
Pat::Expr(e) => match &**e {
Expr::Ident(..) => return true,
_ => false,
},
_ => false, _ => false,
}, },
} }
} }
pub(super) fn always_terminates(s: &Stmt) -> bool {
match s {
Stmt::Return(..) | Stmt::Throw(..) | Stmt::Break(..) | Stmt::Continue(..) => true,
Stmt::If(IfStmt { cons, alt, .. }) => {
always_terminates(&cons) && alt.as_deref().map(always_terminates).unwrap_or(false)
}
Stmt::Block(s) => s.stmts.iter().any(always_terminates),
_ => false,
}
}

View File

@ -55,7 +55,7 @@ impl Optimizer<'_> {
.map(|var| var.is_fn_local) .map(|var| var.is_fn_local)
.unwrap_or(false) .unwrap_or(false)
{ {
log::trace!( log::debug!(
"dead_code: Dropping an assigment to a varaible declared in \ "dead_code: Dropping an assigment to a varaible declared in \
function because function is being terminated" function because function is being terminated"
); );

View File

@ -65,7 +65,7 @@ impl Optimizer<'_> {
sym: js_word!("undefined"), sym: js_word!("undefined"),
.. ..
}) => { }) => {
log::trace!("evaluate: `undefined` -> `void 0`"); log::debug!("evaluate: `undefined` -> `void 0`");
self.changed = true; self.changed = true;
*e = *undefined(*span); *e = *undefined(*span);
return; return;
@ -76,7 +76,7 @@ impl Optimizer<'_> {
sym: js_word!("Infinity"), sym: js_word!("Infinity"),
.. ..
}) => { }) => {
log::trace!("evaluate: `Infinity` -> `1 / 0`"); log::debug!("evaluate: `Infinity` -> `1 / 0`");
self.changed = true; self.changed = true;
*e = Expr::Bin(BinExpr { *e = Expr::Bin(BinExpr {
span: span.with_ctxt(self.done_ctxt), span: span.with_ctxt(self.done_ctxt),
@ -149,7 +149,7 @@ impl Optimizer<'_> {
1 => match &*args[0].expr { 1 => match &*args[0].expr {
Expr::Lit(Lit::Str(exp)) => { Expr::Lit(Lit::Str(exp)) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Converting RegExpr call into a regexp literal `/{}/`", "evaluate: Converting RegExpr call into a regexp literal `/{}/`",
exp.value exp.value
); );
@ -165,7 +165,7 @@ impl Optimizer<'_> {
_ => match (&*args[0].expr, &*args[1].expr) { _ => match (&*args[0].expr, &*args[1].expr) {
(Expr::Lit(Lit::Str(exp)), Expr::Lit(Lit::Str(flags))) => { (Expr::Lit(Lit::Str(exp)), Expr::Lit(Lit::Str(flags))) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Converting RegExpr call into a regexp literal `/{}/{}`", "evaluate: Converting RegExpr call into a regexp literal `/{}/{}`",
exp.value, exp.value,
flags.value flags.value
@ -209,7 +209,7 @@ impl Optimizer<'_> {
match char::from_u32(v) { match char::from_u32(v) {
Some(v) => { Some(v) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evanluate: Evaluated `String.charCodeAt({})` as `{}`", "evanluate: Evaluated `String.charCodeAt({})` as `{}`",
char_code, char_code,
v v
@ -362,7 +362,7 @@ impl Optimizer<'_> {
match c { match c {
Some(v) => { Some(v) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Evaluated `charCodeAt` of a string literal as `{}`", "evaluate: Evaluated `charCodeAt` of a string literal as `{}`",
v v
); );
@ -373,7 +373,7 @@ impl Optimizer<'_> {
} }
None => { None => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Evaluated `charCodeAt` of a string literal as `NaN`", "evaluate: Evaluated `charCodeAt` of a string literal as `NaN`",
); );
*e = Expr::Ident(Ident::new( *e = Expr::Ident(Ident::new(
@ -389,7 +389,7 @@ impl Optimizer<'_> {
}; };
self.changed = true; self.changed = true;
log::trace!("evaluate: Evaluated `{}` of a string literal", method); log::debug!("evaluate: Evaluated `{}` of a string literal", method);
*e = Expr::Lit(Lit::Str(Str { *e = Expr::Lit(Lit::Str(Str {
value: new_val.into(), value: new_val.into(),
..s ..s
@ -412,7 +412,7 @@ impl Optimizer<'_> {
.. ..
}) => { }) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Reducing a call to `Number` into an unary operation" "evaluate: Reducing a call to `Number` into an unary operation"
); );
@ -442,7 +442,7 @@ impl Optimizer<'_> {
Expr::Call(..) => { Expr::Call(..) => {
if let Some(value) = self.eval_as_number(&e) { if let Some(value) = self.eval_as_number(&e) {
self.changed = true; self.changed = true;
log::trace!("evaluate: Evaluated an expression as `{}`", value); log::debug!("evaluate: Evaluated an expression as `{}`", value);
*e = Expr::Lit(Lit::Num(Number { *e = Expr::Lit(Lit::Num(Number {
span: e.span(), span: e.span(),
@ -462,7 +462,7 @@ impl Optimizer<'_> {
if let Known(l) = l { if let Known(l) = l {
if let Known(r) = r { if let Known(r) = r {
self.changed = true; self.changed = true;
log::trace!("evaluate: Evaulated `{:?} ** {:?}`", l, r); log::debug!("evaluate: Evaulated `{:?} ** {:?}`", l, r);
let value = l.powf(r); let value = l.powf(r);
*e = Expr::Lit(Lit::Num(Number { *e = Expr::Lit(Lit::Num(Number {
@ -507,7 +507,7 @@ impl Optimizer<'_> {
} }
self.changed = true; self.changed = true;
log::trace!("evaluate: `0 / 0` => `NaN`"); log::debug!("evaluate: `0 / 0` => `NaN`");
// Sign does not matter for NaN // Sign does not matter for NaN
*e = Expr::Ident(Ident::new( *e = Expr::Ident(Ident::new(
@ -518,7 +518,7 @@ impl Optimizer<'_> {
} }
(FpCategory::Normal, FpCategory::Zero) => { (FpCategory::Normal, FpCategory::Zero) => {
self.changed = true; self.changed = true;
log::trace!("evaluate: `constant / 0` => `Infinity`"); log::debug!("evaluate: `constant / 0` => `Infinity`");
// Sign does not matter for NaN // Sign does not matter for NaN
*e = if ln.is_sign_positive() == rn.is_sign_positive() { *e = if ln.is_sign_positive() == rn.is_sign_positive() {
@ -590,7 +590,7 @@ impl Optimizer<'_> {
let value = num_to_fixed(num.value, precision + 1); let value = num_to_fixed(num.value, precision + 1);
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Evaluating `{}.toFixed({})` as `{}`", "evaluate: Evaluating `{}.toFixed({})` as `{}`",
num, num,
precision, precision,
@ -816,7 +816,7 @@ impl Optimizer<'_> {
match call.args.len() { match call.args.len() {
0 => { 0 => {
self.changed = true; self.changed = true;
log::trace!("evaluate: Dropping array.slice call"); log::debug!("evaluate: Dropping array.slice call");
*e = *obj.take(); *e = *obj.take();
return; return;
} }
@ -825,7 +825,7 @@ impl Optimizer<'_> {
let start = start.floor() as usize; let start = start.floor() as usize;
self.changed = true; self.changed = true;
log::trace!("evaluate: Reducing array.slice({}) call", start); log::debug!("evaluate: Reducing array.slice({}) call", start);
if start >= arr.elems.len() { if start >= arr.elems.len() {
*e = Expr::Array(ArrayLit { *e = Expr::Array(ArrayLit {
@ -850,7 +850,7 @@ impl Optimizer<'_> {
if let Known(end) = end { if let Known(end) = end {
let end = end.floor() as usize; let end = end.floor() as usize;
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Reducing array.slice({}, {}) call", "evaluate: Reducing array.slice({}, {}) call",
start, start,
end end
@ -936,7 +936,7 @@ impl Optimizer<'_> {
} }
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaludate: Reduced `funtion.valueOf()` into a function expression" "evaludate: Reduced `funtion.valueOf()` into a function expression"
); );
@ -977,7 +977,7 @@ impl Optimizer<'_> {
// As we used as_pure_bool, we can drop it. // As we used as_pure_bool, we can drop it.
if v && e.op == op!("&&") { if v && e.op == op!("&&") {
self.changed = true; self.changed = true;
log::trace!("Removing `b` from `a && b && c` because b is always truthy"); log::debug!("Removing `b` from `a && b && c` because b is always truthy");
left.right.take(); left.right.take();
return; return;
@ -985,7 +985,7 @@ impl Optimizer<'_> {
if !v && e.op == op!("||") { if !v && e.op == op!("||") {
self.changed = true; self.changed = true;
log::trace!("Removing `b` from `a || b || c` because b is always falsy"); log::debug!("Removing `b` from `a || b || c` because b is always falsy");
left.right.take(); left.right.take();
return; return;
@ -1011,7 +1011,7 @@ impl Optimizer<'_> {
// //
if is_pure_undefined_or_null(&obj) { if is_pure_undefined_or_null(&obj) {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Reduced an optioanl chaining operation because object is \ "evaluate: Reduced an optioanl chaining operation because object is \
always null or undefined" always null or undefined"
); );
@ -1028,7 +1028,7 @@ impl Optimizer<'_> {
}) => { }) => {
if is_pure_undefined_or_null(&callee) { if is_pure_undefined_or_null(&callee) {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"evaluate: Reduced a call expression with optioanl chaining operation \ "evaluate: Reduced a call expression with optioanl chaining operation \
because object is always null or undefined" because object is always null or undefined"
); );

View File

@ -0,0 +1,97 @@
use super::Optimizer;
use crate::util::{sort::is_sorted_by, MoudleItemExt};
use std::cmp::Ordering;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
impl Optimizer<'_> {
/// Calls `reorder_stmts_inner` after splitting stmts.
pub(super) fn reorder_stmts<T>(&mut self, stmts: &mut Vec<T>)
where
T: MoudleItemExt,
{
if self.ctx.in_asm {
return;
}
self.reorder_stmts_inner(stmts);
}
/// Sorts given statements.
fn reorder_stmts_inner<T>(&mut self, stmts: &mut Vec<T>)
where
T: MoudleItemExt,
{
if !self.options.join_vars {
return;
}
// Sort by position
if is_sorted_by(stmts.iter(), |a, b| {
// Check for function declarations.
match (a.as_module_decl(), b.as_module_decl()) {
(
Err(Stmt::Decl(
Decl::Fn(..)
| Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
}),
)),
Err(Stmt::Decl(
Decl::Fn(..)
| Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
}),
)),
) => Some(Ordering::Less),
(_, Err(Stmt::Decl(Decl::Fn(..)))) => Some(Ordering::Greater),
_ => Some(Ordering::Less),
}
}) {
return;
}
self.changed = true;
log::debug!("fns: Reordering statements");
let mut fns = vec![];
let mut other = vec![];
for stmt in stmts.take() {
let stmt = stmt.into_module_item();
match stmt {
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(..)))
| ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(..),
..
}))
| ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
})))
| ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl:
Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
}),
..
})) => {
fns.push(T::from_module_item(stmt));
}
_ => {
other.push(T::from_module_item(stmt));
}
}
}
fns.extend(other);
*stmts = fns;
}
}

View File

@ -88,7 +88,7 @@ impl Optimizer<'_> {
match &p.key { match &p.key {
PropName::Str(s) => { PropName::Str(s) => {
log::trace!( log::debug!(
"hoist_props: Storing a varaible to inline \ "hoist_props: Storing a varaible to inline \
properties" properties"
); );
@ -96,7 +96,7 @@ impl Optimizer<'_> {
.insert((name.to_id(), s.value.clone()), value); .insert((name.to_id(), s.value.clone()), value);
} }
PropName::Ident(i) => { PropName::Ident(i) => {
log::trace!( log::debug!(
"hoist_props: Storing a varaible to inline \ "hoist_props: Storing a varaible to inline \
properties" properties"
); );
@ -118,9 +118,12 @@ impl Optimizer<'_> {
.data .data
.as_ref() .as_ref()
.and_then(|data| { .and_then(|data| {
data.vars data.vars.get(&name.to_id()).map(|v| {
.get(&name.to_id()) v.ref_count == 1
.map(|v| v.ref_count == 1 && v.has_property_access) && v.has_property_access
&& v.is_fn_local
&& !v.used_in_loop
})
}) })
.unwrap_or(false) .unwrap_or(false)
{ {
@ -132,6 +135,15 @@ impl Optimizer<'_> {
None => return, None => return,
}; };
match &*init {
Expr::This(..) => {
n.init = Some(init);
return;
}
_ => {}
}
match self.vars_for_prop_hoisting.insert(name.to_id(), init) { match self.vars_for_prop_hoisting.insert(name.to_id(), init) {
Some(prev) => { Some(prev) => {
panic!( panic!(
@ -140,7 +152,13 @@ impl Optimizer<'_> {
prev prev
); );
} }
None => {} None => {
log::debug!(
"hoist_props: Stored {}{:?} to inline property access",
name.id.sym,
name.id.span.ctxt
);
}
} }
} }
_ => {} _ => {}
@ -161,7 +179,7 @@ impl Optimizer<'_> {
if let Some(value) = self.vars_for_prop_hoisting.remove(&obj.to_id()) { if let Some(value) = self.vars_for_prop_hoisting.remove(&obj.to_id()) {
member.obj = ExprOrSuper::Expr(value); member.obj = ExprOrSuper::Expr(value);
self.changed = true; self.changed = true;
log::trace!("hoist_props: Inlined a property"); log::debug!("hoist_props: Inlined a property");
return; return;
} }
@ -174,7 +192,7 @@ impl Optimizer<'_> {
if let Some(value) = if let Some(value) =
self.simple_props.get(&(obj.to_id(), prop.sym.clone())) self.simple_props.get(&(obj.to_id(), prop.sym.clone()))
{ {
log::trace!("hoist_props: Inlining `{}.{}`", obj.sym, prop.sym); log::debug!("hoist_props: Inlining `{}.{}`", obj.sym, prop.sym);
self.changed = true; self.changed = true;
*e = *value.clone() *e = *value.clone()
} }

View File

@ -1,9 +1,12 @@
use super::Optimizer; use super::Optimizer;
use crate::compress::optimize::is_pure_undefined; use crate::compress::optimize::is_pure_undefined;
use crate::debug::dump;
use crate::util::ExprOptExt; use crate::util::ExprOptExt;
use swc_common::Spanned;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::prepend;
use swc_ecma_utils::undefined; use swc_ecma_utils::undefined;
use swc_ecma_utils::StmtLike; use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_type; use swc_ecma_visit::noop_visit_type;
@ -14,6 +17,216 @@ use swc_ecma_visit::VisitWith;
/// Methods related to the option `if_return`. All methods are noop if /// Methods related to the option `if_return`. All methods are noop if
/// `if_return` is false. /// `if_return` is false.
impl Optimizer<'_> { impl Optimizer<'_> {
pub(super) fn drop_undefined_from_return_arg(&mut self, s: &mut ReturnStmt) {
match s.arg.as_deref() {
Some(e) => {
if is_pure_undefined(e) {
self.changed = true;
log::debug!("Dropped `undefined` from `return undefined`");
s.arg.take();
}
}
None => {}
}
}
/// # Input
///
/// ```js
/// function f(a, b) {
/// if (a) return;
/// console.log(b);
/// }
/// ```
///
/// # Output
/// ```js
/// function f(a, b) {
/// if (!a)
/// console.log(b);
/// }
/// ```
pub(super) fn negate_if_terminate(
&mut self,
stmts: &mut Vec<Stmt>,
handle_return: bool,
handle_continue: bool,
) {
if handle_return {
if !self.options.if_return || !self.options.bools {
return;
}
for s in stmts.iter_mut() {
match s {
Stmt::If(s) => match &mut *s.cons {
Stmt::Block(cons) => {
self.negate_if_terminate(&mut cons.stmts, true, false);
}
_ => {}
},
_ => {}
}
}
}
let len = stmts.len();
let pos_of_if = stmts.iter().enumerate().rposition(|(idx, s)| {
idx != len - 1
&& match s {
Stmt::If(IfStmt {
cons, alt: None, ..
}) => match &**cons {
Stmt::Return(ReturnStmt { arg: None, .. }) => handle_return,
Stmt::Continue(ContinueStmt { label: None, .. }) => handle_continue,
_ => false,
},
_ => false,
}
});
let pos_of_if = match pos_of_if {
Some(v) => v,
_ => return,
};
self.changed = true;
log::debug!(
"if_return: Negating `foo` in `if (foo) return; bar()` to make it `if (!foo) bar()`"
);
let mut new = vec![];
new.extend(stmts.drain(..pos_of_if));
let cons = stmts.drain(1..).collect::<Vec<_>>();
let if_stmt = stmts.take().into_iter().next().unwrap();
match if_stmt {
Stmt::If(mut s) => {
assert_eq!(s.alt, None);
self.negate(&mut s.test);
s.cons = if cons.len() == 1 {
Box::new(cons.into_iter().next().unwrap())
} else {
Box::new(Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: cons,
}))
};
new.push(Stmt::If(s))
}
_ => {
unreachable!()
}
}
*stmts = new;
}
pub(super) fn merge_nested_if(&mut self, s: &mut IfStmt) {
if !self.options.conditionals && !self.options.bools {
return;
}
if s.alt.is_some() {
return;
}
match &mut *s.cons {
Stmt::If(IfStmt {
test,
cons,
alt: None,
..
}) => {
self.changed = true;
log::debug!("if_return: Merging nested if statements");
s.test = Box::new(Expr::Bin(BinExpr {
span: s.test.span(),
op: op!("&&"),
left: s.test.take(),
right: test.take(),
}));
s.cons = cons.take();
}
_ => {}
}
}
pub(super) fn merge_else_if(&mut self, s: &mut IfStmt) {
match s.alt.as_deref_mut() {
Some(Stmt::If(IfStmt {
span: span_of_alt,
test: test_of_alt,
cons: cons_of_alt,
alt: Some(alt_of_alt),
..
})) => {
match &**cons_of_alt {
Stmt::Return(..) | Stmt::Continue(ContinueStmt { label: None, .. }) => {}
_ => return,
}
match &mut **alt_of_alt {
Stmt::Block(..) => {}
Stmt::Expr(..) => {
*alt_of_alt = Box::new(Stmt::Block(BlockStmt {
span: DUMMY_SP,
stmts: vec![*alt_of_alt.take()],
}));
}
_ => {
return;
}
}
self.changed = true;
log::debug!("if_return: Merging `else if` into `else`");
match &mut **alt_of_alt {
Stmt::Block(alt_of_alt) => {
prepend(
&mut alt_of_alt.stmts,
Stmt::If(IfStmt {
span: *span_of_alt,
test: test_of_alt.take(),
cons: cons_of_alt.take(),
alt: None,
}),
);
}
_ => {
unreachable!()
}
}
s.alt = Some(alt_of_alt.take());
return;
}
_ => {}
}
}
fn merge_nested_if_returns(&mut self, s: &mut Stmt) {
match s {
Stmt::Block(s) => self.merge_if_returns(&mut s.stmts),
Stmt::If(s) => {
self.merge_nested_if_returns(&mut s.cons);
if let Some(alt) = &mut s.alt {
self.merge_nested_if_returns(&mut **alt);
}
}
_ => {}
}
}
/// Merge simple return statements in if statements. /// Merge simple return statements in if statements.
/// ///
/// # Example /// # Example
@ -34,33 +247,94 @@ impl Optimizer<'_> {
/// return a ? foo() : bar(); /// return a ? foo() : bar();
/// } /// }
/// ``` /// ```
pub(super) fn merge_if_returns<T>(&mut self, stmts: &mut Vec<T>) pub(super) fn merge_if_returns(&mut self, stmts: &mut Vec<Stmt>) {
where if !self.options.if_return {
T: StmtLike, return;
Vec<T>: VisitWith<ReturnFinder>, }
{
if !self.options.if_return || stmts.is_empty() { for stmt in stmts.iter_mut() {
self.merge_nested_if_returns(stmt)
}
if stmts.len() <= 1 {
return;
}
let idx_of_not_mergable = stmts.iter().rposition(|stmt| match stmt.as_stmt() {
Some(v) => !self.can_merge_stmt_as_if_return(v),
None => true,
});
let skip = idx_of_not_mergable.map(|v| v + 1).unwrap_or(0);
log::trace!("if_return: Skip = {}", skip);
if stmts.len() <= skip + 1 {
log::trace!("if_return: [x] Aborting because of skip");
return; return;
} }
{ {
let return_count = count_leaping_returns(&*stmts); let stmts = &stmts[skip..];
let return_count: usize = stmts.iter().map(|v| count_leaping_returns(v)).sum();
let ends_with_if = stmts // There's no return statment so merging requires injecting unnecessary `void 0`
.last() if return_count == 0 {
.map(|stmt| match stmt.as_stmt() { log::trace!("if_return: [x] Aborting because we failed to find return");
Some(Stmt::If(..)) => true, return;
}
// If the last statement is a return statement and last - 1 is an if statement
// is without return, we don't need to fold it as `void 0` is too much for such
// cases.
let if_return_count = stmts
.iter()
.filter(|s| match s {
Stmt::If(IfStmt {
cons, alt: None, ..
}) => always_terminates_with_return_arg(&cons),
_ => false, _ => false,
}) })
.unwrap(); .count();
if return_count <= 1 && ends_with_if {
if stmts.len() >= 2 {
match (
&stmts[stmts.len() - 2].as_stmt(),
&stmts[stmts.len() - 1].as_stmt(),
) {
(_, Some(Stmt::If(IfStmt { alt: None, .. }) | Stmt::Expr(..)))
if if_return_count <= 1 =>
{
log::trace!(
"if_return: [x] Aborting because last stmt is a not return stmt"
);
return; return;
} }
(
Some(Stmt::If(IfStmt {
cons, alt: None, ..
})),
Some(Stmt::Return(..)),
) => match &**cons {
Stmt::Return(ReturnStmt { arg: Some(..), .. }) => {}
_ => {
log::trace!(
"if_return: [x] Aborting because stmt before last is an if stmt \
and cons of it is not a return stmt"
);
return;
}
},
_ => {}
}
}
} }
{ {
let start = stmts let start = stmts
.iter() .iter()
.skip(skip)
.position(|stmt| match stmt.as_stmt() { .position(|stmt| match stmt.as_stmt() {
Some(v) => self.can_merge_stmt_as_if_return(v), Some(v) => self.can_merge_stmt_as_if_return(v),
None => false, None => false,
@ -70,16 +344,16 @@ impl Optimizer<'_> {
let ends_with_mergable = stmts let ends_with_mergable = stmts
.last() .last()
.map(|stmt| match stmt.as_stmt() { .map(|stmt| match stmt.as_stmt() {
Some(Stmt::Return(..)) | Some(Stmt::Expr(..)) => true, Some(s) => self.can_merge_stmt_as_if_return(s),
_ => false, _ => false,
}) })
.unwrap(); .unwrap();
if stmts.len() - start == 1 && ends_with_mergable { if stmts.len() == start + skip + 1 || !ends_with_mergable {
return; return;
} }
let can_merge = stmts.iter().skip(start).all(|stmt| match stmt.as_stmt() { let can_merge = stmts.iter().skip(skip).all(|stmt| match stmt.as_stmt() {
Some(s) => self.can_merge_stmt_as_if_return(s), Some(s) => self.can_merge_stmt_as_if_return(s),
_ => false, _ => false,
}); });
@ -88,28 +362,33 @@ impl Optimizer<'_> {
} }
} }
log::trace!("if_return: Merging returns"); log::debug!("if_return: Merging returns");
if cfg!(feature = "debug") {
let block = BlockStmt {
span: DUMMY_SP,
stmts: stmts.clone(),
};
log::trace!("if_return: {}", dump(&block))
}
self.changed = true; self.changed = true;
let mut cur: Option<Box<Expr>> = None; let mut cur: Option<Box<Expr>> = None;
let mut new = Vec::with_capacity(stmts.len()); let mut new = Vec::with_capacity(stmts.len());
for stmt in stmts.take() { for (idx, stmt) in stmts.take().into_iter().enumerate() {
let stmt = match stmt.try_into_stmt() { if let Some(not_mergable) = idx_of_not_mergable {
Ok(stmt) => { if idx < not_mergable {
if !self.can_merge_stmt_as_if_return(&stmt) { new.push(stmt);
continue;
}
}
let stmt = if !self.can_merge_stmt_as_if_return(&stmt) {
debug_assert_eq!(cur, None); debug_assert_eq!(cur, None);
new.push(T::from_stmt(stmt)); new.push(stmt);
continue; continue;
} else { } else {
stmt stmt
}
}
Err(item) => {
debug_assert_eq!(cur, None);
new.push(item);
continue;
}
}; };
let is_nonconditional_return = match stmt { let is_nonconditional_return = match stmt {
Stmt::Return(..) => true, Stmt::Return(..) => true,
@ -203,17 +482,17 @@ impl Optimizer<'_> {
let expr = self.ignore_return_value(&mut cur); let expr = self.ignore_return_value(&mut cur);
if let Some(cur) = expr { if let Some(cur) = expr {
new.push(T::from_stmt(Stmt::Expr(ExprStmt { new.push(Stmt::Expr(ExprStmt {
span: DUMMY_SP, span: DUMMY_SP,
expr: Box::new(cur), expr: Box::new(cur),
}))) }))
} }
} }
_ => { _ => {
new.push(T::from_stmt(Stmt::Return(ReturnStmt { new.push(Stmt::Return(ReturnStmt {
span: DUMMY_SP, span: DUMMY_SP,
arg: Some(cur), arg: Some(cur),
}))); }));
} }
} }
} }
@ -227,6 +506,11 @@ impl Optimizer<'_> {
fn merge_if_returns_to(&mut self, stmt: Stmt, mut exprs: Vec<Box<Expr>>) -> Expr { fn merge_if_returns_to(&mut self, stmt: Stmt, mut exprs: Vec<Box<Expr>>) -> Expr {
// //
match stmt { match stmt {
Stmt::Block(s) => {
assert_eq!(s.stmts.len(), 1);
self.merge_if_returns_to(s.stmts.into_iter().next().unwrap(), exprs)
}
Stmt::If(IfStmt { Stmt::If(IfStmt {
span, span,
test, test,
@ -276,9 +560,10 @@ impl Optimizer<'_> {
} }
fn can_merge_stmt_as_if_return(&self, s: &Stmt) -> bool { fn can_merge_stmt_as_if_return(&self, s: &Stmt) -> bool {
match s { let res = match s {
Stmt::Expr(..) | Stmt::Return(..) => true, Stmt::Expr(..) | Stmt::Return(..) => true,
Stmt::If(stmt) if self.options.conditionals => { Stmt::Block(s) => s.stmts.len() == 1 && self.can_merge_stmt_as_if_return(&s.stmts[0]),
Stmt::If(stmt) => {
self.can_merge_stmt_as_if_return(&stmt.cons) self.can_merge_stmt_as_if_return(&stmt.cons)
&& stmt && stmt
.alt .alt
@ -287,7 +572,12 @@ impl Optimizer<'_> {
.unwrap_or(true) .unwrap_or(true)
} }
_ => false, _ => false,
} };
// if !res {
// log::trace!("Cannot merge: {}", dump(s));
// }
res
} }
} }
@ -323,3 +613,19 @@ impl Visit for ReturnFinder {
fn visit_function(&mut self, _: &Function, _: &dyn Node) {} fn visit_function(&mut self, _: &Function, _: &dyn Node) {}
fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {} fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {}
} }
fn always_terminates_with_return_arg(s: &Stmt) -> bool {
match s {
Stmt::Return(ReturnStmt { arg: Some(..), .. }) => true,
Stmt::If(IfStmt { cons, alt, .. }) => {
always_terminates_with_return_arg(&cons)
&& alt
.as_deref()
.map(always_terminates_with_return_arg)
.unwrap_or(false)
}
Stmt::Block(s) => s.stmts.iter().any(always_terminates_with_return_arg),
_ => false,
}
}

View File

@ -2,7 +2,6 @@ use super::Optimizer;
use crate::compress::optimize::Ctx; use crate::compress::optimize::Ctx;
use crate::util::idents_used_by; use crate::util::idents_used_by;
use crate::util::make_number; use crate::util::make_number;
use crate::util::IdentUsageCollector;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use std::collections::HashMap; use std::collections::HashMap;
use std::mem::replace; use std::mem::replace;
@ -13,20 +12,17 @@ use swc_common::Spanned;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::find_ids;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::undefined; use swc_ecma_utils::undefined;
use swc_ecma_utils::DestructuringFinder; use swc_ecma_utils::ExprFactory;
use swc_ecma_utils::ExprExt;
use swc_ecma_utils::Id; use swc_ecma_utils::Id;
use swc_ecma_visit::VisitMutWith; use swc_ecma_visit::VisitMutWith;
use swc_ecma_visit::VisitWith;
/// Methods related to the option `negate_iife`. /// Methods related to the option `negate_iife`.
impl Optimizer<'_> { impl Optimizer<'_> {
/// Negates iife, while ignore return value. /// Negates iife, while ignore return value.
pub(super) fn negate_iife_ignoring_ret(&mut self, e: &mut Expr) { pub(super) fn negate_iife_ignoring_ret(&mut self, e: &mut Expr) {
if !self.options.negate_iife || self.ctx.in_bang_arg { if !self.options.negate_iife || self.ctx.in_bang_arg || self.ctx.dont_use_negated_iife {
return; return;
} }
@ -42,7 +38,7 @@ impl Optimizer<'_> {
match callee { match callee {
Expr::Fn(..) => { Expr::Fn(..) => {
log::trace!("negate_iife: Negating iife"); log::debug!("negate_iife: Negating iife");
*e = Expr::Unary(UnaryExpr { *e = Expr::Unary(UnaryExpr {
span: DUMMY_SP, span: DUMMY_SP,
op: op!("!"), op: op!("!"),
@ -54,37 +50,71 @@ impl Optimizer<'_> {
} }
} }
/// Returns true if it did any work.
///
/// ///
/// - `iife ? foo : bar` => `!iife ? bar : foo` /// - `iife ? foo : bar` => `!iife ? bar : foo`
pub(super) fn negate_iife_in_cond(&mut self, e: &mut Expr) { pub(super) fn negate_iife_in_cond(&mut self, e: &mut Expr) -> bool {
let cond = match e { let cond = match e {
Expr::Cond(v) => v, Expr::Cond(v) => v,
_ => return, _ => return false,
}; };
let test_call = match &mut *cond.test { let test_call = match &mut *cond.test {
Expr::Call(e) => e, Expr::Call(e) => e,
_ => return, _ => return false,
}; };
let callee = match &mut test_call.callee { let callee = match &mut test_call.callee {
ExprOrSuper::Super(_) => return, ExprOrSuper::Super(_) => return false,
ExprOrSuper::Expr(e) => &mut **e, ExprOrSuper::Expr(e) => &mut **e,
}; };
match callee { match callee {
Expr::Fn(..) => { Expr::Fn(..) => {
log::trace!("negate_iife: Swapping cons and alt"); log::debug!("negate_iife: Swapping cons and alt");
cond.test = Box::new(Expr::Unary(UnaryExpr { cond.test = Box::new(Expr::Unary(UnaryExpr {
span: DUMMY_SP, span: DUMMY_SP,
op: op!("!"), op: op!("!"),
arg: cond.test.take(), arg: cond.test.take(),
})); }));
swap(&mut cond.cons, &mut cond.alt); swap(&mut cond.cons, &mut cond.alt);
return true;
}
_ => false,
}
}
pub(super) fn restore_negated_iife(&mut self, cond: &mut CondExpr) {
if !self.ctx.dont_use_negated_iife {
return; return;
} }
_ => {}
match &mut *cond.test {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) => match &mut **arg {
Expr::Call(CallExpr {
span: call_span,
callee: ExprOrSuper::Expr(callee),
args,
..
}) => match &**callee {
Expr::Fn(..) => {
cond.test = Box::new(Expr::Call(CallExpr {
span: *call_span,
callee: callee.take().as_callee(),
args: args.take(),
type_args: Default::default(),
}));
swap(&mut cond.cons, &mut cond.alt);
} }
_ => {}
},
_ => {}
},
_ => {}
};
} }
} }
@ -168,21 +198,33 @@ impl Optimizer<'_> {
if usage.reassigned { if usage.reassigned {
continue; continue;
} }
if usage.ref_count != 1 {
continue;
}
} }
if let Some(arg) = arg { if let Some(arg) = arg {
// NOTE
//
// This function is misdesigned and should be removed.
// This is wrong because the order of execution is not guaranteed.
match &**arg {
Expr::Ident(..) | Expr::Lit(..) => {}
_ => continue,
}
let should_be_inlined = self.can_be_inlined_for_iife(arg); let should_be_inlined = self.can_be_inlined_for_iife(arg);
if should_be_inlined { if should_be_inlined {
log::trace!( log::debug!(
"iife: Trying to inline {}{:?}", "iife: Trying to inline argument ({}{:?})",
param.id.sym, param.id.sym,
param.id.span.ctxt param.id.span.ctxt
); );
vars.insert(param.to_id(), arg.clone()); vars.insert(param.to_id(), arg.clone());
} }
} else { } else {
log::trace!( log::debug!(
"iife: Trying to inline {}{:?} (undefined)", "iife: Trying to inline argument ({}{:?}) (undefined)",
param.id.sym, param.id.sym,
param.id.span.ctxt param.id.span.ctxt
); );
@ -206,10 +248,11 @@ impl Optimizer<'_> {
} }
} }
fn inline_vars_in_node<N>(&mut self, n: &mut N, vars: FxHashMap<Id, Box<Expr>>) pub(super) fn inline_vars_in_node<N>(&mut self, n: &mut N, vars: FxHashMap<Id, Box<Expr>>)
where where
N: VisitMutWith<Self>, N: VisitMutWith<Self>,
{ {
log::debug!("inline: inline_vars_in_node");
let ctx = Ctx { let ctx = Ctx {
inline_prevented: false, inline_prevented: false,
..self.ctx ..self.ctx
@ -249,10 +292,6 @@ impl Optimizer<'_> {
} }
} }
if self.ctx.inline_prevented {
return;
}
let call = match e { let call = match e {
Expr::Call(v) => v, Expr::Call(v) => v,
_ => return, _ => return,
@ -267,31 +306,44 @@ impl Optimizer<'_> {
ExprOrSuper::Expr(e) => &mut **e, ExprOrSuper::Expr(e) => &mut **e,
}; };
if call.args.iter().any(|arg| match &*arg.expr { if self.ctx.inline_prevented {
Expr::Member(MemberExpr { log::trace!("iife: [x] Inline is prevented");
computed: false, ..
}) => false,
_ => arg.expr.may_have_side_effects(),
}) {
return; return;
} }
match callee { match callee {
Expr::Arrow(f) => { Expr::Arrow(f) => {
if self.ctx.in_top_level() && !self.ctx.in_call_arg && self.options.negate_iife {
match &f.body {
BlockStmtOrExpr::BlockStmt(body) => {
let has_decl = body.stmts.iter().any(|stmt| match stmt {
Stmt::Decl(..) => true,
_ => false,
});
if has_decl {
return;
}
}
BlockStmtOrExpr::Expr(_) => {}
}
}
if f.params.iter().any(|param| !param.is_ident()) { if f.params.iter().any(|param| !param.is_ident()) {
return; return;
} }
if is_param_used_by_body(&f.params, &f.body) { let param_ids = f
return; .params
} .iter()
.map(|p| p.clone().ident().unwrap().id)
.collect::<Vec<_>>();
match &mut f.body { match &mut f.body {
BlockStmtOrExpr::BlockStmt(body) => { BlockStmtOrExpr::BlockStmt(body) => {
let new = self.inline_fn_like(body); let new = self.inline_fn_like(&param_ids, body, &mut call.args);
if let Some(new) = new { if let Some(new) = new {
self.changed = true; self.changed = true;
log::trace!("inline: Inlining a function call (arrow)"); log::debug!("inline: Inlining a function call (arrow)");
*e = new; *e = new;
} }
@ -306,23 +358,6 @@ impl Optimizer<'_> {
_ => {} _ => {}
}, },
} }
let mut inline_map = HashMap::default();
for (idx, param) in f.params.iter().enumerate() {
let arg_val = if let Some(arg) = call.args.get(idx) {
arg.expr.clone()
} else {
undefined(DUMMY_SP)
};
match param {
Pat::Ident(param) => {
inline_map.insert(param.id.to_id(), arg_val);
}
_ => {}
}
}
self.inline_vars_in_node(&mut f.body, inline_map);
match &mut f.body { match &mut f.body {
BlockStmtOrExpr::BlockStmt(_) => { BlockStmtOrExpr::BlockStmt(_) => {
@ -330,17 +365,74 @@ impl Optimizer<'_> {
} }
BlockStmtOrExpr::Expr(body) => { BlockStmtOrExpr::Expr(body) => {
self.changed = true; self.changed = true;
log::trace!("inline: Inlining a call to an arrow function");
{
let vars = f
.params
.iter()
.cloned()
.map(|name| VarDeclarator {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
name,
init: Default::default(),
definite: Default::default(),
})
.collect::<Vec<_>>();
if !vars.is_empty() {
self.prepend_stmts.push(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: vars,
})));
}
}
let mut exprs = vec![];
exprs.push(Box::new(make_number(DUMMY_SP, 0.0)));
for (idx, param) in f.params.iter().enumerate() {
if let Some(arg) = call.args.get_mut(idx) {
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
op: op!("="),
left: PatOrExpr::Pat(Box::new(param.clone())),
right: arg.expr.take(),
})));
}
}
if call.args.len() > f.params.len() {
for arg in &mut call.args[f.params.len()..] {
exprs.push(arg.expr.take());
}
}
exprs.push(body.take());
log::debug!("inline: Inlining a call to an arrow function");
*e = Expr::Seq(SeqExpr { *e = Expr::Seq(SeqExpr {
span: DUMMY_SP, span: DUMMY_SP,
exprs: vec![Box::new(make_number(DUMMY_SP, 0.0)), body.take()], exprs,
}); });
return; return;
} }
} }
} }
Expr::Fn(f) => { Expr::Fn(f) => {
if self.ctx.in_top_level() && !self.ctx.in_call_arg && self.options.negate_iife {
let body = f.function.body.as_ref().unwrap();
let has_decl = body.stmts.iter().any(|stmt| match stmt {
Stmt::Decl(..) => true,
_ => false,
});
if has_decl {
log::trace!("iife: [x] Found decl");
return;
}
}
if f.function.is_generator { if f.function.is_generator {
log::trace!("iife: [x] Cannot inline generator");
return; return;
} }
@ -349,18 +441,30 @@ impl Optimizer<'_> {
Pat::Object(..) | Pat::Array(..) | Pat::Assign(..) | Pat::Rest(..) => true, Pat::Object(..) | Pat::Array(..) | Pat::Assign(..) | Pat::Rest(..) => true,
_ => false, _ => false,
}) { }) {
log::trace!("iife: [x] Found complex pattern");
return; return;
} }
if let Some(i) = &f.ident { if let Some(i) = &f.ident {
if idents_used_by(&f.function.body).contains(&i.to_id()) { if idents_used_by(&f.function.body).contains(&i.to_id()) {
log::trace!("iife: [x] Recursive?");
return; return;
} }
} }
if is_param_used_by_body(&f.function.params, &f.function.body) { for arg in &call.args {
if arg.spread.is_some() {
log::trace!("iife: [x] Found spread argument");
return; return;
} }
match &*arg.expr {
Expr::Fn(..) | Expr::Arrow(..) => {
log::trace!("iife: [x] Found callable argument");
return;
}
_ => {}
}
}
let body = f.function.body.as_mut().unwrap(); let body = f.function.body.as_mut().unwrap();
if body.stmts.is_empty() { if body.stmts.is_empty() {
@ -368,10 +472,22 @@ impl Optimizer<'_> {
return; return;
} }
let new = self.inline_fn_like(body); let param_ids = f
.function
.params
.iter()
.map(|p| p.pat.clone().ident().unwrap().id)
.collect::<Vec<_>>();
if !self.can_inline_fn_like(&param_ids, body) {
log::trace!("iife: [x] Body is not inliable");
return;
}
let new = self.inline_fn_like(&param_ids, body, &mut call.args);
if let Some(new) = new { if let Some(new) = new {
self.changed = true; self.changed = true;
log::trace!("inline: Inlining a function call"); log::debug!("inline: Inlining a function call");
*e = new; *e = new;
} }
@ -382,11 +498,39 @@ impl Optimizer<'_> {
} }
} }
fn inline_fn_like(&mut self, body: &mut BlockStmt) -> Option<Expr> { fn can_inline_fn_like(&self, param_ids: &[Ident], body: &BlockStmt) -> bool {
if !body.stmts.iter().all(|stmt| match stmt { if !body.stmts.iter().all(|stmt| match stmt {
Stmt::Expr(e) if e.expr.is_await_expr() => false, Stmt::Decl(Decl::Var(VarDecl {
kind: VarDeclKind::Var | VarDeclKind::Let,
decls,
..
})) => {
if decls.iter().any(|decl| match decl.name {
Pat::Ident(..) => false,
_ => true,
}) {
return false;
}
if self.ctx.executed_multiple_time {
return false;
}
true
}
Stmt::Expr(e) => match &*e.expr {
Expr::Await(..) => false,
// TODO: Check if paramter is used and inline if call is not related to parameters.
Expr::Call(e) => {
let used = idents_used_by(e);
param_ids.iter().all(|param| !used.contains(&param.to_id()))
}
_ => true,
},
Stmt::Expr(..) => true,
Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() { Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
Some(Expr::Await(..)) => false, Some(Expr::Await(..)) => false,
@ -401,12 +545,92 @@ impl Optimizer<'_> {
}, },
_ => false, _ => false,
}) { }) {
return false;
}
if idents_used_by(&*body)
.iter()
.any(|v| v.0 == js_word!("arguments"))
{
return false;
}
true
}
fn inline_fn_like(
&mut self,
params: &[Ident],
body: &mut BlockStmt,
args: &mut [ExprOrSpread],
) -> Option<Expr> {
if !self.can_inline_fn_like(&params, &*body) {
return None; return None;
} }
self.changed = true;
log::debug!("inline: Inling an iife");
let mut exprs = vec![]; let mut exprs = vec![];
for stmt in body.stmts.take() {
{
let vars = params
.iter()
.cloned()
.map(BindingIdent::from)
.map(Pat::Ident)
.map(|name| VarDeclarator {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
name,
init: Default::default(),
definite: Default::default(),
})
.collect::<Vec<_>>();
if !vars.is_empty() {
self.prepend_stmts.push(Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: vars,
})));
}
}
for (idx, param) in params.iter().enumerate() {
if let Some(arg) = args.get_mut(idx) {
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
op: op!("="),
left: PatOrExpr::Pat(Box::new(Pat::Ident(param.clone().into()))),
right: arg.expr.take(),
})));
}
}
if args.len() > params.len() {
for arg in &mut args[params.len()..] {
exprs.push(arg.expr.take());
}
}
for mut stmt in body.stmts.take() {
match stmt { match stmt {
Stmt::Decl(Decl::Var(ref mut var)) => {
for decl in &mut var.decls {
if decl.init.is_some() {
exprs.push(Box::new(Expr::Assign(AssignExpr {
span: DUMMY_SP.apply_mark(self.marks.non_top_level),
op: op!("="),
left: PatOrExpr::Pat(Box::new(decl.name.clone())),
right: decl.init.take().unwrap(),
})))
}
}
self.prepend_stmts.push(stmt);
}
Stmt::Expr(stmt) => { Stmt::Expr(stmt) => {
exprs.push(stmt.expr); exprs.push(stmt.expr);
} }
@ -513,29 +737,3 @@ impl Optimizer<'_> {
} }
} }
} }
// We can't remove a function call if a parameter is declared by function and
// the body of the function uses it.
fn is_param_used_by_body<P, B>(params: &P, body: &B) -> bool
where
P: for<'any> VisitWith<DestructuringFinder<'any, Id>>,
B: VisitWith<IdentUsageCollector>,
{
let declared: Vec<Id> = find_ids(params);
let used = idents_used_by(body);
for id in declared {
if used.contains(&id) {
return true;
}
}
for (sym, _) in used {
if sym == js_word!("arguments") {
return true;
}
}
false
}

View File

@ -1,9 +1,13 @@
use crate::compress::optimize::util::class_has_side_effect; use crate::compress::optimize::util::class_has_side_effect;
use crate::compress::optimize::util::is_valid_for_lhs; use crate::compress::optimize::util::is_valid_for_lhs;
use crate::debug::dump;
use crate::util::has_mark;
use crate::util::idents_used_by;
use super::Optimizer; use super::Optimizer;
use swc_atoms::js_word; use swc_atoms::js_word;
use swc_common::Spanned; use swc_common::Spanned;
use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
@ -21,9 +25,18 @@ impl Optimizer<'_> {
None => return, None => return,
}; };
let should_preserve = (!self.options.top_level() && self.options.top_retain.is_empty()) let should_preserve = !has_mark(var.span, self.marks.non_top_level)
&& (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level(); && self.ctx.in_top_level();
if cfg!(feature = "debug") {
log::trace!(
"inline: store_var_for_inining({}, should_preserve = {:?})",
dump(&var.name),
should_preserve
);
}
if self if self
.data .data
.as_ref() .as_ref()
@ -57,13 +70,34 @@ impl Optimizer<'_> {
} }
if should_preserve && usage.var_kind != Some(VarDeclKind::Const) { if should_preserve && usage.var_kind != Some(VarDeclKind::Const) {
if cfg!(feature = "debug") {
log::trace!(
"inline: Preserving non-const variable `{}` because it's top-level",
dump(&var.name)
);
}
return; return;
} }
if usage.cond_init || usage.used_above_decl { if usage.cond_init || usage.used_above_decl {
if cfg!(feature = "debug") {
log::trace!("inline: [x] It's cond init or used before decl",);
}
return; return;
} }
if !usage.is_fn_local {
match &**init {
Expr::Lit(..) => {}
_ => {
if cfg!(feature = "debug") {
log::trace!("inline: [x] It's not fn-local");
}
return;
}
}
}
if !usage.reassigned { if !usage.reassigned {
match &**init { match &**init {
Expr::Fn(..) | Expr::Arrow(..) => { Expr::Fn(..) | Expr::Arrow(..) => {
@ -95,14 +129,12 @@ impl Optimizer<'_> {
&& (!usage.mutated || (usage.assign_count == 0 && !usage.reassigned)) && (!usage.mutated || (usage.assign_count == 0 && !usage.reassigned))
&& match &**init { && match &**init {
Expr::Lit(lit) => match lit { Expr::Lit(lit) => match lit {
Lit::Str(_) Lit::Str(_) => true,
| Lit::Bool(_) Lit::Bool(_) | Lit::Null(_) | Lit::Num(_) | Lit::BigInt(_) => true,
| Lit::Null(_)
| Lit::Num(_)
| Lit::BigInt(_) => true,
Lit::Regex(_) => self.options.unsafe_regexp, Lit::Regex(_) => self.options.unsafe_regexp,
_ => false, _ => false,
}, },
Expr::This(..) => usage.is_fn_local,
Expr::Arrow(arr) => is_arrow_simple_enough(arr), Expr::Arrow(arr) => is_arrow_simple_enough(arr),
_ => false, _ => false,
} }
@ -114,7 +146,7 @@ impl Optimizer<'_> {
_ => true, _ => true,
} }
{ {
log::trace!( log::debug!(
"inline: Decided to inline '{}{:?}' because it's simple", "inline: Decided to inline '{}{:?}' because it's simple",
i.id.sym, i.id.sym,
i.id.span.ctxt i.id.span.ctxt
@ -122,7 +154,7 @@ impl Optimizer<'_> {
self.lits.insert(i.to_id(), init.take()); self.lits.insert(i.to_id(), init.take());
} else { } else {
log::trace!( log::debug!(
"inline: Decided to copy '{}{:?}' because it's simple", "inline: Decided to copy '{}{:?}' because it's simple",
i.id.sym, i.id.sym,
i.id.span.ctxt i.id.span.ctxt
@ -133,6 +165,13 @@ impl Optimizer<'_> {
return; return;
} }
if self.ctx.inline_as_assignment {
if cfg!(feature = "debug") {
log::trace!("inline: [x] inline_as_assignment is true");
}
return;
}
// Single use => inlined // Single use => inlined
if is_inline_enabled if is_inline_enabled
&& !should_preserve && !should_preserve
@ -158,7 +197,11 @@ impl Optimizer<'_> {
_ => {} _ => {}
} }
match &**init { match &**init {
Expr::Lit(Lit::Regex(..)) => return, Expr::Lit(Lit::Regex(..)) => {
if !usage.is_fn_local || usage.used_in_loop {
return;
}
}
Expr::This(..) => { Expr::This(..) => {
// Don't inline this if it passes function boundaries. // Don't inline this if it passes function boundaries.
@ -170,17 +213,20 @@ impl Optimizer<'_> {
_ => {} _ => {}
} }
if init.may_have_side_effects() { if usage.used_in_loop {
if !self match &**init {
.vars_accessible_without_side_effect Expr::Lit(..) | Expr::Ident(..) | Expr::Fn(..) => {}
.contains(&i.to_id()) _ => {
{
// TODO: Inline partially
return; return;
} }
} }
}
log::trace!( if init.may_have_side_effects() {
return;
}
log::debug!(
"inline: Decided to inline '{}{:?}' because it's used only once", "inline: Decided to inline '{}{:?}' because it's used only once",
i.id.sym, i.id.sym,
i.id.span.ctxt i.id.span.ctxt
@ -210,6 +256,10 @@ impl Optimizer<'_> {
_ => {} _ => {}
}, },
Stmt::Try(TryStmt { block, .. }) => {
return self.is_fn_body_simple_enough_to_inline(block)
}
_ => {} _ => {}
} }
} }
@ -234,7 +284,7 @@ impl Optimizer<'_> {
.and_then(|data| data.vars.get(&i.to_id())) .and_then(|data| data.vars.get(&i.to_id()))
{ {
if !usage.reassigned { if !usage.reassigned {
log::trace!("typeofs: Storing typeof `{}{:?}`", i.sym, i.span.ctxt); log::debug!("typeofs: Storing typeof `{}{:?}`", i.sym, i.span.ctxt);
match &*decl { match &*decl {
Decl::Fn(..) => { Decl::Fn(..) => {
self.typeofs.insert(i.to_id(), js_word!("function")); self.typeofs.insert(i.to_id(), js_word!("function"));
@ -286,6 +336,10 @@ impl Optimizer<'_> {
return; return;
} }
if self.ctx.inline_as_assignment {
return;
}
if let Some(usage) = self if let Some(usage) = self
.data .data
.as_ref() .as_ref()
@ -295,7 +349,7 @@ impl Optimizer<'_> {
return; return;
} }
if usage.reassigned { if usage.reassigned || usage.inline_prevented {
return; return;
} }
@ -305,14 +359,14 @@ impl Optimizer<'_> {
match &f.function.body { match &f.function.body {
Some(body) => { Some(body) => {
if self.is_fn_body_simple_enough_to_inline(body) { if self.is_fn_body_simple_enough_to_inline(body) {
log::trace!( log::debug!(
"inline: Decided to inline function '{}{:?}' as it's very \ "inline: Decided to inline function '{}{:?}' as it's very \
simple", simple",
f.ident.sym, f.ident.sym,
f.ident.span.ctxt f.ident.span.ctxt
); );
self.lits.insert( self.vars_for_inlining.insert(
i.to_id(), i.to_id(),
match decl { match decl {
Decl::Fn(f) => Box::new(Expr::Fn(FnExpr { Decl::Fn(f) => Box::new(Expr::Fn(FnExpr {
@ -351,14 +405,14 @@ impl Optimizer<'_> {
self.changed = true; self.changed = true;
match &decl { match &decl {
Decl::Class(c) => { Decl::Class(c) => {
log::trace!( log::debug!(
"inline: Decided to inline class '{}{:?}' as it's used only once", "inline: Decided to inline class '{}{:?}' as it's used only once",
c.ident.sym, c.ident.sym,
c.ident.span.ctxt c.ident.span.ctxt
); );
} }
Decl::Fn(f) => { Decl::Fn(f) => {
log::trace!( log::debug!(
"inline: Decided to inline function '{}{:?}' as it's used only once", "inline: Decided to inline function '{}{:?}' as it's used only once",
f.ident.sym, f.ident.sym,
f.ident.span.ctxt f.ident.span.ctxt
@ -366,6 +420,7 @@ impl Optimizer<'_> {
} }
_ => {} _ => {}
} }
self.vars_for_inlining.insert( self.vars_for_inlining.insert(
i.to_id(), i.to_id(),
match decl.take() { match decl.take() {
@ -399,33 +454,92 @@ impl Optimizer<'_> {
return; return;
} }
// //
if let Some(value) = self.lits.get(&i.to_id()).cloned() { if let Some(value) = self
.lits
.get(&i.to_id())
.and_then(|v| {
// Prevent infinite recursion.
let ids = idents_used_by(&**v);
if ids.contains(&i.to_id()) {
None
} else {
Some(v)
}
})
.cloned()
{
match &*value { match &*value {
Expr::Lit(Lit::Num(..)) => { Expr::Lit(Lit::Num(..)) => {
if self.ctx.in_lhs_of_assign { if self.ctx.in_lhs_of_assign {
return; return;
} }
} }
Expr::Member(..) => {
if self.ctx.executed_multiple_time {
return;
}
}
_ => {} _ => {}
} }
self.changed = true; self.changed = true;
log::trace!("inline: Replacing a variable with cheap expression"); log::debug!("inline: Replacing a variable with cheap expression");
*e = *value; *e = *value;
} else if let Some(value) = self.vars_for_inlining.get(&i.to_id()) { return;
}
if let Some(value) = self.vars_for_inlining.get(&i.to_id()) {
if self.ctx.is_exact_lhs_of_assign && !is_valid_for_lhs(&value) { if self.ctx.is_exact_lhs_of_assign && !is_valid_for_lhs(&value) {
return; return;
} }
match &**value {
Expr::Member(..) => {
if self.ctx.executed_multiple_time {
return;
}
}
_ => {}
}
}
if self.ctx.inline_as_assignment {
if let Some(value) = self.vars_for_inlining.remove(&i.to_id()) {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"inline: Inlining '{}{:?}' using assignment",
i.sym,
i.span.ctxt
);
*e = Expr::Assign(AssignExpr {
span: DUMMY_SP,
op: op!("="),
left: PatOrExpr::Pat(Box::new(Pat::Ident(i.clone().into()))),
right: value,
});
if cfg!(feature = "debug") {
log::trace!("inline: [Change] {}", dump(&*e))
}
return;
}
}
if let Some(value) = self.vars_for_inlining.get(&i.to_id()) {
self.changed = true;
log::debug!(
"inline: Replacing '{}{:?}' with an expression", "inline: Replacing '{}{:?}' with an expression",
i.sym, i.sym,
i.span.ctxt i.span.ctxt
); );
*e = *value.clone(); *e = *value.clone();
if cfg!(feature = "debug") {
log::trace!("inline: [Change] {}", dump(&*e))
}
} }
} }
_ => {} _ => {}

View File

@ -46,7 +46,7 @@ impl Optimizer<'_> {
} }
} }
log::trace!("join_vars: Joining variables"); log::debug!("join_vars: Joining variables");
self.changed = true; self.changed = true;
let mut cur: Option<VarDecl> = None; let mut cur: Option<VarDecl> = None;

View File

@ -1,5 +1,6 @@
use crate::compress::optimize::unused::UnreachableHandler; use crate::compress::optimize::unused::UnreachableHandler;
use crate::compress::optimize::Optimizer; use crate::compress::optimize::Optimizer;
use swc_common::Spanned;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
@ -19,7 +20,7 @@ impl Optimizer<'_> {
match s { match s {
Stmt::While(stmt) => { Stmt::While(stmt) => {
self.changed = true; self.changed = true;
log::trace!("loops: Converting a while loop to a for loop"); log::debug!("loops: Converting a while loop to a for loop");
*s = Stmt::For(ForStmt { *s = Stmt::For(ForStmt {
span: stmt.span, span: stmt.span,
init: None, init: None,
@ -32,7 +33,7 @@ impl Optimizer<'_> {
let val = stmt.test.as_pure_bool(); let val = stmt.test.as_pure_bool();
if let Known(true) = val { if let Known(true) = val {
self.changed = true; self.changed = true;
log::trace!("loops: Converting an always-true do-while loop to a for loop"); log::debug!("loops: Converting an always-true do-while loop to a for loop");
*s = Stmt::For(ForStmt { *s = Stmt::For(ForStmt {
span: stmt.span, span: stmt.span,
@ -79,7 +80,7 @@ impl Optimizer<'_> {
} }
self.changed = true; self.changed = true;
log::trace!("loops: Removing a for loop with instant break"); log::debug!("loops: Removing a for loop with instant break");
self.prepend_stmts self.prepend_stmts
.extend(f.init.take().map(|init| match init { .extend(f.init.take().map(|init| match init {
VarDeclOrExpr::VarDecl(var) => Stmt::Decl(Decl::Var(var)), VarDeclOrExpr::VarDecl(var) => Stmt::Decl(Decl::Var(var)),
@ -97,6 +98,70 @@ impl Optimizer<'_> {
*s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }) *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP })
} }
/// # Input
///
/// ```js
/// for(; size--;)
/// if (!(result = eq(a[size], b[size], aStack, bStack)))
/// break;
/// ```
///
///
/// # Output
///
/// ```js
/// for (; size-- && (result = eq(a[size], b[size], aStack, bStack)););
/// ```
pub(super) fn merge_for_if_break(&mut self, s: &mut ForStmt) {
match &mut *s.body {
Stmt::If(IfStmt {
test,
cons,
alt: None,
..
}) => {
match &**cons {
Stmt::Break(BreakStmt { label: None, .. }) => {
// We only care about instant breaks.
//
// Note: As the minifier of swc is very fast, we don't
// care about block statements with a single break as a
// body.
//
// If it's optimizable, other pass for if statements
// will remove block and with the next pass we can apply
// this pass.
self.changed = true;
log::debug!("loops: Compressing for-if-break into a for statement");
// We negate because this `test` is used as a condition for `break`.
self.negate(test);
match s.test.take() {
Some(left) => {
s.test = Some(Box::new(Expr::Bin(BinExpr {
span: s.test.span(),
op: op!("&&"),
left,
right: test.take(),
})));
}
None => {
s.test = Some(test.take());
}
}
// Remove body
s.body.take();
}
_ => {}
}
}
_ => {}
}
}
/// ///
/// - `while(false) { var a; foo() }` => `var a;` /// - `while(false) { var a; foo() }` => `var a;`
pub(super) fn optiimze_loops_if_cond_is_false(&mut self, stmt: &mut Stmt) { pub(super) fn optiimze_loops_if_cond_is_false(&mut self, stmt: &mut Stmt) {
@ -112,7 +177,7 @@ impl Optimizer<'_> {
let changed = UnreachableHandler::preserve_vars(stmt); let changed = UnreachableHandler::preserve_vars(stmt);
self.changed |= changed; self.changed |= changed;
if changed { if changed {
log::trace!( log::debug!(
"loops: Removing unreachable while statement without side effects" "loops: Removing unreachable while statement without side effects"
); );
} }
@ -120,7 +185,7 @@ impl Optimizer<'_> {
let changed = UnreachableHandler::preserve_vars(&mut w.body); let changed = UnreachableHandler::preserve_vars(&mut w.body);
self.changed |= changed; self.changed |= changed;
if changed { if changed {
log::trace!("loops: Removing unreachable body of a while statement"); log::debug!("loops: Removing unreachable body of a while statement");
} }
} }
} }
@ -132,7 +197,7 @@ impl Optimizer<'_> {
let changed = UnreachableHandler::preserve_vars(&mut f.body); let changed = UnreachableHandler::preserve_vars(&mut f.body);
self.changed |= changed; self.changed |= changed;
if changed { if changed {
log::trace!("loops: Removing unreachable body of a for statement"); log::debug!("loops: Removing unreachable body of a for statement");
} }
self.changed |= f.init.is_some() | f.update.is_some(); self.changed |= f.init.is_some() | f.update.is_some();
@ -154,7 +219,7 @@ impl Optimizer<'_> {
} else if let Known(true) = val { } else if let Known(true) = val {
if purity.is_pure() { if purity.is_pure() {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"loops: Remving `test` part of a for stmt as it's always true" "loops: Remving `test` part of a for stmt as it's always true"
); );
f.test = None; f.test = None;
@ -190,7 +255,7 @@ impl Optimizer<'_> {
} else { } else {
s.init = None; s.init = None;
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"loops: Removed side-effect-free expressions in `init` of a for stmt" "loops: Removed side-effect-free expressions in `init` of a for stmt"
); );
} }

View File

@ -0,0 +1,38 @@
use super::Optimizer;
use swc_ecma_ast::*;
use unicode_xid::UnicodeXID;
impl Optimizer<'_> {
pub(super) fn optimize_prop_name(&mut self, name: &mut PropName) {
match name {
PropName::Str(s) => {
if s.value.is_reserved() || s.value.is_reserved_in_es3() {
return;
}
if s.value.starts_with(|c: char| c.is_xid_start())
&& s.value.chars().all(|c: char| c.is_xid_continue())
&& !s.value.contains("𝒶")
{
self.changed = true;
log::debug!("misc: Optimizing string property name");
*name = PropName::Ident(Ident {
span: s.span,
sym: s.value.clone(),
optional: false,
});
return;
}
}
_ => {}
}
}
pub(super) fn remove_useless_return(&mut self, stmts: &mut Vec<Stmt>) {
if let Some(Stmt::Return(ReturnStmt { arg: None, .. })) = stmts.last() {
self.changed = true;
log::debug!("misc: Removing useless return");
stmts.pop();
}
}
}

View File

@ -1,10 +1,10 @@
use crate::analyzer::analyze;
use crate::analyzer::ProgramData; use crate::analyzer::ProgramData;
use crate::analyzer::UsageAnalyzer; use crate::analyzer::UsageAnalyzer;
use crate::marks::Marks;
use crate::option::CompressOptions; use crate::option::CompressOptions;
use crate::util::contains_leaping_yield; use crate::util::contains_leaping_yield;
use crate::util::MoudleItemExt;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use fxhash::FxHashSet;
use retain_mut::RetainMut; use retain_mut::RetainMut;
use std::fmt::Write; use std::fmt::Write;
use std::mem::take; use std::mem::take;
@ -13,7 +13,9 @@ use swc_atoms::JsWord;
use swc_common::comments::Comments; use swc_common::comments::Comments;
use swc_common::iter::IdentifyLast; use swc_common::iter::IdentifyLast;
use swc_common::pass::Repeated; use swc_common::pass::Repeated;
use swc_common::sync::Lrc;
use swc_common::Mark; use swc_common::Mark;
use swc_common::SourceMap;
use swc_common::Spanned; use swc_common::Spanned;
use swc_common::SyntaxContext; use swc_common::SyntaxContext;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
@ -24,6 +26,8 @@ use swc_ecma_utils::undefined;
use swc_ecma_utils::ExprExt; use swc_ecma_utils::ExprExt;
use swc_ecma_utils::ExprFactory; use swc_ecma_utils::ExprFactory;
use swc_ecma_utils::Id; use swc_ecma_utils::Id;
use swc_ecma_utils::IsEmpty;
use swc_ecma_utils::ModuleItemLike;
use swc_ecma_utils::StmtLike; use swc_ecma_utils::StmtLike;
use swc_ecma_utils::Type; use swc_ecma_utils::Type;
use swc_ecma_utils::Value; use swc_ecma_utils::Value;
@ -41,12 +45,14 @@ mod computed_props;
mod conditionals; mod conditionals;
mod dead_code; mod dead_code;
mod evaluate; mod evaluate;
mod fns;
mod hoist_props; mod hoist_props;
mod if_return; mod if_return;
mod iife; mod iife;
mod inline; mod inline;
mod join_vars; mod join_vars;
mod loops; mod loops;
mod misc;
mod numbers; mod numbers;
mod ops; mod ops;
mod properties; mod properties;
@ -61,8 +67,11 @@ const DISABLE_BUGGY_PASSES: bool = true;
/// This pass is simillar to `node.optimize` of terser. /// This pass is simillar to `node.optimize` of terser.
pub(super) fn optimizer<'a>( pub(super) fn optimizer<'a>(
options: CompressOptions, cm: Lrc<SourceMap>,
marks: Marks,
options: &'a CompressOptions,
comments: Option<&'a dyn Comments>, comments: Option<&'a dyn Comments>,
data: &'a ProgramData,
) -> impl 'a + VisitMut + Repeated { ) -> impl 'a + VisitMut + Repeated {
assert!( assert!(
options.top_retain.iter().all(|s| s.trim() != ""), options.top_retain.iter().all(|s| s.trim() != ""),
@ -72,6 +81,8 @@ pub(super) fn optimizer<'a>(
let done = Mark::fresh(Mark::root()); let done = Mark::fresh(Mark::root());
let done_ctxt = SyntaxContext::empty().apply_mark(done); let done_ctxt = SyntaxContext::empty().apply_mark(done);
Optimizer { Optimizer {
cm,
marks,
comments, comments,
changed: false, changed: false,
options, options,
@ -83,11 +94,10 @@ pub(super) fn optimizer<'a>(
simple_props: Default::default(), simple_props: Default::default(),
_simple_array_values: Default::default(), _simple_array_values: Default::default(),
typeofs: Default::default(), typeofs: Default::default(),
data: Default::default(), data: Some(data),
ctx: Default::default(), ctx: Default::default(),
done, done,
done_ctxt, done_ctxt,
vars_accessible_without_side_effect: Default::default(),
label: Default::default(), label: Default::default(),
} }
} }
@ -100,6 +110,16 @@ struct Ctx {
/// `true` if the [VarDecl] has const annotation. /// `true` if the [VarDecl] has const annotation.
has_const_ann: bool, has_const_ann: bool,
in_bool_ctx: bool,
in_asm: bool,
is_callee: bool,
in_call_arg: bool,
/// If this is `true`, the first usage will be inlined as an assignment.
inline_as_assignment: bool,
var_kind: Option<VarDeclKind>, var_kind: Option<VarDeclKind>,
/// `true` if we should not inline values. /// `true` if we should not inline values.
@ -130,6 +150,8 @@ struct Ctx {
/// `true` while handling inner statements of a labelled statement. /// `true` while handling inner statements of a labelled statement.
stmt_lablled: bool, stmt_lablled: bool,
dont_use_negated_iife: bool,
/// `true` while handling top-level export decls. /// `true` while handling top-level export decls.
is_exported: bool, is_exported: bool,
@ -172,10 +194,14 @@ impl Ctx {
} }
struct Optimizer<'a> { struct Optimizer<'a> {
cm: Lrc<SourceMap>,
marks: Marks,
comments: Option<&'a dyn Comments>, comments: Option<&'a dyn Comments>,
changed: bool, changed: bool,
options: CompressOptions, options: &'a CompressOptions,
/// Statements prepended to the current statement. /// Statements prepended to the current statement.
prepend_stmts: Vec<Stmt>, prepend_stmts: Vec<Stmt>,
@ -196,14 +222,12 @@ struct Optimizer<'a> {
/// ///
/// This is calculated multiple time, but only once per one /// This is calculated multiple time, but only once per one
/// `visit_mut_module`. /// `visit_mut_module`.
data: Option<ProgramData>, data: Option<&'a ProgramData>,
ctx: Ctx, ctx: Ctx,
/// In future: This will be used to `mark` node as done. /// In future: This will be used to `mark` node as done.
done: Mark, done: Mark,
done_ctxt: SyntaxContext, done_ctxt: SyntaxContext,
vars_accessible_without_side_effect: FxHashSet<Id>,
/// Closest label. /// Closest label.
label: Option<Id>, label: Option<Id>,
} }
@ -221,15 +245,22 @@ impl Repeated for Optimizer<'_> {
impl Optimizer<'_> { impl Optimizer<'_> {
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>) fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
where where
T: StmtLike + VisitMutWith<Self>, T: StmtLike + ModuleItemLike + MoudleItemExt + VisitMutWith<Self>,
Vec<T>: VisitMutWith<Self> + VisitWith<UsageAnalyzer>, Vec<T>: VisitMutWith<Self>
+ VisitWith<UsageAnalyzer>
+ VisitWith<self::collapse_vars::VarWithOutInitCounter>
+ VisitMutWith<self::collapse_vars::VarPrepender>
+ VisitMutWith<self::collapse_vars::VarMover>,
{ {
match self.data { match self.data {
Some(..) => {} Some(..) => {}
None => { None => {
self.data = Some(analyze(stmts)); unreachable!()
} }
} }
let mut use_asm = false;
let prepend_stmts = self.prepend_stmts.take();
let append_stmts = self.append_stmts.take();
{ {
let mut child_ctx = Ctx { ..self.ctx }; let mut child_ctx = Ctx { ..self.ctx };
@ -245,6 +276,12 @@ impl Optimizer<'_> {
if v.value == *"use strict" && !v.has_escape { if v.value == *"use strict" && !v.has_escape {
child_ctx.in_strict = true; child_ctx.in_strict = true;
} }
if v.value == *"use asm" && !v.has_escape {
child_ctx.in_asm = true;
self.ctx.in_asm = true;
use_asm = true;
}
} }
_ => {} _ => {}
}, },
@ -271,17 +308,33 @@ impl Optimizer<'_> {
*stmts = new; *stmts = new;
} }
self.ctx.in_asm |= use_asm;
self.drop_useless_blocks(stmts);
self.reorder_stmts(stmts);
self.merge_simillar_ifs(stmts); self.merge_simillar_ifs(stmts);
self.join_vars(stmts); self.join_vars(stmts);
self.make_sequences(stmts); self.make_sequences(stmts);
self.collapse_consequtive_vars(stmts); self.collapse_vars_without_init(stmts);
self.drop_else_token(stmts);
self.break_assignments_in_seqs(stmts);
stmts.extend(self.append_stmts.drain(..).map(T::from_stmt));
stmts.retain(|stmt| match stmt.as_stmt() { stmts.retain(|stmt| match stmt.as_stmt() {
Some(Stmt::Empty(..)) => false, Some(Stmt::Empty(..)) => false,
_ => true, _ => true,
}) });
debug_assert_eq!(self.prepend_stmts, vec![]);
self.prepend_stmts = prepend_stmts;
self.append_stmts = append_stmts;
} }
/// `a = a + 1` => `a += 1`. /// `a = a + 1` => `a += 1`.
@ -310,6 +363,15 @@ impl Optimizer<'_> {
_ => return, _ => return,
}; };
// Don't break code for old browsers.
match op {
BinaryOp::LogicalOr => return,
BinaryOp::LogicalAnd => return,
BinaryOp::Exp => return,
BinaryOp::NullishCoalescing => return,
_ => {}
}
let op = match op { let op = match op {
BinaryOp::In | BinaryOp::InstanceOf => return, BinaryOp::In | BinaryOp::InstanceOf => return,
@ -540,7 +602,7 @@ impl Optimizer<'_> {
} }
} }
log::trace!("Compressing array.join()"); log::debug!("Compressing array.join()");
self.changed = true; self.changed = true;
*e = Expr::Lit(Lit::Str(Str { *e = Expr::Lit(Lit::Str(Str {
@ -580,7 +642,7 @@ impl Optimizer<'_> {
match lit { match lit {
Lit::Bool(v) => { Lit::Bool(v) => {
self.changed = true; self.changed = true;
log::trace!("Compressing boolean literal"); log::debug!("Compressing boolean literal");
*e = Expr::Unary(UnaryExpr { *e = Expr::Unary(UnaryExpr {
span: v.span, span: v.span,
op: op!("!"), op: op!("!"),
@ -618,15 +680,19 @@ impl Optimizer<'_> {
/// Returns [None] if expression is side-effect-free. /// Returns [None] if expression is side-effect-free.
/// If an expression has a side effect, only side effects are returned. /// If an expression has a side effect, only side effects are returned.
fn ignore_return_value(&mut self, e: &mut Expr) -> Option<Expr> { fn ignore_return_value(&mut self, e: &mut Expr) -> Option<Expr> {
self.optimize_bang_within_logical_ops(e, true);
self.compress_cond_to_logical_ignoring_return_value(e);
match e { match e {
Expr::Ident(..) | Expr::This(_) | Expr::Invalid(_) | Expr::Lit(..) => { Expr::Ident(..) | Expr::This(_) | Expr::Invalid(_) | Expr::Lit(..) => {
log::trace!("ignore_return_value: Dropping unused expr"); log::debug!("ignore_return_value: Dropping unused expr");
self.changed = true; self.changed = true;
return None; return None;
} }
// Function expression cannot have a side effect. // Function expression cannot have a side effect.
Expr::Fn(_) => { Expr::Fn(_) => {
log::trace!( log::debug!(
"ignore_return_value: Dropping unused fn expr as it does not have any side \ "ignore_return_value: Dropping unused fn expr as it does not have any side \
effect" effect"
); );
@ -692,7 +758,12 @@ impl Optimizer<'_> {
right, right,
.. ..
}) => { }) => {
let new_r = self.ignore_return_value(right); let ctx = Ctx {
dont_use_negated_iife: self.ctx.dont_use_negated_iife
|| self.options.side_effects,
..self.ctx
};
let new_r = self.with_ctx(ctx).ignore_return_value(right);
match new_r { match new_r {
Some(r) => { Some(r) => {
@ -763,15 +834,36 @@ impl Optimizer<'_> {
_ => false, _ => false,
} && args.is_empty() => } && args.is_empty() =>
{ {
log::trace!("ignore_return_value: Dropping a pure call"); log::debug!("ignore_return_value: Dropping a pure call");
self.changed = true; self.changed = true;
return None; return None;
} }
Expr::Call(CallExpr {
callee: ExprOrSuper::Expr(callee),
args,
..
}) => {
if args.is_empty() {
match &mut **callee {
Expr::Fn(f) => {
if f.function.body.is_none()
|| f.function.body.as_ref().unwrap().is_empty()
{
return None;
}
}
_ => {}
}
}
return Some(e.take());
}
Expr::MetaProp(_) Expr::MetaProp(_)
| Expr::Await(_) | Expr::Await(_)
| Expr::Call(_)
| Expr::New(..) | Expr::New(..)
| Expr::Call(..)
| Expr::Yield(_) | Expr::Yield(_)
| Expr::Assign(_) | Expr::Assign(_)
| Expr::PrivateName(_) | Expr::PrivateName(_)
@ -796,6 +888,10 @@ impl Optimizer<'_> {
if usage.var_kind.is_none() { if usage.var_kind.is_none() {
return None; return None;
} }
if !usage.reassigned && usage.no_side_effect_for_member_access {
return None;
}
} }
} }
_ => {} _ => {}
@ -818,7 +914,7 @@ impl Optimizer<'_> {
Expr::Array(arr) => { Expr::Array(arr) => {
let mut exprs = vec![]; let mut exprs = vec![];
self.changed = true; self.changed = true;
log::trace!("ignore_return_value: Inverting an array literal"); log::debug!("ignore_return_value: Inverting an array literal");
for elem in arr.elems.take() { for elem in arr.elems.take() {
match elem { match elem {
Some(mut elem) => { Some(mut elem) => {
@ -841,7 +937,7 @@ impl Optimizer<'_> {
Expr::Object(obj) => { Expr::Object(obj) => {
let mut exprs = vec![]; let mut exprs = vec![];
self.changed = true; self.changed = true;
log::trace!("ignore_return_value: Inverting an object literal"); log::debug!("ignore_return_value: Inverting an object literal");
for prop in obj.props.take() { for prop in obj.props.take() {
match prop { match prop {
PropOrSpread::Spread(mut e) => { PropOrSpread::Spread(mut e) => {
@ -898,6 +994,7 @@ impl Optimizer<'_> {
}) if (self.options.negate_iife }) if (self.options.negate_iife
|| self.options.reduce_vars || self.options.reduce_vars
|| self.options.side_effects) || self.options.side_effects)
&& !self.ctx.dont_use_negated_iife
&& match &**arg { && match &**arg {
Expr::Call(arg) => match &arg.callee { Expr::Call(arg) => match &arg.callee {
ExprOrSuper::Expr(callee) => match &**callee { ExprOrSuper::Expr(callee) => match &**callee {
@ -909,14 +1006,14 @@ impl Optimizer<'_> {
_ => false, _ => false,
} => } =>
{ {
log::trace!("ignore_return_value: Preserving negated iife"); log::debug!("ignore_return_value: Preserving negated iife");
return Some(e.take()); return Some(e.take());
} }
// `delete` is handled above // `delete` is handled above
Expr::Unary(expr) => { Expr::Unary(expr) => {
self.changed = true; self.changed = true;
log::trace!("ignore_return_value: Reducing unary ({})", expr.op); log::debug!("ignore_return_value: Reducing unary ({})", expr.op);
return self.ignore_return_value(&mut expr.arg); return self.ignore_return_value(&mut expr.arg);
} }
@ -934,10 +1031,24 @@ impl Optimizer<'_> {
} }
Expr::Cond(cond) => { Expr::Cond(cond) => {
self.restore_negated_iife(cond);
let ctx = Ctx {
dont_use_negated_iife: self.ctx.dont_use_negated_iife
|| self.options.side_effects,
..self.ctx
};
let cons_span = cond.cons.span(); let cons_span = cond.cons.span();
let alt_span = cond.alt.span(); let alt_span = cond.alt.span();
let cons = self.ignore_return_value(&mut cond.cons).map(Box::new); let cons = self
let alt = self.ignore_return_value(&mut cond.alt).map(Box::new); .with_ctx(ctx)
.ignore_return_value(&mut cond.cons)
.map(Box::new);
let alt = self
.with_ctx(ctx)
.ignore_return_value(&mut cond.alt)
.map(Box::new);
// TODO: Remove if test is side effect free. // TODO: Remove if test is side effect free.
@ -973,7 +1084,11 @@ impl Optimizer<'_> {
if idx == 0 && self.ctx.is_this_aware_callee && is_injected_zero { if idx == 0 && self.ctx.is_this_aware_callee && is_injected_zero {
return Some(*expr.take()); return Some(*expr.take());
} }
self.ignore_return_value(&mut **expr) let ctx = Ctx {
dont_use_negated_iife: idx != 0,
..self.ctx
};
self.with_ctx(ctx).ignore_return_value(&mut **expr)
}) })
.map(Box::new) .map(Box::new)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -998,6 +1113,10 @@ impl Optimizer<'_> {
} }
} }
if exprs.is_empty() {
return None;
}
return Some(Expr::Seq(SeqExpr { return Some(Expr::Seq(SeqExpr {
span: seq.span, span: seq.span,
exprs, exprs,
@ -1082,7 +1201,7 @@ impl Optimizer<'_> {
}) })
.unwrap_or(js_word!("")); .unwrap_or(js_word!(""));
log::trace!("Converting call to RegExp into a regexp literal"); log::debug!("Converting call to RegExp into a regexp literal");
self.changed = true; self.changed = true;
*e = Expr::Lit(Lit::Regex(Regex { *e = Expr::Lit(Lit::Regex(Regex {
span, span,
@ -1147,7 +1266,7 @@ impl Optimizer<'_> {
_ => false, _ => false,
}) { }) {
self.changed = true; self.changed = true;
log::trace!("Merging variable declarations"); log::debug!("Merging variable declarations");
let orig = take(stmts); let orig = take(stmts);
let mut new = Vec::with_capacity(orig.len()); let mut new = Vec::with_capacity(orig.len());
@ -1195,11 +1314,16 @@ impl Optimizer<'_> {
// Remove nested blocks // Remove nested blocks
if bs.stmts.len() == 1 { if bs.stmts.len() == 1 {
if let Stmt::Block(block) = &mut bs.stmts[0] { if let Stmt::Block(block) = &mut bs.stmts[0] {
log::trace!("optimizer: Removing nested block"); if block.stmts.iter().all(|stmt| match stmt {
Stmt::Decl(..) => false,
_ => true,
}) {
log::debug!("optimizer: Removing nested block");
self.changed = true; self.changed = true;
bs.stmts = block.stmts.take(); bs.stmts = block.stmts.take();
} }
} }
}
// Unwrap a block with only `var`s. // Unwrap a block with only `var`s.
// //
@ -1213,7 +1337,7 @@ impl Optimizer<'_> {
_ => false, _ => false,
}) })
{ {
log::trace!("optimizer: Unwrapping a block with variable statements"); log::debug!("optimizer: Unwrapping a block with variable statements");
self.changed = true; self.changed = true;
*s = bs.stmts[0].take(); *s = bs.stmts[0].take();
return; return;
@ -1223,7 +1347,7 @@ impl Optimizer<'_> {
if let Stmt::Block(block) = &stmt { if let Stmt::Block(block) = &stmt {
if block.stmts.is_empty() { if block.stmts.is_empty() {
self.changed = true; self.changed = true;
log::trace!("optimizer: Removing empty block"); log::debug!("optimizer: Removing empty block");
*stmt = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); *stmt = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
} }
} }
@ -1233,12 +1357,12 @@ impl Optimizer<'_> {
match &bs.stmts[0] { match &bs.stmts[0] {
Stmt::Expr(..) | Stmt::If(..) => { Stmt::Expr(..) | Stmt::If(..) => {
*s = bs.stmts[0].take(); *s = bs.stmts[0].take();
log::trace!("optimizer: Unwrapping block stmt"); log::debug!("optimizer: Unwrapping block stmt");
self.changed = true; self.changed = true;
} }
Stmt::Decl(Decl::Fn(..)) if !self.ctx.in_strict => { Stmt::Decl(Decl::Fn(..)) if !self.ctx.in_strict => {
*s = bs.stmts[0].take(); *s = bs.stmts[0].take();
log::trace!("optimizer: Unwrapping block stmt in non strcit mode"); log::debug!("optimizer: Unwrapping block stmt in non strcit mode");
self.changed = true; self.changed = true;
} }
_ => {} _ => {}
@ -1267,6 +1391,14 @@ impl Optimizer<'_> {
self.try_removing_block(&mut s.body, true); self.try_removing_block(&mut s.body, true);
} }
Stmt::For(s) => {
self.try_removing_block(&mut s.body, true);
}
Stmt::ForOf(s) => {
self.try_removing_block(&mut s.body, true);
}
_ => {} _ => {}
} }
@ -1303,7 +1435,7 @@ impl Optimizer<'_> {
match &mut *stmt.cons { match &mut *stmt.cons {
Stmt::Expr(cons) => { Stmt::Expr(cons) => {
self.changed = true; self.changed = true;
log::trace!("Converting if statement to a form `test && cons`"); log::debug!("Converting if statement to a form `test && cons`");
*s = Stmt::Expr(ExprStmt { *s = Stmt::Expr(ExprStmt {
span: stmt.span, span: stmt.span,
expr: Box::new(stmt.test.take().make_bin(op!("&&"), *cons.expr.take())), expr: Box::new(stmt.test.take().make_bin(op!("&&"), *cons.expr.take())),
@ -1345,8 +1477,6 @@ impl VisitMut for Optimizer<'_> {
self.compress_bin_assignment_to_left(e); self.compress_bin_assignment_to_left(e);
self.compress_bin_assignment_to_right(e); self.compress_bin_assignment_to_right(e);
self.vars_accessible_without_side_effect.clear();
} }
fn visit_mut_assign_pat_prop(&mut self, n: &mut AssignPatProp) { fn visit_mut_assign_pat_prop(&mut self, n: &mut AssignPatProp) {
@ -1363,7 +1493,22 @@ impl VisitMut for Optimizer<'_> {
} }
fn visit_mut_bin_expr(&mut self, n: &mut BinExpr) { fn visit_mut_bin_expr(&mut self, n: &mut BinExpr) {
n.visit_mut_children_with(self); {
let ctx = Ctx {
in_cond: self.ctx.in_cond
|| match n.op {
op!("&&") | op!("||") | op!("??") => true,
_ => false,
},
..self.ctx
};
n.visit_mut_children_with(&mut *self.with_ctx(ctx));
}
self.compress_typeof_undefined(n);
self.compress_comparsion_of_typeof(n);
self.optimize_bin_operator(n); self.optimize_bin_operator(n);
@ -1404,9 +1549,15 @@ impl VisitMut for Optimizer<'_> {
} }
fn visit_mut_call_expr(&mut self, e: &mut CallExpr) { fn visit_mut_call_expr(&mut self, e: &mut CallExpr) {
let is_this_undefined = match &e.callee {
ExprOrSuper::Super(_) => false,
ExprOrSuper::Expr(e) => e.is_ident(),
};
{ {
let ctx = Ctx { let ctx = Ctx {
is_this_aware_callee: match &e.callee { is_callee: true,
is_this_aware_callee: is_this_undefined
|| match &e.callee {
ExprOrSuper::Super(_) => false, ExprOrSuper::Super(_) => false,
ExprOrSuper::Expr(callee) => is_callee_this_aware(&callee), ExprOrSuper::Expr(callee) => is_callee_this_aware(&callee),
}, },
@ -1415,18 +1566,31 @@ impl VisitMut for Optimizer<'_> {
e.callee.visit_mut_with(&mut *self.with_ctx(ctx)); e.callee.visit_mut_with(&mut *self.with_ctx(ctx));
} }
match &e.callee { if is_this_undefined {
ExprOrSuper::Super(_) => {} match &mut e.callee {
ExprOrSuper::Expr(e) => { ExprOrSuper::Expr(callee) => match &mut **callee {
if e.may_have_side_effects() { Expr::Member(..) => {
self.vars_accessible_without_side_effect.clear(); let zero = Box::new(Expr::Lit(Lit::Num(Number {
span: DUMMY_SP,
value: 0.0,
})));
log::trace!("injecting zero to preserve `this` in call");
*callee = Box::new(Expr::Seq(SeqExpr {
span: callee.span(),
exprs: vec![zero, callee.take()],
}));
} }
_ => {}
},
_ => {}
} }
} }
{ {
let ctx = Ctx { let ctx = Ctx {
is_this_aware_callee: false, is_this_aware_callee: false,
in_call_arg: true,
..self.ctx ..self.ctx
}; };
// TODO: Prevent inline if callee is unknown. // TODO: Prevent inline if callee is unknown.
@ -1436,8 +1600,6 @@ impl VisitMut for Optimizer<'_> {
self.optimize_symbol_call_unsafely(e); self.optimize_symbol_call_unsafely(e);
self.inline_args_of_iife(e); self.inline_args_of_iife(e);
self.vars_accessible_without_side_effect.clear();
} }
fn visit_mut_class(&mut self, n: &mut Class) { fn visit_mut_class(&mut self, n: &mut Class) {
@ -1471,6 +1633,8 @@ impl VisitMut for Optimizer<'_> {
fn visit_mut_cond_expr(&mut self, n: &mut CondExpr) { fn visit_mut_cond_expr(&mut self, n: &mut CondExpr) {
n.visit_mut_children_with(self); n.visit_mut_children_with(self);
self.negate_cond_expr(n);
self.optimize_expr_in_bool_ctx(&mut n.test); self.optimize_expr_in_bool_ctx(&mut n.test);
} }
@ -1546,15 +1710,6 @@ impl VisitMut for Optimizer<'_> {
self.collapse_seq_exprs(e); self.collapse_seq_exprs(e);
// Normalize
match e {
Expr::Paren(paren) => {
self.changed = true;
*e = *paren.expr.take();
}
_ => {}
}
self.drop_unused_assignments(e); self.drop_unused_assignments(e);
self.compress_regexp(e); self.compress_regexp(e);
@ -1583,7 +1738,7 @@ impl VisitMut for Optimizer<'_> {
Expr::Bin(bin) => { Expr::Bin(bin) => {
let expr = self.optimize_lit_cmp(bin); let expr = self.optimize_lit_cmp(bin);
if let Some(expr) = expr { if let Some(expr) = expr {
log::trace!("Optimizing: Literal comparison"); log::debug!("Optimizing: Literal comparison");
self.changed = true; self.changed = true;
*e = expr; *e = expr;
} }
@ -1598,14 +1753,14 @@ impl VisitMut for Optimizer<'_> {
self.handle_negated_seq(e); self.handle_negated_seq(e);
self.compress_array_join(e); self.compress_array_join(e);
self.compress_logical_exprs_with_negated_lhs(e);
self.remove_useless_pipes(e); self.remove_useless_pipes(e);
self.optimize_bools(e); self.optimize_bools(e);
self.handle_property_access(e); self.handle_property_access(e);
self.lift_seqs_of_bin(e);
self.lift_seqs_of_cond_assign(e); self.lift_seqs_of_cond_assign(e);
if self.options.negate_iife { if self.options.negate_iife {
@ -1619,11 +1774,30 @@ impl VisitMut for Optimizer<'_> {
self.invoke_iife(e); self.invoke_iife(e);
self.optimize_bangbang(e); self.optimize_bangbang(e);
match e {
Expr::Seq(s) if s.exprs.is_empty() => {
e.take();
}
_ => {}
}
} }
fn visit_mut_expr_stmt(&mut self, n: &mut ExprStmt) { fn visit_mut_expr_stmt(&mut self, n: &mut ExprStmt) {
n.visit_mut_children_with(self); n.visit_mut_children_with(self);
let mut need_ignore_return_value = false;
// If negate_iife is true, it's already handled by
// visit_mut_children_with(self) above.
if !self.options.negate_iife {
// I(kdy1) don't know why this check if required, but there are two test cases
// with `options.expressions` as only difference.
if !self.options.expr {
need_ignore_return_value |= self.negate_iife_in_cond(&mut n.expr);
}
}
self.negate_iife_ignoring_ret(&mut n.expr); self.negate_iife_ignoring_ret(&mut n.expr);
let is_directive = match &*n.expr { let is_directive = match &*n.expr {
@ -1635,9 +1809,18 @@ impl VisitMut for Optimizer<'_> {
return; return;
} }
if self.options.unused if need_ignore_return_value
|| self.options.unused
|| self.options.side_effects || self.options.side_effects
|| (self.options.sequences() && n.expr.is_seq()) || (self.options.sequences() && n.expr.is_seq())
|| (self.options.conditionals
&& match &*n.expr {
Expr::Bin(BinExpr {
op: op!("||") | op!("&&"),
..
}) => true,
_ => false,
})
{ {
// Preserve top-level negated iifes. // Preserve top-level negated iifes.
match &*n.expr { match &*n.expr {
@ -1656,6 +1839,18 @@ impl VisitMut for Optimizer<'_> {
} }
let expr = self.ignore_return_value(&mut n.expr); let expr = self.ignore_return_value(&mut n.expr);
n.expr = expr.map(Box::new).unwrap_or_else(|| undefined(DUMMY_SP)); n.expr = expr.map(Box::new).unwrap_or_else(|| undefined(DUMMY_SP));
} else {
match &mut *n.expr {
Expr::Seq(e) => {
// Non-last items are handled by visit_mut_seq_expr
if let Some(e) = e.exprs.last_mut() {
self.optimize_bang_within_logical_ops(e, true);
}
}
_ => {
self.optimize_bang_within_logical_ops(&mut *n.expr, true);
}
}
} }
} }
@ -1685,6 +1880,13 @@ impl VisitMut for Optimizer<'_> {
n.left.visit_mut_with(&mut *self.with_ctx(ctx)); n.left.visit_mut_with(&mut *self.with_ctx(ctx));
n.body.visit_mut_with(self); n.body.visit_mut_with(self);
match &mut *n.body {
Stmt::Block(body) => {
self.negate_if_terminate(&mut body.stmts, false, true);
}
_ => {}
}
} }
fn visit_mut_for_of_stmt(&mut self, n: &mut ForOfStmt) { fn visit_mut_for_of_stmt(&mut self, n: &mut ForOfStmt) {
@ -1697,22 +1899,42 @@ impl VisitMut for Optimizer<'_> {
n.left.visit_mut_with(&mut *self.with_ctx(ctx)); n.left.visit_mut_with(&mut *self.with_ctx(ctx));
n.body.visit_mut_with(self); n.body.visit_mut_with(self);
match &mut *n.body {
Stmt::Block(body) => {
self.negate_if_terminate(&mut body.stmts, false, true);
}
_ => {}
}
} }
fn visit_mut_for_stmt(&mut self, s: &mut ForStmt) { fn visit_mut_for_stmt(&mut self, s: &mut ForStmt) {
s.visit_mut_children_with(self); let ctx = Ctx {
executed_multiple_time: true,
..self.ctx
};
self.optimize_init_of_for_stmt(s); s.visit_mut_children_with(&mut *self.with_ctx(ctx));
self.drop_if_break(s); self.with_ctx(ctx).merge_for_if_break(s);
self.with_ctx(ctx).optimize_init_of_for_stmt(s);
self.with_ctx(ctx).drop_if_break(s);
if let Some(test) = &mut s.test { if let Some(test) = &mut s.test {
self.optimize_expr_in_bool_ctx(test); self.with_ctx(ctx).optimize_expr_in_bool_ctx(test);
}
match &mut *s.body {
Stmt::Block(body) => {
self.negate_if_terminate(&mut body.stmts, false, true);
}
_ => {}
} }
} }
fn visit_mut_function(&mut self, n: &mut Function) { fn visit_mut_function(&mut self, n: &mut Function) {
let old_vars = take(&mut self.vars_accessible_without_side_effect);
{ {
let ctx = Ctx { let ctx = Ctx {
stmt_lablled: false, stmt_lablled: false,
@ -1721,6 +1943,8 @@ impl VisitMut for Optimizer<'_> {
n.decorators.visit_mut_with(&mut *self.with_ctx(ctx)); n.decorators.visit_mut_with(&mut *self.with_ctx(ctx));
} }
let old_in_asm = self.ctx.in_asm;
{ {
let ctx = Ctx { let ctx = Ctx {
stmt_lablled: false, stmt_lablled: false,
@ -1736,6 +1960,9 @@ impl VisitMut for Optimizer<'_> {
Some(body) => { Some(body) => {
// Bypass block scope handler. // Bypass block scope handler.
body.visit_mut_children_with(optimizer); body.visit_mut_children_with(optimizer);
optimizer.remove_useless_return(&mut body.stmts);
optimizer.negate_if_terminate(&mut body.stmts, true, false);
if let Some(last) = body.stmts.last_mut() { if let Some(last) = body.stmts.last_mut() {
optimizer.drop_unused_stmt_at_end_of_fn(last); optimizer.drop_unused_stmt_at_end_of_fn(last);
@ -1745,6 +1972,10 @@ impl VisitMut for Optimizer<'_> {
} }
} }
if let Some(body) = &mut n.body {
self.merge_if_returns(&mut body.stmts);
}
{ {
let ctx = Ctx { let ctx = Ctx {
can_inline_arguments: true, can_inline_arguments: true,
@ -1753,11 +1984,7 @@ impl VisitMut for Optimizer<'_> {
self.with_ctx(ctx).optimize_usage_of_arguments(n); self.with_ctx(ctx).optimize_usage_of_arguments(n);
} }
if let Some(body) = &mut n.body { self.ctx.in_asm = old_in_asm;
self.merge_if_returns(&mut body.stmts);
}
self.vars_accessible_without_side_effect = old_vars;
} }
fn visit_mut_if_stmt(&mut self, n: &mut IfStmt) { fn visit_mut_if_stmt(&mut self, n: &mut IfStmt) {
@ -1772,7 +1999,13 @@ impl VisitMut for Optimizer<'_> {
n.alt.visit_mut_with(&mut *self.with_ctx(ctx)); n.alt.visit_mut_with(&mut *self.with_ctx(ctx));
self.negate_if_stmt(n);
self.optimize_expr_in_bool_ctx(&mut n.test); self.optimize_expr_in_bool_ctx(&mut n.test);
self.merge_nested_if(n);
self.merge_else_if(n);
} }
fn visit_mut_labeled_stmt(&mut self, n: &mut LabeledStmt) { fn visit_mut_labeled_stmt(&mut self, n: &mut LabeledStmt) {
@ -1820,17 +2053,55 @@ impl VisitMut for Optimizer<'_> {
} }
fn visit_mut_new_expr(&mut self, n: &mut NewExpr) { fn visit_mut_new_expr(&mut self, n: &mut NewExpr) {
n.callee.visit_mut_with(self); {
let ctx = Ctx {
if n.callee.may_have_side_effects() { is_callee: true,
self.vars_accessible_without_side_effect.clear(); ..self.ctx
};
n.callee.visit_mut_with(&mut *self.with_ctx(ctx));
} }
{ {
n.args.visit_mut_with(self); let ctx = Ctx {
in_call_arg: true,
..self.ctx
};
n.args.visit_mut_with(&mut *self.with_ctx(ctx));
}
} }
self.vars_accessible_without_side_effect.clear(); fn visit_mut_opt_stmt(&mut self, s: &mut Option<Box<Stmt>>) {
s.visit_mut_children_with(self);
match s.as_deref() {
Some(Stmt::Empty(..)) => {
self.changed = true;
log::debug!("misc: Removing empty statement");
*s = None;
}
_ => {}
}
}
fn visit_mut_opt_var_decl_or_expr(&mut self, n: &mut Option<VarDeclOrExpr>) {
n.visit_mut_children_with(self);
match n {
Some(VarDeclOrExpr::Expr(e)) => match &mut **e {
Expr::Seq(SeqExpr { exprs, .. }) if exprs.is_empty() => {
*n = None;
return;
}
_ => {}
},
Some(VarDeclOrExpr::VarDecl(v)) => {
if v.decls.is_empty() {
*n = None;
return;
}
}
_ => {}
}
} }
fn visit_mut_param(&mut self, n: &mut Param) { fn visit_mut_param(&mut self, n: &mut Param) {
@ -1855,18 +2126,32 @@ impl VisitMut for Optimizer<'_> {
p.visit_mut_children_with(self); p.visit_mut_children_with(self);
self.optimize_computed_prop_name_as_normal(p); self.optimize_computed_prop_name_as_normal(p);
self.optimize_prop_name(p);
} }
fn visit_mut_return_stmt(&mut self, n: &mut ReturnStmt) { fn visit_mut_return_stmt(&mut self, n: &mut ReturnStmt) {
n.visit_mut_children_with(self); n.visit_mut_children_with(self);
self.drop_undefined_from_return_arg(n);
if let Some(arg) = &mut n.arg { if let Some(arg) = &mut n.arg {
self.optimize_in_fn_termiation(&mut **arg); self.optimize_in_fn_termiation(&mut **arg);
} }
} }
fn visit_mut_seq_expr(&mut self, n: &mut SeqExpr) { fn visit_mut_seq_expr(&mut self, n: &mut SeqExpr) {
n.visit_mut_children_with(self); {
let ctx = Ctx {
dont_use_negated_iife: true,
..self.ctx
};
n.visit_mut_children_with(&mut *self.with_ctx(ctx));
}
self.shift_void(n);
self.shift_assignment(n);
{ {
let exprs = n let exprs = n
@ -1879,6 +2164,7 @@ impl VisitMut for Optimizer<'_> {
Expr::Lit(Lit::Num(v)) => v.span.is_dummy(), Expr::Lit(Lit::Num(v)) => v.span.is_dummy(),
_ => false, _ => false,
}; };
let can_remove = let can_remove =
!last && (idx != 0 || !is_injected_zero || !self.ctx.is_this_aware_callee); !last && (idx != 0 || !is_injected_zero || !self.ctx.is_this_aware_callee);
@ -1898,16 +2184,19 @@ impl VisitMut for Optimizer<'_> {
n.exprs = exprs; n.exprs = exprs;
} }
self.merge_sequences_in_seq_expr(n);
self.lift_seqs_of_assign(n); self.lift_seqs_of_assign(n);
} }
fn visit_mut_stmt(&mut self, s: &mut Stmt) { fn visit_mut_stmt(&mut self, s: &mut Stmt) {
let ctx = Ctx { let ctx = Ctx {
in_bang_arg: false, is_callee: false,
is_delete_arg: false, is_delete_arg: false,
is_exported: false,
is_update_arg: false, is_update_arg: false,
in_lhs_of_assign: false, in_lhs_of_assign: false,
in_bang_arg: false,
is_exported: false,
in_obj_of_non_computed_member: false, in_obj_of_non_computed_member: false,
..self.ctx ..self.ctx
}; };
@ -1932,7 +2221,7 @@ impl VisitMut for Optimizer<'_> {
_ => false, _ => false,
} }
{ {
log::trace!("Removing 'use strict'"); log::debug!("Removing 'use strict'");
*s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
return; return;
} }
@ -1943,7 +2232,7 @@ impl VisitMut for Optimizer<'_> {
if can_be_removed { if can_be_removed {
self.changed = true; self.changed = true;
log::trace!("Dropping an expression without side effect"); log::debug!("Dropping an expression without side effect");
*s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
return; return;
} }
@ -1957,7 +2246,7 @@ impl VisitMut for Optimizer<'_> {
Stmt::Debugger(..) => { Stmt::Debugger(..) => {
self.changed = true; self.changed = true;
*s = Stmt::Empty(EmptyStmt { span: DUMMY_SP }); *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
log::trace!("drop_debugger: Dropped a debugger statement"); log::debug!("drop_debugger: Dropped a debugger statement");
return; return;
} }
_ => {} _ => {}
@ -1997,6 +2286,11 @@ impl VisitMut for Optimizer<'_> {
top_level: false, top_level: false,
..self.ctx ..self.ctx
}; };
self.with_ctx(ctx).inject_else(stmts);
self.with_ctx(ctx).merge_sequences_in_stmts(stmts);
self.with_ctx(ctx).handle_stmt_likes(stmts); self.with_ctx(ctx).handle_stmt_likes(stmts);
self.with_ctx(ctx).merge_var_decls(stmts); self.with_ctx(ctx).merge_var_decls(stmts);
@ -2021,6 +2315,12 @@ impl VisitMut for Optimizer<'_> {
} }
} }
fn visit_mut_str(&mut self, s: &mut Str) {
s.visit_mut_children_with(self);
s.kind = Default::default()
}
fn visit_mut_switch_cases(&mut self, n: &mut Vec<SwitchCase>) { fn visit_mut_switch_cases(&mut self, n: &mut Vec<SwitchCase>) {
n.visit_mut_children_with(self); n.visit_mut_children_with(self);
@ -2110,8 +2410,6 @@ impl VisitMut for Optimizer<'_> {
}; };
n.visit_mut_children_with(&mut *self.with_ctx(ctx)); n.visit_mut_children_with(&mut *self.with_ctx(ctx));
self.vars_accessible_without_side_effect.clear();
} }
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) { fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
@ -2131,7 +2429,7 @@ impl VisitMut for Optimizer<'_> {
if let Some(e) = &var.init { if let Some(e) = &var.init {
if is_pure_undefined(e) { if is_pure_undefined(e) {
self.changed = true; self.changed = true;
log::trace!("Dropping explicit initializer which evaluates to `undefined`"); log::debug!("Dropping explicit initializer which evaluates to `undefined`");
var.init = None; var.init = None;
} }
@ -2250,7 +2548,7 @@ fn is_pure_undefined_or_null(e: &Expr) -> bool {
fn is_callee_this_aware(callee: &Expr) -> bool { fn is_callee_this_aware(callee: &Expr) -> bool {
match &*callee { match &*callee {
Expr::Arrow(..) => return false, Expr::Arrow(..) => return false,
Expr::Seq(s) => return is_callee_this_aware(s.exprs.last().unwrap()), Expr::Seq(..) => return true,
Expr::Member(MemberExpr { Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(obj), obj: ExprOrSuper::Expr(obj),
.. ..

View File

@ -9,7 +9,7 @@ impl Optimizer<'_> {
let value = if value.is_empty() { 0f64 } else { 1f64 }; let value = if value.is_empty() { 0f64 } else { 1f64 };
self.changed = true; self.changed = true;
log::trace!("numbers: Converting a string literal to {:?}", value); log::debug!("numbers: Converting a string literal to {:?}", value);
*e = Expr::Lit(Lit::Num(Number { span: *span, value })); *e = Expr::Lit(Lit::Num(Number { span: *span, value }));
return; return;
} }
@ -37,7 +37,7 @@ impl Optimizer<'_> {
.. ..
}) => { }) => {
self.changed = true; self.changed = true;
log::trace!("numbers: Lifting `-`"); log::debug!("numbers: Lifting `-`");
*e = Expr::Unary(UnaryExpr { *e = Expr::Unary(UnaryExpr {
span: arg.span, span: arg.span,
@ -55,7 +55,7 @@ impl Optimizer<'_> {
Expr::Lit(Lit::Num(Number { span, value, .. })) => { Expr::Lit(Lit::Num(Number { span, value, .. })) => {
if value.is_sign_negative() { if value.is_sign_negative() {
self.changed = true; self.changed = true;
log::trace!("numbers: Lifting `-` in a literal"); log::debug!("numbers: Lifting `-` in a literal");
*e = Expr::Unary(UnaryExpr { *e = Expr::Unary(UnaryExpr {
span: arg.span, span: arg.span,

View File

@ -1,4 +1,8 @@
use super::Optimizer; use super::Optimizer;
use crate::compress::optimize::bools::is_ok_to_negate_for_cond;
use crate::compress::optimize::bools::is_ok_to_negate_rhs;
use crate::compress::optimize::is_pure_undefined;
use crate::debug::dump;
use crate::util::make_bool; use crate::util::make_bool;
use crate::util::ValueExt; use crate::util::ValueExt;
use std::mem::swap; use std::mem::swap;
@ -74,7 +78,7 @@ impl Optimizer<'_> {
match (lt, rt) { match (lt, rt) {
(Type::Undefined, Type::Null) | (Type::Null, Type::Undefined) => { (Type::Undefined, Type::Null) | (Type::Null, Type::Undefined) => {
if op == op!("===") { if op == op!("===") {
log::trace!( log::debug!(
"Reducing `!== null || !== undefined` check to `!= null`" "Reducing `!== null || !== undefined` check to `!= null`"
); );
return Some(BinExpr { return Some(BinExpr {
@ -84,7 +88,7 @@ impl Optimizer<'_> {
right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))),
}); });
} else { } else {
log::trace!( log::debug!(
"Reducing `=== null || === undefined` check to `== null`" "Reducing `=== null || === undefined` check to `== null`"
); );
return Some(BinExpr { return Some(BinExpr {
@ -167,7 +171,7 @@ impl Optimizer<'_> {
if e.op == op!("===") || e.op == op!("!==") { if e.op == op!("===") || e.op == op!("!==") {
if (e.left.is_ident() || e.left.is_member()) && e.left.eq_ignore_span(&e.right) { if (e.left.is_ident() || e.left.is_member()) && e.left.eq_ignore_span(&e.right) {
self.changed = true; self.changed = true;
log::trace!("Reducing comparison of same variable ({})", e.op); log::debug!("Reducing comparison of same variable ({})", e.op);
e.op = if e.op == op!("===") { e.op = if e.op == op!("===") {
op!("==") op!("==")
@ -187,7 +191,7 @@ impl Optimizer<'_> {
if lt == rt { if lt == rt {
e.op = op!("=="); e.op = op!("==");
self.changed = true; self.changed = true;
log::trace!("Reduced `===` to `==` because types of operands are identical") log::debug!("Reduced `===` to `==` because types of operands are identical")
} }
} }
} }
@ -209,7 +213,7 @@ impl Optimizer<'_> {
let value = if n.op == op!("==") { l == r } else { l != r }; let value = if n.op == op!("==") { l == r } else { l != r };
log::trace!("Optimizing: literal comparison => bool"); log::debug!("Optimizing: literal comparison => bool");
self.changed = true; self.changed = true;
return Some(Expr::Lit(Lit::Bool(Bool { return Some(Expr::Lit(Lit::Bool(Bool {
span: n.span, span: n.span,
@ -249,7 +253,7 @@ impl Optimizer<'_> {
| Expr::Bin(BinExpr { op: op!(">"), .. }) => { | Expr::Bin(BinExpr { op: op!(">"), .. }) => {
if let Known(Type::Bool) = arg.get_type() { if let Known(Type::Bool) = arg.get_type() {
self.changed = true; self.changed = true;
log::trace!("Optimizing: `!!expr` => `expr`"); log::debug!("Optimizing: `!!expr` => `expr`");
*e = *arg.take(); *e = *arg.take();
} }
@ -278,9 +282,9 @@ impl Optimizer<'_> {
/// ///
/// TODO: Handle special cases like !1 or !0 /// TODO: Handle special cases like !1 or !0
pub(super) fn negate(&mut self, e: &mut Expr) { pub(super) fn negate(&mut self, e: &mut Expr) {
self.changed = true; let start_str = dump(&*e);
let arg = Box::new(e.take());
self.changed = true;
match e { match e {
Expr::Bin(bin @ BinExpr { op: op!("=="), .. }) Expr::Bin(bin @ BinExpr { op: op!("=="), .. })
| Expr::Bin(bin @ BinExpr { op: op!("!="), .. }) | Expr::Bin(bin @ BinExpr { op: op!("!="), .. })
@ -304,15 +308,68 @@ impl Optimizer<'_> {
} }
}; };
self.changed = true; self.changed = true;
log::trace!("negate: binary"); log::debug!("negate: binary");
return; return;
} }
Expr::Bin(BinExpr {
left,
right,
op: op @ op!("&&"),
..
}) if is_ok_to_negate_rhs(&right) => {
log::debug!("negate: a && b => !a || !b");
self.negate(&mut **left);
self.negate(&mut **right);
*op = op!("||");
return;
}
Expr::Bin(BinExpr {
left,
right,
op: op @ op!("||"),
..
}) if is_ok_to_negate_rhs(&right) => {
log::debug!("negate: a || b => !a && !b");
self.negate(&mut **left);
self.negate(&mut **right);
*op = op!("&&");
return;
}
Expr::Cond(CondExpr { cons, alt, .. })
if is_ok_to_negate_for_cond(&cons) && is_ok_to_negate_for_cond(&alt) =>
{
log::debug!("negate: cond");
self.negate(&mut **cons);
self.negate(&mut **alt);
return;
}
Expr::Seq(SeqExpr { exprs, .. }) => {
if let Some(last) = exprs.last_mut() {
log::debug!("negate: seq");
self.negate(&mut **last);
return;
}
}
_ => {}
}
let mut arg = Box::new(e.take());
match &mut *arg {
Expr::Unary(UnaryExpr { Expr::Unary(UnaryExpr {
op: op!("!"), arg, .. op: op!("!"), arg, ..
}) => match &mut **arg { }) => match &mut **arg {
Expr::Unary(UnaryExpr { op: op!("!"), .. }) => { Expr::Unary(UnaryExpr { op: op!("!"), .. }) => {
log::trace!("negate: !!bool => !bool"); log::debug!("negate: !!bool => !bool");
*e = *arg.take(); *e = *arg.take();
return; return;
} }
@ -321,22 +378,33 @@ impl Optimizer<'_> {
op: op!("instanceof"), op: op!("instanceof"),
.. ..
}) => { }) => {
log::trace!("negate: !bool => bool"); log::debug!("negate: !bool => bool");
*e = *arg.take(); *e = *arg.take();
return; return;
} }
_ => {} _ => {
if self.ctx.in_bool_ctx {
log::debug!("negate: !expr => expr (in bool context)");
*e = *arg.take();
return;
}
}
}, },
_ => {} _ => {}
} }
log::trace!("negate: e => !e"); log::debug!("negate: e => !e");
*e = Expr::Unary(UnaryExpr { *e = Expr::Unary(UnaryExpr {
span: DUMMY_SP, span: DUMMY_SP,
op: op!("!"), op: op!("!"),
arg, arg,
}); });
if cfg!(feature = "debug") {
log::trace!("[Change] Negated `{}` as `{}`", start_str, dump(&*e));
}
} }
pub(super) fn handle_negated_seq(&mut self, n: &mut Expr) { pub(super) fn handle_negated_seq(&mut self, n: &mut Expr) {
@ -352,7 +420,7 @@ impl Optimizer<'_> {
if exprs.is_empty() { if exprs.is_empty() {
return; return;
} }
log::trace!("optimizing negated sequences"); log::debug!("optimizing negated sequences");
{ {
let last = exprs.last_mut().unwrap(); let last = exprs.last_mut().unwrap();
@ -427,24 +495,60 @@ impl Optimizer<'_> {
_ => return, _ => return,
}; };
log::trace!("Compressing: `e = 3 & e` => `e &= 3`"); log::debug!("Compressing: `e = 3 & e` => `e &= 3`");
self.changed = true; self.changed = true;
e.op = op; e.op = op;
e.right = left.take(); e.right = left.take();
} }
/// Rules:
/// - `l > i` => `i < l`
fn can_swap_bin_operands(&mut self, l: &Expr, r: &Expr, is_for_rel: bool) -> bool { fn can_swap_bin_operands(&mut self, l: &Expr, r: &Expr, is_for_rel: bool) -> bool {
match (l, r) { match (l, r) {
(Expr::Ident(l), Expr::Ident(r)) => { (Expr::Member(_), _) if is_for_rel => false,
self.options.comparisons && (is_for_rel || l.sym > r.sym)
(Expr::Update(..) | Expr::Assign(..), Expr::Lit(..)) if is_for_rel => false,
(
Expr::Member(..)
| Expr::Call(..)
| Expr::Assign(..)
| Expr::Update(..)
| Expr::Bin(BinExpr {
op: op!("&&") | op!("||"),
..
}),
Expr::Lit(..),
) => true,
(
Expr::Member(..) | Expr::Call(..) | Expr::Assign(..),
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}),
) if match &**arg {
Expr::Lit(..) => true,
_ => false,
} =>
{
true
} }
(Expr::Member(..) | Expr::Call(..) | Expr::Assign(..), r) if is_pure_undefined(r) => {
true
}
(Expr::Ident(..), Expr::Lit(..)) if is_for_rel => false,
(Expr::Ident(..), Expr::Ident(..)) => false,
(Expr::Ident(..), Expr::Lit(..)) (Expr::Ident(..), Expr::Lit(..))
| ( | (
Expr::Ident(..), Expr::Ident(..) | Expr::Member(..),
Expr::Unary(UnaryExpr { Expr::Unary(UnaryExpr {
op: op!("void"), .. op: op!("void") | op!("!"),
..
}), }),
) )
| ( | (
@ -479,7 +583,7 @@ impl Optimizer<'_> {
} }
if self.can_swap_bin_operands(&left, &right, false) { if self.can_swap_bin_operands(&left, &right, false) {
log::trace!("Swapping operands of binary exprssion"); log::debug!("Swapping operands of binary exprssion");
swap(left, right); swap(left, right);
return true; return true;
} }
@ -494,7 +598,7 @@ impl Optimizer<'_> {
| Expr::Bin(e @ BinExpr { op: op!("<"), .. }) => { | Expr::Bin(e @ BinExpr { op: op!("<"), .. }) => {
if self.options.comparisons && self.can_swap_bin_operands(&e.left, &e.right, true) { if self.options.comparisons && self.can_swap_bin_operands(&e.left, &e.right, true) {
self.changed = true; self.changed = true;
log::trace!("comparisons: Swapping operands of {}", e.op); log::debug!("comparisons: Swapping operands of {}", e.op);
e.op = if e.op == op!("<=") { e.op = if e.op == op!("<=") {
op!(">=") op!(">=")
@ -568,13 +672,13 @@ impl Optimizer<'_> {
if rb { if rb {
self.changed = true; self.changed = true;
log::trace!("Optimizing: e && true => !!e"); log::debug!("Optimizing: e && true => !!e");
self.negate_twice(&mut bin.left); self.negate_twice(&mut bin.left);
*e = *bin.left.take(); *e = *bin.left.take();
} else { } else {
self.changed = true; self.changed = true;
log::trace!("Optimizing: e && false => e"); log::debug!("Optimizing: e && false => e");
*e = *bin.left.take(); *e = *bin.left.take();
} }
@ -588,7 +692,7 @@ impl Optimizer<'_> {
if !rb { if !rb {
self.changed = true; self.changed = true;
log::trace!("Optimizing: e || false => !!e"); log::debug!("Optimizing: e || false => !!e");
self.negate_twice(&mut bin.left); self.negate_twice(&mut bin.left);
*e = *bin.left.take(); *e = *bin.left.take();
@ -648,7 +752,7 @@ impl Optimizer<'_> {
match l { match l {
Expr::Lit(Lit::Null(..)) => { Expr::Lit(Lit::Null(..)) => {
log::trace!("Removing null from lhs of ??"); log::debug!("Removing null from lhs of ??");
self.changed = true; self.changed = true;
*e = r.take(); *e = r.take();
return; return;
@ -658,7 +762,7 @@ impl Optimizer<'_> {
| Expr::Lit(Lit::BigInt(..)) | Expr::Lit(Lit::BigInt(..))
| Expr::Lit(Lit::Bool(..)) | Expr::Lit(Lit::Bool(..))
| Expr::Lit(Lit::Regex(..)) => { | Expr::Lit(Lit::Regex(..)) => {
log::trace!("Removing rhs of ?? as lhs cannot be null nor undefined"); log::debug!("Removing rhs of ?? as lhs cannot be null nor undefined");
self.changed = true; self.changed = true;
*e = l.take(); *e = l.take();
return; return;
@ -682,7 +786,7 @@ impl Optimizer<'_> {
}) => match &**arg { }) => match &**arg {
Expr::Ident(arg) => { Expr::Ident(arg) => {
if let Some(value) = self.typeofs.get(&arg.to_id()).cloned() { if let Some(value) = self.typeofs.get(&arg.to_id()).cloned() {
log::trace!( log::debug!(
"Converting typeof of variable to literal as we know the value" "Converting typeof of variable to literal as we know the value"
); );
self.changed = true; self.changed = true;
@ -697,7 +801,7 @@ impl Optimizer<'_> {
} }
Expr::Arrow(..) | Expr::Fn(..) => { Expr::Arrow(..) | Expr::Fn(..) => {
log::trace!("Converting typeof to 'function' as we know the value"); log::debug!("Converting typeof to 'function' as we know the value");
self.changed = true; self.changed = true;
*e = Expr::Lit(Lit::Str(Str { *e = Expr::Lit(Lit::Str(Str {
span: *span, span: *span,
@ -709,7 +813,7 @@ impl Optimizer<'_> {
} }
Expr::Array(..) | Expr::Object(..) => { Expr::Array(..) | Expr::Object(..) => {
log::trace!("Converting typeof to 'object' as we know the value"); log::debug!("Converting typeof to 'object' as we know the value");
self.changed = true; self.changed = true;
*e = Expr::Lit(Lit::Str(Str { *e = Expr::Lit(Lit::Str(Str {
span: *span, span: *span,

View File

@ -60,6 +60,10 @@ impl Optimizer<'_> {
return; return;
} }
if self.ctx.is_callee {
return;
}
if obj.props.iter().any(|prop| match prop { if obj.props.iter().any(|prop| match prop {
PropOrSpread::Spread(_) => false, PropOrSpread::Spread(_) => false,
PropOrSpread::Prop(p) => match &**p { PropOrSpread::Prop(p) => match &**p {
@ -87,7 +91,7 @@ impl Optimizer<'_> {
Prop::Shorthand(_) => {} Prop::Shorthand(_) => {}
Prop::KeyValue(p) => { Prop::KeyValue(p) => {
if prop_name_eq(&p.key, &key.sym) { if prop_name_eq(&p.key, &key.sym) {
log::trace!("properties: Inlining a key-value property `{}`", key.sym); log::debug!("properties: Inlining a key-value property `{}`", key.sym);
self.changed = true; self.changed = true;
*e = *p.value.clone(); *e = *p.value.clone();
return; return;

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,7 @@ impl Optimizer<'_> {
match &*args[0].expr { match &*args[0].expr {
Expr::Lit(Lit::Str(..)) => { Expr::Lit(Lit::Str(..)) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"strings: Unsafely reduced `RegExp` call in a string context" "strings: Unsafely reduced `RegExp` call in a string context"
); );
@ -87,7 +87,7 @@ impl Optimizer<'_> {
Expr::Lit(Lit::Str(..)) => { Expr::Lit(Lit::Str(..)) => {
*n = *e.expr.take(); *n = *e.expr.take();
self.changed = true; self.changed = true;
log::trace!("string: Removed a paren in a string context"); log::debug!("string: Removed a paren in a string context");
} }
_ => {} _ => {}
} }
@ -101,7 +101,7 @@ impl Optimizer<'_> {
let value = n.as_string(); let value = n.as_string();
if let Known(value) = value { if let Known(value) = value {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"strings: Converted an expression into a string literal (in string context)" "strings: Converted an expression into a string literal (in string context)"
); );
*n = Expr::Lit(Lit::Str(Str { *n = Expr::Lit(Lit::Str(Str {
@ -116,7 +116,7 @@ impl Optimizer<'_> {
match n { match n {
Expr::Lit(Lit::Num(v)) => { Expr::Lit(Lit::Num(v)) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"strings: Converted a numeric literal ({}) into a string literal (in string \ "strings: Converted a numeric literal ({}) into a string literal (in string \
context)", context)",
v.value v.value
@ -136,7 +136,7 @@ impl Optimizer<'_> {
return; return;
} }
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"strings: Converted a regex (/{}/{}) into a string literal (in string context)", "strings: Converted a regex (/{}/{}) into a string literal (in string context)",
v.exp, v.exp,
v.flags v.flags
@ -163,7 +163,7 @@ impl Optimizer<'_> {
.unwrap_or(false) .unwrap_or(false)
{ {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"strings: Converting a reference ({}{:?}) into `undefined` (in string \ "strings: Converting a reference ({}{:?}) into `undefined` (in string \
context)", context)",
i.sym, i.sym,
@ -274,7 +274,7 @@ impl Optimizer<'_> {
if let Some(l_last) = l.quasis.last_mut() { if let Some(l_last) = l.quasis.last_mut() {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"template: Concatted a string (`{}`) on rhs of `+` to a template literal", "template: Concatted a string (`{}`) on rhs of `+` to a template literal",
rs.value rs.value
); );
@ -294,7 +294,7 @@ impl Optimizer<'_> {
if let Some(r_first) = r.quasis.first_mut() { if let Some(r_first) = r.quasis.first_mut() {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"template: Prepended a string (`{}`) on lhs of `+` to a template literal", "template: Prepended a string (`{}`) on lhs of `+` to a template literal",
ls.value ls.value
); );
@ -332,7 +332,7 @@ impl Optimizer<'_> {
debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{:?} is invalid", l); debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{:?} is invalid", l);
self.changed = true; self.changed = true;
log::trace!("strings: Merged to template literals"); log::debug!("strings: Merged to template literals");
} }
_ => {} _ => {}
@ -365,7 +365,7 @@ impl Optimizer<'_> {
let left_span = left.span; let left_span = left.span;
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"string: Concatting `{} + {}` to `{}`", "string: Concatting `{} + {}` to `{}`",
second_str, second_str,
third_str, third_str,
@ -413,7 +413,7 @@ impl Optimizer<'_> {
.. ..
})) => { })) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"string: Dropping empty string literal (in lhs) because it \ "string: Dropping empty string literal (in lhs) because it \
does not changes type" does not changes type"
); );
@ -430,7 +430,7 @@ impl Optimizer<'_> {
.. ..
})) => { })) => {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"string: Dropping empty string literal (in rhs) because it \ "string: Dropping empty string literal (in rhs) because it \
does not changes type" does not changes type"
); );

View File

@ -1,3 +1,5 @@
use std::mem::take;
use super::Optimizer; use super::Optimizer;
use crate::util::ExprOptExt; use crate::util::ExprOptExt;
use swc_common::EqIgnoreSpan; use swc_common::EqIgnoreSpan;
@ -5,7 +7,9 @@ use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::prepend;
use swc_ecma_utils::ExprExt; use swc_ecma_utils::ExprExt;
use swc_ecma_utils::StmtExt;
use swc_ecma_utils::Type; use swc_ecma_utils::Type;
use swc_ecma_utils::Value::Known; use swc_ecma_utils::Value::Known;
use swc_ecma_visit::noop_visit_type; use swc_ecma_visit::noop_visit_type;
@ -45,6 +49,7 @@ impl Optimizer<'_> {
}); });
if let Some(case_idx) = matching_case { if let Some(case_idx) = matching_case {
let mut var_ids = vec![];
let mut stmts = vec![]; let mut stmts = vec![];
let should_preserve_switch = stmt.cases.iter().skip(case_idx).any(|case| { let should_preserve_switch = stmt.cases.iter().skip(case_idx).any(|case| {
@ -60,9 +65,9 @@ impl Optimizer<'_> {
return; return;
} }
log::trace!("switches: Removing unreachable cases from a constant switch"); log::debug!("switches: Removing unreachable cases from a constant switch");
} else { } else {
log::trace!("switches: Removing a constant switch"); log::debug!("switches: Removing a constant switch");
} }
self.changed = true; self.changed = true;
@ -81,6 +86,21 @@ impl Optimizer<'_> {
} }
} }
for case in &stmt.cases[..case_idx] {
for cons in &case.cons {
var_ids.extend(
cons.extract_var_ids()
.into_iter()
.map(|name| VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(name.into()),
init: None,
definite: Default::default(),
}),
);
}
}
for case in stmt.cases.iter_mut().skip(case_idx) { for case in stmt.cases.iter_mut().skip(case_idx) {
let mut found_break = false; let mut found_break = false;
case.cons.retain(|stmt| match stmt { case.cons.retain(|stmt| match stmt {
@ -110,6 +130,18 @@ impl Optimizer<'_> {
} }
} }
if !var_ids.is_empty() {
prepend(
&mut stmts,
Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
declare: Default::default(),
decls: take(&mut var_ids),
})),
)
}
let inner = if should_preserve_switch { let inner = if should_preserve_switch {
let mut cases = stmt.cases.take(); let mut cases = stmt.cases.take();
let case = SwitchCase { let case = SwitchCase {
@ -193,7 +225,7 @@ impl Optimizer<'_> {
if !preserve_cases { if !preserve_cases {
if let Some(last_non_empty) = last_non_empty { if let Some(last_non_empty) = last_non_empty {
if last_non_empty + 1 != cases.len() { if last_non_empty + 1 != cases.len() {
log::trace!("switches: Removing empty cases at the end"); log::debug!("switches: Removing empty cases at the end");
self.changed = true; self.changed = true;
cases.drain(last_non_empty + 1..); cases.drain(last_non_empty + 1..);
} }
@ -203,7 +235,7 @@ impl Optimizer<'_> {
if let Some(last) = cases.last_mut() { if let Some(last) = cases.last_mut() {
match last.cons.last() { match last.cons.last() {
Some(Stmt::Break(BreakStmt { label: None, .. })) => { Some(Stmt::Break(BreakStmt { label: None, .. })) => {
log::trace!("switches: Removing `break` at the end"); log::debug!("switches: Removing `break` at the end");
self.changed = true; self.changed = true;
last.cons.pop(); last.cons.pop();
} }
@ -215,12 +247,23 @@ impl Optimizer<'_> {
/// If a case ends with break but content is same with the consequtive case /// If a case ends with break but content is same with the consequtive case
/// except the break statement, we merge them. /// except the break statement, we merge them.
fn merge_cases_with_same_cons(&mut self, cases: &mut Vec<SwitchCase>) { fn merge_cases_with_same_cons(&mut self, cases: &mut Vec<SwitchCase>) {
let stop_pos = cases.iter().position(|case| match case.test.as_deref() {
Some(Expr::Update(..)) => true,
_ => false,
});
let mut found = None; let mut found = None;
'l: for (li, l) in cases.iter().enumerate().rev() { 'l: for (li, l) in cases.iter().enumerate().rev() {
if l.cons.is_empty() { if l.cons.is_empty() {
continue; continue;
} }
if let Some(stop_pos) = stop_pos {
if li > stop_pos {
continue;
}
}
if let Some(l_last) = l.cons.last() { if let Some(l_last) = l.cons.last() {
match l_last { match l_last {
Stmt::Break(BreakStmt { label: None, .. }) => {} Stmt::Break(BreakStmt { label: None, .. }) => {}
@ -253,7 +296,7 @@ impl Optimizer<'_> {
if let Some(idx) = found { if let Some(idx) = found {
self.changed = true; self.changed = true;
log::trace!("switches: Merging cases with same cons"); log::debug!("switches: Merging cases with same cons");
cases[idx].cons.clear(); cases[idx].cons.clear();
} }
} }

View File

@ -1,19 +1,77 @@
use crate::compress::optimize::util::class_has_side_effect;
use crate::option::PureGetterOption;
use super::Optimizer; use super::Optimizer;
use crate::compress::optimize::util::class_has_side_effect;
use crate::debug::dump;
use crate::option::PureGetterOption;
use crate::util::has_mark;
use swc_atoms::js_word; use swc_atoms::js_word;
use swc_common::Span;
use swc_common::DUMMY_SP; use swc_common::DUMMY_SP;
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::contains_ident_ref; use swc_ecma_utils::contains_ident_ref;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::StmtLike;
use swc_ecma_visit::noop_visit_mut_type; use swc_ecma_visit::noop_visit_mut_type;
use swc_ecma_visit::VisitMut; use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith; use swc_ecma_visit::VisitMutWith;
/// Methods related to the option `unused`. /// Methods related to the option `unused`.
impl Optimizer<'_> { impl Optimizer<'_> {
pub(super) fn drop_useless_blocks<T>(&mut self, stmts: &mut Vec<T>)
where
T: StmtLike,
{
fn is_inliable(b: &BlockStmt) -> bool {
b.stmts.iter().all(|s| match s {
Stmt::Decl(Decl::Fn(FnDecl {
ident:
Ident {
sym: js_word!("undefined"),
..
},
..
})) => false,
Stmt::Decl(
Decl::Var(VarDecl {
kind: VarDeclKind::Var,
..
})
| Decl::Fn(..),
) => true,
Stmt::Decl(..) => false,
_ => true,
})
}
if stmts.iter().all(|stmt| match stmt.as_stmt() {
Some(Stmt::Block(b)) if is_inliable(b) => false,
_ => true,
}) {
return;
}
self.changed = true;
log::debug!("Dropping useless block");
let mut new = vec![];
for stmt in stmts.take() {
match stmt.try_into_stmt() {
Ok(v) => match v {
Stmt::Block(v) if is_inliable(&v) => {
new.extend(v.stmts.into_iter().map(T::from_stmt));
}
_ => new.push(T::from_stmt(v)),
},
Err(v) => {
new.push(v);
}
}
}
*stmts = new;
}
pub(super) fn drop_unused_stmt_at_end_of_fn(&mut self, s: &mut Stmt) { pub(super) fn drop_unused_stmt_at_end_of_fn(&mut self, s: &mut Stmt) {
match s { match s {
Stmt::Return(r) => match r.arg.as_deref_mut() { Stmt::Return(r) => match r.arg.as_deref_mut() {
@ -22,7 +80,7 @@ impl Optimizer<'_> {
op: op!("void"), op: op!("void"),
arg, arg,
})) => { })) => {
log::trace!("unused: Removing `return void` in end of a function"); log::debug!("unused: Removing `return void` in end of a function");
self.changed = true; self.changed = true;
*s = Stmt::Expr(ExprStmt { *s = Stmt::Expr(ExprStmt {
span: *span, span: *span,
@ -48,7 +106,7 @@ impl Optimizer<'_> {
.. ..
}) => {} }) => {}
_ => { _ => {
self.drop_unused_vars(&mut var.name, Some(init)); self.drop_unused_vars(var.span, &mut var.name, Some(init));
if var.name.is_invalid() { if var.name.is_invalid() {
let side_effects = self.ignore_return_value(init); let side_effects = self.ignore_return_value(init);
@ -62,7 +120,7 @@ impl Optimizer<'_> {
} }
}, },
None => { None => {
self.drop_unused_vars(&mut var.name, var.init.as_deref_mut()); self.drop_unused_vars(var.span, &mut var.name, var.init.as_deref_mut());
} }
} }
} }
@ -87,21 +145,43 @@ impl Optimizer<'_> {
return; return;
} }
self.take_pat_if_unused(pat, None) self.take_pat_if_unused(DUMMY_SP, pat, None)
} }
pub(super) fn drop_unused_vars(&mut self, name: &mut Pat, init: Option<&mut Expr>) { pub(super) fn drop_unused_vars(
if !self.options.unused || self.ctx.in_var_decl_of_for_in_or_of_loop || self.ctx.is_exported &mut self,
var_declarator_span: Span,
name: &mut Pat,
init: Option<&mut Expr>,
) {
let has_mark = has_mark(var_declarator_span, self.marks.non_top_level);
if !has_mark {
if !self.options.unused
|| self.ctx.in_var_decl_of_for_in_or_of_loop
|| self.ctx.is_exported
{ {
return; return;
} }
}
if cfg!(feature = "debug") {
log::trace!("unused: drop_unused_vars({})", dump(&*name));
}
// Top-level // Top-level
if !has_mark {
match self.ctx.var_kind { match self.ctx.var_kind {
Some(VarDeclKind::Var) => { Some(VarDeclKind::Var) => {
if (!self.options.top_level() && self.options.top_retain.is_empty()) if (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level() && self.ctx.in_top_level()
{ {
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving `var` `{}` because it's top-level",
dump(&*name)
);
}
return; return;
} }
} }
@ -112,6 +192,7 @@ impl Optimizer<'_> {
} }
None => {} None => {}
} }
}
if let Some(scope) = self if let Some(scope) = self
.data .data
@ -127,12 +208,12 @@ impl Optimizer<'_> {
return; return;
} }
self.take_pat_if_unused(name, init); self.take_pat_if_unused(var_declarator_span, name, init);
} }
pub(super) fn drop_unused_params(&mut self, params: &mut Vec<Param>) { pub(super) fn drop_unused_params(&mut self, params: &mut Vec<Param>) {
for param in params.iter_mut().rev() { for param in params.iter_mut().rev() {
self.take_pat_if_unused(&mut param.pat, None); self.take_pat_if_unused(DUMMY_SP, &mut param.pat, None);
if !param.pat.is_invalid() { if !param.pat.is_invalid() {
return; return;
@ -140,7 +221,13 @@ impl Optimizer<'_> {
} }
} }
pub(super) fn take_pat_if_unused(&mut self, name: &mut Pat, mut init: Option<&mut Expr>) { /// `parent_span` should be [Span] of [VarDeclarator] or [AssignExpr]
pub(super) fn take_pat_if_unused(
&mut self,
parent_span: Span,
name: &mut Pat,
mut init: Option<&mut Expr>,
) {
let had_value = init.is_some(); let had_value = init.is_some();
let can_drop_children = had_value; let can_drop_children = had_value;
@ -153,7 +240,9 @@ impl Optimizer<'_> {
match name { match name {
Pat::Ident(i) => { Pat::Ident(i) => {
if self.options.top_retain.contains(&i.id.sym) { if !has_mark(parent_span, self.marks.non_top_level)
&& self.options.top_retain.contains(&i.id.sym)
{
return; return;
} }
@ -165,7 +254,7 @@ impl Optimizer<'_> {
.unwrap_or(false) .unwrap_or(false)
{ {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"unused: Dropping a variable '{}{:?}' because it is not used", "unused: Dropping a variable '{}{:?}' because it is not used",
i.id.sym, i.id.sym,
i.id.span.ctxt i.id.span.ctxt
@ -173,6 +262,10 @@ impl Optimizer<'_> {
// This will remove variable. // This will remove variable.
name.take(); name.take();
return; return;
} else {
if cfg!(feature = "debug") {
log::trace!("unused: Cannot drop ({}) becaue it's used", dump(&*i));
}
} }
} }
@ -188,7 +281,7 @@ impl Optimizer<'_> {
.as_mut() .as_mut()
.and_then(|expr| self.access_numeric_property(expr, idx)); .and_then(|expr| self.access_numeric_property(expr, idx));
self.take_pat_if_unused(p, elem); self.take_pat_if_unused(parent_span, p, elem);
} }
None => {} None => {}
} }
@ -224,7 +317,7 @@ impl Optimizer<'_> {
continue; continue;
} }
self.take_pat_if_unused(&mut p.value, prop); self.take_pat_if_unused(parent_span, &mut p.value, prop);
} }
ObjectPatProp::Assign(_) => {} ObjectPatProp::Assign(_) => {}
ObjectPatProp::Rest(_) => {} ObjectPatProp::Rest(_) => {}
@ -303,7 +396,7 @@ impl Optimizer<'_> {
.unwrap_or(false) .unwrap_or(false)
{ {
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"unused: Dropping a decl '{}{:?}' because it is not used", "unused: Dropping a decl '{}{:?}' because it is not used",
ident.sym, ident.sym,
ident.span.ctxt ident.span.ctxt
@ -327,7 +420,14 @@ impl Optimizer<'_> {
} }
pub(super) fn drop_unused_assignments(&mut self, e: &mut Expr) { pub(super) fn drop_unused_assignments(&mut self, e: &mut Expr) {
if !self.options.unused { let assign = match e {
Expr::Assign(e) => e,
_ => return,
};
let has_mark = has_mark(assign.span, self.marks.non_top_level);
if !has_mark && !self.options.unused {
return; return;
} }
@ -344,19 +444,36 @@ impl Optimizer<'_> {
return; return;
} }
if (!self.options.top_level() && self.options.top_retain.is_empty()) if cfg!(feature = "debug") {
log::trace!(
"unused: drop_unused_assignments: Target: `{}`",
dump(&assign.left)
)
}
if !has_mark
&& (!self.options.top_level() && self.options.top_retain.is_empty())
&& self.ctx.in_top_level() && self.ctx.in_top_level()
{ {
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving assignment to `{}` because it's top-level",
dump(&assign.left)
)
}
return; return;
} }
let assign = match e {
Expr::Assign(e) => e,
_ => return,
};
match &mut assign.left { match &mut assign.left {
PatOrExpr::Expr(_) => return, PatOrExpr::Expr(_) => {
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving assignment to `{}` because it's an expression",
dump(&assign.left)
)
}
return;
}
PatOrExpr::Pat(left) => match &**left { PatOrExpr::Pat(left) => match &**left {
Pat::Ident(i) => { Pat::Ident(i) => {
if self.options.top_retain.contains(&i.id.sym) { if self.options.top_retain.contains(&i.id.sym) {
@ -369,7 +486,7 @@ impl Optimizer<'_> {
.and_then(|data| data.vars.get(&i.to_id())) .and_then(|data| data.vars.get(&i.to_id()))
{ {
if var.is_fn_local && var.usage_count == 0 { if var.is_fn_local && var.usage_count == 0 {
log::trace!( log::debug!(
"unused: Dropping assignment to var '{}{:?}', which is never used", "unused: Dropping assignment to var '{}{:?}', which is never used",
i.id.sym, i.id.sym,
i.id.span.ctxt i.id.span.ctxt
@ -377,6 +494,14 @@ impl Optimizer<'_> {
self.changed = true; self.changed = true;
*e = *assign.right.take(); *e = *assign.right.take();
return; return;
} else {
if cfg!(feature = "debug") {
log::trace!(
"unused: Preserving assignment to `{}` because of usage: {:?}",
dump(&assign.left),
var
)
}
} }
} }
} }
@ -405,7 +530,7 @@ impl Optimizer<'_> {
if can_remove_ident { if can_remove_ident {
self.changed = true; self.changed = true;
log::trace!("Removing ident of an class / function expression"); log::debug!("Removing ident of an class / function expression");
*name = None; *name = None;
} }
} }
@ -429,7 +554,7 @@ impl Optimizer<'_> {
} }
self.changed = true; self.changed = true;
log::trace!( log::debug!(
"unused: Removing the name of a function expression because it's not used by \ "unused: Removing the name of a function expression because it's not used by \
it'" it'"
); );

View File

@ -12,6 +12,12 @@ use swc_ecma_utils::prop_name_eq;
use swc_ecma_utils::ExprExt; use swc_ecma_utils::ExprExt;
impl<'b> Optimizer<'b> { impl<'b> Optimizer<'b> {
pub(super) fn line_col(&self, span: Span) -> String {
let loc = self.cm.lookup_char_pos(span.lo);
format!("{}:{}", loc.line, loc.col_display)
}
pub(super) fn access_property<'e>( pub(super) fn access_property<'e>(
&mut self, &mut self,
expr: &'e mut Expr, expr: &'e mut Expr,
@ -224,6 +230,40 @@ pub(crate) fn class_has_side_effect(c: &Class) -> bool {
false false
} }
pub(crate) fn get_lhs_ident(e: &PatOrExpr) -> Option<&Ident> {
match e {
PatOrExpr::Expr(v) => match &**v {
Expr::Ident(i) => Some(i),
_ => None,
},
PatOrExpr::Pat(v) => match &**v {
Pat::Ident(i) => Some(&i.id),
Pat::Expr(v) => match &**v {
Expr::Ident(i) => Some(i),
_ => None,
},
_ => None,
},
}
}
pub(crate) fn get_lhs_ident_mut(e: &mut PatOrExpr) -> Option<&mut Ident> {
match e {
PatOrExpr::Expr(v) => match &mut **v {
Expr::Ident(i) => Some(i),
_ => None,
},
PatOrExpr::Pat(v) => match &mut **v {
Pat::Ident(i) => Some(&mut i.id),
Pat::Expr(v) => match &mut **v {
Expr::Ident(i) => Some(i),
_ => None,
},
_ => None,
},
}
}
pub(crate) fn is_valid_for_lhs(e: &Expr) -> bool { pub(crate) fn is_valid_for_lhs(e: &Expr) -> bool {
match e { match e {
Expr::Lit(..) => return false, Expr::Lit(..) => return false,

View File

@ -41,6 +41,10 @@ pub(crate) fn dump<N>(node: &N) -> String
where where
N: swc_ecma_codegen::Node + Clone + VisitMutWith<DropSpan> + VisitMutWith<Debugger>, N: swc_ecma_codegen::Node + Clone + VisitMutWith<DropSpan> + VisitMutWith<Debugger>,
{ {
if !cfg!(feature = "debug") {
return String::new();
}
let mut node = node.clone(); let mut node = node.clone();
node.visit_mut_with(&mut Debugger); node.visit_mut_with(&mut Debugger);
node = drop_span(node); node = drop_span(node);
@ -109,5 +113,5 @@ pub(crate) fn invoke(module: &Module) {
); );
} }
log::debug!("[SWC_RUN]\n{}", String::from_utf8_lossy(&output.stdout)) log::info!("[SWC_RUN]\n{}", String::from_utf8_lossy(&output.stdout))
} }

View File

@ -14,6 +14,7 @@
use crate::compress::compressor; use crate::compress::compressor;
use crate::hygiene::unique_marker; use crate::hygiene::unique_marker;
use crate::marks::Marks;
use crate::option::ExtraOptions; use crate::option::ExtraOptions;
use crate::option::MinifyOptions; use crate::option::MinifyOptions;
use crate::pass::compute_char_freq::compute_char_freq; use crate::pass::compute_char_freq::compute_char_freq;
@ -23,9 +24,13 @@ use crate::pass::hygiene::hygiene_optimizer;
pub use crate::pass::hygiene::optimize_hygiene; pub use crate::pass::hygiene::optimize_hygiene;
use crate::pass::mangle_names::name_mangler; use crate::pass::mangle_names::name_mangler;
use crate::pass::mangle_props::mangle_properties; use crate::pass::mangle_props::mangle_properties;
use crate::pass::single::single_pass_optimizer; use crate::pass::precompress::precompress_optimizer;
use analyzer::analyze; use analyzer::analyze;
use pass::postcompress::postcompress_optimizer;
use std::time::Instant;
use swc_common::comments::Comments; use swc_common::comments::Comments;
use swc_common::sync::Lrc;
use swc_common::SourceMap;
use swc_ecma_ast::Module; use swc_ecma_ast::Module;
use swc_ecma_visit::FoldWith; use swc_ecma_visit::FoldWith;
use swc_ecma_visit::VisitMutWith; use swc_ecma_visit::VisitMutWith;
@ -35,6 +40,7 @@ mod analyzer;
mod compress; mod compress;
mod debug; mod debug;
mod hygiene; mod hygiene;
mod marks;
pub mod option; pub mod option;
mod pass; mod pass;
pub mod timing; pub mod timing;
@ -43,11 +49,15 @@ mod util;
#[inline] #[inline]
pub fn optimize( pub fn optimize(
mut m: Module, mut m: Module,
cm: Lrc<SourceMap>,
comments: Option<&dyn Comments>, comments: Option<&dyn Comments>,
mut timings: Option<&mut Timings>, mut timings: Option<&mut Timings>,
options: &MinifyOptions, options: &MinifyOptions,
extra: &ExtraOptions, extra: &ExtraOptions,
) -> Module { ) -> Module {
let marks = Marks::new();
let start = Instant::now();
if let Some(defs) = options.compress.as_ref().map(|c| &c.global_defs) { if let Some(defs) = options.compress.as_ref().map(|c| &c.global_defs) {
// Apply global defs. // Apply global defs.
// //
@ -60,10 +70,13 @@ pub fn optimize(
m.visit_mut_with(&mut global_defs::globals_defs(defs, extra.top_level_mark)); m.visit_mut_with(&mut global_defs::globals_defs(defs, extra.top_level_mark));
} }
} }
log::info!("global_defs took {:?}", Instant::now() - start);
m.visit_mut_with(&mut single_pass_optimizer( if let Some(options) = &options.compress {
options.compress.clone().unwrap_or_default(), let start = Instant::now();
)); m.visit_mut_with(&mut precompress_optimizer(options.clone()));
log::info!("precompress took {:?}", Instant::now() - start);
}
m.visit_mut_with(&mut unique_marker()); m.visit_mut_with(&mut unique_marker());
@ -95,8 +108,14 @@ pub fn optimize(
t.section("compress"); t.section("compress");
} }
if let Some(options) = &options.compress { if let Some(options) = &options.compress {
m = m.fold_with(&mut compressor(&options, comments)); let start = Instant::now();
m = m.fold_with(&mut compressor(cm.clone(), marks, &options, comments));
log::info!("compressor took {:?}", Instant::now() - start);
// Again, we don't need to validate ast // Again, we don't need to validate ast
let start = Instant::now();
m.visit_mut_with(&mut postcompress_optimizer(options));
log::info!("postcompressor took {:?}", Instant::now() - start);
} }
if let Some(ref mut _t) = timings { if let Some(ref mut _t) = timings {

View File

@ -0,0 +1,18 @@
use swc_common::Mark;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Marks {
/// [Mark] applied to non-top level varaibles which is injected while
/// inlining.
pub(crate) non_top_level: Mark,
}
impl Marks {
pub fn new() -> Marks {
fn m() -> Mark {
Mark::fresh(Mark::root())
}
Marks { non_top_level: m() }
}
}

View File

@ -55,6 +55,9 @@ pub struct MangleOptions {
#[serde(default, alias = "keep_fnames")] #[serde(default, alias = "keep_fnames")]
pub keep_fn_names: bool, pub keep_fn_names: bool,
#[serde(default, alias = "keep_private_props")]
pub keep_private_props: bool,
#[serde(default, alias = "ie8")] #[serde(default, alias = "ie8")]
pub ie8: bool, pub ie8: bool,
@ -171,6 +174,7 @@ pub struct CompressOptions {
#[serde(alias = "hoist_vars")] #[serde(alias = "hoist_vars")]
pub hoist_vars: bool, pub hoist_vars: bool,
/// No effect.
#[serde(default)] #[serde(default)]
#[serde(alias = "ie8")] #[serde(alias = "ie8")]
pub ie8: bool, pub ie8: bool,

View File

@ -40,6 +40,21 @@ impl HygieneAnalyzer<'_> {
let ret = op(self); let ret = op(self);
let mut ids = vec![];
{
let scope = self.scope();
for (sym, ctxts) in &scope.declared_symbols {
if ctxts.len() == 1 {
let id = (sym.clone(), *ctxts.iter().next().unwrap());
ids.push(id)
}
}
}
for id in ids {
self.hygiene.preserved.remove(&id);
self.hygiene.modified.insert(id);
}
self.cur_scope = old; self.cur_scope = old;
ret ret

View File

@ -69,7 +69,7 @@ impl VisitMut for Optimizer {
} }
fn visit_mut_module(&mut self, n: &mut Module) { fn visit_mut_module(&mut self, n: &mut Module) {
log::debug!("hygiene: Analyzing span hygiene"); log::info!("hygiene: Analyzing span hygiene");
let start = Instant::now(); let start = Instant::now();
let mut analyzer = HygieneAnalyzer { let mut analyzer = HygieneAnalyzer {
@ -82,12 +82,12 @@ impl VisitMut for Optimizer {
self.hygiene = analyzer.hygiene; self.hygiene = analyzer.hygiene;
let end = Instant::now(); let end = Instant::now();
log::debug!("hygiene: Span hygiene analysis took {:?}", end - start); log::info!("hygiene: Span hygiene analysis took {:?}", end - start);
let start = end; let start = end;
log::debug!("hygiene: Optimizing span hygiene"); log::info!("hygiene: Optimizing span hygiene");
n.visit_mut_children_with(self); n.visit_mut_children_with(self);
let end = Instant::now(); let end = Instant::now();
log::debug!("hygiene: Span hygiene optimiation took {:?}", end - start); log::info!("hygiene: Span hygiene optimiation took {:?}", end - start);
} }
} }

View File

@ -28,9 +28,12 @@ pub fn name_mangler(options: MangleOptions, _char_freq_info: CharFreqInfo) -> im
struct Mangler { struct Mangler {
options: MangleOptions, options: MangleOptions,
n: usize, n: usize,
private_n: usize,
preserved: FxHashSet<Id>, preserved: FxHashSet<Id>,
preserved_symbols: FxHashSet<JsWord>, preserved_symbols: FxHashSet<JsWord>,
renamed: FxHashMap<Id, JsWord>, renamed: FxHashMap<Id, JsWord>,
renamed_private: FxHashMap<Id, JsWord>,
data: Option<ProgramData>, data: Option<ProgramData>,
} }
@ -65,6 +68,23 @@ impl Mangler {
break; break;
} }
} }
fn rename_private(&mut self, private_name: &mut PrivateName) {
let id = private_name.id.to_id();
let new_sym = if let Some(cached) = self.renamed_private.get(&id) {
cached.clone()
} else {
let sym: JsWord = base54(self.private_n).into();
self.private_n += 1;
self.renamed_private.insert(id.clone(), sym.clone());
sym
};
private_name.id.sym = new_sym;
}
} }
impl VisitMut for Mangler { impl VisitMut for Mangler {
@ -95,6 +115,12 @@ impl VisitMut for Mangler {
} }
} }
fn visit_mut_private_name(&mut self, private_name: &mut PrivateName) {
if !self.options.keep_private_props {
self.rename_private(private_name);
}
}
fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) { fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) {
self.rename(&mut n.ident); self.rename(&mut n.ident);
n.function.visit_mut_with(self); n.function.visit_mut_with(self);

View File

@ -6,8 +6,8 @@ use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use swc_atoms::JsWord; use swc_atoms::JsWord;
use swc_ecma_ast::{ use swc_ecma_ast::{
CallExpr, Expr, ExprOrSuper, Ident, KeyValueProp, Lit, MemberExpr, Module, PrivateName, Prop, CallExpr, Expr, ExprOrSuper, Ident, KeyValueProp, Lit, MemberExpr, Module, Prop, PropName, Str,
PropName, Str, StrKind, StrKind,
}; };
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::{VisitMut, VisitMutWith}; use swc_ecma_visit::{VisitMut, VisitMutWith};
@ -37,11 +37,9 @@ struct ManglePropertiesState {
// Cache of already mangled names // Cache of already mangled names
cache: HashMap<JsWord, JsWord>, cache: HashMap<JsWord, JsWord>,
private_cache: HashMap<JsWord, JsWord>,
// Numbers to pass to base54() // Numbers to pass to base54()
n: usize, n: usize,
private_n: usize,
} }
impl ManglePropertiesState { impl ManglePropertiesState {
@ -103,23 +101,6 @@ impl ManglePropertiesState {
None None
} }
} }
fn gen_private_name(&mut self, name: &JsWord) -> JsWord {
// Always mangleable
if let Some(cached) = self.private_cache.get(&name) {
cached.clone()
} else {
let private_n = self.private_n;
self.private_n += 1;
let mangled_name: JsWord = base54(private_n).into();
self.private_cache
.insert(name.clone(), mangled_name.clone());
mangled_name
}
}
} }
pub fn mangle_properties<'a>(m: &mut Module, options: ManglePropertiesOptions) { pub fn mangle_properties<'a>(m: &mut Module, options: ManglePropertiesOptions) {
@ -309,8 +290,4 @@ impl VisitMut for Mangler<'_> {
} }
} }
} }
fn visit_mut_private_name(&mut self, private_name: &mut PrivateName) {
private_name.id.sym = self.state.gen_private_name(&private_name.id.sym);
}
} }

View File

@ -4,4 +4,5 @@ pub mod global_defs;
pub mod hygiene; pub mod hygiene;
pub mod mangle_names; pub mod mangle_names;
pub mod mangle_props; pub mod mangle_props;
pub mod single; pub mod postcompress;
pub mod precompress;

View File

@ -0,0 +1,76 @@
use crate::option::CompressOptions;
use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
pub fn postcompress_optimizer<'a>(options: &'a CompressOptions) -> impl 'a + VisitMut {
PostcompressOptimizer { options }
}
struct PostcompressOptimizer<'a> {
options: &'a CompressOptions,
}
impl PostcompressOptimizer<'_> {
fn optimize_in_bool_ctx(&mut self, e: &mut Expr) {
if !self.options.bools {
return;
}
// Note: `||` is not handled because of precedence.
match e {
Expr::Bin(BinExpr {
op: op @ op!("&&"),
right,
left,
..
}) => {
match &**left {
Expr::Bin(BinExpr { op: op!("&&"), .. }) => return,
_ => {}
}
match &mut **right {
Expr::Unary(UnaryExpr {
op: op!("!"), arg, ..
}) if arg.is_ident() => {
let new_op = if *op == op!("&&") {
op!("||")
} else {
op!("&&")
};
log::debug!(
"bools: `(a {} !b)` => `(a {} b)` (in bool context)",
*op,
new_op
);
*op = new_op;
*right = arg.take();
return;
}
_ => {}
}
}
_ => {}
}
}
}
impl VisitMut for PostcompressOptimizer<'_> {
noop_visit_mut_type!();
fn visit_mut_cond_expr(&mut self, e: &mut CondExpr) {
e.visit_mut_children_with(self);
self.optimize_in_bool_ctx(&mut *e.test);
}
fn visit_mut_if_stmt(&mut self, s: &mut IfStmt) {
s.visit_mut_children_with(self);
self.optimize_in_bool_ctx(&mut *s.test);
}
}

View File

@ -9,10 +9,11 @@ use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::{ident::IdentLike, Id}; use swc_ecma_utils::{ident::IdentLike, Id};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
/// Optimizer invoked before invoking compressor.
/// ///
/// - Remove parens. /// - Remove parens.
pub fn single_pass_optimizer(options: CompressOptions) -> impl VisitMut { pub fn precompress_optimizer(options: CompressOptions) -> impl VisitMut {
SinglePassOptimizer { PrecompressOptimizer {
options, options,
data: Default::default(), data: Default::default(),
fn_decl_count: Default::default(), fn_decl_count: Default::default(),
@ -21,7 +22,7 @@ pub fn single_pass_optimizer(options: CompressOptions) -> impl VisitMut {
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct SinglePassOptimizer { struct PrecompressOptimizer {
options: CompressOptions, options: CompressOptions,
data: ProgramData, data: ProgramData,
fn_decl_count: FxHashMap<Id, usize>, fn_decl_count: FxHashMap<Id, usize>,
@ -33,7 +34,7 @@ struct Ctx {
in_var_pat: bool, in_var_pat: bool,
} }
impl VisitMut for SinglePassOptimizer { impl VisitMut for PrecompressOptimizer {
noop_visit_mut_type!(); noop_visit_mut_type!();
fn visit_mut_decl(&mut self, n: &mut Decl) { fn visit_mut_decl(&mut self, n: &mut Decl) {

View File

@ -9,6 +9,7 @@ use swc_ecma_ast::*;
use swc_ecma_transforms_base::ext::MapWithMut; use swc_ecma_transforms_base::ext::MapWithMut;
use swc_ecma_utils::ident::IdentLike; use swc_ecma_utils::ident::IdentLike;
use swc_ecma_utils::Id; use swc_ecma_utils::Id;
use swc_ecma_utils::ModuleItemLike;
use swc_ecma_utils::StmtLike; use swc_ecma_utils::StmtLike;
use swc_ecma_utils::Value; use swc_ecma_utils::Value;
use swc_ecma_visit::noop_visit_type; use swc_ecma_visit::noop_visit_type;
@ -26,6 +27,55 @@ pub(crate) fn make_number(span: Span, value: f64) -> Expr {
Expr::Lit(Lit::Num(Number { span, value })) Expr::Lit(Lit::Num(Number { span, value }))
} }
pub trait MoudleItemExt: StmtLike + ModuleItemLike {
fn as_module_decl(&self) -> Result<&ModuleDecl, &Stmt>;
fn from_module_item(item: ModuleItem) -> Self;
fn into_module_item(self) -> ModuleItem {
match self.into_module_decl() {
Ok(v) => ModuleItem::ModuleDecl(v),
Err(v) => ModuleItem::Stmt(v),
}
}
fn into_module_decl(self) -> Result<ModuleDecl, Stmt>;
}
impl MoudleItemExt for Stmt {
fn as_module_decl(&self) -> Result<&ModuleDecl, &Stmt> {
Err(self)
}
fn from_module_item(item: ModuleItem) -> Self {
item.expect_stmt()
}
fn into_module_decl(self) -> Result<ModuleDecl, Stmt> {
Err(self)
}
}
impl MoudleItemExt for ModuleItem {
fn as_module_decl(&self) -> Result<&ModuleDecl, &Stmt> {
match self {
ModuleItem::ModuleDecl(v) => Ok(v),
ModuleItem::Stmt(v) => Err(v),
}
}
fn from_module_item(item: ModuleItem) -> Self {
item
}
fn into_module_decl(self) -> Result<ModuleDecl, Stmt> {
match self {
ModuleItem::ModuleDecl(v) => Ok(v),
ModuleItem::Stmt(v) => Err(v),
}
}
}
/// ///
/// - `!0` for true /// - `!0` for true
/// - `!1` for false /// - `!1` for false
@ -45,6 +95,18 @@ pub(crate) trait ExprOptExt: Sized {
fn as_expr(&self) -> &Expr; fn as_expr(&self) -> &Expr;
fn as_mut(&mut self) -> &mut Expr; fn as_mut(&mut self) -> &mut Expr;
fn first_expr_mut(&mut self) -> &mut Expr {
let expr = self.as_mut();
match expr {
Expr::Seq(seq) => seq
.exprs
.first_mut()
.expect("Sequence expressions should have at least one element")
.first_expr_mut(),
expr => expr,
}
}
/// This returns itself for normal expressions and returns last exprssions /// This returns itself for normal expressions and returns last exprssions
/// for sequence expressions. /// for sequence expressions.
fn value_mut(&mut self) -> &mut Expr { fn value_mut(&mut self) -> &mut Expr {
@ -279,11 +341,36 @@ where
#[derive(Default)] #[derive(Default)]
pub(crate) struct IdentUsageCollector { pub(crate) struct IdentUsageCollector {
ids: FxHashSet<Id>, ids: FxHashSet<Id>,
ignore_nested: bool,
} }
impl Visit for IdentUsageCollector { impl Visit for IdentUsageCollector {
noop_visit_type!(); noop_visit_type!();
fn visit_block_stmt_or_expr(&mut self, n: &BlockStmtOrExpr, _: &dyn Node) {
if self.ignore_nested {
return;
}
n.visit_children_with(self);
}
fn visit_constructor(&mut self, n: &Constructor, _: &dyn Node) {
if self.ignore_nested {
return;
}
n.visit_children_with(self);
}
fn visit_function(&mut self, n: &Function, _: &dyn Node) {
if self.ignore_nested {
return;
}
n.visit_children_with(self);
}
fn visit_ident(&mut self, n: &Ident, _: &dyn Node) { fn visit_ident(&mut self, n: &Ident, _: &dyn Node) {
self.ids.insert(n.to_id()); self.ids.insert(n.to_id());
} }
@ -295,13 +382,37 @@ impl Visit for IdentUsageCollector {
n.prop.visit_with(n, self); n.prop.visit_with(n, self);
} }
} }
fn visit_prop_name(&mut self, n: &PropName, _: &dyn Node) {
match n {
PropName::Computed(..) => {
n.visit_children_with(self);
}
_ => {}
}
}
} }
pub(crate) fn idents_used_by<N>(n: &N) -> FxHashSet<Id> pub(crate) fn idents_used_by<N>(n: &N) -> FxHashSet<Id>
where where
N: VisitWith<IdentUsageCollector>, N: VisitWith<IdentUsageCollector>,
{ {
let mut v = IdentUsageCollector::default(); let mut v = IdentUsageCollector {
ignore_nested: false,
..Default::default()
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.ids
}
pub(crate) fn idents_used_by_ignoring_nested<N>(n: &N) -> FxHashSet<Id>
where
N: VisitWith<IdentUsageCollector>,
{
let mut v = IdentUsageCollector {
ignore_nested: true,
..Default::default()
};
n.visit_with(&Invalid { span: DUMMY_SP }, &mut v); n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
v.ids v.ids
} }

View File

@ -1,3 +1,5 @@
extern crate swc_node_base;
use ansi_term::Color; use ansi_term::Color;
use anyhow::bail; use anyhow::bail;
use anyhow::Context; use anyhow::Context;
@ -11,6 +13,7 @@ use std::panic::catch_unwind;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::time::Instant;
use swc_common::comments::SingleThreadedComments; use swc_common::comments::SingleThreadedComments;
use swc_common::errors::Handler; use swc_common::errors::Handler;
use swc_common::sync::Lrc; use swc_common::sync::Lrc;
@ -132,6 +135,8 @@ fn run(
let top_level_mark = Mark::fresh(Mark::root()); let top_level_mark = Mark::fresh(Mark::root());
let minification_start = Instant::now();
let lexer = Lexer::new( let lexer = Lexer::new(
Default::default(), Default::default(),
Default::default(), Default::default(),
@ -155,8 +160,10 @@ fn run(
_ => return None, _ => return None,
}; };
let optimization_start = Instant::now();
let output = optimize( let output = optimize(
program, program,
cm.clone(),
Some(&comments), Some(&comments),
None, None,
&MinifyOptions { &MinifyOptions {
@ -174,9 +181,22 @@ fn run(
..Default::default() ..Default::default()
}, },
&ExtraOptions { top_level_mark }, &ExtraOptions { top_level_mark },
) );
.fold_with(&mut hygiene()) let end = Instant::now();
.fold_with(&mut fixer(None)); log::info!(
"optimize({}) took {:?}",
input.display(),
end - optimization_start
);
let output = output.fold_with(&mut hygiene()).fold_with(&mut fixer(None));
let end = Instant::now();
log::info!(
"process({}) took {:?}",
input.display(),
end - minification_start
);
Some(output) Some(output)
} }
@ -199,8 +219,52 @@ fn stdout_of(code: &str) -> Result<String, Error> {
Ok(String::from_utf8_lossy(&actual_output.stdout).to_string()) Ok(String::from_utf8_lossy(&actual_output.stdout).to_string())
} }
fn find_config(dir: &Path) -> String {
let mut cur = Some(dir);
while let Some(dir) = cur {
let config = dir.join("config.json");
if config.exists() {
let config = read_to_string(&config).expect("failed to read config.json");
return config;
}
cur = dir.parent();
}
panic!("failed to find config file for {}", dir.display())
}
#[testing::fixture("tests/compress/fixture/**/input.js")] #[testing::fixture("tests/compress/fixture/**/input.js")]
fn base_fixture(input: PathBuf) { fn base_fixture(input: PathBuf) {
let dir = input.parent().unwrap();
let config = find_config(&dir);
eprintln!("---- {} -----\n{}", Color::Green.paint("Config"), config);
testing::run_test2(false, |cm, handler| {
let output = run(cm.clone(), &handler, &input, &config, None);
let output_module = match output {
Some(v) => v,
None => return Ok(()),
};
let output = print(cm.clone(), &[output_module.clone()], false);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
println!("{}", input.display());
NormalizedOutput::from(output)
.compare_to_file(dir.join("output.js"))
.unwrap();
Ok(())
})
.unwrap()
}
#[testing::fixture("tests/projects/files/*.js")]
fn projects(input: PathBuf) {
let dir = input.parent().unwrap(); let dir = input.parent().unwrap();
let config = dir.join("config.json"); let config = dir.join("config.json");
let config = read_to_string(&config).expect("failed to read config.json"); let config = read_to_string(&config).expect("failed to read config.json");
@ -213,14 +277,19 @@ fn base_fixture(input: PathBuf) {
None => return Ok(()), None => return Ok(()),
}; };
let output = print(cm.clone(), &[output_module.clone()]); let output = print(cm.clone(), &[output_module.clone()], false);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output); eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
println!("{}", input.display()); println!("{}", input.display());
NormalizedOutput::from(output) NormalizedOutput::from(output)
.compare_to_file(dir.join("output.js")) .compare_to_file(
dir.parent()
.unwrap()
.join("output")
.join(input.file_name().unwrap()),
)
.unwrap(); .unwrap();
Ok(()) Ok(())
@ -241,7 +310,7 @@ fn base_exec(input: PathBuf) {
let output = run(cm.clone(), &handler, &input, &config, None); let output = run(cm.clone(), &handler, &input, &config, None);
let output = output.expect("Parsing in base test should not fail"); let output = output.expect("Parsing in base test should not fail");
let output = print(cm.clone(), &[output]); let output = print(cm.clone(), &[output], false);
eprintln!( eprintln!(
"---- {} -----\n{}", "---- {} -----\n{}",
@ -297,7 +366,7 @@ fn fixture(input: PathBuf) {
None => return Ok(()), None => return Ok(()),
}; };
let output = print(cm.clone(), &[output_module.clone()]); let output = print(cm.clone(), &[output_module.clone()], false);
eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output); eprintln!("---- {} -----\n{}", Color::Green.paint("Ouput"), output);
@ -327,7 +396,7 @@ fn fixture(input: PathBuf) {
ModuleItem::Stmt(Stmt::Empty(..)) => false, ModuleItem::Stmt(Stmt::Empty(..)) => false,
_ => true, _ => true,
}); });
print(cm.clone(), &[expected]) print(cm.clone(), &[expected], false)
}; };
if output == expected { if output == expected {
@ -368,7 +437,7 @@ fn fixture(input: PathBuf) {
}); });
} }
let output_str = print(cm.clone(), &[drop_span(output_module.clone())]); let output_str = print(cm.clone(), &[drop_span(output_module.clone())], false);
assert_eq!(DebugUsingDisplay(&output_str), DebugUsingDisplay(&expected)); assert_eq!(DebugUsingDisplay(&output_str), DebugUsingDisplay(&expected));
@ -377,12 +446,12 @@ fn fixture(input: PathBuf) {
.unwrap() .unwrap()
} }
fn print<N: swc_ecma_codegen::Node>(cm: Lrc<SourceMap>, nodes: &[N]) -> String { fn print<N: swc_ecma_codegen::Node>(cm: Lrc<SourceMap>, nodes: &[N], minify: bool) -> String {
let mut buf = vec![]; let mut buf = vec![];
{ {
let mut emitter = Emitter { let mut emitter = Emitter {
cfg: Default::default(), cfg: swc_ecma_codegen::Config { minify },
cm: cm.clone(), cm: cm.clone(),
comments: None, comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)), wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),

View File

@ -0,0 +1,10 @@
function isUndefined(
value
) {
return "undefined" == typeof value;
}
function isDefined(
value
) {
return "undefined" != typeof value;
}

View File

@ -0,0 +1,6 @@
function isUndefined(value) {
return void 0 === value;
}
function isDefined(value) {
return void 0 !== value;
}

View File

@ -0,0 +1,12 @@
var h = destination.$$hashKey;
forEach(
destination,
function (
value, key
) {
delete destination[key];
}
);
for (var key in source) destination[key] = copy(
source[key]
);

View File

@ -0,0 +1,4 @@
var h = destination.$$hashKey;
for(var key in forEach(destination, function(value, key) {
delete destination[key];
}), source)destination[key] = copy(source[key]);

View File

@ -0,0 +1,5 @@
if (foo) {
if (bar) {
var baz = console.log('Foo bar');
}
}

View File

@ -0,0 +1 @@
if (foo && bar) var baz = console.log("Foo bar");

View File

@ -0,0 +1,15 @@
export const obj = {
eq: function (
jqLite,
index
) {
return index >= 0
? jqLite(
this[index]
)
: jqLite(
this[this.length + index]
);
},
}

View File

@ -0,0 +1,5 @@
export const obj = {
eq: function(jqLite, index) {
return jqLite(index >= 0 ? this[index] : this[this.length + index]);
}
};

View File

@ -0,0 +1,24 @@
function forEach(obj, iterator, context) {
var key;
if (obj) {
if (isFunction(obj)) {
for (key in obj) {
if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key);
}
}
} else if (obj.forEach && obj.forEach !== forEach) {
obj.forEach(iterator, context);
} else if (isArrayLike(obj)) {
for (key = 0; key < obj.length; key++)
iterator.call(context, obj[key], key);
} else {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key);
}
}
}
}
return obj;
}

View File

@ -0,0 +1,8 @@
function forEach(obj, iterator, context) {
var key;
if (obj) if (isFunction(obj)) for(key in obj)"prototype" != key && "length" != key && "name" != key && obj.hasOwnProperty(key) && iterator.call(context, obj[key], key);
else if (obj.forEach && obj.forEach !== forEach) obj.forEach(iterator, context);
else if (isArrayLike(obj)) for(key = 0; key < obj.length; key++)iterator.call(context, obj[key], key);
else for(key in obj)obj.hasOwnProperty(key) && iterator.call(context, obj[key], key);
return obj;
}

View File

@ -0,0 +1,2 @@
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');

View File

@ -0,0 +1,2 @@
var _ = root._;
_ || "undefined" == typeof require || (_ = require("underscore"));

View File

@ -0,0 +1,15 @@
const obj = {
_ensureElement: function () {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
}
}

View File

@ -0,0 +1,12 @@
const obj = {
_ensureElement: function() {
if (this.el) this.setElement(_.result(this, "el"), !1);
else {
var attrs = _.extend({
}, _.result(this, "attributes"));
this.id && (attrs.id = _.result(this, "id")), this.className && (attrs.class = _.result(this, "className"));
var $el = Backbone.$("<" + _.result(this, "tagName") + ">").attr(attrs);
this.setElement($el, !1);
}
}
};

View File

@ -0,0 +1,8 @@
const obj = {
navigate: function (fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = { trigger: !!options };
}
}

View File

@ -0,0 +1,8 @@
const obj = {
navigate: function(fragment, options) {
if (!History.started) return !1;
options && !0 !== options || (options = {
trigger: !!options
});
}
};

View File

@ -0,0 +1,6 @@
var names, i, l;
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
}

View File

@ -0,0 +1,2 @@
var names, i, l;
for(i = 0, l = (names = name ? [name] : _.keys(this._events)).length; i < l; i++)name = names[i];

View File

@ -0,0 +1,10 @@
export const obj = {
_validate: function (attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, _.extend(options, { validationError: error }));
return false;
}
}

View File

@ -0,0 +1,11 @@
export const obj = {
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return !0;
attrs = _.extend({
}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
return !error || (this.trigger("invalid", this, error, _.extend(options, {
validationError: error
})), !1);
}
};

View File

@ -0,0 +1,68 @@
export const obj = {
set: function (key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
}

View File

@ -0,0 +1,18 @@
export const obj = {
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (null == key) return this;
if ("object" == typeof key ? (attrs = key, options = val) : (attrs = {
})[key] = val, options || (options = {
}), !this._validate(attrs, options)) return !1;
for(attr in unset = options.unset, silent = options.silent, changes = [], changing = this._changing, this._changing = !0, changing || (this._previousAttributes = _.clone(this.attributes), this.changed = {
}), current = this.attributes, prev = this._previousAttributes, this.idAttribute in attrs && (this.id = attrs[this.idAttribute]), attrs)val = attrs[attr], _.isEqual(current[attr], val) || changes.push(attr), _.isEqual(prev[attr], val) ? delete this.changed[attr] : this.changed[attr] = val, unset ? delete current[attr] : current[attr] = val;
if (!silent) {
changes.length && (this._pending = !0);
for(var i = 0, l = changes.length; i < l; i++)this.trigger("change:" + changes[i], this, current[changes[i]], options);
}
if (changing) return this;
if (!silent) for(; this._pending;)this._pending = !1, this.trigger("change", this, options);
return this._pending = !1, this._changing = !1, this;
}
};

View File

@ -0,0 +1,12 @@
export const obj = {
changedAttributes: function (diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
}
}

View File

@ -0,0 +1,9 @@
export const obj = {
changedAttributes: function(diff) {
if (!diff) return !!this.hasChanged() && _.clone(this.changed);
var val, changed = !1, old = this._changing ? this._previousAttributes : this.attributes;
for(var attr in diff)_.isEqual(old[attr], val = diff[attr]) || ((changed || (changed = {
}))[attr] = val);
return changed;
}
};

View File

@ -0,0 +1,23 @@
export const obj = {
remove: function (models, options) {
var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = models[i] = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return singular ? models[0] : models;
},
}

View File

@ -0,0 +1,8 @@
export const obj = {
remove: function(models, options) {
var i, l, index, model, singular = !_.isArray(models);
for(options || (options = {
}), i = 0, l = (models = singular ? [models] : _.clone(models)).length; i < l; i++)(model = models[i] = this.get(models[i])) && (delete this._byId[model.id], delete this._byId[model.cid], index = this.indexOf(model), this.models.splice(index, 1), this.length--, options.silent || (options.index = index, model.trigger("remove", model, this, options)), this._removeReference(model));
return singular ? models[0] : models;
}
};

View File

@ -0,0 +1,15 @@
export const obj = {
create: function (model, options) {
options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false;
if (!options.wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function (model, resp, options) {
if (options.wait) collection.add(model, options);
if (success) success(model, resp, options);
};
model.save(null, options);
return model;
},
}

View File

@ -0,0 +1,11 @@
export const obj = {
create: function(model, options) {
if (options = options ? _.clone(options) : {
}, !(model = this._prepareModel(model, options))) return !1;
options.wait || this.add(model, options);
var collection = this, success = options.success;
return options.success = function(model, resp, options) {
options.wait && collection.add(model, options), success && success(model, resp, options);
}, model.save(null, options), model;
}
};

View File

@ -0,0 +1,11 @@
export const obj = {
_routeToRegExp: function (route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function (match, optional) {
return optional ? match : '([^\/]+)';
})
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
}
}

View File

@ -0,0 +1,7 @@
export const obj = {
_routeToRegExp: function(route) {
return route = route.replace(escapeRegExp, "\\$&").replace(optionalParam, "(?:$1)?").replace(namedParam, function(match, optional) {
return optional ? match : "([^/]+)";
}).replace(splatParam, "(.*?)"), new RegExp("^" + route + "$");
}
};

View File

@ -0,0 +1,40 @@
export const obj = {
navigate: function (fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = { trigger: !!options };
var url = this.root + (fragment = this.getFragment(fragment || ''));
// Strip the fragment of the query and hash for matching.
fragment = fragment.replace(pathStripper, '');
if (this.fragment === fragment) return;
this.fragment = fragment;
// Don't include a trailing slash on the root.
if (fragment === '' && url !== '/') url = url.slice(0, -1);
// If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if (!options.replace) this.iframe.document.open().close();
this._updateHash(this.iframe.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
if (options.trigger) return this.loadUrl(fragment);
},
}

View File

@ -0,0 +1,18 @@
export const obj = {
navigate: function(fragment, options) {
if (!History.started) return !1;
options && !0 !== options || (options = {
trigger: !!options
});
var url = this.root + (fragment = this.getFragment(fragment || ""));
if (fragment = fragment.replace(pathStripper, ""), this.fragment !== fragment) {
if (this.fragment = fragment, "" === fragment && "/" !== url && (url = url.slice(0, -1)), this._hasPushState) this.history[options.replace ? "replaceState" : "pushState"]({
}, document.title, url);
else {
if (!this._wantsHashChange) return this.location.assign(url);
this._updateHash(this.location, fragment, options.replace), this.iframe && fragment !== this.getFragment(this.getHash(this.iframe)) && (options.replace || this.iframe.document.open().close(), this._updateHash(this.iframe.location, fragment, options.replace));
}
if (options.trigger) return this.loadUrl(fragment);
}
}
};

View File

@ -0,0 +1,13 @@
function foo() {
if (!eventsApi(this, "on", name, [callback, context]) || !callback)
return this;
return (
this._events || (this._events = {}),
(this._events[name] || (this._events[name] = [])).push({
callback: callback,
context: context,
ctx: context || this,
}),
this
);
}

View File

@ -0,0 +1,9 @@
function foo() {
return eventsApi(this, "on", name, [callback,
context]) && callback && (this._events || (this._events = {
}), (this._events[name] || (this._events[name] = [])).push({
callback: callback,
context: context,
ctx: context || this
})), this;
}

View File

@ -0,0 +1,92 @@
export const E = {
set: function (models, options) {
options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options);
var singular = !_.isArray(models);
models = singular ? (models ? [models] : []) : _.clone(models);
var i, l, id, model, attrs, existing, sort;
var at = options.at;
var targetModel = this.model;
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, l = models.length; i < l; i++) {
attrs = models[i];
if (attrs instanceof Model) {
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute];
}
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(id)) {
if (remove) modelMap[existing.cid] = true;
if (merge) {
attrs = attrs === model ? model.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
if (order) order.push(existing || model);
}
// Remove nonexistent models if appropriate.
if (remove) {
for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
}
if (toRemove.length) this.remove(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || (order && order.length)) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (i = 0, l = orderedModels.length; i < l; i++) {
this.models.push(orderedModels[i]);
}
}
}
// Silently sort the collection if appropriate.
if (sort) this.sort({ silent: true });
// Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) {
for (i = 0, l = toAdd.length; i < l; i++) {
(model = toAdd[i]).trigger('add', model, this, options);
}
if (sort || (order && order.length)) this.trigger('sort', this, options);
}
// Return the added (or merged) model (or models).
return singular ? models[0] : models;
}
}

View File

@ -0,0 +1,35 @@
export const E = {
set: function(models, options) {
(options = _.defaults({
}, options, setOptions)).parse && (models = this.parse(models, options));
var singular = !_.isArray(models);
models = singular ? models ? [models] : [] : _.clone(models);
var i, l, id, model, attrs, existing, sort, at = options.at, targetModel = this.model, sortable = this.comparator && null == at && !1 !== options.sort, sortAttr = _.isString(this.comparator) ? this.comparator : null, toAdd = [], toRemove = [], modelMap = {
}, add = options.add, merge = options.merge, remove = options.remove, order = !sortable && !!add && !!remove && [];
for(i = 0, l = models.length; i < l; i++){
if (id = (attrs = models[i]) instanceof Model ? model = attrs : attrs[targetModel.prototype.idAttribute], existing = this.get(id)) remove && (modelMap[existing.cid] = !0), merge && (attrs = attrs === model ? model.attributes : attrs, options.parse && (attrs = existing.parse(attrs, options)), existing.set(attrs, options), sortable && !sort && existing.hasChanged(sortAttr) && (sort = !0)), models[i] = existing;
else if (add) {
if (!(model = models[i] = this._prepareModel(attrs, options))) continue;
toAdd.push(model), model.on("all", this._onModelEvent, this), this._byId[model.cid] = model, null != model.id && (this._byId[model.id] = model);
}
order && order.push(existing || model);
}
if (remove) {
for(i = 0, l = this.length; i < l; ++i)modelMap[(model = this.models[i]).cid] || toRemove.push(model);
toRemove.length && this.remove(toRemove, options);
}
if (toAdd.length || order && order.length) if (sortable && (sort = !0), this.length += toAdd.length, null != at) for(i = 0, l = toAdd.length; i < l; i++)this.models.splice(at + i, 0, toAdd[i]);
else {
order && (this.models.length = 0);
var orderedModels = order || toAdd;
for(i = 0, l = orderedModels.length; i < l; i++)this.models.push(orderedModels[i]);
}
if (sort && this.sort({
silent: !0
}), !options.silent) {
for(i = 0, l = toAdd.length; i < l; i++)(model = toAdd[i]).trigger("add", model, this, options);
(sort || order && order.length) && this.trigger("sort", this, options);
}
return singular ? models[0] : models;
}
};

View File

@ -0,0 +1,4 @@
if (!name && !callback && !context) {
console.log('foo')
}

View File

@ -0,0 +1 @@
name || callback || context || console.log("foo");

Some files were not shown because too many files have changed in this diff Show More