Egor Urvanov

Rice's theorem in plain wordsWhy no check works on every program and what to do about it

What Rice's theorem says and why checking a program's behaviour runs into the halting problem. Predicates about text and about behaviour, a proof on a payment example, the link to the barber paradox and three frames that make a check exact.

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.

a behaviour rule: “never charges money without an order”the checkany, however clever✓×✓×✓?programsthere is always a programwhere the answer is wrongabout code text you can answer; about the behaviour of any program you can’t; the way out is a narrower frame● model, people, systems, data  ● agent and harness  ● risk, stop, boundaryFor any behaviour check there is a program on which it is wrong

However clever the check, there is a program on which it answers wrongly.

Meaning
Example
Semantic
a property of behaviour, of what the program does when it runs
“never charges money without an order”, “never writes to someone else's table”
Non-trivial
some programs have the property, others don't
one payment program checks the order, another forgot to
Undecidable
no program answers “yes” or “no” for every program without ever being wrong
a universal checker for “money without an order” doesn't exist

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 changesWhat you getExample
one specific program instead of anyits property can be proved by hand or by machinea correctness proof of a sorting algorithm
allow “don't know”the check honestly declines on hard casesa code analyser that flags a doubtful spot
a text question instead of behaviourthe answer comes from reading the code“are there no direct queries to someone else's table”
a small world instead of an infinite oneevery state up to given numbers is trieda Quint model for 3 rooms and 5 bookings
Takeaway
In this exampleAn agent brought 2,000 lines of payment code. No linter, review or other model can guarantee for arbitrary code that it never charges money without an order.
In generalThis limit is mathematical. Improving the check doesn't lift it as long as the check must work for every program.
Next stepFor each rule you want to check, first ask: is it about the code text or about behaviour?

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.

codequestionwhat must be examinedanswerhow it is written40 linesdoes the code writeto the payments table?lines: 1, 2 … 40 — the endfinite: read them allyes or nowhat comes outthe same codewill it ever charge moneywithout an order?input 1input 2input 3input 4… ∞infinitely many runs, some never end?undecidableif some programs say “yes”and others “no”● model, people, systems, data  ● agent and harness  ● risk, stop, boundaryHow it is written: read a finite list of lines. What comes out: go through every run, and there are infinitely many

The code is the same. The difference is what you must look at to answer: a finite text or an infinite number of runs.

How it is written
What comes out
Question
does the code write to the payments table?
will the program ever charge money without an order?
What to examine
lines of code; finitely many, all can be read
every run with every input; infinitely many, some never end
Answer
always available
in general, none

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.

QuestionAboutDecidable?
does the function have more than 50 linestextyes, by counting
does the code call deletetextyes, by search
does only one module write to the orders tabletextyes, by code search
will the program finish on every inputbehaviourno, in general
can the function ever return a negative sumbehaviourno, in general
will the handler charge money without an orderbehaviourno, 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.

Takeaway
In this example“Only the payment module writes to the payments table” is a text question; a code search checks it. “A payment never goes through without an order” is a behaviour question.
In generalA behaviour question can often be replaced with a text question if the code is written so that only one place can break the rule, such as one file that writes to the payments table.
Next stepRewrite important rules so they become text questions: a single writer per table, a database constraint, the rule in one place.

The proof on one example

why: such a check would solve the halting problemP(w)any programP(w)B: breakQ: first P(w), then Brule check“Q breaks it”= “P(w) halts”and that is undecidable● model, people, systems, data  ● agent and harness  ● risk, stop, boundaryChecking the rule for Q would tell whether P(w) halts, and that is undecidable

Checking the rule for QQ would tell whether P(w)P(w) 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 PP with input ww and build a new program QQ from it:

Q(x)  =  run P(w); when it finishes, run B(x)Q(x) \;=\; \text{run } P(w)\text{; when it finishes, run } B(x)

Step B(x)B(x) breaks the rule, while a program that runs forever does not. So QQ reaches the breach only if P(w)P(w) ever finishes:

Q breaks the rule  ⟺  P(w) haltsQ \text{ breaks the rule} \iff P(w) \text{ halts}

If a check of the rule existed for every program, then given QQ it would also answer whether P(w)P(w) 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:

  1. 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 BB.
  2. Take any program PP and its input ww for which the question is whether P(w)P(w) halts.
  3. Glue QQ together: first P(w)P(w), then BB. The gluing is a simple mechanical procedure you could write in a minute.
  4. Hand QQ to the imagined universal check. Its answer “breaks” means “P(w)P(w) halted”, and “doesn't break” means “didn't halt”.
  5. 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.

Takeaway
In this exampleAn agent wrote a payment handler: first it calls a discount calculation from someone else's library, then it charges money without checking that an order exists. Whether it breaks the rule depends only on whether the discount calculation finishes: if it hangs forever, the charge is never reached.
In generalTo answer exactly from the code, a check would have to decide whether that calculation halts. It is the same scheme: P(w)P(w) is the discount calculation, BB is the charge without an order.
Next stepClose such a rule with a frame: a database constraint refuses to record a charge without an order, whatever the code did before it.

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.

contrarian“judge, will I halt?”judge“halts”“won’t halt”the contrarian loops foreverthe judge was wrongthe contrarian stops at oncethe judge was wrongas in the barber paradox: a question about itself● model, people, systems, data  ● agent and harness  ● risk, stop, boundaryThe contrarian asks the judge about itself and does the opposite: the judge is wrong whatever it says

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.

AA = shave themselvesthe barber doesn’t shave themBB = shaved by the barberrule: B is everyone not in Athe barber is a villager too?in A → shaves himself,so the barber shaves him → he is in Bin B → the barber shaves him,that is, himself → he is in Aevery villager is in exactly one set; the barber has no place● model, people, systems, data  ● risk, stop, boundaryEvery villager is in exactly one set, A or B. The barber is in neither: put him in A and he lands in B, and vice versa

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 AA, those who shave themselves, and set BB, those the barber shaves, and everyone is in exactly one. The barber has no place: in AA he ends up in BB, in BB he ends up in AA. Russell has the same about sets: R=x:x∉xR = {x : x \notin x}, and R∈R  ⟺  R∉RR \in R \iff R \notin R.

YearWhoWhatTrick
1901Bertrand Russellthe set of all sets that don't contain themselvesa question about itself
1936Alan Turingno judge can tell whether any program haltsthe contrarian asks about itself
1951Henry Gordon Riceno check of any non-trivial behavioural propertyreduction 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 way out: narrow the check with a framesmall world3 rooms, 5 bookings: try everythingnarrow languageask about text, not behaviour?“don’t know”a person or a test decides● model, people, systems, data  ● agent and harness  ● risk, stop, boundaryThree ways to narrow a check so 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 QQ 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.

WayWhat is done in practiceWhy the answer is exactWhat is lost
Small worldwrite 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 brokean error that appears only with 50 simultaneous bookings won't be seen by a search with 5
Narrow languagethe 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 tablethe 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 answerthe 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 editthe 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 exacta 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 errorWhat happenedWhen it is tolerableBooking example
a missthe check said “all fine”, but the rule is brokenwhen a breach is easy to notice and cheap to fixtests checked booking by one client, while two clients simultaneously got room 2 at 10:00
a false alarmthe check said “broken” or “can't tell”, but the rule holdswhen a miss is costly: money, access, personal datathe 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.

Takeaway
In this exampleThe rule “bookings of one room never overlap”: a Quint model tries every command order for 3 rooms, a database constraint forbids overlap, and a text check makes sure only one module writes to the bookings table.
In generalThe three frames are usually combined: the small world catches design errors, the narrow language pins the rule in code, and “don't know” shows where a person is needed.
Next stepFor each rule, mark which frame holds it. A rule with no frame is only checked by example tests.

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.

client“how many bookings?”server 1server 2the fresh booking is here×link brokenC: wait for the linkdata is right, no answer yetA: answer nowan answer, but without the fresh bookingwait or guess:like a program check● model, people, systems, data  ● agent and harness  ● risk, stop, boundaryThe link between servers is broken: either wait and not answer, or answer from your own copy, which may be stale

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.

waiting time is unboundedhas the neighbour answered?will the program finish?pick one of twowaitthe answer will be right,but it may never comeanswer nowthere is always an answer,but it may be wrong×one rules out the otheror change the condition itselfbound the time: wait no longer than a limitafter the limit, “never” is already known,so the answer both comes and is right● model, people, systems, data  ● agent and harness  ● risk, stop, boundary

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 outIn a network (CAP)In a program check
waitthe 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 timethe check waits for the program to finish, or answers “can't tell”; there is no error, but there may be no answer
answer nowthe server answers from its own copy; there is an answer, but it may lack a fresh booking from the other serverthe 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.

Takeaway
In this exampleA checking agent waits for a test run that has hung. Waiting longer is like a server waiting for the link; cutting off on a timeout and saying “passed” is like a server answering from a stale copy.
In generalWaiting and guessing are the shared ways out for both bans, and the reliable way out in both cases is the same: bound the time in advance.
Next stepGive every automatic check a time limit and decide in advance what exceeding it means: “don't know” handed to a person, or “broken”.

Frequently asked questions

What is Rice's theorem in plain words?
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.
How is Rice's theorem related to the halting problem?
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.
So checking programs is pointless?
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.
What does the barber paradox have to do with it?
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.
© 2026 Egor Urvanov