Kleine Lisp, kleine Dolmetscherin

33

Lisp- Programmierer rühmen sich, dass Lisp eine mächtige Sprache ist, die aus einer sehr kleinen Menge primitiver Operationen aufgebaut werden kann . Lassen Sie uns diese Idee verwirklichen, indem wir einen Dolmetscher für einen Dialekt namens Golf spielen tinylisp.

Sprachspezifikation

In dieser Spezifikation kann jede Bedingung, deren Ergebnis als "undefiniert" beschrieben wird, irgendetwas in Ihrem Interpreter bewirken: abstürzen, unbemerkt fehlschlagen, zufälliges Gobbldegook erzeugen oder wie erwartet arbeiten. Eine Referenzimplementierung in Python 3 finden Sie hier .

Syntax

Token in tinylisp sind (, )oder eine beliebige Zeichenfolge aus einem oder mehreren druckbaren ASCII-Zeichen mit Ausnahme von Klammern oder Leerzeichen. (Dh der folgende reguläre Ausdruck:. [()]|[^() ]+) Jedes Token, das ausschließlich aus Ziffern besteht, ist ein Ganzzahlliteral. (Führende Nullen sind in Ordnung.) Alle Token , das nicht-Ziffern enthält ein Symbol, auch numerische aussehenden Beispiele mögen 123abc, 3.14und -10. Alle Leerzeichen (einschließlich mindestens der ASCII-Zeichen 32 und 10) werden ignoriert, mit Ausnahme der Tatsache, dass sie Token trennen.

Ein tinylisp-Programm besteht aus einer Reihe von Ausdrücken. Jeder Ausdruck ist entweder eine Ganzzahl, ein Symbol oder ein S-Ausdruck (Liste). Listen bestehen aus null oder mehr Ausdrücken in Klammern. Zwischen den Elementen wird kein Trennzeichen verwendet. Hier sind Beispiele für Ausdrücke:

4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))

Ungeformte Ausdrücke (insbesondere Ausdrücke mit nicht übereinstimmenden Klammern) führen zu undefiniertem Verhalten. (Die Referenzimplementierung schließt offene Parens automatisch und beendet das Parsen für nicht übereinstimmende nahe Parens.)

Datentypen

Die Datentypen von tinylisp sind Ganzzahlen, Symbole und Listen. Eingebaute Funktionen und Makros können auch als Typ betrachtet werden, obwohl ihr Ausgabeformat undefiniert ist. Eine Liste kann beliebig viele Werte eines beliebigen Typs enthalten und beliebig tief verschachtelt sein. Ganzzahlen müssen mindestens von -2 ^ 31 bis 2 ^ 31-1 unterstützt werden.

Die leere Liste ()- auch als Null bezeichnet - und die Ganzzahl 0sind die einzigen Werte, die als logisch falsch betrachtet werden. Alle anderen Ganzzahlen, nicht leeren Listen, eingebauten Werte und alle Symbole sind logisch wahr.

Auswertung

Ausdrücke in einem Programm werden in der angegebenen Reihenfolge ausgewertet und die Ergebnisse an stdout gesendet (mehr zur Ausgabeformatierung später).

  • Ein ganzzahliges Literal wird zu sich selbst ausgewertet.
  • Die leere Liste ()wertet sich selbst aus.
  • Eine Liste mit einem oder mehreren Elementen wertet das erste Element aus, behandelt es als Funktion oder Makro und ruft es mit den verbleibenden Elementen als Argumente auf. Wenn das Element keine Funktion / kein Makro ist, ist das Verhalten undefiniert.
  • Ein Symbol wird als Name ausgewertet und gibt den Wert an, der in der aktuellen Funktion an diesen Namen gebunden ist. Wenn der Name in der aktuellen Funktion nicht definiert ist, wird er im globalen Bereich als an ihn gebundener Wert ausgewertet. Wenn der Name im aktuellen oder globalen Bereich nicht definiert ist, ist das Ergebnis undefiniert (die Referenzimplementierung gibt eine Fehlermeldung aus und gibt null zurück).

Eingebaute Funktionen und Makros

Es gibt sieben integrierte Funktionen in tinylisp. Eine Funktion wertet jedes ihrer Argumente aus, bevor sie eine Operation auf sie anwendet und das Ergebnis zurückgibt.

  • c- Konsumentenliste. Nimmt zwei Argumente, einen Wert und eine Liste, und gibt eine neue Liste zurück, die durch Hinzufügen des Werts am Anfang der Liste erhalten wird.
  • h- Kopf ( Auto , in Lisp-Terminologie). Nimmt eine Liste und gibt das erste Element in der Liste zurück, oder nil, wenn nil angegeben wird.
  • t- tail ( cdr , in der Lisp-Terminologie). Nimmt eine Liste und gibt eine neue Liste zurück, die alle Elemente außer dem ersten enthält, oder nil, wenn nil angegeben wird.
  • s- subtrahieren. Nimmt zwei ganze Zahlen und gibt die erste minus die zweite zurück.
  • l- weniger als. Nimmt zwei ganze Zahlen; Gibt 1 zurück, wenn der erste kleiner als der zweite ist, andernfalls 0.
  • e- gleich. Nimmt zwei Werte desselben Typs (beide Ganzzahlen, beide Listen oder beide Symbole); Gibt 1 zurück, wenn die beiden gleich sind (oder in jedem Element identisch sind), andernfalls 0. Das Testen von Builtins auf Gleichheit ist nicht definiert (Referenzimplementierung funktioniert wie erwartet).
  • v- eval. Nimmt eine Liste, eine Ganzzahl oder ein Symbol, die einen Ausdruck darstellen, und wertet ihn aus. Zum Beispiel (v (q (c a b)))ist das Tun dasselbe wie das Tun (c a b); (v 1)gibt 1.

"Wert" umfasst hier eine Liste, eine Ganzzahl, ein Symbol oder eine integrierte Funktion, sofern nicht anders angegeben. Wenn eine Funktion als typenspezifisch aufgeführt ist, ist die Übergabe unterschiedlicher Typen undefiniertes Verhalten, ebenso wie die Übergabe der falschen Anzahl von Argumenten (die Referenzimplementierung stürzt im Allgemeinen ab).

Es gibt drei eingebaute Makros in tinylisp. Ein Makro wertet im Gegensatz zu einer Funktion seine Argumente nicht aus, bevor Operationen auf sie angewendet werden.

  • q- Zitat. Nimmt einen Ausdruck und gibt ihn ohne Bewertung zurück. Das Auswerten (1 2 3)gibt beispielsweise einen Fehler aus, weil versucht wird, 1eine Funktion oder ein Makro aufzurufen , (q (1 2 3))die Liste jedoch zurückgegeben wird (1 2 3). Das Auswerten agibt den Wert an, der an den Namen gebunden ist a, aber (q a)gibt den Namen selbst an.
  • i- ob. Es werden drei Ausdrücke verwendet: eine Bedingung, ein iftrue-Ausdruck und ein iffalse-Ausdruck. Wertet zuerst die Bedingung aus. Wenn das Ergebnis falsch ( 0oder null) ist, wird der iffalse-Ausdruck ausgewertet und zurückgegeben. Andernfalls wird der iftrue-Ausdruck ausgewertet und zurückgegeben. Beachten Sie, dass der Ausdruck, der nicht zurückgegeben wird, niemals ausgewertet wird.
  • d- def. Nimmt ein Symbol und einen Ausdruck. Wertet den Ausdruck aus und bindet ihn an das angegebene Symbol, das im globalen Bereich als Name behandelt wird. Anschließend wird das Symbol zurückgegeben. Der Versuch, einen Namen neu zu definieren, sollte fehlschlagen (unbeaufsichtigt, mit einer Meldung oder durch Absturz; die Referenzimplementierung zeigt eine Fehlermeldung an). Hinweis: Es ist nicht erforderlich, den Namen vor der Übergabe in Anführungszeichen zu setzen d. Es ist jedoch erforderlich, den Ausdruck in Anführungszeichen zu setzen, wenn es sich um eine Liste oder ein Symbol handelt, das nicht ausgewertet werden soll, z (d x (q (1 2 3))).

Die Übergabe der falschen Anzahl von Argumenten an ein Makro ist ein undefiniertes Verhalten (Absturz der Referenzimplementierung). Übergeben von etwas, das kein Symbol ist, als erstes Argument für dundefiniertes Verhalten (die Referenzimplementierung gibt keinen Fehler aus, aber der Wert kann später nicht referenziert werden).

Benutzerdefinierte Funktionen und Makros

Ausgehend von diesen zehn integrierten Funktionen kann die Sprache durch die Erstellung neuer Funktionen und Makros erweitert werden. Diese haben keinen dedizierten Datentyp. Es handelt sich lediglich um Listen mit einer bestimmten Struktur:

  • Eine Funktion ist eine Liste von zwei Elementen. Der erste ist entweder eine Liste mit einem oder mehreren Parameternamen oder ein einzelner Name, der eine Liste aller an die Funktion übergebenen Argumente erhält (wodurch Funktionen mit variabler Arität möglich sind). Der zweite ist ein Ausdruck, der der Funktionskörper ist.
  • Ein Makro ist dasselbe wie eine Funktion, enthält jedoch nil vor dem oder den Parameternamen und ist daher eine Liste mit drei Elementen. (Der Versuch, Listen mit drei Elementen aufzurufen, die nicht mit nil beginnen, ist undefiniert. Die Referenzimplementierung ignoriert das erste Argument und behandelt sie auch als Makros.)

Der folgende Ausdruck ist beispielsweise eine Funktion, die zwei Ganzzahlen hinzufügt:

(q               List must be quoted to prevent evaluation
 (
  (x y)          Parameter names
  (s x (s 0 y))  Expression (in infix, x - (0 - y))
 )   
)

Und ein Makro, das eine beliebige Anzahl von Argumenten akzeptiert und das erste auswertet und zurückgibt:

(q
 (
  ()
  args
  (v (h args))
 )
)

Funktionen und Makros können direkt aufgerufen, über Namen gebunden dund an andere Funktionen oder Makros übergeben werden.

Da Funktionskörper zur Definitionszeit nicht ausgeführt werden, können rekursive Funktionen einfach definiert werden:

(d len
 (q (
  (list)
  (i list                      If list is nonempty
   (s 1 (s 0 (len (t list))))  1 - (0 - len(tail(list)))
   0                           else 0
  )
 ))
)

Beachten Sie jedoch, dass die obige Methode nicht geeignet ist, um eine Längenfunktion zu definieren, da sie nicht verwendet wird ...

Endanruf-Rekursion

Die Rückrufrekursion ist ein wichtiges Konzept in Lisp. Es implementiert bestimmte Arten von Rekursionen als Schleifen, wodurch der Aufrufstapel klein bleibt. Ihr tinylisp-Interpreter muss die korrekte Tail-Call-Rekursion implementieren!

  • Wenn der Rückgabeausdruck einer benutzerdefinierten Funktion oder eines benutzerdefinierten Makros ein Aufruf einer anderen benutzerdefinierten Funktion oder eines anderen benutzerdefinierten Makros ist, darf Ihr Interpreter zur Auswertung dieses Aufrufs keine Rekursion verwenden. Stattdessen müssen die aktuelle Funktion und die aktuellen Argumente durch die neue Funktion und die neuen Argumente und die neue Schleife ersetzt werden, bis die Aufrufkette aufgelöst ist.
  • Wenn der Rückgabeausdruck einer benutzerdefinierten Funktion oder eines benutzerdefinierten Makros ein Aufruf von ist i, werten Sie den ausgewählten Zweig nicht sofort aus. Überprüfen Sie stattdessen, ob es sich um einen Aufruf einer anderen benutzerdefinierten Funktion oder eines anderen benutzerdefinierten Makros handelt. Wenn ja, tauschen Sie die Funktion und die Argumente wie oben aus. Dies gilt für beliebig tief verschachtelte Vorkommen von i.

Die Schwanzrekursion muss sowohl für die direkte Rekursion (eine Funktion ruft sich selbst auf) als auch für die indirekte Rekursion (Funktion aruft Funktion auf, bdie [etc] aufruft, die Funktion aufruft) funktionieren a.

Eine rekursive Längenfunktion (mit einer Hilfsfunktion len*):

(d len*
 (q (
  (list accum)
  (i list
   (len*
    (t list)
    (s 1 (s 0 accum))
   )
   accum
  )
 ))
)
(d len
 (q (
  (list)
  (len* list 0)
 ))
)

Diese Implementierung funktioniert für beliebig große Listen, die nur durch die maximale Ganzzahlgröße begrenzt sind.

Umfang

Funktionsparameter sind lokale Variablen (eigentlich Konstanten, da sie nicht geändert werden können). Sie befinden sich im Gültigkeitsbereich, während der Hauptteil des Aufrufs dieser Funktion ausgeführt wird, und außerhalb des Gültigkeitsbereichs, wenn tiefere Aufrufe ausgeführt werden und nachdem die Funktion zurückgegeben wurde. Sie können global definierte Namen "schattieren", wodurch der globale Name vorübergehend nicht verfügbar wird. Der folgende Code gibt beispielsweise 5 und nicht 41 zurück:

(d x 42)
(d f
 (q (
  (x)
  (s x 1)
 ))
)
(f 6)

Der folgende Code gibt jedoch 41 zurück, da xauf Aufrufebene 1 von Aufrufebene 2 aus nicht zugegriffen werden kann:

(d x 42)
(d f
 (q (
  (x)
  (g 15)
 ))
)
(d g
 (q (
  (y)
  (s x 1)
 ))
)
(f 6)

Die einzigen Namen, die zu einem bestimmten Zeitpunkt im Geltungsbereich sind, sind 1) die lokalen Namen der aktuell ausgeführten Funktion (falls vorhanden) und 2) globale Namen.

Anforderungen für die Vorlage

Ein- und Ausgabe

Ihr Interpreter liest das Programm möglicherweise aus stdin oder aus einer Datei, die über stdin oder ein Befehlszeilenargument angegeben wurde. Nachdem jeder Ausdruck ausgewertet wurde, sollte er das Ergebnis dieses Ausdrucks mit einer nachgestellten Newline an stdout ausgeben.

  • Ganzzahlen sollten in der natürlichsten Darstellung Ihrer Implementierungssprache ausgegeben werden. Es können negative ganze Zahlen mit führenden Minuszeichen ausgegeben werden.
  • Symbole sollten als Zeichenfolgen ohne umgebende Anführungszeichen oder Escapezeichen ausgegeben werden .
  • Listen sollten mit allen durch Leerzeichen getrennten und in Klammern gesetzten Elementen ausgegeben werden. Ein Leerzeichen in den Klammern ist optional (1 2 3)und kann ( 1 2 3 )in beiden Formaten verwendet werden.
  • Die Ausgabe von integrierten Funktionen und Makros ist undefiniertes Verhalten. (Die Referenzinterpretation zeigt sie als <built-in function>.)

Andere

Der Referenzinterpreter enthält eine REPL-Umgebung und die Möglichkeit, tinylisp-Module aus anderen Dateien zu laden. Diese werden zur Vereinfachung bereitgestellt und sind für diese Herausforderung nicht erforderlich.

Testfälle

Die Testfälle sind in mehrere Gruppen unterteilt, so dass Sie einfachere testen können, bevor Sie sich mit komplexeren befassen. Sie funktionieren jedoch auch einwandfrei, wenn Sie sie alle in einer Datei zusammen ablegen. Vergessen Sie jedoch nicht, die Überschriften und die erwartete Ausgabe zu entfernen, bevor Sie sie ausführen.

Wenn Sie die Tail-Call-Rekursion ordnungsgemäß implementiert haben, wird der abschließende (mehrteilige) Testfall zurückgegeben, ohne dass ein Stapelüberlauf verursacht wird. Die Referenzimplementierung berechnet es in ungefähr sechs Sekunden auf meinem Laptop.

DLosc
quelle
"Jedes Token, das ausschließlich aus Ziffern besteht, ist ein Integer-Literal. (Führende Nullen sind in Ordnung.) Jedes Token, das keine Ziffern enthält, ist ein Symbol, selbst Beispiele mit numerischem Aussehen wie 123abc, 3.14 und -10." scheint zu widersprechen "Ganzzahlen müssen mindestens von -2 ^ 31 bis 2 ^ 31-1 unterstützt werden."
msh210
3
@ msh210 Nicht wirklich, da ersteres über Token spricht, während letzteres über Werte spricht . Auch wenn es keine direkte Eingabemöglichkeit gibt -1, kann ich dennoch den Wert -1 generieren (s 0 1).
DLosc
1
@coredump Nachdem ich den entsprechenden Wikipedia-Artikel gelesen habe, bin ich zu dem Schluss gekommen, dass die Implementierung tatsächlich dynamischer ist, jedoch ohne Verschachtelung des Bereichs. Variablen in function Fsind in function nicht verfügbar, Gwenn sie Faufgerufen werden G(wie bei dynamischem Scoping), aber sie sind auch nicht verfügbar, Hwenn Heine verschachtelte Funktion in function definiert ist F(wie bei lexikalischem Scoping) - siehe Testfall 5. So nennen Sie es "lexikalisch" "könnte irreführend sein.
DLosc
1
Anders ausgedrückt: Aufgrund der fehlenden Verschachtelung des Gültigkeitsbereichs kann eine Implementierung entweder eine dynamische oder eine lexikalische Scoping-Strategie verwenden und zu denselben Ergebnissen führen. Die einzigen Namen, die zu einem bestimmten Zeitpunkt im Geltungsbereich sind, sind 1) die lokalen Namen der aktuell ausgeführten Funktion (falls vorhanden) und 2) globale Namen. Closures werden nicht unterstützt. (Die Referenzimplementierung behält einen Stapel von Namensbindungen bei, der dem Aufrufstapel entspricht - ein Ansatz im dynamischen Stil, der meiner Meinung nach am einfachsten zu implementieren ist.)
DLosc,
1
Obligatorisches xkcd .
Mittwoch,

Antworten:

11

Python 2, 685 675 660 657 646 642 640 Bytes

import sys,re
E=[]
G=zip("chtsle",[eval("lambda x,y=0:"+f)for f
in"[x]+y (x+[E])[0] x[1:] x-y +(x<y) +(x==y)".split()])
def V(e,L=E):
 while 1:
    try:return e and int("0%s"%e)
    except:A=e[1:]
    if""<e:return dict(G+L).get(e,e)
    f=V(e[0],L)
    if""<f:
     if f in"iv":t=V(A[0],L);e=(e[~bool(t)],t)[f>"u"];continue
     if"e">f:G[:]+=(A[0],V(A[1],L)),
     return A[0]
    if[]>f or f[0]:A=[V(a,L)for a in A]
    if[]>f:return f(*A)
    P,e=f[-2:];L=([(P,A)],zip(P,A))[P<""]
F=lambda x:V<x<""and"(%s)"%" ".join(map(F,x))or"%s"%x
for t in re.sub("([()])"," \\1 ",sys.stdin.read()).split():
 if")"==t:t=E.pop()
 if"("==t:E+=[],
 elif E:E[-1]+=t,
 else:print F(V(t))

Liest die Eingabe von STDIN und schreibt die Ausgabe nach STDOUT.

Obwohl dies nicht unbedingt erforderlich ist, unterstützt der Interpreter Nullfunktionen und Makros und optimiert die durchgeführten Tail Calls v.

Erläuterung

Parsing

Um die Eingabe zu analysieren, umgeben wir zuerst jedes Vorkommen von (und )mit Leerzeichen und teilen die resultierende Zeichenfolge in Wörter auf. Dies gibt uns die Liste der Token. Wir pflegen einen Ausdrucksstapel E, der anfangs leer ist. Wir scannen die Token in der Reihenfolge:

  • Wenn wir auf ein stoßen (, schieben wir eine leere Liste oben in den Ausdrucksstapel.
  • Wenn wir auf ein stoßen ), platzieren wir den Wert oben im Ausdrucksstapel und hängen ihn an die Liste an, die zuvor darunter im Stapel war.
  • Andernfalls hängen wir das aktuelle Token als Zeichenfolge an die Liste oben im Ausdrucksstapel an. (In diesem Stadium behalten wir Ganzzahlen als Zeichenfolgen bei und analysieren sie während der Auswertung.)

Wenn )der Ausdrucksstapel beim Verarbeiten eines normalen Tokens oder nach dem Entfernen eines Ausdrucks aus dem Stapel aufgrund von leer ist, befindet sich der Ausdruck auf der obersten Ebene, und wir bewerten den Wert, den wir sonst angehängt hätten, mithilfe von V()und Drucken Sie das Ergebnis entsprechend formatiert aus F().

Auswertung

Wir pflegen den globalen Geltungsbereich Gals Liste von Schlüssel / Wert-Paaren. Anfangs enthält es nur die eingebauten Funktionen (aber nicht die Makros und nicht die v, die wir als Makro behandeln), die als Lambdas implementiert sind.

Die Auswertung erfolgt im Inneren V(), das den auszuwertenden Ausdruck benötigt e, und im lokalen Bereich, Lder auch eine Liste von Schlüssel / Wert-Paaren ist (bei der Auswertung eines Ausdrucks der obersten Ebene ist der lokale Bereich leer.) Die Eingeweide von V()Live in einer Endlosschleife, auf diese Weise führen wir die Tail-Call-Optimierung (TCO) durch, wie später erläutert wird.

Wir verarbeiten enach seiner Art:

  • Wenn es sich um eine leere Liste oder eine Zeichenfolge handelt, die in ein int konvertierbar ist, geben wir sie sofort zurück (möglicherweise nach der Konvertierung in int). Andernfalls,

  • Wenn es sich um eine Zeichenfolge handelt, schlagen wir sie in einem Wörterbuch nach, das aus der Verkettung des globalen und des lokalen Bereichs besteht. Wenn wir einen zugehörigen Wert finden, geben wir ihn zurück. andernfalls emuss der Name einer eingebauten Makro (dh sein q, i, doder v), und wir unverändert zurück. Andernfalls, wenn ees sich nicht um eine Zeichenfolge handelt,

  • eist eine (nicht leere) Liste, dh ein Funktionsaufruf. Wir bewerten das erste Element der Liste, dh den Funktionsausdruck, durch V()rekursiven Aufruf (unter Verwendung des aktuellen lokalen Gültigkeitsbereichs). Wir nennen das Ergebnis f. Der Rest der Liste Aist die Liste der Argumente. fkann nur eine Zeichenfolge sein, in diesem Fall ein eingebautes Makro (oder die Funktion v), ein Lambda, in diesem Fall eine eingebaute Funktion oder eine Liste, in diesem Fall eine benutzerdefinierte Funktion oder ein benutzerdefiniertes Makro.

    Wenn fes sich um eine Zeichenfolge handelt, dh um ein eingebautes Makro, wird es direkt verarbeitet. Wenn es sich um das Makro ioder handelt v, werten wir seinen ersten Operanden aus und wählen entweder den zweiten oder dritten Operanden im Fall von entsprechend aus ioder verwenden das Ergebnis des ersten Operanden im Fall von v; Anstatt den ausgewählten Ausdruck rekursiv auszuwerten, was TCO zunichte machen würde, ersetzen wir ihn einfach durch eden genannten Ausdruck und springen an den Anfang der Schleife. Wenn fes sich um das Makro handelt d, fügen wir ein Paar, dessen erstes Element der erste Operand ist und dessen zweites Element das Ergebnis der Auswertung des zweiten Operanden ist, an den globalen Gültigkeitsbereich an Gund geben den ersten Operanden zurück. Andernfalls fist das Makro q. In diesem Fall geben wir den Operanden einfach direkt zurück.

    Wenn falso ein Lambda ist oder eine Liste, deren erstes Element nicht ist (), dann ist es eine nichtnulläre Funktion, kein Makro. In diesem Fall werden die Argumente, dh die Elemente von A, ausgewertet und durch Adas Ergebnis ersetzt.

    Wenn fes ein Lambda ist, nennen wir es, übergeben ihm die entpackten Argumente Aund geben das Ergebnis zurück.

    Andernfalls fhandelt es sich um eine Liste, dh eine benutzerdefinierte Funktion oder ein benutzerdefiniertes Makro. Seine Parameterliste ist das vorletzte Element, und sein Hauptteil ist das letzte Element. Wie bei den Makros iund v, um die TCO durchzuführen, wird der Body nicht rekursiv ausgewertet, sondern durch eden Body ersetzt und mit der nächsten Iteration fortgefahren. Im Gegensatz zu iund versetzen wir jedoch auch den lokalen Gültigkeitsbereich Ldurch den neuen lokalen Gültigkeitsbereich der Funktion. Wenn es sich bei der Parameterliste Ptatsächlich um eine Liste handelt, wird der neue lokale Gültigkeitsbereich erstellt, indem die Parameterliste Pmit der Argumentliste komprimiert wird A. Andernfalls handelt es sich um eine variable Funktion. In diesem Fall enthält der neue lokale Bereich nur ein Element, das Paar (P, A).

REPL

Wenn Sie damit spielen möchten, finden Sie hier eine REPL-Version des Interpreters. Es unterstützt das Neudefinieren von Symbolen und das Importieren von Dateien entweder über die Befehlszeilenargumente oder über das (import <filename>)Makro. Um den Interpreter zu beenden, beenden Sie die Eingabe (normalerweise Strg + D oder Strg + Z).

Und hier ist eine Beispielsitzung, in der die Zusammenführungssortierung implementiert wird:

Ell
quelle
Mit zlib können Sie etwas immer Kürzeres erreichen :) Komprimieren Sie Ihren in Bytes konvertierten Code und ersetzen Sie ihn durch:import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
Labo
Sie können zwei Bytes sparen, indem Sie A[0]eine Variable mit einem
Zeichen
@HannesKarppila Das stimmt, aber das würde nulläre Funktionen zerstören (da Ain diesem Fall leer ist), und ich möchte nicht "regressieren".
Ell
4

C (GNU), 1095 Bytes

Ein Großteil der Aktion findet in der Riesenfunktion statt v. Anstatt die Schwanzrekursion explizit zu implementieren, vist sie so strukturiert, dass viele der Aufrufe von vbis vvon der Optimierung der Schwanzrekursion von gcc verarbeitet werden. Es gibt keine Müllabfuhr.

Dies nutzt die GCC-Erweiterungen stark aus, sodass sie nur mit gcc kompiliert werden können (verwenden Sie den Befehl gcc -w -Os tl.c). Es werden auch einige scanfErweiterungen verwendet, die unter Windows, das ich normalerweise verwende, nicht verfügbar waren. Die Aussicht, den Parser mit Standard zu schreiben, scanfwar so schrecklich, dass ich stattdessen eine Linux-VM verwendete, um das Programm zu testen. Das Parsen ohne scanfZeichenklassen hätte wahrscheinlich mehr als 100 Bytes hinzugefügt.

#define O(...)({o*_=malloc(32);*_=(o){__VA_ARGS__};_;})
#define P printf
#define F(I,B)({for(I;x->c;x=x->l)B;})
#define Z return
typedef struct o{struct o*n,*l,*c;int i,t;}o;E(o a,o b){Z
a.n?!strcmp(a.n,b.n):a.c?b.c&&E(*a.c,*b.c)&E(*a.l,*b.l):!b.c&a.i==b.i;}p(o*x){x->t?P("%d ",x->i):x->n?P("%s ",x->n):F(P("("),p(x->c);P(")"));}o*x,G,N;*C(o*h,o*t){Z
O(c:h,l:t);}o*v(o*x,o*e){o*W(o*l,o*e){Z
l->c?C(v(l->c,e),W(l->l,e)):&N;}o*y,*a,*f;int t;Z
x->c?y=v(x->c,e),x=x->l,t=y->i,t?9/t?a=v(x->c,e),t>7?(t>8?a->c:a->l)?:a:t>6?v(a,e):t<6?x=v(x->l->c,e),t>4?C(a,x):O(t:1,i:t>3?E(*a,*x):t>2?a->i<x->i:a->i-x->i):v((a-&N&&!a->t|a->i?x:x->l)->l->c,e):(t&1&&d(x->c->n,v(x->l->c,e)),x->c):(y->l->l->l?y=y->l:(x=W(x,e)),a=y->c,v(y->l->c,a->n?O(n:a->n,c:x,l:&G):F(f=&G,(f=O(n:a->c->n,c:x->c,l:f),a=a->l);f))):x->n?e->n?strcmp(x->n,e->n)?v(x,e->l):e->c:e:x;}d(o*n,o*x){*v(O(n:""),&G)=(o){n:n,c:x,l:O()};}*R(h){char*z,*q;Z
scanf(" %m[^ \n()]",&q)>0?h=strtoul(q,&z,10),C(*z?O(n:q):O(t:1,i:h),R()):~getchar()&1?q=R(),C(q,R()):&N;}main(i){for(;++i<12;)d(strndup("slecivthqd"+i-2,1),O(i:i));F(x=R(),p(v(x->c,&G)));}

Semi-ungolfed

typedef struct o o;
struct o {
    char* n;
    o* l, //next in this list
     * c; 
    int i,
        t;
} ;



#define O(...)({o*_=malloc(32);*_=(o){__VA_ARGS__};_;})

E(o a, o b) { //tests equality 
    return
        a.n ? !strcmp(a.n,b.n) :
        a.t ? a.i==b.i :
        a.c ? b.c && E(*a.c,*b.c)&E(*a.l,*b.l) :
        !b.c
    ;
}

#define P printf


p(o*x){
    x->t?P("%d ",x->i):x->n?P("%s ",x->n):({for(P("(");x->c;x=x->l)p(x->c);P(")");});
}


o*_,G,N; //N = nil



o*C(o*h,o*t){return O(c:h,l:t);}


/*
        2 3 4 5 6 7 8 9 10 11
        s l e c i v t h d  q
    */


o* v(o* x, o* e) { //takes list, int, or name
    o*W(o* l, o* e) { //eval each item in list
        return l->c ? C(v(l->c ,e), W(l->l, e)) : &N;
    }

    o*y,*a,*f;int t;
    return x->c ? //nonempty list = function/macro call
        y = v(x->c,e), //evals to function/macro
        x = x->l,   //list position of first arg (if it exists)
        (t=y->t)?   //builtin no., if any
             t>9 ?
              t&1 ? x->c // 11 = q
                : //10 = d
                (d(x->c,v(x->l->c,e)),x->c)
           : (a = v(x->c,e), //eval'd first arg
             t)>7 ? // t/h
                (t > 8 ? a->c : a->l) ?: a
           : t>6 ? //v
                v(a,e)
           : (x = x->l, //position of 2nd arg in list
             t)>5 ? //i
                v( (a->n||a->l||a->i|a->t>1 ? x : x->l)->c, e)
           : (x = v(x->c,e), //evaluated 2nd arg
             t)>4 ? // c
                C(a,x)
           : O(t:1,i:
                t>3 ? E(*a,*x) :  //e
                t>2 ? a->i<x->i : //l
                      a->i-x->i   //s
              )
        :
        (
            y->l->l->l ? //whether this is macro
                y = y->l :
                (x = W(x,e)),  //eval args
            a = y->c,  //a = arg list
            //a = a->n ? x=C(x, &N), C(a, &N) : a, //transform variadic style to normal
            v(y->l->c,
               a->n ? //variadic
                O(n:a->n,c:x,l:&G)
              : ({
                   for(f=&G; a->c; a=a->l,x=x->l)
                      f=O(n:a->c->n, c: x->c, l:f);
                   f;
                })
            )
        )
    :
    x->n ? // name
        e->n ?
            strcmp(x->n,e->n) ?
                v(x,e->l)
            : e->c
        : e
     : x; //int or nil
}

d(o*n,o*x){
    * v(O(n:""),&G) =
        (o){n:n->n,c:x,l:O()};
}


;
o*R(){
    char*z,*q;int h;
return scanf(" %m[^ \n()]",&q)>0?
    h=strtoul(q,&z,10),
    C(*z ? O(n:q) : O(t:1,i:h), R())
: getchar()&1?&N:(q=R(),C(q,R()));
}
main(i) {

    for(;++i<12;) d(O(n:strndup("slecivthdq"+i-2,1)),O(t:i));

    o *q;
    for(q=R(); q->c; q=q->l) p(v(q->c,&G));

}
Feersum
quelle
Was ist die Verwendung der kompilierten ausführbaren Datei? Ist es REPL? Nimmt es einen Dateinamen als Eingabe?
ckjbgames
@ckjbgames Liest ein Programm von stdin.
Feersum
Okay. Ich denke, Sie sollten Ihre Antwort bearbeiten und das zur Kenntnis nehmen.
ckjbgames
1

Ceylon, 2422 Bytes

(Ich denke, das ist mein längstes Golfprogramm.)

import ceylon.language{sh=shared,va=variable,fo=formal,O=Object}import ceylon.language.meta.model{F=Function}interface X{sh fo V v(S t);sh fo G g;}class G(va Map<S,V>m)satisfies X{v(S t)=>m[t]else nV;g=>this;sh S d(S s,V v){assert(!s in m);m=map{s->v,*m};return s;}}V nV=>nothing;class LC(G c,Map<S,V>m)satisfies X{g=>c;v(S t)=>m[t]else g.v(t);}alias C=>V|Co;interface Co{sh fo C st();}interface V{sh fo C l(X c,V[]a);sh default Boolean b=>0<1;sh fo C vO(X c);sh default V vF(X c){va C v=vO(c);while(is Co n=v){v=n.st();}assert(is V r=v);return r;}}class L(sh V*i)satisfies V{vO(X c)=>if(nonempty i)then i[0].vF(c).l(c,i.rest)else this;equals(O o)=>if(is L o)then i==o.i else 1<0;b=>!i.empty;string=>"(``" ".join(i)``)";hash=>i.hash;sh actual C l(X c,V[]p){value[h,ns,x]=i.size<3then[f,i[0],i[1]]else[m,i[1],i[2]];value n=if(is L ns)then[*ns.i.narrow<S>()]else ns;assert(is S|S[]n,is V x);V[]a=h(c,p);LC lC=if(is S n)then LC(c.g,map{n->L(*a)})else LC(c.g,map(zipEntries(n,a)));return object satisfies Co{st()=>x.vO(lC);};}}class S(String n)satisfies V{vO(X c)=>c.v(this);l(X c,V[]a)=>nV;equals(O o)=>if(is S o)then n==o.n else 1<0;hash=>n.hash;string=>n;}class I(sh Integer i)satisfies V{vO(X c)=>this;l(X c,V[]a)=>nV;equals(O o)=>if(is I o)then i==o.i else 1<0;hash=>i;b=>!i.zero;string=>i.string;}V[]f(X c,V[]a)=>[for(v in a)v.vF(c)];V[]m(X c,V[]a)=>a;L c(X c,V h,L t)=>L(h,*t.i);V h(X c,L l)=>l.i[0]else L();V t(X c,L l)=>L(*l.i.rest);I s(X c,I f,I s)=>I(f.i-s.i);I l(X c,I f,I s)=>I(f.i<s.i then 1else 0);I e(X c,V v1,V v2)=>I(v1==v2then 1else 0);C v(X c,V v)=>v.vO(c);V q(X c,V a)=>a;C i(X c,V d,V t,V f)=>d.vF(c).b then t.vO(c)else f.vO(c);S d(X c,S s,V x)=>c.g.d(s,x.vF(c));class B<A>(F<C,A>nat,V[](X,V[])h=f)satisfies V given A satisfies[X,V+]{vO(X c)=>nV;string=>nat.declaration.name;l(X c,V[]a)=>nat.apply(c,*h(c,a));}{<S->V>*}b=>{S("c")->B(`c`),S("h")->B(`h`),S("t")->B(`t`),S("s")->B(`s`),S("l")->B(`l`),S("e")->B(`e`),S("v")->B(`v`),S("q")->B(`q`,m),S("i")->B(`i`,m),S("d")->B(`d`,m)};[V*]p(String inp){value ts=inp.split(" \n()".contains,1<0,1<0);va[[V*]*]s=[];va[V*]l=[];for(t in ts){if(t in" \n"){}else if(t=="("){s=[l,*s];l=[];}else if(t==")"){l=[L(*l.reversed),*(s[0]else[])];s=s.rest;}else if(exists i=parseInteger(t),i>=0){l=[I(i),*l];}else{l=[S(t),*l];}}return l.reversed;}sh void run(){va value u="";while(exists l=process.readLine()){u=u+l+"\n";}V[]e=p(u);X c=G(map(b));for(v in e){print(v.vF(c));}}

Ich hätte ein paar Bytes mehr Golf spielen können, da ich an einigen Stellen zwei Buchstaben als Bezeichner verwendet habe, aber für diese habe ich keine aussagekräftigen Einzelbuchstaben mehr. Obwohl es auch so nicht so aussieht wie Ceylon ...

Dies ist eine objektorientierte Implementierung.

Wir haben eine Werteschnittstelle Vmit implementierenden Klassen L(Liste - nur ein Wrapper um eine Ceylon - Sequenz von V), S(Symbol - Wrapper um eine Zeichenkette), I(Ganzzahl - Wrapper um eine Ceylon - Ganzzahl) und B(eingebaute Funktion oder Makro, ein Wrapper um eine Ceylon-Funktion).

Ich verwende die Standard-Ceylon-Gleichheitsnotation, indem ich die equalsMethode (und auch das hashAttribut, das wirklich nur für Symbole benötigt wird) sowie das Standardattribut stringfür die Ausgabe implementiere .

Wir haben ein boolesches Attribut b(das standardmäßig wahr ist, in leeren Listen überschrieben wird Iund Lfalse zurückgibt) und zwei Methoden l(dieses Objekt aufrufen, dh als Funktion verwenden) und vO(einen Schritt auswerten). Beide geben entweder einen Wert oder ein Fortsetzungsobjekt zurück, das dann die Auswertung für einen weiteren Schritt ermöglicht, und führen vF(vollständige Auswertung) Schleifen durch, bis das Ergebnis keine Fortsetzung mehr ist.

Eine Kontextschnittstelle ermöglicht den Zugriff auf Variablen. Es gibt zwei Implementierungen Gfür den globalen Kontext (der das Hinzufügen von Variablen mithilfe des deingebauten Kontexts ermöglicht) und LCfür einen lokalen Kontext, der aktiv ist, wenn der Ausdruck einer Benutzerfunktion ausgewertet wird (er greift auf den globalen Kontext zurück).

Die Symbolauswertung greift auf den Kontext zu, Listen (falls nicht leer) bewerten, indem sie zuerst ihr erstes Element auswerten und dann ihre Aufrufmethode aufrufen. Der Aufruf wird nur von Listen und eingebauten Elementen implementiert - er wertet zuerst das Argument aus (wenn eine Funktion, nicht ein Makro) und erledigt dann die eigentlichen interessanten Dinge - für eingebaute Elemente nur das, was fest codiert ist, für Listen erstellt er einen neuen lokalen Kontext und gibt a zurück Fortsetzung damit.

Für die eingebauten Funktionen habe ich einen ähnlichen Trick verwendet wie in meinem Shift-Interpreter , der es mir ermöglicht, sie mit den benötigten Argumenttypen zu definieren, sie jedoch mit einer generischen Sequenz unter Verwendung von Reflection aufzurufen (die Typen werden zum Zeitpunkt des Aufrufs überprüft). Dies vermeidet Typkonvertierungs- / Assertionsprobleme innerhalb der Funktionen / Makros, benötigt jedoch Funktionen der obersten Ebene, damit ich deren Metamodellobjekte erhalten Functionkann.

Die pFunktion (Parsen) teilt den String in Leerzeichen, Zeilenumbrüche und Klammern auf, durchläuft dann die Token und erstellt Listen mithilfe eines Stapels und einer laufenden Liste.

Der Interpreter (in der runMethode, die der Einstiegspunkt ist) nimmt dann diese Liste von Ausdrücken (die nur Werte sind), wertet sie aus und gibt das Ergebnis aus.


Unten finden Sie eine Version mit Kommentaren und einem Formatierer.

Eine frühere Version , bevor ich (mit einigen Missverständnissen über Listenauswertung und immer noch) Golf spielt begonnen wird bei gefunden meiner Github - Repository , werde ich diese setzt es bald (so sicher zu betrachten macht die erste Version , wenn Sie das Original will).

//  Tiny Lisp, tiny interpreter
//
// An interpreter for a tiny subset of Lisp, from which most of the
// rest of the language can be bootstrapped.
//
// Question:   https://codegolf.stackexchange.com/q/62886/2338
// My answer:  https://codegolf.stackexchange.com/a/63352/2338
//
import ceylon.language {
    sh=shared,
    va=variable,
    fo=formal,
    O=Object
}
import ceylon.language.meta.model {
    F=Function
}

// context

interface X {
    sh fo V v(S t);
    sh fo G g;
}
// global (mutable) context, with the buildins 
class G(va Map<S,V> m) satisfies X {
    // get entry throws error on undefined variables. 
    v(S t) => m[t] else nV;
    g => this;
    sh S d(S s, V v) {
        // error when already defined
        assert (!s in m);
        // building a new map is cheaper (code-golf wise) than having a mutable one.
        m = map { s->v, *m };
        return s;
    }
}

// This is simply a shorter way of writing "this is not an allowed operation".
// It will throw an exception when trying to access it.
// nV stands for "no value".
V nV => nothing;

// local context
class LC(G c, Map<S,V> m) satisfies X {
    g => c;
    v(S t) => m[t] else g.v(t);
    // sh actual String string => "[local: ``m``, global: ``g``]";
}

// continuation or value
alias C => V|Co;

// continuation
interface Co {
    sh fo C st();
}

// value
interface V {
    // use this as a function and call with arguments.
    // will not work for all types of stuff.
    sh fo C l(X c, V[] a);
    // check the truthiness. Defaults to true, as
    // only lists and integers can be falsy.
    sh default Boolean b => 0 < 1;
    // evaluate once (return either a value or a continuation).
    // will not work for all kinds of expression.
    sh fo C vO(X c);
    /// evaluate fully
    sh default V vF(X c) {
        va C v = vO(c);
        while (is Co n = v) {
            v = n.st();
        }
        assert (is V r = v);
        return r;
    }
}
class L(sh V* i) satisfies V {

    vO(X c) => if (nonempty i) then i[0].vF(c).l(c, i.rest) else this;
    equals(O o) => if (is L o) then i == o.i else 1 < 0;
    b => !i.empty;
    string => "(``" ".join(i)``)";
    hash => i.hash;

    sh actual C l(X c, V[] p) {
        value [h, ns, x] =
                i.size < 3
                then [f, i[0], i[1]]
                else [m, i[1], i[2]];
        // parameter names – either a single symbol, or a list of symbols.
        // If it is a list, we filter it to throw out any non-symbols.
        // Should throw an error if there are any, but this is harder.
        value n = if (is L ns) then [*ns.i.narrow<S>()] else ns;
        assert (is S|S[] n, is V x);
        V[] a = h(c, p);

        // local context
        LC lC = if (is S n) then
            LC(c.g, map { n -> L(*a) })
        else
            LC(c.g, map(zipEntries(n, a)));
        // return a continuation instead of actually
        // calling it here, to allow stack unwinding.
        return object satisfies Co {
            st() => x.vO(lC);
        };
    }
}

// symbol
class S(String n) satisfies V {
    // evaluate: resolve
    vO(X c) => c.v(this);
    // call is not allowed
    l(X c, V[] a) => nV;
    // equal if name is equal
    equals(O o) => if (is S o) then n == o.n else 1 < 0;
    hash => n.hash;
    string => n;
}

// integer
class I(sh Integer i) satisfies V {

    vO(X c) => this;
    l(X c, V[] a) => nV;
    equals(O o) => if (is I o) then i == o.i else 1 < 0;
    hash => i;
    b => !i.zero;
    string => i.string;
}

// argument handlers for functions or macros
V[] f(X c, V[] a) => [for (v in a) v.vF(c)];
V[] m(X c, V[] a) => a;

// build-in functions
// construct
L c(X c, V h, L t) => L(h, *t.i);
// head
V h(X c, L l) => l.i[0] else L();
// tail
V t(X c, L l) => L(*l.i.rest);
// subtract
I s(X c, I f, I s) => I(f.i - s.i);
// lessThan
I l(X c, I f, I s) => I(f.i < s.i then 1 else 0);
// equal
I e(X c, V v1, V v2) => I(v1 == v2 then 1 else 0);
// eval (returns potentially a continuation)
C v(X c, V v) => v.vO(c);

// build-in macros
// quote
V q(X c, V a) => a;
// if (also returns potentially a continuation)
C i(X c, V d, V t, V f) => d.vF(c).b then t.vO(c) else f.vO(c);
// define symbol in global context
S d(X c, S s, V x) => c.g.d(s, x.vF(c));

// buildin function or macro, made from a native function and an argument handler
class B<A>(F<C,A> nat, V[](X, V[]) h = f)
        satisfies V
        given A satisfies [X, V+] {
    vO(X c) => nV;
    string => nat.declaration.name;
    // this "apply" is a hack which breaks type safety ...
    // but it will be checked at runtime.
    l(X c, V[] a) => nat.apply(c, *h(c, a));
}

// define buildins
{<S->V>*} b => {
    S("c") -> B(`c`),
    S("h") -> B(`h`),
    S("t") -> B(`t`),
    S("s") -> B(`s`),
    S("l") -> B(`l`),
    S("e") -> B(`e`),
    S("v") -> B(`v`),
    S("q") -> B(`q`, m),
    S("i") -> B(`i`, m),
    S("d") -> B(`d`, m)
};

// parses a string into a list of expressions.
[V*] p(String inp) {
    // split string into tokens (retain separators, don't group them –
    // whitespace and empty strings will be sorted out later in the loop)
    value ts = inp.split(" \n()".contains, 1 < 0, 1 < 0);
    // stack of not yet finished nested lists, outer most at bottom
    va [[V*]*] s = [];
    // current list, in reverse order (because appending at the start is shorter)
    va [V*] l = [];
    for (t in ts) {
        if (t in " \n") {
            // do nothing for empty tokens
        } else if (t == "(") {
            // push the current list onto the stack, open a new list.
            s = [l, *s];
            l = [];
        } else if (t == ")") {
            // build a lisp list from the current list,
            // pop the latest list from the stack, append the created lisp list. 
            l = [L(*l.reversed), *(s[0] else [])];
            s = s.rest;
        } else if (exists i = parseInteger(t), i >= 0) {
            // append an integer to the current list.
            l = [I(i), *l];
        } else {
            // append a symbol to the current list.
            l = [S(t), *l];
        }
    }
    return l.reversed;
}

// Runs the interpreter.
// This handles input and output, calls the parser and evaluates the expressions.
sh void run() {
    va value u = "";
    while (exists l = process.readLine()) {
        u = u + l + "\n";
    }
    V[] e = p(u);
    // create global context
    X c = G(map(b));
    // iterate over the expressions, ...
    for (v in e) {
        // print("  '``v``' → ...");
        // ... evaluate each (fully) and print the result.
        print(v.vF(c));
    }
}
Paŭlo Ebermann
quelle