Lisp minúsculo, intérprete minúsculo


33

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.
  • eigual. 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)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 chamar 1como uma função ou macro, mas (q (1 2 3))retorna a lista (1 2 3). A avaliação afornece 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á-lo d, 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 de i.

A recursão de cauda deve funcionar tanto para recursão direta (uma função se chama) quanto recursão indireta (função achama função bque 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 xno 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.


"Qualquer token que consiste inteiramente de dígitos é um literal inteiro. (Os zeros à esquerda são aceitáveis.) Qualquer token que contenha não-dígitos é um símbolo, até mesmo exemplos de aparência numérica como 123abc, 3.14 e -10." parece contradizer "Os números inteiros devem ser suportados pelo menos de -2 ^ 31 a 2 ^ 31-1".
Msh210

3
@ msh210 Na verdade não, porque o primeiro está falando sobre tokens, enquanto o último está falando sobre valores . Mesmo que não haja uma maneira direta de entrar -1, ainda posso gerar o valor -1 fazendo (s 0 1).
DLosc 4/11/15

1
@coredump Depois de ler o artigo pertinente da Wikipedia , concluí que a implementação está realmente mais próxima da dinâmica, mas sem aninhamento de escopo. Variáveis ​​na função Fnão estão disponíveis na função Gse Fchamadas G(como no escopo dinâmico), mas também não estão disponíveis na função Hse Hfor 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.
DLosc

1
Em outras palavras: devido à falta de aninhamento de escopo, uma implementação pode usar uma estratégia de escopo dinâmica ou lexical e gerar os mesmos resultados. 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. Encerramentos não são suportados. (A implementação de referência mantém uma pilha de ligações de nome correspondente ao pilha de chamadas -. Uma abordagem de estilo dinâmico, o que eu acho que vai ser mais fácil de implementar)
DLosc

1
Xkcd obrigatório .
mınxomaτ

Respostas:


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

Lê a entrada de STDIN e grava a saída em STDOUT.

Embora não seja estritamente necessário, o intérprete suporta funções e macros nulas e otimiza as chamadas finais executadas v.

Explicação

Análise

Para analisar a entrada, primeiro envolvemos cada ocorrência de (e )com espaços e dividimos a sequência resultante em palavras; isso nos dá a lista de tokens. Mantemos uma pilha de expressões E, inicialmente vazia. Analisamos os tokens, em ordem:

  • se encontrarmos a (, empurramos uma lista vazia no topo da pilha de expressões;
  • se encontrarmos um ) , exibimos o valor no topo da pilha de expressões e o anexamos à lista que estava anteriormente abaixo na pilha;
  • caso contrário, anexamos o token atual, como uma string, à lista na parte superior da pilha de expressões (mantemos inteiros como strings nesse estágio e os analisamos durante a avaliação).

Se, ao processar um token comum ou depois de exibir uma expressão da pilha devido a ), a pilha de expressões estiver vazia, estamos em uma expressão de nível superior e avaliamos o valor que, de outra forma, teríamos acrescentado, usando V()e imprima seu resultado, formatado adequadamente usando F().

Avaliação

Mantemos o escopo global, G , como uma lista de pares de chave / valor. Inicialmente, ele contém apenas as funções internas (mas não as macros, e não v, que tratamos como macro), que são implementadas como lambdas.

A avaliação acontece dentro V(), que pega a expressão para avaliar ee o escopo local L, que também é uma lista de pares de chave / valor (ao avaliar uma expressão de nível superior, o escopo local está vazio.)V() viver dentro de um loop infinito, que é como executamos a otimização de chamada de cauda (TCO), conforme explicado mais adiante.

Processamos de eacordo com seu tipo:

  • se for a lista vazia ou uma string conversível em int, retornamos imediatamente (possivelmente após a conversão em int); de outra forma,

  • se for uma string, procuramos em um dicionário construído a partir da concatenação dos escopos global e local. Se encontrarmos um valor associado, retornamos; caso contrário, edeve ser o nome de uma macro embutido (ie q, i, dou v), e devolvê-lo inalterado. Caso contrário, se enão for uma sequência,

  • eé uma lista (não vazia), ou seja, uma chamada de função. Avaliamos o primeiro elemento da lista, ou seja, a expressão da função, chamando V()recursivamente (usando o escopo local atual); nós chamamos o resultado f. O restante da lista,, Aé a lista de argumentos. fpode ser apenas uma string; nesse caso, é uma macro interna (ou a funçãov ), um lambda; nesse caso, é uma função incorporada ou uma lista; nesse caso, é uma função ou macro definida pelo usuário.

    Se ffor uma string, ou seja, uma macro interna, nós a manipularemos no local. Se for a macro iou v, avaliamos seu primeiro operando e selecionamos o segundo ou terceiro operando de acordo, no caso de i, ou usamos o resultado do primeiro operando, no caso de v; em vez de avaliar a expressão selecionada recursivamente, o que derrotaria o TCO, simplesmente substituímos epela expressão mencionada e pulamos para o início do loop. Se ffor a macro d, anexamos um par, cujo primeiro elemento é o primeiro operando, e cujo segundo elemento é o resultado da avaliação do segundo operando, no escopo global G, e retornamos o primeiro operando. Caso contrário, fé a macro q; nesse caso, simplesmente retornamos seu operando diretamente.

    De maneira oposta, se ffor um lambda ou uma lista cujo primeiro elemento não é (), então é uma função não nula, não uma macro; nesse caso, avaliamos seus argumentos, ou seja, os elementos de Ae substituímos Apelo resultado.

    Se fé um lambda, nós o chamamos, passando os argumentos descompactados Ae retornando o resultado.

    Caso contrário, fé uma lista, ou seja, uma função ou macro definida pelo usuário; sua lista de parâmetros é o penúltimo elemento e seu corpo é o último elemento. Como no caso das macros ie v, para executar o TCO, não avaliamos o corpo recursivamente, mas substituímos epelo corpo e continuamos para a próxima iteração. Ao contrário ie v, no entanto, também substituímos o escopo local L, pelo novo escopo local da função. Se a lista de parâmetros,, Pé, de fato, uma lista, o novo escopo local é construído fechando a lista de parâmetros,, Pcom a lista de argumentos A; caso contrário, estamos lidando com uma função variável, caso em que o novo escopo local possui apenas um elemento, o par (P, A).

REPL

Se você quiser brincar com ele, aqui está uma versão REPL do intérprete. Ele suporta a redefinição de símbolos e a importação de arquivos por meio dos argumentos da linha de comando ou da (import <filename>)macro. Para sair do intérprete, encerre a entrada (geralmente, Ctrl + D ou Ctrl + Z).

E aqui está uma sessão de exemplo, implementando a classificação de mesclagem:


Você pode obter algo cada vez mais curto usando o zlib :) Comprima seu código convertido em bytes e substitua-o por: #import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
Labo

Você poderia salvar dois bytes, atribuindo A[0]a alguma variável de um caractere logo após exceto bloco
Hannes Karppila

@HannesKarppila Isso mesmo, mas isso quebraria funções nulas (já que Aestá vazio neste caso), e eu não quero "regredir".
Ell

4

C (GNU), 1095 bytes

Grande parte da ação ocorre na vfunção gigante . Em vez de implementar explicitamente a recursão de cauda, vestá estruturado para que muitas das chamadas de vpara vsejam tratadas pela otimização de recursão de cauda do gcc. Não há coleta de lixo.

Isso faz uso pesado de extensões do GCC, portanto, ele só pode ser compilado com o gcc (use o comando gcc -w -Os tl.c). Ele também usa algumas scanfextensões que não estavam disponíveis no Windows, que eu costumo usar. A perspectiva de escrever o analisador com o padrão scanfera tão terrível que usei uma VM Linux para testar o programa. A análise sem scanfclasses de caracteres provavelmente teria adicionado mais 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-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));

}

Qual é o uso do executável compilado? É REPL? É necessário um nome de arquivo como entrada?
Ckjbgames

@ckjbgames Lê um programa a partir de stdin.
feersum

OK. Eu acho que você deve editar sua resposta e anotar isso.
Ckjbgames

1

Ceilão, 2422 bytes

(Acho que este é o meu programa de golfe mais longo até agora.)

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

Eu poderia ter jogado mais alguns bytes, pois usei alguns identificadores de duas letras em alguns lugares, mas fiquei sem letras únicas significativas para eles. Embora, mesmo assim, não pareça muito com o Ceilão ...

Esta é uma implementação orientada a objetos .

Temos uma interface de valor Vcom as classes de implementação L(lista - apenas um invólucro em torno de um seqüencial de Ceilão V), S(invólucro de símbolo em torno de uma string), I(número inteiro - invólucro em torno de um número inteiro de Ceilão) e B(função ou macro incorporada, um invólucro em torno de um Função Ceilão).

Eu uso a notação de igualdade padrão do Ceilão implementando o equalsmétodo (e também o hashatributo, que é realmente necessário apenas para símbolos), e também o stringatributo padrão para saída.

Temos um atributo booleano b(que é verdadeiro por padrão, substituído Ie Lretornar falso para listas vazias) e dois métodos l(chamada, ou seja, usar esse objeto como uma função) e vO(avaliar uma etapa). Ambos retornam um valor ou um objeto Continuation que permite a avaliação para mais uma etapa e vF(avaliam completamente) os loops até que o resultado não seja mais uma continuação.

Uma interface de contexto permite o acesso a variáveis. Existem duas implementações, Gpara o contexto global (que permite adicionar variáveis ​​usando o dbuiltin) e LCpara um contexto local, que é ativo ao avaliar a expressão de uma função do usuário (ela volta ao contexto global).

A avaliação de símbolos acessa o contexto, as listas (se não estiverem vazias) são avaliadas avaliando primeiro seu primeiro elemento e depois chamando seu método de chamada. A chamada é implementada apenas por listas e built-in - ele primeiro avalia o argumento (se uma função, não uma macro) e, em seguida, faz as coisas realmente interessantes - para os built-in exatamente o que é codificado, para listas, ele cria um novo contexto local e retorna um continuação com isso.

Para os componentes internos, usei um truque semelhante ao usado no meu Shift Interpreter , que permite defini-los com os tipos de argumento de que eles precisam, mas chamá-los com uma sequência genérica usando reflexão (os tipos serão verificados no momento da chamada). Isso evita problemas de conversão / asserção de tipo dentro das funções / macros, mas precisa de funções de nível superior para que eu possa obter seus Functionobjetos de metamodelo .

A pfunção (análise) divide a cadeia de caracteres em espaços, novas linhas e parênteses, passa pelos tokens e cria listas usando uma pilha e uma lista em execução.

O intérprete (no runmétodo, que é o ponto de entrada), pega essa lista de expressões (que são apenas valores), avalia cada uma delas e imprime o resultado.


Abaixo está uma versão com comentários e executada através de um formatador.

Uma versão anterior antes de começar a jogar golfe (e ainda com alguns mal-entendidos sobre a avaliação de listas) é encontrada no meu repositório do Github , colocarei esta em breve (então, verifique a primeira versão, se quiser a 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));
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.