2010-m1s1-compilation/lisp/notes/soutenance.markdown
2011-01-13 00:00:49 +01:00

7.6 KiB
Raw Blame History

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 et go, les throw et catch, les block et return et return-from et le unwind-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) et eval-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 de unwind/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).
    • 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 en simple-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 la lambda, on la marque comme étant capturée.
      • On fusionne tous les let d'une lambda en les remontant dans un let unique à la racine de la lamdba.
      • On sort toutes les lambdas (fonctions anonymes), on les nomme avec un symbole unique, et on les met au top-level.

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'un case mais où chaque test est un motif qui doit correspondre avec l'expression donnée au début du cond-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 le match-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 fonction format pour être autonomes.
  • Implémentation de loop avec toutes les extensions sauf celles de typage (analysées mais ignorées silencieusement)