feat(es/transforms/optimization): Improve inline_globals (#2479)

swc_ecma_transforms_optimization:
 - `inline_globals`: Support inlining into shorthand properties.
 - `inline_globals`: Support replacing member expressions.

swc:
 - Add an option to disable simplifier when using `inline_globals`.

node_swc:
 - Improve error message on panic.
This commit is contained in:
Donny/강동윤 2021-10-19 23:58:10 +09:00 committed by GitHub
parent 9b96885171
commit b0361caa58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 270 additions and 97 deletions

6
Cargo.lock generated
View File

@ -883,6 +883,7 @@ dependencies = [
"autocfg 1.0.1",
"hashbrown",
"rayon",
"serde",
]
[[package]]
@ -2324,13 +2325,14 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "swc"
version = "0.75.0"
version = "0.76.0"
dependencies = [
"ahash",
"anyhow",
"base64 0.13.0",
"dashmap",
"either",
"indexmap",
"lru",
"once_cell",
"pathdiff",
@ -2878,7 +2880,7 @@ dependencies = [
[[package]]
name = "swc_ecma_transforms_optimization"
version = "0.58.0"
version = "0.58.1"
dependencies = [
"ahash",
"dashmap",

View File

@ -21,7 +21,7 @@ include = ["Cargo.toml", "src/**/*.rs"]
license = "Apache-2.0/MIT"
name = "swc"
repository = "https://github.com/swc-project/swc.git"
version = "0.75.0"
version = "0.76.0"
[lib]
name = "swc"
@ -51,6 +51,7 @@ anyhow = "1"
base64 = "0.13.0"
dashmap = "4.0.2"
either = "1"
indexmap = {version = "1", features = ["serde"]}
lru = "0.6.1"
once_cell = "1"
pathdiff = "0.2.0"

View File

@ -47,6 +47,7 @@
"elems",
"esbuild",
"esms",
"eval",
"Eval",
"exponentation",
"fargs",

View File

@ -6,7 +6,7 @@ edition = "2018"
license = "Apache-2.0/MIT"
name = "swc_ecma_transforms_optimization"
repository = "https://github.com/swc-project/swc.git"
version = "0.58.0"
version = "0.58.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]

View File

@ -2,21 +2,44 @@ use swc_atoms::{js_word, JsWord};
use swc_common::{
collections::{AHashMap, AHashSet},
sync::Lrc,
EqIgnoreSpan,
};
use swc_ecma_ast::*;
use swc_ecma_transforms_base::perf::Parallel;
use swc_ecma_transforms_macros::parallel;
use swc_ecma_utils::{collect_decls, Id};
use swc_ecma_utils::{collect_decls, ident::IdentLike, Id};
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
/// The key will be compared using [EqIgnoreSpan::eq_ignore_span], and matched
/// expressions will be replaced with the value.
pub type GlobalExprMap = Lrc<Vec<(Expr, Expr)>>;
/// Create a global inlining pass, which replaces expressions with the specified
/// value.
pub fn inline_globals(
envs: Lrc<AHashMap<JsWord, Expr>>,
globals: Lrc<AHashMap<JsWord, Expr>>,
typeofs: Lrc<AHashMap<JsWord, JsWord>>,
) -> impl Fold + VisitMut {
inline_globals2(envs, globals, Default::default(), typeofs)
}
/// Create a global inlining pass, which replaces expressions with the specified
/// value.
///
/// See [GlobalExprMap] for description.
///
/// Note: Values specified in `global_exprs` have higher precedence than
pub fn inline_globals2(
envs: Lrc<AHashMap<JsWord, Expr>>,
globals: Lrc<AHashMap<JsWord, Expr>>,
global_exprs: GlobalExprMap,
typeofs: Lrc<AHashMap<JsWord, JsWord>>,
) -> impl Fold + VisitMut {
as_folder(InlineGlobals {
envs,
globals,
global_exprs,
typeofs,
bindings: Default::default(),
})
@ -26,6 +49,8 @@ pub fn inline_globals(
struct InlineGlobals {
envs: Lrc<AHashMap<JsWord, Expr>>,
globals: Lrc<AHashMap<JsWord, Expr>>,
global_exprs: GlobalExprMap,
typeofs: Lrc<AHashMap<JsWord, JsWord>>,
bindings: Lrc<AHashSet<Id>>,
@ -44,14 +69,28 @@ impl VisitMut for InlineGlobals {
noop_visit_mut_type!();
fn visit_mut_expr(&mut self, expr: &mut Expr) {
expr.visit_mut_children_with(self);
match expr {
Expr::Ident(Ident { ref sym, span, .. }) => {
if self.bindings.contains(&(sym.clone(), span.ctxt)) {
return;
}
}
_ => {}
}
for (key, value) in self.global_exprs.iter() {
if key.eq_ignore_span(&*expr) {
*expr = value.clone();
expr.visit_mut_with(self);
return;
}
}
expr.visit_mut_children_with(self);
match expr {
Expr::Ident(Ident { ref sym, .. }) => {
// It's ok because we don't recurse into member expressions.
if let Some(value) = self.globals.get(sym) {
let mut value = value.clone();
@ -152,6 +191,30 @@ impl VisitMut for InlineGlobals {
module.visit_mut_children_with(self);
}
fn visit_mut_prop(&mut self, p: &mut Prop) {
p.visit_mut_children_with(self);
match p {
Prop::Shorthand(i) => {
if self.bindings.contains(&i.to_id()) {
return;
}
// It's ok because we don't recurse into member expressions.
if let Some(mut value) = self.globals.get(&i.sym).cloned().map(Box::new) {
value.visit_mut_with(self);
*p = Prop::KeyValue(KeyValueProp {
key: PropName::Ident(i.clone()),
value,
});
}
return;
}
_ => {}
}
}
fn visit_mut_script(&mut self, script: &mut Script) {
self.bindings = Lrc::new(collect_decls(&*script));
@ -212,12 +275,7 @@ mod tests {
test!(
::swc_ecma_parser::Syntax::default(),
|tester| as_folder(InlineGlobals {
envs: envs(tester, &[]),
globals: globals(tester, &[]),
typeofs: Default::default(),
bindings: Default::default()
}),
|tester| inline_globals(envs(tester, &[]), globals(tester, &[]), Default::default(),),
issue_215,
r#"if (process.env.x === 'development') {}"#,
r#"if (process.env.x === 'development') {}"#
@ -225,12 +283,11 @@ mod tests {
test!(
::swc_ecma_parser::Syntax::default(),
|tester| as_folder(InlineGlobals {
envs: envs(tester, &[("NODE_ENV", "development")]),
globals: globals(tester, &[]),
typeofs: Default::default(),
bindings: Default::default()
}),
|tester| inline_globals(
envs(tester, &[("NODE_ENV", "development")]),
globals(tester, &[]),
Default::default(),
),
node_env,
r#"if (process.env.NODE_ENV === 'development') {}"#,
r#"if ('development' === 'development') {}"#
@ -238,25 +295,23 @@ mod tests {
test!(
::swc_ecma_parser::Syntax::default(),
|tester| as_folder(InlineGlobals {
envs: envs(tester, &[]),
globals: globals(tester, &[("__DEBUG__", "true")]),
typeofs: Default::default(),
bindings: Default::default()
}),
inline_globals,
|tester| inline_globals(
envs(tester, &[]),
globals(tester, &[("__DEBUG__", "true")]),
Default::default(),
),
globals_simple,
r#"if (__DEBUG__) {}"#,
r#"if (true) {}"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|tester| as_folder(InlineGlobals {
envs: envs(tester, &[]),
globals: globals(tester, &[("debug", "true")]),
typeofs: Default::default(),
bindings: Default::default()
}),
|tester| inline_globals(
envs(tester, &[]),
globals(tester, &[("debug", "true")]),
Default::default(),
),
non_global,
r#"if (foo.debug) {}"#,
r#"if (foo.debug) {}"#
@ -264,12 +319,7 @@ mod tests {
test!(
Default::default(),
|tester| as_folder(InlineGlobals {
envs: envs(tester, &[]),
globals: globals(tester, &[]),
typeofs: Default::default(),
bindings: Default::default()
}),
|tester| inline_globals(envs(tester, &[]), globals(tester, &[]), Default::default(),),
issue_417_1,
"const test = process.env['x']",
"const test = process.env['x']"
@ -277,12 +327,11 @@ mod tests {
test!(
Default::default(),
|tester| as_folder(InlineGlobals {
envs: envs(tester, &[("x", "FOO")]),
globals: globals(tester, &[]),
typeofs: Default::default(),
bindings: Default::default()
}),
|tester| inline_globals(
envs(tester, &[("x", "FOO")]),
globals(tester, &[]),
Default::default(),
),
issue_417_2,
"const test = process.env['x']",
"const test = 'FOO'"

View File

@ -1,5 +1,7 @@
pub use self::{
const_modules::const_modules, inline_globals::inline_globals, json_parse::json_parse,
const_modules::const_modules,
inline_globals::{inline_globals, inline_globals2, GlobalExprMap},
json_parse::json_parse,
simplify::simplifier,
};

View File

@ -1,11 +1,11 @@
use crate::{
complete_output, get_compiler,
util::{CtxtExt, MapErr},
util::{try_with, CtxtExt, MapErr},
};
use napi::{CallContext, JsObject, Task};
use serde::Deserialize;
use std::sync::Arc;
use swc::{try_with_handler, TransformOutput};
use swc::TransformOutput;
use swc_common::{collections::AHashMap, sync::Lrc, FileName, SourceFile, SourceMap};
struct MinifyTask {
@ -48,7 +48,7 @@ impl Task for MinifyTask {
type JsValue = JsObject;
fn compute(&mut self) -> napi::Result<Self::Output> {
try_with_handler(self.c.cm.clone(), |handler| {
try_with(self.c.cm.clone(), |handler| {
let fm = self.code.to_file(self.c.cm.clone());
self.c.minify(fm, &handler, &self.opts)
@ -82,8 +82,7 @@ pub fn minify_sync(cx: CallContext) -> napi::Result<JsObject> {
let fm = code.to_file(c.cm.clone());
let output =
try_with_handler(c.cm.clone(), |handler| c.minify(fm, &handler, &opts)).convert_err()?;
let output = try_with(c.cm.clone(), |handler| c.minify(fm, &handler, &opts)).convert_err()?;
complete_output(&cx.env, output)
}

View File

@ -1,6 +1,6 @@
use crate::{
get_compiler,
util::{CtxtExt, MapErr},
util::{try_with, CtxtExt, MapErr},
};
use anyhow::Context as _;
use napi::{CallContext, Either, Env, JsObject, JsString, JsUndefined, Task};
@ -8,7 +8,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use swc::{config::ParseOptions, try_with_handler, Compiler};
use swc::{config::ParseOptions, Compiler};
use swc_common::{FileName, SourceFile};
use swc_ecma_ast::Program;
@ -38,7 +38,7 @@ impl Task for ParseTask {
type JsValue = JsString;
fn compute(&mut self) -> napi::Result<Self::Output> {
let program = try_with_handler(self.c.cm.clone(), |handler| {
let program = try_with(self.c.cm.clone(), |handler| {
self.c.parse_js(
self.fm.clone(),
&handler,
@ -63,7 +63,7 @@ impl Task for ParseFileTask {
type JsValue = JsString;
fn compute(&mut self) -> napi::Result<Self::Output> {
try_with_handler(self.c.cm.clone(), |handler| {
try_with(self.c.cm.clone(), |handler| {
self.c.run(|| {
let fm = self
.c
@ -125,7 +125,7 @@ pub fn parse_sync(cx: CallContext) -> napi::Result<JsString> {
FileName::Anon
};
let program = try_with_handler(c.cm.clone(), |handler| {
let program = try_with(c.cm.clone(), |handler| {
c.run(|| {
let fm = c.cm.new_source_file(filename, src);
c.parse_js(
@ -150,7 +150,7 @@ pub fn parse_file_sync(cx: CallContext) -> napi::Result<JsString> {
let options: ParseOptions = cx.get_deserialized(1)?;
let program = {
try_with_handler(c.cm.clone(), |handler| {
try_with(c.cm.clone(), |handler| {
let fm =
c.cm.load_file(Path::new(path.as_str()?))
.expect("failed to read program file");

View File

@ -1,6 +1,6 @@
use crate::{
complete_output, get_compiler,
util::{deserialize_json, CtxtExt, MapErr},
util::{deserialize_json, try_with, CtxtExt, MapErr},
};
use anyhow::{Context as _, Error};
use napi::{CallContext, Env, JsBoolean, JsObject, JsString, Task};
@ -9,7 +9,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use swc::{config::Options, try_with_handler, Compiler, TransformOutput};
use swc::{config::Options, Compiler, TransformOutput};
use swc_common::{FileName, SourceFile};
use swc_ecma_ast::Program;
@ -35,7 +35,7 @@ impl Task for TransformTask {
type JsValue = JsObject;
fn compute(&mut self) -> napi::Result<Self::Output> {
try_with_handler(self.c.cm.clone(), |handler| {
try_with(self.c.cm.clone(), |handler| {
self.c.run(|| match self.input {
Input::Program(ref s) => {
let program: Program =
@ -93,7 +93,7 @@ where
options.config.adjust(Path::new(&options.filename));
}
let output = try_with_handler(c.cm.clone(), |handler| {
let output = try_with(c.cm.clone(), |handler| {
c.run(|| {
if is_module.get_value()? {
let program: Program =

View File

@ -1,7 +1,35 @@
use anyhow::{Context, Error};
use anyhow::{anyhow, Context, Error};
use napi::{CallContext, JsBuffer, Status};
use serde::de::DeserializeOwned;
use std::any::type_name;
use std::{
any::type_name,
panic::{catch_unwind, AssertUnwindSafe},
};
use swc::try_with_handler;
use swc_common::{errors::Handler, sync::Lrc, SourceMap};
pub fn try_with<F, Ret>(cm: Lrc<SourceMap>, op: F) -> Result<Ret, Error>
where
F: FnOnce(&Handler) -> Result<Ret, Error>,
{
try_with_handler(cm, |handler| {
//
let result = catch_unwind(AssertUnwindSafe(|| op(handler)));
let p = match result {
Ok(v) => return v,
Err(v) => v,
};
if let Some(s) = p.downcast_ref::<String>() {
Err(anyhow!("failed to handle: {}", s))
} else if let Some(s) = p.downcast_ref::<&str>() {
Err(anyhow!("failed to handle: {}", s))
} else {
Err(anyhow!("failed to handle with unknown panic message"))
}
})
}
pub trait MapErr<T>: Into<Result<T, anyhow::Error>> {
fn convert_err(self) -> napi::Result<T> {

View File

@ -3,6 +3,7 @@ use crate::{builder::PassBuilder, SwcComments, SwcImportResolver};
use anyhow::{bail, Context, Error};
use dashmap::DashMap;
use either::Either;
use indexmap::IndexMap;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
@ -23,7 +24,7 @@ use swc_common::{
errors::Handler,
FileName, Mark, SourceMap,
};
use swc_ecma_ast::{Expr, ExprStmt, ModuleItem, Stmt};
use swc_ecma_ast::Expr;
use swc_ecma_ext_transforms::jest;
use swc_ecma_loader::resolvers::{
lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver,
@ -37,11 +38,12 @@ use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
use swc_ecma_transforms::{
hygiene, modules,
modules::{hoist::import_hoister, path::NodeImportResolver, util::Scope},
optimization::{const_modules, inline_globals, json_parse, simplifier},
optimization::{const_modules, json_parse, simplifier},
pass::{noop, Optional},
proposals::{decorators, export_default_from, import_assertions},
react, resolver_with_mark, typescript,
};
use swc_ecma_transforms_optimization::{inline_globals2, GlobalExprMap};
use swc_ecma_visit::Fold;
#[cfg(test)]
@ -218,7 +220,6 @@ impl Options {
transform.legacy_decorator = true;
}
let optimizer = transform.optimizer;
let enable_optimizer = optimizer.is_some();
let const_modules = {
let enabled = transform.const_modules.is_some();
@ -236,6 +237,8 @@ impl Options {
}
};
let enable_simplifier = optimizer.as_ref().map(|v| v.simplify).unwrap_or_default();
let optimization = {
let pass =
if let Some(opts) = optimizer.map(|o| o.globals.unwrap_or_else(Default::default)) {
@ -255,7 +258,7 @@ impl Options {
const_modules,
optimization,
Optional::new(export_default_from(), syntax.export_default_from()),
Optional::new(simplifier(Default::default()), enable_optimizer),
Optional::new(simplifier(Default::default()), enable_simplifier),
json_parse_pass
);
@ -968,6 +971,9 @@ pub struct OptimizerConfig {
#[serde(default)]
pub globals: Option<GlobalPassOption>,
#[serde(default = "true_by_default")]
pub simplify: bool,
#[serde(default)]
pub jsonify: Option<JsonifyOption>,
}
@ -987,7 +993,7 @@ fn default_jsonify_min_cost() -> usize {
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GlobalPassOption {
#[serde(default)]
pub vars: AHashMap<JsWord, JsWord>,
pub vars: IndexMap<JsWord, JsWord, ahash::RandomState>,
#[serde(default)]
pub envs: GlobalInliningPassEnvs,
@ -1016,6 +1022,28 @@ impl GlobalPassOption {
pub fn build(self, cm: &SourceMap, handler: &Handler) -> impl 'static + Fold {
type ValuesMap = Arc<AHashMap<JsWord, Expr>>;
fn expr(cm: &SourceMap, handler: &Handler, src: String) -> Box<Expr> {
let fm = cm.new_source_file(FileName::Anon, src);
let lexer = Lexer::new(
Syntax::Es(Default::default()),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut p = Parser::new_from(lexer);
let expr = p.parse_expr();
for e in p.take_errors() {
e.into_diagnostic(handler).emit()
}
match expr {
Ok(v) => v,
_ => panic!("{} is not a valid expression", fm.src),
}
}
fn mk_map(
cm: &SourceMap,
handler: &Handler,
@ -1031,36 +1059,10 @@ impl GlobalPassOption {
(*v).into()
};
let v_str = v.clone();
let fm = cm.new_source_file(FileName::Custom(format!("GLOBAL.{}", k)), v);
let lexer = Lexer::new(
Syntax::Es(Default::default()),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut p = Parser::new_from(lexer);
let module = p.parse_module();
let e = expr(cm, handler, v_str);
for e in p.take_errors() {
e.into_diagnostic(handler).emit()
}
let mut module = module
.map_err(|e| e.into_diagnostic(handler).emit())
.unwrap_or_else(|()| {
panic!(
"failed to parse global variable {}=`{}` as module",
k, v_str
)
});
let expr = match module.body.pop() {
Some(ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. }))) => *expr,
_ => panic!("{} is not a valid expression", v_str),
};
m.insert((*k).into(), expr);
m.insert((*k).into(), *e);
}
Arc::new(m)
@ -1117,6 +1119,37 @@ impl GlobalPassOption {
}
};
let global_exprs = {
static CACHE: Lazy<DashMap<Vec<(JsWord, JsWord)>, GlobalExprMap, ahash::RandomState>> =
Lazy::new(|| Default::default());
let cache_key = self
.vars
.iter()
.filter(|(k, _)| k.contains('.'))
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<Vec<_>>();
if let Some(v) = CACHE.get(&cache_key) {
(*v).clone()
} else {
let map = self
.vars
.iter()
.filter(|(k, _)| k.contains('.'))
.map(|(k, v)| {
(
*expr(cm, handler, k.to_string()),
*expr(cm, handler, v.to_string()),
)
})
.collect::<Vec<_>>();
let map = Arc::new(map);
CACHE.insert(cache_key, map.clone());
map
}
};
let global_map = {
static CACHE: Lazy<DashMap<Vec<(JsWord, JsWord)>, ValuesMap, ahash::RandomState>> =
Lazy::new(|| Default::default());
@ -1124,18 +1157,24 @@ impl GlobalPassOption {
let cache_key = self
.vars
.iter()
.filter(|(k, _)| !k.contains('.'))
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<Vec<_>>();
if let Some(v) = CACHE.get(&cache_key) {
(*v).clone()
} else {
let map = mk_map(cm, handler, self.vars.into_iter(), false);
let map = mk_map(
cm,
handler,
self.vars.into_iter().filter(|(k, _)| !k.contains('.')),
false,
);
CACHE.insert(cache_key, map.clone());
map
}
};
inline_globals(env_map, global_map, Arc::new(self.typeofs))
inline_globals2(env_map, global_map, global_exprs, Arc::new(self.typeofs))
}
}

View File

@ -2,6 +2,7 @@
"jsc": {
"transform": {
"optimizer": {
"simplify": false,
"globals": {
"envs": {
"NODE_ENV_ALT": "true"

View File

@ -2,6 +2,7 @@
"jsc": {
"transform": {
"optimizer": {
"simplify": false,
"globals": {
"envs": {
"NODE_ENV_ALT": "true"

View File

@ -0,0 +1,14 @@
{
"jsc": {
"transform": {
"optimizer": {
"simplify": false,
"globals": {
"vars": {
"process.browser": "true"
}
}
}
}
}
}

View File

@ -0,0 +1,4 @@
if (process.browser) {
console.log('Pass')
}

View File

@ -0,0 +1,3 @@
if (true) {
console.log('Pass');
}

View File

@ -0,0 +1,15 @@
{
"jsc": {
"transform": {
"optimizer": {
"simplify": false,
"globals": {
"vars": {
"value.debug": "true",
"value": "'foo'"
}
}
}
}
}
}

View File

@ -0,0 +1,8 @@
console.log(value.debug)
if (value.debug) {
console.log('Pass')
}
console.log(value)

View File

@ -0,0 +1,5 @@
console.log(true);
if (true) {
console.log('Pass');
}
console.log('foo');

View File

@ -2,6 +2,7 @@
"jsc": {
"transform": {
"optimizer": {
"simplify": false,
"globals": {
"typeofs": {
"window": "object"