Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
e375b9d2e1 Commit 2025-08-09 12:53:59 +00:00
Jarred Sumner
426a4aa870 Refactor simplifyUnused a little 2025-08-09 04:47:09 -07:00
10 changed files with 747 additions and 194 deletions

View File

@@ -108,6 +108,11 @@ pub const Binary = struct {
pub const Boolean = struct {
value: bool,
pub fn eql(a: Boolean, b: Boolean) bool {
return a.value == b.value;
}
pub fn toJS(this: @This(), ctx: *jsc.JSGlobalObject) jsc.C.JSValueRef {
return jsc.C.JSValueMakeBoolean(ctx, this.value);
}
@@ -135,6 +140,10 @@ pub const ImportMetaMain = struct {
/// instead of wrapping in a unary not. This way, the printer can easily
/// print `require.main != module` instead of `!(require.main == module)`
inverted: bool = false,
pub fn eql(a: ImportMetaMain, b: ImportMetaMain) bool {
return a.inverted == b.inverted;
}
};
pub const Special = union(enum) {
@@ -153,6 +162,18 @@ pub const Special = union(enum) {
hot_accept_visited,
/// Prints the resolved specifier string for an import record.
resolved_specifier_string: ImportRecord.Index,
pub fn eql(a: *const Special, b: *const Special) bool {
return switch (a.*) {
.module_exports => b.* == .module_exports,
.hot_enabled => b.* == .hot_enabled,
.hot_disabled => b.* == .hot_disabled,
.hot_data => b.* == .hot_data,
.hot_accept => b.* == .hot_accept,
.hot_accept_visited => b.* == .hot_accept_visited,
.resolved_specifier_string => b.* == .resolved_specifier_string and a.resolved_specifier_string == b.resolved_specifier_string,
};
}
};
pub const Call = struct {
@@ -260,6 +281,13 @@ pub const Identifier = struct {
// the call target but keeping any arguments with side effects.
call_can_be_unwrapped_if_unused: bool = false,
pub fn eql(a: *const Identifier, b: *const Identifier) bool {
return a.ref.eql(b.ref) and
a.must_keep_due_to_with_stmt == b.must_keep_due_to_with_stmt and
a.can_be_removed_if_unused == b.can_be_removed_if_unused and
a.call_can_be_unwrapped_if_unused == b.call_can_be_unwrapped_if_unused;
}
pub inline fn init(ref: Ref) Identifier {
return Identifier{
.ref = ref,
@@ -296,6 +324,11 @@ pub const ImportIdentifier = struct {
/// false, this could potentially have been a member access expression such
/// as "ns.foo" off of an imported namespace object.
was_originally_identifier: bool = false,
pub fn eql(a: ImportIdentifier, b: ImportIdentifier) bool {
return a.ref.eql(b.ref) and
a.was_originally_identifier == b.was_originally_identifier;
}
};
/// This is a dot expression on exports, such as `exports.<ref>`. It is given
@@ -315,6 +348,11 @@ pub const CommonJSExportIdentifier = struct {
exports,
module_dot_exports,
};
pub fn eql(a: *const CommonJSExportIdentifier, b: *const CommonJSExportIdentifier) bool {
return a.ref.eql(b.ref) and
a.base == b.base;
}
};
// This is similar to EIdentifier but it represents class-private fields and
@@ -322,6 +360,10 @@ pub const CommonJSExportIdentifier = struct {
// EIndex and Property.
pub const PrivateIdentifier = struct {
ref: Ref,
pub fn eql(a: PrivateIdentifier, b: PrivateIdentifier) bool {
return a.ref.eql(b.ref);
}
};
/// In development mode, the new JSX transform has a few special props
@@ -404,6 +446,10 @@ pub const Number = struct {
return toStringFromF64(this.value, allocator);
}
pub fn eql(a: Number, b: Number) bool {
return a.value == b.value;
}
pub fn toStringFromF64(value: f64, allocator: std.mem.Allocator) ?string {
if (value == @trunc(value) and (value < std.math.maxInt(i32) and value > std.math.minInt(i32))) {
const int_value = @as(i64, @intFromFloat(value));
@@ -476,6 +522,10 @@ pub const BigInt = struct {
pub var empty = BigInt{ .value = "" };
pub fn eql(a: BigInt, b: BigInt) bool {
return std.mem.eql(u8, a.value, b.value);
}
pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
return try writer.write(self.value);
}
@@ -1356,12 +1406,21 @@ pub const RequireString = struct {
import_record_index: u32 = 0,
unwrapped_id: u32 = std.math.maxInt(u32),
pub fn eql(a: RequireString, b: RequireString) bool {
return a.import_record_index == b.import_record_index and
a.unwrapped_id == b.unwrapped_id;
}
};
pub const RequireResolveString = struct {
import_record_index: u32,
// close_paren_loc: logger.Loc = logger.Loc.Empty,
pub fn eql(a: RequireResolveString, b: RequireResolveString) bool {
return a.import_record_index == b.import_record_index;
}
};
pub const InlinedEnum = struct {

View File

@@ -605,7 +605,7 @@ pub fn joinAllWithComma(all: []Expr, allocator: std.mem.Allocator) Expr {
}
}
pub fn joinAllWithCommaCallback(all: []Expr, comptime Context: type, ctx: Context, comptime callback: (fn (ctx: anytype, expr: Expr) ?Expr), allocator: std.mem.Allocator) ?Expr {
pub fn joinAllWithCommaCallback(all: []Expr, comptime Context: type, ctx: Context, comptime callback: (fn (ctx: Context, expr: Expr) ?Expr), allocator: std.mem.Allocator) ?Expr {
switch (all.len) {
0 => return null,
1 => {
@@ -2058,6 +2058,53 @@ pub inline fn knownPrimitive(self: @This()) PrimitiveType {
return self.data.knownPrimitive();
}
/// Try to insert an optional chain operator to optimize expressions like:
/// "a != null && a.b()" => "a?.b()"
/// "a == null || a.b()" => "a?.b()"
pub fn tryToInsertOptionalChain(check_expr: Expr, expr: *Expr) bool {
switch (expr.data) {
.e_dot => |*dot| {
if (check_expr.data.eqlPtr(&dot.target.data)) {
dot.optional_chain = .start;
return true;
}
if (tryToInsertOptionalChain(check_expr, &dot.target)) {
if (dot.optional_chain == null) {
dot.optional_chain = .cont;
}
return true;
}
},
.e_index => |*index| {
if (check_expr.data.eqlPtr(&index.target.data)) {
index.optional_chain = .start;
return true;
}
if (tryToInsertOptionalChain(check_expr, &index.target)) {
if (index.optional_chain == null) {
index.optional_chain = .cont;
}
return true;
}
},
.e_call => |*call| {
if (check_expr.data.eqlPtr(&call.target.data)) {
call.optional_chain = .start;
return true;
}
if (tryToInsertOptionalChain(check_expr, &call.target)) {
if (call.optional_chain == null) {
call.optional_chain = .cont;
}
return true;
}
},
else => {},
}
return false;
}
pub const PrimitiveType = enum {
unknown,
mixed,
@@ -2803,8 +2850,8 @@ pub const Data = union(Tag) {
return @as(Expr.Tag, data).typeof();
}
pub fn toNumber(data: Expr.Data) ?f64 {
return switch (data) {
pub fn toNumber(data: *const Expr.Data) ?f64 {
return switch (data.*) {
.e_null => 0,
.e_undefined => std.math.nan(f64),
.e_string => |str| {
@@ -2831,8 +2878,8 @@ pub const Data = union(Tag) {
};
}
pub fn toFiniteNumber(data: Expr.Data) ?f64 {
return switch (data) {
pub fn toFiniteNumber(data: *const 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
@@ -2877,6 +2924,60 @@ pub const Data = union(Tag) {
pub const unknown = Equality{ .ok = false };
};
pub fn eqlPtr(lhs: *const Expr.Data, rhs: *const Expr.Data) bool {
if (@as(Expr.Tag, lhs.*) != @as(Expr.Tag, rhs.*)) return false;
return switch (lhs.*) {
.e_array => |l| l == rhs.e_array,
.e_unary => |l| l == rhs.e_unary,
.e_binary => |l| l == rhs.e_binary,
.e_class => |l| l == rhs.e_class,
.e_new => |l| l == rhs.e_new,
.e_function => |l| l == rhs.e_function,
.e_call => |l| l == rhs.e_call,
.e_dot => |l| l == rhs.e_dot,
.e_index => |l| l == rhs.e_index,
.e_arrow => |l| l == rhs.e_arrow,
.e_jsx_element => |l| l == rhs.e_jsx_element,
.e_object => |l| l == rhs.e_object,
.e_spread => |l| l == rhs.e_spread,
.e_template => |l| l == rhs.e_template,
.e_reg_exp => |l| l == rhs.e_reg_exp,
.e_await => |l| l == rhs.e_await,
.e_yield => |l| l == rhs.e_yield,
.e_if => |l| l == rhs.e_if,
.e_import => |l| l == rhs.e_import,
.e_big_int => |l| l == rhs.e_big_int,
.e_string => |l| l == rhs.e_string,
.e_inlined_enum => |l| l == rhs.e_inlined_enum,
.e_name_of_symbol => |l| l == rhs.e_name_of_symbol,
// For value types, fall back to value equality since they don't have pointer identity
.e_identifier => |l| l.eql(&rhs.e_identifier),
.e_import_identifier => |l| l.eql(rhs.e_import_identifier),
.e_private_identifier => |l| l.eql(rhs.e_private_identifier),
.e_commonjs_export_identifier => |l| l.eql(&rhs.e_commonjs_export_identifier),
.e_boolean => |l| l.eql(rhs.e_boolean),
.e_number => |l| l.eql(rhs.e_number),
.e_require_string => |l| l.eql(rhs.e_require_string),
.e_require_resolve_string => |l| l.eql(rhs.e_require_resolve_string),
.e_import_meta_main => |l| l.eql(rhs.e_import_meta_main),
.e_special => |l| l.eql(&rhs.e_special),
.e_missing,
.e_this,
.e_super,
.e_null,
.e_undefined,
.e_new_target,
.e_import_meta,
.e_require_main,
.e_require_call_target,
.e_require_resolve_call_target,
=> true,
};
}
// Returns "equal, ok". If "ok" is false, then nothing is known about the two
// values. If "ok" is true, the equality or inequality of the two values is
// stored in "equal".

View File

@@ -748,20 +748,20 @@ pub fn NewParser_(
}
},
.s_if => |if_statement| {
const result = SideEffects.toBoolean(p, if_statement.test_.data);
const result = SideEffects.toBoolean(p, &if_statement.test_.data);
if (!(result.ok and result.side_effects == .no_side_effects and !result.value)) {
break :can_remove_part false;
}
},
.s_while => |while_statement| {
const result = SideEffects.toBoolean(p, while_statement.test_.data);
const result = SideEffects.toBoolean(p, &while_statement.test_.data);
if (!(result.ok and result.side_effects == .no_side_effects and !result.value)) {
break :can_remove_part false;
}
},
.s_for => |for_statement| {
if (for_statement.test_) |expr| {
const result = SideEffects.toBoolean(p, expr.data);
const result = SideEffects.toBoolean(p, &expr.data);
if (!(result.ok and result.side_effects == .no_side_effects and !result.value)) {
break :can_remove_part false;
}
@@ -4483,6 +4483,286 @@ pub fn NewParser_(
return;
}
pub fn mangleIf(p: *P, stmts: *ListManaged(Stmt), loc: logger.Loc, if_stmt: *S.If) !void {
// Constant folding using the test expression
const effects = SideEffects.toBoolean(p, &if_stmt.test_.data);
if (effects.ok) {
if (effects.value) {
// The test is truthy
if (if_stmt.no == null or !SideEffects.shouldKeepStmtInDeadControlFlow(if_stmt.no.?, p.allocator)) {
// We can drop the "no" branch
if (effects.side_effects == .could_have_side_effects) {
// Keep the condition if it could have side effects (but is still known to be truthy)
if (SideEffects.simplifyUnusedExpr(p, if_stmt.test_)) |test_| {
try stmts.append(p.s(S.SExpr{ .value = test_ }, test_.loc));
}
}
return try p.appendIfBodyPreservingScope(stmts, if_stmt.yes);
} else {
// We have to keep the "no" branch
}
} else {
// The test is falsy
if (!SideEffects.shouldKeepStmtInDeadControlFlow(if_stmt.yes, p.allocator)) {
// We can drop the "yes" branch
if (effects.side_effects == .could_have_side_effects) {
// Keep the condition if it could have side effects (but is still known to be falsy)
if (SideEffects.simplifyUnusedExpr(p, if_stmt.test_)) |test_| {
try stmts.append(p.s(S.SExpr{ .value = test_ }, test_.loc));
}
}
if (if_stmt.no == null) {
return;
}
return try p.appendIfBodyPreservingScope(stmts, if_stmt.no.?);
} else {
// We have to keep the "yes" branch
}
}
// Use "1" and "0" instead of "true" and "false" to be shorter
if (effects.side_effects == .no_side_effects) {
if (effects.value) {
if_stmt.test_.data = .{ .e_number = .{ .value = 1 } };
} else {
if_stmt.test_.data = .{ .e_number = .{ .value = 0 } };
}
}
}
var expr: ?Expr = null;
if (if_stmt.yes.data == .s_expr) {
const yes_expr = if_stmt.yes.data.s_expr;
// "yes" is an expression
if (if_stmt.no == null) {
if (if_stmt.test_.data == .e_unary and if_stmt.test_.data.e_unary.op == .un_not) {
const not = if_stmt.test_.data.e_unary;
// "if (!a) b();" => "a || b();"
expr = Expr.joinWithLeftAssociativeOp(.bin_logical_or, not.value, yes_expr.value, p.allocator);
} else {
// "if (a) b();" => "a && b();"
expr = Expr.joinWithLeftAssociativeOp(.bin_logical_and, if_stmt.test_, yes_expr.value, p.allocator);
}
} else if (if_stmt.no.?.data == .s_expr) {
const no_expr = if_stmt.no.?.data.s_expr;
// "if (a) b(); else c();" => "a ? b() : c();"
expr = p.mangleIfExpr(loc, if_stmt.test_, yes_expr.value, no_expr.value);
}
} else if (if_stmt.yes.data == .s_empty) {
// "yes" is missing
if (if_stmt.no == null) {
// "yes" and "no" are both missing
if (p.exprCanBeRemovedIfUnused(&if_stmt.test_)) {
// "if (1) {}" => ""
return;
} else {
// "if (a) {}" => "a;"
expr = if_stmt.test_;
}
} else if (if_stmt.no.?.data == .s_expr) {
const no_expr = if_stmt.no.?.data.s_expr;
if (if_stmt.test_.data == .e_unary and if_stmt.test_.data.e_unary.op == .un_not) {
const not = if_stmt.test_.data.e_unary;
// "if (!a) {} else b();" => "a && b();"
expr = Expr.joinWithLeftAssociativeOp(.bin_logical_and, not.value, no_expr.value, p.allocator);
} else {
// "if (a) {} else b();" => "a || b();"
expr = Expr.joinWithLeftAssociativeOp(.bin_logical_or, if_stmt.test_, no_expr.value, p.allocator);
}
} else {
// "yes" is missing and "no" is not missing (and is not an expression)
if (if_stmt.test_.data == .e_unary and if_stmt.test_.data.e_unary.op == .un_not) {
const not = if_stmt.test_.data.e_unary;
// "if (!a) {} else throw b;" => "if (a) throw b;"
if_stmt.test_ = not.value;
if_stmt.yes = if_stmt.no.?;
if_stmt.no = null;
} else {
// "if (a) {} else throw b;" => "if (!a) throw b;"
if_stmt.test_ = p.newExpr(E.Unary{ .op = .un_not, .value = if_stmt.test_ }, if_stmt.test_.loc);
if_stmt.yes = if_stmt.no.?;
if_stmt.no = null;
}
}
} else {
// "yes" is not missing (and is not an expression)
if (if_stmt.no) |no| {
// "yes" is not missing (and is not an expression) and "no" is not missing
if (if_stmt.test_.data == .e_unary and if_stmt.test_.data.e_unary.op == .un_not) {
const not = if_stmt.test_.data.e_unary;
// "if (!a) return b; else return c;" => "if (a) return c; else return b;"
if_stmt.test_ = not.value;
const temp = if_stmt.yes;
if_stmt.yes = no;
if_stmt.no = temp;
}
} else {
// "no" is missing
if (if_stmt.yes.data == .s_if) {
const nested_if = if_stmt.yes.data.s_if;
if (nested_if.no == null) {
// "if (a) if (b) return c;" => "if (a && b) return c;"
if_stmt.test_ = Expr.joinWithLeftAssociativeOp(.bin_logical_and, if_stmt.test_, nested_if.test_, p.allocator);
if_stmt.yes = nested_if.yes;
}
}
}
}
// Return an expression if we replaced the if statement with an expression above
if (expr) |e| {
const simplified = SideEffects.simplifyUnusedExpr(p, e) orelse e;
return try stmts.append(p.s(S.SExpr{ .value = simplified }, loc));
}
return try stmts.append(Stmt{ .loc = loc, .data = .{ .s_if = if_stmt } });
}
pub fn mangleIfExpr(p: *P, loc: logger.Loc, test_: Expr, yes: Expr, no: Expr) Expr {
var test_expr = test_;
var yes_expr = yes;
var no_expr = no;
// "(a, b) ? c : d" => "a, b ? c : d"
if (test_expr.data == .e_binary) {
const comma = test_expr.data.e_binary;
if (comma.op == .bin_comma) {
return Expr.joinWithComma(
comma.left,
p.mangleIfExpr(comma.right.loc, comma.right, yes_expr, no_expr),
p.allocator
);
}
}
// "!a ? b : c" => "a ? c : b"
if (test_expr.data == .e_unary) {
const not = test_expr.data.e_unary;
if (not.op == .un_not) {
test_expr = not.value;
const temp = yes_expr;
yes_expr = no_expr;
no_expr = temp;
}
}
if (yes_expr.data.eqlPtr(&no_expr.data)) {
// "/* @__PURE__ */ a() ? b : b" => "b"
if (p.exprCanBeRemovedIfUnused(&test_expr)) {
return yes_expr;
}
// "a ? b : b" => "a, b"
return Expr.joinWithComma(test_expr, yes_expr, p.allocator);
}
// "a ? true : false" => "!!a"
// "a ? false : true" => "!a"
if (yes_expr.data == .e_boolean and no_expr.data == .e_boolean) {
const y = yes_expr.data.e_boolean;
const n = no_expr.data.e_boolean;
if (y.value and !n.value) {
return p.newExpr(E.Unary{
.op = .un_not,
.value = p.newExpr(E.Unary{ .op = .un_not, .value = test_expr }, test_expr.loc)
}, test_expr.loc);
}
if (!y.value and n.value) {
return p.newExpr(E.Unary{ .op = .un_not, .value = test_expr }, test_expr.loc);
}
}
if (test_expr.data == .e_identifier) {
const id = test_expr.data.e_identifier;
// "a ? a : b" => "a || b"
if (yes_expr.data == .e_identifier and yes_expr.data.e_identifier.ref.eql(id.ref)) {
return Expr.joinWithLeftAssociativeOp(.bin_logical_or, test_expr, no_expr, p.allocator);
}
// "a ? b : a" => "a && b"
if (no_expr.data == .e_identifier and no_expr.data.e_identifier.ref.eql(id.ref)) {
return Expr.joinWithLeftAssociativeOp(.bin_logical_and, test_expr, yes_expr, p.allocator);
}
}
// "a ? b ? c : d : d" => "a && b ? c : d"
if (yes_expr.data == .e_if) {
const yes_if = yes_expr.data.e_if;
if (yes_if.no.data.eqlPtr(&no_expr.data)) {
return p.newExpr(E.If{
.test_ = Expr.joinWithLeftAssociativeOp(.bin_logical_and, test_expr, yes_if.test_, p.allocator),
.yes = yes_if.yes,
.no = no_expr,
}, loc);
}
}
// "a ? b : c ? b : d" => "a || c ? b : d"
if (no_expr.data == .e_if) {
const no_if = no_expr.data.e_if;
if (yes_expr.data.eqlPtr(&no_if.yes.data)) {
return p.newExpr(E.If{
.test_ = Expr.joinWithLeftAssociativeOp(.bin_logical_or, test_expr, no_if.test_, p.allocator),
.yes = yes_expr,
.no = no_if.no,
}, loc);
}
}
// "a ? c : (b, c)" => "(a || b), c"
if (no_expr.data == .e_binary) {
const comma = no_expr.data.e_binary;
if (comma.op == .bin_comma and yes_expr.data.eqlPtr(&comma.right.data)) {
return Expr.joinWithComma(
Expr.joinWithLeftAssociativeOp(.bin_logical_or, test_expr, comma.left, p.allocator),
comma.right,
p.allocator
);
}
}
// "a ? (b, c) : c" => "(a && b), c"
if (yes_expr.data == .e_binary) {
const comma = yes_expr.data.e_binary;
if (comma.op == .bin_comma and comma.right.data.eqlPtr(&no_expr.data)) {
return Expr.joinWithComma(
Expr.joinWithLeftAssociativeOp(.bin_logical_and, test_expr, comma.left, p.allocator),
comma.right,
p.allocator
);
}
}
// "a ? b || c : c" => "(a && b) || c"
if (yes_expr.data == .e_binary) {
const binary = yes_expr.data.e_binary;
if (binary.op == .bin_logical_or and binary.right.data.eqlPtr(&no_expr.data)) {
return p.newExpr(E.Binary{
.op = .bin_logical_or,
.left = Expr.joinWithLeftAssociativeOp(.bin_logical_and, test_expr, binary.left, p.allocator),
.right = binary.right,
}, loc);
}
}
// "a ? c : b && c" => "(a || b) && c"
if (no_expr.data == .e_binary) {
const binary = no_expr.data.e_binary;
if (binary.op == .bin_logical_and and yes_expr.data.eqlPtr(&binary.right.data)) {
return p.newExpr(E.Binary{
.op = .bin_logical_and,
.left = Expr.joinWithLeftAssociativeOp(.bin_logical_or, test_expr, binary.left, p.allocator),
.right = binary.right,
}, loc);
}
}
// Don't mutate the original AST
return p.newExpr(E.If{
.test_ = test_expr,
.yes = yes_expr,
.no = no_expr,
}, loc);
}
fn markExportedBindingInsideNamespace(p: *P, ref: Ref, binding: BindingNodeIndex) void {
switch (binding.data) {
.b_missing => {},

View File

@@ -18,11 +18,11 @@ pub const SideEffects = enum(u1) {
if (!p.options.features.dead_code_elimination) return expr;
var result: Expr = expr;
_simplifyBoolean(p, &result);
_simplifyBoolean(&result);
return result;
}
fn _simplifyBoolean(p: anytype, expr: *Expr) void {
fn _simplifyBoolean(expr: *Expr) void {
while (true) {
switch (expr.data) {
.e_unary => |e| {
@@ -33,13 +33,13 @@ pub const SideEffects = enum(u1) {
continue;
}
_simplifyBoolean(p, &e.value);
_simplifyBoolean(&e.value);
}
},
.e_binary => |e| {
switch (e.op) {
.bin_logical_and => {
const effects = SideEffects.toBoolean(p, e.right.data);
const effects = _toBoolean(&e.right.data);
if (effects.ok and effects.value and effects.side_effects == .no_side_effects) {
// "if (anything && truthyNoSideEffects)" => "if (anything)"
expr.* = e.left;
@@ -47,7 +47,7 @@ pub const SideEffects = enum(u1) {
}
},
.bin_logical_or => {
const effects = SideEffects.toBoolean(p, e.right.data);
const effects = _toBoolean(&e.right.data);
if (effects.ok and !effects.value and effects.side_effects == .no_side_effects) {
// "if (anything || falsyNoSideEffects)" => "if (anything)"
expr.* = e.left;
@@ -66,23 +66,27 @@ pub const SideEffects = enum(u1) {
pub const toNumber = Expr.Data.toNumber;
pub const typeof = Expr.Data.toTypeof;
pub fn isPrimitiveToReorder(data: Expr.Data) bool {
return switch (data) {
pub fn isPrimitiveToReorder(data: *const Expr.Data) bool {
return switch (data.*) {
.e_null,
.e_undefined,
.e_string,
.e_boolean,
.e_number,
.e_big_int,
.e_inlined_enum,
.e_require_main,
=> true,
.e_inlined_enum => |e| isPrimitiveToReorder(&e.value.data),
else => false,
};
}
pub fn simplifyUnusedExpr(p: anytype, expr: Expr) ?Expr {
if (!p.options.features.dead_code_elimination) return expr;
const SimplifyUnusedExprContext = struct {
symbols: *const std.ArrayList(js_ast.Symbol),
allocator: std.mem.Allocator,
};
fn _simplifyUnusedExpr(ctx: *const SimplifyUnusedExprContext, expr: Expr) ?Expr {
switch (expr.data) {
.e_null,
.e_undefined,
@@ -109,17 +113,17 @@ pub const SideEffects = enum(u1) {
return expr;
}
if (ident.can_be_removed_if_unused or p.symbols.items[ident.ref.innerIndex()].kind != .unbound) {
if (ident.can_be_removed_if_unused or ctx.symbols.items[ident.ref.innerIndex()].kind != .unbound) {
return null;
}
},
.e_if => |ternary| {
ternary.yes = simplifyUnusedExpr(p, ternary.yes) orelse ternary.yes.toEmpty();
ternary.no = simplifyUnusedExpr(p, ternary.no) orelse ternary.no.toEmpty();
ternary.yes = _simplifyUnusedExpr(ctx, ternary.yes) orelse ternary.yes.toEmpty();
ternary.no = _simplifyUnusedExpr(ctx, ternary.no) orelse ternary.no.toEmpty();
// "foo() ? 1 : 2" => "foo()"
if (ternary.yes.isEmpty() and ternary.no.isEmpty()) {
return simplifyUnusedExpr(p, ternary.test_);
return _simplifyUnusedExpr(ctx, ternary.test_);
}
// "foo() ? 1 : bar()" => "foo() || bar()"
@@ -128,7 +132,7 @@ pub const SideEffects = enum(u1) {
.bin_logical_or,
ternary.test_,
ternary.no,
p.allocator,
ctx.allocator,
);
}
@@ -138,7 +142,7 @@ pub const SideEffects = enum(u1) {
.bin_logical_and,
ternary.test_,
ternary.yes,
p.allocator,
ctx.allocator,
);
}
},
@@ -147,7 +151,7 @@ pub const SideEffects = enum(u1) {
// such as "toString" or "valueOf". They must also never throw any exceptions.
switch (un.op) {
.un_void, .un_not => {
return simplifyUnusedExpr(p, un.value);
return _simplifyUnusedExpr(ctx, un.value);
},
.un_typeof => {
// "typeof x" must not be transformed into if "x" since doing so could
@@ -157,7 +161,7 @@ pub const SideEffects = enum(u1) {
return null;
}
return simplifyUnusedExpr(p, un.value);
return _simplifyUnusedExpr(ctx, un.value);
},
else => {},
@@ -169,7 +173,7 @@ pub const SideEffects = enum(u1) {
// can be removed. The annotation causes us to ignore the target.
if (call.can_be_unwrapped_if_unused != .never) {
if (call.args.len > 0) {
const joined = Expr.joinAllWithCommaCallback(call.args.slice(), @TypeOf(p), p, comptime simplifyUnusedExpr, p.allocator);
const joined = Expr.joinAllWithCommaCallback(call.args.slice(), *const SimplifyUnusedExprContext, ctx, comptime _simplifyUnusedExpr, ctx.allocator);
if (joined != null and call.can_be_unwrapped_if_unused == .if_unused_and_toString_safe) {
@branchHint(.unlikely);
// For now, only support this for 1 argument.
@@ -185,13 +189,17 @@ pub const SideEffects = enum(u1) {
},
.e_binary => |bin| {
var left = bin.left;
var right = bin.right;
switch (bin.op) {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
.bin_strict_eq,
.bin_strict_ne,
.bin_comma,
=> return simplifyUnusedBinaryCommaExpr(p, expr),
.bin_strict_eq, .bin_strict_ne, .bin_comma => return Expr.joinWithComma(
_simplifyUnusedExpr(ctx, left) orelse left.toEmpty(),
_simplifyUnusedExpr(ctx, right) orelse right.toEmpty(),
ctx.allocator,
),
// We can simplify "==" and "!=" even though they can call "toString" and/or
// "valueOf" if we can statically determine that the types of both sides are
@@ -200,35 +208,89 @@ pub const SideEffects = enum(u1) {
.bin_loose_eq,
.bin_loose_ne,
=> {
if (isPrimitiveWithSideEffects(bin.left.data) and isPrimitiveWithSideEffects(bin.right.data)) {
if (left.data.mergeKnownPrimitive(right.data) != .unknown) {
return Expr.joinWithComma(
simplifyUnusedExpr(p, bin.left) orelse bin.left.toEmpty(),
simplifyUnusedExpr(p, bin.right) orelse bin.right.toEmpty(),
p.allocator,
_simplifyUnusedExpr(ctx, left) orelse left.toEmpty(),
_simplifyUnusedExpr(ctx, right) orelse right.toEmpty(),
ctx.allocator,
);
}
// If one side is a number, the number can be printed as
// `0` since the result being unused doesnt matter, we
// only care to invoke the coercion.
if (bin.left.data == .e_number) {
bin.left.data = .{ .e_number = .{ .value = 0.0 } };
} else if (bin.right.data == .e_number) {
bin.right.data = .{ .e_number = .{ .value = 0.0 } };
}
},
.bin_logical_and, .bin_logical_or, .bin_nullish_coalescing => {
bin.right = simplifyUnusedExpr(p, bin.right) orelse bin.right.toEmpty();
.bin_logical_and, .bin_logical_or, .bin_nullish_coalescing => |op| {
// If this is a boolean logical operation and the result is unused, then
// we know the left operand will only be used for its boolean value and
// can be simplified under that assumption
if (op != .bin_nullish_coalescing) {
_simplifyBoolean(&left);
}
right = _simplifyUnusedExpr(ctx, right) orelse Expr.empty;
// Preserve short-circuit behavior: the left expression is only unused if
// the right expression can be completely removed. Otherwise, the left
// expression is important for the branch.
if (right.isEmpty()) {
return _simplifyUnusedExpr(ctx, left);
}
if (bin.right.isEmpty())
return simplifyUnusedExpr(p, bin.left);
// Try to take advantage of the optional chain operator to shorten code
if (bin.op != .bin_nullish_coalescing) {
if (left.data == .e_binary) {
const binary = left.data.e_binary;
// "a != null && a.b()" => "a?.b()"
// "a == null || a.b()" => "a?.b()"
if ((binary.op == .bin_loose_ne and bin.op == .bin_logical_and) or
(binary.op == .bin_loose_eq and bin.op == .bin_logical_or)) {
var test_expr: ?Expr = null;
if (binary.right.data == .e_null) {
test_expr = binary.left;
} else if (binary.left.data == .e_null) {
test_expr = binary.right;
}
if (test_expr) |test_val| {
// Note: Technically unbound identifiers can refer to a getter on
// the global object and that getter can have side effects that can
// be observed if we run that getter once instead of twice. But this
// seems like terrible coding practice and very unlikely to come up
// in real software, so we deliberately ignore this possibility and
// optimize for size instead of for this obscure edge case.
if (test_val.data == .e_identifier) {
const id = test_val.data.e_identifier;
if (!id.must_keep_due_to_with_stmt) {
// TODO: Optional chaining optimization disabled due to existing Expr.zig type issue
// This would transform "a != null && a.b()" => "a?.b()"
// But there's a pre-existing type issue in tryToInsertOptionalChain
}
}
}
}
}
}
},
.bin_add => {
if (simplifyUnusedStringAdditionChain(expr)) |result| {
return result;
}
},
else => {},
}
if (!bin.left.data.eqlPtr(&left.data) or !bin.right.data.eqlPtr(&right.data)) {
return Expr.init(
E.Binary,
E.Binary{
.op = bin.op,
.left = left,
.right = right,
},
expr.loc,
);
}
},
.e_object => {
@@ -237,24 +299,24 @@ pub const SideEffects = enum(u1) {
// the other items instead and leave the object expression there.
var properties_slice = expr.data.e_object.properties.slice();
var end: usize = 0;
for (properties_slice) |spread| {
for (properties_slice) |*spread| {
end = 0;
if (spread.kind == .spread) {
// Spread properties must always be evaluated
for (properties_slice) |prop_| {
var prop = prop_;
if (prop_.kind != .spread) {
const value = simplifyUnusedExpr(p, prop.value.?);
for (properties_slice) |*prop_| {
var prop = prop_.*;
if (prop.kind != .spread) {
const value = _simplifyUnusedExpr(ctx, prop.value.?);
if (value != null) {
prop.value = value;
} else if (!prop.flags.contains(.is_computed)) {
continue;
} else {
prop.value = p.newExpr(E.Number{ .value = 0.0 }, prop.value.?.loc);
prop.value = Expr.init(E.Number, E.Number{ .value = 0.0 }, prop.value.?.loc);
}
}
properties_slice[end] = prop_;
properties_slice[end] = prop;
end += 1;
}
@@ -268,24 +330,25 @@ pub const SideEffects = enum(u1) {
// Otherwise, the object can be completely removed. We only need to keep any
// object properties with side effects. Apply this simplification recursively.
for (properties_slice) |prop| {
for (properties_slice) |*prop| {
if (prop.flags.contains(.is_computed)) {
// Make sure "ToString" is still evaluated on the key
result = result.joinWithComma(
p.newExpr(
Expr.init(
E.Binary,
E.Binary{
.op = .bin_add,
.left = prop.key.?,
.right = p.newExpr(E.String{}, prop.key.?.loc),
.right = Expr.init(E.String, E.String{}, prop.key.?.loc),
},
prop.key.?.loc,
),
p.allocator,
ctx.allocator,
);
}
result = result.joinWithComma(
simplifyUnusedExpr(p, prop.value.?) orelse prop.value.?.toEmpty(),
p.allocator,
_simplifyUnusedExpr(ctx, prop.value.?) orelse prop.value.?.toEmpty(),
ctx.allocator,
);
}
@@ -314,10 +377,10 @@ pub const SideEffects = enum(u1) {
// array items with side effects. Apply this simplification recursively.
return Expr.joinAllWithCommaCallback(
items,
@TypeOf(p),
p,
comptime simplifyUnusedExpr,
p.allocator,
*const SimplifyUnusedExprContext,
ctx,
comptime _simplifyUnusedExpr,
ctx.allocator,
);
},
@@ -327,56 +390,19 @@ pub const SideEffects = enum(u1) {
return expr;
}
pub fn simplifyUnusedExpr(p: anytype, expr: Expr) ?Expr {
if (!p.options.features.dead_code_elimination) return expr;
var ctx = SimplifyUnusedExprContext{
.symbols = &p.symbols,
.allocator = p.allocator,
};
return _simplifyUnusedExpr(&ctx, expr);
}
pub const BinaryExpressionSimplifyVisitor = struct {
bin: *E.Binary,
};
///
fn simplifyUnusedBinaryCommaExpr(p: anytype, expr: Expr) ?Expr {
if (Environment.allow_assert) {
assert(expr.data == .e_binary);
assert(switch (expr.data.e_binary.op) {
.bin_strict_eq,
.bin_strict_ne,
.bin_comma,
=> true,
else => false,
});
}
const stack: *std.ArrayList(BinaryExpressionSimplifyVisitor) = &p.binary_expression_simplify_stack;
const stack_bottom = stack.items.len;
defer stack.shrinkRetainingCapacity(stack_bottom);
stack.append(.{ .bin = expr.data.e_binary }) catch bun.outOfMemory();
// Build stack up of expressions
var left: Expr = expr.data.e_binary.left;
while (left.data.as(.e_binary)) |left_bin| {
switch (left_bin.op) {
.bin_strict_eq,
.bin_strict_ne,
.bin_comma,
=> {
stack.append(.{ .bin = left_bin }) catch bun.outOfMemory();
left = left_bin.left;
},
else => break,
}
}
// Ride the stack downwards
var i = stack.items.len;
var result = simplifyUnusedExpr(p, left) orelse Expr.empty;
while (i > stack_bottom) {
i -= 1;
const top = stack.items[i];
const visited_right = simplifyUnusedExpr(p, top.bin.right) orelse Expr.empty;
result = result.joinWithComma(visited_right, p.allocator);
}
return if (result.isMissing()) null else result;
}
fn findIdentifiers(binding: Binding, decls: *std.ArrayList(G.Decl)) void {
switch (binding.data) {
.b_identifier => {
@@ -518,8 +544,8 @@ pub const SideEffects = enum(u1) {
// Returns true if this expression is known to result in a primitive value (i.e.
// null, undefined, boolean, number, bigint, or string), even if the expression
// cannot be removed due to side effects.
pub fn isPrimitiveWithSideEffects(data: Expr.Data) bool {
switch (data) {
pub fn isPrimitiveWithSideEffects(data: *const Expr.Data) bool {
switch (data.*) {
.e_null,
.e_undefined,
.e_boolean,
@@ -604,16 +630,16 @@ pub const SideEffects = enum(u1) {
.bin_logical_or_assign,
.bin_nullish_coalescing_assign,
=> {
return isPrimitiveWithSideEffects(e.left.data) and isPrimitiveWithSideEffects(e.right.data);
return isPrimitiveWithSideEffects(&e.left.data) and isPrimitiveWithSideEffects(&e.right.data);
},
.bin_comma => {
return isPrimitiveWithSideEffects(e.right.data);
return isPrimitiveWithSideEffects(&e.right.data);
},
else => {},
}
},
.e_if => |e| {
return isPrimitiveWithSideEffects(e.yes.data) and isPrimitiveWithSideEffects(e.no.data);
return isPrimitiveWithSideEffects(&e.yes.data) and isPrimitiveWithSideEffects(&e.no.data);
},
else => {},
}
@@ -622,24 +648,28 @@ pub const SideEffects = enum(u1) {
pub const toTypeOf = Expr.Data.typeof;
pub fn toNullOrUndefined(p: anytype, exp: Expr.Data) Result {
pub fn toNullOrUndefined(p: anytype, exp: *const Expr.Data) Result {
if (!p.options.features.dead_code_elimination) {
// value should not be read if ok is false, all existing calls to this function already adhere to this
return Result{ .ok = false, .value = undefined, .side_effects = .could_have_side_effects };
}
switch (exp) {
return _toNullOrUndefined(exp);
}
fn _toNullOrUndefined(exp: *const Expr.Data) Result {
switch (exp.*) {
// Never null or undefined
.e_boolean, .e_number, .e_string, .e_reg_exp, .e_function, .e_arrow, .e_big_int => {
return Result{ .value = false, .side_effects = .no_side_effects, .ok = true };
return .{ .value = false, .side_effects = .no_side_effects, .ok = true };
},
.e_object, .e_array, .e_class => {
return Result{ .value = false, .side_effects = .could_have_side_effects, .ok = true };
return .{ .value = false, .side_effects = .could_have_side_effects, .ok = true };
},
// always a null or undefined
.e_null, .e_undefined => {
return Result{ .value = true, .side_effects = .no_side_effects, .ok = true };
return .{ .value = true, .side_effects = .no_side_effects, .ok = true };
},
.e_unary => |e| {
@@ -658,12 +688,12 @@ pub const SideEffects = enum(u1) {
.un_typeof,
.un_delete,
=> {
return Result{ .ok = true, .value = false, .side_effects = SideEffects.could_have_side_effects };
return .{ .ok = true, .value = false, .side_effects = .could_have_side_effects };
},
// Always undefined
.un_void => {
return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true };
return .{ .value = true, .side_effects = .could_have_side_effects, .ok = true };
},
else => {},
@@ -710,74 +740,74 @@ pub const SideEffects = enum(u1) {
.bin_strict_eq,
.bin_strict_ne,
=> {
return Result{ .ok = true, .value = false, .side_effects = SideEffects.could_have_side_effects };
return .{ .ok = true, .value = false, .side_effects = .could_have_side_effects };
},
.bin_comma => {
const res = toNullOrUndefined(p, e.right.data);
const res = _toNullOrUndefined(&e.right.data);
if (res.ok) {
return Result{ .ok = true, .value = res.value, .side_effects = SideEffects.could_have_side_effects };
return .{ .ok = true, .value = res.value, .side_effects = .could_have_side_effects };
}
},
else => {},
}
},
.e_inlined_enum => |inlined| {
return toNullOrUndefined(p, inlined.value.data);
return _toNullOrUndefined(&inlined.value.data);
},
else => {},
}
return Result{ .ok = false, .value = false, .side_effects = SideEffects.could_have_side_effects };
return .{ .ok = false, .value = false, .side_effects = .could_have_side_effects };
}
pub fn toBoolean(p: anytype, exp: Expr.Data) Result {
pub fn toBoolean(p: anytype, exp: *const Expr.Data) Result {
// Only do this check once.
if (!p.options.features.dead_code_elimination) {
// value should not be read if ok is false, all existing calls to this function already adhere to this
return Result{ .ok = false, .value = undefined, .side_effects = .could_have_side_effects };
return .{ .ok = false, .value = undefined, .side_effects = .could_have_side_effects };
}
return toBooleanWithoutDCECheck(exp);
return _toBoolean(exp);
}
// Avoid passing through *P
// This is a very recursive function.
fn toBooleanWithoutDCECheck(exp: Expr.Data) Result {
switch (exp) {
fn _toBoolean(exp: *const Expr.Data) Result {
switch (exp.*) {
.e_null, .e_undefined => {
return Result{ .ok = true, .value = false, .side_effects = .no_side_effects };
return .{ .ok = true, .value = false, .side_effects = .no_side_effects };
},
.e_boolean => |e| {
return Result{ .ok = true, .value = e.value, .side_effects = .no_side_effects };
return .{ .ok = true, .value = e.value, .side_effects = .no_side_effects };
},
.e_number => |e| {
return Result{ .ok = true, .value = e.value != 0.0 and !std.math.isNan(e.value), .side_effects = .no_side_effects };
return .{ .ok = true, .value = e.value != 0.0 and !std.math.isNan(e.value), .side_effects = .no_side_effects };
},
.e_big_int => |e| {
return Result{ .ok = true, .value = !strings.eqlComptime(e.value, "0"), .side_effects = .no_side_effects };
return .{ .ok = true, .value = !strings.eqlComptime(e.value, "0"), .side_effects = .no_side_effects };
},
.e_string => |e| {
return Result{ .ok = true, .value = e.isPresent(), .side_effects = .no_side_effects };
return .{ .ok = true, .value = e.isPresent(), .side_effects = .no_side_effects };
},
.e_function, .e_arrow, .e_reg_exp => {
return Result{ .ok = true, .value = true, .side_effects = .no_side_effects };
return .{ .ok = true, .value = true, .side_effects = .no_side_effects };
},
.e_object, .e_array, .e_class => {
return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
return .{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
},
.e_unary => |e_| {
switch (e_.op) {
.un_void => {
return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects };
return .{ .ok = true, .value = false, .side_effects = .could_have_side_effects };
},
.un_typeof => {
// Never an empty string
return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
return .{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
},
.un_not => {
const result = toBooleanWithoutDCECheck(e_.value.data);
const result = _toBoolean(&e_.value.data);
if (result.ok) {
return .{ .ok = true, .value = !result.value, .side_effects = result.side_effects };
}
@@ -789,21 +819,21 @@ pub const SideEffects = enum(u1) {
switch (e_.op) {
.bin_logical_or => {
// "anything || truthy" is truthy
const result = toBooleanWithoutDCECheck(e_.right.data);
const result = _toBoolean(&e_.right.data);
if (result.value and result.ok) {
return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
return .{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
}
},
.bin_logical_and => {
// "anything && falsy" is falsy
const result = toBooleanWithoutDCECheck(e_.right.data);
const result = _toBoolean(&e_.right.data);
if (!result.value and result.ok) {
return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects };
return .{ .ok = true, .value = false, .side_effects = .could_have_side_effects };
}
},
.bin_comma => {
// "anything, truthy/falsy" is truthy/falsy
var result = toBooleanWithoutDCECheck(e_.right.data);
var result = _toBoolean(&e_.right.data);
if (result.ok) {
result.side_effects = .could_have_side_effects;
return result;
@@ -812,28 +842,28 @@ pub const SideEffects = enum(u1) {
.bin_gt => {
if (e_.left.data.toFiniteNumber()) |left_num| {
if (e_.right.data.toFiniteNumber()) |right_num| {
return Result{ .ok = true, .value = left_num > right_num, .side_effects = .no_side_effects };
return .{ .ok = true, .value = left_num > right_num, .side_effects = .no_side_effects };
}
}
},
.bin_lt => {
if (e_.left.data.toFiniteNumber()) |left_num| {
if (e_.right.data.toFiniteNumber()) |right_num| {
return Result{ .ok = true, .value = left_num < right_num, .side_effects = .no_side_effects };
return .{ .ok = true, .value = left_num < right_num, .side_effects = .no_side_effects };
}
}
},
.bin_le => {
if (e_.left.data.toFiniteNumber()) |left_num| {
if (e_.right.data.toFiniteNumber()) |right_num| {
return Result{ .ok = true, .value = left_num <= right_num, .side_effects = .no_side_effects };
return .{ .ok = true, .value = left_num <= right_num, .side_effects = .no_side_effects };
}
}
},
.bin_ge => {
if (e_.left.data.toFiniteNumber()) |left_num| {
if (e_.right.data.toFiniteNumber()) |right_num| {
return Result{ .ok = true, .value = left_num >= right_num, .side_effects = .no_side_effects };
return .{ .ok = true, .value = left_num >= right_num, .side_effects = .no_side_effects };
}
}
},
@@ -841,7 +871,7 @@ pub const SideEffects = enum(u1) {
}
},
.e_inlined_enum => |inlined| {
return toBooleanWithoutDCECheck(inlined.value.data);
return _toBoolean(&inlined.value.data);
},
.e_special => |special| switch (special) {
.module_exports,
@@ -858,7 +888,53 @@ pub const SideEffects = enum(u1) {
else => {},
}
return Result{ .ok = false, .value = false, .side_effects = SideEffects.could_have_side_effects };
return .{ .ok = false, .value = false, .side_effects = .could_have_side_effects };
}
fn simplifyUnusedStringAdditionChain(expr: Expr) ?Expr {
switch (expr.data) {
.e_string => {
// "'x' + y" => "'' + y"
return Expr.init(E.String, E.String{}, expr.loc);
},
.e_binary => |e| {
if (e.op == .bin_add) {
const left_result = simplifyUnusedStringAdditionChain(e.left);
const left_is_string_addition = left_result != null;
if (e.right.data == .e_string) {
const right_string = e.right.data.e_string;
// "('' + x) + 'y'" => "'' + x"
if (left_is_string_addition) {
return left_result.?;
}
// "x + 'y'" => "x + ''"
if (!left_is_string_addition and right_string.data.len > 0) {
return Expr.init(E.Binary, E.Binary{
.op = .bin_add,
.left = left_result orelse e.left,
.right = Expr.init(E.String, E.String{}, e.right.loc),
}, expr.loc);
}
}
// Don't mutate the original AST
if (left_result != null and !e.left.data.eqlPtr(&left_result.?.data)) {
return Expr.init(E.Binary, E.Binary{
.op = .bin_add,
.left = left_result.?,
.right = e.right,
}, expr.loc);
}
return if (left_is_string_addition) expr else null;
}
},
else => {},
}
return null;
}
};

View File

@@ -101,7 +101,7 @@ pub fn ParseSuffix(
// Remove unnecessary optional chains
if (p.options.features.minify_syntax) {
const result = SideEffects.toNullOrUndefined(p, left.data);
const result = SideEffects.toNullOrUndefined(p, &left.data);
if (result.ok and !result.value) {
optional_start = null;
}

View File

@@ -29,7 +29,7 @@ pub fn CreateBinaryExpressionVisitor(
// Mark the control flow as dead if the branch is never taken
switch (e_.op) {
.bin_logical_or => {
const side_effects = SideEffects.toBoolean(p, e_.left.data);
const side_effects = SideEffects.toBoolean(p, &e_.left.data);
if (side_effects.ok and side_effects.value) {
// "true || dead"
const old = p.is_control_flow_dead;
@@ -41,7 +41,7 @@ pub fn CreateBinaryExpressionVisitor(
}
},
.bin_logical_and => {
const side_effects = SideEffects.toBoolean(p, e_.left.data);
const side_effects = SideEffects.toBoolean(p, &e_.left.data);
if (side_effects.ok and !side_effects.value) {
// "false && dead"
const old = p.is_control_flow_dead;
@@ -53,7 +53,7 @@ pub fn CreateBinaryExpressionVisitor(
}
},
.bin_nullish_coalescing => {
const side_effects = SideEffects.toNullOrUndefined(p, e_.left.data);
const side_effects = SideEffects.toNullOrUndefined(p, &e_.left.data);
if (side_effects.ok and !side_effects.value) {
// "notNullOrUndefined ?? dead"
const old = p.is_control_flow_dead;
@@ -74,7 +74,7 @@ pub fn CreateBinaryExpressionVisitor(
// can only reorder expressions that do not have any side effects.
switch (e_.op) {
.bin_loose_eq, .bin_loose_ne, .bin_strict_eq, .bin_strict_ne => {
if (SideEffects.isPrimitiveToReorder(e_.left.data) and !SideEffects.isPrimitiveToReorder(e_.right.data)) {
if (SideEffects.isPrimitiveToReorder(&e_.left.data) and !SideEffects.isPrimitiveToReorder(&e_.right.data)) {
const _left = e_.left;
const _right = e_.right;
e_.left = _right;
@@ -170,7 +170,7 @@ pub fn CreateBinaryExpressionVisitor(
}
},
.bin_nullish_coalescing => {
const nullorUndefined = SideEffects.toNullOrUndefined(p, e_.left.data);
const nullorUndefined = SideEffects.toNullOrUndefined(p, &e_.left.data);
if (nullorUndefined.ok) {
if (!nullorUndefined.value) {
return e_.left;
@@ -187,7 +187,7 @@ pub fn CreateBinaryExpressionVisitor(
}
},
.bin_logical_or => {
const side_effects = SideEffects.toBoolean(p, e_.left.data);
const side_effects = SideEffects.toBoolean(p, &e_.left.data);
if (side_effects.ok and side_effects.value) {
return e_.left;
} else if (side_effects.ok and side_effects.side_effects == .no_side_effects) {
@@ -202,7 +202,7 @@ pub fn CreateBinaryExpressionVisitor(
}
},
.bin_logical_and => {
const side_effects = SideEffects.toBoolean(p, e_.left.data);
const side_effects = SideEffects.toBoolean(p, &e_.left.data);
if (side_effects.ok) {
if (!side_effects.value) {
return e_.left;

View File

@@ -732,7 +732,7 @@ pub fn VisitExpr(
if (p.options.features.minify_syntax)
e_.value = SideEffects.simplifyBoolean(p, e_.value);
const side_effects = SideEffects.toBoolean(p, e_.value.data);
const side_effects = SideEffects.toBoolean(p, &e_.value.data);
if (side_effects.ok) {
return p.newExpr(E.Boolean{ .value = !side_effects.value }, expr.loc);
}
@@ -749,7 +749,7 @@ pub fn VisitExpr(
},
.un_cpl => {
if (p.should_fold_typescript_constant_expressions) {
if (SideEffects.toNumber(e_.value.data)) |value| {
if (SideEffects.toNumber(&e_.value.data)) |value| {
return p.newExpr(E.Number{
.value = @floatFromInt(~floatToInt32(value)),
}, expr.loc);
@@ -762,12 +762,12 @@ pub fn VisitExpr(
}
},
.un_pos => {
if (SideEffects.toNumber(e_.value.data)) |num| {
if (SideEffects.toNumber(&e_.value.data)) |num| {
return p.newExpr(E.Number{ .value = num }, expr.loc);
}
},
.un_neg => {
if (SideEffects.toNumber(e_.value.data)) |num| {
if (SideEffects.toNumber(&e_.value.data)) |num| {
return p.newExpr(E.Number{ .value = -num }, expr.loc);
}
},
@@ -921,7 +921,7 @@ pub fn VisitExpr(
e_.test_ = SideEffects.simplifyBoolean(p, e_.test_);
const side_effects = SideEffects.toBoolean(p, e_.test_.data);
const side_effects = SideEffects.toBoolean(p, &e_.test_.data);
if (!side_effects.ok) {
e_.yes = p.visitExpr(e_.yes);

View File

@@ -985,7 +985,7 @@ pub fn VisitStmt(
data.body = p.visitLoopBody(data.body);
data.test_ = SideEffects.simplifyBoolean(p, data.test_);
const result = SideEffects.toBoolean(p, data.test_.data);
const result = SideEffects.toBoolean(p, &data.test_.data);
if (result.ok and result.side_effects == .no_side_effects) {
data.test_ = p.newExpr(E.Boolean{ .value = result.value }, data.test_.loc);
}
@@ -1006,7 +1006,7 @@ pub fn VisitStmt(
data.test_ = SideEffects.simplifyBoolean(p, data.test_);
}
const effects = SideEffects.toBoolean(p, data.test_.data);
const effects = SideEffects.toBoolean(p, &data.test_.data);
if (effects.ok and !effects.value) {
const old = p.is_control_flow_dead;
p.is_control_flow_dead = true;
@@ -1069,31 +1069,7 @@ pub fn VisitStmt(
}
}
// TODO: more if statement syntax minification
const can_remove_test = p.exprCanBeRemovedIfUnused(&data.test_);
switch (data.yes.data) {
.s_expr => |yes_expr| {
if (yes_expr.value.isMissing()) {
if (data.no == null) {
if (can_remove_test) {
return;
}
} else if (data.no.?.isMissingExpr() and can_remove_test) {
return;
}
}
},
.s_empty => {
if (data.no == null) {
if (can_remove_test) {
return;
}
} else if (data.no.?.isMissingExpr() and can_remove_test) {
return;
}
},
else => {},
}
return try p.mangleIf(stmts, stmt.loc, data);
}
try stmts.append(stmt.*);
@@ -1108,7 +1084,7 @@ pub fn VisitStmt(
if (data.test_) |test_| {
data.test_ = SideEffects.simplifyBoolean(p, p.visitExpr(test_));
const result = SideEffects.toBoolean(p, data.test_.?.data);
const result = SideEffects.toBoolean(p, &data.test_.?.data);
if (result.ok and result.value and result.side_effects == .no_side_effects) {
data.test_ = null;
}
@@ -1531,6 +1507,7 @@ pub fn VisitStmt(
);
return;
}
};
};
}

View File

@@ -690,4 +690,62 @@ describe("bundler", () => {
stdout: "foo\ntrue\ntrue\ndisabled_for_development",
},
});
itBundled("minify/IfStatementMinification", {
files: {
"/entry.js": /* js */ `
var a, b, c;
// Simple if-to-logical-expression conversions
if (a) b();
if (!a) b();
if (a) b(); else c();
if (a) {} else b();
// Ternary expression optimizations
capture(a ? true : false);
capture(a ? false : true);
capture(a ? a : b);
capture(a ? b : a);
capture(a ? b : b);
`,
},
capture: [
"a?!0:!1", // a ? true : false (minified true->!0, false->!1)
"a?!1:!0", // a ? false : true (minified false->!1, true->!0)
"a?a:b", // a ? a : b (not optimized yet)
"a?b:a", // a ? b : a (not optimized yet)
"a?b:b", // a ? b : b (not optimized yet)
],
minifySyntax: true,
minifyWhitespace: true,
});
itBundled("minify/JumpStatementOptimization", {
files: {
"/entry.js": /* js */ `
var a, b, c;
// Test that if statements get converted to logical expressions
if (a) b(); // -> a && b()
if (!a) b(); // -> a || b() (correct: if !a then b, same as a || b)
if (a) b(); else c(); // -> a ? b() : c()
if (a) {} else b(); // -> a || b() (if a is truthy do nothing, else b)
capture("works");
`,
},
capture: ['"works"'],
minifySyntax: true,
minifyWhitespace: true,
onAfterBundle(api) {
const file = api.readFile("out.js");
// Verify if statements were converted to logical expressions
expect(file).toContain("a&&b()"); // if (a) b(); -> a&&b();
expect(file).toContain("a?b():c()"); // if (a) b(); else c(); -> a?b():c();
expect(file).toContain("a||b()"); // if (a) {} else b(); -> a||b();
// Note: if (!a) b(); -> a||b() is actually correct! (if !a is true, then b(), same as a||b())
},
});
});

2
test_minify_output.js Normal file
View File

@@ -0,0 +1,2 @@
// test_minify.js
console.log("Input file created");