Robert Cunningham
· 17 min read

On The Unreasonably Broad Uses of Diagonalization

Think of any program which mangles source code. This program should take source code as a string and do something with it: hash it, encrypt it, append it to itself, delete it, or anything else.

Here’s a very surprising result. Each of those mangling transformations is weak to a specific string of source code, whose runtime behavior will be completely unchanged by the mangling.

In other words, regardless of how intense your mangling is, it is guaranteed to not to be able to affect some strangely strong and robust program. We call that specific program a fixed point of your transformation.

Your transformation might be “encrypt the source code, interpret that as an integer, then print the third largest prime factor of that integer twelve times.” And yet, I tell you, you’re in luck: I have a program in my back pocket which will behave exactly the same before and after.

Let’s take a basic example: the transformation that swaps ‘a’ and ‘z’. What are its fixed points? As expected console.log("hello"), as well as every other program with neither ‘a’s nor ‘z’s, are unchanged.

const swapAandZ = (input: string) =>
  input.replace(/[az]/g, c => c === 'a' ? 'z' : 'a')

const input = 'console.log("hello")'

eval(input) === eval(swapAandZ(input))
// our === implies the same return value, console outputs, error text, etc.

Some source transformations are slightly more stubborn; can you think of a program which will be unchanged by “append the source to itself?”

It turns out that while (true) {...} (or the empty program) are both fixed points of the doubling transformation.

const double = (source: string) => source + source

const input = "while (true) {console.log('loop');} "

eval(input) // => loop loop loop loop loop ...
eval(double(input)) // => loop loop loop loop loop ...

But then there are transformations whose fixed points are extremely non-obvious. Can you think of a fixed point of “hash the source and execute that hash”? The program would need to behave exactly the same hashed and unhashed.

const hash = (source: string) => 
  createHash('sha256').update(source).digest('hex');

const input = "some crazy program"

// eval(input) === eval(hash(input))

Nevertheless, as promised, we will prove that in general, any transformation, no matter how crazy, must have a fixed point.

// for any transformation
const transform: (source: string) => string;

// there exists an input
const input: string;

// where executing the program is the same as executing its transformation
eval(input) === eval(transform(input))

The technique we’ll use to prove this remarkable truth is a diagonalization trilemma. To warm up to the technique, we’ll start with a reminder of the proof that there are more reals than natural numbers. Then we’ll prove the above result, that all code transformation programs have a fixed point. This result is called Rogers’s Theorem. Finally, we’ll prove that a seemingly powerful class of functions, the primitive recursive functions, can’t be complete.

We’ll then examine a terse and opaque piece of math called the Diagonal Lemma, which we will understand by analogy, as Rogers’s theorem applied to math instead of code. We’ll then prove it with our familiar diagonalization trilemma technique.

Finally, with the powerful Diagonal Lemma in hand, we’ll sketch a pair of excellent proofs of Gödel’s First Incompleteness Theorem (all theories must be either incomplete or inconsistent) and Tarski’s Undefinability of Truth (theories cannot define an internal notion of truth).


Warm Up: Cantor

You might recognize diagonalization from the famous proof that there are more real numbers than natural numbers. We’ll walk through it again here, with an eye towards introducing the trilemma that will guide us through Rogers and the Diagonal Lemma, into Gödel and Tarski.

The argument starts by building a complete numbered list of every real number between 0 and 1:

1 => 0.139042986720983745...
2 => 0.349857294587293845...
3 => 0.239841509204872018...

We’ll define an evaluation function f(i)(j)f(i)(j) that takes the iith real number and returns its jjth decimal digit: [0]

f(3)(3) => 0.23[9]841509204872018... => 9

We also create a “make it different” function δ\delta, which takes a digit and returns a different digit. Many rules works; we’ll map everything to 1, except 1 gets mapped to 2:

const delta = (x: number) => x === 1 ? 2 : 1

Now, we’ll build a new real number gg by going through the diagonal values (f(1)(1)f(1)(1), f(2)(2)f(2)(2), etc) and applying δ\delta to choose a different digit at each location for gg: g(j)=δ(f(j)(j))g(j) = \delta(f(j)(j)). Reading the 1st digit of the 1st number, the 2nd digit of the 2nd, the 3rd of the 3rd, and so on, the diagonal starts 1, 4, 9, and so gg starts 0.211…

d(f(1)(1)) => d(0.[1]39042986720983745...)  => d(1) => 2
d(f(2)(2)) => d(0.3[4]9857294587293845...)  => d(4) => 1
d(f(3)(3)) => d(0.23[9]841509204872018...)  => d(9) => 1

gg differs from the first number at digit 1 (it must differ, because δ\delta always changes its input, and the first digit of g is δ\delta(the first digit of f), the second number at digit 2, and so on. But we started by listing all the rational numbers between zero and one, so how could gg not be in this list?

One of three things must have gone wrong:

  1. Maybe δ\delta is broken, and doesn’t actually change every digit. If δ\delta has a fixed point, gg might simply be hidden somewhere in the list.
  2. Maybe gg is a new kind of object, and does not belong in the list.
  3. Maybe the list is incomplete.

This is the diagonalization trilemma.

Well, δ\delta is clearly not broken, and gg is definitely a real number, so it must that our list of real numbers is incomplete.

And since every natural number is listed on the left side of the list, but there’s at least one real number not listed on the right side of the list, there must be more reals than naturals.

Rogers’ Theorem: Fixed Points of Program Transformations

Now let’s try the exact same trick, but on programs.

This time we’re going to list all the JavaScript programs, alphabetically:

1 => eval('a')
2 => eval('b')
3 => eval('c')
...
10348423 => eval('x => x + x')

Our evaluation function f(i)(j)f(i)(j) takes the iith program in the list and runs it on the input jj.

f(10348423)(1)
  => eval('x => x + x')(eval('a'))
  => 'aa'

We’re going to use const delta = (x: string) => x + " + 1". Recall that δ\delta operates on the source, so for example

delta(f(10348423)(1))
  => eval('x => x + x')((eval('a')) + 1
  => 'aa' + 1
  => 'aa1'

Just like before, we build a new object gg (in this case a program and not a real number), with g(j)=δ(f(j)(j))g(j) = \delta(f(j)(j)). And just like before, gg can’t be any program on our list — it disagrees with every program at the diagonal position.

Check this by arguing to yourself: why is gg not the program x => x + 4, for example? Because that program appears in our list at position 10521488, and when evaluated at 10521488 returns 10521488 + 4 = 10521492. But g(10521488)g(10521488) returns δ(10521492)\delta(10521492), which is anything other than 10521492. So they can’t be the same program.

So gg isn’t on the list. What went wrong? Again, our three suspects are the δ\delta, the gg, and the list. It must be one of:

  1. δ\delta has a fixed point. It doesn’t, right?
  2. gg might be misbehaved or not writable in JavaScript. But we can describe how to compute gg: “look up the nnth program, run it on input nn, append 1.” That sure sounds like something you could write in JavaScript.
  3. Our list of JavaScript programs might not be exhaustive. But surely we’ll eventually print out even the longest of JavaScript programs by iterating alphabetically?

Do you see the issue? It turns out that our δ\delta does have a fixed point. In particular, eval("while (true) {}") is a fixed point of δ\delta. If your program loops forever, and you add one to the result, it still loops forever.

Not only that, but all viable δ\deltas share this flaw. To build the “change that output” program δ\delta, you need to know whether your original program halts or not. And in general, you can’t know whether it halts or loops, so you can’t build a good δ\delta. [1]

This constitutes a somewhat shocking proof that it’s impossible to build a function δ\delta that takes any program and modifies it to produce a different result. In other words, all programs mapping functions to functions have a fixed point. This result is called Rogers’s Theorem.

There’s a nice immediate corollary: any language allows you either solve the halting problem or build an interpreter for itself. JavaScript allows interpreters but not halting-solvers. Primitive recursive functions, which we’ll examine next, allow halting-solvers (all P.R. functions halt), but can’t build their own interpreters. These correspond to the (1) and (3) branches of the trilemma respectively.


Example: Primitive Recursion

Next, we’ll look at primitive recursive functions. Primitive recursive functions are all the functions made of combinations five functional building blocks: z,s,p,cz, s, p, c and ρ\rho. Don’t worry if you haven’t seen them before. The upshot is that you can easily list them, numbering them sequentially:

1 => z() // zero function
2 => s() // +1 function
3 => p() // "projection" function
...
372 => c(s, z) // composition of 0 and +1 (1)
373 => c(s, s) // composition of +1 and +1 (+2)
...

We’ll choose f(i)(j)f(i)(j) as the evaluation function, which takes the iith primitive recursive function and computes it on jj. For example, evaluating the 373rd function at the value 3:

f(373, 3) 
  => c(s, s)(3)
  => s(s(3))
  => 3 + 1 + 1
  => 5

We can easily build a function δ(x)=x+1\delta(x) = x + 1 that has no fixed point.

So we consult our trilemma, and decide that either (1) δ\delta is broken, (2) the list f(i)f(i) isn’t complete, or (3) gg is a new kind of object.

δ\delta has no fixed point, and f(n)f(n) obviously enumerates all the primitive recursive functions (by definition). So it must be that g(i)=δ(f(i)(i))g(i) = \delta(f(i)(i)) is a new kind of object: a non-primitive-recursive (but computable) function.

This might surprise you if you’ve only played with primitive recursive functions for a little bit: they can compute functions like plus, times, isPrime, nthPrime, ifElse, and more. But it turns out that they aren’t Turing complete, since they can’t simulate the eval function.


Diagonalization Trilemma

We’re going to prove this by asserting that one of three things must always be true. Situation: you have a list of all the objects of some type, and a “make it different” function, which takes an output value and returns a different output value in that group. We will build a new object “g”, which has the same shape but is definitely not on the list. Therefore, we live in one of three worlds:

  1. The list is broken and does not include every object in the group. “g” is a well behaved object of the type which you somehow failed to list.
  2. “g” is somehow of a different type from the list objects, even though it looks similar.
  3. The “make it different” function is broken, and does not always “make it different”; for some objects it actually returns the same thing.

Trilemma Proof

We’ll make a quick stop to formalize and prove this trilemma that we’ve been using.

Diagonalization. For all sets XX and YY with a function f:X×XYf: X \times X \rightarrow Y and a function δ:YY\delta: Y \rightarrow Y with no fixed point, there exists at least one function g:XYg: X \rightarrow Y such that there exists no kk such that f(k)=gf(k) = g.

Proof. Let g(x)=δ(f(x,x))g(x) = \delta(f(x, x)). For any kk, gf(k)g \neq f(k), since g(k)f(k)(k)g(k) \neq f(k)(k).

The important part of this proof is that it sets up our favorite trilemma:

Trilemma. For all sets XX and YY with a function f:X×XYf: X \times X \rightarrow Y, and a function δ:YY\delta: Y \rightarrow Y, at least one of the following three things must be true:

  1. δ:YY\delta: Y \rightarrow Y has a fixed point.
  2. There exists some g:XYg: X \rightarrow Y which isn’t found in f(n)f(n) for nNn \in \mathbb{N}, but gg is obviously the right kind of object, so the list of objects f(n)f(n) must not be exhaustive.
  3. There exists some g:XYg: X \rightarrow Y which isn’t found in f(n)f(n) for nNn \in \mathbb{N}, but f(n)f(n) obviously lists all the possible objects, so gg must actually be a new and different kind of object.

New Tool: Diagonal Lemma

We’ve just seen that all programs that map programs to other programs have a fixed point. Next, we’re going to see a very similar result, but this time applied to mathematical theorems. It’s wrapped up in an opaque piece of math called the Diagonal Lemma. But once we understand the Diagonal Lemma, it opens the door to amazingly short demonstrations of Godel’s Theorem and Tarski’s Theorem.

Let’s say you’re working in some proof system, which is basically some laws (“axioms”) plus some deductive rules. Call that system TT.

Give me a statement A(x)A(x). Then TT can prove that there exists a sentence ϕ\phi where ϕ\phi is only true if A(ϕ)A(\ulcorner \phi \urcorner) is true, where ϕ\ulcorner \phi \urcorner is the numerical representation of the bytes in ϕ\phi. [2]

Mathematically, we can express the same as follows, with TT \vdash read “T proves:”

AϕTϕ    A(ϕ)\forall A \, \exists \phi \quad T \vdash \phi \iff A(\ulcorner \phi \urcorner)

Suppose we’re dealing with A(x):=x2=4A(x) := x * 2 = 4. Then there’s a sentence ϕ\phi where strong enough theories can prove that ϕ    ϕ2=4\phi \iff \ulcorner \phi \urcorner * 2 = 4. In this case, we can choose ϕ\phi to be the sentence 0=10 = 1. Then ϕ=3161393\ulcorner \phi \urcorner = 3161393, and indeed TT can prove that

0=1    31613932=40 = 1 \iff 3161393 * 2 = 4

since both sides are false.

In the domain of programs, we’ve seen that any program you can name that transform programs into other programs has a fixed point. The Diagonal Lemma says something extremely similar for math: any statement you can name that maps the numerical representation of a statement to another statement has a fixed point. And as a bonus, strong enough theories can prove that.

Proof Sketch: Diagonal Lemma

Consider a sufficiently strong theory TT and any arbitrary formula AA with one free variable. Then there exists a ψ\psi so that TT proves that ψ    A(ψ)\psi \iff A(\ulcorner \psi \urcorner).

The set of formulas with one free variable are enumerable (for example, by printing all strings and then discarding any that have more than one variable symbol):

274 => "x = 1"
...
672 => "x = x"
...
1837501 => "5x = x && 4 = 5"

We’re going to define a function f(s)(s)f(s)(s') which evaluates ss on the byte-representation of ss'. For example, f(xx is prime)(xx * xx = 100) evaluates to “33823693116354608 is prime” (the string x*x=100 in UTF-8 has the decimal representation 33823693116354608). Notice that ff maps two formulas with a free variable into a single sentence with no free variables.

Let δ(ϕ)=A(ϕ)\delta(\phi) = A(\ulcorner \phi \urcorner).

Now for the trilemma. All the formulas in our system definitely have a written representation, and all written representations with one free variable definitely count as formulas in our system.

So it must be that δ\delta has a fixed point. And that fixed point is exactly what we’re after: some ψ\psi so that ψ\psi and A(ψ)A(\ulcorner \psi \urcorner) are logically equivalent. (Notice that we’ve shifted from “output equivalence” to “logical equivalence.”)

So we’ve seen how we can prove this. We’re going to omit the proof that TT itself can also prove this, since it’s a bit too hairy for our taste. If you look into it, it basically involves proving that TT can construct and prove this same argument inside itself.

Proof Sketch: Gödel’s Incompleteness Theorem

We’ve finally arrived at Gödel’s Incompleteness Theorem. Here’s a quick reminder of the statement.

Gödel’s First Incompleteness Theorem. Any sufficiently powerful theory must be either inconsistent or incomplete.

The core idea is to construct a sentence SS like “this sentence is unprovable.” Then if SS is provable, it’s wrong (the surrounding theory can prove things that are contradictory; this is called being inconsistent). If TT can’t prove SS, then SS is true (the surrounding theory has truths that it can’t prove; this is called being incomplete).

That’s a slick argument, but all of the complexity hides inside questions like, are sentences like “this sentence is unprovable” even allowed? How do you express something like “this sentence” formally? To our happy surprise, the Diagonal Lemma allows us to build precisely these sentences.

Let A(x)=A(\ulcorner x \urcorner) = p\forall p, the string pp is not a proof of xx. In other words, A(x)A(\ulcorner x \urcorner) = xx is unprovable. Then by the Diagonal Lemma, we can use TT to prove that there exists an xx with x    A(x)x \iff A(\ulcorner x \urcorner), or x    x \iff (xx is unprovable). And ”x    x \iff (xx is unprovable)” is exactly “this sentence is unprovable.” So we can build these sentences!

Proof Sketch: Tarski’s Theorem

Here’s a quick reminder of what Tarski’s Theorem says.

Tarski’s Theorem. Any sufficiently powerful theory cannot define its internal notion of truth.

To prove it, we take A(x)A(\ulcorner x \urcorner) = (xx is not true). The Diagonal Lemma guarantees that we can prove the existence of an xx such that x    x \iff (xx is not true). But ”xx is true     \iff xx is not true” is obviously contradictory.

The only thing we can point to is our original AA, which must have been flawed. It must be that theories cannot define predicates like xx is true or xx is not true.

Conclusion

I hope you’re convinced that diagonalization is an extremely powerful tool.

From our original diagonalization trilemma, we were able to immediately show that primitive recursive functions aren’t complete, that there are more reals than naturals, and that program-transforming-programs must all have fixed points.

Then we used the same trilemma to construct a new tool, the Diagonal Lemma, and closed out the post by using it to construct the strange sentences required for Gödel and Tarski’s famous theorems.

We’ve kept the math relatively light, leaning on intuition and code where necessary. If you want a more rigorous treatment of this topic, I highly recommend continuing with Yanofsky [0], upon which this post is based.

Notes

[0] Note that we’ll use the curried f(3)(5)f(3)(5) and uncurried f(3,5)f(3, 5) forms interchangeably in this post.

[1] A delightful corollary: if your language is weak enough that you can solve the halting problem, then it must not be able to build an interpreter for itself. That’s what happens with P.R. functions, which all halt.

Appendix

Gödel Numbers

Gödel numbers are a way of converting formula into numbers. We denote the Gödel number of xx as x\ulcorner x \urcorner. You can imagine x\ulcorner x \urcorner just interpreting the underlying bytes of xx as an integer instead of a string.

const gödel = (s: string): bigint => {
  const byteArray = new TextEncoder().encode(s);
  const hexString = Array.from(byteArray)
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
  return BigInt(`0x${hexString}`);
};

const unGödel = (n: bigint): string => {
  const hexString = n.toString(16);
  const byteArray = new Uint8Array(
    hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),
  );
  return new TextDecoder().decode(byteArray);
};

console.log(gödel("x * 2 = 4")); // 2215926989203043065908n
console.log(unGödel(2215926989203043065908n)); // x * 2 = 4

Diagonalization Proof

Here’s the straight math version of the general diagonalization argument.

Diagonalization. For all sets XX and YY with a function f:X×XYf: X \times X \rightarrow Y and a function δ:YY\delta: Y \rightarrow Y with no fixed point, there exists at least one function g:XYg: X \rightarrow Y such that there exists no kk such that f(k)=gf(k) = g.

Proof. Let g(x)=δ(f(x,x))g(x) = \delta(f(x, x)). For any kk, gf(k)g \neq f(k), since g(k)f(k)(k)g(k) \neq f(k)(k).

The Diagonalization Trilemma with Exposition

Diagonalization. Let’s say you have an “evaluation” function f:X×XYf: X \times X \rightarrow Y. You can think of f(3)(5)f(3)(5) as selecting the 3rd object from a long list of objects, and evaluating it on 5. [0]

Let’s say you also have a δ:YY\delta: Y \rightarrow Y with no fixed point. You can think of δ\delta as a “give me something different” function: it changes every yy into a different yy.

Then there exists a function g:XYg: X \rightarrow Y that isn’t equal to f(k)f(k) for any kk. In other words, there’s at least one new object gg that isn’t equal to f(3)f(3) or f(55)f(55) or f(574)f(574), or any other object f(x)f(x) in the original list of objects. (Note that f(55)f(55) and gg are both functions representing objects.)

Proof. Take g(x)g(x) as δ(f(x)(x))\delta(f(x)(x)). For any kk, gf(k)g \neq f(k), since g(k)f(k)(k)g(k) \neq f(k)(k). \blacksquare

Spelling out the proof, if you’re concerned that maybe g=f(234)g = f(234), you can check that at gg and f(234)f(234) differ at index 234: g(234)f(234)(234)g(234) \neq f(234)(234). That’s because g(234)g(234) does equal δ(f(234)(234))\delta(f(234)(234)), which is different from f(234)(234)f(234)(234) because δ\delta is the “give me something different” function.

The most important part of this proof is that it establishes a crucial trilemma, which we’ve used repeatedly above.

Acknowledgements

This post (and the primitive recursive post) were prepared as part of my final work for the MIT class Seminar in Logic (18.504). Thanks to Henry Cohn, Lisa Kondrich, and Laker Newhouse for their extensive suggestions, feedback, and help with both this post and the primitive recursive introduction post.

References

[0] Noson S. Yanofsky (2003). A Universal Approach to Self-Referential Paradoxes, Incompleteness and Fixed Points*. Bulletin of Symbolic Logic, 9(3), 362–386.*

[1] Garrabrant, S., & Eisenstat, S. (n.d.). Diagonalization fixed point exercises. Retrieved from https://www.lesswrong.com/posts/FZkLa3GRLW97fpknG/diagonalization-fixed-point-exercises

[2] Kelly, K. T. (n.d.). Computability and Learnability.

[3] Primitive recursive function. (2023). Retrieved from https://en.wikipedia.org/wiki/Primitive_recursive_function

[4] Smith, P. (2013). An Introduction to Gödel’s Theorems. Cambridge, England: Cambridge University Press.

[5] Solomon, E. (1958). Unpacking the diagonal lemma. Retrieved from https://math.stackexchange.com/questions/127852/unpacking-the-diagonal-lemma