Rice’s theorem
An interactive tutorial
Turing famously showed that computers can’t decide whether your code halts. But in 1951, Henry Rice proved a much more devastating result: “computers can’t decide anything interesting about your code’s input-output!” In this chapter, you’ll learn exactly what he meant, and how he proved it, by building on Turing’s famous theorem.
Let’s start with a very practical question: can you replace a function with a regular expression? Imagine, one day at work, you come across this function in the codebase:
function isNumeric(str: string) {
  if (str === "") return false;
  for (const c of str) {
    if (!"0123456789".includes(c))
      return false;
  }
  return true;
}Hmm, you think ... could calls to this function be replaced by a regular expression test, like /a+/.test(str)?
Yeah, we could replace it with something like:
Actually, I think it could. We could replace it with something like this:
function isNumericRegex(s: string) {
  return /^[0-9]+$/.test(s);
}But wouldn’t it be cool if your IDE gave you a refactoring hint? Imagine: “function isNumeric can be replaced by a regular expression.” The IDE is smart enough to give us other hints, like “unused variable”. Would this regular expression test be much harder?
Warm-up: canBeRegex
If your IDE could make this decision, it would have an internal function like this:
function canBeRegex(funcStr: string) {
  /* Complex logic here */
}canBeRegex takes the source code for a function, analyzes it, and returns true or false, telling us whether the function could be replaced with a regular expression. For example, what should the following call return?
canBeRegex(`
  function isLen3(s: string) {
    return s.length === 3;
  }
`)Yes, it could be replaced by the regex ^...$, for example.
Actually, I think it should return true. It’s considering this function:
function isLen3(s: string) {
  return s.length === 3;
}This function could be replaced by the regex ^...$, or by ^.{3}$, either of which matches strings that have exactly three characters.
Let’s try another function, bad_isLen3:
function bad_isLen3(str: string) {
  for (let i = 0; i <= 99; i++);
  return str.length === 3;
}What should canBeRegex return when given the source code of bad_isLen3?
Yeah: bad_isLen3 does some useless upfront work, but otherwise has the same input-output behavior as isLen3.
Well ... yes, bad_isLen3 does some useless upfront work that a regex probably wouldn’t do. But we don’t want our IDE to fuss over the implementation details or efficiency; it should just tell us whether there’s a regex that has the same input-output as the function.
But now what about awful_isLen3:
function awful_isLen3(str: string) {
  for (let i = 0; i <= i; i++); // hmm
  return str.length === 3;
}Actually, this function is not replaceable by a regex. That for loop never halts, because i <= i is always true! This means that awful_isLen3 never halts, and its input-output is actually equivalent to the loop function:
Right, that for loop never halts! So awful_isLen3 never halts, and its input-output is actually equivalent to the loop function:
function loop(s) { while(true); }This is clearly not replaceable by a regex, so canBeRegex should return false on it. Perhaps you see the problem: doesn’t canBeRegex need to decide whether that first line halts?
That old halting problem again
In Chapter 1, we discovered the halting problem. Here’s a quick reminder:
function halts(code: string) { ... }The halts function is given some JavaScript code in string form, and it must tell us whether it halts. For example, what should this return?
halts(`
  let x = 1;
  while (x > 0) x = x / 2;
`)No, halts should always halt, even if the function that it’s given does not halt!
Alright, there are two reasonable answers here! If you think of x as a real or rational number,  x would never reach zero, so halts should return false. But in JavaScript floating-point numbers, it eventually gets to 5e-324 / 2, which evaluates to zero, so halts should return true!
The point is: deciding whether code halts is freaking hard. And in Chapter 1, we learned Turing’s proof that halts is actually impossible to implement: any implementation you write will have a bug!
So, isn’t canBeRegex impossible too? Put on your logician’s hat: what would we need to do to prove that canBeRegex is impossible to write?
No, but this is a common mistake! If you implement canBeRegex using halts, you’re just showing that your implementation can’t exist. Perhaps there’s a cleverer way to implement canBeRegex that doesn’t rely on using halts!
However, if you can implement halts using canBeRegex, you’ve got a strong argument that canBeRegex can’t exist. Let’s see how.
Right! If you implement halts using canBeRegex, you’ve got a proof by contradiction that canBeRegex can’t exist.
So, here’s an idea for implementing halts using canBeRegex:
function halts(code: string) {
  return canBeRegex(`
    function isLen3(s: string) {
      ${code}
      return s.length === 3;
    }
  `);
}Take your time and read it closely! We take our function isLen3, which we know can be replaced with a regex. Then we inject the code at the top of that function. Finally, we ask whether the function can still be replaced with a regex after this change.
Does this really solve the halting problem? Let’s check. First, assume that code does halt. Now read the above halts(code): what will it return?
Now assume that code does not halt. Then what will the above halts(code) return?
So this does solve the halting problem. But we know that’s impossible! We have a contradiction, so our initial assumption must be false. That is, canBeRegex cannot exist! This trick is the core idea of Rice’s theorem.
Code isolation
Now for a short interlude. Sharp-eyed readers might wonder whether the injected ${code} can “escape its sandbox”. Here’s a tricky example:
halts(`throw new Error("escape");`)The code throw new Error("escape") would halt with an error. But what would halts return, if we use the above implementation?
I think it would return false, because the following function cannot be replaced by a regex:
function (str: string) {
  throw new Error("escape");
  return str.length === 3;
}The bug is that raw string injection like ${code} does not isolate the code. So, when we write ${code}, think of this as a shorthand for running the code in an isolated sandbox, like this one.
canHalt: maybe an easier question?
So, we showed that we can’t implement canBeRegex, because a regular expression always halts, and we can’t tell whether a function halts.
But perhaps you’re thinking: “A regular expression always halts. So implementing canBeRegex is actually even harder than solving the halting problem. This is not so surprising.”
Well, let’s consider a new property, canHalt. Given a function’s source code, it tells us whether there is at least one argument that causes the function to halt. For example, what should this return?
canHalt(`function (n: number) {
  if (isPrime(n) && isEven(n)) return;
  while (true);
}`)Right, the input 2 would cause this function to halt, because 2 is a prime number and an even number.
I apologize! This was a bit tricksy! There’s one input that would cause this function to halt: the input 2, because 2 is both a prime number and an even number!
Now, this canHalt property does not imply the function always halts. So does Rice’s argument still work to show that canHalt cannot be written? Let’s try running it through the proof anyway! Assume we have canHalt, and then write:
function halts(code: string) {
  const t = `function ret42() {
    ${code}
    return 42;
  }`;
  return canHalt(t);
}Check this implementation of halts! If code halts, it should return true, and if code does not halt, it should return false. So, does it correctly solve the halting problem?
I think it does solve the halting problem, and here’s why.
First, assume code halts. Then we’re effectively asking whether this function can halt:
function ret42() {
  return 42;
}It certainly can halt (it always does), so halts would return true here.
Then assume code does not halt. Then it transforms ret42 into the loop function, which cannot halt, so halts would return false here.
Either way, halts returns the correct answer, so it does solve the halting problem.
So, similarly, canHalt is also impossible to implement! Here’s how we were able to prove it. We have two functions, loop and ret42:
// does NOT satisfy canHalt
function loop (x) { while (true); }
// DOES satisfy canHalt
function ret42 (x) { return 42; }By injecting the code at the top of the ret42 function, it either 
• transforms it into the loop function (if code does not halt), or
• leaves it unmodified (if code does halt).
We then use canHalt to distinguish whether the injected code transformed the ret42 function into loop, or left it alone. And this tells us whether code halts.
Final boss: numHaltsIsEven
Let’s try an even trickier function property: numHaltsIsEven. This considers how many distinct values you can pass to the function that would cause it to halt. It then returns true if that number is even.
As before, let’s write a function that will satisfy the property:
function haltIf4Or8(n: number) {
  if (n === 4 || n === 8) return;
  while (true) { }
}How many distinct values of n will cause haltIf4Or8 to halt?
There are exactly two values, 4 and 8, that cause this function to halt. And two is an even number, so this function should satisfy numHaltsIsEven.
Now let’s use our standard trick, and try to solve the halting problem by injecting the code at the start of haltIf4Or8:
function halts(code: string) {
  const t = `function haltIf4Or8(n) {
    ${code}
    if (n === 4 || n === 8) return;
    while (true) { }
  }`;
  return numHaltsIsEven(t);
}Use the same “proof-by-cases” technique, first assuming code halts, then assuming it doesn’t. Does the above solve the halting problem?
Actually, it doesn’t! Let’s check both cases. If code halts, then halts returns true, which is all good and correct.
But if code does not halt, what happens? Then the function analyzed by numHaltsIsEven is equivalent to the loop function. So, does the loop function have an even number of inputs that cause it to halt?
Yes: it has zero inputs that cause it to halt, and zero is an even number! So in this case, halts also returns true, which is incorrect!
Indeed! This version of halts always returns true! The cause is that the loop function has zero distinct inputs that cause it to halt, and zero is an even number.
But this is fixable. We just need to construct a function that does not satisfy numHaltsIsEven. Which of these will do the job?
function haltIf3Or7(n: number) {
  if (n === 3 || n === 7) return;
  while (true) { }
}
function haltIf3(n: number) {
  if (n === 3) return;
  while (true) { }
}No, haltIf3Or7 also has an even number of inputs that cause it to halt. So it also satisfies numHaltsIsEven. But we’re looking for a function that does not satisfy numHaltsIsEven.
The function haltIf3 has one input that causes it to halt, namely the input 3. One is not considered even, so this function does not satisfy numHaltsIsEven.
Right! haltIf3 has one input that causes it to halt, namely the input 3. One is not considered even, so this function does not satisfy numHaltsIsEven.
Second time lucky: let’s try again to implement halts, by injecting the code into haltIf3, then inverting the final result with !:
function halts(code: string) {
  const t = `function haltIf3(n) {
    ${code}
    if (n === 3) return;
    while (true) { }
  }`;
  return !numHaltsIsEven(t);
}If you check it, you should find that this correctly solves the halting problem — thus proving that numHaltsIsEven cannot exist!
Now for an any property P
Finally, you’re prepared to see Rice’s trick in its full generality. We’re given any function property P, like “can be a regex” or “number of halting inputs is even”. We want to show that a function isP cannot be written.
Rice’s trick
Assume, by contradiction, that isP exists.
We will find some function t, and then inject code at the top of it. If the loop function does not satisfy P, we set t to a function that does satisfy P; otherwise, we set t to a function that does not satisfy P.
We then test whether some arbitrary code halts, by asking isP: “would injecting code at the top of t convert it into the loop function?” But this answers the halting problem, so isP cannot exist!
Well ... almost. Henry Rice says there are two properties of P that make this trick work: first, P must be a semantic property, and second, it must be a non-trivial property. Let’s see what they mean.
Too many variables
Perhaps you’ve seen IDE warnings like: “Function f has more than 20 variables. Consider splitting into smaller functions.” But that sounds like a property of functions! Didn’t we just see those are impossible to decide?
Can we really write a function hasMoreThan20Vars that takes a function string and tells us whether it has more than 20 variables?
Sure we can! It reads through the source code, and counts every const x, var y, et cetera. No problem here! So, what’s different about this property? To see, let’s assume we have hasMoreThan20Vars, and try out Rice’s trick:
function halts(code: string) {
  const t = `function lotsOfVars(n) {
    ${code}
    const a,b,c,d,e,f,g,h,i,j,k,l,m,n,
      o,p,q,r,s,t,u,v,w,x,y,z;
    return;
  }`;
  return hasMoreThan20Vars(t);
}Use the usual proof-by-cases: does this solve the halting problem?
The function lotsOfVars starts with 26 variables, so it satisfies hasMoreThan20Vars. Injecting some code at the top doesn’t make any difference to that. It doesn’t tell us anything about whether code halts.
When Mr. Rice says P must be semantic, he means it must be solely concerned with the input and output of the function when called. That is: if two functions have the same input-output, then P cannot be true of one, and false of the other.
Let’s see a couple of example properties. Consider a property returnsInput, which tells us whether a function returns its own argument. For examples
// satisfies `returnsInput`
function id(x) { return x; }
// does not satisfy `returnsInput`
function increment(x) { return x+1; }Is the property returnsInput semantic?
I think it’s semantic. The property can be decided by only looking at the input-output table, e.g. 1 -> 1, "foo" -> "foo", etc.
(Bonus exercise: consider the property returnsOwnSourceCode, which tells us whether a function will return its own source code. Is returnsOwnSourceCode a semantic property? Will Rice’s trick work on it? Answers on a postcard!)
Solving the halting problem is trivial?!
The final property we’ll consider today is called solvesHaltingProblem. Given a function’s source code, it tells us whether that function solves the halting problem — that is, whether it’s a correct implementation of halts.
As always, let’s try to run Rice’s proof. First: does the loop function satisfy solvesHaltingProblem?
It does not! The loop function never terminates, let alone with a correct answer.
So, next, we must set t to a function that does satisfy solvesHaltingProblem. Can you find one?
Really?! If you can find such a t, please email it to me, because you’ve solved the halting problem!
The problem is, there is no such function — Turing taught us this. And so we can’t proceed with Rice’s proof.
If there are no functions that satisfy the property P, then we can’t use isP to decide anything, because it always returns false. Similarly, if every function satisfies P, then we can’t use isP to decide anything, because it always returns true.
This is what Rice means by triviality. The property P is trivial if all functions satisfy it, or if none do. To show that isP cannot be written, Rice’s proof requires that P is non-trivial: that it is satisfied by some, but not all, functions.
Conclusion
And this concludes the proof! Quoting Wikipedia: “Rice’s theorem states that all non-trivial semantic properties of programs are undecidable.” By now, you should understand what that theorem means, how to prove it, and why those “non-trivial semantic” qualifications are in there.
In the next chapter, we’ll be exploring Kurt Gödel’s second incompleteness theorem, which shows that proof systems can’t prove their own consistency! Stay tuned!
This chapter is free this week — to read the rest of Busy Beavers and support me writing it, please buy the course! Or if you want to know when the next chapter comes out, sign up for updates here. You might also enjoy our course Everyday Data Science, which uses similar interactive storytelling. Or if you’re feeling inspired, you can use TigYog to write your own course just like these ones!
Next in Busy Beavers!:
8. Just how busy can a busy beaver be?
Let’s play a game! We’ll write a JavaScript program of less than 10 characters that prints as much as possible before halting. Whoever’s program prints more is the busiest beaver, and they win the game.
What’s the busiest program? Just how busy is it? How can we find it? In this chapter, we’ll have a crack at these questions, but fair warning: we might not get many answers.