Environnements, fermetures lexicales, cellules mutables, objets par passage de messages, listes, et transpilation LISP → Python.
Depuis le cours 3, la bibliothèque sexpression.h/.c
permet de parser et évaluer des
S-expressions. La fonction evaluer gère les opérateurs
arithmétiques, les listes, et les formes spéciales de base.
/* Ce qui fonctionne avec evaluer() de la bibliothèque */ (+ 1 2) → 3 (car (quote (a b))) → a (if 1 2 3) → 2 /* Ce qui ne fonctionne PAS : */ (let x 42) ; pas de variable (+ x 1) ; x n'existe pas (function f (n) ...) ; pas de fonction
Problème : evaluer ne garde aucune trace
des évaluations précédentes. Chaque appel repart de zéro. Pour aller
plus loin, il faut un environnement : une structure
qui retient les associations nom → valeur.
evaluer sans environnement, c'est comme une
calculatrice qui oublie le résultat dès que vous appuyez sur « = ».
Utile, mais frustrant. On va lui ajouter une mémoire.
let.c
Le fichier let.c (environ 580 lignes) étend la
bibliothèque Sexp avec des environnements chaînés, des closures,
des cellules mutables, et des opérations sur listes.
Contrairement à calc.c qui compile vers la VM,
let.c est un interprète complet :
il évalue les expressions directement dans une boucle REPL ou en
ligne de commande.
| Fichier | Rôle | Lignes |
|---|---|---|
sexpression.h | Types Sexp, Cell ; constructeurs ; parseur | 33 |
sexpression.c | Implémentation : parse, zip, cherche, print, free, copy | 313 |
let.c | Évaluateur avec envs, closures, cellules, listes | ~580 |
transpile.c | Transpileur LISP → Python | ~360 |
Un environnement est une table d'association
(symbole . valeur) organisée en chaîne. Chaque nouveau
let empile une nouvelle portée. La recherche remonte
la chaîne jusqu'à trouver le symbole.
Envtypedef struct Env Env; struct Env { Sexp *bindings; /* alist : ((sym val) …) */ Env *parent; /* portée englobante */ }; static Env *env_new(Env *parent) { Env *e = malloc(sizeof(Env)); e->bindings = NULL; e->parent = parent; return e; }
/* Exemple de chaîne : */
[2] ((x 10) (y 20))
↑ parent
[1] ((z 100))
↑ parent
[0] () ← globale
env_lookupParcourt la chaîne d'environnements en remontant les parents. Retourne la première liaison trouvée (résolution lexicale).
static Sexp *env_lookup(Env *env, Sexp *key) { while (env) { Sexp *v = cherche(key, env->bindings); if (v) return v; /* trouvé dans cette portée */ env = env->parent; /* remonte d'un niveau */ } return NULL; /* pas trouvé → erreur */ }
La fonction cherche de la bibliothèque parcourt
une liste d'association :
Sexp *cherche(Sexp *key, Sexp *alist) { while (alist) { if (key->entier == alist->car->car->entier) return alist->car->cdr->car; alist = alist->cdr; } return NULL; }
let — lier une variable
(let x 42) crée une nouvelle portée
avec la liaison x = 42 et retourne la valeur.
Le nouvel environnement devient l'environnement courant pour les
expressions suivantes.
if (strcmp(name, "let") == 0) { Sexp *sym = args->car; Sexp *val = evaluer_env( args->cdr->car, env); Env *local = env_new(*env); local->bindings = sexp_cons( sexp_cons( sexp_copie(sym), sexp_cons(stored, NULL)), local->bindings); *env = local; return sexp_copie(stored); }
expr dans l'env courant(sym . val) dans le nouvel env$ ./let '(let x 42)' '(+ x 1)' '(let y (* x 2))' ; État initial : env: [0] () ; (let x 42) → 42 | env: [0] (x 42) parent: [1] () ; (+ x 1) → 43 | env inchangé ; (let y (* x 2)) → 84 | env: [0] (y 84) parent: [1] (x 42) parent: [2] ()
Chaque let empile une portée. L'environnement est
une pile de dictionnaires. La recherche commence toujours par
la portée la plus récente.
let ne prend que
deux arguments : (let symbole expression).
Ce n'est pas le let de Scheme qui permet d'en lier
plusieurs à la fois. Si vous voulez lier plusieurs variables,
imbriquez les let ou utilisez begin.
set! — muter une liaison
(set! x 43) modifie la valeur de la liaison existante
la plus proche dans la chaîne d'environnements.
set! a deux comportements selon le type de la valeur
stockée :
x a été trouvé.
;; mutation d'une liaison simple (let x 42) ; x → 42 (set! x 100) ; x → 100 (remplace dans l'alist)
Cell est modifiée in-situ.
Toutes les références à cette cellule voient la modification.
;; mutation d'une cellule (let c (cell 10)) ; c → #<cell:10> (set! c 42) ; cell:10 → cell:42 (cell-ref c) ; → 42
for (Env *e = *env; e; e = e->parent) { Sexp *v = cherche(sym, e->bindings); if (v) { if (v->type == SEXPR_CELL) { v->cell->value = new_val->entier; sexp_free(new_val); } else { /* remplace dans l'alist */ } return ...; } } /* si on arrive ici : x n'existe dans aucun env */ fprintf(stderr, "ERREUR: set! symbole inconnu\n");
set! ne crée pas de liaison.
Si x n'existe dans aucun environnement de la chaîne,
c'est une erreur. C'est délibéré : cela évite les bugs silencieux
où une faute de frappe créerait une nouvelle variable au lieu de
muter une existante.
Une closure est une fonction accompagnée de l'environnement dans lequel elle a été créée. C'est le mécanisme qui permet de « capturer » des variables.
Dans let.c, une closure est représentée par une
liste S-expression :
(fun (params) corps snapshot_env)
Avec les accesseurs suivants :
static int is_closure(Sexp *v) { return v && v->type == SEXPR_CONS && v->car->type == SEXPR_SYM && strcmp(v->car->symbole, "fun") == 0; } static Sexp *closure_params(Sexp *c) { return c->cdr->car; } static Sexp *closure_body(Sexp *c) { return c->cdr->cdr->car; } static Sexp *closure_env(Sexp *c) { return c->cdr->cdr->cdr->car; }
Quand on crée une closure, on fige la chaîne d'environnements. Le snapshot est une copie profonde des bindings, stockée dans la closure.
static Sexp *env_snapshot(Env *env) { if (!env) return NULL; return sexp_cons( sexp_copie(env->bindings), env_snapshot(env->parent)); }
static Env *env_from_snapshot(Sexp *s) { if (!s) return NULL; Env *e = env_new(env_from_snapshot(s->cdr)); e->bindings = sexp_copie(s->car); return e; }
sexp_copie pour un
SEXPR_CELL ne crée pas une nouvelle cellule, il
partage la même en incrémentant le compteur de références.
Ainsi, une cellule mutable capturée dans une closure reste
partagée avec l'environnement d'origine. C'est ce qui permet
les objets par passage de messages.
L'application d'une closure se fait en trois étapes :
zip/* extrait de evaluer_env — application de closure */ if (is_closure(op)) { Sexp *params = closure_params(op); Sexp *body = closure_body(op); Env *saved = env_from_snapshot(closure_env(op)); Env *local = env_new(saved); /* nouvelle portée */ local->bindings = zip(params, args); return evaluer_env(body, &local); }
lambda & functionlambda — fonction anonyme
(lambda (x) (+ x 1)) crée une closure anonyme
et la retourne. Elle peut être appelée immédiatement ou stockée dans
une variable.
;; appel immédiat d'une lambda $ ./let '((lambda (x) (* x 2)) 21)' → 42 ;; stocker une lambda dans une variable $ ./let '(let double (lambda (x) (* x 2)))' '(double 21)' → 42
function — fonction nommée
(function f (x) (+ x 1)) crée une closure et
l'enregistre dans l'environnement courant sous le nom
f. Le corps peut faire référence à f
pour la récursion.
if (strcmp(name, "function") == 0) { Sexp *sym = args->car; Sexp *params = args->cdr->car; Sexp *body = args->cdr->cdr->car; Sexp *clos = make_closure(params, body, *env); /* enregistre dans l'env courant */ (*env)->bindings = sexp_cons( sexp_cons(sym, sexp_cons(clos, NULL)), (*env)->bindings); return sexp_copie(clos); }
$ ./let '(function fact (n)
(if (eq? n 1) 1
(* n (fact (- n 1)))))'
'(print (fact 5))'
120
La récursion fonctionne car fact est dans
l'environnement quand le corps de la fonction s'exécute.
Au moment de l'appel, on a reconstruit l'environnement depuis
le snapshot, qui contient déjà la liaison fact.
make_closure, puis ajoutée à l'env. Pendant la
création de la closure, fact n'est pas encore dans
l'env — mais ce n'est pas un problème car le snapshot est pris
après.
Une cellule est une boîte mutable sur le tas. Elle permet de partager une valeur modifiable entre plusieurs environnements (ou plusieurs closures).
SEXPR_CELLtypedef struct Cell { int value; /* valeur stockée */ int refs; /* compteur de références */ } Cell; Cell *cell_new(int v) { Cell *c = malloc(sizeof(Cell)); c->value = v; c->refs = 1; return c; }
/* sexp_copie pour SEXPR_CELL : partage la même Cell, incrémente refs */ if (s->type == SEXPR_CELL && s->cell) { s->cell->refs++; return sexp_cell(s->cell); } /* sexp_free pour SEXPR_CELL : décrémente refs, libère si 0 */ if (s->type == SEXPR_CELL && s->cell && --s->cell->refs == 0) free(s->cell);
;; créer une cellule (cell 42) → #<cell:42> ;; lire la valeur (cell-ref c) → 42 ;; muter avec set! (set! c 100) ; la cellule contient maintenant 100
env_snapshot, les deux closures
partagent la même Cell. Une mutation
via l'une est immédiatement visible par l'autre. C'est exactement
ce dont on a besoin pour les objets.
Une closure + une cellule mutable
= un objet. Le message est passé comme argument (un symbole)
que la closure interprète avec eq?.
(function make-compteur (init) (let c (cell init)) (lambda (msg) (if (eq? msg 'inc) (begin (set! c (+ (cell-ref c) 1)) (cell-ref c)) (if (eq? msg 'value) (cell-ref c) (if (eq? msg 'reset) (begin (set! c 0) (cell-ref c)) 0)))))
$ ./let '(let c (make-compteur 100))' '(c 'inc)' '(c 'inc)' '(c 'value)' 101 102 102
$ ./let '(let a (make-compteur 0))' '(let b (make-compteur 50))' '(a 'inc)' '(b 'inc)' '(a 'inc)' '(b 'inc)' 1 51 2 52
make-compteur est une fonction qui crée une cellule
c et retourne une closure qui « voit » c'inc, 'value,
'reset) et agit en conséquencemake-compteur crée une nouvelle
cellule → chaque compteur a son état proprethis, pas de classe, pas d'héritage — juste
une fermeture lexicale et une cellule mutable(function make-bank (initial) (let solde (cell initial)) (let historique (cell (list))) (lambda (msg mt) (if (eq? msg 'deposit) (begin (set! solde (+ (cell-ref solde) mt)) (cell-ref solde)) (if (eq? msg 'withdraw) (if (>= (cell-ref solde) mt) (begin (set! solde (- (cell-ref solde) mt)) (cell-ref solde)) 0) ; refusé (if (eq? msg 'balance) (cell-ref solde) 0)))))
Ici, le message peut porter un paramètre (mt pour
le montant). La condition (>= (cell-ref solde) mt)
implémente la vérification de solde suffisant.
list car cdr cons null?
Les listes sont déjà représentées en interne par les
SEXPR_CONS de la bibliothèque. On ajoute simplement
des mots-clés au langage pour les construire et les décomposer.
| Forme | Comportement | Exemple | Résultat |
|---|---|---|---|
(list a b c …) | Construit une liste | (list 1 2 3) | (1 2 3) |
(car lst) | Premier élément | (car (list 10 20)) | 10 |
(cdr lst) | Reste de la liste | (cdr (list 1 2 3)) | (2 3) |
(cons a b) | Ajoute en tête | (cons 1 (list 2 3)) | (1 2 3) |
(null? x) | Test de liste vide | (null? (list)) | 1 |
car (extrait)if (strcmp(name, "car") == 0) { Sexp *v = evaluer_env(args->car, env); if (!v || v->type != SEXPR_CONS) { fprintf(stderr, "ERREUR: car attend une paire\n"); exit(1); } Sexp *r = sexp_copie(v->car); sexp_free(v); return r; }
Avec la récursion, on peut implémenter des algorithmes classiques :
;; longueur d'une liste (function len (lst) (if (null? lst) 0 (+ 1 (len (cdr lst))))) ;; map : appliquer f à chaque élément (function map (f lst) (if (null? lst) (list) (cons (f (car lst)) (map f (cdr lst))))) ;; filter : garder les éléments satisfaisant pred (function filter (pred lst) (if (null? lst) (list) (if (pred (car lst)) (cons (car lst) (filter pred (cdr lst))) (filter pred (cdr lst)))))
./let '(function len (lst) …)' '(len (list 10 20 30))'.
Combien de récursions sont nécessaires pour calculer la longueur
d'une liste de n éléments ?
Le langage est suffisamment complet pour être traduit vers Python.
Le fichier transpile.c (environ 360 lignes) implémente
un transpileur qui parcourt l'AST et émet du
code Python 3.
int main(int argc, char *argv[]) { for (int i = 1; i < argc; i++) { Sexp *s = sexp_parse(argv[i]); transpile(s, 0); /* 0 = context statement */ printf("\n"); sexp_free(s); } }
La fonction récursive transpile(s, in_expr) examine
le type de s :
- et ? remplacés par _)| LISP | Python |
|---|---|
(+ a b) | a + b |
(let x 42) | x = 42(x := 42) en expression |
(lambda (x) body) | lambda x: body |
(function f (x) body) | def f(x): return body |
(if c t e) | t if c else e |
(quote (a b)) | ['a', 'b'] |
(list a b c) | [a, b, c] |
(car lst) | lst[0] |
(cdr lst) | lst[1:] |
(cons a b) | [a] + b |
(null? x) | x == [] or x is None |
(display x) | print(x, end='') |
(print x) | print(x) |
(begin a b c) | a; b; return c (dans def)(a, b, c)[-1] (dans lambda) |
foo-bar? | foo_bar_ (sanitized) |
begin dans les fonctions
Le begin pose un problème : en Python, un
lambda ne peut contenir qu'une expression, pas une
séquence d'instructions. Le transpileur utilise deux stratégies :
function (def);; LISP (function f (x) (begin (display x) (* x 2))) ;; Python def f(x): print(x, end='') return x * 2
lambda;; LISP (lambda (x) (begin (display x) (* x 2))) ;; Python (tuple trick) lambda x: (print(x, end=''), x * 2)[-1]
(a, b, c)[-1] évalue
a, b, c en séquence, puis
retourne le dernier élément. C'est un moyen de simuler un bloc
dans une expression Python.
# Factorielle récursive $ ./transpile '(function fact (n) (if (eq? n 1) 1 (* n (fact (- n 1)))))' '(print (fact 5))' | python3 120 # Map et filter $ ./transpile '(function map (f lst) …)' \ '(function even (x) (eq? 0 (% x 2)))' \ '(print (filter even (list 1 2 3 4 5 6)))' | python3 [2, 4, 6] # Générer du Python et l'enregistrer $ ./transpile '(let x 42)' '(+ x 1)' > script.py $ python3 script.py
double-all qui prend une liste et retourne une liste
où chaque élément est multiplié par 2. Testez avec
(double-all (list 1 2 3)).
countdown qui prend n
et affiche les nombres de n à 0 avec
display, puis print "BOOM".
Astuce : le cas de base est n < 0.
let, lambda,
if, et list. Transpilez-le avec
./transpile … | python3 et vérifiez que le résultat
est identique à ./let ….
(function pair? (x) …) qui teste si x
est une paire. Utilisez-la avec filter pour extraire
les paires d'une liste mixte.
make-accumulator : (let a (make-accumulator 10))
(a 5) → 15 (a 3) → 18.
(function append (a b) …) qui concatène deux listes.
Testez : (append (list 1 2) (list 3 4)) → (1 2 3 4).
make-bank avec 'deposit,
'withdraw, 'balance.
'withdraw doit refuser si solde insuffisant
(retourner 0). Ajoutez 'history
qui retourne la liste des opérations sous forme de paires
(type . montant).
split (partager une liste en deux),
merge (fusionner deux listes triées), et
sort (récursif).
Le fichier let.c est le programme le plus complexe
du cours — environ 580 lignes de C qui implémentent un langage
de programmation complet. Prenez le temps de le lire et de le
comprendre. Voici quelques pistes pour aller plus loin :
map, filter,
fold comme primitives du langage
(pas juste des fonctions utilisateur) pour les rendre plus
efficacescalc.c le fait) pour votre nouveau langagedefine qui
permet de définir des variables globales sans imbrication./let et interagissez en mode interactiflet.c. Et souriez.
make let-run · make transpile-run ·
./let (REPL) ·
./transpile '(+ 1 2)' | python3