aboutsummaryrefslogtreecommitdiff
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
parentb400dfb386be65ca1d16ada005e75683ec48c1a5 (diff)
downloadbun-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.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
-rw-r--r--test/bun.js/transpiler.test.js429
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");