Tutorial - Compiler und Virtual Machine (nicht beschreiben)

Hier kannst du häufig gestellte Fragen/Antworten und Tutorials lesen und schreiben.
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

4.11. Der Scanner TTCS von Kapitel 4

Code: Alles auswählen

; *******************************************************************
; * Scanner Version TTCS 1.0                                        *
; *                                                                 *
; *   'N'ame, 'I'nteger, 'F'loat, ".." -> 'S'string                 *
; *   <> -> 'u', >= -> 'g', <= -> 'k'                               *
; *   Other char -> Token = char, Lexem = chr(char)                 *
; *                                                                 *
; *   ist das Include-File: Scanner10.pbi                           *
; *                         ^^^^^^^^^^^^^                           *
; *******************************************************************

DeclareModule Scanner    
; -
; - Public Declarations ---------------------------------------------
; -  

    ; --- public globale Variablen ---

      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 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)      
    
    ; --- 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 --------------------------------------------
; -
    
  ; --- globale Variablen ---

    Global *Source_Code        ; Source Code 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- & Stop-Prozedur ------------------------------------------
; -
  Procedure Start(file_name.s="") 
  
    ; laden des Source-Files in Memory-Bereich *Source_Code
    ; Wenn Argument leer, dann kein neues Laden!  
      If file_name<>"":Load(file_name.s):EndIf
      
    ; '*Source_Pos' auf 1. Zeichen stellen
      *Source_Pos = *Source_Code      
      
    ; LineNr auf 1. Zeile stellen
      LineNr=1
      
    ; das erste aktuelle Token-Lexem-Paar holen  
      Lexem = ""   ; falls kein Neuladen erfolgt ist
      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    
  
  EndProcedure  
  Procedure Stop()
      FreeMemory(*Source_Code)  
  EndProcedure
  ;
  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  

; - Lade Source-File ------------------------------------------------
; -
  Procedure Load(file_name.s)
      
    ; lade Source-File mit Filename   
      file = ReadFile(#PB_Any,file_name)   
      If Not file
          Error("Scanner: 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
  
; - Get - Prozeduren ------------------------------------------------
; -
  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     
  Procedure GetToken()

    ; --> in Look ist das 1. Zeichen dieses Token-Lexems
        
    ; Entscheide, welcher Token-Typ vorliegt und verzweige entsprechend    
      If      IsNumber1(Look): GetNumber()
      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("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
  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 
  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   
  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

; - Skip - Prozeduren -----------------------------------------------
; -
  Procedure SkipWhite()
   
    ; solange in Look ein White
      While IsWhite(Look)
          
          ; Zeichen hinter Look holen
          ; *Source_Pos steht nach letztem
          ; GetChar() schon richtig darauf
            next_Look = *Source_Pos\a  

          ; Zeilenkommentar '// ... #eol/0-Byte'
            If Look='/' And next_Look='/'
                SkipLineComment()
                   
          ; Blockkommentar '/* ... */'
            ElseIf Look='/' And next_Look='*'
                SkipBlockComment()

          ; einfaches '/' als nicht-White im Stream belassen
            ElseIf Look='/'
                ProcedureReturn

          ; sonstige White-Zeichen überspringen
            Else
                GetChar()
            EndIf

      Wend
	    
  EndProcedure
  Procedure SkipLineComment()

    ; bis Zeilenende oder Ende des Source-Files (0-Byte)
      While ( Look<>'¶' And Look<>0 )
          GetChar()
      Wend 
        
    ; --> Look steht auf #eol 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 #eol steht  
        
  EndProcedure
  Procedure SkipBlockComment()

    ; '/' überspringen
      GetChar() 
    
    ; solange bis '*/'  
      Repeat      
            
          ; Zeichen holen, bei Ersteintritt '*' überspringen
            GetChar()
          
          ; Zeichen hinter Look holen
          ; *Source_Pos steht nach letztem
          ; GetChar() schon richtig darauf
            next_Look = *Source_Pos\a  

          ; verschachtelte Block-Kommentare ermöglichen
            If Look='/' And next_Look='*'
                SkipBlockComment()
            EndIf
            
          ; auf 0-Byte achten -> sofort raus
            If Look=0: ProcedureReturn: EndIf
        
      Until Look='*' And next_Look='/'
     
    ; '*/' 2-mal überspringen  
      GetChar()
      GetChar() 
      
    ; --> Look steht auf 1. Zeichen nach '*/'   
  
  EndProcedure

; - Error - Prozeduren ----------------------------------------------
; -
  Procedure Error(fehlertext.s)
  
  ; Fehlertext in Message Requester ausgeben.
    MessageRequester("Scanner Error",
                      fehlertext+#CRLF$+#CRLF$+
                      "Token-Zeichen: "+Chr(Token)+#CRLF$+
                      "Token-Code: "+Token+#CRLF$+
                      "Lexem: "+Lexem+#CRLF$+
                      "Zeile: "+LineNr) 
  
  ; Scan- und somit Compilevorgang brutal abbrechen 
    End  
      
  EndProcedure
  Procedure Expected(expected_object.s)
      Error(expected_object+" wird erwartet.")  
  EndProcedure

EndModule


; *******************************************************************
; * Debug-Prozeduren (außerhalb der Module)                         *
; *******************************************************************
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
Procedure Debug_GetToken()  
   
  Scanner::Start("source-code.ttcs")

  While ( Scanner::Token <> 0 )
        Debug " | "+Chr(Scanner::Token)+              ; CHAR des Token-Codes
              " | "+RSet(Str(Scanner::Token),3," ")+  ; Code-Nr des Tokens
              " | "+Scanner::Lexem                    ; Lexem
        Scanner::GetToken()  
  Wend   
  Debug "0-Token: außerhalb der While-Schleife"

EndProcedure


; Aufruf je nach Ziel:

; Debug_GetChar()
Debug_GetToken()
Zuletzt geändert von puretom am 23.11.2013 19:47, insgesamt 22-mal geändert.
Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

5. Kurze Einführung in Assembler/Maschinensprache



5.1. Vorbemerkungen

Diese Einführung stellt noch nicht die genaue Darstellung der VM dar.
Sie ist gedacht, um dem Leser einen Leitfaden zu geben, damit er den Vorgang des Komplilierens versteht, vor allem den Code des mathematischen Parsers.



5.2. Was ist Maschinensprache

Maschinensprache ist die Sprache, die die CPU bzw. die VM (wenn wir eine Virtual Machine haben) wirklich versteht.
Die einzelnen Opcodes (=Operationscodes, Befehle) tun zumeist recht elementare Dinge, wie z.B. addieren, subtrahieren, springen, usw.


Ein Assemblerprogramm besteht oft aus 3 Elementen:
  • Zeilen mit Opcodes, das sind einzelne Befehle:

    Code: Alles auswählen

            ipushc    // Opcode "ipushc" mit Parameter "4"
            ipushc    // Opcode "ipushc" mit Parameter "3"
            iadd      // Opcode "iadd"   ohne Parameter 
    
  • Zeilen mit Labels, das sind Stellen/Positionen im Programm, man nennt sie auch Sprungmarken, weil sie mit Sprungbefehlen angesprungen werden können:

    Code: Alles auswählen

            ipushc 40     
            ipushc 30         
            ipushc 1    // 1 liegt am oben am Stack
            JZ WEITER   // Jump if Zero -> wenn Stack=0, 
                        // dann springe zu Label "WEITER"
                        // (ist er hier aber nicht, oberstes Element = 1)
            isub
    
    [WEITER]            // Label (viele ASM-Sprachen verwenden Label wie 
                        // PB mit ":" dahinter, unsere schauen so aus)
            iadd     
    
  • Zeilen mit Pseudobefehlen, das sind Anweisungen an den Assembler:



5.3. Was ist ein Stack (Stapelspeicher)?

Ein Stack ist ein Speicherbereich, der wie ein Stapel oder Keller aussieht.

Was man oben als Letztes drauflegt, muss man auch wieder als Erstes nehmen.
Dieses Prinzip nennt man auch Last-In-First-Out-Prinzip (LIFO), also "Als Letztes rein, als Erstes raus" genannt.

Zum Arbeiten mit dem Stack werden zumeist folgende Operationen zur Verfügung gestellt:
  • push (schieben, stoßen) legt das Objekt oben auf den Stapel.
  • pull (ziehen) liefert das oberste Objekt und entfernt es vom Stapel.
    Diese Aktion wird bei vielen Prozessoren auch als pop bezeichnet.
  • peek (nachsehen) liefert das oberste Objekt, ohne es zu entfernen.
(Zitat vgl: http://de.wikipedia.org/wiki/Stapelspei ... onsprinzip)


Ein Beispiel mit dem Stack:

Code: Alles auswählen

push 6  push 1  push 7  pull    pull    peek    push 2  push 3

                +---+                                   +---+
                + 7 +                                   + 3 +
        +---+   +---+   +---+                   +---+   +---+
        + 1 +   + 1 +   + 1 +                   + 2 +   + 2 +
+---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+
+ 6 +   + 6 +   + 6 +   + 6 +   + 6 +   + 6 +   + 6 +   + 6 +
+---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+


5.4. Auswerten von mathematischen Ausdrücken in Assembler

Zum Auswerten nicht nur, aber besonders von mathematischen Ausdrücken benötigt man unbedingt einen Stack.
Jeder weiß, dass man in einer mathematischen Rechnung z.B. die Punktrechnungen vor den Strichrechnungen berechnet und dass z.B. Klammerausdrücke überhaupt die höchste Prioriät haben.

Damit, wie man das in ASM kompiliert, beschäftige ich mich im nächsten Kapitel, jetzt kümmern wir uns darum, dass wir das von Hand in ASM können, um zu verstehen, was der Compiler übersetzt.



5.4.1. Berechnungen in einer Stack Machine

Unsere Virtual Machine soll eine Stack Machine werden.
Stack Code ist am leichtesten durch einen Compiler zu erzeugen.

Nehmen wir folgende Rechnung:

Code: Alles auswählen

1+2*3
Wir sehen auf den ersten Blick, dass wir 2*3 zuerst auswerten müssen.
Versuchen wir einen intuitiv verständlichen Stack Code dafür zu schreiben.

Code: Alles auswählen

        ipushc 1
        ipushc 2
        ipushc 3
        imul
        iadd
Mit einer Grafik wird alles klar:

Code: Alles auswählen

        push 1  push 2  push 3   mul             add  
                                 2*3=6 ---.      1+6=7 ---.
                        +---+     ^       |       ^       |             
                        + 3 +     |       v       |       |              
                +---+   +---+     |     +---+     |       |
                + 2 +   + 2 +     |     + 6 +     |       v 
        +---+   +---+   +---+   +---+   +---+     |     +---+                
        + 1 +   + 1 +   + 1 +   + 1 +   + 1 +     |     + 7 +                
+---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+                
Ich möchte für uns in Worte fassen, was hier passiert ist:
  • Stack ist leer (bzw. in einem bestimmten Ur-Zustand)
  • push 1: auf dem Stack liegt 1 (rechts hier ist oben beim Stack)
  • push 2: auf dem Stack liegt 1,2
  • push 3: auf dem Stack liegt 1,2,3
  • mul:
    • entfernt die beiden obersten Elemente vom Stack: 1
    • multipliziert sie --> 6
    • pusht das Ergebnis auf den Stack: 1,6
  • add:
    • entfernt die beiden obersten Elemente vom Stack: -
    • addiert sie --> 7
    • pusht das Ergebnis auf den Stack: 7
  • Stack ist nicht leer, d.h es ist 1 Element mehr als im Ur-Zustand am Stack, nämlich das Ergebnis der Rechnung "1+2*3".
Man sieht, dass das Erzeugen von Stack-Code recht einfach sein wird, denn der Stack ist implizit immer vorhanden.

Anders gesagt: Um die Stackverwaltung braucht sich der Compiler keine Sorgen machen, darum kümmert sich die Zielmaschine (hier unsere Stack-VM), auf dem das Programm dann abläuft, durch die Opcodes von selbst.

Die meisten echten CPUs sind allerdings keine Stack Machines, Stack Machines sind ein Domäne der Virtuellen Maschinen (der Klassiker: Die Java Virtual Machine).



5.4.2. Berechnungen in einer Registermaschine mit wenig Registern

Die meisten Silikon-CPUs (also die echten) sind Register-Maschinen, d.h. sie haben mehr oder weniger in der CPU liegende schnelle Speicherstellen.

Nehmen wir den Fall, wir kompilieren auf eine CPU, die wenige Register zur Verfügung hat.

Stellen wir uns vor, wir hätten 2 Register zur Verfügung: EAX, EBX (nur "zufällig" ähnlich dem x86).
Und natürlich haben wir auch einen Stack. Eine Registermaschine kann aber nur IN den Registern rechnen, anders als die Stack Machine, die direkt vom und zum Stack rechnet. Auch Werte können wir nur mit den Registern laden.

D.h. wir müssen bei einer Register Machine unseren Stack von Hand bedienen und die Register, in denen wir rechnen, von Hand laden (pop) und von Hand auf den Stack sichern (push).

Nehmen wir wieder die Rechnung:

Code: Alles auswählen

1+2*3
Wir sehen auch jetzt wieder auf den ersten Blick, dass wir 2*3 zuerst auswerten müssen.

Code: Alles auswählen

mov   EAX, 1    // lade Register EAX mit 1, da wir es laufend benötigen, müssen wir den Inhalt immer wieder sichern
push  EAX       // push 1 (= sichern des Inhalts auf den Stack)
mov   EAX, 2    // lade Register EAX mit 2
push  EAX       // push 2
mov   EAX, 3    // lade Register EAX mit 3
push  EAX       // push 3
pop   EAX       // pop Stack nach EAX = 3, Rechenregister laden, wir können nur IN den Registern rechnen
pop   EBX       // pop Stack nach EBX = 2, Rechenregister laden, wir können nur IN den Registern rechnen
mul   EAX, EBX  // EAX(6) = EAX(3) * EBX(2)
push  EAX       // push 6
pop   EAX       // pop Stack nach EAX = 6, Rechenregister laden, wir können nur IN den Registern rechnen
pop   EBX       // pop Stack nach EBX = 1, Rechenregister laden, wir können nur IN den Registern rechnen
add   EAX, EBX  // EAX(7) = EAX(6) + EBX(1)
push  EAX       // push 7 (wenn wir das Ergebnis am Stack wollen)
Wirklicher ASM-Code schaut noch ein bisschen anders aus, etwas effizienter, denn das hier ist ein bisschen tollpatschig.
Denken wir uns die unnötigen "push EAX -> pop EAX" einfach weg, dann ist die Sache realistisch, aber auch verwirrender. Deshalb für unsere Zwecke, gut so.

Ich denke aber, der Unterschied zur Stack Machine ist klar:
  • Stack von Hand pushen und popen/pullen
  • Rechnen und laden nur in Registern

Im Internet gibt es unzählige Diskussionen, ob man seine VM nun als Stack Machine (wie die Java) oder als Register Machine (wie Googles Dalvik) programmiert.
Instinktiv und auf den ersten Blick wirkt Register Code länger und umständlicher, aber das ist nur ein Scheinargument. Ich habe im Internet viel darüber gefunden, dass auch Register-VMs hocheffizient sind. Weiter möchte ich die Diskussion gar nicht vertiefen. Mir ging es nur um eine kurze Einführung, um den Unterschied zu zeigen.

Eins ist aber klar:
Beim Kompilieren und zum Lernen des Compilerbaus ist eine Stack Machine leichter und ideal.
Wer die Stack Machine kann, der kann auch (auf den ersten Blick umständlicheren) Register Code schreiben.
Zuletzt geändert von puretom am 05.11.2013 16:22, insgesamt 22-mal geändert.
Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

6. Compiler TTCC und Parser TTCP Version 0.5



6.1. Wichige Überlegungen, bevor wir starten


6.1.1. Umfang der Sprache TTC 0.5

Wir werden jetzt ziemlich schnell eine kleine Tiny Scriptsprache erarbeiten. Der Compiler wird TTCC heißen und den Parser TTCP aufrufen, der gemeinsam mit dem Scanner TTCS das .ttcs-File (=Tiny Toy C Source) zu einem .ttca-File (= Tiny Toy C Assembler) kompilieren wird.


In der Version 0.5 werde ich TTC auf ein Niveau zurückbauen, das leicht nachvollziehbar sein wird:
  • Aus diesem Grund verzichten wir vor allem auf die Vielfalt von Variablentypen, auf einen voll ausgebauten mathematischen Ausdrucksparser sowie ausgefeilte Input-Funktionen und Output-Prozeduren.
    Das werden wir alles auf simplesten Niveau halten.
  • Dafür gibt es aber im Gegenzug dazu ein überrschend reichhaltiges Set von Schleifen und Verzweigungen.
    Diese sind nämlich - entgegen ersten Annahmen - sehr leicht zu implementieren.
Am Ende von Teil Zwei sollte jeder verstanden haben, dass ein einfacher Compiler kein Mystikum ist.

Die Herausforderungen kommen erst später.
Doch sogar mit TTC 0.5 und einigen kleinen Erweiterungen (dann wäre es wohl so etwas wie TTC 0.8 ;-) ) lässt sich bereits ein kleines Skriptsystem betreiben, das man für ein kleines Spiel, das man selbst geschrieben hat, sinnvoll einsetzen kann.



Unsere Prozeduren, die wir in diesem Kapitel erarbeiten werden, gehören in das Modul Parser.

Ich werde die Prozeduren so anordnen, wie ich sie selbst als in ihrer natürlichen Reihenfolge empfinde.
Völlig ungeordnet lege ich mir eine "Halde" von Declares an, und zwar ausschließlich nur dann, wenn der Compiler meckert, dass die Prozedur unbekannt sei. Ich nutze das Editor-Feature von Pure Basic, dass man mit ";{ ... ;}" Code-Teile ein- und ausklappen kann, um für mehr Übersichtlichkeit zu sorgen.


Das TTC-Compilersystem:

Code: Alles auswählen


 ***********************************************
 *                    Token                    *
 * .------------.     Lexem     .------------. *
 * |  lexikal.  |-------------->|            | *
 * |            |               |   Parser   | *
 * |  Scanner   |<--------------|            | *
 * '------------'   GetToken()  '------------' *
 *    ^                               |        *
 *    |                               |        *
 *    |                               v        *
 *  SOURCE-                       ASSEMBLER-   *
 *   CODE                            FILE      *
 *  .ttcs                           .ttca      *
 *              .--------------.               *
 ***************| TTC-COMPILER |****************
                |    SYSTEM    |
                |     TTCC     |
                '--------------'  


Sprachspezifikationen von TTC 0.5:

Code: Alles auswählen


 (1)   VARIABLEN:     * globale Integer
 
                      * Müssen vor 1. Verwendung mit
                        Typ deklariert werden.    


 (2)   MATHEMATIK:    * Matheparser mit Klammern, aber
                        keine neg. Zahlen und keine 
                        automatische Punkt-vor-Strich-
                        Rechnung


 (3)   VERZWEIGUNG:     if - else


 (4)   SCHLEIFEN:       do - while 
                        do - until 
                        do - (forever)  
                        while 

                        break      
                        continue   
                        

 (5)   SPRÜNGE:         goto           
                        gosub - return 


 (6)   EIN-/ AUS- 
       GABE:            input, print (function)
                        cin, cout (function, undokumentiert)


 (7)   SONSTIGES:       end


 (8)   SYNTAX:          C-ähnlich stark gemischt
                        mit Basic-Elementen.
                        Blockstruktur durch { ... }.
                        TTC ist also eine "Curly-bracket 
                        or brace language".


          


6.1.2. Das Modul-, Include-File- und Aufrufkonzept

Der Compiler ist ein Modul, in dem derzeit nur 1 Startprozedur ist, diese ruft dann alle aufeinanderfolgenden Schritte des Gesamtprozesses, der am Ende zu einem laufenden TTC-Programm führt, auf.
Außerdem habe ich alles in Include-Dateien zur besseren Übersicht für mich beim Programmieren aufgeteilt (siehe Code-Teil).

Code: Alles auswählen

; *******************************************************************
; * Compiler Version TTCC                                           *
; *******************************************************************
DeclareModule TTCCompiler05
; =
; = Public Declarations =============================================
; =  
    ; --- Start-Prozedur ---  
      Declare Start(file_name.s)

EndDeclareModule
Module TTCCompiler05

  Procedure Start(file_name.s)
  
    ; Schritte zum fertigen TTC-Executable
      TTCParser05::Start(file_name)
     ;TTCAssembler05::Start(file_name)
     ;TTCVirtualMachine05::Start(file_name)
      
  EndProcedure
  
EndModule
  • Wir sollten zunächst die auf den Parseraufruf folgenden Schritte (wie hier) auskommentieren.
Wir starten unsere Tests ab jetzt grundsätzlich mit:

Code: Alles auswählen


TTCCompiler05::Start("source-code.ttcs") ; .ttcs = Tiny Toy C Source

  • Im Modul TTCCompiler steuern wir durch Auskommentieren, welche Schritte durchgeführt werden sollen.
    Wenn wir alles "einkommentieren", dann läuft der TTC-Source-Code unmittelbar nach dem Kompilieren in der VM, wenn wir den fertigen Code aus dem Codeteil von Teil Zwei vor dem Lesen nach PB übertragen haben.


6.1.3. Fehlerbehandlung

Jeder Compiler sollte mehr oder weniger sinnvolle Fehlermeldungen ausgeben, wenn der Programmierer Unsinn eingetippt hat.

Gleich vorweg, um niemanden zu enttäuschen.
Fehlerbehandlung wäre ein eigenes Tutorial und wird in nur wenigen Fachbüchern wirklich behandelt.

Unsere Fehlerbehandlung in TTC existiert zwar (wir haben im Scanner damit begonnen), doch sie gibt zum Teil falsche Zeilennummern aus (vor allem wenn der Fehler in der letzten beschriebenen Zeile ist und dahinter noch Leerzeilen weitergehen - das liegt an SkipWhite) und zum Teil auch falsche Fehlergründe (In Assignment wäre es wohl sinnvoller nicht eine nicht deklarierten Variable zu kritisieren, sondern einen unbekannten Befehl).
Die Fehler werden jedoch zumeist erkannt.
Außerdem stoppt der Compiler ab dem ersten erkannten Fehler, es wird keine Warnliste erstellt und nach der fehlerhaften Stelle weitergearbeitet. Das macht auch der PB-Compiler so, doch viele moderne Compiler machen einfach weiter und geben am Ende eine Liste der Fehler aus.

All dieses Verhalten lässt sich nur mit je nach Fall mehr oder weniger großem Zusatzaufwand ändern und das mögen andere tun, die sich in dieses spezielle und natürlich auch wichtige Unterthema der Compilerprogrammierung eingearbeitet haben. Ich werde nur Dinge einfügen, die ich im Vorbeigehen ändern kann, wenn sie nicht die Klarheit des Codes zu stark beeinträchtigen.


Das bringt uns zu unseren Fehlerprozeduren.
Grundsätzlich habe ich mich dazu entschieden, die Fehlermeldungen von einem zentralen Ort aus auszugeben. Deshalb benutze ich enumerierte Konstanten, die den Fehler bereits verständlich beschreiben, um die Fehlermeldung anzusteuern. Für Lern- und Debugzwecke geben wir außerdem das Modul (also Scanner oder Parser), das den Fehler registriert hat, und die Parse-Prozedur als Ort an, an dem der Fehler aufgetreten ist. Lexem, Token und Zeilennummer dürfen natürlich auch nicht fehlen.

Ich möchte in diesem Zusammenhang auf die Prozeduren TestToken(), EmitError() und Error() sowie die enumerierten Fehler-Konstanten im privaten Deklarationsteil verweisen, die wir im Code-Teil nachlesen können.




6.2. Das Statement


Ich wähle diesmal im Vergleich zum anderen Tutorial den direkten - man muss es fast so sagen - Hineinsturz in das Thema.


Wir werden zunächst versuchen, folgenden Source-Code zu kompilieren, speichern wir unser kleines TinyToyC-Programm unter "source-code.ttcs":

Code: Alles auswählen

/* Variablen-Deklarationen */
 int a; int b; int c
Hier sollten 3 Integer-Variablen deklariert werden.



Wie immer benötigen wir eine Start-Prozedur mit dem Namen Start():

Code: Alles auswählen

  Procedure Start(file_name.s)

    ; Open .ttca-File
      TTCA_File = CreateFile(#PB_Any,GetFilePart(file_name, 
                                 #PB_FileSystem_NoExtension)+".ttca")
      If Not TTCA_File
         EmitError("Start()","Parser: Assembler-File konnte nicht "+
                   "erstellt werden.")
      EndIf      

    ; Starte Scanner-Modul (1. Token-Lexem liegt danach im Stream)
      Scanner::Start(file_name)            
      
    ; Inits  
      ClearMap(GlobalVar())        
      GlobalIntegerIndex = 0     
      GlobalFloatIndex   = 0
      GlobalStringIndex  = 0
      LabelNr            = 1
      ClearList(LabelStack())
    
    ; Prolog vorbereiten
      EmitX(Space(50))  ; reserviert für setGlobalIntSize usw.   
      EmitX("// Beginn des Programms")    
      EmitX()        
      
    ; so lange, bis Token = 0-Byte
      Debug "========================================"
      Debug " PARSER - START"
      
      While ( Scanner::Token<>0 )
        Statement()
      Wend
      
      Debug " PARSER - STOP"
      Debug "----------------------------------------"                       
             
    ; Prolog schreiben
      FileSeek(TTCA_File,0)
      Emit(0,"setGlobalIntSize ", Str(GlobalIntegerIndex))
      
    ; Stoppe Scanner-Modul (free Memory-Bereich im Scanner)
      Scanner::Stop()

    ; Close .ttca-File
      CloseFile(TTCA_File)    
      
    ; DEBUG
      Debug "----------------------------------------"
      Debug " GLOBALE VARIABLEN IN 'GLOBALVAR()'"
      Debug "----------------------------------------"     
      Debug "Typ | Index | Name"
      Debug "----------------------------------------"
      ForEach GlobalVar()      
          Debug RSet("'"+Chr(GlobalVar()\typ)+"'",3," ")+
                " | "+RSet(Str(GlobalVar()\index),5," ")+
                " | "+MapKey(GlobalVar())  
      Next            
    
    ; Free
      FreeList(LabelStack())
      FreeMap(GlobalVar())

  EndProcedure
  • Die Parser-Start-Prozedur legt zunächst ein Tiny-Toy-C-Assembler-File an und gibt eine Fehlermeldung bei Scheitern aus.
  • Der Scanner wird gestartet: incl. Laden des Source-Files sowie einladen des 1. Token-Lexem-Paars.
    Wir erinnern uns: Wenn wir hier durch die Formulierung "Scanner::Start()" keinen Parameter angeben würden, dann würde der Scanner gestartet, aber der Memory-Bereich *Source_Code (im Modul Scanner) nicht neu beladen werden. Der Assembler wird dieses Feature nutzen und auch der Parser, wenn wir vom Programmierer definierte Prozeduren und Funktionen einführen.
  • Die Map und die Index-Nummern für die globalen TTC-Variablen (obwohl wir zunächst nur Integer-Zahlen haben, belasse ich die Inits, wie sie sind) und einiges mehr werden initialisiert.
  • Prolog vorbereiten: Es wird im Text Platz mit Leerzeichen geschaffen, um am Ende die Anzahl der globalen und lokalen Variablen eintragen zu können. Denn diese Anzahl ist statisch, d.h. nach dem Compile-Vorgang bekannt.
  • Emit...(): Sind Prozeduren, die uns eine raffiniert formatierte Ausgabe ins Assembler-File ermöglichen - sie zu verstehen ist nicht Teil des Parsens, wir hätten den ASM-Code auch unformatiert hässlich ausgeben können (siehe Code-Teil).
  • Eine While-Schleife geht TTC-Befehl für TTC-Befehl (=Statement) durch, bis eben ein 0-Token als Terminator (=Programmende) auftritt.
  • Prolog schreiben: Der Schreib-Zeiger des ASM-Files wird auf 0 an den Anfang gesetzt und die Werte der Variablenanzahl in den Platz, den wir gelassen haben, eingetragen.
  • Debug Variablen: Eine Tabelle zeigt uns die im TTC-Programm verwendeten globalen Variablen an.


Die Prozedur Statement() brauchen wir natürlich auch, sonst funktioniert die obige Startprozedur natürlich nicht. Diese Prozedur ist der ZENTRALE VERTEILER des Parsers.
Jeder Befehl - ALLES - nimmt von hier seinen Ausgang.

Code: Alles auswählen

  Procedure Statement()
  
  ; nur für Debugzwecke (später löschen)    
    Debug " | "+Chr(Scanner::Token)+              ; CHAR des Token-Codes
          " | "+RSet(Str(Scanner::Token),3," ")+  ; Code-Nr des Tokens
          " | "+Scanner::Lexem                    ; Lexem

  ; // je nach Statement Aktionen setzen //
    Select Scanner::Lexem
        
  ; Statements            
   Case "int"   : Declare_Statement('i') ;<== BEACHTE: WIR ÜBERGEBEN 
   ; ...                                               DEN TYP IM PARAMETER    
   Default      : Scanner::GetToken()
         
  EndSelect       
  
  EndProcedure  
  • Debug-Ausgabe: Gibt wieder die einzelnen Token-Codes, die Token-Codes als Character und die Lexeme aus.
  • Select-Case: Das ist der Kern der Prozedur. Je nach TTC-Schlüsselwort wird in eine Prozedur verzweigt, die alles weitere parst und Assemblercode ausgibt.
  • Beachte: Wir übergeben der Prozedur Declare_Statement() den Typ der zu deklarierenden Variable, dadurch können wir später diese Prozedur auch z.B. bei 's' oder 'f' verwenden.
  • In Default steht nur derzeit noch einfach ein GetToken(), damit der Parser nicht bei einem unbekannten Schlüsselwort abbricht.
Die Prozedur Declare_Statement() wird im nächsten Absatz nachgeliefert.




6.3. Deklarieren globaler Variablen

Ich werde nach Vorbild der Java-Bytecode-Assembler bereits den Compiler/Parser die Variablen verwalten lassen.
Somit muss der Parser die Variablennamen und den Variablentyp speichern.
Die Variable bekommt eine Index-Zahl, unter der sie später in der Virtuellen Maschine, also wenn das Programm "läuft", im Variablen-Array gefunden werden kann. Diesen Index weist in unserem Anwendungsbeispiel der Parser bereits zu.

Wir erlauben für TTC zunächst ausschließlich Integervariablen (ich habe die anderen Typen aber gleich in den Deklarationen belassen), deren Typ-Kennzahl sinnvollerweise mit 'i' (=105) sehr einfach zu merken ist und nun wirklich keine eigene enumerierte Konstante braucht.

Am schnellsten ist hier meiner Meinung nach eine Map, die die Variablennamen als Key enthält und deshalb werden wir das genau so programmieren.


Ein Ausschnitt aus dem Declare-Teil des Parsers, um sich einen Überblick zu verschaffen:

Code: Alles auswählen

  ; --- Handle des Assembler-Files ---    
    Global TTCA_File 
    
  ; --- TTC-Globals verwalten ---
    Structure gv                    ; Struktur einer globalen
        Typ.i                       ; Variable vom Typ 'i','f','s'
        Index.i                      
    EndStructure
    Global NewMap GlobalVar.gv()    ; enthält Variablenname + Typ
                                    ; + Index                                    
    Global GlobalIntegerIndex      
    Global GlobalFloatIndex
    Global GlobalStringIndex
    
  ; --- TTC-Labels verwalten ---
    Global LabelNr                  ; Label-Nummer
    
  ; --- break, continue verwalten ---    
    Structure ls
        Continue_.i
        Break_.i
    EndStructure
    Global NewList LabelStack.ls()        
     
  ; --- Expression-Typ ---  
    Global ExpTyp          ; Typ der aktuellen Expression (=Ausdruck)
  • Hier haben wir vorausblickend bereits einiges mehr eingebaut, als wir zunächst in TTC 0.5 benötigen werden.


Die Prozedur NewGlobalVar():

Code: Alles auswählen

  Procedure NewGlobalVar(var_name.s,var_typ) ; legt neue Variable an
    
  ; Variable bereits vorhanden? -> Fehler!
    If FindMapElement(GlobalVar(),var_name)                   
      Error("NewGlobalVar()",#Global_bereits_deklariert)                          
    EndIf
    
  ; lege neue Variable an und trage Typ und Index in Map ein
  ; erhöhe nach Eintrag den Index für das nächste Mal

    GlobalVar(var_name)\Typ=var_typ
    
    If     var_typ='i': GlobalVar(var_name)\Index=GlobalIntegerIndex
                        GlobalIntegerIndex+1 
    ElseIf var_typ='s': GlobalVar(var_name)\Index=GlobalStringIndex
                        GlobalStringIndex+1   
    ElseIf var_typ='f': GlobalVar(var_name)\Index=GlobalFloatIndex          
                        GlobalFloatIndex+1                    
    EndIf
        
  EndProcedure
  • Ich habe auch hier der Einfachheit halber gleich die Typen float und String im der Prozedur belassen, weil sie uns nicht stören und wir sie ohnehin später benötigen werden.
  • Die Variable darf nicht schon unter demselben Namen deklariert worden sein.
    Für die Fehlermeldung gibt es eine Parser-eigene Error-Prozedur (nicht mit der Scanner::Error() verwechseln).
  • Bei der neuen Variable merkt sich der Parser folgende Angaben: Name (Mapkey), Typ ('i', 'f' oder 's'), Index (je nach Typ unterschiedlich).
  • Es ist also nicht erlaubt, eine String- und eine Integer-Variable gleich zu benennen.
  • Obwohl Strings, Integers und Floats in derselben Map sind (damit man sie beim Testen schneller findet und nicht mehrere Maps abklappern muss), haben sie jeweils getrennte Index-Zahlen.


Die Prozedur IsGlobalVar():

Code: Alles auswählen

  Procedure IsGlobalVar(var_name.s) ; Var. deklariert? Nein -> Fehler
  
  ; falls Variable nicht deklariert wurde --> Fehler
    If Not FindMapElement(GlobalVar(),var_name)             
      Error("IsGlobalVar()",#Global_noch_nicht_deklariert)                           
    EndIf  
       
  EndProcedure
  • Diese Prozedur schaut nach, ob eine Variable überhaupt jemals deklariert worden ist.
  • Mit ihr muss dann später bei jeder Verwendung eines Variablennamens getestet werden.

Jetzt brauchen wir nur mehr die Prozedur Declare_Statement() eine Variable deklarieren lassen:

Code: Alles auswählen

  Procedure Declare_Statement(var_typ) ; deklariert Variable mit typ 
    
  ; 'int', 'float' oder 'string' überspringen
    Scanner::GetName()
       
  ; Variable anlegen, Variablen-Name ist in Lexem
  ; Variablen-Typ wird weitergereicht
    NewGlobalVar(Scanner::Lexem,var_typ)
      
  ; am Ende nächstes Token-Lexem-Paar holen    
    Scanner::GetToken()  

  EndProcedure
  1. Das hier ist eine typische Parse-Prozedur. An ihr können wir typische Merkmale des Parsens durchbesprechen:
  2. Zunächst befindet sich noch das Lexem in Lexem, das in Statement den Aufruf aus der Select-Case-Konstruktion bewirkt hat.
    Das kann hier nur "int" gewesen sein. Wir überspringen das, indem wir ein neues Token-Lexem-Paar vom Scanner anfordern.
    Wir wählen hier bewusst GetName(), denn wir erwarten ja einen Variablennamen.
  3. Wir tun, was immer wir tun wollen in unserer Parse-Prozedur: hier legen wir die Variable an.
  4. Zuletzt müssen wir immer dafür sorgen, dass wir für Statement (denn dorthin kehren wir jetzt zurück) ein gültiges Token-Lexem-Paar vorweisen können.


Starten wir unseren Parser mit dem Source-Code "int a; int b; int c"!
Wir erhalten folgende Debug-Ausgabe:

Code: Alles auswählen

 | N |  78 | int
 | N |  78 | int
 | N |  78 | int
 PARSER - STOP
----------------------------------------
 GLOBALE VARIABLEN IN 'GLOBALVAR()'
----------------------------------------
Typ | Index | Name
----------------------------------------
'i' |     0 | a
'i' |     1 | b
'i' |     2 | c
  • Wir haben also 3 Variablen deklariert:
    1. Variable "a" mit dem typ 'i' und dem Index 0
    2. Variable "b" mit dem typ 'i' und dem Index 1
    3. Variable "c" mit dem typ 'i' und dem Index 2
Sehen wir uns noch das ASM-File an:

Code: Alles auswählen

        setGlobalIntSize    3
                                      
// Beginn des Programms
  • Der Prolog wurde richtig geschrieben, wir haben 3 Integer-Variablen als global deklariert.
    Ich werde in Zukunft bei ASM-Ausgaben den Prolog nicht abdrucken, außer es ist ausdrücklich notwendig.




6.4. Ein-/Ausgabe auf unterstem Level

Lernen wir mit unserem Parser umzugehen, indem wir zunächst auf unterstem Level versuchen, Befehle hinzuzufügen.
Diese ermöglichen uns, das Parsen eines Programms genauer zu erlernen und zu festigen, eben weil sie so low-level sind und nicht viel können.

Ein TTC-Befehl ist entweder eine Prozedur (wir sagen dann Interne Prozedur, wenn sie schon im Parser selbst eingebaut ist) oder eine Funktion, die einen Wert ZURÜCKliefert (und auch hier gibt es Interne Funktionen und die, die der Programmierer definiert hat).
Alle hier in diesem Unterkapitel behandelten neuen Befehle sind Interne Prozeduren, d.h. sie sind von uns im Parser fix eingebaut und sie liefern keinen Wert zurück.



6.4.1. Input (Console)

Der TTC-Befehl input erwartet eine Eingabe des Benutzers auf der Tastatur in der Konsole gefolgt von der Enter-Taste.
Er übergibt das Eingegebene einer Variable.


Zu Beginn müssen wir den Befehl in Statement() erlauben, d.h. wir fügen eine Case-Zeile hinzu, und das machen wir ab jetzt immer, wenn wir einen neuen Befehl einfügen (ich gebe nur die Select-Case-Konstruktion von Statement() wieder):

Code: Alles auswählen

  ; je nach Statement Aktionen setzen
    Select Scanner::Lexem
        
    ; Statements  
      Case "int"    : Declare_Statement('i')
      Case "print"  : Print_Statement()
      Case "input"  : Input_Statement()    
      Default       : Scanner:GetToken()
        
    EndSelect       
  • Wir sehen hier gleich alle neuen Befehle, die wir am Ende dieses Unterkapitels hinzugefügt haben werden.

Untersuchen wir die Parse-Prozedur Input_Statement() ein wenig:

Code: Alles auswählen

  Procedure Input_Statement()  
  
  ; Übersichtlichkeit im ASM-Code
    Emit(0,"// input func")
  
  ; '(' vorhanden ?
    Scanner::GetOther(): TestToken('(')
    Scanner::GetName()

  ; In folgende Variable einlesen
    IsGlobalVar(Scanner::Lexem)
          Emit(GlobalVar(Scanner::Lexem)\Typ,"in")
          Emit(GlobalVar(Scanner::Lexem)\Typ,"pullg",
                          Str(GlobalVar(Scanner::Lexem)\Index),
                          "="+Scanner::Lexem)
    
  ; ')' vorhanden ?  
    Scanner::GetOther(): TestToken(')')
    
  ; nächstes Token-Lexem holen
    Scanner::GetToken()  

  ; Leerzeile zur Übersichtlichkeit
    EmitX()  
    
  EndProcedure  
  • Die Prozedur hat einen magischen Punkt, auf den ich genauer hinweisen möchte, nämlich die 2 Zeilen unter dem Kommentar "In folgende Variable einlesen":
    • Wir sehen, in beiden Code-Ausgaben (Emit) wird der Typ der Variable als 1. Zeichen des Opcodes in den ASM-Befehl eingebaut, wenn die Variable ein Integer ist (steht in GlobalVar()\Typ) ist das ein 'i', bei (später) String ist das ein 's' und bei einem Float dürfen wir 3-mal raten ;-) . Der Typ ergibt sich also automatisch ohne kompliziertes Programmieren durch den Compilerschreiber!
    • Betrachten wir uns nochmals genau diese Art der ASM-Opcode-Ausgabe bzw. -Bildung. Wir werden das öfter sehen.
    Gegen eine Fehleingabe sind wir übrigens hier immerhin durch Scanner:GetName() gar nicht so schlecht abgesichert.


6.4.2. Print (Console)

Der TTC-Befehl Print gibt Folgendes auf der Konsole aus:
  1. String in "..."
  2. Inhalt einer Variablen
  3. Enter-Taste (new line) durch den Modifikator "endl"
Getrennt werden mehrere Objekte in einem Print-Befehl durch den ","-Operator.


Schauen wir uns die Parse-Prozedur Print_Statement() genauer an:

Code: Alles auswählen

  Procedure Print_Statement()  
  
  ; Übersichtlichkeit im ASM-Code
    Emit(0,"// print")
    
  ; '(' vorhanden ?
    Scanner::GetOther(): TestToken('(')
    
  ; // Ausgabe von mehreren Objekten //
    Repeat

      ; Token-Lexem holen
        Scanner::GetToken()
    
      ; Manipulator "endl"
        If Scanner::Lexem="endl"  
          Emit(0,"endl")    
        
      ; String in ".."    
        ElseIf Scanner::Token='S'
          Emit(0,"spushc",#DQ+Scanner::Lexem+#DQ)
          Emit(0,"sout")
      
      ; Integer-Konstante
        ElseIf Scanner::Token='I'
          Emit(0,"ipushc",Scanner::Lexem)
          Emit(0,"iout")
        
      ; Variablen-Name  
        ElseIf Scanner::Token='N'
          IsGlobalVar(Scanner::Lexem)
          Emit(GlobalVar(Scanner::Lexem)\Typ,"pushg",
                          Str(GlobalVar(Scanner::Lexem)\Index),
                          Scanner::Lexem)
          Emit(0,"iout")
        
        EndIf
      
       ; Token-Lexem holen    
        Scanner::GetToken()
  
    Until Scanner::Token<>','
  
  ; ')' vorhanden ?
    TestToken(')')
    
  ; nächstes Token-Lexem holen
    Scanner::GetToken()      
  
  ; Leerzeile zur Übersichtlichkeit
    EmitX()        
  
  EndProcedure
  • Sprechen wir nochmals eine typische Parse-Prozedur durch.
  • Zunächst befindet sich noch das Lexem "print" in Lexem, das in Statement() den Aufruf aus der Select-Case-Konstruktion bewirkt hat.
  • '(' wird geholt, getestet, aber nicht das nächste Token-Lexem geholt, sonst passt das nächste Holmaneuver nicht mehr in der Schleife.
    • Das nächste Token-Lexem wird geholt.
    • Danach ist in Lexem, das auszugebende Objekt, in Token ist der Typ dieses Objekts
      Je nach Typ: Das Richtige Objekt wird auf der Konsole ausgegeben.
    • Wenn ein weiteres Objekt ausgegeben werden soll, dann muss nach dem Holen des nächsten Token-Lexem jetzt ein ',' in Token sein und das Spiel beginnt am Schleifenkopf aufs Neue.
  • ')' wird geholt
  • Zuletzt müssen wir immer dafür sorgen, dass wir für Statement (denn dorthin kehren wir jetzt zurück) ein gültiges Token-Lexem-Paar vorweisen können.

Zum Abschluss nehmen wir folgenden Source-Code und kompileren ihn:

Code: Alles auswählen

    int zahl
    print("Bitte Zahl eingeben!", endl)
    input(zahl)
    print("Danke. Sie haben ",zahl," eingegeben", endl)
Unser ASM-Code sollte folgendermaßen aussehen.

Code: Alles auswählen

        // print                
        spushc      "Bitte Zahl eingeben!"
        sout                    
        endl                    

        // input func            
        iin                     
        ipullg      0           // =zahl

        // print                
        spushc      "Danke. Sie haben "
        sout                    
        ipushg      0           // zahl
        iout                    
        spushc      " eingegeben"
        sout                    
        endl                    
Zugeben, es ist nicht viel, aber es ist unser erstes selbst geparstes und zu VM-ASM compiliertes Programm! Hurrraa :D !


(Anmerkung: Unschwer ist der jweils gleichnamige C++ Befehl als Vorbild zu leugnen bei den beiden Befehlen "cin" und "cout", die ich im Tutorial zwar nicht dokumentiere, aber programmiert habe und deshalb im Code von TTC 0.5 zum Spaß des Users belasse.)
Zuletzt geändert von puretom am 16.11.2013 12:38, insgesamt 46-mal geändert.
Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

6.5. Variablenzuweisung (Assignment) an globale Integer-Variablen

Einer Variable kann man einen Wert zuweisen, im Falle einer Integer-Variable ist das eine Integer-Zahl, im Falle einer String-Variable ein String und im Falle einer Fließkommazahl eben eine Float-Variable.

Wir werden also versuchen, folgenden Source-Code zu kompilieren:

Code: Alles auswählen

/* Variablen-Deklarationen */
 int a; int b; int c
	
// Variablenzuweisung (=Assignment)
 a=10; b=100; c=10000
 ^^ ^
 || |
 || '--- rechte Seite: Expression
 |'-----      =      : Zuweisungsoperator
 '------ linke Seite : belegte Variable a

                       */
Hier sollen die Variablen a, b und c mit Werten belegt werden.

Man nennt so eine Zuweisungoperation Assignment, wobei die rechte Seite, hier zunächst die Zahl 10, als Expression bezeichnet wird.


Die Prozedur Assignment_Statement() wird diese Aufgabe übernehmen.
Da alles in einer Programmiersprache, was nicht etwas anderes ist, vermutlich ein Assignment werden wird, geben wir diese Prozedur in Statement() in den Default-Zweig (Ich habe hier nur die Select-Case-Konstruktion wiedergegeben):

Code: Alles auswählen

    ; je nach Statement Aktionen setzen
      Select Scanner::Lexem
          
        ; Statements  
          Case "int"    : Declare_Statement('i')
        ; ...
          Default       : Assignment_Statement()
         
      EndSelect       
Programmieren wir die Prozedur Assignment_Statement(), die einer Variable einen Wert zuweist:

Code: Alles auswählen

  Procedure Assignment_Statement()     ; weist Var eine Expression zu
  
  ; --> Variablenname in Lexem      
    
  ; zur Übersichtlichkeit
    Emit(0,"// assignment")  
    
  ; Variablenname merken, Variable bekannt?
    var_name.s = Scanner::Lexem   
    IsGlobalVar(var_name)         
  
  ; den Typ der Expression herausfinden
  ; d.i. der Typ der Variablen, dem das Ergebnis zugewiesen wird 
  ; wird GLOBALER Variable ExpTyp zugewiesen 
    ExpTyp = GlobalVar(var_name)\typ
  
  ; '=' holen, testen
    Scanner::GetOther(): TestToken('=')
  
  ; 1. Token-Lexem der Expression() holen (=ValueFactor)
    Scanner::GetToken()
  
  ; MathExpression aufrufen
    MathExpression() 
  
  ; Wert der Expression (liegt am Stack) der Variable zuweisen
  ; für Variable (Name oben gemerkt) wird Index verwendet
    Emit(ExpTyp,"pullg",Str(GlobalVar(var_name)\index),"="+var_name)         
  
  ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
    EmitX()
  
  ; --> Token-Lexem ist bereits von Expression
  ; --> richtig auf das nächste vorbereitet

  EndProcedure 
  • Wir benötigen für später den Variablennamen für das Speichern des Wertes in die Variable, der befindet sich bei Aufruf in Lexem.
  • Wir testen, ob die Variable bereits deklariert worden ist, wenn nicht -> Fehler.
  • Wir brauchen den Typ (also z.B. 'i' für Integers, später 'f' für Floats oder 's' für Strings usw. - wie in PB) der Variablen, um den Typ der Gesamt-Zuweisung - den Expression-Typ - herauszufinden. Den Wert speichern wir in einer globalen Variablen mit dem Namen ExpTyp.

    Anmerkung: Wer das Entstehen dieses Tutorials über alle Irrtümer und Fehlversuche hinweg aktiv mitverfolgt, der weiß, dass ich ursprünglich in den meisten meiner Compiler-Versionen den Wert als Parameter tiefer in die Expression hineingeschickt habe. Das ist aber umständlicher als eine simple globale Variable, aber man hat wegen der Verwendung der globalen Variable immer wieder das unangenehme Gefühl, ein blutiger Anfänger zu sein.
    Doch ich halte es hier wie Jack Crenshaw: KISS (=Keep ist simple stupid) :lol: statt Schönheit im Formalismus.
    (Wer weiß, ob ich das nochmals ändern muss ;-) - denn ich arbeite an den höheren Versionen parallel und muss immer wieder, wenn ich oben mein Design ändere, unten das Anfangstutorial anpassen und ändern.)
  • Das '=' muss getestet und übersprungen werden.
  • Passende Expression: Das ist der magische Punkt.
    Auf der rechten Seite einer Zuweisung (Assignment) befindet sich immer ein Ausdruck (Expression).
    Deshalb rufen wir jetzt MathExpression() auf.
    • Entweder unsere Variable ist vom Typ 'i' oder 'f', dann verzweigen wir zu MathExpression()
    • oder unsere Variable ist vom Typ 's' (String), dann verzweigen wir zu StringExpression() (kommt im Kapitel Zeichenketten (Strings)) .
      Da wir derzeit aber nur Integer-Variablen kennen und erlauben, schlagen wir sofort den Weg nach MathExpression() ein.
    MathExpression() hinterlässt am Stack das Ergebnis der Expression.
  • Das Ergebnis der Expression liegt am Stack.
    Wir geben einen Befehl aus, der unserer gemerkten Variable diesen Wert vom Stack zuweist. Wir nehmen als Typ den in ExpTyp.
    Wichtig: Wir arbeiten hier mit dem Variablenindex.


Wir könnten also unsere Sprachgrammatik auch schreiben als:

Code: Alles auswählen


; *** ALLE STATEMENTS, DIE TTC KENNT ***
  
  Statement = Declare_Statement    |          ; alle Statements, die in der Case-Konstruktion auftauchen.       
              Assignment_Statement            ; Das Zeichen '|' bedeutet "oder".
                                              ; ';' leitet Kommentar ein.


; *** VERWALTUNG GLOBALER VARIABLEN ***

  Declare_Statement    = "int" Variablenname  ; "int" gefolgt von einem Variablennamen.
  Assignment_Statement = Var "=" 
                         MathExpression |     ; Assignment_Statement ist ein Variablenname, 
                                              ; gefolgt von einem '=' und 
                                              ; gefolgt von einer MathExpression
  
Diese Art, eine Grammatik anzuschreiben, nennt man BNF bzw. EBNF oder ABNF.
Da diese Form der Darstellung nicht ganz so "meins" ist, erlaube man mir bitte hier etwas künstlerische Freiheit. Sollte ich hier Fehler haben, bitte ich um Berichtigung im Diskussionsthread.


In diesem Zusammenhang möchte ich auf ein kleines Detail im Code hinweisen.
Ich arbeite recht gerne mit - ich nenne sie so - Aufruf- und Weiterleitungsprozeduren, vor allem auch deshalb, weil ich hier mit "Emit(0, "// math expression") eine kleine Ausgabe für die Behübschung des ASM-Codes eingefügt habe.
Assignment (siehe die BNF oben) ruft also immer MathExpression() oder StringExpression() auf, egal wie dann die oberste Ebene wirklich heißen wird (die wird bei TTC 0.5 dann SimpleExpression() sein). Sollte jemand den "Overhead" wegen der vielen Prozeduraufrufen scheuen, dann kann jeder das anders halten. Wir werden sehen, dass wir das aber möglicherweise noch so brauchen werden.

Code: Alles auswählen

  Procedure MathExpression() 

  ; Zur Übersichtlichkeit
    Emit(0,"// math expression")

  ; Abstieg zu SimpleExpression()    
    SimpleExpression()  

  EndProcedure
  • Math Expression(), das aufgerufen wird, ruft hier in dieser Version seinerseits z.B. SimpleExpression() auf.



6.6. Mathematische Ausdrücke (Expression) mit Integers

Uns fehlt noch die Prozedur SimpleExpression().
Die Prozedur SimpleExpression() wird ja von Assignment_Statement() (über die Prozedur MathExpression()), nachdem sie auf das Vorhandensein eines "=" geprüft hat, aufgerufen.

Die Prozedur SimpleExpression(), was so viel wie einfacher (mathematischer) Ausdruck heißt, ist also der Startpunkt - die oberste Ebene, der Eingangspunkt des mathematischen Ausdrucksparsers, des Kernstücks jedes Compilers.
Der Mathe-Parser wird es sein, der mathematische Ausdrücke wie "3+5*2+(43+44*6)" lösen kann.

Die Prozedur SimpleExpression(), der derzeitige Startpunkt des mathematischen Ausdrucksparsers:

Code: Alles auswählen

  Procedure SimpleExpression() 

    ; Abstieg zu ValueFactor()    
      ValueFactor()  

  EndProcedure

6.6.1. Einfache positive konstante Werte (=Values)

Ein konstanter Wert ist eine Integer-Zahl, und zwar eine positive. Erlaubt sind also zunächst nur positive Zahlen.

SimpleExpression() ruft also - siehe darüber - die Prozedur ValueFactor() auf, deren einzige Aufgabe es ist, einen einzelnen Wert (=Value) auf den Stack zu pushen:

Code: Alles auswählen

  Procedure ValueFactor() 
       
    ; Wert (Value) einer Konstanten auf Stack pushen
      Emit(ExpTyp,"pushc",Scanner::Lexem,Scanner::Lexem)

    ; holt nächstes Token
      Scanner::GetToken()
    
    ; --> in Token/Lexem ist Token/Lexem nach Value
    ; --> Aufstieg zu (derzeit) SimpleExpression()      
      
  EndProcedure
Wir sehen also, auch ein einzelner Wert ist ein mathematischer Ausdruck - eine Expression, wenn auch eine klitzekleine bescheidene :lol: .
  • Schauen wir uns die Funktionalität der Emit-Prozedur, die wir im Code-Teil finden, an:

    Sie baut aus der Parameterzeile folgende Assembler-Ausgabe zusammen:

    Code: Alles auswählen

    
     Spaces + typ + "pushc " + Parameter + Kommentar
               i  +  pushc   +    10          + // Kommentar
               ^         ^        ^       
               |         |        '-- Parameter: Konstante 10
               |         '----------- c = constant, 
               |                      g = global
               '--------------------- i = Integer, 
                                      f = float,
                                      s = string
    
    
Zwei Begriffe fallen in den beiden Prozeduren, d.i. "Abstieg" und "Aufstieg". Ich verweise hier in diesem Zusammenhang auf die Compilerbau-Technik des rekursiven Abstiegs.



Der mathematische Parser beginnt also bei SimpleExpression(). Wir könnten also unsere Sprachgrammatik erweitern (nur Ausschnitt: mathematischer Expression Parser, die anderen Zeilen bleiben gleich):

Code: Alles auswählen


; *** MATHEMATISCHER AUSDRUCKSPARSER (EXPRESSION PARSER) ***

  MathExpression   = SimpleExpression ;   |           Aufstieg ^
  SimpleExpression = ValueFactor      ;   |                    |
  ValueFactor      = Integerzahl      ;   v Abstieg            |


Kompilieren wir jetzt unseren Source-Code von oben:

Code: Alles auswählen

/* Variablen-Deklarationen */
 int a; int b; int c
	
// Variablenzuweisung (=Assignment)
 a=10; b=100; c=10000
In der Datei "source-code.ttca" (nicht in der Debug-Ausgabe), also unserem Assembler-File, sollte Folgendes stehen:

Code: Alles auswählen

        // assignment            
        // math expression            
        ipushc      10          // 10
        ipullg      0           // =a

        // assignment            
        // math expression            
        ipushc      100         // 100
        ipullg      1           // =b

        // assignment            
        // math expression            
        ipushc      10000       // 10000
        ipullg      2           // =c
  • Der Stack ist in einem bestimmten Urzustand.
  • Die Zuweisungsaktion beginnt:
    • Eine Integer-Konstante wird auf den Operanden-Stack (TOS = Top Of Stack) gepusht.
    • Der TOS (=Integer) wird in die globale Variable mit dem bestimmten Index gepullt.
  • Wichtig: Das war ein "pull" und kein "peek", d.h. der Stack ist wieder im Urzustand wie genau vor der Aktion.
Falls es niemandem aufgefallen ist: WIR HABEN UNSER ERSTES PROGRAMM UND UNSEREN ERSTEN MATHEMATISCHEN AUSDRUCK KOMPILIERT, doch das können wir noch besser!



6.6.2. Variablennamen

Es ist ein Leichtes, die TinyToyC-Sprachgrammatik so zu erweitern, dass der Parser zusätzlich zu einer konstanten Zahl auch einen Variablennamen akzeptiert.

Code: Alles auswählen


; *** MATHEMATISCHER AUSDRUCKSPARSER (EXPRESSION PARSER) ***

  MathExpression   = SimpleExpression ; |  Aufstieg ^
  SimpleExpression = ValueFactor      ; |           |
  ValueFactor      = Integerzahl   |  ; |           |
                     Variablenname    ; v Abstieg   |

  • Ein ValueFactor ist also nicht nur eine Integerzahl, sondern kann auch (| = oder) ein Variablenname sein.
Wir müssen also, wenn wir der BNF folgen, unsere Änderungen in ValueFactor() durchführen. Bauen wir auch gleich eine Fehlerbehandlung mit ein:

Code: Alles auswählen

  Procedure ValueFactor() 
       
  ; Wert (Value) einer Konstanten auf Stack pushen
    If Scanner::Token='I':      
      Emit(ExpTyp,"pushc",Scanner::Lexem,Scanner::Lexem) 
      
         
  ; Wert einer Integer-Variable auf den Stack pushen
    ElseIf Scanner::Token='N':      
      IsGlobalVar(Scanner::Lexem) ; Variable existiert?
      Emit('i',"pushg",Str(GlobalVar(Scanner::Lexem)\Index),
                       Scanner::Lexem)       
        
  ; sonst -> Fehlermeldung
    Else
      Error("ValueFactor",#Ungueltiger_Operand)    
        
    EndIf
    
  ; holt nächstes Token
    Scanner::GetToken()
  
  ; --> in Token/Lexem ist Token/Lexem nach Value
  ; --> Aufstieg zu (derzeit) SimpleExpression()      
      
  EndProcedure 
  • Schauen wir uns die Prozedur Value_Factor() genauer an:
  • 'I'-Token, also Integer-Zahl: pusht Konstante (constant).
  • 'N'-Token, also Name:
    • Es wird überprüft, ob die Variable überhaupt jemals deklariert worden ist. Ist das nicht der Fall, endet hier unser Parser mit einer passenden Fehlermeldung.
    • pusht globale Variable (mit Index) und gibt als Kommentar rechts daneben den Namen aus.
  • Default: weder 'I' noch 'N': Fehlermeldung.


Wir kompilieren den folgenden Source-Code:

Code: Alles auswählen

/* Variablen-Deklarationen */
 int a; int b; int c
 
// Variablenzuweisung
 a=10; b=a; c=b
Und wir erhalten das richtige ASM-Programm:

Code: Alles auswählen

        // assignment            
        // math expression            
        ipushc      10          // 10
        ipullg      0           // =a

        // assignment            
        // math expression            
        ipushg      0           // a
        ipullg      1           // =b

        // assignment            
        // math expression            
        ipushg      1           // b
        ipullg      2           // =c
  • Wir können Variablennamen verwenden, aber nur welche, die wir zuvor deklariert haben. Wir sollten absichtlich ein paar Fehlermeldungen provozieren!


6.6.3. Einfache Ausdrücke (Simple Expressions) mit 2 oder mehr Operanden

Bis jetzt können wir mathematische Ausdrücke bewältigen, die aus einem Value (Variablenname oder konstante Integer-Zahl) bestehen.

Erweitern wir unsere TinyToyC-Sprachgrammatik so, dass wir mathematische Ausdrücke mit 2 oder Operanden auflösen können, die einen Operator dazwischen haben, also z.B. "a*2" oder "b>=4".


Hier haben wir 2 Möglichkeiten, die BNF anzugeben, jede der beiden Möglichkeiten beschreibt eine unterschiedliche Funktionalität in TTC:
  1. 1. Fall: optionale, aber maximal 2 Operanden, keine Kette, z.B. "x=3+a".
    2. Fall: optionale unendliche Operandenkette, z.B. "x=3+a*4+23" usw.


Die BNF für den 1. Fall , also maximal 2 Operanden:
  • Code: Alles auswählen

    
    ; *** MATHEMATISCHER AUSDRUCKSPARSER (EXPRESSION PARSER) ***
    
      MathExpression   = SimpleExpression           ; | Aufstieg ^
      SimpleExpression = ValueFactor                ; |          |
                         ["alle Ops" ValueFactor ]  ; |          |
      ValueFactor      = Integerzahl   |            ; |          |
                         Variablenname              ; v Abstieg  |
    
    wobei "alle Ops" folgende mathemat. Operatoren meint:
    
    "<"|"<="|"<>"|">="|">"|"="|"+"|"-"|"or"|"xor"|"and"|"*"|"/"|"%"
    
    
    • Eine SimpleExpression besteht nach dieser Grammatik aus einem Operanden optional gefolgt von einem Operator wieder gefolgt von einem Operanden, also z.B. "3*1".
      Bitte nicht übersehen, dass der Operator und der 2. Operand eine Option sind, die vorkommen kann, aber nicht muss (siehe "[ ... ]").
      Die BNF besagt also, dass die Option in "[...]" KEINMAL oder EINMAL vorkommen kann.

      Diesen Fall würden wir mit einer If-Abfrage lösen, die ja genau das aussagt, nämlich keinmal oder einmal.


Die BNF für den 2. Fall , also beliebige Anzahl von Operanden:
  • Code: Alles auswählen

    
    ; *** MATHEMATISCHER AUSDRUCKSPARSER (EXPRESSION PARSER) ***
    
      MathExpression   = SimpleExpression           ; | Aufstieg ^
      SimpleExpression = ValueFactor                ; |          |
                         {"alle Ops" ValueFactor }  ; |          |  <== !!!!
      ValueFactor      = Integerzahl   |            ; |          |
                         Variablenname              ; v Abstieg  |
    
    
    • Alles, was ich verändert habe, das ist das "[ ... ]" in "{ ... }" in der Produktion (so nennt man eine Regel in einer Grammatik) SimpleExpression. Die eckige Klammer meint keinmal oder einmal, die geschweifte Klammer meint KEINMAL oder BELIEBIG OFT.

      Den Keinmal- oder Einmal-Fall lösten wir mit einer If-Abfrage.
      Keinmal oder beliebig oft implementieren wir mit mit einer While-Schleife. Sie ist ebenso am Kopf gesteuert wie If, aber eben nicht nur einmal, sodern keinmal oder beliebig oft.

Hier möchte ich Crenshaw nacheifern und ausdrücklich darauf hinweisen, dass sich die BNF fast wie von selbst in Strukturen eines Programms verwandelt. "Keinmal oder einmal" ist eindeutig eine If-Sache und "keinmal oder beliebig oft" eine eindeutige Schleife.
Die BNF wird 1 zu 1 zu Pure-Basic-Code.



Füllen wir die Prozedur SimpleExpression() mit dem Code für mathematische Ausdrücke. Sie ist wirklich nur für den derzeitigen einfachen Fall (wie der Name schon andeutet), damit wir in TinyToyC voranschreiten können. Ein echter mathematischer Ausdrucksparser wird in der Entwicklung ein eigenes ganzes Kapitel einnehmen:

Code: Alles auswählen

  Procedure SimpleExpression()
  
  ; --> Token/Lexem steht auf Value
  
  ; Abstieg zu ValueFactor()    
    ValueFactor()    
    
  ; Ist irgendein Operator in Token/Lexem?
    While Scanner::Token='+' Or Scanner::Token='-' Or 
          Scanner::Token='*' Or Scanner::Token='/' Or
          Scanner::Token='%' Or 
          Scanner::Token='=' Or Scanner::Token='u' Or 
          Scanner::Token='<' Or Scanner::Token='>' Or
          Scanner::Token='k' Or Scanner::Token='g' Or
          Scanner::Lexem="and" Or 
          Scanner::Lexem="or"  Or
          Scanner::Lexem="xor" 
         
      ; 'and', 'or', 'xor' hier vor Ort zu einem Token machen
        If     Scanner::Lexem="and" : Scanner::Token='a'
        ElseIf Scanner::Lexem="or"  : Scanner::Token='o'        
        ElseIf Scanner::Lexem="xor" : Scanner::Token='x'  
        EndIf      
        
      ; Operator merken
        operator = Scanner::Token
        
      ; vor Abstieg nächstes Token (=Value) holen
        Scanner::GetToken()
        
      ; Abstieg zu ValueFactor()    
        ValueFactor()
        
      ; Ausgabe des ASM-Codes des Operators
      ; mit gemerktem Operator                
        Select operator
            
            Case '+'  : Emit(ExpTyp,"add","", "+" )
            Case '-'  : Emit(ExpTyp,"sub","","-"  )
            Case '*'  : Emit(ExpTyp,"mul","","*"  )
            Case '/'  : Emit(ExpTyp,"div","","/"  )
            Case '%'  : Emit(ExpTyp,"mod","","%"  )
            Case 'a'  : Emit(ExpTyp,"and","","and") 
            Case 'o'  : Emit(ExpTyp,"or" ,"","or" ) 
            Case 'x'  : Emit(ExpTyp,"xor","","xor")
            Case '='  : Emit(ExpTyp,"eq" ,"","="  )
            Case 'u'  : Emit(ExpTyp,"ne" ,"","<>" )
            Case '<'  : Emit(ExpTyp,"lt" ,"","<"  )
            Case '>'  : Emit(ExpTyp,"gt" ,"",">"  )
            Case 'k'  : Emit(ExpTyp,"le" ,"","<=" )
            Case 'g'  : Emit(ExpTyp,"ge" ,"",">=" )
            
        EndSelect
          
    Wend   
        
  ; --> Aufstieg zu MathExpression()
  
  EndProcedure
Die Prozedur ist nur allein deshalb so lange, weil ich für den Anfang einfach ALLE denkbaren Operatoren hineingepackt habe.
  • Laden und pushen des 1. Operanden: Abstieg bis zu ValueFactor()
  • Wenn kein Operator folgt, endet die Expression.
  • Wenn Operator folgt (If- oder While-Abfrage), dann:
    1. wandle die Wortoperatoren "and", "or", "xor" hier vor Ort in eine Token-Zahl um ('a', 'o', 'x')
    2. merke Operator
    3. Laden und pushen des 2. Operanden: Abstieg bis zu ValueFactor()
    4. bei Rückkehr: gemerkten Operator ausführen
  • Prozedur-Ende

    Zwischen dem oben erwähnten Fall 1 (mit If-Endif) und Fall 2 (mit While-Wend) können wir durch bloßes "Umschalten" zwischen If und While wählen.



Wir kompilieren zum Testen den folgenden Source-Code:

Code: Alles auswählen

/*  Variablen-Deklarationen */
    int a; int b; int c
 
/*  Variablenzuweisung mit einfachen Ausdrücken 
 *  und maximal 2 Operanden
 */
    c =  8564
    a =  10 + b
    b =  c  < =  a 
    c =  34 * b

//  bis hierher sollte die If-Version problemlos kommen.

// -----------------------------------------------------------------------
//  Ab hier funktioniert nur die bessere While-Version,
//  denn wir haben mehr als 2 Operanden

/*  Parser kennt noch keine Punkt-vor-Strich-Rechnung.
 *  Er rechnet also diese letzte Rechung falsch aus. 
 */
    c = 1 + 10*20
//            ^-- Hier beim 2. Operator bleibt die If-Version hängen.
//                TTCP erwartet nämlich eine neue Expression, dadurch einen Operanden.
//                TTCP erzeugt also in ValueFactor() eine Fehlermeldung.
  • In den Kommentaren des Source-Codes ist das unterschiedliche Verhalten der If- und While-Version erklärt.
    Wir sollten diesen Source-Code mit der einen und der anderen Version testen, einfach um den Unterschied zu sehen und das Verhalten verstehen zu können.

Das ASM-Programm zeigt uns, dass wir richtig rechnen:

Code: Alles auswählen

        // assignment            
        // math expression            
        ipushc      8564        // 8564
        ipullg      2           // =c

        // assignment            
        // math expression            
        ipushc      10          // 10
        ipushg      1           // b
        iadd                    // +
        ipullg      0           // =a

        // assignment            
        // math expression            
        ipushg      2           // c
        ipushg      0           // a
        ile                     // <=
        ipullg      1           // =b

        // assignment            
        // math expression            
        ipushc      34          // 34
        ipushg      1           // b
        imul                    // *
        ipullg      2           // =c

        // assignment            
        // math expression            
        ipushc      1           // 1
        ipushc      10          // 10
        iadd                    // +
        ipushc      20          // 20
        imul                    // *
        ipullg      2           // =c
  • Die letzte Rechnung ist natürlich falsch, unser mathematischer Parser kennt noch keine Punkt-vor-Strich-Rechnung, er hätte als Erstes 10*20 rechnen und dann erst 1 dazuaddieren sollen, nicht umgekehrt. Statt dessen hat er alles (falscherweise) von links nach rechts gerechnet.

    Dem kommen wir in dieser Version nur mit Klammern (die wir aber noch nicht eingebaut haben, aber noch schnell einbauen werden).
    Ein echter mathematische Parser erkennt die Punkt-vor-Strich-Regel automatisch. Aber das ist Teil eines der nächsten Kapitel.


6.6.4. Klammerausdrücke

Die Erweiterung der Sprachgrammatik um Klammerausdrücke ist sehr einfach:

Code: Alles auswählen


; *** MATHEMATISCHER AUSDRUCKSPARSER (EXPRESSION PARSER) ***

  MathExpression   = SimpleExpression             ; | Aufstieg ^
  SimpleExpression = ParaExpression               ; |          |
                     {"alle Ops" ParaExpression}  ; |          |
  ParaExpression   = "(" SimpleExpression ")" |   ; |          |
                     ValueFactor                  ; |          |
  ValueFactor      = Integerzahl   |              ; |          |
                     Variablenname                ; v Abstieg  |

  • Die Neuigkeit ist ParaExpression.
    Wie der Name Klammer-Ausdruck schon andeutet, befindet sich innerhalb der Klammer eine vollständige Expression, d.h. hier haben wir letzlich eine rekursive in sich verwickelte und sich selbst aufrufende Schnur, ähnlich einem Hund, der sich in den Schwanz beißt.
    Genaueres zu dieser Parse-Technik möchte ich in einem eigenen Kapitel, das einen vollständigen mathematischen Parser zeigen wird, durchbesprechen.


Fügen wir also die Prozedur ParaExpression() ("Para" von Parantheses = Klammern) unserem Programm hinzu, um Klammerausdrücke zu ermöglichen.
Sie wird unterhalb von SimpleExpression() und oberhalb von ValueFactor() der BNF folgend eingefügt:

Code: Alles auswählen

  Procedure ParaExpression()
  
    ; --> Token-Lexem steht auf Value ODER
    ; --> auf '('
    
    ; Klammerausdruck beginnt mit "("
      If Scanner::Token ='('
      
            Scanner::GetToken() ; '(' überspringen und
                                ;     nächstes Token-Lexem laden
            
            SimpleExpression () ; ----> oberste Stufe Math Parser                      
                                ;       ineinander verschachtelt                                
            
            TestToken(')')      ; ')' überspringen, dann Aufstieg                             
            Scanner::GetToken() ;     zu SimpleExpression()

                                                  
      
    ; keine Klammer
      Else
      
            ValueFactor()       ; weiterer Abstieg zu ValueFactor()
      
      EndIf
      
   ; --> Aufstieg zu SimpleExpression()
    
  EndProcedure 
  • Nicht vergessen dürfen wir auch, in SimpleExpression() jetzt alle Aufrufe von ValueFactor() auf ParaExpression() umzustellen.

Testen wie abschließend das folgende Source-Programm:

Code: Alles auswählen

/* Variablen-Deklarationen */
 int a; int b; int c
 
// Parser kennt noch keine 
// Punkt-vor-Strich-Rechnung
 a =  1+10*20
 b = (1+10)*20 // sollte dasselbe wie vorige Rechnung sein, denn das ist der Normalzustand unseres noch 'dummen' Math Parsers
 c = 1+(10*2)  // sollte zuerst 10*2 rechnen, dann addieren
Das ergibt folgendes ASM-Programm:

Code: Alles auswählen

        // assignment            
        // math expression            
        ipushc      1           // 1
        ipushc      10          // 10
        iadd                    // +
        ipushc      20          // 20
        imul                    // *
        ipullg      0           // =a

        // assignment            
        // math expression            
        // math expression            
        ipushc      1           // 1
        ipushc      10          // 10
        iadd                    // +
        ipushc      20          // 20
        imul                    // *
        ipullg      1           // =b

        // assignment            
        // math expression            
        ipushc      1           // 1
        // math expression            
        ipushc      10          // 10
        ipushc      2           // 2
        imul                    // *
        iadd                    // +
        ipullg      2           // =c
  • Der Klammerausdruck wird VORHER ausgerechnet, das Ergebnis ist das oberste Element des Stacks.
    Sehr schön sieht man hier (weil wir es so eingestellt haben), dass ein Klammernausdruck wieder nichts anders ist, als eine neue Math Expression, was die Kommentarzeile "// math expression" deutlich beweist.


Wir werden es bei dieser Lösung jetzt belassen, denn wir haben alle Vorarbeiten geleistet und alle Komponenten bereit, um endlich die Kontrollstrukturen, Schleifen usw. zu programmieren, sie auch testen zu können und beim Bau viel zu lernen.
Wer übrigens negative Zahlen eingeben will, der muss z.B. x = 0-43 oder z.B. x = 0-n schreiben.
Diese Lösung ist zwar dumm, aber jetzt machen wir wirklich endliche die Kontrollstrukturen und Schleifen, damit mal auch wirklich endlich etwas "läuft" :lol: !
Zuletzt geändert von puretom am 16.11.2013 01:13, insgesamt 43-mal geändert.
Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

6.7. Sprünge


6.7.1. Einleitung

Alle unsere jetzt kommenden Befehle müssen wir natürlich wieder wie immer in der Prozedur Statement() "anmelden", damit unser TTC sie als Befehle erkennt und zur entsprechenden Handlungsprozedur/Parse-Prozedur verzweigt.

Um ein wenig mehr Lust zu machen, weiterzulesen, gebe ich die Prozedur Statement() wieder, so wie sie ausssieht, wenn das Kapitel 6 ganz zu Ende gelesen worden ist ;-) :

Code: Alles auswählen

  Procedure Statement()     ; erkennt Statement -> Statement-Prozedur
  
  ; DEBUG
    Debug " | "+Chr(Scanner::Token)+              ; CHAR des Token-Codes
          " | "+RSet(Str(Scanner::Token),3," ")+  ; Code-Nr des Tokens
          " | "+Scanner::Lexem                    ; Lexem
    
  ; // je nach Statement Aktionen setzen //
    Select Scanner::Lexem
        
  ; Statements  
    Case "{"        : Block()
    Case "int"      : Declare_Statement('i')
    Case "if"       : If_Statement()
    Case "while"    : While_Statement()
    Case "do"       : Do_Statement()    
    Case "break"    : Break_Statement()
    Case "continue" : Continue_Statement() 
    Case "goto"     : Goto_Statement()
    Case "gosub"    : GoSub_Statement()
    Case "return"   : Return_Statement()
    Case "print"    : Print_Statement()
    Case "input"    : Input_Statement()    
    Case "cout"     : Cout_Statement()   
    Case "cin"      : Cin_Statement()         
    Case "end"      : End_Statement()  
    Case "rand"     : Rand_Statement()   
    
    Default         : If Scanner::Look=':'     ; -- Sprungmarke (Label) --
                        EmitX("[UL_"+UCase(Scanner::Lexem)+"]")   
                        Scanner::GetOther()        ; ':' holen
                        Scanner::GetToken()        ; nächstes Token-Lexem
                    
                      Else
                        Assignment_Statement() ; -- Assignment -- 
          
                      EndIf
                                        
    EndSelect       
  
  EndProcedure  
  • Anmerkung: cin und cout sind undokumentiert (funktionieren tw. ähnlich wie die gleichnmigen Cpp-Funktionen).


6.7.2. Goto und Sprungmarken (Labels)

Der Befehl "goto" ist ein unbedingter Sprung zu einer Sprungmarke (einem Label).


Folgender Source-Code() zeigt ein Beispiel mit den 3 Sprungmarken "eins:", "zwei:" und "drei:" sowie 3 Goto-Befehlen:

Code: Alles auswählen

     goto eins

zwei:
     print("zwei, ")
     goto drei
     
eins:
     print("Eins, ")
     goto zwei

drei:
     print("drei.", endl)
  • TTC-Labels werden mit einem Doppelpunkt dahinter versehen (wie PB).
    Die Hüpferei im Code sollte dann, wenn wir mit allem fertig sind, zu einer korrekten Ausgabe von "Eins, zwei, drei." in der Konsole führen.

Die Umsetzung eines Goto-Befehls ist sehr einfach. Nach dem gewohnten Hinzufügen der Statement-Prozedur in der Select-Case-Konstruktion in Statement() programmieren wir folgende Prozedur Goto_Statement():

Code: Alles auswählen

  Procedure Goto_Statement()   ; springt zu Sprungmarke

  ; Zur Übersichtlichkeit
    EmitX("// GOTO")

  ; Sprungmarken-Name holen und als ASM-Code ausgeben 
    Scanner::GetName()    
    Emit(0,"j","UL_"+UCase(Scanner::Lexem))
  
  ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
    EmitX()
  
  ; Nächstes Token-Lexem-Paar laden  
    Scanner::GetToken()

  EndProcedure
  • Sie ist recht kurz, sie tut ja auch nichts anderes, als ein "j" (steht für jump) gefolgt von einem Label-Namen auszugeben.
    Zur Sicherheit (ev. spätere Namenskonflikte, PB macht es ähnlich) geben wir den Labels, die vom TTC-Programmierer bereits im TTC-Source verwendet werden, ein "UL_" (soll heißen: User Label) voran. Zur Sicherheit, man weiß nie ;-) .

Das Erkennen von Labels mit nachfolgendem ":" - wie in unserem Source-Programm die Labels "eins:", "zwei:", "drei:" - ist ebenso einfach.
Der einzig logische Ort für die Label-Erkennung ist der Default-Zweig. Dort kommen wir ja hin, wenn kein reserviertes Befehlsschlüsselwort vorgelegen hat (siehe Statement() in der Einleitung 6.6.1.).

Gemäß unserer neuen Überlegung ist die Sache so:
  • 'N'-Token MIT danach folgendem ":": Es ist ein Label.
  • 'N'-Token OHNE danach folgendes ":": Es ist vermulich eine Variablenzuweisung mittels Assignment_Statement().
Wir haben hier übrigens eine Ideal-Anwendung einer Abfrage nach Look (nicht Token), weil ja der Scanner - wir erinnern uns - durch SkipWhite() in Look schon das Zeichen NACH dem aktuellen Token-Lexem-Paar hat.


Damit sollte der oben vorgestellte Source-Code folgenden ASM-Code() ergeben:

Code: Alles auswählen

// GOTO
        j           UL_EINS     

[UL_ZWEI]
        // print                
        spushc      "zwei, "    
        sout                    

// GOTO
        j           UL_DREI     

[UL_EINS]
        // print                
        spushc      "Eins, "    
        sout                    

// GOTO
        j           UL_ZWEI     

[UL_DREI]
        // print                
        spushc      "drei."     
        sout                    
        endl                    
  • Wir sehen, dass die Label-Namen einfach durchgereicht werden, nur ein "UL_" wird vorangestellt, was so viel "User Label" heißt und andeutet, dass der TTC-Programmierer eigenhändig ein Label in seinem TTC-Text eingefügt hat (was er ja nicht tun sollte, der Schlingel).
    Sie werden auch nicht darauf überprüft, ob sie doppelt sind. Das überlassen wir dem Assembler, denn diese Art der Programmierung sollte vom Compiler ja ohnehin nicht noch gefördert werden, ist sie ja doch so schrecklich böse unstrukturiert :lol: .


6.7.3. Gosub und return

Der nächste Befehl, den die guten alten Basic-Versionen gekannt haben, das ist "gosub". Diese Anweisung - es steckt im Namen - ruft eine Subroutine (ein Unterprogramm) auf. Es ist ein "goto", aber die VM wird sich bei diesem Befehl in einem Daten-Frame, das wir Return-Stack nennen, d.i. ein Stapel-Speicher, die Stelle im Programm merken, von dem die Subroutine aus angesprungen worden ist.
Mit einem einfachen "return" springt das Programm wieder dorthin zurück.
Wichtig ist, dass wir Unterprogramme nicht mit Prozeduren oder Funktionen verwechseln, so etwas wie lokale Variablen und dergleichen kennt TTC noch nicht.


Folgender Source-Code zeigt die Verwendung von Unterprogrammen:

Code: Alles auswählen

          gosub eins
          gosub zwei
          gosub drei
          gosub vier   
          
          goto ende
     
vier:     
          print("vier.", endl)
          return
drei:     
          print("drei, ")
          return
zwei:     
          print("zwei, ")
          return
eins:     
          print("Eins, ")
          return

ende:
  • Den Befehl "end", der am Ende dieses Kapitels zu finden ist und der das TTC-Skript an Ort und Stelle beendet, verwende ich bewusst nicht und wir springen zu einem Label am Ende des Programms und über die Subroutinen hinweg, sonst würden wir wieder in die Return-Befehle laufen, was die Virtual Machine empfindlich stören würde, weil der Return-Stack einen Unterlauf bekommt (und der aus Speed-Gründen auch nicht abgesichert ist).

Die kurzen Prozeduren GoSub_Statement() - sie sieht aus wie die von "goto" - sowie Return_Statement() bauen das Gosub-Return-Befehlspaar ein:

Code: Alles auswählen

  Procedure GoSub_Statement()
    
    ; Sprungmarken-Name holen und als ASM-Code ausgeben 
      Scanner::GetName()    
      EmitC("call   UL_"+UCase(Scanner::Lexem))    
    
    ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
      EmitX()
      
    ; Nächstes Token-Lexem-Paar laden  
      Scanner::GetToken()
      
  EndProcedure
  Procedure Return_Statement()
    
    ; ASM-Code ausgeben 
      EmitC("ret")    
    
    ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
      EmitX()
      
    ; Nächstes Token-Lexem-Paar laden  
      Scanner::GetToken()
      
  EndProcedure
  • Die Frage ist, warum ist da kein Unterschied zu "goto", außer natürlich der ASM-Befehl "call" statt "j", der das Unterprogramm anspringt.
    Ganz einfach: Das Merken der Rücksprungadresse ist Sache der VM, diese Tätigkeit wird nicht hier zur Compiletime, sondern während der Runtime durchgeführt.

Der ASM-Code unseres Programm-Beispiels:

Code: Alles auswählen

// GOSUB
        call        UL_EINS ----> Sprung zu [UL_EINS]   
                       
// GOSUB               <---- Rückkehr mit ret von UL_EINS    
        call        UL_ZWEI ----> Sprung zu [UL_ZWEI]       

// GOSUB               <---- Rückkehr mit ret von UL_ZWEI
        call        UL_DREI ----> Sprung zu [UL_DREI]       

// GOSUB               <---- Rückkehr mit ret von UL_DREI 
        call        UL_VIER ----> Sprung zu [UL_VIER]       

// GOTO                <---- Rückkehr mit ret von UL_VIER 
        j           UL_ENDE ----> Sprung zu [UL_ENDE]           

[UL_VIER]
        // print                
        spushc      "vier."     
        sout                    
        endl                    

// RETURN
        ret                     

[UL_DREI]
        // print                
        spushc      "drei, "    
        sout                    

// RETURN
        ret                     

[UL_ZWEI]
        // print                
        spushc      "zwei, "    
        sout                    

// RETURN
        ret                     

[UL_EINS]
        // print                
        spushc      "Eins, "    
        sout                    

// RETURN
        ret                     

[UL_ENDE]
  • Die Call-Befehle springen zu den entsprechenden Labels, die Ret-Befehle kehren zu dem Befehl zurück, der nach dem Aufruf steht.
Zuletzt geändert von puretom am 16.11.2013 00:29, insgesamt 41-mal geändert.
Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

6.8. Die bedingte Anweisung if und die Verzweigungen if-else und if-else if-else


6.8.1. Einige Vorbemerkungen

Jetzt, ja, jetzt kommen endlich die lang erwarteten If-Statements und dann die Schleifen.
Beides Kapitel, die mir persönlich besonders am Herzen liegen, weil sie mich weit in Themen meiner Jugend zurückführen ;-) .

Als ich begonnen habe, mich mit dem weiten Feld von Compilerprogrammierung auseinanderzusetzen (und da habe ich schon mit dem C64 als Kind herumprobiert eine Art DOS-Shell für die Floppy 1541 zu machen), haben mich eigentlich die If-then-Anweisung sowie Schleifen aller Art am meisten fasziniert, weil ich in meinem jugendlichen Alter lange nicht dahinterkommen konnte, wie leicht die Umsetzung letztlich von der Logik her ist.

Das Enzige natürlich, was die Sache "tricky" macht, ist das Herumschlagen mit der jeweiligen Assemblersprache des Zielprozessors, auf den ich kompiliere, aber keinesfalls die Logik von if-artigen Konstrukten oder von Schleifenkonstrukten selbst. Die ist - im Gegenteil - sehr einfach.

Wir kompilieren unseren Source-Code auf eine Virtual Machine, d.h. wir können für uns passende Maschinensprache-Befehle (=Opcodes, Operationscodes) erfinden, die uns die genau diesen Aspekt, d.i. Ausarbeitung der Strukturen in Assembler, stark vereinfachen werden.
Somit brauchen wir uns nur mehr um die Logik kümmern, was das Erlernen des Compilerprogrammierens stark vereinfacht, auch das ist ein Grund, warum ich als Zielmaschine eine Virtuelle Maschine gewählt habe.



6.8.2. If-Statement

If ermöglicht die Entscheidung. ob ein Statement ausgeführt wird oder nicht.
Sie ermöglicht also eine bedingte Anweisung.


Logischer Aufbau einer bedingten Anweisung (wieder einmal ASCII-Art :lol: ):

Code: Alles auswählen


   if (x=12) print("Die Zahl X war ZWÖLF.")
   print ("Ende des Programms.")


 .- BEDINGUNG (CONDITION) -----------------.
 |                                         |
 |  Prüfen   :   X=12 --> True oder False  |
 |                                         |      False?
 |  Springen :   wenn False                |----. 
 |                                         |    | => True-Block 
 '-----------------------------------------'    |    überspringen zur 
                                                |    passenden
 .- TRUE-Block ----------------------------.    |    Sprungmarke
 |                                         |    |
 |  print("Die Zahl X war ZWÖLF.")         |    |
 |                                         |    |
 '-----------------------------------------'    | 
                                                |
    Fuß-/False-Label/Sprungmarke:  <------------'

    STATEMENT danach

    print ("Ende des Programms.")

  • Ein if-Anweisung ist also folgendermaßen aufgebaut:
[*]Eine Condition (=Bedingung) (hier x=12) wird ausgewertet:

[*]Ist diese Condition true (richtig), dann wird eine 1 auf den Operanden-Stack gepusht.
[*]Ist diese Condition false (falsch), dann wird eine 0 auf den Operanden-Stack gepusht.

Liegt auf dem Stack eine 0 (also false), dann wird der True-Block übersprungen und das Statement unmittelbar danach ausgeführt.[/list]

Das ist das ganze Geheimnis von If![/list]


Programmieren wir also die Parse-Prozedur If_Statement() (Die Fußnoten dienen zum Vergleichen mit dem ASM-Code, der weiter unten kommt):

Code: Alles auswählen

  Procedure If_Statement()
        
  ; Token-Lexem "if" überspringen
  ; steht auf 1. Token-Lexem der Condition
    Scanner::GetToken()
  
  ; Labels erzeugen   
    l1=LabelNr:LabelNr+1
  
  ; Condition ermitteln, bei False -> True-Block überspringen
    EmitX("// IF")               ;(1)
    Condition()                  ;(2)
    Emit(ExpTyp,"JF","L_"+l1)    ;(3)
    EmitX()                      ;(4)
  
  ; True-Block  
    Emit(0,"// TRUE BLOCK")      ;(5)
    Statement()                  ;(6)
  
  ; Fuss-Label von if
    EmitX("// ENDIF")            ;(7)
    EmitX("[L_"+l1+"]")          ;(8)
    
  EndProcedure
  • Wow :D ! Ein If-Parser bestehend aus ganzen 10 Zeilen Code (von denen ein Paar für die reine If-Konstruktion gar nicht benötigt würden und nur der ASM-Code Behübschung dienen):
    1. Token-Lexem steht noch auf dem "if", überspringen.
    2. Ein Label (Sprungmarke) wird erzeugt mit der globalen Variable LabelNr.
    3. Die Condition (=Bedingung) wird durch die Prozedur Condition() ausgewertet (Erklärung folgt gleich).
      Am Ende dieser Auswertung liegt 0 (false) oder 1 (true) auf dem Operanden-Stack.
    4. Ausgabe des ASM-Befehls "JF", was so viel wie Jump if False to Label bedeutet.
      Ist also zur Runtime des Programms am Stack eine 0, dann springt er zum Label, das hinter "JF" genannt wird.
      Es ist genau das Label, das wir vorhin erzeugt haben. Dadurch wird der jetzt folgende True-Block übersprungen.
    5. Wir schwenken in den True-Block ein, indem wir natürlich wieder Statement aufrufen, sonst kann im True-Block ja kein Statement stehen.
      (Bitte beachten wir für folgende Überlegungen, dass derzeit im True-Block nur exakt 1 Statement stehen darf)
    6. Wir setzen nach dem True-Block unser oben erzeugtes Label, damit der True-Block übersprungen werden kann.
    Alles andere ist ASM-Code-Behübschung.

Die Prozedur Condition(), das ist wieder eine typische Aufruf-/Weiterleitungsprozedur - wir erinnern uns an MathExpression(), tut in TTC 0.5 (und vielleicht später auch ;-) ) nicht viel mehr, als den Typ der Expression auf Integer zu stellen, denn mehr kennt TTC noch nicht, und dann eine MathExpression() aufzurufen (nebst einer ASM-Code-Kommentierung):

Code: Alles auswählen

  Procedure Condition()
  
  ; Zur Übersichtlichkeit
    Emit(0,"// CONDITION")
    
  ; Eine Condition beginnt mit Math-Expression vom Typ Integer
    ExpTyp='i'
    MathExpression()
  
  EndProcedure

Kompilieren wir folgenen Source-Code:

Code: Alles auswählen

int x
if x=12 
     Print("X ist ZWÖLF.")
print("Das Programm ist zu Ende!")
  • Zu beachten ist, dass in TTC die Bedingung auch mit Klammer wie in "( x=12 )" geschrieben werden darf, aber anders als in C nicht muss.

Das ASM-Programm sieht so aus:

Code: Alles auswählen

// IF                                  ;(1)
        // CONDITION                   ;(2) in der Condition
        // math expression             ;(2) in der Condition/Math Expression
        ipushg      0           // x   ;(2) in der Condition/Math Expression
        ipushc      12          // 12  ;(2) in der Condition/Math Expression
        ieq                     // =   ;(2) in der Condition/Math Expression
        iJF         L_1                ;(3) bedingter Sprungbefehl (if false) 
                                       ;(4) Leerzeile EmitX()
        // TRUE BLOCK                  ;(5)
        // print                       ;(6) in anderem Statement()
        spushc      "X ist ZWÖLF."     ;(6) in anderem Statement()
        sout                           ;(6) in anderem Statement()
                                       ;(6) in anderem Statement()
// ENDIF                               ;(7)
[L_1]                                  ;(6) Fuß-Label
        // print                
        spushc      "Das Programm ist zu Ende!"
        sout                    
  • Besprechen wir die Situation durch:
    • Am Kopfder If-Anweisung befindet sich immer die Auswertung der Bedingung (=Condition).
      • Wichtig ist der Befehl "ieq" (sprich: Integer Equal): Er vergleicht die beiden obersten Integerwerte am Operandenstack (das sind hier x und 12).
        Sind sie gleich, pusht er 1 (true) auf den Stack, sind sie ungleich dann 0 (false).
      • der Befehl "iJF" (sprich: Integer Jump if False to Label L_1) holt den obersten Integerwert vom Stack.
        Ist er 1 (true), dann wird einfach im Code der Reihe nach weitergemacht.
        Ist er 0 (false), dann springt der Befehl nach "L_1", das ist unser erzeugtes Label. Er überspringt dadurch den True-Block.
    • Der Körper (Body) der If-Anweisung besteht hier aus dem True-Block.
    • "[L_1]": Der Fuß der If-Anweisung ist dann das Label, um den True-Block überspringen zu können.


6.8.3. If-else-Statement

Versuchen wir nun - als kleines Projekt - über die nächsten Unterkapitel hinweg ein kleines Zahlenratespiel zu programmieren, das immer ausgebauter wird, je mehr Kontrollkonstrukte TTC beherrscht!

Unser erstes Zahlenratespiel Version 1:

Code: Alles auswählen

/*************************************************
 * DAS ZAHLENRATESPIEL Version 1
 *************************************************/
 int zufallsZahl; int eingabeZahl
 
 print("Das Zahlenratespiel",endl)
 rand(zufallsZahl,100)  // 100 = Maximum

// Spielschleife
 schleife:

     print("Geben Sie eine Zahl zwischen 0 und 100 ein! (",zufallsZahl,")",endl)
     input(eingabeZahl)
 
     if zufallsZahl<eingabeZahl print("Meine Zahl ist kleiner.",endl,endl)
     if zufallsZahl>eingabeZahl print("Meine Zahl ist groesser.",endl,endl)
 
 if eingabeZahl<>zufallsZahl goto schleife
 
// Siegesmeldung 
 print("Sie haben gewonnen!",endl)
  • Den Befehl "rand" finden wir weiter unten implementiert, er erzeugt eine Zufallszahl.
    In der Print-Ausgabe "Geben Sie eine Zahl zwischen ..." ist am Ende in Klammer die Lösung für uns während des Testens (sonst raten wir ja ewig) ausgegeben.
    Wir wollen ja unseren Compiler testen und nicht wirklich ernsthaft Zahlen raten :lol: .

Wenn wir unser Zahlenratespiel kompilieren, dann sehen wir, dass die 2. If-Abfrage "zufallsZahl>eingabeZahl" auch dann durchgeführt wird, wenn die 1. Abfrage schon wahr gewesen ist.
Nur ist das extrem ineffizient, denn wenn die Zahl kleiner ist, dann kann sie nicht gleichzeitig größer sein. Das ist unnötig.

Ein Ausschnitt aus dem ASM-Code (nur die beiden if-Zahlenprüfungen):

Code: Alles auswählen

// IF
        // CONDITION            
        // math expression            
        ipushg      0           // zufallszahl
        ipushg      1           // eingabezahl
        ilt                     // <
        iJF         L_1         

        // TRUE BLOCK            
        // print                
        spushc      "Meine Zahl ist kleiner."
        sout                    
        endl                    
        endl                    

// ENDIF
[L_1]
// IF
        // CONDITION            
        // math expression            
        ipushg      0           // zufallszahl
        ipushg      1           // eingabezahl
        igt                     // >
        iJF         L_2         

        // TRUE BLOCK            
        // print                
        spushc      "Meine Zahl ist groesser."
        sout                    
        endl                    
        endl                    

// ENDIF
[L_2]
  • Wenn also die erste If-Anweisung true ist, dann wird trotzdem danach einfach mit der zweiten if-Anweisung weitergemacht, ohne diese auszulassen.


Die Lösung ist also, entweder das eine oder das andere auszuführen, das ist eine Idealanwendung für eine If-else-Anweisung:

Der Else-Block, das müssen wir kurz noch definieren, muss nicht vorhanden sein, er ist optional.

Code: Alles auswählen

"if" Condition Statement [ "else" Statement ]
Unser If-else-Statement schaut also grafisch dargestellt jetzt so aus:

Code: Alles auswählen


     if zufallsZahl<eingabeZahl print("Meine Zahl ist kleiner.",endl,endl)
     else                       print("Meine Zahl ist groesser.",endl,endl)


 .- BEDINGUNG (CONDITION) -----------------.
 |                                         |
 |  Prüfen   :   zufallsZahl<eingabeZahl   |
 |                                         |      False?
 |  Springen :   wenn False                |----. 
 |                                         |    | => True-Block 
 '-----------------------------------------'    |    überspringen zur 
                                                |    passenden
 .- TRUE-Block ----------------------------.    |    Sprungmarke
 |                                         |    |
 |  print("Meine Zahl ist kleiner."...)    |    |
 |                                         |    |
 |  Springen: Else-Block nicht ausführen   |---------. Wenn der True-Block aus-
 |                                         |    |    | geführt worden ist, 
 '-----------------------------------------'    |    | darf natürlich nicht auch
                                                |    | der Else-Block ausgeführt
    False/Else-Label/Sprungmarke:  <------------'    | werden.
                                                     |
 .- FALSE/ELSE-Block ----------------------.         | Der Else-Block muss über-
 |                                         |         | sprungen werden.
 |  print("Meine Zahl ist groesser."...)   |         |
 |                                         |         |
 '-----------------------------------------'         |
                                                     |
    Fuß-Label/Sprungmarke:  <------------------------'

    STATEMENT danach

Die erweiterte Prozedur If_Statement() (Die Fußnoten sind zum Vergleichen mit dem ASM-Code, der weiter unten kommt):

Code: Alles auswählen

  Procedure If_Statement()
        
  ; Token-Lexem "if" überspringen
  ; steht auf 1. Token-Lexem der Condition
    Scanner::GetToken()
  
  ; Labels erzeugen   
    l1=LabelNr:LabelNr+1
    l2=l1
  
  ; Condition ermitteln, bei False -> True-Block überspringen
    EmitX("// IF")               ;(1)
    Condition()                  ;(2)
    Emit(ExpTyp,"JF","L_"+l1)    ;(3)
    EmitX()                      ;(4)
  
  ; True-Block  
    Emit(0,"// TRUE BLOCK")      ;(5)
    Statement()                  ;(6)


  ; Else-Block vohanden?
    If Scanner::Lexem="else"
        l2=LabelNr:LabelNr+1
        Emit(0,"j","L_"+l2)    ;(7) "j" überspringt Else-Block
        EmitX("[L_"+l1+"]")    ;(8) <== hier geht der Else-Block los
        Emit(0,"// ELSE BLOCK");(9)
        Scanner::GetToken()  
        Statement()            ;(10)
    EndIf                       

  
  ; Fuss-Label von if
    EmitX("// ENDIF")          ;(11)
    EmitX("[L_"+l2+"]")        ;(12)
    
  EndProcedure
  • Besprechen wir diese meiner Meinung nach unerwartet kurze und unkomplizierte Prozedur - wenn wir bedenken, welch komplexes Problem sie löst - durch:
    1. Token-Lexem steht noch auf dem "if", überspringen.
    2. Zwei Labels (Sprungmarke) werden erzeugt.
      Beide Labels zeigen zunächst auf den Fuß (weil l2=l1), also das Ende von if.
    3. Die Condition (=Bedingung) wird durch die Prozedur Condition() ausgewertet.
      Am Ende dieser Auswertung liegt 0 (false) oder 1 (true) auf dem Operanden-Stack.
    4. Ausgabe des ASM-Befehls "JF", was so viel wie Jump if False to Label bedeutet.
      Wenn ein Else-Block vorhanden ist, ist das der Else-Block, sonst der Fuß.
      Ist also zur Runtime des Programms am Stack eine 0, dann springt er zum Label, das hinter "JF" genannt wird.
    5. Wir schwenken in den True-Block ein, indem wir natürlich wieder Statement aufrufen, sonst kann im True-Block ja kein Statement stehen.
      (Bitte beachten wir für folgende Überlegungen, dass derzeit im True-Block nur exakt 1 Statement stehen darf)
    6. Sollte ein Else-Block vorhanden sein (das wirkt jetzt von der Reihenfolge her verdreht, aber vergessen wir nicht, wir kompilieren, haben also Compile-Time und nicht Run-Time):
      • Wir benötigen ein neues Fußlabel (das war ja l1 vorher), denn wir haben es für den Sprung in den Else-Block verbraucht. Erzeugen wir es.
      • Wir setzen jetzt den Sprung an den Fuß - vergessen wir nicht, wir sind am Ende des True-Blocks, und wenn der ausgeführt worden ist, dann müssen wir jetzt noch vor dem Else-Block einen Sprung ausgeben, um den Else-Block zu überspringen.
      • Jetzt setzen wir das Else-Label l1, d.h. alles was wir bis jetzt hier gemacht haben, war noch am Ende des True-Blocks und vor dem Else-Block, der geht jetzt erst los.
      • Wir schwenken wieder in ein Statement() ein.
    7. Wir setzen nach dem Else-Block unser Fuß-Label l2 oder l1 (l2=l1).
      Das Label ist immer noch l1 (obwohl hier l2 steht - wir hatten oben ja l2=l1) oder es ist ein neues, das kurz vor dem Else-Block erzeugt worden ist.
    Alles andere ist wieder nur ASM-Code-Behübschung.


Ändern wir unseren Source-Code zu Zahlenratespiel Version 2 und kompilieren wir ihn:

Code: Alles auswählen

     if zufallsZahl<eingabeZahl print("Meine Zahl ist kleiner.",endl,endl)
     else                       print("Meine Zahl ist groesser.",endl,endl)
  • Jetzt wird entweder "Meine Zahl ist kleiner." oder "Meine Zahl ist größer." gedruckt.
    Ganz befriedigt uns das Ergebnis noch nicht, wenn wir es ausprobieren, denn wenn die Zahl erraten worden ist, dann druckt er trotzdem zusätzlich "Meine Zahl ist größer." aus (wir hätten gerne wie Pure Basic auch einen ElseIf-Block), aber implementieren wir einmal einen funktionierenden Else-Block und dann denken wir danach die Sache weiter.
Wir erhalten folgende ASM-Ausgabe der If-else-Anweisung:

Code: Alles auswählen

// IF                                          ;(1)
        // CONDITION                           ;(2) in der Condition
        // math expression                     ;(2) in der Condition/Math Expression
        ipushg      0           // zufallszahl ;(2) in der Condition/Math Expression
        ipushg      1           // eingabezahl ;(2) in der Condition/Math Expression
        ilt                     // <           ;(2) in der Condition/Math Expression
        iJF         L_1                        ;(3) bedingter Sprungbefehl (if false) 
                                               ;(4) Leerzeile EmitX()
        // TRUE BLOCK                          ;(5)
        // print                               ;(6) in anderem Statement()
        spushc      "Meine Zahl ist kleiner."  ;(6) in anderem Statement()
        sout                                   ;(6) in anderem Statement()
        endl                                   ;(6) in anderem Statement() 
        endl                                   ;(6) in anderem Statement()    
                                               ;(6) in anderem Statement()
        j           L_2                        ;(7) überspringt Else-Block
[L_1]                                          ;(8)
        // ELSE BLOCK                          ;(9)
        // print                               ;(10) in anderem Statement()
        spushc      "Meine Zahl ist groesser." ;(10) in anderem Statement()
        sout                                   ;(10) in anderem Statement()
        endl                                   ;(10) in anderem Statement()
        endl                                   ;(10) in anderem Statement()
                                               ;(10) in anderem Statement()
// ENDIF                                       ;(11)
[L_2]                                          ;(12) Fuß-Label

6.8.4. If-else if-else-Statement

Ich habe den Code kurz in Pure Basic umgesetzt, um zu zeigen, was wir eigentlich wünschen würden.

Code: Alles auswählen

    If zufallsZahl<eingabeZahl
        Print("Meine Zahl ist kleiner."+#CRLF$+#CRLF$)        
    ElseIf zufallsZahl>eingabeZahl
        Print("Meine Zahl ist groesser."+#CRLF$+#CRLF$)
    EndIf    
Diesen Code können wir in Pure Basic auch folgendermaßen formulieren:

Code: Alles auswählen

    If zufallsZahl<eingabeZahl
        Print("Meine Zahl ist kleiner."+#CRLF$+#CRLF$)        
    Else: If zufallsZahl>eingabeZahl
              Print("Meine Zahl ist groesser."+#CRLF$+#CRLF$)
          EndIf
    EndIf    
  • Das sieht in Pure Basic furchtbar seltsam aus, aber wie sieht das in einer C-ähnlichen Syntax wie der von TTC aus?

Ändern wir also unseren Source-Code folgendermaßen zu Zahlenratespiel Version 3 und kompilieren wir ihn:

Code: Alles auswählen

     if      (zufallsZahl<eingabeZahl) print("Meine Zahl ist kleiner.",endl,endl)
     else if (zufallsZahl>eingabeZahl) print("Meine Zahl ist groesser.",endl,endl)
  • So sieht das also in TTC aus und das sieht nicht mehr so komisch, eigentlich überhaupt nicht mehr komisch aus (Diesmal freiwillig mit Klammern um die Condition wegen der Übersichtlichkeit)!
Wir erhalten folgenden ASM-Code unseres Source-Programms:

Code: Alles auswählen

// IF
        // CONDITION            
        // math expression            
        ipushg      0           // zufallszahl
        ipushg      1           // eingabezahl
        ilt                     // <
        iJF         L_1         

        // TRUE BLOCK            
        // print                
        spushc      "Meine Zahl ist kleiner."
        sout                    
        endl                    
        endl                    

        j           L_2         
[L_1]
        // ELSE BLOCK            
// IF
        // CONDITION            
        // math expression            
        ipushg      0           // zufallszahl
        ipushg      1           // eingabezahl
        igt                     // >
        iJF         L_3         

        // TRUE BLOCK            
        // print                
        spushc      "Meine Zahl ist groesser."
        sout                    
        endl                    
        endl                    

// ENDIF
[L_3]
// ENDIF
[L_2]
  • Wir haben es, wie im "hässlichen" PB-Beispiel natürlich mit einem If in einem If zu tun, was aber für den TTC-Programmierer nicht spürbar ist (außer dass else von if mit einem Leerzeichen getrennt werden muss).
    Man merkt dieses Tatsache erst im ASM-Code -> man beachte die 2 (!) Endif-Fuß-Labels, es sind definitiv 2 If-Statements, die ineinander verschachtelt sind!
    • Tatsächlich machen es die Big-C-Boys genauso, es gibt dort keine ElseIf-Schlüsselwort.
    • Die Logik dahinter entsteht durch das Parsen eines Statements(), in dem ein If-Statement ist, automatisch.
      Wir könnten unendlich weiter verschachteln, als TTC- oder auch C#- oder Cpp-Programmierer merke ich gar nicht, dass ich Ifs in Ifs verschachtele.
    • Programmiersprachen mit einer PB-ähnlichen Philosophie der Block-Ende-Schlüsselwörter sind etwas schwerer zu parsen (vielleicht zeige ich mal den Unterschied) und benötigen im If_Statement() eine Abfrage, die genauso wie die nach "else" aussieht und ein "elseif" bearbeitet.
    • Wir hier - in einer C-ähnlichen Syntax - tun einfach nichts, "else if" ist automatisch unmerkbar für uns bereits eingebaut.


Im Internet finden sich großartigen Artikel über das meiner Meinung nach nicht vorhandene bzw. künstliche "Problem" des sogenannten hängenden Else (dangling else).
Die große Frage ist, wohin in unserem Beispiel unten das Else gehört, wenn es keine Endif-Schlüsselwörter gibt.
Einfache Antwort: logischerweise natürlich und selbstverständlich immer zum vorigen If, denn in dessen Parse-Prozedur fragt der Compiler ja nach "else".
Da gibt es nichts zu diskutieren, wenn man weiß, wie der Compiler das parst.

Ein abschließendes Beispiel soll zeigen, dass der Compiler sich auch hier nicht verhaspelt und alle Sprungmarken richtig sind:

Code: Alles auswählen

int i; int x
if      (i=0) x=0
else if (i=1) x=10
else if (i=2) x=20
else          x=99

Code: Alles auswählen


          // IF
                  // CONDITION            
                  // math expression            
                  ipushg      0           // i
                  ipushc      0           // 0
                  ieq                     // =
                  iJF         L_1 ----------------.        
                                                  |
                  // TRUE BLOCK                   |
                  // assignment                   |
                  // math expression              |
                  ipushc      0           // 0    |
                  ipullg      1           // =x   |
                                                  |
    .-------------j           L_2                 |
    |     [L_1]                                   v
    |             // ELSE BLOCK       ;<== HIER SIEHT MAN SEHR SCHÖN, DASS _IM_ ELSE-BLOCK       
    |     // IF                       ;    DES 1. IFs EIN WEITERS IF BEGINNT
    |             // CONDITION            
    |             // math expression            
    |             ipushg      0           // i
    |             ipushc      1           // 1
    |             ieq                     // =
    |             iJF         L_3 ----------------.        
    |                                             |
    |             // TRUE BLOCK                   |
    |             // assignment                   |
    |             // math expression              |
    |             ipushc      10          // 10   |
    |             ipullg      1           // =x   |
    |                                             |
    | .-----------j           L_4                 |
    | |   [L_3]                                   v
    | |           // ELSE BLOCK       ;<== HIER SIEHT MAN SEHR SCHÖN, DASS _IM_ ELSE-BLOCK          
    | |   // IF                       ;    DES 2. IFs EIN WEITERS IF BEGINNT
    | |           // CONDITION            
    | |           // math expression            
    | |           ipushg      0           // i
    | |           ipushc      2           // 2      ;<== DAS IF MIT '2' UND (siehe unten) ...
    | |           ieq                     // =
    | |           iJF         L_5 ----------------.         
    | |                                           |
    | |           // TRUE BLOCK                   |          
    | |           // assignment                   |
    | |           // math expression              |
    | |           ipushc      20          // 20   | ;<== ... MIT '20'
    | |           ipullg      1           // =x   |
    | |                                           |
    | | .---------j           L_6                 |
    | | | [L_5]                                   v
    | | |         // ELSE BLOCK       ;<== EINDEUTIG: DER LETZTE ELSE-BLOCK mit '99' GEHÖRT      
    | | |         // assignment       ;    ZUM VORIGEN IF mit '2' und '20'
    | | |         // math expression            
    | | |         ipushc      99          // 99
    | | |         ipullg      1           // =x
    | | |
    | | | // ENDIF     ;<== MAN BEACHTE DIE 3 FUSS-LABELS DER 3 IF's             
    | | '>[L_6]        ; Jetzt geht es im Rücklauf durch die Prozeduren  
    | |   // ENDIF
    | '-->[L_4]        ; d.h. zuerst wird das letzte If, dann das vorletzte und dann das 
    |     // ENDIF     ; erste If mit seinem Fußlabel geschlossen, deshalb überspringt er auch
    '---->[L_2]        ; korrekt ALLE else if, wenn z.B. schon das erste (i=0) korrekt
                       ; gewesen ist (einfach L_2, L_4, L_6 verfolgen!)

  • Auf ASM-Code-Ebene sieht man die ineinandergeschachtelten If-else-Blöcke sehr schön.
    Im Source-Code sieht man es jedenfalls nicht und man meint in C-ähnlichen Sprachen ein "echtes" ElseIf wie in PB zu haben, hat aber tatsächlich keins.
Zuletzt geändert von puretom am 17.11.2013 12:40, insgesamt 50-mal geändert.
Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
puretom
Beiträge: 109
Registriert: 06.09.2013 22:02

Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

Beitrag von puretom »

6.9. Blocks von Statements und Schleifen


6.9.1. Blocks von Statements

Wir arbeiten noch immer an unserem Projekt Zahlenratespiel.
Machen wir kurz ein Update und schauen wir uns die Version 3 nochmals an:

Code: Alles auswählen

/*************************************************
 * DAS ZAHLENRATESPIEL Version 3
 *************************************************/
 int zufallsZahl; int eingabeZahl
 
 print("Das Zahlenratespiel",endl)
 rand(zufallsZahl,100)  // 100 = Maximum

// Spielschleife
 schleife:

     print("Geben Sie eine Zahl zwischen 0 und 100 ein! (",zufallsZahl,")",endl)
     input(eingabeZahl)
 
     if      (zufallsZahl<eingabeZahl) print("Meine Zahl ist kleiner.",endl,endl)
     else if (zufallsZahl>eingabeZahl) print("Meine Zahl ist groesser.",endl,endl)
 
 if eingabeZahl<>zufallsZahl goto schleife
 
// Siegesmeldung 
 print("Sie haben gewonnen!",endl)
Ich denke, es wird Zeit, dass wir das Label und das Goto durch ordentliche strukturierte Konstrukte eliminieren.

Die Schleife, die wir hier vorliegen haben, ist fußgesteuert, d.h. es wird am ENDE des Durchlaufs entschieden, ob weitergemacht werden soll.
Die Bedingung in unserem Programm ist: Mache weiter, solange eingabeZahl ungleich zufallsZahl ist.
Das ist eine Idealanwendung der C-Schleife do..while.


Doch jetzt tauchen erste Probleme auf!

Wir werden die Schleife folgendermaßen aufbauen:
  • Schleifenkopf: Kopflabel setzen
  • Schleifenkörper: Statement() aufrufen
  • Schleifenfuß:
    • Condition prüfen
    • iJT zu Kompflabel, wenn die Condition stimmt

Das Problem liegt bei Statement().
Es ist nur exakt 1 Statement erlaubt (ich habe weiter oben mehrfach darauf hingewiesen).
Das ist zu wenig!

Wir haben viel mehr Statements innerhalb des Schleifenkörpers.


Lösen wir das Problem und führen wir damit endlich Blöcke von Statements ein, was ohnehin schon lange überfällig ist.

Wir befehlen dem Compiler mittels einer geöffneten geschwungenen Klammer "{", dass er in einem Statement mehrere Statements erwarten soll, und zwar so lange, bis er ein "}" findet.
Wir erklären der Prozedur Statement(), dass sie einen Block von Statements vor sich hat, wenn ein "{" als Lexem erscheint.

Die Prozedure Statement() ist in ihrer voller Pracht weiter oben abgebildet, ich gebe hier einen kleinen Ausschnitt wieder:

Code: Alles auswählen

  ; // je nach Statement Aktionen setzen //
    Select Scanner::Lexem
        
  ; Statements  
    Case "{"        : Block()                ; <=== HIER IST DIE ERWEITERUNG !!!
    Case "int"      : Declare_Statement('i')
    Case "if"       : If_Statement()
   ;...
    Case "end"      : End_Statement()  
    Case "rand"      : Rand_Statement()   
    
    Default         : If Scanner::Look=':'     ; -- Sprungmarke (Label) --
                        EmitX("[UL_"+UCase(Scanner::Lexem)+"]")   
                        Scanner::GetOther()        ; ':' holen
                        Scanner::GetToken()        ; nächstes Token-Lexem
                    
                      Else
                        Assignment_Statement() ; -- Assignment -- 
          
                      EndIf
                                        
    EndSelect       
  • Ein "{" beginnt also einen Block (of Statements).
Wir benötigen dazu klarerweise auch die Prozedur Block(), die die Verwaltung der "{" und "}" übernimmt:

Code: Alles auswählen

  Procedure Block()
   
  ; --> in Token-Lexem ist jetzt '{'   
  
  ; ueberlesen des '{'
    Scanner::GetToken()     
  
  ; solange kein '}' 
    While ( Scanner::Token<>'}' )       
      Statement()      
      If Scanner::Token=0:Error("Block()",#Block_unbeendet): EndIf; 0-Byte vor '}'
    Wend        
  
  ; ueberlesen des '}'
    Scanner::GetToken()         
  
  ; --> in Token-Lexem ist jetzt das Token-Lexem exakt nach '}'
        
  EndProcedure
  • Besprechen wir kurz, wie Block arbeitet:
    • Statement() erkennt ein "{" und ruft Block() auf
    • In Token/Lexem ist immer noch "{" -> GetToken() überspringt das.
    WHILE solange bis Token="}"
    • 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!
    • Sollte Token einmal 0 (=Source-Code-Ende) sein, dann wurde der Block nie geschlossen, denn am Ende eines Blocks muss ein "}" sein.
      Es wird eine passende Fehlermeldung (siehe Fehlercodes) ausgegeben.
    ENDWHILE
    • In Token/Lexem ist "}" -> GetToken() überspringt das.
      Das erklärt auch, warum die Debug-Anzeige von Pure-Basic 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.

So, mehr braucht es auch wirklich nicht, das war es schon, unglaublich einfach, nicht?
Wir haben jetzt funktionierende Blocks und können innerhalb eines Blocks beliebig viele Statements (und darin wieder Blocks und darin wieder Statements mit darin wieder Blocks ...) verwenden!
Machen wir uns auf, um TTC ein reichhaltiges Sortiment an Schleifen zu schenken.



6.9.2. Die Schleifen Do-While, Do-Until, Do: Das fußgesteuerte Universalgenie

Die Do-Schleife ist fußgesteuert, das heißt, dass die Condition erst am Ende (=am Fuß) der Schleife, nach dem ersten Durchlauf, überprüft wird.


do ... while


Das Zahlenratespiel Version 4 sieht mit einer Do-While-Schleife folgendermaßen aus:

Code: Alles auswählen

/*************************************************
 * DAS ZAHLENRATESPIEL Version 4 (mit Do-While)
 *************************************************/
 int zufallsZahl; int eingabeZahl
 
 print("Das Zahlenratespiel",endl)
 rand(zufallsZahl,100)  // 100 = Maximum

// Spielschleife
 do
 {
     print("Geben Sie eine Zahl zwischen 0 und 100 ein! (",zufallsZahl,")",endl)
     input(eingabeZahl)
 
     if      (zufallsZahl<eingabeZahl) print("Meine Zahl ist kleiner.",endl,endl)
     else if (zufallsZahl>eingabeZahl) print("Meine Zahl ist groesser.",endl,endl)
 
 } while eingabeZahl<>zufallsZahl 
 
// Siegesmeldung 
 print("Sie haben gewonnen!",endl)
  • Es wird zu do gesprungen, solange eingabeZahl<>zufallsZahl.

Nehmen wir zum Entwerfen der Struktur der Do-While-Schleife eine einfachere Angabe als das Zahlenratespiel:

Code: Alles auswählen

    int x
    do
    { 
        x=x+1 
        print(x)
    } 
    while x<10

 
    Kopf-/True-Label:  <------------------------.
                                                |
 .- SCHLEIFENKÖRPER-Block -----------------.    |
 |                                         |    |
 |  x=x+1                                  |    |
 |                                         |    | 
 |  print(x)                               |    | 
 |                                         |    | 
 '-----------------------------------------'    |    
                                                |    
 .- BEDINGUNG (CONDITION) -----------------.    | => Sprung zu 
 |                                         |    |    Schleifen-Kopf
 |  Prüfen   :   X<10 --> True oder False  |    |
 |                                         |    |   
 |  Springen :   wenn True                 |----' True?
 |                                         |                    
 '-----------------------------------------'                          
                                                  
    STATEMENT danach

  • Der Unterschied zu früher ist, dass die Sprungbedingung true und nicht false ist wie bisher in der If-Anweisung.
    Wir benötigen einen neuen ASM-Opcode und werden deshalb iJT (Integer Jumpf if True) erfinden.

Die Prozedur Do_Statement() ist sehr geradlinig:

Code: Alles auswählen

  Procedure Do_Statement()
  
  ; "do" überspringen -> auf 1. Token-Lexem des nächsten Statements
    Scanner::GetToken()  
  
  ; Labels erzeugen   
    l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "do"
  
  ; Schleifen-Kopf
    EmitX("[L_"+l1+"]")  
    EmitX("// DO")
    
  ; Schleifen-Körper    
    Statement()
        
  ; Schleifen-Fuß:
  ; "while" überspringen
    Scanner::GetToken()
    Emit(0,"// DO ... WHILE")        
    Condition()        
    Emit(ExpTyp,"JT","L_"+l1)  
  
  ; Ende-Markierung mit Leerzeile      
    EmitX("// END DO")  
    EmitX()
    
  EndProcedure
  • Wie oben schon erwähnt, ist der Unterschied ist, dass die Sprungbedingung true und nicht false ist.
    Darüber hinaus, denke ich, erklärt sich die Prozedur von selbst.

Kompilieren wir jetzt die Angabe:

Code: Alles auswählen

[L_1] <-------------------------------.
// DO                                 |
        // assignment                 |
        // math expression            |
        ipushg      0           // x  |
        ipushc      1           // 1  |
        iadd                    // +  |
        ipullg      0           // =x |
                                      |
        // print                      |
        ipushg      0           // x  |
        iout                          |
                                      |
        // DO ... WHILE               |
        // CONDITION                  |
        // math expression            |
        ipushg      0           // x  |
        ipushc      10          // 10 |
        ilt                     // <  |
        iJT         L_1 --------------' Wenn Condition true
// END DO
do ... until


Was wir jetzt entwerfen, das kennt C gar nicht.
Es ist eine Schleife, die der Repeat-Until-Schleife von Pure Basic entspricht.
Sie ist so einfach hinzuzufügen, dass ich es mir nicht verkneifen kann.

Das Zahlenratespiel Version 4 sieht mit einer Do-Until-Schleife folgendermaßen aus:

Code: Alles auswählen

/*************************************************
 * DAS ZAHLENRATESPIEL Version 4 (mit Do-Until)
 *************************************************/
 int zufallsZahl; int eingabeZahl
 
 print("Das Zahlenratespiel",endl)
 rand(zufallsZahl,100)  // 100 = Maximum

// Spielschleife
 do
 {
     print("Geben Sie eine Zahl zwischen 0 und 100 ein! (",zufallsZahl,")",endl)
     input(eingabeZahl)
 
     if      (zufallsZahl<eingabeZahl) print("Meine Zahl ist kleiner.",endl,endl)
     else if (zufallsZahl>eingabeZahl) print("Meine Zahl ist groesser.",endl,endl)
 
 } until eingabeZahl=zufallsZahl 
 
// Siegesmeldung 
 print("Sie haben gewonnen!",endl)
  • Es wird zu do gesprungen, bis eingabeZahl=zufallsZahl.

Nehmen wir auch hier zum Entwerfen der Struktur der Do-Until-Schleife eine einfachere Angabe als das Zahlenratespiel:

Code: Alles auswählen

    int x
    do
    {    
        x=x+1 
        print(x)
    } 
    until x=10

 
    Kopf-/False-Label:  <-----------------------.
                                                |
 .- SCHLEIFENKÖRPER-Block -----------------.    |
 |                                         |    |
 |  x=x+1                                  |    |
 |                                         |    | 
 |  print(x)                               |    | 
 |                                         |    | 
 '-----------------------------------------'    |    
                                                |    
 .- BEDINGUNG (CONDITION) -----------------.    | => Sprung zu 
 |                                         |    |    Schleifen-Kopf
 |  Prüfen   :   X=10 --> True oder False  |    |
 |                                         |    |   
 |  Springen :   wenn False                |----' False?
 |                                         |                    
 '-----------------------------------------'                          
                                                  
    STATEMENT danach

  • Mit Until haben wir also wieder eine Prüfung auf false.

Die Prozedur Do_Statement() ist leicht um diesen Fall erweitert:

Code: Alles auswählen

  Procedure Do_Statement()
  
  ; "do" überspringen -> auf 1. Token-Lexem des nächsten Statements
    Scanner::GetToken()  
  
  ; Labels erzeugen   
    l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "do"
  
  ; Schleifen-Kopf
    EmitX("[L_"+l1+"]")  
    EmitX("// DO")
    
  ; Schleifen-Körper    
    Statement()
        
  ; Schleifen-Fuß:
  
  ; "until" vorhanden?
    If Scanner::Lexem="until"        
        Scanner::GetToken()
        Emit(0,"// DO ... UNTIL")        
        Condition()        
        Emit(ExpTyp,"JF","L_"+l1)
        
  ; "while" vorhanden?
    ElseIf Scanner::Lexem="while"
        Scanner::GetToken()
        Emit(0,"// DO ... WHILE")        
        Condition()        
        Emit(ExpTyp,"JT","L_"+l1)
  
    EndIf  
  
  ; Ende-Markierung mit Leerzeile      
    EmitX("// END DO")  
    EmitX()
    
  EndProcedure
  • Wir brauchen also nur prüfen, ob ein While oder ein Until dem Schleifen-Körper folgt.
    Beachten wir, dass die Sprungbedingung bei Until false und bei While true ist.

Kompilieren wir jetzt die Angabe für die Do-Until-Schleife:

Code: Alles auswählen

[L_1] <-------------------------------.
// DO                                 |
        // assignment                 |
        // math expression            |
        ipushg      0           // x  |
        ipushc      1           // 1  |
        iadd                    // +  |
        ipullg      0           // =x |
                                      |
        // print                      |
        ipushg      0           // x  |
        iout                          |
                                      |
        // DO ... UNTIL               |
        // CONDITION                  |
        // math expression            |
        ipushg      0           // x  |
        ipushc      10          // 10 | 
        ieq                     // =  |
        iJF         L_1 --------------' Wenn Condition false       
// END DO
do ... (forever)


Entwerfen wir noch ein Äquivalent zur Repeat-Forever-Schleife von Pure Basic, wenn wir schon im Laufen sind.
Das kennt C in der Form auch nicht.
Obwohl TTC tiny ist, muss ich einfach noch schnell diese Schleife mit nur effektiv 2 (!!!) ;-) Zeilen PB-Code hinzufügen (Rest ist Code-Verschönerung und Kommentierung).

Diese Schleife hat keine Abbruchbedingung, sie läuft also ewig.

Auch sie ist leicht in die bisherige Prozedur Do_Statement() einzubauen.
Es ist der einfache Fall, dass dem Schleifenkörper weder ein While noch ein Until folgt. Es gibt schlichtweg keine Condition.


Die Prozedur Do_Statement() mit einer unendlichen Schleife:

Code: Alles auswählen

  Procedure Do_Statement()
  
  ; "do" überspringen -> auf 1. Token-Lexem des nächsten Statements
    Scanner::GetToken()  
  
  ; Labels erzeugen   
    l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "do"
  
  ; Schleifen-Kopf
    EmitX("[L_"+l1+"]")  
    EmitX("// DO")
    
  ; Schleifen-Körper    
    Statement()
        
  ; Schleifen-Fuß:
  
  ; "until" vorhanden?
    If Scanner::Lexem="until"        
        Scanner::GetToken()
        Emit(0,"// DO ... UNTIL")        
        Condition()        
        Emit(ExpTyp,"JF","L_"+l1)
        
  ; "while" vorhanden?
    ElseIf Scanner::Lexem="while"
        Scanner::GetToken()
        Emit(0,"// DO ... WHILE")        
        Condition()        
        Emit(ExpTyp,"JT","L_"+l1)
  
  ; unendliche Schleife
    Else
        Emit(0,"// DO ... FOREVER")        
        Emit(0,"j","L_"+l1)  
      
    EndIf  
  
  ; Leerzeile       
    EmitX("// END DO")  
    EmitX()
    
  EndProcedure
Testen wir schnell folgenden Source-Code:

Code: Alles auswählen

int x
do
{
    x=x+1
    print(x)
}
    Das ergibt als ASM-Code:

    Code: Alles auswählen

    [L_1] <-------------------------------.
    // DO                                 |
            // assignment                 |
            // math expression            |
            ipushg      0           // x  |
            ipushc      1           // 1  |
            iadd                    // +  |
            ipullg      0           // =x |
                                          |
            // print                      |
            ipushg      0           // x  |
            iout                          |
                                          |
            // DO ... FOREVER             | springt 
            j           L_1 --------------' IMMER        
    // END DO
    
    • Es wird immer zu do gesprungen, ohne Chance, jemals zu enden.


    6.9.3. Die While-Schleife: Der kopfgesteuerte Klassiker

    Eine der berühmtesten Schleifen fehlt uns noch. Es ist die Schleife, die auch der in PB geschriebene TTC-Compiler am häufigsten einsetzt.
    Es ist dies die kopfgesteuerte While-Schleife.

    Bei einer kopfgesteuerten Schleife wird die Condition (Bedingung) als Erstes geprüft und erst dann entschieden, ob der Schleifenkörper durchlaufen werden soll.
    Es kann also sehr gut sein, dass die Schleife nie durchlaufen wird, was bei den von uns zuvor eingebauten nicht passieren kann, sie werden zumindest 1-mal durchlaufen.

    Eine While-Schleife sieht eigentlich genauso aus wie ein If-Konstrukt. Der einzige Unterschied ist, dass zum Kopf der Abfrage zurückgekehrt wird.

    Aufbau einer While-Schleife wieder an einem einfachen Beispiel getestet:

    Code: Alles auswählen

       int x
       x=0
    
       while x<10
       {   
           print(x)
           x=x+1
       }	
    
    
        Kopf-Label: <-----------------------------.
                                                  |
     .- BEDINGUNG (CONDITION) -----------------.  |
     |                                         |  |
     |  Prüfen   :   X<10 --> True oder False  |  |
     |                                         |  |   False?
     |  Springen :   wenn False                |----. 
     |                                         |  | | => Schleifen-Körper  
     '-----------------------------------------'  | |    überspringen zur 
                                                  | |    passenden
     .- SCHLEIFENKÖRPER-Block -----------------.  | |    Sprungmarke
     |                                         |  | |
     |  print(x)                               |  | |
     |                                         |  | |
     |  x=x+1                                  |  | |
     |                                         |  | |
     |  Springen:    immer, zum Kopf           |--' |
     |                                         |    |
     '-----------------------------------------'    | 
                                                    |
        Fuß-/False-Label: <-------------------------'
    
        STATEMENT danach
    
    Wir implementieren diese Schleife jetzt genau so, wie sie grafisch dargestellt ist.
    Es gäbe noch eine andere Möglichkeit - man könnte die Abfragen im ASM-Code umstellen, was für den TTC-Programmierer nicht zu merken wäre, damit würde sie deutlich an Effizienz gewinnen, aber um der Klarheit willen wählen wir jetzt einmal den geraden Weg (und merken uns die While-Schleife als Optimierungskandidaten).


    Auch die Prozedur While_Statement ist kurz und verständlich:

    Code: Alles auswählen

     
      Procedure While_Statement()
      
      ; "while" überspringen, steht auf 1. Token-Lexem der Condition
        Scanner::GetToken()  
      
      ; Labels erzeugen   
        l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "while"
        l2=LabelNr:LabelNr+1 ; für Schleifen-Ende "wend"
    
      ; Schleifen-Kopf, Condition, False -> Schleifen-Körper überspringen
        EmitX("[L_"+l1+"]")
        EmitX("// WHILE")
        Condition()
        Emit(ExpTyp,"JF","L_"+l2)
        EmitX()
        
      ; Schleifen-Körper
        Statement()
      
      ; Schleifen-Fuß -> springe zu Kopf
        Emit(0,"j","L_"+l1)  
        
      ; Schleifen-Ende
        EmitX("// WEND")    
        EmitX("[L_"+l2+"]")  
        
      EndProcedure
    
    Unser Beispiel aus der Grafik sieht kompiliert zu ASM-Code so aus (nur die While-Schleife dargestellt):

    Code: Alles auswählen

    [L_1]
    // WHILE
            // CONDITION            
            // math expression            
            ipushg      0           // x
            ipushc      10          // 10
            ilt                     // <
            iJF         L_0         
    
            // print                
            ipushg      0           // x
            iout                    
    
            // assignment            
            // math expression            
            ipushg      0           // x
            ipushc      1           // 1
            iadd                    // +
            ipullg      0           // =x
    
            j           L_1         
    // WEND
    [L_0]
    

    6.9.4. Die For-Schleife, kein Befehl für TTC 0.5

    Um gleich vorweg die Enttäuschung zu nehmen, wir werden in TTC 0.5 KEINE For-Schleife implementieren.
    Nicht, dass sie schwer zu verstehen wäre, ich erkläre sie unten, aber es erfordert einen kleinen Eingriff in die Code-Ausgabe (Emit), den ich hier in V0.5 aus Gründen der Übersichtlichkeit nicht vornehmen möchte.

    Die For-Schleife stellt uns vor eine Entscheidung, nämlich vor die, welche Syntax wir ihr geben werden.
    Ich werde eine vereinfachte C-Syntax wählen, wobei wir auch die Semikolons ";" zwischen den einzelnen Bedingungen in der For-Anweisung setzen sollten, aber vergessen wird nicht, wir bräuchten keine, denn sie sind White Characters.


    Syntax der For-Schleife:

    Code: Alles auswählen

    
         for (  Assignment   [;]   Expression   [;]   Assignment  )  Statement
                     ^                     ^             ^
                     |                     |             |
    Initialisation --'    Ende-Bedingung --'             '-- Schrittweite
                          und zwar solange diese
                          Bedingung true ist
                          ==> while
    
    Eine For-Schleife ist nichts anderes als eine übersichtlich bzw. bloß anders gestaltete While-Schleife. Von der Funktionalität her benötigen wir sie in der Sprache eigentlich nicht.

    Jede For-Schleife kann in eine While-Schleife übertragen werden:

    Code: Alles auswählen

    for ( x=1 ; x<11 ; x=x+1 )
    {
        print(x)
    }
    Das entspricht folgender While-Schleife:

    Code: Alles auswählen

    x=1
    while (x<11)
    {
        print(x)
        x=x+1
    }
    
    Jetzt sehen wir vielleicht auch, warum ich die For-Schleife jetzt noch nicht einbaue.
    Wir müssten den ASM-Code der Schrittweite zwischenspeichern z.B. in einer List oder Ähnlichem, denn sie befindet sich ja am Schleifen-Kopf.
    Dann kommt der Schleifen-Körper und danach müssten wir erst am Schleifen-Fuß den zwischengespeicherten Code ausgeben.
    Dazu brauchen wir eine kleine Veränderung der Code-Ausgabe - nicht schwer und nicht viel -, aber ich möchte das Tutorial nicht schon wieder mit Einzelgenialitäten überfrachten.



    6.9.5. Break und continue

    Break ermöglicht, eine Schleife jederzeit sofort zu verlassen, während continue sofort an den Schleifenkopf springt.
    Die beiden Befehle simulieren also ein "goto Schleifenfuß" bzw. ein "goto Schleifenkopf", wenn man es anders sagen will.

    Um diesen Befehl zu implementieren, benötigt man einen Stapel, also einen Stack, denn Schleifen könnten ja ineinander verschachtelt sein und dann soll break ja immer nur die letzte innerste Schleife verlassen bzw. continue am Kopf der letzten innersten Schleife weitermachen.
    Die jeweiligen Labels müssen auf diesem Stack gespeichert werden, damit break und continue ein Ziel haben.

    Crenshaw zeigt in seinem Text, dass ein externer Stack eigentlich gar nicht nötig wäre, weil man die Labels auch als Procedure-Parameter weiterschicken kann, dann ensteht durch die Aufrufreihenfolge der Prozeduren wie von selbst ein Stack.

    Wir gehen diesen Weg bewusst nicht, weil wir - es geht einfach so einfach ;-) - auch einen Break-Level (z.B. break 2) wie in Pure Basic ermöglichen wollen.

    Wir fügen also unserem Parser eine globale Linked List hinzu:

    Code: Alles auswählen

      ; --- break, continue verwalten ---    
        Structure ls
            Continue_.i
            Break_.i
        EndStructure
        Global NewList LabelStack.ls()        
    
    • In dieser Linked List mit dem sinnvollen Namen LabelStack speichern wir jeweils das letzte Label-Ziel für continue und für break.

    Wir müssen nun in allen Schleifen, also immer wenn es ein Label an einem Schleifenkopf sowie an einem Schleifenfuß gibt, unsere LabelStack-Verwaltung hinzufügen.


    Die Veränderungen in der Prozedur Do_Statement() sind erstaunlich gering:

    Code: Alles auswählen

      Procedure Do_Statement()  ; mit Break, Continue
      
      ; "do" überspringen -> auf 1. Token-Lexem des nächsten Statements
        Scanner::GetToken()  
      
      ; Labels erzeugen, Label-Stack füllen   
        l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "do"
        l2=LabelNr:LabelNr+1 ; für Schleifen-Fuß "until, while, end do"
        Add_ContinueBreakLabel(l1,l2)
      
      ; Schleifen-Kopf
        EmitX("[L_"+l1+"]")  ; <=== KOPFLABEL FÜR CONTINUE
        EmitX("// DO")
        
      ; Schleifen-Körper    
        Statement()
            
      ; Schleifen-Fuß:
      
      ; "until" vorhanden?
        If Scanner::Lexem="until"        
            Scanner::GetToken()
            Emit(0,"// DO ... UNTIL")        
            Condition()        
            Emit(ExpTyp,"JF","L_"+l1)
            
      ; "while" vorhanden?
        ElseIf Scanner::Lexem="while"
            Scanner::GetToken()
            Emit(0,"// DO ... WHILE")        
            Condition()        
            Emit(ExpTyp,"JT","L_"+l1)
      
      ; unendliche Schleife
        Else
            Emit(0,"// DO ... FOREVER")            
            Emit(0,"j","L_"+l1)  
          
        EndIf  
      
      ; Leerzeile       
        EmitX("// END DO")  
        EmitX("[L_"+l2+"]") ; <=== FUSSLABEL FÜR BREAK
        
      ; Label-Stack um 1 Element verringern  ; <=== OBERSTES LÖSCHEN, VORIGES WIRD
                                             ;      ZUM OBERSTEN ELEMENT
        DeleteElement(LabelStack())
        
      EndProcedure
    
    • Vergessen wir dabei die Umbauten bitte auch nicht in der While-Schleife und in allen anderen Schleifen, die wir vielleicht noch einbauen wollen.

      Denken wir noch einmal darüber nach, was wir getan haben!

      Wir benötigen in Schleifen immer 2 Labels:
      1. Label am - eigentlich exakt vor dem - Schleifenkopf für continue
      2. Label am - eigenlich nach dem - Schleifenfuß für break
      Im Schleifenkörper (=Statement) könnte ja wieder eine Schleife sein. Sollte dort eine neue Schleife sein, dann werden die Labels von dieser Schleife am Stack gespeichert. Die alten Labels bleiben eine Stufe darunter natürlich am Stack weiterhin gemerkt (das ist ja gerade der Sinn eines Stacks).
      Wenn eine Schleife endet, also nach dem Schleifen-Körper, wird ganz am Ende der oberste Eintrag des Stacks gelöscht und der vorige Eintrag ist ab dann wieder der oberste.
    Wie immer benötigen wir auch eine Prozedur Continue_Statement() und wie immer dürfen wir auch nicht vergessen, in Statement den neuen Befehl einzutragen:

    Code: Alles auswählen

      Procedure Continue_Statement() 
      
      ; Zur Übersichtlichkeit
        Emit(0,"// CONTINUE")
        
      ; Break-Jump ausgeben  
        Emit(0,"j","L_"+LabelStack()\Continue_)
           
      ; Leerzeile  
        EmitX()
      
      ; nächstes Token-Lexem  
        Scanner::GetToken() 
    
      EndProcedure
    
    • Es wird einfach das oberste Continue-Label als Jump ausgegeben, fertig!


    Die Behandlung von break wäre genauso einfach, aber ich möchte hier Pure Basic nachahmen und einen Sprung aus beliebig tief geschachtelten Schleifen ermöglichen.
    Aus diesem Grund sieht Break_Statement() etwas komplizierter aus, aber nur auf den ersten Blick:

    Code: Alles auswählen

      Procedure Break_Statement()    
    
      ; nächstes Token-Lexem laden        
        Scanner::GetToken() 
        
      ; ist Token eine Integer wie in 'break 2'    
        If Scanner::Token='I'
          
          ; Zahl hinter 'break' holen
          ; -1, weil 'break 2' darf nur 1 Element zurückgehen am Stack          
            break_zahl = Val(Scanner::Lexem) - 1
            
          ; Zur Übersichtlichkeit
            Emit(0,"// BREAK "+Scanner::Lexem)    
            
          ; benötigte Elemente zurückgehen   
            SelectElement(LabelStack(),ListIndex(LabelStack())-break_zahl)
          
          ; Break-Jump ausgeben  
            Emit(0,"j","L_"+LabelStack()\Break_)
          
          ; Label Stack zurückstellen
            LastElement(LabelStack())
          
          ; nächstes Token-Lexem laden        
            Scanner::GetToken() 
           
      ; keine Break-Zahl folgt 
        Else
        
          ; Zur Übersichtlichkeit
            Emit(0,"// BREAK")    
            
          ; Break-Jump ausgeben  
            Emit(0,"j","L_"+LabelStack()\Break_)
            
        EndIf            
        
      ; Leerzeile 
        EmitX()
         
      EndProcedure
    
    • Der Else-Zweig wäre die ganze Break-Prozedur, wenn wir den Befehl ohne Break-Level haben wollen würden!

      Der Fall mit Break-Level sieht folgendermaßen aus:
      • Parser holt die Zahl (wenn 'I' - Integer) hinter break.
      • Die Zahl wird um 1 verringert. Break 1 würde aus der aktuellen Schleife springen (also 0 Stack-Levels zurückgehen, d.h. der oberste Eintrag wird verwendet), break 2 verlässt 2 Schleifen, also diese und die vorige (also 1 Stack-Level zurückgehen, d.h. der direkt unter dem obersten Eintrag wird verwendet, deshalb -1 statt -2).

    Testen wir unser Werk mit folgendem Source-Code:

    Code: Alles auswählen

    int x
    while (x=1)
    {
         continue  // springt vor while
         break     // springt nach (*)
         
         do
         {
              continue  // springt vor do
              break     // springt nach (**)
              break 2   /* springt nach (*),  
                           verlaesst 2 Schleifen */
         }//(**)
         
    }//(*)
    
    Wir erhalten folgende ASM-Ausgabe:

    Code: Alles auswählen

    [L_1]
    // WHILE
      ^     // CONDITION            
      |     // math expression            
      |     ipushg      0           // x
      |     ipushc      1           // 1
      |     ieq                     // =
      |     iJF         L_2         
      |
      |     // CONTINUE            
      '---- j           L_1         
    
            // BREAK                Außenschleife
            j           L_2 -----------------. 
                                             |
    [L_3]                                    |
    // DO                                    |
      ^     // CONTINUE                      |
      '---- j           L_3                  |
                                             |
            // BREAK         Innenschleife   |
            j           L_4 ---.             |
                               |             |
            // BREAK 2         | 2 Schleifen |
            j           L_2 ---|------------.|
                               |            ||
            // DO ... FOREVER  |            ||     
            j           L_3    |            ||
    // END DO                  |            ||
    [L_4] <--------------------'            ||
            j           L_1  Innenschleife  || 
    // WEND                                 ||                                  
          <---------------------------------'|
    [L_2] <----------------------------------' 
                                    Außenschleife
    
    Bemerkenswert einfach! :D
    Zuletzt geändert von puretom am 17.11.2013 12:39, insgesamt 13-mal geändert.
    Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
    puretom
    Beiträge: 109
    Registriert: 06.09.2013 22:02

    Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

    Beitrag von puretom »

    6.10. Rand und end

    Neben den bis jetzt besprochenen Befehlen gibt es noch die Befehle cin und cout, die ich bereits kurz erwähnt habe, aber nicht weiter dokumentieren möchte. Sie lehnen sich an ihre gleichnamigen Brüder der Sprache C an (einfach googeln).

    Interessant ist noch der Befehl rand(), was für Random, also Zufall, steht.
    Hier ein Source-Code als Verwendungsbeispiel:

    Code: Alles auswählen

    int x
    rand(x,100)  // weist Variable x eine Integerzahl zwischen 0 und 100 zu
    
    Der Befehl end schließlich beendet sofort und ohne Bedinungen die Virtual Machine.

    Für die Umsetzung beider Befehle verweise ich auf den Code-Teil.



    6.11. Abschließende Bemerkungen

    Hier sind wir nun mit einem funktionierenden Tiny Compiler für eine Tiny Toy Scriptsprache in der Version 0.5.
    Ich habe versucht, alles aus diesem Beginner-Kapitel herauszuprogrammieren, was die Übersichtlichkeit stören könnte.

    Bei einigen Dingen habe ich mich aber - weil es eben an vielen Stellen so einfach ist - leider dann doch nicht zurückhalten können, zu zeigen, wie einfach teilweise durch einfache Fallunterscheidungen zusätzliche Sprach-Features sind (die Do-(forever)-Schleife oder zuletzt break mit Break-Level zum Beispiel).

    Die Erweiterungen, die jetzt dazukommen sind zumeist bloß zusätzliche Fallunterscheidungen, d.h. in den meisten Fällen werden es einfach mehr ElseIf's oder mehr Parse-Prozeduren. Die Sache wird vom Prinzip her nicht schwieriger, sondern einfach nur unübersichtlicher.

    Natürlich, das, was ich hier zeige, ist eine Art von vielen, einen Compiler zu schreiben.
    Es ist sogar - so scheint mir, aber ich weiß es nicht sicher - eine veraltete Art, wenn ich im Vergleich moderne Compiler wie den von C# oder JAVA hernehme?

    Aber es ist ein Anfang und wird für die meisten, die in ihrem Game eine kleine Skriptsprache einbauen wollen, bereits vollkommen ausreichen, zumindest ab Version 1.0, denn Stringvariablen bräuchten wir wohl schon noch.

    Darum freuen wir uns darüber, was wir ab jetzt draufhaben.

    Ich jedenfalls wäre, als mein Irrweg vor Jahren mit teuren Büchern auf Englisch begonnen hat, hoch erfreut gewesen, damals eine einfachere Einführung zu bekommen, mein Retter war dann Jack Crenshaw, dem ich mit meinem Tutorial nachzufolgen versuche.
    Und, und das darf man nicht vergessen, bist hierher war es für mich bereits ein langer Weg. Wer einen modernen objekt-orientierten hyperoptimierenden JIT-Compiler schreiben will, hat ebenfalls noch einen sehr langen Weg vor sich.

    Doch das war nicht Ziel dieses Tutorials, sondern den für viele schon zu unerreichbar wirkenden Einstieg, der durch zu wissenschaftliche Bücher noch zusätzlich vernebelt wird, klar sichtbar zu machen. Compilerbau ist auf der primitiven Grundebene leicht.

    Also, als Resumee, der Einstieg ist leicht, eine erste Skriptsprache schafft jeder, man darf sich nicht verschrecken lassen, aber dann muss man wohl zum Wissenschaftler werden, wenn man so etwas wie einen JAVA-Compiler oder so schreiben möchte. Oder doch nicht?
    Ich weiß es selbst nicht, denn ich kann es auch nicht ;-) !

    Viel Spaß mit dem Einsteigerkapitel!
    Zuletzt geändert von puretom am 16.11.2013 00:54, insgesamt 12-mal geändert.
    Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
    puretom
    Beiträge: 109
    Registriert: 06.09.2013 22:02

    Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

    Beitrag von puretom »

    6.12. Code-Teil des Parsers

    Abschließend präsentiere ich den Code-Teil des Gesamt-Compilers (=Steuerteil) und des Parsers (=eine Include-Datei).
    Die Include-Datei, die den Scanner beeinhaltet ist im entsprechenden Teil weiter oben vollständig veröffentlicht.

    Der Steuerteil: Das Programm TTCCompiler05.pb:

    Code: Alles auswählen

    ; XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ; X                                                                 X
    ; X   TTC COMPILER 0.5: TinyToyC (TTCC) (TEIL ZWEI)                 X
    ; X                                                                 X
    ; X   HAUPTDATEI                                                    X
    ; X                                                                 X
    ; XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    
    XIncludeFile "Scanner10.pbi"         ; siehe Kapitel 4
    XIncludeFile "Parser05.pbi"          
    ;XIncludeFile "Assembler05.pbi"      ; - GIBT ES DERZEIT NOCH NICHT
    ;XIncludeFile "VirtualMachine05.pbi" ; - GIBT ES DERZEIT NOCH NICHT
    
    ; *******************************************************************
    ; * Compiler Version TTCC                                           *
    ; *******************************************************************
    DeclareModule TTCCompiler05
    ; =
    ; = Public Declarations =============================================
    ; =  
        ; --- Start-Prozedur ---  
          Declare Start(file_name.s)
    
    EndDeclareModule
    Module TTCCompiler05
    
      Procedure Start(file_name.s)
      
        ; Schritte zum fertigen TTC-Executable
          TTCParser05::Start(file_name)
          ;TTCAssembler05::Start(file_name)     ; - GIBT ES DERZEIT NOCH NICHT
          ;TTCVirtualMachine05::Start(file_name); - GIBT ES DERZEIT NOCH NICHT
          
      EndProcedure
      
    EndModule
    
    
    ; *******************************************************************
    ; * Debug-Prozeduren (außerhalb der Module)                         *
    ; *******************************************************************
    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
    Procedure Debug_GetToken()  
       
      Scanner::Start("source-code.ttcs")
    
      While ( Scanner::Token <> 0 )
            Debug " | "+Chr(Scanner::Token)+              ; CHAR des Token-Codes
                  " | "+RSet(Str(Scanner::Token),3," ")+  ; Code-Nr des Tokens
                  " | "+Scanner::Lexem                    ; Lexem
            Scanner::GetToken()  
      Wend   
      Debug "0-Token: außerhalb der While-Schleife"
    
    EndProcedure
    
    
    ; Aufruf je nach Ziel:
    
    ; Debug_GetChar()
    ; Debug_GetToken()
    TTCCompiler05::Start("source-code.ttcs") ; .ttcs = Tiny Toy C Source
    

    Der Hochsprachen-Parser: Das Programm Parser05.pbi:

    Code: Alles auswählen

    ; *******************************************************************
    ; * Parser Version TTCP 0.5                                         *
    ; *                                                                 *
    ; *   ist das Include-File: Parser05.pbi                            *
    ; *                         ^^^^^^^^^^^^                            *
    ; *******************************************************************
    DeclareModule TTCParser05
    ; =
    ; = Public Declarations =============================================
    ; =  
        ; --- Start-Prozedur ---  
          Declare Start(file_name.s)
          
    EndDeclareModule
    
    Module TTCParser05
    ; =
    ; = Private Declarations ============================================
    ; =        
      ; --- Handle des Assembler-Files ---    
        Global TTCA_File 
        
      ; --- TTC-Globals verwalten ---
        Structure gv                    ; Struktur einer globalen
            Typ.i                       ; Variable vom Typ 'i','f','s'
            Index.i                      
        EndStructure
        Global NewMap GlobalVar.gv()    ; enthält Variablenname + Typ
                                        ; + Index                                    
        Global GlobalIntegerIndex      
        Global GlobalFloatIndex
        Global GlobalStringIndex
        
      ; --- TTC-Labels verwalten ---
        Global LabelNr                  ; Label-Nummer
        
      ; --- break, continue verwalten ---    
        Structure ls
            Continue_.i
            Break_.i
        EndStructure
        Global NewList LabelStack.ls()        
         
      ; --- Expression-Typ ---  
        Global ExpTyp          ; Typ der aktuellen Expression (=Ausdruck)
      
      ; --- Fehler-Nummern und sonstige nützliche Konstanten ---  
        Enumeration
          #Unbekannter_Fehler
          #Ungueltiger_Operand
          #Global_bereits_deklariert
          #Global_noch_nicht_deklariert
          #Block_unbeendet
        EndEnumeration
        
        #DQ = #DOUBLEQUOTE$ 
          
      ; --- Versions-Kontrolle ---  
        #VERSIONNR = "0.5" ; für Fehlermeldungen wichtig
            
      ; --- notwendige Declares (eine Halde) ---
      ;{  
        Declare SimpleExpression()  
        Declare ValueFactor()  
        Declare Statement() 
        Declare Block()     
        Declare ParaExpression()   
        Declare Assignment_Statement() 
      ;} 
    
    ; =
    ; = PROGRAMM  =======================================================
    ; =   
        
    ; - Code-Ausgabe-Prozeduren -----------------------------------------
    ; -     
      #lSpc   = 8  ; Space-Abstand vom linken Rand
      #OpcLen = 12 ; Länge des Opcode-Felds, bis der Parameter kommt
      #ParLen = 12 ; Parameterfeld
     ;
     ; allgemeines Emit  
      Procedure WriteLine(s.s)
          WriteStringN(TTCA_File,s)      
      EndProcedure
      Procedure Emit(typ,opcode.s,parameter.s="",comment.s="")
      
      ; Kein Typ gewünscht? -> 0 in Typ schreiben !!!
        If typ=0: t.s="": Else: t.s=Chr(typ): EndIf
        
      ; Opcode hat zu viele Zeichen? -> erlauben  
        If Len(t+opcode)<#OpcLen-1 : opc.s=LSet(t+opcode,#OpcLen," ")        
        Else                       : opc.s=t+opcode
        EndIf
        
      ; Parameter überschießt Längenbegrenzung? -> erlauben  
        If Len(parameter)<=#ParLen-1: parameter=LSet(parameter,#ParLen," ")
        EndIf                      
        
      ; Comment vorhanden?
        If comment<>"": comment="// "+comment: EndIf
        
      ; Ausgabe      
        WriteLine(Space(#lSpc)+opc+parameter+comment)  
        
      EndProcedure  
      Procedure EmitX(ohne_linkes_space_oder_leer.s="")    
        WriteLine(ohne_linkes_space_oder_leer)    
      EndProcedure  
    
    ; - Token-Stream-Kontrolle, Fehlerprozeduren ------------------------
    ; -
      Procedure TestToken(token)
          
        If Scanner::Token<>token
          Scanner::Expected(#DQ+Chr(token)+#DQ+" statt "+#DQ+
                            Chr(Scanner::Token)+#DQ)
        EndIf
        
      EndProcedure
      Procedure EmitError(ort.s,fehlertext.s)
        
      ; bei 0-Byte ordentliche Ausgabe ermöglichen   
        If Scanner::Token=0: chrtoken.s="0"
        Else               : chrtoken=Chr(Scanner::Token)
        EndIf
        
      ; Fehlertext im Message Requester ausgeben
        MessageRequester("Compiler V"+#VERSIONNR+" Error",
              fehlertext+#CRLF$+#CRLF$+
              "Fehlerort in Compilerprozedur: "+ort+#CRLF$+
              "Token-Zeichen: "+chrtoken+#CRLF$+
              "Token-Code: "+Scanner::Token+#CRLF$+
              "Lexem: "+Scanner::Lexem+#CRLF$+
              "Zeile: "+Scanner::LineNr) 
              
      ; Compile-Vorgang brutal beenden
        End
         
      EndProcedure  
      Procedure Error(ort.s,fehler_typ)
      
      ; Fehlertext über EmitError() ausgeben
        Select fehler_typ
        
        Case #Ungueltiger_Operand:
          EmitError(ort,"'"+Scanner::Lexem+"' ist kein gültiger "+"Operand."+
                        #CRLF$+"Eine Konstante, ein Variablen- oder "+ 
                        "Funktionsname wird erwartet.") 
        
        Case #Global_bereits_deklariert:
          EmitError(ort,"'"+Scanner::Lexem+"' ist als globale Variable "+
                        "bereits deklariert.") 
        
        Case #Global_noch_nicht_deklariert:
          EmitError(ort,"'"+Scanner::Lexem+"' ist als globale Variable "+
                        "nicht deklariert.")     
        
        Case #Block_unbeendet:
          EmitError(ort,"Ein Block ist nicht mit '}' geschlossen worden.")
          
        Default
          EmitError(ort,"Ein unbekannter Fehler ist aufgetreten.")
        
        EndSelect 
        
      EndProcedure
    
    ; - automatische Label-Verwaltung -----------------------------------
    ; - 
      Procedure Add_ContinueBreakLabel(cont,brk)
        AddElement(LabelStack())
        LabelStack()\Continue_=cont
        LabelStack()\Break_=brk
      EndProcedure
     
    ; - Globale Variablen verwalten -------------------------------------
    ; - 
      Procedure NewGlobalVar(var_name.s,var_typ) ; legt neue Variable an
        
      ; Variable bereits vorhanden? -> Fehler!
        If FindMapElement(GlobalVar(),var_name)                   
          Error("NewGlobalVar()",#Global_bereits_deklariert)                          
        EndIf
        
      ; lege neue Variable an und trage Typ und Index in Map ein
      ; erhöhe nach Eintrag den Index für das nächste Mal
      
        GlobalVar(var_name)\Typ=var_typ
        
        If     var_typ='i': GlobalVar(var_name)\Index=GlobalIntegerIndex
                            GlobalIntegerIndex+1 
        ElseIf var_typ='s': GlobalVar(var_name)\Index=GlobalStringIndex
                            GlobalStringIndex+1   
        ElseIf var_typ='f': GlobalVar(var_name)\Index=GlobalFloatIndex          
                            GlobalFloatIndex+1                    
        EndIf
            
      EndProcedure
      Procedure IsGlobalVar(var_name.s) ; Var. deklariert? Nein -> Fehler
      
      ; falls Variable nicht deklariert wurde --> Fehler
        If Not FindMapElement(GlobalVar(),var_name)             
          Error("IsGlobalVar()",#Global_noch_nicht_deklariert)                           
        EndIf  
           
      EndProcedure
                                               
    ; - mathematischer Parser (simple Version)---------------------------
    ; -   
      Procedure MathExpression() 
    
      ; Zur Übersichtlichkeit
        Emit(0,"// math expression")
    
      ; Abstieg zu SimpleExpression()    
        SimpleExpression()  
    
      EndProcedure
     ;
      Procedure SimpleExpression()
      
      ; --> Token/Lexem steht auf Value
      
      ; Abstieg zu ParaExpression()    
        ParaExpression()    
        
      ; Ist irgendein Operator in Token/Lexem?
        While Scanner::Token='+' Or Scanner::Token='-' Or 
              Scanner::Token='*' Or Scanner::Token='/' Or
              Scanner::Token='%' Or 
              Scanner::Token='=' Or Scanner::Token='u' Or 
              Scanner::Token='<' Or Scanner::Token='>' Or
              Scanner::Token='k' Or Scanner::Token='g' Or
              Scanner::Lexem="and" Or 
              Scanner::Lexem="or"  Or
              Scanner::Lexem="xor" 
             
          ; 'and', 'or', 'xor' hier vor Ort zu einem Token machen
            If     Scanner::Lexem="and" : Scanner::Token='a'
            ElseIf Scanner::Lexem="or"  : Scanner::Token='o'        
            ElseIf Scanner::Lexem="xor" : Scanner::Token='x'  
            EndIf      
            
          ; Operator merken
            operator = Scanner::Token
            
          ; vor Abstieg nächstes Token (=Value) holen
            Scanner::GetToken()
            
          ; Abstieg zu ParaExpression()    
            ParaExpression()
            
          ; Ausgabe des ASM-Codes des Operators
          ; mit gemerktem Operator                
            Select operator
                
                Case '+'  : Emit(ExpTyp,"add","", "+"  )
                Case '-'  : Emit(ExpTyp,"sub","","-"  )
                Case '*'  : Emit(ExpTyp,"mul","","*"  )
                Case '/'  : Emit(ExpTyp,"div","","/"  )
                Case '%'  : Emit(ExpTyp,"mod","","%"  )
                Case 'a'  : Emit(ExpTyp,"and","","and") 
                Case 'o'  : Emit(ExpTyp,"or" ,"","or" ) 
                Case 'x'  : Emit(ExpTyp,"xor","","xor")
                Case '='  : Emit(ExpTyp,"eq" ,"","="  )
                Case 'u'  : Emit(ExpTyp,"ne" ,"","<>" )
                Case '<'  : Emit(ExpTyp,"lt" ,"","<"  )
                Case '>'  : Emit(ExpTyp,"gt" ,"",">"  )
                Case 'k'  : Emit(ExpTyp,"le" ,"","<=" )
                Case 'g'  : Emit(ExpTyp,"ge" ,"",">=" )
                
            EndSelect
              
        Wend   
            
      ; --> Aufstieg zu MathExpression()
      
      EndProcedure
      Procedure ParaExpression()
      
      ; --> Token-Lexem steht auf Value ODER
      ; --> auf '('
        
        
      ; Klammerausdruck beginnt mit "("
        If Scanner::Token ='('
          
          Scanner::GetToken() ; '(' überspringen und
                              ;     nächstes Token-Lexem laden
          
          SimpleExpression()  ; ----> oberste Stufe Math Parser                      
                              ;       ineinander verschachtelt                                
          
          TestToken(')')      ; ')' überspringen, dann                                    
          Scanner::GetToken() ;     Aufstieg zu Negation  
      
      
      ; keine Klammer
        Else
          
          ValueFactor()       ; weiterer Abstieg zu ValueFactor()
          
        EndIf
        
        ; --> Aufstieg zu SimpleExpression()
        
      EndProcedure 
      Procedure ValueFactor() 
           
      ; Wert (Value) einer Konstanten auf Stack pushen
        If Scanner::Token='I':      
          Emit(ExpTyp,"pushc",Scanner::Lexem,Scanner::Lexem) 
          
             
      ; Wert einer Integer-Variable auf den Stack pushen
        ElseIf Scanner::Token='N':      
          IsGlobalVar(Scanner::Lexem) ; Variable existiert?
          Emit('i',"pushg",Str(GlobalVar(Scanner::Lexem)\Index),
                           Scanner::Lexem)       
            
      ; sonst -> Fehlermeldung
        Else
          Error("ValueFactor",#Ungueltiger_Operand)    
            
        EndIf
        
      ; holt nächstes Token
        Scanner::GetToken()
      
      ; --> in Token/Lexem ist Token/Lexem nach Value
      ; --> Aufstieg zu (derzeit) ParaExpression()      
          
      EndProcedure 
      
    ; - bedingte Anweisungen, Verzweigungen und Schleifen ---------------
    ; -
      Procedure Condition()
      
      ; Zur Übersichtlichkeit
        Emit(0,"// CONDITION")
        
      ; Eine Condition beginnt mit Math-Expression vom Typ Integer
        ExpTyp='i'
        MathExpression()
      
      EndProcedure
      Procedure If_Statement()
            
      ; Token-Lexem "if" überspringen
      ; steht auf 1. Token-Lexem der Condition
        Scanner::GetToken()
      
      ; Labels erzeugen   
        l1=LabelNr:LabelNr+1
        l2=l1
      
      ; Condition ermitteln, bei False -> True-Block überspringen
        EmitX("// IF")    
        Condition()
        Emit(ExpTyp,"JF","L_"+l1)
        EmitX()
      
      ; True-Block  
        Emit(0,"// TRUE BLOCK")
        Statement()            
    
      ; Else-Block vohanden?
        If Scanner::Lexem="else"
            l2=LabelNr:LabelNr+1
            Emit(0,"j","L_"+l2)    ; "j" überspringt Else-Block
            EmitX("[L_"+l1+"]")    ; <== hier geht der Else-Block los
            Emit(0,"// ELSE BLOCK")
            Scanner::GetToken()
            Statement() 
        EndIf                       
      
      ; Fuss-Label von if
        EmitX("// ENDIF")    
        EmitX("[L_"+l2+"]")    
        
      EndProcedure
      Procedure While_Statement() 
      
      ; "while" überspringen, steht auf 1. Token-Lexem der Condition
        Scanner::GetToken()  
      
      ; Labels erzeugen, Label-Stack füllen   
        l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "while"
        l2=LabelNr:LabelNr+1 ; für Schleifen-Ende "wend"
        Add_ContinueBreakLabel(l1,l2)
    
      ; Schleifen-Kopf, Condition, False -> Schleifen-Körper überspringen
        EmitX("[L_"+l1+"]")
        EmitX("// WHILE")
        Condition()
        Emit(ExpTyp,"JF","L_"+l2)
        EmitX()
        
      ; Schleifen-Körper
        Statement()
      
      ; Schleifen-Fuß -> springe zu Kopf
        Emit(0,"j","L_"+l1)  
        
      ; Schleifen-Ende
        EmitX("// WEND")    
        EmitX("[L_"+l2+"]")  
    
      ; Label-Stack um 1 Element verringern   
        DeleteElement(LabelStack())
        
      EndProcedure
      Procedure Do_Statement()  ; mit Break, Continue
      
      ; "do" überspringen -> auf 1. Token-Lexem des nächsten Statements
        Scanner::GetToken()  
      
      ; Labels erzeugen, Label-Stack füllen   
        l1=LabelNr:LabelNr+1 ; für Schleifen-Kopf "do"
        l2=LabelNr:LabelNr+1 ; für Schleifen-Fuß "until, while, end do"
        Add_ContinueBreakLabel(l1,l2)
      
      ; Schleifen-Kopf
        EmitX("[L_"+l1+"]")  
        EmitX("// DO")
        
      ; Schleifen-Körper    
        Statement()
            
      ; Schleifen-Fuß:
      
      ; "until" vorhanden?
        If Scanner::Lexem="until"        
            Scanner::GetToken()
            Emit(0,"// DO ... UNTIL")        
            Condition()        
            Emit(ExpTyp,"JF","L_"+l1)
            
      ; "while" vorhanden?
        ElseIf Scanner::Lexem="while"
            Scanner::GetToken()
            Emit(0,"// DO ... WHILE")        
            Condition()        
            Emit(ExpTyp,"JT","L_"+l1)
      
      ; unendliche Schleife
        Else
            Emit(0,"// DO ... FOREVER")            
            Emit(0,"j","L_"+l1)  
          
        EndIf  
      
      ; Leerzeile       
        EmitX("// END DO")  
        EmitX("[L_"+l2+"]") 
        
      ; Label-Stack um 1 Element verringern   
        DeleteElement(LabelStack())
        
      EndProcedure
    
    ; - Break, Continue -------------------------------------------------
      Procedure Break_Statement()    
    
      ; nächstes Token-Lexem laden        
        Scanner::GetToken() 
        
      ; ist Token eine Integer wie in 'break 2'    
        If Scanner::Token='I'
          
          ; Zahl hinter 'break' holen
          ; -1, weil 'break 2' darf nur 1 Element zurückgehen am Stack          
            break_zahl = Val(Scanner::Lexem) - 1
            
          ; Zur Übersichtlichkeit
            Emit(0,"// BREAK "+Scanner::Lexem)    
            
          ; benötigte Elemente zurückgehen   
            SelectElement(LabelStack(),ListIndex(LabelStack())-break_zahl)
          
          ; Break-Jump ausgeben  
            Emit(0,"j","L_"+LabelStack()\Break_)
          
          ; Label Stack zurückstellen
            LastElement(LabelStack())
          
          ; nächstes Token-Lexem laden        
            Scanner::GetToken() 
           
      ; keine Break-Zahl folgt 
        Else
        
          ; Zur Übersichtlichkeit
            Emit(0,"// BREAK")    
            
          ; Break-Jump ausgeben  
            Emit(0,"j","L_"+LabelStack()\Break_)
            
        EndIf            
        
      ; Leerzeile 
        EmitX()
         
      EndProcedure
      Procedure Continue_Statement() 
      
      ; Zur Übersichtlichkeit
        Emit(0,"// CONTINUE")
        
      ; Break-Jump ausgeben  
        Emit(0,"j","L_"+LabelStack()\Continue_)
           
      ; Leerzeile  
        EmitX()
      
      ; nächstes Token-Lexem  
        Scanner::GetToken() 
    
      EndProcedure
    
    ; - IO-Prozeduren ---------------------------------------------------
    ; -
      Procedure Cout_Statement()  
      
      ; Übersichtlichkeit im ASM-Code
        Emit(0,"// cout")
      
      ; // Ausgabe von mehreren Objekten //
        While Scanner::Look='<'
        
          ; '<<' testen, danach Token holen
            Scanner::GetOther(): TestToken('<')
            Scanner::GetOther(): TestToken('<')
            Scanner::GetToken()
    
          ; Manipulator "endl"
            If Scanner::Lexem="endl"  
              Emit(0,"endl")    
          
          ; String in ".."    
            ElseIf Scanner::Token='S'
              Emit(0,"spushc",#DQ+Scanner::Lexem+#DQ)
              Emit(0,"sout")
          
          ; Integer-Konstante
            ElseIf Scanner::Token='I'
              Emit(0,"ipushc",Scanner::Lexem)
              Emit(0,"iout")
            
          ; Variablen-Name  
            ElseIf Scanner::Token='N'
              IsGlobalVar(Scanner::Lexem)
              Emit(GlobalVar(Scanner::Lexem)\Typ,"pushg",
                              Str(GlobalVar(Scanner::Lexem)\Index),
                              Scanner::Lexem)
              Emit(0,"iout")
          
        EndIf               
      
      Wend
      
      ; Leerzeile zur Übersichtlichkeit
        EmitX()  
        
      ; nächstes Token-Lexem holen
        Scanner::GetToken()
      
      EndProcedure
      Procedure Cin_Statement()  
      
      ; Übersichtlichkeit im ASM-Code
        Emit(0,"// cin")
      
      ; 1. '>' holen
        Scanner::GetOther(): TestToken('>')
        Scanner::GetOther(): TestToken('>')
        Scanner::GetToken()
    
      ; In folgende Variable einlesen
        IsGlobalVar(Scanner::Lexem)
              Emit(GlobalVar(Scanner::Lexem)\Typ,"in")
              Emit(GlobalVar(Scanner::Lexem)\Typ,"pullg",
                              Str(GlobalVar(Scanner::Lexem)\Index),
                              "// ="+Scanner::Lexem)
        
      ; nächstes Token-Lexem holen
        Scanner::GetToken()  
    
      ; Leerzeile zur Übersichtlichkeit
        EmitX()  
        
      EndProcedure  
      Procedure Print_Statement()  
      
      ; Übersichtlichkeit im ASM-Code
        Emit(0,"// print")
        
      ; '(' vorhanden ?
        Scanner::GetOther(): TestToken('(')
        
      ; // Ausgabe von mehreren Objekten //
        Repeat
    
          ; Token-Lexem holen
            Scanner::GetToken()
        
          ; Manipulator "endl"
            If Scanner::Lexem="endl"  
              Emit(0,"endl")    
            
          ; String in ".."    
            ElseIf Scanner::Token='S'
              Emit(0,"spushc",#DQ+Scanner::Lexem+#DQ)
              Emit(0,"sout")
          
          ; Integer-Konstante
            ElseIf Scanner::Token='I'
              Emit(0,"ipushc",Scanner::Lexem)
              Emit(0,"iout")
            
          ; Variablen-Name  
            ElseIf Scanner::Token='N'
              IsGlobalVar(Scanner::Lexem)
              Emit(GlobalVar(Scanner::Lexem)\Typ,"pushg",
                              Str(GlobalVar(Scanner::Lexem)\Index),
                              Scanner::Lexem)
              Emit(0,"iout")
            
            EndIf
          
           ; Token-Lexem holen    
            Scanner::GetToken()
      
        Until Scanner::Token<>','
      
      ; ')' vorhanden ?
        TestToken(')')
        
      ; nächstes Token-Lexem holen
        Scanner::GetToken()      
      
      ; Leerzeile zur Übersichtlichkeit
        EmitX()        
      
      EndProcedure
      Procedure Input_Statement()  
      
      ; Übersichtlichkeit im ASM-Code
        Emit(0,"// input func")
      
      ; '(' vorhanden ?
        Scanner::GetOther(): TestToken('(')
        Scanner::GetName()
    
      ; In folgende Variable einlesen
        IsGlobalVar(Scanner::Lexem)
              Emit(GlobalVar(Scanner::Lexem)\Typ,"in")
              Emit(GlobalVar(Scanner::Lexem)\Typ,"pullg",
                              Str(GlobalVar(Scanner::Lexem)\Index),
                              "="+Scanner::Lexem)
        
      ; ')' vorhanden ?  
        Scanner::GetOther(): TestToken(')')
        
      ; nächstes Token-Lexem holen
        Scanner::GetToken()  
    
      ; Leerzeile zur Übersichtlichkeit
        EmitX()  
        
      EndProcedure  
    
    ; - reine Verzweigungen ---------------------------------------------
    ; -
      Procedure Goto_Statement()   ; springt zu Sprungmarke
    
      ; Zur Übersichtlichkeit
        EmitX("// GOTO")
    
      ; Sprungmarken-Name holen und als ASM-Code ausgeben 
        Scanner::GetName()    
        Emit(0,"j","UL_"+UCase(Scanner::Lexem))
      
      ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
        EmitX()
      
      ; Nächstes Token-Lexem-Paar laden  
        Scanner::GetToken()
    
      EndProcedure
      Procedure GoSub_Statement()  ; -"-, aber merkt sich Return-Adressse
      
      ; Zur Übersichtlichkeit
        EmitX("// GOSUB")
        
      ; Sprungmarken-Name holen und als ASM-Code ausgeben 
        Scanner::GetName()    
        Emit(0,"call","UL_"+UCase(Scanner::Lexem))    
      
      ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
        EmitX()
      
      ; Nächstes Token-Lexem-Paar laden  
        Scanner::GetToken()
          
      EndProcedure
      Procedure Return_Statement() ; 'ret'-Befehl setzen
      
      ; Zur Übersichtlichkeit
        EmitX("// RETURN")
        
      ; ASM-Code ausgeben 
        Emit(0,"ret")    
      
      ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
        EmitX()
      
      ; Nächstes Token-Lexem-Paar laden  
        Scanner::GetToken()
          
      EndProcedure
    
    ; - Sonstiges -------------------------------------------------------
    ; - 
      Procedure End_Statement()    ; Rücksprung aus dem Script in die VM
      
      ; Übersichtlichkeit im ASM-Code  
        Emit(0,"// end")
        
      ; ASM-Code ausgeben 
        Emit(0,"end")    
      
      ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
        EmitX()
      
      ; Nächstes Token-Lexem-Paar laden  
        Scanner::GetToken()
          
      EndProcedure
      Procedure Rand_Statement()
      
      ; Übersichtlichkeit im ASM-Code  
        Emit(0,"// random")
        
      ; '(' vorhanden ?  
        Scanner::GetOther(): TestToken('(')
      
      ; Variablen-Name holen, merken, testen 
        Scanner::GetName()
        var_name.s = Scanner::Lexem    
        IsGlobalVar(var_name)
        
      ; ',' holen, testen
        Scanner::GetOther(): TestToken(',')
      
      ; Maximal-Bedingung holen (=Zahl) und pushen
        Scanner::GetNumber()
        Emit('i',"pushc",Scanner::Lexem)   
          
      ; ASM-Code "rnd" ausgeben 
        Emit('i',"rnd","","Random Number")    
    
      ; und der gemerkten Variable zuweisen
        Emit(GlobalVar(var_name)\typ,"pullg",
             Str(GlobalVar(var_name)\index),
             "="+var_name)         
      
      ; ')' holen, testen
        Scanner::GetOther(): TestToken(')')  
      
      ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
        EmitX()
      
      ; Nächstes Token-Lexem-Paar laden  
        Scanner::GetToken()
        
      EndProcedure
    
    ; - Variablen anlegen & Werte zuweisen ------------------------------
    ; - 
      Procedure Declare_Statement(var_typ) ; deklariert Variable mit typ 
        
      ; 'int', 'float' oder 'string' überspringen
        Scanner::GetName()
           
      ; Variable anlegen, Variablen-Name ist in Lexem
      ; Variablen-Typ wird weitergereicht
        NewGlobalVar(Scanner::Lexem,var_typ)
          
      ; am Ende nächstes Token-Lexem-Paar holen    
        Scanner::GetToken()  
    
      EndProcedure
      Procedure Assignment_Statement()     ; weist Var eine Expression zu
      
      ; --> Variablenname in Lexem      
        
      ; zur Übersichtlichkeit
        Emit(0,"// assignment")  
        
      ; Variablenname merken, Variable bekannt?
        var_name.s = Scanner::Lexem   
        IsGlobalVar(var_name)         
      
      ; den Typ der Expression herausfinden
      ; d.i. der Typ der Variablen, dem das Ergebnis zugewiesen wird 
      ; wird GLOBALER Variable ExpTyp zugewiesen 
        ExpTyp = GlobalVar(var_name)\typ
      
      ; '=' holen, testen
        Scanner::GetOther(): TestToken('=')
      
      ; 1. Token-Lexem der Expression() holen (=ValueFactor)
        Scanner::GetToken()
      
      ; MathExpression aufrufen
        MathExpression() 
      
      ; Wert der Expression (liegt am Stack) der Variable zuweisen
      ; für Variable (Name oben gemerkt) wird Index verwendet
        Emit(ExpTyp,"pullg",Str(GlobalVar(var_name)\index),"="+var_name)         
      
      ; Leere Zeile im ASM-File ausgeben zur Übersichtlichkeit
        EmitX()
      
      ; --> Token-Lexem ist bereits von Expression
      ; --> richtig auf das nächste vorbereitet
    
      EndProcedure 
    
    ; - Statement, Block ------------------------------------------------
    ; -  
      Procedure Statement()     ; erkennt Statement -> Statement-Prozedur
      
      ; DEBUG
        Debug " | "+Chr(Scanner::Token)+              ; CHAR des Token-Codes
              " | "+RSet(Str(Scanner::Token),3," ")+  ; Code-Nr des Tokens
              " | "+Scanner::Lexem                    ; Lexem
        
      ; // je nach Statement Aktionen setzen //
        Select Scanner::Lexem
            
      ; Statements  
        Case "{"        : Block()
        Case "int"      : Declare_Statement('i')    
        Case "if"       : If_Statement()
        Case "do"       : Do_Statement()    
        Case "while"    : While_Statement()
        Case "break"    : Break_Statement()
        Case "continue" : Continue_Statement() 
        Case "goto"     : Goto_Statement()
        Case "gosub"    : GoSub_Statement()
        Case "return"   : Return_Statement()
        Case "print"    : Print_Statement()
        Case "input"    : Input_Statement()    
        Case "cout"     : Cout_Statement()   
        Case "cin"      : Cin_Statement()         
        Case "end"      : End_Statement()  
        Case "rand"     : Rand_Statement()   
        
        Default         : If Scanner::Look=':'  ; -- Sprungmarke (Label) --
                            EmitX("[UL_"+UCase(Scanner::Lexem)+"]")   
                            Scanner::GetOther() ; ':' 
                            Scanner::GetToken()     
                        
                          Else
                            Assignment_Statement(); -- Assignment -- 
              
                          EndIf
                                            
        EndSelect       
      
      EndProcedure  
      Procedure Block()
       
      ; --> in Token-Lexem ist jetzt '{'   
      
      ; ueberlesen des '{'
        Scanner::GetToken()     
      
      ; solange kein '}' 
        While ( Scanner::Token<>'}' )       
          Statement()      
          If Scanner::Token=0:Error("Block()",#Block_unbeendet): EndIf
        Wend        
      
      ; ueberlesen des '}'
        Scanner::GetToken()         
      
      ; --> in Token-Lexem ist jetzt das Token-Lexem exakt nach '}'
            
      EndProcedure
    
    ; ===================================================================  
    ;   START (~MAIN) PROZEDUR 
    ; ===================================================================  
      Procedure Start(file_name.s)
    
        ; Open .ttca-File
          TTCA_File = CreateFile(#PB_Any,GetFilePart(file_name, 
                                     #PB_FileSystem_NoExtension)+".ttca")
          If Not TTCA_File
             EmitError("Start()","Parser: Assembler-File konnte nicht "+
                       "erstellt werden.")
          EndIf      
    
        ; Starte Scanner-Modul (1. Token-Lexem liegt danach im Stream)
          Scanner::Start(file_name)            
          
        ; Inits  
          ClearMap(GlobalVar())        
          GlobalIntegerIndex = 0     
          GlobalFloatIndex   = 0
          GlobalStringIndex  = 0
          LabelNr            = 1
          ClearList(LabelStack())
        
        ; Prolog vorbereiten
          EmitX(Space(50))  ; reserviert für setGlobalIntSize usw.   
          EmitX("// Beginn des Programms")    
          EmitX()        
          
        ; so lange, bis Token = 0-Byte
          Debug "========================================"
          Debug " PARSER - START"
          
          While ( Scanner::Token<>0 )
            Statement()
          Wend
          
          Debug " PARSER - STOP"
          Debug "----------------------------------------"                       
                 
        ; Prolog schreiben
          FileSeek(TTCA_File,0)
          Emit(0,"setGlobalIntSize ", Str(GlobalIntegerIndex))
          
        ; Stoppe Scanner-Modul (free Memory-Bereich im Scanner)
          Scanner::Stop()
    
        ; Close .ttca-File
          CloseFile(TTCA_File) 
          
        ; DEBUG
          Debug "----------------------------------------"
          Debug " GLOBALE VARIABLEN IN 'GLOBALVAR()'"
          Debug "----------------------------------------"     
          Debug "Typ | Index | Name"
          Debug "----------------------------------------"
          ForEach GlobalVar()      
              Debug RSet("'"+Chr(GlobalVar()\typ)+"'",3," ")+
                    " | "+RSet(Str(GlobalVar()\index),5," ")+
                    " | "+MapKey(GlobalVar())  
          Next            
        
        ; Free
          FreeList(LabelStack())
          FreeMap(GlobalVar())
    
      EndProcedure
    
    EndModule
    
    Zuletzt geändert von puretom am 16.11.2013 12:39, insgesamt 17-mal geändert.
    Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
    puretom
    Beiträge: 109
    Registriert: 06.09.2013 22:02

    Re: Tutorial - Compiler und Virtual Machine (nicht beschreib

    Beitrag von puretom »

    Spielcode für das Forum, maximal undokumentierte Features im Vor-Vorstadium zum Herumspielen

    Achtung:
    • Keine Gewähr, dass diese Teile hier unverändert länger bleiben.
    • Ich doktere hier laufend herum, lösche, tausche aus, … und zwar völlig ohne Anmerkung und unangekündigt.
    • Ist ein reines Sonderservice von mir (macht zusätzlich Arbeit), falls jemand meine Baustellenversionen zum Ausprobieren verwenden will.

    Diese Code-Fragmente sollten mit dem gesamten System incl. Virtual Machine so wie bei mir zuhause bereits funktionieren:

    Code: Alles auswählen

    
    /* Solche Kettendeklarationen mit Direkt-Assignments 
       sind leichtestens eingebaut
     */
       int a,b=3,c=1
    
    /* Das geht bereits: String-Expression mit Input 
       in der Mitte
     */
       print("Geben Sie einen etwas ein: ")
       printn("Ihre Eingabe lautet '"+input()+"'.")
    
    /* Auf Enter-Taste warten
     */  
       printn("") // Leerzeile ohne Parameter
       printn("Druecken Sie ENTER!")
       input() // ohne Zuweisung (Stack-Bereinigung!!)
    
    /*  Mit einer Variablenzuweisung und Direkt-Assignment
     */
       printn("") // ohne Parameter
       print("Geben Sie ein Code-Wort ein: ")
       string code = input()
       string ausgabe = "Ihr Code-Wort lautet <<"+code+">>"
       printn(ausgabe)
    
     /*************************************************************
      *  GOTO-SPIELE
      */
         goto eins
    
    zwei:
         print("zwei, ")
         goto drei
         
    eins:
         print("Eins, ")
         goto zwei
    
    drei:
         printn("drei.")
    
     /*************************************************************
      *  GOSUB-SPIELE
      */
    
         gosub eins1
         gosub zwei2
         gosub drei3
         gosub vier4   
    
         goto ende          
         
         
    vier4:     
         printn("vier.")
         return
    
    drei3:     
    
         print("drei, ")
         return
    
    zwei2:     
    
         print("zwei, ")
         return
    
    eins1:     
    
         print("Eins, ")
         return
    
    
    ende:
    
    

    ANLEITUNG FÜR DEN CODE-TEIL:

    Ich teile ab jetzt alles in Include-Files auf.
    • Für das Forum ist das Zusammensetzen zwar schwieriger, aber für mich ist es zum Programmieren überschaubarer.
    • Außerdem – glaube ich – wird dadurch der Aufbau etwas übersichtlicher.
    • Nicht zuletzt überschieße ich auch bereits die Zeichenbeschränkung des Forums.
    • Alle Teile sollten sich leicht zusammensetzen lassen und mit den Code-Beispielen in diesem Posting laufen.

    • Der Code lässt sich im Modul Parser umschalten zwischen #Version = 0.5 und #Version 1.0 (nur die kann den Source-Code hier oben lösen)
    Lg puretom und viel Spaß
    Zuletzt geändert von puretom am 15.11.2013 23:55, insgesamt 9-mal geändert.
    Windows 7 und Windows 10 (Laptop), PB 5.62 | Projekt: Tutorial - Compiler und Virtual Machine | vielleicht einmal ein Old School Text-Adventure Tutorial | Neu: Spielereien, Üben rund um OOP in PB
    Antworten