mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
7 Commits
jarred/bum
...
claude/iif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26125888bd | ||
|
|
adc1a6b05c | ||
|
|
8a11a03297 | ||
|
|
baea21f0c7 | ||
|
|
8f66535291 | ||
|
|
4fd14c9086 | ||
|
|
9c803caf74 |
@@ -26,7 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget curl git python3 python3-pip ninja-build \
|
||||
software-properties-common apt-transport-https \
|
||||
ca-certificates gnupg lsb-release unzip \
|
||||
libxml2-dev ruby ruby-dev bison gawk perl make golang ccache \
|
||||
libxml2-dev ruby ruby-dev bison gawk perl make golang ccache qemu-user-static \
|
||||
&& add-apt-repository ppa:ubuntu-toolchain-r/test \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y gcc-13 g++-13 libgcc-13-dev libstdc++-13-dev \
|
||||
|
||||
@@ -537,6 +537,66 @@ function getLinkBunStep(platform, options) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the artifact triplet for a platform, e.g. "bun-linux-aarch64" or "bun-linux-x64-musl-baseline".
|
||||
* Matches the naming convention in cmake/targets/BuildBun.cmake.
|
||||
* @param {Platform} platform
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTargetTriplet(platform) {
|
||||
const { os, arch, abi, baseline } = platform;
|
||||
let triplet = `bun-${os}-${arch}`;
|
||||
if (abi === "musl") {
|
||||
triplet += "-musl";
|
||||
}
|
||||
if (baseline) {
|
||||
triplet += "-baseline";
|
||||
}
|
||||
return triplet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a platform needs QEMU-based baseline CPU verification.
|
||||
* x64 baseline builds verify no AVX/AVX2 instructions snuck in.
|
||||
* aarch64 builds verify no LSE/SVE instructions snuck in.
|
||||
* @param {Platform} platform
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function needsBaselineVerification(platform) {
|
||||
const { os, arch, baseline } = platform;
|
||||
if (os !== "linux") return false;
|
||||
return (arch === "x64" && baseline) || arch === "aarch64";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Platform} platform
|
||||
* @param {PipelineOptions} options
|
||||
* @returns {Step}
|
||||
*/
|
||||
function getVerifyBaselineStep(platform, options) {
|
||||
const { arch } = platform;
|
||||
const targetKey = getTargetKey(platform);
|
||||
const archArg = arch === "x64" ? "x64" : "aarch64";
|
||||
|
||||
return {
|
||||
key: `${targetKey}-verify-baseline`,
|
||||
label: `${getTargetLabel(platform)} - verify-baseline`,
|
||||
depends_on: [`${targetKey}-build-bun`],
|
||||
agents: getLinkBunAgent(platform, options),
|
||||
retry: getRetry(),
|
||||
cancel_on_build_failing: isMergeQueue(),
|
||||
timeout_in_minutes: 5,
|
||||
command: [
|
||||
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
|
||||
`unzip -o '${getTargetTriplet(platform)}.zip'`,
|
||||
`unzip -o '${getTargetTriplet(platform)}-profile.zip'`,
|
||||
`chmod +x ${getTargetTriplet(platform)}/bun ${getTargetTriplet(platform)}-profile/bun-profile`,
|
||||
`./scripts/verify-baseline-cpu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}/bun`,
|
||||
`./scripts/verify-baseline-cpu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}-profile/bun-profile`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Platform} platform
|
||||
* @param {PipelineOptions} options
|
||||
@@ -1126,6 +1186,10 @@ async function getPipeline(options = {}) {
|
||||
steps.push(getBuildZigStep(target, options));
|
||||
steps.push(getLinkBunStep(target, options));
|
||||
|
||||
if (needsBaselineVerification(target)) {
|
||||
steps.push(getVerifyBaselineStep(target, options));
|
||||
}
|
||||
|
||||
return getStepWithDependsOn(
|
||||
{
|
||||
key: getTargetKey(target),
|
||||
|
||||
@@ -69,8 +69,18 @@ if(ENABLE_VALGRIND)
|
||||
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_VALGRIND=ON)
|
||||
endif()
|
||||
|
||||
# Enable SIMD optimizations when not building for baseline (older CPUs)
|
||||
if(NOT ENABLE_BASELINE)
|
||||
# Enable architecture-specific optimizations when not building for baseline.
|
||||
# On Linux aarch64, upstream mimalloc force-enables MI_OPT_ARCH which adds
|
||||
# -march=armv8.1-a (LSE atomics). This crashes on ARMv8.0 CPUs
|
||||
# (Cortex-A53, Raspberry Pi 4, AWS a1 instances). Use MI_NO_OPT_ARCH
|
||||
# to prevent that, but keep SIMD enabled. -moutline-atomics for runtime
|
||||
# dispatch to LSE/LL-SC. macOS arm64 always has LSE (Apple Silicon) so
|
||||
# MI_OPT_ARCH is safe there.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64|AARCH64" AND NOT APPLE)
|
||||
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_NO_OPT_ARCH=ON)
|
||||
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OPT_SIMD=ON)
|
||||
list(APPEND MIMALLOC_CMAKE_ARGS "-DCMAKE_C_FLAGS=-moutline-atomics")
|
||||
elseif(NOT ENABLE_BASELINE)
|
||||
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OPT_ARCH=ON)
|
||||
list(APPEND MIMALLOC_CMAKE_ARGS -DMI_OPT_SIMD=ON)
|
||||
endif()
|
||||
|
||||
@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
|
||||
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
|
||||
|
||||
if(NOT WEBKIT_VERSION)
|
||||
set(WEBKIT_VERSION 9a2cc42ae1bf693a0fd0ceb9b1d7d965d9cfd3ea)
|
||||
set(WEBKIT_VERSION 515344bc5d65aa2d4f9ff277b5fb944f0e051dcd)
|
||||
endif()
|
||||
|
||||
# Use preview build URL for Windows ARM64 until the fix is merged to main
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Version: 26
|
||||
# Version: 27
|
||||
|
||||
# A script that installs the dependencies needed to build and test Bun.
|
||||
# This should work on macOS and Linux with a POSIX shell.
|
||||
@@ -1061,6 +1061,11 @@ install_build_essentials() {
|
||||
go \
|
||||
xz
|
||||
install_packages apache2-utils
|
||||
# QEMU user-mode for baseline CPU verification in CI
|
||||
case "$arch" in
|
||||
x64) install_packages qemu-x86_64 ;;
|
||||
aarch64) install_packages qemu-aarch64 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
100
scripts/verify-baseline-cpu.sh
Executable file
100
scripts/verify-baseline-cpu.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Verify that a Bun binary doesn't use CPU instructions beyond its baseline target.
|
||||
# Uses QEMU user-mode emulation with restricted CPU features.
|
||||
# Any illegal instruction (SIGILL) causes exit code 132 and fails the build.
|
||||
#
|
||||
# QEMU must be pre-installed in the CI image (see .buildkite/Dockerfile and
|
||||
# scripts/bootstrap.sh).
|
||||
|
||||
ARCH=""
|
||||
BINARY=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--binary) BINARY="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$ARCH" ] || [ -z "$BINARY" ]; then
|
||||
echo "Usage: $0 --arch <x64|aarch64> --binary <path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "ERROR: Binary not found: $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Select QEMU binary and CPU model
|
||||
HOST_ARCH=$(uname -m)
|
||||
if [ "$ARCH" = "x64" ]; then
|
||||
QEMU_BIN="qemu-x86_64"
|
||||
if [ -f "/usr/bin/qemu-x86_64-static" ]; then
|
||||
QEMU_BIN="qemu-x86_64-static"
|
||||
fi
|
||||
QEMU_CPU="Nehalem"
|
||||
CPU_DESC="Nehalem (SSE4.2, no AVX/AVX2/AVX512)"
|
||||
elif [ "$ARCH" = "aarch64" ]; then
|
||||
QEMU_BIN="qemu-aarch64"
|
||||
if [ -f "/usr/bin/qemu-aarch64-static" ]; then
|
||||
QEMU_BIN="qemu-aarch64-static"
|
||||
fi
|
||||
# cortex-a53 is ARMv8.0-A (no LSE atomics, no SVE). It's the most widely
|
||||
# supported ARMv8.0 model across QEMU versions.
|
||||
QEMU_CPU="cortex-a53"
|
||||
CPU_DESC="Cortex-A53 (ARMv8.0-A+CRC, no LSE/SVE)"
|
||||
else
|
||||
echo "ERROR: Unknown arch: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v "$QEMU_BIN" &>/dev/null; then
|
||||
echo "ERROR: $QEMU_BIN not found. It must be pre-installed in the CI image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_NAME=$(basename "$BINARY")
|
||||
|
||||
echo "--- Verifying $BINARY_NAME on $CPU_DESC"
|
||||
echo " Binary: $BINARY"
|
||||
echo " QEMU: $QEMU_BIN -cpu $QEMU_CPU"
|
||||
echo " Host: $HOST_ARCH"
|
||||
echo ""
|
||||
|
||||
run_test() {
|
||||
local label="$1"
|
||||
shift
|
||||
echo "+++ $BINARY_NAME: $label"
|
||||
if "$QEMU_BIN" -cpu "$QEMU_CPU" "$@"; then
|
||||
echo " PASS"
|
||||
return 0
|
||||
else
|
||||
local exit_code=$?
|
||||
echo ""
|
||||
if [ $exit_code -eq 132 ]; then
|
||||
echo " FAIL: Illegal instruction (SIGILL)"
|
||||
echo ""
|
||||
echo " The $BINARY_NAME binary uses CPU instructions not available on $QEMU_CPU."
|
||||
if [ "$ARCH" = "x64" ]; then
|
||||
echo " The baseline x64 build targets Nehalem (SSE4.2)."
|
||||
echo " AVX, AVX2, and AVX512 instructions are not allowed."
|
||||
else
|
||||
echo " The aarch64 build targets Cortex-A53 (ARMv8.0-A+CRC)."
|
||||
echo " LSE atomics, SVE, and dotprod instructions are not allowed."
|
||||
fi
|
||||
else
|
||||
echo " FAIL: exit code $exit_code"
|
||||
fi
|
||||
exit $exit_code
|
||||
fi
|
||||
}
|
||||
|
||||
run_test "bun --version" "$BINARY" --version
|
||||
run_test "bun -e eval" "$BINARY" -e "console.log(JSON.stringify({ok:1+1}))"
|
||||
|
||||
echo ""
|
||||
echo " All checks passed for $BINARY_NAME on $QEMU_CPU."
|
||||
@@ -1509,8 +1509,104 @@ pub fn VisitExpr(
|
||||
}
|
||||
};
|
||||
|
||||
// IIFE folding optimization: simplify immediately invoked function expressions
|
||||
// Reference: OXC's substitute_iife_call in substitute_alternate_syntax.rs:1599-1679
|
||||
if (p.options.features.minify_syntax) {
|
||||
if (tryFoldIIFE(p, e_, expr.loc)) |folded| {
|
||||
return folded;
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
/// Optimizes Immediately Invoked Function Expressions (IIFEs)
|
||||
/// - `(() => {})()` → `void 0`
|
||||
/// - `(() => expr)()` → `expr`
|
||||
/// - `(() => { return expr })()` → `expr`
|
||||
/// - `(() => { sideEffect() })()` → `(sideEffect(), void 0)`
|
||||
/// - `(function() {})()` → `void 0`
|
||||
fn tryFoldIIFE(p: *P, call: *E.Call, loc: logger.Loc) ?Expr {
|
||||
// Condition 1: No arguments in the call
|
||||
if (call.args.len != 0) return null;
|
||||
|
||||
// Condition 2: Not an optional chain
|
||||
if (call.optional_chain != null) return null;
|
||||
|
||||
// Case A: Arrow function (() => ...)()
|
||||
if (call.target.data.as(.e_arrow)) |arrow| {
|
||||
// No parameters allowed
|
||||
if (arrow.args.len != 0) return null;
|
||||
|
||||
// Skip async arrows (they return Promises)
|
||||
if (arrow.is_async) return null;
|
||||
|
||||
const stmts = arrow.body.stmts;
|
||||
|
||||
// Case A1: Empty body → void 0
|
||||
// (() => {})() → void 0
|
||||
if (stmts.len == 0) {
|
||||
return p.newExpr(E.Undefined{}, loc);
|
||||
}
|
||||
|
||||
// Case A2: Single statement body
|
||||
if (stmts.len == 1) {
|
||||
const stmt = stmts[0];
|
||||
|
||||
switch (stmt.data) {
|
||||
.s_return => |ret| {
|
||||
if (ret.value) |value| {
|
||||
// Skip if the return value is a member access (e_dot or e_index).
|
||||
// Inlining (() => obj.foo)() to obj.foo would change `this` binding
|
||||
// when the result is called: (() => obj.foo)()() should have
|
||||
// `this === undefined`, but obj.foo() would have `this === obj`.
|
||||
if (value.data == .e_dot or value.data == .e_index) {
|
||||
return null;
|
||||
}
|
||||
// (() => { return expr })() → expr
|
||||
// Also handles: (() => expr)() → expr
|
||||
return value;
|
||||
}
|
||||
// (() => { return })() → void 0
|
||||
return p.newExpr(E.Undefined{}, loc);
|
||||
},
|
||||
.s_expr => |expr_stmt| {
|
||||
// (() => { sideEffect() })() → (sideEffect(), void 0)
|
||||
return p.newExpr(E.Binary{
|
||||
.op = .bin_comma,
|
||||
.left = expr_stmt.value,
|
||||
.right = p.newExpr(E.Undefined{}, loc),
|
||||
}, loc);
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Case B: Function expression (function() {})()
|
||||
if (call.target.data.as(.e_function)) |func| {
|
||||
const f = &func.func;
|
||||
|
||||
// No parameters allowed
|
||||
if (f.args.len != 0) return null;
|
||||
|
||||
// Skip async/generator functions (they return Promise/Generator)
|
||||
if (f.flags.contains(.is_async) or f.flags.contains(.is_generator)) return null;
|
||||
|
||||
// Only handle empty body (to avoid `this` binding issues)
|
||||
// (function() {})() → void 0
|
||||
if (f.body.stmts.len == 0) {
|
||||
return p.newExpr(E.Undefined{}, loc);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn e_new(p: *P, expr: Expr, _: ExprIn) Expr {
|
||||
const e_ = expr.data.e_new;
|
||||
e_.target = p.visitExpr(e_.target);
|
||||
|
||||
117
test/bundler/transpiler/iife-folding.test.ts
Normal file
117
test/bundler/transpiler/iife-folding.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
describe("IIFE folding", () => {
|
||||
async function minify(code: string): Promise<string> {
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["/input.js"],
|
||||
minify: { syntax: true },
|
||||
files: {
|
||||
"/input.js": code,
|
||||
},
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.logs.map(l => l.message).join("\n"));
|
||||
}
|
||||
return (await result.outputs[0].text()).trim();
|
||||
}
|
||||
|
||||
describe("arrow function IIFEs", () => {
|
||||
test("empty arrow IIFE to void 0", async () => {
|
||||
const code = await minify("export const x = (() => {})()");
|
||||
expect(code).toContain("void 0");
|
||||
expect(code).not.toContain("=>");
|
||||
});
|
||||
|
||||
test("arrow expression IIFE inlined", async () => {
|
||||
const code = await minify("export const x = (() => 42)()");
|
||||
// Variable may be renamed, check the value is inlined
|
||||
expect(code).toMatch(/=\s*42/);
|
||||
expect(code).not.toContain("=>");
|
||||
});
|
||||
|
||||
test("arrow expression with call inlined", async () => {
|
||||
const code = await minify("export const x = (() => foo())()");
|
||||
// Variable may be renamed, check the call is inlined
|
||||
expect(code).toMatch(/=\s*foo\(\)/);
|
||||
expect(code).not.toContain("=>");
|
||||
});
|
||||
|
||||
test("arrow with return statement inlined", async () => {
|
||||
const code = await minify("export const x = (() => { return 42 })()");
|
||||
// Variable may be renamed, check the value is inlined
|
||||
expect(code).toMatch(/=\s*42/);
|
||||
expect(code).not.toContain("return");
|
||||
});
|
||||
|
||||
test("arrow with return call inlined", async () => {
|
||||
const code = await minify("export const x = (() => { return foo() })()");
|
||||
// Variable may be renamed, check the call is inlined
|
||||
expect(code).toMatch(/=\s*foo\(\)/);
|
||||
expect(code).not.toContain("return");
|
||||
});
|
||||
|
||||
test("arrow with expression statement becomes sequence", async () => {
|
||||
const code = await minify("export const x = (() => { sideEffect() })()");
|
||||
expect(code).toContain("sideEffect()");
|
||||
expect(code).toContain("void 0");
|
||||
});
|
||||
|
||||
test("nested IIFE in call argument", async () => {
|
||||
const code = await minify("console.log((() => 42)())");
|
||||
expect(code).toContain("console.log(42)");
|
||||
expect(code).not.toContain("=>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("function expression IIFEs", () => {
|
||||
test("empty function IIFE to void 0", async () => {
|
||||
const code = await minify("export const x = (function() {})()");
|
||||
expect(code).toContain("void 0");
|
||||
expect(code).not.toContain("function");
|
||||
});
|
||||
|
||||
test("function with parameters NOT folded", async () => {
|
||||
const code = await minify("export const x = (function(a) { return a + 1 })(5)");
|
||||
expect(code).toContain("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases - should NOT be folded", () => {
|
||||
test("async arrow NOT folded (returns Promise)", async () => {
|
||||
const code = await minify("export const x = (async () => { await foo() })()");
|
||||
expect(code).toContain("async");
|
||||
});
|
||||
|
||||
test("arrow with arguments NOT folded", async () => {
|
||||
const code = await minify("export const x = ((a) => a + 1)(5)");
|
||||
expect(code).toContain("=>");
|
||||
});
|
||||
|
||||
test("function with non-empty body NOT folded", async () => {
|
||||
const code = await minify("export const x = (function() { return this.x })()");
|
||||
expect(code).toContain("function");
|
||||
});
|
||||
|
||||
test("generator function NOT folded", async () => {
|
||||
const code = await minify("export const x = (function*() {})()");
|
||||
expect(code).toContain("function*");
|
||||
});
|
||||
|
||||
test("async function NOT folded", async () => {
|
||||
const code = await minify("export const x = (async function() {})()");
|
||||
expect(code).toContain("async");
|
||||
});
|
||||
|
||||
test("member access return NOT folded (this binding)", async () => {
|
||||
// (() => obj.foo)()() should have `this === undefined`
|
||||
// but if inlined to obj.foo(), `this === obj`
|
||||
const code = await minify("export const x = (() => obj.foo)()");
|
||||
expect(code).toContain("=>");
|
||||
});
|
||||
|
||||
test("index access return NOT folded (this binding)", async () => {
|
||||
const code = await minify("export const x = (() => obj['foo'])()");
|
||||
expect(code).toContain("=>");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user