80. Constant folding and propagation
Some arithmetic in a program can be done before it ever runs. Doing it means one sum, one substitution, and a careful respect for JavaScript’s odd corners, and, done optimistically, it can even prove which branches are never taken.
- Fold an instruction whose operands are constants, without changing what the program means.
- Propagate constants through SSA values, and explain how folding and propagation feed each other.
- Merge constants at a phi with a three-level lattice.
- Explain how sparse conditional constant propagation uses unreachable edges to find more constants.
Work you could do in advance
const DAY = 60 * 60 * 24;
let k = 4;
let n = k * 2;
if (n > 5) { /* ... */ }
Every time this runs, the machine multiplies 60 by 60 by 24 and gets 86400. It multiplies 4 by 2 and gets 8, then checks whether 8 is more than 5. The answers never change. A compiler can work them out once, while compiling, and write the answers into the program instead.
Folding
Planning a road trip, you add up the distances on paper the night before. You don’t redo the same sum at every petrol station.
In SSA it is one rule: if every operand of an instruction is a known
constant, and the operation has no side effects and can’t throw,
compute it now and replace the instruction with a const.
%1 = const 60
%2 = binop * %1, %1 → %2 = const 3600
The catch: the compiler must get exactly the answer the program would have got at run time. In JavaScript that includes all of these:
console.log('1' + 2);
console.log(1 / 0);
console.log(0.1 + 0.2);
console.log(Object.is(0 * -1, -0));
console.log(2 ** 53 + 1);'1' + 2 is a string. Dividing by zero is not an error.
0.1 + 0.2 has the rounding of a 64-bit float (chapter 22). -0 is a
real value, and 2 ** 53 + 1 can’t be represented. A folder written
in JavaScript gets these right for free by doing the same operation.
A folder written in another language must copy JavaScript’s rules
exactly, or it changes the program.
Constant folding replaces an operation whose operands are all constants with its result, computed at compile time. It is only allowed when the operation has no side effects, can’t throw, and gives exactly the result it would give at run time.
Propagation
Folding needs operands that are constants. After %2 = const 3600,
any instruction using %2 now has a constant operand. In SSA that
needs no checking at all: %2 is defined once, so every use of it is
3600. Replace each use with the constant, and fold whatever that
makes foldable. Each fold can enable more folds further down.
%3 = const 24
%4 = binop * %2, %3 → %4 = const 86400
Without SSA you’d need chapter 79’s reaching definitions first, to be sure no other assignment to the variable could reach the use.
Constant propagation replaces uses of a value known to be a constant with the constant itself. Together with folding it runs to a fixpoint: propagate, fold what became foldable, and repeat until no new constants appear.
Constants meet at phis
What about %6 = φ(%4, %5)? If both operands are 2, %6 is 2. If
one is 2 and the other 3, %6 is not a constant. And inside a loop,
an operand may not have been worked out yet. That gives a lattice with
three levels:
- ⊤ (“top”): no information yet;
- a constant: 2, or
'hi', ortrue; - ⊥ (“bottom”): varies, not a constant.
Merging ⊤ with anything gives the other thing. Merging two equal
constants gives that constant. Anything else gives ⊥. Start every
value at ⊤ and you have chapter 79’s optimistic analysis again. It is
what finds x constant in a loop like this one, which never changes
it:
b1: ; loop header
%5 = phi %1, %5 ; %1 = const 7
Optimistically, %5 starts at ⊤, so merging gives 7, and 7 is
consistent. It is the trivial phi of chapter 78, seen by an analysis.
A loop counter φ(0, i + 1) goes to ⊥: 0 merged with 1 varies.
Using the branches too
Plain propagation treats every block as reachable. But if a branch
condition folds to true, the else side can never run, and its
values should not count. Sparse conditional constant propagation
(SCCP) tracks this with a second kind of fact: which CFG edges are
executable. It starts optimistic about both. Every value is ⊤ and
every block is unreachable until proven otherwise:
let x = 1;let y;if (x > 0) { y = 2; } else { y = 3; }let z = y * 10;
SCCP keeps two worklists: SSA values whose lattice value changed (to
revisit their users), and CFG edges that just became executable (to
visit the block they lead to). A phi only merges operands arriving on
executable edges. A branch on a constant marks only one edge. Plain
propagation would merge 2 with 3 at the phi and say “varies”. SCCP
proves z is 20, and that b2 is dead.
LLVM’s sccp pass is this algorithm. Its documentation sums it up as:
assume values are constant and blocks dead unless proven otherwise,
replace values proven constant, and turn branches proven to go one way
into unconditional ones. It also notes that the pass leaves dead code
behind for a later DCE pass[1].
V8’s TurboFan has a ConstantFoldingReducer. Its typer works out a
type for each node. If the type is a single value (null,
undefined, NaN, -0, one particular object, or one plain number)
and the node can be removed safely, the reducer replaces it with that
constant[2].
Viscosity, by contrast, has no constant folding pass: it emits your
own source with a new data layout, and leaves arithmetic alone. But
its numeric inference uses the same machinery as SCCP, an optimistic
fixpoint over SSA values, on the smaller number/maybe-not lattice.
Your turn
Write fold(insts) for straight-line SSA code. Each instruction has an id and an op: 'const' (with a value), 'param' (unknown), or one of 'add', 'sub', 'mul' with two args, the ids of earlier instructions.
Return a new list, in the same order. Any arithmetic instruction whose args are all known constants, either a 'const' or something you have already folded, becomes { id, op: 'const', value } with the result. Everything else is returned unchanged. Use OPS to compute.
// insts: [{ id, op, args, value }]
// op: 'const' | 'param' | 'add'
// | 'sub' | 'mul'; args are ids
// of earlier instructions.
const OPS = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
};
function fold(insts) {
return insts;
}Write meet(a, b) for the three-level constant lattice. TOP means "no information yet", BOT means "varies", and anything else is a constant. Merging TOP with x gives x; BOT with anything gives BOT; two constants give that constant if they are the same value in JavaScript's strictest sense (so 0 and -0 differ), and BOT otherwise.
const TOP = 'top';
const BOT = 'varies';
// a, b: TOP, BOT, or a constant.
function meet(a, b) {
return a;
}What you learned
- Constant folding computes an operation on constants at compile time. It must match run-time semantics exactly, quirks included, and must not fold anything with side effects or that can throw.
- Constant propagation replaces uses of a constant value with the constant. In SSA every use of a constant value is that constant. Folding and propagation feed each other until a fixpoint.
- At phis, constants merge in a three-level lattice: ⊤, a constant, ⊥. Starting at ⊤ is what lets loops keep their constants.
- SCCP is also optimistic about edges. Branches on constants make only one edge executable, and phis ignore the rest.
Next: deleting the code these passes just made useless.
Check your understanding
0/6- 1.
Which of these may a JavaScript compiler fold to a constant?
- 2.
In SSA,
%2 = const 3600. Why can every use of%2be replaced by 3600 without further checks? - 3.
In the three-level constant lattice, what is
merge(⊤, 5)? - 4.
x = 1; if (x > 0) y = 2; else y = 3; z = y * 10;What does plain constant propagation (without edge tracking) conclude aboutz? - 5.
What makes SCCP find more constants than plain propagation?
- 6.
What does Viscosity share with SCCP?
References
- [1] The LLVM Project. LLVM’s Analysis and Transform Passes. LLVM documentation. llvm.org/docs/Passes.html (checked 2026-10-31)
- [2] The V8 project authors (2018). constant-folding-reducer.cc: TurboFan’s ConstantFoldingReducer (V8 source code). V8 JavaScript engine, official source repository (github.com/v8/v8), src/compiler. github.com/v8/v8/blob/main/src/compiler/constant-folding-reducer.cc (checked 2026-10-31)