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.
-1
, aún puedo generar el valor -1 haciendo (s 0 1)
.
F
no están disponibles en la función G
si las F
llamadas G
(como con el alcance dinámico), pero tampoco están disponibles en la función H
si H
es 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.