A report from the Prepack revival · November 2026
Prepack, revived
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
40 of 194 pass
Prepack 0.2.54 (2018) · 43 silently wrong
190 of 194 pass
Revived Prepack (2026) · 0 silently wrong
- Pass
- Correct but too large
- Error or crash
- Silently wrong
Part 1
Prepack and its eight hard problems
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.”
Date.now() can’t be known until the app runs, so it stays in the output as code. Every output on this page is the verbatim output of the tool named in its header, produced by site/scripts/run-examples.js; the table under each comparison runs the original program and each output and compares the results.// 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 };(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);| Run | Original program | Prepack 0.2.54 output |
|---|---|---|
| app.bytesPer.TB, typeof app.startedAt | 1099511627776,number | 1099511627776,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.
Eight hard problems
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.
P1 Loops with unknown trip counts
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.
P2 Recursion under unknown conditions
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.
P3 Joining control flow
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.
P4 Reasoning about path conditions
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”.
P5 Numeric facts
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.
P6 Objects escaping to unknown code
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.
P7 Modeling the environment
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.
P8 Output size
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.
Part 2
Reviving Prepack
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.
The hard-case corpus
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.
| Corpus folder | 0.2.54 passes | wrong | Revived passes | wrong |
|---|---|---|---|---|
| P1 Loops | 0/21 | 2 | 21/21 | 0 |
| P2 Recursion | 2/13 | 0 | 13/13 | 0 |
| P3 Control-flow joins | 9/21 | 4 | 21/21 | 0 |
| P4 Path conditions | 7/14 | 1 | 14/14 | 0 |
| P5 Numeric facts | 1/10 | 0 | 9/10 | 0 |
| P6 Havoc and built-ins | 9/19 | 2 | 18/19 | 0 |
| P7 Environment | 2/19 | 11 | 19/19 | 0 |
| P8 Output size | 1/9 | 0 | 9/9 | 0 |
| P9 Generators and async | 0/4 | 3 | 3/4 | 0 |
| Fuzzer-found bugs | 0/44 | 19 | 43/44 | 0 |
| Real-world init snippets | 7/12 | 0 | 12/12 | 0 |
| Real library excerpts | 2/8 | 1 | 8/8 | 0 |
| All 194 cases | 40/194 | 43 | 190/194 | 0 |
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.
P1 · Loops
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);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);In 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.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);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| sum(0), sum(5), sum(10) | 0,10,45 | 0,10,45 | 0,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.
n0. The revived build keeps the pre-loop state and emits the loop itself.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;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);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);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| n0 = 5 | {"0":"a","1":"b","2":"x","3":"x","4":"x"} i=5 | {"3":"x","4":"x","5":"x"} i=5 ✗ differs | {"0":"a","1":"b","2":"x","3":"x","4":"x"} i=5 |
And #2055, Nikolai Tillmann’s “hard problem”: a while loop scanning a string whose contents are
unknown.
function fn(props, splitPoint) {
var text = props.text || "";
text = text.replace(/\s*$/, "");
if (splitPoint !== null) {
while (text[splitPoint - 1] === "\n") {
splitPoint--;
}
}
return splitPoint;
}
if (global.__optimize) __optimize(fn);var fn;
(function () {
var _$8 = this;
var _1 = function (props, splitPoint) {
var __leaked_0, __leaked_1;
var _C = function () {
for (; __leaked_1[__leaked_0 - 1] === "\n";) {
__leaked_0--;
}
};
var _$0 = props.text;
if (_$0) {
var _3 = /\s*$/;
var _$2 = _$0.replace(_3, "");
}
var _5 = splitPoint !== null;
var _8 = _$0 ? _$2 : "";
if (_5) {
__leaked_0 = splitPoint;
__leaked_1 = _8;
var _$6 = _C();
} else {
__leaked_0 = splitPoint;
__leaked_1 = _8;
}
var _$7 = __leaked_0;
return _$7;
};
_$8.fn = _1;
}).call(this);In input file loops-2055.js(5:37) FatalError PP0001: (https://github.com/facebook/prepack/wiki/PP0001)
at fn (loops-2055.js:5:37)
Prepack failed, reporting 1 fatal error.var fn;
(function () {
var _$D = this;
_$D.fn = function (props, splitPoint) {
var __l0, __l1;
var _G = function (text) {
for (; text[__l1 - 1] === "\n";) {
__l1--;
}
};
var _$0 = props.text;
var _$1 = props.text;
if (_$0) {
var _$2 = _$0.replace(/\s*$/, "");
}
var _8 = splitPoint !== null;
if (_8) {
__l1 = splitPoint;
_G(_$0 ? _$2 : "");
var _$C = __l1;
}
return _8 ? _$C : splitPoint;
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| fn({text:"ab\n\ncd"}, 4), fn({}, null), fn({text:"a\n\n\nb"}, 4) | [ 2, null, 1 ] | [ 2, null, 1 ] | [ 2, null, 1 ] |
All 21 loop cases pass, including Babel-lowered for-of loops with large switch bodies
(#2053, #2477), labeled break/continue, and return or
throw inside loops. What stays out is honest: a loop inside an optimized function that mutates global
state, or a global-code loop that calls unknown functions, is refused with a clear diagnostic, because summarizing
arbitrary heap effects over an unknown number of iterations needs whole-program reasoning. That is the part of the
problem that is undecidable in general, and the residual loop sidesteps it only when the effects are local.
P2 · Recursion
TODO #1586: abstractRecursionSummarization is disabled for now, as it is likely too limiting (as observed in large internal tests).
The disabled code had five structural bugs (it always aborted at the end anyway). It is replaced
by recursion.js, built on one insight: recursion that stays concrete keeps unrolling, but as soon as a recursive call is
reached under a new abstract condition, Prepack stops and emits a call to the original function. In optimized
functions it first checks that the cluster of functions involved only touches state it can account for; in global code
it snapshots everything the function reads and proves that nothing changes it afterwards.
fib called with an unknown argument. 0.2.54 unrolls the recursion without bound; the run was killed after 60 seconds with no output. The revived build emits a residual call.function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
function f(n) {
return "fib=" + fib(n);
}
if (global.__optimize) __optimize(f);var fib, f;
(function () {
var _$K = this;
var _3 = function (n) {
var _$I = _2(n);
var _$J = "fib=" + _$I;
return _$J;
};
var _2 = function (n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
};
_$K.fib = _2;
_$K.f = _3;
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| f(7), f(12) | fib=13,fib=144 | no program | fib=13,fib=144 |
fact to the maximum stack depth and stops with PP0045.var k = global.__abstract ? __abstract("number", "k0") : k0;
function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1);
}
var r = k > 3 ? fact(k) : -1;In input file recursion-fact.js(3:36) FatalError PP0045: Maximum stack depth exceeded (https://github.com/facebook/prepack/wiki/PP0045)
at fact (recursion-fact.js:3:27)
at fact (recursion-fact.js:3:27)
at fact (recursion-fact.js:3:27)
at fact (recursion-fact.js:3:27)
at fact (recursion-fact.js:3:27)
… 108 more linesvar fact, r, k;
(function () {
var _$4 = this;
var _9 = function (n) {
return n <= 1 ? 1 : n * fact(n - 1);
};
_$4.fact = _9;
var _1 = k0;
_$4.k = _1;
var _3 = _1 > 3;
if (_3) {
var _$3 = _9(_1);
}
_$4.r = _3 ? _$3 : -1;
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| k0 = 6 | 720 | no program | 720 |
All 13 recursion cases pass, including mutual recursion, tree walks, a recursive-descent parser over an unknown
string, and deep clone. #543’s original one-liner is worth a footnote: by 0.2.54 it no longer exploded to 1,065
lines, because a later fix removed the dead Date.now() calls. The underlying problem, unrolling recursion
on unknown values, is what the cases above measure.
P3 · Control-flow joins and the fuzzer
When an if on an unknown value contains a return, Prepack has to carry both futures
forward and join them later. The original design spread that across ten completion classes, host exceptions, and a
piece of mutable state on the realm. The fuzzer’s 2018 bugs live here:
NaN where the program returns false.(function() {
"use strict";
function main(a1, a2, a3) {
if (a1) {
if (a2) {
return false;
}
}
if (!a3) {
return false;
}
return true;
}
if (global.__optimize) __optimize(main);
global.main = main;
})();(function () {
"use strict";
var _$0 = this;
var _1 = function (a1, a2, a3) {
var _7 = !a2;
var _5 = a1 && _7;
var _9 = !a1;
var _4 = _5 || _9;
var _3 = _4 && a3;
var _2 = _4 ? _3 : false;
return _2;
};
_$0.main = _1;
}).call(this);(function () {
"use strict";
var _$0 = this;
_$0.main = function (a1, a2, a3) {
return a1 && !a2 || !a1 ? !!a3 : false;
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| main(false, false, false) | false | false | false |
| main(0, "", NaN) | false | NaN ✗ differs | false |
if (a) return true; return false; must return a boolean.(function() {
"use strict";
function main(a1) {
if (a1) {
return true;
}
return false;
}
if (global.__optimize) __optimize(main);
global.main = main;
})();(function () {
"use strict";
var _$0 = this;
var _1 = function (a1) {
return a1;
};
_$0.main = _1;
}).call(this);(function () {
"use strict";
var _$0 = this;
_$0.main = function (a1) {
return !!a1;
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| [1, 0, "", "x", null, NaN].map(main) | [ true, false, false, true, false, false ] | [ 1, 0, '', 'x', null, NaN ] ✗ differs | [ true, false, false, true, false, false ] |
break out of a labeled block under an unknown condition. 0.2.54 drops the break and folds the function to a constant.function f(a, b) {
var r = "start";
block: {
if (a) break block;
r += "-1";
if (b) break block;
r += "-2";
}
return r;
}
if (global.__optimize) __optimize(f);var f;
(function () {
var _$0 = this;
var _1 = function (a, b) {
return "start-1-2";
};
_$0.f = _1;
}).call(this);var f;
(function () {
var _$0 = this;
_$0.f = function (a, b) {
return !a ? !b ? "start-1-2" : "start-1" : "start";
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| f(true, true), f(false, true), f(false, false) | start,start-1,start-1-2 | start-1-2,start-1-2,start-1-2 ✗ differs | start,start-1,start-1-2 |
These trace back to a handful of root causes: the saved-completion state was never scoped at
switch, labeled-statement and try boundaries; two simplifier rules erased boolean
conversions; a joined completion reported only its first branch’s value. Fixing those made room for the two features
the tracker had asked for. switch on an unknown value (#1378, “level 4 (hard)”) now handles fall-through
and returns:
function weight(v) {
var w = 0;
switch (v) {
case 1:
w += 1;
case 2:
w += 10;
break;
case 3:
return "three";
case 4:
case 5:
w += 100;
default:
w += 1000;
}
return w;
}
if (global.__optimize) __optimize(weight);In input file joins-switch-1378.js(4:10) FatalError PP0001: This operation is not yet supported on abstract value (https://github.com/facebook/prepack/wiki/PP0001)
at weight (joins-switch-1378.js:4:10)
Prepack failed, reporting 1 fatal error.var weight;
(function () {
var _$0 = this;
_$0.weight = function (v) {
var _J = 1 === v;
var _H = 2 === v;
return _J || 1 !== v && (_H || 2 !== v && 3 !== v) ? _J ? 11 : _H ? 10 : 4 === v ? 1100 : 5 === v ? 1100 : 1000 : "three";
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| [0, 1, 2, 3, 4, 5, "1"].map(weight) | [ 1000, 11, 10, 'three', 1100, 1100, 1000 ] | no program | [ 1000, 11, 10, 'three', 1100, 1100, 1000 ] |
And try/catch around code that might throw, which used to be a hard error (PP0021, #2120), is
now emitted as a residual try:
catch; that code throws where the original returns 42.function f(g) {
try {
return g();
} catch (e) {
return 42;
}
}
if (global.__optimize) __optimize(f);var f;
(function () {
var _$1 = this;
var _1 = function (g) {
var _$0 = g();
return _$0;
};
_$1.f = _1;
}).call(this);In input file joins-try-2120.js(3:12) RecoverableError PP0021: Possible throw inside try/catch is not yet supported (https://github.com/facebook/prepack/wiki/PP0021)
at f (joins-try-2120.js:3:12)
Prepack failed, reporting 1 recoverable error.var f;
(function () {
var _$3 = this;
_$3.f = function (g) {
{
var _$1;
try {
var _$0 = g();
} catch (e) {
_$1 = [e];
}
}
var _3 = void 0 !== _$1;
return _3 ? 42 : _$0;
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| f(() => 1), f(() => { throw 0 }) | 1,42 | Error: x ✗ differs | 1,42 |
A differential fuzzer generated 48,898 programs covering loops, labels, switch,
try, objects, arrays and closures. It found 1,260 silently wrong programs and 469 internal invariant
failures, which minimized to dozens of distinct bugs. Every one became a corpus case: 44 now live in
test/serializer/hard/fuzz/. On those 44, 0.2.54 is wrong on 19 and crashes on
25. The revived build passes 43.
P4 · Path conditions, checked by Z3
Prepack’s logic engine was a set of hand-written rewrite and implication rules. Rather than write better rules,
the revived Prepack asks a real SMT solver. Z3 runs as a persistent child process behind two named pipes, so
Prepack, which is synchronous throughout, can ask it questions with blocking reads (about 1 ms per query). Every
JavaScript value is encoded as a tagged datatype with exact IEEE-754 special cases for NaN,
±Infinity and truthiness, and the encoding itself was tested against Node on 3,000 random expressions.
Z3 then did two jobs. In normal runs it answers implication and feasibility questions the rules can’t, and prunes
branches that can never execute. In audit mode, it checks every rewrite the old rules perform: if
X → Y and X ≠ Y is satisfiable, that is a bug, with a counterexample. Run over the test suite
and the corpus, the audit produced 3,993 failure reports from 20 distinct places in the code. A few of them:
| Rule in the original simplifier | Z3 counterexample |
|---|---|
c ? true : false → c | c = 0: left is false, right is 0 |
(0 !== x) implies x is truthy | x = false (also "", null, NaN) |
x != null implies x is truthy | x = false |
!(x != null) implies x === null | x = undefined |
x == null implies x === null | x = undefined |
x === +0 implies !(x === -0) | x = 0 |
((c ? 1 : y) === y) → !c | c = false, y = NaN |
Thirteen unsound rules were fixed or deleted, and the audit now reports zero failures across 34,047 checks. Here is one the fuzzer found and the audit explained:
!(a == null ? 0 : a) was simplified to a == null, which is wrong for every falsy non-null value.(function() {
"use strict";
function main(a) {
return !(a == null ? 0 : a);
}
if (global.__optimize) __optimize(main);
global.main = main;
})();(function () {
"use strict";
var _$0 = this;
var _1 = function (a) {
var _2 = a == null;
return _2;
};
_$0.main = _1;
}).call(this);(function () {
"use strict";
var _$0 = this;
_$0.main = function (a) {
return !(a == null ? 0 : a);
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| [0, "", false, NaN, null, 1].map(main) | [ true, true, true, true, true, false ] | [ false, false, false, false, true, false ] ✗ differs | [ true, true, true, true, true, false ] |
The cost is real but bounded: on the full serializer suite, Z3 adds about two and a half minutes to a four and a
half minute run, because 98.8% of queries hit a cache. What Z3 can’t do here is also documented: it reasons about
numbers as exact reals, so it believes x + 1 > x even at 253.
P5 · Numeric facts
The fix is an interval domain: every numeric abstract value carries bounds, whether it is an integer,
and whether it might be NaN or -0, derived from the expression and refined by the current
path conditions. An array whose length is “0 or 1” can then be pushed to, by splitting on the few possible lengths:
var a = [];
var x = global.__abstract ? __abstract("boolean", "x0") : x0;
(function() {
if (x) a.push(42);
a.push(23);
})();In input file ranges-1066.js(5:10) FatalError PP0001: This operation is not yet supported on abstract value x0 (https://github.com/facebook/prepack/wiki/PP0001)
at push (native)
at ranges-1066.js:5:3
at ranges-1066.js:3:1
Prepack failed, reporting 1 fatal error.var x, a;
(function () {
var _$0 = this;
var _5 = x0;
var _0 = [!_5 ? 23 : 42];
if (_5) _0[1] = 23;
_$0.a = _0;
_$0.x = _5;
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| x0 = true | 42,23 | length 2 | no program | 42,23 | length 2 |
| x0 = false | 23 | length 1 | no program | 23 | length 1 |
The same machinery handles table[n & 3], clamped loop bounds and array methods on short
abstract-length arrays. Along the way it turned up a bug unrelated to abstract values: the original
Array.prototype.shift started its copy loop at the wrong index, writing the removed element to
O["-1"].
P6 · Built-ins and the outer fixed point
If we havoced everything that is read by an optimized function, we'll probably find it impossible to do any optimizations at all.
That was the bind: anything handed to code Prepack couldn’t see had to be havoced, and havocing too much makes the
analysis useless. #2319 listed Facebook’s top built-in bail-outs: RegExp.prototype.test (275),
Array.prototype.indexOf (201), parseInt (176) and Math.abs (58). This is the
kind of tedious, spec-heavy work a small team never has time for: 81 built-ins now carry declarative effect
summaries (what they coerce, whether they can throw, whether they can run user code), each with a differential test.
function classify(s, n) {
var re = /^[a-z]+$/;
var meta = { kind: "other", size: 0 };
if (re.test(s)) meta.kind = "word";
meta.size = parseInt(n, 10) + Math.abs(-2);
return meta.kind + ":" + meta.size + ":" + [10, 12, 14].indexOf(meta.size);
}
if (global.__optimize) __optimize(classify);var classify;
(function () {
var _$9 = this;
var _$A = _$9.parseInt;
var _1 = function (s, n) {
var _2 = /^[a-z]+$/;
var _$0 = _2.test(s);
var _F = _$A;
var _$1 = _F(n, 10);
var _$2 = _$1 + 2;
var _N = _$0 ? "word" : "other";
var _M = _N + ":";
var _$3 = _M + _$2;
var _6 = [10, 12, 14];
_6[0] = 10;
_6[1] = 12;
_6[2] = 14;
var _$4 = _6.indexOf(_$2);
var _I = _$3 + ":";
var _$5 = _I + _$4;
return _$5;
};
_$9.classify = _1;
}).call(this);In input file builtins-2319.js(4:15) FatalError PP0001: (https://github.com/facebook/prepack/wiki/PP0001)
at test (native)
at classify (builtins-2319.js:4:7)
In input file builtins-2319.js(5:27) FatalError PP0001: (https://github.com/facebook/prepack/wiki/PP0001)
at parseInt (native)
at classify (builtins-2319.js:5:15)
… 4 more linesvar classify;
(function () {
var _$2 = this;
_$2.classify = function (s, n) {
var _$0 = /^[a-z]+$/.test(s);
var _$1 = _$2.parseInt(n, 10);
var _C = 2 + _$1;
return (_$0 ? "word:" : "other:") + _C + ":" + (10 === _C ? 0 : 12 === _C ? 1 : 14 === _C ? 2 : -1);
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| classify("abc", "10"), classify("A1", "12px") | word:12:1,other:14:2 | word:12:1,other:14:2 | word:12:1,other:14:2 |
The lazy cache from #1750 needed the “outer fixed point” Venter called a bridge too far. The revived build evaluates the optimized function while recording which outer state it reads; any state it both reads and writes is made abstract, and the function is evaluated again until nothing changes. 0.2.54 baked the first call’s cache state into every call:
var cache = {};
function getEntry(arg) {
cache[arg] = cache[arg] || { n: 0, key: arg };
cache[arg].n++;
return cache[arg].key + cache[arg].n;
}
if (global.__optimize) __optimize(getEntry);var getEntry, cache;
(function () {
var _$O = this;
var _$P = _$O.Object;
var _$Q = _$P.prototype;
var _3 = function (arg) {
var _$0 = "" + arg;
var _m = _$Q;
var _$1 = _m[_$0];
var _$2 = "" + arg;
var _$4 = "" + arg;
var _$5 = "" + arg;
var _$7 = _1[_$5];
var _5 = _$4 === _$5;
if (_5) {
if (_$1) {
var _$8 = _$1.n;
}
} else {
var _$9 = _$7.n;
}
var _A = _$1 ? _$8 : 0;
var _9 = _5 ? _A : _$9;
var _$A = _9 + 1;
var _E = {};
_E.n = 0;
_E.key = arg;
var _I = _$1 || _E;
var _H = _5 ? _I : _$7;
_H.n = _$A;
var _$B = "" + arg;
var _$D = _1[_$B];
var _M = _$4 === _$B;
if (_M) {
if (_$1) {
var _$E = _$1.key;
} else {
var _$F = _E.key;
}
} else {
var _$G = _$D.key;
}
var _$H = "" + arg;
var _$J = _1[_$H];
var _O = _$4 === _$H;
if (_O) {
if (_$1) {
var _$K = _$1.n;
} else {
var _$L = _E.n;
}
} else {
var _$M = _$J.n;
}
var _T = _$1 ? _$E : _$F;
var _S = _M ? _T : _$G;
var _Y = _$1 ? _$K : _$L;
var _X = _O ? _Y : _$M;
var _$N = _S + _X;
_1[_$4] = _I;
return _$N;
};
var _1 = {};
_$O.getEntry = _3;
_$O.cache = _1;
}).call(this);In input file havoc-1750.js(1:13) Warning PP1007: side-effects from mutating a property at location havoc-1750.js[1:12] (https://github.com/facebook/prepack/wiki/PP1007) Prepack succeeded, reporting 1 warning.
var getEntry, cache;
(function () {
var _$Y = this;
var _1 = {};
_$Y.getEntry = function (arg) {
var _$O = _1[arg];
var _$P = _1[arg];
var _5 = {};
_5.n = 0;
_5.key = arg;
var _9 = _$O || _5;
_1[arg] = _9;
var _$Q = _1[arg];
var _$R = _$Q.n;
var _$S = +_$R;
var _E = 1 + _$S;
_$Q.n = _E;
var _$T = _1[arg];
var _$U = _$T.key;
var _$V = _1[arg];
var _$W = _$V.n;
var _$X = _$U + _$W;
return _$X;
};
_$Y.cache = _1;
}).call(this);In input file havoc-1750.js(1:13) Warning PP1007: side-effects from mutating a property at location havoc-1750.js[1:12] (https://github.com/facebook/prepack/wiki/PP1007) Prepack succeeded, reporting 1 warning.
| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| getEntry("a") ×2, getEntry("b"), getEntry("a") | a1,a2,b1,a3 | a1,a1,b1,a1 ✗ differs | a1,a2,b1,a3 |
Two parts of P6 remain open in general. Distinct object parameters are still assumed not to alias unless something
proves otherwise, and a method call on an untyped receiver (arr.indexOf(x) where arr could
be anything) still has to assume the worst about its arguments.
P7 · The environment
Some previous work on analyzing JavaScript has already highlighted just how difficult things become once you try to support all of JavaScript and you have to deal with untyped call backs. The conclusion reached in those efforts was that, in the end, the analyzer knows nothing much.
The original models were closed-world: a global Prepack didn’t know about was assumed not to exist. That produced the worst kind of failure, a build that succeeds with a warning and ships a bundle that throws on startup:
process into a program whose only action is to throw a ReferenceError. The revived build leaves environment reads residual.var isProd = process.env.NODE_ENV === "production";
var config = {
minify: isProd,
logLevel: process.env.DEBUG === "1" ? "debug" : isProd ? "error" : "info",
};var config, isProd;
(function () {
var $$0 = {
enumerable: false,
configurable: true,
writable: true
};
var _$0 = this;
var _$1 = _$0.ReferenceError;
var _$2 = _$1.prototype;
var _$3 = _$0.Object;
var _$4 = _$3.defineProperty;
var __constructor = function () {};
var _3 = _$2;
var _0 = (__constructor.prototype = _3, new __constructor());
$$0.value = "process is not defined", _$4(_0, "message", $$0);
$$0.value = "ReferenceError: process is not defined\n at env-process.js:1:14", _$4(_0, "stack", $$0);
throw _0;
}).call(this);In input file env-process.js(1:14) Warning PP0023: Program may terminate with exception: process is not defined
ReferenceError: process is not defined
at env-process.js:1:14 (https://github.com/facebook/prepack/wiki/PP0023)
Prepack succeeded, reporting 1 warning.var config, isProd;
(function () {
var _$4 = this;
var _$0 = process.env;
var _$1 = _$0.NODE_ENV;
var _0 = "production" === _$1;
_$4.isProd = _0;
var _$2 = process.env;
var _$3 = _$2.DEBUG;
_$4.config = {
minify: _0,
logLevel: _$3 === "1" ? "debug" : _0 ? "error" : "info"
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| in Node with NODE_ENV=production, DEBUG=1 | { minify: true, logLevel: 'debug' } | ReferenceError: process is not defined ✗ differs | { minify: true, logLevel: 'debug' } |
fetch is told it has none.var root = typeof window !== "undefined" ? window : global;
var api = {
hasFetch: typeof root.fetch === "function",
name: "api",
};var api, root;
(function () {
var _$0 = this;
var _5 = _$0;
_$0.root = _5;
var _1 = {
hasFetch: false,
name: "api"
};
_$0.api = _1;
}).call(this);var api, root;
(function () {
var _$2 = this;
var _7 = _$2;
_$2.root = _7;
var _$0 = _7.fetch;
var _$1 = typeof _$0;
_$2.api = {
hasFetch: "function" === _$1,
name: "api"
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| in a browser (window === this, fetch defined) | { hasFetch: true, name: 'api' } | { hasFetch: false, name: 'api' } ✗ differs | { hasFetch: true, name: 'api' } |
Three changes fix it. Unknown globals are now open-world: they become residual
reads. Models for the DOM, Node and Intl are generated from TypeScript’s lib.dom.d.ts and
@types/node (77 interfaces, 102 DOM globals), which, as it happens, is what a commenter on #24
suggested in 2017. And specializing on the environment is now explicit: --assume folds an assumption and
emits a runtime guard, with the original code as the fallback:
--assume 'process.env.NODE_ENV="production"' --assume timezoneOffset=0. The time-zone guard checks two dates half a year apart so daylight saving time can’t pass it by accident. On a machine in India the guard fails and the original code runs.var mode = process.env.NODE_ENV === "production" ? "prod" : "dev";
var newYear = new Date(2020, 0, 1).getTime();
var offset = new Date(2020, 6, 1).getTimezoneOffset();var offset, newYear, mode;
if (typeof process !== "undefined" && process != null && process.env != null && process.env.NODE_ENV === "production" && new Date(0).getTimezoneOffset() === 0 && new Date(1593561600000).getTimezoneOffset() === 0 && new Date(1577836800000).getTimezoneOffset() === 0 && new Date(1593561600000).getTimezoneOffset() === 0) {
(function () {
var _$1 = this;
_$1.mode = "prod";
_$1.newYear = 1577836800000;
_$1.offset = 0;
}).call(this);
} else {
var mode = process.env.NODE_ENV === "production" ? "prod" : "dev";
var newYear = new Date(2020, 0, 1).getTime();
var offset = new Date(2020, 6, 1).getTimezoneOffset();
}| Run | Original program | Revived Prepack output |
|---|---|---|
| NODE_ENV=production, TZ=UTC | prod,1577836800000,0 | prod,1577836800000,0 |
| NODE_ENV=development, TZ=Asia/Kolkata | dev,1577817000000,-330 | dev,1577817000000,-330 |
P8 · Output size
The revived Prepack attacks bloat at three levels: serializer peepholes (dead intermediate writes, compact
Error objects, Object.create instead of a prototype-swapping constructor), a cost model that
decides at serialization time whether to write out a computed object graph or the code that computed it, and limits on
inlining. The cost model is the important one:
i. The revived build verifies the loop is pure and self-contained, then keeps the loop. Long outputs are trimmed where marked; nothing else is edited.var table = [];
for (var i = 0; i < 1000; i++) {
table.push((i * i) % 7);
}var i, table;
(function () {
var _$0 = this;
var _0 = [0, 1, 4, 2, 2, 4, 1, 0, 1, 4, 2, 2, 4, 1, 0, 1, 4, 2, 2, 4, 1, 0, 1, 4, 2, 2, 4, 1, 0, 1, 4, 2, 2, 4, 1, 0, 1, 4, 2, 2, 4, 1, 0, 1, 4, 2, 2, 4, 1, 0 /* … 2,852 more characters */
_$0.table = _0;
_$0.i = 0; _$0.i = 998;
_$0.i = 999;
_$0.i = 1000;
}).call(this);var i, table;
(function () {
var _$0 = this;
_$0.table = [];
for (i = 0; i < 1000; i++) {
table.push(i * i % 7);
}
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| table.length, sum | 1000 entries, sum 2001 | 1000 entries, sum 2001 | 1000 entries, sum 2001 |
Error created at init. 0.2.54 rebuilds it as a plain object with the build machine’s stack trace baked in, so it is no longer a real Error. The revived build emits new TypeError(...).var INVALID = new TypeError("Invalid config");
global.check = function (c) {
if (!c) throw INVALID;
return c;
};var INVALID;
(function () {
var $$0 = {
enumerable: false,
configurable: true,
writable: true
};
var _$0 = this;
var _$1 = _$0.TypeError;
var _$2 = _$1.prototype;
var _$3 = _$0.Object;
var _$4 = _$3.defineProperty;
var _6 = function (c) {
if (!c) throw INVALID;
return c;
};
var __constructor = function () {};
var _5 = _$2;
var _0 = (__constructor.prototype = _5, new __constructor());
$$0.value = "Invalid config", _$4(_0, "message", $$0);
$$0.value = "TypeError: Invalid config\n at size-error.js:1:15", _$4(_0, "stack", $$0);
_$0.INVALID = _0;
_$0.check = _6;
}).call(this);var INVALID;
(function () {
var _$0 = this;
var _$1 = _$0.TypeError;
_$0.INVALID = new _$1("Invalid config");
_$0.check = function (c) {
if (!c) throw INVALID;
return c;
};
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| what check() throws | [object Error] Invalid config instanceof TypeError: true | [object Object] Invalid config instanceof TypeError: true ✗ differs | [object Error] Invalid config instanceof TypeError: true |
The same recomputation logic fixed the biggest real-world regression. lodash builds its whole API inside
runInContext() and keeps a reference to that factory, so materializing the result paid for the factory
and its output twice. Re-emitting the call instead took lodash from 3.16× to 1.05× its minified size; curried
function factories took ramda from 2.54× to 1.23×. Across the corpus, total output shrank by 40.5%, and no case is
larger than twice its input.
Modern JavaScript
Venter expected updating Prepack to modern JavaScript to “be a lot of work,” and it was, but it was the kind of work
that is mostly transcription. The parser now accepts ES2022 class fields and private members, optional catch bindings,
??, optional chaining and BigInt. Generators that are created and finished during initialization are
lowered to a state machine and run, and settled Promises are serialized as Promise.resolve(…) after
draining a real job queue. A generator still suspended at the end of initialization is refused on purpose:
its state lives in lowered closures with no equivalent in the source, and emitting a runtime to resume it would trade
fidelity for coverage. Venter’s question from #712, how to iterate a generator through an if whose
condition is abstract, is still open: generators run at build time only when everything about them is concrete.
const KEYWORDS = new Set(["let", "const", "if"]);
class Lexer {
#pos = 0;
constructor(src) { this.src = src; }
#peek() { return this.src[this.#pos]; }
static isKeyword(w) { return KEYWORDS.has(w); }
word() {
let w = "";
while (this.#pos < this.src.length && /\w/.test(this.#peek())) w += this.src[this.#pos++];
return w;
}
get position() { return this.#pos; }
}
globalThis.lex = (s) => {
const l = new Lexer(s);
const w = l.word();
return (Lexer.isKeyword(w) ? "kw:" : "id:") + w + "@" + l.position;
};let KEYWORDS;
(function () {
var $$0 = {
enumerable: false,
configurable: true,
writable: true
};
var _$0 = this;
var _$1 = _$0.ReferenceError;
var _$2 = _$1.prototype;
var _$3 = _$0.Object;
var _$4 = _$3.defineProperty;
var _$5 = _$0.Set;
var __constructor = function () {};
var _4 = class {
constructor(src) {
this.src = src;
}
static isKeyword(w) {
return KEYWORDS.has(w);
}
word() {
let w = "";
while (this.#pos < this.src.length && /\w/.test(this.#peek())) w += this.src[this.#pos++];
return w;
}
get position() {
return this.#pos;
}
};
_4.prototype.position = void 0;
_$0.Lexer = _4;
var _7 = _$2;
var _1 = (__constructor.prototype = _7, new __constructor());
$$0.value = "globalThis is not defined", _$4(_1, "message", $$0);
$$0.value = "ReferenceError: globalThis is not defined\n at private-class.js:14:1", _$4(_1, "stack", $$0);
throw _1;
var _B = new _$5(["let", "const", "if"]);
KEYWORDS = _B;
}).call(this);In input file private-class.js(14:1) Warning PP0023: Program may terminate with exception: globalThis is not defined
ReferenceError: globalThis is not defined
at private-class.js:14:1 (https://github.com/facebook/prepack/wiki/PP0023)
Prepack succeeded, reporting 1 warning.let KEYWORDS;
(function () {
var _$0 = this;
var _$1 = _$0.Set;
var _2 = class {
constructor(src) {
this.src = src;
}
#pos = 0;
#peek() {
return this.src[this.#pos];
}
static isKeyword(w) {
return KEYWORDS.has(w);
}
word() {
let w = "";
while (this.#pos < this.src.length && /\w/.test(this.#peek())) w += this.src[this.#pos++];
return w;
}
get position() {
return this.#pos;
}
};
_$0.Lexer = _2;
_$0.lex = s => {
const l = new Lexer(s);
const w = l.word();
return (Lexer.isKeyword(w) ? "kw:" : "id:") + w + "@" + l.position;
};
KEYWORDS = new _$1(["let", "const", "if"]);
}).call(this);| Run | Original program | Prepack 0.2.54 output | Revived Prepack output |
|---|---|---|---|
| lex("let x"), lex("abc") | kw:let@3,id:abc@3 | SyntaxError: Private field '#pos' must be declared in an enclosing class ✗ differs | kw:let@3,id:abc@3 |
Real-world results
The benchmark corpus has 22 real libraries and app bundles, from 3.6 KB to 8.9 MB, each with a smoke test whose
output digest must match the original’s in a fresh node process and in a vm context. Each is
compiled under six option profiles (default, browser, seeded random, old JavaScriptCore, Node/React, lazy objects):
132 compilations. Results on the revived build:
Revived Prepack on real code, browser profile. “Code cache” is load plus smoke test in a vm
context with V8’s code cache, the setting Prepack was designed for; “exec only” excludes compilation entirely; “cold”
is a fresh node process minus Node’s own 27 ms startup, shown only where the
original takes at least 5 ms. Medians of 30 runs (15 for inputs over 750 KB) on a shared 4-core machine.
| Workload | KB | Profiles OK | gzip size vs original | Code cache | Exec only | Cold node |
|---|---|---|---|---|---|---|
| lodash | 533.1 | 4/6 | 0.98× | 3.2 → 3.4 ms 0.92× | 1.04× | 16 → 17 ms 0.95× |
| underscore | 67.3 | 6/6 | 1.12× | 0.61 → 0.59 ms 1.03× | 1.03× | under 5 ms |
| ramda | 342.9 | 6/6 | 1.10× | 0.82 → 0.45 ms 1.84× | 1.89× | under 5 ms |
| immutable | 175.4 | 6/6 | 0.99× | 1.4 → 1.3 ms 1.12× | 1.12× | 6.9 → 6.5 ms 1.07× |
| moment | 637.8 | 6/6 | 1.32× | 12 → 2.7 ms 4.28× | 4.32× | 25 → 26 ms 0.97× |
| chroma-js | 109.2 | 6/6 | 0.94× | 0.97 → 0.90 ms 1.07× | 1.06× | under 5 ms |
| acorn | 240.9 | 6/6 | 1.07× | 1.9 → 1.5 ms 1.22× | 1.27× | 7.9 → 7.7 ms 1.02× |
| crc-32 | 3.6 | 6/6 | 55.12× | 0.34 → 0.22 ms 1.52× | 1.69× | under 5 ms |
| marked | 49.4 | 5/6 | 0.99× | 1.2 → 1.2 ms 0.99× | 1.01× | 35 → 36 ms 0.97× |
| d3-scale | 148.9 | 5/6 | 1.25× | 0.97 → 0.61 ms 1.59× | 1.70× | under 5 ms |
| redux | 18.1 | 6/6 | 0.89× | 0.33 → 0.28 ms 1.18× | 1.19× | under 5 ms |
| handlebars-app | 211.9 | 5/6 | 1.02× | 3.4 → 1.1 ms 3.13× | 3.03× | 16 → 4.2 ms 3.88× |
| preact-ssr-app | 22 | 4/6 | 1.01× | 0.41 → 0.36 ms 1.15× | 1.11× | under 5 ms |
| react-ssr-app | 47 | 5/6 | 1.17× | 0.89 → 0.59 ms 1.50× | 1.52× | under 5 ms |
| yaml-router-app | 157.1 | 6/6 | 1.02× | 1.8 → 0.62 ms 2.95× | 2.60× | 6.5 → 3.2 ms 2.04× |
| moment-timezone | 913.3 | 4/6 | 1.34× | 7.8 → 5.4 ms 1.44× | 1.37× | 24 → 20 ms 1.16× |
| prettier | 1,065.6 | 0/6 | Stops: PP0001: This operation is not yet supported on abstract value global | |||
| highlight-js | 1,834.5 | 5/6 | 2.08× | 16 → 12 ms 1.30× | 1.31× | 90 → 97 ms 0.93× |
| three | 1,914.4 | 5/6 | 1.18× | 4.7 → 3.5 ms 1.34× | 1.36× | 35 → 31 ms 1.10× |
| mathjs | 2,117.9 | 0/6 | Stops: PP0001: This operation is not yet supported on abstract value | |||
| babel-standalone | 5,248.9 | 0/6 | Stops: PP0001: This operation is not yet supported on abstract value | |||
| typescript | 8,899.1 | 0/6 | Stops: PP0001: This operation is not yet supported on abstract value | |||
Two things stand out. Where initialization is real work, skipping it pays: moment registers well over a hundred locales at
load and starts 4.3× faster from the code cache, handlebars and the YAML router app about 3×. Where a library mostly defines
functions, there is little to skip and the result is a wash (lodash, marked). Size is now close to the original for
most inputs; the exceptions are deliberate trades the cost model made, such as crc-32, whose 4,352
precomputed CRC constants are 44 KB gzipped against 0.8 KB of source in exchange for skipping the computation. The four
failures stop where open-world environment values reach loops Prepack can’t summarize yet (for @babel/standalone, a
localStorage read; with localStorage, navigator and document assumed absent it compiles, in about 279 seconds).
React, revisited
The React compiler of 2018 tried to fold React itself. The revival took a narrower route: specialize React 18
server rendering of a known app for unknown request data. The app is a 30-component e-commerce page
(the same one behind the Northstar demo linked below); the data is a different
product listing on every request. Instead of evaluating the real react-dom/server, which keeps its state
in long-lived queues Prepack can’t summarize, we ported React’s server renderer into about 1,300 lines written
in a style Prepack can evaluate: every function restores what it changes. The attribute tables are copied
verbatim from react-dom 18.3.1. Prepack then specializes the whole render for a data schema inferred from training
inputs. Here is one small component and the code Prepack produced for one place it is used:
app.jsx) and the corresponding
fragment of the revived Prepack’s generated renderer (one expression, verbatim; the full program is 83,754 bytes). React
is gone: the result is string concatenation with the branches the data needs.function StockBadge(props) {
var stock = props.stock;
if (stock <= 0) {
return (
<span className="ns-stock ns-stock-out" data-stock-state="out">
Out of stock
</span>
);
}
if (stock <= 5) {
return (
<span className="ns-stock ns-stock-low" data-stock-state="low">
Only {stock} left
</span>
);
}
return (
<span className="ns-stock ns-stock-in" data-stock-state="in">
In stock
{typeof props.shipsAtSec === "number"
? " · Ships " + formatDateUTC(props.shipsAtSec, false)
: ""}
</span>
);
}(_$5E <= 0
? "<span class=\"ns-stock ns-stock-out\" data-stock-state=\"out\">Out of stock</span>"
: _$5E <= 5
? "<span class=\"ns-stock ns-stock-low\" data-stock-state=\"low\">Only <!-- -->" + _$5E + "<!-- --> left</span>"
: "<span class=\"ns-stock ns-stock-in\" data-stock-state=\"in\">In stock</span>")The examples below are bigger. Each is a small app built from real components of the same page, compiled on its
own with the same compiler and training inputs, so the renderer Prepack generated for those components can be read in
one place. The inputs are the components exactly as they appear in app.jsx, plus a few lines of wrapper;
the outputs are excerpts of the generated code, with the omitted lines marked.
A list of unknown length
In 2018 this is where the React compiler stopped: components with loops were excluded from compilation, and a
map() callback over unknown data was left unevaluated, dragging the original component into the bundle
(#2141, #2142). Here the callback is specialized once and run by a residual loop
(__ssrMapJoin). Each item becomes a single string expression: the badge conditions turn into
ternaries, the title is escaped at runtime, and React’s <!-- --> separators around the stock count
appear exactly where React puts them.
StockBadge is the component shown above). Checked against renderToString on the benchmark’s 500 check inputs:
500/500 byte-identical, 0 fallbacks. This component alone renders
4.9× faster than renderToString (2.8× including the per-request
schema check).function ProductList(props) {
return (
<ul className="ns-grid">
{props.products.map(function (p) {
return (
<li key={p.sku} className={cx("ns-card", { "ns-card-out": p.stock <= 0 })}>
<ProductBadges product={p} />
<h3 className="ns-card-title">{p.title}</h3>
<StockBadge stock={p.stock} shipsAtSec={p.shipsAtSec} />
</li>
);
})}
</ul>
);
}
function App(props) {
return <ProductList products={props.data.products} />;
}
function ProductBadges(props) {
var p = props.product;
var onSale =
typeof p.originalPrice === "number" && p.originalPrice > p.price;
if (!p.featured && !p.clearance && !onSale) {
return null;
}
return (
<div className="ns-badges">
{p.featured ? (
<span className="ns-badge ns-badge-featured">Featured</span>
) : null}
{onSale ? <span className="ns-badge ns-badge-sale">Sale</span> : null}
{p.clearance ? (
<span className="ns-badge ns-badge-clearance">Clearance</span>
) : null}
</div>
);
}var _1 = function (data) {
var _C = function (_$4, _$5) {
"use strict";
var _$6 = _$4.sku;
var _$7 = _$4.stock;
var _$8 = _$4.title;
var _$9 = _$4.stock;
var _$A = _$4.shipsAtSec;
var _$B = _$4.originalPrice; var _3G = 1 + (_3J - _$S);
var _3t = 400 * _$K + _$O;
return "<li class" + (!(_$7 <= 0) ? "=\"ns-card\"" : "=\"ns-card ns-card-out\"") + ">" + (!_Y ? "" : "<div class=\"ns-badges\">" + ((!(void 0 === _$G ? void 0 : _$G) ? "" : "<span class=\"ns-badge ns-badge-featured\">Featured</span>") + (void 0 === _$B || !_f ? "" : "<span class=\"ns-badge ns-badge-sale\">Sale</span>") + (!(void 0 === _$H ? void 0 : _$H) ? "" : "<span class=\"ns-badge ns-badge-clearance\">Clearance</span>")) + "</div>") + "<h3 class=\"ns-card-title\">" + _43(_$8) + "</h3>" + (_p ? "<span class=\"ns-stock ns-stock-out\" data-stock-state=\"out\">Out of stock</span>" : _t ? "<span class=\"ns-stock ns-stock-low\" data-stock-state=\"low\">Only <!-- -->" + _$9 + "<!-- --> left</span>" : "<span class=\"ns-stock ns-stock-in\" data-stock-state=\"in\">In stock" + (void 0 === _$A ? "" : "<!-- --> \xB7 Ships " + (_43(_$U) + (!(_3G < 10) ? "" + _3G : "0" + _3G)) + ", " + ((_1X ? _1S <= 2 : _1V <= 2) ? 1 + _3t : _3t)) + "</span>") + "</li>";
};
var _$0 = data.products;
var _$3 = __ssrMapJoin(_$0, _C);
var _$V = _$3;
return "<ul class=\"ns-grid\">" + _$V + "</ul>";
};var _43 = function (value) {
var str = "" + value;
return _4c.test(str) ? str.replace(_4d, _4l) : str;
};Context and useMemo
Contexts are pure plumbing at render time, so the providers and useContext calls fold away entirely:
the locale becomes a plain read of data.locale. useMemo runs once per server render, so its
body is specialized in place. The loop that totals the cart becomes a residual loop over the items, and the
digit-grouping loop inside formatMoney stays as a small residual function. Everything else is arithmetic
and string building.
useMemo over the cart, locale-aware money formatting and a list of line items. Checked against renderToString on the benchmark’s 500 check inputs:
500/500 byte-identical, 0 fallbacks. This component alone renders
4.5× faster than renderToString (1.9× including the per-request
schema check).function App(props) {
var data = props.data;
return (
<LocaleContext.Provider value={data.locale}>
<UserContext.Provider value={data.user}>
<CartSummaryAside cart={data.cart} />
</UserContext.Provider>
</LocaleContext.Provider>
);
}
function CartSummaryAside(props) {
var cart = props.cart;
var locale = React.useContext(LocaleContext);
var user = React.useContext(UserContext);
var totals = React.useMemo(
function () {
var subtotal = 0;
var unitCount = 0;
for (var i = 0; i < cart.items.length; i++) {
var it = cart.items[i];
subtotal += it.qty * it.unitPrice;
unitCount += it.qty;
}
var discountRate =
user && user.tier === "vip"
? 0.1
: user && user.tier === "pro"
? 0.05
: 0;
var memberDiscount = subtotal > 0 ? -(subtotal * discountRate) : 0;
var afterDiscount = subtotal + memberDiscount + cart.creditAdjustment;
var taxable = afterDiscount > 0 ? afterDiscount : 0;
var shipping =
unitCount === 0 || taxable >= cart.freeShippingThreshold ? 0 : 14.95;
var tax = taxable * cart.taxRate;
var grandTotal = afterDiscount + shipping + tax;
return {
subtotal: subtotal,
unitCount: unitCount,
memberDiscount: memberDiscount,
shipping: shipping,
tax: tax,
grandTotal: grandTotal,
};
},
[cart, user]
);
var isEmpty = cart.items.length === 0;
var showBreakdown = !isEmpty || cart.creditAdjustment !== 0;
return (
<aside className="ns-cart" data-empty={isEmpty}>
<h2 className="ns-cart-title">Order Quote ({totals.unitCount})</h2>
{isEmpty ? (
<p className="ns-cart-empty">Your quote list is empty.</p>
) : (
<ul className="ns-cart-list">
{cart.items.map(function (it) {
return <CartLineItem key={it.sku} item={it} />;
})}
</ul>
)}
{showBreakdown ? (
<dl className="ns-cart-totals">
<dt>Subtotal</dt>
<dd>{formatMoney(totals.subtotal, locale)}</dd>
{totals.memberDiscount !== 0 ? (
<>
<dt>Member Discount</dt>
<dd>{formatMoney(totals.memberDiscount, locale)}</dd>
</>
) : null}
{cart.creditAdjustment !== 0 ? (
<>
<dt>Account Credit</dt>
<dd>{formatMoney(cart.creditAdjustment, locale)}</dd>
</>
) : null}
<dt>Shipping</dt>
<dd>
{totals.shipping === 0
? "FREE"
: formatMoney(totals.shipping, locale)}
</dd>
<dt>Est. Tax</dt>
<dd>{formatMoney(totals.tax, locale)}</dd>
<dt className="ns-cart-grand-label">Total</dt>
<dd className="ns-cart-grand-val">
{formatMoney(totals.grandTotal, locale)}
</dd>
</dl>
) : null}
<details className="ns-cart-notes" open={Boolean(cart.notesOpen)}>
<summary>Freight & Net-30 Terms</summary>
<p>Orders above {formatMoney(cart.freeShippingThreshold, locale)} ship free.</p>
</details>
<ActionButton variant="primary" disabled={isEmpty}>
Proceed to Checkout
</ActionButton>
</aside>
);
}
function CartLineItem(props) {
var item = props.item;
var locale = React.useContext(LocaleContext);
var lineTotal = item.qty * item.unitPrice;
return (
<li className="ns-cart-item" data-sku={item.sku}>
<div className="ns-cart-item-main">
<span className="ns-cart-item-title">{item.title}</span>
{item.backordered ? (
<span className="ns-cart-backorder">Backorder</span>
) : null}
</div>
<div className="ns-cart-item-math">
{item.qty} × {formatMoney(item.unitPrice, locale)} ={" "}
<strong>{formatMoney(lineTotal, locale)}</strong>
</div>
</li>
);
}var _1 = function (data) {
var __l0, __l4, __l5, __lV, __lZ, __la, __l1, __l2, __l3, __l6, __lW, __lX, __lY, __lb, __l7, __l8, __l9, __lc, __ld, __le, __lA, __lB, __lC, __lf, __lg, __lh, __lD, __lE, __lF, __li, __lj, __lk, __lG, __lH, __lI, __ll, __lm, __ln, __lJ, __lK, __lL, __lo, __lp, __lq, __lM, __lN, __lO, __lr, __ls, __lt;
var _A7 = function (_$4V, _$4W) { return "<li class=\"ns-cart-item\" data-sku=\"" + _9c(_$4a) + "\"><div class=\"ns-cart-item-main\"><span class=\"ns-cart-item-title\">" + _9c(_$4b) + "</span>" + (!(void 0 === _$4c ? void 0 : _$4c) ? "" : "<span class=\"ns-cart-backorder\">Backorder</span>") + "</div><div class=\"ns-cart-item-math\">" + _$4d + "<!-- --> \xD7 " + (!(_$4e < 0) || !(_$4n > 0) ? _CV ? "" !== _$5A ? "<!-- -->" + _CI : "" : "" !== _$59 ? "<!-- -->" + _CQ : "" : "<!-- -->-" + (_CV ? _CI : _CQ)) + "<!-- --> =<!-- --> <strong>" + (!(_Cx < 0) || !(_$5J > 0) ? _Cm : "-" + _Cm) + "</strong></div></li>";
};var _$0 = data.locale;
var _$1 = data.user;
var _$2 = data.cart;
__lb = 0;
__lW = void 0;
__lX = 0;
__lY = 0;
_9S(_$2);
var _$L = __lb;
var _$M = __lW;
var _$N = __lX;
var _$O = __lY;
var _B = _$1 !== null;
var _E = _$1;
if (_B) {
var _$P = _E.tier;
}
var _H = "vip" === _$P;
var _G = _B && _H;
if (!_G) {
if (_B) {
var _$Q = _E.tier;
}
}
var _$R = _$N > 0;
if (_$R) {
var _$S = _$N * (_G ? 0.1 : _B && "pro" === _$Q ? 0.05 : 0);
}
var _Y = -_$S;
var _X = _$R ? _Y : 0;
var _$T = _$N + _X;
var _$U = _$2.creditAdjustment;
var _$V = _$T + _$U; var _$4R = _9a(_$O, true);
var _$4S = _9b(_$O, true);
if (_x) {
var _$4U = __ssrMapJoin(_$g, _A7);
var _$5h = _$4U;
}
var _$5i = _$5j.Boolean(_11);
var _6O = !_1Y ? _9c(_$1E) : _9c(_$1D);
var _6l = !_2B ? _9c(_$1k) : _9c(_$1j);
var _7C = !_2r ? _9c(_$2I) : _9c(_$2H);
var _7p = !_43 ? _9c(_$3K) : _9c(_$3J);
var _8A = !_4g ? _9c(_$3r) : _9c(_$3q);
var _93 = !_5J;
var _8q = _9c(_$4Q);
var _8y = _9c(_$4P);
return "<aside class=\"ns-cart\" data-empty=\"" + _11 + "\"><h2 class=\"ns-cart-title\">Order Quote (" + _$4R + (_$4S ? "<!-- -->)" : ")") + "</h2>" + (_x ? "<ul class=\"ns-cart-list\">" + _$5h + "</ul>" : "<p class=\"ns-cart-empty\">Your quote list is empty.</p>") + (_11 && 0 === _$f ? "" : "<dl class=\"ns-cart-totals\"><dt>Subtotal</dt><dd>" + (!_$q || !(_$r > 0) ? _6O : "-" + _6O) + "</dd>" + (0 === _X ? "" : "<dt>Member Discount</dt><dd>" + (!_$R || !(_Y < 0) || !(_$1N > 0) ? _6l : "-" + _6l) + "</dd>") + (0 === _$1l ? "" : "<dt>Account Credit</dt><dd>" + (!(_$1m < 0) || !(_$1v > 0) ? _7C : "-" + _7C) + "</dd>") + "<dt>Shipping</dt><dd>" + (0 !== _$O && !_$Y ? !_3S ? _9c(_$2o) : _9c(_$2n) : "FREE") + "</dd><dt>Est. Tax</dt><dd>" + (!(_$a < 0) || !(_$2x > 0) ? _7p : "-" + _7p) + "</dd><dt class=\"ns-cart-grand-label\">Total</dt><dd class=\"ns-cart-grand-val\">" + (!_$3T || !(_$3U > 0) ? _8A : "-" + _8A) + "</dd></dl>") + "<details class=\"ns-cart-notes\"" + (!_$3t ? "" : " open=\"\"") + "><summary>Freight & Net-30 Terms</summary><p>Orders above " + (!(_$3u < 0) || !(_$43 > 0) ? _93 ? "" !== _$4Q ? "<!-- -->" + _8q : "" : "" !== _$4P ? "<!-- -->" + _8y : "" : "<!-- -->-" + (_93 ? _8q : _8y)) + "<!-- --> ship free.</p></details><button type=\"button\" class=\"ns-btn ns-btn-primary\"" + (!_$5i ? "" : " disabled=\"\"") + " data-variant=\"primary\">Proceed to Checkout</button></aside>";
};useId inside nested lists
React 18’s ids depend on a component’s position in the tree, and they have to match React’s format exactly.
Outside a list the position is known at build time, so :R0: and :R0H1: are folded straight into
the HTML. Inside a list of unknown length it isn’t, so the generated code carries React’s tree-id arithmetic and
computes each option’s :R…: id at runtime. Boolean attributes (disabled, hidden,
and checked from defaultChecked) become conditional pieces of the attribute string.
useId label, nested in two lists of unknown length. Checked against renderToString on the benchmark’s 500 check inputs:
500/500 byte-identical, 0 fallbacks. This component alone renders
9.9× faster than renderToString (2.2× including the per-request
schema check).function App(props) {
var data = props.data;
return (
<LocaleContext.Provider value={data.locale}>
<FilterSidebar filters={data.filters} inStockOnly={data.search.inStockOnly} />
</LocaleContext.Provider>
);
}
function FilterSidebar(props) {
var headingId = React.useId();
var stockToggleId = React.useId();
var filters = props.filters;
return (
<aside className="ns-sidebar" aria-labelledby={headingId}>
<h2 id={headingId} className="ns-sidebar-title">
Filters
</h2>
<div className="ns-stock-toggle">
<input
type="checkbox"
id={stockToggleId}
name="in_stock"
defaultChecked={Boolean(props.inStockOnly)}
/>
<label htmlFor={stockToggleId}>In-stock items only</label>
</div>
{filters.map(function (group) {
return <FilterGroup key={group.id} group={group} />;
})}
</aside>
);
}
function FilterGroup(props) {
var group = props.group;
return (
<fieldset
className="ns-filter-group"
disabled={Boolean(group.disabled)}
data-filter-key={group.id}
>
<legend className="ns-filter-legend">{group.title}</legend>
{group.options.length === 0 ? (
<p className="ns-filter-empty">No options</p>
) : (
<ul className="ns-filter-list">
{group.options.map(function (opt) {
return (
<FilterOption
key={opt.value}
groupId={group.id}
groupDisabled={group.disabled}
option={opt}
/>
);
})}
</ul>
)}
</fieldset>
);
}
function FilterOption(props) {
var opt = props.option;
var optId = React.useId();
var disabled = Boolean(props.groupDisabled || opt.count === 0);
return (
<li className={cx("ns-filter-opt", { "is-selected": opt.selected })}>
<input
type="checkbox"
id={optId}
name={"f_" + props.groupId}
value={opt.value}
defaultChecked={Boolean(opt.selected)}
disabled={disabled}
/>
<label htmlFor={optId}>{opt.label}</label>
<CountPill count={opt.count} hot={false} />
</li>
);
}var _1 = function (data) {
var _Y = function (_$8, _$9) {
"use strict";
var _1W = function (_$L, _$M) {
"use strict";
var _$N = _$L.value; var _2w = _4d(15, _$6, _$9);
var _2v = _4d(_2w, 2, 1);
var _2m = "=\":R" + _1L(_4c(_4d(_2v, _$J, _$M), _4e(_2v, _4e(_2w, _4e(15, "", _$6), 2), _$J))) + ":\"";
var _3n = "<span class=\"ns-pill\"" + (!(_$X <= 0) ? "" : " hidden=\"\"") + " data-zero=\"" + (0 === _$X) + "\">";
var _4J = _2I && _$11 == null;
var _4H = _1L(_$12);
return "<li class" + (!_$S ? "=\"ns-filter-opt\"" : "=\"ns-filter-opt is-selected\"") + "><input type=\"checkbox\" id" + _2m + " name=\"f_" + _1L(_$O) + "\"" + (!_$R ? "" : " disabled=\"\"") + (!_$V ? "" : " checked=\"\"") + " value=\"" + _1L(_$T) + "\"/><label for" + _2m + ">" + _1L(_$W) + "</label>" + ((_2Q ? _3n : _3n + (_4J ? "null" : _2F ? _2I ? "boolean" !== _2M && _2M !== "number" ? _1L(_$11) : _$14 : _4H : _$13)) + (_1x ? "" : _4J ? "" : _2I ? _$15 : "" !== _$12 ? _4H : "")) + "</span></li>";
};
var _$A = _$8.id;
var _$B = _$8.disabled;
var _$C = _$19.Boolean(void 0 === _$B ? void 0 : _$B);
var _$D = _$8.id;
var _$E = _$8.title;
var _$F = _$8.options;
var _$G = _$F.length;
if (!(0 === _$G)) {
var _$H = _$8.options;
}
var _i = 0 !== _$G;
if (_i) {
var _1S = _$H;
var _$J = _1S.length;
var _$K = __ssrMapJoin(_1S, _1W);
var _$17 = _$K;
}
return "<fieldset class=\"ns-filter-group\"" + (!_$C ? "" : " disabled=\"\"") + " data-filter-key=\"" + _1L(_$D) + "\"><legend class=\"ns-filter-legend\">" + _1L(_$E) + "</legend>" + (_i ? "<ul class=\"ns-filter-list\">" + _$17 + "</ul>" : "<p class=\"ns-filter-empty\">No options</p>") + "</fieldset>";
};
var _$0 = data.locale;
var _$1 = data.filters;
var _$2 = data.search;
var _$3 = _$2.inStockOnly;
var _$4 = _$19.Boolean(_$3);
var _W = _$1;
var _$6 = _W.length;
var _$7 = __ssrMapJoin(_W, _Y);
var _$18 = _$7;
return "<aside class=\"ns-sidebar\" aria-labelledby=\":R0:\"><h2 id=\":R0:\" class=\"ns-sidebar-title\">Filters</h2><div class=\"ns-stock-toggle\"><input type=\"checkbox\" id=\":R0H1:\" name=\"in_stock\"" + (!_$4 ? "" : " checked=\"\"") + "/><label for=\":R0H1:\">In-stock items only</label></div>" + _$18 + "</aside>";
};Fragments, text separators and raw HTML
dangerouslySetInnerHTML is inserted as is, without escaping. The keyed fragments in the breadcrumb list
flatten into adjacent <li> elements, and the {" "}(Min. order …) text run gets the
<!-- --> separators React emits between adjacent text nodes. A null promo removes the
whole section.
renderToString on the benchmark’s 500 check inputs:
500/500 byte-identical, 0 fallbacks. This component alone renders
9.4× faster than renderToString (1.2× including the per-request
schema check).function App(props) {
var data = props.data;
return (
<LocaleContext.Provider value={data.locale}>
<AnnouncementBanner promo={data.promo} />
<Breadcrumbs breadcrumbs={data.breadcrumbs} />
</LocaleContext.Provider>
);
}
function AnnouncementBanner(props) {
var locale = React.useContext(LocaleContext);
var promo = props.promo;
if (!promo) {
return null;
}
return (
<section
className={cx("ns-promo", { "ns-promo-urgent": Boolean(promo.urgent) })}
hidden={!promo.active}
data-campaign={promo.code}
>
<span
className="ns-promo-html"
dangerouslySetInnerHTML={{ __html: promo.html }}
/>
{promo.minOrder !== undefined && promo.minOrder > 0 ? (
<span className="ns-promo-min">
{" "}
(Min. order {formatMoney(promo.minOrder, locale)})
</span>
) : null}
<code className="ns-promo-code">{promo.code}</code>
</section>
);
}
function Breadcrumbs(props) {
var crumbs = props.breadcrumbs;
if (!crumbs || crumbs.length === 0) {
return null;
}
return (
<nav className="ns-crumbs" aria-label="Breadcrumb">
<ol className="ns-crumb-list">
{crumbs.map(function (crumb, idx) {
var isLast = idx === crumbs.length - 1;
return (
<React.Fragment key={crumb.href + ":" + idx}>
{idx > 0 ? (
<li className="ns-crumb-sep" aria-hidden="true">
/
</li>
) : null}
<li className={cx("ns-crumb-item", { "is-current": isLast })}>
{isLast ? (
<span aria-current="page">{crumb.label}</span>
) : (
<a href={crumb.href}>{crumb.label}</a>
)}
</li>
</React.Fragment>
);
})}
</ol>
</nav>
);
}var _2z = function (_$m, _$n) {
"use strict";
var _$o = _2r.length;
var _$p = _$m.href;
var _34 = _$o - 1;
if (_34 === _$n) {
var _$q = _$m.label;
} else {
var _$r = _$m.href;
var _$s = _$m.label;
}
var _3P = _34 !== _$n;
return (_$n <= 0 ? "" : "<li class=\"ns-crumb-sep\" aria-hidden=\"true\">/</li>") + "<li class" + (_3P ? "=\"ns-crumb-item\"" : "=\"ns-crumb-item is-current\"") + ">" + (_3P ? "<a href=\"" + _2q(_$r) + "\">" + _2q(_$s) + "</a>" : "<span aria-current=\"page\">" + _2q(_$q) + "</span>") + "</li>";
};var _$0 = data.locale;
var _$1 = data.promo;
var _$2 = data.breadcrumbs;
if (_$1 !== null) {
var _8 = _$1;
var _$3 = _8.urgent;
var _$4 = _$u.Boolean(_$3);
var _$5 = _8.active;
var _$6 = _8.code;
var _$7 = _8.html;
var _$8 = _8.minOrder; var _$h = _8.code;
}
var _2r = _$2;
var _$i = _2r.length;
var _17 = 0 !== _$i;
if (_17) {
var _$l = __ssrMapJoin(_2r, _2z);
var _$t = _$l;
}
var _2C = !_v;
var _1z = _2q(_$g);
var _27 = _2q(_$f);
return (_$1 === null ? "" : "<section class" + (!_$4 ? "=\"ns-promo\"" : "=\"ns-promo ns-promo-urgent\"") + (_$5 ? "" : " hidden=\"\"") + " data-campaign=\"" + _2q(_$6) + "\"><span class=\"ns-promo-html\">" + _$7 + "</span>" + (void 0 === _$8 || void 0 === _$9 || !_M ? "" : "<span class=\"ns-promo-min\"> <!-- -->(Min. order " + (_d || !(_b < 0) || !(_$J > 0) ? _2C ? "" !== _$g ? "<!-- -->" + _1z : "" : "" !== _$f ? "<!-- -->" + _27 : "" : "<!-- -->-" + (_2C ? _1z : _27)) + "<!-- -->)</span>") + "<code class=\"ns-promo-code\">" + _2q(_$h) + "</code></section>") + (0 === _$i ? "" : "<nav class=\"ns-crumbs\" aria-label=\"Breadcrumb\"><ol class=\"ns-crumb-list\">" + _$t + "</ol></nav>");
};The per-component timings are in-process, warm, on a shared machine, and the schema check validates the whole
request on every call, which dominates for one small component. The page-level numbers below are the meaningful
ones. On the full page, over the benchmark’s fixed 500-input check corpus, the output is byte-identical to
ReactDOMServer.renderToString (re-checked for this article: 500/500). It renders 4–5.5× faster than React
with the JIT and about 9–10× faster with --jitless, within 1.4–2× of a page specialized by hand. Data
that doesn’t match the inferred schema falls back to React.
Renders per second relative to ReactDOMServer.renderToString, same machine, same run
(react-bench/perf.js, November 5, 2026). Small pages are about 6 KB of HTML, large ones about 285 KB.
| JIT, small | JIT, large | jitless, small | jitless, large | |
|---|---|---|---|---|
React renderToString | 1.00× | 1.00× | 1.00× | 1.00× |
| Revived Prepack | 5.39× | 4.10× | 8.96× | 10.08× |
| Hand-written specialization | 8.42× | 6.51× | 18.53× | 17.45× |
The generated program is 83.8 KB (20.6 KB gzipped) and doesn’t load React at all. The Northstar Supply demo serves the same page rendered by plain React and by the specialized code.
What still breaks
Two app shapes still produce broken output; both turned up while building the Northstar store. One is a list reached
through an object that may be null (listing.products, where listing is either
null or an object). The other is a loop and a .map over the same array inside one
conditional branch. In both cases Prepack doesn’t recognize the array as a list of unknown length, falls back to a
generic path that is broken, and the build still succeeds. The generated code contains a residual
__bailout call and throws on the requests that render the list. It never returns wrong HTML, but the
compiler should refuse instead of shipping code that throws. The Northstar store works around both by keeping its
collections at the top level of the data and computing listing stats outside the view branch.
site/scripts/run-react-examples.js)._4Z is not a function); 66 render correctlyfunction Grid(props) {
return <ul className="grid">{props.products.map(function (p) {
return <li key={p.sku}>{p.title}{p.stock > 0 ? " in stock" : null}</li>;
})}</ul>;
}
function Listing(props) {
var inStock = React.useMemo(function () {
var n = 0;
for (var i = 0; i < props.listing.products.length; i++) if (props.listing.products[i].stock > 0) n++;
return n;
}, [props.listing]);
return <section><h1>{props.listing.title}</h1><p>{inStock} in stock</p><Grid products={props.listing.products} /></section>;
}
function App(props) {
var listing = props.data.listing; // null, or { title, products: [...] }
return <main>{listing ? <Listing listing={listing} /> : <p>No listing</p>}</main>;
}_3S is not a function); 104 render correctlyfunction Grid(props) {
return <ul className="grid">{props.products.map(function (p) {
return <li key={p.sku}>{p.title}</li>;
})}</ul>;
}
function ListingPage(props) {
var inStock = React.useMemo(function () {
var n = 0;
for (var i = 0; i < props.products.length; i++) if (props.products[i].stock > 0) n++;
return n;
}, [props.products]);
return <section><p>{inStock} in stock</p><Grid products={props.products} /></section>;
}
function App(props) {
var data = props.data;
return <main>{data.view === "listing" ? <ListingPage products={data.products} /> : <p>Home</p>}</main>;
}Part 3
What we learned
What is solved, what isn’t, and when the revived Prepack is the right tool.
Most of the problems that stopped Prepack turned out to be solvable, in the sense that matters: the cases from the tracker now compile to correct, compact code, and the ones that don’t stop with a diagnostic instead of shipping something wrong. None of them was solved by one clever idea. Each was a few thousand lines of careful work, checked by a corpus, a fuzzer and a solver.
| Problem | Verdict | What that means |
|---|---|---|
| P1 Loops | Solved for local effects | Residual loops with a one-iteration soundness check. Loops whose effects reach global or outer state still stop with a diagnostic: summarizing those needs whole-program reasoning, and loop behavior is undecidable in general. |
| P2 Recursion | Solved | Residual calls once recursion depends on unknown values, with effect checks; refused (PP0050) when it can’t be proven safe. |
| P3 Joins | Solved | Every known miscompile fixed; switch and residual try/catch implemented. The completion design is still fragile; a simpler representation is sketched. |
| P4 Path conditions | Solved with Z3 | Solver-backed implications, a rewrite audit with zero failures, and documented limits (no IEEE-754 rounding, opaque string conversions). |
| P5 Numeric facts | Solved for small ranges | Interval domain plus case splitting up to 8 values; wide or unbounded lengths still need relational summaries. |
| P6 Havoc | Mostly | 81 built-in summaries and the outer fixed point. Aliasing between unknown parameters and untyped receivers remain conservative. |
| P7 Environment | Solved, by making it explicit | Open-world globals, generated models, guarded assumptions; zero silent miscompiles on 22 libraries × 6 profiles. |
| P8 Size | Solved on the corpus | Cost model and peepholes; no case above 2×. The UMD three-way split can still double a library when no target is given. |
| P9 Generators and async | By choice, partly | Generators finished at init and settled Promises work; a generator suspended at the end of init is refused rather than approximated. |
What isn’t solved
Loops and recursion are undecidable in general, and the revived Prepack doesn’t pretend otherwise. It handles the shapes whose effects it can bound and refuses the rest. Modern libraries still expose gaps in its 35,000-line spec implementation at a steady rate, and compile times on large inputs are long (279 seconds for @babel/standalone). Without a target environment, a UMD bundle is evaluated once per module system, which can double the output. And the size problem is only solved in the sense Markbåge meant if output never grows: on our corpus it doesn’t, but a compiler that evaluates code will always be able to produce more code than it was given. The cost model only decides when that is worth it.
When it is the right tool
Specializing on unknown data
A renderer for a known app, an optimized function for unknown arguments, an init path that depends on an environment you can describe. This is where symbolic evaluation earns its keep, and where the revived Prepack is strongest.
Init-heavy bundles with a code cache
Libraries that compute tables, parse locale data or register plugins at load start several times faster from V8’s code cache (moment 4.3×, handlebars 3.1×), usually at about the original size.
Not when the engine can do it
Node’s and V8’s own startup snapshots remain the cheapest way to skip initialization where they’re available, and they were the first thing our initial research found still alive from Prepack’s idea.
Herman Venter wrote in 2018 that getting Prepack to run on real browser applications would take “a lot of very hard work and a long time,” and that shortcuts would produce something that works “only for some ill defined subset of JavaScript.” Both halves held up. What made the hard work tractable this time was checking it: a corpus built from the tracker’s own hard cases, a differential fuzzer, and a solver that audits every rewrite. The final count is 190 of 194 hard cases, with zero silently wrong.
Links
- github.com/jlongster/prepack-revived: the code, the corpus
(
test/serializer/hard/), the design notes (docs/revive/) and this site (site/). The repository is private for now. - Northstar Supply: the React specialization running live (source).
- facebookarchive/prepack: the original, archived.
- #2639 “Is prepack dead?”: Herman Venter’s post-mortem.
Appendix
Methods and the full scoreboard
How every number and output on this page was produced, and all 194 hard cases.
How this page was made
Every code panel shows the unedited output of a real tool run, produced by
site/scripts/run-examples.js from the inputs in site/examples/. Long outputs are trimmed only
where a panel says so, and long lines in the two React panels wrap; the Prepack fragment there also has line breaks
added at ? and :. The script then runs the original program and each output in a fresh Node process and
evaluates the expression shown in the “Run” table, so every “differs” mark is measured.
- Prepack 0.2.54: the last published release (October 8, 2018), from npm:
node node_modules/prepack/lib/prepack-cli.js input.js. - Revived Prepack: the project repository, built with
yarn build:node lib/prepack-cli.js input.js, Z3 4.16 installed (used by default). - Everything ran on Node v26.7.0 on Linux x64, on November 5, 2026. Prepack runs with
TZ=UTC. - The corpus results below come from
node scripts/hard-scoreboard.jsrun against each build on the same 194 programs. The serializer suite count (3,744 of 3,744) is fromyarn test-serializeron the same build. - The React component examples come from
site/scripts/run-react-examples.js: it builds each small app from components of the react-bench page (app.jsx), compiles it withreact-ssr/compiler.json the benchmark’s 440 training inputs, compares it withrenderToStringon the 500 check inputs, and times both warm, in one process (median of 7 rounds). The two failing shapes are compiled and run on seeded synthetic data by the same script. - Quotes were copied from the GitHub and Hacker News APIs; each links to the specific comment, issue, gist or
source line. The fuzzing, Z3 and size statistics come from the design notes in
docs/revive/.
All 194 hard cases
Folder names give the problem (P1–P9), fuzz/ holds minimized fuzzer findings, and the
real-world folders hold library excerpts. Hover a status for the scoreboard’s detail; the last column is
0.2.54’s first diagnostic or mismatch.
Show the table (194 rows)
| Case | 0.2.54 | Revived | 0.2.54 detail |
|---|---|---|---|
P1-loops/collatz-data-dependent.js | Error | Pass | FatalError PP0001: |
P1-loops/do-while-build-object.js | Wrong | Pass | expected "[{\"first\":true,\"k0\":0,\"count\":1},{\"first\":true,\"k0\":0,\"k1\":1,\"k2\":4,\"count\":3},{\"first\":true,\"k0\":0,\"k1\":1,\"k2\":4,\"k3\":9,\"k |
P1-loops/for-in-unknown-object.js | Error | Pass | FatalError PP0013: for in loops over unknown objects are not yet supported |
P1-loops/gcd-while-two-vars.js | Error | Pass | FatalError PP0001: |
P1-loops/issue-1174-global-unknown-bound.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value (7) |
P1-loops/issue-1174-unknown-bound-counter.js | Error | Pass | FatalError PP0001: |
P1-loops/issue-1722-state-before-loop.js | Wrong | Pass | expected "{\"0\":\"a\",\"1\":\"b\",\"2\":\"x\",\"3\":\"x\",\"4\":\"x\"} i=5" got "{\"3\":\"x\",\"4\":\"x\",\"5\":\"x\"} i=5" |
P1-loops/issue-1959-shallow-equal.js | Error | Pass | FatalError PP0001: ; FatalError PP0037: failed to recover from a for/while loop bail-out due to unsupported logic in loop body |
P1-loops/issue-2053-babel-for-of.js | Error | Pass | FatalError PP0001: ; FatalError PP0035: break, throw or return cannot be guarded by abstract condition |
P1-loops/issue-2055-while-string-search.js | Error | Pass | FatalError PP0001: |
P1-loops/issue-2396-while-pure.js | Error | Pass | FatalError PP0001: |
P1-loops/issue-2396-while-return.js | Error | Pass | FatalError PP0001: |
P1-loops/issue-2477-babel-for-of-switch-return.js | Error | Pass | FatalError PP0001: (x2); FatalError PP0037: failed to recover from a for/while loop bail-out due to unsupported logic in loop body |
P1-loops/labeled-continue-nested.js | Error | Pass | FatalError PP0001: ; FatalError PP0037: failed to recover from a for/while loop bail-out due to unsupported logic in loop body |
P1-loops/loop-break-in-optimize.js | Error | Pass | FatalError PP0001: |
P1-loops/loop-return-in-optimize.js | Error | Pass | FatalError PP0001: ; FatalError PP0037: failed to recover from a for/while loop bail-out due to unsupported logic in loop body |
P1-loops/loop-string-accumulator.js | Error | Pass | FatalError PP0001: |
P1-loops/loop-throw-in-optimize.js | Error | Pass | FatalError PP0001: ; FatalError PP0037: failed to recover from a for/while loop bail-out due to unsupported logic in loop body |
P1-loops/loop-writes-known-object-unknown-keys.js | Error | Pass | FatalError PP0001: |
P1-loops/nested-loops-unknown-bounds.js | Error | Pass | FatalError PP0001: |
P1-loops/while-string-trim-global.js | Error | Pass | RecoverableError PP0030: unknown property access might need to invoke a getter; FatalError PP0001: This operation is not yet supported on abstract values ("hell |
P2-recursion/concrete-recursion-under-abstract-cond.js | Pass | Pass | |
P2-recursion/deep-clone.js | Error | Pass | FatalError PP0001: |
P2-recursion/descent-parser.js | Error | Pass | FatalError PP0045: Maximum stack depth exceeded (x2); FatalError PP0001: (x74) |
P2-recursion/factorial-under-condition.js | Error | Pass | FatalError PP0045: Maximum stack depth exceeded |
P2-recursion/fib-optimized.js | Error | Pass | timeout after 30000ms |
P2-recursion/flatten-recursive.js | Error | Pass | FatalError PP0001: |
P2-recursion/gcd-recursive.js | Error | Pass | timeout after 30000ms |
P2-recursion/issue-1586-fibonacci-abstract.js | Error | Pass | FatalError PP0045: Maximum stack depth exceeded |
P2-recursion/issue-543-fib-date-now.js | Pass | Pass | |
P2-recursion/mutual-recursion-even-odd.js | Error | Pass | FatalError PP0045: Maximum stack depth exceeded |
P2-recursion/recursive-path-lookup.js | Error | Pass | FatalError PP0045: Maximum stack depth exceeded |
P2-recursion/tree-depth.js | Error | Pass | FatalError PP0001: |
P2-recursion/tree-walk-sum.js | Error | Pass | timeout after 30000ms |
P3-joins/catch-rethrow-abstract.js | Error | Pass | RecoverableError PP0021: Possible throw inside try/catch is not yet supported (x2) |
P3-joins/fuzz-nested-conditional-assign-return.js | Pass | Pass | |
P3-joins/fuzz-returns-counters.js | Pass | Pass | |
P3-joins/global-conditional-throw-then-work.js | Too large | Pass | output 1169B is 3.39x input 345B (max 2x) |
P3-joins/issue-1378-switch-abstract.js | Pass | Pass | |
P3-joins/issue-1378-switch-fallthrough.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value |
P3-joins/issue-1869-duplicate-console-log.js | Pass | Pass | |
P3-joins/issue-2120-havoced-object-access-throws.js | Wrong | Pass | expected "7,42,42" got "Error: no" |
P3-joins/issue-2120-try-catch-abstract-call.js | Error | Pass | RecoverableError PP0021: Possible throw inside try/catch is not yet supported |
P3-joins/issue-2354-multiple-returns.js | Wrong | Pass | expected "false,true,false,true,false,true,false,false|falsetrue" got "false,true,false,true,false,true,false,false|NaNx" |
P3-joins/issue-2363-possibly-normal-completion.js | Pass | Pass | |
P3-joins/issue-2363-with-values.js | Pass | Pass | |
P3-joins/issue-2561-throw-before-constructor.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value (true) |
P3-joins/labeled-block-break-global.js | Pass | Pass | |
P3-joins/labeled-block-break.js | Wrong | Pass | expected "start,start-1,start-1-2" got "start-1-2,start-1-2,start-1-2" |
P3-joins/nested-returns-side-effects.js | Error | Pass | FatalError PP0001: |
P3-joins/return-in-nested-function-conditional-objects.js | Wrong | Pass | expected "[{\"kind\":\"A\"},{\"kind\":\"A\",\"touched\":true},{\"kind\":\"B\",\"touched\":true,\"extra\":1},{\"kind\":\"B\",\"touched\":true}]" got "[{\"kind\": |
P3-joins/short-circuit-abstract-calls.js | Pass | Pass | |
P3-joins/switch-global-abstract.js | Pass | Pass | |
P3-joins/throw-in-constructor.js | Error | Pass | RecoverableError PP0021: Possible throw inside try/catch is not yet supported (x6) |
P3-joins/try-finally-return-abstract.js | Error | Pass | RecoverableError PP0021: Possible throw inside try/catch is not yet supported (x2) |
P4-paths/comparison-chain-numbers.js | Too large | Pass | output 505B is 2.03x input 249B (max 2x) |
P4-paths/de-morgan-mixed-types.js | Pass | Pass | |
P4-paths/equality-refinement.js | Pass | Pass | |
P4-paths/infeasible-branch-pruning.js | Too large | Pass | contains "unreachable" |
P4-paths/issue-2327-simplify-binary.js | Pass | Pass | |
P4-paths/issue-2355-truthiness-return.js | Wrong | Pass | expected "true,false,false,true,false,false,false,true" got "1,0,,x,,,NaN,[object Object]" |
P4-paths/issue-2361-pushing-false.js | Pass | Pass | |
P4-paths/issue-2563-undefined-not-null.js | Pass | Pass | |
P4-paths/issue-2564-null-not-undefined.js | Pass | Pass | |
P4-paths/nested-ternary-falsy-values.js | Error | Pass | FatalError PP0001: |
P4-paths/null-check-then-property.js | Pass | Pass | |
P4-paths/repeated-condition-reuse.js | Error | Pass | FatalError PP0001: |
P4-paths/truthiness-ternary-mixed.js | Error | Pass | FatalError PP0001: |
P4-paths/typeof-refinement.js | Too large | Pass | output 811B is 2.08x input 389B (max 2x) |
P5-ranges/clamp-then-loop.js | Error | Pass | FatalError PP0001: |
P5-ranges/conditional-index-write.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value |
P5-ranges/conditional-pop-length-arith.js | Error | Pass | FatalError PP0001: |
P5-ranges/conditional-push-global-many-unique-names.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value (true) |
P5-ranges/conditional-push-global-many.js | Error | Error | FatalError PP0019: An abstract value with the same name exists |
P5-ranges/conditional-push-last-element.js | Error | Pass | FatalError PP0001: |
P5-ranges/conditional-push-optimized.js | Error | Pass | FatalError PP0001: |
P5-ranges/issue-1066-conditional-push.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value (true) |
P5-ranges/masked-index-lookup.js | Too large | Pass | output 434B is 2.54x input 171B (max 2x) |
P5-ranges/string-length-range.js | Pass | Pass | |
P6-havoc/abstract-fn-leaks-global-object.js | Error | Error | RecoverableError PP0017: Unfrozen object leaked before end of global code |
P6-havoc/aliasing-two-params.js | Pass | Pass | |
P6-havoc/array-filter-callback-mutates-local.js | Pass | Pass | |
P6-havoc/getter-on-abstract-object.js | Pass | Pass | |
P6-havoc/issue-1750-lazy-cache.js | Wrong | Pass | expected "a1,a2,b1,a3" got "a1,a1,b1,a1" |
P6-havoc/issue-1750-lazy-singleton.js | Wrong | Pass | expected "true:1:1" got "false:1:1" |
P6-havoc/issue-2011-leaked-binding.js | Pass | Pass | |
P6-havoc/issue-2012-havoc-binding-capture.js | Too large | Pass | output 730B is 2.53x input 289B (max 2x) |
P6-havoc/issue-2179-topval-alias.js | Pass | Pass | |
P6-havoc/issue-2226-setter-on-prototype.js | Too large | Pass | output 748B is 2.8x input 267B (max 2x) |
P6-havoc/issue-2319-indexof.js | Pass | Pass | |
P6-havoc/issue-2319-json-stringify-local.js | Error | Pass | FatalError PP0001: |
P6-havoc/issue-2319-math-abs.js | Error | Pass | FatalError PP0001: (x7) |
P6-havoc/issue-2319-parseint.js | Error | Pass | FatalError PP0001: |
P6-havoc/issue-2319-regexp-test.js | Error | Pass | FatalError PP0001: |
P6-havoc/issue-977-read-write-conflict.js | Pass | Pass | |
P6-havoc/object-keys-loop-over-param.js | Error | Pass | FatalError PP0001: |
P6-havoc/object-passed-to-unknown-fn.js | Pass | Pass | |
P6-havoc/unknown-fn-mutates-captured-binding.js | Pass | Pass | |
P7-environment/date-local-timezone.js | Wrong | Pass | expected "1577817000000,-330" got "1577836800000,0" |
P7-environment/date-now-typeof.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value |
P7-environment/dom-build-tree-at-init.js | Error | Pass | FatalError PP0012: member expression object abstract value document is unknown |
P7-environment/dom-get-element-by-id.js | Error | Pass | FatalError PP0012: member expression object abstract value document is unknown |
P7-environment/guarded-speculation-fallback.js | Wrong | Pass | expected "1577817000000,-330" got "1577836800000,0" |
P7-environment/guarded-speculation-holds.js | Wrong | Pass | expected "prod,1577817000000,-330" got "ReferenceError: process is not defined" |
P7-environment/intl-number-format.js | Wrong | Pass | expected "1.234,5" got "n/a" |
P7-environment/local-storage-init.js | Wrong | Pass | expected "5,0" got "ReferenceError: localStorage is not defined" |
P7-environment/math-random-init.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value |
P7-environment/navigator-user-agent.js | Wrong | Pass | expected "{\"isMac\":true,\"isWindows\":false,\"lang\":\"en\"}Cmd" got "ReferenceError: navigator is not defined" |
P7-environment/node-require-module.js | Wrong | Pass | expected "src/lib/a/b" got "ReferenceError: require is not defined" |
P7-environment/number-to-locale-string.js | Error | Pass | FatalError: TODO #1005: implement Date.prototype.toLocaleDateString |
P7-environment/process-env-branch.js | Wrong | Pass | expected "{\"minify\":true,\"logLevel\":\"debug\"}" got "ReferenceError: process is not defined" |
P7-environment/react-dom-vendor-prefix-init.js | Error | Pass | FatalError PP0012: member expression object abstract value document is unknown |
P7-environment/request-animation-frame.js | Wrong | Pass | expected "frame@1620px" got "ReferenceError: requestAnimationFrame is not defined" |
P7-environment/timers-set-interval-count.js | Pass | Pass | |
P7-environment/timers-set-timeout-order.js | Pass | Pass | |
P7-environment/umd-three-way-probe.js | Wrong | Pass | expected "sum=60" got "TypeError: module.exports.format is not a function" |
P7-environment/window-global-alias.js | Wrong | Pass | expected "api:true" got "api:false" |
P8-size/crc32-table.js | Too large | Pass | output 3053B is 9.25x input 330B (max 2x) |
P8-size/issue-2011-binding-writes.js | Too large | Pass | output 326B is 1.83x input 178B (max 1.5x) |
P8-size/issue-2140-inlining-explosion.js | Error | Pass | RecoverableError PP0008: might be a symbol or an object with an unknown valueOf or toString or Symbol.toPrimitive method (x8); FatalError PP0001: |
P8-size/issue-2220-empty-if.js | Too large | Pass | contains "{}" |
P8-size/issue-2286-cse-effectful.js | Too large | Pass | output 236B is 1.58x input 149B (max 1.5x) |
P8-size/repeated-defaults-objects.js | Too large | Pass | output 1835B is 6.33x input 290B (max 2x) |
P8-size/residual-closure-drags-code.js | Pass | Pass | |
P8-size/string-builder-unrolled.js | Too large | Pass | output 769B is 5.57x input 138B (max 2x) |
P8-size/unrolled-table-1000.js | Too large | Pass | output 12105B is 70.79x input 171B (max 2x) |
P9-generators-async/async-function-defined-at-init.js | Wrong | Pass | expected "object,function,object,function" got "number,undefined,number,undefined" |
P9-generators-async/generator-function-defined-not-called.js | Wrong | Pass | expected "0,1,2" got "SyntaxError: Unexpected identifier 'i'" |
P9-generators-async/generator-suspended-at-init.js | Error | Error | Invariant Violation: |
P9-generators-async/promise-then-at-init.js | Wrong | Pass | expected "2,then-ok" got "pending,then-throws" |
fuzz/array-abstract-index-set-length.js | Wrong | Pass | expected "1,2,5,1" got "1,1,1,1" |
fuzz/call-after-nested-returns.js | Wrong | Pass | expected "0,0,0,0,undefined,undefined,undefined,0" got "0,0,0,0,0,0,undefined,0" |
fuzz/composed-saved-completion-labeled-break.js | Wrong | Pass | expected "done,0,throw:RangeError,throw:TypeError,done" got "throw:RangeError,0,throw:RangeError,throw:TypeError,done" |
fuzz/conditional-call-base-nullish.js | Wrong | Pass | expected "42,throw:TypeError,throw:TypeError" got "42,null,undefined" |
fuzz/conditional-inner-throw-outer-alternate.js | Wrong | Pass | expected "outer,outer,outer,outer,0,0,throw:boom" got "0,0,0,0,0,0,throw:boom" |
fuzz/conditional-nested-temporal-scope.js | Error | Pass | FatalError PP0001: |
fuzz/conditional-null-check-negation.js | Wrong | Pass | expected "true,false,true,true,true,false,true,true|true,false,true,true,true,false,true,true" got "false,false,true,true,false,false,false,false|false,false,tr |
fuzz/conditional-push-while-join-mismatch.js | Error | Pass | Invariant Violation: serialized 21 of 23 |
fuzz/conditional-unknown-write-empty-to-undefined.js | Wrong | Pass | expected "[[[\"p1\"],true,\"undefined\"],[[\"length\",\"p1\"],true,\"undefined\"],[[\"q\",\"p1\"],true,\"undefined\"]]" got "[[[\"p1\"],true,\"undefined\"],[[\" |
fuzz/dead-if-hoisted-var.js | Wrong | Pass | expected "[\"undefined\",null,null]" got "ReferenceError: x is not defined" |
fuzz/delete-abstract-coercible-base.js | Error | Pass | Invariant Violation: |
fuzz/do-while-break-abrupt-widen.js | Error | Pass | Invariant Violation: if a loop iteration ends abruptly, there is no need for fixed point computation |
fuzz/do-while-emitter-active-generator.js | Error | Pass | Invariant Violation: |
fuzz/do-while-false-abrupt.js | Wrong | Pass | expected "throw:body,early,throw:boom,no-throw,late,test-ok,throw:body,early,throw:boom,no-throw,late,test-ok" got "no-throw,late,test-ok,no-throw,late,test-ok, |
fuzz/do-while-object-emitter.js | Error | Pass | Invariant Violation: |
fuzz/do-while-object-hoisted-param.js | Wrong | Pass | expected "42" got "ReferenceError: a is not defined" |
fuzz/for-in-continue-return-join.js | Wrong | Pass | expected "0,3,0,0,2" got "0,0,0,0,0" |
fuzz/infeasible-path-explored-crash.js | Error | Pass | Error: Infeasible path explored |
fuzz/inlining-probe-nullish-arg.js | Error | Pass | FatalError PP0001: (x2) |
fuzz/labeled-break-before-throw-call.js | Error | Error | Invariant Violation: |
fuzz/labeled-break-skips-code-after-block.js | Wrong | Pass | expected "called:true,called:false,called:1,called:0" got "undefined,called:false,undefined,called:0" |
fuzz/leaked-binding-declared-across-passes.js | Error | Pass | RecoverableError PP0008: might be a symbol or an object with an unknown valueOf or toString or Symbol.toPrimitive method (x2); FatalError PP0001: |
fuzz/leaked-object-in-operator.js | Error | Pass | FatalError PP0001: |
fuzz/loop-null-check-path-condition.js | Error | Pass | Invariant Violation: assuming that false equals true is asking for trouble |
fuzz/map-cases-path-condition-leak.js | Error | Pass | FatalError PP0001: ; FatalError PP0037: failed to recover from a for/while loop bail-out due to unsupported logic in loop body |
fuzz/merged-join-uses-earlier-declared-value.js | Error | Pass | FatalError PP0001: |
fuzz/nested-try-leaked-object-mutation.js | Error | Pass | RecoverableError PP0021: Possible throw inside try/catch is not yet supported (x2) |
fuzz/not-nullish-falsy-logical.js | Wrong | Pass | expected "nullish,nullish,0,,false,NaN,true" got "nullish,nullish,true,true,true,true,true" |
fuzz/path-implies-both-true-visitor.js | Error | Pass | Invariant Violation: |
fuzz/primitive-receiver-strict-set.js | Wrong | Pass | expected "throw:TypeError,throw:TypeError,throw:TypeError,throw:RangeError,ok,ok,throw:RangeError|log=2" got "ok,ok,throw:TypeError,throw:RangeError,ok,throw:Ty |
fuzz/pure-unary-coercion-throws.js | Error | Pass | RecoverableError PP0008: might be a symbol or an object with an unknown valueOf or toString or Symbol.toPrimitive method (x2) |
fuzz/residual-loop-closure-mutated-param.js | Wrong | Pass | expected "throw:TypeError,NaN" got "throw:TypeError,abc1" |
fuzz/switch-empty-nan-discriminant.js | Error | Pass | Invariant Violation: |
fuzz/switch-hoisted-var-loop.js | Error | Pass | Invariant Violation: |
fuzz/switch-nested-abrupt-default.js | Error | Pass | Invariant Violation: |
fuzz/throw-before-try-catch-finally.js | Wrong | Pass | expected "throw:before,ok,throw:before,ok,finally,throw:before,swallowed" got "caught:before,ok,throw:before,ok,finally+finally,swallowed,swallowed" |
fuzz/throw-in-call-argument-join.js | Error | Pass | FatalError PP0001: |
fuzz/unknown-key-write-property-order.js | Error | Pass | FatalError PP0001: (x2) |
fuzz/unknown-property-concrete-get-partial.js | Error | Pass | Invariant Violation: |
fuzz/unknown-property-concrete-get.js | Error | Pass | Invariant Violation: |
fuzz/unused-in-non-object-throws.js | Wrong | Pass | expected "throw:TypeError,throw:TypeError,throw:TypeError,1,1" got "1,1,1,1,1" |
fuzz/unused-property-read-throws.js | Wrong | Pass | expected "throw:TypeError,throw:TypeError,1,1,1,1,throw:getter|getterRan=1" got "1,1,1,1,1,1,1|getterRan=0" |
fuzz/update-expression-string.js | Wrong | Pass | expected "1,1,NaN,1,NaN,2" got "1,01,a1,1,NaN,2" |
fuzz/widen-nested-loop-in-helper.js | Error | Pass | Invariant Violation: |
real-world/babel-class-hierarchy.js | Pass | Pass | |
real-world/config-parser-concrete.js | Pass | Pass | |
real-world/config-parser-env-override.js | Error | Pass | FatalError PP0031: property access on unknown object |
real-world/event-emitter-init.js | Pass | Pass | |
real-world/i18n-plural-rules.js | Pass | Pass | |
real-world/lodash-init.js | Pass | Pass | |
real-world/lookup-table-char-classes.js | Error | Pass | FatalError PP0001: |
real-world/module-system.js | Too large | Pass | output 2207B is 2.04x input 1080B (max 2x) |
real-world/react-like-render.js | Error | Pass | FatalError PP0001: (x4) |
real-world/template-compiler.js | Pass | Pass | |
real-world/template-render-optimized.js | Pass | Pass | |
real-world/url-router.js | Error | Pass | FatalError PP0001: (x5) |
real-world-libs/crc32-sequential-loop-leaked-typedarray.js | Pass | Pass | |
real-world-libs/d3-internmap-class-extends-map.js | Error | Pass | Invariant Violation: serialized 7 of 8 |
real-world-libs/d3-scale-default-param-factory.js | Error | Pass | FatalError: TODO: do not know how to deal with non-Identifier parameters |
real-world-libs/handlebars-globalthis-definegetter.js | Wrong | Pass | expected "4.7.10:a&<b>" got "TypeError: not an object" |
real-world-libs/marked-class-static-getter.js | Error | Pass | FatalError PP0022: a class method incorrectly went through the serializeValue() code path |
real-world-libs/marked-optional-catch-binding.js | Pass | Pass | |
real-world-libs/redux-random-split-join.js | Error | Pass | FatalError PP0001: This operation is not yet supported on abstract value |
real-world-libs/underscore-document-childnodes.js | Error | Pass | FatalError PP0001: This operation is not yet supported on document at childNodes |