Rice's theorem in plain words: no program can tell, for every program, whether something holds about its behaviour, such as “never charges money without an order”. About the code text you can answer; about the behaviour of any program you can't. So checks are narrowed by a frame inside which the answer is exact. Below: the predicate, the proof, where the trick comes from, three frames and the link to the CAP theorem.
The point: what Rice's theorem rules out
Rice's theorem says that all non-trivial semantic properties of programs are undecidable. Henry Gordon Rice proved it in his 1951 dissertation. The whole theorem sits in three words of that statement.
However clever the check, there is a program on which it answers wrongly.
How to read this in everyday terms. Imagine a checker who is handed any program and asked: “Will it ever charge money without an order?” The checker can be as clever as you like, work as long as you like and know every programming language. The theorem says there is still a program on which it answers wrongly or never answers. This is a property of the task itself, and improving the checker doesn't lift it, just as improving a design doesn't produce a perpetual motion machine.
It matters what the theorem leaves allowed. The ban covers one combination: “any program” plus “an exact yes or no” plus “a behavioural property”. Remove any one part and checking becomes possible again.
| What changes | What you get | Example |
|---|---|---|
| one specific program instead of any | its property can be proved by hand or by machine | a correctness proof of a sorting algorithm |
| allow “don't know” | the check honestly declines on hard cases | a code analyser that flags a doubtful spot |
| a text question instead of behaviour | the answer comes from reading the code | “are there no direct queries to someone else's table” |
| a small world instead of an infinite one | every state up to given numbers is tried | a Quint model for 3 rooms and 5 bookings |
Predicate: a question about the text or about behaviour
A predicate is a yes-or-no question about a program. There are two kinds, and Rice's theorem concerns only the second.
The code is the same. The difference is what you must look at to answer: a finite text or an infinite number of runs.
Why the word “semantic” is key here. A semantic property depends only on what the program does and not on how it is written. If two programs behave the same on every input, they get the same answer to a semantic question. Example: one program sums purchases with a loop, another with a library function. The texts differ, the behaviour is the same, and “can the sum be negative” has one answer for both. But “does the code contain a loop” has different answers, because it is a question about the text.
| Question | About | Decidable? |
|---|---|---|
| does the function have more than 50 lines | text | yes, by counting |
does the code call delete | text | yes, by search |
| does only one module write to the orders table | text | yes, by code search |
| will the program finish on every input | behaviour | no, in general |
| can the function ever return a negative sum | behaviour | no, in general |
| will the handler charge money without an order | behaviour | no, in general |
A close analogy is the imperative and declarative approaches. Code writes down steps, the “how”, while a rule about the result is a statement, the “what”. Rice's theorem says that in general you can't mechanically derive from the steps whether the statement about the result holds. A caveat: in a powerful declarative language behaviour can't be checked from the text either. Exact answers come from poor notations such as a database constraint, whose behaviour is visible in the notation itself.
The proof on one example
Checking the rule for would tell whether halts, and no program answers that.
The idea of the proof is a reduction. If one task could be solved, so could another that is already known to be unsolvable. So the first is unsolvable too. Here the first task is checking a behaviour rule and the second is the halting problem.
Take any program with input and build a new program from it:
Step breaks the rule, while a program that runs forever does not. So reaches the breach only if ever finishes:
If a check of the rule existed for every program, then given it would also answer whether halts. But Alan Turing proved that no program answers that question, the halting problem. So there is no universal rule check either. The rule can be anything: any non-trivial behavioural property fits in place of “money without an order”.
The same steps in order:
- Take any non-trivial behaviour rule. Since it is non-trivial, some program breaks it. The part of that program which breaks the rule is step .
- Take any program and its input for which the question is whether halts.
- Glue together: first , then . The gluing is a simple mechanical procedure you could write in a minute.
- Hand to the imagined universal check. Its answer “breaks” means “ halted”, and “doesn't break” means “didn't halt”.
- That is a solution to the halting problem, which doesn't exist. So the universal check doesn't exist either.
One caveat for precision. The construction assumes that a program which runs forever and does nothing doesn't break the rule. If that isn't so for some rule, take the opposite rule: checking it is no easier, and the argument repeats word for word.
Where the trick comes from: the halting problem and the barber paradox
Rice's theorem reduces any behaviour check to the halting problem and so inherits its undecidability. The halting problem in turn is proved with a trick over a hundred years old: a question about itself.
The contrarian asks the judge about itself and does the opposite, so the judge is wrong whatever it says.
Suppose there is a judge that says, for any program, whether it halts. Write a contrarian program: it asks the judge about itself and does the opposite. If the judge says “halts”, the contrarian loops forever; if it says “won't halt”, the contrarian stops at once. The judge is wrong either way, so it can't exist.
The barber shaves everyone who doesn't shave themselves. He himself fits into neither of the two sets.
The same trick sits in the barber paradox, a vivid version of Russell's 1901 paradox. The villagers split into set , those who shave themselves, and set , those the barber shaves, and everyone is in exactly one. The barber has no place: in he ends up in , in he ends up in . Russell has the same about sets: , and .
| Year | Who | What | Trick |
|---|---|---|---|
| 1901 | Bertrand Russell | the set of all sets that don't contain themselves | a question about itself |
| 1936 | Alan Turing | no judge can tell whether any program halts | the contrarian asks about itself |
| 1951 | Henry Gordon Rice | no check of any non-trivial behavioural property | reduction to halting |
Why a question about itself is so strong. A program is text too, so it can be fed to another program and even to itself. As soon as a checker can speak about any program, it must also speak about a program that asks it about itself and does the opposite. Then any answer it gives is wrong. The same move appears in the barber paradox, Russell's paradox and Gödel's incompleteness theorem.
What to do: three frames inside which the answer is exact
The theorem rules out a check for every program. For programs inside a frame a check is possible.
The theorem bans one combination of three conditions at once: the check must work for any program, on any behaviour question, and answer exactly “yes” or “no”. The proof relies on all three: it builds a program the check must accept, asks a behaviour question about it, and demands a definite answer. Each way removes one condition, and the proof stops working.
| Way | What is done in practice | Why the answer is exact | What is lost |
|---|---|---|---|
| Small world | write a Quint model of booking: 3 rooms, up to 5 bookings, actions “book” and “cancel”. A program tries every sequence of these actions and after each step checks the rule “bookings of one room never overlap” | there are finitely many variants, so the search reaches the end and either says “the rule holds” or shows the chain of steps where it broke | an error that appears only with 50 simultaneous bookings won't be seen by a search with 5 |
| Narrow language | the rule is written as a PostgreSQL constraint, and only the file booking/repository.ts may write to the bookings table. On every agent edit a search checks that no other file writes to the bookings table | the constraint is one line in the database schema and the database checks it on every write; a file search always finishes and gives an exact answer | the agent can't write bookings from wherever is convenient: even a bulk import of bookings has to go through the same allowed file |
| “Don't know” | the check on an agent's edit returns one of three answers: “holds”, “broken”, “can't tell”. The third answer puts a “needs a person” label on the edit | the check doesn't have to solve hard cases, such as a database query built from strings on the fly: there it honestly says “can't tell”, and where it says “holds” or “broken” the answer is exact | a developer spends time on edits labelled “needs a person” |
Each of the three answers is exact only within its limits. The search proves the rule holds in a model with 3 rooms and 5 bookings, not in the production database with thousands of bookings. The code search proves there is no write around the rule, not that the database constraint itself is written correctly. A “holds” answer is exact, but some edits still go to a person. So the ways are used together: the model checks the design before code, the constraint and the search check the code itself, and a person looks at whatever is left with a label.
Any check that works inside a frame makes errors of two kinds. It helps to decide in advance which one you can live with.
| Check error | What happened | When it is tolerable | Booking example |
|---|---|---|---|
| a miss | the check said “all fine”, but the rule is broken | when a breach is easy to notice and cheap to fix | tests checked booking by one client, while two clients simultaneously got room 2 at 10:00 |
| a false alarm | the check said “broken” or “can't tell”, but the rule holds | when a miss is costly: money, access, personal data | the search found a write to the bookings table in a data migration file that doesn't break the rule, and the edit was held until a person looked |
For rules about money and access, choose checks that raise false alarms but never miss a breach: a database constraint, type checking, a single writer per table. A developer clears an extra label in a few minutes, while a missed double charge comes back as a customer complaint and a refund.
In DDD terms it looks like this. The bounded context “Booking” talks only about rooms, bookings and time: it has no words like “payment” or “notification”. The rule “bookings never overlap” concerns one table, and one aggregate, “Booking”, writes to it. So “can the code break the rule” reduces to a text question: does anything other than the aggregate write to that table. That is the narrow language: the context's vocabulary is small, each word has one meaning, and the rule is checked by search. That is why the aggregate border is a convenient place for a check.
The link to the CAP theorem: a common core, different reasons
The CAP theorem is a well-known ban from distributed systems. When data lives on several servers and the link between them breaks, the system can't both answer every request and show everyone the same data. It has to choose: wait for the link to return, or answer from its own copy, which may be stale. Eric Brewer stated this in 2000; Seth Gilbert and Nancy Lynch proved it in 2002.
The common core. In both bans: while an event hasn't happened yet, you can't tell whether it will happen later or never, however long you wait. A server doesn't know whether a neighbour's message is lost or just late. A program check doesn't know whether the program will finish in a billion steps or never. While the waiting time is unbounded, both have exactly two ways out, and only one of them can be chosen: wait or answer now.
The top of the diagram is a fork between two mutually exclusive ways out. The bottom is a third path: it changes the condition that created the fork.
What the two branches of the fork look like in a network and in a program check:
| Way out | In a network (CAP) | In a program check |
|---|---|---|
| wait | the server waits for the link to return; the data is right, but there is no answer yet, and it may not come for a long time | the check waits for the program to finish, or answers “can't tell”; there is no error, but there may be no answer |
| answer now | the server answers from its own copy; there is an answer, but it may lack a fresh booking from the other server | the check answers from tests or a 1000-step run; there is an answer, but the program could break the rule at step 1001 |
The third path: bound the time. It removes the fork itself. If a message in the network is known to take no longer than 2 seconds, the server waits 2 seconds and then knows for sure: the message is lost. The answer both comes and is right. Programs are the same: if a language or model guarantees every program finishes within a known number of steps, the check runs it to the end and answers exactly. That is how the small world and the narrow language from the previous section work. The price is that the bound must be ensured in advance: a network with a guaranteed delay and a language in which an endless loop can't be written.
Different reasons. A CAP server lacks information: even an infinitely clever server can't learn what happens at its neighbour beyond the broken link. A program check has the information, the whole code is in front of it, but the answer can't be computed from that code. These are different kinds of impossibility, so Rice's theorem and CAP are not the same thing.
How strict this is. Both halves are established: Rice's theorem and the CAP proof are published. Matching the ways out is the author's observation; there is no single theorem for both cases. Brewer himself later warned that the “two of three” formula oversimplifies: in reality it is a scale, and the choice arises only during a partition. Checks are the same: what matters is how often a check says “don't know” or is wrong.
Frequently asked questions
No program can tell, for every program, whether something holds about its behaviour, if some programs have that property and others don't. About the code text you can answer; about the behaviour of any program you can't.
Any behaviour check can be turned into a question of whether a program halts: build a program that breaks the rule only if another program finishes. No program answers the halting question, so there is no behaviour check either.
Only a check for every program is impossible. Inside a frame — a small world, a narrow language or a “don't know” answer — the check is exact, and that is how Quint, database constraints and linters work.
The halting problem is proved with the same trick as the barber paradox and Russell's paradox: a question about itself. Rice's theorem inherits that trick by reducing to halting.