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 fonction G
si des F
appels G
(comme avec la portée dynamique), mais elles ne le sont pas non plus dans la fonction H
si H
est une fonction imbriquée définie à l'intérieur F
(comme avec la portée lexicale) - voir scénario de test 5. Appelle-la donc "lexical" "pourrait être trompeur.