Interpreterbau - Ein Anfang



  • Dummie schrieb:

    Inkludierst du auch Token.h? Gibst du dem Compiler alle notwendigen .cpp und .h Dateien mit?

    Ja alles richtig.

    Dummie schrieb:

    Hast du überhaupt einen Konstruktor implementiert?

    Token::Token(TokenType type, int value) 
        : myType(type), myValue(value)
    {
     // ...
    }
    

    Nein. das wars....

    Dummie schrieb:

    Was für einen Compiler/IDE nutzt du überhaupt?

    Ehm... ich schreibe in Kate. Und bau den kram mit CMake im build Ordner mit make. Sag dir das was? 🙂
    Danke.

    Achso hier hab es alles in 3 Dateien:
    .h:

    #ifndef OOPJS_H
    #define OOPJS_H
    
    #include <iostream> 
    #include <string> 
    
    enum TokenType{                           // token definition
      TT_RBRACKET,
      TT_LBRACKET,
      TT_RBRACE,
      TT_LBRACE,
      TT_COMMA,
      TT_SEMI,
      TT_COLON,
      TT_DOT,
      TT_STRING,
      TT_NIL
    };
    
    static const char *TokenTypStr[] = {     // for error message
      "TT_RBRACKET",
      "TT_LBRACKET",
      "TT_RBRACE",
      "TT_LBRACE",
      "TT_COMMA",
      "TT_SEMI",
      "TT_COLON",
      "TT_DOT",
      "TT_STRING",
      "TT_NIL"
    };
    
    class Token{
    private:
      TokenType myType;
      int myValue;
    public:
      Token(TokenType type = TT_NIL, int value = 0);
      TokenType getType() const { return myType; }
      int getValue() const { return myValue; }
      const char *toString() const { return TokenTypStr[myType]; }  // error message
      bool equal(TokenType type) const { return myType == type; }         //comparison
    };
    
    class Scanner{
    public:
      Scanner(const std::string& input);
      Token getNextToken();
    private:
      std::string myInput;
      unsigned int myPos;                                            //Position at input
      char myCh;                                                     //last char
    private:
      void skipSpaces();
      void readNextChar();
    };
    
    Token::Token(TokenType type, int value) : myType(type), myValue(value){
    }
    /*
    class Parser{
    public:
      Parser(const std::string& input):
      int parse();
    private:
      Scanner myScanner;
      Token myTok;
    private:
      int start();
      void accept(TokenType);
      void getNextToken();
    };
    */
    
    #endif
    

    .cpp:

    #include "oopjs.h"
    
    void Scanner::readNextChar(){
      if(myPos > myInput.length()){
        myCh = 0;
        return;
      }
      myCh = myInput[myPos++];
    }
    
    void Scanner::skipSpaces(){
      while(myCh == ' ' || myCh == '\t' || myCh == '\r' || myCh == '\n'){
        readNextChar();
      }
    }
    
    Scanner::Scanner(const std::string& input) : myInput(input), myPos(0){
      readNextChar();
    }
    
    Token Scanner::getNextToken(){
      std::string buf;
      skipSpaces();                          //to skip chars which are not used
      switch(myCh){
        case '(':
          readNextChar();
          return Token(TT_RBRACKET);
        case ')':
          readNextChar();
          return Token(TT_LBRACKET);
        case '{':
          readNextChar();
          return Token(TT_RBRACE);
        case '}':
          readNextChar();
          return Token(TT_LBRACE);
        case ',':
          readNextChar();
          return Token(TT_COMMA);
        case ';':
          readNextChar();
          return Token(TT_SEMI);
        case ':':
          readNextChar();
          return Token(TT_COLON);
        case '.':
          readNextChar();
          return Token(TT_DOT);
                                            //read as long its a letter or "
        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '"':
          while(isdigit(myCh)){
    	buf += myCh;
    	readNextChar();
          }
          return Token(TT_STRING);
        default:
          if(myCh != 0){
    	std::cerr << "Error: not used Char '" << myCh << "'" << std::endl;
          }
          break;
      } 
      return Token(TT_NIL);
    }
    /*
    void Parser::getNextToken(){
      myTok = myScanner.getNextToken();
    }
    
    void Parser::accept(){
      if(!myTok.equl(type)){
        std::cerr << "Error: unexpected Token " << myTok.toString() << ", " << TokenTypeStr[type] << " expected" << std::edl;
      }
      getNextToken();
    }
    Parser::Parser(const std::string& input) : myScanner(input){
      getNextToken();
    }
    
    int Parser::parse(){
      int res = start();
      accept(TT_NIL);
      return res;
    }
    
    void Parser::start(){
    
    }
    */
    

    main:

    #include <iostream> 
    #include <string> 
    #include "oopjs.h" 
    
    int main(){ 
      std::string input = "class";
      std::cout << "Eingabe: \"" << input << "\"\n" << std::endl; 
      Scanner scanner(input); 
      Token tok = scanner.getNextToken(); 
      while (!tok.equal(TT_NIL)){ 
        std::cout << tok.toString() << " = " << tok.getValue() << std::endl; 
        tok = scanner.getNextToken(); 
      } 
    }
    


  • Funktioniert es denn nun korrekt?

    Ich würde für Bezeichner und Strings eigene Token einführen.

    Und für reservierte Keywords wie class, if, else... würde ich auch eigene Token einführen. Wenn du also einen Bezeichner eingelesen hast, kannst du prüfen, ob es evtl. ein Keyword ist...

    Also in etwa so:

    case 'a': case 'b': case 'c': ....... case 'z':
    // Weiter einlesen....
    
    if (value == "else")
     return Token(TT_ELSE);
    else if (value == "if")
     return Token(TT_IF);
    
    return Token(TT_IDENT, value );
    

    Edit: Und deine Codeaufteilung ist so leider nicht wirklich besser. Wenn du unbedingt nur eine Datei inkludieren willst, dann mach dir eben eine Datei oopjs.h die einfach alles inkludiert, was du immer brauchst.



  • Dummie schrieb:

    Funktioniert es denn nun korrekt?

    Nein ist eine endlos Schleife. 😞 Kommt immer TT_STRING = 0.

    Dummie schrieb:

    Ich würde für Bezeichner und Strings eigene Token einführen.

    Ja klingt gut. 🙂

    Dummie schrieb:

    Und für reservierte Keywords wie class, if, else... würde ich auch eigene Token einführen. Wenn du also einen Bezeichner eingelesen hast, kannst du prüfen, ob es evtl. ein Keyword ist...

    Also in etwa so:

    case 'a': case 'b': case 'c': ....... case 'z':
    // Weiter einlesen....
    
    if (value == "else")
     return Token(TT_ELSE);
    else if (value == "if")
     return Token(TT_IF);
    
    return Token(TT_IDENT, value );
    

    Ok vielen Dank. Sowas hatte ich vor. 🙂

    Dummie schrieb:

    Edit: Und deine Codeaufteilung ist so leider nicht wirklich besser. Wenn du unbedingt nur eine Datei inkludieren willst, dann mach dir eben eine Datei oopjs.h die einfach alles inkludiert, was du immer brauchst.

    Ehm ... hab ich das nicht? Hab doch eine oopjs.h die alles inkludiert? Oder versteh ich dich falsch?
    Danke.



  • Ja, es ist natürlich eine Endlosschleife, weil du isdigit nutzt. Das fragt ab, ob es eine Zahl ist. Das ist bei "class" natürlich immer false.

    Stattdessen musst du isprint verwenden, wenn du Buchstaben willst:
    http://www.cplusplus.com/reference/clibrary/cctype/isprint/

    Deine Codegliederung ist insofern schlecht, weil du Scanner und Parser und Token alles in eine Datei packen willst. Stattdessen bietet es sich an, wenn du diese Dinge trennst.

    Scanner.h
    Parser.h
    Token.h (evtl. sogar TokenType.h)

    Das wird sonst sehr unübersichtlich...



  • Dummie-Off schrieb:

    Ja, es ist natürlich eine Endlosschleife, weil du isdigit nutzt. Das fragt ab, ob es eine Zahl ist. Das ist bei "class" natürlich immer false.

    Stattdessen musst du isprint verwenden, wenn du Buchstaben willst:
    http://www.cplusplus.com/reference/clibrary/cctype/isprint/

    Ok. Danke für den hinweis. 🙂

    Dummie-Off schrieb:

    Deine Codegliederung ist insofern schlecht, weil du Scanner und Parser und Token alles in eine Datei packen willst. Stattdessen bietet es sich an, wenn du diese Dinge trennst.

    Scanner.h
    Parser.h
    Token.h (evtl. sogar TokenType.h)

    Das wird sonst sehr unübersichtlich...

    Hm.. joar vielleicht. Ich glaube aber es soll so sein muss ich mal mein Ausbilder fragen. Der gibt mir meistens vor wie ich es machen soll. Ob eine oder mehrere. Trotzdem danke nochmal. 🙂

    P.S.: Dein Artikel ist btw ziemlich gut mag ich. 🙂 Gut zum lernen geeignet.



  • Hey ich noch mal!
    Ehm ... also irgendwie versteh ich nicht warum er bei mir bei einem String immer abbricht. Also nach dem er ein String Token gemacht hat hört er auf. Liegt das am "?

    Token Scanner::getNextToken(){
      std::string buf;
      skipSpaces();                          //to skip chars which are not used
      switch(myCh){
        case '(':
          readNextChar();
          return Token(TT_RBRACKET);
        case ')':
          readNextChar();
          return Token(TT_LBRACKET);
        case '{':
          readNextChar();
          return Token(TT_RBRACE);
        case '}':
          readNextChar();
          return Token(TT_LBRACE);
        case ',':
          readNextChar();
          return Token(TT_COMMA);
        case ';':
          readNextChar();
          return Token(TT_SEMI);
        case ':':
          readNextChar();
          return Token(TT_COLON);
        case '.':
          readNextChar();
          return Token(TT_DOT);
                                            //read as long its a letter or "
        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '"':
    
          while(isprint(myCh)){
    	buf += myCh;
    	readNextChar();
          }
          return Token(TT_STRING);
        default:
          if(myCh != 0){
    	std::cerr << "Error: not used Char '" << myCh << "'" << std::endl;
          }
          break;
      }  
      return Token(TT_NIL);
    };
    
    std::string input = "{ :(); classa {public:   function hello(){    console.log(""hello"");  }};class b {public:  function bye(){     console.log(""Bye"");  }};class c : public a,b{public:  function greet(){    console.log(""greeting"");  }}";
    
    Eingabe: "{ :(); classa {public:   function hello(){    console.log(hello);  }};class b {public:  function bye(){     console.log(Bye);  }};class c : public a,b{public:  function greet(){    console.log(greeting);  }}"
    
    TT_RBRACE = 0
    TT_COLON = 0
    TT_RBRACKET = 0
    TT_LBRACKET = 0
    TT_SEMI = 0
    TT_STRING = 0
    


  • Überleg' mal was die while-Schleife macht....



  • Th69 schrieb:

    Überleg' mal was die while-Schleife macht....

    while(isprint(myCh)){ 
        buf += myCh; 
        readNextChar(); 
          }
    

    Ehm...
    Solange myCh ein druckbarer char ist (frage ist ob der das nun nur für die cases oben macht oder ob für ihn alle druckbar sind?)
    myCh in wird an buf ran gehangen
    nächsten char lesen



  • Anstatt isprint musst du isalpha verwenden, denn isprint sieht auch all die anderen Zeichen als druckbar an.

    http://www.cplusplus.com/reference/clibrary/cctype/isalpha/

    Zur Not musst du auch einfach selber mal nach der Funktion im Internet suchen und dir die Dokumentation dieser ansehen.
    Oft sind auch verwandete Funktionen dort beschrieben, die dann ggf. besser geeignet sind.

    Und noch was:
    Warum benennst du "(" als RBRACKET und ")" als LBRACKET (selbiges gilt für BRACE)?

    Eigentlich wäre es genau andersrum sinnvoller, denn das R steht für Right und das L für Left.



  • Dummie schrieb:

    Anstatt isprint musst du isalpha verwenden, denn isprint sieht auch all die anderen Zeichen als druckbar an.

    Und noch was:
    Warum benennst du "(" als RBRACKET und ")" als LBRACKET (selbiges gilt für BRACE)?

    Eigentlich wäre es genau andersrum sinnvoller, denn das R steht für Right und das L für Left.

    Öhm ... ich hab mir gedacht "(" ist nach rechts geöffnet also RBRACKET. 🙂

    EDIT: Ach so und warum muss ich den constructor löschen wenn ich was an dem string = input ändere? Wenn ich wieder was ändere muss ich ihn wieder rein machen. Versteh ich nicht.

    Token::Token(TokenType type, int value) : myType(type), myValue(value){
    }
    


  • Ich hatte es eben von der Perspektive gesehen, dass sie eben links steht (sind ja immer Klammerpaare). Das finde ich auch beim Schreiben bzw. Lesen schneller verstanden. Denn bei deiner Variante muss ich mir erst vorstellen, wie denn "(" aussieht und entsprechend entscheiden, ob ich L oder R meine.

    Kannst du letztendlich aber eh so halten, wie du damit am besten arbeiten kannst.



  • Dummie schrieb:

    Ich hatte es eben von der Perspektive gesehen, dass sie eben links steht (sind ja immer Klammerpaare). Das finde ich auch beim Schreiben bzw. Lesen schneller verstanden. Denn bei deiner Variante muss ich mir erst vorstellen, wie denn "(" aussieht und entsprechend entscheiden, ob ich L oder R meine.

    Kannst du letztendlich aber eh so halten, wie du damit am besten arbeiten kannst.

    Hmm... ok das klingt auch logisch. 🙂 Ich glaub ich änder das. 🙂



  • Man schaue sich das hier einmal an:

    So wird es im Code definiert:

    case '}':
          readNextChar();
          Toki.push_back(TT_LBRACE);
          return Token(TT_LBRACE);
        case '{':
          readNextChar();
          Toki.push_back(TT_RBRACE);
          return Token(TT_RBRACE);
    

    Lets twist it bei der Ausgabe:

    Eingabe: "class a {"
    
    TT_CLASS = 0
    TT_STRING = 0
    TT_LBRACE = 0
    

    TT_LBRACE sollte eigentlich nach dem Code TT_RBRACE heißen...



  • Hallo,
    ich fände eine Weiterführung des Interpreters, vorallem aus dem mathematischen Raum hinaus eine wirklich interessante Artikelreihe 😃



  • 🙂



  • Ja, das stimmt natürlich und ich hatte es ja auch vor. Eventuell gehe ich das demnächst mal an. Es erfordert halt nur sehr viel Zeit. 😉

    Ich bin mir auch noch nicht sicher, was für eine Sprache es dann werden soll. Eventuell einfach eine Modellsprache, wie PL/0. Es geht ja in erster Linie um das Konzept. 🙂

    PL/0 ist grundsätzlich eine recht vollständige Sprache mit Abfragen, Schleifen und so weiter, aber kennt nur den Datentyp Integer. Der Vorteil ist auch, dass es dazu sehr viele fertige Projekte und anderes Material gibt. Da kann man sein Wissen dann noch zusätzlich vertiefen. Mal sehen. 😉



  • Hallo, Tutorials zu so leichten Sprachen findet man überall im Netz, ich fände ein Tutorial zu einem Webserver (für den man programmieren kann), also so wie PHP sehr viel spannender. Natürlich nicht so umfangreich 😃

    Danke



  • Hallo123 schrieb:

    Hallo, Tutorials zu so leichten Sprachen findet man überall im Netz, ich fände ein Tutorial zu einem Webserver (für den man programmieren kann), also so wie PHP sehr viel spannender. Natürlich nicht so umfangreich 😃

    Danke

    Schau dir mal Tntnet an: http://www.tntnet.org/



  • Hallo, ich habe ein kleines Problem mit meinem eigenen Interpreter, nämlich: Was mache ich, wenn ich mehrere Ausdrücke hintereinander habe (z.B.:

    i = 0;
    a = 6;
    

    )? Muss ich ein Array von Nodes machen? Und wie würde ich dann z.B. einen C++-SC in einen AST überführen? (ich weiß, dass das viel zu komplex ist ;))

    Viel Dank im Voraus



  • C++ompiler schrieb:

    Hallo, ich habe ein kleines Problem mit meinem eigenen Interpreter, nämlich: Was mache ich, wenn ich mehrere Ausdrücke hintereinander habe (z.B.:

    i = 0;
    a = 6;
    

    )? Muss ich ein Array von Nodes machen? Und wie würde ich dann z.B. einen C++-SC in einen AST überführen? (ich weiß, dass das viel zu komplex ist ;))

    Viel Dank im Voraus

    Das kommt ganz auf den Anwendungsfall an. Wenn du eine Sprache entwickelst, bei der das Programm direkt ausgeführt bzw. in eine andere Sprache kompiliert werden soll, dann kann es Sinn machen, dass du für jede Deklaration ein Node anlegst. Dann könntest du zur Laufzeit den Speicher berechnen bzw. bei der neue Sprache so die Deklaration generieren.

    Ansonsten musst du eine Symboltabelle haben (solltest du für die semantische Analyse ohnehin schon haben) und für die Variablen eine Adresse berechnen. Dann kann jeder Ident mit dem entsprechenden Eintrag in der Symboltabelle verknüpft werden und bei der Codegenerierung weiß der Compiler so, wie er den Code zu generieren hat.

    Das ganze ist dann im Detail doch etwas anspruchsvoller, also am besten mal eine fertige Implementierung suchen und davon abschauen. Suchwörter sind jedenfalls Symboltabelle, Adressgenerierung, usw.


Anmelden zum Antworten