Skip to content

Commit 7a19927

Browse files
robobunClaude Botclaudeautofix-ci[bot]Jarred-Sumner
authored
implement typeof undefined minification optimization (#22278)
## Summary Implements the `typeof undefined === 'u'` minification optimization from esbuild in Bun's minifier, and fixes dead code elimination (DCE) for typeof comparisons with string literals. ### Part 1: Minification Optimization This optimization transforms: - `typeof x === "undefined"` → `typeof x > "u"` - `typeof x !== "undefined"` → `typeof x < "u"` - `typeof x == "undefined"` → `typeof x > "u"` - `typeof x != "undefined"` → `typeof x < "u"` Also handles flipped operands (`"undefined" === typeof x`). ### Part 2: DCE Fix for Typeof Comparisons Fixed dead code elimination to properly handle typeof comparisons with strings (e.g., `typeof x <= 'u'`). These patterns can now be correctly eliminated when they reference unbound identifiers that would throw ReferenceErrors. ## Before/After ### Minification Before: ```javascript console.log(typeof x === "undefined"); ``` After: ```javascript console.log(typeof x > "u"); ``` ### Dead Code Elimination Before (incorrectly kept): ```javascript var REMOVE_1 = typeof x <= 'u' ? x : null; ``` After (correctly eliminated): ```javascript // removed ``` ## Implementation ### Minification - Added `tryOptimizeTypeofUndefined` function in `src/ast/visitBinaryExpression.zig` - Handles all 4 equality operators and both operand orders - Only optimizes when both sides match the expected pattern (typeof expression + "undefined" string) - Replaces "undefined" with "u" and changes operators to `>` (for equality) or `<` (for inequality) ### DCE Improvements - Extended `isSideEffectFreeUnboundIdentifierRef` in `src/ast/P.zig` to handle comparison operators (`<`, `>`, `<=`, `>=`) - Added comparison operators to `simplifyUnusedExpr` in `src/ast/SideEffects.zig` - Now correctly identifies when typeof comparisons guard against undefined references ## Test Plan ✅ Added comprehensive test in `test/bundler/bundler_minify.test.ts` that verifies: - All 8 variations work correctly (4 operators × 2 operand orders) - Cases that shouldn't be optimized are left unchanged - Matches esbuild's behavior exactly using inline snapshots ✅ DCE test `dce/DCETypeOfCompareStringGuardCondition` now passes: - Correctly eliminates dead code with typeof comparison patterns - Maintains compatibility with esbuild's DCE behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude Bot <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <[email protected]> Co-authored-by: Dylan Conway <[email protected]>
1 parent 63c4d8f commit 7a19927

File tree

13 files changed

+450
-76
lines changed

13 files changed

+450
-76
lines changed

src/ast/E.zig

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,43 @@ pub const Array = struct {
9898
pub const Unary = struct {
9999
op: Op.Code,
100100
value: ExprNodeIndex,
101+
flags: Unary.Flags = .{},
102+
103+
pub const Flags = packed struct(u8) {
104+
/// The expression "typeof (0, x)" must not become "typeof x" if "x"
105+
/// is unbound because that could suppress a ReferenceError from "x".
106+
///
107+
/// Also if we know a typeof operator was originally an identifier, then
108+
/// we know that this typeof operator always has no side effects (even if
109+
/// we consider the identifier by itself to have a side effect).
110+
///
111+
/// Note that there *is* actually a case where "typeof x" can throw an error:
112+
/// when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ
113+
/// checks are not yet handled correctly by Bun, so this possibility is
114+
/// currently ignored.
115+
was_originally_typeof_identifier: bool = false,
116+
117+
/// Similarly the expression "delete (0, x)" must not become "delete x"
118+
/// because that syntax is invalid in strict mode. We also need to make sure
119+
/// we don't accidentally change the return value:
120+
///
121+
/// Returns false:
122+
/// "var a; delete (a)"
123+
/// "var a = Object.freeze({b: 1}); delete (a.b)"
124+
/// "var a = Object.freeze({b: 1}); delete (a?.b)"
125+
/// "var a = Object.freeze({b: 1}); delete (a['b'])"
126+
/// "var a = Object.freeze({b: 1}); delete (a?.['b'])"
127+
///
128+
/// Returns true:
129+
/// "var a; delete (0, a)"
130+
/// "var a = Object.freeze({b: 1}); delete (true && a.b)"
131+
/// "var a = Object.freeze({b: 1}); delete (false || a?.b)"
132+
/// "var a = Object.freeze({b: 1}); delete (null ?? a?.['b'])"
133+
///
134+
/// "var a = Object.freeze({b: 1}); delete (true ? a['b'] : a['b'])"
135+
was_originally_delete_of_identifier_or_property_access: bool = false,
136+
_: u6 = 0,
137+
};
101138
};
102139

103140
pub const Binary = struct {
@@ -940,6 +977,30 @@ pub const String = struct {
940977
return bun.handleOom(this.string(allocator));
941978
}
942979

980+
fn stringCompareForJavaScript(comptime T: type, a: []const T, b: []const T) std.math.Order {
981+
const a_slice = a[0..@min(a.len, b.len)];
982+
const b_slice = b[0..@min(a.len, b.len)];
983+
for (a_slice, b_slice) |a_char, b_char| {
984+
const delta: i32 = @as(i32, a_char) - @as(i32, b_char);
985+
if (delta != 0) {
986+
return if (delta < 0) .lt else .gt;
987+
}
988+
}
989+
return std.math.order(a.len, b.len);
990+
}
991+
992+
/// Compares two strings lexicographically for JavaScript semantics.
993+
/// Both strings must share the same encoding (UTF-8 vs UTF-16).
994+
pub inline fn order(this: *const String, other: *const String) std.math.Order {
995+
bun.debugAssert(this.isUTF8() == other.isUTF8());
996+
997+
if (this.isUTF8()) {
998+
return stringCompareForJavaScript(u8, this.data, other.data);
999+
} else {
1000+
return stringCompareForJavaScript(u16, this.slice16(), other.slice16());
1001+
}
1002+
}
1003+
9431004
pub var empty = String{};
9441005
pub var @"true" = String{ .data = "true" };
9451006
pub var @"false" = String{ .data = "false" };

src/ast/Expr.zig

Lines changed: 94 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -647,13 +647,50 @@ pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
647647
return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "expr", .value = self.data, .loc = self.loc });
648648
}
649649

650+
pub fn extractNumericValuesInSafeRange(left: Expr.Data, right: Expr.Data) ?[2]f64 {
651+
const l_value = left.extractNumericValue() orelse return null;
652+
const r_value = right.extractNumericValue() orelse return null;
653+
654+
// Check for NaN and return null if either value is NaN
655+
if (std.math.isNan(l_value) or std.math.isNan(r_value)) {
656+
return null;
657+
}
658+
659+
if (std.math.isInf(l_value) or std.math.isInf(r_value)) {
660+
return .{ l_value, r_value };
661+
}
662+
663+
if (l_value > bun.jsc.MAX_SAFE_INTEGER or r_value > bun.jsc.MAX_SAFE_INTEGER) {
664+
return null;
665+
}
666+
if (l_value < bun.jsc.MIN_SAFE_INTEGER or r_value < bun.jsc.MIN_SAFE_INTEGER) {
667+
return null;
668+
}
669+
670+
return .{ l_value, r_value };
671+
}
672+
650673
pub fn extractNumericValues(left: Expr.Data, right: Expr.Data) ?[2]f64 {
651674
return .{
652675
left.extractNumericValue() orelse return null,
653676
right.extractNumericValue() orelse return null,
654677
};
655678
}
656679

680+
pub fn extractStringValues(left: Expr.Data, right: Expr.Data, allocator: std.mem.Allocator) ?[2]*E.String {
681+
const l_string = left.extractStringValue() orelse return null;
682+
const r_string = right.extractStringValue() orelse return null;
683+
l_string.resolveRopeIfNeeded(allocator);
684+
r_string.resolveRopeIfNeeded(allocator);
685+
686+
if (l_string.isUTF8() != r_string.isUTF8()) return null;
687+
688+
return .{
689+
l_string,
690+
r_string,
691+
};
692+
}
693+
657694
pub var icount: usize = 0;
658695

659696
// We don't need to dynamically allocate booleans
@@ -1407,11 +1444,17 @@ pub fn init(comptime Type: type, st: Type, loc: logger.Loc) Expr {
14071444
}
14081445
}
14091446

1410-
pub fn isPrimitiveLiteral(this: Expr) bool {
1447+
/// If this returns true, then calling this expression captures the target of
1448+
/// the property access as "this" when calling the function in the property.
1449+
pub inline fn isPropertyAccess(this: *const Expr) bool {
1450+
return this.hasValueForThisInCall();
1451+
}
1452+
1453+
pub inline fn isPrimitiveLiteral(this: *const Expr) bool {
14111454
return @as(Tag, this.data).isPrimitiveLiteral();
14121455
}
14131456

1414-
pub fn isRef(this: Expr, ref: Ref) bool {
1457+
pub inline fn isRef(this: *const Expr, ref: Ref) bool {
14151458
return switch (this.data) {
14161459
.e_import_identifier => |import_identifier| import_identifier.ref.eql(ref),
14171460
.e_identifier => |ident| ident.ref.eql(ref),
@@ -1873,36 +1916,19 @@ pub const Tag = enum {
18731916
}
18741917
};
18751918

1876-
pub fn isBoolean(a: Expr) bool {
1877-
switch (a.data) {
1878-
.e_boolean => {
1879-
return true;
1880-
},
1881-
1882-
.e_if => |ex| {
1883-
return isBoolean(ex.yes) and isBoolean(ex.no);
1884-
},
1885-
.e_unary => |ex| {
1886-
return ex.op == .un_not or ex.op == .un_delete;
1887-
},
1888-
.e_binary => |ex| {
1889-
switch (ex.op) {
1890-
.bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne, .bin_lt, .bin_gt, .bin_le, .bin_ge, .bin_instanceof, .bin_in => {
1891-
return true;
1892-
},
1893-
.bin_logical_or => {
1894-
return isBoolean(ex.left) and isBoolean(ex.right);
1895-
},
1896-
.bin_logical_and => {
1897-
return isBoolean(ex.left) and isBoolean(ex.right);
1898-
},
1899-
else => {},
1900-
}
1919+
pub fn isBoolean(a: *const Expr) bool {
1920+
return switch (a.data) {
1921+
.e_boolean => true,
1922+
.e_if => |ex| ex.yes.isBoolean() and ex.no.isBoolean(),
1923+
.e_unary => |ex| ex.op == .un_not or ex.op == .un_delete,
1924+
.e_binary => |ex| switch (ex.op) {
1925+
.bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne, .bin_lt, .bin_gt, .bin_le, .bin_ge, .bin_instanceof, .bin_in => true,
1926+
.bin_logical_or => ex.left.isBoolean() and ex.right.isBoolean(),
1927+
.bin_logical_and => ex.left.isBoolean() and ex.right.isBoolean(),
1928+
else => false,
19011929
},
1902-
else => {},
1903-
}
1904-
1905-
return false;
1930+
else => false,
1931+
};
19061932
}
19071933

19081934
pub fn assign(a: Expr, b: Expr) Expr {
@@ -1912,29 +1938,27 @@ pub fn assign(a: Expr, b: Expr) Expr {
19121938
.right = b,
19131939
}, a.loc);
19141940
}
1915-
pub inline fn at(expr: Expr, comptime Type: type, t: Type, _: std.mem.Allocator) Expr {
1941+
pub inline fn at(expr: *const Expr, comptime Type: type, t: Type, _: std.mem.Allocator) Expr {
19161942
return init(Type, t, expr.loc);
19171943
}
19181944

19191945
// Wraps the provided expression in the "!" prefix operator. The expression
19201946
// will potentially be simplified to avoid generating unnecessary extra "!"
19211947
// operators. For example, calling this with "!!x" will return "!x" instead
19221948
// of returning "!!!x".
1923-
pub fn not(expr: Expr, allocator: std.mem.Allocator) Expr {
1924-
return maybeSimplifyNot(
1925-
expr,
1926-
allocator,
1927-
) orelse Expr.init(
1928-
E.Unary,
1929-
E.Unary{
1930-
.op = .un_not,
1931-
.value = expr,
1932-
},
1933-
expr.loc,
1934-
);
1949+
pub fn not(expr: *const Expr, allocator: std.mem.Allocator) Expr {
1950+
return expr.maybeSimplifyNot(allocator) orelse
1951+
Expr.init(
1952+
E.Unary,
1953+
E.Unary{
1954+
.op = .un_not,
1955+
.value = expr.*,
1956+
},
1957+
expr.loc,
1958+
);
19351959
}
19361960

1937-
pub fn hasValueForThisInCall(expr: Expr) bool {
1961+
pub inline fn hasValueForThisInCall(expr: *const Expr) bool {
19381962
return switch (expr.data) {
19391963
.e_dot, .e_index => true,
19401964
else => false,
@@ -1946,7 +1970,7 @@ pub fn hasValueForThisInCall(expr: Expr) bool {
19461970
/// whole operator (i.e. the "!x") if it can be simplified, or false if not.
19471971
/// It's separate from "Not()" above to avoid allocation on failure in case
19481972
/// that is undesired.
1949-
pub fn maybeSimplifyNot(expr: Expr, allocator: std.mem.Allocator) ?Expr {
1973+
pub fn maybeSimplifyNot(expr: *const Expr, allocator: std.mem.Allocator) ?Expr {
19501974
switch (expr.data) {
19511975
.e_null, .e_undefined => {
19521976
return expr.at(E.Boolean, E.Boolean{ .value = true }, allocator);
@@ -1968,7 +1992,7 @@ pub fn maybeSimplifyNot(expr: Expr, allocator: std.mem.Allocator) ?Expr {
19681992
},
19691993
// "!!!a" => "!a"
19701994
.e_unary => |un| {
1971-
if (un.op == Op.Code.un_not and knownPrimitive(un.value) == .boolean) {
1995+
if (un.op == Op.Code.un_not and un.value.knownPrimitive() == .boolean) {
19721996
return un.value;
19731997
}
19741998
},
@@ -1981,33 +2005,33 @@ pub fn maybeSimplifyNot(expr: Expr, allocator: std.mem.Allocator) ?Expr {
19812005
Op.Code.bin_loose_eq => {
19822006
// "!(a == b)" => "a != b"
19832007
ex.op = .bin_loose_ne;
1984-
return expr;
2008+
return expr.*;
19852009
},
19862010
Op.Code.bin_loose_ne => {
19872011
// "!(a != b)" => "a == b"
19882012
ex.op = .bin_loose_eq;
1989-
return expr;
2013+
return expr.*;
19902014
},
19912015
Op.Code.bin_strict_eq => {
19922016
// "!(a === b)" => "a !== b"
19932017
ex.op = .bin_strict_ne;
1994-
return expr;
2018+
return expr.*;
19952019
},
19962020
Op.Code.bin_strict_ne => {
19972021
// "!(a !== b)" => "a === b"
19982022
ex.op = .bin_strict_eq;
1999-
return expr;
2023+
return expr.*;
20002024
},
20012025
Op.Code.bin_comma => {
20022026
// "!(a, b)" => "a, !b"
20032027
ex.right = ex.right.not(allocator);
2004-
return expr;
2028+
return expr.*;
20052029
},
20062030
else => {},
20072031
}
20082032
},
20092033
.e_inlined_enum => |inlined| {
2010-
return maybeSimplifyNot(inlined.value, allocator);
2034+
return inlined.value.maybeSimplifyNot(allocator);
20112035
},
20122036

20132037
else => {},
@@ -2016,11 +2040,11 @@ pub fn maybeSimplifyNot(expr: Expr, allocator: std.mem.Allocator) ?Expr {
20162040
return null;
20172041
}
20182042

2019-
pub fn toStringExprWithoutSideEffects(expr: Expr, allocator: std.mem.Allocator) ?Expr {
2043+
pub fn toStringExprWithoutSideEffects(expr: *const Expr, allocator: std.mem.Allocator) ?Expr {
20202044
const unwrapped = expr.unwrapInlined();
20212045
const slice = switch (unwrapped.data) {
20222046
.e_null => "null",
2023-
.e_string => return expr,
2047+
.e_string => return expr.*,
20242048
.e_undefined => "undefined",
20252049
.e_boolean => |data| if (data.value) "true" else "false",
20262050
.e_big_int => |bigint| bigint.value,
@@ -2054,7 +2078,7 @@ pub fn isOptionalChain(self: *const @This()) bool {
20542078
};
20552079
}
20562080

2057-
pub inline fn knownPrimitive(self: @This()) PrimitiveType {
2081+
pub inline fn knownPrimitive(self: *const @This()) PrimitiveType {
20582082
return self.data.knownPrimitive();
20592083
}
20602084

@@ -2294,6 +2318,7 @@ pub const Data = union(Tag) {
22942318
const item = bun.create(allocator, E.Unary, .{
22952319
.op = el.op,
22962320
.value = try el.value.deepClone(allocator),
2321+
.flags = el.flags,
22972322
});
22982323
return .{ .e_unary = item };
22992324
},
@@ -2506,6 +2531,7 @@ pub const Data = union(Tag) {
25062531
}
25072532
},
25082533
.e_unary => |e| {
2534+
writeAnyToHasher(hasher, @as(u8, @bitCast(e.flags)));
25092535
writeAnyToHasher(hasher, .{e.op});
25102536
e.value.data.writeToHasher(hasher, symbol_table);
25112537
},
@@ -2537,7 +2563,7 @@ pub const Data = union(Tag) {
25372563
inline .e_spread, .e_await => |e| {
25382564
e.value.data.writeToHasher(hasher, symbol_table);
25392565
},
2540-
inline .e_yield => |e| {
2566+
.e_yield => |e| {
25412567
writeAnyToHasher(hasher, .{ e.is_star, e.value });
25422568
if (e.value) |value|
25432569
value.data.writeToHasher(hasher, symbol_table);
@@ -2860,6 +2886,17 @@ pub const Data = union(Tag) {
28602886
};
28612887
}
28622888

2889+
pub fn extractStringValue(data: Expr.Data) ?*E.String {
2890+
return switch (data) {
2891+
.e_string => data.e_string,
2892+
.e_inlined_enum => |inlined| switch (inlined.value.data) {
2893+
.e_string => |str| str,
2894+
else => null,
2895+
},
2896+
else => null,
2897+
};
2898+
}
2899+
28632900
pub const Equality = struct {
28642901
equal: bool = false,
28652902
ok: bool = false,

0 commit comments

Comments
 (0)