Viscosity
Review
Part VI · The compiler middle-end · Day 21

84. Alias analysis

Chapter 4 showed that two names can share one object. A compiler that wants to change how objects are stored has to know, for every name, which objects it might be sharing. When it can’t tell, it must assume the worst.

About 16 minutes, including the exercises and quiz
By the end you will be able to
  • Compute points-to sets for simple assignments, and use them to decide whether two references may alias.
  • Distinguish must-alias, may-alias and no-alias answers, and say which one an optimizer may act on.
  • Explain Viscosity's alias reasoning, which trusts only references it can follow and bails out on every other use.
  • Predict which uses of an element (a store, a call, `===`, a closure, a phi) stop Viscosity changing an array's layout.

Chapter 4, at compiler scale

Here is the puzzle from chapter 4, grown up:

const ps = [];
for (let i = 0; i < 100; i++) {
  ps.push({ x: i });
}
const p = ps[3];
p.x = 42;
const keep = [];
keep.push(p);
keep[0].x = 7;
for (let i = 0; i < ps.length; i++) {
  ps[i].x += 1;
}
console.log(ps[3].x);

It prints 8. keep[0] and ps[3] are the same object, so the 7 written through keep is what the final loop increments. Delete the three lines that mention keep, and it prints 43, and Viscosity turns ps into a struct of arrays. With them, it refuses:

ps (line 1) {x} → kept as array of objects
  · unsafe: line 5: an element is used by
    push at line 8

Not because the loop got harder. Because one element now has a second name that Viscosity can’t keep track of.

Who might point at what?

Intuition

Give every name in the program a list of the houses its address card might lead to. If two names’ lists share a house, they might be aliases. If the lists don’t overlap, a change made through one name can never be seen through the other.

That list is a points-to set. A program can create millions of objects, so the analysis names them by where they are created: every object made by the { x: i } on line 3 is the one abstract object “line 3”, however many times the loop runs.

The rules for simple assignments are short:

a = {}    (site o1)  o1 is in pts(a)
b = a                pts(b) ⊇ pts(a)
a.f = b   for each o in pts(a):
                     pts(o.f) ⊇ pts(b)
c = a.f   for each o in pts(a):
                     pts(c) ⊇ pts(o.f)

Apply them over and over until no set grows: a fixpoint, as in chapter 79. For example:

const a = { x: 1 };  // site o1
const b = { x: 2 };  // site o2
let c = a;
if (flag) c = b;

pts(a) = {o1}, pts(b) = {o2}, pts(c) = {o1, o2}. So c may alias a and may alias b, but a and b can never alias: their sets don’t overlap.

Definition

Alias analysis decides, for two references, whether they must point at the same object, may point at the same object, or never do. A reference’s points-to set is the set of abstract objects (usually allocation sites) it may point at. Two references may alias if their points-to sets intersect. LLVM’s alias analysis interface returns exactly these kinds of answers: MustAlias, MayAlias and NoAlias (plus PartialAlias for overlapping memory)[1].

An optimizer may act on “must” and “no”. “May” means it has to keep both possibilities alive, which usually means doing nothing clever. The whole game of alias analysis is turning as many “may” answers as possible into “must” or “no”, without ever being wrong.

Why keep.push(p) is fatal

Step through the program from the top of the chapter as a graph of arrows:

Step throughOne push that blocks the layout
const ps = []; // 100 × ps.push({ x: i })
const p = ps[3];
p.x = 42;
const keep = [];
keep.push(p);
keep[0].x = 7;
References on the left, ps’s slots and records on the right, and keep’s slot pointing at record 3NAMESOBJECTSps[0]x:0[1]x:1[2]x:2[3]x:3… 100 slots
ps holds 100 records. Each record was a fresh { x: i } pushed straight in, so each one has exactly one arrow pointing at it: from its own slot in ps.
1/6

The problem in the last step is general. In struct-of-arrays form an element is not an object any more: it is a position in some columns. Any reference to it that survives the transformation needs to become an index. Viscosity can make that change only for references it can see and rewrite. Here is what it does with const p = ps[3] when there is no keep:

const p = 3;
ps.x[__vsc_bc(ps, p, 'x', true)] = 42;

p became the number 3. That works because p is an SSA value whose target is known exactly: a must-alias of “element 3 of ps”. An arrow stored inside keep is different. Whatever reads keep[0] later expects an object, and there is none to give it.

Viscosity’s rule: account for every use

Viscosity doesn’t build general points-to sets. It asks a narrower question about each candidate array, with a deliberately simple answer.

The references it understands are SSA values: loads of the array’s own variable (even const q = ps is fine, since q is the same SSA value), the results of ps[i], and parameters of known functions that every call site fills with this same array (chapter 82). For each of these it knows the points-to set exactly: {this array}, or {an element of this array}. Every use of such a value must be one it can rewrite: .length, .push of a fresh object literal, ps[i], and reading or writing a field the records have. Anything else is a place where an arrow may leave its sight, so it is treated as a possible alias, and the array stays as it is.

Here is what the engine says when one line is added after the push loop of a particle program (messages trimmed to the reason):

const peek = () => keep.x;  (keep = ps[0])
  an element is stored in another
  variable (keep)
const other = [ps[0]];
  an element is put inside another array
const box = { p: ps[0] };
  an element is put inside another object
console.log(ps[0]);
  an element is passed to a method call
console.log(ps[0] === ps[1]);
  an element is compared or combined
  with "==="

Each one is a different way of creating a second name:

  • A captured variable. keep is read by a closure, so it is not an SSA value but a variable living in memory, and the closure can read it whenever it likes.
  • Inside another object or array. The arrow is now data. Anything that reaches other or box reaches the element.
  • Passed to code it doesn’t model. console.log could do anything with the object, including keep it.
  • Compared with ===. Identity needs an object. After the transformation there is no object to compare.
  • Merged in a phi. let p = ps[0]; if (c) p = ps[5]; makes p one of two elements. Viscosity reports that “an element is merged with other values” rather than tracking a reference that could be either.

The same logic covers whole arrays passed to functions. In chapter 82, total(xs) was called with a and with b. In points-to terms, pts(xs) = {a, b}. Viscosity treats a parameter as the collection only when that set is exactly one array.

Try it in the real compiler. peek captures keep, so ps is kept as objects. Delete the peek line and print keep.x directly instead: keep becomes an index, and ps becomes a struct of arrays. Run both versions to see that they agree either way.

PlaygroundA captured element
Under the hood

General-purpose compilers use alias analysis everywhere, mostly to move and delete memory operations. LLVM’s simplest alias analysis, basic-aa, “implements identities (two different globals cannot alias, etc)” without any stateful analysis, and its loop-invariant code motion pass uses alias analysis to decide whether a load inside a loop can be hoisted (chapter 83)[2].

Viscosity uses aliasing for a different decision: is it safe to change how an entire collection is stored? Research systems that switch JavaScript arrays to columnar storage automatically face the same requirement[3]. Viscosity’s answer is all or nothing per collection: one use it can’t account for, and the whole array keeps its layout. That gives up some programs that a cleverer analysis could transform, and in exchange it is easy to check and never wrong in the direction that matters. The next chapter makes “an arrow leaves its sight” precise: escape analysis.

Your turn

ExercisePoints-to sets for simple assignments

Statements are ['new', a, site] (a = {} at allocation site site) or ['copy', a, b] (a = b). Order doesn’t matter (the analysis is flow-insensitive). Write pointsTo(stmts) returning an object mapping every variable mentioned to its sorted points-to set, and mayAlias(pts, x, y).

function pointsTo(stmts) {
  // your code here
  return {};
}

function mayAlias(pts, x, y) {
  // your code here
  return false;
}

const pts = pointsTo([
  ['new', 'a', 'o1'],
  ['new', 'b', 'o2'],
  ['copy', 'c', 'a'],
  ['copy', 'c', 'b'],
]);
console.log(pts.c); // ['o1', 'o2']
Extra practicePoints-to through fields

Add two statements: ['store', a, f, b] for a.f = b and ['load', c, a, f] for c = a.f. Track a points-to set for each heap cell site.f (for example 'o1.f'). Write fieldPointsTo(stmts) returning an object mapping each variable (not heap cell) to its sorted set.

function fieldPointsTo(stmts) {
  // your code here
  return {};
}

const pts = fieldPointsTo([
  ['new', 'a', 'o1'],
  ['new', 'b', 'o2'],
  ['store', 'a', 'f', 'b'],
  ['load', 'c', 'a', 'f'],
]);
console.log(pts.c); // ['o2']

What you learned

  • Alias analysis decides whether two references must, may or never point at the same object. Optimizers can act on “must” and “no”; “may” forces them to be careful.
  • A points-to set lists the abstract objects (allocation sites) a reference may point at. Copies and field stores make sets grow, and the rules are iterated to a fixpoint. Two references may alias if their sets intersect.
  • In struct-of-arrays form an element is a position, not an object, so every reference to it must be rewritable as an index.
  • Viscosity trusts only references it can follow (SSA values for the array, its elements, and parameters every call fills with the same array) and bails out on any other use: stored elsewhere, captured, passed to unknown code, compared with ===, or merged in a phi.

Check your understanding

0/6
  1. 1.

    An analysis finds pts(p) = {o1, o2} and pts(q) = {o2, o3}. What can it say about p and q?

  2. 2.

    Which line, added after the push loop, still lets Viscosity turn ps into a struct of arrays?

  3. 3.

    A flow-insensitive points-to analysis sees c = a; and later c = b;, with a and b pointing at sites o1 and o2. What is pts(c)?

  4. 4.

    Suppose an analysis wrongly answered “no alias” for keep[0] and ps[3] in this chapter’s first program, and Viscosity transformed ps. What would happen?

  5. 5.

    Why does let p = ps[0]; if (c) p = ps[5]; p.x = 1; keep ps as objects?

  6. 6.

    What is the trade-off in Viscosity’s all-or-nothing rule (one unexplained use and the array keeps its layout)?

References

  1. [1] The LLVM Project. LLVM Alias Analysis Infrastructure. LLVM documentation. llvm.org/docs/AliasAnalysis.html (checked 2026-10-31)
  2. [2] The LLVM Project. LLVM’s Analysis and Transform Passes. LLVM documentation. llvm.org/docs/Passes.html (checked 2026-10-31)
  3. [3] 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)