Skip to content

Commit

Permalink
feat: basic treeshaking (#372)
Browse files Browse the repository at this point in the history
<!-- Thank you for contributing! -->

### Description

Closes #16.

<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->

### Test Plan

<!-- e.g. is there anything you'd like reviewers to focus on? -->

---
  • Loading branch information
hyf0 authored Nov 27, 2023
1 parent 7bc5f92 commit b240923
Show file tree
Hide file tree
Showing 18 changed files with 171 additions and 19 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"rolldown",
"rollup",
"rustc",
"smallvec"
"smallvec",
"treeshake"
],
"git.ignoreLimitWarning": true,
"json.schemas": [
Expand Down
1 change: 1 addition & 0 deletions crates/rolldown/src/bundler/linker/linker_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl LinkingInfo {
// Since the facade symbol is used, it should be referenced. This will be used to
// create correct cross-chunk links
referenced_symbols: vec![symbol_ref],
side_effect: true,
..Default::default()
});
}
Expand Down
8 changes: 5 additions & 3 deletions crates/rolldown/src/bundler/module/normal_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,11 @@ impl NormalModule {
symbols: &mut Symbols,
) -> SymbolRef {
let symbol_ref = symbols.create_symbol(self.id, name);
self_linking_info
.facade_stmt_infos
.push(StmtInfo { declared_symbols: vec![symbol_ref], ..Default::default() });
self_linking_info.facade_stmt_infos.push(StmtInfo {
declared_symbols: vec![symbol_ref],
side_effect: true,
..Default::default()
});
symbol_ref
}

Expand Down
12 changes: 11 additions & 1 deletion crates/rolldown/src/bundler/module_loader/normal_module_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,17 @@ impl<'task, T: FileSystem + Default + 'static> NormalModuleTask<'task, T> {
&self.path,
);
let namespace_symbol = scanner.namespace_symbol;
let scan_result = scanner.scan(program.program());
let mut scan_result = scanner.scan(program.program());
if !self.ctx.input_options.treeshake {
// FIXME(hyf0): should move this to scanner
let mut stmt_infos = scan_result.stmt_infos.iter_mut();
// Skip the facade namespace declaration
stmt_infos.next();
for stmt_info in stmt_infos {
stmt_info.side_effect = true;
stmt_info.is_included = true;
}
}
(program, ast_scope, scan_result, symbol_for_module, namespace_symbol)
}

Expand Down
8 changes: 7 additions & 1 deletion crates/rolldown/src/bundler/options/input_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,17 @@ pub struct InputOptions {
pub input: Vec<InputItem>,
pub cwd: PathBuf,
pub external: External,
pub treeshake: bool,
}

impl Default for InputOptions {
fn default() -> Self {
Self { input: vec![], cwd: std::env::current_dir().unwrap(), external: External::default() }
Self {
input: vec![],
cwd: std::env::current_dir().unwrap(),
external: External::default(),
treeshake: true,
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion crates/rolldown/src/bundler/renderer/impl_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ impl<'ast, 'r> AstRenderer<'r> {
self.ctx.first_stmt_start = Some(stmt.span().start);
}
}
self.visit_statement(stmt);
if self.current_stmt_info.get().is_included {
self.visit_statement(stmt);
} else {
self.remove_node(stmt.span());
}
}
}

Expand Down
7 changes: 4 additions & 3 deletions crates/rolldown/src/bundler/stages/link_stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,10 @@ impl LinkStage {
// Skip the first one, because it's the namespace variable declaration.
// We want to include it on demand.
stmt_infos.next();
// Since we won't implement tree shaking, we just include all statements.
stmt_infos.for_each(|(stmt_info_id, _)| {
include_statement(context, module, stmt_info_id);
stmt_infos.for_each(|(stmt_info_id, stmt_info)| {
if stmt_info.side_effect {
include_statement(context, module, stmt_info_id);
}
});
if module.is_entry {
let linking_info = &self.linking_infos[module.id];
Expand Down
104 changes: 103 additions & 1 deletion crates/rolldown/src/bundler/visitors/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,20 @@ impl<'ast> Scanner<'ast> {
}
}

impl<'ast> Scanner<'ast> {
fn visit_top_level_stmt(&mut self, stmt: &oxc::ast::ast::Statement<'ast>) {
self.current_stmt_info.side_effect =
SideEffectDetector { scope: self.scope }.detect_side_effect_of_stmt(stmt);
self.visit_statement(stmt);
}
}

impl<'ast> Visit<'ast> for Scanner<'ast> {
#[tracing::instrument(skip_all)]
fn visit_program(&mut self, program: &oxc::ast::ast::Program<'ast>) {
for (idx, stmt) in program.body.iter().enumerate() {
self.current_stmt_info.stmt_idx = Some(idx);
self.visit_statement(stmt);
self.visit_top_level_stmt(stmt);
self.result.stmt_infos.add_stmt_info(std::mem::take(&mut self.current_stmt_info));
}
}
Expand Down Expand Up @@ -434,3 +442,97 @@ impl<'ast> Visit<'ast> for Scanner<'ast> {
self.visit_expression(&expr.callee);
}
}

struct SideEffectDetector<'a> {
scope: &'a AstScope,
}

impl<'a> SideEffectDetector<'a> {
fn is_unresolved_reference(&self, ident_ref: &IdentifierReference) -> bool {
self.scope.is_unresolved(ident_ref.reference_id.get().unwrap())
}

fn detect_side_effect_of_class(&self, cls: &oxc::ast::ast::Class) -> bool {
use oxc::ast::ast::ClassElement;
cls.body.body.iter().any(|elm| match elm {
ClassElement::StaticBlock(static_block) => {
static_block.body.iter().any(|stmt| self.detect_side_effect_of_stmt(stmt))
}
ClassElement::MethodDefinition(_) => false,
ClassElement::PropertyDefinition(def) => {
(match &def.key {
oxc::ast::ast::PropertyKey::Identifier(_)
| oxc::ast::ast::PropertyKey::PrivateIdentifier(_) => false,
oxc::ast::ast::PropertyKey::Expression(expr) => self.detect_side_effect_of_expr(expr),
} || def.value.as_ref().is_some_and(|init| self.detect_side_effect_of_expr(init)))
}
ClassElement::AccessorProperty(def) => {
(match &def.key {
oxc::ast::ast::PropertyKey::Identifier(_)
| oxc::ast::ast::PropertyKey::PrivateIdentifier(_) => false,
oxc::ast::ast::PropertyKey::Expression(expr) => self.detect_side_effect_of_expr(expr),
} || def.value.as_ref().is_some_and(|init| self.detect_side_effect_of_expr(init)))
}
ClassElement::TSAbstractMethodDefinition(_)
| ClassElement::TSAbstractPropertyDefinition(_)
| ClassElement::TSIndexSignature(_) => unreachable!("ts should be transpiled"),
})
}

fn detect_side_effect_of_expr(&self, expr: &oxc::ast::ast::Expression) -> bool {
use oxc::ast::ast::Expression;
match expr {
Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::NumberLiteral(_)
| Expression::BigintLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::FunctionExpression(_)
| Expression::ArrowExpression(_)
| Expression::StringLiteral(_) => false,
Expression::ClassExpression(cls) => self.detect_side_effect_of_class(cls),
// Accessing global variables considered as side effect.
Expression::Identifier(ident) => self.is_unresolved_reference(ident),
_ => true,
}
}

pub fn detect_side_effect_of_stmt(&self, stmt: &oxc::ast::ast::Statement) -> bool {
use oxc::ast::ast::{Declaration, Statement};
match stmt {
Statement::Declaration(decl) => match decl {
Declaration::VariableDeclaration(var_decl) => var_decl
.declarations
.iter()
.any(|decl| decl.init.as_ref().is_some_and(|init| self.detect_side_effect_of_expr(init))),
Declaration::FunctionDeclaration(_) => false,
Declaration::ClassDeclaration(cls_decl) => self.detect_side_effect_of_class(cls_decl),
Declaration::UsingDeclaration(_) => todo!(),
Declaration::TSTypeAliasDeclaration(_)
| Declaration::TSInterfaceDeclaration(_)
| Declaration::TSEnumDeclaration(_)
| Declaration::TSModuleDeclaration(_)
| Declaration::TSImportEqualsDeclaration(_) => unreachable!("ts should be transpiled"),
},
Statement::ExpressionStatement(expr) => self.detect_side_effect_of_expr(&expr.expression),
Statement::BlockStatement(_)
| Statement::BreakStatement(_)
| Statement::DebuggerStatement(_)
| Statement::DoWhileStatement(_)
| Statement::EmptyStatement(_)
| Statement::ForInStatement(_)
| Statement::ForOfStatement(_)
| Statement::ForStatement(_)
| Statement::IfStatement(_)
| Statement::LabeledStatement(_)
| Statement::ReturnStatement(_)
| Statement::SwitchStatement(_)
| Statement::ThrowStatement(_)
| Statement::TryStatement(_)
| Statement::WhileStatement(_)
| Statement::WithStatement(_)
| Statement::ModuleDeclaration(_)
| Statement::ContinueStatement(_) => true,
}
}
}
1 change: 1 addition & 0 deletions crates/rolldown/tests/common/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl Fixture {
.unwrap(),
cwd: fixture_path.to_path_buf(),
external: test_config.input.external.map(|e| External::ArrayString(e)).unwrap_or_default(),
treeshake: test_config.input.treeshake.unwrap_or(true),
});

if fixture_path.join("dist").is_dir() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"name": "entry_js",
"import": "entry.js"
}
]
],
"treeshake": false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"name": "function-nested_js",
"import": "function-nested.js"
}
]
],
"treeshake": false
}
}
5 changes: 3 additions & 2 deletions crates/rolldown/tests/fixtures/basic_commonjs/artifacts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ init_esm();
console.log(import_commonjs.default, esm_default_fn, esm_named_var, esm_named_fn, esm_named_class)
// test commonjs warp symbol deconflict
const require_commonjs = () => {}
const require_commonjs = () => { }
// test esm export function symbol deconflict
function esm_default_fn$1() {}
function esm_default_fn$1() { }
console.log(require_commonjs, esm_default_fn$1)
```
7 changes: 4 additions & 3 deletions crates/rolldown/tests/fixtures/basic_commonjs/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import foo from './commonjs.js'
import esm, { esm_named_var, esm_named_fn, esm_named_class } from './esm.js'
import esm, { esm_named_var, esm_named_fn, esm_named_class } from './esm.js'
console.log(foo, esm, esm_named_var, esm_named_fn, esm_named_class)
// test commonjs warp symbol deconflict
const require_commonjs = () => {}
const require_commonjs = () => { }
// test esm export function symbol deconflict
function esm_default_fn() {}
function esm_default_fn() { }
console.log(require_commonjs, esm_default_fn)
1 change: 1 addition & 0 deletions crates/rolldown_binding/src/options/input_options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ impl From<InputOptions>
input: value.input.into_iter().map(Into::into).collect::<Vec<_>>(),
cwd,
external,
treeshake: false,
}),
value.plugins.into_iter().map(JsAdapterPlugin::new_boxed).collect::<napi::Result<Vec<_>>>(),
)
Expand Down
7 changes: 6 additions & 1 deletion crates/rolldown_binding_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ pub fn bundle(file_list: Vec<FileItem>) -> Vec<AssetItem> {
})
.collect::<Vec<_>>();
let mut bundler = Bundler::with_plugins_and_fs(
InputOptions { input, cwd: "/".into(), external: External::ArrayString(vec![]) },
InputOptions {
input,
cwd: "/".into(),
external: External::ArrayString(vec![]),
treeshake: true,
},
vec![],
memory_fs,
);
Expand Down
7 changes: 7 additions & 0 deletions crates/rolldown_common/src/stmt_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ impl std::ops::Deref for StmtInfos {
}
}

impl std::ops::DerefMut for StmtInfos {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.infos
}
}

index_vec::define_index_type! {
pub struct StmtInfoId = u32;
}
Expand All @@ -59,4 +65,5 @@ pub struct StmtInfo {
// here instead of `SymbolId`.
/// Top level symbols referenced by this statement.
pub referenced_symbols: Vec<SymbolRef>,
pub side_effect: bool,
}
1 change: 1 addition & 0 deletions crates/rolldown_testing/src/test_config/input_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::Deserialize;
pub struct InputOptions {
pub input: Option<Vec<InputItem>>,
pub external: Option<Vec<String>>,
pub treeshake: Option<bool>,
}

#[derive(Deserialize, JsonSchema)]
Expand Down
6 changes: 6 additions & 0 deletions crates/rolldown_testing/test.config.scheme.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
"items": {
"$ref": "#/definitions/InputItem"
}
},
"treeshake": {
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
Expand Down

0 comments on commit b240923

Please sign in to comment.