100. What Viscosity refuses to touch
The most common thing Viscosity says is no. Every no comes with a reason, and every reason is a small lesson in how a program can notice where its data lives.
- Define a bailout, and tell a safety bailout apart from a decision that a change wouldn't pay off.
- Explain, for each refusal in the differential corpus, what the program could observe if Viscosity went ahead.
- Change a refused program so that Viscosity can prove it safe, without changing what it prints.
- Contrast Viscosity's compile-time bailouts with an engine's run-time deoptimization.
Saying no
Viscosity’s differential corpus is a folder of small programs, each run
twice, once as written and once compiled, and the two outputs must
match. Each file’s first line, such as // viscosity: ps=aos, also
says which layout Viscosity must pick. Nearly half of them are there to make Viscosity refuse. A wrong
refusal costs some speed. A wrong acceptance changes what a program
prints. So the refusals are tested as carefully as the transformations.
A bank teller who can’t verify a signature doesn’t cash the cheque. It isn’t an accusation: the cheque may be perfectly good. The teller just can’t prove it, and the cost of being wrong falls on someone else.
A bailout is leaving a collection unchanged because its safety can’t
be proved. In Viscosity it is a reason string in the collection’s
bailouts list, and viscosity explain prints it after unsafe:.
A bailout says nothing is known to be wrong; only that something might
be.
Step through six refusals from the corpus. Each traces a chain of uses from the collection to the first use Viscosity can’t rewrite:
The rest of this chapter takes them one lesson at a time, with the real explain output (wrapped to fit).
Lesson 1: two names, one object
From aliasing_identity.js:
const keep = [];
for (let i = 0; i < ps.length; i++)
keep.push(ps[i]);
keep[0].x = 42;
console.log(ps[0].x, keep[0] === ps[0]);
ps (line 2) {x} → kept as array of
objects
· unsafe: line 7: an element is
compared or combined with "==="
· unsafe: line 5: an element is used
by push
It prints 42 true: one object, two names (chapter 4). In a struct of
arrays an element is a row number, not an object. To give keep an
element, Viscosity would have to build one, and a built object is a
copy. Writes to it are lost, and === compares identity, so a fresh
copy is never the same object[1]:
// Columns, and a way to build an
// element object on demand.
const soa = { x: [0, 1, 2] };
const get = (i) => ({ x: soa.x[i] });
const keep = [get(0)];
keep[0].x = 42;
console.log(soa.x[0]);
console.log(keep[0] === get(0));
// With real objects these would
// print 42 and true.Chapter 106 looks at what it would take to keep identity anyway. Here, Viscosity’s answer is to refuse.
Lesson 2: a name that outlives the line
From closure_alias.js:
const p = ps[3];
const f = () => p.x;
for (let i = 0; i < ps.length; i++)
ps[i].x += 1;
console.log(f());
· unsafe: line 4: an element is stored
in another variable (p)
f() must print 4, the value after the loop. Only a real object
shared by p and ps[3] gives that. Viscosity doesn’t try to follow
p into the closure; storing an element under another name is enough
to stop it. There is an easy fix, though: keep the index instead of
the element. Try it: replace the first two lines with const k = 3; and
const f = () => ps[k].x;.
Lesson 3: code that can’t be seen
From escapes_log.js and map_method.js:
// console.log(ps[3]);
· unsafe: line 5: an element is passed
to a method call
// ps.map((p) => p.x).slice(0, 3)
· unsafe: line 5: uses .map on the
array (only .length and .push are
supported)
· unsafe: the array is passed to a
method call at line 5
console.log prints the object, { x: 4, y: 3 }, keys and all. map
calls a function with every element and collects the results. Neither
can be rewritten into columns without building objects, and in general a
function Viscosity can’t see into might do anything with what it’s
given. Unknown code counts as code that observes.
Lesson 4: shapes, names and types
Four more refusals come from facts the earlier chapters established:
// shape_mismatch.js (chapter 97)
· unsafe: line 4: records with
different fields or field order
({x, y} vs {y, x})
// record_shared.js (chapter 97)
· unsafe: line 6: the new record is also
referenced elsewhere, so it could be
aliased
// reassigned.js: later, ps = [];
· unsafe: "ps" is reassigned (line 5),
so it may not always hold this array
// string_index.js: const k = '5';
· unsafe: line 6: indexes the array with
something that may not be a number
reassigned.js is the plainest. After ps = [], the name means a
different array, one built after Viscosity’s rewrite was planned; every
ps[i] would have to work on both. And ps[k] with a string happens to
work for '5', but the same line with 'length' reads a property, so
a string can’t be trusted as an index.
Lesson 5: callers it can’t see
// interproc_mixed.js: total(a), total(b)
· unsafe: line 10: passed to total(),
which is also called with other values
// param_unknown.js:
// export function nudge(dx) {...}
ps (line 2) {x} → kept as array of
objects
· no field is provably numeric, so
there are no typed columns to gain
The second one is not a bailout at all; notice there is no unsafe:.
nudge is exported, so its callers aren’t all visible and dx might be
anything. ps[i].x += dx could then put a string in x, so x gets no
typed column, and with no typed column there is nothing to gain. The
same goes for all_generic.js, whose only field is a string, and for
no_loops.js and small_auto.js: safe, but not worth changing. Those
are the cost model’s decisions, in the next chapter.
Engines bail out too, but later. V8’s optimizing tiers speculate from the feedback they’ve seen and, when a guess fails at run time, deoptimize: they throw the optimized code away and carry on in a slower tier[2]. A bailout there costs time at the moment it happens. Viscosity’s bailouts happen before the program runs and can’t be undone, so they last for the whole run. In exchange, the decision is written down, with a reason you can read and act on.
Your turn
A collection is { name, line, fields, bailouts }. Write refusal(c), returning the lines viscosity explain prints when it bails out, as an array of strings: first NAME (line L) {F1, F2} → kept as array of objects, then one line ' · unsafe: ' + reason per reason, in order, without duplicates. If there are no bailouts, return null (the collection is safe, and other questions decide its layout).
function refusal(c) {
// your code here
return null;
}
console.log(refusal({
name: 'ps', line: 2, fields: ['x'],
bailouts: ['"ps" is reassigned'],
}));Write lesson(reason) that sorts a real bailout reason into a lesson. Return 'identity' if it mentions "===" or stored in another variable; 'unknown code' if it mentions method call, function call or on the array; 'shape' if it mentions field order or referenced elsewhere; 'type' if it mentions may not be a number; otherwise 'other'. Check them in that order.
function lesson(reason) {
// your code here
}
console.log(lesson('line 6: indexes ' +
'the array with something that ' +
'may not be a number'));What you learned
- A bailout leaves a collection unchanged because safety can’t be
proved. explain prints each one after
unsafe:. - Identity: a second name (another array, a variable, a closure) or
===needs real element objects, which columns don’t have. - Unknown code: calls and array methods that Viscosity can’t see into might observe the elements, so they count as observing.
- Shapes, names, types: mismatched or shared literals, a reassigned binding, and indexes that may not be numbers.
- Callers: a known function must get the same collection at every call. Unknown callers can also make columns non-numeric, which is a cost decision, not a bailout.
Check your understanding
0/6- 1.
explain says
ps … → kept as array of objects · no loop reads or writes its fields. Is this a bailout? - 2.
Why does
keep.push(ps[i])makepsunsafe? - 3.
Which change lets Viscosity accept
closure_alias.js, with the same output? - 4.
Why is
ps.map(f)a bailout even iffonly readsp.x? - 5.
A program has
let ps = [], fills it, and later doesps = []. Why does Viscosity refuse? - 6.
How do Viscosity’s bailouts differ from V8’s deoptimizations?
References
- [1] MDN contributors. Equality comparisons and sameness. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness (checked 2026-10-31)
- [2] Toon Verwaest, Leszek Swirski, Victor Gomes, Olivier Flückiger, Darius Mercadier, Camillo Bruni (2023). Maglev - V8’s Fastest Optimizing JIT. V8 blog (v8.dev), 5 December 2023. v8.dev/blog/maglev (checked 2026-10-31)