Unten ist meine (aktuelle) Lieblingsdemonstration, warum das Parsen von C ++ (wahrscheinlich) Turing-vollständig ist , da es ein Programm zeigt, das genau dann syntaktisch korrekt ist, wenn eine bestimmte Ganzzahl eine Primzahl ist.
Ich behaupte also, dass C ++ weder kontextfrei noch kontextsensitiv ist .
Wenn Sie beliebige Symbolsequenzen auf beiden Seiten einer Produktion zulassen, erstellen Sie eine Typ-0-Grammatik ("uneingeschränkt") in der Chomsky-Hierarchie , die leistungsfähiger ist als eine kontextsensitive Grammatik. Uneingeschränkte Grammatiken sind Turing-vollständig. Eine kontextsensitive Grammatik (Typ 1) erlaubt mehrere Kontextsymbole auf der linken Seite einer Produktion, aber der gleiche Kontext muss auf der rechten Seite der Produktion erscheinen (daher der Name "kontextsensitiv"). [1] Kontextsensitive Grammatiken entsprechen linear begrenzten Turing-Maschinen .
In dem Beispielprogramm könnte die Hauptberechnung von einer linear begrenzten Turing-Maschine durchgeführt werden, so dass die Turing-Äquivalenz nicht ganz bewiesen wird, aber der wichtige Teil ist, dass der Parser die Berechnung durchführen muss, um eine syntaktische Analyse durchzuführen. Es könnte jede Berechnung gewesen sein, die als Vorlageninstanziierung ausgedrückt werden kann, und es gibt allen Grund zu der Annahme, dass die C ++ - Vorlageninstanziierung vollständig ist. Siehe zum Beispiel Todd L. Veldhuizens Artikel von 2003 .
Unabhängig davon kann C ++ von einem Computer analysiert werden, sodass es sicherlich von einer Turing-Maschine analysiert werden kann. Folglich könnte eine uneingeschränkte Grammatik dies erkennen. Das Schreiben einer solchen Grammatik wäre tatsächlich unpraktisch, weshalb der Standard dies nicht versucht. (Siehe unten.)
Das Problem mit der "Mehrdeutigkeit" bestimmter Ausdrücke ist meist ein roter Hering. Ambiguität ist zunächst ein Merkmal einer bestimmten Grammatik, keine Sprache. Selbst wenn nachgewiesen werden kann, dass eine Sprache keine eindeutigen Grammatiken aufweist, ist sie kontextfrei, wenn sie von einer kontextfreien Grammatik erkannt werden kann. Wenn es von einer kontextfreien Grammatik nicht erkannt werden kann, aber von einer kontextsensitiven Grammatik erkannt werden kann, ist es ebenfalls kontextsensitiv. Mehrdeutigkeit ist nicht relevant.
In jedem Fall sind auto b = foo<IsPrime<234799>>::typen<1>();
die Ausdrücke jedoch , wie in Zeile 21 (dh ) im folgenden Programm, überhaupt nicht mehrdeutig. Sie werden je nach Kontext einfach unterschiedlich analysiert. Im einfachsten Ausdruck des Problems hängt die syntaktische Kategorie bestimmter Bezeichner davon ab, wie sie deklariert wurden (z. B. Typen und Funktionen). Dies bedeutet, dass die formale Sprache die Tatsache erkennen müsste, dass zwei Zeichenfolgen beliebiger Länge enthalten sind Das gleiche Programm ist identisch (Deklaration und Verwendung). Dies kann durch die "Kopier" -Grammatik modelliert werden, bei der es sich um die Grammatik handelt, die zwei aufeinanderfolgende exakte Kopien desselben Wortes erkennt. Mit dem Pump-Lemma ist es leicht zu beweisendass diese Sprache nicht kontextfrei ist. Eine kontextsensitive Grammatik für diese Sprache ist möglich, und eine Grammatik vom Typ 0 ist in der Antwort auf diese Frage enthalten: /math/163830/context-sensitive-grammar-for-the- Kopiersprache .
Wenn man versuchen würde, eine kontextsensitive (oder uneingeschränkte) Grammatik zu schreiben, um C ++ zu analysieren, würde dies möglicherweise das Universum mit Skizzen füllen. Das Schreiben einer Turing-Maschine zum Parsen von C ++ wäre ein ebenso unmögliches Unterfangen. Selbst das Schreiben eines C ++ - Programms ist schwierig, und meines Wissens hat sich keines als richtig erwiesen. Aus diesem Grund versucht der Standard nicht, eine vollständige formale Grammatik bereitzustellen, und beschließt, einige der Parsing-Regeln in technischem Englisch zu schreiben.
Was im C ++ - Standard wie eine formale Grammatik aussieht, ist nicht die vollständige formale Definition der Syntax der C ++ - Sprache. Es ist nicht einmal die vollständige formale Definition der Sprache nach der Vorverarbeitung, die möglicherweise einfacher zu formalisieren ist. (Dies wäre jedoch nicht die Sprache: Die im Standard definierte C ++ - Sprache enthält den Präprozessor, und die Funktionsweise des Präprozessors wird algorithmisch beschrieben, da es in jedem grammatikalischen Formalismus äußerst schwierig wäre, sie zu beschreiben. Sie befindet sich in diesem Abschnitt des Standards, in dem die lexikalische Zerlegung beschrieben wird, einschließlich der Regeln, in denen sie mehrmals angewendet werden muss.)
Die verschiedenen Grammatiken (zwei überlappende Grammatiken für die lexikalische Analyse, eine vor der Vorverarbeitung und die andere, falls erforderlich, danach sowie die "syntaktische" Grammatik) sind in Anhang A mit diesem wichtigen Hinweis (Hervorhebung hinzugefügt) zusammengefasst:
Diese Zusammenfassung der C ++ - Syntax soll das Verständnis erleichtern. Es ist keine genaue Aussage der Sprache . Insbesondere akzeptiert die hier beschriebene Grammatik eine Obermenge gültiger C ++ - Konstrukte . Disambiguierungsregeln (6.8, 7.1, 10.2) müssen angewendet werden, um Ausdrücke von Deklarationen zu unterscheiden. Darüber hinaus müssen Zugriffssteuerungs-, Mehrdeutigkeits- und Typregeln verwendet werden, um syntaktisch gültige, aber bedeutungslose Konstrukte auszusondern.
Zum Schluss hier das versprochene Programm. Zeile 21 ist genau dann syntaktisch korrekt, wenn das N in IsPrime<N>
eine Primzahl ist. Andernfalls typen
handelt es sich um eine Ganzzahl und nicht um eine Vorlage. typen<1>()
Daher wird sie als (typen<1)>()
syntaktisch falsch analysiert, da ()
es sich nicht um einen syntaktisch gültigen Ausdruck handelt.
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] Technisch gesehen muss jede Produktion in einer kontextsensitiven Grammatik die folgende Form haben:
αAβ → αγβ
wobei A
ein Nicht-Terminal und α
, β
sind möglicherweise leere Sequenzen von Symbolen der Grammatik, und γ
ist eine nicht-leere Sequenz. (Grammatiksymbole können entweder Terminals oder Nicht-Terminals sein).
Dies kann A → γ
nur im Kontext gelesen werden [α, β]
. In einer kontextfreien (Typ 2) Grammatik α
und β
muss leer sein.
Es stellt sich heraus, dass Sie Grammatiken auch mit der "monotonen" Einschränkung einschränken können, wobei jede Produktion die Form haben muss:
α → β
wo |α| ≥ |β| > 0
( |α|
bedeutet "die Länge von α
")
Es ist möglich zu beweisen, dass der Satz von Sprachen, die von monotonen Grammatiken erkannt werden, genau der gleiche ist wie der Satz von Sprachen, die von kontextsensitiven Grammatiken erkannt werden, und es ist häufig einfacher, Beweise auf monotonen Grammatiken zu basieren. Folglich ist es ziemlich üblich, dass "kontextsensitiv" verwendet wird, als ob es "monoton" bedeutet.