67

I was trying to explain to someone that C is Turing-complete, and realized that I don't actually know if it is, indeed, technically Turing-complete. (C as in the abstract semantics, not as in an actual implementation.)

The "obvious" answer (roughly: it can address an arbitrary amount of memory, so it can emulate a RAM machine, so it's Turing-complete) isn't actually correct, as far as I can tell, as although the C standard allows for size_t to be arbitrarily large, it must be fixed at some length, and no matter what length it is fixed at it is still finite. (In other words, although you could, given an arbitrary halting Turing machine, pick a length of size_t such that it will run "properly", there is no way to pick a length of size_t such that all halting Turing machines will run properly)

So: is C99 Turing-complete?

Raphael
  • 73,461
  • 31
  • 185
  • 415
TLW
  • 1,500
  • 1
  • 10
  • 16
  • 5
    What are the "abstract semantics" of C? Are they defined anywhere? – Yuval Filmus Jul 26 '16 at 15:01
  • 6
    @YuvalFilmus - See e.g. here, i.e. C as defined in the standard as opposed to e.g. "this is how gcc does it". – TLW Jul 26 '16 at 18:18
  • 3
    there is the "technicality" that modern computers do not have infinite memory like the TM yet are still considered "universal computers". and note that programming languages in their "semantics" do not really assume a finite memory except that all their implementations are of course limited in memory. see eg does our pc work as a Turing machine. anyway essentially all "mainstream" programming languages are Turing complete. – vzn Jul 27 '16 at 15:02
  • 7
    C (like Turing machines) isn't limited to using internal computer memory for its computations, so this really isn't a valid argument against the Turing completeness of C. – reinierpost Oct 03 '16 at 21:05
  • @reinierpost - that's like saying a teletype is sapient. That's saying that "C + an external TM-equivalent" is Turing-complete, not that C is Turing-complete. – TLW Dec 27 '16 at 04:07
  • @TLW: No, it isn't; but perhaps this is a better subject for chat. – reinierpost Dec 28 '16 at 18:22
  • If #pragma is allowed, an implementation can be Turing-complete trivially. – johnchen902 Jan 11 '18 at 04:51
  • Does it make sense to think of a theoretical device such as a Turing Machine (that has unlimited resources and cannot exist) in terms of a concrete language (designed to be implementable) ? –  Mar 07 '19 at 17:24
  • @YvesDaoust - Is Lisp a "concrete language", by your definition? – TLW Mar 18 '19 at 01:43
  • @tlw: it is both. –  Mar 18 '19 at 07:54
  • A term that I created: [a computation is Turing Complete] when its implementation language would otherwise be Turing Complete except for the lack of an unlimited tape and the lack of an unlimited tape has no effect on the computation. This would indicate than many computations done in "c" would be Turing complete computations. – polcott Jun 02 '20 at 00:01
  • @polcott - I don't quite understand that definition... lack of an unlimited tape (memory space? It's not obvious what "tape" would be for a language like C) does indeed have effects on computations done in C in some cases. – TLW Jun 08 '20 at 00:46
  • @TLW The simple fact that aTuring Machine has unlimited storage and every actual hardware machine does not have unlimited storage does not prevent the hardware machine from computing most of the things that a TM can compute. – polcott Jun 08 '20 at 01:11

15 Answers15

43

I'm not sure but I think the answer is no, for rather subtle reasons. I asked on Theoretical Computer Science a few years ago and didn't get an answer that goes beyond what I'll present here.

In most programming languages, you can simulate a Turing machine by:

  • simulating the finite automaton with a program that uses a finite amount of memory;
  • simulating the tape with a pair of linked lists of integers, representing the content of the tape before and after the current position. Moving the pointer means transferring the head of one of the lists onto the other list.

A concrete implementation running on a computer would run out of memory if the tape got too long, but an ideal implementation could execute the Turing machine program faithfully. This can be done with pen and paper, or by buying a computer with more memory, and a compiler targeting an architecture with more bits per word and so on if the program ever runs out of memory.

This doesn't work in C because it's impossible to have a linked list that can grow forever: there's always some limit on the number of nodes.

To explain why, I first need to explain what a C implementation is. C is actually a family of programming languages. The ISO C standard (more precisely, a specific version of this standard) defines (with the level of formality that English allows) the syntax and semantics a family of programming languages. C has a lot of undefined behavior and implementation-defined behavior. An “implementation” of C codifies all the implementation-defined behavior (the list of things to codify is in appendix J for C99). Each implementation of C is a separate programming language. Note that the meaning of the word “implementation” is a bit peculiar: what it really means is a language variant, there can be multiple different compiler programs that implement the same language variant.

In a given implementation of C, a byte has $2^{\texttt{CHAR_BIT}}$ possible values. All data can represented as an array of bytes: a type t has at most $2^{\texttt{CHAR_BIT} \times \texttt{sizeof(t)}}$ possible values. This number varies in different implementations of C, but for a given implementation of C, it's a constant.

In particular, pointers can only take at most $2^{\texttt{CHAR_BIT} \times \texttt{sizeof(void*)}}$ values. This means that there is a finite maximum number of addressable objects.

The values of CHAR_BIT and sizeof(void*) are observable, so if you run out of memory, you can't just resume running your program with larger values for those parameters. You would be running the program under a different programming language — a different C implementation.

If programs in a language can only have a bounded number of states, then the programming language is no more expressive than finite automata. The fragment of C that's restricted to addressable storage only allows at most $n \times 2^{\texttt{CHAR_BIT} \times \texttt{sizeof(void*)}}$ program states where $n$ is the size of the abstract syntax tree of the program (representing the state of the control flow), therefore this program can be simulated by a finite automaton with that many states. If C is more expressive, it has to be through the use of other features.

C does not directly impose a maximum recursion depth. An implementation is allowed to have a maximum, but it's also allowed not to have one. But how do we communicate between a function call and its parent? Arguments are no good if they're addressable, because that would indirectly limit the depth of recursion: if you have a function int f(int x) { … f(…) …} then all the occurrences of x on active frames of f have their own address and so the number of nested calls is bounded by the number of possible addresses for x.

A C program can use non-addressable storage in the form of register variables. “Normal” implementations can only have a small, finite number of variables that don't have an address, but in theory an implementation could allow an unbounded amount of register storage. In such an implementation, you can make an unbounded amount of recursive calls to a function, as long as its argument are register. But since the arguments are register, you can't make a pointer to them, and so you need to copy their data around explicitly: you can only pass around a finite amount of data, not an arbitrary-sized data structure that's made of pointers.

With unbounded recursion depth, and the restriction that a function can only get data from its direct caller (register arguments) and return data to its direct caller (the function return value), you get the power of deterministic pushdown automata.

I can't find a way to go further.

(Of course you could make the program store the tape content externally, through file input/output functions. But then you wouldn't be asking whether C is Turing-complete, but whether C plus an infinite storage system is Turing-complete, to which the answer is a boring “yes”. You might as well define the storage to be a Turing oracle — call fopen("oracle", "r+"), fwrite the initial tape content to it and fread back the final tape content.)

Gilles 'SO- stop being evil'
  • 44,219
  • 8
  • 122
  • 187
  • It is not immediately clear why the set of addresses should be finite. I wrote some thoughts in an answer to the question you link: http://cstheory.stackexchange.com/a/37036/43393 – Alexey B. Nov 25 '16 at 16:02
  • 16
    Sorry, but by the same logic, there are no Turing-complete programming languages at all. Each language has an explicit or implicit limitation on address space. If you create a machine with infinite memory, then random access pointers will obviously also be of infinite length. Therefore, if such machine appears, it will have to offer an instruction set for sequential memory access, along with an API for high-level languages. – IMil Nov 25 '16 at 18:39
  • 33
    @IMil This is not true. Some programming languages have no limitation on address space, not even implicitly. To take an obvious example, a universal Turing machine where the initial state of the tape forms the program is a Turing-complete programming language. Many programming languages that are actually used in practice have the same property, for example Lisp and SML. A language doesn't have to have a concept of “random access pointer”. (cont.) – Gilles 'SO- stop being evil' Nov 25 '16 at 19:03
  • 21
    @IMil (cont.) Implementations usually do for performance, but we know that an implementation running on a particular computer is not Turing-complete since it's bounded by the size of the computer's memory. But that means that the implementation doesn't implement the whole language, only a subset (of programs running in N bytes of memory). You can run the program on a computer, and if it runs out of memory, move it to a bigger computer, and so on forever or until it halts. That would be a valid way to implement the whole language. – Gilles 'SO- stop being evil' Nov 25 '16 at 19:05
  • What practical difference in terms of computability is there between a machine with unlimited memory and a machine with very large memory? – polcott Mar 11 '20 at 20:21
  • 1
    @polcott No matter how much memory you have, sooner or later you'll find some application or data set that wants even more. A theoretical machine that has unlimited memory would not have this problem. – Gilles 'SO- stop being evil' Mar 11 '20 at 20:57
  • So in other words you are not aware of any defined category of computation that requires unlimited memory? – polcott Mar 11 '20 at 21:17
  • 2
    @polcott I don't understand what you mean by “defined category of computation”. An example of something a computer with finite memory can't do, but a computer with unlimited memory can do, is to simulates itself. This answer may be what you're looking for. – Gilles 'SO- stop being evil' Mar 11 '20 at 21:41
  • I have a very excellent libx86emu x86 emulator that is running on an x86 platform taking a very tiny little fraction of total memory. https://github.com/wfeldt/libx86emu This is a machine that simulates itself and it does so in far less than 0.1% of its memory. – polcott Mar 11 '20 at 23:15
  • 1
    @polcott It can't emulate itself. It can only emulate a similar machine with less memory. – Gilles 'SO- stop being evil' Mar 12 '20 at 10:46
  • It emulates its own functionality perfectly and has 99.9% of the same memory. – polcott Mar 12 '20 at 14:33
  • For every computation C that is decidable on a Turing Machine M that is decidable on another machine N this other machine N is Turing complete for this computation C. (paraphrase of the first answer) https://stackoverflow.com/questions/7284/what-is-turing-complete

    Thus the "C" programming language and corresponding real world machines having finite memory are Turing Complete for every decidable computation that is also decidable by a Turing Machine.

    – polcott Mar 12 '20 at 18:24
  • 10
    @polcott, it makes no sense to speak about "practical" and "computability" in the same sentence. Computability (and Turing-completeness) is not about what it is practical; it is about what is mathematically true. You seem to want to talk about what holds "for all engineering purposes" but that's not what Turing-completeness is about. – D.W. Mar 12 '20 at 20:16
  • C uses separate compilation. Different compiled units do not need to use the same values of CHAR_BIT or sizeof(void *). (They do in practice, of course. But that is not a property of C, as far as I know.) – reinierpost Mar 13 '20 at 23:23
  • 1
    @reinierpost The value of CHAR_BIT is a constant in a given C implementation. So are sizeof values. The definition of the C language allows separate compilation (it defines “translation units” which can be translated independently) but does not define linking between different implementations of C. – Gilles 'SO- stop being evil' Mar 14 '20 at 20:42
  • @Gilles , OK so I suppose that rules it out then – reinierpost Mar 16 '20 at 12:03
  • "This means that there is a finite maximum number of addressable objects." I would argue this is actually wrong. As DR 260 points out, at https://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_260.htm : "If two objects have identical bit-pattern representations and their types are the same they may still compare as unequal", and "[Implementations] may also treat pointers based on different origins as distinct even though they are bitwise identical.", which implies an implementation could store an infinite amount of information on each pointer and use that when derefencing/comparing pointers. – Gabriel Ravier Jul 21 '22 at 22:45
  • 3
    @GabrielRavier That DR is only about objects with indeterminate values (the examples are all about pointer objects which neither point inside-or-just-after an object nor are null). A valid pointer can be memcpy'd to an unsigned char[] and back, so the bit-pattern representation has to contain enough information to distinguish it from all other value pointers. – Gilles 'SO- stop being evil' Jul 22 '22 at 09:53
  • @Gilles'SO-stopbeingevil' Hmmm, that makes sense. Though the abstract machine could associate the extra information with each of the bytes of the pointer such that recreating the pointer with each corresponding byte works out to the same one, I guess that means if you swap bytes from pointers with identical bit-pattern representations it won't work out correctly, and I'm pretty sure doing so is valid unless one tries to argue that integers have provenance (which would be madness) – Gabriel Ravier Jul 22 '22 at 21:09
  • 1
    @Gilles'SO-stopbeingevil' "If it runs out of memory, just move it to a bigger computer." But that is like someone saying, "My car's engine is a perpetual motion machine, but of course I have to put another gallon of gas in every 20 miles or so. So in theory it's a perpetual motion machine." But in the real world every machine will have a practical limit. A language specification, however elegant - as languages like Lisp and SML are - can't circumvent corporeal existence. – Hank Igoe Jan 09 '23 at 17:23
  • 2
    @HankIgoe This metaphor doesn't work. An engine that consumes gas is not a perpetual motion machine, even in theory. A better metaphor would be: I can drive from New York to Los Angeles in my car, even though it has a limited fuel tank. If it runs out of fuel, just add more. – Gilles 'SO- stop being evil' Jan 09 '23 at 19:03
  • "whether C plus an infinite storage system is Turing-complete, to which the answer is a boring “yes”" - isn't that the whole question? A Turing-complete machine is one with infinite (that is, indefinitely extensible) storage - any machine without infinite storage cannot be Turing-complete. The fixed length of a pointer - any by implication the number of addressable locations - can be expanded indefinitely with bank switching. – Steve Sep 02 '25 at 19:03
14

C99's addition of va_copy to the variadic argument API may give us a back door to Turing-completeness. Since it becomes possible to iterate through a variadic arguments list more than once in a function other than the one that originally received the arguments, va_args can be used to implement a pointerless pointer.

Of course, a real implementation of the variadic argument API is probably going to have a pointer somewhere, but in our abstract machine it can be implemented using magic instead.

Here's a demo implementing a 2-stack pushdown automaton with arbitrary transition rules:

#include <stdarg.h>
typedef struct { va_list va; } wrapped_stack; // Struct wrapper needed if va_list is an array type.
#define NUM_SYMBOLS /* ... */
#define NUM_STATES /* ... */
typedef enum { NOP, POP1, POP2, PUSH1, PUSH2 } operation_type;
typedef struct { int next_state; operation_type optype; int opsymbol; } transition;
transition transition_table[NUM_STATES][NUM_SYMBOLS][NUM_SYMBOLS] = { /* ... */ };

void step(int state, va_list stack1, va_list stack2);
void push1(va_list stack2, int next_state, ...) {
    va_list stack1;
    va_start(stack1, next_state);
    step(next_state, stack1, stack2);
}
void push2(va_list stack1, int next_state, ...) {
    va_list stack2;
    va_start(stack2, next_state);
    step(next_state, stack1, stack2);
}
void step(int state, va_list stack1, va_list stack2) {
    va_list stack1_copy, stack2_copy;
    va_copy(stack1_copy, stack1); va_copy(stack2_copy, stack2);
    int symbol1 = va_arg(stack1_copy, int), symbol2 = va_arg(stack2_copy, int);
    transition tr = transition_table[state][symbol1][symbol2];
    wrapped_stack ws;
    switch(tr.optype) {
        case NOP: step(tr.next_state, stack1, stack2);
        // Note: attempting to pop the stack's bottom value results in undefined behavior.
        case POP1: ws = va_arg(stack1_copy, wrapped_stack); step(tr.next_state, ws.va, stack2);
        case POP2: ws = va_arg(stack2_copy, wrapped_stack); step(tr.next_state, stack1, ws.va);
        case PUSH1: va_copy(ws.va, stack1); push1(stack2, tr.next_state, tr.opsymbol, ws);
        case PUSH2: va_copy(ws.va, stack2); push2(stack1, tr.next_state, tr.opsymbol, ws);
    }
}
void start_helper1(va_list stack1, int dummy, ...) {
    va_list stack2;
    va_start(stack2, dummy);
    step(0, stack1, stack2);
}
void start_helper0(int dummy, ...) {
    va_list stack1;
    va_start(stack1, dummy);
    start_helper1(stack1, 0, 0);
}
// Begin execution in state 0 with each stack initialized to {0}
void start() {
    start_helper0(0, 0);
}

Note: If va_list is an array type, then there are actually hidden pointer parameters to the functions. So it would probably be better to change the types of all va_list arguments to wrapped_stack.

feersum
  • 313
  • 2
  • 6
  • 1
    This might work. A possible concern is that it relies on allocating an unbounded number of automatic va_list variables stack. These variables must have an address &stack, and we can only have a bounded number of these. This requirement could be circumvented by declaring every local variable register, maybe? – chi Jun 09 '18 at 22:02
  • 1
    @chi AIUI a variable is not required to have an address unless someone tries to take the address. Also, it's illegal to declare the argument immediately preceding the ellipsis register. – feersum Jun 10 '18 at 01:21
  • By the same logic, shouldn't an int be not required to have a bound unless someone uses the bound or sizeof(int)? – chi Jun 10 '18 at 07:54
  • 4
    @chi Not at all. The standard defines as part of the abstract semantics that an int has a value between some finite bounds INT_MIN and INT_MAX. And if the value of an int overflows those bound, undefined behavior occurs. On the other hand, the standard intentionally does not require that all objects be physically present in memory at a particular address, as this permits optimizations such as storing the object in a register, storing only part of the object, representing it differently than the standard layout, or omitting it altogether if it is not needed. – feersum Jun 10 '18 at 09:11
  • 1
    Doesn't work: the size of a struct must <= SIZE_T_MAX; which in turn means there are only 1<<SIZE_T_MAX possible invocations of step before it blows up. All types are blittable in C which means you could write a va_list to a file, discard the original, read it back again and it's still valid provided whatever it referenced is still in scope. – Joshua Aug 26 '20 at 01:42
  • @Joshua As pointed out by @chi in the first comment, the game is up if you take the address of a variable (by, for example, passing it to memcpy). But if we avoid doing operations which force a standard violation to be exposed, then perhaps the implementation can cooperate with our aspirations to Turing-completeness. – feersum Aug 31 '20 at 04:25
3

Nonstandard arithmetic, maybe?

So, it seems that the issue is the finite size of sizeof(t). However, I think I know a work around.

As far as I know, C does not require an implementation to use the standard integers for its integer type. Therefore, we could use a non-standard model of arithmetic. Then, we would set sizeof(t) to some nonstandard number, and now we will never reach it in a finite number of steps. Therefore, the length of the Turing machines tape will always be less than the "maximum", since the maximum is literally impossible to reach. sizeof(t) simply is not a number in the regular sense of the word.

This is one technicality of course: Tennenbaum's theorem. It states that the only computable model of Peano arithmetic is the standard one, which obviously would not do. However, as far as I know, C does not require implementations to use data types that satisfy the Peano axioms, nor does it require the implementation to be computable, so this should not be an issue.

What should happen if you try to output a nonstandard integer? Well, you can represent any nonstandard integer using a nonstandard string, so just stream digits from the front of that string.

user3840170
  • 123
  • 7
Christopher King
  • 878
  • 1
  • 6
  • 19
1

The answer is yes, but for an unexpected reason.

I believe the above comments are correct — given size_t is bounded, you cannot represent a turing machine with unbounded states by using C as intended. However, we can use C in a way which completely circumvents the size_t issue — using only the C preprocessor.

I will not go over the whole proof here — this answer explains it best. Essentially, using deferred expressions it is possible to create recursively expanding macros that expand forever. In this way, the depth of the recursion becomes limited only to the number of scans which the machine executes — this is a physical limitation, but in theory the machine could scan forever. The answer also explains how logical operations can be constructed.

So in conclusion: yes, C is turing-complete, but you have to totally misuse the preprocessor.

John
  • 35
  • 1
  • 3
    https://stackoverflow.com/a/3136798 argues that the technique in that answer is does not yield Turing-completeness. Do you have a rebuttal to that argument? – D.W. Apr 12 '20 at 02:55
1

Compilers with pointer providence are allowed to expose Turing-complete C

Under a pointer providence model such as N2676, (size_t)-1 isn't necessarily the max allocatable bytes. Despite that being C17, compilers like Clang and GCC already assume pointer providence no matter the language, so an ideal optimizing compiler is allowed to expose Turing-complete behavior in C99.

The accepted answer is based on supercat's answer on cstheory.SE. However, a crucial portion wasn't copied. My answer provides modern justification for this condition.

[...] a number of pointers to distinct objects in excess of that could not comply with the C standard if code ever examined the sequence of characters associated with those pointers. It would be possible in some cases, however, for a compiler to determine that no code was ever going to examine the sequence of characters associated with a pointer. If each "char" was actually capable of holding any finite integer [not needed for this answer], and the machine's memory was a countably-infinite sequence of integers [given an unlimited-tape Turing machine, one could emulate such a machine although it would be really slow], then it would indeed be possible to make C a Turing-complete language.

Assume an implementation where size_t and uintptr_t are 32-bit. It's the full C language, not a subset that leaves them indeterminate at infinity. I claim that a compiler is allowed to have the following linked-list be able to allocate as much as $2^{40}$ bytes:

// tape.h
#pragma once
void init(void);
void prev(void);
void next(void);
int get(void);
void set(int);
// tape.c
#include <stdlib.h>
#include "tape.h"

// No non-C library required. Just this pure-C library

struct node { int value; struct node *next; struct node *prev; };

// The static keyword makes All the difference // It allows a closed-world assumption that only the code below in this file can touch this // See also the set/call part of // https://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html static struct node * cur;

void init(void) { cur = calloc(sizeof(struct node), 1); }

void prev(void) { if (!cur->prev) cur->prev = calloc(sizeof(struct node), 1); cur->prev->next = cur; cur = cur->prev; }

void next(void) { if (!cur->next) cur->next = calloc(sizeof(struct node), 1); cur->next->prev = cur; cur = cur->next; }

int get(void) { return cur->value; }

void set(int x) { cur->value = x; }

// turing_machine.c
int main() {
    // Not important, just adapt https://cs.stackexchange.com/a/92903/163343
    // or https://cstheory.stackexchange.com/a/12498/74073
}

Part 1: Memory allocation doesn't always correspond to integral types

sizeof(size_t) is the wrong constant to examine. A 3-bit size_t can allow allocating $2^{64}$ bytes total, as it only needs to hold the size of the largest object, not its address.

Comparing uintptr_t is better. However, casting between integers and pointers is not always one-to-one like on x86. A compiler for a 32-bit CPU can have a 24-bit uintptr_t if that's the address space, requiring a sign extension / masking for casting. Pointer authentication on ARM further diverges from bit-copy bijectivity. On ARM CHERI, sizeof(size_t) < sizeof(uintptr_t). It wouldn't be far to use a global lookup table for casting, which we will in part 4.

Gilles' accepted answer said that "pointers can only take at most $2^{\texttt{CHAR_BIT} \times \texttt{sizeof(void*)}}$ values". That is only true after being cast to uintptr_t in a non-pathological implementation. Following the ISO C standard link he provided, we see:

Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type. [emphasis mine]

"All data can [be] represented as an array of bytes" is the harder 6.2.6.1.4, but here's an analogy:

// analogy.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct foo {
    int data[400000000]; // ~ 1.5 GiB
};
struct bar {
    char data[1600000000]; // same size
};
int main() {
    struct foo *original = malloc(sizeof(struct foo));
    fread(original->data, 4, 400000000, fopen("stuff", "r"));
    // Reachable
    fwrite(original->data, 1, 1, fopen("first", "w"));
    fwrite(&original->data[1600000000-1], 1, 1, fopen("last", "w"));
    struct bar *copy = malloc(sizeof(struct bar));
    // Unreachable
    memcpy(copy->data, original->data, 1600000000);
    fwrite(copy->data, 1, 1600000000, fopen("out", "w"));
}

Use 32-bit Windows (see part 2), and the memcpy is unreachable. It'd be ~3 GiB after the second malloc, exceeding the 2 GiB limit. But creating a 1.5 GiB integer array works as expected, despite 6.2.6.1.4 "The value may be copied into an object of type unsigned char [n] (e.g., by memcpy);". It's because the compiler interprets the "resulting set of bytes ..." sentences as only applying after memcpy returns. In the analogy and part 4, it's unobservable that pointer "Values stored in non-bit-field objects of any other object type consist of n × CHAR_BIT bits" as long as no memcpy is made, so a pointer can exist without an "object representation".

Part 2: Implementation decides how much memory you can allocate

Consider Windows on a 32-bit CPU with 4 GiB, or it running a 32-bit process on 64-bit with 128 GiB. Despite a 32-bit uintptr_t, only 2**31 bytes can be allocated. Windows with the /3GB switch lets the same C99-source program allocate 3 GB of RAM. Setting ulimit -m on Linux, the same can allocate that much max. Another factor is RAM/swap installed and RAM in other programs. Embedded sometimes has malloc always return 0. In conclusion, the implementation can choose to limit allocatable memory, so it's meaningless to consider anything but the ideal theoretical optimizing compiler.

In C, I can write:

// twice.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main(){
    if(!!malloc(-1) && !!malloc(-1))
        printf("%u\n",SIZE_MAX);
}

With clang -O3 -m32 twice.c (not GCC), ./a.out prints 4294967295. It's analogous for 64-bit. While people were worrying about size_t, I just went ahead and allocated 2*SIZE_MAX and Clang let me. At first glance, this may appear to violate 6.2.4.2 of the C standard as only 1*SIZE_MAX addresses are available:

An object exists, has a constant address,25) and retains its last-stored value throughout its lifetime 25) The term ‘‘constant address’’ means that two pointers to the object constructed at possibly different times will compare equal.

But the compiler knows nobody in twice.c is "compare"ing the pointers. When it can prove so, the "has a constant address" part of the standard has no effect and can be ignored by an optimizing compiler.

Part 3: Union of a Turing-complete and -incomplete language is Turing-complete

A Turing-complete lang's compiler can be combined with a Turing-incomplete lang's one depending on the file received. An evolution is handling both in one file using extern blocks:

Consider this code:

// Example only. Will return to C99.

extern "eBPF" { int prime_16bit(short nth) { // Assume this is implemented } }

extern "C" { int main() { printf("%d\n", prime_16bit(10)); // Prints "29" } }

eBPF before 5.15 isn't Turing-complete. Assume it's invoked by the C like a CUDA launch.

Let's return to C99. Change extern to autodetection, and switch those languages to "Turing-complete C99" and "Turing-incomplete C99". Based on part 2, a compiler can choose to allow allocating 1.5 GiB if sizeof(void*) is not actually observed by the specific source code, and 0.5 GiB if it appears. glibc 2.32 did similar to detect threads. Part 4 will be a compiler allocating 16 GiB max with an unmet condition, and infinite memory when satisfied.

Physics doesn't matter for abstract C99. Real numbers are too and unstoreable but still studied. Likewise, a C99-conformant Turing-complete compiler is discussable. glibc's malloc uses mmap via sysenter. An everything-is-a-file platform without sysenter may require read and write to call mmap. Note that the platform, not our C program, calls those functions, so we stay within the other question's only putchar / getchar / standard functions restriction. A 32-bit 2 GiB platform may read and write to disk for 32 GiB memory and software-emulate 64-bit pointers. The abstract C99 machine is in theoretical space, so isn't prevented from containing a Turing machine. Its compiler can optimize tape.c into interactions with said ideal Turing machine and still conform to the C99 standard.

Remember from part 2, the implementation can refuse allocating any amount, such as beyond 2 GiB despite 32 GiB available. Again, it's only meaningful to ask "What is the limit of the largest, as we consider more abstract executions of valid terminating programs, across more compilations by all abstract compilers, sum of malloc invocations that succeeded?".

Part 4: Unexposed allocations under compilers using pointer providence

Why was the static in static struct node * cur = malloc(sizeof(node)); so important? Let's summarize N2676:

  • The Abstract Machine has a global list of all “exposed” allocations
  • Allocations come into the world unexposed (unaliased)
  • Casting a pointer to an integer exposes the allocation for that pointer’s provenance
  • Casting an integer to a pointer does a global hittest on all exposed allocations
  • If you hit an exposed allocation, congrats! You get provenance to it.
  • If you hit two exposed allocations (because of one-past-the-end), be sad and tell the programmer to “just be consistent” and only access one of them.

An implementation can have part 1's "global lookup table for casting" be the "global list of all “exposed” allocations". static in tape.c means no other files can reference cur, so the compiler can closed-world analyse. There's provably no "casting of a pointer to an integer" that "exposes the allocation for that pointer’s provenance". It only has unexposed allocations, occupying zero space in the global list. Therefore, I can allocate n max, conditional on not overflowing uintptr_t's capacity, i.e. $n * 0 < \texttt{SIZE_MAX}$. The indeterminate $\infty * 0$ becomes $0$ because the allocations are ordered. Therefore, an ideal optimizing compiler is allowed to give us the infinite tape required for our Turing machine!

The C standard doesn't say we can examine a running program's internal memory. If you insist, then before we look we might observe an unlimited number of bytes allocated successfully, then after we look, only a finite number of bytes can be allocated before crashing. Remember malloc is nondeterministic. Since the time before looking is arbitrary, we could wait infinitely long to look so that the program may allocate infinite memory.

TL;DR

  • An abstract standard-compliant C compiler can choose to let your program be Turing-complete
  • Neither you nor the rest of the program may observe the internals of tape.c while it runs
  • That compiler won't work on the computers in our universe, but it should work in the theoretical mathematical space of the C standard
Daniel T
  • 111
  • 4
  • Interesting. Most of the arguments in this answer boil down to "this is not allowed by the standard, but compilers will optimize under the as-if rule to... treat it as allowed?". This appears to be evidence of compiler bugs, not evidence that the C standard allows this behavior. – TLW Sep 26 '25 at 13:23
0

IMO, a strong limitation is that the addressable space (via the pointer size) is finite, and this is unrecoverable.

One could advocate that memory can be "swapped to disk", but at some point the address information will itself exceed the addressable size.

  • 1
    Isn't this the main point of the accepted answer? I don't think it adds anything new to the answers of this 2016 question. – chi Mar 08 '19 at 09:14
  • 1
    @chi: no, the accepted answer doesn't mention swapping to external memory, which might be believed to be a workaround. –  Mar 08 '19 at 09:20
-1

You could define a language quite similar to C, where it is allowed to change sizeof, for example by assigning

sizeof(int) = 20;
sizeof(void*) += 4;

etc. That would be Turing complete. As your requirements go up, you would just modify sizeof(void*) to be sufficiently large, same with sizeof(size_t), and then you can use realloc to make your arrays bigger.

gnasher729
  • 33,034
  • 37
  • 58
  • 2
    Wouldn't changing type sizes have to invalidate all existing structs, and arrays of pointers, at least? Or I guess this hypothetical C environment could be "managed" and adjust things to still point to the right places after growing structs? Note that changing the size of void* necessarily changes (char*)&arr[10] - (char*)&arr[0] for an array of void*. But possibly you could define rules for how this works and force code to deal with the potentially massive inconvenience. I guess if opaque things like FILE* become fully abstract, changing their internals doesn't matter? – Peter Cordes Aug 27 '20 at 09:54
-1

The problem isn’t that computers, being limited by the real world, cannot implement a Turing-complete machine. The problem in this question here is that the C language itself doesn’t allow it.

The C language could easily be changed to be Turing complete. Adding an integer type of unlimited size that can be used in malloc() and in pointer arithmetic would have solved the problem. In practice it wouldn’t make any difference, since we can’t even build a computer where 64 bit pointer sizes are the limit.

gnasher729
  • 33,034
  • 37
  • 58
-1

Removable media allows us to circumvent the unbounded memory problem. Perhaps people will think this is an abuse, but I think it's OK and essentially unavoidable anyway.

Fix any implementation of a universal Turing machine. For the tape, we use removable media. When the head runs off the end or beginning of the current disc, the machine prompts the user to insert the next or previous one. We can either use a special marker to denote the left end of the simulated tape, or have a tape that's unbounded in both directions.

The key point here is that everything the C program must do is finite. The computer only needs enough memory to simulate the automaton, and size_t only needs to be big enough to allow addressing that (actually rather small) amount of memory and on the discs, which can be of any fixed finite size. Since the user is only prompted to insert the next or previous disc, we don't need unboundedly large integers to say "Please insert disc number 123456..."

I suppose the principal objection is likely to be to the involvement of the user but that seems to be unavoidable in any implementation, because there seems to be no other way of implementing unbounded memory.

David Richerby
  • 82,782
  • 26
  • 147
  • 242
  • 3
    I'd argue that, unless the C definition requires such unbounded external storage, then this can not be accepted as a proof of Turing completeness. (ISO 9899 does not require that, of course, being written for the real-world engineering.) What concerns me is that, if we accept this, by a similar reasoning we might claim that DFAs are Turing complete, since they could be used to drive a head over a tape (the external storage). – chi Jun 10 '18 at 08:08
  • @chi I don't see how the DFA argument follows. The whole point of a DFA is that it only has read access to storage. If you allow it to "drive a head over a tape", isn't that exactly a Turing machine? – David Richerby Jun 10 '18 at 10:01
  • 4
    Indeed, I'm nitpicking a bit here. The point is: why is it OK to add a "tape" to C, let C simulate a DFA, and use this fact to claim that C is Turing complete, when we can't do the same to DFAs? If C has no way to implement unbounded memory on its own, then it should not be considered Turing complete. (I'd still call it "morally" Turing complete, at least, since the bounds are so large that in practice they do not matter in most cases) I think that to definitively settle the matter, one would need a rigorous formal spec of C (the ISO standard does not suffice) – chi Jun 10 '18 at 11:06
  • 2
    @chi It's OK because C includes file I/O routines. DFAs don't. – David Richerby Jun 10 '18 at 11:38
  • 4
    C does not completely specify what these routines do -- most of their semantics is implementation defined. A C implementation is not required to store the file contents, for instance, I think it can behave as if every file was "/dev/null", so to speak. It is also not required to store an unbounded amount of data. I'd say that your argument is correct, when considering what the vast majority of C implementations do, and generalizing that behaviour to an ideal machine. If we strictly rely on the C definition, only, forgetting the practice, I don't think it holds. – chi Jun 10 '18 at 11:56
  • @chi OK, those are fair points. I guess the direction I'm going in is that an ordinary C implementation on an ordinary computer can simulate a Turing machine, without needing to mess around with things like size_t. – David Richerby Jun 10 '18 at 12:09
  • @DavidRicherby - This seems to be "C + an external Turing-complete system is Turing-complete", which is somewhat less interesting. (External system being Turing complete? FSM? Yes, as given by it being able to follow the user prompts. Unbounded tape storage? Yes, as stated in the solution.) – TLW Jun 10 '18 at 20:43
  • @TLW What “external Turing-complete system” are you referring to? The machine’s operator just inserts the next or previous disc from the stack on request. How is that Turing-complete? – David Richerby Jun 10 '18 at 21:23
  • @DavidRicherby - The operator is following a finite state machine, and has access to an infinite tape. Swap out the FSM of 'wait for prompt, remove disk and put it in the next or previous stack, pick next or previous disk, deal with potentially running off of the tape, insert disk' with one describing a simple UTM directly. Remember that UTMs can be surprisingly simple - you can run an UTM with 4 states and 6 symbols, for instance. 6 symbols is doable purely via orientation. – TLW Jun 10 '18 at 22:03
  • @TLW Sorry, I’m not seeing your point. My answer explicitly says that the C program implements a universal Turing machine. The operator is not doing any computation so the concept of Turing-completeness doesn’t apply to them. They’re just loading the discs. – David Richerby Jun 10 '18 at 22:16
  • You don't need removable media, just an unbounded /dev/tape that supports relative seeks. Then you have fread, fwrite, and fseek(fp, long_rel_offset, SEEK_CUR). (fseek with SEEK_SET or SEEK_END absolute seeks could return -1 and perhaps even set errno=EIO or errno=ESPIPE. And fgetpos / fsetpos would always produce garbage or fail on this file: fpos_t is an opaque type anyway, not guaranteed meaningful. ftell returns a long but I think it's ok if it simply errors on some types of files, like probably a tty in real systems.) – Peter Cordes Aug 27 '20 at 09:45
-3

The problem here is that you're artificially limiting the set of C programs to just the ones that use pointers (and size_t, intptr_t, what have you), but that's not necessary. If you're forced to use only the standard library, then "no" is kinda correct like in the accepted answer, but the semantics of C are indeed Turing complete.

Say you have a Real Turing Machine (TM) and it provides a library for pushing and popping from two stacks of unbounded size, and (for thoroughness) lets also say that they just compile to push and pop assembler instructions. Now your C program can push and pop infinitely to two stacks, unrestrained by the semantics of C. Et voila, Turing completeness.

This is not the only way we can show that C is Turing complete, and it turns out that this is not a high bar to meet for programming languages. The real challenge is in constructing a language that is not turing complete (e.g. SQL), and giving a proof for it.

Note: Other answers seem to suggest that Turing completeness requires arbitrary memory access, but that's simply not true. A push-down automaton with two unbounded stacks is sufficient, hence the scenario above.

Brent
  • 2,593
  • 3
  • 16
  • 23
  • 4
    C plus a library with unbounded push and pop is Turing-complete. Sure. But the question was about C, not C plus a library that provides whatever bits might be missing from C. You've shown that it's possible to give access to a PDA in C, not that it's possible to implement a PDA in C. – Gilles 'SO- stop being evil' Jan 15 '21 at 19:16
  • 1
    The library doesn't give access to a PDA. It gives access to stacks. The question specifically asks about the semantics of C, and calling functions to access an unbounded stack is just as semantically valid as using a pointer to access RAM. You might as well argue that C doesn't let you implement RAM, but only gain access to it. The distinction is meaningless. – Brent Jan 15 '21 at 20:13
-3

Mapping "C" to a Turing equivalent abstract model of computation

C as it currently exists is not Turing complete because C inherently requires some fixed pointer size. We can however map a C like language to an abstract model of computation that is Turing complete.

The basic syntax and semantics of C could be mapped to an abstract model of computation that is Turing equivalent. The RASP model of computation is a model that "C" can be mapped to.

A variation of the RASP model is shown below that the x86/x64 language maps to. Since it is already known that "C" maps to the x86/x64 concrete models of computation we know that "C" maps to the following abstract model:

Instruction
     : INTEGER ":" OPCODE                     // Address:Opcode 
     | INTEGER ":" OPCODE INTEGER             // Address:Opcode Operand 
     | INTEGER ":" OPCODE INTEGER "," INTEGER // Address:Opcode Operand, Operand
HEXDIGIT [a-fA-F0-9]
INTEGER  {HEXDIGIT}+ 
OPCODE   HEXDIGIT{4} 
// OPCODE distinguishes between INTEGER literal and machine address operand.
// OPCODE INTEGER is space delimited.

Mapping the C language to the above abstract model of computation would enable the C language to become equivalent to a Turing machine.

Because the x86/x64 language maps to the above abstract model this also provides the basis for recognizing the subset of Turing equivalent x86/x64/C computations. As long as the required pointer size is no larger than the pointer size that is available the computation is Turing equivalent on finite hardware.

Turing equivalent computations derive equivalent output or fail to halt on equivalent input between the concrete machine and its Turing machine equivalent.

polcott
  • 95
  • 1
  • 1
  • 14
  • Being able to map A to a subset of B does not imply that A is as powerful as B. You can map DFAs to a subset of Turing machines; that does not imply that DFAs are as powerful as Turing machines. – TLW Oct 01 '22 at 22:49
  • Or do you know a way around the problems with size_t / etc as discussed in other answers? Without a workaround here size_t can only address a finite subset of a RASP machine's ram. – TLW Oct 01 '22 at 22:50
  • @TLW it is common knowledge that most computing languages are Turing equivalent except for unbounded memory access. – polcott Oct 01 '22 at 23:52
  • @TLW language features such as x64 RIP relative addressing provide for linked lists of unbounded length. – polcott Oct 01 '22 at 23:59
  • "most computing languages are Turing equivalent except for unbounded memory access" is about as true as "most DFAs are Turing equivalent except for unbounded memory access". – TLW Oct 02 '22 at 00:03
  • "x64 RIP relative addressing" is not part of the C99 standard, to the best of my knowledge. – TLW Oct 02 '22 at 00:05
  • @TLW It is a part of the Intel 64 bit processor architecture. The C programming language could be mapped to this architecture. – polcott Oct 02 '22 at 01:11
  • You can map DFAs to a subset of the Intel 64 bit processor architecture too; that does not mean that DFAs are Turing-complete. – TLW Oct 02 '22 at 01:15
  • @TLW This will have to be my last reply to you or SE will cut off all comments. As I said most any conventional programming languages C, Fortran, COBOL, C++ et cetera are Turing equivalent except for the unlimited memory requirement. – polcott Oct 02 '22 at 01:38
  • I believe my previous comment already addressed that. By that same logic, "most DFAs are Turing equivalent except for the unlimited memory requirement". – TLW Oct 02 '22 at 01:46
  • @TLW No you are wrong: Chomsky_hierarchy – polcott Oct 02 '22 at 02:25
  • Precisely. It is wrong to state that "most computing languages are Turing equivalent except for unbounded memory access", just as it is wrong to state most DFAs are Turing equivalent except for unbounded memory access. – TLW Oct 02 '22 at 03:36
  • Sorry, sarcasm sometimes doesn't come over well on the internet. Read "is about as true as" from my previous comment as "is obviously not true, because the same logic would also prove the following thing that is obviously false: ". – TLW Oct 02 '22 at 03:38
  • @TLW I opened up this thread on Google Groups comp.theory We can talk this all the way through there. – polcott Oct 02 '22 at 03:44
-3

Unlike what has been stated, the C standard is more powerful than a DFA. Ignoring other features for information access other than pointers, it is probably true that a DFA could perfectly model C for a fixed character size and size_t due to the finite amount of memory.

But for a given program, i.e a specific input for the C language, we can change size_t to fit the needs of the program. Without some fancy workaround we (for the sake of argument) still can't have an infinite amount of memory, but we can change the amount we have, i.e the size of the band, depending on the input. This means that the C family is pretty obviously a deterministic linear bound automata (a deterministic Turing machine with bounded memory), which is quite a bit stronger than a DFA or even a NDPA. Since I dont see any reason why our function of the input can't be quadratic, and NSPACE(O(n)) being a subset of DSPACE(O(n^2)) per Savitch's theorem, C is just as powerful as any nondeterministic LBA. This puts it just below a full Turing machine in terms of power.

I spent some time trying to figure out if you could use streams to bypass the size_t constraints, but all options either only go in one direction (like stdin) or have a moving pointer. I think for you to be able to access an infinite tape of sorts you would need some sort of operating system or another way to change the direction. It might be a matter of insufficient creativity on my part but I dont think so.

In conclusion: C probably is not turing complete, but it is very close to it, both in what would need to change, and in terms of computational power.

Cris
  • 1
  • 1
  • 1
    How do you change size_t based on the input? The program is compiled once, based on a specific value of size_t, before the input is available, and then the one executable program needs to work on all inputs. It is "Turing", not "turning". – D.W. Jun 04 '24 at 21:12
  • Consider a program that outputs sizeof(size_t) and then calls run_arbitrary_dlba_from_stdout() What size of size_t do we need to execute this program? – TLW Jun 06 '24 at 15:40
  • Unfortunately, output is irrevocable. And the size of size_t is potentially visible in the output. As a result we cannot change size_t to fit the needs of an arbitrary program without time travel. – TLW Jun 06 '24 at 15:40
-3

In practice, these restrictions are irrelevant to Turing completeness. The real requirement is to allow the tape to be arbitrary long, not infinite. That would create a halting problem of a different kind (how does the universe "compute" the tape?)

It's as bogus as saying "Python isn't Turing complete because you can't make a list infinitely large".

[Edit: thanks to Mr. Whitledge for clarifying how to edit.]

  • 8
    I don't think this answers the quesiton. The question already anticipated this answer, and explained why it isn't valid: "although the C standard allows for size_t to be arbitrarily large, it must be fixed at some length, and no matter what length it is fixed at it is still finite". Do you have any response to that argument? I don't think we can count the question as answered unless the answer explains why that argument is wrong (or right). – D.W. Nov 26 '16 at 18:39
  • 6
    At any given time, a value of type size_t is finite. The problem is that you can't establish a bound for size_t that's valid throughout the computation: for any bound, a program might overflow it. But the C language states that there exists a bound for size_t: on a given implementation, it can only grow up to sizeof(size_t) bytes. Also, be nice. Saying that people who criticize you “can't think on their own” is rude. – Gilles 'SO- stop being evil' Nov 28 '16 at 07:51
  • 2
    This is the correct answer. A Turning machine does not require an infinite tape, it requires an "arbitrarily long" tape. That is, you may assume that the tape is as long as it needs to be to complete the computation. You may also assume that your computer has as much memory as it needs. An infinite tape is absolutely not required, because any computation that halts in finite time cannot possibly use an infinite amount of tape. – Jeffrey L Whitledge Jan 03 '18 at 19:28
  • 1
    What this answer shows is that for each TM you can write a C implementation with sufficient pointer length to simulate it. It however isn't possible to write one C implementation that can simulate any TM. So the specification prohibits that any particular implementation is T-complete. It isn't T-complete itself either, because pointer length is fixed. –  Jan 04 '18 at 08:31
  • But that's just saying that you can't make a Turing machine ANYWHERE: because you'll always be limited by address size or some equivalent restriction (how many skips, etc.). – Mark Rosenblitt-Janssen Jan 07 '18 at 16:53
  • Pointer length isn't fixed: I think you can have different object files compiled with different pointer lengths. – reinierpost Feb 22 '19 at 22:22
  • 2
    This is another correct answer which is hardly visible due to the incapacity of most individuals in this community. Meanwhile, the accepted answer is false and its comment section is guarded by moderators deleting critical comments. Bye bye, cs.stackexchange. –  Mar 07 '19 at 19:31
  • 2
    This answer is actually right. – Brent Jan 15 '21 at 17:51
  • @JeffreyLWhitledge - the problem is that size_t is observable, whereas unused portions of a Turing machine's tape are not. Consider a hypothetical compiler that picked a "large enough" value of size_t. Now consider what happens when you compile and run printf("%zu", SIZE_MAX); run_utm_on_stdin();. – TLW Oct 01 '22 at 22:46
  • @reinierpost - "Pointer length isn't fixed". It is actually, at least within a single compile by C89 spec. (The as-if rule applies, so an implementation could mix pointer sizes "under-the-hood", but it can't let the fact it's doing so affect the behavior of a standard-compliant program, so it's a moot point.) – TLW Oct 01 '22 at 23:11
  • "The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type. The size is determined from the type of the operand. The result is an integer. If the type of the operand is a variable length array type, the operand is evaluated; otherwise, the operand is not evaluated and the result is an integer constant.". Note the "constant" part there. – TLW Oct 01 '22 at 23:13
  • @TLW: Yes, but C has separate compilation, and dynamic linking. However, it can be argued that those are not features of the C language itself. – reinierpost Oct 03 '22 at 11:13
-3

Choose size_t to be infinitely large

You could choose size_t to be infinitely large. Naturally, it is impossible to realize such an implementation. But that's no surprise, given the finite nature of the world we live in.

Practical Implications

But even if it were possible to realize such an implementation, there would be practical issues. Consider the following C statement:

printf("%zu\n",SIZE_MAX);

This prints a decimal representation of SIZE_MAX to standard out. Presumably, SIZE_MAX is $O(2^{size\_t})$. So if size_t is infinitely large, SIZE_MAX is also infinitely large. The only way I know to print a decimal form of an infinitely large number is to produce an infinite stream of decimal digits. This means that printf will not terminate in some cases.

Fortunately, for our theoretical purposes, I could not find any requirement in the specification that guarantees printf will terminate for all inputs. So, as far as I am aware, we do not violate the C specification here.

On Computational Completeness

It still remains to prove that our theoretical implementation is Turing Complete. We can show this by implementing "any single-taped Turing Machine".

Most of us have probably implemented a Turing Machine as a school project. I won't give the details of a specific implementation, but here's a commonly used strategy:

  • The number of states, number of symbols, and state transition table are fixed for any given machine. So we can represent states and symbols as numbers, and the state transition table as a 2-dimensional array.
  • The tape can be represented as a linked list. We can either use a single double-linked list, or two single-linked lists (one for each direction from the current position).

Now let's see what's required to realize such an implementation:

  • The ability to represent some fixed, but arbitrarily large, set of numbers. In order to represent any arbitrary number, we choose MAX_INT to be infinite as well. (Alternatively, we could use other objects to represent states and symbols.)
  • The ability to construct an arbitrarily large linked list for our tape. Once again, there is no finite limit on the size. This means we cannot construct this list up-front, as we would spend forever just to construct our tape. But, we can construct this list incrementally if we use dynamic memory allocation. We can use malloc, but there's a little more we must consider:
    • The C specification allows malloc to fail if, e.g., available memory has been exhausted. So our implementation is only truly universal if malloc never fails.
    • However, if our implementation is run on a machine with infinite memory, then there is no need for malloc to fail. Without violating the C standard, our implementation will guarantee that malloc will never fail.
  • The ability to dereference pointers, lookup array elements, and access the members of a linked list node.

So the above list is what is necessary to implement a Turing Machine in our hypothetical C implementation. These features must terminate. Anything else, however, may be allowed to not terminate (unless required by the standard). This includes arithmetic, IO, etc.

Nathan Davis
  • 642
  • 1
  • 5
  • 13
  • 6
    What would printf("%zu\n",SIZE_MAX); print on such an implementation? – Ruslan Nov 26 '16 at 13:26
  • 1
    @Ruslan, such an implementation is impossible, just like it's impossible to implement a Turing machine. But if such an implementation were possible, I imagine it would print some decimal representation of an infinitely large number -- presumably, an infinite stream of decimal digits. – Nathan Davis Nov 26 '16 at 22:48
  • 3
    @NathanDavis It's possible to implement a Turing machine. The trick is that you don't need to build an infinite tape — you just build the used portion of the tape incrementally as needed. – Gilles 'SO- stop being evil' Nov 28 '16 at 07:46
  • 2
    @Gilles: In this finite universe in which we are living, it is is impossible to implement a Turing machine. – gnasher729 Nov 28 '16 at 08:45
  • @Gilles, formal definitions aside, that argument seems suspiciously close to the OPs original objection regarding size_t. You could incrementally increase size_t as well by suspending the machine, saving its state, building a new machine with an increased size_t, restoring the state, and resuming execution on the new machine. – Nathan Davis Nov 28 '16 at 17:48
  • 3
    @NathanDavis But if you do that, then you've changed sizeof(size_t) (or CHAR_BITS). You can't resume from the new state, you have to start again, but the execution of the program may be different now that those constants are different – Gilles 'SO- stop being evil' Nov 28 '16 at 20:08
  • @Gilles, I don't know why you'd have to start over. You could certainly pad the words of the saved state when you resume to adjust for the new word size. And you can certainly write a C program simulating a Turing Machine that does not rely on sizeof(size_t), CHAR_BITS, or anything of that nature. But, even if you did have to restart, you would only need to repeat a finite number of steps to reach the previous point. – Nathan Davis Nov 28 '16 at 20:28
  • 3
    @NathanDavis Then you wouldn't have an implementation of the C language, you'd have an implementation of the subset of the C language consisting of programs whose behavior does not depend on sizeof(size_t). Not only that, but if you add up all the things that change (sizeof(ssize_t), sizeof(void*), etc.) you might not end up with a Turing-complete language anymore (I don't know if you do but it isn't obvious). – Gilles 'SO- stop being evil' Nov 28 '16 at 20:43
  • @Gilles, huh? Are you saying there exists some finite sizeof(size_t) for which it is theoretically impossible to implement the C specification? I'm just saying that a particular C program simulating a Turing Machine need not rely on sizeof(size_t) (other than, perhaps, to check whether it is necessary to switch to a larger machine). But that does not limit the implementation of C upon which that program is built. – Nathan Davis Nov 28 '16 at 21:42
  • @NathanDavis The issue is not whether it's possible to implement the C specification, but whether the resulting language is Turing-complete. Which was kind of the point of this thread. You would need to check not only that the program doesn't contain sizeof(size_t) but more generally that it behaves identically under the expanded C implementation — for example, malloc would need to return the same pointer values if the Turing machine simulator uses it, etc. Increasing CHAR_BIT may be a better bet. – Gilles 'SO- stop being evil' Nov 29 '16 at 19:25
  • @NathanDavis - That would work, if all operations were restartable. However, not all operations are. (For instance: output to stdout.) – TLW Dec 27 '16 at 04:03
  • @TLW, not sure what you mean by "that" (something in the question or in the comments?). However, there's no requirement that a Turing machine simulation use stdout. So perhaps the program we use to simulate a Turing machine would simply not use stdout. – Nathan Davis Dec 27 '16 at 07:12
  • @NathanDavis: From a Turing-machine-theoretical standpoint, every program would be restartable, and any C program that would hit size limits associated with CHAR_BIT would do so i finite time. So perhaps an implementation which runs until a program terminates or hits the limits of CHAR_BIT, then restarts with CHAR_BIT being 1<<(CHAR_BIT-1), so if it CHAR_BIT starts out being 16 for the first pass, it would be 32768 for the second pass, about 1E9800 for the third pass, etc. so restarts really shouldn't hurt speed much. – supercat Jan 03 '18 at 06:51
  • C programs can use dynamic linking; the fragments linked together don't need to use the same size_t. So in principle, a C program can use arbitrarily high size_t values. This doesn't buy you anything unless the memory being addressed can be arbitrarily extended as well, as NathanDavis remarked. – reinierpost Jan 03 '18 at 21:50
  • 1
    @reinierpost: I don't think that's true. sizeof(size_t) is an implementation-defined constant, the same across all translation units. (And in practice will be part of the ABI for struct layouts and so on). inline allowing identical definitions of the same function in different TUs is one clear way that it's not ok for different TUs to actually have the same code do something different. Like most simply inline foo() { return sizeof(size_t); } has to return the same thing when called from anywhere in the program. – Peter Cordes Aug 27 '20 at 04:28
  • 1
    ISO C doesn't define linking together code from different implementations so that's already a non-starter. And real implementations don't define the behaviour of linking together object files / libraries compiled for incompatible ABIs. It usually breaks in real life when asm was generated with different struct layouts. – Peter Cordes Aug 27 '20 at 04:28
  • @Peter Cordes: OK, if linking is defined in the C standard and requires all units to have been compiled with the same size_t, then that is out. – reinierpost Aug 27 '20 at 07:42
  • 1
    @reinierpost: Not in those exact words, but yes a program can consist of multiple translation units. I don't remember if there's explicit language requiring implementation-defined constants like sizeof(size_t) to actually be constant, but it's at least implied that they're constant across every translation unit. And yes, the standard is written as a single C implementation handling every TU o a program; using a common ABI to enable linking between compilers is an implementation-defined feature. As Gilles' answer explains, agreeing on constants/layout = a C implementation in standardese. – Peter Cordes Aug 27 '20 at 07:48
-4

The main argument here was that the size of the size_t is finite, although can be infinitely large.

There is a workaround for it, though I am not sure if this coincides with ISO C.

Assume you have a machine with infinite memory. Thus you are not bounded for pointer size. You still have your size_t type. If you ask me what is sizeof(size_t) the answer will be just sizeof(size_t). If you ask if it is greater than 100 for example the answer is yes. If you ask what is sizeof(size_t) / 2 as you could guess the answer is still sizeof(size_t). If you want to print it we can agree on some output. The difference of these two can be NaN and so on.

The summary is that relaxing the condition for size_t have finite size won't break any programs already existing.

P.S. Allocating memory sizeof(size_t) is still possible, you need only countable size, so let's say you take all evens (or similar trick).

Eugene
  • 1,126
  • 1
  • 6
  • 15
  • 2
    "The difference of these two can be NaN". No it cannot be. There is no such thing as a NaN of integer type in C. – TLW Dec 24 '16 at 05:32
  • 3
    According to the standard, sizeof has to return a size_t. So you have to choose some particular value. – Draconis Mar 09 '19 at 00:20
  • My reasoning is similar. Instead I make sizeof(size_t) whatever finite size that is large enough for the current computation. This works for all computations requiring a finite amount of memory. Counting to infinity and storing the value of each increment is not on of these computations. – polcott Sep 03 '20 at 22:57
  • @polcott - what would printf("%zu\n",sizeof(size_t)); print on such an implementation? – TLW Oct 01 '22 at 19:58
  • @TLW The size_t that I am referring to is a variable and not the conventional fixed constant. – polcott Oct 01 '22 at 20:07
  • Then it is an interesting language; it is not standard C. The C89 spec, for instance, requires that SIZE_MAX be a constant expression. Different implementations may choose different values, but it must be constant within a single compile. – TLW Oct 01 '22 at 22:41
  • 7.18.3 Limits of other integer types: "Each instance of these macros shall be replaced by a constant expression [...] limit of size_t SIZE_MAX" – TLW Oct 01 '22 at 22:41