web-dev-qa-db-fra.com

Le préprocesseur C99 Turing est-il terminé?

Après avoir découvert les capacités du préprocesseur Boost je me suis demandé: le préprocesseur C99 Turing est-il complet?

Sinon, que manque-t-il pour ne pas se qualifier?

69
Anycorn

Ici est un exemple d'utilisation abusive du préprocesseur pour implémenter une machine de Turing. Notez qu'un script de build externe est nécessaire pour réinjecter la sortie du préprocesseur dans son entrée, donc le préprocesseur en lui-même n'est pas Turing complet. Pourtant, c'est un projet intéressant.

D'après la description du projet lié ci-dessus:

le préprocesseur n'est pas Turing terminé, du moins pas si le programme n'est prétraité qu'une seule fois. Cela est vrai même si le programme est autorisé à s’inclure. (La raison étant que pour un programme donné, le préprocesseur n'a qu'un nombre fini d'états, plus une pile composée des emplacements à partir desquels le fichier a été inclus. Ce n'est qu'un automate déroulant.)

La réponse de Paul Fultz II est assez impressionnante et certainement plus proche que je ne le pensais jamais du préprocesseur, mais ce n'est pas une vraie machine de Turing. Le préprocesseur C a certaines limites qui l'empêchent d'exécuter un programme arbitraire comme une machine Turing, même si vous aviez une mémoire et un temps infinis. La section 5.2.4.1 de la spécification C donne les limites minimales suivantes pour un compilateur C:

  • 63 niveaux d'imbrication d'expressions entre parenthèses dans une expression complète
  • 63 caractères initiaux significatifs dans un identifiant interne ou un nom de macro
  • 4095 identificateurs de macro définis simultanément dans une unité de traduction de prétraitement
  • 4095 caractères dans une ligne source logique

Le mécanisme de compteur ci-dessous nécessite une définition de macro par valeur, donc la limite de définition de macro limitera le nombre de fois que vous pouvez boucler (EVAL(REPEAT(4100, M, ~)) donnerait un comportement indéfini). Cela met essentiellement un plafond à la complexité du programme que vous pouvez exécuter. L'imbrication et la complexité des extensions à plusieurs niveaux peuvent également atteindre l'une des autres limites.

Ceci est fondamentalement différent de la limitation de la "mémoire infinie". Dans ce cas, la spécification indique spécifiquement qu'un compilateur C conforme aux normes n'est requis que pour se conformer à ces limites, même s'il a un temps infini, de la mémoire, etc. Tout fichier d'entrée dépassant ces limites peut être traité de manière imprévisible ou indéfinie (ou carrément rejeté). Certaines implémentations peuvent avoir des limites plus élevées, voire aucune limite, mais cela est considéré comme "spécifique à l'implémentation" et ne fait pas partie de la norme. Il peut être possible d'utiliser la méthode de Paul Fultz II pour implémenter quelque chose comme une machine de Turing sur une implémentation spécifique du compilateur qui n'a pas de limites finies, mais d'une manière générale sens de "cela peut-il être fait sur n'importe quel pré-processeur C99 arbitraire et conforme aux normes", la réponse est non. Puisque la limite ici est intégrée dans le langage lui-même et non pas simplement un effet secondaire de notre incapacité à construire un ordinateur infini, je dis que cela rompt l'intégralité de Turing.

31
bta

Les macros ne se développent pas directement récursivement, mais il existe des moyens de contourner ce problème.

La façon la plus simple de faire une récursivité dans le préprocesseur est d'utiliser une expression différée. Une expression différée est une expression qui nécessite plus d'analyses pour se développer complètement:

#define EMPTY()
#define DEFER(id) id EMPTY()
#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()
#define EXPAND(...) __VA_ARGS__

#define A() 123
A() // Expands to 123
DEFER(A)() // Expands to A () because it requires one more scan to fully expand
EXPAND(DEFER(A)()) // Expands to 123, because the EXPAND macro forces another scan

Pourquoi est-ce important? Eh bien, lorsqu'une macro est analysée et développée, elle crée un contexte désactivant. Ce contexte de désactivation entraînera un jeton, qui fait référence à la macro en cours d'expansion, à être peint en bleu. Ainsi, une fois son bleu peint, la macro ne se dilate plus. C'est pourquoi les macros ne se développent pas récursivement. Cependant, un contexte de désactivation n'existe que pendant une analyse, donc en différant une expansion, nous pouvons empêcher nos macros de devenir peintes en bleu. Nous aurons juste besoin d'appliquer plus d'analyses à l'expression. Nous pouvons le faire en utilisant cette macro EVAL:

#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
#define EVAL5(...) __VA_ARGS__

Maintenant, si nous voulons implémenter une macro REPEAT à l'aide de la récursivité, nous avons d'abord besoin d'opérateurs d'incrémentation et de décrémentation pour gérer l'état:

#define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__)
#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__

#define INC(x) PRIMITIVE_CAT(INC_, x)
#define INC_0 1
#define INC_1 2
#define INC_2 3
#define INC_3 4
#define INC_4 5
#define INC_5 6
#define INC_6 7
#define INC_7 8
#define INC_8 9
#define INC_9 9

#define DEC(x) PRIMITIVE_CAT(DEC_, x)
#define DEC_0 0
#define DEC_1 0
#define DEC_2 1
#define DEC_3 2
#define DEC_4 3
#define DEC_5 4
#define DEC_6 5
#define DEC_7 6
#define DEC_8 7
#define DEC_9 8

Ensuite, nous avons besoin de quelques macros supplémentaires pour faire de la logique:

#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)

#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 ~, 1,

#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0

#define BOOL(x) COMPL(NOT(x))

#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t

#define IF(c) IIF(BOOL(c))

#define EAT(...)
#define EXPAND(...) __VA_ARGS__
#define WHEN(c) IF(c)(EXPAND, EAT)

Maintenant, avec toutes ces macros, nous pouvons écrire une macro récursive REPEAT. Nous utilisons un REPEAT_INDIRECT macro pour faire référence à elle-même de manière récursive. Cela empêche la macro d'être peinte en bleu, car elle se développera sur une analyse différente (et en utilisant un contexte de désactivation différent). Nous utilisons OBSTRUCT ici, ce qui retardera l'expansion deux fois. Ceci est nécessaire car le WHEN conditionnel applique déjà une analyse.

#define REPEAT(count, macro, ...) \
    WHEN(count) \
    ( \
        OBSTRUCT(REPEAT_INDIRECT) () \
        ( \
            DEC(count), macro, __VA_ARGS__ \
        ) \
        OBSTRUCT(macro) \
        ( \
            DEC(count), __VA_ARGS__ \
        ) \
    )
#define REPEAT_INDIRECT() REPEAT

//An example of using this macro
#define M(i, _) i
EVAL(REPEAT(8, M, ~)) // 0 1 2 3 4 5 6 7

Maintenant, cet exemple est limité à 10 répétitions, en raison des limites du compteur. Tout comme un compteur de répétition dans un ordinateur serait limité par la mémoire finie. Plusieurs compteurs de répétition peuvent être combinés ensemble pour contourner cette limitation, tout comme dans un ordinateur. De plus, nous pourrions définir une macro FOREVER:

#define FOREVER() \
    ? \
    DEFER(FOREVER_INDIRECT) () ()
#define FOREVER_INDIRECT() FOREVER
// Outputs question marks forever
EVAL(FOREVER())

Cela va essayer de sortir ? pour toujours, mais finira par s'arrêter car il n'y a plus d'analyses appliquées. Maintenant, la question est, si nous lui donnions un nombre infini d'analyses, cet algorithme serait-il complet? Ceci est connu comme le problème d'arrêt, et l'exhaustivité de Turing est nécessaire pour prouver l'indécidabilité du problème d'arrêt. Donc, comme vous pouvez le voir, le préprocesseur peut agir comme un langage complet de Turing, mais au lieu d'être limité à la mémoire finie d'un ordinateur, il est plutôt limité par le nombre fini d'analyses appliquées.

131
Paul Fultz II

Pour que Turing soit complet, il faut définir une récursion qui ne finira jamais - on les appelle opérateur mu-récursif .

Pour définir un tel opérateur, il faut un espace infini d'identifiants définis (au cas où chaque identifiant est évalué un nombre fini de fois), car on ne peut pas savoir a priori une limite supérieure de temps dans laquelle le résultat est trouvé. Avec un nombre fini d'opérateurs dans le code, il faut pouvoir vérifier un nombre illimité de possibilités.

Donc cette classe de fonctions ne peut pas être calculée par le préprocesseur C car dans le préprocesseur C il y a un nombre limité de macros définies et chacune n'est développée qu'une seule fois.

Le préprocesseur C utilise le algorithme de Dave Prosser (écrit par Dave Prosser pour l'équipe WG14 en 1984). Dans cet algorithme, une macro est peinte en bleu au moment de la première expansion; un appel récursif (ou appel récursif mutuel) ne le développe pas, car il a déjà été peint en bleu au moment où la première expansion commence. Ainsi, avec un nombre fini de lignes de prétraitement, il est impossible de faire des appels infinis de fonctions (macros), ce qui caractérise les opérateurs mu-récursifs.

Le préprocesseur C ne peut calculer que opérateurs sigma-récursifs .

Pour plus de détails, voir le cours de calcul de Marvin L. Minsky (1967) - Computation: Finite and Infinite Machines , Prentice-Hall, Inc. Englewood Cliffs, N.J. etc.

5
alinsoar

C'est Turing complet dans certaines limites (comme tous les ordinateurs car ils n'ont pas de RAM infinie). Découvrez le genre de choses que vous pouvez faire avec Boost Preprocessor .

Modifier en réponse aux modifications de questions:

La principale limitation de Boost est la profondeur maximale d'expansion des macros qui est spécifique au compilateur. De plus, les macros qui implémentent la récursivité (FOR ..., ENUM ..., etc.) ne sont pas vraiment récursives, elles apparaissent simplement de cette façon grâce à un tas de macros presque identiques. Dans l'ensemble, cette limitation n'est pas différente d'avoir une taille de pile maximale dans un langage réellement récursif.

Les deux seules choses qui sont vraiment nécessaires pour une complétude de Turing limitée (compatibilité de Turing?) Sont l'itération/la récursivité (constructions équivalentes) et la ramification conditionnelle.

4
Cogwheel