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.
In einem ersten Schritt wird eine ggb. XML-Datei analysiert und in eine äquivalente JSON-Datei umgewandelt.
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]+
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 ::= [^"]*
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'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>
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:
Et voilà, Sie haben eine eigene Skriptsprache spezifiziert und implementiert!
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.
slang kennt nur die folgenden Datentypen
nil
- repräsentiert "nichts", also einen nicht definierten Wert (JavaScript undefined
)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
Werte der o.g. Datentypen werden wie folgt im Programmtext definiert
nil
true
oder false
0b
(also z.B. 0b01101100
) mit bis zu 32 Ziffern/Bits0x
(also z.B. 0x0123AFFE
) mit bis zu 8 Ziffern (32 Bits)\b
, \f
, \n
, \r
, \t
, \v
, \0
, \'
, \"
, \\
sowie \x##
und \u####
){
...}
(...) => {...}
native (...) =>
string"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 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 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 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 ParameternDie 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
return
-Anweisung"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 ParameternDer 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 bestehen aus
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 ä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.
Zur Steuerung des Programmflusses stehen folgende Anweisungen zur Verfügung:
if (...) then {...}
if (...) then {...} else {...}
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 wurdeselect {
when (...) then {...}
otherwise {...}
}
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 wurdefor identifier from ... to ... by ... repeat {...}
for identifier from ... down to ... by ... repeat {...}
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 Schleifenblockeswhile (...) repeat {...}
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 Schleifenblockesrepeat {...} until (...)
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 Schleifenblockesbreak
continue
return expr
nil
zurückgegebenslang unterstützt einzeilige und mehrzeilige (nicht schachtelbare) Kommentare:
// ... bis Zeilenende
/* ...ein- oder mehrzeilig, nicht schachtelbar */
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.
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 pie
- enthält den Wert für die Euler-KonstanteDie 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 anminus(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 Ergebnissesmod(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
giltle(a,b)
- liefert true
dann und nur dann, wenn a <= b
gilteq(a,b)
- liefert true
dann und nur dann, wenn a = b
giltge(a,b)
- liefert true
dann und nur dann, wenn a >= b
giltgt(a,b)
- liefert true
dann und nur dann, wenn a > b
giltne(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
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 istAuch 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)
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 wurdeselect_when (otherwise_clause, condition,block,...)
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 Blockesfor_repeat (identifier, downwards, start_value, stop_value, step_value, loop_body)
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)
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)
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)
value
zurückcontinue
return (value)
value
zurückassert (value)
value
den Wert true
hat - falls ja, wird die Programmausführung fortgesetzt, anderenfalls wird sie mit einer Fehlermeldung abgebrochenassert_failure (block)
block
ein Fehler auftritt - falls ja, wird der Fehler ignoriert und die Programmausführung fortgesetzt, anderenfalls wird sie mit einer Fehlermeldung abgebrochenlog (value)
value
auf die Browser-Konsole ausDie 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
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 }
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 }
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')
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.