Reviving Prepack, Facebooks partial JavaScript evaluator
A report from the Prepack revival · November 2026
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
A report from the Prepack revival · November 2026
Facebook set down its JavaScript partial evaluator in 2018, leaving behind problems its own authors called hard, undecidable and “a bridge too far.” We went back in with AI agents, an SMT solver and a fuzzer. This is what worked, what didn’t, and what it cost. Code: github.com/jlongster/prepack-revived Live demo: a React storefront rendered by the revived Prepack
Prepack 0.2.54 (2018) · 43 silently wrong
Revived Prepack (2026) · 0 silently wrong
A partial evaluator for JavaScript, and the problems its authors left open, in their own words.
Prepack is Facebook’s partial evaluator for JavaScript. It runs the code a bundle would run when it loads, using its own interpreter, and writes out a new bundle that rebuilds the result directly. Whatever it can’t know at build time (the clock, the user, the DOM) it tracks as an abstract value and leaves as residual code that runs later. Facebook set it down at the end of 2018; asked in 2020 whether it was dead, its lead answered that “no one currently at Facebook seems to have a scenario in mind where they believe that Prepack can be of use.”
// Build a lookup table when the app starts. const units = ["B", "KB", "MB", "GB", "TB"]; const bytesPer = {}; for (let i = 0; i < units.length; i++) { bytesPer[units[i]] = Math.pow(1024, i); }
// Only known when the app actually runs. const startedAt = Date.now();
global.app = { bytesPer, startedAt };
Prepack 0.2.54the last release, 2018
✓ Succeeds: same result as the original
(function () { var _$1 = this; var _$0 = _$1.Date.now(); var _1 = { B: 1, KB: 1024, MB: 1048576, GB: 1073741824, TB: 1099511627776 }; var _0 = { bytesPer: _1, startedAt: _$0 }; _$1.app = _0; }).call(this);
16 lines · 248 bytes · exit 0 · 0.3 s
RunOriginal programPrepack 0.2.54 outputapp.bytesPer.TB, typeof app.startedAt1099511627776,number1099511627776,number
Prepack also has a second mode: __optimize(fn) specializes a function for abstract arguments, running its body with every parameter unknown. Most examples below use it, because it is where the hard problems show up fastest.
The code and the issue tracker document the technical problems the original team never solved. We grouped them into eight. Here they are in the developers’ own words.
Prepack could only unroll loops. When the bound was unknown it gave up, or after six speculative iterations failed. #1174 (“Compute fixed points for loops”) was opened in 2017 and never closed.
Prepack gives up when hitting the loop with the abstract condition. This is a hard problem.
Yes, with loops you have to widen and once you widen, precision is lost. Range domains let you regain some of this precision. If you range over an unbounded collection of strings and use them as property names, it's pretty much game over.
Recursion was unrolled to the maximum stack depth. The summarization that should have stopped it was switched off (#1586). The most-upvoted bug in the tracker, #543 with 121 reactions, is a one-line change that turned a recursive call into 1,065 lines of output.
In general, the question whether a function call is going to result in unbounded recursion is undecidable. The check we have currently is known to be conceptually too conservative --- it just looks whether a recursive call is guarded by any abstract condition.
Prepack cannot just skip a loop (or recursive call), as Prepack then wouldn't know what all possible side effects on the heap would, and what values the code could return or exceptions it could throw. That's a hard problem, and it's on our longer term roadmap.
return, throw and break under unknown conditions; switch on unknown values (labeled “level 4 (hard)” in #1378); try/catch around code that might throw. The fuzzer the team built in 2018 found silent miscompiles here (#2354, #2355).
Prepack outputs invalid code without a diagnostic.
Facts like “we are inside if (x != null)” were tracked by a hand-written simplifier and implication engine, with arbitrary recursion caps. #2355: if (a) return true; return false; was simplified to return a. The serializer carries a TODO that admits “something else going on that is not yet understood”.
After if (x) a.push(42), the length of a is “0 or 1”. Prepack couldn’t represent that, so the next push failed (#1066).
I'm not sure we are ready to head down this slippery slope of reasoning over abstract arithmetic.
Anything passed to code Prepack couldn’t see had to be assumed modified (“havoced”), including arguments to harmless built-ins: #2319 counted RegExp.prototype.test, indexOf, parseInt and Math.abs as the top bail-outs in Facebook’s bundle. Caches that an optimized function fills lazily (#1750) needed what Venter called the outer fixed point:
This is indeed kind of like a loop. In the static analysis world, this is know as computing the outer fixed point. Doing it properly is a bridge too far for us right now, so some hackery will be require in the meanwhile.
Prepack had to know what window, document, timers and time zones do. #24 (“Add DOM shims”, 76 reactions) predates the launch and stayed open; the models that existed were thousands of lines, written by hand.
Getting to the point where it can usefully run on real browser based applications is going to take a lot of very hard work and a long time.
The flip side of evaluating everything: unrolled loops, materialized tables, duplicated helpers. The published 0.2.54 makes lodash about 3× larger after minification, 2× larger gzipped, and slower to load on a modern V8. Sebastian Markbåge (gist) named output size as “ultimately the reason we abandoned that particular approach.”
One compiler bailout, and the difference in the bundle is huge.
Modern language features sat outside the list because they are “only” spec work, but one of them touches the core of abstract interpretation. The generators discussion in #712 stalled on suspending a computation in the middle of exploring both sides of an unknown branch:
More importantly, how do you iterate an if statement where the condition is abstract?
Part 2 takes the problems one at a time: how the revived Prepack approaches each one, what is still out of reach, and the original and revived outputs side by side. We also explored a different architecture that sidesteps the interpreter entirely, running initialization in the real engine and emitting the heap it builds; that is written up separately.
Problem by problem: the technique, the key insight, what is still out of reach, and the original and revived outputs side by side.
Each problem below gets the same treatment: what the original developers said about it, the technique the revived Prepack uses, what is still out of reach, and a real before/after. One rule holds throughout: when Prepack can’t show that a transformation is right, it stops with a diagnostic instead of emitting something that might be wrong. First, the overall result.
To measure the problems rather than argue about them, we turned the issue tracker into a corpus of 194 small, deterministic programs: the repros from the issues above, variants of them, minimized bugs found by a differential fuzzer, and excerpts of real libraries. Each program names its source issue and its goal in a header, and runs unchanged in plain Node, so every case can be checked by running the original and Prepack’s output side by side.
On the 194 cases, the published Prepack 0.2.54 passes 40, crashes or reports an error on 96, and is silently wrong on 43. The revived build passes 190, with 0 wrong and 4 clean errors. Prepack’s own regression suite passes 3,744 of 3,744 runs (the original has 3,378), and across 22 real-world libraries and six option profiles each, the revived build produces no wrong output.
The four remaining errors are deliberate: a test that checks the duplicate-name diagnostic, an unfrozen object leaking from global code (reported, as the original test suite expects), a generator left suspended at the end of initialization (see modern JavaScript), and one fuzzer case that bails out cleanly with correct recovered output.
If the behavior of a loop can be nicely summarized as a set of unknown properties on known objects, the the effects of the loop on the heap is known and it will be safe to emit a residual loop into the code at the point of time associated with the loop.
That was the plan in 2017, and it is what the revived Prepack does, in residualLoop.js. It still tries to unroll a loop first. When that fails it rewrites the loop into a small closure, passes in the values the loop only reads, marks the objects it writes as escaped, and emits the loop as residual code at the right point in time. Before accepting the residual loop it evaluates one symbolic iteration against that state and checks every effect: if the body could touch anything that wasn’t accounted for, Prepack refuses with a diagnostic instead of guessing. The first case is #1174 itself:
function sum(n) { var s = 0; for (var i = 0; i < n; i++) { s += i; } return s; } if (global.__optimize) __optimize(sum);
Prepack 0.2.54the last release, 2018
– Reports an error (exit 1), then prints its fallback
var sum; (function () { var _$3 = this; var _1 = function (n) { var __get_scope_binding_0 = function (__selector) { var __captured; switch (__selector) { case 0: __captured = [void 0]; break; } __scope_0[__selector] = __captured; return __captured; }; var __scope_0 = new Array(1); var __leaked_0, __leaked_1; var _5 = function () { var __captured__scope_1 = __scope_0[0] || __get_scope_binding_0(0); for (__captured__scope_1[0] = 0; __captured__scope_1[0] < __leaked_0; __captured__scope_1[0]++) { __leaked_1 += __captured__scope_1[0]; } }; __leaked_0 = n; __leaked_1 = 0; var _$1 = _5(); var _$2 = __leaked_1; return _$2; }; _$3.sum = _1; }).call(this);
stderrIn input file loops-1174.js(3:23) FatalError PP0001: (https://github.com/facebook/prepack/wiki/PP0001) at sum (loops-1174.js:3:23) Prepack failed, reporting 1 fatal error.
30 lines · 792 bytes · exit 1 · 0.3 s
Revived Prepackthis project
✓ Succeeds: same result as the original
var sum; (function () { var _$7 = this;
_$7.sum = function (n) { var __l0, __l1, __l2, __l3;
var _6 = function (n) { for (; __l2 < n; __l2++) { __l3 += __l2; } };
__l2 = 0; __l3 = 0;
_6(n);
var _$5 = __l2; var _$6 = __l3; return _$6; }; }).call(this);
23 lines · 319 bytes · exit 0 · 0.4 s
RunOriginal programPrepack 0.2.54 outputRevived Prepack outputsum(0), sum(5), sum(10)0,10,450,10,450,10,45
#1722 is the case Venter answered with “game over.” State built before the loop is lost when the loop is widened. In 0.2.54 the output is not just imprecise, it is wrong: the first two keys disappear and the rest shift by one.
var n = global.__abstract ? __abstract("number", "n0") : n0; var arr = { 0: "a", 1: "b" }; var i = 2; do { arr[i] = "x"; i++; } while (i < n); var result = arr;
Prepack 0.2.54the last release, 2018
✗ Succeeds: wrong result
var result, i, arr, n; (function () { var _$2 = this; var __empty = {}; var _0 = n0; _$2.n = _0; var _2 = {}; _$2.arr = _2; _$2.i = 2; var _J = _$2; _J.i = 2; do { var _7 = 1 + _J.i; _$2.i = _7; var _$0 = _7; var _C = "" + _J.i; _2[_C] = "x"; _J.i = _$0; var _H = _7 < _0; var _$1 = H; } while ($1); _$2.result = _2; }).call(this);
23 lines · 390 bytes · exit 0 · 0.3 s
Revived Prepackthis project
✓ Succeeds: same result as the original
var result, i, arr, n; (function () { var _$3 = this;
_$3.n = n0; var _2 = {}; _$3.arr = _2; _$3.i = 2; _2[0] = "a"; _2[1] = "b"; delete _2[2]; delete _2[3];
(function () { do { arr[i] = "x"; i++; } while (i < n); })();
_$3.result = _2; }).call(this);
22 lines · 299 bytes · exit 0 · 0.4 s
RunOriginal programPrepack 0.2.54 outputRevived Prepack outputn0 = 5{"0":"a","1":"b","2":"x","3":"x","4":"x"} i=5{"3":"x","4":"x","5":"x"} i=5 ✗ di
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。