Os programadores do Lisp se gabam de que o Lisp é uma linguagem poderosa que pode ser criada a partir de um conjunto muito pequeno de operações primitivas . Vamos colocar essa idéia em prática jogando golfe em um intérprete para um dialeto chamado tinylisp
.
Especificação de idioma
Nesta especificação, qualquer condição cujo resultado seja descrito como "indefinido" pode fazer qualquer coisa no seu intérprete: travar, falhar silenciosamente, produzir um registro aleatório do gobbld ou funcionar como esperado. Uma implementação de referência no Python 3 está disponível aqui .
Sintaxe
Tokens em tinylisp são (
, )
ou qualquer seqüência de um ou mais imprimíveis caracteres ASCII exceto parênteses ou espaço. (Ou seja, o seguinte regex:. [()]|[^() ]+
) Qualquer token que consiste inteiramente de dígitos é um literal inteiro. (Os zeros à esquerda estão corretos.) Qualquer token que contenha não dígitos é um símbolo, mesmo exemplos com aparência numérica 123abc
, como 3.14
, e -10
. Todo o espaço em branco (incluindo, no mínimo, caracteres ASCII 32 e 10) é ignorado, exceto na medida em que separa os tokens.
Um programa tinylisp consiste em uma série de expressões. Cada expressão é um número inteiro, um símbolo ou uma expressão s (lista). As listas consistem em zero ou mais expressões entre parênteses. Nenhum separador é usado entre os itens. Aqui estão exemplos de expressões:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
Expressões que não são bem formadas (em particular, que possuem parênteses não correspondentes) fornecem um comportamento indefinido. (A implementação de referência fecha automaticamente as parênteses abertas e para de analisar as parênteses próximas sem comparação.)
Tipos de dados
Os tipos de dados de tinylisp são números inteiros, símbolos e listas. Funções e macros internas também podem ser consideradas um tipo, embora seu formato de saída seja indefinido. Uma lista pode conter qualquer número de valores de qualquer tipo e pode ser aninhada arbitrariamente profundamente. Os números inteiros devem ser suportados pelo menos entre -2 ^ 31 e 2 ^ 31-1.
A lista vazia - ()
também chamada de zero - e o número inteiro0
são os únicos valores que são considerados logicamente falsos; todos os outros números inteiros, listas não vazias, componentes internos e todos os símbolos são logicamente verdadeiros.
Avaliação
As expressões em um programa são avaliadas em ordem e os resultados de cada um são enviados para o stdout (mais sobre a formatação de saída posteriormente).
- Um literal inteiro é avaliado por si próprio.
- A lista vazia
()
avaliada por si mesma. - Uma lista de um ou mais itens avalia seu primeiro item e o trata como uma função ou macro, chamando-o com os itens restantes como argumentos. Se o item não for uma função / macro, o comportamento será indefinido.
- Um símbolo é avaliado como um nome, fornecendo o valor vinculado a esse nome na função atual. Se o nome não estiver definido na função atual, ele avalia o valor a ele associado no escopo global. Se o nome não estiver definido no escopo atual ou global, o resultado será indefinido (a implementação de referência fornece uma mensagem de erro e retorna nulo).
Funções e macros incorporadas
Existem sete funções integradas no tinylisp. Uma função avalia cada um de seus argumentos antes de aplicar alguma operação a eles e retornar o resultado.
c
- contras [lista de produtos]. Pega dois argumentos, um valor e uma lista, e retorna uma nova lista obtida adicionando o valor na frente da lista.h
- cabeça ( carro , na terminologia Lisp). Pega uma lista e retorna o primeiro item nela, ou nulo se nulo.t
- cauda ( cdr , na terminologia Lisp). Pega uma lista e retorna uma nova lista contendo todos, exceto o primeiro item, ou nulo se for nulo.s
- subtrair. Toma dois números inteiros e retorna o primeiro menos o segundo.l
- menos que. Toma dois números inteiros; retorna 1 se o primeiro for menor que o segundo, 0 caso contrário.e
igual. Leva dois valores do mesmo tipo (ambos os números inteiros, ambas as listas ou ambos os símbolos); retorna 1 se os dois forem iguais (ou idênticos em todos os elementos), 0 caso contrário. O teste dos componentes internos para igualdade não é definido (a implementação de referência funciona como esperado).v
- avaliação. Pega uma lista, número inteiro ou símbolo, representando uma expressão, e a avalia. Por exemplo, fazer(v (q (c a b)))
é o mesmo que fazer(c a b)
;(v 1)
dá1
.
"Valor" aqui inclui qualquer lista, número inteiro, símbolo ou interno, a menos que especificado de outra forma. Se uma função é listada como tendo tipos específicos, passar tipos diferentes é um comportamento indefinido, assim como passar o número errado de argumentos (a implementação de referência geralmente falha).
Existem três macros internas no tinylisp. Uma macro, diferente de uma função, não avalia seus argumentos antes de aplicar operações a eles.
q
- citação. Pega uma expressão e a retorna sem avaliação. Por exemplo, avaliar(1 2 3)
gera um erro porque tenta chamar1
como uma função ou macro, mas(q (1 2 3))
retorna a lista(1 2 3)
. A avaliaçãoa
fornece o valor associado ao nomea
, mas(q a)
o próprio nome.i
- E se. Toma três expressões: uma condição, uma expressão iftrue e uma expressão iffalse. Avalia a condição primeiro. Se o resultado for falso (0
ou nulo), avalia e retorna a expressão iffalse. Caso contrário, avalia e retorna a expressão iftrue. Observe que a expressão que não é retornada nunca é avaliada.d
- def. Pega um símbolo e uma expressão. Avalia a expressão e a vincula ao símbolo especificado tratado como um nome no escopo global e , em seguida, retorna o símbolo. A tentativa de redefinir um nome deve falhar (silenciosamente, com uma mensagem ou travando; a implementação de referência exibe uma mensagem de erro). Nota: não é necessário citar o nome antes de passá-lod
, embora seja necessário citar a expressão se for uma lista ou símbolo que você não deseja avaliar: por exemplo(d x (q (1 2 3)))
,.
Passar o número errado de argumentos para uma macro é um comportamento indefinido (falhas na implementação de referência). Passar algo que não é um símbolo como o primeiro argumento de d
é um comportamento indefinido (a implementação de referência não gera um erro, mas o valor não pode ser referenciado posteriormente).
Funções e macros definidas pelo usuário
A partir dessas dez built-ins, o idioma pode ser estendido através da construção de novas funções e macros. Estes não têm tipo de dados dedicado; são simplesmente listas com uma certa estrutura:
- Uma função é uma lista de dois itens. O primeiro é uma lista de um ou mais nomes de parâmetros ou um único nome que receberá uma lista de todos os argumentos passados para a função (permitindo, assim, funções de variável variável). A segunda é uma expressão que é o corpo da função.
- Uma macro é igual a uma função, exceto que ela contém nada antes do (s) nome (s) do parâmetro, tornando-a uma lista de três itens. (Tentar chamar listas de três itens que não começam com zero é um comportamento indefinido; a implementação de referência ignora o primeiro argumento e os trata como macros também.)
Por exemplo, a seguinte expressão é uma função que adiciona dois números inteiros:
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
E uma macro que pega qualquer número de argumentos, avalia e retorna o primeiro:
(q
(
()
args
(v (h args))
)
)
Funções e macros podem ser chamadas diretamente, ligadas a nomes usando d
e passadas para outras funções ou macros.
Como os corpos das funções não são executados no momento da definição, as funções recursivas são facilmente definíveis:
(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
)
))
)
Observe, porém, que o exposto acima não é uma boa maneira de definir uma função de comprimento porque ela não usa ...
Recursão de chamada de cauda
A recursão de chamada de cauda é um conceito importante no Lisp. Ele implementa certos tipos de recursão como loops, mantendo assim a pilha de chamadas pequena. Seu intérprete tinylisp deve implementar a recursão adequada da chamada de cauda!
- Se a expressão de retorno de uma função ou macro definida pelo usuário for uma chamada para outra função ou macro definida pelo usuário, seu intérprete não deverá usar recursão para avaliar essa chamada. Em vez disso, ele deve substituir a função e os argumentos atuais pela nova função e argumentos e fazer um loop até que a cadeia de chamadas seja resolvida.
- Se a expressão de retorno de uma função ou macro definida pelo usuário for uma chamada para
i
, não avalie imediatamente a ramificação selecionada. Em vez disso, verifique se é uma chamada para outra função ou macro definida pelo usuário. Nesse caso, troque a função e os argumentos como acima. Isso se aplica a ocorrências arbitrariamente profundamente aninhadas dei
.
A recursão de cauda deve funcionar tanto para recursão direta (uma função se chama) quanto recursão indireta (função a
chama função b
que chama [etc] que chama função a
).
Uma função de comprimento recursivo da cauda (com uma função 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)
))
)
Essa implementação funciona para listas arbitrariamente grandes, limitadas apenas pelo tamanho máximo máximo.
Escopo
Os parâmetros de função são variáveis locais (na verdade constantes, pois não podem ser modificados). Eles estão no escopo enquanto o corpo dessa chamada dessa função está sendo executada e fora do escopo durante todas as chamadas mais profundas e após o retorno da função. Eles podem "ocultar" nomes definidos globalmente, tornando o nome global temporariamente indisponível. Por exemplo, o código a seguir retorna 5, não 41:
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
No entanto, o código a seguir retorna 41, porque x
no nível de chamada 1 não é acessível no nível de chamada 2:
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
Os únicos nomes no escopo a qualquer momento são 1) os nomes locais da função atualmente em execução, se houver, e 2) nomes globais.
Requisitos de envio
Entrada e saída
Seu intérprete pode ler o programa do stdin ou de um arquivo especificado via stdin ou argumento da linha de comando. Após avaliar cada expressão, ele deve gerar o resultado dessa expressão em stdout com uma nova linha à direita.
- Os números inteiros devem ser impressos na representação mais natural da sua linguagem de implementação. Inteiros negativos podem ser gerados, com sinais de menos.
- Os símbolos devem ser impressos como cadeias, sem aspas ou escapes.
- As listas devem ser exibidas com todos os itens separados por espaço e entre parênteses. Um espaço entre parênteses é opcional:
(1 2 3)
e( 1 2 3 )
ambos são formatos aceitáveis. - A saída de funções e macros internas é um comportamento indefinido. (A interpretação de referência os exibe como
<built-in function>
.)
De outros
O intérprete de referência inclui um ambiente REPL e a capacidade de carregar módulos tinylisp de outros arquivos; estes são fornecidos por conveniência e não são necessários para este desafio.
Casos de teste
Os casos de teste são separados em vários grupos para que você possa testar casos mais simples antes de trabalhar em casos mais complexos. No entanto, eles também funcionarão bem se você despejar todos eles em um arquivo juntos. Só não se esqueça de remover os títulos e a saída esperada antes de executá-lo.
Se você implementou corretamente a recursão de chamada de cauda, o caso de teste final (com várias partes) retornará sem causar um estouro de pilha. A implementação de referência calcula em cerca de seis segundos no meu laptop.
-1
, ainda posso gerar o valor -1 fazendo (s 0 1)
.
F
não estão disponíveis na função G
se F
chamadas G
(como no escopo dinâmico), mas também não estão disponíveis na função H
se H
for uma função aninhada definida dentro F
(como no escopo lexical) - consulte o caso de teste 5. Chamando isso de "léxico" "pode ser enganoso.