7.6 KiB
7.6 KiB
Tests unitaires
- «tests de régression» : on a pu modifier des morceaux de code qui avait déjà les tests et voir que ça tournait toujours (après modifs)
- framework extensible : possibilité d'ajouter de nouveaux types de tests (ex.
deftest-error
,deftest-equiv
) - séparation des tests en modules / sous-modules, possibilité de lancer des tests de certains (sous-)modules seulement.
- Statistiques : contabilisation du nombre de tests, d'erreurs, etc.
mini-meval
mini-meval
est un méta-évaluateur «naïf» mais qui supporte pratiquement tout LISP sauf CLOS (Common Lisp Object System), les packages, les hash, ….
Syntaxe supportée par mini-meval et le simplificateur
- Les macros
- Les
eval-when
- Les
tagbody
etgo
, lesthrow
etcatch
, lesblock
etreturn
etreturn-from
et leunwind-protect
- Les
let
,let*
,flet
,labels
,macrolet
, - Les
macro
- Les
lambda
avec capture de variables - Dans la
lambda-list
:&optional
,&rest
,&key
,&allow-other-keys
,&aux
, mais pas les&body
- Plus
progn
,if
,#'
,quote
, etc.
lisp2li
- On a choisi de ne pas utiliser le langage intermédiaire proposé par les encadrants, mais au contraire d'utiliser du LISP (très) simplifié qui est donc exécutable ce qui nous a permis de faire des tests unitaires pour voir si la transformation préservait le sens du programme.
- Vu qu'on rajoute des sortes de special-form dans le langage intermédiaire qui ne font pas partie de LISP, pour que le langage
intermédiaire reste exécutable, on emballe le code généré dans un
macrolet
qui définit des macros transformant des special-operator en code LISP équivalent. - Cette transformation est assurée par la fonction squash-lisp décrite ci-dessous.
squash-lisp
- Pour transformer les let/let*/flet/labels/lambda en let simplifiés : décorrelation de l'étape de déclaration d'une variable, son affectation, et son utilisation.
- La transformation est hygénique (les special-form intermédiaires qu'on ajoute sont des symboles uniques. On aurait pu utiliser le système de package, mais on aurait dû supporter les packages à ce moment-là).
- En 3 passes :
- Passe 1 :
- macro-expansion (on utilise
mini-meval
) eteval-when
. - simplification globale de la syntaxe :
*
(let (a (b 2) c) (list a b c))
→ (let ((a nil) (b 2) (c nil)) (list a b c))
- unification de la syntaxe des let, let*, flet, labels.
- tous les appels de fonction sont transformés en funcall.
- ré-écriture des
tagbody
/go
,throw
/catch
,block
/return
/return-from
en termes deunwind
/unwind-protect
,jump
/jump-label
plus quelques variations de ces derniers. - noms uniques pour les étiquettes des
jump
/jump-label
. - toutes les constantes sont emballées dans
(quote)
. - toutes les lectures de variables sont emballées dans
(get-var)
.
- macro-expansion (on utilise
- Passe 2 :
- noms uniques pour les variables locales. Il n'y a donc plus besoin de connaître l'imbrication des environnements pour savoir à quelle définition fait référence l'utilisation d'une variable.
- toutes les déclarations de variables locales (
let
,let*
,flet
,labels
,lambda
) sont transformées ensimple-let
- le
simple-let
ne fait que déclarer, il n'afecte pas de valeurs : (let ((a nil) (b 2)) (list a b)) → (simple-let (a b) (setq a nil) (setq b 2) (list a b)) - simplification de la lambda-list (élimination de
&optional
,&rest
,&key
,&allow-other-keys
,&aux
) - suppression des paramètres de la
lambda
: (lambda (x y) (+ x y)) → (simple-lambda (simple-let (x y) (setq x (get-param 0)) (setq y (get-param 1)) (+ x y)))
- Passe 3
- Lorsqu'une variable à l'intérieur d'une
lambda
référence une déclaration à l'extérieur de lalambda
, on la marque comme étant capturée. - On fusionne tous les
let
d'unelambda
en les remontant dans unlet
unique à la racine de lalamdba
. - On sort toutes les lambdas (fonctions anonymes), on les nomme avec un symbole unique, et on les met au top-level.
- Lorsqu'une variable à l'intérieur d'une
- Passe 1 :
pattern-match
- Nous avons cherché des bibliothèques de pattern-matching pour LISP, mais la seule qui était suffisemment puissante pour nos besoins
utilisait une syntaxe très lourde qui prenait pratiquement autant de place que des tests du type
(if (eq (car expr) progn) ...)
. - Nous avons fait une fonction de pattern matching,
pattern-match
qui utilise une syntaxe proche de celle des expressions régulières pour reconnaître des expressions LISP qui ont une certaine forme (suivant un motif). On peut «capturer» des portions de ce motif dans des variables qui sont accessibles dans le corps du match. Exemple : (match (lambda :params @ :body _*) expr (format t "La liste des paramètres est : ~a" params) (format t "Le premier élément du corps est : ~a" (car body))) - On a aussi
cond-match
qui est un dérivé d'uncase
mais où chaque test est un motif qui doit correspondre avec l'expression donnée au début ducond-match
. - On a aussi
match-automaton
qui est une sorte d'automate où les transitions ne sont pas des lettres mais des motifs. On avance dans une liste en sélectionnant à chaque fois la première transition qui match avec l'élément en cours. On peut «collecter» des données à chaque transition qui seront mises côte à côte dans une liste, renvoyée par lematch-automaton
. defmatch
permet de définir une famille de fonctions, dont une seule est exécutée à l'appel (celle qui match avec le paramètre).- Pas d'optimisation à la compilation permettant de détecter de manière statuque quelle fonction de la famille exécuter : la syntaxe de match est assez lourde, et vu qu'on ne match pas sur la forme de l'appel de fonction, mais sur les valeurs passées en paramètre, l'optimisation n'est pas faisable 90% du temps (on ne sait pas à la «compilation» quelle sera la valeur du paramètre).
compilation
- On compile vers de l'assembleur assez proche de l'x86.
- Le code généré reste exécutable, tout comme le lisp simplifié. On utilise la même technique : emballer le code généré dans un
macrolet
. - On avait déjà un système d'exploitation assez simple qui tournait de manière autonome sur un PC.
- On a donc stotché ensemble une REPL compilée et le SE de manière à avoir une REPL autonome qui ne dépend de rien.
- Le système d'exploitation sous-jacent fournit juste de quoi faire des entrées-sorties.
- On a choisi de l'hypothèse d'un «monde fermé», dans lequel on connait tout le code à compiler dès le départ.
Ramasse-miettes
- gestion du tas
- On a un gc très simpliste qui copie les données d'une zone de la mémoire vers une autre et vice versa à chaque fois qu'elle est pleine.
- Ce type de gc s'appelle un two-finger garbage collector.
- La raison de ce choix de modèle de gc est que les autres types de gc nécessitent une occupation variable de la mémoire pour l'exécution du gc (nécessaire pour le parcours en largeur/profondeur) qui peut être aussi grosse que la mémoire occupée (𝑶(𝑛)) dans le pire des cas.
Implémentation de fonctions LISP
- On a notre propre fonction
read
et notre propre fonctionformat
pour être autonomes. - Implémentation de loop avec toutes les extensions sauf celles de typage (analysées mais ignorées silencieusement)