SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

Rewrite Bun.escapeHTML using Highway SIMD (#31483)

Rewrites `Bun.escapeHTML` from Rust into a dedicated C++ binding
(`src/jsc/bindings/escapeHTML.cpp`) using Highway SIMD, and makes it
faster than both the previous Rust and Zig implementations in every
measured case.

## What changed

**New C++ binding** — `jsFunctionBunEscapeHTML`, structured like
`stringWidth.cpp`:
- Coerces the argument to a string, branches on 8-bit (Latin-1) vs
16-bit (UTF-16) backing.
- **Passthrough (nothing to escape): returns the input `JSString`
unchanged** — zero allocation. A SWAR (8-bytes-at-a-time) scan handles
short strings without SIMD-dispatch overhead; long strings use a Highway
kernel.
- **Escape path**: one SIMD pass computes the exact escaped length, the
result is allocated once via `WTF::String::tryCreateUninitialized`, and
a single **table-driven** scalar pass fills it (a 256-entry length table
→ one load + branch per character; entities written directly). An
explicit `outLength > String::MaxLength` check throws `OutOfMemory` for
pathologically large output.

**Highway SIMD kernels** in `highway_strings.cpp` (dispatched via
`HWY_DYNAMIC_DISPATCH`):
- `IndexOfHTMLEscapeChar8Impl` / `IndexOfHTMLEscapeChar16Impl` —
first-metacharacter scan.
- `HtmlEscapeExtraLen8Impl` / `HtmlEscapeExtraLen16Impl` — exact
escaped-length count.

## Behavior

Unchanged from the previous implementation:
- The five entities: `&`→`&amp;`, `<`→`&lt;`, `>`→`&gt;`, `"`→`&quot;`,
`'`→`&#x27;` (numeric, not `&apos;`).
- UTF-16 surrogate pairs and lone surrogates are copied through verbatim
(they're > 0x80 and can't match a metacharacter) — only markup
characters change.
- Non-string arguments are coerced
(`number`/`boolean`/`null`/`undefined` short-circuit; a `Symbol` still
throws `TypeError`).
- `Bun.escapeHTML` remains a `function` with `.length === 2`.

## Deletions

- The Rust implementation (`escape_html_for_latin1_input`,
`escape_html_for_utf16_input`, `Escaped<T>`, scalar helpers), the
`Bun__escapeHTML8`/`Bun__escapeHTML16` FFI, the orphaned
`ScalarVec`/`AsciiVector` SIMD stand-ins, and the dead
`functionBunEscapeHTMLWithoutTypeCheck` declaration.

**Kept:** the byte→entity lookups `html_escape_entity` /
`xml_escape_entity` — the markdown renderer, SSR attribute escaping, and
the `bun test` JUnit XML reporter still use them.

## Benchmarks

Linux x64 (Intel Xeon Platinum 8375C), release builds,
`bench/snippets/escapeHTML.mjs`, `mitata`. main = `db928c247` (Rust),
this PR C++, and Bun `v1.3.14` (the last Zig release). Lower is better;
the last two columns are this PR's speedup.

| input | v1.3.14 (Zig) | main (Rust) | this PR (C++) | PR vs main | PR
vs 1.3.14 |
|---|--:|--:|--:|--:|--:|
| latin1 short, escapes (29 chars) | 109 ns | 96 ns | 77 ns | **1.24×
faster** | **1.41× faster** |
| latin1 short, passthrough (26 chars) | 24 ns | 22 ns | 15 ns | **1.44×
faster** | **1.53× faster** |
| latin1 long, escapes (~57 KB) | 109 µs | 81 µs | 42 µs | **1.94×
faster** | **2.63× faster** |
| latin1 long, passthrough (~56 KB) | 4.1 µs | 29 µs | 1.2 µs | **25×
faster** | **3.6× faster** |
| utf16 short, escapes (25 chars) | 201 ns | 165 ns | 87 ns | **1.90×
faster** | **2.31× faster** |
| utf16 short, passthrough (24 chars) | 40 ns | 37 ns | 21 ns | **1.75×
faster** | **1.88× faster** |
| utf16 long, escapes (~60 KB) | 170 µs | 120 µs | 52 µs | **2.31×
faster** | **3.27× faster** |
| utf16 long, passthrough (~58 KB) | 25 µs | 55 µs | 2.4 µs | **23×
faster** | **10.7× faster** |

## Verification

- `test/js/bun/util/escapeHTML.test.js` — all pass under `bun bd`
(ASAN), including the existing latin1/utf16 fuzz and lone-surrogate
cases. Added coverage for: every metacharacter, argument coercion (incl.
the symbol-throws path), zero-copy passthrough, escapes at SIMD lane
boundaries (offsets around 8/16/32/64), and densely packed escapes.
- Consumers of the kept helpers pass: markdown
(`test/cli/run/markdown-entrypoint.test.ts`) and the JUnit reporter
(`test/js/junit-reporter/junit.test.js`).
- New SIMD kernels added to the three
`scripts/verify-baseline-static/allowlist-*.txt` baseline static-scan
allowlists.

## Related issues

Fixes #21818 — the reported segfault was in `escapeHTMLForLatin1Input`;
that code path is replaced here by a bounds-checked C++ implementation
that passes the existing "bad input doesn't crash" fuzz test (lone
surrogates and `0xff` bytes at every offset) clean under ASAN.

Contributes to #8782 (switch from Zig `@Vector` to Google Highway with
runtime dispatch) — this migrates `Bun.escapeHTML`. Other `@Vector` call
sites remain, so that tracking issue stays open.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
R
robobun committed
472a06a2fb33587126ff6115dbecc9fbb4416f79
Parent: 4ee835b
Committed by GitHub <noreply@github.com> on 5/28/2026, 4:14:58 AM