Hm, wenn man sich also mit so ner obskuren Programmiersprache (wie du selbst schreibst) rumschlägt, dann bekommt man das doch bestimmt auch in Baby Pascal hin oder?
Sortier Haskell nicht gleich in die selbe Schlublade wie INTERCAL, Brainfuck, Befunge usw. nur weil ich es als obskur bezeichnet habe - letztere sind eher "Spielzeugsprachen" die designt sind um möglichst umständlich programmierbar zu sein, Haskell ist dagegen genauso "ernst gemeint" wie zB C/C++, Pascal, Fortran usw.
Allerdings werde ich dir nicht den Gefallen tun das ganze in Pascal umzusetzen - ich habe zur Zeit keinen Pascal-Compiler installiert, es ist viele Jahre her das ich zuletzt was in Pascal geschrieben habe und ausserdem lernst du dann nichts. =)
Daher gibts hier jetzt Parserbau für Anfänger (Light-Version):
Wie schon geschrieben will man zum Erkennen der Schlüsselworte am besten einen Automaten konstruieren der in jedem Zustand sagt ob der bisher gelesene Teil einem Teil eines Schlüsselwortes entspricht, einem kompletten Schlüsselwort entspricht oder auf gar kein Schlüsselwort passt. So ein Automat lässt sich anschaulich als gerichteter Graph darstellen, bei dem jeder Knoten einen Zustand repräsentiert. Kanten in diesem Graphen sind Übergänge zwischen den Zuständen, die Zeichen die an die Kanten geschrieben sind geben an welcher Buchstabe den Übergang auslöst. Ich habe mal ein Beispiel für sowas nur für die Befehle "GO", "GOTO" und "GOSUB" als Grafik angehangen:
Dieser Automat fängt oben im Zustand "start" an und geht über zu Z1 wenn er als nächstes (ok, erstes) Zeichen ein "g" liest. Eigentlich müsste man noch für alle anderen Zeichen als "g" eine Kante von "start" aus hinzufügen die auf "NoMatch" zeigt, aber das wäre zu unübersichtlich geworden. Das "Part" in der zweiten Zeile im Z1-Kringel steht für "Partial match", d.h. die bisher gelesenen Zeichen sind der Anfang eines Schlüsselwortes. Auch in Z1 gilt wieder, das eigentlich noch diverse Kanten auf "NoMatch" fehlen. Wie in jedem Zustand wird der Automat nun versuchen ein weiteres Zeichen der Eingabe zu lesen, wenn es ein "o" ist geht er über in Zustand Z2. Das "Full" darin steht für "Full match", d.h. die bisher gelesenen Zeichen können ein komplettes Schlüsselwort sein (Z2 ist ein Rechteck um die Full/Part-Unterscheidung visuell deutlicher zu machen). Man könnte jetzt meinen, dass man ja an dieser Stelle aufhören könnte weil die Ausgabe "Full" lautet und somit ein komplettes Schlüsselwort erkannt wurde - aber der einzige Zustand der die Abarbeitung des Automaten wirklich beendet ist "NoMatch". Daher würde der Automat nun ein weiteres Zeichen lesen und bei einem "t" in Z3 oder bei einem "s" in Z5 übergehen usw. bis irgendwann "NoMatch" erreicht wird.
Nehmen wir nun einmal an, dass du schon so einen Automatengraphen für alle Schlüsselworte des C64-BASICs hättest und damit ein Syntaxhighlighting machen willst. Dann müsstest du die aktuelle Zeile zeichenweise an den Automaten verfüttern bis die Ausgabe irgendwann "NoMatch" lautet. Das kann natürlich schon beim ersten Zeichen passieren, dann muss das Zeichen nicht gehighlightet werden und der Automat kann mit dem nächsten Zeichen der Zeile neu gestartet werden.
Möglicherweise kommen auch erst einige "Part"-Ausgaben vor dem "NoMatch" - dann gibst du genau das erste Zeichen das du dem Automaten gegeben hast ohne Highlighting aus und startest ihn im Startzustand mit dem Zeichen danach wieder neu. Das geht im Prinzip auch effizienter, macht aber die Konstruktion des Automaten deutlich schwieriger und da die längsten Schlüsselworte im C64-BASIC gerade mal 7 Zeichen lang sind kostet das ganze nicht übermäßig viel Rechenzeit.
Interessant wird es wenn in der Ausgabe des Automaten irgendwo "Full" vorgekommen ist, dann wurde ein Schlüsselwort komplett erkannt. Da einige Schlüsselworte Prefixe von anderen sind ist an dieser Stelle nur die längste Eingabe interessant die als "Full" erkannt wurde (beim Aufruf des Automaten einfach bei jedem "Full" eine Variable auf die Anzahl der bisher reingesteckten Zeichen setzen, ihr Endwert ist die Länge des längsten gefundenen Schlüsselwortes). Genau so viele Zeichen sind gehighlighted auszugeben, der Automat wird dann mit dem Zeichen danach neu gestartet. Das ist die einzige Situation in der du mehr als ein Zeichen der zu highlightenden Eingabe fertig bearbeitet ausgibst.
Zusätzlich gibts noch ein paar "kleinere" Komplikationen: Wenn du ein Anführungszeichen in deiner Eingabe siehst muss in den Stringmodus umgeschaltet werden bis wieder ein Anführungszeichen kommt oder die Zeile zu Ende ist - idealerweise prüft man das an einem Punkt an dem der Automat in den Startzustand versetzt wurde und mit einem neuen Zeichen gefüttert werden soll da sonst einige Sonderbehandlungen anfallen damit Schlüsselworte vor dem Anführungszeichen korrekt behandelt werden.
Für DATA und REM gilt ähnliches, am bequemsten sind die zu finden indem man schaut was gerade als zu highlightender Text gefunden wurde. Bei DATA sollte man beachten, dass der "Data-Modus" zwar durch einen Doppelpunkt verlassen werden kann, aber nur wenn der Doppelpunkt nicht in Anführungszeichen steht (-> Data-Modus und String-in-Data-Modus).
Das einzige was noch fehlt ist eine Umsetzung des Automatengraphen in irgendwas das man vernünftig im Programm benutzen kann und eine Methode um den Graphen möglichst automatisch zu konstruieren.
Ersteres geht beispeilsweise indem man alle Knoten des Graphen beliebig nummeriert (zB 1 für den Startzustand, 2 für Z1 usw.) und ein 2D-Array anlegt in dem jede Zeile für einen Zustand steht. Die Zeile verwendet die Zeichen an den ausgehenden Kanten als Arrayindex (ineffizient: 256 für ASCII, besser: 26 Buchstaben plus ein paar Sonderzeichen, extra sparsam: Verkette Liste der nicht auf NoMatch führenden Einträge), der Inhalt der Arrayposition ist die Nummer des neuen Zustands in den übergegangen wird wenn das zugehörige Zeichen gelesen wurde. Ausserdem muss man noch speichern welche Ausgabe die Knoten geben sollen wenn sie erreicht werden, zB in einem zweiten Array oder mit einem ARRAY OF RECORD statt eines 2D-Arrays.
Im Pseudocode könnte ein Automatendurchlauf dann ungefähr so aussehen:
|
Source code
|
1
2
3
4
5
6
7
|
VAR akt_zustand: Integer;
FUNCTION automatenaufruf(zeichen: Char): automatenausgabe;
BEGIN
akt_zustand := automaten_matrix[akt_zustand,zeichen];
return automaten_ausgaben[akt_zustand];
END;
|
Im Prinzip reicht das alles schon aus um einen Syntaxhighlighter nach Wunsch zu konstruieren der sich nicht auf so lästige Kleinigkeiten wie zB Leerzeichen zwischen den Schlüsselworten verlassen muss, aber den Automaten von Hand aufstellen ist schon für das kleine GO/GOTO/GOSUB-Beispiel lästig und fehleranfällig. Im Prinzip reicht es aber wenn man ein Programm bastelt welches eine Liste von Schlüsselworten einliest und die Automaten-Matrix in Form eines Quellcode-Schnippsels ausgibt, das man dann in den fertigen Syntaxhighlighter einbindet.
Zur Konstruktion der Tabelle fängt man mit einem hinreichend grossen Array (oder einem das man dynamisch in der Grösse ändern kann) an, bei dem alle Zeichen in allen Zustanden in den Zustand NoMatch führen (es würde sich jetzt irgendwie anbieten den als Zustand 0 zu verwenden und das Array mit Index 1 anzufangen...). Für jedes Schlüsselwort in der Liste hangelt sich das Programm dann so lange zeichenweise durch die Tabelle bis entweder das Ende des Schlüsselwortes erreicht wurde oder das erste Zeichen erreicht wurde das in den Zustand "NoMatch" führt. In erstem Fall markiert man einfach den letzten gefundenen Zustand als "Full", in letzterem Fall erzeugt man einen neuen Zustand der entweder als "Part" oder "Full" markiert wird (je nach dem ob es das letzte Zeichen des Schlüsselwortes war oder nicht) und lässt das bisher auf NoMatch zeigende Zeichen auf den neuen Zustand zeigen. Wenn alle Schlüsselworte abgearbeitet sind wird die konstruierte Tabelle in irgendeiner nützlichen Form ausgegeben.
Diese Art die Tabelle aufzubauen erzeugt keine minimale Tabelle, zB kann man einen Zustand einsparen indem für das P in STEP und STOP der gleiche Zustand verwendet wird. Es gibt auf jeden Fall eine ineffiziente Möglichkeit die Zustandsanzahl zu minimieren (so lange Zustände zusammenfassen bis keine zusammenfassbaren mehr gefunden werden; Zustände sind zusammenfassbar wenn sie das gleiche Ergebis ausgeben und für alle Zeichen in die gleichen Zustände übergehen), wie sowas effizient minimierbar ist müsste ich erstmal nachschlagen.
Ach ja: Die beschriebene Konstruktion eines Syntaxhighlighters ist nicht zu empfehlen wenn die zu erkennenden Schlüsselworte unbeschränkte Länge haben können (zB um Rechenausdrücke einzufärben), dann kann man Eingaben konstruieren bei denen für jedes ausgegebene Zeichen der Automat einen grossen Teil der Resteingabe einliest ohne einen Full-Match zu finden -> quadratische Laufzeit in der Eingabelänge.
Ach ja 2: Das ganze ist der erste Schritt auf dem Weg zu einem Compiler oder Interpreter.