AnyLearn
All lessons

What Undecidability Costs Real Tools

Rice's theorem closes every interesting question a type checker, linter or verifier wants to answer, yet those tools exist and work. They work by choosing which way to be wrong. This lesson covers the sound and unsound bargains, the provable approximation in pointer analysis, and the option of leaving Turing completeness behind on purpose.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Every useful tool asks a forbidden question

Line up what the tooling on a normal project is trying to establish. Will this array index go out of bounds? Can this pointer be null here? Is this lock always released? Does this refactoring preserve behaviour? Does this loop terminate?

Every one of those is a semantic property of the program, and every one is non-trivial, so Rice's theorem says no algorithm decides any of them.

And yet the tools ship, run in continuous integration, and catch real defects. There is no loophole involved. What they abandon is the requirement that made the question impossible: answering correctly on every program. Drop either half and the space of achievable tools opens up immediately.

The engineering question is therefore not whether to be wrong. It is which kind of wrong to be, and that choice is the single most important fact about any analysis tool.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Every useful tool asks a forbidden question

Line up what the tooling on a normal project is trying to establish. Will this array index go out of bounds? Can this pointer be null here? Is this lock always released? Does this refactoring preserve behaviour? Does this loop terminate?

Every one of those is a semantic property of the program, and every one is non-trivial, so Rice's theorem says no algorithm decides any of them.

And yet the tools ship, run in continuous integration, and catch real defects. There is no loophole involved. What they abandon is the requirement that made the question impossible: answering correctly on every program. Drop either half and the space of achievable tools opens up immediately.

The engineering question is therefore not whether to be wrong. It is which kind of wrong to be, and that choice is the single most important fact about any analysis tool.

2. The two ways to be wrong

Fix a property, say "this program never dereferences null". An analyser reports either safe or warning. Two error modes exist and they cannot both be eliminated.

A false alarm is a warning on code that is actually fine. A missed bug is a clean report on code that is actually broken.

An analyser is called sound for the property when it has no missed bugs: if it says safe, the code is safe. The cost is false alarms, and the cost is not optional. It is the undecidability, paid in a currency the tool can survive.

An analyser that instead eliminates false alarms will miss bugs. Silence from it means nothing was found, not that nothing is there.

Both are legitimate. They serve different jobs, and confusing them is how teams end up trusting a tool for a guarantee it never claimed.

3. Which way the tool is allowed to fail

Three exits from the same impossibility, and a tool takes exactly one.

The first two keep the language as it is and give up on one error mode. They are duals, and the trade is quantitative: pushing false alarms down on a sound analyser means weakening its abstraction until it starts missing things.

The third exit is different in kind. Rather than approximating the answer, it removes the programs that make the question hard. If a language cannot express unbounded recursion, termination is decidable for it outright, and no approximation is needed.

The choice is visible in every tool's documentation if you know to look for it, and it is usually stated as what the tool guarantees rather than as which error it tolerates.

flowchart TD
A["Undecidable property"] --> B["Refuse to miss anything"]
A --> C["Refuse to cry wolf"]
A --> D["Change the language"]
B --> E["Sound: safe means safe, expect false alarms"]
C --> F["Bug finder: quiet does not mean safe"]
D --> G["Restricted language: the question becomes decidable"]

4. Sound analysis, running on aircraft

The sound branch is not a theoretical position. Astree is a static analyser for C built on abstract interpretation, and it is designed to prove the absence of runtime errors: overflow, division by zero, out-of-bounds access, and similar.

It is sound, so a clean report is a proof rather than an absence of evidence. It was applied to the primary flight control software of the Airbus A340 and later the A380, and on that target it reached the outcome that makes sound analysis practical rather than merely correct: zero false alarms.

That number deserves attention, because a sound analyser is entitled to produce false alarms and usually does. Reaching none is not a consequence of soundness. It comes from tuning the abstract domains to the specific shape of the code being analysed, which is feasible for control software generated from synchronous specifications and much harder for arbitrary C.

5. Pointer analysis: approximate by proof, not by laziness

It is tempting to read every imprecise answer as an implementation shortcut. For pointers there is a theorem saying otherwise.

William Landi established the result in "Undecidability of Static Analysis", ACM Letters on Programming Languages and Systems, volume 1, issue 4, 1992, pages 323 to 337. In a language with conditionals, loops, dynamic allocation and recursive data structures, precise alias information cannot be computed. The may-alias relation is not recursive, and the must-alias relation is not even recursively enumerable.

G. Ramalingam sharpened and extended the treatment in "The Undecidability of Aliasing", ACM Transactions on Programming Languages and Systems, volume 16, issue 5, 1994, pages 1467 to 1471.

Read the two relations against the tiers from the previous lesson. Must-alias sits in the bottom tier, so it cannot even be confirmed by unbounded search. Any tool reporting that two pointers definitely alias is reporting a sufficient condition, never the exact answer.

6. What conservatism looks like in code

Undecidability shows up in ordinary review comments. Consider a checker deciding whether the indexing below is in bounds.

def pick(xs, flag):
    i = 0
    while collatz_step(i) != 1:    # halts? nobody knows in general
        i += 1
    return xs[i] if flag else xs[0]

To prove xs[i] safe the checker must bound i, which means knowing when that loop exits, which is the halting problem. A sound checker therefore warns, even if a human can see the loop is fine for the inputs that actually occur.

This is the everyday texture of the theory. The warning is not a bug in the tool and no amount of engineering removes it. It is why sound analysers offer annotations, assumptions and suppression: those are channels for a human to supply the fact the tool provably cannot derive.

7. The third exit: give up Turing completeness on purpose

The undecidability results all assume the language can express arbitrary computation. Remove that and they stop applying.

Proof assistants take this exit deliberately. In Coq, Agda and Lean, every function must be shown to terminate, usually by structural recursion on an argument that provably shrinks. The resulting language is total: it cannot express a non-terminating program, so termination is trivially decided, and the cost is that some genuinely terminating algorithms must be restructured or proved terminating by hand before they are accepted.

The same bargain appears far outside verification, usually without being named. Regular expressions are not Turing complete, which is exactly why matching can be linear-time. A configuration format that cannot loop is a format whose evaluation always terminates.

Every one of these is the same trade: expressive power surrendered in exchange for a guarantee that is unobtainable above the line.

8. Reading a tool by the bargain it struck

Tool classRefuses toAcceptsSo a clean run means
Sound verifierMiss a real errorFalse alarmsThe property is proved
Bug finder, linterCry wolfMissed bugsNothing was found
Static type systemAccept some bad programsRejecting some good onesType errors are excluded, others are not
Test suiteReport untriggered faultsMissed pathsThe covered paths passed
Total languageAllow non-terminationReduced expressivenessTermination is guaranteed

The fourth column is the one to read first, because it is what the tool actually licenses you to believe.

Most production setups combine rows rather than picking one, and the combination is coherent precisely because the rows fail differently. A bug finder and a test suite both stay silent for reasons that are not evidence of correctness. A sound verifier's silence is evidence, and it is bought with alarms elsewhere.

9. What the limit actually bought

The arc of this cursus is one narrowing question. Finite states could not count. A stack could count one thing at a time. A tape removed the memory limit entirely, and what remained was not a shortage of resources but a genuine impossibility, provable in eight lines and generalised by Rice to every semantic property at once.

The practical yield is not pessimism. It is that a whole family of proposals can be dismissed without investigation. A tool that promises exact answers to a semantic question about arbitrary programs is either restricting the language, approximating in one direction, or mistaken, and knowing which takes one question rather than a pilot project.

The theory does not say what cannot be built. It says precisely what any build must give up, and that turns an open-ended engineering argument into a choice with three named options.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. An analyser is sound for 'never dereferences null'. What does a clean report license you to believe?
    • The code is proved free of null dereferences, though the tool may warn elsewhere without cause
    • The tool found no evidence of a null dereference, but one may exist
    • The code has no bugs of any kind
    • The code contains no pointers
  2. Astree reached zero false alarms on Airbus primary flight control software. Why is that not simply a consequence of being sound?
    • Soundness forbids false alarms by definition
    • Sound analysers are entitled to false alarms; reaching none came from tuning the abstract domains to that specific class of code
    • The software was small enough to check exhaustively
    • The analysis was unsound on that particular target
  3. Landi's 1992 result placed must-alias in which tier?
    • Decidable, but only in exponential time
    • Recognisable, so unbounded search confirms it eventually
    • Decidable for programs without recursion only
    • Not even recursively enumerable, so unbounded search cannot confirm it
  4. Why is termination decidable in Coq, Agda and Lean?
    • They analyse loops with a more powerful abstract interpretation
    • They bound every program's running time at compile time
    • Their total fragment cannot express a non-terminating program, so the question no longer applies to arbitrary computation
    • They solve the halting problem for a restricted set of inputs
  5. A linter that avoids false positives reports nothing on a module. What follows?
    • Nothing about correctness; it accepts missed bugs, so silence is not evidence of safety
    • The module is free of the defects the linter targets
    • The module compiles without warnings
    • The module's semantic properties have been proved

Related lessons

Programming
advanced

Refs: A Branch Is a File Containing a Hash

Hashes are unusable by hand, so Git names them. A branch is not a copy of anything, not a directory, and not a container for commits: it is a forty-character file. Once that is clear, checkout, reset, detached HEAD and the reflog stop being separate concepts and become one operation on one kind of file.

8 steps·~12 min
Programming
intermediate

The Object Database: Git Is a Content-Addressed Store

Git is usually taught as a set of commands. Underneath it is a key-value store where the key is a hash of the content, and four object types built on that one idea. This lesson derives the hash by hand, shows what a commit actually contains, and explains why the design makes history tamper-evident.

8 steps·~12 min
Programming
advanced

The Design Philosophy: What Go Left Out

Go is unusual for what it refuses to include. Errors are values, not exceptions. Interfaces are satisfied without declaring it. Formatting is not a choice. This lesson works through the omissions, the reasoning behind each, and the real costs, because every one of them trades expressiveness for something else.

8 steps·~12 min
Programming
advanced

Channels: Share Memory by Communicating

Cheap concurrency without a coordination primitive is a bug generator. Go's answer is a typed conduit that carries values between goroutines, from a 1978 idea: rather than protecting shared state with locks, pass ownership through a channel so there is no sharing to protect. This lesson builds channels, select, and where the model does not fit.

8 steps·~12 min