6. Statements und Blocks: Die Struktur eines Programms
6.1. Der Plan
Steigen wir ein in die Programmierung eines Compilers für eine Hochsprache, nennen wir sie Toy-C (Spielzeug-C), der ASM-Code für eine Virtuelle Stack Machine erzeugt.
Diese Sprache wird sich an C anlehen, manche Konzepte dann aber wieder von Pure Basic und manche doch von C übernehmen. Sie ist unsere Erfindung und deshalb darf sie aussehen, wie wir wollen.
Wer wollte nicht schon immer eine eigene Programmiersprache erfinden, eine ganz persönliche?
6.2. Einleitende Worte zum Aufbau eines Programms
Ein Programm, jedes Programm, besteht aus Anweisungen (oder Befehlen).
Ein Statement ist in einer Programmiersprache eine Anweisung / ein Befehl, also z.B.
Code: Alles auswählen
while (x=2)
if (a=5)
print(y)
x=3+a
int a
Eine Anweisung / ein Befehl besteht dann aus einem oder mehreren Token-Lexemen.
Für unsere Zwecke definiere ich vereinfacht, dass aus Sicht der Schleife in Compile() (das wird unsere Start-Prozedur sein), das Statement die kleinste Struktur ist.
Die atomare Struktur des Parsers ist also die genau 1 (!) Statement.
Die weiter unten folgende Prozedur Statement() wird die Aufgabe haben, Statements anhand reservierter Schlüsselwörter/Befehlswörter (reserved key words) zu erkennen.
Falls Sie mehr über Statements als solches (meine Darstellung ist grob vereinfachend) wissen wollen: http://de.wikipedia.org/wiki/Anweisung_(Programmierung)
Stellen wir uns also folgenden Source-Code vor, die Einrückung zeigt die Blockstruktur:
Code: Alles auswählen
if (a=3)
print(a)
Was ist aber, wenn wir folgenden Source-Code haben?
Das hätten wir gemeint:
Code: Alles auswählen
if (a=3)
print("Die Zahl ist: ")
print(a)
Code: Alles auswählen
if (a=3)
print("Die Zahl ist: ")
print(a)
Wir wollten also einen Block von Statements bilden und haben das dem Compiler nicht mitgeteilt. Er hatte kein Erkennungszeichen.
In C ist das Erkennungszeichen für einen Block von Statements traditionell "{ ... }":
Code: Alles auswählen
if (a=3)
{
print("Die Zahl ist: ")
print(a)
}
Wir setzen das programmtechnisch um, indem wir später eine Prozedur Block() einführen werden!
6.3. Start-Gerüst für den Parser des 6. Kapitels
Dieses Gerüst ist aus Gründen der Übersichtlichkeit nur für die Prozeduren des 6. Kapitels gedacht.
Es wird dann in den nächsten Kapiteln ausgebaut werden.
Natürlich brauchen wir dazu unseren lexikalischen Scanner vom vorigen Kapitel, binden Sie ihn ein!
Code: Alles auswählen
; ******************************************************************************
; * PARSER (KAPITEL 6)
; * mit eingebunden muss sein:
; * 1) Module Scanner
; ******************************************************************************
DeclareModule Scanner :EndDeclareModule
Module Scanner :EndModule
; ******************************************************************************
; * Modul Parser
; ***
DeclareModule Parser
; - Public Declarations ---------------------------------------------------------
; -
Declare Compile(file_name.s) ; Compiliert ein Source-File und erzeugt eine
; Text-Datei in Toy-C-Assembler mit dem Namen
; "Source-File-Name.ta" .ta = Toy-Assembler
EndDeclareModule
Module Parser
; - Private Declarations --------------------------------------------------------
; -
Global Debug_BlockEbene ; Debug-Zwecke, später löschen
;
Declare Statement() ; erkennt eine Anweisung und führt ihre Be-
; handlungsprozedur aus
Declare Block() ; behandelt Block von Statements
; - Start, Statement, Block -----------------------------------------------------
; -
Procedure Compile(file_name.s) :EndProcedure
Procedure Statement() :EndProcedure
Procedure Block() :EndProcedure
EndModule
Parser::Compile("Source-Code.tc")
6.4. Compile()-Prozedur: Lesen des Token-Lexem-Stroms
Zunächst retten wir einmal unser Wissen vom Scanner und übertragen die Token-Lexem-Hol-Schleife (Was für ein Wort ) in eine neue Prozedur Compile(), die als Parameter den Dateinamen des Source-Files hat.
Es handelt sich dabei um die Schleife aus der Prozedur Scanner::Debug_TokenLexemStream0(), also die einfache Version ohne Behübschung, die wir ja nicht benötigen, weil den Parser z.B. #LF usw. nie erreichen. Die von mir durchgeführten leichten Veränderungen sind dem Modul-System und dem späteren Debuggen geschuldet.
Die Prozedur Compile() wird das Source-File kompilieren und eine Text-Datei in Toy-C-Assemblersprache anlegen, die denselben Namen wie das Source-File trägt, nur mit der Endung ".ta", was Toy-Assembler meint.
Code: Alles auswählen
Procedure Compile(file_name.s)
; Open .ta-File
CreateFile(1,GetFilePart(file_name, #PB_FileSystem_NoExtension)+".ta")
; Inits
Debug_BlockEbene = 0
; Starte Scanner (1. Token-Lexem liegt danach im Stream)
Scanner::Start(file_name.s)
; so lange, bis Token = 0-Byte <==== DIESER ABSCHNITT IST IM PRINZIP IST AUS Scanner::Debug_TokenLexemStream0() kopiert
While ( Scanner::Token<>0 )
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
Scanner::GetToken()
Wend
Debug "(außerhalb While-Schleife)"
; Close .ta-File
CloseFile(1)
EndProcedure
Erzeugen wir uns noch schnell ein Source-File mit dem Namen "Source-Code.tc" und starten danach unser Werk:
Code: Alles auswählen
/* Variable wird deklariert */
int Var_1
Var_1 = 273 // in Var_1 wird 273 gespeichert
/* Variable wird angezeigt */
print(Var_1) // 273 wird in Console ausgegeben
Code: Alles auswählen
120 | 'x' | INT <=== TOKEN: NAME
120 | 'x' | VAR_1 <=== TOKEN: NAME
120 | 'x' | VAR_1 <=== TOKEN: NAME
61 | '=' | = <=== TOKEN: OTHER
35 | '#' | 273 <=== TOKEN: NUM
120 | 'x' | PRINTI <=== TOKEN: NAME
40 | '(' | ( <=== TOKEN: OTHER
120 | 'x' | VAR_1 <=== TOKEN: NAME
41 | ')' | ) <=== TOKEN: OTHER
(außerhalb While-Schleife)
6.5. Ein Statement (Befehl, Anweisung)
6.5.1. Prozedur Compile() und Prozedur Statement()
Ändern wir unsere Prozedur Compile() zu:
Code: Alles auswählen
Procedure Compile(file_name.s)
; Open .ta-File
CreateFile(1,GetFilePart(file_name, #PB_FileSystem_NoExtension)+".ta")
; Inits
Debug_BlockEbene = 0
; Starte Scanner (1. Token-Lexem liegt danach im Stream)
Scanner::Start(file_name.s)
; so lange, bis Token = 0-Byte
While ( Scanner::Token<>0 )
Statement()
Wend
Debug "(außerhalb While-Schleife)"
; Close .ta-File
CloseFile(1)
EndProcedure
Die erste Version von Statement() füllen wir plump mit der Debug-Ausgabe und GetToken() und starten unser Programm, das Ergebnis im Debug-Fenster muss dasselbe wie beim vorigen Test sein. Außerdem sollte bereits vorhin ein leeres .ta-File angelegt worden sein, schauen Sie mal, ob es sich in Ihrem Projektordner befindet. (Und ja, in einer Release-Version sollte man CreateFile() testen auf Erfolg).
Code: Alles auswählen
Procedure Statement()
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
Scanner::GetToken()
EndProcedure
6.5.2. Das Erkennen von Befehlswörtern in Statement()
Jetzt wird es ernst und das Vorspiel ist vorbei.
In der Einleitung (siehe Kapitel 6.2.) habe ich bereits angesprochen, dass ein Statement in einer Programmiersprache eine Anweisung / ein Befehl ist.
Die Prozedur Statement() hat die Aufgabe, Statements anhand reservierter Schlüsselwörter/Befehlswörter (reserved key words) zu erkennen und dann entsprechend zu handeln, wie wir bereits erkannt haben.
Das machen wir am besten mit einer Select-Case-Default-Konstruktion:
Wir suchen nach Toy-C-Schlüsselwörtern und springen in die entsprechende weiterführende Prozedur (die Do-Prozeduren hinter Case, siehe Code-Box). Diese weiterführende Prozedur bearbeitet dann je nach Befehl die Token-Lexem-Abfolge weiter.
Anmerkung: Eine schnellere Variante für später statt vielen Stringvergleichen wäre sicher eine Map, die einen entsprechenden Zahlencode zurückgibt. Dieser Zahlencode wird dann in der Select-Case-Default-Konstruktion (und hier wäre dann eine Sprungtabelle noch flotter) überprüft und nicht jeder einzelne Lexem-String.
Ich möchte zum Verständnis hier eine Version von Statement() vorstellen, die dem Aussehen näher kommt, wenn der Compiler bereits weiter fortgeschritten ist. Verwenden Sie sie bitte jetzt noch nicht:
Code: Alles auswählen
Procedure Statement()
; nur für Debugzwecke (später löschen)
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
; nach Statements/Befehlen suchen
Select Scanner::Lexem
Case "INT" : Do_Int()
Case "PRINT" : Do_Print()
Case "IF" : Do_If()
Case "WHILE" : Do_While()
; ...
; ... weitere Statements
; ...
;
Default : Do_Assignment()
EndSelect
EndProcedure
Sehen wir uns jetzt an, wie wir mit Statement() beginnen wollen.
Die erste Version von Statement() :
Code: Alles auswählen
Procedure Statement()
; nur für Debugzwecke (später löschen)
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
; nach Statements/Befehlen suchen
Select Scanner::Lexem
Case "INT" ; <=== HIER IST DER FEHLER
; ...
; ... weitere Statements
; ...
;
Default : Scanner::GetToken()
EndSelect
EndProcedure
Starten Sie den Compiler (wir haben immer noch den Source-Code von weiter oben)! Der Parser bleibt bei "INT" hängen. Warum?
Weil im Case-Teil zu "INT" kein nächstes Token-Lexem-Paar geholt wird.
In Token/Lexem bleibt also auf ewig "INT" und wir kommen nie wieder aus der Schleife. Sie sehen das sehr schön in der Debug-Ausgabe.
Das führt uns zu einer wichtigen Regel!
Wir müssen immer darauf achten, dass nach der Select-Case-Default-Konstruktion ein neues, sprich das nächste, Token-Lexem-Paar in den beiden Variablen Token und Lexem liegt.
Ergibt sich das durch den Parse-Vorgang nicht automatisch am Ende jeder Do-Prozedur (das sind die hinter "Case"), dann müssen wir eben dort mittels GetToken() oder dergleichen selbst dafür sorgen (schon oft vergessen und viel geflucht deshalb).
Wenn Sie in den Case-Teil "Scanner::GetToken()" schreiben, dann ist alles wieder brauchbar und sollte wie vorhin aussehen. Ändern Sie die Zeile bitte entsprechend ab und testen Sie den Compiler:
Code: Alles auswählen
Case "INT": Scanner::GetToken()
6.6. Ein Block von Statements
In Toy-C ist liegt dann ein Block vor, wenn er mit "{" geöffnet und wieder mit "}" geschlossen wird.
In diesem Block können von 1 (was man ev. eher selten macht) bis zu beliebig viele Statements und auch weitere Blöcke sein.
Blöcke können auch in Blöcken sein, man kann sie ineinander verschachteln.
Die Sprache C macht es dem Compiler/Parser-Programmierer sehr einfach, die Blockstruktur umzusetzen, d.h. diese Art Blöcke, die C verwendet, ist leicht zu parsen (leichter als die Blöcke von z.B. Pure Basic oder Lua. Vielleicht mache ich einen extra Anhang dazu, wie man eine PB-artige Blockstruktur parst). Auch Pascal-Blöcke sind genauso aufgebaut, ersetzt Pascal doch einfach die "{ ... }" durch die Wörter "Begin ... End".
Eine Beispiel in Toy-C:
Code: Alles auswählen
if(x>0)
{
if(x=1)
{
print("Die Zahl ist ")
print("1.")
}
else if(x=2)
{
print("Die Zahl ist ")
print("2.")
}
else
{
print("Die Zahl ist weder 1 noch 2. ")
print("Sie ist unerlaubt!")
}
}
else
{
print("Die Zahl ist 0 oder ")
print("kleiner als 0. ")
print("Sie ist unerlaubt!")
}
Code: Alles auswählen
If x>0
If x=1
Print("Die Zahl ist ")
Print("1.")
ElseIf x=2
Print("Die Zahl ist ")
Print("2.")
Else
Print("Die Zahl ist weder 1 noch 2. ")
Print("Sie ist unerlaubt!")
EndIf
Else
Print("Die Zahl ist 0 oder ")
Print("kleiner als 0. ")
Print("Sie ist unerlaubt!")
EndIf
Wir erweitern Statement() zu:
Code: Alles auswählen
Procedure Statement()
; nur für Debugzwecke (später löschen)
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
; nach Statements/Befehlen suchen
Select Scanner::Lexem
Case "INT" : Scanner::GetToken() ; <=== DAS KANN AB JETZT ENTFERNT WERDEN
Case "{" : Block() ; <=== HIER NEUERUNGEN
; ...
;
Default : Scanner::GetToken()
EndSelect
EndProcedure
Die Zeile "Case "INT" ..." kann jetzt entfernt werden, weil jetzt ein anderer Case-Teil da ist und wir diese Zeile momentan nicht benötigen.
Beachten wir unbedingt, dass in der Mitte der Debug-Zeile die Variable Debug_BlockEbene eingebaut ist.
Wenn wir dann unser Werk testen, soll je nach Block-Ebene das Lexem um eine bestimmte Anzahl von Leerzeichen eingerückt werden, um gut erkennen zu können, ob die Blöcke korrekt betreten und wieder korrekt verlassen werden.
Damit ist sichergestellt, dass wir gut sehen, ob die Prozedur Block() richtig arbeitet.
Zunächst präsentiere ich eine Version von Block() ohne alle Debug-Werkzeuge, damit wir besser verstehen können, wie sie arbeitet.
Zum Testen des Parsers verwende ich dann aber die Version weiter unten.
Die Prozedur Block() ohne Debug-Werkzeuge:
Code: Alles auswählen
Procedure Block()
; --> in Token/Lexem ist jetzt ({)
; ueberlesen des ({)
Scanner::GetToken()
; solange kein (}) oder Source-Code-Ende
While ( Scanner::Token<>'}' And Scanner::Token<>0 )
Statement()
Wend
; prüfen, ob 0-Byte (=Source-Code-Ende)
; Ja? Fehler, Block nicht geschlossen mit (})
If Scanner::Token=0: Scanner::Expected("'}'"):EndIf
; ueberlesen des (})
Scanner::GetToken()
; --> in Token/Lexem ist jetzt das Token/Lexem nach (})
EndProcedure
- Statement() erkennt ein "{" und ruft Block() auf
- In Token/Lexem ist immer noch "{" ---> GetToken() überspringt das
- Token ruft wieder Statement() auf und in Statement() könnte wieder ein Block sein.
Sollte in Statement() irgendwann wieder "{" sein, dann beginnt wieder eine neue Instanz von Block().
Diese neue Instanz von Block() ruft wieder Statement() auf und in Statement() könnte wieder ein Block sein, und so weiter.
Das ist Rekursion!
- In Token/Lexem ist "}" ---> GetToken() überspringt das
Das erklärt auch, warum unsere Debug-Anzeige das schließende "}" nicht anzeigt, denn es wird hier aufgegessen. - Token/Lexem steht auf dem Token/Lexem nach "}"
- Block() endet und kehrt zurück zu Statement, das seinerseits zu Block() zurückkehren könnte und so weiter.
Legen wir uns einen neuen Source-Code an:
Code: Alles auswählen
aussen_davor_1
aussen_davor_2
{
block_1a
block_1b
{
block_2a
block_2b
}
block_1c
block_1d
}
aussen_danach_1
aussen_danach_2
Unsere rekursive Konstruktion Statement() -> Block () -> Statement() -> ... usw. ... sollte dieses File folgendermaßen durchlaufen:
Code: Alles auswählen
Statement: "aussen_davor_1"
Statement: "aussen_davor_2"
Statement: "{" --> Block() Ebene 1 --> Statement: "block_1a"
Statement: "block_1b"
Statement: "{" --> Block() Ebene 2 --> Statement: "block_2a"
Statement: "block_2b"
Statement: "block_1c" <--- Block() 2 verlassen --- While-Schleife sieht "}"
Statement: "block_1d"
Statement: "aussen_danach_1" <--- Block() 1 verlassen --- While-Schleife sieht "}"
Statement: "aussen_danach_2"
Statement: 0-Token
Schauen wir, ob das stimmt!
Fügen wir die Block()-Prozedur mit allen Debug-Werkzeugen ein.
Die Prozedur Block() mit Debug-Werkzeugen:
Code: Alles auswählen
Procedure Block()
; Debug-Zwecke, später löschen
Debug_BlockEbene+1
; ------------------------------------------------------
; --> in Token/Lexem ist jetzt ({)
; ueberlesen des ({)
Scanner::GetToken()
; solange kein (}) oder Source-Code-Ende
While ( Scanner::Token<>'}' And Scanner::Token<>0 )
Statement()
Wend
; prüfen, ob 0-Byte (=Source-Code-Ende)
; Ja? Fehler, Block nicht geschlossen mit (})
If Scanner::Token=0: Scanner::Expected("}"):EndIf
; ueberlesen des (})
Scanner::GetToken()
; --> in Token/Lexem ist jetzt das Token/Lexem nach (})
; ------------------------------------------------------
; Debug-Zwecke, später löschen
Debug_BlockEbene-1
EndProcedure
Starten Sie den Parser! Mein Debug-Fenster zeigt an (wie immer ohne meine Kommentare):
Code: Alles auswählen
120 | 'x' | AUSSEN_DAVOR_1
120 | 'x' | AUSSEN_DAVOR_2
123 | '{' | {
120 | 'x' | BLOCK_1A
120 | 'x' | BLOCK_1B
123 | '{' | {
120 | 'x' | BLOCK_2A
120 | 'x' | BLOCK_2B <=== das schließende "}" wurde von Block() gegessen
120 | 'x' | BLOCK_1C
120 | 'x' | BLOCK_1D <=== das schließende "}" wurde von Block() gegessen
120 | 'x' | AUSSEN_DANACH_1
120 | 'x' | AUSSEN_DANACH_2
(außerhalb While-Schleife)
Die Blöcke werden korrekt erkannt, sie werden korrekt betreten und korrekt wieder verlassen.
Wichtig, nicht verwechseln:
Die Einrückungen im Debug-Fenster entstehen NICHT durch die Einrückungen im Source-Code, denn die entfernt der Scanner bereits.
Die Einrückungen im Debug-Fenster entstehen durch folgende Zeile mit der Variable Debug_BlockEbene in der Prozedur Statement():
Code: Alles auswählen
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
*****************
Code: Alles auswählen
Procedure Block()
; Debug-Zwecke, später löschen
Debug_BlockEbene+1
...
; Debug-Zwecke, später löschen
Debug_BlockEbene-1
EndProcedure
Das 0-Token am Ende des Source-Codes wird auch berücksichtigt, sonst würden wir in der While-Schleife festhängen.
6.7. Der Gesamt-Code von Kapitel 6 (mit dem kompletten Scanner von Kapitel 4)
Code: Alles auswählen
; ******************************************************************************
; * PARSER (KAPITEL 6)
; * mit eingebunden muss sein:
; * 1) Module 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 GetChar() ; holt nächsten Character von *Source_Code
;
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()
; 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
Procedure Debug_TokenLexemStream0()
; 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
Procedure Debug_TokenLexemStream1()
; 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
; - Start - Prozeduren ----------------------------------------------------------
; -
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
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 dieses 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_TokenLexemStream0() ; nur zu Debug-Zwecken
;Debug_TokenLexemStream1() ; nur zu Debug-Zwecken
EndProcedure
; - Is?() - Prozeduren ----------------------------------------------------------
; -
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 IsNum(c)
If (c>='0' And c<='9')
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
; - Get - Prozeduren ------------------------------------------------------------
; -
Procedure GetChar()
Look = PeekA(*Source_Code+Source_Position) ; aktuelles Zeichen nach Look
Source_Position+1 ; auf nächstes Zeichen stellen
EndProcedure
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
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
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
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
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
; - Skip - Prozeduren -----------------------------------------------------------
; -
Procedure SkipWhite()
; solange in Look ein White
While IsWhite(Look)
; Zeilenkommentar (// ... #LF/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
; sonstige White-Zeichen überspringen
Else
GetChar()
EndIf
Wend
EndProcedure
Procedure SkipWhite1()
; 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
Procedure SkipLineComment()
; bis Zeilenende oder Ende des Source-Files (0-Byte)
While Look<>#LF And Look<>0
GetChar()
Wend
; --> Look steht auf #LF 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 steht
EndProcedure
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
; - Error - Prozeduren -----------------------------------------------------------
; -
Procedure Error(error_message.s)
MessageRequester("Fehler",error_message)
End
EndProcedure
Procedure Expected(expected_object.s)
Error(expected_object+" wird erwartet.")
EndProcedure
EndModule
; ******************************************************************************
; * Modul Parser
; ***
DeclareModule Parser
; - Public Declarations ---------------------------------------------------------
; -
Declare Compile(file_name.s) ; Compiliert ein Source-File und erzeugt eine
; Text-Datei in Toy-C-Assembler mit dem Namen
; "Source-File-Name.ta" .ta = Toy-Assembler
EndDeclareModule
Module Parser
; - Private Declarations --------------------------------------------------------
; -
Global Debug_BlockEbene ; Debug-Zwecke, später löschen
;
Declare Statement() ; erkennt eine Anweisung und führt ihre Be-
; handlungsprozedur aus
Declare Block() ; behandelt Block von Statements
; - Start, Statement, Block -----------------------------------------------------
; -
Procedure Compile(file_name.s)
; Open .ta-File
CreateFile(1,GetFilePart(file_name, #PB_FileSystem_NoExtension)+".ta")
; Inits
Debug_BlockEbene = 0
; Starte Scanner (1. Token-Lexem liegt danach im Stream)
Scanner::Start(file_name.s)
; so lange, bis Token = 0-Byte
While ( Scanner::Token<>0 )
Statement()
Wend
Debug "(außerhalb While-Schleife)"
; Close .ta-File
CloseFile(1)
EndProcedure
Procedure Statement()
; nur für Debugzwecke (später löschen)
Debug RSet(Str(Scanner::Token),3," ")+" | '"+Chr(Scanner::Token)+"' | "+Space(4*Debug_BlockEbene)+Scanner::Lexem
; nach Statements/Befehlen suchen
Select Scanner::Lexem
Case "{" : Block()
; ...
Default : Scanner::GetToken()
EndSelect
EndProcedure
Procedure Block()
; Debug-Zwecke, später löschen
Debug_BlockEbene+1
; ------------------------------------------------------
; --> in Token/Lexem ist jetzt ({)
; ueberlesen des ({)
Scanner::GetToken()
; solange kein (}) oder Source-Code-Ende
While ( Scanner::Token<>'}' And Scanner::Token<>0 )
Statement()
Wend
; prüfen, ob 0-Byte (=Source-Code-Ende)
; Ja? Fehler, Block nicht geschlossen mit (})
If Scanner::Token=0: Scanner::Expected("'}'"):EndIf
; ueberlesen des (})
Scanner::GetToken()
; --> in Token/Lexem ist jetzt das Token/Lexem nach (})
; ------------------------------------------------------
; Debug-Zwecke, später löschen
Debug_BlockEbene-1
EndProcedure
EndModule
Parser::Compile("Source-Code.tc")