09. Mai 2025

Implizite Syntax

FĂŒr meine Programmiersprache wollte ich eine möglichst minimale Syntax. PrimĂ€r ging es mir darum, möglichst wenige spezielle Sonderzeichen (;, :, ...) oder SchlĂŒsselwörter zu verwenden. Hier beschreibe ich einen theoretischen Ansatz bzw. Entwurf, wie man dabei vorgehen könnte ...

Hierbei habe ich zwei Bereiche festgelegt, die ganz allgemein voneinander getrennt werden mĂŒssen: Blöcke und Statements.

Blöcke und Statements

AusdrĂŒcke sind einfach nur eine Ansammlung von Tokens, die auf eine bestimmte Art und Weise angeordnet werden mĂŒssen. Besagte Tokens teilen wir hier zwischen Operanden und Operatoren auf.

Statements definiere ich jetzt einfach mal so, dass sie jedoch fĂŒr sich "alleine stehen können". Das können nĂ€mlich AusdrĂŒcke (4 * x + 5) nicht! Das entsprechende Statement wĂ€re z.B.: y = 4 * x + 5. Ausnahmen bilden hierbei Funktionsaufrufe und spezielle Anweisungen in Verbindung mit SchlĂŒsselwörtern. Diese Unterscheidung ist im spĂ€teren Verlauf sehr wichtig, wobei ich hier meistens von "Statements" sprechen werde, obwohl streng genommen AusdrĂŒcke gemeint sind.

Blöcke dagegen beinhalten Statements, welche dann in Konstrukten wie einer Funktion, Schleifen, usw. verschachtelt sind.

Statements trennen

Die meisten Programmiersprachen machen sich es insofern einfach, dass sie ein spezielles Zeichen festlegen, um Statements zu trennen.

// Entweder so ...
x = 5;
y = 4;
 
// Oder auch so ...
x = 5; y = 3;

Der Parser weiß hier, dass z.B. x = 5 in sich abgeschlossen sein muss.

Hat man nicht so ein spezielles Zeichen, dann muss der Parser selbst wissen, wann so ein Statement abgeschlossen sein "könnte". Hierbei kann sich der Parser eben auch nicht auf das Zeilenende verlassen (also das Zeilenende markiert NICHT das Ende Statements), da es sein kann, dass Statements in der nÀchsten Zeilen weitergehen (hier wird ein Zeilenende wie ein Leerzeichen behandelt).

Mit ein paar Regeln, die man mit Operanden mit Operatoren aufstellt, kann der Parser selbst wissen, wann ein Statement terminiert.

Operanden

Operanden sind z.B. 500, 0x200, x, "hallo, welt", usw.

Ein (alleinstehender) Operand an sich bildet bereits einen gĂŒltigen Ausdruck, aber man kann sagen, dass Operanden stets den nĂ€chsten binĂ€ren Operatoren "an sich binden" können. Wenn das passiert, fĂŒhrt der Parser das Statement fort. Operanden können aber nicht den nĂ€chsten Operanden an sich binden, damit ist das Statement vorbei, weil <Operand> <Operand> kein gĂŒltiges Statement produziert.

Operatoren

Bei Operatoren gibt es zwei Arten von Operatoren: BinÀre und unÀre Operatoren. BinÀre Operatoren haben links und rechts einen Operanden: x + 3. UnÀre Operatoren haben nur rechts einen Operanden: -x. Kleines Beispiel:

x + 3

x bindet "+" an sich und "+" bindet wiederum "3" an sich. Jedoch gilt zu beachten, dass ein + (egal ob un- oder binÀr) immer einen Operanden fordert.

Es gibt auch Operatoren, die je nach Kontext unÀr oder binÀr sind: -3 (negatives Vorzeichen), x - 3 (Subtraktion).

x = 5
-2

Dieser Code hĂ€tte nun zwei Lesarten: x = 5 - 2 und x = 5 mit -2. Wir haben jedoch festgelegt, dass Statements fĂŒr sich alleine stehen können, und AusdrĂŒcke eben nicht. 5 wĂŒrde also -2 an sich binden, und damit ist nur die erste Lesart die richtige.

Ein Àhnliches Problem gibt es mit ++ bzw. --, die entweder als Postfix oder PrÀfix genutzt werden können:

x = y
++z

Hier wĂŒrde y das ++ an sich binden und z alleine produziert kein Statement. Hier muss eine Alternative geschaffen werden oder der Operator gĂ€nzlich entfernt werden.

Generell sind hier auch Lösungen zu prĂ€ferieren, die nicht versuchen eine AmbiguitĂ€t aufzulösen (etwa weil z kein gĂŒltiges Statement produziert, und es daher anders "gemeint sein muss").

Programmieransatz

Der Compiler besteht zunÀchst zumindest aus einem Lexer und Parser.

Der Lexer hat hier eine Funktion next(), um den nĂ€chsten Token aus dem Quellcode zu holen. Desweiteren gibt es einen "Cache", um den letzten Token zurĂŒckzulegen. Der Token, der im "Cache" liegt, wird beim nĂ€chsten Aufruf von next() sofort zurĂŒckgegeben. Die Funktion des Token-ZurĂŒcklegens könnte nennen wir put_back().

Andere Implementierung haben oft eine peek()-Funktion. Hier wird der nÀchste Token zwar "geholt", jedoch nicht "konsumiert"! Die beschriebene Vorgehensweise verfolgt also eher einen optimistischen Ansatz.

Der Lexer wird regelmĂ€ĂŸig vom Parser aufgerufen. Der Parser produziert so lange AusdrĂŒcke (durch die Tokens, die der Lexer liefert), bis der produzierte Ausdruck ungĂŒltig sein wĂŒrde, z.B. (wie oben beschrieben) wenn zwei Operanden aufeinander folgen. In so einem Fall haben wir ein gĂŒltiges, maximales Statement (oder zumindest Ausdruck) gefunden und können den letzten Token mit put_back() zurĂŒcklegen.

Blöcke trennen

Mit den Grundlagen von den Statements können wir uns jetzt ansehen, wie wir Blöcke voneinander trennen können. Blöcke sind nichts anderes als eine Sammlung von Statements und weiteren/verschachtelten Blöcken.

Beispiel: if-Abfragen

if(x % 2 == 0) {
    ...
}

Der obige Code hat zwei Klammerpaare, wobei (...) eine Ă€hnliche Funktion wie bei den Statements erfĂŒllt, nĂ€mlich dass der Ausdruck darin abgeschlossen sein muss. Da wir bereits wissen, wie wir durch den Kontext feststellen können, ob der Ausdruck abgeschlossen ist, brauchen wir die runden Klammern sowieso nicht.

FĂŒr { gilt dasselbe, da der darauffolgende Block bereits implizit eröffnet wird.

Bei } wird es allerdings komplizierter. TatsÀchlich gibt es nicht direkt eine Möglichkeit zu erkennen, wann ein Block aufhört - das ergibt sich nicht aus dem Kontext heraus! Das verdeutlicht folgendes Beispiel (SyntaxÀnderungen inkludiert):

// 1. Lesart
if x % 2 == 0
    y = 10
if z % 2 == 0
    x = 10
 
// 2. Lesart
if x % 2 == 0
    y = 10
    if z % 2 == 0
        x = 10
    

Aus dem Kontext ergibt sich nicht, ob der zweite if-Block im ersten ist, da beide Varianten gĂŒltig sind (Vergleich mit Statements: Wo so viele Tokens aufgesammelt werden, bis kein gĂŒltiges Statement mehr produziert wird).

Sprachen, die kein spezielles Symbol oder SchlĂŒsselwort haben, greifen meistens auf Tabs zurĂŒck (bzw. die EinrĂŒckung ergibt sich durch das Format). FĂŒr meine Programmiersprache habe ich mich fĂŒr das SchlĂŒsselwort end entschieden.

if x % 2 == 0
    y = 10
end
 
if z % 2 == 0
    x = 10
end