Ich arbeite langsam daran, mein Studium zu beenden, und dieses Semester ist Compilers 101. Wir verwenden das Drachenbuch . Kurz in den Kurs und wir sprechen über die lexikalische Analyse und wie sie über deterministische endliche Automaten (im Folgenden DFA) implementiert werden kann. Richten Sie Ihre verschiedenen Lexer-Zustände ein, definieren Sie Übergänge zwischen ihnen usw.
Sowohl der Professor als auch das Buch schlagen jedoch vor, sie über Übergangstabellen zu implementieren, die sich auf ein riesiges 2D-Array belaufen (die verschiedenen Nicht-Terminal-Zustände als eine Dimension und die möglichen Eingabesymbole als die andere) und eine switch-Anweisung, um alle Terminals zu behandeln sowie Versand an die Übergangstabellen, wenn sie sich in einem nicht terminalen Zustand befinden.
Die Theorie ist in Ordnung und gut, aber als jemand, der jahrzehntelang Code geschrieben hat, ist die Implementierung abscheulich. Es ist nicht testbar, es ist nicht wartbar, es ist nicht lesbar, und es ist eineinhalb Schmerz, durch die man debuggen muss. Schlimmer noch, ich kann nicht erkennen, wie praktisch es wäre, wenn die Sprache UTF-fähig wäre. Etwa eine Million Übergangstabelleneinträge pro nicht-terminalem Zustand zu haben, wird in Eile unübersichtlich.
Also, was ist der Deal? Warum heißt es in dem endgültigen Buch zu diesem Thema so?
Ist der Aufwand für Funktionsaufrufe wirklich so hoch? Funktioniert das gut oder ist es notwendig, wenn die Grammatik nicht im Voraus bekannt ist (reguläre Ausdrücke?)? Oder vielleicht etwas, das alle Fälle behandelt, auch wenn spezifischere Lösungen für spezifischere Grammatiken besser funktionieren?
( Hinweis: Mögliches Duplikat " Warum einen OO-Ansatz anstelle einer riesigen switch-Anweisung verwenden? " ist nahe liegend, aber OO interessiert mich nicht. Ein funktionaler Ansatz oder ein noch vernünftigerer imperativer Ansatz mit eigenständigen Funktionen wäre in Ordnung.)
Betrachten Sie zum Beispiel eine Sprache, die nur Bezeichner enthält, und diese Bezeichner sind [a-zA-Z]+
. In der DFA-Implementierung erhalten Sie Folgendes:
private enum State
{
Error = -1,
Start = 0,
IdentifierInProgress = 1,
IdentifierDone = 2
}
private static State[][] transition = new State[][]{
///* Start */ new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
///* IdentifierInProgress */ new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
///* etc. */
};
public static string NextToken(string input, int startIndex)
{
State currentState = State.Start;
int currentIndex = startIndex;
while (currentIndex < input.Length)
{
switch (currentState)
{
case State.Error:
// Whatever, example
throw new NotImplementedException();
case State.IdentifierDone:
return input.Substring(startIndex, currentIndex - startIndex);
default:
currentState = transition[(int)currentState][input[currentIndex]];
currentIndex++;
break;
}
}
return String.Empty;
}
(obwohl etwas, das das Dateiende korrekt handhaben würde)
Im Vergleich zu dem, was ich erwarten würde:
public static string NextToken(string input, int startIndex)
{
int currentIndex = startIndex;
while (currentIndex < startIndex && IsLetter(input[currentIndex]))
{
currentIndex++;
}
return input.Substring(startIndex, currentIndex - startIndex);
}
public static bool IsLetter(char c)
{
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}
Wenn der Code NextToken
in seiner eigenen Funktion überarbeitet wurde, haben Sie vom Start des DFA an mehrere Ziele.