Tiny Lisp, pequeño intérprete

33

Los programadores de Lisp se jactan de que Lisp es un lenguaje poderoso que se puede construir a partir de un conjunto muy pequeño de operaciones primitivas . Pongamos en práctica esa idea jugando al golf a un intérprete para un dialecto llamado tinylisp.

Especificación de idioma

En esta especificación, cualquier condición cuyo resultado se describa como "indefinido" puede hacer algo en su intérprete: bloquearse, fallar silenciosamente, producir gobbldegook aleatorio o funcionar como se espera. Una implementación de referencia en Python 3 está disponible aquí .

Sintaxis

Los tokens en tinylisp son (, )o cualquier cadena de uno o más caracteres ASCII imprimibles, excepto paréntesis o espacio. (Es decir, la siguiente expresión regular:. [()]|[^() ]+) Cualquier token que consista completamente en dígitos es un literal entero. (Los ceros iniciales están bien.) Cualquier señal de que no contiene dígitos es un símbolo, incluso ejemplos de aspecto numérico como 123abc, 3.14, y -10. Todo el espacio en blanco (incluidos, como mínimo, los caracteres ASCII 32 y 10) se ignora, excepto en la medida en que separa los tokens.

Un programa tinylisp consiste en una serie de expresiones. Cada expresión es un número entero, un símbolo o una expresión s (lista). Las listas constan de cero o más expresiones entre paréntesis. No se utiliza separador entre los elementos. Aquí hay ejemplos de expresiones:

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

Las expresiones que no están bien formadas (en particular, que tienen paréntesis sin igual) dan un comportamiento indefinido. (La implementación de referencia cierra automáticamente los parens abiertos y deja de analizar los parens cercanos inigualables).

Tipos de datos

Los tipos de datos de tinylisp son enteros, símbolos y listas. Las funciones y macros incorporadas también pueden considerarse un tipo, aunque su formato de salida no está definido. Una lista puede contener cualquier número de valores de cualquier tipo y puede anidarse arbitrariamente en profundidad. Los enteros deben ser compatibles al menos entre -2 ^ 31 y 2 ^ 31-1.

La lista vacía, ()también conocida como nula, y el entero0 son los únicos valores que se consideran lógicamente falsos; todos los demás enteros, listas no vacías, incorporados y todos los símbolos son lógicamente verdaderos.

Evaluación

Las expresiones en un programa se evalúan en orden y los resultados de cada uno se envían a stdout (más sobre el formato de salida más adelante).

  • Un literal entero se evalúa a sí mismo.
  • La lista vacía se ()evalúa a sí misma.
  • Una lista de uno o más elementos evalúa su primer elemento y lo trata como una función o macro, llamándolo con los elementos restantes como argumentos. Si el elemento no es una función / macro, el comportamiento es indefinido.
  • Un símbolo se evalúa como un nombre, dando el valor vinculado a ese nombre en la función actual. Si el nombre no está definido en la función actual, se evalúa según el valor vinculado a él en el ámbito global. Si el nombre no está definido en el ámbito actual o global, el resultado no está definido (la implementación de referencia muestra un mensaje de error y devuelve cero).

Funciones integradas y macros

Hay siete funciones integradas en tinylisp. Una función evalúa cada uno de sus argumentos antes de aplicarles alguna operación y devolver el resultado.

  • c- contras [lista de resultados]. Toma dos argumentos, un valor y una lista, y devuelve una nueva lista obtenida al agregar el valor al principio de la lista.
  • h- cabeza ( automóvil , en terminología Lisp). Toma una lista y devuelve el primer elemento en ella, o nulo si se le da nulo.
  • t- cola ( cdr , en terminología Lisp). Toma una lista y devuelve una nueva lista que contiene todos menos el primer elemento, o nula si se proporciona nula.
  • s- restar. Toma dos enteros y devuelve el primero menos el segundo.
  • l- menos que. Toma dos enteros; devuelve 1 si el primero es menor que el segundo, 0 de lo contrario.
  • e- igual. Toma dos valores del mismo tipo (ambos enteros, ambas listas o ambos símbolos); devuelve 1 si los dos son iguales (o idénticos en cada elemento), 0 en caso contrario. Las pruebas de igualdad de las construcciones no están definidas (la implementación de referencia funciona como se esperaba).
  • v- eval. Toma una lista, entero o símbolo, que representa una expresión, y la evalúa. Por ejemplo, hacer (v (q (c a b)))es lo mismo que hacer (c a b); (v 1)da 1.

"Valor" aquí incluye cualquier lista, número entero, símbolo o incorporado, a menos que se especifique lo contrario. Si una función aparece como que toma tipos específicos, pasarle diferentes tipos es un comportamiento indefinido, al igual que pasar una cantidad incorrecta de argumentos (la implementación de referencia generalmente falla).

Hay tres macros integradas en tinylisp. Una macro, a diferencia de una función, no evalúa sus argumentos antes de aplicarles operaciones.

  • q- cita Toma una expresión y la devuelve sin evaluar. Por ejemplo, evaluar (1 2 3)da un error porque intenta llamar 1como una función o macro, pero (q (1 2 3))devuelve la lista (1 2 3). Evaluar ada el valor vinculado al nombre a, pero (q a)da el nombre mismo.
  • i- Si. Toma tres expresiones: una condición, una expresión iftrue y una expresión iffalse. Evalúa la condición primero. Si el resultado es falso ( 0o nulo), evalúa y devuelve la expresión iffalse. De lo contrario, evalúa y devuelve la expresión iftrue. Tenga en cuenta que la expresión que no se devuelve nunca se evalúa.
  • d- def. Toma un símbolo y una expresión. Evalúa la expresión y la vincula al símbolo dado tratado como un nombre en el ámbito global , luego devuelve el símbolo. Intentar redefinir un nombre debería fallar (en silencio, con un mensaje o bloqueándose; la implementación de referencia muestra un mensaje de error). Nota: no es necesario citar el nombre antes de pasarla a d, aunque es necesario citar la expresión si se trata de una lista o símbolo que no quiere evaluados: por ejemplo, (d x (q (1 2 3))).

Pasar el número incorrecto de argumentos a una macro es un comportamiento indefinido (la implementación de referencia se bloquea). Pasar algo que no es un símbolo como primer argumento des un comportamiento indefinido (la implementación de referencia no da un error, pero el valor no puede ser referenciado posteriormente).

Funciones definidas por el usuario y macros

A partir de estos diez elementos integrados, el lenguaje puede ampliarse mediante la construcción de nuevas funciones y macros. Estos no tienen ningún tipo de datos dedicado; son simplemente listas con cierta estructura:

  • Una función es una lista de dos elementos. El primero es una lista de uno o más nombres de parámetros, o un solo nombre que recibirá una lista de cualquier argumento pasado a la función (permitiendo así funciones de aridad variable). La segunda es una expresión que es el cuerpo de la función.
  • Una macro es lo mismo que una función, excepto que contiene cero antes de los nombres de los parámetros, lo que la convierte en una lista de tres elementos. (Intentar llamar a listas de tres elementos que no comienzan con nil es un comportamiento indefinido; la implementación de referencia ignora el primer argumento y también los trata como macros).

Por ejemplo, la siguiente expresión es una función que agrega dos enteros:

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

Y una macro que toma cualquier número de argumentos y evalúa y devuelve el primero:

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

Las funciones y las macros se pueden invocar directamente, enlazar a nombres usando dy pasar a otras funciones o macros.

Como los cuerpos de las funciones no se ejecutan en el momento de la definición, las funciones recursivas son fácilmente definibles:

(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
  )
 ))
)

Sin embargo, tenga en cuenta que lo anterior no es una buena manera de definir una función de longitud porque no usa ...

Recurrencia de llamada de cola

La recursividad de llamadas de cola es un concepto importante en Lisp. Implementa ciertos tipos de recursión como bucles, manteniendo así la pila de llamadas pequeña. ¡Su intérprete tinylisp debe implementar una recursión de llamada de cola adecuada!

  • Si la expresión de retorno de una función o macro definida por el usuario es una llamada a otra función o macro definida por el usuario, su intérprete no debe usar la recursividad para evaluar esa llamada. En su lugar, debe reemplazar la función y los argumentos actuales con la nueva función y los argumentos y el bucle hasta que se resuelva la cadena de llamadas.
  • Si la expresión de retorno de una función o macro definida por el usuario es una llamada a i, no evalúe de inmediato la rama seleccionada. En su lugar, verifique si se trata de una llamada a otra función o macro definida por el usuario. Si es así, cambie la función y los argumentos como se indicó anteriormente. Esto se aplica a ocurrencias arbitrariamente anidadas de i.

La recursión de cola debe funcionar tanto para la recursión directa (una función se llama a sí misma) como para la recursión indirecta (la función allama a la función bque llama a [etc.] a la que llama a la funcióna ).

Una función de longitud recursiva de cola (con una función auxiliar len*):

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

Esta implementación funciona para listas arbitrariamente grandes, limitadas solo por el tamaño entero máximo.

Alcance

Los parámetros de función son variables locales (en realidad constantes, ya que no pueden modificarse). Están dentro del alcance mientras se ejecuta el cuerpo de esa llamada de esa función, y fuera de alcance durante cualquier llamada más profunda y después de que la función regrese. Pueden "sombrear" nombres definidos globalmente, haciendo que el nombre global no esté disponible temporalmente. Por ejemplo, el siguiente código devuelve 5, no 41:

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

Sin embargo, el siguiente código devuelve 41, porque xen el nivel de llamada 1 no se puede acceder desde el nivel de llamada 2:

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

Los únicos nombres en el alcance en un momento dado son 1) los nombres locales de la función que se está ejecutando actualmente, si existe, y 2) nombres globales.

Requerimientos de la sumisión

Entrada y salida

Su intérprete puede leer el programa desde stdin o desde un archivo especificado mediante stdin o argumento de línea de comandos. Después de evaluar cada expresión, debería generar el resultado de esa expresión en stdout con una nueva línea final.

  • Los enteros se deben generar en la representación más natural de su lenguaje de implementación. Se pueden generar enteros negativos, con signos negativos iniciales.
  • Los símbolos deben aparecer como cadenas, sin comillas o escapes circundantes.
  • Las listas deben aparecer con todos los elementos separados por espacios y entre paréntesis. Un espacio dentro de los paréntesis es opcional: (1 2 3)y ( 1 2 3 )ambos son formatos aceptables.
  • La salida de funciones integradas y macros es un comportamiento indefinido. (La interpretación de referencia los muestra como <built-in function>.)

Otro

El intérprete de referencia incluye un entorno REPL y la capacidad de cargar módulos tinylisp desde otros archivos; estos se proporcionan por conveniencia y no son necesarios para este desafío.

Casos de prueba

Los casos de prueba se separan en varios grupos para que pueda probar los más simples antes de trabajar en los más complejos. Sin embargo, también funcionarán bien si los vuelcas todos juntos en un archivo. Simplemente no olvide eliminar los encabezados y la salida esperada antes de ejecutarlo.

Si ha implementado correctamente la recursividad de cola, el caso de prueba final (de varias partes) regresará sin causar un desbordamiento de la pila. La implementación de referencia lo calcula en aproximadamente seis segundos en mi computadora portátil.

DLosc
fuente
"Cualquier ficha que consista completamente en dígitos es un literal entero. (Los ceros iniciales están bien.) Cualquier ficha que contenga no dígitos es un símbolo, incluso ejemplos de aspecto numérico como 123abc, 3.14 y -10". parece contradecir "Los enteros deben ser compatibles al menos desde -2 ^ 31 a 2 ^ 31-1".
msh210
3
@ msh210 En realidad no, porque el primero habla de tokens mientras que el segundo habla de valores . Aunque no hay una forma directa de ingresar -1, aún puedo generar el valor -1 haciendo (s 0 1).
DLosc
1
@coredump Después de leer el artículo pertinente de Wikipedia , llegué a la conclusión de que la implementación en realidad está más cerca de la dinámica, pero sin anidar el alcance. Las variables en la función Fno están disponibles en la función Gsi las Fllamadas G(como con el alcance dinámico), pero tampoco están disponibles en la función Hsi Hes una función anidada definida dentro F(como con el alcance léxico) - vea el caso de prueba 5. Entonces llamándolo "léxico "podría ser engañoso.
DLosc
1
Para decirlo de otra manera: debido a la falta de anidación del alcance, una implementación podría usar una estrategia de alcance dinámica o léxica y generar los mismos resultados. Los únicos nombres en el alcance en un momento dado son 1) los nombres locales de la función que se está ejecutando actualmente, si existe, y 2) nombres globales. Los cierres no son compatibles. (La implementación de referencia mantiene una pila de enlaces de nombres correspondientes a la pila de llamadas, un enfoque de estilo dinámico, que creo que será el más fácil de implementar.)
DLosc
1
Obligatorio xkcd .
mınxomaτ

Respuestas:

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))

Lee la entrada de STDIN y escribe la salida en STDOUT.

Aunque no es estrictamente necesario, el intérprete admite funciones nulares y macros, y optimiza las llamadas de cola ejecutadas v.

Explicación

Analizando

Para analizar la entrada, primero rodeamos cada ocurrencia de (y )con espacios, y dividimos la cadena resultante en palabras; Esto nos da la lista de tokens. Mantenemos una pila de expresiones E, que inicialmente está vacía. Escaneamos los tokens, en orden:

  • si encontramos a (, empujamos una lista vacía en la parte superior de la pila de expresiones;
  • si encontramos a ), sacamos el valor en la parte superior de la pila de expresiones y lo agregamos a la lista que estaba debajo de la pila;
  • de lo contrario, agregamos el token actual, como una cadena, a la lista en la parte superior de la pila de expresiones (mantenemos enteros como cadenas en esta etapa y los analizamos durante la evaluación).

Si, al procesar un token ordinario, o después de extraer una expresión de la pila debido a ), la pila de expresiones está vacía, estamos en una expresión de nivel superior, y evaluamos el valor que de otro modo habríamos agregado, usando V()y imprima su resultado, formateado de manera apropiada utilizando F().

Evaluación

Mantenemos el alcance global G, como una lista de pares clave / valor. Inicialmente, contiene solo las funciones integradas (pero no las macros, y no v, que tratamos como una macro), que se implementan como lambdas.

Evaluación sucede en el interior V(), que toma la expresión a evaluar, ey el ámbito local, Lque es, también, una lista de pares clave / valor (cuando se evalúa una expresión de nivel superior, el ámbito local está vacía.) Las tripas de V()vivo dentro de un bucle infinito, que es cómo realizamos la optimización de llamada de cola (TCO), como se explica más adelante.

Procesamos esegún su tipo:

  • si es la lista vacía o una cadena convertible en int, la devolvemos inmediatamente (posiblemente después de la conversión a int); de otra manera,

  • si es una cadena, la buscamos en un diccionario construido a partir de la concatenación de los ámbitos global y local. Si encontramos un valor asociado, lo devolvemos; de lo contrario, edebe ser el nombre de un macro orden interna (es decir q, i, do v), y devolverlo sin cambios. De lo contrario, si eno es una cadena,

  • ees una lista (no vacía), es decir, una llamada a función. Evaluamos el primer elemento de la lista, es decir, la expresión de la función, llamando de forma V()recursiva (utilizando el ámbito local actual); Llamamos al resultado f. El resto de la lista A, es la lista de argumentos. fsolo puede ser una cadena, en cuyo caso es una macro integrada (o la función v), una lambda, en cuyo caso es una función integrada o una lista, en cuyo caso es una función o macro definida por el usuario.

    Si fes una cadena, es decir, una macro integrada, la manejamos en su lugar. Si es la macro io v, evaluamos su primer operando, y seleccionamos el segundo o tercer operando en consecuencia, en el caso de i, o utilizamos el resultado del primer operando, en el caso de v; en lugar de evaluar la expresión seleccionada de forma recursiva, lo que vencería al TCO, simplemente la reemplazamos econ dicha expresión y saltamos al comienzo del ciclo. Si fes la macro d, agregamos un par, cuyo primer elemento es el primer operando, y cuyo segundo elemento es el resultado de evaluar el segundo operando, al alcance global G, y devuelve el primer operando. De lo contrario, fes la macro q, en cuyo caso simplemente devolvemos su operando directamente.

    De lo contrario, si fes una lambda, o una lista cuyo primer elemento no es (), entonces es una función no nulary, no una macro, en cuyo caso evaluamos sus argumentos, es decir, los elementos de A, y los reemplazamos Acon el resultado.

    Si fes un lambda, lo llamamos, pasándole los argumentos desempaquetados Ay devolvemos el resultado.

    De lo contrario, fes una lista, es decir, una función o macro definida por el usuario; su lista de parámetros es el penúltimo elemento y su cuerpo es el último elemento. Como en el caso de las macros iy v, para realizar el TCO, no evaluamos el cuerpo de forma recursiva, sino que lo reemplazamos econ el cuerpo y continuamos con la siguiente iteración. A diferencia iy v, sin embargo, también reemplazamos el ámbito local L, con el nuevo ámbito local de la función. Si la lista de parámetros P, es, de hecho, una lista, el nuevo ámbito local se construye comprimiendo la lista de parámetros P, con la lista de argumentos A,; de lo contrario, estamos tratando con una función variada, en cuyo caso el nuevo ámbito local tiene solo un elemento, el par (P, A).

REPL

Si quieres jugar con él, aquí hay una versión REPL del intérprete. Es compatible con la redefinición de símbolos y la importación de archivos mediante los argumentos de la línea de comandos o la (import <filename>)macro. Para salir del intérprete, finalice la entrada (generalmente, Ctrl + D o Ctrl + Z).

Y aquí hay una sesión de ejemplo, implementando el tipo de fusión:

Ana
fuente
Puede obtener algo cada vez más corto usando zlib :) Comprima su código convertido en bytes y reemplácelo con:import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
Labo
Podrías guardar dos bytes asignándolos A[0]a una variable de un solo carácter justo después del bloque
Hannes Karppila
@HannesKarppila Así es, pero esto rompería las funciones nulares (ya que Aestá vacío en este caso), y no quiero "retroceder".
Ell
4

C (GNU), 1095 bytes

Gran parte de la acción tiene lugar en la vfunción gigante . En lugar de implementar la recursión de cola explícitamente, vestá estructurada de manera que muchas de las llamadas desde va vserán manejadas por la optimización de recursión de cola de gcc. No hay recolección de basura.

Esto hace un uso intensivo de las extensiones GCC, por lo que solo puede compilarse con gcc (use el comando gcc -w -Os tl.c). También usa algunas scanfextensiones que no estaban disponibles en Windows, que generalmente uso. La posibilidad de escribir el analizador con el estándar scanfera tan horrible que en su lugar utilicé una máquina virtual Linux para probar el programa. El análisis sin scanfclases de caracteres probablemente habría agregado más de 100 bytes.

#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-sin golf

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
fuente
¿Cuál es el uso del ejecutable compilado? ¿Es REPL? ¿Toma un nombre de archivo como entrada?
ckjbgames
@ckjbgames Lee un programa de stdin.
feersum
Bueno. Creo que deberías editar tu respuesta y tener en cuenta eso.
ckjbgames
1

Ceilán, 2422 bytes

(Creo que este es mi programa de golf más largo hasta ahora).

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));}}

Podría haber jugado algunos bytes más, ya que usé algunos identificadores de dos letras en algunos lugares, pero me he quedado sin letras simples algo significativas para esos. Aunque incluso de esta manera no se parece mucho a Ceilán ...

Esta es una implementación orientada a objetos .

Tenemos una interfaz de valores Vcon la implementación de clases L(lista: solo un contenedor alrededor de una secuencia de Ceilán de V), S(símbolo: contenedor alrededor de una cadena), I(entero - contenedor alrededor de un entero de Ceilán) y B(función o macro incorporada, un contenedor alrededor de un Función de Ceilán).

Utilizo la notación de igualdad estándar de Ceilán implementando el equalsmétodo (y también el hashatributo, que realmente solo es necesario para los símbolos), y también el stringatributo estándar para la salida.

Tenemos un atributo booleano b (que es verdadero por defecto, reemplazado Iy Ldevuelto falso para listas vacías), y dos métodos l(llamar, es decir, usar este objeto como función) y vO(evaluar un paso). Ambos devuelven un valor o un objeto de Continuación que permite la evaluación de un paso más, y vF(evaluar completamente) bucles hasta que el resultado ya no sea una continuación.

Una interfaz de contexto permite el acceso a variables. Hay dos implementaciones, Gpara el contexto global (que permite agregar variables usando el dincorporado) y LCpara un contexto local, que está activo cuando se evalúa la expresión de una función de usuario (vuelve al contexto global).

La evaluación de símbolos accede al contexto, las listas (si no están vacías) evalúan evaluando primero su primer elemento y luego llamando a su método de llamada. La llamada se implementa solo mediante listas y elementos incorporados: primero evalúa el argumento (si es una función, no si es una macro) y luego hace las cosas realmente interesantes: para los elementos incorporados justo lo que está codificado, para las listas crea un nuevo contexto local y devuelve un continuación con eso.

Para las construcciones utilicé un truco similar al que usé en mi Shift Interpreter , que me permite definirlas con los tipos de argumentos que necesitan, pero llamarlas con una secuencia genérica usando la reflexión (los tipos se verificarán en el momento de la llamada). Esto evita problemas de conversión / aserción de tipo dentro de las funciones / macros, pero necesita funciones de nivel superior para que pueda obtener sus Functionobjetos de metamodelo.

La función p(parse) divide la cadena en espacios, líneas nuevas y paréntesis, luego recorre los tokens y crea listas usando una pila y una lista de ejecución.

El intérprete (en el runmétodo, que es el punto de entrada) toma esta lista de expresiones (que son solo valores), evalúa cada una de ellas e imprime el resultado.


A continuación se muestra una versión con comentarios y se ejecuta a través de un formateador.

Una versión anterior antes de comenzar a jugar golf (y aún con algunos malentendidos sobre la evaluación de la lista) se encuentra en mi repositorio de Github , pondré esta allí pronto (así que asegúrese de mirar la primera versión si desea la original).

//  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
fuente