aboutsummaryrefslogtreecommitdiff
path: root/src/js_parser/js_parser.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/js_parser/js_parser.zig')
-rw-r--r--src/js_parser/js_parser.zig206
1 files changed, 188 insertions, 18 deletions
diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig
index 7fb347c19..7a859a3a5 100644
--- a/src/js_parser/js_parser.zig
+++ b/src/js_parser/js_parser.zig
@@ -520,6 +520,155 @@ pub const SideEffects = enum {
}
}
+ pub fn simpifyUnusedExpr(p: *P, expr: Expr) ?Expr {
+ switch (expr.data) {
+ .e_null, .e_undefined, .e_missing, .e_boolean, .e_number, .e_big_int, .e_string, .e_this, .e_reg_exp, .e_function, .e_arrow, .e_import_meta => {
+ return null;
+ },
+
+ .e_dot => |dot| {
+ if (dot.can_be_removed_if_unused) {
+ return null;
+ }
+ },
+ .e_identifier => |ident| {
+ if (ident.must_keep_due_to_with_stmt) {
+ return expr;
+ }
+
+ if (ident.can_be_removed_if_unused or p.symbols.items[ident.ref.inner_index].kind != .unbound) {
+ return null;
+ }
+ },
+ .e_if => |__if__| {
+ __if__.yes = simpifyUnusedExpr(p, __if__.yes) orelse __if__.yes.toEmpty();
+ __if__.no = simpifyUnusedExpr(p, __if__.no) orelse __if__.no.toEmpty();
+
+ // "foo() ? 1 : 2" => "foo()"
+ if (__if__.yes.isEmpty() and __if__.no.isEmpty()) {
+ return simpifyUnusedExpr(p, __if__.test_);
+ }
+ },
+
+ .e_call => |call| {
+ // A call that has been marked "__PURE__" can be removed if all arguments
+ // can be removed. The annotation causes us to ignore the target.
+ if (call.can_be_unwrapped_if_unused) {
+ return Expr.joinAllWithComma(call.args, p.allocator);
+ }
+ },
+
+ .e_binary => |bin| {
+ switch (bin.op) {
+ // 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
+ // primitives. In that case there won't be any chance for user-defined
+ // "toString" and/or "valueOf" to be called.
+ .bin_loose_eq, .bin_loose_ne => {
+ if (isPrimitiveWithSideEffects(bin.left.data) and isPrimitiveWithSideEffects(bin.right.data)) {
+ return Expr.joinWithComma(simpifyUnusedExpr(p, bin.left) orelse bin.left.toEmpty(), simpifyUnusedExpr(p, bin.right) orelse bin.right.toEmpty(), p.allocator);
+ }
+ },
+ else => {},
+ }
+ },
+
+ .e_new => |call| {
+ // A constructor call that has been marked "__PURE__" can be removed if all arguments
+ // can be removed. The annotation causes us to ignore the target.
+ if (call.can_be_unwrapped_if_unused) {
+ return Expr.joinAllWithComma(call.args, p.allocator);
+ }
+ },
+ else => {},
+ }
+
+ return expr;
+ }
+
+ // If this is in a dead branch, then we want to trim as much dead code as we
+ // can. Everything can be trimmed except for hoisted declarations ("var" and
+ // "function"), which affect the parent scope. For example:
+ //
+ // function foo() {
+ // if (false) { var x; }
+ // x = 1;
+ // }
+ //
+ // We can't trim the entire branch as dead or calling foo() will incorrectly
+ // assign to a global variable instead.
+
+ // The main goal here is to trim conditionals
+ pub fn shouldKeepStmtInDeadControlFlow(stmt: Stmt) bool {
+ switch (stmt.data) {
+ .s_empty, .s_expr, .s_throw, .s_return, .s_break, .s_continue, .s_class, .s_debugger => {
+ // Omit these statements entirely
+ return false;
+ },
+
+ .s_local => |local| {
+ return local.kind != .k_var;
+ // if (local.kind != .k_var) {
+ // // Omit these statements entirely
+ // return false;
+ // }
+ },
+
+ .s_block => |block| {
+ for (block.stmts) |child| {
+ if (shouldKeepStmtInDeadControlFlow(child)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ .s_if => |_if_| {
+ if (shouldKeepStmtInDeadControlFlow(_if_.yes)) {
+ return true;
+ }
+
+ const no = _if_.no orelse return false;
+
+ return shouldKeepStmtInDeadControlFlow(no);
+ },
+
+ .s_while => |__while__| {
+ return shouldKeepStmtInDeadControlFlow(__while__.body);
+ },
+
+ .s_do_while => |__while__| {
+ return shouldKeepStmtInDeadControlFlow(__while__.body);
+ },
+
+ .s_for => |__for__| {
+ if (__for__.init) |init_| {
+ if (shouldKeepStmtInDeadControlFlow(init_)) {
+ return true;
+ }
+ }
+
+ return shouldKeepStmtInDeadControlFlow(__for__.body);
+ },
+
+ .s_for_in => |__for__| {
+ return shouldKeepStmtInDeadControlFlow(__for__.init) or shouldKeepStmtInDeadControlFlow(__for__.body);
+ },
+
+ .s_for_of => |__for__| {
+ return shouldKeepStmtInDeadControlFlow(__for__.init) or shouldKeepStmtInDeadControlFlow(__for__.body);
+ },
+
+ .s_label => |label| {
+ return shouldKeepStmtInDeadControlFlow(label.stmt);
+ },
+ else => {
+ return true;
+ },
+ }
+ }
+
pub const Equality = struct { equal: bool = false, ok: bool = false };
// Returns "equal, ok". If "ok" is false, then nothing is known about the two
@@ -642,9 +791,10 @@ pub const SideEffects = enum {
.bin_comma => {
return isPrimitiveWithSideEffects(e.right.data);
},
+ else => {},
}
},
- .e_if => {
+ .e_if => |e| {
return isPrimitiveWithSideEffects(e.yes.data) and isPrimitiveWithSideEffects(e.no.data);
},
else => {},
@@ -1283,6 +1433,14 @@ pub const Parser = struct {
var result: js_ast.Result = undefined;
if (self.p) |p| {
+
+ // Consume a leading hashbang comment
+ var hashbang: string = "";
+ if (p.lexer.token == .t_hashbang) {
+ hashbang = p.lexer.identifier;
+ try p.lexer.next();
+ }
+
// Parse the file in the first pass, but do not bind symbols
var opts = ParseStatementOptions{ .is_module_scope = true };
debugl("<p.parseStmtsUpTo>");
@@ -1499,8 +1657,8 @@ const ParseStatementOptions = struct {
var e_missing_data = E.Missing{};
var s_missing = S.Empty{};
-var nullExprData = Expr.Data{ .e_missing = &e_missing_data };
-var nullStmtData = Stmt.Data{ .s_empty = &s_missing };
+var nullExprData = Expr.Data{ .e_missing = e_missing_data };
+var nullStmtData = Stmt.Data{ .s_empty = s_missing };
pub const Prefill = struct {
pub const StringLiteral = struct {
pub var Key = [3]u16{ 'k', 'e', 'y' };
@@ -1523,10 +1681,10 @@ pub const Prefill = struct {
pub var BMissing = B{ .b_missing = &BMissing_ };
pub var BMissing_ = B.Missing{};
- pub var EMissing = Expr.Data{ .e_missing = &EMissing_ };
+ pub var EMissing = Expr.Data{ .e_missing = EMissing_ };
pub var EMissing_ = E.Missing{};
- pub var SEmpty = Stmt.Data{ .s_empty = &SEmpty_ };
+ pub var SEmpty = Stmt.Data{ .s_empty = SEmpty_ };
pub var SEmpty_ = S.Empty{};
pub var Filename = Expr.Data{ .e_string = &Prefill.String.Filename };
@@ -4032,7 +4190,7 @@ pub const P = struct {
const name = p.lexer.identifier;
var emiss = E.Missing{};
// Parse either an async function, an async expression, or a normal expression
- var expr: Expr = Expr{ .loc = loc, .data = Expr.Data{ .e_missing = &emiss } };
+ var expr: Expr = Expr{ .loc = loc, .data = Expr.Data{ .e_missing = emiss } };
if (is_identifier and strings.eqlComptime(p.lexer.raw(), "async")) {
var async_range = p.lexer.range();
try p.lexer.next();
@@ -4589,7 +4747,7 @@ pub const P = struct {
const name = p.lexer.identifier;
const loc = p.lexer.loc();
- const e_str = p.lexer.toEString();
+ const e_str = E.String{ .utf8 = name };
if (!p.lexer.isIdentifierOrKeyword()) {
try p.lexer.expect(.t_identifier);
@@ -7262,7 +7420,7 @@ pub const P = struct {
}
return p.e(E.Array{
.items = items.toOwnedSlice(),
- .comma_after_spread = comma_after_spread,
+ .comma_after_spread = comma_after_spread.toNullable(),
.is_single_line = is_single_line,
}, loc);
},
@@ -7325,7 +7483,7 @@ pub const P = struct {
}
return p.e(E.Object{
.properties = properties.toOwnedSlice(),
- .comma_after_spread = comma_after_spread,
+ .comma_after_spread = comma_after_spread.toNullable(),
.is_single_line = is_single_line,
}, loc);
},
@@ -9707,11 +9865,8 @@ pub const P = struct {
.s_expr => |data| {
p.stmt_expr_value = data.value.data;
data.value = p.visitExpr(data.value);
-
- // TODO:
- // if (p.options.mangle_syntax) {
-
- // }
+ // simplify unused
+ data.value = SideEffects.simpifyUnusedExpr(p, data.value) orelse data.value.toEmpty();
},
.s_throw => |data| {
data.value = p.visitExpr(data.value);
@@ -10622,9 +10777,10 @@ pub const P = struct {
// 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 = List(Stmt).init(p.allocator);
+ var visited = try List(Stmt).initCapacity(p.allocator, stmts.items.len);
var before = List(Stmt).init(p.allocator);
var after = List(Stmt).init(p.allocator);
defer before.deinit();
@@ -10657,8 +10813,21 @@ pub const P = struct {
try p.visitAndAppendStmt(list, stmt);
}
- p.is_control_flow_dead = old_is_control_flow_dead;
- try stmts.resize(visited.items.len + before.items.len + after.items.len);
+ var visited_count = visited.items.len;
+ if (p.is_control_flow_dead) {
+ var end: usize = 0;
+ for (visited.items) |item, i| {
+ if (!SideEffects.shouldKeepStmtInDeadControlFlow(item)) {
+ continue;
+ }
+
+ visited.items[end] = item;
+ end += 1;
+ }
+ visited_count = end;
+ }
+
+ try stmts.resize(visited_count + before.items.len + after.items.len);
var i: usize = 0;
for (before.items) |item| {
@@ -10666,7 +10835,8 @@ pub const P = struct {
i += 1;
}
- for (visited.items) |item| {
+ const visited_slice = visited.items[0..visited_count];
+ for (visited_slice) |item| {
stmts.items[i] = item;
i += 1;
}