lisp-in-rs-macros est un interpréteur Lisp simple à portée lexicale qui fonctionne uniquement avec les macros déclaratives de Rust, et la macro lisp! évalue le code à la compilation pour produire une valeur Lisp convertie en chaîne
lisp!(CAR (CONS (QUOTE A) (QUOTE (B)))) est calculé pendant l’expansion des macros par rustc et se développe en la chaîne "A" ; l’implémentation complète fait moins de 250 lignes
- Les exemples utilisent
CAR, LIST, QUOTE, PROGN, DEFINE, LAMBDA, DISPLAY, et l’exemple de quine montre du code Lisp qui s’évalue en lui-même
- La récursion explicite n’est pas prise en charge pour l’instant, mais il est possible d’écrire des comportements récursifs comme un append de liste via la self application ; en revanche,
DEFINE lui-même ne gère pas les définitions récursives
- L’exemple d’interpréteur métacirculaire semble fonctionner, mais l’évaluation de
((lambda (X) X) (quote a)) prend plus de 30 secondes et génère plus d’un million de tokens, au point que cargo finit tué par sigkill, ce qui le rend très inefficace
Un Lisp qui s’exécute dans les macros Rust
lisp-in-rs-macros est un interpréteur Lisp à portée lexicale écrit uniquement avec les macros déclaratives de Rust
- La macro
lisp! évalue le code Lisp fourni, puis convertit en chaîne la valeur Lisp calculée
- Par exemple,
lisp!(CAR (CONS (QUOTE A) (QUOTE (B)))) se développe en la chaîne "A"
- Ce calcul n’a pas lieu à l’exécution, mais à la compilation, pendant l’expansion des macros par rustc
- L’implémentation fait moins de 250 lignes
Exemple d’utilisation de base
- En combinant
CAR, LIST et QUOTE, on peut récupérer le premier élément d’une liste
let output = lisp!(CAR (LIST (QUOTE A) (QUOTE B) (QUOTE C)));
assert_eq!(output, "A");
- Pour évaluer plusieurs expressions, on utilise
PROGN
PROGN évalue toutes les expressions et renvoie la valeur de la dernière
DISPLAY évalue d’abord son argument, puis se développe sous la forme println!("{}", stringify!(evaled_argument)) afin d’afficher les tokens convertis en chaîne
lisp!(PROGN
(DEFINE message (LAMBDA () (QUOTE "hello there")))
(DISPLAY (message))
(DEFINE NOT (LAMBDA (X) (COND (X NIL) (TRUE TRUE))) )
(DISPLAY (NOT NIL))
);
- L’exemple ci-dessus affiche
"hello there" et "TRUE"
Un quine qui s’évalue en lui-même
- L’exemple de quine montre du code Lisp qui s’évalue en lui-même
lisp!
((LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s)))
(QUOTE (LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s)))));
- Ce code se développe en l’appel
stringify! suivant
stringify!(((LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s)))
(QUOTE (LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s))))));
Récursion et self application
- Ce Lisp ne prend actuellement pas en charge la récursion explicite
- Même sans récursion explicite, il est possible de créer des comportements récursifs avec uniquement des lambda
- Dans l’exemple, la fonction
append ne mentionne jamais directement le nom append dans son corps ; elle effectue l’appel récursif via auto-application au moyen de l’argument self
lisp!(PROGN
(DEFINE append
(LAMBDA (self X Y)
(COND
((EQ X NIL) Y)
(TRUE (CONS (CAR X) (self self (CDR X) Y)))
)))
(append append (QUOTE (A B)) (QUOTE (C D)))
)
- Ce code produit
"(A B C D)"
Contraintes d’utilisation
- La macro
lisp! n’évalue qu’une seule expression
- Pour plusieurs expressions, il faut les regrouper dans
(PROGN expr1 expr2 expr3)
- La liste vide n’est pas auto-évaluée
- On peut obtenir la valeur liste vide avec
NIL ou (QUOTE ())
- La liste vide est le seul objet falsy
- Les dotted lists ne sont pas prises en charge
CONS suppose que son dernier argument est une liste
DEFINE peut être utilisé n’importe où et s’évalue en liste vide, mais la récursion n’est pas prise en charge
TRUE est le seul atome auto-évalué qui ne soit pas une fonction
Formes prises en charge
DEFINE
QUOTE
LAMBDA
LET
PROGN
CAR
CDR
CONS
LIST
EQ
ATOM
APPLY
DEFINE est plus proche des définitions internes de Scheme que d’une vraie définition récursive à la Lisp
Un interpréteur Lisp écrit en Lisp
- Le dépôt contient un exemple d’interpréteur métacirculaire écrit dans ce Lisp
- L’exemple définit un combinateur
Y2 à deux arguments, CADR, CAAR, ASSOC, eval, etc.
- L’interpréteur semble fonctionner, mais évaluer
((lambda (X) X) (quote a)) prend plus de 30 secondes
- Cette évaluation génère plus d’un million de tokens et finit par grossir au point que cargo se fait tuer par sigkill
- La récursion avec un combinateur Y explicite est ici particulièrement inefficace
- L’auteur indique qu’il faudrait ajouter une primitive de récursion explicite pour corriger cela
- Pour un walkthrough sur l’écriture d’un évaluateur métacirculaire,
"Roots of Lisp" de Paul Graham est recommandé
Implémentation et ressources
- Les explications techniques se trouvent dans
EXPLANATION.md
- Les macros simulent essentiellement une machine SECD
- La machine SECD est une machine abstraite simple à pile pour évaluer des termes du lambda-calcul
Ressources
Functional Programming: Application and Implementation by Peter Henderson
- Ager, Mads Sig, et al.
"A functional correspondence between evaluators and abstract machines."
The Implementation of Functional Programming Languages by Simon Peyton Jones
- Billets de blog de Matt Might sur Lisp : https://matt.might.net
TODO
- Ajouter
letrec
- Ajouter un
define récursif
1 commentaires
Avis sur Hacker News
La dixième règle de Greenspun a encore frappé : https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule
Il faudra attendre C++26 pour pouvoir obtenir le
card’un pack de paramètres de noms de types avecArgs...[0]Je ne comprends pas pourquoi on n’introduit pas un
nilpour les packs de paramètres vides et des fonctionscar/cdr, et pourquoi on ne permet pas de stocker des packs de paramètres au lieu du bazar syntaxique actuelJ’avais essayé quelque chose de similaire il y a quelque temps, mais il y avait un problème : impossible de définir des symboles avec des tirets
Des choses comme
DEFINE MY-FN...ne fonctionnaient pas, parce que Rust découpe les tokens au niveau des tiretsC’est un petit détail, mais ça empêchait de coller tels quels de vrais morceaux de code Lisp : il fallait tout remplacer par des underscores. Je me demande si cette implémentation a le même souci
$x:ident, donc les tirets dans les atomes ne sont pas pris en chargeEn revanche, je pense qu’on pourrait faire une correspondance du genre
$x:ident $(- $y:ident)*. Il faudrait modifier quelques détails dans certaines branches de macros, mais ça semble possibleDEFINE MYᜭFN...fonctionne bienJ’aimerais bien qu’il existe une implémentation de Lisp basée sur Rust et bien prise en charge, pas seulement des macros
Je me demande dans quelle mesure on conserverait ou perdrait la sûreté mémoire en la construisant au-dessus de Rust. Est-il vraiment possible d’exploiter le borrow checker de façon sensée ?
Lisp est généralement défini par son caractère dynamique, et la vérification des types à l’exécution en est une grande partie. Forcer le programmeur à se préoccuper à l’avance de la manière dont les objets sont gérés entre en conflit avec le degré de liberté et d’expressivité attendu de ce type de système
En contrepartie, le compilateur lui-même peut rester relativement simple. Le code ordinaire sans déclarations supplémentaires est sûr par défaut, et sur une machine virtuelle à bytecode comme CLISP ou sur des machines Lisp avec vérification matérielle des types, ces déclarations peuvent être ignorées tout en restant toujours sûres
SBCL compile le code assez rapidement, et j’ai entendu dire que d’autres implémentations sont encore plus rapides. À l’inverse, le compilateur Rust est davantage susceptible de faire découvrir aux jeunes programmeurs le concept de thrashing
Je pense que, malgré les apparences, ce sont deux mondes difficilement compatibles. Lisp est par essence le langage emblématique de la philosophie « The Right Thing », tandis que C est le langage du « Worse is Better ». Rust n’est ni l’un ni l’autre ; c’est quelque chose d’assez différent pour mériter un nouveau nom reflétant les mauvais côtés de ces deux philosophies
Cela dit, je ne veux pas dénigrer l’article original : ça reste un hack très chouette
Il existe aussi d’autres Lisp (https://github.com/alilleybrinker/langs-in-rust). Ils semblent toutefois moins activement maintenus
Je me suis bien amusé à le faire, et j’ai aussi appris que rust-analyser n’arrive pas à gérer une macro qui génère des millions de tokens
L’ambiance voudrait que tout le monde s’exclame « c’est amusant », mais chaque fois que je vois ce genre de chose, je n’aime pas le fait que ce soit possible en Rust
Rust n’a jamais été un langage simple, mais j’ai l’impression qu’il est devenu bien plus difficile à maîtriser qu’au début
En revanche, je ne comprends pas vraiment pourquoi le fait que ce soit possible te dérange. Le système de macros peut générer du code d’une complexité presque illimitée, mais je ne suis pas sûr qu’implémenter un Lisp sandboxé avec des macros soit un exemple très probant montrant que Rust serait devenu plus difficile à gérer qu’à ses débuts
Cela dit, comme le système de types de Rust est Turing-complet, à la manière des templates C++ ou du système de types de Haskell, ça me donne envie de voir un Lisp implémenté de cette façon aussi
Les exemples typiques sont les durées de vie non lexicales,
impl Traiten position de retour et les traits asynchrones. Avant la 1.0, il y avait même des références GC intégrées avec une syntaxe spéciale, et ce genre de fonctionnalités a été suppriméSi tu veux un langage qui fasse de la simplicité un principe, Rust n’a jamais été ce langage-là, et il existe beaucoup d’autres options
J’ai vérifié, et je gagne ce pari : https://github.com/kchanqvq/CSP
Je parle surtout du fait de les « écrire » : c’est plutôt une fonctionnalité supplémentaire qu’on peut utiliser ou non
Wow, ça utilise macro_rules
Mais on ne disait pas que C++ n’était pas un langage sain parce que ses templates sont Turing-complets ?
Je ne sais pas de quel côté se situe le système de macros de Rust
macro_expand, et le fait que l’outillage Rust soit bien conçu compte beaucoupCarp mérite aussi d’être mentionné. C’est un Lisp qui utilise la vérification des emprunts, un peu le « Rust » du monde Lisp
1 : https://github.com/carp-lang/Carp