*** KAPITEL 4 ***
4. Schrittweise Entwicklung eines lexikalischen Scanners
4.1. Vorbemerkungen
Ich möchte mit dem Scanner/Lexer/Tokenizer beginnen. Diese Komponente ist die am einfachsten zu realisierende.
Ich halte mich dabei mit einigen Verfeinerungen, die ich über die Jahre nach und nach eingebaut habe, an Crenshaw/Wirth/Arbayo.
Crenshaw beginnt in seiner Erklärung mit Lexemen, die aus nur einem Buchstaben bestehen, weil er der Meinung ist, dass dadurch die Techniken des Parsens für die Leser leichter zu erkären seien.
Nun, das ist nur bedingt richtig. Crenshaw hat ja seine damalige Serie geschrieben, während er die diskutierten Programme erst "live" sozusagen vor den Augen der Leser getestet und entwickelt hat. Ich mache für dieses Tutorial auch alles neu, weiß aber durch mein vorher Gelesenes uns schon Programmiertes, was geht und was nicht.
Dadurch habe ich gegenüber Crenshaw einen Vorteil, ich weiß, wo der Zug fahren kann und wo nicht.
Der Lexer belastet die Verständlichkeit des Späteren eben nicht, weil es aus Sicht des späteren Parsers egal ist, ob er nur wie bei Crenshaw 1 Zeichen mit einer Prozedur namens GetChar(), d.i. hole genau 1 Zeichen, oder eben mit GetToken() gleich ein ganzes Token-Lexem-Paar vom Scanner anfordert. Aus Sicht des Parsers ist es egal, er macht eine Anforderung und bekommt geliefert.
Also steigen wir gleich und sofort mit mehrbuchstabigen Lexemen ein, ohne Crenshaws Umweg über die 1-Zeichen-Lexeme.
Ein Beispiel:
Code: Alles auswählen
Normaler Code mit mehrbuchstabigen Lexemen:
if (a=3)
printn(a)
In den ersten Kapiteln von Crenshaw könnte der Einbuchstabencode dafür lauten (ein von mir jetzt erfundenes Beispiel):
i(a=3)p(a)
Eine wichtige Sache vor Beginn:
Ich werde nicht immer die effizienteste Implementierung wählen.
- Das kann 2 Gründe haben:
- ganz profan: Ich weiß selber nichts Besseres
Wird wohl teilweise so sein, bin ja selbst kein Vollprofi.
- absichtlich: aus Gründen der Übersichtlichkeit wähle ich einfach die geradlinigste Implementierung, weil es mir ja nicht um Speed, sondern Verständlichkeit geht.
An Stellen, wo ich schon ahne, dass man etwas besser machen kann, werde ich bereits den einen oder anderen Wink geben.
Interessierte Leser können und sollen die Diskussion bereichern, indem sie alternative (bessere? schnellere?) Code-Möglichkeiten im Diskussionsthread vorschlagen.
4.2. Hintergrundgedanken zum Scanvorgang
Das 1. Zeichen jedes
NOCH NICHT GESCANNTEN Lexems ist ein Nicht-White-Zeichen (gleich mehr) und wird
Look Ahead Character (wörtlich: Schau-eins-nach-vorn-Zeichen) genannt. Wir nennen die Variable, die wir verwenden, um den Look Ahead zu speichern,
Look.
Anders formuliert:
Ist der Scanner mit einem Token-Lexem-Paar fertig, dann hat er das
aktuelle Token-Lexem-Paar in den Variablen
Token und
Lexem gespeichert, in
Look steht das
1. Zeichen des nächsten Token-Lexem-Pärchens.
Ein
White Character ist jedes Zeichen, von dem wir wollen, dass der Scanner es ignoriert, also einfach überspringt. Das naheliegendste ist das White
Space (daher auch der Name für diesen Zeichentyp), aber auch den
Tabulator,
Kommentare,
Carriage Return,
Line Feed, ja sogar
Semikolons ";" und
Doppelpunkte ":" werde ich in meinem Code dazurechnen.
Ein Beispiel:
Im Source-File steht (Die Punkte setze ich für Leerzeichen):
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist "i" (Anmerkung: Look ist kein String, also liegt in Look der Char-Code von "i")
- Der Scanner erkennt mit IsName1(): "i" ist ein Buchstabe und verzweigt nach GetName()
- GetName() holt jetzt Zeichen für Zeichen, bis zum ersten Nicht-Name-Zeichen. Der Scanner steht mit Look auf dem Leerzeichen nach "f"
- GetName() ruft nun SkipWhite() auf, diese Prozedur überspringt alle White-Zeichen (1 Zeichen)
- Am Ende von GetName() haben wir folgendes Ergebnis:
- In Token ist die Code-Zahl von #Name (zu den Codes später mehr)
- In Lexem ist der String "if"
- In Look ist "(", das erste Nicht-White-Zeichen des NÄCHSTEN Token/Lexems
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist "("
- Der Scanner erkennt kein genau definiertes Zeichen (wir nennen es #Other, dazu später mehr)
- GetOther() holt jetzt 1 Zeichen. Der Scanner steht mit Look auf "V" nach "("
- GetOther() ruft nun SkipWhite() auf, diese Prozedur überspringt alle White-Zeichen (keine da)
- Am Ende von GetOther() haben wir folgendes Ergebnis:
- In Token ist die Code-Zahl von "(" (warum das so ist, dazu später mehr)
- In Lexem ist der String "("
- In Look ist "V", das erste Nicht-White-Zeichen des NÄCHSTEN Token/Lexems
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist "V"
- Der Scanner erkennt mit IsName1(): "V" ist ein Buchstabe und verzweigt nach GetName()
- GetName() holt jetzt Zeichen für Zeichen, bis zum ersten Nicht-Name-Zeichen. Der Scanner steht mit Look auf dem "=" nach "e"
- GetName() ruft nun SkipWhite() auf, diese Prozedur überspringt alle White-Zeichen (keine da)
- Am Ende von GetName() haben wir folgendes Ergebnis:
- In Token ist die Code-Zahl von #Name (gleich mehr)
- In Lexem ist der String "VarName"
- In Look ist "=", das erste Nicht-White-Zeichen des NÄCHSTEN Token/Lexems
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist "="
- Das kennen wir schon von oben, GetOther() holt das "="
- In Look ist "3", das erste Nicht-White-Zeichen des NÄCHSTEN Token/Lexems
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist "3"
- Der Scanner erkennt mit IsNum(): "3" ist eine Zahl, verzweigt nach GetNum()
- GetNum() holt jetzt Zeichen für Zeichen, bis zum ersten Nicht-Ziffern-Zeichen. Der Scanner steht mit Look auf ")" nach "5"
- GetNum() ruft nun SkipWhite() auf, diese Prozedur überspringt alle White-Zeichen (keine da)
- Am Ende von GetNum() haben wir folgendes Ergebnis:
- In Token ist die Code-Zahl von #Number (gleich mehr)
- In Lexem ist der String "35"
- In Look ist ")", das erste Nicht-White-Zeichen des NÄCHSTEN Token/Lexems
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist ")"
- Das kennen wir schon von oben, GetOther() holt das ")"
- GetOther() ruft nun SkipWhite() auf, diese Prozedur überspringt 3 White-Zeichen zwischen ")" und "p"
- In Look ist "p", das erste Nicht-White-Zeichen des NÄCHSTEN Token/Lexems
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
if.(VarName=35)...printn(VarName)
^--- in Look ist "p"
- und so weiter und so weiter
Am Ende des Toy-C-Source-Programms muss dann ein
0-Byte stehen, das durch
size+1 bereits beim Laden erzeugt wird.
Hier sieht man auch etwas, das ich im Vergleich zu Crenshaw geändert habe.
Während er sein SkipWhite()
vor dem Holen z.B. einer Nummer oder eines Namens aufruft, mache ich es
danach. Crenshaw steht also nach einem GetToken()-Durchgang mit Look auf dem
ersten White-Zeichen nach dem aktuellen Token-Lexem-Paar.
Ich stehe mit Look auf dem
ersten Zeichen des nächsten Token-Lexem-Paars, weil ich, bevor ich Look belege, noch alle White-Zeichen überspringe.
4.3. Das Grundgerüst des Scanners, laden und testen des Character Streams
Ich lege mir jetzt einen Projektordner zu und darin ein Textfile mit dem Namen
"Scanner_Test01.tc" an, wobei ".tc" für Toy-C steht. Das ist das
Source-File in der von uns erfundenen neuen Hochsprache. Bitte aufpassen, einfache Ascii-Kodierung zu verwenden, also bitte kein Unicode oder dergleichen. Wer das später möchte, bekommt das sicher alleine hin, wenn er es schafft, einen Compiler zu programmieren.
In "Scanner_Test01.tc" speichere ich folgenden Code, wieder ist ein "." ein Leerzeichen. "{enter}" ist die Enter-Taste:
Anmerkung:
Ich verwende dafür den Programmierer-Notepad
Notepad++ (
http://notepad-plus-plus.org/), weil ich damit so schön Syntax-Highlighting meiner erfundenen Sprache machen kann. Für Toy-C stelle ich die Einstellung einfach auf "C".
Ich bin sicher, dass andere Programmierer andere Vorlieben haben. Vielleicht hat jemand Tipps im Thread?
Entwerfen wir ein
Grundgerüst des Scanners, das wir dann laufend füllen werden (sollte selbsterklärend sein).
Alle Prozeduren sind zunächst Dummys, damit beim schrittweisen Erstellen keine Prozeduren fehlen.
Code: Alles auswählen
; ******************************************************************************
; * Module: LEXIKALISCHER SCANNER
; ******************************************************************************
DeclareModule Scanner
; - Public Declarations ---------------------------------------------------------
; -
Global Look ; Look Ahead Character
Global Token ; Token-Typ als Zahl
Global Lexem.s ; Lexem = Token als String
; --- Start mit Aufruf dieser Prozedur ---
Declare Start(file_name.s) ; file_name des Source-Files
; --- Look Ahead Character holen mit dieser Prozedur ---
Declare GetChar() ; holt nächsten Character von *Source_Code
; --- Token-Lexem-Paare holen mit diesen Prozeduren ---
Declare GetToken() ; holt nächstes Token-Lexem-Paar
Declare GetName() ; holt nächsten Name
Declare GetNum() ; holt nächste Num (Integer)
Declare GetString() ; holt nächsten String
Declare GetOther() ; holt nächstes Other-Zeichen
; --- Fehlermeldung ausgeben ---
Declare Error(error_message.s) ; zeigt Meldung und beendet Compiler
Declare Expected(expected_object.s) ; zeigt, was erwartet wurde
; und beendet Compiler
EndDeclareModule
Module Scanner
; - Private Declarations --------------------------------------------------------
; -
Global Source_Position ; Zeichen-Position im *Source_Code
Global *Source_Code ; Source-Code Zeichen für Zeichen im Speicher
;
Declare Load(file_name.s) ; lädt das ASCII-Source-File
;
Declare IsName1(c) ; testet, ob Zeichen der Start eines Namens ist
Declare IsName(c) ; testet, ob Zeichen zu einem Namen gehört
Declare IsNum(c) ; testet, ob Zeichen zu einer Nummer gehört
Declare IsString(c) ; testet, ob Zeichen der Start eines Strings ist
Declare IsWhite(c) ; testet, ob Zeichen ein White-Character ist
;
Declare SkipWhite() ; überspringt jedes White-Zeichen
Declare SkipLineComment() ; überspringt ab Comment-Start bis Zeilenende
Declare SkipBlockComment() ; überspringt von Block-Start bis Block-Ende
; - Debug - Prozeduren (am Ende löschen) ----------------------------------------
; -
Procedure Debug_CharStream() : Endprocedure
Procedure Debug_TokenLexemeStream0() : Endprocedure
Procedure Debug_TokenLexemeStream1() : Endprocedure
; - Start - Prozeduren ----------------------------------------------------------
; -
Procedure Load(file_name.s) : Endprocedure
Procedure Start(file_name.s) : Endprocedure
; - Is?() - Prozeduren ----------------------------------------------------------
; -
Procedure IsName1(c) : Endprocedure
Procedure IsName(c) : Endprocedure
Procedure IsNum(c) : Endprocedure
Procedure IsString(c) : Endprocedure
Procedure IsWhite(c) : Endprocedure
; - Get - Prozeduren ------------------------------------------------------------
; -
Procedure GetChar() : Endprocedure
Procedure GetToken() : Endprocedure
Procedure GetName() : Endprocedure
Procedure GetNum() : Endprocedure
Procedure GetString() : Endprocedure
Procedure GetOther() : Endprocedure
; - Skip - Prozeduren -----------------------------------------------------------
; -
Procedure SkipWhite() : Endprocedure
Procedure SkipLineComment() : Endprocedure
Procedure SkipBlockComment() : Endprocedure
; - Error - Prozeduren -----------------------------------------------------------
; -
Procedure Error(error_message.s) : Endprocedure
Procedure Expected(expected_object.s) : Endprocedure
EndModule
Scanner::Start("Scanner_Test01.tc")
Das Source-File laden wir ganz einfach in einen Speicherbereich, den wir mit dem Zeiger
*Source_Code adressieren. In
Source_Position läuft dann die aktuelle Char-Position mit.
Am Ende des Memory-Bereichs muss ein
0-Byte sein, deshalb
size+1. Wenn wir später mal bei einem Programmdurchlauf das size+1 weglassen, sehen wir ja, was passiert.
Natürlich sollte man in einer Release-Version auch testen, ob das File überhaupt geöffnet werden konnte.
Code: Alles auswählen
Procedure Load(file_name.s)
ReadFile(0,file_name)
size = Lof(0)
*Source_Code = AllocateMemory(size+1) ; damit auch sicher am Ende 0-Byte ist
ReadData(0, *Source_Code, size)
CloseFile(0)
EndProcedure
Als Nächstes programmieren wir die Prozedur
GetChar(). Sie hat die Aufgabe, bei Aufruf das Zeichen (Char = Character) vom Source-File-Speicherbereich
*Source_Code in die globale Variable
Look zu holen. Die Variable
Source_Position gibt dabei die Position innerhalb von *Source_Code an. Dann wird
Source_Position um ein Zeichen erhöht.
Ich weiß, hier wäre wohl ein Pointer besser als eine Funktion, so scheint es mir aber schlank und verständlich zu sein. Alternativen im Thread, wenn jemand will.
Code: Alles auswählen
Procedure GetChar()
Look = PeekA(*Source_Code+Source_Position) ; aktuelles Zeichen nach Look
Source_Position+1 ; auf nächstes Zeichen stellen
EndProcedure
Die Prozedur
Start() ruft das Laden des Source-Files auf, stellt die
Source_Position auf das erste Zeichen im Memory-Bereich des
*Source_Code und tut dann etwas, das sie später nicht tun wird, sie ruft eine
Debug-Prozedur auf, damit wir unser Werk einmal testen können.
Code: Alles auswählen
Procedure Start(file_name.s)
Load(file_name.s)
Source_Position = 0
;
Debug_CharStream() ; nur zu Debug-Zwecken
EndProcedure
Die Debug-Prozedur
Debug_CharStream() müssen wir eingehender besprechen.
Code: Alles auswählen
Procedure Debug_CharStream()
; um ersten Look in den Stream zu legen
GetChar()
; so lange, bis Look = 0-Byte
While ( Look<>0 )
Debug "'"+Chr(Look)+"' : "+Str(Look)
GetChar()
Wend
Debug Str(Look)+"-Byte beendet (außerhalb While-Schleife)"
EndProcedure
Zunächst, und das werden wir dann auch beim Token-Lexem-Holen sehen, müssen wir den Character-Strom einmal das erste Mal "aufpumpen", sprich das erste Zeichen noch vor irgendeiner Schleife mit Look vorbelegen. Warum? Lesen wir uns oben nochmals durch, wie der Scanner arbeitet. Er erwartet nämlich, dass immer ein
aktuelles Look-Zeichen in Look
bereits vorhanden ist, also auch ganz zu Beginn.
Dann geht eine While-Schleife durch den Speicherbereich
*Source_Code, aber nur, wenn
kein 0-Byte als Terminator (Ende-Signalzeichen) des *Source_Code in
Look ist.
Dabei bekommen wir das Zeichen und den Char-Code im Debug-Fenster angezeigt.
Am ENDE der While-Schleife wird Look für den NÄCHSTEN Durchlauf geladen, d.h. der Debug-Befehl innerhalb der Schleife bekommt das 0-Byte nie zu sehen. Genauso wird dann auch mit Token verfahren werden, dazu später beim Parser mehr.
Lassen wir den Scanner laufen, alles sollte funktionieren und folgende Debug-Ausgabe sollte nicht nur bei mir zu sehen sein (ohne Anmerkungen natürlich):
Code: Alles auswählen
' ' : 32 <------ Chr(32) - Leerzeichen
' ' : 32 (das sind WHITE-Zeichen)
' ' : 32
'i' : 105
'n' : 110
't' : 116
' ' : 32
' ' : 32
'a' : 97
'=' : 61
'0' : 48
' <------ Enter-Taste (Erklärung folgt sofort)
' : 13 (werden wir als WHITE-Zeichen behandeln)
'
' : 10
0-Byte beendet (außerhalb While-Schleife)
4.4. Das Ende einer Programmzeile und Semikolons
Interessant ist, was bei der
ENTER-Taste passiert.
Windows setzt die Enter-Taste (= das Zeilenendezeichen) als
2 Zeichen um: die Kombination #CR (13: Carriage Return) und #LF (10: Line Feed). Dafür gibt es bei Pure Basic die Einzelkonstanten und auch die Kombinationskonstante #CRLF$ als String. Andere Bezeichnungen für Zeilenendezeichen sind auch
New Line oder
End Of Line (EOL).
Wer
nicht mit Windows arbeitet, der ist zumeist mit einem einfachen #LF als Zeilenende konfrontiert. Es gibt aber sogar die Kombination #LF und #CR, also umgekehrt zu Windows oder nur #CR.
Ich verweise hier auf Wikipedia (
http://de.wikipedia.org/wiki/Zeilenumbruch#ASCII), die verschiedene Möglichkeiten erklärt. Die englischsprachige Wikipedia ist hier viel genauer, aber eben auf Englisch (
http://en.wikipedia.org/wiki/Newline).
Was bedeutet das für die Praxis?
Sollten wir also wollen, dass bei jedem New Line ein Zeilenzähler um eins hochgezählt wird, um zum Beispiel später in einem Debugger "Fehler in Zeile 25" ausdrucken zu können, dann sollten wir uns auf keinen Fall nur mit einer Lösung begnügen (Ja, zum Üben, aber zuletzt nicht). Wir sollten eine Prozedur schreiben, die mit allen möglichen Zeilenenden fertig wird.
In der Line-Comment-Schleife (kommt später) werde ich sicherheitshalber beides, also #CR und #LF, abfragen.
In meiner Implementation eines Scanners - das muss man nicht unbedingt so machen - werden die
Zeilenenden, also die Zeichen #CR und #LF als
White-Zeichen einfach ignoriert werden.
Verhaspelt sich dann der Parser nicht, wenn er keine Zeilenenden hat, fragt sich der eine oder andere sicher?
Bevor ich antworte, gehe ich noch einen Schritt weiter.
Sogar die oft gehassten
Semikolons ";", die wir von C oder Pascal oder Java her kennen, sind bei mir
White-Zeichen, also einfach zu überspringen.
Verhaspelt sich dann der Parser nicht, wenn er keine Semikolons hat, die die Befehle abschließen, fragt sich der eine oder andere sicher noch intensiver?
Nein, der Parser braucht weder das eine noch das andere, werden wir sehen.
4.5. Die Token-Typen und die Is?()-Erkennungsprozeduren
Im Kapitel "Hintergrundgedanken zum Scanvorgang" erwähne ich immer wieder Prozeduren wie
IsName() oder
IsNum().
Diese Prozeduren sollen anhand des
Look erkennen, welcher Token-Typ folgen könnte. Außerdem kann eine Is?()-Prozedur anhand eines Zeichens erkennen, ob er in einem bestimmten Token-Typ vorkommt.
Hier eine Auflistung der Erkennungsprozeduren:
Code: Alles auswählen
Erkennungs- Zeichen Anmerkung
prozedur
-------------------------------------------------------------
IsName1() a..z A..z 1. Zeichen eines Namens
-------------------------------------------------------------
IsName() a..z A..z ab 2. Zeichen eines Namens
0..9 _ z.B. Variable_01
-------------------------------------------------------------
IsNum() 0..9 Integer-Zahl
-------------------------------------------------------------
IsString() " Start-Zeichen eines Strings
Ende-Zeichen eines Strings
-------------------------------------------------------------
IsWhite() #SPACE #TAB / Zeichen, die der Scanner
#LF #CR ; : überspringt
(bzw. / löst Kommentar aus)
Anmerkung:
Wer bis jetzt gut mitgedacht hat, dem wird auffallen, dass ich irgendwo weiter oben vom Typ #Op (Operand: z.B. = + - ) geschrieben habe und der hier in der Tabelle fehlt. Diesen Typ werde ich nicht als ausdrücklichen Operatortyp, sondern als bei den Get()-Prozeduren behandeln.
Diese
Is?()-Prozeduren sind wirklich nicht viel mehr als Tester. Sie liefern #True zurück, wenn die Bedingung für den übergebenen Char-Code zutrifft. Achtung, die Prozeduren testen KEINE Strings, die Zeichen sind Char-Code, wie an den ' ' zu erkennen ist.
Ich habe das schon mit Macros, aber auch mit Select...Case gemacht. Hier mal mit If...Then, aber die exakte Ausführung des Codes ist nebensächlich, das kann jeder halten, wie er will:
Code: Alles auswählen
Procedure IsNum(c)
If (c>='0' And c<='9')
ProcedureReturn #True
EndIf
EndProcedure
Procedure IsName1(c)
If ((c>='a' And c<='z') Or (c>='A' And c<='Z'))
ProcedureReturn #True
EndIf
EndProcedure
Procedure IsName(c)
If (IsNum(c) Or IsName1(c) Or c='_')
ProcedureReturn #True
EndIf
EndProcedure
Procedure IsString(c)
If c='"'
ProcedureReturn #True
EndIf
EndProcedure
Procedure IsWhite(c)
If (c=' ' Or c=#TAB Or c='/' Or c=#LF Or c=#CR Or c=';' Or c=':')
ProcedureReturn #True
EndIf
; Anmerkung: / startet einen Zeilenkommentar mit // (wie in C)
; / startet einen Blockkommentar mit /* (wie in C)
EndProcedure
Sehr schön ist auch, dass man, wenn man zum Beispiel das #CR nicht mehr als White-Zeichen führen will, es einfach aus der Prozedur herauslöscht, und schon ist die Sache gelöst. Es wird dann zu einem Other-Token.
Man braucht nicht hundertmal an verschiedenen Stellen die Abfrage löschen.
4.6. Die Get()-Prozeduren und die Token-Codes
4.6.1. GetToken(), GetName(), GetNum(), GetString(), GetOther()
Die wichtigste Prozedur, die zumeist vom Parser aufgerufen werden wird, ist die GetToken()-Prozedur.
Sie dient als Verteiler. Mittels den Is?()-Prozeduren wird der aktuelle Look getestet, und dann zur passenden Get()-Prozedur verzweigt.
Achtung: Wenn der Parser die Get()-Prozeduren aufruft, dann muss vorher schon ein aktuelles Look-Zeichen im Stream sein, das das 1. Zeichen des jetzt zu scannenden Token-Lexem-Pärchens ist. Die Prozedur Start() müssen wir dann später so verändern, dass sie GetChar() und danach SkipWhite() aufruft, um den 1. gültigen Look Character zu laden.
Obwohl die einzelnen Get()-Prozeduren jeweils ein SkipWhite() am Ende haben (SkipWhite() überspringt alle White-Zeichen und belegt Look mit dem ersten Zeichen des nächsten Token-Lexem-Paares), steht auch ein Aufruf am Ende von GetToken(), einfach zur Sicherheit. Mutige können den Aufruf weglassen.
Code: Alles auswählen
Procedure GetToken()
; --> in Look ist das 1. Zeichen dieses Token-Lexems
; Entscheide, welcher Token-Typ vorliegt und verzweige entsprechend
If IsNum (Look): GetNum()
ElseIf IsName1 (Look): GetName()
ElseIf IsString(Look): GetString()
Else : GetOther()
EndIf
; ueberspringe alle White Characters und Comments (zur Sicherheit)
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
Die folgenden Get()-Prozeduren sollten selbsterklärend sein. GetName() holt einen Namen nach Token/Lexem, GetNum() eine Integer-Zahl.
GetString() holte einen String, der mit " beginnt und mit " endet, als Begrenzung hätte auch jedes andere Zeichen dienen können, definiert haben wir das in IsString(). Innerhalb von "..." wird nicht SkipWhite() angesprungen, d.h. in den String werden alle Zeichen übernommen, auch White-Zeichen.
Diese Get()-Prozeduren schauen alle fast identisch aus, nur das Objekt, das sie ins Token-Lexem-Paar holen, ist jeweils unterschiedlich.
Code: Alles auswählen
Procedure GetName()
; --> in Look ist das 1. Zeichen dieses Token-Lexems
; 1. Zeichen korrekt fuer Name?
If Not IsName1(Look):Expected("Name"):EndIf
; Token-Typ zuordnen
Token = 'x'
; Lexem mit Name füllen
Lexem = ""
Repeat
Lexem = Lexem + Chr(Look)
GetChar()
Until Not IsName(Look)
; Name-Identifier sind nicht Case sensitiv
Lexem = UCase(Lexem)
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
Code: Alles auswählen
Procedure GetNum()
; --> in Look ist jetzt das 1. Zeichen dieses Token-Lexems
; 1. Zeichen korrekt fuer Num?
If Not IsNum(Look):Expected("Integer"):EndIf
; Token-Typ zuordnen
Token = '#'
; Lexem mit Name füllen
Lexem = ""
Repeat
Lexem = Lexem + Chr(Look)
GetChar()
Until Not IsNum(Look)
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
Code: Alles auswählen
Procedure GetString()
; --> in Look ist jetzt das 1. Zeichen dieses Token-Lexems
; 1. Zeichen korrekt fuer String?
If Not IsString(Look):Expected("String"):EndIf
; Token-Typ zuordnen
Token = '$'
; (") String-Start-Zeichen überspringen
GetChar()
; Lexem mit Name füllen
Lexem = ""
Repeat
Lexem = Lexem + Chr(Look)
GetChar()
Until IsString(Look)
; String-Ende-Zeichen überspringen (")
GetChar()
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
Kommen wir zu GetOther(), die Prozedur, die auch die Operatoren aufnehmen wird. Beim Programmieren des mathematischen Scanners wird klar werden, warum ich diese Form der Einfachheit gewählt habe.
Diese Prozedur bleibt bei GetToken() als Default-Möglichkeit über. Wenn also das 1. Zeichen eines Token-Lexem-Paares keine der anderen Typen ist, dann bleibt als letzte Möglichkeit nur Other (engl., anders, ein anderes) über.
GetOther() liest 1 Zeichen, überspringt alle White-Zeichen danach und übergibt als Token den Char-Code des gelesenen 1. Zeichens, als Lexem den String des gelesenen 1. Zeichens.
Code: Alles auswählen
Procedure GetOther()
; --> in Look ist jetzt das 1. Zeichen dieses Token-Lexems
; Token-Typ zuordnen (=genau dieses Zeichen)
Token = Look
; Lexem füllen (=genau dieses Zeichen)
Lexem = Chr(Look)
; nächstes Look holen
GetChar()
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
4.6.2. Die Token-Codes()
Die Token-Codes sind Zahlen, die in der Variable Token von den Get()-Prozeduren gespeichert werden.
Die 4 Tokentypen, die ich in meiner Implementierung unterscheide, kennen wir bereits: Name, Num, String, Other.
In der Tabelle stehen die entsprechenden Token-Codes.
Ich habe mich allerdings dazu entschlossen, keine Konstanten dafür anzulegen, also nicht z.B. #Name oder #Num, obwohl das in der Literatur üblich ist (z.B. Wirth), weil mir der Trick von Crenshaw gefällt.
Ich verwende die Char-Codes als in einfache Anführungszeichen eingeschlossene Zeichen, die Pure Basic als entsprechenden Char-Code, also als Zahl in das PB-Programm einfügt. Das Nummerzeichen für Zahlen, einen Buchstaben für einen Namen, ein Dollar für einen String und bei einem Other ist der Char-Code des Zeichen selbst in Token abgespeichert. Überprüfen Sie das in der jeweiligen Get()-Prozedur.
Code: Alles auswählen
Char-Code Typ und Beispiele
als Zeichen als Zahl Get()-Prozedur
----------------------------------------------------------------
'x' 35 Name Variablennamen
C-Befehle
----------------------------------------------------------------
'#' 120 Num Integer-Zahlen
----------------------------------------------------------------
'$' 36 String alles zwischen "..."
----------------------------------------------------------------
das Zeichen selbst Other alles, was nicht
'x' '#' '$' ist
(auch das 0-Byte wird
hier zu einem 0-Token)
4.7. Heureka! Es funktioniert!
4.7.1. Die neue Start()-Prozedur
Um dem Parser zu ermöglich, sich des Scanners zu bedienen, bauen wir jetzt die Start-Prozedur etwas um.
Code: Alles auswählen
Procedure Start(file_name.s)
; laden des Source-Files
Load(file_name.s)
; auf 1. Zeichen stellen
Source_Position = 0
; das erste aktuelle Token-Lexem-Paar holen
GetChar() ; 1. Zeichen in Zeichen-Strom
SkipWhite() ; alle White bis zum 1. gültigen Look
GetToken() ; anhand 1. gültigem Look erstes Token-Lexem-Paar holen
; --> ab hier ist alles zum Parser-Start vorbereitet
; --> ein gültiges Token-Lexem-Paar liegt bereit
; --> der Parser kann übernehmen und weitermachen
;Debug_CharStream() ; nur zu Debug-Zwecken
;Debug_TokenLexemeStream0() ; nur zu Debug-Zwecken
;Debug_TokenLexemeStream1() ; nur zu Debug-Zwecken
EndProcedure
Der Parser ruft zunächst Scanner::Start(Source-File-Name) auf.
Zunächst wird durch Start() das Source-File in den Speicherbereich geladen, dann Source_Position auf das 1. Zeichen gesetzt.
Wesentlich ist der nächste Absatz. Da der Parser ab jetzt mit Token arbeitet und nicht mehr mit einzelnen Zeichen, müssen wir dafür sorgen, dass der Parser, bevor er das erste Mal eine Analyse der Token vornimmt, auch ein Token im Token Stream vorfindet. Danach springt der Scanner zurück zum aufrufenden Parser.
Die Debug-Prozeduren habe ich zunächst auskommentiert.
4.7.2. Die Token-Lexem-Schleife des späteren Parsers, eine Debug-Prozedur
Die Token-Lexem-Schleife sieht eigentlich genauso aus wie die Look-Schleife in Debug_CharStream() von Kapitel 4.3.
Statt dem Look Ahead Character werden aber jetzt jeweils ein Token-Lexem-Paar angefordert.
Die Prozedur Start() hat das erste Token-Lexem-Pärchen schon in den Token-Lexem-Strom gelegt.
Ich habe 2 Prozeduren erstellt. Die erste macht genau dasselbe wie die zweite, nur dass die 1. zum Vergleichen mit der Look-Schleife übersichtlicher ist und die zweite einen übersichtlicheren Debug-Ausdruck macht.
Code: Alles auswählen
Procedure Debug_TokenLexemeStream0()
; so lange, bis Token = 0-Byte
While ( Token<>0 )
Debug RSet(Str(Token),3," ")+" | '"+Chr(Token)+"' | "+Lexem
GetToken()
Wend
Debug "(außerhalb While-Schleife)"
EndProcedure
Code: Alles auswählen
Procedure Debug_TokenLexemeStream1()
; so lange, bis Token = 0-Byte
While ( Token<>0 )
If Token = #LF: Debug RSet(Str(Token),3," ")+" | 'LF' | "
ElseIf Token = #CR: Debug RSet(Str(Token),3," ")+" | 'CR' | "
Else
Debug RSet(Str(Token),3," ")+" | '"+Chr(Token)+"' | "+Lexem
EndIf
;
GetToken()
Wend
If Token=0:Debug "'0'-Token beendet (außerhalb While-Schleife)":EndIf
EndProcedure
4.7.3. Erster Testlauf des Scanners
Ich beginne mit den Scanner-Tests gleich mit Debug_TokenLexemeStream1(), indem ich bei dieser Prozedur in Start() den Kommentar entferne.
Legen wir eine neue ".tc"-Datei mit dem Namen "Scanner_Test02.tc". In diese Datei geben wir kein Toy-C-Programm, sondern eine Sammlung von Lexemen, um zu sehen, ob unser Scanner alles korrekt erkennt. Wieder gilt: "." ist Leerzeichen, "{enter}" ist die Enter-Taste (Tipp: Man kann in jedem ernstzunehmenden Notepad den "." mit "ersetzen" zu einem Leerzeichen umändern und damit ebenso einfach alle {enter} automatisch löschen lassen):
Code: Alles auswählen
.Name_Variable_03{enter}
.452348{enter}
."Ich.bin.ein.///.String"{enter}
.+.-.={enter}
.<.>{enter}
.<>{enter}
.6/2{enter}
.//.Zeilenkommentar{enter}
./*.Blockkommentar.*/{enter}
Jetzt kommt der große Moment, lassen wir unseren Scanner laufen, indem wir am Ende den Aufrauf auf die neue Toy-C-Datei "Scanner_Test02.tc" ändern:
Der Testlauf sollte erfolgreich sein, außer den Kommentaren von mir und eventuellen Unterschieden wegen Linux bei {enter} sollte das Ergebnis im Debug-Fenster wie in der nächsten Code-Box unten aussehen.
Da die Skip()-Prozeduren noch Dummys sind, in ihnen als nichts gemacht wird, sollten alle White-Zeichen noch da sein. Ich markiere am Rand mit <---, was mit Skip() alles verschwinden sollte, mit <=== die erkannten Token.
Die erste Spalte zeigt den Inhalt der Variable Token an, also den Token-Code, die zweite Spalte zeigt, den Token-Code als Zeichen an, die 3. Spalte das Lexem im String Lexem.
Code: Alles auswählen
32 | ' ' | <--- Skip: Leerzeichen
120 | 'x' | NAME_VARIABLE_03 <=== TOKEN: NAME
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
35 | '#' | 452348 <=== TOKEN: NUM
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
36 | '$' | Ich bin ein /// String <=== TOKEN: STRING (mit allen Zeichen von " bis ")
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
43 | '+' | + <=== TOKEN: OTHER (Zeichen selbst wird als Token-Code übernommen)
32 | ' ' | <--- Skip: Leerzeichen
45 | '-' | - <=== TOKEN: OTHER (Zeichen selbst ...)
32 | ' ' | <--- Skip: Leerzeichen
61 | '=' | = <=== TOKEN: OTHER (Zeichen selbst ...)
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
60 | '<' | < <=== TOKEN: OTHER (Zeichen selbst ...)
32 | ' ' | <--- Skip: Leerzeichen
62 | '>' | > <=== TOKEN: OTHER (Zeichen selbst ...)
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
60 | '<' | < <=== TOKEN: OTHER (Zeichen selbst ...)
62 | '>' | > <=== TOKEN: OTHER (Zeichen selbst ...)
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
35 | '#' | 6 <=== TOKEN: NUM
47 | '/' | / <=== TOKEN: OTHER (Zeichen selbst ...)
35 | '#' | 2 <=== TOKEN: NUM
13 | 'CR' | <--- Skip: Zeilenende (enter)
10 | 'LF' | <--- Skip: Zeilenende (enter)
32 | ' ' | <--- Skip: Leerzeichen
47 | '/' | / <--- Skip: TOKEN: OTHER (Zeichen selbst ...)
47 | '/' | / Hier sollte Skip() erkennen, dass ein Zeilenkommentar vorliegt
32 | ' ' | Noch nimmt der Scanner das Zeichen als Other
120 | 'x' | ZEILENKOMMENTAR ...
13 | 'CR' | ...
10 | 'LF' | ... Ende Zeilenkommentar
32 | ' ' | <--- Skip: Leerzeichen
47 | '/' | / <--- Skip: TOKEN: OTHER (Zeichen selbst ...)
42 | '*' | * Hier sollte Skip() erkennen, dass ein Blockkommentar vorliegt
32 | ' ' | ...
120 | 'x' | BLOCKKOMMENTAR ...
32 | ' ' | ...
42 | '*' | * ...
47 | '/' | / ... Ende Blockkommentar
'0'-Token beendet (außerhalb While-Schleife)
Interessant ist das letzte Zeichen. Ein 0-Byte ist aus Sicht des Scanners ein enfaches Other. Er gibt in Token und Lexem einfach das 0-Byte als 0-Token weiter. Dadurch kann die Token-Lexem-Schleife in Debug_TokenLexemeStream1() beendet werden, denn sie endet, wenn ein Token 0 beeinhaltet.
4.7.4. SkipWhite(), das Überspringen von White-Zeichen
Als Erstes lassen wir alle unnötigen
White-Zeichen verschwinden, bevor wir uns um die Kommentare kümmern. Die Prozedur
SkipWhite() ist zunächst sehr einfach und kurz:
Die erste Version für einfache White-Zeichen: Skipwhite() (V 1.0):
Code: Alles auswählen
Procedure SkipWhite()
; solange in Look ein White
While IsWhite(Look)
GetChar()
Wend
EndProcedure
Starten wir den Scanner! Ah! Das Ergebnis ist im Vergleich zu vorher schon besser, alle White-Zeichen (alle, die wir in IsWhite() definiert haben) sind verschwunden. Nur mehr gültige Token-Lexem-Paare werden auf Anforderung an den Parser übergeben werden. Der Datenmüll oder das Datenrauschen dazwischen wird den Parser nie mehr erreichen, der Scanner filtert das heraus.
Code: Alles auswählen
120 | 'x' | NAME_VARIABLE_03 <=== TOKEN: NAME
35 | '#' | 452348 <=== TOKEN: NUM
36 | '$' | Ich bin ein /// String <=== TOKEN: STRING (mit allen Zeichen von " bis ")
43 | '+' | + <=== TOKEN: OTHER (Zeichen selbst wird als Token-Code übernommen)
45 | '-' | - <=== TOKEN: OTHER -"-
61 | '=' | = <=== TOKEN: OTHER -"-
60 | '<' | < <=== TOKEN: OTHER -"-
62 | '>' | > <=== TOKEN: OTHER -"-
60 | '<' | < <=== TOKEN: OTHER -"-
Das Leerzeichen zwischen < > ist verschwunden!
Anmerkung dazu weiter unten.
62 | '>' | >
35 | '#' | 6 <=== TOKEN: NUM
DAS / ZWISCHEN 6/2 IST WEG -> D A S I S T F A L S C H !!!
35 | '#' | 2 <=== TOKEN: NUM
120 | 'x' | ZEILENKOMMENTAR <--- Hier müssen wir noch etwas mit den Kommentaren tun.
42 | '*' | * ...
120 | 'x' | BLOCKKOMMENTAR ...
42 | '*' | * ...
'0'-Token beendet (außerhalb While-Schleife)
Anmerkung: Wo ist das "//" vor dem Wort "Zeilenkommentar" hingekommen? Wo ist das "/" zwischen "6/2". Die "/" hat SkipWhite() als einfache White-Zeichen übersprungen. Dasselbe gilt für das "/" vor "*" bei "Blockkommentar" und danach.
Das Problem damit ist nur, das ist nicht richtig!
Was ist, wenn wir eine Division als mathematischen Ausdruck wie hier "6/2" in unserem Source-Code haben?
Eben! Der Scanner schluckt auch alle einfachen "/"-Zeichen. Das wäre kein Problem, wenn wir wie Pure Basic zum Beispiel ";" als Kommentarbeginn definiert hätten, wir verwenden aber wie C das doppelte "/", also "//" als Zeilenkommentar und die Kombination "/* ... */" als Blockkommentar.
Das ist aber leicht zu reparieren.
Die zweite Version für einfache White-Zeichen: Skipwhite() (V 2.0):
Code: Alles auswählen
Procedure SkipWhite()
; solange in Look ein White
While IsWhite(Look)
; einfaches (/) als nicht-White im Stream belassen
If Look='/': ProcedureReturn: EndIf
; nächsten Look laden
GetChar()
Wend
EndProcedure
Diese Version schaut zunächst seltsam umständlich aus. Die umständliche Vorgangsweise ergibt nachfolgend einen Sinn.
Wenn wir den Scanner jetzt laufen lassen, dann passt das Ergebnis: Alle "/" sind als einfache Other-Zeichen wieder da.
Code: Alles auswählen
120 | 'x' | NAME_VARIABLE_03
35 | '#' | 452348
36 | '$' | Ich bin ein /// String
43 | '+' | +
45 | '-' | -
61 | '=' | =
60 | '<' | <
62 | '>' | >
60 | '<' | <
62 | '>' | >
35 | '#' | 6
47 | '/' | /
35 | '#' | 2
47 | '/' | /
47 | '/' | /
120 | 'x' | ZEILENKOMMENTAR
47 | '/' | /
42 | '*' | *
120 | 'x' | BLOCKKOMMENTAR
42 | '*' | *
47 | '/' | /
'0'-Token beendet (außerhalb While-Schleife)
Abschließende Bemerkungen zum SkipWhite-Vorgang:
Oben in der Code-Box habe ich als Beispiel den Ungleich-Operator "<>" als beliebiges Beispiel hergenommen (einmal mit und einmal ohne Leerzeichen dazwischen: "<>" und "<.>"), um zu zeigen:
Für den Parser werden folgende Situationen keinen Unterschied machen, alles fließt über die Zeilenenden, Leerzeichen, Tabulatoren, Kommentare hindurch. Das macht die Programmiersprache ungemein flexibel. Tatsächlich ist es so, dass es schwieriger ist, solche Flexibilitäten zu verbieten (also z.B. Zeilenenden mehr beachten), als sie zu erlauben.
Jedes "if(a<>2)" hier sollte die exakt identische Token-Lexem-Folge ergeben:
Erzeugen Sie sich selbst einfach eine solche ".tc"-Datei, die einen ähnlichen Inhalt wie in der Code-Box oben hat, machen Sie Tabulatoren, Leerzeichen usw., dann lassen Sie sie scannen.
Sie werden sehen, dass die Token-Lexem-Paare brav hintereinander kommen, die Zeilenumbrüche, die Leerzeilen, die Tabulatoren, alles ist weg. Hier mein Ergebnis:
Code: Alles auswählen
120 | 'x' | IF
40 | '(' | (
120 | 'x' | A
60 | '<' | <
62 | '>' | >
35 | '#' | 2
41 | ')' | )
120 | 'x' | IF
40 | '(' | (
120 | 'x' | A
60 | '<' | <
62 | '>' | >
35 | '#' | 2
41 | ')' | )
120 | 'x' | IF
40 | '(' | (
120 | 'x' | A
60 | '<' | <
62 | '>' | >
35 | '#' | 2
41 | ')' | )
'0'-Token beendet (außerhalb While-Schleife)
Wir sehen 3-mal dieselben "if(a<>2)", egal wie sie im Source-File geschrieben wurden. Leerzeichen, Zeilenenden, ja sogar Kommentare zwischen den Tokens (müssen wir erst einbauen, siehe nächstes Kapitel) werden vom Scanner vollständig entfernt, es bleiben nur gültige Token-Lexem-Paare für den Parser übrig.
4.7.5. SkipLineComment(), das Überspringen von Zeilenkommentaren
Die große Frage, die sich stellt, ist:
Wo fangen wir die Kommentare ab?
Wer Englisch nicht scheut, kann in Crenshaw eine Diskussion über das Abfangen von Kommentaren lesen.
Möglichkeit 1 ist, das bereits in GetChar() zu tun, dann sind aber in Strings auch keine "//" und "/*" erlaubt, mehr noch, der Scanner würde im String in den Kommentar-Modus springen und Dinge aus dem Char-Stream herausschneiden. Das kann man zwar wieder mit einem Statusflag (z.B. IsStringMode oder Ähnliches) vermeiden, aber der Aufwand lohnt sich meiner Meinung nach nicht. Besser ist Methode 2.
Möglichkeit 2, die ich in meiner Implementation wähle, ist, die Kommentare in SkipWhite() abzufangen.
Das habe ich in IsWhite() bereits vorbereitet. Wer sich gewundert hat, warum eines der White-Zeichen in IsWhite() ein "/" ist und warum wir dann in SkipWhite() wieder bei "/" zurückspringen (wirkt sinnlos), der bekommt jetzt die Erklärung dafür.
"/" muss ein White-Zeichen sein, sonst wird unsere Abfrage in der gleich folgenden erweiterten SkipWhite()-Prozedur nie erreicht, da der Kopf der While-Schleife für ein Verlassen der Schleife sorgt.
Das für Zeilenkommentare erweiterte SkipWhite() (V 3.0):
Code: Alles auswählen
Procedure SkipWhite()
; solange in Look ein White
While IsWhite(Look)
; Zeilenkommentar (// ... #LF/0-Byte)
If Look='/' And PeekA(*Source_Code+Source_Position)='/'
SkipLineComment()
; einfaches (/) als nicht-White im Stream belassen
ElseIf Look='/'
ProcedureReturn
; sonstiges White-Zeichen überspringen
Else
GetChar()
EndIf
Wend
EndProcedure
Wir erweitern die While-Schleife um eine If-ElseIf-Abfrage mit Else-Block.
Sollte ein Zeilenkommentar, der mit "//" beginnt, erkannt werden, dann springt das Programm nach SkipLineComment() (=überspringe Zeilenkommentar).
In Look ist ja bereits ein "/", denn das hat ja den Einsprung in die While-Schleife erlaubt. Source_Position wurde von GetChar bereits um 1 erhöht, d.h. es steht exakt richtig auf dem Look unmittelbar nach dem 1. "/" (Anmerkung: Diesmal wirklich auf dem exakt nächsten Zeichen, d.h. Leerzeichen zwischen Kommentarerkennungszeichen sind anders wie bei zum Beispiel "< >" nicht erlaubt). Dieses Zeichen sollte auch "/" sein und wir haben einen Line Comment erkannt.
Sollte nach "/" irgendetwas anderes als "/" folgen (wie bei uns 6/2, da folgt nach "/" ein "2"), dann bleibt das "/" als Token-Lexem im Token-Lexem-Stream und wird auch als "/" (Other) an den Parser weitergereicht, weil KEIN GetChar() erfolgt, sondern sofort ein ProcedureReturn.
Die Prozedur SkipLineComment():
Code: Alles auswählen
Procedure SkipLineComment()
; bis Zeilenende oder Ende des Source-Files (0-Byte)
While ( Look<>#LF And Look<>#CR And Look<>0 )
GetChar()
Wend
; --> Look steht auf #LF oder #CR oder 0-Byte
; --> v.a. beim 0-Byte ist wichtig, dass es als
; --> Token weitergegeben wird, was beim nächsten
; --> GetToken() auch passiert, weil Look ja
; --> auf dem 0-Byte oder #LF oder #CR steht
EndProcedure
SkipLineComment() überspringt so lange alle Zeichen, bis es am Zeilenende (erinnern wir uns an Line Feed #LF und Linux!) angelangt ist.
Ein Sonderfall ist, wenn in einem Source-File der Cursor nach dem allerletzten Zeichen nicht weiterbewegt wurde, vom Programmierer insbesondere kein {enter} gedrückt wurde. Wenn die letzte Zeile eines Programms ein Zeilenkommentar ohne Enter ist, dann kommt kein Zeilenende, also auch kein #LF. Der Scanner stürzt ab und läuft im Speicher nach dem 0-Byte Amok, vielleicht trifft er ja irgendwann zufällig auf ein #LF (= Zahl 10) oder auch nicht. Dieses Problem werden wir auch in SkipBlockComment() nicht vergessen.
Diesem Fehler bin ich aufgesessen und habe ewig danach gesucht. Die Lösung ist bestechend einfach. Sehen Sie sich diese in der Code-Box an.
Eine andere Möglichkeit wäre, einfach am Ende jedes allozierten *Source_Code statt mit size+1 ein 0-Byte anzuhängen, händisch ein #LF und dann ein 0-Byte anzufügen. Habe ich auch schon gemacht, ich mag die vorige Lösungsvariante mehr. Wieder gilt: Geschmäcker ...
Jetzt können wir wieder "Scanner_Test02.tc" scannen lassen:
Code: Alles auswählen
120 | 'x' | NAME_VARIABLE_03
35 | '#' | 452348
36 | '$' | Ich bin ein /// String
43 | '+' | +
45 | '-' | -
61 | '=' | =
60 | '<' | <
62 | '>' | >
60 | '<' | <
62 | '>' | >
35 | '#' | 6
47 | '/' | / <=== TOKEN: OTHER, KORREKT!
35 | '#' | 2
<=== ZEILENKOMMENTAR IST WEG, KORREKT!
47 | '/' | / <--- Skip: Noch als OTHER gehandhabt
42 | '*' | * <--- Skip: Noch OTHER
120 | 'x' | BLOCKKOMMENTAR <--- Skip: Noch NAME
42 | '*' | * <--- Skip: Noch OTHER
47 | '/' | / <--- Skip: Noch OTHER
'0'-Token beendet (außerhalb While-Schleife)
Sehr schön, der Zeilenkommentar ist verschwunden, der Blockkommentar ist natürlich noch da. Auch das "/" in "6/2" ist als Other-Zeichen korrekt vorhanden.
4.7.6. SkipBlockComment(), das Überspringen von Kommentarblöcken
Die Prozedur SkipWhite() muss zunächst für die Verwendung von Block-Kommentaren angepasst werden.
Das für Blockkommentare erweiterte SkipWhite() (V 4.0):
Code: Alles auswählen
Procedure SkipWhite()
; solange in Look ein White
While IsWhite(Look)
; Zeilenkommentar (// ... #LF|#CR|0-Byte)
If Look='/' And PeekA(*Source_Code+Source_Position)='/'
SkipLineComment()
; Blockkommentar (/* ... */)
ElseIf Look='/' And PeekA(*Source_Code+Source_Position)='*'
SkipBlockComment()
; einfaches (/) als nicht-White im Stream belassen
ElseIf Look='/'
ProcedureReturn
; sonstiges White-Zeichen überspringen
Else
GetChar()
EndIf
Wend
EndProcedure
Wir erweitern die While-Schleife um einen weiteren ElseIf-Block.
Die Prozedur SkipBlockComment():
Code: Alles auswählen
Procedure SkipBlockComment()
; (/) überspringen
GetChar()
; solange bis (*/)
Repeat
; Zeichen holen, bei Ersteintritt (*) überspringen
GetChar()
; verschachtelte Block-Kommentare ermöglichen
If Look='/' And PeekA(*Source_Code+Source_Position)='*'
SkipBlockComment()
EndIf
; auf 0-Byte achten
If Look=0: ProcedureReturn: EndIf
Until Look='*' And PeekA(*Source_Code+Source_Position)='/'
; (*/) überspringen
GetChar()
GetChar()
; --> Look steht auf 1. Zeichen nach (*/)
EndProcedure
- Zunächst wird "/" übersprungen und Look steht auf "*".
REPEAT
- Nach dem ersten Eintritt in die Repeat-Schleife holt GetChar() das nächste Look und überspringt damit "*".
In der laufenden Repeat-Schleife holt ChetChar() immer den nächsten Look Ahead Character.
- Sollte irgendwann innerhalb des Blockkommentars wieder "/*" folgen, wird voll auf Rekursion gesetzt und erneut SkipBlockComment() aufgerufen. Verschachtelte Kommentare sind also kein Problem und bereits eingebaut.
--> bei Punkt 1 gehts in der neuen Instanz von SkipBlockComment() wieder los.
UNTIL Look = "*/"
- 2-mal GetChar(), um "*/" zu überspringen, damit Look auf 1. Zeichen nach "*/" steht.
Daher sind folgende Situationen kein Problem:
Jetzt können wir wieder "Scanner_Test02.tc" scannen lassen ...
Code: Alles auswählen
120 | 'x' | NAME_VARIABLE_03
35 | '#' | 452348
36 | '$' | Ich bin ein /// String
43 | '+' | +
45 | '-' | -
61 | '=' | =
60 | '<' | <
62 | '>' | >
60 | '<' | <
62 | '>' | >
35 | '#' | 6
47 | '/' | / <=== TOKEN: OTHER, KORREKT!
35 | '#' | 2
'0'-Token beendet (außerhalb While-Schleife)
... und stellen fest. Der Blockkommentar ist verschwunden, das einfache "/" in "6/2" ist korrekterweise noch da! Voila!
Testen Sie, testen Sie, testen Sie! Verschachteln Sie Kommentare, ignorieren Sie Zeilenenden. Schauen Sie, ob alles funktioniert!
4.7.7. Die Error()-Prozeduren
Wir können bereits auf Scanner-Ebene eine einfache Fehlerbehandlung realisieren.
Sie haben sicher bemerkt, dass alle Get()-Prozeduren (als Beispiel drucke ich GetNum() in der folgenden Code-Box ab) eine Abfrage ihrer entsprechenden Is?()-Prozedur eingebaut haben. Wofür?
Code: Alles auswählen
Procedure GetNum()
; --> in Look ist jetzt das 1. Zeichen dieses Token-Lexems
; 1. Zeichen korrekt fuer Num?
If Not IsNum(Look):Expected("Integer"):EndIf <=== HIER BEFINDET SICH DIE GEMEINTE ABFRAGE
; Token-Typ zuordnen
Token = '#'
; Lexem mit Name füllen
Lexem = ""
Repeat
Lexem = Lexem + Chr(Look)
GetChar()
Until Not IsNum(Look)
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
Der Parser wird in bestimmten Situationen nicht immer GetToken() aufrufen. Wenn er zum Beispiel weiß, dass er sicher einen Name oder eine Num erwartet, dann ruft er gleich GetName() bzw. GetNum() auf. Sollte jetzt im Source-Code z.B. kein Name folgen, den der Parser erwartet, dann erhalten wir richtigerweise eine Fehlermeldung.
Die beiden Fehlerprozeduren, die das ermöglichen, erklären sich selbst.
Auf ein "Fehler in Zeile X" verzichte ich der Einfachheit halber, vielleicht baue ich das später ein. Es ist nicht schwer zu realisieren, aber je mehr Programmzeilen wir erzeugen, desto unübersichtlicher wird der Code und ich möchte eigentlich beim Thema Compilergrundlagentechnik bleiben. Wenn man dann diese sicher und zuverlässig verstanden hat, dann kann man den Compiler später "aufpeppen". Außerdem lasse ich den Compiler beim ersten Fehler, den er findet, enden, es wird keine Warnungsliste oder dergleichen erstellt.
Code: Alles auswählen
Procedure Error(error_message.s)
MessageRequester("Fehler",error_message)
End
EndProcedure
Code: Alles auswählen
Procedure Expected(expected_object.s)
Error(expected_object+" wird erwartet.")
EndProcedure
4.8. Schlussbemerkungen
So, wir haben einen Scanner, der unseren Ansprüchen zunächst genügen wird. Sollten Änderungen da und dort nötig werden, werden diese im Laufe der weiteren Programmentwicklung nachträglich eingebaut werden.
Der Scanner wird folgendermaßen aufgerufen, verwendet und wieder beendet:
- Start: Gestartet wird er vom Parser mit Scanner::Start(Name des Source-Files.FileExtension)
- Lauf: Weitergetrieben wird er vom Parser mit Scanner::GetToken(), Scanner::GetName(), Scanner::GetNum(), Scanner::GetString(), Scanner::GetOther() - je nach Verwendungszweck. Für besondere Fälle steht auch Scanner::GetChar() zur Verfügung, doch Vorsicht, den Scanner nicht aus dem Schritt bringen mit dem Lesen von einzelnen Look Ahead Charactern!
- Ende: Da der Scanner ein 0-Byte am Ende von *Source_Code findet und als Other-Token zurückgibt, bietet es sich an, dass der Parser das als End-Of-Source-File verwendet, um sich zu beenden.
Ein kleiner Scherz zuletzt, der aber die Leistungsfähigkeit des Scanners zeigt:
Code: Alles auswählen
if (a <> 2)
if (a < /*Kommentar mitten im Ungleich-Operator gefällig*/ > 2)
if (a < // kleiner
> // größer
2 // Nummer 2
/* noch ein Blockkommentar vor der Klammer gefällig*/ )
Es sollte dennoch nur 3-mal "if(a<>2)" gescannt werden und alles andere herausgefiltert sein.
Mein Scanner gibt aus:
Code: Alles auswählen
120 | 'x' | IF
40 | '(' | (
120 | 'x' | A
60 | '<' | <
62 | '>' | >
35 | '#' | 2
41 | ')' | )
120 | 'x' | IF
40 | '(' | (
120 | 'x' | A
60 | '<' | <
62 | '>' | >
35 | '#' | 2
41 | ')' | )
120 | 'x' | IF
40 | '(' | (
120 | 'x' | A
60 | '<' | <
62 | '>' | >
35 | '#' | 2
41 | ')' | )
'0'-Token beendet (außerhalb While-Schleife)
Was allerdings
nicht funktioniert (weder gewollt noch üblich), sind Kommentare
mitten in Token-Lexemen (also z.B. mitten in Namen oder Nummern).
Kommentare (jedes White) sind aber an jeder Stelle
zwischen Token-Lexemen möglich.
*** ENDE KAPITEL 4 ***