Andreas Rozek
[ Impressum ]   [ Datenschutzerklärung ]   [ Kontakt ]       deutsche Fassung   [ english version ]

PEG.js

Von David Majda stammt der bekannte Parser-Generator PEG.js, der inzwischen von Futago-za Ryuu weitergeführt wird. Der Generator kann auch ohne eine vorherige Installation rein online betrieben werden.

In der Vorlesung "Grundlagen der Informatik" an der Hochschule für Technik in Stuttgart können sich Studenten mithilfe von PEG.js aktiv mit der Syntax (nicht nur) von Programmiersprachen sowie den notwendigen Schritten bei der Analyse und Verarbeitung von Texten vertraut machen.

Eine Anleitung erklärt Ihnen die Bedienung der Seiten in diesem Web-Auftritt, in den Einstellungen können Sie deren Verhalten anpassen.

XML-Konverter nach JSON

In einem ersten Schritt wird eine ggb. XML-Datei analysiert und in eine äquivalente JSON-Datei umgewandelt.

vereinfachte XML-Grammatik für PEG.js

Um in der wenigen zur Verfügung stehenden Zeit als Lehrbeispiel dienen zu können, deckt die verwendete Grammatik nur die wichtigsten Aspekte von XML ab.

XML           = Declaration _ Element _
Declaration = '<?xml' (__ Attribute)* _ '?>'
Element = emptyElement / filledElement
emptyElement = '<' _ Tag (__ Attribute)* _ '/>'
filledElement = '<' _ Tag (__ Attribute)* _ '>' (_ Content)* _ '</' _ Tag _ '>'
Content = Element / Text / CDATA
Attribute = Name (_ '=' _ '"' Value '"')?
CDATA = '<![CDATA[' [^\]]* ']]>'
Tag = [a-zA-Z]+
Name = [a-zA-Z]+
Text = [^<]+
Value = [^"]*
_ = [ \t\n\r]*
__ = [ \t\n\r]+

EBNF-Version für Syntax-Diagramme

Syntax-Diagramme sind (vor allem für Einsteiger) bisweilen einfacher verständlich als eine abstrakte (E)BNF- oder PEG-Notation.

Der bekannte Railroad Diagram Generator erzeugt für eine (in einer BNF-artigen Notation) gegebene Grammatik die zugehörigen Syntax-Diagramme.

XML           ::= Declaration Element
Declaration ::= '<?xml' Attribute* '?>'
Element ::= emptyElement | filledElement
emptyElement ::= '<' Tag Attribute* '/>'
filledElement ::= '<' Tag Attribute* '>' Content* '</' Tag '>'
Content ::= Element | Text | CDATA
Attribute ::= Name ('=' '"' Value '"')?
CDATA ::= '<![CDATA[' [^#x5D]* ']]>'
Tag ::= [a-zA-Z]+
Name ::= [a-zA-Z]+
Text ::= [^<]+
Value ::= [^"]*

Konverter-Definition für PEG.js

Mit entsprechenden JavaScript-Anweisungen dekoriert, wird aus der reinen XML-Grammatik für PEG.js ein Konverter, der aus einer gegebenen XML-Datei das zugehörige JSON-Äquivalent erzeugt.

{ function indented (Text) {
return Text.split('\n').map(Line => ' ' + Line).join('\n')
} }

XML = Declaration _ Element:Element _
{ return Element }
Declaration = '<?xml' (__ Attribute)* _ '?>'
Element = emptyElement / filledElement
emptyElement = '<' _ Tag:Tag Attributes:(__ Attribute:Attribute { return Attribute })* _ '/>'
{ return '{\n' + indented(
' "Tag":"' + Tag + '"' + (
Attributes.length > 0
? ',\n "Attributes": {' + Attributes.join(', ') + '}'
: ''
) + '\n}')
}
filledElement = '<' _ StartTag:Tag Attributes:(__ Attribute:Attribute { return Attribute })*
_ '>' Contents:(_ Content:Content {return Content})*
_ '</' _ EndTag:Tag _ '>'
{ if (StartTag !== EndTag) {
error('end tag differs from start tag')
}

return '{\n' + indented(
' "Tag":"' + StartTag + '"' + (
Attributes.length > 0
? ',\n "Attributes": {' + Attributes.join(', ') + '}'
: ''
) + (
Contents.length > 0
? ',\n "Contents": [' + Contents.join(', ') + ']'
: ''
) + '\n}')
}
Content = Element / Text / CDATA
Attribute = Name:Name Value:(_ '=' _ '"' inner:Value '"' { return inner })?
{ return (
Value == null
? Name + ':true'
: Name + ':' + Value
) }
CDATA = '<![CDATA[' Content:[^\]]* ']]>'
{ return '"' + Content.join('').replace(/"/g,'\\"') + '"' }
Tag = [a-zA-Z]+ { return text() }
Name = [a-zA-Z]+ { return '"' + text() + '"' }
Text = [^<]+ { return '"' + text().replace(/"/g,'\\"') + '"' }
Value = [^"]* { return '"' + text() + '"' }
_ = [ \t\n\r]* { return '' }
__ = [ \t\n\r]+ { return '' }

Als Beispiel-Datei dient ein Beitrag für einen fingierten Blog

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Mustermann&#39;s Blog</title>
<link href="/atom.xml" rel="self"/>

<author>
<name>Mustermann</name>
<email>Max.Mustermann@gmx.de</email>
</author>

<entry>
<title>Just a simple Blog Entry</title>
<content type="html"><![CDATA[<p>...</p>]]></content>
</entry>
</feed>

PEG.js als Script-Interpreter

Ein Datei-Konverter ist ja schon ganz praktisch, aber eigentlich dürstet es einen Informatik-Studenten nach einer eigenen Programmiersprache.

Dank PEG.js ist der Aufwand dafür selbst für einen Anfänger sehr überschaubar:

  1. sobald man eine Vorstellung davon hat, wie die Sprache aussehen und was sie können soll,
  2. erstellt man zweckmäßigerweise erst einmal ein paar theoretische Beispiele (die möglichst alle syntaktischen Aspekte der Sprache abdecken).
  3. Als nächstes definiert man die Syntax der Programmiersprache und
  4. testet sie anhand der zuvor entwickelten Beispiele. Diese Tests sollten durchaus sorgfältig durchgeführt werden, weil spätere Änderungen schnell sehr aufwändig werden können)
  5. Sobald die reine Grammatik funktioniert, kann man sie mit JavaScript-Anweisungen dekorieren, die einen "abstract syntax tree" (AST) erzeugen - möglichst in einer Form, die in der PEG.js-Ausgabe gut kontrolliert werden kann.
  6. Auch diese Grammatik sollte wieder getestet werden - diesmal allerdings anhand von Beispielen, die später auch ausgeführt werden können (also einen semantischen Sinn beinhalten)
  7. Zu guter Letzt wird die Ausgabe der primären Regel (d.h. der Regel für das gesamte Skript) dahingehend geändert, dass der AST nicht ausgegeben, sondern interpretiert wird.

Et voilà, Sie haben eine eigene Skriptsprache spezifiziert und implementiert!

slang - die "simple language"

Als Beispiel diene "slang", eine sehr einfache Skript-Sprache, die vor allem zur Demonstration der grundlegenden Mechanismen eines Interpreters (und weniger für einen praktischen Einsatz) gedacht ist.

Datentypen

slang kennt nur die folgenden Datentypen

  • nil - repräsentiert "nichts", also einen nicht definierten Wert (JavaScript undefined)
  • logische Werte ('true' und 'false')
  • numerische Werte (Gleitkommazahlen im doppelt-genauen IEEE 754 Format)
  • Zeichenketten
  • Funktionen und Blöcke
  • "native" Funktionen (JavaScript)

Insbesondere kennt slang keinerlei Datenstrukturen (dadurch wird die Grammatik etwas einfacher)

Funktionen und Blöcke sind "first-class values", können also an Variablen zugewiesen und als Argumente für Funktionsaufrufe verwendet werden

Literale

Werte der o.g. Datentypen werden wie folgt im Programmtext definiert

  • nil
  • true oder false
  • Zahlen
    • im Binär-Format (nur für vorzeichenlose ganze Zahlen) mit vorangestelltem 0b (also z.B. 0b01101100) mit bis zu 32 Ziffern/Bits
    • im Hexadezimal-Format (nur für vorzeichenlose ganze Zahlen) mit vorangestelltem 0x (also z.B. 0x0123AFFE) mit bis zu 8 Ziffern (32 Bits)
    • als Dezimalzahl mit opt. Vorzeichen, Vor- und Nachkommastellen und Exponenten
  • Zeichenketten
    • einzeilig in einfachen oder doppelten Hochkommata ('...' oder "...")
    • mehrzeilig in einfachen oder doppelten Hochkommata ('''...''' oder """...""")
    • mit Unterstützung der (unter JavaScript) üblichen Escape-Sequenzen (\b, \f, \n, \r, \t, \v, \0, \', \", \\ sowie \x## und \u####)
  • Blöcke {...}
  • Lambdas (Skript-Funktionen) (...) => {...}
  • native Funktionen native (...) => string

Bezeichner

"Bezeichner" sind die Namen von Kontext-Einträgen, also globalen, lokalen oder lexikalischen Variablen und Funktionen.

Sie beginnen mit einem der Buchstaben a-z bzw. A-Z oder einem der beiden Zeichen $ bzw. _, optional gefolgt von einem oder mehreren weiteren Buchstaben, $ oder _ bzw. dezimalen Ziffern (0-9).

Variablen und Funktionen dürfen aber nicht wie ein reserviertes Wort heißen. Reservierte Worte sind global, local, it, native, nil, true, false, nan, e, pi, div, mod, and, or, not, if, then, else, select, when, otherwise, for, from, down, to, by, repeat, while, until, continue, break und return

Die Groß-/Kleinschreibung der Bezeichner ist signifikant.

Kontexte

Kontexte fassen Werte und Funktionen zusammen, so dass diese aus Funktionen und Blöcken heraus anhand ihres Namens angesprochen werden können.

Es gibt "äußere", "innere" und "lexikalische" Kontexte.

Der äußerste Kontext ist der "globale Kontext" mit allen globalen Werten und Funktionen.

Wenn eine Funktion gestartet wird, erzeugt sie einen "inneren" Kontext für Aufrufparameter und lokale Variablen. Beim Lesen werden Kontext-Einträge zuerst lokal und anschließend global gesucht. Beim Schreiben werden Einträge stets nur im lokalen Kontext gesucht (und dort ggfs. neu angelegt)

Wenn ein Block gestartet wird, erzeugt dieser ebenfalls einen lokalen Kontext, diesmal allerdings im "lexikalischen" Kontext der Stelle, an der er definiert wurde. Beim Lesen werden Kontext-Einträge deshalb zunächst im lokalen, anschließend (ggfs. rekursiv) in den zugehörigen lexikalischen Kontexten und am Ende auch im globalen Kontext gesucht. Beim Schreiben werden Einträge ebenfalls im lokalen und in den lexikalischen Kontexten gesucht - niemals aber im globalen Kontext. Gefundene Einträge werden dort geändert, wo sie gefunden wurden, nicht gefundene Einträge werden lokal angelegt.

Blöcke

Blöcke beinhalten Abfolgen von Anweisungen, die im Programmtext auf Wunsch durch ein oder mehrere Semikola voneinander getrennt werden können (aber nicht müssen)

Allein stehende Ausdrücke gelten ebenfalls als Anweisung.

Jede ausgeführte Anweisung liefert ein Ergebnis, dieses wird in der lokalen Variable it gespeichert.

Das Ergebnis eines Blockes ist das Ergebnis der letzten ausgeführten Anweisung.

Lambdas (Skript-Funktionen)

Lambdas sind (anonyme) Funktionen mit 0, 1 oder mehr "Parametern". Definiert werden sie durch Ausdrücke der Form

  • () => {...} für parameterlose Lambdas,
  • (parameter) => {...} für Lambdas mit einem Parameter,
  • (parameter, parameter, ...) => {...} für Lambdas mit mehreren Parametern

Die Parameter stehen innerhalb der Funktion als lokale Variablen zur Verfügung und werden zu Beginn mit den Werten der beim Aufruf übergebenen Argumente vorbelegt.

Lambdas können sofort aufgerufen, als Argumente an andere Funktionen übergeben oder einer Variablen zugewiesen werden.

Das Ergebnis einer Funktion ist

  • das Ergebnis der letzten Anweisung im Funktionsblock oder
  • das Ergebnis einer return-Anweisung
Nota bene: derzeit wird noch nicht abgesichert, dass alle Parameter unterschiedliche Namen tragen

Native Funktionen

"Native Funktionen" sind in JavaScript programmierte und von slang aus aufrufbare Funktionen und dienen der Erweiterung der Skript-Sprache. Definiert werden sie durch Ausdrücke der Form

  • native () => string für parameterlose Funktionen,
  • native (parameter) => string für Funktionen mit einem Parameter,
  • native (parameter, parameter, ...) => string für Funktionen mit mehreren Parametern

Der Quelltext der nativen Funktion wird (innerhalb von slang) als ein- oder mehrzeiliger slang-String vorgegeben.

Die Parameter der JavaScript-Funktion tragen die bei der Definition festgelegten Namen; der lexikalische Kontext, in dem die native Funkton aufgerufen wurde, steht als "this"-Objekt zur Verfügung.

Ausdrücke und Operatoren

Ausdrücke bestehen aus

  • Zuweisungen,
  • Operator-Verknüpfungen,
  • Aufrufen,
  • Literalen, Variablen, Klammerungen oder
  • Lambdas bzw. nativen Funktionen

slang unterstützt die folgenden Operatoren (mit abnehmendem Vorrang)

  • (...) (Klammerung)
  • ^ (Potenzierung)
  • *, /, div (Ganzzahl-Division), mod (Modulo-Division)
  • +, -
  • <, <=, =, >=, > und <> (ungleich)
  • not (logische Negation)
  • or (logische Disjunkton)
  • and (logische Konjunktion)
  • := (Zuweisung)

Die "Punkt-vor-Strich"-Konvention (PEMDAS) wird berücksichtigt.

Die meisten Operatoren sind "links-assoziativ", nur Zuweisungen, Potenzierungen und die logische Negation werden "rechts-assoziativ" ausgewertet.

Zuweisungen

Zuweisungen ändern den Wert eines bestehenden Kontext-Eintrages (meist einer Variablen) oder legen diesen an.

Eine "generische Zuweisung" hat die Form

identifier := expression

und ändert einen bestehenden Eintrag im lokalen oder lexikalischen Kontext bzw. legt einen neuen Eintrag im lokalen Kontext an.

Eine "lokale Zuweisung" hat die Form

local identifier := expression

und schreibt stets einen Eintrag im lokalen Kontext.

Eine "globale Zuweisung" hat die Form

global identifier := expression

und schreibt einen Eintrag im globalen Kontext. Globale Zuweisungen sind die einzige Möglichkeit, Einträge im globalen Kontext anzulegen oder zu ändern.

Auch Zuweisungen liefern ein Ergebnis (und können somit in Ausdrücken verwendet werden), und zwar den Wert, der zugewiesen wurde.

Steueranweisungen

Zur Steuerung des Programmflusses stehen folgende Anweisungen zur Verfügung:

  • if (...) then {...}
    if (...) then {...} else {...}
    ist der hinter if angegebene Ausdruck true, so wird der hinter then notierte Block ausgeführt. Ist der Ausdruck false wird stattdessen der hinter else notierte Block ausgeführt - sofern ein solcher existiert, anderenfalls wird die Anweisung einfach ignoriert. Das Ergebnis der Anweisung ist das Ergebnis des ausgeführten Blockes bzw. nil, falls kein Block ausgeführt wurde
  • select {
      when (...) then {...}
      ...
      otherwise {...}
    }
    wertet der Reihe nach alle hinter when notierten Ausdrücke aus, hält an sobald der erste Ausdruck mit dem Wert true gefunden wurde und führt daraufhin den Block hinter dem zugehörigen then aus. Liefert kein when-Ausdruck den Wert true, so wird stattdessen der Block hinter otherwise ausgeführt - sofern ein solcher existiert, anderenfalls wird die Anweisung ignoriert. Das Ergebnis der Anweisung ist das Ergebnis des ausgeführten Blockes bzw. nil, falls kein Block ausgeführt wurde
  • for identifier from ... to ... by ... repeat {...}
    for identifier from ... down to ... by ... repeat {...}
    führt den hinter repeat notierten Schleifenblock wiederholt und mit unterschiedlichen Werten für den gegebenen identifier aus, wobei diese Werte mit dem Wert des Ausdruckes hinter from beginnen und bei jedem Durchlauf um den Wert des Ausdruckes hinter by verändert werden. In der Version mit dem Schlüsselwort down endet die Schleife mit dem Unterschreiten des Wertes des Ausdruckes hinter to, ansonsten mit dessen Überschreiten. Hat der Ausdruck hinter by den Wert nil, so wird stattdessen +1 (in der Version ohne down) bzw. -1 (in der Version mit down) angenommen. Der identifier darf eine bereits vorhandene Variable aus dem lexikalischen Kontext bezeichnen, ansonsten wird die entsprechende Variable im Kontext der Schleife angelegt. Im Schleifenblock angelegte lokale Variablen behalten von einem Durchlauf zum nächsten ihren Wert. Das Ergebnis der Schleife ist das Ergebnis der letzten Ausführung des Schleifenblockes
  • while (...) repeat {...}
    führt den hinter repeat notierten Schleifenblock wiederholt durch, solange der hinter while notierte Ausdruck den Wert true liefert. Im Schleifenblock angelegte lokale Variablen behalten von einem Durchlauf zum nächsten ihren Wert, und die Abbruchbedingung wird ebenfalls im Kontext des Schleifenblockes ausgeführt. Das Ergebnis der Schleife ist das Ergebnis der letzten Ausführung des Schleifenblockes
  • repeat {...} until (...)
    führt den hinter repeat notierten Schleifenblock wiederholt durch, bis der hinter until notierte Ausdruck den Wert true liefert. Im Schleifenblock angelegte lokale Variablen behalten von einem Durchlauf zum nächsten ihren Wert, und die Abbruchbedingung wird ebenfalls im Kontext des Schleifenblockes ausgeführt. Das Ergebnis der Schleife ist das Ergebnis der letzten Ausführung des Schleifenblockes
  • break
    bricht die innerste in der gerade ausgeführten (Script-)Funktion laufende Schleife ab
  • continue
    fordert eine neue Iteration der innersten in der gerade ausgeführten (Script-)Funktion laufenden Schleife an - oder bricht diese ab, falls die Abbruchbedingung der Schleife erfüllt ist
  • return expr
    bricht die gerade laufende Funktion ab und liefert den Wert des (optionalen) Ausdrucks zurück - fehlt dieser Ausdruck, so wird nil zurückgegeben

Kommentare

slang unterstützt einzeilige und mehrzeilige (nicht schachtelbare) Kommentare:

  • einzeilig // ... bis Zeilenende
  • mehrzeilig /* ...ein- oder mehrzeilig, nicht schachtelbar */

slang-Laufzeitumgebung

Genauso wie "richtige" Programmiersprachen bietet auch slang mit einer Grundausstattung an "intrinsischen" (d.h. eingebauten) Werten und Funktionen an. Diese liegen im globalen Kontext und sind somit von überall erreichbar - sofern sie nicht von gleichnamigen Funktionen im lokalen Kontext "überdeckt" werden.

intrinsische Werte

  • nil - repräsentiert "nichts", also einen nicht definierten Wert (JavaScript undefined)
  • true - repräsentiert den logischen Wert für "wahr"
  • false - repräsentiert den logischen Wert für "falsch"
  • pi - enthält den Wert für die Kreiszahl pi
  • e - enthält den Wert für die Euler-Konstante

Operatoren als Funktionen

Die logischen, mathematischen und vergleichenden Operatoren können auch als Funktionen aufgerufen werden - durch Überschreiben dieser Funktionen kann demnach das Verhalten der zugehörigen Operatoren verändert werden. Der Parser ruft Operatoren in Ausdrücken übrigens direkt aus dem globalen Kontext heraus auf, so dass lokale Funktionen des gleichen Namens keine Operatoren überdecken können.

  • neg(a) - liefert die Zahl a mit umgekehrtem Vorzeichen (also -a)
  • plus(a,b) - addiert die beiden Zahlen a und b bzw. hängt Zeichenketten aneinander an
  • minus(a,b) - subtrahiert die Zahl b von der Zahl a
  • times(a,b) - berechnet das Produkt der beiden Zahlen a und b
  • through(a,b) - dividiert die Zahl a durch die Zahl b
  • div(a,b) - dividiert die Zahl a durch die Zahl b und liefert den ganzzahligen Anteil des Ergebnisses
  • mod(a,b) - liefert den Rest der Division der Zahl a durch die Zahl b
  • raised(a,b) - liefert die b-te Potenz der Zahl a
     
  • lt(a,b) - liefert true dann und nur dann, wenn a < b gilt
  • le(a,b) - liefert true dann und nur dann, wenn a <= b gilt
  • eq(a,b) - liefert true dann und nur dann, wenn a = b gilt
  • ge(a,b) - liefert true dann und nur dann, wenn a >= b gilt
  • gt(a,b) - liefert true dann und nur dann, wenn a > b gilt
  • ne(a,b) - liefert true dann und nur dann, wenn a <> b gilt
     
  • not(a) - liefert die logische Negation des boole'schen Wertes a
  • and(a,b) - liefert die logische Konjunktion der boole'schen Werte a und b
  • or(a,b) - liefert die logische Disjunktion der boole'schen Werte a und b

intrinsische mathematische Funktionen

Folgende mathematische Funktionen sind in slang eingebaut:

  • is_nan(a) - liefert true dann und nur dann, wenn a den Wert NaN besitzt
     
  • sqrt(a) - liefert die Quadratwurzel von a
  • sin(a) - liefert den Sinus von a
  • cos(a) - liefert den Cosinus von a
  • tan(a) - liefert den Tangens von a
     
  • rnd(a) - liefert eine Pseudo-Zufallszahl im Bereich 0...a, wobei a selbst ausgeschlossen ist

Kontrollanweisungen als Funktionen

Auch die Anweisungen zur Steuerung des Programmflusses liegen als globale Funktionen vor und können demzufolge geändert werden. Der Parser ruft Anweisungen übrigens direkt aus dem globalen Kontext heraus auf, so dass lokale Funktionen des gleichen Namens keine Anweisungen überdecken können.

  • if_then_else (condition, then_clause, else_clause)
    ist der boole'sche Wert condition true, wird der Block then_clause ausgeführt; ist er false, wird der Block else_clause ausgeführt - sofern ein solcher Block existiert, ansonsten wird die Anweisung einfach ignoriert. Das Ergebnis der Funktion ist das Ergebnis des ausgeführten Blockes bzw. nil, falls kein Block ausgeführt wurde
  • select_when (otherwise_clause, condition,block,...)
    wertet der Reihe nach alle condition-Blöcke bzw. (logischen) Werte der gegebenen condition-block-Paare aus, hält an sobald die erste condition mit dem Ergebnis true gefunden wird und führt daraufhin den zugehörigen block aus. Liefert keine condition den Wert true, so wird stattdessen der otherwise_clause ausgeführt. Das Ergebnis der Funktion ist das Ergebnis des ausgeführten Blockes
  • for_repeat (identifier, downwards, start_value, stop_value, step_value, loop_body)
    führt den Block loop_body wiederholt mit unterschiedlichen Werten für den gegebenen identifier aus, wobei diese Werte mit start_value beginnen und bei jedem Durchlauf um step_value verändert werden. Hat das boole'sche Argument downwards den Wert true, so endet die Schleife mit dem Unterschreiten des stop_value, ansonsten mit dessen Überschreiten. Hat step_value den Wert nil, so wird stattdessen der Wert +1 (falls downwards = false) bzw. -1 (falls downwards = true) angenommen. Der identifier darf eine bereits vorhandene Variable aus dem lexikalischen Kontext bezeichnen, ansonsten wird die Variable im Kontext der Schleife angelegt. In loop_body angelegte lokale Variablen behalten von einem Durchlauf zum nächsten ihren Wert. Das Ergebnis der Schleife ist das Ergebnis der letzten Ausführung des loop_body
  • while_repeat (condition, loop_body)
    führt den Block loop_body wiederholt durch, solange eine Ausführung des Blockes condition den Wert true liefert. In loop_body angelegte lokale Variablen behalten von einem Durchlauf zum nächsten ihren Wert. Das Ergebnis der Schleife ist das Ergebnis der letzten Ausführung des loop_body
  • repeat_until (loop_body, condition)
    führt den Block loop_body wiederholt durch, bis eine Ausführung des Blockes condition den Wert true liefert. In loop_body angelegte lokale Variablen behalten von einem Durchlauf zum nächsten ihren Wert. Das Ergebnis der Schleife ist das Ergebnis der letzten Ausführung des loop_body
  • break (value)
    bricht die innerste in der gerade ausgeführten (Script-)Funktion laufende Schleife ab und liefert den gegebenen value zurück
  • continue
    fordert eine neue Iteration der innersten in der gerade ausgeführten (Script-)Funktion laufenden Schleife an - oder bricht diese ab, falls die Abbruchbedingung der Schleife erfüllt ist
  • return (value)
    bricht die gerade laufende Funktion ab und liefert den gegebenen value zurück
  • assert (value)
    prüft, ob value den Wert true hat - falls ja, wird die Programmausführung fortgesetzt, anderenfalls wird sie mit einer Fehlermeldung abgebrochen
  • assert_failure (block)
    prüft, ob bei der Ausführung des Blockes block ein Fehler auftritt - falls ja, wird der Fehler ignoriert und die Programmausführung fortgesetzt, anderenfalls wird sie mit einer Fehlermeldung abgebrochen
  • log (value)
    gibt value auf die Browser-Konsole aus

slang-Grammatik

Die Grammatik zu slang in PEG.js-Notation kommt (wie üblich) ohne separaten "Lexer" aus und ist dennoch erstaunlich kompakt.

script               = statement_list? _
block = '{' statement_list _ '}'
statement_list = (_ statement / _ ';' )*
statement = if_statement / select_statement
/ for_statement / while_statement / until_statement
/ continue_statement / break_statement / return_statement
/ expression

if_statement = 'if' _ '(' _ expression _ ')' _ 'then' _ block (_ 'else' _ block)?
select_statement = 'select' _ '{' _ when_clause* otherwise_clause? '}'
when_clause = 'when' _ '(' _ expression _ ')' _ 'then' _ block _
otherwise_clause = 'otherwise' (__ 'do')? _ block _

for_statement = 'for' __ identifier _ 'from' alone _ expression _
('down' _)? 'to' alone _ expression _ ('by' alone _ expression)? _
'repeat' _ block
while_statement = 'while' _ '(' _ expression _ ')' _ 'repeat' _ block
until_statement = 'repeat' _ block _ 'until' _ '(' _ expression _ ')'

continue_statement = 'continue' alone
break_statement = 'break' alone
return_statement = 'return' alone _ expression?

expression = assignment / or_term
or_term = (and_term _ 'or' alone _)* and_term
and_term = (not_term _ 'and' alone _)* not_term
not_term = ('not' alone _)* comparison
comparison = (additive_term _ ('<' ![=>] / '<=' / '=' !'>' / '>=' / '>' !'=' / '<>') _)? additive_term
additive_term = (multiplicative_term _ ('+' / '-') _)* multiplicative_term
multiplicative_term = (exponential_term _ ('*' / '/' / 'div' alone / 'mod' alone) _)* exponential_term
exponential_term = invocation (_ '^' _ exponential_term)?
invocation = primary ( _ '(' _ argument_list? _ ')')?
primary = literal / identifier / '(' _ expression _ ')' ! (_ '=>')
/ lambda_definition / native_definition

assignment = global_assignment / local_assignment / generic_assignment
global_assignment = 'global' __ identifier _ ':=' _ expression
local_assignment = 'local' __ identifier _ ':=' _ expression
generic_assignment = identifier _ ':=' _ expression

lambda_definition = '(' _ parameter_list? _ ')' _ '=>' _ block
native_definition = 'native' _ '(' _ parameter_list? _ ')' _ '=>' _ string

parameter_list = identifier (_ ',' _ identifier)*
argument_list = expression (_ ',' _ expression)*

literal = 'nil' alone / boolean / number / string
boolean = 'true' alone / 'false' alone
number = integer / floating_point / 'pi' alone / 'e' alone / 'nan' alone
integer = binary / decimal / hexadecimal
binary = '0b' [01]+
decimal = [+-]? digit+ ! '.'
hexadecimal = '0x' hex_digit+
floating_point = mantissa exponent?
mantissa = [+-]? (digit+ '.' digit* / '.' digit+)
exponent = [eE] [+-]? digit+
string = single_quoted_text / double_quoted_text /
single_quoted_string / double_quoted_string
single_quoted_string = "'" (escape_sequence / & no_control_char [^\'])* "'"
double_quoted_string = '"' (escape_sequence / & no_control_char [^\"])* '"'
single_quoted_text = "'''" (escape_sequence / !"'''" [\'] / [^\'])* "'''"
double_quoted_text = '"""' (escape_sequence / !'"""' [\"] / [^\"])* '"""'
escape_sequence = ('\\' [bfnrtv0'"\\]) / ('\\x' hex_digit{2}) / ('\\u' hex_digit{4})
no_control_char = [^\x00-\x1F\x7F-\x9F\u200B\u2028\u2029\u2060]
digit = [0-9]
hex_digit = [0-9a-fA-F]

identifier = ! (reserved_word alone) identifier_start identifier_part*
identifier_start = [$_a-zA-Z]
identifier_part = [$_a-zA-Z0-9]

alone = ! identifier_part

reserved_word = 'global' / 'local' / 'it' / 'native'
/ 'nil' / 'true' / 'false' / 'nan' / 'e' / 'pi'
/ 'div' / 'mod' / 'and' / 'or' / 'not'
/ 'if' / 'then' / 'else' / 'select' / 'when' / 'otherwise'
/ 'for' / 'from' / 'down' / 'to' / 'by' / 'repeat'
/ 'while' / 'until' / 'continue' / 'break' / 'return'

comment = (line_comment / block_comment)
line_comment = '//' [^\n\r]* [\n\r]*
block_comment = '/*' (!'*/' .)* '*/'

_ = ([ \t\n\r] / comment)*
__ = ([ \t\n\r] / comment)+
/**** syntax smoke tests ****/

/* block comment */
// line comment

/**** literals ****/

nil
true false
e pi nan; +123; -123. 123.456 .456 .456e+78 .456e-78 .456e78
'' 'single-quoted string' 'escape sequences \b\f\n\r\t\v\0\'\"\\ \x12 \u1234 '
"" "double-quoted string" "escape sequences \b\f\n\r\t\v\0\'\"\\ \x12 \u1234 "
'''
single-quoted text
with ' " and escape sequences \b\f\n\r\t\v\0\'\"\\ \x12 \u1234
'''
"""
double-quoted text
with ' " and escape sequences \b\f\n\r\t\v\0\'\"\\ \x12 \u1234
"""

/**** variable definition and access ****/

a b := true local c := nil global d := pi

a := () => {}
b := (a) => { a }
c := (a,b,c) => { a+b+c }

a := native () => ''
b := native (a) => "return a"
c := native (a,b,c) => "return a+b+c"

/**** statements ****/

if (a = b) then { true }
if (a = b) then { true } else { false }

select {
when (a > b) then { 'greater than' }
when (a = b) then { 'equal' }
otherwise { 'smaller than' }
}

for i from 0 to 9 repeat { i }
for i from 9 down to 0 by -1 repeat { i }

i := 0
while (i < 9) repeat { i := i+1 }
repeat { i := i-1 } until (i = 0)

break continue return

/**** expressions ****/

a + b - c * d / e div f mod g ^ h
a < b b <= c c = d d >= e e > f f <> g
not a or b and not c

slang-Grammatik mit AST-Generierung

Mit ein paar zusätzlichen JavaScript-Dekorierungen ist diese Grammatik in der Lage, einen "abstract syntax tree" (AST) auszugeben.

{
let whitespace = {}

function without_whitespace (list) {
let result = []
for (let i = 0, l = list.length; i < l; i++) {
switch (true) {
case (list[i] === whitespace):
continue
case Array.isArray(list[i]):
result.push(without_whitespace(list[i]))
break
default:
result.push(list[i])
}
}
return result
}

let name_of = Object.assign(Object.create(null),{
'and':'and', 'or':'or', 'not':'not',
'<':'lt', '<=':'le', '=':'eq', '>=':'ge', '>':'gt',
'+':'plus', '-':'minus', '*':'times', '/':'through', 'div':'div', 'mod':'mod',
'^':'raised'
})

function prefixed (operators, operand) {
return (
operators == null
? operand
: operators.reduceRight(
(result,operator) => ['#call',name_of[operator],result], operand
)
)
}

function left_associative (left_operand, operations) {
return (
operations == null
? left_operand
: without_whitespace(operations).reduce(
(result,operation) => ['#call',name_of[operation[0]],result,operation[1]],
left_operand
)
)
}

function right_associative (left_operand, operation) {
return (
operation == null
? left_operand
: (
operation = without_whitespace(operation),
['#call',name_of[operation[0]],left_operand,operation[1]]
)
)
}

function unescaped (char_list) {
return char_list.join('').replace(/\\[bfnrtv0'"\\]|\\x[0-9]{2}|\\u[0-9a-f]{4}/gi,(match) => {
switch (match.charAt(1)) {
case 'b': return '\b'; case 'f': return '\\f'
case 'n': return '\n'; case 'r': return '\\r'
case 't': return '\t'; case 'v': return '\\v'
case '0': return '\0'; case "'": return "'"
case '"': return '"'; case '\\': return '\\'
case 'x':
case 'u': return String.fromCharCode(parseInt(match.slice(2),16))
}
})
}
}

script = statements:statement_list? _ { return statements }
block = '{' statements:statement_list _ '}'
{ return ['#block',statements] }
statement_list = statements:(_ statement / _ ';' )*
{ return statements.filter((stmt) => stmt[1] !== ';').map((stmt) => stmt[1]) }
statement = if_statement / select_statement
/ for_statement / while_statement / until_statement
/ continue_statement / break_statement / return_statement
/ expression

if_statement = 'if' _ '(' _ condition:expression _ ')' _ 'then' _ then_clause:block
else_clause:(_ 'else' _ block:block { return block })?
{ return ['#if_then_else', condition, then_clause, else_clause] }
select_statement = 'select' _ '{' _ when_clauses:when_clause* otherwise_clause:otherwise_clause? '}'
{ return ['#select_when', otherwise_clause].concat(when_clauses) }
when_clause = 'when' _ '(' _ condition:expression _ ')' _ 'then' _ block:block _
{ return [condition,block] }
otherwise_clause = 'otherwise' (__ 'do')? _ block:block _ { return block }

for_statement = 'for' __ identifier:identifier _ 'from' alone _ start_value:expression _
downwards:('down' _ { return true })? 'to' alone _ stop_value:expression _
step_value:('by' alone _ value:expression { return value })? _
'repeat' _ loop_body:block
{ return ['#for_repeat', identifier, ['#value',!!downwards], start_value, stop_value, step_value, loop_body] }
while_statement = 'while' _ '(' _ expression:expression _ ')' _ 'repeat' _ block:block
{ return ['#while_repeat',['#block',expression],block] }
until_statement = 'repeat' _ block:block _ 'until' _ '(' _ expression:expression _ ')'
{ return ['#repeat_until',block,['#block',expression]] }

continue_statement = 'continue' alone { return ['#continue'] }
break_statement = 'break' alone { return ['#break'] }
return_statement = 'return' alone _ argument:expression? { return ['#return'].concat(argument || []) }

expression = assignment / or_term
or_term = left_operand:and_term operations:(_ 'or' alone _ and_term)*
{ return left_associative(left_operand,operations) }
and_term = left_operand:not_term operations:(_ 'and' alone _ not_term)*
{ return left_associative(left_operand,operations) }
not_term = operators:('not' alone _)* operand:comparison
{ return prefixed(operators,operand) }
comparison = left_operand:additive_term operations:(_ ('<' ![=>] / '<=' / '=' !'>' / '>=' / '>' !'=' / '<>') _ additive_term)?
{ return left_associative(left_operand,operations) }
additive_term = left_operand:multiplicative_term operations:(_ ('+' / '-') _ multiplicative_term)*
{ return left_associative(left_operand,operations) }
multiplicative_term = left_operand:exponential_term operations:(_ ('*' / '/' / 'div' alone / 'mod' alone) _ exponential_term)*
{ return left_associative(left_operand,operations) }
exponential_term = left_operand:invocation operation:(_ '^' _ exponential_term)?
{ return right_associative(left_operand,operation) }
invocation = callee:primary args:( _ '(' _ argument_list? _ ')')?
{ return (args == null ? callee : ['#call',callee].concat(args[3] || [])) }
primary = literal
/ 'global' __ identifier:identifier { return ['#get-global',identifier] }
/ 'local' __ identifier:identifier { return ['#get-local',identifier] }
/ identifier:identifier { return ['#get-var',identifier] }
/ '(' _ expression:expression _ ')' ! (_ '=>') { return expression }
/ lambda_definition / native_definition

assignment = global_assignment / local_assignment / generic_assignment
global_assignment = 'global' __ key:identifier _ ':=' _ value:expression
{ return ['#set-global',key,value] }
local_assignment = 'local' __ key:identifier _ ':=' _ value:expression
{ return ['#set-local',key,value] }
generic_assignment = key:identifier _ ':=' _ value:expression
{ return ['#set-var',key,value] }

lambda_definition = '(' _ parameters:parameter_list? _ ')' _ '=>' _ body:block
{ return ['#lambda',parameters,body] }
native_definition = 'native' _ '(' _ parameters:parameter_list? _ ')' _ '=>' _ body:string
{ return ['#native',parameters,body] }

parameter_list = identifier:identifier identifiers:(_ ',' _ identifier)*
{ return [identifier].concat((identifiers || []).map((list) => list[3])) }
argument_list = expression:expression expressions:(_ ',' _ expression)*
{ return [expression].concat((expressions || []).map((list) => list[3])) }

literal = value:('nil' alone { return undefined } / boolean / number / string)
{ return ['#value',value] }
boolean = 'true' alone { return true }
/ 'false' alone { return false }
number = integer / floating_point / 'nan' alone { return NaN }
integer = binary { return parseInt(text().slice(2),2) }
/ decimal { return parseFloat(text()) }
/ hexadecimal { return parseInt(text().slice(2),16) }
binary = '0b' [01]+
decimal = [+-]? digit+ ! '.'
hexadecimal = '0x' hex_digit+
floating_point = mantissa exponent?
mantissa = [+-]? (digit+ '.' digit* / '.' digit+)
exponent = [eE] [+-]? digit+
string = single_quoted_text / double_quoted_text /
single_quoted_string / double_quoted_string
single_quoted_string = "'" content:(escape_sequence / & no_control_char [^\'])* "'" { return unescaped(content) }
double_quoted_string = '"' content:(escape_sequence / & no_control_char [^\"])* '"' { return unescaped(content) }
single_quoted_text = "'''" content:(escape_sequence / !"'''" [\'] / [^\'])* "'''" { return unescaped(content) }
double_quoted_text = '"""' content:(escape_sequence / !'"""' [\"] / [^\"])* '"""' { return unescaped(content) }
escape_sequence = ('\\' [bfnrtv0'"\\]) / ('\\x' hex_digit{2}) / ('\\u' hex_digit{4})
no_control_char = [^\x00-\x1F\x7F-\x9F\u200B\u2028\u2029\u2060]
digit = [0-9]
hex_digit = [0-9a-fA-F]

identifier = ! (reserved_word alone) identifier_start identifier_part*
{ return ['#value',text()] } /* makes control statements callable */
identifier_start = [$_a-zA-Z]
identifier_part = [$_a-zA-Z0-9]

alone = ! identifier_part

reserved_word = 'global' / 'local' / 'native'
/ 'nil' / 'true' / 'false' / 'nan'
/ 'div' / 'mod' / 'and' / 'or' / 'not'
/ 'if' / 'then' / 'else' / 'select' / 'when' / 'otherwise'
/ 'for' / 'from' / 'down' / 'to' / 'by' / 'repeat'
/ 'while' / 'until' / 'continue' / 'break' / 'return'

comment = (line_comment / block_comment) { return whitespace }
line_comment = '//' [^\n\r]* [\n\r]*
block_comment = '/*' (!'*/' .)* '*/'

_ = ([ \t\n\r] / comment)* { return whitespace }
__ = ([ \t\n\r] / comment)+ { return whitespace }

Vollständiger slang-Interpreter

Von der AST-Ausgabe zu einem vollwertigen Interpreter ist es nicht mehr weit - allerdings muss dazu JavaScript programmiert werden, die Domäne der reinen Syntax-Spezifikation wird folglich verlassen.

{
let whitespace = {}

function without_whitespace (list) {
let result = []
for (let i = 0, l = list.length; i < l; i++) {
switch (true) {
case (list[i] === whitespace):
case (list[i] === undefined):
continue
case Array.isArray(list[i]):
result.push(without_whitespace(list[i]))
break
default:
result.push(list[i])
}
}
return result
}

/**** constructor functions ****/

function form (type, items) {
Object.assign(this,{ type,items })
}

function block (statement_list, context) {
Object.assign(this,{ statement_list,context })
}

function lambda (parameter_list, statement_list) {
Object.assign(this,{ parameter_list, statement_list })
}

function new_native (parameter_list, native_body) {
return (
parameter_list.length === 0
? new Function(native_body)
: new Function(parameter_list.join(),native_body)
)
}

/**** operator handling ****/

let name_of = Object.assign(Object.create(null),{
'and':'and', 'or':'or', 'not':'not',
'<':'lt', '<=':'le', '=':'eq', '>=':'ge', '>':'gt', '<>':'ne',
'+':'plus', '-':'minus', '*':'times', '/':'through', 'div':'div', 'mod':'mod',
'neg':'neg', '^':'raised'
})

function prefixed (operators, operand) {
return (
operators.length === 0
? operand
: without_whitespace(operators).reduceRight(
(result,operator) => new form('#call-global',[name_of[operator],result]), operand
)
)
}

function left_associative (left_operand, operations) {
return (
operations.length === 0
? left_operand
: without_whitespace(operations).reduce(
(result,operation) => new form('#call-global',[name_of[operation[0]],result,operation[1]]),
left_operand
)
)
}

function right_associative (left_operand, operation) {
return (
operation == null
? left_operand
: (
operation = without_whitespace(operation),
new form('#call-global',[name_of[operation[0]],left_operand,operation[1]])
)
)
}

function unescaped (text) {
return text.replace(/\\[bfnrtv0'"\\]|\\x[0-9]{2}|\\u[0-9a-f]{4}/gi,(match) => {
switch (match.charAt(1)) {
case 'b': return '\b'; case 'f': return '\\f'
case 'n': return '\n'; case 'r': return '\\r'
case 't': return '\t'; case 'v': return '\\v'
case '0': return '\0'; case "'": return "'"
case '"': return '"'; case '\\': return '\\'
case 'x':
case 'u': return String.fromCharCode(parseInt(match.slice(2),16))
}
})
}

function flattened (list) {
return list.reduce((result,sublist) => result.concat(sublist),[])
}

/**** script evaluation ****/

function evaluated_script (statement_list) {
let context = Object.create(global_context)
let callee = new block(statement_list,context)

activation_stack.push({ callee,context })
let result
try {
result = executed_block(callee,[],context)
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
throw new Error('no loop to be continued')
case (signal instanceof loop_termination):
throw new Error('no loop to be terminated')
case (signal instanceof function_termination):
throw new Error('cannot "return" from script')
case (signal instanceof function_replacement):
throw new Error('cannot "return" from script')
default:
throw signal
}
}
activation_stack.pop()

console.log('script result',result)
return result
}

/**** runtime system ****/

let global_context = Object.create(null)
let activation_stack = []

function context_contains (context, var_name) {
return Object.prototype.hasOwnProperty.call(context,var_name)
}

function evaluated (value, context) {
return (value instanceof form ? evaluated_form(value,context) : value)
}

function evaluated_form (form, context) {
let callee, value_list
switch (form.type) {
case '#block': return new block(form.items,context)
case '#get-var': return context[form.items[0]]
case '#set-var': return set_var(context,form.items[0],evaluated(form.items[1],context))
case '#get-global': return global_context[form.items[0]]
case '#set-global': return global_context[form.items[0]] = evaluated(form.items[1],context)
case '#get-local': return (context_contains(context,form.items[0]) ? context[form.items[0]] : undefined)
case '#set-local': return context[form.items[0]] = evaluated(form.items[1],context)
case '#call':
value_list = form.items.map((value) => evaluated(value,context))
return executed(value_list[0],value_list.slice(1))
case '#call_tce':
value_list = form.items.map((value) => evaluated(value,context))
throw new function_replacement(value_list[0],value_list.slice(1))
case '#call-global':
callee = global_context[form.items[0]]
value_list = form.items.slice(1).map((value) => evaluated(value,context))
return executed(callee,value_list)
default: throw new Error('unforeseen form type "' + form.type + '"')
}
}

function executed (callee, argument_list) {
for (;;) {
let context, stack_depth, result
try {
switch (true) {
case (typeof callee === 'function'):
return callee.apply(activation_stack,argument_list)
case (callee instanceof block):
context = Object.create(callee.context)
stack_depth = activation_stack.length
activation_stack.push({ callee,context })
result = executed_block(callee,argument_list,context)
activation_stack.pop()
return result
case (callee instanceof lambda):
context = Object.create(global_context)
stack_depth = activation_stack.length
activation_stack.push({ callee, context })
result = executed_block(callee,argument_list,context)
activation_stack.pop()
return result
default:
console.log('cannot execute',callee,'(',argument_list,')')
throw new Error('cannot execute value of type "' + typeof callee + '"')
}
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
if (callee instanceof lambda) {
throw new Error('no loop to be continued')
}
throw signal
case (signal instanceof loop_termination):
if (callee instanceof lambda) {
throw new Error('no loop to be terminated')
}
throw signal
case (signal instanceof function_termination):
if (callee instanceof lambda) {
activation_stack.length = stack_depth
return signal.value
}
throw signal
case (signal instanceof function_replacement):
if (callee instanceof lambda) {
activation_stack.length = stack_depth
callee = signal.callee; argument_list = signal.argument_list
continue
}
throw signal
default: throw signal
}
}
}
}

function executed_block (callee, argument_list, context) {
if (callee.parameter_list != null) {
callee.parameter_list.forEach((parameter,index) => {
context[parameter] = argument_list[index]
})
}

let statement_list = callee.statement_list
for (let i = 0, l = statement_list.length; i < l; i++) {
context.it = evaluated(statement_list[i],context)
}

return context.it
}

function set_var (context, var_name, var_value) {
let original_context = context
while (context !== global_context) {
if (context_contains(context,var_name)) { return context[var_name] = var_value }
context = Object.getPrototypeOf(context)
}
return original_context[var_name] = var_value
}

/**** special exceptions ****/

function loop_continuation () { /* nop */ }
function loop_termination (value) { this.value = value }

function function_termination (value) { this.value = value }
function function_replacement (callee, argument_list) {
Object.assign(this,{ callee,argument_list })
}

/**** auxiliary functions ****/

function active_context () {
return activation_stack[activation_stack.length-1].context
}/**** runtime environment ****/

function throwError (message) { throw new Error(message) }

function boolean_type (a) {
return (typeof a === 'boolean') ||
throwError('boolean value expected (got ' + typeof a + ')')
}

function boolean_types (a,b) {
return boolean_type(a) || boolean_type(b)
}

function numeric_type (a) {
return (typeof a === 'number') ||
throwError('numeric value expected (got ' + typeof a + ')')
}

function numeric_types (a,b) {
return numeric_type(a) || numeric_type(b)
}

function block_type (a) {
return (a instanceof block) ||
throwError('statement block expected')
}

function identifier_type (a) {
return (typeof a === 'string') && /^[$_a-z][$_a-z0-9]*$/i.test(a) ||
throwError('valid identifier expected (got ' + typeof a + ')')
}

function same_types (a,b) {
return (typeof a === typeof b) ||
throwError('values are of different types (' + typeof a + ' <> ' + typeof b + ')')
}

function addable_types (a,b) {
return (
((typeof a === 'number') || (typeof a === 'string')) &&
((typeof b === 'number') || (typeof b === 'string')) ||
throwError('numeric or literal values expected')
)
}

let assertion_counter = 0

Object.assign(global_context,{
nil:undefined,
true:true, false:false,

plus: (a,b) => addable_types(a,b) && (a+b),
minus: (a,b) => numeric_types(a,b) && (a-b),
times: (a,b) => numeric_types(a,b) && (a*b),
through: (a,b) => numeric_types(a,b) && (a/b),
div: (a,b) => numeric_types(a,b) && Math.trunc(a/b),
mod: (a,b) => numeric_types(a,b) && (a%b),
raised: (a,b) => numeric_types(a,b) && Math.pow(a,b),

neg: (a) => numeric_type(a) && (-a),

lt: (a,b) => same_types(a,b) && (a < b),
le: (a,b) => same_types(a,b) && (a <= b),
eq: (a,b) => (a === b),
ge: (a,b) => same_types(a,b) && (a >= b),
gt: (a,b) => same_types(a,b) && (a > b),
ne: (a,b) => (a !== b),

and: (a,b) => boolean_types(a,b) && (a && b),
or: (a,b) => boolean_types(a,b) && (a || b),
not: (a) => boolean_type(a) && (! a),

pi: Math.PI,
e: Math.E,

is_nan: isNaN,
sqrt: (a) => numeric_type(a) && Math.sqrt(a),
sin: (a) => numeric_type(a) && Math.sin(a),
cos: (a) => numeric_type(a) && Math.cos(a),
tan: (a) => numeric_type(a) && Math.tan(a),
rnd: (a) => numeric_type(a) && (Math.random() * a),

if_then_else: (condition, then_clause, else_clause) => {
switch (condition) {
case true: return block_type(then_clause) && executed(then_clause,[])
case false: return (
else_clause == null
? undefined
: block_type(else_clause) && executed(else_clause,[])
)
default: throw new Error('boolean value expected')
}
},

select_when: function (otherwise_clause /* condition,block... */) {
let clause_list = Array.prototype.slice.call(arguments,1)

for (let i = 0, l = clause_list.length; i < l; i += 2) {
let condition = clause_list[i]
if (
boolean_type(
condition instanceof block
? condition = executed(condition,[])
: condition
) && (condition == true)
) {
return executed(clause_list[i+1],[])
}
}

return otherwise_clause == null ? undefined : executed(otherwise_clause,[])
},

for_repeat: (identifier, downwards, start_value, stop_value, step_value, loop_body) => {
identifier_type(identifier) && boolean_type(downwards) &&
numeric_type(start_value) && numeric_type(stop_value) &&
((step_value == null) || numeric_type(step_value)) &&
block_type(loop_body)

if (step_value === 0) { throw new Error('step value must not be 0') }
if (step_value == null) { step_value = (downwards ? -1 : 1) }

let callee = global_context.for_repeat
let context = Object.create(active_context())
activation_stack.push({ callee,context }); let stack_depth = activation_stack.length
let i, to_be_continued = () => (downwards ? i >= stop_value : i <= stop_value)

for (i = start_value; to_be_continued(); i += step_value) {
context[identifier] = i

try {
executed_block(loop_body,[],context)
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
activation_stack.length = stack_depth
break
case (signal instanceof loop_termination):
activation_stack.length = stack_depth-1
return signal.value
default: throw signal
}
}
}
activation_stack.pop()

return context.it
},

while_repeat: (condition, loop_body) => {
block_type(condition) && block_type(loop_body)

let callee = global_context.while_repeat
let context = Object.create(active_context())
activation_stack.push({ callee,context }); let stack_depth = activation_stack.length
for (;;) {
try {
let condition_value = executed_block(condition,[],context)
if (boolean_type(condition_value) && (condition_value == false)) { break }
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
throw new Error('must not "continue" in loop condition')
case (signal instanceof loop_termination):
throw new Error('must not "break" in loop condition')
default: throw signal
}
}

try {
executed_block(loop_body,[],context)
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
activation_stack.length = stack_depth
continue
case (signal instanceof loop_termination):
activation_stack.length = stack_depth-1
return signal.value
default: throw signal
}
}
}
activation_stack.pop()

return context.it
},
repeat_until: (loop_body, condition) => {
block_type(loop_body)

let callee = global_context.while_repeat
let context = Object.create(active_context())
activation_stack.push({ callee,context }); let stack_depth = activation_stack.length
for (;;) {
try {
executed_block(loop_body,[],context)
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
activation_stack.length = stack_depth
break
case (signal instanceof loop_termination):
activation_stack.length = stack_depth-1
return signal.value
default: throw signal
}
}

try {
let condition_value = executed_block(condition,[],context)
if (boolean_type(condition_value) && (condition_value == false)) { break }
} catch (signal) {
switch (true) {
case (signal instanceof loop_continuation):
throw new Error('must not "continue" in loop condition')
case (signal instanceof loop_termination):
throw new Error('must not "break" in loop condition')
default: throw signal
}
}
}
activation_stack.pop()

return context.it
},

'break': (value) => {
throw new loop_termination( value == null ? undefined : value )
},

'continue': () => {
throw new loop_continuation()
},

'return': (value) => {
throw new function_termination( value == null ? undefined : value )
},

assert: (value) => {
assertion_counter++
if (value === true) {
return true
} else {
throw new Error('assertion #' + assertion_counter + ' failed')
}
},

assert_failure: (block) => {
block_type(block)

assertion_counter++
try {
executed(block,[])
} catch (signal) { return true }

throw new Error(
'assertion #' + assertion_counter + ' failed (block did not fail)'
)
},

log: (value) => {
console.log(value)
}
})
}

script = statements:statement_list _
{ console.log('statements',statements); return evaluated_script(statements) }
block = '{' statements:statement_list _ '}'
{ return new form('#block',statements) }
statement_list = statements:(_ statement / _ ';' )*
{ return statements.filter((stmt) => stmt[1] !== ';').map((stmt) => stmt[1]) }
statement = if_statement / select_statement
/ for_statement / while_statement / until_statement
/ continue_statement / break_statement / return_statement
/ expression

if_statement = 'if' _ '(' _ condition:expression _ ')' _ 'then' _ then_clause:block
else_clause:(_ 'else' _ block:block { return block })?
{ return new form('#call-global',['if_then_else',condition,then_clause,else_clause]) }
select_statement = 'select' _ '{' _ when_clauses:when_clause* otherwise_clause:otherwise_clause? '}'
{ return new form('#call-global',['select_when',otherwise_clause].concat(flattened(when_clauses))) }
when_clause = 'when' _ '(' _ condition:expression _ ')' _ 'then' _ block:block _
{ return [condition,block] }
otherwise_clause = 'otherwise' (__ 'do')? _ block:block _ { return block }

for_statement = 'for' __ identifier:identifier _ 'from' alone _ start_value:expression _
downwards:('down' _ { return true })? 'to' alone _ stop_value:expression _
step_value:('by' alone _ value:expression { return value })? _
'repeat' _ loop_body:block
{ return new form('#call-global',['for_repeat',identifier,!!downwards,start_value,stop_value,step_value,loop_body]) }
while_statement = 'while' _ '(' _ expression:expression _ ')' _ 'repeat' _ loop_body:block
{ return new form('#call-global',['while_repeat',new form('#block',[expression]),loop_body]) }
until_statement = 'repeat' _ loop_body:block _ 'until' _ '(' _ expression:expression _ ')'
{ return new form('#call-global',['repeat_until',loop_body,new form('#block',[expression])]) }

continue_statement = 'continue' alone { return new form('#call-global',['continue']) }
break_statement = 'break' alone (_ result:expression { return result })?
{ return new form('#call-global',['break',result]) }
return_statement = 'return' alone _ result:expression?
{
if ((result instanceof form) && (result.type === '#call')) {
result.type = '#call_tce'
}
return new form('#call-global',['return',result])
}

expression = assignment / or_term
or_term = left_operand:and_term operations:(_ 'or' alone _ and_term)*
{ return left_associative(left_operand,operations) }
and_term = left_operand:not_term operations:(_ 'and' alone _ not_term)*
{ return left_associative(left_operand,operations) }
not_term = operators:('not' alone _)* operand:comparison
{ return prefixed(operators,operand) }
comparison = left_operand:additive_term operation:(_ ('<' ![=>] { return '<' } / '<=' / '=' !'>' { return '=' } / '>=' / '>' !'=' { return '>' } / '<>') _ additive_term)?
{ return left_associative(left_operand,operation == null ? [] : [operation]) }
additive_term = left_operand:multiplicative_term operations:(_ ('+' / '-') _ multiplicative_term)*
{ return left_associative(left_operand,operations) }
multiplicative_term = left_operand:negation_term operations:(_ ('*' / '/' / 'div' alone { return 'div' } / 'mod' alone { return 'mod' }) _ negation_term)*
{ return left_associative(left_operand,operations) }
negation_term = operators:('-' ! (_ digit) _ { return 'neg' })* operand:exponential_term
{ return prefixed(operators,operand) }
exponential_term = left_operand:invocation operation:(_ '^' _ exponential_term)?
{ return right_associative(left_operand,operation) }
invocation = callee:primary args:( _ '(' _ argument_list? _ ')')?
{ return (args == null ? callee : new form('#call',[callee].concat(args[3] || []))) }
primary = literal
/ 'global' __ identifier:identifier { return new form('#get-global',[identifier]) }
/ 'local' __ identifier:identifier { return new form('#get-local',[identifier]) }
/ identifier:identifier { return new form('#get-var',[identifier]) }
/ '(' _ expression:expression _ ')' ! (_ '=>') { return expression }
/ lambda_definition / native_definition

assignment = global_assignment / local_assignment / generic_assignment
global_assignment = 'global' __ key:identifier _ ':=' _ value:expression
{ return new form('#set-global',[key,value]) }
local_assignment = 'local' __ key:identifier _ ':=' _ value:expression
{ return new form('#set-local',[key,value]) }
generic_assignment = key:identifier _ ':=' _ value:expression
{ return new form('#set-var',[key,value]) }

lambda_definition = '(' _ parameters:parameter_list? _ ')' _ '=>' _ body:block
{ return new lambda(parameters || [],body.items) }
native_definition = 'native' _ '(' _ parameters:parameter_list? _ ')' _ '=>' _ body:string
{ return new_native(parameters || [],body) }

parameter_list = identifier:identifier identifiers:(_ ',' _ identifier)*
{ return [identifier].concat((identifiers || []).map((list) => list[3])) }
argument_list = expression:expression expressions:(_ ',' _ expression)*
{ return [expression].concat((expressions || []).map((list) => list[3])) }

literal = 'nil' alone { return undefined }
/ boolean / number / string
/ block
boolean = 'true' alone { return true }
/ 'false' alone { return false }
number = binary / hexadecimal / decimal / 'nan' alone { return NaN }
binary = '0b' [01]+ { return parseInt(text().slice(2),2) }
hexadecimal = '0x' hex_digit+ { return parseInt(text().slice(2),16) }
decimal = mantissa exponent? { return parseFloat(text()) }
mantissa = [+-]? (digit+ ('.' digit*)? / '.' digit+)
exponent = [eE] [+-]? digit+
string = single_quoted_text / double_quoted_text /
single_quoted_string / double_quoted_string
single_quoted_string = "'" content:(escape_sequence / & no_control_char char:[^\'] { return char })* "'" { return unescaped(content.join('')) }
double_quoted_string = '"' content:(escape_sequence / & no_control_char char:[^\"] { return char })* '"' { return unescaped(content.join('')) }
single_quoted_text = "'''" content:(escape_sequence / !"'''" char:[\'] { return char } / [^\'])* "'''" { return unescaped(content.join('')) }
double_quoted_text = '"""' content:(escape_sequence / !'"""' char:[\"] { return char } / [^\"])* '"""' { return unescaped(content.join('')) }
escape_sequence = (('\\' [bfnrtv0'"\\]) / ('\\x' hex_digit{2}) / ('\\u' hex_digit{4}))
{ return text() }
no_control_char = [^\x00-\x1F\x7F-\x9F\u200B\u2028\u2029\u2060]
digit = [0-9]
hex_digit = [0-9a-fA-F]

identifier = ! (reserved_word alone) identifier_start identifier_part*
{ return text() }
identifier_start = [$_a-zA-Z]
identifier_part = [$_a-zA-Z0-9]

alone = ! identifier_part

reserved_word = 'global' / 'local' / 'native'
/ 'nil' / 'true' / 'false' / 'nan'
/ 'div' / 'mod' / 'and' / 'or' / 'not'
/ 'if' / 'then' / 'else' / 'select' / 'when' / 'otherwise'
/ 'for' / 'from' / 'down' / 'to' / 'by' / 'repeat'
/ 'while' / 'until' / 'continue' / 'break' / 'return'

comment = (line_comment / block_comment) { return whitespace }
line_comment = '//' [^\n\r]* [\n\r]*
block_comment = '/*' (!'*/' .)* '*/'

_ = ([ \t\n\r] / comment)* { return whitespace }
__ = ([ \t\n\r] / comment)+ { return whitespace }

Beispiele

Die folgenden Beispiele zeigen die Verwendung der unterschiedlichen Werte, Operatoren und Anweisungen - und dienen zugleich als eine Art "Smoke Test" für den Interpreter

  assert(nil = nil)

assert(true)
assert(not false)

assert(pi > 3)
assert(e > 2)
assert(is_nan(nan))

assert(0b1001 = 9)
assert(0x11 = 17)

assert(-123 < 0)
assert(+123.456 > 123)
assert(.123e3 = 123)

assert('Test' = "Test")
assert('''
Test
''' = """
Test
""")
assert('''
Test
''' = '\n Test\n ')

global test := 'Test ' + (0b0001+2*0x03)
assert(test = 'Test 7')
  assert((true and false) = false)
assert((true or false) = true)
assert((not true) = false)

assert(0 < 1)
assert(0 <= 0)
assert(1 = 1)
assert(1 >= 1)
assert(1 > 0)
assert(1 <> 0)

assert(1 + 1 = 2)
assert(1 - 1 = 0)
assert(1 * 1 = 1)
assert(1 / 1 = 1)
assert(3 div 2 = 1)
assert(3 mod 2 = 1)
assert(-(1) < 0)
assert(2 ^ 3 = 8)
  global a := 'global a'
a := 'a'
{
assert(a = 'a')
assert(global a = 'global a')

a := 'context a'
assert(a = 'context a')
assert(global a = 'global a')

local a := 'local a'
assert(a = 'local a')
assert(global a = 'global a')
}()

assert(a = 'context a')
  a := 'test'
block := { b := 'function ' b + a }
assert (block() = 'function test')

fn := (a,b) => { return a + 'function ' + b }
assert (fn('this is a ',a) = 'this is a function test')

fn := native (a,b) => "return a + 'function ' + b"
assert (fn('this is a ',a) = 'this is a function test')
  a := 0
if (a = 0) then { 'zero' } else { 'not zero' }
assert(it = 'zero')

if (a < 0) then { 'negative' } else { 'not negative' }
assert(it = 'not negative')

select {
when (a < 0) then { 'negative' }
when (a = 0) then { 'zero' }
otherwise { 'positive' }
}
assert(it = 'zero')

test := ''
for i from 1 to 10 by 2 repeat { test := test + i }
assert(test = '13579')

test := ''
for i from 9 down to 1 by -2 repeat { test := test + i }
assert(test = '97531')

test := '' i := 1
while (i < 10) repeat {
if (i mod 2 = 0) then { test := test + i }
i := i + 1
}
assert(test = '2468')

test := '' i := 1
repeat {
if (i mod 2 = 0) then { test := test + i }
i := i + 1
} until (i < 10)
assert(test = '2468')

test := ''
for i from 1 to 10 repeat {
if (i mod 2 = 0) then { continue }
test := test + i
}
assert(test = '13579')

test := '' i := 1
while (i < 100) repeat {
if (i mod 2 = 0) then { test := test + i }
i := i + 1
if (i > 9) then { break } else { continue }
}
assert(test = '2468')

Test der "tail call elimination" in slang

Zu den Besonderheiten von "slang" gehört das Auflösen von "Endaufrufen" und insbesondere auch "Endrekursionen" - auf diese Weise können (ähnlich wie in manchen streng funktionalen Sprachen) Schleifen gefahrlos durch Rekursionen ersetzt werden.

Zu den klassischen Beispielen für Funktionen, die für eine "tail recursion elimination" umgeschrieben werden können, gehören die mathematische Fakultät und die Berechnung der Fibonacci-Reihe - auch wenn moderne Sprachen (wie z.B. JavaScript) auf leistungsfähigen Rechnern heutzutage eher die Grenze des numerischen Wertebereiches erreichen als auf Probleme mit der Rekursion stoßen.

  native_factorial := native (n) => '''
function factorial (n) {
return (n <= 1 ? 1 : n * factorial(n-1))
}
return factorial(n)
'''

global factorial := (n) => {
if (n <= 1) then { 1 } else { n * factorial(n-1) }
}
assert(native_factorial(170) = factorial(170))
  native_fibonacci := native (n) => '''
function fibonacci (n, aux1, aux2) {
if (n <= 1) {
return aux1 + aux2
} else {
return fibonacci(n-1, aux1 + aux2, aux1)
}
}
return fibonacci(n-1,1,0)
'''

global fibonacci := (n, aux1, aux2) => {
if ((aux1 = nil) or (aux2 = nil)) then {
return fibonacci(n-1,1,0)
} else {
if (n <= 1) then {
return aux1 + aux2
} else {
return fibonacci(n-1, aux1 + aux2, aux1)
}
}
}

assert(native_fibonacci(1476) = fibonacci(1476))

Will man einen modernen Rechner in die Knie zwingen, muss man schon zu "heftigeren" Methoden greifen - z.B. die Verwendung von Rekursionen anstelle von Schleifen.

  native_sum_up := native (n, sum) => '''
function sum_up (n, sum) {
return (n <= 0 ? sum : sum_up(n-1,sum+n))
}
return sum_up(n,sum)
'''
native_sum_up(100000,0)
// Maximum call stack size exceeded
  global sum_up := (n, sum)  => {
if (n <= 0) then { sum } else { return sum_up(n-1,sum+n) }
}
sum_up(100000,0)

Diese Web-Seite verwendet die folgenden Drittanbieter-Bibliotheken oder -Materialien bzw. StackOverflow-Antworten:

Der Autor dankt den Entwicklern und Autoren der genannten Beiträge für ihre Mühe und die Bereitschaft, ihre Werke der Allgemeinheit zur Verfügung zu stellen.