Implement typeof evaluation for defined globals at build time

Major improvement: typeof expressions for defined globals like Bun are now
evaluated at build time, enabling much better dead code elimination.

Changes:
- typeof Bun evaluates to "object" for --target=bun, "undefined" for --target=browser
- typeof globalThis.Bun also evaluates at build time
- Const expressions like `const isBun = typeof Bun !== 'undefined'` now become
  `var isBun = !0` (true) or `var isBun = !1` (false) at build time

This enables automatic DCE for patterns like:
- if (typeof Bun === "object") { /* bun code */ }
- if (typeof Bun !== "undefined") { /* bun code */ }

The implementation checks for defined identifiers and dot expressions in the
typeof operator and replaces them with the appropriate string literal based on
the define configuration.

Note: While typeof is now evaluated, full constant propagation for local
variables is not yet implemented, so patterns like:
`const isBun = typeof Bun !== 'undefined'; if (isBun) { ... }`
still require the if statement optimization separately.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-09-09 23:34:07 +00:00
parent e0853184a3
commit 7fb2b7a12d
2 changed files with 138 additions and 14 deletions

View File

@@ -698,6 +698,54 @@ pub fn VisitExpr(
switch (e_.op) {
.un_typeof => {
const id_before = e_.value.data == .e_identifier;
// Check if this is a defined identifier BEFORE visiting it
// This allows us to evaluate typeof for defined globals like Bun
if (id_before) {
const ident = e_.value.data.e_identifier;
const name = p.loadNameFromRef(ident.ref);
// Check if this identifier is defined
if (p.define.forIdentifier(name)) |def| {
// For truthy defines (like Bun in --target=bun), return "object"
if (def.is_truthy()) {
return p.newExpr(E.String{ .data = "object" }, expr.loc);
}
// For undefined defines (like Bun in --target=browser), return "undefined"
if (def.value == .e_undefined) {
return p.newExpr(E.String{ .data = "undefined" }, expr.loc);
}
// For other literal values, check their typeof
if (SideEffects.typeof(def.value)) |typeof_str| {
return p.newExpr(E.String{ .data = typeof_str }, expr.loc);
}
}
}
// Check for dot expressions like typeof globalThis.Bun
if (e_.value.data == .e_dot) {
const dot = e_.value.data.e_dot;
if (p.define.dots.get(dot.name)) |parts| {
for (parts) |*define| {
if (p.isDotDefineMatch(e_.value, define.parts)) {
// For truthy defines (like globalThis.Bun in --target=bun), return "object"
if (define.data.is_truthy()) {
return p.newExpr(E.String{ .data = "object" }, expr.loc);
}
// For undefined defines (like globalThis.Bun in --target=browser), return "undefined"
if (define.data.value == .e_undefined) {
return p.newExpr(E.String{ .data = "undefined" }, expr.loc);
}
// For other literal values, check their typeof
if (SideEffects.typeof(define.data.value)) |typeof_str| {
return p.newExpr(E.String{ .data = typeof_str }, expr.loc);
}
break;
}
}
}
}
e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() });
const id_after = e_.value.data == .e_identifier;

View File

@@ -163,16 +163,10 @@ var require_HASH = __commonJS((exports) => {
process.versions.bun, exports.test3 = "process-versions-bun-exists";
exports.test3a = "process-isBun-true";
exports.test4 = "typeof-bun-defined";
globalThis.Bun, exports.test5 = "typeof-globalThis-bun-defined";
exports.test5 = "typeof-globalThis-bun-defined";
exports.test6 = "typeof-bun-reverse-defined";
if (typeof Bun === "object")
exports.test6a = "typeof-bun-object";
else
exports.test6a = "typeof-bun-not-object";
if (typeof Bun !== "object")
exports.test6b = "typeof-bun-not-object-2";
else
exports.test6b = "typeof-bun-object-2";
exports.test6a = "typeof-bun-object";
exports.test6b = "typeof-bun-object-2";
if (Bun.version)
exports.test7 = "bun-version-exists";
else
@@ -202,7 +196,7 @@ var require_HASH = __commonJS((exports) => {
else
exports.test17 = "node-version-missing";
exports.test18 = "window-missing";
var isBun = typeof Bun !== "undefined";
var isBun = !0;
if (!isBun)
exports.test19 = "const-not-bun";
else
@@ -240,7 +234,10 @@ export default require_HASH();"
// typeof window check is eliminated since window is undefined for bun target
expect(bundled).not.toContain("typeof window");
// Const patterns don't work (needs constant propagation)
// Const patterns: typeof is evaluated but const propagation not yet implemented
// The const isBun is now "var isBun = !0" (true) instead of "typeof Bun !== 'undefined'"
// But the if statement using isBun isn't optimized yet - needs constant propagation
expect(bundled).toContain("var isBun = !0");
expect(bundled).toContain("const-not-bun");
expect(bundled).toContain("const-is-bun");
});
@@ -392,8 +389,9 @@ if (isBun) {
expect(bundled).toContain('exports.hasBun = !1'); // false
expect(bundled).not.toContain('exports.hasBun = !0'); // true eliminated
// typeof Bun === "object" not fully optimized yet - both branches present
expect(bundled).toContain('typeof Bun === "object"');
// typeof Bun === "object" should now be optimized (false for browser)
expect(bundled).toContain('exports.bunIsObject = !1'); // false
expect(bundled).not.toContain('exports.bunIsObject = !0'); // true eliminated
expect(bundled).toContain('exports.isBun = !1'); // false
expect(bundled).not.toContain('exports.isBun = !0'); // true eliminated
@@ -408,4 +406,82 @@ if (isBun) {
// Const pattern should ideally be optimized but might not work yet
// For now just check it's there
expect(bundled).toContain('constBun');
});
});
test("typeof Bun is evaluated at build time", async () => {
const code = `
// Direct typeof comparison should be fully optimized
if (typeof Bun === "object") {
exports.test1 = "is-object";
} else {
exports.test1 = "not-object";
}
if (typeof Bun !== "undefined") {
exports.test2 = "defined";
} else {
exports.test2 = "undefined";
}
if (typeof globalThis.Bun === "object") {
exports.test3 = "global-is-object";
} else {
exports.test3 = "global-not-object";
}
// Const assignment - typeof is evaluated but const propagation not yet done
const isBun = typeof Bun !== "undefined";
exports.isBunValue = isBun;
`;
// Test for --target=bun
using bunDir = tempDir("typeof-bun", { "index.js": code });
await using bunProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--target=bun", "--outfile=bundle.js", "--minify-syntax"],
env: bunEnv,
cwd: String(bunDir),
stderr: "pipe",
});
expect(await bunProc.exited).toBe(0);
const bunBundle = await Bun.file(String(bunDir) + "/bundle.js").text();
// typeof Bun === "object" should be optimized to true
expect(bunBundle).toContain('exports.test1 = "is-object"');
expect(bunBundle).not.toContain('exports.test1 = "not-object"');
// typeof Bun !== "undefined" should be optimized to true
expect(bunBundle).toContain('exports.test2 = "defined"');
expect(bunBundle).not.toContain('exports.test2 = "undefined"');
// typeof globalThis.Bun === "object" should be optimized to true
expect(bunBundle).toContain('exports.test3 = "global-is-object"');
expect(bunBundle).not.toContain('exports.test3 = "global-not-object"');
// Const isBun should be evaluated to true (minified as !0)
expect(bunBundle).toContain("var isBun = !0");
// Test for --target=browser
using browserDir = tempDir("typeof-browser", { "index.js": code });
await using browserProc = Bun.spawn({
cmd: [bunExe(), "build", "index.js", "--target=browser", "--outfile=bundle.js", "--minify-syntax"],
env: bunEnv,
cwd: String(browserDir),
stderr: "pipe",
});
expect(await browserProc.exited).toBe(0);
const browserBundle = await Bun.file(String(browserDir) + "/bundle.js").text();
// typeof Bun === "object" should be optimized to false
expect(browserBundle).toContain('exports.test1 = "not-object"');
expect(browserBundle).not.toContain('exports.test1 = "is-object"');
// typeof Bun !== "undefined" should be optimized to false
expect(browserBundle).toContain('exports.test2 = "undefined"');
expect(browserBundle).not.toContain('exports.test2 = "defined"');
// typeof globalThis.Bun === "object" should be optimized to false
expect(browserBundle).toContain('exports.test3 = "global-not-object"');
expect(browserBundle).not.toContain('exports.test3 = "global-is-object"');
// Const isBun should be evaluated to false (minified as !1)
expect(browserBundle).toContain("var isBun = !1");
});