La Complexité de la Complétude de Turing en C

Introduction

La question de savoir si le langage de programmation C est Turing-complet⁤ suscite des débats ‌parmi les ​informaticiens. Bien que des discussions aient eu lieu dans le passé, ⁣il semble que la réponse soit ‍négative pour des ⁤raisons assez subtiles.

Simulation d’une Machine de Turing

Dans la plupart des langages de programmation, il est possible de simuler une machine de Turing. Cela peut être réalisé ⁣par :

  • La simulation ‍d’un automate fini à l’aide‍ d’un programme utilisant une quantité limitée de mémoire.
  • La représentation de la bande⁢ par une paire de ⁤listes chaînées ⁤d’entiers, qui symbolisent le contenu de la ​bande avant et ⁣après la position actuelle. Déplacer le pointeur implique de transférer la tête d’une liste à l’autre.

Bien ⁣qu’une implémentation concrète sur un ordinateur puisse manquer de mémoire si la bande devient ‍trop longue, une version idéale pourrait exécuter le programme de la machine de Turing de manière fidèle. Cela pourrait être réalisé avec du papier et⁢ un stylo, ou en utilisant un ordinateur doté de plus de mémoire et un compilateur adapté à une architecture avec une plus grande taille de mot.

Limitations⁢ de C

Cependant, cette approche ne⁤ fonctionne‌ pas avec le langage C, car il est impossible d’avoir une liste chaînée qui puisse croître indéfiniment : il existe toujours une limite au nombre de nœuds.

Comprendre l’Implémentation en C

Pour saisir pourquoi cela est le⁢ cas, il est essentiel de comprendre ce qu’est une implémentation en C. C est en réalité⁢ une⁤ famille de ‍langages de programmation. La norme ISO C définit la syntaxe et la sémantique de cette famille de langages. C présente de‍ nombreux comportements indéfinis ‌et des comportements définis‍ par l’implémentation. Chaque implémentation ⁤de C représente un langage distinct, et le terme « implémentation » désigne en fait une variante de langage.

Dans une implémentation donnée de C, un octet peut avoir $2^{texttt{CHAR_BIT}}$ valeurs possibles. Toutes les⁢ données ​peuvent être représentées sous forme de tableau d’octets ‍: un ‌type t a au maximum $2^{texttt{CHAR_BIT} times texttt{sizeof(t)}}$ valeurs possibles. Ce nombre varie selon les implémentations, mais pour une implémentation donnée, il reste constant.

Limites des ⁤Pointeurs

En particulier,​ les⁤ pointeurs peuvent prendre au maximum $2^{texttt{CHAR_BIT} times texttt{sizeof(void)}}$ valeurs. Cela signifie qu’il existe un nombre maximum d’objets adressables. Les‌ valeurs de CHAR_BIT et sizeof(void) sont observables, donc si vous manquez de mémoire, vous ne pouvez pas simplement relancer votre programme avec des valeurs plus grandes pour ces paramètres. Vous exécuteriez le programme sous un langage de programmation différent — une autre implémentation de C.

État Limité⁣ des Programmes

Si ⁣les programmes d’un langage ne peuvent avoir qu’un nombre limité d’états, alors ce langage n’est pas‌ plus expressif que les automates finis. La ‍partie‍ de C qui est limitée au stockage adressable ne permet au⁢ maximum ‌que ‍$n times 2^{texttt{CHAR_BIT} times texttt{sizeof(void*)}}$ états de programme, ​où $n$ est la taille de l’arbre de syntaxe abstraite du programme. Par conséquent, ce programme peut être simulé par⁤ un automate‍ fini avec ce nombre d’états. Si ​C ⁤est plus expressif, cela doit passer par l’utilisation d’autres fonctionnalités.

Profondeur de Récursion

C n’impose pas directement une profondeur de récursion maximale. Une implémentation peut en avoir une, ⁣mais elle peut aussi ne pas ‍en avoir. Cependant, comment communiquer entre⁣ un appel de fonction et son parent ? Les arguments ne sont pas utiles s’ils sont adressables, car cela limiterait indirectement la profondeur de ⁤récursion. Par exemple, si vous avez une fonction int f(int x) { … f(…) …}, toutes​ les occurrences de x ​ dans les frames actives de f ont leur propre adresse, et donc le nombre d’appels imbriqués est limité par le ‍nombre d’adresses ⁣possibles pour x.

Utilisation de Stockage Non-Adressable

Un programme C peut utiliser un stockage non-adressable sous la forme de variables register. Les implémentations « normales » ‌ne⁤ peuvent avoir qu’un nombre limité de variables sans adresse, mais en théorie, une implémentation pourrait permettre une quantité illimitée de stockage register. Dans‍ une telle implémentation, ⁢vous pourriez effectuer un nombre illimité d’appels récursifs à une fonction, tant que ses arguments sont register. ⁤Cependant, comme les arguments sont register, vous ne pouvez pas créer de pointeur​ vers eux, ce qui nécessite‌ de copier explicitement leurs données : vous ne pouvez passer qu’une quantité finie de données, et non une structure de données de taille arbitraire composée de pointeurs.

Conclusion

Avec une profondeur de récursion illimitée et la restriction selon laquelle une‌ fonction ne peut obtenir des données ‍que ‌de son⁢ appelant direct ⁤(arguments register) et renvoyer des ⁣données à ‌son⁣ appelant direct (valeur de retour de la fonction), vous obtenez⁤ la puissance des‌ automates à pile déterministes.‍

Il est difficile de trouver une manière d’aller plus loin dans cette discussion. Bien sûr, il serait possible ‍de stocker le contenu de la ⁢bande à l’extérieur, via des fonctions d’entrée/sortie de fichiers. Mais cela soulèverait ‌une question différente : celle de savoir si C, associé à un système de stockage infini, est Turing-complet, à laquelle la réponse serait un ennuyeux « oui ».

Show Comments (0)
Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *