How Can We Know a Program Is Correct?
Published:
A program runs.
It produces output.
It passes tests.
Is it correct?
That question is deeper than it first appears.
To call a program correct, we need more than successful execution.
We need a specification of what the program is supposed to do.
Correct Relative to What?
A program cannot be correct in the abstract.
It is correct relative to a specification.
Suppose a sorting function receives:
[ [a_1,a_2,\ldots,a_n] ]
A reasonable specification might require:
- output contains exactly the same elements,
- output is in nondecreasing order.
Only then can correctness be evaluated.
Specification Comes First
Without a specification, observed behavior may look acceptable while violating the actual goal.
A program that always returns:
42
is perfectly predictable.
It is not a correct sorting algorithm.
Correctness is a relation between:
- implementation,
- intended behavior.
Functional Correctness
Functional correctness asks whether the program returns the right result.
For a function computing factorial:
[ f(n)=n! ]
we want:
[ program(n)=n! ]
for every valid n.
Partial Correctness
A program is partially correct if:
whenever it terminates,
its result satisfies the specification.
This does not guarantee termination.
An infinite loop can be partially correct in a vacuous sense if it never returns an incorrect result.
Total Correctness
Total correctness requires:
- partial correctness,
- termination.
So the program must:
- eventually finish,
- produce the correct result.
This is usually what users mean by “works.”
Testing
The most common way to gain confidence is testing.
Run the program on selected inputs.
Compare actual output with expected output.
Testing is essential.
But testing has limits.
Dijkstra’s Observation
A famous principle attributed to Edsger Dijkstra is that testing can reveal the presence of bugs, but not prove their absence.
The reason is simple.
A program may have infinitely many possible inputs or states.
A finite test suite samples only some of them.
Exhaustive Testing
For small finite domains, exhaustive testing may be possible.
A Boolean function with three inputs has only:
[ 2^3=8 ]
cases.
Test all eight.
But real systems often have enormous state spaces.
State Explosion
Suppose a system has 100 Boolean state variables.
Then there are:
[ 2^{100} ]
possible configurations.
Exhaustive testing becomes impossible.
Combinatorial explosion limits brute-force confidence.
Edge Cases
Good testing seeks inputs likely to reveal mistakes:
- empty input,
- minimum values,
- maximum values,
- duplicate values,
- malformed input.
Boundary conditions often expose hidden assumptions.
Property-Based Testing
Instead of listing expected outputs manually, property-based testing specifies general properties.
For sorting:
- result is ordered,
- result is a permutation of input.
The testing framework generates many examples.
This improves coverage.
But it still does not prove universality.
Random Testing
Random inputs can expose unexpected bugs.
They are especially useful when the input space is large.
Yet randomness can miss rare failures.
Probability of detection is not proof of absence.
Fuzzing
Fuzzing generates unusual, malformed, or adversarial inputs.
It has become extremely effective in security testing.
Fuzzers discover crashes that human-written tests may overlook.
Again, discovery of bugs is easier than certification of no bugs.
Static Analysis
Static analysis examines code without executing every possible run.
It can infer properties such as:
- variable initialization,
- possible null references,
- unreachable code.
Static analysis trades precision against tractability.
Sound Analysis
A sound analyzer may guarantee:
If it says a certain error cannot occur, then the error really cannot occur within the model.
To remain sound, it may produce false alarms.
Incomplete Analysis
Because general semantic program properties are undecidable, no static analyzer can be both:
- perfect,
- universally applicable
for arbitrary programs.
Undecidability becomes an engineering fact.
Assertions
An assertion states a condition expected to be true at some point.
Example:
assert balance >= 0
Assertions convert assumptions into executable checks.
They make hidden expectations visible.
Preconditions
A precondition describes what must hold before a function runs.
For division:
[ b\neq0 ]
may be required.
Correctness can depend on callers satisfying the precondition.
Postconditions
A postcondition describes what must hold after execution.
For:
[ max(a,b) ]
we might require:
[ result\geq a ]
and:
[ result\geq b ]
and:
[ result=a \lor result=b ]
Hoare Triples
Tony Hoare introduced a notation:
[ {P}\ C\ {Q} ]
where:
- P is precondition,
- C is program,
- Q is postcondition.
It means:
If P holds before C, and C terminates appropriately, then Q holds afterward.
Example
For:
x := x + 1
we may write:
[ {x=4}\ x:=x+1\ {x=5} ]
This turns program behavior into logical reasoning.
Hoare Logic
Hoare logic provides rules for reasoning about:
- assignments,
- sequences,
- conditionals,
- loops.
Programs become mathematical objects.
Correctness becomes proof.
Loop Invariants
Loops are especially difficult.
A loop invariant is a property true:
- before the loop,
- after every iteration.
At termination, the invariant helps establish the postcondition.
Example: Summation
Suppose after k iterations:
[ sum=a_1+\cdots+a_k ]
If this invariant is preserved, then when k reaches n:
[ sum=a_1+\cdots+a_n ]
The invariant compresses reasoning across arbitrarily many iterations.
Termination Proofs
Correct output is not enough.
We also need to show a loop terminates.
A common method uses a variant or ranking function that:
- decreases each iteration,
- cannot decrease forever.
Natural numbers provide a well-founded order.
Structural Recursion
Functional programs often guarantee termination by recursive calls on structurally smaller data.
A tree function calls itself on subtrees.
The structure itself provides a termination measure.
Formal Verification
Formal verification uses mathematical proof to establish program properties.
Instead of testing examples, we prove:
for every allowed input,
the specification holds.
This can provide extremely strong assurance.
Proof Assistants
Systems such as:
- Coq,
- Lean,
- Isabelle
allow machine-checked proofs.
A human constructs or guides the proof.
A small trusted kernel checks correctness.
Model Checking
Model checking explores state-transition models automatically.
It verifies properties such as:
- safety,
- liveness,
- deadlock freedom.
For finite-state systems, model checking can be exhaustive.
Temporal Logic
Properties of reactive systems often concern time.
Examples:
- request is eventually answered,
- two processes are never simultaneously in a critical section.
Temporal logic expresses such requirements.
Safety
A safety property says:
something bad never happens.
Example:
Two trains never occupy the same track section.
A finite bad prefix can often demonstrate failure.
Liveness
A liveness property says:
something good eventually happens.
Example:
Every accepted request eventually receives a response.
Liveness is different from absence of error.
Refinement
Large systems can be verified through refinement.
Start with an abstract specification.
Gradually introduce implementation detail.
At each step, prove behavior is preserved.
This manages complexity.
Type Systems
Type systems are lightweight formal verification.
A compiler may prove properties such as:
- this operation receives an integer,
- this reference cannot be null,
- this value has the required ownership.
Types eliminate whole categories of mistakes.
Dependent Types
Dependent type systems allow types to express richer properties involving values.
For example, a vector type might encode its length.
Programs and proofs begin to merge.
Curry–Howard Returns
The Curry–Howard correspondence relates:
- propositions to types,
- proofs to programs.
A program inhabiting a type can serve as evidence for a proposition.
Logic and computation meet again.
Verified Compilers
A verified compiler has machine-checked proofs that compilation preserves specified program semantics.
This reduces one part of the trusted computing base.
But hardware and specifications still matter.
Specification Bugs
A program can be verified against the wrong specification.
Formal proof then guarantees the wrong thing perfectly.
Verification cannot rescue a mistaken goal.
The Specification Problem
Suppose an autonomous vehicle is specified to:
“minimize travel time.”
The implementation may satisfy this exactly.
But the specification may omit:
- safety,
- comfort,
- legality.
Correctness is only as meaningful as the specification.
Security Correctness
Security introduces adversaries.
A program may be functionally correct yet vulnerable to:
- side channels,
- injection,
- privilege escalation.
Security properties require explicit threat models.
Probabilistic Programs
Some algorithms are randomized.
Correctness may be expressed probabilistically:
[ Pr[\text{correct output}] \geq 1-\epsilon ]
Verification must then reason about distributions.
Numerical Programs
Floating-point arithmetic introduces approximation.
A mathematically correct real-number algorithm can behave differently in finite precision.
Correctness may require error bounds.
Concurrency
Concurrent programs are especially hard to reason about.
Possible interleavings can explode combinatorially.
Bugs may appear only under rare timing conditions.
Race Conditions
A race condition occurs when result depends on uncontrolled ordering of concurrent events.
Traditional deterministic testing may miss it.
Correctness must include synchronization properties.
Distributed Systems
Distributed systems introduce:
- delay,
- message loss,
- partial failure.
No component sees the whole system instantly.
Correctness must be defined under explicit assumptions about the network.
Fault Models
A distributed protocol may be correct if:
- at most f nodes fail,
- messages are eventually delivered.
Change the assumptions and the proof may no longer apply.
Correctness is conditional.
The Real Meaning of a Proof
A correctness proof establishes:
Given these assumptions,
this implementation satisfies this specification.
That is enormously valuable.
It is not metaphysical certainty about the entire physical world.
Hardware Faults
Formal software proofs usually assume hardware behaves according to a model.
Cosmic rays, manufacturing faults, or malicious hardware may violate it.
Every proof has a boundary.
Compiler and Toolchain
Source-level correctness may depend on the compiler preserving semantics.
Compilers themselves can have bugs.
Verified toolchains reduce this risk.
Trust can be narrowed but not eliminated entirely.
Human Review
Code review remains useful because many failures involve:
- misunderstood requirements,
- maintainability,
- architecture.
Formal methods answer precise questions.
Humans decide which questions matter.
Testing and Proof Are Complementary
Testing is excellent for:
- discovering unexpected behavior,
- validating assumptions about real environments.
Proof is excellent for:
- universal claims inside a model.
Strong engineering uses both.
The Philosophical Lesson
Program correctness is never just:
“It seems to work.”
It is a structured relationship among:
- specification,
- implementation,
- assumptions,
- proof or evidence.
Formal verification can provide remarkable certainty.
But certainty is always relative to what has been formalized.
The Next Question
Computer science gives us models powerful enough to:
- predict,
- simulate,
- verify,
- explain.
But how far can computational explanation go?
Can computation explain:
- life,
- mind,
- physical law?
Or is it only one level among many?
The next essay asks:
Can Computation Explain Nature?
