4. Lexikalischer Scanner
4.1. Vorbemerkungen
Ich möchte mit dem
lexikalischen Scanner (kurz Lexer, auch Tokenizer genannt) beginnen.
Unser kleiner Scanner soll TTCS heißen, also Tiny Toy C Scanner
.
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.
Die Aufgabe des Scanners ist, aus einzelnen Zeichen des Source-Code-Files klar abgegrenzte
Zeichengruppen, also Token zu machen. Dazu füllt der Scanner einen
Token-Code (also eine
Zahl) in die
globale Variable Token und in die ebenfalls
globale Variable Lexem den
Inhalt der Zeichengruppe (also einen
String)
Code: Alles auswählen
.--------------------.
| einzelne Zeichen |
| ------------------ |
| i |
| f |
| ( |
| X |
| _ |
| P |
| o |
| s |
| > |
| 3 |
| ) |
| #CR |
| #LR |
'--------------------'
|
|
| SCANNER
| ^^^^^^^
|
v
.--------------------.
| Token Lexem |
| ------------------ |
| #NAME if |
| '(' egal |
| #NAME x_pos |
| '>' egal |
| #INTEGER 3 |
| #eol egal | (#eol=End of Line,
'--------------------' Ende einer Zeile)
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 "#TAB",
Kommentare und sogar die in Java oder allen C-Varianten oder auch Pascal 'heiligen'
Semikolons ";" werde ich in meinem Code dazurechnen.
Vor allem die Frage der Behandlung des "
#CR" (Carriage Return) bzw. des "
#LF" (Line Feed) am Zeilenende sowei des "
;" am Ende eines TTC-Befehls ist eine durchaus zu diskutierende. Wir werden #CR und #LF durch ein ein Ersatzzeichen ersetzen (siehe weiter unten Kapitel über Zeilenenden).
Falls eine Erweiterung von TTC vorgesehen ist, werde ich mich dieser Frage wieder zuwenden, jetzt behandeln wir die Sache einmal so und ignorieren diese Zeichen.
Ein Beispiel :
Stellen wir uns eine Textdatei mit dem Namen "
source-code.ttcs" vor (.ttcs = Tiny Toy C Source)
Ein Source-Code ist eine so genannte
Quelltextdatei. Hier steht also unser Programm der Programmiersprache TinyToyC. Wichtig ist, dass es sich um reine Textform handelt, in der wir diese Datei abspeichern. Aus dieser Datei liest dann der lex. Scanner seine Zeichen aus, und zwar Zeichen für Zeichen, bis er ein 0-Byte sieht und abbricht.
Das Ergebnis werden wir dann im Debug-Fenster bewundern können.
Wir würden die Zeile oben zum Einfügen in unsere Source-Code-Datei benutzen. Die nächste Zeile zeigt uns zur Verdeutlichung nur die Leerzeichen "
_" und die Zeilenendzeichen (hier wurde beim Eingeben ENTER gedrückt) "
#CR" gefolgt von "
#LF" (wenn wir Windows benutzen).
Lassen wir unseren Parser laufen, dann arbeitet folgende Schritte ab:
Der Parser fordert vom Scanner mit GetToken() ein neues aktuelles Token-Lexem-Paar an:
Code: Alles auswählen
If _ (Var=35) _ _ _ print(Var) #CR #LF
^--- 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 _ (Var=35) _ _ _ print(Var) #CR #LF
^--- 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 _ (Var=35) _ _ _ print(Var) #CR #LF
^--- 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 "r"
- 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 "Var"
- 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 _ (Var=35) _ _ _ print(Var) #CR #LF
^--- 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 _ (Var=35) _ _ _ print(Var) #CR #LF
^--- in Look ist "3"
- Der Scanner erkennt mit IsNum(): "3" ist eine Zahl, verzweigt nach GetNumber()
- GetNumber() holt jetzt Zeichen für Zeichen, bis zum ersten Nicht-Ziffern-Zeichen. Der Scanner steht mit Look auf ")" nach "5"
- GetNumber() ruft nun SkipWhite() auf, diese Prozedur überspringt alle White-Zeichen (keine da)
- Am Ende von GetNumber() haben wir folgendes Ergebnis:
- In Token ist die Code-Zahl von #INTEGER (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 _ (Var=35) _ _ _ print(Var) #CR #LF
^--- in Look ist ")"
- Das kennen wir schon von oben, GetOther() holt das ")"
- GetOther() ruft nun SkipWhite() auf, diese Prozedur überspringt 3 White-Zeichen (hier Leerzeichen) 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 _ (Var=35) _ _ _ print(Var) #CR #LF
^--- 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
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 und Pure Basic uns mit Fehlermeldungen von nicht vorhandenen Prozeduren plagt.
Code: Alles auswählen
; XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
; X X
; X LEXIKALISCHER TTC-SCANNER : TinyToyC (TTC) - Kapitel 4 X
; X X
; XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
; *******************************************************************
; * Scanner Version TTCS Kap. 4 *
; *******************************************************************
DeclareModule Scanner
; -
; - Public Declarations ---------------------------------------------
; -
Global Look ; Look Ahead Character
Global Token ; Token-Typ als Zahl
Global Lexem.s ; Lexem = Token als String
Global LineNr ; aktuelle Zeilennummer
; --- Start- & Stop-Prozedur ---
Declare Start(file_name.s="") ; Filename des Source-Files
Declare Stop()
; --- 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 GetNumber() ; holt nächste Zahl
Declare GetString() ; holt nächsten String
Declare GetOther() ; holt Rest
; --- Fehlermeldung ausgeben ---
Declare Error(error_message.s) ; zeigt Meldung, Scanende
Declare Expected(expected_object.s) ; zeigt, was erwartet
; wurde, dann Scanende
; --- Is?-Erkennungs-Macros ---
Macro IsNumber(c) ; Zeichen gehört zu einer Zahl?
EndMacro
Macro IsName1(c) ; Zeichen ist Start eines Namens?
EndMacro
Macro IsName(c) ; Zeichen gehört zu einem Namen?
EndMacro
Macro IsString(c) ; Zeichen ist der Start eines Strings?
EndMacro
Macro IsWhite(c) ; Zeichen ist ein White-Character?
EndMacro
; --- Debug-Prozeduren (Im Release löschen) ---
Declare Start_GetChar(file_name.s)
; Anmerkung: / startet einen Zeilenkommentar mit '//' (wie in C)
; / startet einen Blockkommentar mit '/*' (wie in C)
EndDeclareModule
Module Scanner
; -
; - Private Declarations --------------------------------------------
; -
Global *Source_Code ; Zeichen für Zeichen im Speicher
Global *Source_Pos.ASCII ; nächste Zeichen-Lese-Position
; --- Lade Source-File ---
Declare Load(file_name.s) ; lädt das Text-Zeichen-Source-File
; --- Skip - Prozeduren ---
Declare SkipWhite() ; überspringt White-Zeichen
Declare SkipLineComment() ; überspringt ab Comment-Start bis #eol
Declare SkipBlockComment() ; überspringt von Block-Start bis -Ende
; -
; - Start - Prozedur -----------------------------------------------
; -
Procedure Start(file_name.s="") :EndProcedure
Procedure Stop() :EndProcedure
Procedure Start_GetChar(file_name.s) :EndProcedure
; - Lade Source-File -----------------------------------------------
; -
Procedure Load(file_name.s) :EndProcedure
; - Get - Prozeduren -----------------------------------------------
; -
Procedure GetChar() :EndProcedure
Procedure GetToken() :EndProcedure
Procedure GetName() :EndProcedure
Procedure GetNumber() :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
; *******************************************************************
; * Debug-Prozeduren (außerhalb der Module) *
; *******************************************************************
Procedure Debug_GetChar() :EndProcedure
Procedure Debug_GetToken() :EndProcedure
; Aufruf je nach Ziel:
; Debug_GetChar()
; Debug_GetToken()
4.4. Laden und testen des Character Streams
Wir legen uns jetzt einen beliebigen Projektordner und darin ein Textfile mit dem Namen
"source-code.ttcs" an, wobei ".ttcs" für Tiny Toy C und das S für Source-Code 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.
Auch sollte IN diesem Projektordner das Pure-Basic-File
"ScannerTTC.pb" sein, am besten wir kopieren das Gerüst von Kapitel 4.3. einfach in dieses .pb-File.
In
"source-code.ttcs" speichern wir für unsere kommenden Tests folgenden Code, ein "
_ " stellt ein Leerzeichen dar. Die üblichen Zeilenendezeichen sind ebenfalls vorhanden, also bitte nach der if-Zeile 1-mal ENTER drücken:
Code: Alles auswählen
name "Ich String" 56547 // Zeilenkommentar
( < < > <= > /*Blockkommentar*/
6/2
Code: Alles auswählen
Zur Verdeutlichung nochmals Zeile 2:
( < < > <= > /*Blockkommentar*/
^ ^ ^^^ ^^ ^
| | | | | Token
| | | | | -----
| | | | '--- > '>'
| | | '----- <= 'k'leinergkleich
| | '---------- <> 'u'ngleich
| '-------------- < '<'
'---------------- ( '('
Code: Alles auswählen
name _ _ "Ich _ String" _ _ 56547 _ // _ Zeilenkommentar #CR #LF
( _ < _ _ < _ > _ _ <= _ > _ /*Blockkommentar*/ #CR #LF
6/2
Anmerkung:
Ich verwende dafür den Programmierer-Notepad
Notepad++, weil ich damit auf einfache Weise ein Syntax-Highlighting in meiner erfundenen Sprache realisieren kann. Für Toy C stelle ich die Einstellung einfach auf "C", denn Toy C ist C-ähnlich.
Wir müssen zunächst eine
Start()-Prozedur programmieren, die für uns alle notwendigen
Initialisierungen und Vorbereitungen durchführt:
Code: Alles auswählen
Procedure Start_GetChar(file_name.s)
; --> Die Prozedur heißt aus Debug-Gründen Start_GetChar()
; --> Die echte Start-Prozedur wird nur mehr Start() heißen
; laden des Source-Files
Load(file_name.s)
; '*Source_Pos' auf 1. Zeichen stellen
*Source_Pos = *Source_Code
; das erste aktuelle Zeichen (Look) holen
GetChar()
; --> ab hier ist alles zum Character-Stream-Test bereit
; --> ein gültiger Look liegt im Stream
EndProcedure
Anmerkung: Später wird diese Prozedur einfach nur Start() heißen. Der Name oben ist dem noch folgenden Debug-Vorgang geschuldet.
Mit dieser Prozedur beginnen wir später den Vorgang des Kompilierens, also immer Scanner:Start() aufrufen, bevor kompilert werden soll.
- Wir laden in dieser Prozedur unser Source-File.
- Wir stellen den Zeichenzeiger *Source_Pos auf den Start des Memorybereichs *Source_Code, der den Text unseres Source-Codes aufnimmt.
Der Zeiger *Source_Pos zeigt auf die nächste Scanposition, dieser Zeiger zeigt also exakt auf das Zeichen im Source-Code, das beim nächsten Aufruf von GetChar() nach Look geholt werden wird.
- An der Prozedur Start() sehen wir durch den Aufruf von GetChar()/ an dieser Stelle eine Tatsache deutlich.
Wir müssen wir den Character-Strom einmal das erste Mal "aufpumpen", sprich das erste Zeichen noch vor irgendeiner Schleife mit Look vorbelegen. Warum? Der Scanner erwartet nämlich, dass immer ein aktuelles Look-Zeichen in Look bereits vorhanden ist, also auch ganz zu Beginn.
Die Prozedur
Load() zum Laden des Codes im
Source-File in einen Memorybereich namens
*Source_Code:
Code: Alles auswählen
Procedure Load(file_name.s)
; lade Source-File mit Filename
file = ReadFile(#PB_Any,file_name)
If Not file
Error("Das Source-File "+#DQUOTE$+file_name+#DQUOTE$+
" konnte nicht geöffnet werden.")
EndIf
; speichere Source-File in Memory-Bereich
size = Lof(file)
*Source_Code = AllocateMemory(size+1) ; damit am Ende 0-Byte
ReadData(file, *Source_Code, size)
CloseFile(file)
EndProcedure
- Das Source-File laden wir ganz einfach in einen Speicherbereich, dessen Startpunkt wir mit einem Zeiger mit dem Namen *Source_Code adressieren.
- Am Ende des Memory-Bereichs muss ein 0-Byte sein, deshalb size+1 im Befehl AllocateMemory (siehe Code).
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 .
Als Nächstes programmieren wir die bereits erwähnte Prozedur
GetChar(). Ich weiß, hier ist ein Pointer besser als eine Funktion.
Code: Alles auswählen
Procedure GetChar()
; Look aus dem Source-Code-Stream holen
Look = PeekA(*Source_Pos)
*Source_Pos+1
EndProcedure
Im Code dieses Kapitels setze ich letztlich die auch nicht schwerer zu verstehende
Pointer-Variante um. Wir dürfen aber hier nicht vergessen, die Variable *Source_Pos als *Source_Pos.Ascii zu deklarieren:
Code: Alles auswählen
Procedure GetChar()
; Look aus dem Source-Code-Stream holen
Look = *Source_Pos\a
*Source_Pos+1
EndProcedure
Dies Prozedur macht dasselbe wie die erste Version, nur ist sie etwas schneller.
- Die Prozedur GetChar() hat zunächst die Aufgabe, bei Aufruf das Zeichen (Char = Character) vom Source-File-Speicherbereich an der *Source_Pos in die globale Variable Look zu holen.
- Dann wird *Source_Pos um ein Zeichen erhöht und zeigt somit auf das Zeichen im Zeichenstrom, das beim nächsten Aufruf von GetChar() geladen werden würde.
Programmieren wir uns jetzt noch schnell eine Debug-Prozedur (außerhalb des Moduls – siehe Gerüst) mit dem Namen
Debug_GetChar, um alles, was wir bis hierher programmiert haben, zu testen.
Die Debug-Prozedur
Debug_GetChar() müssen kurz besprechen:
Code: Alles auswählen
Procedure Debug_GetChar()
; --> wir laden Start_GetChar() aus Debug-Zwecken
; --> später heißt die Prozedur nur mehr Start()
Scanner::Start_GetChar("source-code.ttcs")
While ( Scanner::Look <> 0 )
Debug " | "+Chr(Scanner::Look)+ ; CHAR des ASCII-Codes
" | "+Scanner::Look ; CHAR-Code in Look
Scanner::GetChar()
Wend
Debug "0-Byte: außerhalb der While-Schleife"
EndProcedure
- Nachdem wir die Start-Prozedur des Scanners gestartet haben, geht eine While-Schleife durch den Speicherbereich *Source_Code (über den Zeiger *Source_Pos), aber nur, wenn kein 0-Byte als Terminator (Ende-Signalzeichen) in Look ist.
- Dabei bekommen wir im Debug-Fenster links das Zeichen als Character und rechts den entsprechenden Character-Code dieses Zeichens angezeigt, wenn wir Debug_GetChar() starten.
- 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 laufenl, indem wir
Debug_GetChar() aufrufen.
Falls bei mir andere Zeichen zu sehen sind, dann liegt es daran, dass ich als
Schriftart für die Debug-Ausgabe "Courier New" eingestellt habe.
Alles sollte funktionieren und folgende
Debug-Ausgabe sollte zu sehen sein (andere Systeme als Windows könnten am Zeilenende andere Ergebnisse haben – dazu gleich im nächsten Unterkapitel mehr):
Code: Alles auswählen
| n | 110
| a | 97
| m | 109
| e | 101
| | 32 <== ASCII 32 = Leerzeichen
| | 32
| " | 34 <== Start: String
| I | 73
| c | 99
| h | 104
| | 32
| S | 83
| t | 116
| r | 114
| i | 105
| n | 110
| g | 103
| " | 34 <== Ende: String
| | 32
| | 32
| 5 | 53
| 6 | 54
| 5 | 53
| 4 | 52
| 7 | 55
| | 32
| / | 47 <== Start: Zeilenkommentar
| / | 47
| | 32
| Z | 90
| e | 101
| i | 105
| l | 108
| e | 101
| n | 110
| k | 107
| o | 111
| m | 109
| m | 109
| e | 101
| n | 110
| t | 116
| a | 97
| r | 114
| <== Ende: Zeilenkommentar
| 13 <== ASCII 13: #CR -> Zeilenende (ENTER-TASTE)
|
| 10 <== ASCII 10: #LF -> Zeilenende (ENTER-TASTE)
| ( | 40
| | 32
| < | 60 <== ASCII 60 = Char-Code für "<"
| | 32
| | 32
| < | 60 -.
| | 32 | <>: mehrteiliger Operator 'u'ngleich
| > | 62 -' (mit White-Zeichen dazwischen)
| | 32
| | 32
| < | 60 -.
| = | 61 -' <=: mehrteiliger Operator 'k'leinergleich
| | 32
| > | 62
| | 32
| / | 47 <== Start: Blockkommentar
| * | 42
| B | 66
| l | 108
| o | 111
| c | 99
| k | 107
| k | 107
| o | 111
| m | 109
| m | 109
| e | 101
| n | 110
| t | 116
| a | 97
| r | 114
| * | 42
| / | 47 <== Ende: Blockkommentar
|
| 13 <== ASCII 13: #CR -> Zeilenende (ENTER-TASTE)
|
| 10 <== ASCII 10: #LF -> Zeilenende (ENTER-TASTE)
| 6 | 54
| / | 47 <== einfaches '/': Division
| 2 | 50
0-Byte: außerhalb der While-Schleife
.-------------------------------------.
| Die Kombination 13 10 ist typisch |
| für WINDOWS als Zeilenende (ENTER). |
| Andere Systeme können abweichen! |
'-------------------------------------'
4.5. Die ENTER-Taste: das Ende einer Programmzeile
Interessant ist, was bei der
ENTER-Taste passiert.
Windows setzt die Enter-Taste (= das Zeilenendezeichen) als
2 Zeichen um: die Kombination "
#CR" (ASCII 13: Carriage Return) und "
#LF" (ASCII 10: Line Feed). Dafür gibt es bei Pure Basic die beiden genannten 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 danach "
#CR", also umgekehrt zu Windows oder auch einfach nur ein einfaches "
#CR". Alles ist möglich, selbst exotische Zeichen auf manchen Servern.
Ich verweise hier auf
Wikipedia-Eintrag über den Zeilenumbruch, der verschiedene Möglichkeiten erklärt. Die
englischsprachige Wikipedia über Newline ist hier viel genauer, aber eben leider auf Englisch.
Was bedeutet das für die Praxis? Eine neue GetChar()-Prozedur!
Wir sollten eine Prozedur schreiben, die mit allen möglichen Zeilenenden fertig wird, auch schon deshalb, weil jemand, der unseren Compiler z.B. in einem Mac oder auf Linux benutzt, Probleme bekommt, wenn wir hier nicht hilfreich diese Probleme im Vorfeld vermeiden.
Die
neue Prozedur GetChar(), die mit vielen gängigen Zeilenumbruchsvarianten zurechtkommt:
Code: Alles auswählen
Procedure GetChar()
; Look aus dem Source-Code-Stream holen
Look = *Source_Pos\a
*Source_Pos+1
; alle möglichen Zeilenenden zu '¶' umwandeln
; in '¶'=182: End of Line, Zeilenende
; Zeilenummer hochzählen
If (Look=#CR And *Source_Pos\a=#LF) Or
(Look=#LF And *Source_Pos\a=#CR)
Look='¶'
LineNr+1
*Source_Pos+1 ; überspringe 2. Zeichen
ElseIf Look=#CR Or Look=#LF
Look='¶'
LineNr+1
EndIf
EndProcedure
Diese Prozedur schickt ein von uns definiertes Token mit der Konstante 182 statt den anderen Zeilenendzeichen als Look weiter.
Es tauscht also etwas im Character Stream aus. Ein zweite wichtige Sache ist hier eingbaut, jedesmal wenn ein Zeilenende erfolgt, wird die Zeilennummer um 1 erhöht. Damit können wir später den Ort eines Fehlers eingrenzen.
Starten wir jetzt noch einmal den Scanner, dann müssten alle "13 10" am Ende ausgetauscht sein gegen Token-Code Nr. 182, den ich durchaus bewusst (wie viele andere Token-Codes auch) so gewählt habe. Hier ist es ein von Word verwendeter Code, der ein Zeilenende anzeigt und sich als Zeichen im Debug-Fenster schön anzeigen lässt (Code 182 ist das "¶" - Paragraphzeichen, bei uns sagt man im Volksmund "die Pi's" dazu wegen der Ähnlichkeit zu PI).
Wir hätten prinzipiell auch jede andere beliebige Zahl wählen können.
Code: Alles auswählen
alte Lösung:
^^^^^^^^^^^
... gekürzt ...
| * | 42
| / | 47 <== Ende: Blockkommentar
|
| 13 <== ASCII 13: #CR -> Zeilenende (ENTER-TASTE)
|
| 10 <== ASCII 10: #LF -> Zeilenende (ENTER-TASTE)
| 6 | 54
| / | 47
| 2 | 50
0-Byte: außerhalb der While-Schleife
neue Lösung:
^^^^^^^^^^^
... gekürzt ...
| * | 42
| / | 47 <== Ende: Blockkommentar
| ¶ | 182 <=== AUSGETAUSCHT GEGEN TOKEN-CODE 182
| 6 | 54
| / | 47
| 2 | 50
0-Byte: außerhalb der While-Schleife
4.6. Die Is?()-Token-Typ-Erkennungsmacros
Im Kapitel "Hintergrundgedanken zum Scanvorgang" erwähne ich immer wieder Prozeduren wie
IsName() oder
IsNumber().
Ich werde sie als
Macros implementieren.
Diese Macros sollen anhand des
Look erkennen, welcher Token-Typ folgen könnte. Außerdem kann ein Is?()-Macro anhand eines Zeichens erkennen, ob dieses in einem bestimmten Token-Typ vorkommt.
Hier eine Auflistung der Is?()-Erkennungsmacros:
Code: Alles auswählen
=============================================================
Erkennungs- Zeichen Anmerkung
macro
=============================================================
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
-------------------------------------------------------------
IsNumber() 0..9 . Zahl
-------------------------------------------------------------
IsString() " Start-Zeichen eines Strings
Ende-Zeichen eines Strings
-------------------------------------------------------------
IsWhite() Leerzeichen Zeichen, die der Scanner
#TAB ¶ ; überspringt
/ (bzw. / löst Kommentar aus)
=============================================================
Die als Macros ausgeführten
Is?()-Macros:
Code: Alles auswählen
; --- Is?-Erkennungs-Macros ---
Macro IsNumber1(c) ; Zeichen gehört zu Start einer Zahl?
(c>='0' And c<='9')
EndMacro
Macro IsNumber(c) ; Zeichen gehört zu einer Zahl?
((c>='0' And c<='9') Or c='.')
EndMacro
Macro IsName1(c) ; Zeichen ist Start eines Namens?
((c>='a' And c<='z') Or (c>='A' And c<='Z'))
EndMacro
Macro IsName(c) ; Zeichen gehört zu einem Namen?
(IsNumber(c) Or IsName1(c) Or c='_')
EndMacro
Macro IsString(c) ; Zeichen ist der Start eines Strings?
c='"'
EndMacro
Macro IsWhite(c) ; Zeichen ist ein White-Character?
(c=' ' Or c=#TAB Or c='/' Or c=';' Or c='¶')
EndMacro
;Anmerkung: / startet einen Zeilenkommentar mit '//' (wie in C)
; / startet einen Blockkommentar mit '/*' (wie in C)
Sehr schön ist auch, dass man, wenn man zum Beispiel Token 182 ('¶') - wir haben gerade über End of Line weiter oben diskutiert - nicht mehr als White-Zeichen führen will, es einfach aus dem Macro 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.7. Die Get-Prozeduren und die Token-Codes
Die
wichtigste Prozedur, die zumeist vom Parser aufgerufen werden wird, ist die
GetToken()-Prozedur.
Sie dient als
Verteiler. Mittels den Is?()-Macros 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() sorgt dafür.
Die zentrale Verteilerprozedur
GetToken():
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
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
Die folgenden Get()-Prozeduren holen jeweils 1 Token aus dem Strom von Zeichen des Source-Codes.
Die Prozedur, die Namen holt -
GetName():
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("Ein Variablen-, Prozedurname oder TTC-Befehlswort")
EndIf
; Token mit Token-Code (=78) für Name füllen
Token = 'N'
; Lexem mit Name füllen
Lexem = ""
Repeat
Lexem = Lexem + Chr(Look)
GetChar()
Until Not IsName(Look)
; Name-Identifier sind nicht Case sensitiv
Lexem = LCase(Lexem)
; am Ende ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
- GetName() holt zunächst in der Repeat-Schleife alle Zeichen, die in ein Name-Token gehören. Beim ersten Nicht-Name-Zeichen ist das Lexem korrekt gefüllt.
- Es wird überprüft, ober das Lexem ein reserviertes Schlüsselwort ist und dessen Token zugewiesen.
- Es ist irgendein Variablen- oder Prozedurname und das Token 'N' (Code 78) wird zugewiesen.
Die Prozedur, die Zahlen holt -
GetNumber():
Code: Alles auswählen
Procedure GetNumber()
; --> in Look ist jetzt das 1. Zeichen dieses Token-Lexems
; 1. Zeichen korrekt fuer Number?
If Not IsNumber1(Look):Expected("Eine Zahl"):EndIf
; Lexem mit Number füllen
Lexem = ""
Repeat
Lexem = Lexem + Chr(Look)
GetChar()
If Look='.':point+1:EndIf
Until Not IsNumber(Look)
; Float-Number, Fehler oder Integer?
If point=0:
Token='I'
ElseIf point=1:
Token='F'
; Testen, ob zu kurz?
If Len(Lexem)<3:
Error("Die Fließkommazahl ist unvollständig.")
EndIf
Else
Error("In einer Fließkommazahl darf nur maximal "+
"ein Kommapunkt vorkommen und nicht "+Str(Point)+".")
EndIf
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
- GetNumber() holt eine Zahl, Ziffer für Ziffer. Bei der ersten Nichtziffer ist das Lexem korrekt gefüllt. Es wird hier unterschieden zwischen Integer (ohne Dezimalpunkt) und Float.
- Anmerkung: Genau hier könnte man auch z.B. eine Hex-Zahl oder Ähnliches erkennen und der jeweiligen Situation entsprechende Token-Codes zuweisen und/oder Umwandlungen vornehmen.
Die Prozedur, die Strings holt -
GetString():
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("Ein konstanter String in "+#DQUOTE$+#DQUOTE$)
EndIf
; Token mit Token-Code (=83) für String füllen
Token = 'S'
; '"' String-Start-Zeichen überspringen
GetChar()
; Lexem mit String füllen
; bis Ende-Zeichen '"'
Lexem = ""
While Not IsString(Look)
Lexem = Lexem + Chr(Look)
GetChar()
Wend
; 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
- GetString() holt einen String innerhalb von Anführungszeichen. Wir dürfen nicht vergessen, dass wir die Erkennungszeichen in IsString() eigenhändig definiert haben. Wir könnten das DORT jederzeit z.B. auf einfache Anführungszeichen, oder was auch immer wir wünschen, ändern, wenn wir das wollen würden.
Die Prozedur, die mehrteilige Operatoren und alles andere holt, also den Rest, der übrig bleibt -
GetOther():
Code: Alles auswählen
Procedure GetOther()
; --> in Look ist jetzt das 1. Zeichen dieses Token-Lexems
; Look sichern
look1 = Look
; nächsten nicht-White-Character holen (siehe SkipWhite())
GetChar()
; ueberspringe alle White Characters und Comments
SkipWhite()
; ** mehrteilige Operatoren testen und abschicken **
If look1='<' And Look='>' : Token='u' ; 'u'ngleich
GetChar() ; Token-Code 117
ElseIf look1='<' And Look='=' : Token='k' ; 'k'leinergleich
GetChar() ; Token-Code 107
ElseIf look1='>' And Look='=' : Token='g' ; 'g'rößergleich
GetChar() ; Token-Code 103
Else : Token = look1
Lexem = Chr(look1)
EndIf
; ueberspringe alle White Characters und Comments
SkipWhite()
; --> in Look ist jetzt das 1. Zeichen des nächsten Token-Lexems
EndProcedure
- GetOther() 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() wird auch alle mathematischen Operatoren (sowohl einteilige als auch mehrteilige) aufnehmen.
- GetOther() sucht nach mehrteiligen mathematischen Operatoren (wie "<>", ">=", ...) und weist bei Fund den entsprechenden Token-Code (siehe oben) zu. Dabei werden dann natürlich 2 Look übersprungen.
- Durch diese Implementation der mehrteiligen Operatoren darf man einen mehrteiligen Operator auch durch ein White-Zeichen trennen, also es ist z.B. "< LEERZEICHEN >" oder auch "< ENTER >" erlaubt.
- Findet GetOther() keinen mehrteiligen Operator, dann schickt er als Token-Nr. einfach die ASCII-Zahl des Look (hier konkrekt look1) weiter, als Lexem den String von Look. Wir müssen also darauf achten, niemals einen Token-Code zu erfinden, der gleichzeitig als Other weitergeschickt werden sollte.
Warum ich hier nicht auch für alle anderen Zeichen, wie z.B. einteilige Operatoren oder die Klammer usw., eine Token-Nr. vergeben lasse? Speed und unnötige Arbeit, eine Code-Zahl ist eine Code-Zahl , auch wenn sie vorher "zufällig" die Ascii-Zahl des Look war.
Abschließend noch eine zusammenfassende Übersichtstabelle, die die Tokencodes in gruppierter n Form mit den entsprechenden Prozeduren zeigt.
Zusammenfassung der Tokencodes in TTC:
Code: Alles auswählen
=============================================================
Bearbeitungs- Token- Lexem für
prozedur Codes den Parser
notwendig?
=============================================================
Variablennamen, Prozedurenamen
reservierte Schlüsselwörter, ...
GetName() ja
bekommt TOKEN-Code 78, 'N'ame
=============================================================
Integer-Zahl oder Float-Zahl
GetNumber() ja
bekommt Token-Code 73, 'I'nteger
bekommt Token-Code 70, 'F'loat
=============================================================
String in "..."
GetString() ja
bekommt Token-Code 83, 'S'tring
=============================================================
mehrteiliger Operator wie:
<>: 117, 'u'gleich
<=: 107, 'k'leinergleich
>=: 103, 'g'rößergleich nein
jeder Operator bekommt
einen EIGENEN TOKEN-Code
GetOther() ------------------------------------------------
nichts von allem -> Rest
jedes Zeichen (es handelt sich
hier nur mehr um jeweils zum Teil
1 Zeichen) bekommt als
TOKEN-Code seinen eigenen
ASCII-Code
Anmerkung:
^^^^^^^^^
Neben den einteiligen Operatoren
(wie <,>,=,*,...),
und anderen Zeichen ({,},...),
die der Scanner nicht näher einordnen
kann, ist auch das 0-BYTE für das
Ende des Parse-Vorgangs hier als Other
dabei.
=============================================================
Erklärung zur Frage: "Lexem für den Parser notwendig?"
-------------------------------------------------------------
nein: Parser kann Token bereits aus Token-Code-Zahl erkennen,
ohne das Lexem weiter untersuchen zu müssen.
zum
Teil: manche Token, z.B. "{" als Block-Beginn-Zeichen oder
"[" als Label-Kennung für den Assembler, werden be-
nötigt werden, deshalb wird ein einfaches Other mit
seinem Character als Lexem weitergeschickt.
ja: Parser wird das Lexem weiter untersuchen bzw. be-
nützen müssen oder wollen.
=============================================================