Compare commits

...

7 Commits

Author SHA1 Message Date
SUZUKI Sosuke
26125888bd Merge branch 'main' into claude/iife-folding 2026-01-30 12:39:46 +09:00
Dylan Conway
adc1a6b05c Fix aarch64 SIGILL: disable mimalloc LSE atomics + update WebKit + QEMU verification (#26586)
Fixes illegal instruction (SIGILL) crashes on ARMv8.0 aarch64 CPUs
(Cortex-A53, Raspberry Pi 4, AWS a1 instances).

## Root cause

Upstream mimalloc force-enables `MI_OPT_ARCH` on arm64, which adds
`-march=armv8.1-a` and emits LSE atomic instructions (`casa`, `swpa`,
`ldaddl`). These are not available on ARMv8.0 CPUs.

## Fix

- Pass `MI_NO_OPT_ARCH=ON` to mimalloc on aarch64 (has priority over
`MI_OPT_ARCH` in mimalloc's CMake)
- Update WebKit to autobuild-596e48e22e3a1090e5b802744a7938088b1ea860
which explicitly passes `-march` flags to the WebKit build

## Verification

Includes QEMU-based baseline CPU verification CI steps (#26571) that
catch these regressions automatically.
2026-01-29 17:18:57 -08:00
Dylan Conway
8a11a03297 [publish images] 2026-01-29 16:04:44 -08:00
Dylan Conway
baea21f0c7 ci: add QEMU-based baseline CPU verification steps (#26571)
## Summary

Add CI steps that verify baseline builds don't use CPU instructions
beyond their target. Uses QEMU user-mode emulation with restricted CPU
features — any illegal instruction causes SIGILL and fails the build.

## Platforms verified

| Build Target | QEMU Command | What it catches |
|---|---|---|
| `linux-x64-baseline` (glibc) | `qemu-x86_64 -cpu Nehalem` | AVX, AVX2,
AVX512 |
| `linux-x64-musl-baseline` | `qemu-x86_64 -cpu Nehalem` | AVX, AVX2,
AVX512 |
| `linux-aarch64` (glibc) | `qemu-aarch64 -cpu cortex-a35` | LSE
atomics, SVE, dotprod |
| `linux-aarch64-musl` | `qemu-aarch64 -cpu cortex-a35` | LSE atomics,
SVE, dotprod |

## How it works

Each verify step:
1. Downloads the built binary artifact from the `build-bun` step
2. Installs `qemu-user-static` on-the-fly (dnf/apk/apt-get)
3. Runs two smoke tests under QEMU with restricted CPU features:
   - `bun --version` — validates startup, linker, static init code
   - `bun -e eval` — validates JSC initialization and basic execution
4. Hard fails on SIGILL (exit code 132)

The verify step runs in the build group after `build-bun`, with a
5-minute timeout.

## Known issue this will surface

**mimalloc on aarch64**: Built with `MI_OPT_ARCH=ON` which adds
`-march=armv8.1-a`, enabling LSE atomics. This will SIGILL on
Cortex-A35/A53 CPUs. The aarch64 verify steps are expected to fail
initially, confirming the test catches real issues. Fix can be done
separately in `cmake/targets/BuildMimalloc.cmake`.
2026-01-29 15:53:34 -08:00
autofix-ci[bot]
8f66535291 [autofix.ci] apply automated fixes 2026-01-29 08:43:49 +00:00
Sosuke Suzuki
4fd14c9086 fix: skip IIFE folding for member access returns
Fixes a bug where inlining (() => obj.foo)() to obj.foo would change
the `this` binding when the result is called.

Also:
- Use Bun.build with files option in tests (per review)
- Rename misleading test name
- Add tests for member access edge cases
2026-01-29 17:41:55 +09:00
Sosuke Suzuki
9c803caf74 feat(minify): add IIFE folding optimization
Simplify immediately invoked function expressions during minification:
- `(() => {})()` → `void 0`
- `(() => expr)()` → `expr`
- `(() => { return expr })()` → `expr`
- `(() => { sideEffect() })()` → `(sideEffect(), void 0)`
- `(function() {})()` → `void 0`

Async/generator functions and functions with parameters are not folded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:41:55 +09:00
8 changed files with 397 additions and 5 deletions

View File

@@ -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 \

View File

@@ -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),

View File

@@ -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()

View File

@@ -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

View File

@@ -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
View 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."

View File

@@ -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);

View 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("=>");
});
});
});