The program that cannot exist

We formalise the notion of a computational problem, introduce decidability, semi-decidability, and Turing reductions, and prove the undecidability of the Halting Problem via a self-referential diagonal argument. Along the way several related undecidable problems are established — HaltsOnEmpty and GuessConsistent — which will become the direct technical tools for the incompleteness proof.

32 min read

Computer science is no more about computers than astronomy is about telescopes.

Edsger W. Dijkstra, as quoted in Haines et al. (1993)1

In the previous chapter, we mentioned that Turing machines and Python programs are equivalent in terms of the "problems" they can solve. In this chapter, we will formally define the notion of a problem and delve into one of the central axes of computation theory: decidability (Section 3.1).

Next, we will introduce the universal machine (Section 3.2), a concept Turing introduced when presenting his model of computation2. This machine will help us find undecidable problems (Section 3.3). Finally, we will introduce the concept of reduction (Section 3.5), which will allow us to find even more undecidable problems.

We will do all of this to deduce the main result of this chapter: the undecidability of the Halting Problem (Section 3.6). This problem is extremely relevant, not only historically, but because it will be one of the pillars of the main proof of this work, which we will carry out in Chapter 5.

3.1 #Decidable problems

We begin, as anticipated, by precisely defining what a problem is.34

Definition3.1—Computational problem

A computational problem (or problem)5 FF is a function F:X⟶YF : X \longrightarrow Y, where:

  • XX is the set of inputs. An element x∈Xx \in X is called an input.
  • YY is the set of solutions. An element y∈Yy \in Y is called a solution.

Given an x∈Xx \in X, we call F(x)F(x) the solution to xx.

A problem is nothing more than a function that maps inputs to outputs, which we call solutions. Any problem can be reduced to this terminology, no matter how complex it may be. Let us see the first description of a problem.

Problem3.2—HamiltonianCycle\textsc{HamiltonianCycle}
  • Input: a graph GG.
  • Solution: the Hamiltonian cycles present in GG.

Let us define the input and output sets of Problem 3.2. The input to our problem is a graph GG, which is nothing more than a set of vertices VV and an adjacency matrix AA representing its edges (G=(V,A)G = (V, A)). As output, we will have a set of nn paths in GG, π1,π2,…,πn\pi_1, \pi_2, \ldots, \pi_n (it is possible that a graph has none, one, or several Hamiltonian cycles). We can specify the paths as ordered subsets of VV.

HAMILTONIANCYCLEGf¼1;¼2;:::;¼ng
Figure 3.1. Scheme of HamiltonianCycle\textsc{HamiltonianCycle}

To prove the results of our work, we will use a subset of problems: those whose output is only yes or no. We will call them decision problems.

Definition3.3—Decision problem

Given an alphabet AA, a decision problem6 is a computational problem F:X⟶YF : X \longrightarrow Y where Y={yes,no}Y = \{\texttt{yes}, \texttt{no}\}.

Any computational problem can be "transformed" into a decision problem by asking about the existence of solutions. An example of this is Problem 3.4.

Problem3.4—HamiltonianCycleExists\textsc{HamiltonianCycleExists}
  • Input: encoding of a graph GG.
  • Solution: whether there is at least one Hamiltonian cycle in GG.

This problem is a decision problem, since it does not explicitly ask which Hamiltonian cycles are present in a given graph, but rather whether a Hamiltonian cycle exists in it. Evidently, these problems are more restrictive than general computational problems, but they are of much greater interest when discussing which problems can be computed, as we will see later.

HAMILTONIANCYCLEEXISTSGfyes;nog
Figure 3.2. Scheme of HamiltonianCycleExists\textsc{HamiltonianCycleExists}

In the case of decision problems, it makes sense to speak of their complement.

Definition3.5—Complementary problem

Given a decision problem DD, we define its complementary problem (or contrary problem), denoted D‾\overline{D}, as:

D‾(a)=yes  ⟺  D(a)=no,D‾(a)=no  ⟺  D(a)=yes\overline{D}(a) = \texttt{yes} \iff D(a) = \texttt{no}, \qquad \overline{D}(a) = \texttt{no} \iff D(a) = \texttt{yes}

On some occasions, we will use the notation C- to refer to the complementary problem: given a problem Problem\textsc{Problem}, we will call C-Problem\textsc{C-Problem} its contrary problem.

To continue, it is necessary to comment on some particularities of Python programs for dealing with problems. As we well know, programs only accept strings as input and strings as output. We can think of a Python program PP as a function

P:S⟶SP : S \longrightarrow S

where SS is the set of all possible strings in Python. Depending on the version of Python, this set will be larger or smaller.7

For a computer to solve a problem, we will need to encode the inputs and solutions of the problem in a format it can understand: character strings (strings). As shown in Proposition 3.6, for any problem we can assume that inputs and solutions are encoded as strings.

Proposition3.6

For every problem F:X⟶YF:X\longrightarrow Y, there exists an equivalent problem F^:S⟶S\widehat{F}:S\longrightarrow S.

Proof

To solve a computational problem on a computer, we must encode inputs and outputs in a format that the computer can process. The idea is to translate a problem FF into a string problem F^\widehat{F} that processes the same function FF for correctly encoded inputs, and returns no whenever the input encoding is invalid.

To formalise this conveniently, define X⊘=X∪{⊘}X^\oslash = X \cup \{\oslash\} and Y⊘=Y∪{⊘}Y^\oslash = Y \cup \{\oslash\}, where ⊘\oslash is the error element.

We extend the problem FF to accept the error element as input, mapping it to the error element as output. Call this extension F⊘:X⊘⟶Y⊘F^\oslash : X^\oslash \longrightarrow Y^\oslash, defined by:

F⊘(x)={F(x),if x∈X⊘,if x=⊘F^\oslash(x) = \begin{cases} F(x), & \text{if } x \in X \\ \oslash, & \text{if } x = \oslash \end{cases}

Let CX:S⟶X⊘\mathcal{C}_X : S \longrightarrow X^\oslash be the function that maps each string encoding to the corresponding input in XX, or to ⊘\oslash for invalid encodings.

Let CY∗:Y⊘⟶S\mathcal{C}_Y^* : Y^\oslash \longrightarrow S be the function that maps each output in YY to its string encoding, and maps the error element ⊘\oslash to no. Similarly, we can define CY:S⟶Y⊘\mathcal{C}_Y : S \longrightarrow Y^\oslash, which maps each string encoding of an output to the corresponding element of YY, and assigns ⊘\oslash to invalid encodings.

We now define F^=CY∗∘F⊘∘CX\widehat{F} = \mathcal{C}_Y^* \circ F^\oslash \circ \mathcal{C}_X.

The problem F^\widehat{F} solves the same problem as FF, and is able to handle invalid inputs. Indeed, if I∈SI\in S is a valid encoding of some input x∈Xx\in X (i.e. CX(I)=x\mathcal{C}_X(I)=x), then:

F^(I)=CY∗(F⊘(CX(I)))=CY∗(F⊘(x))=CY∗(F(x))=O\widehat{F}(I) = \mathcal{C}_Y^*(F^\oslash(\mathcal{C}_X(I))) = \mathcal{C}_Y^*(F^\oslash(x)) = \mathcal{C}_Y^*(F(x)) = O

where O=CY∗(F(x))O=\mathcal{C}_Y^*(F(x)) is the encoding of F(x)F(x). Given the encoded output OO, the solution is easily recovered as F(x)=CY(O)F(x)=\mathcal{C}_Y(O).

If I∈SI\in S is an invalid input encoding, then:

F^(I)=CY∗(F⊘(CX(I)))=CY∗(F⊘(⊘))=CY∗(⊘)=no\widehat{F}(I) = \mathcal{C}_Y^*(F^\oslash(\mathcal{C}_X(I))) = \mathcal{C}_Y^*(F^\oslash(\oslash)) = \mathcal{C}_Y^*(\oslash) = \mathtt{no}
∎

This is not a restriction: inputs and solutions can be encoded as character strings. We must, however, keep in mind that there may be inputs that are not a correct encoding of an input to the original problem. In such a case, we will assign the solution no to that input.

We see an example of this in Problem 3.7. This problem will have a certain encoding associated with its inputs and solutions. Observe that this problem is identical to Problem 3.2, and we can translate the inputs and solutions of one and the other knowing the chosen encoding.

Problem3.7—HamiltonianCycleEncoding\textsc{HamiltonianCycleEncoding}
  • Input: encoding of a graph GG.
  • Solution: encoding of the Hamiltonian cycles present in GG.

In the case of decision problems, the solutions will always be yes and no. Note that no will be the solution both for negative instances of the problem and for incorrect encodings.

From here on, all problems will have character strings as inputs and solutions.

Next, we will make precise the relationship between programs and problems that we have been intuiting since Chapter 2.

Definition3.8—Solving and deciding

Given a problem F:S⟶SF : S \longrightarrow S and a program PP, we say that PP solves FF if, for each input I∈SI \in S, PP halts and returns a string such that P(I)=F(I)P(I) = F(I).

If FF is a decision problem solved by PP, we say that PP decides FF.

A computational problem describes what we want to solve. A program describes the particular way of computing the solution to a problem.

It is very important to note that, in the previous definition, it is necessary that PP halt for all inputs. Or, equivalently, if PP loops for some input, it does not solve FF. This condition can be relaxed, giving rise to semi-decidable problems (see Section 3.4).

In the case of decision problems, this characteristic of "solving" leads us to the definition of computability.

Definition3.9—Computable and decidable problem

Let FF be a computational problem. If there exists a program PP that solves it, we say the problem is computable.

In the case where FF is a decision problem, if there exists a program PP that decides it, we say the problem is decidable.

Let us see a simpler problem than Problem 3.2. Problem 3.10 has as input a word over the alphabet {a,b}\{\texttt{a}, \texttt{b}\}, and as solution yes in case the word has more a\texttt{a}s than b\texttt{b}s, and no otherwise or in the case of an incorrect encoding. For this problem, the encoding is incorrect if the input is a character string that does not form a word over the alphabet {a,b}\{\texttt{a}, \texttt{b}\}.

Evidently, MoreAsThanBs\textsc{MoreAsThanBs} is a decision problem. Now we must ask ourselves whether this problem is decidable. The answer is affirmative, and it is demonstrated in detail in Proposition 3.11.

Problem3.10—MoreAsThanBs\textsc{MoreAsThanBs}
  • Input: a character string over the alphabet {a,b}\{\texttt{a}, \texttt{b}\}.
  • Output: whether the character string has more a\texttt{a}s than b\texttt{b}s.

If we want to know whether the problem is decidable, we must write a program that solves it and never loops, and that takes incorrect encodings into account. This is precisely what the following program does:

Program3.1—more_as_than_b.py
import utilities
 
def more_as_than_b(word):
# check whether the encoding is correct (i.e. over {'a', 'b'}); if not, return 'no'
if not utilities.in_alphabet(word, {'a', 'b'}):
return 'no'
 
if word.count('a') > word.count('b'):
return 'yes'
 
return 'no'

We explain the program briefly. In the more_as_than_b function we see the program's main function, which accepts a word as input. We check that the word belongs to the alphabet {a,b}\{\texttt{a}, \texttt{b}\} using the in_alphabet function from the utilities library (which we import at the top of the file). If the word does not belong to the alphabet, we return no, since the encoding is incorrect. If it belongs to the alphabet, we check whether it has more a\texttt{a}s than b\texttt{b}s. If so, it returns yes; otherwise, it returns no. We see some of the program's outputs in the table below.

Command Output
more_as_than_b('abaab') yes
more_as_than_b('abbab') no
more_as_than_b('') no
more_as_than_b('abaabc') no
Table 3.1. Example outputs of more_as_than_b.py

Since we have found a program more_as_than_b.py that decides MoreAsThanBs\textsc{MoreAsThanBs}, the problem MoreAsThanBs\textsc{MoreAsThanBs} is decidable.

Proposition3.11

MoreAsThanBs\textsc{MoreAsThanBs} is decidable.

Proof

Listing 3.1 decides MoreAsThanBs\textsc{MoreAsThanBs}. Indeed, we see that the program halts for all inputs I∈SI \in S and returns:

  • If I∉{a,b}∗I \notin \{\texttt{a}, \texttt{b}\}^*, then P(I)=MoreAsThanBs(I)=noP(I) = \textsc{MoreAsThanBs}(I) = \texttt{no} (input with incorrect encoding).
  • If I∈{a,b}∗I \in \{\texttt{a}, \texttt{b}\}^*:
  • If II has more a\texttt{a}s than b\texttt{b}s, then P(I)=MoreAsThanBs(I)=yesP(I) = \textsc{MoreAsThanBs}(I) = \texttt{yes}.
  • If II does not have more a\texttt{a}s than b\texttt{b}s, then P(I)=MoreAsThanBs(I)=noP(I) = \textsc{MoreAsThanBs}(I) = \texttt{no}.
∎

A note regarding our definition of decidability. Normally, in computation theory courses, this definition is made with respect to Turing machines. However, having proved the equivalence with Python programs in Chapter 2, we can define it directly in terms of programs. According to the definition of Turing machines, however, we could also prove that MoreAsThanBs\textsc{MoreAsThanBs} is decidable: it suffices to observe that the machine M#a>#bM_{\#a>\#b} from Example 2.11 decides the problem, since it never loops and accepts the words that have more aas than bbs.

3.2 #Universality

In Section 2.3 we saw how we can simulate a Turing machine using a Python program. But is it possible to simulate a Python program using a Python program? The answer is yes, and it should not be surprising, since many of the operations we execute day to day on our computers involve this type of simulation.

For example, when we run any Python program, we are executing the Python interpreter, which is a program in itself.

This idea of "programs that execute programs" may seem obvious, but it was truly revolutionary, and is the reason personal computers are the norm today, even fitting in our pockets. Perhaps the most incredible thing a computer can do is run anything — not simply a fixed set of programs preinstalled by the manufacturer.8

To understand how this can be possible in the case of a Python program, we must understand the operation of the native exec function9. If we try it in the interpreter:

>>> exec("print('resultado:', 11+2)")
resultado: 13

We see how we pass a Python statement to exec in the form of a string, and it executes it. In fact, observe how it executed the sum 11+211 + 2, and did not simply print 11+2.

We can take this one step further: inside exec we can insert several lines and even define functions, variables, etc.

>>> exec("def hola(nombre):\n\tprint(f'Hola, {nombre}')\nhola('TFG')")
Hola, TFG

Once we know this, we can create the following program:

import utilities
 
def universal_machine(program, input):
# this defines the functions in 'program', but does not execute them yet
try:
exec(program)
except Exception as excepcion:
return 'error: ' + str(excepcion)
 
# extract a reference to the main function, which is defined locally
main = utilities.extract_main(program, locals())
 
# invoke the function
return main(input)

Let us see what Listing 3.2 does. First, we see that the main function accepts two parameters: a program and an input, both strings. We execute the program defined in program, as we explained above. This line is inside a try-except block since the program may be ill-defined, in which case an exception is raised. We catch this exception, since according to the Definition 2.2 definition, the program must not raise any exception. If there is an exception, we return a string with the error details.

If there is no error, exec(program) will execute successfully, and the functions defined in program will be accessible from the universal_machine function. In particular, all definitions from program will be accessible from locals(). We take advantage of this to extract a reference to the main function of program, which we finally execute with the input.

We have managed to create a Python program that is capable of executing another program. This will allow us to execute any of the problems we have created, simply by passing the program as a string. In the utilities library we find the read function, which given a path to a file returns its contents as a single string.

Some examples of execution of universal_machine.py are specified in the following table.

Command Output
universal_machine('not a program', 'any input') 'error'...
universal_machine(read('./more_as_than_b.py'), 'abaab') yes
universal_machine(read('./more_as_than_b.py'), read('./more_as_than_b.py')) no
universal_machine(read('./more_as_than_b_v2.py'), read('./more_as_than_b.py')) yes
universal_machine(read('./yes.py'), read('./more_as_than_b.py')) yes
universal_machine(read('./simulate_turing.py'), utilities.MAU(read('./turing_machines/more_as_than_b.mt'), 'abaab')) an accepting final configuration
Table 3.2. Example outputs of universal_machine.py

In the first row we can see that, when inserting an invalid program as input, the output of universal_machine.py will be an error. In the second row, we execute the program more_as_than_b.py with the input abaab. The result is exactly as expected (observe the first row of the MoreAsThanBs\textsc{MoreAsThanBs} output table above).

In the third row, we use the code of more_as_than_b.py itself as input to itself. This makes sense, since a program, as we have commented, is nothing more than a string. Remember that Listing 3.1 treats inputs outside the alphabet {a,b}\{\texttt{a}, \texttt{b}\} as an incorrect input, and that is why the output is no. We modify this in Listing 3.3, in which we accept any input and count its number of a\texttt{a}s and b\texttt{b}s. We see how, in the fourth row, the output is yes, since in this case the encoding is correct.

def more_as_than_b_v2(word):
if word.count('a') > word.count('b'):
return 'yes'
 
return 'no'

In the fifth row, we use Listing 3.4. This program always returns yes, regardless of the input we pass. Evidently, this program is the one that decides Problem 3.12 (that is, Yes\textsc{Yes} is decidable).

Program3.4—yes.py
def si(input):
# always returns 'yes'
return 'yes'
Problem3.12—Yes\textsc{Yes}
  • Input: any character string.
  • Output: yes.

In the sixth and last row we see how universal_machine.py can execute any Python program, even Listing 2.2, which simulates Turing machines. As input to the program we pass the encoding of a Turing machine and the input. Here, the output describes an accepting final configuration.

This ability of programs to execute themselves is extremely interesting and is a characteristic that will cause us many problems, as we will see in Section 3.3.

We call a machine universal if it is capable of simulating any Turing machine, since Turing introduced this concept in his famous publication2. By the equivalence result (Theorem 2.23), we see that Listing 3.2 is, indeed, a universal machine (something we could intuit from the program's name).

As we saw at the beginning, the concept of universality is a constant in the computers we use: the hardware is universal, because it can be used to execute any Turing machine, just as an operating system is, since it can compile programs that simulate Turing machines. The Python interpreter is also universal for this same reason, as is the Java virtual machine. Even a program itself (Listing 3.2) can be universal.

3.3 #Undecidable problems

In Section 3.1 we showed how to prove that a problem is decidable, which is straightforward: it suffices to find a program that decides it. However, proving that a problem is not decidable is much more complicated. In this section we will find several undecidable problems.

Problem3.13—Universal\textsc{Universal}
  • Input: a program PP and an input II.
  • Output: yes if PP is well-defined and P(I)=yesP(I) = \texttt{yes}; no otherwise.

Let us begin with the Universal\textsc{Universal} problem.10 Clearly, it is a decision problem, since it returns yes or no. In this section, we will prove that writing a program that decides Universal\textsc{Universal} is impossible. If it were possible to do so, we see in the table below the possible outputs it would have.

Command Output
universal('not a program', 'any input') no
universal(read('./more_as_than_b_v2.py'), 'aaba') yes
universal(read('./more_as_than_b_v2.py'), read('./more_as_than_b_v2.py')) yes
universal(read('./yes.py'), 'aaba') yes
universal(read('./yes.py'), read('./yes.py')) yes
Table 3.3. Example outputs of universal.py

In the first row we see how, for incorrect encodings, the program returns no. The following rows are evident, and similar to those of the universal_machine.py table.

We will introduce two more problems. The first of them, Diagonal\textsc{Diagonal}, can be obtained from Universal\textsc{Universal} (Problem 3.13) with I=PI = P.

Problem3.14—Diagonal\textsc{Diagonal}
  • Input: a program PP.
  • Output: yes if P(P)P(P) is well-defined and P(P)=yesP(P) = \texttt{yes}; no otherwise.

If a program that decides this problem existed, we see in the following table the possible outputs it would have.

Command Output
diagonal('not a program') no
diagonal(read('./more_as_than_b_v2.py')) yes
diagonal(read('./yes.py')) yes
diagonal(read('./diagonal.py')) ? yes or no ?
Table 3.4. Example outputs of diagonal.py

The first three outputs require no further explanation. However, let us focus on the last one: what would be the result of executing diagonal.py with the code of diagonal.py?

It seems that both yes and no are correct answers — it suffices to look at the definition of Problem 3.14. Taking II as diagonal.py and PP as the same program, then if P(I)=yesP(I) = \texttt{yes}, the program will return yes, whereas if P(I)=noP(I) = \texttt{no}, then the program will return no.

At this point we should suspect that things are not going to go well. Let us finish this "recursive circus" with one last problem, C-Diagonal\textsc{C-Diagonal}.11 If a program that decides it existed, c_diagonal.py, we see its possible outputs in the table below.

Problem3.15—C-Diagonal\textsc{C-Diagonal}
  • Input: a program PP.
  • Output: no if P(P)P(P) is well-defined and P(P)=yesP(P) = \texttt{yes}; yes otherwise.
Command Output
c_diagonal('not a program') yes
c_diagonal(read('./more_as_than_b_v2.py')) no
c_diagonal(read('./yes.py')) no
c_diagonal(read('./c_diagonal.py')) ? yes or no ?
Table 3.5. Example outputs of c_diagonal.py

We repeat the same question we asked ourselves for the case of Diagonal\textsc{Diagonal} (Problem 3.14). What output would a hypothetical program that solves it have, if it uses itself as input? In this case, this question is precisely what shows us that writing such a program is impossible. This tells us that the C-Diagonal\textsc{C-Diagonal} problem is not decidable. Let us see a more precise proof of this fact.

Proposition3.16

C-Diagonal\textsc{C-Diagonal} is undecidable.

Proof

Suppose C-Diagonal\textsc{C-Diagonal} is decidable. Then, there exists a Python program c_diagonal.py that solves C-Diagonal\textsc{C-Diagonal}. Then, we can ask about the output of the command:

c_diagonal(read('./c_diagonal.py'))

This command asks:

Is it true that c_diagonal.py does NOT return 'yes' when executed on itself?

The answer to the above question can be affirmative or negative. We will see that neither can be possible.

If the answer is affirmative, then we know that c_diagonal.py does not return yes when executed on itself, hence the answer should have been negative — a contradiction, that is, the answer cannot be affirmative.

If the answer is negative, then c_diagonal.py returns no when executed on itself, hence the answer should have been affirmative — again a contradiction, in other words, the answer cannot be negative either.

We conclude, therefore, that it is impossible to write the program c_diagonal.py. Since we cannot write a program that decides C-Diagonal\textsc{C-Diagonal}, the problem is undecidable.

∎

We have just found our first undecidable problem: C-Diagonal\textsc{C-Diagonal}. This fact is of special relevance since, from it, we can prove that Universal\textsc{Universal} is not decidable either.

Proposition3.17

The Universal\textsc{Universal} problem (Problem 3.13) is undecidable.

Proof

We proceed by contradiction. We will assume that Universal\textsc{Universal} is decidable. Then, we can use it to write Listing 3.5 and Listing 3.6, which solve (decide) Diagonal\textsc{Diagonal} (Problem 3.14) and C-Diagonal\textsc{C-Diagonal} (Problem 3.15), respectively. But this contradicts the undecidability of C-Diagonal\textsc{C-Diagonal} proved in Proposition 3.16.

∎
Program3.5—diagonal.py
from universal import universal
 
def diagonal(program):
return universal(program, program)
Program3.6—c_diagonal.py
from diagonal import diagonal
 
def c_diagonal(program):
output = diagonal(program)
 
if output == 'yes':
return 'no'
else:
return 'yes'

We can see the chain of problems we used in Proposition 3.17 in the figure below.

UNIVERSAL(P;I){returnyes,ifP(I)=yes{returnno,otherwiseDIAGONAL(P){returnyes,ifP(P)=yes{returnno,otherwiseC-DIAGONAL(P){returnno,ifP(P)=yes{returnyes,otherwiseproblembehavior
Figure 3.3. A chain of undecidable problems

Proving the undecidability of Universal\textsc{Universal} has been a long process, which required other problems. It would be possible to demonstrate this result using a single program, but, using Turing's own words, "[such a] proof, although perfectly sound, [would have] the disadvantage that it may leave the reader with the feeling that 'there must be something wrong'"2.

We have already proved that two of the three problems in the chain above are not decidable. The following result will allow us to prove that the last one is not decidable either.

Proposition3.18

Let DD be a decidable problem. Then, D‾\overline{D} is decidable.

Proof

If DD is a decidable problem, we have a program d.py that decides it. Then, we can write the following program, which decides D‾\overline{D} (observe that Listing 3.6 uses the same structure):

Program3.7—c_d.py
from d import d
 
def c_d(input):
output = d(input)
 
if output == 'yes':
return 'no'
else:
return 'yes'
∎
Corollary3.19

Let DD be an undecidable problem. Then, D‾\overline{D} is undecidable.

By Corollary 3.19, knowing that C-Diagonal\textsc{C-Diagonal} (Problem 3.15) is undecidable (Proposition 3.16), we have that Diagonal\textsc{Diagonal} (Problem 3.14) is undecidable.

Knowing that these problems are not decidable is very useful, since it allows us to prove what we seek in this chapter: that the Halting Problem, which we will introduce in Section 3.6, is not decidable either.

To be able to prove this, however, we must use a new tool, which we will develop in Section 3.5. Before that, we will comment on another class of problems: those that "almost" are decidable.

3.4 #Semi-decidable problems

In Definition 3.9, we required that the program halt for each input in order to say that the problem is decidable. If we relax this condition we obtain a new category of problems.

Definition3.20—Semi-deciding

Given a decision problem D:S⟶SD : S \longrightarrow S and a program PP, we say that PP semi-decides DD if:

  • for each input I∈SI \in S with D(I)=yesD(I) = \texttt{yes}, PP halts and P(I)=yesP(I) = \texttt{yes};
  • for each input I∈SI \in S with D(I)=noD(I) = \texttt{no}, P(I)P(I) is undefined or P(I)=noP(I) = \texttt{no}.

We say that a decision problem DD is semi-decidable if there exists a program that semi-decides it.

It is clear that every decidable problem is semi-decidable. The converse is not true. We will see an example of this later, in Problem 4.25.

We will not delve into the notion of semi-decidable problem, since it will not be necessary for our study. When necessary, we will refer to the definition specified here.

3.5 #Reductions

In Section 3.3, we used some problems to be able to solve others or to prove that they cannot be solved (see the chain-of-problems figure above). In computation theory, this intuition is formalized in the concept of reduction. When we say that "FF reduces to GG", we mean that "FF can be solved if GG can be solved".

Definition3.21—Turing reduction

Let F:S⟶SF : S \longrightarrow S and G:S⟶SG : S \longrightarrow S be two problems. We say that FF Turing-reduces, or simply reduces,12 to GG, and denote it F≤TGF \leq_T G, if there exists a program PFP_F that solves FF assuming the existence of a program PGP_G that solves GG.

We call the program PGP_G whose existence we assume an oracle, following Turing’s oracle-machine terminology.13

Intuitively, if we can reduce FF to GG, it is because GG is at least as "hard" as FF, or, reciprocally, FF is at least as "easy" as GG. This intuition is captured in the following proposition.

Proposition3.22

Let FF and GG be problems with F≤TGF \leq_T G. Then:

  • (a) If GG is decidable, FF is decidable.
  • (b) If FF is undecidable, GG is undecidable.
Proof

Statement (a) follows directly from the definition of Turing reduction: if problem GG is decidable, we can create a program PGP_G that solves it. Since FF reduces to GG, we can create a program PFP_F that solves FF from PGP_G, so it is decidable.

Statement (b) is immediate from (a).

∎

This proposition allows us to prove that a problem is decidable (or not) from another. This allows us to proceed in several ways. First, if we want to know that a problem FF is easy, it suffices to find a decidable problem GG to which FF reduces, F≤TGF \leq_T G. Then, FF will be decidable.

On the other hand, we can prove that a problem GG is hard. It suffices to have an undecidable problem FF such that F≤TGF \leq_T G. Then, GG will not be decidable either.

Let us see a first example of reduction. In Section 3.3 we already saw that Diagonal\textsc{Diagonal} (Problem 3.14) and Universal\textsc{Universal} (Problem 3.13) are both undecidable. Suppose we only know that Diagonal\textsc{Diagonal} is undecidable. Then, we will be able to prove the undecidability of Universal\textsc{Universal} through a simple reduction.14 Observe the following program.

from universal import universal  # oracle
 
def diagonal(program):
output = universal(program, program)
 
if output == 'yes':
return 'yes'
else:
return 'no'

It is straightforward to verify that diagonal_to_universal.py is a reduction of Diagonal\textsc{Diagonal} to Universal\textsc{Universal}. Indeed, the program diagonal_to_universal.py solves Diagonal\textsc{Diagonal} assuming the existence of a program universal.py that solves Universal\textsc{Universal}.

3.6 #The Halting Problem

Given a program PP and an input II, we have asked what the output of such a program is. Now, we are going to ask whether the program halts.

This problem, of recognizing whether a program halts given an input, is known as the Halting Problem. There are several reasons why we study it: first, this problem has practical application. If it were decidable, we could write an algorithm that, for any program, would check whether it is capable of terminating. Evidently, not all programs halt: some of them are designed to run indefinitely, as is the case with web servers or an operating system.

On the other hand, this problem has special historical relevance. It was one of the first problems to be proved uncomputable, and is one of the seeds of computation theory.

In our case, however, studying this problem will allow us to deduce the central result of this work.

Before talking about this problem, we must remember that the notion of halting that Turing defined is related to machines of the same name: a Turing machine, given an input, can halt and accept, halt and reject, or loop.

In our case, we will say that a Python program PP halts on input II if the output P(I)P(I) is defined, according to the Definition 2.2 definition. A program could, in this way, fail to halt for many reasons: it could enter an infinite loop, raise an exception, or return an object that is not a string. In the last two cases, it may seem counterintuitive to say that the program has not halted: after all, the program halts when it raises an exception or returns an object that is not a string. In these cases, it is better to think that the program has "frozen": it is not executing more code, but it cannot terminate satisfactorily. In this sense, the program "has not halted".

We are now in a position to present the formulation of the problem.

Problem3.23—Halts\textsc{Halts}
  • Input: a program PP and an input II.
  • Solution: whether program PP halts on input II.

We ask whether this problem is decidable. The answer is negative, and expected given all the problems of this style that we have already proved undecidable (Problem 3.13, Problem 3.14, Problem 3.15).

Proposition3.24

Halts\textsc{Halts} is undecidable.

Proof

We perform the reduction Universal≤THalts\textsc{Universal} \leq_T \textsc{Halts} via Listing 3.10.

To do this, we transform a program PP that returns yes / does not return yes into a program P′P' that halts / does not halt. Specifically, PP will be the program program inserted as input in the universal_to_halting function of Listing 3.10, and P′P' will be Listing 3.9.

Specifically,

  • If program returns yes on the input (this means Universal(program,input)=yes\textsc{Universal}(\allowbreak \texttt{program},\allowbreak \texttt{input}) = \texttt{yes}), then universal_machine_halting halts on the input (Halts(universal_machine_halting,input)=yes\textsc{Halts}(\allowbreak \texttt{universal\_}\allowbreak \texttt{machine\_}\allowbreak \texttt{halting},\allowbreak \texttt{input}) = \texttt{yes}). Observe how in Listing 3.9 we return yes. The value we return is irrelevant; what matters is that the program halts.
  • If program does not return yes on the input (Universal(program,input)=no\textsc{Universal}(\allowbreak \texttt{program},\allowbreak \texttt{input}) = \texttt{no}), then universal_machine_halting does not halt (loops) on the input (Halts(universal_machine_halting,input)=no\textsc{Halts}(\allowbreak \texttt{universal\_}\allowbreak \texttt{machine\_}\allowbreak \texttt{halting},\allowbreak \texttt{input}) = \texttt{no}). In Listing 3.9 we use the loop_forever function from the utilities library, which simply enters an infinite loop.

We have proved that Listing 3.10 is a reduction of Universal\textsc{Universal} to Halts\textsc{Halts}. The result follows from Proposition 3.17 and Proposition 3.22.

∎
import utilities
from universal_machine import universal_machine
 
def universal_machine_halting(encoded_input):
(program, input) = utilities.UAM(encoded_input)
output = universal_machine(program, input)
 
if output == 'yes':
return 'yes'        # halts
else:
utilities.loop_forever()  # loops forever
import utilities
from halts import halts  # oracle
 
def universal_to_halting(program, input):
encoded_input = utilities.MAU(program, input)
universal_machine_halting = utilities.read('universal_machine_halting.py')
 
return halts(universal_machine_halting, encoded_input)

In the previous proof we used a new technique: creating a new program from another. Specifically, we modify Listing 3.2, the universal machine in Python that we introduced in Section 3.2, to create a universal machine that halts for positive outputs and loops for any other output.

Next, we will introduce a new halting problem, simpler than the Halts\textsc{Halts} problem already stated.15

Problem3.25—HaltsOnEmpty\textsc{HaltsOnEmpty}
  • Input: a program PP.
  • Solution: whether program PP halts on the empty input.

Since it is a "simpler" problem, it need not initially be undecidable. To prove it, we must perform a "trick": ignore the input.

Program3.11—ignore_input.py
import utilities
from universal_machine import universal_machine
 
def ignore_input(ignored_input):
program = utilities.read('disk/program.txt')
input_str = utilities.read('disk/input.txt')
return universal_machine(program, input)
{ignoreI{bPÃcontentsofprogram.txt{bIÃcontentsofinput.txtignoreinput.pyIbP(bI)diskprogram.txtinput.txt
Figure 3.4. Scheme of the program ignore_input.py

For this, we use Listing 3.11. This program, instead of executing the input ignored_input, executes the universal machine on a program and an input stored on disk. Specifically, it uses as program the one stored in disk/program.txt, and as input the one stored in disk/input.txt.

Once we know how ignore_input.py works, it is straightforward to find a reduction of Halts\textsc{Halts} to HaltsOnEmpty\textsc{HaltsOnEmpty}.

Proposition3.26

HaltsOnEmpty\textsc{HaltsOnEmpty} is undecidable.

Proof

It suffices to perform the reduction Halts≤THaltsOnEmpty\textsc{Halts} \leq_T \textsc{HaltsOnEmpty} via Listing 3.12. Indeed, we see that such a program solves Halts\textsc{Halts}, assuming the existence of a program halting_on_empty.py that solves HaltsOnEmpty\textsc{HaltsOnEmpty}. Given a program program and an input, halting_to_halting_on_empty will store the values of the variables in disk/program.txt and disk/input.txt, respectively. Next, it returns whether the program ignore_input.py halts or not, which is equivalent to saying, by the way that program is written, whether program halts on the input, thus solving Halts\textsc{Halts}.

The result follows from Proposition 3.24 and Proposition 3.22.

∎
import utilities
from halting_on_empty import halting_on_empty  # oracle
 
def halting_to_halting_on_empty(program, input):
utilities.write('disk/program.txt', program)
utilities.write('disk/input.txt', input)
 
return halting_on_empty(utilities.read('ignore_input.py'))

To conclude, we introduce a prediction task: GuessConsistent\textsc{GuessConsistent}.16

Problem3.27—GuessConsistent\textsc{GuessConsistent}
  • Input: a program PP that, on any input, returns yes (accepts) or no (rejects) whenever it halts.
  • Required output: yes if PP accepts on empty input; no if it rejects on empty input. If PP loops, either answer is allowed.
  • Termination: the predictor must halt on every input, including programs PP that loop.

Unlike an ordinary decision problem, this task does not specify a unique correct answer for every input: either answer is allowed for a looping program. We seek a total computable predictor that agrees with every program that does halt. Even with this freedom, no such predictor exists.

Proposition3.28

No total computable program solves GuessConsistent\textsc{GuessConsistent}.

Proof

Suppose a predictor guess_consistent.py satisfies these requirements. Construct the program DD in Program 3.13: given the description of a program PP, it prepares a no-input program that simulates P(P)P(P), queries the predictor, and flips its answer.

If P(P)P(P) accepts, D(P)D(P) returns no; if P(P)P(P) rejects, D(P)D(P) returns yes. If P(P)P(P) loops, the predictor may give either answer and D(P)D(P) returns the opposite. In every case D(P)D(P) halts because the predictor is total: DD does not execute the queried simulation.

Now run DD on its own description. Since D(D)D(D) halts, there are only two cases:

  • If D(D)D(D) returns yes, the predictor must answer yes about the program simulating D(D)D(D), but DD flips that answer and returns no.
  • If D(D)D(D) returns no, the predictor must answer no, but DD returns yes.

Both cases are contradictory. The freedom to answer arbitrarily on looping programs cannot help, because D(D)D(D) halts.

Program3.13—modify_guess_consistent.py — pseudocode
import utilities
from guess_consistent import guess_consistent  # assumed total predictor
 
def modify_guess_consistent(program):
utilities.write('disk/program.txt', program)
utilities.write('disk/input.txt', program)
 
output = guess_consistent(utilities.read('ignore_input.py'))
 
if output == 'yes':
return 'no'
else:
return 'yes'
∎

The decision problem HaltsOnEmpty\textsc{HaltsOnEmpty} and the prediction task GuessConsistent\textsc{GuessConsistent} will be central to the proof of two variants of the main result of this work, in Chapter 5. As a summary, the table below is provided with all the problems presented in this chapter.

3.7 #Summary

Problem Input Output Decidable?
MoreAsThanBs\textsc{MoreAsThanBs} u∈{a,b}∗u \in \{\texttt{a}, \texttt{b}\}^* — yes, if #(u,a)>#(u,b)\#(u,a) > \#(u,b); — no, otherwise Yes (Proposition 3.11)
Yes\textsc{Yes} any — yes Yes
Universal\textsc{Universal} PP, II — yes, if P(I)=yesP(I) = \texttt{yes}; — no, otherwise No (Proposition 3.17)
Diagonal\textsc{Diagonal} PP — yes, if P(P)=yesP(P) = \texttt{yes}; — no, otherwise No
C-Diagonal\textsc{C-Diagonal} PP — no, if P(P)=yesP(P) = \texttt{yes}; — yes, otherwise No (Proposition 3.16)
Halts\textsc{Halts} PP, II — yes, if PP halts on input II; — no, otherwise No (Proposition 3.24)
HaltsOnEmpty\textsc{HaltsOnEmpty} PP — yes, if PP halts on ϵ\epsilon; — no, otherwise No (Proposition 3.26)
GuessConsistent\textsc{GuessConsistent} PP yes if it accepts; no if it rejects; either if it loops. The predictor must always halt No total computable predictor (Proposition 3.28)
Table 3.6. Summary of the problems presented in this chapter.

Footnotes

  1. Haines, L., Horning, J., & Wulf, W. (eds.) (1993). Report on the workshop on grand challenges in computer science. Communications of the ACM, 36(11).

    ↩
  2. Turing, A.M. (1937). On computable numbers, with an application to the Entscheidungsproblem. Proceedings of the London Mathematical Society, 42(1):230–265.

    ↩ ↩2 ↩3
  3. MacCormick, J. (2018). What Can Be Computed? Princeton University Press.

    ↩
  4. Sipser, M. (2012). Introduction to the Theory of Computation (3rd ed.). Cengage Learning.

    ↩
  5. We call computational problems FF and not PP to distinguish them from Python programs, and to reinforce the notion that a computational problem is a function.

    ↩
  6. When we refer to a "problem" without specifying whether it is computational or a decision problem, there is no ambiguity: it suffices to look at the outputs to tell whether it is a decision problem.

    ↩
  7. In recent versions of Python, it is possible to work with UTF-8.

    ↩
  8. See the Howard Aiken quote at the beginning of Chapter 2.

    ↩
  9. Lutz, M. (2013). Learning Python (5th ed.). O'Reilly Media.

    ↩
  10. This problem has, initially, no relation whatsoever to the universal machine we defined in Listing 3.2.

    ↩
  11. Note that C-Diagonal=Diagonal‾\textsc{C-Diagonal} = \overline{\textsc{Diagonal}}.

    ↩
  12. There are other types of reductions, but we will only use this one. That is why we use the term reduction instead of Turing reduction, since it does not lead to ambiguity.

    ↩
  13. Turing, A.M. (1939). Systems of logic based on ordinals. Proceedings of the London Mathematical Society, 45:161–228. https://people.math.ethz.ch/~halorenz/4students/Literatur/TuringOrdinalLogic.pdf

    ↩
  14. Recall that we already said we could obtain Diagonal\textsc{Diagonal} from Universal\textsc{Universal}.

    ↩
  15. In some books, this problem is stated as the Halting Problem itself.

    ↩
  16. Aaronson, S. (2011). Rosser's Theorem via Turing machines. https://scottaaronson.blog/?p=710

    ↩