Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
59fe054
feat(minify): optimize Error constructors by removing 'new' keyword
Sep 8, 2025
b5f92c9
feat(minify): convert Object/Array constructors to literals
Sep 8, 2025
30ebd57
test: update source map positions and file size for ReactSSR test
Sep 8, 2025
f3fd6ec
Merge branch 'main' into claude/minify-error-constructors
Sep 8, 2025
c30688d
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
69ca5ae
fix: address CodeRabbit feedback
Sep 8, 2025
8170f7d
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
852115e
refactor: extract callFromNew helper to reduce code duplication
Sep 8, 2025
9a381e5
Fix Array constructor optimization to preserve sparse array semantics
Sep 8, 2025
a0a8bbe
Fix: Don't apply minification to node_modules code at runtime
Sep 8, 2025
64bd06e
Fix test expectations for updated minification
Sep 8, 2025
b11b823
Fix
Sep 8, 2025
657ce27
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
a11cc56
Update inspect-error.test.js
Jarred-Sumner Sep 8, 2025
91153ba
Apply suggestion from @Jarred-Sumner
Jarred-Sumner Sep 8, 2025
943a112
Extend Array minification to handle object/array literals
Sep 8, 2025
d27a2fc
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
84ebe74
Remove RegExp optimization to preserve identity semantics
Sep 8, 2025
1237cc8
Fix tests
Jarred-Sumner Sep 8, 2025
db6e0ed
more minification
dylan-conway Sep 9, 2025
53ff1ed
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 9, 2025
e6f98ca
fix bug
dylan-conway Sep 9, 2025
ae2bf8a
revert
dylan-conway Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 139 additions & 14 deletions src/ast/KnownGlobal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,144 @@ pub const KnownGlobal = enum {
Response,
TextEncoder,
TextDecoder,
Error,
TypeError,
SyntaxError,
RangeError,
ReferenceError,
EvalError,
URIError,
AggregateError,
Array,
Object,
Function,
RegExp,

pub const map = bun.ComptimeEnumMap(KnownGlobal);

pub noinline fn maybeMarkConstructorAsPure(noalias e: *E.New, symbols: []const Symbol) void {
const id = if (e.target.data == .e_identifier) e.target.data.e_identifier.ref else return;
inline fn callFromNew(e: *E.New, loc: logger.Loc) js_ast.Expr {
const call = E.Call{
.target = e.target,
.args = e.args,
.close_paren_loc = e.close_parens_loc,
.can_be_unwrapped_if_unused = e.can_be_unwrapped_if_unused,
};
return js_ast.Expr.init(E.Call, call, loc);
}

pub noinline fn minifyGlobalConstructor(allocator: std.mem.Allocator, noalias e: *E.New, symbols: []const Symbol, loc: logger.Loc) ?js_ast.Expr {
_ = allocator;
const id = if (e.target.data == .e_identifier) e.target.data.e_identifier.ref else return null;
const symbol = &symbols[id.innerIndex()];
if (symbol.kind != .unbound)
return;
return null;

const constructor = map.get(symbol.original_name) orelse return;
const constructor = map.get(symbol.original_name) orelse return null;

switch (constructor) {
// Error constructors can be called without 'new' with identical behavior
.Error, .TypeError, .SyntaxError, .RangeError, .ReferenceError, .EvalError, .URIError, .AggregateError => {
// Convert `new Error(...)` to `Error(...)` to save bytes
return callFromNew(e, loc);
},

.Object => {
const n = e.args.len;

if (n == 0) {
// new Object() -> {}
return js_ast.Expr.init(E.Object, E.Object{}, loc);
}

if (n == 1) {
const arg = e.args.ptr[0];
switch (arg.data) {
.e_object, .e_array => {
// new Object({a: 1}) -> {a: 1}
// new Object([1, 2]) -> [1, 2]
return arg;
},
.e_null, .e_undefined => {
// new Object(null) -> {}
// new Object(undefined) -> {}
return js_ast.Expr.init(E.Object, E.Object{}, loc);
},
else => {},
}
}

// For other cases, just remove 'new'
return callFromNew(e, loc);
},

.Array => {
const n = e.args.len;

if (n == 0) {
// new Array() -> []
return js_ast.Expr.init(E.Array, E.Array{}, loc);
}

// new Array(1, 2, 3) -> [1, 2, 3]
// But NOT new Array(3) which creates an array with 3 empty slots
if (n > 1) {
return js_ast.Expr.init(E.Array, .{ .items = e.args }, loc);
}

// For single argument, only convert to literal if we're SURE it's not a number
if (n == 1) {
const arg = e.args.ptr[0];

// Check if it's an object or array literal first
switch (arg.data) {
.e_object, .e_array => {
// new Array({}) -> [{}], new Array([1]) -> [[1]]
// These are definitely not numbers, safe to convert
return js_ast.Expr.init(E.Array, .{ .items = e.args }, loc);
},
else => {},
}

// For other types, check via knownPrimitive
const primitive = arg.knownPrimitive();
// Only convert if we know for certain it's not a number
// unknown could be a number at runtime, so we must preserve Array() call
switch (primitive) {
.null, .undefined, .boolean, .string, .bigint => {
// These are definitely not numbers, safe to convert
return js_ast.Expr.init(E.Array, .{ .items = e.args }, loc);
},
.number, .unknown, .mixed => {
// Could be a number, preserve Array() call
return callFromNew(e, loc);
},
}
}

// For new Array(number), just remove 'new'
return callFromNew(e, loc);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unreachable. i think this should be switch (n) { 0 => {...}, 1 => {...}, else => {...}}

},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

.Function => {
// Just remove 'new' for Function
return callFromNew(e, loc);
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
.RegExp => {
// Don't optimize RegExp - the semantics are too complex:
// - new RegExp(re) creates a copy, but RegExp(re) returns the same instance
// - This affects object identity and lastIndex behavior
// - The difference only applies when flags are undefined
// Keep the original new RegExp() call to preserve correct semantics
return null;
},
.WeakSet, .WeakMap => {
const n = e.args.len;

if (n == 0) {
// "new WeakSet()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

if (n == 1) {
Expand All @@ -50,6 +168,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},
.Date => {
const n = e.args.len;
Expand All @@ -58,7 +177,7 @@ pub const KnownGlobal = enum {
// "new Date()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

if (n == 1) {
Expand All @@ -78,6 +197,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},

.Set => {
Expand All @@ -86,7 +206,7 @@ pub const KnownGlobal = enum {
if (n == 0) {
// "new Set()" is pure
e.can_be_unwrapped_if_unused = .if_unused;
return;
return null;
}

if (n == 1) {
Expand All @@ -102,6 +222,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},

.Headers => {
Expand All @@ -111,8 +232,9 @@ pub const KnownGlobal = enum {
// "new Headers()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}
return null;
},

.Response => {
Expand All @@ -122,7 +244,7 @@ pub const KnownGlobal = enum {
// "new Response()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

if (n == 1) {
Expand All @@ -142,6 +264,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},
.TextDecoder, .TextEncoder => {
const n = e.args.len;
Expand All @@ -151,11 +274,12 @@ pub const KnownGlobal = enum {
// "new TextDecoder()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

// We _could_ validate the encoding argument
// But let's not bother
return null;
},

.Map => {
Expand All @@ -164,7 +288,7 @@ pub const KnownGlobal = enum {
if (n == 0) {
// "new Map()" is pure
e.can_be_unwrapped_if_unused = .if_unused;
return;
return null;
}

if (n == 1) {
Expand Down Expand Up @@ -193,18 +317,19 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},
}
}
};

const string = []const u8;

const std = @import("std");

const bun = @import("bun");
const logger = bun.logger;

const js_ast = bun.ast;
const E = js_ast.E;
const Symbol = js_ast.Symbol;

const std = @import("std");
const Map = std.AutoHashMapUnmanaged;
4 changes: 3 additions & 1 deletion src/ast/visitExpr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1492,7 +1492,9 @@ pub fn VisitExpr(
}

if (p.options.features.minify_syntax) {
KnownGlobal.maybeMarkConstructorAsPure(e_, p.symbols.items);
if (KnownGlobal.minifyGlobalConstructor(p.allocator, e_, p.symbols.items, expr.loc)) |minified| {
return minified;
}
}
return expr;
}
Expand Down
Loading