A continuación está mi demostración favorita (actual) de por qué analizar C ++ es (probablemente) Turing completo , ya que muestra un programa que es sintácticamente correcto si y solo si un entero dado es primo.
Por lo tanto, afirmo que C ++ no es libre de contexto ni sensible al contexto .
Si permite secuencias de símbolos arbitrarias en ambos lados de cualquier producción, produce una gramática de tipo 0 ("sin restricciones") en la jerarquía de Chomsky , que es más poderosa que una gramática sensible al contexto; las gramáticas sin restricciones son completas de Turing. Una gramática sensible al contexto (Tipo-1) permite múltiples símbolos de contexto en el lado izquierdo de una producción, pero el mismo contexto debe aparecer en el lado derecho de la producción (de ahí el nombre "sensible al contexto"). [1] Las gramáticas sensibles al contexto son equivalentes a las máquinas de Turing con límites lineales .
En el programa de ejemplo, el cálculo principal podría ser realizado por una máquina de Turing con límites lineales, por lo que no prueba la equivalencia de Turing, pero la parte importante es que el analizador debe realizar el cálculo para realizar un análisis sintáctico. Podría haber sido cualquier cálculo expresable como una creación de instancias de plantilla y hay muchas razones para creer que la creación de instancias de plantilla C ++ es completa de Turing. Ver, por ejemplo, el artículo de 2003 de Todd L. Veldhuizen .
De todos modos, C ++ puede ser analizado por una computadora, por lo que ciertamente podría ser analizado por una máquina Turing. En consecuencia, una gramática sin restricciones podría reconocerlo. En realidad, escribir tal gramática no sería práctico, por lo que el estándar no intenta hacerlo. (Vea abajo.)
El problema con la "ambigüedad" de ciertas expresiones es principalmente una pista falsa. Para empezar, la ambigüedad es una característica de una gramática particular, no un lenguaje. Incluso si se puede demostrar que un idioma no tiene gramáticas inequívocas, si puede ser reconocido por una gramática libre de contexto, es libre de contexto. Del mismo modo, si no puede ser reconocido por una gramática libre de contexto pero puede ser reconocido por una gramática sensible al contexto, es sensible al contexto. La ambigüedad no es relevante.
Pero en cualquier caso, como la línea 21 (es decir auto b = foo<IsPrime<234799>>::typen<1>();
) en el programa a continuación, las expresiones no son ambiguas en absoluto; simplemente se analizan de manera diferente según el contexto. En la expresión más simple del problema, la categoría sintáctica de ciertos identificadores depende de cómo se hayan declarado (tipos y funciones, por ejemplo), lo que significa que el lenguaje formal debería reconocer el hecho de que dos cadenas de longitud arbitraria en Los mismos programas son idénticos (declaración y uso). Esto puede ser modelado por la gramática de "copia", que es la gramática que reconoce dos copias exactas consecutivas de la misma palabra. Es fácil de probar con el lema de bombeoque este lenguaje no está libre de contexto. Es posible una gramática sensible al contexto para este idioma, y se proporciona una gramática de tipo 0 en la respuesta a esta pregunta: /math/163830/context-sensitive-grammar-for-the- lenguaje de copia .
Si uno intentara escribir una gramática sensible al contexto (o sin restricciones) para analizar C ++, posiblemente llenaría el universo con garabatos. Escribir una máquina de Turing para analizar C ++ sería una tarea igualmente imposible. Incluso escribir un programa en C ++ es difícil, y que yo sepa, ninguno ha demostrado ser correcto. Esta es la razón por la cual el estándar no intenta proporcionar una gramática formal completa, y por qué elige escribir algunas de las reglas de análisis en inglés técnico.
Lo que parece una gramática formal en el estándar C ++ no es la definición formal completa de la sintaxis del lenguaje C ++. Ni siquiera es la definición formal completa del lenguaje después del preprocesamiento, lo que podría ser más fácil de formalizar. (Sin embargo, ese no sería el lenguaje: el lenguaje C ++ definido por el estándar incluye el preprocesador, y la operación del preprocesador se describe algorítmicamente, ya que sería extremadamente difícil de describir en cualquier formalismo gramatical. Está en esa sección del estándar donde se describe la descomposición léxica, incluidas las reglas en las que debe aplicarse más de una vez).
Las diversas gramáticas (dos gramáticas superpuestas para el análisis léxico, una que tiene lugar antes del preprocesamiento y la otra, si es necesario, después, más la gramática "sintáctica") se recogen en el Apéndice A, con esta nota importante (énfasis agregado):
Este resumen de la sintaxis de C ++ pretende ser una ayuda para la comprensión. No es una declaración exacta del idioma . En particular, la gramática descrita aquí acepta un superconjunto de construcciones válidas de C ++ . Las reglas de desambiguación (6.8, 7.1, 10.2) deben aplicarse para distinguir expresiones de declaraciones. Además, las reglas de control de acceso, ambigüedad y tipo deben usarse para eliminar construcciones sintácticamente válidas pero sin sentido.
Finalmente, aquí está el programa prometido. La línea 21 es sintácticamente correcta si y solo si la N IsPrime<N>
es primo. De lo contrario, typen
es un entero, no una plantilla, por lo que typen<1>()
se analiza como (typen<1)>()
sintácticamente incorrecto porque ()
no es una expresión sintácticamente válida.
template<bool V> struct answer { answer(int) {} bool operator()(){return V;}};
template<bool no, bool yes, int f, int p> struct IsPrimeHelper
: IsPrimeHelper<p % f == 0, f * f >= p, f + 2, p> {};
template<bool yes, int f, int p> struct IsPrimeHelper<true, yes, f, p> { using type = answer<false>; };
template<int f, int p> struct IsPrimeHelper<false, true, f, p> { using type = answer<true>; };
template<int I> using IsPrime = typename IsPrimeHelper<!(I&1), false, 3, I>::type;
template<int I>
struct X { static const int i = I; int a[i]; };
template<typename A> struct foo;
template<>struct foo<answer<true>>{
template<int I> using typen = X<I>;
};
template<> struct foo<answer<false>>{
static const int typen = 0;
};
int main() {
auto b = foo<IsPrime<234799>>::typen<1>(); // Syntax error if not prime
return 0;
}
[1] Para decirlo más técnicamente, cada producción en una gramática sensible al contexto debe tener la forma:
αAβ → αγβ
donde A
es un no terminal y α
, β
posiblemente son secuencias vacías de símbolos gramaticales, y γ
es una secuencia no vacía. (Los símbolos gramaticales pueden ser terminales o no terminales).
Esto se puede leer A → γ
solo en el contexto [α, β]
. En una gramática libre de contexto (Tipo 2), α
y β
debe estar vacía.
Resulta que también puede restringir las gramáticas con la restricción "monotónica", donde cada producción debe tener la forma:
α → β
donde |α| ≥ |β| > 0
( |α|
significa "la longitud de α
")
Es posible demostrar que el conjunto de idiomas reconocidos por las gramáticas monotónicas es exactamente el mismo que el conjunto de idiomas reconocidos por las gramáticas sensibles al contexto, y a menudo es más fácil basar las pruebas en las gramáticas monotónicas. En consecuencia, es bastante común ver "sensible al contexto" usado como si significara "monótono".