Zitat Zitat von OlliH
Ich hoff ich habs halbwegs so gemacht wie es üblich ist, hab nämlich einfach "drauf los" programmiert ohne davor irgendwelche speziellen Compilerbau-Bücher o. ä. zu lesen
Üblich ist das, was man tut

Wenn man alles selber macht ist der Lerneffekt zumindest am grössten und man muss sich nicht mit fremdem Zeug rumschlagen.

Gerne wird bei solchen Aufgaben ein existierender Compiler genommen und angepasst. Genaugenommen ist dein Compiler kein Compiler, sondern ein Transpiler, wenn ich's recht sehe: er soll C-Code nach BASIC (Byte-)Code übersetzten.

Als freien, anpassbaren C-Compiler wäre der tcc (tiny C compiler) zu erwähnen. Da muss man für unterschiedliche Aufgaben Ausgabe-Schnippsel angeben. Die werden in den Ausgabe-Strom als ASCII, Object-Code oder was immer man gerne hätte, eingefügt.

Du wirst bei deinem ccccc gemerkt haben, daß es eine recht aufwändige und nervige Angelegenheit ist, den Eingabestrom (Quelle) in eine brauchbare interne Darstellung zu bekommen und Syntax-Fehlermeldungen mit der richtigen Stelle anzeigen zu lassen. Wenn z.B eine schliessende Klammer fehlt, und man merkt das erst am Ende der Datei, dann sucht sich der Anwender den Wolf nach der Stelle, wenn der Fehler immer das Dateiende angibt.

Nun sind alle Programmiersprachen auf einer sprachlich-formalen Ebene ziemlich gleich. Man definiert dort z.B., wie ein gültiger Ausdruck aussieht:

So sind alle Zahlen und Variablen gültige Ausdrücke. Steht A für einen Ausdruck, dann erhält man weitere gültige Ausfrücke (in C), indem man gültige Ausdrücke weiter kombiniert:
A -> A + A
A -> A * A
A -> A / A
A -> A - A
A -> - A
A -> (A)
A -> (A) ? A : A
...
Indem man die Regeln nacheinander anwendet, kommt man zu allen möglichen (unendlich vielen) Ausdrücken, die die Sprache erlaubt, etwa
A+A*(A+(A/A))-A
Die Regeln, nach der ein neues A aus vorhandenen gebildet werden kann, sind leicht zu durchschauen. Ein Compiler muss aber zu Anfang den umgekehrten Weg gehen. Aus einer Zeichenkette muss die Struktur des Ausdrucks wiederhergestellt werden. Das ist eine recht nervige Angelegenheit, die schnell unübersichtlich wird. Änderungen sind da nur schwer einzuflicken.

Zu diesem Zwecke gibt es fertige Parser wie zB den bison. Bison wird mit der Sprachbeschreibung gefüttert! Also damit, wie die Sprache entsteht, ohwohl er genau die umgekehrte Aufgabe zu lösen hat! Zudem kann man für Operationen Prioritäten angeben, so daß ein
a+b*c nicht gelesen wird wie ein (a+b)*c, wie es billige Taschenfalschrechner gerne machen.
Was für Ausdrüche zutrifft, gilt auch genauso für einen C-Block, eine C-Funktion, Argumentliste, Deklaration, Definition, etc. Man sagt dem Parser nur, wie sie gebildet werden, und er dröselt die Quelle dementsprechend auf.

Zur Vorverdauung lässt man lex oder flex die Eingabe in Tokens (Schlüsselworte) zusammenfassen. flex kann man in verschiedenen Modi betreiben: in einem Kommentar oder einem String haben Zeichen eine andere Bedeutung als ausserhalb. Kommentare in Strings oder Strings im Kommentaren oder Kommentare in Kommentaren sind damit leicht und übersichtlich zu handhaben.

Die Tokens werden an bison geliefert, den man verwendet, um das Programm intern Darzustellen: Anweisungen, Ausdrücke, Deklaratoren, etc.

Dadurch kann man sich beim Compilerbau ganz auf die eigentliche Aufgabe konzentrieren.

Übrigens gibt es auch Parser und Lexer für andere Sprachen als C, etwa jflex und cup für Java.