aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/js_ast.zig224
-rw-r--r--src/js_parser/js_parser.zig335
-rw-r--r--src/js_printer.zig2
3 files changed, 411 insertions, 150 deletions
diff --git a/src/js_ast.zig b/src/js_ast.zig
index d3475435f..35bc67c13 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -1676,6 +1676,13 @@ pub const E = struct {
}
}
+ pub fn eqlComptime(s: *const String, comptime value: anytype) bool {
+ return if (s.isUTF8())
+ strings.eqlComptime(s.utf8, value)
+ else
+ strings.eqlComptimeUTF16(s.value, value);
+ }
+
pub fn string(s: *const String, allocator: std.mem.Allocator) !_global.string {
if (s.isUTF8()) {
return s.utf8;
@@ -2746,6 +2753,19 @@ pub const Expr = struct {
// This should never make it to the printer
inline_identifier,
+ pub fn typeof(tag: Tag) ?string {
+ return switch (tag) {
+ .e_null => "object",
+ .e_undefined => "undefined",
+ .e_boolean => "boolean",
+ .e_number => "number",
+ .e_big_int => "bigint",
+ .e_string => "string",
+ .e_function, .e_arrow => "function",
+ else => null,
+ };
+ }
+
pub fn format(tag: Tag, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
try switch (tag) {
.e_string => writer.writeAll("string"),
@@ -3288,6 +3308,34 @@ pub const Expr = struct {
};
}
+ pub inline fn knownPrimitive(self: @This()) PrimitiveType {
+ return self.data.knownPrimitive();
+ }
+
+ pub const PrimitiveType = enum {
+ unknown,
+ mixed,
+ @"null",
+ @"undefined",
+ boolean,
+ number,
+ @"string",
+ bigint,
+
+ pub fn merge(left_known: PrimitiveType, right_known: PrimitiveType) PrimitiveType {
+ if (right_known == .unknown or left_known == .unknown)
+ return .unknown;
+
+ return if (left_known == right_known)
+ left_known
+ else
+ .mixed;
+ }
+
+ // This can be used when the returned type is either one or the other
+
+ };
+
pub const Data = union(Tag) {
e_array: *E.Array,
e_unary: *E.Unary,
@@ -3336,6 +3384,182 @@ pub const Expr = struct {
// If it ends up in JSParser or JSPrinter, it is a bug.
inline_identifier: i32,
+ pub fn knownPrimitive(data: Expr.Data) PrimitiveType {
+ return switch (data) {
+ .e_big_int => .bigint,
+ .e_boolean => .boolean,
+ .e_null => .@"null",
+ .e_number => .number,
+ .e_string => .@"string",
+ .e_undefined => .@"undefined",
+ .e_template => if (data.e_template.tag == null) PrimitiveType.@"string" else PrimitiveType.unknown,
+ .e_if => mergeKnownPrimitive(data.e_if.yes.data, data.e_if.no.data),
+ .e_binary => |binary| brk: {
+ switch (binary.op) {
+ .bin_strict_eq,
+ .bin_strict_ne,
+ .bin_loose_eq,
+ .bin_loose_ne,
+ .bin_lt,
+ .bin_gt,
+ .bin_le,
+ .bin_ge,
+ .bin_instanceof,
+ .bin_in,
+ => break :brk PrimitiveType.boolean,
+ .bin_logical_or, .bin_logical_and => break :brk binary.left.data.mergeKnownPrimitive(binary.right.data),
+
+ .bin_nullish_coalescing => {
+ const left = binary.left.data.knownPrimitive();
+ const right = binary.right.data.knownPrimitive();
+ if (left == .@"null" or right == .@"undefined")
+ break :brk right;
+
+ if (left != .unknown) {
+ if (left != .mixed)
+ break :brk left; // Definitely not null or undefined
+
+ if (right != .unknown)
+ break :brk PrimitiveType.mixed; // Definitely some kind of primitive
+ }
+ },
+
+ .bin_add => {
+ const left = binary.left.data.knownPrimitive();
+ const right = binary.right.data.knownPrimitive();
+
+ if (left == .@"string" or right == .@"string")
+ break :brk PrimitiveType.@"string";
+
+ if (left == .bigint or right == .bigint)
+ break :brk PrimitiveType.bigint;
+
+ if (switch (left) {
+ .unknown, .mixed, .bigint => false,
+ else => true,
+ } and switch (right) {
+ .unknown, .mixed, .bigint => false,
+ else => true,
+ })
+ break :brk PrimitiveType.number;
+
+ break :brk PrimitiveType.mixed; // Can be number or bigint or string (or an exception)
+ },
+
+ .bin_sub,
+ .bin_sub_assign,
+ .bin_mul,
+ .bin_mul_assign,
+ .bin_div,
+ .bin_div_assign,
+ .bin_rem,
+ .bin_rem_assign,
+ .bin_pow,
+ .bin_pow_assign,
+ .bin_bitwise_and,
+ .bin_bitwise_and_assign,
+ .bin_bitwise_or,
+ .bin_bitwise_or_assign,
+ .bin_bitwise_xor,
+ .bin_bitwise_xor_assign,
+ .bin_shl,
+ .bin_shl_assign,
+ .bin_shr,
+ .bin_shr_assign,
+ .bin_u_shr,
+ .bin_u_shr_assign,
+ => break :brk PrimitiveType.mixed, // Can be number or bigint (or an exception)
+
+ .bin_assign,
+ .bin_comma,
+ => break :brk binary.right.data.knownPrimitive(),
+
+ else => {},
+ }
+
+ break :brk PrimitiveType.unknown;
+ },
+
+ .e_unary => switch (data.e_unary.op) {
+ .un_void => PrimitiveType.@"undefined",
+ .un_typeof => PrimitiveType.@"string",
+ .un_not, .un_delete => PrimitiveType.boolean,
+ .un_pos => PrimitiveType.number, // Cannot be bigint because that throws an exception
+ .un_neg, .un_cpl => switch (data.e_unary.value.data.knownPrimitive()) {
+ .bigint => PrimitiveType.bigint,
+ .unknown, .mixed => PrimitiveType.mixed,
+ else => PrimitiveType.number, // Can be number or bigint
+ },
+ .un_pre_dec, .un_pre_inc, .un_post_dec, .un_post_inc => PrimitiveType.mixed, // Can be number or bigint
+
+ else => PrimitiveType.unknown,
+ },
+ else => PrimitiveType.unknown,
+ };
+ }
+
+ pub fn mergeKnownPrimitive(lhs: Expr.Data, rhs: Expr.Data) PrimitiveType {
+ return lhs.knownPrimitive().merge(rhs.knownPrimitive());
+ }
+
+ /// Returns true if the result of the "typeof" operator on this expression is
+ /// statically determined and this expression has no side effects (i.e. can be
+ /// removed without consequence).
+ pub inline fn toTypeof(data: Expr.Data) ?string {
+ return @as(Expr.Tag, data).typeof();
+ }
+
+ pub fn toNumber(data: Expr.Data) ?f64 {
+ return switch (data) {
+ .e_null => 0,
+ .e_undefined => std.math.nan_f64,
+ .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0),
+ .e_number => data.e_number.value,
+ 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
+ // values. If "ok" is true, the equality or inequality of the two values is
+ // stored in "equal".
+ pub fn eql(left: Expr.Data, right: Expr.Data) Equality {
+ var equality = Equality{};
+ switch (left) {
+ .e_null => {
+ equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_null;
+ equality.ok = equality.equal;
+ },
+ .e_undefined => {
+ equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_undefined;
+ equality.equal = equality.ok;
+ },
+ .e_boolean => |l| {
+ equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_boolean;
+ equality.equal = equality.ok and l.value == right.e_boolean.value;
+ },
+ .e_number => |l| {
+ equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_number;
+ equality.equal = equality.ok and l.value == right.e_number.value;
+ },
+ .e_big_int => |l| {
+ equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_big_int;
+ equality.equal = equality.ok and strings.eql(l.value, right.e_big_int.value);
+ },
+ .e_string => |l| {
+ equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_string;
+ if (equality.ok) {
+ const r = right.e_string;
+ equality.equal = r.eql(E.String, l);
+ }
+ },
+ else => {},
+ }
+
+ return equality;
+ }
+
pub fn toJS(this: Data, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef {
return switch (this) {
.e_array => |e| e.toJS(ctx, exception),
diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig
index eda1444d6..e525c4330 100644
--- a/src/js_parser/js_parser.zig
+++ b/src/js_parser/js_parser.zig
@@ -227,9 +227,10 @@ pub const TypeScript = struct {
};
pub fn forStr(str: string) ?StmtIdentifier {
switch (str.len) {
- "type".len => {
- return if (std.mem.readIntNative(u32, str[0..4]) == comptime std.mem.readIntNative(u32, "type")) .s_type else null;
- },
+ "type".len => return if (strings.eqlComptimeIgnoreLen(str, "type"))
+ .s_type
+ else
+ null,
"interface".len => {
if (strings.eqlComptime(str, "interface")) {
return .s_interface;
@@ -260,9 +261,7 @@ pub const TypeScript = struct {
return null;
}
},
- else => {
- return null;
- },
+ else => return null,
}
}
pub const IMap = std.ComptimeStringMap(Kind, .{
@@ -878,7 +877,7 @@ const StaticSymbolName = struct {
};
};
-pub const SideEffects = enum(u2) {
+pub const SideEffects = enum(u1) {
could_have_side_effects,
no_side_effects,
@@ -888,10 +887,12 @@ pub const SideEffects = enum(u2) {
value: bool = false,
};
- // While mangling is not the goal
- // The reason for doing this is to remove dead imports wherever possible
- // We want to prioritize build & runtime performance
- // Sometimes, at the cost of making the source a little harder to read.
+ pub fn canChangeStrictToLoose(lhs: Expr.Data, rhs: Expr.Data) bool {
+ const left = lhs.knownPrimitive();
+ const right = rhs.knownPrimitive();
+ return left == right and left != .unknown and left != .mixed;
+ }
+
pub fn simplifyBoolean(p: anytype, expr: Expr) Expr {
switch (expr.data) {
.e_unary => |e| {
@@ -929,29 +930,8 @@ pub const SideEffects = enum(u2) {
return expr;
}
- pub fn toNumber(data: Expr.Data) ?f64 {
- switch (data) {
- .e_null => {
- return 0;
- },
- .e_undefined => {
- return std.math.nan_f64;
- },
- .e_boolean => {
- const e = data.e_boolean;
-
- return if (e.value) 1.0 else 0.0;
- },
- .e_number => {
- const e = data.e_number;
-
- return e.value;
- },
- else => {},
- }
-
- return null;
- }
+ pub const toNumber = Expr.Data.toNumber;
+ pub const typeof = Expr.Data.toTypeof;
pub fn isPrimitiveToReorder(data: Expr.Data) bool {
switch (data) {
@@ -1030,6 +1010,7 @@ pub const SideEffects = enum(u2) {
return simpifyUnusedExpr(p, un.value);
},
+
else => {},
}
},
@@ -1089,7 +1070,7 @@ pub const SideEffects = enum(u2) {
// can be removed. The annotation causes us to ignore the target.
if (call.can_be_unwrapped_if_unused) {
if (call.args.len > 0) {
- return Expr.joinAllWithComma(call.args.slice(), p.allocator);
+ return Expr.joinAllWithCommaCallback(call.args.slice(), @TypeOf(p), p, simpifyUnusedExpr, p.allocator);
}
}
},
@@ -1211,47 +1192,6 @@ pub const SideEffects = enum(u2) {
}
}
- pub const Equality = struct { equal: bool = false, ok: bool = false };
-
- // 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".
- pub fn eql(left: Expr.Data, right: Expr.Data, _: anytype) Equality {
- var equality = Equality{};
- switch (left) {
- .e_null => {
- equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_null;
- equality.ok = equality.equal;
- },
- .e_undefined => {
- equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_undefined;
- equality.equal = equality.ok;
- },
- .e_boolean => |l| {
- equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_boolean;
- equality.equal = equality.ok and l.value == right.e_boolean.value;
- },
- .e_number => |l| {
- equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_number;
- equality.equal = equality.ok and l.value == right.e_number.value;
- },
- .e_big_int => |l| {
- equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_big_int;
- equality.equal = equality.ok and strings.eql(l.value, right.e_big_int.value);
- },
- .e_string => |l| {
- equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_string;
- if (equality.ok) {
- const r = right.e_string;
- equality.equal = r.eql(E.String, l);
- }
- },
- else => {},
- }
-
- return equality;
- }
-
// 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.
@@ -1350,37 +1290,7 @@ pub const SideEffects = enum(u2) {
return false;
}
- // Returns true if the result of the "typeof" operator on this expression is
- // statically determined and this expression has no side effects (i.e. can be
- // removed without consequence).
- pub fn toTypeof(data: Expr.Data) ?string {
- switch (data) {
- .e_null => {
- return "object";
- },
- .e_undefined => {
- return "undefined";
- },
- .e_boolean => {
- return "boolean";
- },
- .e_number => {
- return "number";
- },
- .e_big_int => {
- return "bigint";
- },
- .e_string => {
- return "string";
- },
- .e_function, .e_arrow => {
- return "function";
- },
- else => {},
- }
-
- return null;
- }
+ pub const toTypeOf = Expr.Data.typeof;
pub fn toNullOrUndefined(exp: Expr.Data) Result {
switch (exp) {
@@ -6185,6 +6095,7 @@ pub fn NewParser(
try p.lexer.expectContextualKeyword("from");
_ = try p.parsePath();
try p.lexer.expectOrInsertSemicolon();
+ Output.debug("Imported type-only import", .{});
return p.s(S.TypeScript{}, loc);
},
else => {},
@@ -6213,6 +6124,7 @@ pub fn NewParser(
// "import defaultItem, {item1, item2} from 'path'"
.t_open_brace => {
const importClause = try p.parseImportClause();
+
stmt.items = importClause.items;
stmt.is_single_line = importClause.is_single_line;
},
@@ -7713,10 +7625,10 @@ pub fn NewParser(
if (!str.prefer_template) {
isDirectivePrologue = true;
- if (str.eql(string, "use strict")) {
+ if (str.eqlComptime("use strict")) {
// Track "use strict" directives
p.current_scope.strict_mode = .explicit_strict_mode;
- } else if (str.eql(string, "use asm")) {
+ } else if (str.eqlComptime("use asm")) {
stmt.data = Prefill.Data.SEmpty;
}
}
@@ -8658,7 +8570,7 @@ pub fn NewParser(
if (!is_computed) {
switch (key.data) {
.e_string => |str| {
- if (str.eql(string, "constructor") or (opts.is_static and str.eql(string, "prototype"))) {
+ if (str.eqlComptime("constructor") or (opts.is_static and str.eqlComptime("prototype"))) {
// TODO: fmt error message to include string value.
p.log.addRangeError(p.source, key_range, "Invalid field name") catch unreachable;
}
@@ -8725,7 +8637,7 @@ pub fn NewParser(
if (opts.is_class and !is_computed) {
switch (key.data) {
.e_string => |str| {
- if (!opts.is_static and str.eql(string, "constructor")) {
+ if (!opts.is_static and str.eqlComptime("constructor")) {
if (kind == .get) {
p.log.addRangeError(p.source, key_range, "Class constructor cannot be a getter") catch unreachable;
} else if (kind == .set) {
@@ -8737,7 +8649,7 @@ pub fn NewParser(
} else {
is_constructor = true;
}
- } else if (opts.is_static and str.eql(string, "prototype")) {
+ } else if (opts.is_static and str.eqlComptime("prototype")) {
p.log.addRangeError(p.source, key_range, "Invalid static method name \"prototype\"") catch unreachable;
}
},
@@ -8928,7 +8840,7 @@ pub fn NewParser(
if (opts.ts_decorators.len > 0) {
switch ((property.key orelse p.panic("Internal error: Expected property {s} to have a key.", .{property})).data) {
.e_string => |str| {
- if (str.eql(string, "constructor")) {
+ if (str.eqlComptime("constructor")) {
p.log.addError(p.source, first_decorator_loc, "TypeScript does not allow decorators on class constructors") catch unreachable;
}
},
@@ -10843,23 +10755,15 @@ pub fn NewParser(
}
fn willNeedBindingPattern(p: *P) bool {
- switch (p.lexer.token) {
- .t_equals => {
- // "[a] = b;"
- return true;
- },
- .t_in => {
- // "for ([a] in b) {}"
- return !p.allow_in;
- },
- .t_identifier => {
- // "for ([a] of b) {}"
- return p.allow_in and p.lexer.isContextualKeyword("of");
- },
- else => {
- return false;
- },
- }
+ return switch (p.lexer.token) {
+ // "[a] = b;"
+ .t_equals => true,
+ // "for ([a] in b) {}"
+ .t_in => !p.allow_in,
+ // "for ([a] of b) {}"
+ .t_identifier => p.allow_in and p.lexer.isContextualKeyword("of"),
+ else => false,
+ };
}
fn parsePrefix(p: *P, level: Level, errors: ?*DeferredErrors, flags: Expr.EFlags) anyerror!Expr {
@@ -10991,7 +10895,9 @@ pub fn NewParser(
// Expressions marked with this are automatically generated and have
// no side effects by construction.
break;
- } else if (!p.exprCanBeRemovedIfUnused(&st.value)) {
+ }
+
+ if (!p.exprCanBeRemovedIfUnused(&st.value)) {
return false;
}
},
@@ -11009,6 +10915,12 @@ pub fn NewParser(
}
},
+ .s_try => |try_| {
+ if (!p.stmtsCanBeRemovedIfUnused(try_.body) or (try_.finally != null and !p.stmtsCanBeRemovedIfUnused(try_.finally.?.stmts))) {
+ return false;
+ }
+ },
+
// Exports are tracked separately, so this isn't necessary
.s_export_clause, .s_export_from => {},
@@ -11016,8 +10928,15 @@ pub fn NewParser(
switch (st.value) {
.stmt => |s2| {
switch (s2.data) {
+ .s_expr => |s_expr| {
+ if (!p.exprCanBeRemovedIfUnused(&s_expr.value)) {
+ return false;
+ }
+ },
+
// These never have side effects
.s_function => {},
+
.s_class => {
if (!p.classCanBeRemovedIfUnused(&s2.data.s_class.class)) {
return false;
@@ -11722,7 +11641,7 @@ pub fn NewParser(
// notimpl();
},
.bin_loose_eq => {
- const equality = SideEffects.eql(e_.left.data, e_.right.data, p);
+ const equality = e_.left.data.eql(e_.right.data);
if (equality.ok) {
return p.e(
E.Boolean{ .value = equality.equal },
@@ -11736,7 +11655,7 @@ pub fn NewParser(
},
.bin_strict_eq => {
- const equality = SideEffects.eql(e_.left.data, e_.right.data, p);
+ const equality = e_.left.data.eql(e_.right.data);
if (equality.ok) {
return p.e(E.Boolean{ .value = equality.equal }, expr.loc);
}
@@ -11746,7 +11665,7 @@ pub fn NewParser(
// TODO: warn about typeof string
},
.bin_loose_ne => {
- const equality = SideEffects.eql(e_.left.data, e_.right.data, p);
+ const equality = e_.left.data.eql(e_.right.data);
if (equality.ok) {
return p.e(E.Boolean{ .value = !equality.equal }, expr.loc);
}
@@ -11760,7 +11679,7 @@ pub fn NewParser(
}
},
.bin_strict_ne => {
- const equality = SideEffects.eql(e_.left.data, e_.right.data, p);
+ const equality = e_.left.data.eql(e_.right.data);
if (equality.ok) {
return p.e(E.Boolean{ .value = !equality.equal }, expr.loc);
}
@@ -12055,7 +11974,7 @@ pub fn NewParser(
);
}
- if (SideEffects.toTypeof(e_.value.data)) |typeof| {
+ if (SideEffects.typeof(e_.value.data)) |typeof| {
return p.e(E.String{ .utf8 = typeof }, expr.loc);
}
},
@@ -12714,7 +12633,20 @@ pub fn NewParser(
return true;
},
.e_if => |ex| {
- return p.exprCanBeRemovedIfUnused(&ex.test_) and p.exprCanBeRemovedIfUnused(&ex.yes) and p.exprCanBeRemovedIfUnused(&ex.no);
+ return p.exprCanBeRemovedIfUnused(&ex.test_) and
+ (p.isSideEffectFreeUnboundIdentifierRef(
+ ex.yes,
+ ex.test_,
+ true,
+ ) or
+ p.exprCanBeRemovedIfUnused(&ex.yes)) and
+ (p.isSideEffectFreeUnboundIdentifierRef(
+ ex.no,
+ ex.test_,
+ false,
+ ) or p.exprCanBeRemovedIfUnused(
+ &ex.no,
+ ));
},
.e_array => |ex| {
for (ex.items.slice()) |*item| {
@@ -12770,7 +12702,27 @@ pub fn NewParser(
},
.e_unary => |ex| {
switch (ex.op) {
- .un_typeof, .un_void, .un_not => {
+ // These operators must not have any type conversions that can execute code
+ // such as "toString" or "valueOf". They must also never throw any exceptions.
+ .un_void, .un_not => {
+ return p.exprCanBeRemovedIfUnused(&ex.value);
+ },
+
+ // The "typeof" operator doesn't do any type conversions so it can be removed
+ // if the result is unused and the operand has no side effects. However, it
+ // has a special case where if the operand is an identifier expression such
+ // as "typeof x" and "x" doesn't exist, no reference error is thrown so the
+ // operation has no side effects.
+ //
+ // Note that there *is* actually a case where "typeof x" can throw an error:
+ // when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ
+ // checks are not yet handled correctly by bun or esbuild, so this possibility is
+ // currently ignored.
+ .un_typeof => {
+ if (ex.value.data == .e_identifier) {
+ return true;
+ }
+
return p.exprCanBeRemovedIfUnused(&ex.value);
},
@@ -12779,24 +12731,92 @@ pub fn NewParser(
},
.e_binary => |ex| {
switch (ex.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,
- .bin_logical_or,
- .bin_logical_and,
.bin_nullish_coalescing,
- => {
- return p.exprCanBeRemovedIfUnused(&ex.left) and p.exprCanBeRemovedIfUnused(&ex.right);
- },
+ => return p.exprCanBeRemovedIfUnused(&ex.left) and p.exprCanBeRemovedIfUnused(&ex.right),
+
+ // Special-case "||" to make sure "typeof x === 'undefined' || x" can be removed
+ .bin_logical_or => return p.exprCanBeRemovedIfUnused(&ex.left) and
+ (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, false) or p.exprCanBeRemovedIfUnused(&ex.right)),
+
+ // Special-case "&&" to make sure "typeof x !== 'undefined' && x" can be removed
+ .bin_logical_and => return p.exprCanBeRemovedIfUnused(&ex.left) and
+ (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, true) or p.exprCanBeRemovedIfUnused(&ex.right)),
+
+ // For "==" and "!=", pretend the operator was actually "===" or "!==". If
+ // we know that we can convert it to "==" or "!=", then we can consider the
+ // operator itself to have no side effects. This matters because our mangle
+ // logic will convert "typeof x === 'object'" into "typeof x == 'object'"
+ // and since "typeof x === 'object'" is considered to be side-effect free,
+ // we must also consider "typeof x == 'object'" to be side-effect free.
+ .bin_loose_eq, .bin_loose_ne => return SideEffects.canChangeStrictToLoose(
+ ex.left.data,
+ ex.right.data,
+ ) and
+ p.exprCanBeRemovedIfUnused(&ex.left) and p.exprCanBeRemovedIfUnused(&ex.right),
else => {},
}
},
+ .e_template => |templ| {
+ if (templ.tag == null) {
+ for (templ.parts) |part| {
+ if (!p.exprCanBeRemovedIfUnused(&part.value) or part.value.knownPrimitive() == .unknown) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ },
else => {},
}
return false;
}
+ fn isSideEffectFreeUnboundIdentifierRef(p: *P, value: Expr, guard_condition: Expr, is_yes_branch: bool) bool {
+ if (value.data != .e_identifier or
+ p.symbols.items[value.data.e_identifier.ref.inner_index].kind != .unbound or
+ guard_condition.data != .e_binary)
+ return false;
+
+ const binary = guard_condition.data.e_binary.*;
+
+ switch (binary.op) {
+ .bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne => {
+ // typeof x !== 'undefined'
+ var typeof: Expr.Data = binary.left.data;
+ var compare: Expr.Data = binary.right.data;
+ // typeof 'undefined' !== x
+ if (typeof == .e_string) {
+ typeof = binary.right.data;
+ compare = binary.left.data;
+ }
+
+ // this order because Expr.Data Tag is not a pointer
+ // so it should be slightly faster to compare
+ if (compare != .e_string or
+ typeof != .e_unary)
+ return false;
+ const unary = typeof.e_unary.*;
+
+ if (unary.op != .un_typeof or unary.value.data != .e_identifier)
+ return false;
+
+ const id = value.data.e_identifier.ref;
+ const id2 = unary.value.data.e_identifier.ref;
+ return ((compare.e_string.eqlComptime("undefined") == is_yes_branch) ==
+ (binary.op == .bin_strict_ne or binary.op == .bin_loose_ne)) and
+ id.eql(id2);
+ },
+ else => return false,
+ }
+ }
+
fn jsxStringsToMemberExpressionAutomatic(p: *P, loc: logger.Loc, is_static: bool) Expr {
return p.jsxStringsToMemberExpression(loc, if (is_static and !p.options.jsx.development)
p.jsxs_runtime.ref
@@ -13324,13 +13344,17 @@ pub fn NewParser(
data.test_ = p.visitExpr(data.test_);
data.body = p.visitLoopBody(data.body);
- // TODO: simplify boolean expression
+ data.test_ = SideEffects.simplifyBoolean(p, data.test_);
+ const result = SideEffects.toBoolean(data.test_.data);
+ if (result.ok and result.side_effects == .no_side_effects) {
+ data.test_ = p.e(E.Boolean{ .value = result.value }, data.test_.loc);
+ }
},
.s_do_while => |data| {
data.body = p.visitLoopBody(data.body);
data.test_ = p.visitExpr(data.test_);
- // TODO: simplify boolean expression
+ data.test_ = SideEffects.simplifyBoolean(p, data.test_);
},
.s_if => |data| {
data.test_ = SideEffects.simplifyBoolean(p, p.visitExpr(data.test_));
@@ -13406,9 +13430,12 @@ pub fn NewParser(
}
if (data.test_) |test_| {
- data.test_ = p.visitExpr(test_);
+ data.test_ = SideEffects.simplifyBoolean(p, p.visitExpr(test_));
- // TODO: boolean with side effects
+ const result = SideEffects.toBoolean(data.test_.?.data);
+ if (result.ok and result.value and result.side_effects == .no_side_effects) {
+ data.test_ = null;
+ }
}
if (data.update) |update| {
@@ -13416,10 +13443,19 @@ pub fn NewParser(
}
data.body = p.visitLoopBody(data.body);
+
+ // Potentially relocate "var" declarations to the top level. Note that this
+ // must be done inside the scope of the for loop or they won't be relocated.
+ if (data.init) |init_| {
+ if (init_.data == .s_local and init_.data.s_local.kind == .k_var) {
+ const relocate = p.maybeRelocateVarsToTopLevel(init_.data.s_local.decls, .normal);
+ if (relocate.stmt) |relocated| {
+ data.init = relocated;
+ }
+ }
+ }
p.popScope();
}
- // TODO: Potentially relocate "var" declarations to the top level
-
},
.s_for_in => |data| {
{
@@ -13520,7 +13556,6 @@ pub fn NewParser(
const enclosing_namespace_arg_ref = p.enclosing_namespace_arg_ref orelse unreachable;
stmts.ensureUnusedCapacity(3) catch unreachable;
stmts.appendAssumeCapacity(stmt.*);
- // i wonder if this will crash
stmts.appendAssumeCapacity(Expr.assignStmt(p.e(E.Dot{
.target = p.e(E.Identifier{ .ref = enclosing_namespace_arg_ref }, stmt.loc),
.name = p.loadNameFromRef(data.func.name.?.ref.?),
diff --git a/src/js_printer.zig b/src/js_printer.zig
index 54b1a2991..2e90009cb 100644
--- a/src/js_printer.zig
+++ b/src/js_printer.zig
@@ -3742,6 +3742,8 @@ pub fn NewPrinter(
},
}
},
+ // for(;)
+ .s_empty => {},
else => {
Global.panic("Internal error: Unexpected stmt in for loop {s}", .{initSt});
},