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)
da1
.
"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 llamar1
como una función o macro, pero(q (1 2 3))
devuelve la lista(1 2 3)
. Evaluara
da el valor vinculado al nombrea
, 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 (0
o 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 ad
, 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 d
es 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 d
y 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 dei
.
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 a
llama a la función b
que 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 x
en 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.
fuente
-1
, aún puedo generar el valor -1 haciendo(s 0 1)
.F
no están disponibles en la funciónG
si lasF
llamadasG
(como con el alcance dinámico), pero tampoco están disponibles en la funciónH
siH
es una función anidada definida dentroF
(como con el alcance léxico) - vea el caso de prueba 5. Entonces llamándolo "léxico "podría ser engañoso.Respuestas:
Python 2,
685675660657646642640 bytesLee 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 expresionesE
, que inicialmente está vacía. Escaneamos los tokens, en orden:(
, empujamos una lista vacía en la parte superior de la pila de expresiones;)
, sacamos el valor en la parte superior de la pila de expresiones y lo agregamos a la lista que estaba debajo de la pila;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, usandoV()
y imprima su resultado, formateado de manera apropiada utilizandoF()
.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 nov
, que tratamos como una macro), que se implementan como lambdas.Evaluación sucede en el interior
V()
, que toma la expresión a evaluar,e
y el ámbito local,L
que 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 deV()
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
e
segú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,
e
debe ser el nombre de un macro orden interna (es decirq
,i
,d
ov
), y devolverlo sin cambios. De lo contrario, sie
no es una cadena,e
es 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 formaV()
recursiva (utilizando el ámbito local actual); Llamamos al resultadof
. El resto de la listaA
, es la lista de argumentos.f
solo puede ser una cadena, en cuyo caso es una macro integrada (o la funciónv
), 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
f
es una cadena, es decir, una macro integrada, la manejamos en su lugar. Si es la macroi
ov
, evaluamos su primer operando, y seleccionamos el segundo o tercer operando en consecuencia, en el caso dei
, o utilizamos el resultado del primer operando, en el caso dev
; en lugar de evaluar la expresión seleccionada de forma recursiva, lo que vencería al TCO, simplemente la reemplazamose
con dicha expresión y saltamos al comienzo del ciclo. Sif
es la macrod
, agregamos un par, cuyo primer elemento es el primer operando, y cuyo segundo elemento es el resultado de evaluar el segundo operando, al alcance globalG
, y devuelve el primer operando. De lo contrario,f
es la macroq
, en cuyo caso simplemente devolvemos su operando directamente.De lo contrario, si
f
es 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 deA
, y los reemplazamosA
con el resultado.Si
f
es un lambda, lo llamamos, pasándole los argumentos desempaquetadosA
y devolvemos el resultado.De lo contrario,
f
es 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 macrosi
yv
, para realizar el TCO, no evaluamos el cuerpo de forma recursiva, sino que lo reemplazamose
con el cuerpo y continuamos con la siguiente iteración. A diferenciai
yv
, sin embargo, también reemplazamos el ámbito localL
, con el nuevo ámbito local de la función. Si la lista de parámetrosP
, es, de hecho, una lista, el nuevo ámbito local se construye comprimiendo la lista de parámetrosP
, con la lista de argumentosA
,; 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).Mostrar fragmento de código
Y aquí hay una sesión de ejemplo, implementando el tipo de fusión:
Mostrar fragmento de código
fuente
import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
A[0]
a una variable de un solo carácter justo después del bloqueA
está vacío en este caso), y no quiero "retroceder".C (GNU), 1095 bytes
Gran parte de la acción tiene lugar en la
v
función gigante . En lugar de implementar la recursión de cola explícitamente,v
está estructurada de manera que muchas de las llamadas desdev
av
será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 algunasscanf
extensiones que no estaban disponibles en Windows, que generalmente uso. La posibilidad de escribir el analizador con el estándarscanf
era tan horrible que en su lugar utilicé una máquina virtual Linux para probar el programa. El análisis sinscanf
clases de caracteres probablemente habría agregado más de 100 bytes.Semi-sin golf
fuente
Ceilán, 2422 bytes
(Creo que este es mi programa de golf más largo hasta ahora).
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
V
con la implementación de clasesL
(lista: solo un contenedor alrededor de una secuencia de Ceilán deV
),S
(símbolo: contenedor alrededor de una cadena),I
(entero - contenedor alrededor de un entero de Ceilán) yB
(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
equals
método (y también elhash
atributo, que realmente solo es necesario para los símbolos), y también elstring
atributo estándar para la salida.Tenemos un atributo booleano
b
(que es verdadero por defecto, reemplazadoI
yL
devuelto falso para listas vacías), y dos métodosl
(llamar, es decir, usar este objeto como función) yvO
(evaluar un paso). Ambos devuelven un valor o un objeto de Continuación que permite la evaluación de un paso más, yvF
(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,
G
para el contexto global (que permite agregar variables usando eld
incorporado) yLC
para 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
Function
objetos 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
run
mé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).
fuente