Les programmeurs Lisp se vantent que Lisp est un langage puissant qui peut être construit à partir d'un très petit ensemble d'opérations primitives . Mettons cette idée en pratique en jouant au golf avec un interprète appelé dialecte tinylisp
.
Spécification de la langue
Dans cette spécification, toute condition dont le résultat est décrit comme "non défini" peut faire n'importe quoi dans votre interprète: planter, échouer de manière silencieuse, produire un gobbldegook aléatoire ou fonctionner comme prévu. Une implémentation de référence dans Python 3 est disponible ici .
Syntaxe
Les jetons dans tinylisp sont (
, )
ou toute chaîne d'un ou plusieurs caractères ASCII imprimables, à l'exception des parenthèses ou des espaces. (C'est-à-dire les expressions rationnelles suivantes:. [()]|[^() ]+
) Tout jeton constitué uniquement de chiffres est un littéral entier. (Les zéros en tête sont corrects.) Tout jeton contenant des chiffres non numériques est un symbole, même des exemples sous forme numérique 123abc
, tels que 3.14
, et -10
. Tous les espaces (y compris au minimum les caractères ASCII 32 et 10) sont ignorés, sauf dans la mesure où ils séparent les jetons.
Un programme tinylisp consiste en une série d'expressions. Chaque expression est un entier, un symbole ou une expression s (liste). Les listes se composent de zéro ou plusieurs expressions entourées de parenthèses. Aucun séparateur n'est utilisé entre les éléments. Voici des exemples d'expressions:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
Les expressions qui ne sont pas bien formées (en particulier, qui ont des parenthèses incomparables) donnent un comportement non défini. (L'implémentation de référence ferme automatiquement les parenthèses ouvertes et arrête l'analyse sur des parents proches non appariés.)
Types de données
Les types de données de tinylisp sont des entiers, des symboles et des listes. Les fonctions intégrées et les macros peuvent également être considérées comme un type, bien que leur format de sortie ne soit pas défini. Une liste peut contenir un nombre quelconque de valeurs de tout type et peut être imbriquée de manière arbitraire et en profondeur. Les entiers doivent être pris en charge au moins de -2 ^ 31 à 2 ^ 31-1.
La liste vide - ()
également appelée nil - et le nombre entier 0
sont les seules valeurs considérées comme logiquement fausses; tous les autres entiers, les listes non vides, les commandes intégrées et tous les symboles sont logiquement vrais.
Évaluation
Les expressions dans un programme sont évaluées dans l'ordre et les résultats de chacune sont envoyés à stdout (plus d'informations sur le formatage de la sortie ultérieurement).
- Un littéral entier est évalué à lui-même.
- La liste vide
()
s’évalue à elle-même. - Une liste d'un ou plusieurs éléments évalue son premier élément et la traite comme une fonction ou une macro, en l'appelant avec les éléments restants comme arguments. Si l'élément n'est pas une fonction / macro, le comportement n'est pas défini.
- Un symbole est évalué en tant que nom, donnant la valeur liée à ce nom dans la fonction en cours. Si le nom n'est pas défini dans la fonction en cours, la valeur qui lui est liée à l'échelle globale est évaluée. Si le nom n'est pas défini dans la portée actuelle ou globale, le résultat est indéfini (l'implémentation de référence donne un message d'erreur et renvoie nil).
Fonctions intégrées et macros
Tinylisp comporte sept fonctions intégrées. Une fonction évalue chacun de ses arguments avant de leur appliquer une opération et de renvoyer le résultat.
c
- contre [liste de texte]. Prend deux arguments, une valeur et une liste, et retourne une nouvelle liste obtenue en ajoutant la valeur au début de la liste.h
- tête ( voiture , dans la terminologie Lisp). Prend une liste et retourne le premier élément de celle-ci, ou nil si nil est donné.t
- tail ( cdr , en terminologie Lisp). Prend une liste et retourne une nouvelle liste contenant tout sauf le premier élément, ou nil si nil est donné.s
- soustraire. Prend deux entiers et retourne le premier moins le second.l
- moins que. Prend deux entiers; renvoie 1 si le premier est inférieur au second, 0 sinon.e
- égal. Prend deux valeurs du même type (les deux entiers, les deux listes ou les deux symboles); renvoie 1 si les deux sont égaux (ou identiques dans chaque élément), 0 sinon. Le test d'égalité dans les éléments intégrés n'est pas défini (l'implémentation de référence fonctionne comme prévu).v
- eval. Prend une liste, un entier ou un symbole représentant une expression et l’évalue. Par exemple, faire(v (q (c a b)))
est la même chose que faire(c a b)
;(v 1)
donne1
.
"Valeur" comprend ici toute liste, entier, symbole ou propriété intégrée, sauf indication contraire. Si une fonction est répertoriée comme prenant des types spécifiques, le fait de transmettre différents types constitue un comportement indéfini, tout comme le fait de transmettre un nombre incorrect d'arguments (l'implémentation de référence se bloque généralement).
Il existe trois macros intégrées dans tinylisp. Une macro, contrairement à une fonction, n'évalue pas ses arguments avant de leur appliquer des opérations.
q
- citation. Prend une expression et la retourne non évaluée. Par exemple, evaluation(1 2 3)
donne une erreur car il essaie d'appeler en1
tant que fonction ou macro mais(q (1 2 3))
renvoie la liste(1 2 3)
. L'évaluationa
donne la valeur liée au noma
, mais(q a)
donne le nom lui-même.i
- si. Prend trois expressions: une condition, une expression iftrue et une expression iffalse. Évalue la condition en premier. Si le résultat est faux (0
ou nul), évalue et renvoie l'expression iffalse. Sinon, évalue et renvoie l'expression iftrue. Notez que l'expression qui n'est pas renvoyée n'est jamais évaluée.d
- def. Prend un symbole et une expression. Evalue l'expression et la lie au symbole donné traité comme un nom à portée globale , puis renvoie le symbole. Tenter de redéfinir un nom doit échouer (en mode silencieux, avec un message ou en cas de plantage; l’implémentation de référence affiche un message d’erreur). Note: il est nécessaire de ne pas citer le nom avant de le transmettred
, mais il est nécessaire de citer l'expression si elle est une liste ou d'un symbole que vous ne voulez pas évalué: par exemple(d x (q (1 2 3)))
.
Le fait de transmettre un nombre incorrect d'arguments à une macro constitue un comportement indéfini (plantage de l'implémentation de référence). Le fait de passer quelque chose qui n'est pas un symbole en tant que premier argument de d
est un comportement indéfini (l'implémentation de référence ne génère pas d'erreur, mais la valeur ne peut pas être référencée ultérieurement).
Fonctions et macros définies par l'utilisateur
À partir de ces dix fonctions intégrées, le langage peut être étendu en créant de nouvelles fonctions et macros. Ceux-ci n'ont pas de type de données dédié; ce sont simplement des listes avec une certaine structure:
- Une fonction est une liste de deux éléments. Le premier est soit une liste d'un ou plusieurs noms de paramètres, soit un nom unique qui recevra une liste des arguments passés à la fonction (permettant ainsi des fonctions à arité variable). La seconde est une expression qui est le corps de la fonction.
- Une macro est identique à une fonction, sauf qu'elle contient nil avant le nom du paramètre, ce qui en fait une liste à trois éléments. (Tenter d'appeler des listes à trois éléments qui ne commencent pas par nil est un comportement indéfini; l'implémentation de la référence ignore le premier argument et les traite également comme des macros.)
Par exemple, l'expression suivante est une fonction qui ajoute deux entiers:
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
Et une macro qui prend un nombre quelconque d'arguments, évalue et renvoie le premier:
(q
(
()
args
(v (h args))
)
)
Les fonctions et les macros peuvent être appelées directement, liées à des noms à l'aide de d
, et transmises à d'autres fonctions ou macros.
Comme les corps de fonction ne sont pas exécutés au moment de la définition, les fonctions récursives sont facilement définissables:
(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
)
))
)
Notez cependant que ce qui précède n’est pas un bon moyen de définir une fonction de longueur car elle n’utilise pas ...
Récursion d'appel
La récursion d'appel est un concept important dans Lisp. Il implémente certains types de récursivité sous forme de boucles, réduisant ainsi la pile d'appels. Votre interprète tinylisp doit implémenter une récursion d’appel de queue appropriée!
- Si l'expression de retour d'une fonction ou d'une macro définie par l'utilisateur est un appel à une autre fonction ou macro définie par l'utilisateur, votre interprète ne doit pas utiliser la récursivité pour évaluer cet appel. Au lieu de cela, il doit remplacer la fonction et les arguments actuels par la nouvelle fonction, les arguments et la boucle jusqu'à la résolution de la chaîne d'appels.
- Si l'expression de retour d'une fonction ou d'une macro définie par l'utilisateur est un appel à
i
, n'évaluez pas immédiatement la branche sélectionnée. Au lieu de cela, vérifiez s'il s'agit d'un appel à une autre fonction ou macro définie par l'utilisateur. Si tel est le cas, remplacez la fonction et les arguments comme ci-dessus. Ceci s'applique aux occurrences arbitrairement profondément imbriquées dei
.
La récursion de la queue doit fonctionner à la fois pour la récursion directe (une fonction s'appelle elle-même) et pour la récursivité indirecte (la fonction a
appelle la fonction b
qui appelle [etc] qui appelle la fonction a
).
Une fonction de longueur récursive de la queue (avec une fonction d'assistance len*
):
(d len*
(q (
(list accum)
(i list
(len*
(t list)
(s 1 (s 0 accum))
)
accum
)
))
)
(d len
(q (
(list)
(len* list 0)
))
)
Cette implémentation fonctionne pour des listes arbitrairement grandes, limitées uniquement par la taille maximale de l'entier.
Portée
Les paramètres de fonction sont des variables locales (en fait, les constantes, car elles ne peuvent pas être modifiées). Ils sont dans la portée pendant l'exécution du corps de l'appel de cette fonction et sortent de la portée lors d'appels plus profonds et après le retour de la fonction. Ils peuvent "masquer" les noms définis globalement, rendant ainsi le nom global temporairement indisponible. Par exemple, le code suivant renvoie 5 et non 41:
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
Toutefois, le code suivant renvoie 41 car x
au niveau d’appel 1, il n’est pas accessible à partir du niveau d’appel 2:
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
Les seuls noms possibles à un moment donné sont 1) les noms locaux de la fonction en cours d'exécution, le cas échéant, et 2) les noms globaux.
Conditions de soumission
Entrée et sortie
Votre interprète peut lire le programme depuis stdin ou depuis un fichier spécifié via stdin ou un argument de ligne de commande. Après avoir évalué chaque expression, le résultat de cette expression doit être exporté sur stdout avec une nouvelle ligne.
- Les entiers doivent apparaître dans la représentation la plus naturelle de votre langage d'implémentation. Des entiers négatifs peuvent être générés, avec des signes moins principaux.
- Les symboles doivent être affichés sous forme de chaînes, sans guillemets ni échappements.
- Les listes doivent être sorties avec tous les éléments séparés par un espace et placés entre parenthèses. Un espace entre les parenthèses est facultative:
(1 2 3)
et( 1 2 3 )
sont tous deux des formats acceptables. - La sortie des fonctions intégrées et des macros est un comportement indéfini. (L'interprétation de référence les affiche comme
<built-in function>
.)
Autre
L'interpréteur de référence comprend un environnement REPL et la possibilité de charger des modules tinylisp à partir d'autres fichiers. ceux-ci sont fournis pour votre commodité et ne sont pas nécessaires pour relever ce défi.
Cas de test
Les scénarios de test sont séparés en plusieurs groupes afin que vous puissiez tester les plus simples avant de passer à des complexes plus complexes. Cependant, ils fonctionneront également très bien si vous les déposez tous dans un fichier. N'oubliez pas de supprimer les en-têtes et la sortie attendue avant de l'exécuter.
Si vous avez correctement implémenté la récursivité d'appel final, le cas de test final (en plusieurs parties) sera renvoyé sans provoquer de débordement de pile. L'implémentation de référence le calcule en six secondes environ sur mon ordinateur portable.
-1
, je peux toujours générer la valeur -1 en le faisant(s 0 1)
.F
ne sont pas disponibles dans la fonctionG
si desF
appelsG
(comme avec la portée dynamique), mais elles ne le sont pas non plus dans la fonctionH
siH
est une fonction imbriquée définie à l'intérieurF
(comme avec la portée lexicale) - voir scénario de test 5. Appelle-la donc "lexical" "pourrait être trompeur.Réponses:
Python 2,
685675660657646642640 octetsLit les entrées de STDIN et écrit les sorties dans STDOUT.
Bien que cela ne soit pas strictement nécessaire, l'interpréteur prend en charge les fonctions et les macros nullaires et optimise les appels finaux exécutés
v
.Explication
L'analyse
Pour analyser l'entrée, nous entourons d'abord chaque occurrence de
(
et)
avec des espaces, puis scindons la chaîne résultante en mots; cela nous donne la liste des jetons. Nous maintenons une pile d'expressionE
, qui est initialement vide. Nous numérisons les jetons, dans l'ordre:(
, nous poussons une liste vide en haut de la pile d'expression;)
, nous affichons la valeur en haut de la pile d’expressions et nous l’ajoutons à la liste qui se trouvait précédemment en dessous de celle-ci sur la pile;Si, lors du traitement d'un jeton ordinaire, ou après avoir extrait une expression de la pile en raison de
)
, la pile d'expression est vide, nous sommes à une expression de niveau supérieur et nous évaluons la valeur que nous aurions autrement ajoutée, à l'aide deV()
, et imprimer son résultat, formaté de manière appropriée en utilisantF()
.Évaluation
Nous maintenons la portée globale
G
, sous forme de liste de paires clé / valeur. Initialement, il ne contient que les fonctions internes (mais pas les macros, etv
que nous traitons comme une macro), qui sont implémentées comme des lambdas.L' évaluation se produit à l' intérieur
V()
, qui prend l'expression à évaluer,e
et le champ d' application locale,L
qui est, lui aussi, une liste de paires clé / valeur (lors de l' évaluation d' une expression de niveau haut, la portée locale est vide.) Les entrailles deV()
concert Comme nous le verrons plus tard, l’optimisation de l’appel final est une boucle infinie.Nous traitons
e
selon son type:s'il s'agit de la liste vide ou d'une chaîne convertible en int, nous la retournons immédiatement (éventuellement après conversion en int); autrement,
s'il s'agit d'une chaîne, nous le recherchons dans un dictionnaire construit à partir de la concaténation des portées globale et locale. Si nous trouvons une valeur associée, nous la retournons; sinon,
e
doit être le nom d'une macro prédéfinie (c'estq
-à- direi
,d
ouv
) et nous le retournons tel quel. Sinon, sie
n'est pas une chaîne,e
est une liste (non vide), c’est-à-dire un appel de fonction. Nous évaluons le premier élément de la liste, c'est-à-dire l'expression de la fonction, en appelant deV()
manière récursive (en utilisant la portée locale actuelle); nous appelons le résultatf
. Le reste de la listeA
, est la liste des arguments.f
il ne peut s'agir que d'une chaîne, auquel cas il s'agit d'une macro intégrée (ou de la fonctionv
), d'un lambda, auquel cas il s'agit d'une fonction intégrée ou d'une liste, auquel cas il s'agit d'une macro ou d'une fonction définie par l'utilisateur.Si
f
est une chaîne, c'est-à-dire une macro intégrée, nous la gérons sur place. S'il s'agit de la macroi
ouv
, nous évaluons son premier opérande et sélectionnons le deuxième ou le troisième opérande en conséquence dans le cas dei
, ou utilisons le résultat du premier opérande dans le cas dev
; au lieu d'évaluer récursivement l'expression sélectionnée, ce qui éliminerait le coût total de possession, nous la remplacerons simplemente
par ladite expression et passerons au début de la boucle. Sif
est la macrod
, nous ajoutons une paire dont le premier élément est le premier opérande et dont le deuxième élément est le résultat de l'évaluation du deuxième opérande, à la portée globaleG
, et renvoie le premier opérande. Sinon,f
c'est la macroq
, auquel cas nous retournons simplement son opérande.Autrement, s'il
f
s'agit d'un lambda ou d'une liste dont le premier élément n'est pas()
, il s'agit d'une fonction non nullary, pas d'une macro. Dans ce cas, nous évaluons ses arguments, c'est-à-dire les éléments deA
, et nous les remplaçonsA
par le résultat.Si
f
est un lambda, nous l'appelons, en lui transmettant les arguments non compressésA
et en renvoyant le résultat.Sinon,
f
est une liste, c'est-à-dire une fonction ou une macro définie par l'utilisateur; sa liste de paramètres est l'avant-dernier élément et son corps est le dernier. Comme dans le cas des macrosi
etv
, pour effectuer le TCO, nous n'évaluons pas le corps de manière récursive, mais le remplaçons plutôte
par le corps et passons à la prochaine itération. Contrairement ài
etv
cependant, nous remplaçons également la portée localeL
par la nouvelle portée locale de la fonction. Si la liste des paramètres,P
est, en fait, une liste, la nouvelle portée locale est construite par zipper la liste des paramètres,P
avec la liste des arguments,A
; sinon, nous avons affaire à une fonction variadique, auquel cas la nouvelle portée locale ne comporte qu'un seul élément, la paire(P, A)
.REPL
Si vous voulez jouer avec, voici une version de l'interpréteur REPL. Il prend en charge la redéfinition des symboles et l'importation de fichiers via les arguments de ligne de commande ou la
(import <filename>)
macro. Pour quitter l’interprète, mettez fin à la saisie (généralement Ctrl + D ou Ctrl + Z).Afficher l'extrait de code
Et voici un exemple de session mettant en œuvre le tri par fusion:
Afficher l'extrait de code
la source
import zlib;exec(zlib.decompress(your_code_compressed_in_bytes))
A[0]
une variable à un caractère juste après le blocA
vide dans ce cas), et je ne veux pas "régresser".C (GNU), 1095 octets
Une grande partie de l'action se déroule dans la
v
fonction géante . Au lieu d'implémenter explicitement la récursion de queue, lav
structure est telle que de nombreux appels dev
àv
seront traités par l'optimisation de la récursion de queue de gcc. Il n'y a pas de collecte de déchets.Cela fait un usage intensif des extensions GCC, il ne peut donc être compilé qu'avec gcc (utilisez la commande
gcc -w -Os tl.c
). Il utilise également desscanf
extensions non disponibles sous Windows, que j'utilise habituellement. La perspective d'écrire l'analyseur avec standardscanf
était si terrible que j'ai plutôt utilisé une machine virtuelle Linux pour tester le programme. L'analyse sansscanf
classes de caractères aurait probablement ajouté plus de 100 octets.Semi-Ungolfed
la source
Ceylan, 2422 octets
(Je pense que c'est mon plus long programme de golf à ce jour.)
J'aurais pu jouer plus sur quelques octets, car j'ai utilisé des identifiants à deux lettres à certains endroits, mais je n'ai plus beaucoup de lettres simples qui ont du sens. Même si cela ne ressemble pas beaucoup à Ceylan ...
Ceci est une implémentation orientée objet .
Nous avons une interface de valeur
V
avec des classes implémentantesL
(list - juste un wrapper autour d’une suite de CeylanV
),S
(symbol - wrapper autour d’une chaîne),I
(entier - un wrapper autour d’un entier de Ceylan) etB
(fonction intégrée ou macro, un wrapper autour d’un Fonction de Ceylan).J'utilise la notation d'égalité de Ceylan standard en implémentant la
equals
méthode (et également l'hash
attribut, qui n'est nécessaire que pour les symboles), ainsi que l'string
attribut standard pour la sortie.Nous avons un attribut booléen
b
(qui est true par défaut, remplacé parI
etL
pour renvoyer false pour les listes vides), et deux méthodesl
(call, c'est-à-dire utiliser cet objet en tant que fonction) etvO
(évaluer une étape). Tous deux renvoient soit une valeur, soit un objet Continuation qui permet ensuite une évaluation pour une étape supplémentaire, etvF
(évalue complètement) en boucle jusqu'à ce que le résultat ne soit plus une continuation.Une interface contextuelle permet d'accéder aux variables. Il y a deux implémentations,
G
pour le contexte global (qui permet d'ajouter des variables à l'aide de la commanded
intégrée) etLC
pour un contexte local, actif lors de l'évaluation de l'expression d'une fonction utilisateur (elle revient au contexte global).L'évaluation des symboles accède au contexte, les listes (si non vides) sont évaluées en évaluant d'abord leur premier élément, puis en appelant sa méthode d'appel. Call est mis en œuvre uniquement par listes et commandes intégrées - il évalue d’abord l’argument (si une fonction, pas si une macro), puis effectue le travail intéressant - pour les commandes intégrées, ce qui est codé en dur, pour les listes, il crée un nouveau contexte local et renvoie un continuation avec ça.
Pour les fonctions intégrées, j'ai utilisé une astuce similaire à celle utilisée dans mon interpréteur de décalage , ce qui me permet de les définir avec les types d'arguments dont ils ont besoin, mais de les appeler avec une séquence générique utilisant la réflexion (les types seront vérifiés au moment de l'appel). Cela évite les problèmes de conversion / assertion de type à l'intérieur des fonctions / macros, mais nécessite des fonctions de niveau supérieur pour que je puisse obtenir leurs
Function
objets de méta-modèle .La fonction
p
(analyse) divise la chaîne au niveau des espaces, des nouvelles lignes et des parenthèses, puis parcourt les marques et construit des listes à l'aide d'une pile et d'une liste en cours d'exécution.L'interpréteur (dans la
run
méthode, qui est le point d'entrée) prend ensuite cette liste d'expressions (qui ne sont que des valeurs), les évalue et affiche le résultat.Vous trouverez ci-dessous une version avec des commentaires et un formateur.
Une version antérieure à mes débuts (et avec quelques incompréhensions à propos de l'évaluation de liste) se trouve dans mon référentiel Github , je vais y mettre celle-ci bientôt (veillez donc à regarder la première version si vous voulez l'original).
la source