Can any known sub-Turing-complete model of computation enumerate precisely the set of prime numbers?

I wish there were more, but the subject pretty much captures my whole question.

Is there a non-Turing-complete model (some constrained term rewriting system or automaton or what have you) which is known to be able to enumerate the prime numbers, all of the prime numbers (well, til you pull the plug on the algorithm), and only the prime numbers?

To be clear, one of the criteria I'm imposing is that this algorithm would be non-halting, and so long as it is left running, it will continue intermittently outputting prime numbers. This seems to necessitate an unbounded working memory, which gets you a lot of the way towards Turing-complete already.

Furthermore, this must be a model with a finite description, meaning no cute "consider the system mapping the natural numbers to the primes" answers, please. Even if technically correct, what I'm really after is whether it seems probable that prime enumeration would be a strong indicator of Turing completeness.

Edit: As for the objection that Turing machines can't yield values at will, I consider that semantics and not relevant to the spirit of the question. A Turing machine could certainly record all prime numbers found thus far on its tape, which we could presumably examine at will.

• You can simply define a model where infinite primes enumeration is the only allowed program. Oct 21 '21 at 13:30
• I don't think there's an answer you'd like. It's easy to contrive such a system, but not one that isn't explicitly designed to do this job or things like it. Emitting primes is a good indicator of Turing completeness unless someone's trying to fool you. Oct 21 '21 at 14:42
• On further thought on my comment for a nameless model, how do you have an unbounded integer in a less-than-turing-complete computation model in the first place? Oct 21 '21 at 18:29
• Even a Turing machine doesn't satisfy your requirement that it be "non-halting," because the classical Turing model does not include an "output current tape contents" instruction (so there's no way to yield individual numbers). Oct 22 '21 at 1:24
• You cannot simply examine the tape "at will" because the machine is actively using (some subset of) that tape! Whatever computation the machine is currently doing may occupy some arbitrary portion of the tape. Now, there are various ways you might go about "fixing" this problem... but if you applied the same level of ingenuity to "fixing" the primitive recursive functions (instead of looking for reasons to reject them), you would find that they are a perfectly acceptable answer to your query. Oct 23 '21 at 19:21

There is an important class of primitive recursive functions. Citing Wikipedia,

[P]rimitive recursive function is roughly speaking a function that can be computed by a computer program whose loops are all "for" loops (that is, an upper bound of the number of iterations of every loop can be determined before entering the loop).

They are powerful enough to check primality: to check primality of $$n$$ it suffices to loop from $$1$$ to $$n$$, where $$n$$ is a known upper bound. Then you can define the function "$$i$$-th prime": it can be bounded by, say, $$2^i$$ due to Bertrand's postulate, then you can check all numbers from $$1$$ to $$2^i$$ and find the $$i$$-th.

On the other hand, they are less powerful than Turing machines, the separating example being the Ackermann function.

Wiki page on primitive recursive functions is quite comprehensive for the introduction.

• Perhaps I wasn't precise, but I meant to convey that the algorithm needs to be able to enumerate primes indefinitely, meaning as long as the algorithm is left running, as opposed to stopping at a finite bound (even an arbitrarily large one) provided ahead of time.
– Trev
Oct 21 '21 at 9:41
• Algorithms in theoretical computer science do not necessarily "yield". Algorithms may, functions may not, it is only the matter of notation. Check the definition of the enumerable set: it can be defined as the set that is yielded by some program; it can also be defined as the set that is semi-decidable by some function. We did even more, we presented the function that explicitly gives the $n$-th element of the set. Oct 21 '21 at 13:09
• I have implemented "is n prime?" and "get i-th prime" as primitive recursive functions: nayuki.io/page/… ; nayuki.io/res/primitive-recursive-functions/primrecfunc.py Oct 21 '21 at 17:21
• Just as an observation, given that testing primes is primitive recursive, I'd be very surprised if generating primes was not primitive corecursive. Oct 22 '21 at 4:44
• Nice answer. Might I suggest to add that while FOR (in the BASIC semantics, not the C or golang semantics ;) ) is the criterion for PR, we immediately arrive at Turing-complete if we allow WHILE or GOTO. Not really relevant, but an interesting demarcation as far as I am concerned.
– AnoE
Oct 22 '21 at 8:56

As observed above, "locally" the problem of enumerating primes is very easy: the function sending $$n$$ to the $$n$$th prime, $$n\mapsto \mathsf{thprime}(n)$$, is primitive recursive (and of course that's massive overkill).

However, you specifically want a framework which allows programs to run forever. This complicates things a bit: in some sense we want the pseudocodish expression

for i in Nat: print$$(\mathsf{thprime}(i))$$

to live inside some simple context despite the unbounded for-loop.

There is in fact something we can do here, which may be highly unnatural from a CS perspective but is quite natural from a logic perspective: require our programs to come with proofs of niceness (in the appropriate sense, over some fixed appropriate theory). For example, let a ZFC-tame program be a pair $$(p,\pi)$$, where:

• $$p$$ is a program in the usual sense, and

• $$\pi$$ is a formal $$\mathsf{ZFC}$$-prove that $$p$$ prints infinitely many things (so there's no "running forever without doing anything").

Of course this is still a bit vague but the point should be clear.

It turns out that this will always fall well short of Turing completeness:

There is no ZFC-tame program $$(p,\pi)$$ which prints the sequence $$(k_i)_{i\in\omega}$$, where $$k_i$$ is the $$i$$th output of the $$i$$th ZFC-tame program in some appropriate enumeration.

(Compare with the "theory-free" result that the set $$\mathsf{Tot}$$ of indices of total computable functions is not computably enumerable.)

This despite the fact that we can very simply enumerate the ZFC-tame programs! This old MSE answer of mine discusses essentially this point.

The primes can be recognized in linear space by a Turing machine. Linear space-bounded Turing machines are not universal. So, I think I have to disappoint you.

• See my comment below, I believe it applies here too. I meant to exclude linear bounded models by virtue of requiring a non-terminating (but obviously non-cycling) algorithm; I'll see if I can make that more clear in my question.
– Trev
Oct 21 '21 at 9:43

Yes. Friant  proved that the language $$\{ a^p \mid p \text{ is prime}\}$$ is a context-sensitive language, which is far stronger than recursively enumerable. My grandfather Benny Brodda  then gave an improved grammar for this purpose in 1968, which:

contains a smaller number of rules; the rules are not so context-dependent; and finally, there are no `dead-ends' in the grammar, i.e., irrespective of the order in which the rules are applied, the grammar either ends up in generating a prime number or stops.

This seems to be precisely what you want. Note that the language $$\{ a^p \mid p \text{ is prime}\}$$ is well-known not to be context-free.

$${}$$

References

1. Friant, J., Grammaires generatives pour l’ensemble des nombres premiers, Rev. Roum. Math. Pures Appl. 14, 473-488 (1969). ZBL0187.27901.
2. Brodda, Benny, A variant of the Friant grammar, Bulletin mathématique de la Société des Sciences Mathématiques de la République Socialiste de Roumanie Nouvelle Série, Vol. 12 (60), No. 4 (1968), pp. 19-23.

I admit that this is a contrived example, but perhaps it is useful for the thought experiment.

Consider a machine that has two registers, A and B, each of which can store an integer of arbitrary size. This means that they must be implemented with infinite storage, but direct access to that storage is not available to the machine.

It also has only the instructions needed to implement the following program:

0: Store A = 3, B = 1                      (A is prime to test, B is divisor)
1: Increment B
2: Conditional branch to 5 if A % B == 0   (not a prime)
3: Conditional branch to 1 if B != A       (loop through all B)
4: Output A                                (found a prime!)
5: Increment A
6: Set B = 1
7: Branch to 1

Now the question becomes whether this machine is Turing complete or not.

As pointed out by Nathaniel, this is close to a two-counter machine, which is known to be Turing complete. The "successor machine" instruction set of CLR, INC, JE is close to the instruction set above. However, there is an important limitation we can apply: the A register never needs to decrease in our machine.

That stops the conversion of two counters into four counters. There is still a question whether the addition of a JDIV instruction could allow Turing completeness by other means.

• Isn't that more or less a counter machine? Oct 21 '21 at 13:38
• @Nathaniel Hmm indeed, very interesting - it is pretty close to 2CM which is Turing complete. I'll have to think about the differences.
– jpa
Oct 21 '21 at 13:42
• @Nathaniel how would you model INC, DEC and JZ having only INC, JDIV, JNE and two registers? Oct 21 '21 at 13:55
• @DmitriUrbanowicz The Ershov model only needs CLR, INC and JE. And instruction 6 is close to CLR.
– jpa
Oct 21 '21 at 13:58
• According to the Wikipedia article, the Successor Machine is a Counter Machine equivalent, which also makes it a TM equivalent. Oct 21 '21 at 15:58

Here is one that I think works, and is arguably not all that unnatural.

We will start with a language along the lines of Hofstadter's BlooP. This is a simple imperative language in which the only looping construct is for loops, which require the number of iterations to be specified when the loop starts executing. There is no way to jump out of a loop conditionally. Let us say that this language has a print type statement, so that it can return results before it finishes executing.

Such a language can be simpler than the original BlooP. All you need is basic arithmetic operations (just incrementing and decrementing registers will do), some basic if type of conditional statement, and for loops and print statements as described above. If such a language is set up correctly then it is not Turing complete and can only compute primitive recursive functions. I will leave the details to the reader.

Now, you want to be able to run indefinitely, printing out results as the program runs. So let us augment this language with another looping construct, infinite_loop. This is like while (True). It enters an infinite loop and never returns, no matter what - the language should provide no way to break out of such a loop.

This is in a sense just an extension of the for loops the language already has - it's still a loop where the number of iterations has to be known in advance, it's just that now we allow an infinite number of iterations as well.

Now you can write a simple program along the lines of

n=0
infinite_loop:
print(nth_prime(n))
n += 1

where nth_prime is a (primitive recursive) subroutine that returns the nth prime number. This program satisfies your requirement of printing primes indefinitely.

However, the language provides no way of breaking out of an infinite loop conditionally. This means that all it can really do is (optionally) enter an infinite loop and then compute a primitive recursive function repeatedly, without ever returning. (You can nest infinite loops, but that doesn't change anything, since you'll never get to the second iteration of the outer one. You can store state in between invocations of your primitive recursive functions, but that doesn't change anything either, since calling a primitive recursive function n times with stored state still results in a primitive recursive function.)

So this language isn't Turing complete, but it can output prime numbers indefinitely, and it arguably isn't completely unnatural as a model of computation.

• note that I am a different Nathaniel to the Nathaniel mentioned in one of the other answers Oct 24 '21 at 6:36

At a high level, that seems straightforward: have an infinite tape, with each cell representing a nonnegative integer. The state of all cells is initially empty, save cell 1 which is filled. Cells can be empty, filled, or chosen. Have an infinite pile of bots (tape heads?) on cell 1. Each bot contains two values, TEMPER and PATIENCE, both initialized to 1.

Every "tick", a bot departs to the right, moving one cell per tick (all the other bots move one to the right too.) Initially it is in mode Child, in which it increments TEMPER every tick. If it advances to an empty cell, it sets the cell to chosen and transitions itself to mode Adult.

In mode Adult, it decrements PATIENCE. If it runs out of PATIENCE, it has a fit on the spot, filling in the cell if it was empty. But then it calms down, takes a deep breath, and copies TEMPER to PATIENCE.

The set of chosen cells represents the prime numbers.

(The less fun version: there's an infinite pile of Adult bots before the beginning of the tape, each with TEMPER initialized to each consecutive integer starting at 2. Proceed as above, but start them all simultaneously rather than letting one go every tick.)

Perhaps this is cheating, but I don't see why an infinite supply of bots / tape heads is any different from an infinite tape. And the per-bot logic is dead simple; there's no way I can see to get a universal Turing machine out of it. Then again, I'm not sure what the dividing line is between "UTM" and "nonprogrammable TM with a single program"? I guess I could provide a set of operations, and the only movement operation is "unconditionally advance by 1 cell".

We can enumerate the prime numbers with just two nested loops, with the number of iterations of the inner loop known beforehand and the iterations of the outer loop limited to N if we want to find all primes <= N.

That’s very restrictive. For a Turing-complete programming language the number of iterations should have no prior limit.