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.14
e -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 0
sono 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)
dà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 chiamare1
come funzione o macro, ma(q (1 2 3))
restituisce l'elenco(1 2 3)
. La valutazionea
fornisce il valore associato al nomea
, 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 (0
o 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 passarlod
, 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 d
comportamento 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 d
e 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 dii
.
La ricorsione della coda deve funzionare sia per la ricorsione diretta (una funzione chiama se stessa) sia per la ricorsione indiretta (funzione a
chiama funzione b
che 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é x
a 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.
-1
, posso comunque generare il valore -1 facendo (s 0 1)
.
F
non sono disponibili in funzione G
se F
chiamate G
(come con scoping dinamico), ma non sono disponibili anche in funzione H
se H
è definita una funzione annidata all'interno F
(come con scoping lessicale) - vedi caso di test 5. Quindi chiamandola "lessicale "potrebbe essere fuorviante.