Viscosity
Review
Part VII · Building Viscosity · Day 25

99. Proving it safe

A compiled program must print exactly what the original printed. Viscosity can only promise that if no line of the program can tell how the elements are stored, and it has to prove that from the code before it changes a thing.

About 17 minutes, including the exercises and quiz
By the end you will be able to
  • State the safety property Viscosity proves, and why it is checked with an allow-list.
  • List the uses of an array and of its elements that Viscosity knows how to rewrite.
  • Explain when passing a collection to a function is safe, using known functions and their call sites.
  • Explain why every index must be provably a number, and how bailout reasons reach explain.

The promise

Viscosity is a source-to-source compiler, and it makes one promise: the program it writes prints exactly what yours printed. The struct of arrays it builds stores the same numbers. But a JavaScript program can notice much more than numbers. It can notice how its data is stored:

ps[1] === ps[2];   // is it one object?
console.log(ps[3]); // print the object
const keep = ps[0]; // a second name
ps.sort(byX);       // move the objects
for (const k in ps[0]) {} // its keys

After the rewrite there are no element objects at all, only columns. If any line like these survived, the program could tell, and the promise would be broken.

Intuition

A theatre crew changes the set during the interval, with the curtain down. That is fine as long as nobody in the audience can see behind the curtain. Before moving anything, the stage manager checks every gap.

Definition

Safety analysis proves that no code can observe a layout change. In Viscosity it is an allow-list: every use of the collection, and of every element it hands out, must be one that Viscosity knows how to rewrite. Any other use adds a reason to the collection’s bailout list. An empty list means safe.

Here is the whole decision; the rest of the chapter looks at each check.

Step throughFrom candidate to safe or unsafe
A flowchart of Viscosity's five safety checkscandidate psbound to this array only once?literals unify, each used once?every use of ps on the allow-list?every element use a field get/set?every index provably a number?bailouts empty?
A candidate arrives from collection discovery: a const or let that is given an empty array or an array literal. Nothing about it is trusted yet.
1/9

What the array may do

Starting from every load of the array, Viscosity walks the uses (the same worklist as chapter 85). Four kinds are allowed. Simplified, from packages/core/src/analysis.ts:

for (const u of usesOf(ref)) {
  const i = u.inst;
  if (isLength(i)) continue;
  if (isPush(i)) {
    // one argument, a fresh literal:
    // setShape (chapter 97)
    continue;
  }
  if (isIndexGet(i)) {
    elements.push(i); // check later
    continue;
  }
  if (isKnownCall(i)) {
    work.push(param); // follow it
    continue;
  }
  bailouts.push('the array is ' +
    describeUse(u));
}

Everything else ends at describeUse, which names what happened. Real messages for ps.reverse(), console.log(ps) and return ps:

· unsafe: line 10: uses .reverse on the
  array (only .length and .push are
  supported)
· unsafe: the array is passed to a
  method call at line 4
· unsafe: returned or branched on in
  make()

The first two can appear together: ps.reverse() both reads a property other than length and passes ps as the receiver of a method call. Each element ps[i] is then checked the same way, with an even shorter list: read a field of the shape, or write one. The safe uses are what fills the access table of chapter 98.

Following a collection into a function

Most real code doesn’t loop over an array where it was made. It passes the array to a function. A call is normally an escape, because the callee could do anything. But if Viscosity can see every call of the function, the parameter is just another name for the collection.

// Known functions: declared once, and
// every reference is a direct call.
for (const f of m.functions) {
  if (storesOf(f).length !== 1) continue;
  let ok = true;
  const sites = [];
  for (const use of refsTo(f))
    if (isDirectCall(use)) sites.push(use);
    else ok = false;
  if (ok) callSites.set(f, sites);
}

When ps is passed to a known function at position k, Viscosity checks that all its call sites pass ps at k. If they do, the parameter joins the worklist, and its uses inside the function are checked like uses of ps. If some call passes something else, it bails out. Two corpus files show both outcomes:

// interproc_ok.js: drift(bodies, 0.5)
// and drift(bodies, -0.25)
bodies (line 2) {x, y, m} → struct of
  arrays

// interproc_mixed.js: total(a), total(b)
a (line 2) {x} → kept as array of objects
  · unsafe: line 10: passed to total(),
    which is also called with other
    values

The rule is points-to analysis on a small scale: the parameter may point to exactly one collection. Two collections would need two versions of total, and Viscosity doesn’t make copies of functions. An exported function is not known either: code in another module could call it with anything. Passing ps to one gives “passed to a function call”.

Every index must be a number

ps[i] is only an element if i is a number. Bracket access with a string asks for a property, and some strings happen to name elements:

Try itStrings that are (and aren’t) indexes
const ps = [{ x: 1 }, { x: 2 }];
for (const k of [1, '1', 'length']) {
  console.log(k, ps[k]);
}
// '1' finds an element, but 'length'
// finds a property. With only a
// string in hand, Viscosity can't
// tell which you meant.

After the rewrite, ps[k] becomes ps.x[k] and friends, which would answer ps['length'] completely differently. So Viscosity requires every index to be provably a number. “Provably a number” is a fact from the numeric fixpoint (chapter 86), so this check waits until the fixpoint has finished:

for (const { c, i } of indexChecks)
  if (!isNumber(i.args[1]))
    c.bailouts.push(`line ${line(i)}: ` +
      'indexes the array with something' +
      ' that may not be a number');

The fixpoint follows parameters of known functions, too. With function get(k) { return ps[k].x; }, the calls get(3) and get(4) are fine, while one call get('4') makes k “maybe not a number” and the whole collection bails out.

From reasons to explain

Viscosity never stops at the first problem. All the reasons are kept, duplicates removed, and handed to the planner, which makes this the very first question it asks. From plan.ts:

if (c.bailouts.length) {
  out.push(keep(...c.bailouts.map(
    (b) => `unsafe: ${b}`)));
  continue;
}

keep means “array of objects”. No cost is weighed for an unsafe collection. Try it on the program below. It is accepted as it stands. Then make one change at a time and watch the reasons appear: add ps.reverse(); before the last line, print ps[3] instead of ps[3].x, add a call step([{ x: 0, vx: 1 }]);, or index with const k = '3';.

PlaygroundMake the proof fail
Under the hood

Makor and colleagues transform arrays of JavaScript objects into columnar storage while the program runs[1]. A run-time system is there as each operation happens. A source-to-source compiler gets no such second look: once its output runs, nothing of Viscosity is left to notice a surprise. That is why it works with an allow-list. Even ps[1] === ps[2], which only asks whether two references name the same object[2], is enough to stop it.

Your turn

ExerciseA tiny safety checker

A collection with fields fields has a list of uses, each { on, op, line } plus extras. on is 'array' or 'element'. Write firstBailout(fields, uses), returning the first reason as 'line N: ' + reason, or null if all are safe.

  • Array uses: 'length' is safe; 'push' is safe if fresh is true; 'index' is safe if num is true, otherwise the reason is 'index may not be a number'. Anything else: 'array used by OP'.
  • Element uses: 'get' and 'set' are safe if field is in fields, otherwise the reason is 'unknown field .F'. Anything else: 'element used by OP'.
function firstBailout(fields, uses) {
  // your code here
  return null;
}

console.log(firstBailout(['x'], [
  { on: 'array', op: 'length',
    line: 3 },
  { on: 'array', op: 'sort',
    line: 4 },
]));
Extra practiceCan Viscosity follow the call?

A function is { stores, refs }: stores is how many times its name is assigned (a declaration counts as one), and each ref is { call: true, args: [...] } for a direct call or { call: false } for any other use. Write followParam(fn, k, name) for a call that passes the collection name as argument k. Return 'unknown' if the function isn’t known (stores !== 1, or any ref isn’t a direct call). Otherwise return 'follow' if every call passes name at position k, and 'mixed' if not.

function followParam(fn, k, name) {
  // your code here
}

console.log(followParam({
  stores: 1,
  refs: [{ call: true, args: ['ps'] }],
}, 0, 'ps'));

What you learned

  • Safety analysis proves that no code can observe the layout change. Viscosity uses an allow-list: anything it can’t rewrite is a bailout reason.
  • The array may be used for .length, push of one fresh literal, ps[i], and as an argument to a known function. Elements may only have fields of the shape read or written.
  • A known function is declared once and only called directly. Its parameter is followed if every call site passes the same collection; exported functions are never known.
  • Every index must be provably a number, which is checked after the numeric fixpoint.
  • All reasons are collected; any reason makes the planner keep the array of objects and print unsafe: reasons.

Check your understanding

0/6
  1. 1.

    What does Viscosity do with a use of the collection that isn’t on its allow-list?

  2. 2.

    Which use of ps is on the allow-list?

  3. 3.

    function step(qs) is declared once and called as step(ps) twice. What does Viscosity do?

  4. 4.

    The same program, but step is export function step(qs). What changes?

  5. 5.

    Why does const k = '3'; ps[k].x make Viscosity bail out, even though it reads element 3?

  6. 6.

    A collection has two bailout reasons. What is the first thing the planner does?

References

  1. [1] Lukas Makor, Sebastian Kloibhofer, David Leopoldseder, Daniele Bonetta, Lukas Stadler, Hanspeter Mössenböck (2022). Automatic Array Transformation to Columnar Storage at Run Time. Proceedings of the 19th International Conference on Managed Programming Languages and Runtimes (MPLR ’22), ACM, pp. 16–28. doi:10.1145/3546918.3546919 (checked 2026-10-31)
  2. [2] 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)