aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-12-10 00:18:44 -0800
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-12-10 00:19:19 -0800
commit047754d5ddae0b760b807adafcb3678d8702d7df (patch)
tree2ef334fdf69e2e2440822bb660ae5c2ae085f1ea /src
parentb400dfb386be65ca1d16ada005e75683ec48c1a5 (diff)
downloadbun-047754d5ddae0b760b807adafcb3678d8702d7df.tar.gz
bun-047754d5ddae0b760b807adafcb3678d8702d7df.tar.zst
bun-047754d5ddae0b760b807adafcb3678d8702d7df.zip
Implement simple version of inlining single-use expressions and statements
Diffstat (limited to 'src')
-rw-r--r--src/bun.js/api/transpiler.zig6
-rw-r--r--src/bundler.zig1
-rw-r--r--src/darwin_c.zig6
-rw-r--r--src/js_ast.zig19
-rw-r--r--src/js_parser.zig756
-rw-r--r--src/options.zig3
-rw-r--r--src/runtime.zig2
7 files changed, 724 insertions, 69 deletions
diff --git a/src/bun.js/api/transpiler.zig b/src/bun.js/api/transpiler.zig
index e932199ae..341a0d9db 100644
--- a/src/bun.js/api/transpiler.zig
+++ b/src/bun.js/api/transpiler.zig
@@ -103,6 +103,7 @@ const TranspilerOptions = struct {
runtime: Runtime.Features = Runtime.Features{ .top_level_await = true },
tree_shaking: bool = false,
trim_unused_imports: ?bool = null,
+ inlining: bool = false,
};
// Mimalloc gets unstable if we try to move this to a different thread
@@ -555,6 +556,10 @@ fn transformOptionsFromJSC(ctx: JSC.C.JSContextRef, temp_allocator: std.mem.Allo
}
}
+ if (object.get(globalThis, "inline")) |flag| {
+ transpiler.runtime.inlining = flag.toBoolean();
+ }
+
if (object.get(globalThis, "sourcemap")) |flag| {
if (flag.isBoolean() or flag.isUndefinedOrNull()) {
if (flag.toBoolean()) {
@@ -784,6 +789,7 @@ pub fn constructor(
bundler.options.trim_unused_imports = transpiler_options.trim_unused_imports;
bundler.options.allow_runtime = transpiler_options.runtime.allow_runtime;
bundler.options.auto_import_jsx = transpiler_options.runtime.auto_import_jsx;
+ bundler.options.inlining = transpiler_options.runtime.inlining;
bundler.options.hot_module_reloading = transpiler_options.runtime.hot_module_reloading;
bundler.options.jsx.supports_fast_refresh = bundler.options.hot_module_reloading and
bundler.options.allow_runtime and transpiler_options.runtime.react_fast_refresh;
diff --git a/src/bundler.zig b/src/bundler.zig
index 01c23dac0..0e21825cc 100644
--- a/src/bundler.zig
+++ b/src/bundler.zig
@@ -1415,6 +1415,7 @@ pub const Bundler = struct {
opts.can_import_from_bundle = bundler.options.node_modules_bundle != null;
opts.tree_shaking = bundler.options.tree_shaking;
+ opts.features.inlining = bundler.options.inlining;
// HMR is enabled when devserver is running
// unless you've explicitly disabled it
diff --git a/src/darwin_c.zig b/src/darwin_c.zig
index b798801fe..57163e0c6 100644
--- a/src/darwin_c.zig
+++ b/src/darwin_c.zig
@@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
-const sysResource = @cImport(@cInclude("sys/resource.h"));
+const imports = @cImport(@cInclude("sys/resource.h"));
const os = std.os;
const mem = std.mem;
const Stat = std.fs.File.Stat;
@@ -540,11 +540,11 @@ pub extern fn getuid(...) std.os.uid_t;
pub extern fn getgid(...) std.os.gid_t;
pub fn get_process_priority(pid: c_uint) i32 {
- return sysResource.getpriority(sysResource.PRIO_PROCESS, pid);
+ return imports.getpriority(imports.PRIO_PROCESS, pid);
}
pub fn set_process_priority(pid: c_uint, priority: c_int) i32 {
- return sysResource.setpriority(sysResource.PRIO_PROCESS, pid, priority);
+ return imports.setpriority(imports.PRIO_PROCESS, pid, priority);
}
pub fn get_version(buf: []u8) []const u8 {
diff --git a/src/js_ast.zig b/src/js_ast.zig
index 6e017849e..f3a10b683 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -2948,6 +2948,14 @@ pub const Expr = struct {
// This should never make it to the printer
inline_identifier,
+ // object, regex and array may have had side effects
+ pub fn isPrimitiveLiteral(tag: Tag) bool {
+ return switch (tag) {
+ .e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => true,
+ else => false,
+ };
+ }
+
pub fn typeof(tag: Tag) ?string {
return switch (tag) {
.e_array, .e_object, .e_null, .e_reg_exp => "object",
@@ -3745,6 +3753,17 @@ pub const Expr = struct {
};
}
+ pub fn toFiniteNumber(data: Expr.Data) ?f64 {
+ return switch (data) {
+ .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0),
+ .e_number => if (std.math.isFinite(data.e_number.value))
+ data.e_number.value
+ else
+ null,
+ else => null,
+ };
+ }
+
pub const Equality = struct { equal: bool = false, ok: bool = false };
// Returns "equal, ok". If "ok" is false, then nothing is known about the two
diff --git a/src/js_parser.zig b/src/js_parser.zig
index b69951377..361467a64 100644
--- a/src/js_parser.zig
+++ b/src/js_parser.zig
@@ -104,6 +104,12 @@ pub const AllocatedNamesPool = ObjectPool(
4,
);
+const Substitution = union(enum) {
+ success: Expr,
+ failure: Expr,
+ continue_: Expr,
+};
+
fn foldStringAddition(lhs: Expr, rhs: Expr) ?Expr {
switch (lhs.data) {
.e_string => |left| {
@@ -1701,6 +1707,34 @@ pub const SideEffects = enum(u1) {
return result;
}
},
+ .bin_gt => {
+ if (e_.right.data.toFiniteNumber()) |left_num| {
+ if (e_.left.data.toFiniteNumber()) |right_num| {
+ return Result{ .ok = true, .value = left_num > right_num, .side_effects = .no_side_effects };
+ }
+ }
+ },
+ .bin_lt => {
+ if (e_.right.data.toFiniteNumber()) |left_num| {
+ if (e_.left.data.toFiniteNumber()) |right_num| {
+ return Result{ .ok = true, .value = left_num < right_num, .side_effects = .no_side_effects };
+ }
+ }
+ },
+ .bin_le => {
+ if (e_.right.data.toFiniteNumber()) |left_num| {
+ if (e_.left.data.toFiniteNumber()) |right_num| {
+ return Result{ .ok = true, .value = left_num <= right_num, .side_effects = .no_side_effects };
+ }
+ }
+ },
+ .bin_ge => {
+ if (e_.right.data.toFiniteNumber()) |left_num| {
+ if (e_.left.data.toFiniteNumber()) |right_num| {
+ return Result{ .ok = true, .value = left_num >= right_num, .side_effects = .no_side_effects };
+ }
+ }
+ },
else => {},
}
},
@@ -3088,7 +3122,7 @@ pub const Parser = struct {
const had_require = p.runtime_imports.contains("__require");
p.resolveCommonJSSymbols();
- const copy_of_runtime_require = p.runtime_imports.__require.?;
+ const copy_of_runtime_require = p.runtime_imports.__require;
if (!had_require) {
p.runtime_imports.__require = null;
}
@@ -3662,6 +3696,7 @@ fn NewParser_(
loop_body: Stmt.Data,
module_scope: *js_ast.Scope = undefined,
is_control_flow_dead: bool = false,
+ is_substituting: bool = false,
// Inside a TypeScript namespace, an "export declare" statement can be used
// to cause a namespace to be emitted even though it has no other observable
@@ -4321,6 +4356,7 @@ fn NewParser_(
}
pub fn recordUsage(p: *P, ref: Ref) void {
+ if (p.is_substituting) return;
// The use count stored in the symbol is used for generating symbol names
// during minification. These counts shouldn't include references inside dead
// code regions since those will be culled.
@@ -4488,6 +4524,492 @@ fn NewParser_(
}) catch unreachable;
}
+ fn substituteSingleUseSymbolInStmt(p: *P, stmt: Stmt, ref: Ref, replacement: Expr) bool {
+ var expr: *Expr = brk: {
+ switch (stmt.data) {
+ .s_expr => |exp| {
+ break :brk &exp.value;
+ },
+ .s_throw => |throw| {
+ break :brk &throw.value;
+ },
+ .s_return => |ret| {
+ if (ret.value) |*value| {
+ break :brk value;
+ }
+ },
+ .s_if => |if_stmt| {
+ break :brk &if_stmt.test_;
+ },
+ .s_switch => |switch_stmt| {
+ break :brk &switch_stmt.test_;
+ },
+ .s_local => |local| {
+ if (local.decls.len > 0) {
+ var first: *Decl = &local.decls[0];
+ if (first.value) |*value| {
+ if (first.binding.data == .b_identifier) {
+ break :brk value;
+ }
+ }
+ }
+ },
+ else => {},
+ }
+
+ return false;
+ };
+
+ // Only continue trying to insert this replacement into sub-expressions
+ // after the first one if the replacement has no side effects:
+ //
+ // // Substitution is ok
+ // let replacement = 123;
+ // return x + replacement;
+ //
+ // // Substitution is not ok because "fn()" may change "x"
+ // let replacement = fn();
+ // return x + replacement;
+ //
+ // // Substitution is not ok because "x == x" may change "x" due to "valueOf()" evaluation
+ // let replacement = [x];
+ // return (x == x) + replacement;
+ //
+ const replacement_can_be_removed = p.exprCanBeRemovedIfUnused(&replacement);
+ switch (p.substituteSingleUseSymbolInExpr(expr.*, ref, replacement, replacement_can_be_removed)) {
+ .success => |result| {
+ if (result.data == .e_binary or result.data == .e_unary or result.data == .e_if) {
+ const prev_substituting = p.is_substituting;
+ p.is_substituting = true;
+ defer p.is_substituting = prev_substituting;
+ expr.* = p.visitExpr(result);
+ } else {
+ expr.* = result;
+ }
+
+ return true;
+ },
+ else => {},
+ }
+
+ return false;
+ }
+
+ fn substituteSingleUseSymbolInExpr(
+ p: *P,
+ expr: Expr,
+ ref: Ref,
+ replacement: Expr,
+ replacement_can_be_removed: bool,
+ ) Substitution {
+ outer: {
+ switch (expr.data) {
+ .e_identifier => |ident| {
+ if (ident.ref.eql(ref) or p.symbols.items[ident.ref.innerIndex()].link.eql(ref)) {
+ p.ignoreUsage(ref);
+ return .{ .success = replacement };
+ }
+ },
+ .e_new => |new| {
+ switch (p.substituteSingleUseSymbolInExpr(new.target, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ new.target = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ new.target = result;
+ return .{ .failure = expr };
+ },
+ }
+
+ if (replacement_can_be_removed) {
+ for (new.args.slice()) |*arg| {
+ switch (p.substituteSingleUseSymbolInExpr(arg.*, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ arg.* = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ arg.* = result;
+ return .{ .failure = expr };
+ },
+ }
+ }
+ }
+ },
+ .e_spread => |spread| {
+ switch (p.substituteSingleUseSymbolInExpr(spread.value, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ spread.value = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ spread.value = result;
+ return .{ .failure = expr };
+ },
+ }
+ },
+ .e_await => |await_expr| {
+ switch (p.substituteSingleUseSymbolInExpr(await_expr.value, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ await_expr.value = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ await_expr.value = result;
+ return .{ .failure = expr };
+ },
+ }
+ },
+ .e_yield => |yield| {
+ switch (p.substituteSingleUseSymbolInExpr(yield.value orelse Expr{ .data = .{ .e_missing = .{} }, .loc = expr.loc }, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ yield.value = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ yield.value = result;
+ return .{ .failure = expr };
+ },
+ }
+ },
+ .e_import => |import| {
+ switch (p.substituteSingleUseSymbolInExpr(import.expr, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ import.expr = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ import.expr = result;
+ return .{ .failure = expr };
+ },
+ }
+
+ // The "import()" expression has side effects but the side effects are
+ // always asynchronous so there is no way for the side effects to modify
+ // the replacement value. So it's ok to reorder the replacement value
+ // past the "import()" expression assuming everything else checks out.
+
+ if (replacement_can_be_removed and p.exprCanBeRemovedIfUnused(&import.expr)) {
+ return .{ .continue_ = expr };
+ }
+ },
+ .e_unary => |e| {
+ switch (e.op) {
+ .un_pre_inc, .un_post_inc, .un_pre_dec, .un_post_dec, .un_delete => {
+ // Do not substitute into an assignment position
+ },
+ else => {
+ switch (p.substituteSingleUseSymbolInExpr(e.value, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ e.value = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ e.value = result;
+ return .{ .failure = expr };
+ },
+ }
+ },
+ }
+ },
+ .e_dot => |e| {
+ switch (p.substituteSingleUseSymbolInExpr(e.target, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ e.target = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ e.target = result;
+ return .{ .failure = expr };
+ },
+ }
+ },
+ .e_binary => |e| {
+ // Do not substitute into an assignment position
+ if (e.op.binaryAssignTarget() == .none) {
+ switch (p.substituteSingleUseSymbolInExpr(e.left, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ e.left = result;
+
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ e.left = result;
+ return .{ .failure = expr };
+ },
+ }
+ } else if (!p.exprCanBeRemovedIfUnused(&e.left)) {
+ // Do not reorder past a side effect in an assignment target, as that may
+ // change the replacement value. For example, "fn()" may change "a" here:
+ //
+ // let a = 1;
+ // foo[fn()] = a;
+ //
+ return .{ .failure = expr };
+ } else if (e.op.binaryAssignTarget() == .update and !replacement_can_be_removed) {
+ // If this is a read-modify-write assignment and the replacement has side
+ // effects, don't reorder it past the assignment target. The assignment
+ // target is being read so it may be changed by the side effect. For
+ // example, "fn()" may change "foo" here:
+ //
+ // let a = fn();
+ // foo += a;
+ //
+ return .{ .failure = expr };
+ }
+
+ // If we get here then it should be safe to attempt to substitute the
+ // replacement past the left operand into the right operand.
+ switch (p.substituteSingleUseSymbolInExpr(e.right, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ e.right = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ e.right = result;
+ return .{ .failure = expr };
+ },
+ }
+ },
+ .e_if => |e| {
+ switch (p.substituteSingleUseSymbolInExpr(expr.data.e_if.test_, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ e.test_ = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ e.test_ = result;
+ return .{ .failure = expr };
+ },
+ }
+
+ // Do not substitute our unconditionally-executed value into a branch
+ // unless the value itself has no side effects
+ if (replacement_can_be_removed) {
+ // Unlike other branches in this function such as "a && b" or "a?.[b]",
+ // the "a ? b : c" form has potential code evaluation along both control
+ // flow paths. Handle this by allowing substitution into either branch.
+ // Side effects in one branch should not prevent the substitution into
+ // the other branch.
+
+ const yes = p.substituteSingleUseSymbolInExpr(e.yes, ref, replacement, replacement_can_be_removed);
+ if (yes == .success) {
+ e.yes = yes.success;
+ return .{ .success = expr };
+ }
+
+ const no = p.substituteSingleUseSymbolInExpr(e.no, ref, replacement, replacement_can_be_removed);
+ if (no == .success) {
+ e.no = no.success;
+ return .{ .success = expr };
+ }
+
+ // Side effects in either branch should stop us from continuing to try to
+ // substitute the replacement after the control flow branches merge again.
+ if (yes != .continue_ or no != .continue_) {
+ return .{ .failure = expr };
+ }
+ }
+ },
+ .e_index => |index| {
+ switch (p.substituteSingleUseSymbolInExpr(index.target, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ index.target = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ index.target = result;
+ return .{ .failure = expr };
+ },
+ }
+
+ // Do not substitute our unconditionally-executed value into a branch
+ // unless the value itself has no side effects
+ if (replacement_can_be_removed or index.optional_chain == null) {
+ switch (p.substituteSingleUseSymbolInExpr(index.index, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ index.index = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ index.index = result;
+ return .{ .failure = expr };
+ },
+ }
+ }
+ },
+
+ .e_call => |e| {
+ // Don't substitute something into a call target that could change "this"
+ switch (replacement.data) {
+ .e_dot, .e_index => {
+ if (e.target.data == .e_identifier and e.target.data.e_identifier.ref.eql(ref)) {
+ break :outer;
+ }
+ },
+ else => {},
+ }
+
+ switch (p.substituteSingleUseSymbolInExpr(e.target, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ e.target = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ e.target = result;
+ return .{ .failure = expr };
+ },
+ }
+
+ // Do not substitute our unconditionally-executed value into a branch
+ // unless the value itself has no side effects
+ if (replacement_can_be_removed or e.optional_chain == null) {
+ for (e.args.slice()) |*arg| {
+ switch (p.substituteSingleUseSymbolInExpr(arg.*, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ arg.* = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ arg.* = result;
+ return .{ .failure = expr };
+ },
+ }
+ }
+ }
+ },
+
+ .e_array => |e| {
+ for (e.items.slice()) |*item| {
+ switch (p.substituteSingleUseSymbolInExpr(item.*, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ item.* = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ item.* = result;
+ return .{ .failure = expr };
+ },
+ }
+ }
+ },
+
+ .e_object => |e| {
+ for (e.properties.slice()) |*property| {
+ // Check the key
+
+ if (property.flags.contains(.is_computed)) {
+ switch (p.substituteSingleUseSymbolInExpr(property.key.?, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ property.key = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ property.key = result;
+ return .{ .failure = expr };
+ },
+ }
+
+ // Stop now because both computed keys and property spread have side effects
+ return .{ .failure = expr };
+ }
+
+ // Check the value
+ if (property.value) |value| {
+ switch (p.substituteSingleUseSymbolInExpr(value, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ if (result.data == .e_missing) {
+ property.value = null;
+ } else {
+ property.value = result;
+ }
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ if (result.data == .e_missing) {
+ property.value = null;
+ } else {
+ property.value = result;
+ }
+ return .{ .failure = expr };
+ },
+ }
+ }
+ }
+ },
+
+ .e_template => |e| {
+ if (e.tag) |*tag| {
+ switch (p.substituteSingleUseSymbolInExpr(tag.*, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ tag.* = result;
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ tag.* = result;
+ return .{ .failure = expr };
+ },
+ }
+ }
+
+ for (e.parts) |*part| {
+ switch (p.substituteSingleUseSymbolInExpr(part.value, ref, replacement, replacement_can_be_removed)) {
+ .continue_ => {},
+ .success => |result| {
+ part.value = result;
+
+ // todo: mangle template parts
+
+ return .{ .success = expr };
+ },
+ .failure => |result| {
+ part.value = result;
+ return .{ .failure = expr };
+ },
+ }
+ }
+ },
+ else => {},
+ }
+ }
+
+ // If both the replacement and this expression have no observable side
+ // effects, then we can reorder the replacement past this expression
+ if (replacement_can_be_removed and p.exprCanBeRemovedIfUnused(&expr)) {
+ return .{ .continue_ = expr };
+ }
+
+ const tag: Expr.Tag = @as(Expr.Tag, expr.data);
+
+ // We can always reorder past primitive values
+ if (tag.isPrimitiveLiteral()) {
+ return .{ .continue_ = expr };
+ }
+
+ // Otherwise we should stop trying to substitute past this point
+ return .{ .failure = expr };
+ }
+
pub fn prepareForVisitPass(p: *P) !void {
{
var count: usize = 0;
@@ -4657,7 +5179,8 @@ fn NewParser_(
if (p.runtime_imports.__require) |*require| {
p.resolveGeneratedSymbol(require);
}
- p.ensureRequireSymbol();
+ if (p.options.features.allow_runtime)
+ p.ensureRequireSymbol();
}
pub fn resolveBundlingSymbols(p: *P) void {
@@ -14849,7 +15372,7 @@ fn NewParser_(
}
pub fn ignoreUsage(p: *P, ref: Ref) void {
- if (!p.is_control_flow_dead) {
+ if (!p.is_control_flow_dead and !p.is_substituting) {
if (comptime Environment.allow_assert) assert(@as(usize, ref.innerIndex()) < p.symbols.items.len);
p.symbols.items[ref.innerIndex()].use_count_estimate -|= 1;
var use = p.symbol_uses.get(ref) orelse return;
@@ -17094,85 +17617,186 @@ fn NewParser_(
@compileError("only_scan_imports_and_do_not_visit must not run this.");
}
- // Save the current control-flow liveness. This represents if we are
- // currently inside an "if (false) { ... }" block.
- var old_is_control_flow_dead = p.is_control_flow_dead;
- defer p.is_control_flow_dead = old_is_control_flow_dead;
+ {
- // visit all statements first
- var visited = try ListManaged(Stmt).initCapacity(p.allocator, stmts.items.len);
- var before = ListManaged(Stmt).init(p.allocator);
- var after = ListManaged(Stmt).init(p.allocator);
+ // Save the current control-flow liveness. This represents if we are
+ // currently inside an "if (false) { ... }" block.
+ var old_is_control_flow_dead = p.is_control_flow_dead;
+ defer p.is_control_flow_dead = old_is_control_flow_dead;
- if (p.current_scope == p.module_scope) {
- p.macro.prepend_stmts = &before;
- }
+ var before = ListManaged(Stmt).init(p.allocator);
+ var after = ListManaged(Stmt).init(p.allocator);
- defer before.deinit();
- defer visited.deinit();
- defer after.deinit();
+ if (p.current_scope == p.module_scope) {
+ p.macro.prepend_stmts = &before;
+ }
- for (stmts.items) |*stmt| {
- const list = list_getter: {
- switch (stmt.data) {
- .s_export_equals => {
- // TypeScript "export = value;" becomes "module.exports = value;". This
- // must happen at the end after everything is parsed because TypeScript
- // moves this statement to the end when it generates code.
- break :list_getter &after;
- },
- .s_function => |data| {
- // Manually hoist block-level function declarations to preserve semantics.
- // This is only done for function declarations that are not generators
- // or async functions, since this is a backwards-compatibility hack from
- // Annex B of the JavaScript standard.
- if (!p.current_scope.kindStopsHoisting() and p.symbols.items[data.func.name.?.ref.?.innerIndex()].kind == .hoisted_function) {
- break :list_getter &before;
- }
- },
- else => {},
- }
- break :list_getter &visited;
- };
- try p.visitAndAppendStmt(list, stmt);
- }
+ // visit all statements first
+ var visited = try ListManaged(Stmt).initCapacity(p.allocator, stmts.items.len);
- var visited_count = visited.items.len;
- if (p.is_control_flow_dead) {
- var end: usize = 0;
- for (visited.items) |item| {
- if (!SideEffects.shouldKeepStmtInDeadControlFlow(item, p.allocator)) {
- continue;
+ defer before.deinit();
+ defer visited.deinit();
+ defer after.deinit();
+
+ for (stmts.items) |*stmt| {
+ const list = list_getter: {
+ switch (stmt.data) {
+ .s_export_equals => {
+ // TypeScript "export = value;" becomes "module.exports = value;". This
+ // must happen at the end after everything is parsed because TypeScript
+ // moves this statement to the end when it generates code.
+ break :list_getter &after;
+ },
+ .s_function => |data| {
+ // Manually hoist block-level function declarations to preserve semantics.
+ // This is only done for function declarations that are not generators
+ // or async functions, since this is a backwards-compatibility hack from
+ // Annex B of the JavaScript standard.
+ if (!p.current_scope.kindStopsHoisting() and p.symbols.items[data.func.name.?.ref.?.innerIndex()].kind == .hoisted_function) {
+ break :list_getter &before;
+ }
+ },
+ else => {},
+ }
+ break :list_getter &visited;
+ };
+ try p.visitAndAppendStmt(list, stmt);
+ }
+
+ var visited_count = visited.items.len;
+ if (p.is_control_flow_dead) {
+ var end: usize = 0;
+ for (visited.items) |item| {
+ if (!SideEffects.shouldKeepStmtInDeadControlFlow(item, p.allocator)) {
+ continue;
+ }
+
+ visited.items[end] = item;
+ end += 1;
}
+ visited_count = end;
+ }
+
+ const total_size = visited_count + before.items.len + after.items.len;
- visited.items[end] = item;
- end += 1;
+ if (total_size != stmts.items.len) {
+ try stmts.resize(total_size);
}
- visited_count = end;
- }
- const total_size = visited_count + before.items.len + after.items.len;
+ var i: usize = 0;
- if (total_size != stmts.items.len) {
- try stmts.resize(total_size);
- }
+ for (before.items) |item| {
+ stmts.items[i] = item;
+ i += 1;
+ }
- var i: usize = 0;
+ const visited_slice = visited.items[0..visited_count];
+ for (visited_slice) |item| {
+ stmts.items[i] = item;
+ i += 1;
+ }
- for (before.items) |item| {
- stmts.items[i] = item;
- i += 1;
+ for (after.items) |item| {
+ stmts.items[i] = item;
+ i += 1;
+ }
}
- const visited_slice = visited.items[0..visited_count];
- for (visited_slice) |item| {
- stmts.items[i] = item;
- i += 1;
+ if (!p.options.features.inlining) {
+ return;
}
- for (after.items) |item| {
- stmts.items[i] = item;
- i += 1;
+ // Inline single-use variable declarations where possible:
+ //
+ // // Before
+ // let x = fn();
+ // return x.y();
+ //
+ // // After
+ // return fn().y();
+ //
+ // The declaration must not be exported. We can't just check for the
+ // "export" keyword because something might do "export {id};" later on.
+ // Instead we just ignore all top-level declarations for now. That means
+ // this optimization currently only applies in nested scopes.
+ //
+ // Ignore declarations if the scope is shadowed by a direct "eval" call.
+ // The eval'd code may indirectly reference this symbol and the actual
+ // use count may be greater than 1.
+ if (p.current_scope != p.module_scope and !p.current_scope.contains_direct_eval) {
+ var output = ListManaged(Stmt).initCapacity(p.allocator, stmts.items.len) catch unreachable;
+
+ for (stmts.items) |stmt| {
+
+ // Keep inlining variables until a failure or until there are none left.
+ // That handles cases like this:
+ //
+ // // Before
+ // let x = fn();
+ // let y = x.prop;
+ // return y;
+ //
+ // // After
+ // return fn().prop;
+ //
+ inner: while (output.items.len > 0) {
+ // Ignore "var" declarations since those have function-level scope and
+ // we may not have visited all of their uses yet by this point. We
+ // should have visited all the uses of "let" and "const" declarations
+ // by now since they are scoped to this block which we just finished
+ // visiting.
+ var prev_statement = &output.items[output.items.len - 1];
+ switch (prev_statement.data) {
+ .s_local => {
+ var local = prev_statement.data.s_local;
+ if (!(local.decls.len == 0 or local.kind == .k_var)) {
+ var last: *Decl = &local.decls[local.decls.len - 1];
+ // The variable must be initialized, since we will be substituting
+ // the value into the usage.
+
+ // The binding must be an identifier that is only used once.
+ // Ignore destructuring bindings since that's not the simple case.
+ // Destructuring bindings could potentially execute side-effecting
+ // code which would invalidate reordering.
+ if (!(last.value == null or last.binding.data != .b_identifier)) {
+ const id = last.binding.data.b_identifier.ref;
+
+ const symbol: *const Symbol = &p.symbols.items[id.innerIndex()];
+
+ // Try to substitute the identifier with the initializer. This will
+ // fail if something with side effects is in between the declaration
+ // and the usage.
+ if (symbol.use_count_estimate == 1) {
+ if (p.substituteSingleUseSymbolInStmt(stmt, id, last.value.?)) {
+ switch (local.decls.len) {
+ 1 => {
+ local.decls.len = 0;
+ output.items.len -= 1;
+ continue :inner;
+ },
+ else => {
+ local.decls.len -= 1;
+ continue :inner;
+ },
+ }
+ }
+ }
+ }
+ }
+ },
+ else => {},
+ }
+ break;
+ }
+
+ if (stmt.data != .s_empty) {
+ output.appendAssumeCapacity(
+ stmt,
+ );
+ }
+ }
+ stmts.deinit();
+ stmts.* = output;
}
}
diff --git a/src/options.zig b/src/options.zig
index 97f0cf62f..d6e4ecb41 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -1241,6 +1241,8 @@ pub const BundleOptions = struct {
prefer_latest_install: bool = false,
install: ?*Api.BunInstall = null,
+ inlining: bool = false,
+
pub inline fn cssImportBehavior(this: *const BundleOptions) Api.CssInJsBehavior {
switch (this.platform) {
.neutral, .browser => {
@@ -1639,6 +1641,7 @@ pub const BundleOptions = struct {
}
opts.tree_shaking = opts.serve or opts.platform.isBun() or opts.production or is_generating_bundle;
+ opts.inlining = opts.tree_shaking;
if (opts.origin.isAbsolute()) {
opts.import_path_format = ImportPathFormat.absolute_url;
diff --git a/src/runtime.zig b/src/runtime.zig
index bceda8130..fcec8549b 100644
--- a/src/runtime.zig
+++ b/src/runtime.zig
@@ -298,6 +298,8 @@ pub const Runtime = struct {
top_level_await: bool = false,
auto_import_jsx: bool = false,
allow_runtime: bool = true,
+ inlining: bool = false,
+
/// Instead of jsx("div", {}, void 0)
/// ->
/// {