diff options
author | 2022-12-10 00:18:44 -0800 | |
---|---|---|
committer | 2022-12-10 00:19:19 -0800 | |
commit | 047754d5ddae0b760b807adafcb3678d8702d7df (patch) | |
tree | 2ef334fdf69e2e2440822bb660ae5c2ae085f1ea | |
parent | b400dfb386be65ca1d16ada005e75683ec48c1a5 (diff) | |
download | bun-047754d5ddae0b760b807adafcb3678d8702d7df.tar.gz bun-047754d5ddae0b760b807adafcb3678d8702d7df.tar.zst bun-047754d5ddae0b760b807adafcb3678d8702d7df.zip |
Implement simple version of inlining single-use expressions and statements
-rw-r--r-- | src/bun.js/api/transpiler.zig | 6 | ||||
-rw-r--r-- | src/bundler.zig | 1 | ||||
-rw-r--r-- | src/darwin_c.zig | 6 | ||||
-rw-r--r-- | src/js_ast.zig | 19 | ||||
-rw-r--r-- | src/js_parser.zig | 756 | ||||
-rw-r--r-- | src/options.zig | 3 | ||||
-rw-r--r-- | src/runtime.zig | 2 | ||||
-rw-r--r-- | test/bun.js/transpiler.test.js | 429 |
8 files changed, 1153 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) /// -> /// { diff --git a/test/bun.js/transpiler.test.js b/test/bun.js/transpiler.test.js index 76356d90f..2be937d78 100644 --- a/test/bun.js/transpiler.test.js +++ b/test/bun.js/transpiler.test.js @@ -1680,6 +1680,435 @@ class Foo { expectPrinted("a = !(b, c)", "a = (b , !c)"); }); + it("substitution", () => { + var transpiler = new Bun.Transpiler({ + inline: true, + platform: "bun", + allowBunRuntime: false, + }); + function check(input, output) { + expect( + transpiler + .transformSync("export function hello() {\n" + input + "\n}") + .trim() + .replaceAll(/^ /gm, ""), + ).toBe( + "export function hello() {\n" + + output + + "\n}".replaceAll(/^ /gm, ""), + ); + } + check("var x = 1; return x", "var x = 1;\nreturn x;"); + check("let x = 1; return x", "return 1;"); + check("const x = 1; return x", "return 1;"); + + check("let x = 1; if (false) x++; return x", "return 1;"); + // TODO: comma operator + // check("let x = 1; if (true) x++; return x", "let x = 1;\nreturn x++, x;"); + check("let x = 1; return x + x", "let x = 1;\nreturn x + x;"); + + // Can substitute into normal unary operators + check("let x = 1; return +x", "return 1;"); + check("let x = 1; return -x", "return -1;"); + check("let x = 1; return !x", "return false;"); + check("let x = 1; return ~x", "return ~1;"); + // TODO: remove needless return undefined; + // check("let x = 1; return void x", "let x = 1;"); + + // esbuild does this: + // check("let x = 1; return typeof x", "return typeof 1;"); + // we do: + check("let x = 1; return typeof x", 'return "number";'); + + // Check substituting a side-effect free value into normal binary operators + // esbuild does this: + // check("let x = 1; return x + 2", "return 1 + 2;"); + // we do: + check("let x = 1; return x + 2", "return 3;"); + check("let x = 1; return 2 + x", "return 3;"); + check("let x = 1; return x + arg0", "return 1 + arg0;"); + // check("let x = 1; return arg0 + x", "return arg0 + 1;"); + check("let x = 1; return x + fn()", "return 1 + fn();"); + check("let x = 1; return fn() + x", "let x = 1;\nreturn fn() + x;"); + check("let x = 1; return x + undef", "return 1 + undef;"); + check("let x = 1; return undef + x", "let x = 1;\nreturn undef + x;"); + + // Check substituting a value with side-effects into normal binary operators + check("let x = fn(); return x + 2", "return fn() + 2;"); + check("let x = fn(); return 2 + x", "return 2 + fn();"); + check("let x = fn(); return x + arg0", "return fn() + arg0;"); + check("let x = fn(); return arg0 + x", "let x = fn();\nreturn arg0 + x;"); + check("let x = fn(); return x + fn2()", "return fn() + fn2();"); + check( + "let x = fn(); return fn2() + x", + "let x = fn();\nreturn fn2() + x;", + ); + check("let x = fn(); return x + undef", "return fn() + undef;"); + check( + "let x = fn(); return undef + x", + "let x = fn();\nreturn undef + x;", + ); + + // Cannot substitute into mutating unary operators + check("let x = 1; ++x", "let x = 1;\n++x;"); + check("let x = 1; --x", "let x = 1;\n--x;"); + check("let x = 1; x++", "let x = 1;\nx++;"); + check("let x = 1; x--", "let x = 1;\nx--;"); + check("let x = 1; delete x", "let x = 1;\ndelete x;"); + + // Cannot substitute into mutating binary operators + check("let x = 1; x = 2", "let x = 1;\nx = 2;"); + check("let x = 1; x += 2", "let x = 1;\nx += 2;"); + check("let x = 1; x ||= 2", "let x = 1;\nx ||= 2;"); + + // Can substitute past mutating binary operators when the left operand has no side effects + // check("let x = 1; arg0 = x", "arg0 = 1;"); + // check("let x = 1; arg0 += x", "arg0 += 1;"); + // check("let x = 1; arg0 ||= x", "arg0 ||= 1;"); + // check("let x = fn(); arg0 = x", "arg0 = fn();"); + // check("let x = fn(); arg0 += x", "let x = fn();\narg0 += x;"); + // check("let x = fn(); arg0 ||= x", "let x = fn();\narg0 ||= x;"); + + // Cannot substitute past mutating binary operators when the left operand has side effects + check("let x = 1; y.z = x", "let x = 1;\ny.z = x;"); + check("let x = 1; y.z += x", "let x = 1;\ny.z += x;"); + check("let x = 1; y.z ||= x", "let x = 1;\ny.z ||= x;"); + check("let x = fn(); y.z = x", "let x = fn();\ny.z = x;"); + check("let x = fn(); y.z += x", "let x = fn();\ny.z += x;"); + check("let x = fn(); y.z ||= x", "let x = fn();\ny.z ||= x;"); + + // TODO: + // Can substitute code without side effects into branches + // check("let x = arg0; return x ? y : z;", "return arg0 ? y : z;"); + // check("let x = arg0; return arg1 ? x : y;", "return arg1 ? arg0 : y;"); + // check("let x = arg0; return arg1 ? y : x;", "return arg1 ? y : arg0;"); + // check("let x = arg0; return x || y;", "return arg0 || y;"); + // check("let x = arg0; return x && y;", "return arg0 && y;"); + // check("let x = arg0; return x ?? y;", "return arg0 ?? y;"); + // check("let x = arg0; return arg1 || x;", "return arg1 || arg0;"); + // check("let x = arg0; return arg1 && x;", "return arg1 && arg0;"); + // check("let x = arg0; return arg1 ?? x;", "return arg1 ?? arg0;"); + + // Can substitute code without side effects into branches past an expression with side effects + // check( + // "let x = arg0; return y ? x : z;", + // "let x = arg0;\nreturn y ? x : z;", + // ); + // check( + // "let x = arg0; return y ? z : x;", + // "let x = arg0;\nreturn y ? z : x;", + // ); + // check("let x = arg0; return (arg1 ? 1 : 2) ? x : 3;", "return arg0;"); + // check( + // "let x = arg0; return (arg1 ? 1 : 2) ? 3 : x;", + // "let x = arg0;\nreturn 3;", + // ); + // check( + // "let x = arg0; return (arg1 ? y : 1) ? x : 2;", + // "let x = arg0;\nreturn !arg1 || y ? x : 2;", + // ); + // check( + // "let x = arg0; return (arg1 ? 1 : y) ? x : 2;", + // "let x = arg0;\nreturn arg1 || y ? x : 2;", + // ); + // check( + // "let x = arg0; return (arg1 ? y : 1) ? 2 : x;", + // "let x = arg0;\nreturn !arg1 || y ? 2 : x;", + // ); + // check( + // "let x = arg0; return (arg1 ? 1 : y) ? 2 : x;", + // "let x = arg0;\nreturn arg1 || y ? 2 : x;", + // ); + // check("let x = arg0; return y || x;", "let x = arg0;\nreturn y || x;"); + // check("let x = arg0; return y && x;", "let x = arg0;\nreturn y && x;"); + // check("let x = arg0; return y ?? x;", "let x = arg0;\nreturn y ?? x;"); + + // Cannot substitute code with side effects into branches + check("let x = fn(); return x ? arg0 : y;", "return fn() ? arg0 : y;"); + check( + "let x = fn(); return arg0 ? x : y;", + "let x = fn();\nreturn arg0 ? x : y;", + ); + check( + "let x = fn(); return arg0 ? y : x;", + "let x = fn();\nreturn arg0 ? y : x;", + ); + check("let x = fn(); return x || arg0;", "return fn() || arg0;"); + check("let x = fn(); return x && arg0;", "return fn() && arg0;"); + check("let x = fn(); return x ?? arg0;", "return fn() ?? arg0;"); + check( + "let x = fn(); return arg0 || x;", + "let x = fn();\nreturn arg0 || x;", + ); + check( + "let x = fn(); return arg0 && x;", + "let x = fn();\nreturn arg0 && x;", + ); + check( + "let x = fn(); return arg0 ?? x;", + "let x = fn();\nreturn arg0 ?? x;", + ); + + // Test chaining + check( + "let x = fn(); let y = x[prop]; let z = y.val; throw z", + "throw fn()[prop].val;", + ); + check( + "let x = fn(), y = x[prop], z = y.val; throw z", + "throw fn()[prop].val;", + ); + + // Can substitute an initializer with side effects + check("let x = 0; let y = ++x; return y", "let x = 0;\nreturn ++x;"); + + // Can substitute an initializer without side effects past an expression without side effects + check( + "let x = 0; let y = x; return [x, y]", + "let x = 0;\nreturn [x, x];", + ); + + // TODO: merge s_local + // Cannot substitute an initializer with side effects past an expression without side effects + // check( + // "let x = 0; let y = ++x; return [x, y]", + // "let x = 0, y = ++x;\nreturn [x, y];", + // ); + + // Cannot substitute an initializer without side effects past an expression with side effects + // TODO: merge s_local + // check( + // "let x = 0; let y = {valueOf() { x = 1 }}; let z = x; return [y == 1, z]", + // "let x = 0, y = { valueOf() {\n x = 1;\n} }, z = x;\nreturn [y == 1, z];", + // ); + + // Cannot inline past a spread operator, since that evaluates code + check("let x = arg0; return [...x];", "return [...arg0];"); + check("let x = arg0; return [x, ...arg1];", "return [arg0, ...arg1];"); + check( + "let x = arg0; return [...arg1, x];", + "let x = arg0;\nreturn [...arg1, x];", + ); + // TODO: preserve call here + // check("let x = arg0; return arg1(...x);", "return arg1(...arg0);"); + // check( + // "let x = arg0; return arg1(x, ...arg1);", + // "return arg1(arg0, ...arg1);", + // ); + check( + "let x = arg0; return arg1(...arg1, x);", + "let x = arg0;\nreturn arg1(...arg1, x);", + ); + + // Test various statement kinds + // TODO: + // check("let x = arg0; arg1(x);", "arg1(arg0);"); + + check("let x = arg0; throw x;", "throw arg0;"); + check("let x = arg0; return x;", "return arg0;"); + check("let x = arg0; if (x) return 1;", "if (arg0)\n return 1;"); + check( + "let x = arg0; switch (x) { case 0: return 1; }", + "switch (arg0) {\n case 0:\n return 1;\n}", + ); + check( + "let x = arg0; let y = x; return y + y;", + "let y = arg0;\nreturn y + y;", + ); + + // Loops must not be substituted into because they evaluate multiple times + check( + "let x = arg0; do {} while (x);", + "let x = arg0;\ndo\n ;\nwhile (x);", + ); + + // TODO: convert while(x) to for (;x;) + check( + "let x = arg0; while (x) return 1;", + "let x = arg0;\nwhile (x)\n return 1;", + // "let x = arg0;\nfor (; x; )\n return 1;", + ); + check( + "let x = arg0; for (; x; ) return 1;", + "let x = arg0;\nfor (;x; )\n return 1;", + ); + + // Can substitute an expression without side effects into a branch due to optional chaining + // TODO: + // check("let x = arg0; return arg1?.[x];", "return arg1?.[arg0];"); + // check("let x = arg0; return arg1?.(x);", "return arg1?.(arg0);"); + + // Cannot substitute an expression with side effects into a branch due to optional chaining, + // since that would change the expression with side effects from being unconditionally + // evaluated to being conditionally evaluated, which is a behavior change + check( + "let x = fn(); return arg1?.[x];", + "let x = fn();\nreturn arg1?.[x];", + ); + check( + "let x = fn(); return arg1?.(x);", + "let x = fn();\nreturn arg1?.(x);", + ); + + // Can substitute an expression past an optional chaining operation, since it has side effects + check( + "let x = arg0; return arg1?.a === x;", + "let x = arg0;\nreturn arg1?.a === x;", + ); + check( + "let x = arg0; return arg1?.[0] === x;", + "let x = arg0;\nreturn arg1?.[0] === x;", + ); + check( + "let x = arg0; return arg1?.(0) === x;", + "let x = arg0;\nreturn arg1?.(0) === x;", + ); + check( + "let x = arg0; return arg1?.a[x];", + "let x = arg0;\nreturn arg1?.a[x];", + ); + check( + "let x = arg0; return arg1?.a(x);", + "let x = arg0;\nreturn arg1?.a(x);", + ); + // TODO: + // check( + // "let x = arg0; return arg1?.[a][x];", + // "let x = arg0;\nreturn arg1?.[a][x];", + // ); + check( + "let x = arg0; return arg1?.[a](x);", + "let x = arg0;\nreturn (arg1?.[a])(x);", + ); + check( + "let x = arg0; return arg1?.(a)[x];", + "let x = arg0;\nreturn (arg1?.(a))[x];", + ); + check( + "let x = arg0; return arg1?.(a)(x);", + "let x = arg0;\nreturn (arg1?.(a))(x);", + ); + + // Can substitute into an object as long as there are no side effects + // beforehand. Note that computed properties must call "toString()" which + // can have side effects. + check("let x = arg0; return {x};", "return { x: arg0 };"); + check( + "let x = arg0; return {x: y, y: x};", + "let x = arg0;\nreturn { x: y, y: x };", + ); + // TODO: + // check( + // "let x = arg0; return {x: arg1, y: x};", + // "return { x: arg1, y: arg0 };", + // ); + check("let x = arg0; return {[x]: 0};", "return { [arg0]: 0 };"); + check( + "let x = arg0; return {[y]: x};", + "let x = arg0;\nreturn { [y]: x };", + ); + check( + "let x = arg0; return {[arg1]: x};", + "let x = arg0;\nreturn { [arg1]: x };", + ); + // TODO: + // check( + // "let x = arg0; return {y() {}, x};", + // "return { y() {\n}, x: arg0 };", + // ); + check( + "let x = arg0; return {[y]() {}, x};", + "let x = arg0;\nreturn { [y]() {\n}, x };", + ); + check("let x = arg0; return {...x};", "return { ...arg0 };"); + check("let x = arg0; return {...x, y};", "return { ...arg0, y };"); + check("let x = arg0; return {x, ...y};", "return { x: arg0, ...y };"); + check( + "let x = arg0; return {...y, x};", + "let x = arg0;\nreturn { ...y, x };", + ); + + // TODO: + // Check substitutions into template literals + // check("let x = arg0; return `a${x}b${y}c`;", "return `a${arg0}b${y}c`;"); + // check( + // "let x = arg0; return `a${y}b${x}c`;", + // "let x = arg0;\nreturn `a${y}b${x}c`;", + // ); + // check( + // "let x = arg0; return `a${arg1}b${x}c`;", + // "return `a${arg1}b${arg0}c`;", + // ); + // check("let x = arg0; return x`y`;", "return arg0`y`;"); + // check( + // "let x = arg0; return y`a${x}b`;", + // "let x = arg0;\nreturn y`a${x}b`;", + // ); + // check("let x = arg0; return arg1`a${x}b`;", "return arg1`a${arg0}b`;"); + // check("let x = 'x'; return `a${x}b`;", "return `axb`;"); + + // Check substitutions into import expressions + // TODO: + // check("let x = arg0; return import(x);", "return import(arg0);"); + // check( + // "let x = arg0; return [import(y), x];", + // "let x = arg0;\nreturn [import(y), x];", + // ); + // check( + // "let x = arg0; return [import(arg1), x];", + // "return [import(arg1), arg0];", + // ); + + // Check substitutions into await expressions + check( + "return async () => { let x = arg0; await x; };", + "return async () => {\n await arg0;\n};", + ); + + // TODO: combine with comma operator + // check( + // "return async () => { let x = arg0; await y; return x; };", + // "return async () => {\n let x = arg0;\n return await y, x;\n};", + // ); + // check( + // "return async () => { let x = arg0; await arg1; return x; };", + // "return async () => {\n let x = arg0;\n return await arg1, x;\n};", + // ); + + // Check substitutions into yield expressions + check( + "return function* () { let x = arg0; yield x; };", + "return function* () {\n yield arg0;\n};", + ); + // TODO: combine with comma operator + // check( + // "return function* () { let x = arg0; yield; return x; };", + // "return function* () {\n let x = arg0;\n yield ; \n return x;\n};", + // ); + // check( + // "return function* () { let x = arg0; yield y; return x; };", + // "return function* () {\n let x = arg0;\n return yield y, x;\n};", + // ); + // check( + // "return function* () { let x = arg0; yield arg1; return x; };", + // "return function* () {\n let x = arg0;\n return yield arg1, x;\n};", + // ); + + // Cannot substitute into call targets when it would change "this" + check("let x = arg0; x()", "arg0();"); + // check("let x = arg0; (0, x)()", "arg0();"); + check("let x = arg0.foo; x.bar()", "arg0.foo.bar();"); + check("let x = arg0.foo; x[bar]()", "arg0.foo[bar]();"); + check("let x = arg0.foo; x()", "let x = arg0.foo;\nx();"); + check("let x = arg0[foo]; x()", "let x = arg0[foo];\nx();"); + check("let x = arg0?.foo; x()", "let x = arg0?.foo;\nx();"); + check("let x = arg0?.[foo]; x()", "let x = arg0?.[foo];\nx();"); + // check("let x = arg0.foo; (0, x)()", "let x = arg0.foo;\nx();"); + // check("let x = arg0[foo]; (0, x)()", "let x = arg0[foo];\nx();"); + // check("let x = arg0?.foo; (0, x)()", "let x = arg0?.foo;\nx();"); + // check("let x = arg0?.[foo]; (0, x)()", "let x = arg0?.[foo];\nx();"); + }); + it("constant folding", () => { expectPrinted("1 && 2", "2"); expectPrinted("1 || 2", "1"); |