Piccolo Lisp, piccolo interprete


33

I programmatori di Lisp si vantano che Lisp è un linguaggio potente che può essere creato da una serie molto piccola di operazioni primitive . Mettiamo in pratica quell'idea giocando a golf un interprete per un dialetto chiamato tinylisp.

Specifica del linguaggio

In questa specifica, qualsiasi condizione il cui risultato è descritto come "indefinito" può fare qualsiasi cosa nel tuo interprete: crash, fallimento silenzioso, produzione di gobbldegook casuali o lavoro come previsto. Un'implementazione di riferimento in Python 3 è disponibile qui .

Sintassi

I token in tinylisp sono (, )o qualsiasi stringa di uno o più caratteri ASCII stampabili ad eccezione delle parentesi o dello spazio. (Vale a dire la seguente regex:. [()]|[^() ]+) Ogni token che è costituito interamente da cifre è un valore intero letterale. (Gli zeri iniziali sono a posto.) Ogni token che contiene cifre non è un simbolo, anche esempi numerici dall'aspetto piace 123abc, 3.14e -10. Tutto lo spazio bianco (inclusi, almeno, i caratteri ASCII 32 e 10) viene ignorato, tranne nella misura in cui separa i token.

Un programma tinylisp è costituito da una serie di espressioni. Ogni espressione è un numero intero, un simbolo o un'espressione s (elenco). Gli elenchi sono composti da zero o più espressioni racchiuse tra parentesi. Nessun separatore viene utilizzato tra gli articoli. Ecco alcuni esempi di espressioni:

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

Le espressioni che non sono ben formate (in particolare, che hanno parentesi senza pari) danno un comportamento indefinito. (L'implementazione di riferimento chiude automaticamente le parentesi aperte e interrompe l'analisi su parentesi chiuse senza pari.)

Tipi di dati

I tipi di dati di tinylisp sono numeri interi, simboli ed elenchi. Anche le funzioni e le macro integrate possono essere considerate un tipo, sebbene il loro formato di output non sia definito. Un elenco può contenere qualsiasi numero di valori di qualsiasi tipo e può essere nidificato in modo arbitrario in modo approfondito. I numeri interi devono essere supportati almeno da -2 ^ 31 a 2 ^ 31-1.

L'elenco vuoto ()- anche indicato come zero - e l'intero 0sono gli unici valori considerati logicamente falsi; tutti gli altri numeri interi, elenchi non vuoti, builtin e tutti i simboli sono logicamente veri.

Valutazione

Le espressioni in un programma vengono valutate in ordine e i risultati di ciascuno vengono inviati a stdout (ulteriori informazioni sulla formattazione dell'output in seguito).

  • Un valore letterale intero viene valutato da solo.
  • L'elenco vuoto ()restituisce se stesso.
  • Un elenco di uno o più elementi valuta il suo primo elemento e lo tratta come una funzione o macro, chiamandolo con gli elementi rimanenti come argomenti. Se l'elemento non è una funzione / macro, il comportamento non è definito.
  • Un simbolo valuta come un nome, dando il valore associato a quel nome nella funzione corrente. Se il nome non è definito nella funzione corrente, valuta il valore associato ad esso nell'ambito globale. Se il nome non è definito nell'ambito corrente o globale, il risultato non è definito (l'implementazione di riferimento fornisce un messaggio di errore e restituisce zero).

Funzioni e macro integrate

Esistono sette funzioni integrate in tinylisp. Una funzione valuta ciascuno dei suoi argomenti prima di applicare alcune operazioni e restituire il risultato.

  • c- contro [elenco di condotta]. Accetta due argomenti, un valore e un elenco e restituisce un nuovo elenco ottenuto aggiungendo il valore all'inizio dell'elenco.
  • h- testa ( macchina , nella terminologia di Lisp). Prende un elenco e restituisce il primo elemento in esso, oppure nullo se indicato come nullo.
  • t- tail ( cdr , nella terminologia di Lisp). Prende un elenco e restituisce un nuovo elenco contenente tutto tranne il primo elemento, oppure zero se indicato come zero.
  • s- sottrai. Prende due numeri interi e restituisce il primo meno il secondo.
  • l- meno di. Accetta due numeri interi; restituisce 1 se il primo è inferiore al secondo, 0 altrimenti.
  • e- uguale. Accetta due valori dello stesso tipo (entrambi numeri interi, entrambi gli elenchi o entrambi i simboli); restituisce 1 se i due sono uguali (o identici in ogni elemento), 0 altrimenti. Il test dei builtin per l'uguaglianza non è definito (l'implementazione di riferimento funziona come previsto).
  • v- eval. Accetta un elenco, un numero intero o un simbolo, che rappresenta un'espressione e la valuta. Ad esempio fare (v (q (c a b)))è come fare (c a b); (v 1)1.

"Valore" qui include qualsiasi elenco, numero intero, simbolo o incorporato, se non diversamente specificato. Se una funzione viene elencata come prendendo tipi specifici, passarne diversi tipi è un comportamento indefinito, così come passare un numero errato di argomenti (l'implementazione di riferimento generalmente si arresta in modo anomalo).

Esistono tre macro integrate in tinylisp. Una macro, a differenza di una funzione, non valuta i suoi argomenti prima di applicarvi le operazioni.

  • q- citazione. Prende un'espressione e la restituisce non valutata. Ad esempio, la valutazione (1 2 3)genera un errore perché tenta di chiamare 1come funzione o macro, ma (q (1 2 3))restituisce l'elenco (1 2 3). La valutazione afornisce il valore associato al nome a, ma (q a)fornisce il nome stesso.
  • i- Se. Accetta tre espressioni: una condizione, un'espressione iftrue e un'espressione iffalse. Valuta prima la condizione. Se il risultato è falso ( 0o nullo), valuta e restituisce l'espressione iffalse. Altrimenti, valuta e restituisce l'espressione iftrue. Si noti che l'espressione che non viene restituita non viene mai valutata.
  • d- def. Prende un simbolo e un'espressione. Valuta l'espressione e la lega al simbolo dato trattato come un nome nell'ambito globale , quindi restituisce il simbolo. Il tentativo di ridefinire un nome dovrebbe fallire (in silenzio, con un messaggio o in crash; l'implementazione di riferimento visualizza un messaggio di errore). Nota: non è necessario citare il nome prima di passarlo d, anche se è necessario citare l'espressione se è un elenco o un simbolo che non si desidera valutare: ad es (d x (q (1 2 3))).

Passare un numero errato di argomenti a una macro è un comportamento indefinito (arresti anomali dell'implementazione di riferimento). Passare qualcosa che non è un simbolo come primo argomento di un dcomportamento indefinito (l'implementazione di riferimento non dà un errore, ma il valore non può essere referenziato successivamente).

Funzioni e macro definite dall'utente

A partire da questi dieci incorporati, il linguaggio può essere esteso costruendo nuove funzioni e macro. Questi non hanno un tipo di dati dedicato; sono semplicemente elenchi con una certa struttura:

  • Una funzione è un elenco di due elementi. Il primo è un elenco di uno o più nomi di parametri o un singolo nome che riceverà un elenco di tutti gli argomenti passati alla funzione (consentendo quindi funzioni di arità variabile). La seconda è un'espressione che è il corpo della funzione.
  • Una macro è uguale a una funzione, tranne per il fatto che contiene zero prima dei nomi dei parametri, rendendola così un elenco di tre elementi. (Cercare di chiamare elenchi di tre elementi che non iniziano con zero è un comportamento indefinito; l'implementazione di riferimento ignora il primo argomento e li tratta anche come macro.)

Ad esempio, la seguente espressione è una funzione che aggiunge due numeri interi:

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

E una macro che accetta un numero qualsiasi di argomenti, valuta e restituisce la prima:

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

Le funzioni e le macro possono essere richiamate direttamente, associate ai nomi utilizzando de passate ad altre funzioni o macro.

Poiché i corpi funzione non vengono eseguiti al momento della definizione, le funzioni ricorsive sono facilmente definibili:

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

Si noti, tuttavia, che quanto sopra non è un buon modo per definire una funzione di lunghezza perché non utilizza ...

Ricorsione di coda

La ricorsione in coda è un concetto importante in Lisp. Implementa alcuni tipi di ricorsione come loop, mantenendo così piccolo lo stack di chiamate. Il tuo interprete tinylisp deve implementare la ricorsione della coda corretta!

  • Se l'espressione di ritorno di una funzione o macro definita dall'utente è una chiamata a un'altra funzione o macro definita dall'utente, l'interprete non deve utilizzare la ricorsione per valutare quella chiamata. Al contrario, deve sostituire la funzione e gli argomenti correnti con la nuova funzione e gli argomenti e il ciclo fino a quando la catena di chiamate non viene risolta.
  • Se l'espressione di ritorno di una funzione o macro definita dall'utente è una chiamata a i, non valutare immediatamente il ramo selezionato. Invece, controlla se si tratta di una chiamata a un'altra funzione o macro definita dall'utente. In tal caso, scambiare la funzione e gli argomenti come sopra. Questo vale per occorrenze arbitrariamente profondamente annidate di i.

La ricorsione della coda deve funzionare sia per la ricorsione diretta (una funzione chiama se stessa) sia per la ricorsione indiretta (funzione achiama funzione bche chiama [ecc] quale chiama funzione a).

Una funzione di lunghezza ricorsiva della coda (con una funzione di aiuto len*):

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

Questa implementazione funziona per elenchi arbitrariamente grandi, limitati solo dalla dimensione massima dell'intero.

Scopo

I parametri di funzione sono variabili locali (in realtà costanti, poiché non possono essere modificate). Sono nell'ambito mentre viene eseguito il corpo di quella chiamata di quella funzione e fuori portata durante qualsiasi chiamata più profonda e dopo il ritorno della funzione. Possono "ombreggiare" nomi definiti globalmente, rendendo così temporaneamente non disponibile il nome globale. Ad esempio, il codice seguente restituisce 5, non 41:

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

Tuttavia, il codice seguente restituisce 41, perché xa livello di chiamata 1 non è accessibile dal livello di chiamata 2:

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

Gli unici nomi nell'ambito in qualsiasi momento sono 1) i nomi locali della funzione attualmente in esecuzione, se presenti, e 2) nomi globali.

Requisiti per la presentazione

Ingresso e uscita

L'interprete può leggere il programma da stdin o da un file specificato tramite stdin o argomento della riga di comando. Dopo aver valutato ciascuna espressione, dovrebbe generare il risultato di quell'espressione su stdout con una nuova riga finale.

  • I numeri interi devono essere stampati nella rappresentazione più naturale del linguaggio di implementazione. Possono essere emessi numeri interi negativi, con segni meno iniziali.
  • I simboli devono essere emessi come stringhe, senza virgolette o escape circostanti.
  • Gli elenchi devono essere stampati con tutti gli elementi separati da spazi e racchiusi tra parentesi. Uno spazio tra parentesi è facoltativo: (1 2 3)e ( 1 2 3 )sono entrambi formati accettabili.
  • L'emissione di funzioni e macro integrate è un comportamento indefinito. (L'interpretazione di riferimento li visualizza come <built-in function>.)

Altro

L'interprete di riferimento include un ambiente REPL e la capacità di caricare moduli tinylisp da altri file; questi sono forniti per comodità e non sono richiesti per questa sfida.

Casi test

I casi di test sono divisi in diversi gruppi in modo da poter testare quelli più semplici prima di passare a quelli più complessi. Tuttavia, funzioneranno anche bene se li scarichi tutti in un file insieme. Non dimenticare di rimuovere le intestazioni e l'output previsto prima di eseguirlo.

Se è stata implementata correttamente la ricorsione di coda, il caso di test finale (in più parti) tornerà senza causare un overflow dello stack. L'implementazione di riferimento lo calcola in circa sei secondi sul mio laptop.


"Qualsiasi token che consiste interamente di cifre è un valore letterale intero. (Gli zeri iniziali vanno bene.) Qualsiasi token che contiene non cifre è un simbolo, anche esempi dall'aspetto numerico come 123abc, 3.14 e -10." sembra contraddire "I numeri interi devono essere supportati almeno da -2 ^ 31 a 2 ^ 31-1".
msh210

3
@ msh210 Non proprio, perché il primo parla di token mentre il secondo parla di valori . Anche se non esiste un modo diretto per entrare -1, posso comunque generare il valore -1 facendo (s 0 1).
DLosc,

1
@coredump Dopo aver letto l'articolo pertinente di Wikipedia , ho concluso che l'implementazione è in realtà più vicina alla dinamica, ma senza nidificazione dell'ambito. Le variabili in funzione Fnon sono disponibili in funzione Gse Fchiamate G(come con scoping dinamico), ma non sono disponibili anche in funzione Hse Hè definita una funzione annidata all'interno F(come con scoping lessicale) - vedi caso di test 5. Quindi chiamandola "lessicale "potrebbe essere fuorviante.
DLosc,

1
Per dirla in altro modo: a causa della mancanza di nidificazione dell'ambito, un'implementazione potrebbe utilizzare una strategia di scoping dinamica o lessicale e ottenere gli stessi risultati. Gli unici nomi nell'ambito in qualsiasi momento sono 1) i nomi locali della funzione attualmente in esecuzione, se presenti, e 2) nomi globali. Le chiusure non sono supportate. (L'implementazione di riferimento mantiene una pila di associazioni di nomi corrispondenti allo stack di chiamate - un approccio in stile dinamico, che penso sia il più facile da implementare.)
DLosc

1
Xkcd obbligatorio .
mınxomaτ,

Risposte:


11

Python 2, 685 675 660 657 646 642 640 byte

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

Legge l'input da STDIN e scrive l'output su STDOUT.

Sebbene non sia strettamente necessario, l'interprete supporta funzioni e macro nulle e ottimizza le chiamate di coda eseguite attraverso v.

Spiegazione

parsing

Per analizzare l'input, per prima cosa circondiamo ogni occorrenza di (e )con spazi e dividiamo la stringa risultante in parole; questo ci dà l'elenco dei token. Manteniamo uno stack di espressioni E, che inizialmente è vuoto. Scansioniamo i token, in ordine:

  • se incontriamo a (, inseriamo un elenco vuoto nella parte superiore dello stack di espressioni;
  • se incontriamo a ), inseriamo il valore nella parte superiore dello stack di espressioni e lo aggiungiamo all'elenco che era precedentemente al di sotto di esso nello stack;
  • in caso contrario, aggiungiamo il token corrente, come una stringa, all'elenco nella parte superiore dello stack di espressioni (manteniamo gli interi come stringhe in questa fase e li analizziamo durante la valutazione).

Se, durante l'elaborazione di un token ordinario o dopo aver estratto un'espressione dallo stack a causa di ), lo stack di espressioni è vuoto, siamo a un'espressione di livello superiore e valutiamo il valore che altrimenti avremmo aggiunto, utilizzando V()e stampa il suo risultato, formattato in modo appropriato usando F().

Valutazione

Manteniamo l'ambito globale G, come un elenco di coppie chiave / valore. Inizialmente, contiene solo le funzioni integrate (ma non le macro e non v, che trattiamo come macro), che sono implementate come lambdas.

Valutazione avviene all'interno V(), che prende l'espressione da valutare, ee l'ambito locale, Lche è anche una lista di coppie chiave / valore (quando valutando un'espressione di livello superiore, l'ambito locale è vuoto.) Il coraggio di V()vivere all'interno di un ciclo infinito, che è il modo in cui eseguiamo l'ottimizzazione della coda (TCO), come spiegato più avanti.

Elaboriamo in ebase al suo tipo:

  • se si tratta dell'elenco vuoto o di una stringa convertibile in un int, lo restituiamo immediatamente (possibilmente dopo la conversione in int); altrimenti,

  • se è una stringa, la cerchiamo in un dizionario costruito dalla concatenazione degli ambiti globale e locale. Se troviamo un valore associato, lo restituiamo; altrimenti, edeve essere il nome di una macro incorporata (cioè q, i, do v), e ci ritorno invariato. Altrimenti, se enon è una stringa,

  • eè un elenco (non vuoto), ovvero una chiamata di funzione. Valutiamo il primo elemento dell'elenco, ovvero l'espressione della funzione, chiamando V()ricorsivamente (utilizzando l'attuale ambito locale); chiamiamo il risultato f. Il resto dell'elenco Aè l'elenco degli argomenti. fpuò essere solo una stringa, nel qual caso è una macro incorporata (o la funzione v), una lambda, nel qual caso è una funzione incorporata o un elenco, nel qual caso è una funzione o una macro definita dall'utente.

    Se fè una stringa, ovvero una macro incorporata, la gestiamo sul posto. Se è la macro iov , valutiamo il suo primo operando e selezioniamo di conseguenza il secondo o il terzo operando, nel caso di i, oppure utilizziamo il risultato del primo operando, nel caso di v; invece di valutare in modo ricorsivo l'espressione selezionata, che sconfiggerebbe il TCO, sostituiamo semplicemente econ detta espressione e passiamo all'inizio del ciclo. Se fè la macro d, aggiungiamo una coppia, il cui primo elemento è il primo operando e il cui secondo elemento è il risultato della valutazione del secondo operando, nell'ambito globale G, e restituiamo il primo operando. Altrimenti, fè la macro q, nel qual caso restituiamo semplicemente il suo operando direttamente.

    Altrimenti, se fè un lambda, o un elenco il cui primo elemento non lo è (), allora è una funzione non nulla, non una macro, nel qual caso valutiamo i suoi argomenti, cioè gli elementi di A, e lo sostituiamo Acon il risultato.

    Se fè un lambda, lo chiamiamo, passando gli argomenti decompressi Ae restituiamo il risultato.

    Altrimenti, fè un elenco, ovvero una funzione o una macro definita dall'utente; la sua lista di parametri è il penultimo elemento e il suo corpo è l'ultimo elemento. Come nel caso delle macro ie v, al fine di eseguire il TCO, non valutiamo il corpo in modo ricorsivo, ma piuttosto sostituiamo econ il corpo e continuiamo alla successiva iterazione. A differenza ie v, tuttavia, sostituiamo anche l'ambito locale L, con il nuovo ambito locale della funzione. Se l'elenco dei parametri P, in realtà, è un elenco, il nuovo ambito locale viene creato comprimendo l'elenco dei parametri,P , con l'elenco degli argomenti A,; altrimenti, abbiamo a che fare con una funzione variadica, nel qual caso il nuovo ambito locale ha solo un elemento, la coppia (P, A).

REPL

Se vuoi giocarci, ecco una versione REPL dell'interprete. Supporta la ridefinizione dei simboli e l'importazione dei file tramite gli argomenti della riga di comando o la (import <filename>)macro. Per uscire dall'interprete, termina l'immissione (di solito, Ctrl + D o Ctrl + Z).

Ed ecco una sessione di esempio, implementando l'ordinamento di tipo merge:


Puoi ottenere qualcosa di sempre più breve usando zlib :) Comprimi il tuo codice convertito in byte e sostituiscilo con:import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
Labo

È possibile salvare due byte assegnando A[0]a qualche variabile di un carattere subito dopo il blocco
Hannes Karppila,

@HannesKarppila Esatto, ma ciò spezzerebbe le funzioni null (dato che Ain questo caso è vuoto), e non voglio "regredire".
Ell

4

C (GNU), 1095 byte

Gran parte dell'azione si svolge nella vfunzione gigante . Invece di implementare esplicitamente la ricorsione della coda, vè strutturato in modo tale che molte delle chiamate da va vsaranno gestite dall'ottimizzazione della ricorsione della coda di gcc. Non esiste una raccolta rifiuti.

Questo fa un uso intensivo delle estensioni GCC, quindi può essere compilato solo con gcc (usare il comando gcc -w -Os tl.c). Utilizza anche alcune scanfestensioni che non erano disponibili su Windows, che di solito uso. La prospettiva di scrivere il parser con lo standard scanfera così terribile che ho usato una macchina virtuale Linux per testare il programma. L'analisi senza scanfclassi di caratteri probabilmente avrebbe aggiunto oltre 100 byte.

#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 è l'utilizzo dell'eseguibile compilato? È REPL? Prende un nome file come input?
ckjbgames,

@ckjbgames Legge un programma da stdin.
feersum

Va bene. Penso che dovresti modificare la tua risposta e notare che.
ckjbgames,

1

Ceylon, 2422 byte

(Penso che questo sia il mio programma di golf più lungo di sempre.)

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

Avrei potuto giocare a golf ancora un po 'di byte, poiché in alcuni punti ho usato alcuni identificatori a due lettere, ma per quelli ho esaurito lettere singole piuttosto significative. Anche se in questo modo non sembra molto Ceylon ...

Questo è orientato agli oggetti un'implementazione .

Abbiamo un'interfaccia di valore Vcon le classi di implementazione L(elenco - solo un wrapper attorno a una sequenza di Ceylon di V), S(simbolo - wrapper attorno a una stringa), I(intero - wrapper attorno a un intero Ceylon) e B(funzione integrata o macro, un wrapper attorno a un Funzione Ceylon).

Uso la notazione di uguaglianza standard di Ceylon implementando il equalsmetodo (e anche l' hashattributo, che è realmente necessario solo per i simboli), e anche lo standardstring attributo per l'output.

Abbiamo un attributo booleano b(che è vero per impostazione predefinita, sovrascritto Ie Lrestituire false per elenchi vuoti) e due metodi l(chiama, cioè usa questo oggetto come una funzione) e vO(valuta un passo). Entrambi restituiscono un valore o un oggetto Continuation che consente quindi la valutazione per un ulteriore passaggio e vF(valuta completamente) i loop fino a quando il risultato non è più una continuazione.

Un'interfaccia di contesto consente l'accesso alle variabili. Esistono due implementazioni, Gper il contesto globale (che consente di aggiungere variabili usando il dbuiltin) e LCper un contesto locale, che è attivo quando si valuta l'espressione di una funzione utente (ricade nel contesto globale).

La valutazione dei simboli accede al contesto, gli elenchi (se non vuoti) valutano prima valutando il loro primo elemento e quindi chiamando il suo metodo call. La chiamata è implementata solo da liste e builtin - prima valuta l'argomento (se una funzione, non se una macro) e poi fa le cose davvero interessanti - per i builtin solo ciò che è hardcoded, per le liste crea un nuovo contesto locale e restituisce un continuazione con quello.

Per i builtin ho usato un trucco simile a quello che ho usato nel mio Shift Interpreter , che mi permette di definirli con i tipi di argomento di cui hanno bisogno, ma chiamarli con una sequenza generica usando la riflessione (i tipi saranno controllati al momento della chiamata). Questo evita il fastidio di conversione / asserzione del tipo all'interno delle funzioni / macro, ma ha bisogno di funzioni di livello superiore in modo da poter ottenere i loro Functionoggetti meta-modello .

La funzione p(analisi) divide la stringa in spazi, newline e parentesi, quindi passa in rassegna i token e crea elenchi utilizzando uno stack e un elenco in esecuzione.

L'interprete (nel runmetodo, che è il punto di ingresso) prende quindi questo elenco di espressioni (che sono solo valori), le valuta ognuna e stampa il risultato.


Di seguito è una versione con commenti ed eseguito attraverso un formattatore.

Una versione precedente prima di iniziare a giocare a golf (e ancora con alcuni fraintendimenti sulla valutazione della lista) si trova nel mio repository Github , lo inserirò presto (quindi assicurati di guardare la prima versione se vuoi l'originale).

//  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));
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.