Gibt es eine Alternative für Flex / Bison, die auf eingebetteten 8-Bit-Systemen verwendet werden kann?

83

Ich schreibe einen kleinen Interpreter für eine einfache BASIC-ähnliche Sprache als Übung auf einem AVR-Mikrocontroller in C unter Verwendung der avr-gcc-Toolchain. Ich frage mich jedoch, ob es Open-Source-Tools gibt, die mir beim Schreiben von Lexer und Parser helfen könnten.

Wenn ich dies schreiben würde, um es auf meiner Linux-Box auszuführen, könnte ich flex / bison verwenden. Jetzt, wo ich mich auf eine 8-Bit-Plattform beschränkt habe, muss ich alles von Hand machen oder nicht?

Johan
quelle
1
Gibt es einen bestimmten Chip, den Sie verwenden möchten? Wie viel ROM / RAM hat es?
Steve S
Update auf den Link von @mre. embedded.com hat ihre URLs verworfen. ( embedded.com/design/prototyping-and-development/4024523/… )
pgvoorhees
Es scheint, dass nur Stapelverzögerungen (her & Co) eine Chance auf 2 KB RAM haben, wobei der Kernel geflasht ist
Jacek Cz

Antworten:

59

Ich habe einen Parser für eine einfache Befehlssprache implementiert, die auf ATmega328p abzielt . Dieser Chip hat 32k ROM und nur 2k RAM. Der Arbeitsspeicher ist definitiv die wichtigere Einschränkung. Wenn Sie noch nicht an einen bestimmten Chip gebunden sind, wählen Sie einen mit möglichst viel Arbeitsspeicher aus. Dies wird Ihr Leben viel einfacher machen.

Zuerst habe ich überlegt, Flex / Bison zu verwenden. Ich habe mich aus zwei Hauptgründen gegen diese Option entschieden:

  • Standardmäßig hängen Flex & Bison von einigen Standardbibliotheksfunktionen (insbesondere für E / A) ab, die in avr-libc nicht verfügbar sind oder nicht gleich funktionieren. Ich bin mir ziemlich sicher, dass es unterstützte Problemumgehungen gibt, aber dies ist ein zusätzlicher Aufwand, den Sie berücksichtigen müssen.
  • AVR hat eine Harvard-Architektur . C ist nicht dafür ausgelegt, dies zu berücksichtigen, daher werden standardmäßig sogar konstante Variablen in den RAM geladen . Sie müssen spezielle Makros / Funktionen verwenden, um Daten in Flash und EEPROM zu speichern und darauf zuzugreifen . Flex & Bison erstellen einige relativ große Nachschlagetabellen, die Ihren RAM ziemlich schnell aufbrauchen. Sofern ich mich nicht irre (was durchaus möglich ist), müssen Sie die Ausgabequelle bearbeiten, um die speziellen Flash- und EEPROM-Schnittstellen nutzen zu können.

Nachdem ich Flex & Bison abgelehnt hatte, suchte ich nach anderen Generatorwerkzeugen. Hier sind einige, die ich in Betracht gezogen habe:

Vielleicht möchten Sie auch einen Blick auf den Vergleich von Wikipedia werfen .

Letztendlich habe ich sowohl den Lexer als auch den Parser von Hand codiert.

Zum Parsen habe ich einen rekursiven Abstiegsparser verwendet. Ich denke, Ira Baxter hat dieses Thema bereits angemessen behandelt, und es gibt viele Online-Tutorials.

Für meinen Lexer habe ich reguläre Ausdrücke für alle meine Terminals geschrieben, die entsprechende Zustandsmaschine grafisch dargestellt und als eine riesige Funktion implementiert, die goto's zum Wechseln zwischen Zuständen verwendet. Das war langweilig, aber die Ergebnisse haben großartig funktioniert. Abgesehen davon gotoist es ein großartiges Werkzeug für die Implementierung von Zustandsautomaten - alle Ihre Zustände können eindeutige Beschriftungen direkt neben dem relevanten Code haben, es gibt keinen Funktionsaufruf oder Overhead für Zustandsvariablen und es ist ungefähr so ​​schnell wie möglich. C hat wirklich kein besseres Konstrukt zum Erstellen statischer Zustandsmaschinen.

Denken Sie darüber nach: Lexer sind eigentlich nur eine Spezialisierung von Parsern. Der größte Unterschied besteht darin, dass reguläre Grammatiken normalerweise für die lexikalische Analyse ausreichen, während die meisten Programmiersprachen (meistens) kontextfreie Grammatiken haben. Es hindert Sie also wirklich nichts daran, einen Lexer als Parser für rekursiven Abstieg zu implementieren oder einen Parser-Generator zum Schreiben eines Lexers zu verwenden. Es ist normalerweise nicht so bequem wie die Verwendung eines spezielleren Tools.

Steve S.
quelle
219

Wenn Sie Parser auf einfache Weise codieren möchten oder nur wenig Platz zur Verfügung stehen, sollten Sie einen Parser für rekursiven Abstieg manuell codieren. Dies sind im Wesentlichen LL (1) -Parser. Dies ist besonders effektiv für Sprachen, die so "einfach" wie Basic sind. (Ich habe einige davon in den 70ern gemacht!). Die gute Nachricht ist, dass diese keinen Bibliothekscode enthalten. genau das, was du schreibst.

Sie sind ziemlich einfach zu codieren, wenn Sie bereits eine Grammatik haben. Zuerst müssen Sie die linken rekursiven Regeln (z. B. X = XY) entfernen. Dies ist im Allgemeinen ziemlich einfach, daher lasse ich es als Übung. (Sie müssen dies nicht für Listenbildungsregeln tun; siehe Diskussion unten).

Wenn Sie dann die BNF-Regel des Formulars haben:

 X = A B C ;

Erstellen Sie für jedes Element in der Regel (X, A, B, C) eine Unterroutine, die einen Booleschen Wert mit der Aufschrift "Ich habe das entsprechende Syntaxkonstrukt gesehen" zurückgibt. Für X Code:

subroutine X()
     if ~(A()) return false;
     if ~(B()) { error(); return false; }
     if ~(C()) { error(); return false; }
     // insert semantic action here: generate code, do the work, ....
     return true;
end X;

Ähnliches gilt für A, B, C.

Wenn ein Token ein Terminal ist, schreiben Sie Code, der den Eingabestream auf die Zeichenfolge überprüft, aus der das Terminal besteht. Überprüfen Sie beispielsweise für eine Zahl, ob der Eingabestream Ziffern enthält, und bewegen Sie den Eingabestream-Cursor über die Ziffern hinaus. Dies ist besonders einfach, wenn Sie aus einem Puffer heraus analysieren (bei BASIC erhalten Sie in der Regel jeweils eine Zeile), indem Sie einfach einen Puffer-Scan-Zeiger vor- oder zurückbewegen. Dieser Code ist im Wesentlichen der Lexer-Teil des Parsers.

Wenn Ihre BNF-Regel rekursiv ist ... machen Sie sich keine Sorgen. Codieren Sie einfach den rekursiven Aufruf. Dies behandelt Grammatikregeln wie:

T  =  '('  T  ')' ;

Dies kann wie folgt codiert werden:

subroutine T()
     if ~(left_paren()) return false;
     if ~(T()) { error(); return false; }
     if ~(right_paren()) { error(); return false; }
     // insert semantic action here: generate code, do the work, ....
     return true;
end T;

Wenn Sie eine BNF-Regel mit einer Alternative haben:

 P = Q | R ;

dann Code P mit alternativen Auswahlmöglichkeiten:

subroutine P()
    if ~(Q())
        {if ~(R()) return false;
         return true;
        }
    return true;
end P;

Manchmal stoßen Sie auf Regeln zur Listenbildung. Diese neigen dazu, rekursiv zu bleiben, und dieser Fall ist leicht zu handhaben. Die Grundidee besteht darin, Iteration anstelle von Rekursion zu verwenden, und dies vermeidet die unendliche Rekursion, die Sie auf "offensichtliche" Weise erhalten würden. Beispiel:

L  =  A |  L A ;

Sie können dies mithilfe der Iteration wie folgt codieren:

subroutine L()
    if ~(A()) then return false;
    while (A()) do { /* loop */ }
    return true;
end L;

Auf diese Weise können Sie mehrere hundert Grammatikregeln an ein oder zwei Tagen codieren. Es gibt mehr Details zu ergänzen, aber die Grundlagen hier sollten mehr als genug sein.

Wenn Sie sehr wenig Platz haben, können Sie eine virtuelle Maschine erstellen, die diese Ideen umsetzt. Das habe ich in den 70ern gemacht, als man 8K 16-Bit-Wörter bekommen konnte.


Wenn Sie dies nicht manuell codieren möchten, können Sie es mit einem Metacompiler ( Meta II) automatisieren ) , der im Wesentlichen dasselbe erzeugt. Dies ist ein umwerfender technischer Spaß, der selbst bei großen Grammatiken die ganze Arbeit kostet.

August 2014:

Ich bekomme viele Anfragen, wie man einen AST mit einem Parser erstellt. Einzelheiten dazu, die diese Antwort im Wesentlichen ausarbeiten, finden Sie in meiner anderen SO-Antwort unter https://stackoverflow.com/a/25106688/120163

Juli 2015:

Es gibt viele Leute, die einen einfachen Ausdrucksauswerter schreiben wollen. Sie können dies tun, indem Sie die gleichen Aktionen ausführen, die der obige Link "AST Builder" vorschlägt. Rechnen Sie einfach, anstatt Baumknoten zu erstellen. Hier ist ein Ausdrucksauswerter, der auf diese Weise erstellt wurde .

Ira Baxter
quelle
2
Ja, es ist nicht allzu schwer, einen rekursiven Abstiegsparser für eine einfache Sprache von Hand zu rollen. Denken Sie daran, Tail Calls zu optimieren, wenn Sie können - Stapelspeicher ist sehr wichtig, wenn Sie nur ein paar Kilobyte RAM haben.
Steve S
2
Alle: Ja, Sie können die Tail-Call-Optimierung durchführen. Dies spielt keine Rolle, es sei denn, Sie erwarten, dass die Verschachtelung in Ihrem analysierten Code wirklich tief geht. Für eine BASIC-Codezeile ist es ziemlich schwierig, Ausdrücke mit einer Tiefe von mehr als 10 Parathenen zu finden, und Sie können zum Booten immer eine Tiefenbegrenzung eingeben. Es ist richtig, dass eingebettete Systeme tendenziell weniger Stapelspeicher haben. Achten Sie also zumindest hier auf Ihre Wahl.
Ira Baxter
2
@Mark: und es mag 2012 sein, aber das technische Papier von 1965, auf das ich mich beziehe, ist jetzt genauso gut wie damals und es ist ziemlich gut, besonders wenn Sie es nicht wissen.
Ira Baxter
2
@ Mark, ah, ok, danke! Es sieht so aus, als ob das Datum auf mysteriöse Weise festgelegt wurde. Danke, Time Lord.
Ira Baxter
2
Wie kann ich mit leeren Zeichenfolgen umgehen?
Dante
11

Sie können flex / bison unter Linux mit seiner nativen gcc verwenden, um den Code zu generieren, den Sie dann mit Ihrer AVR gcc für das eingebettete Ziel kreuzkompilieren.

Paul R.
quelle
2

GCC kann auf eine Vielzahl von Plattformen kompiliert werden, aber Sie führen Flex und Bison auf der Plattform aus, auf der Sie den Compiler ausführen. Sie spucken nur C-Code aus, den der Compiler dann erstellt. Testen Sie es, um zu sehen, wie groß die resultierende ausführbare Datei wirklich ist. Beachten Sie, dass sie Laufzeitbibliotheken ( libfl.ausw.) haben, die Sie auch zu Ihrem Ziel kompilieren müssen.

ConcernedOfTunbridgeWells
quelle
Ich muss noch die Größe dieser Bibliotheken untersuchen und deshalb habe ich die Frage zuerst gestellt. Ich möchte etwas speziell für kleine MCUs.
Johan
-1

Versuchen Sie Boost :: Spirit. Es ist eine reine Header-Bibliothek, in die Sie einen sehr schnellen, sauberen Parser vollständig in C ++ einfügen können. Überladene Operatoren in C ++ werden anstelle einer speziellen Grammatikdatei verwendet.

Erik Aronesty
quelle
Das Problem mit Ihrer Antwort ist, dass die 8-Bit-Plattformbeschränkung nicht bekannt ist. Es wird schwer fallen, gleichzeitig eine Werkzeugkette zu bekommen, die den Schub und eine so winzige Plattform unterstützt.
Waslap
-5

Schauen Sie sich LUA an, anstatt das Rad neu zu erfinden : www.lua.org . Es ist eine Interpretationssprache, die in andere Software eingebettet und auf kleinen Systemen wie eingebetteten Systemen verwendet werden soll. Integrierter Parsing-Baum für prozedurale Syntax, Steuerlogik, Mathematik und Variablenunterstützung - Sie müssen nichts neu erfinden, was Tausende von anderen bereits getestet und verwendet haben. Und es ist erweiterbar, dh Sie können die Grammatik erweitern, indem Sie Ihre eigenen C-Funktionen hinzufügen.

Scott Hall
quelle
2
So klein Lua auch sein mag, ich bin mir ziemlich sicher, dass es immer noch viel zu groß wäre.
icktoofay