De l'arbre syntaxique au langage de programmation
📖 Document complet du cours · 🎯 Référence rapide
Laurent Thiry — Programmation en C
« On commence par un parseur d'S-expressions,
et on finit par écrire un langage de programmation. »
let — lier des noms à des valeursset! — la mutation contrôléelambda & functionlist car cdr cons null?Une S-expression (ou sexp) est une notation pour représenter des données arborescentes :
42 ; entier symbole ; symbole (nom) (a b c) ; liste (paire chaînée) (a . b) ; paire impropre (quote (a b)) ; citation 'a ; sucre pour (quote a)
enum { SEXPR_INT, SEXPR_SYM, SEXPR_CONS, SEXPR_CELL }; typedef struct Sexp { int type; int entier; char *symbole; struct Sexp *car; struct Sexp *cdr; Cell *cell; } Sexp;
La fonction evaluer de la bibliothèque sexpression est un évaluateur pur : pas d'état, pas d'environnement mutable.
/* exemples traités par evaluer : */ (+ 1 2) → 3 (car (quote (a b c))) → a (if 1 2 3) → 2 /* impossibles avec evaluer : */ (let x 42) (+ x 1) ; pas de liaison persistante (set! x 43) ; pas de mutation (function f (n) ...) ; pas de définition de fonction
Problème : chaque appel repart de zéro. Pas de mémoire, pas d'effet de bord.
Un environnement est une table d'association symbole → valeur, organisée en chaîne de portées :
struct Env { Sexp *bindings; /* alist : ((x 42) (y 5)) */ Env *parent; /* portée englobante */ };
bindings est une liste d'association : chaque élément est une paire (symbole . valeur).
La recherche remonte la chaîne : on trouve x au niveau [2].
env_lookupstatic Sexp *env_lookup(Env *env, Sexp *key) { while (env) { Sexp *v = cherche(key, env->bindings); if (v) return v; env = env->parent; /* remonte */ } return NULL; /* pas trouvé */ }
/* bindings : ((x 42)) */ cherche(key, bindings) → parcourt l'alist /* si pas trouvé : */ env = env->parent /* remonte */
La fonction cherche de la bibliothèque parcourt la 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 un nom(let x 42) crée une nouvelle portée avec la liaison x = 42 et retourne la valeur.
if (strcmp(name, "let") == 0) { Sexp *sym = args->car; Sexp *val = evaluer_env(args->cdr->car, env); Env *local = env_new(*env); /* parent = courant */ local->bindings = sexp_cons( sexp_cons(sexp_copie(sym), sexp_cons(stored, NULL)), local->bindings); *env = local; /* swap env courant */ return sexp_copie(stored); }
Le nouvel env devient le courant. Les expressions suivantes voient x.
sexp_copie) de la valeur est stockée dans l'environnement. Ainsi, même si la valeur originale est libérée, la liaison reste valide.$ ./let '(let x 42)' '(+ x 1)' '(let y (* x 2))'
env: [0] ()(let x 42)
env: [0] (x 42) parent: [1] ()(+ x 1)
env inchangé(let y (* x 2))
env: [0] (y 84) parent: [1] (x 42) parent: [2] ()let empile une portée. Au bout de 10 let, votre environnement ressemble à une pile de crêpes — et il y a un parent qui contient du sirop.set! — la mutation(set! x 43) modifie la liaison existante la plus proche dans la chaîne d'environnements.
if (strcmp(name, "set!") == 0) { Sexp *sym = args->car; Sexp *new_val = evaluer_env( args->cdr->car, env); for (Env *e = *env; e; e = e->parent) { Sexp *v = cherche(sym, e->bindings); if (v) { /* si c'est une cellule : mutation */ if (v->type == SEXPR_CELL) { v->cell->value = new_val->entier; sexp_free(new_val); } else { /* sinon : remplace la valeur */ } return ...; } } /* erreur : pas trouvé */ }
Cell est modifié in-situ. Toutes les références à cette cellule voient la modification.set! ne crée pas de liaison. Si x n'existe pas, c'est une erreur. C'est comme essayer de repeindre un mur qui n'existe pas.$ ./let '(let x 42)' '(set! x 100)' '(print x)' 100 $ ./let '(let x (cell 10))' '(cell-ref x)' '(set! x 42)' '(cell-ref x)' 10 42
(let x 42)(set! x 100)x → 100set! en LISP, c'est la version programmeur du « je range mes papiers, mais tout refait surface ailleurs ». Sauf qu'ici, « ailleurs » = exactement la même variable.cell / cell-refUne cellule est une boîte mutable sur le tas, partageable entre plusieurs environnements :
typedef struct Cell { int value; /* la valeur stockée */ int refs; /* compteur de références */ } Cell;
SEXPR_CELL : nouveau type dans la bibliothèque sexpression. Le champ cell du Sexp pointe vers une Cell allouée sur le tas.
Cell *cell_new(int v) { Cell *c = malloc(sizeof(Cell)); c->value = v; c->refs = 1; /* premier propriétaire */ return c; }
Quand on copie un SEXPR_CELL, on incrémente refs (compteur de références partagé). Quand on libère, on décrémente. Si refs == 0, on libère la Cell.
set! via la première closure est immédiatement visible par la seconde. C'est la base des objets.Une closure est une fonction accompagnée de l'environnement dans lequel elle a été créée :
/* (function f (x) (+ x 1)) */ /* est représentée par : */ (fun (x) (+ x 1) (snapshot_env))
(x)(+ x 1)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 :
env_snapshotstatic Sexp *env_snapshot(Env *env) { if (!env) return NULL; return sexp_cons( sexp_copie(env->bindings), env_snapshot(env->parent)); }
sexp_copie copie récursivement les bindings. Pour les SEXPR_CELL, la copie est superficielle : on partage la même Cell en incrémentant le refcount.
env_from_snapshotstatic 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; }
lambda — des fonctions anonymes(lambda (x) (+ x 1)) crée une closure anonyme et la retourne :
if (strcmp(name, "lambda") == 0) { Sexp *params = args->car; Sexp *body = args->cdr->car; Sexp *clos = make_closure(params, body, *env); return clos; /* retourne la closure */ }
$ ./let '((lambda (x) (+ x 1)) 42)' → 43
La closure capturant l'environnement courant, on peut aussi l'utiliser comme valeur :
$ ./let '(let double (lambda (x) (* x 2)))' '(double 21)' → 42
lambda, c'est le « je crée une fonction mais je ne sais pas encore comment l'appeler » du programmeur fonctionnel. C'est aussi la réponse à « tu veux une fonction ? Tiens, prends-en une, là, tout de suite, sans la ranger. »function — nommer une closure(function f (x) (+ x 1)) crée une closure et l'enregistre dans l'environnement sous le nom f :
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 s'exécute.
fact. Si fact n'est pas utilisée dans son propre corps (appel récursif), ce n'est pas un problème. Mais si elle l'est, le snapshot d'env contient déjà fact car on l'ajoute juste après.$ ./let '(
let make-compteur
(lambda (init)
(let c (cell init))
(lambda () (begin
(set! c (+ (cell-ref c) 1))
(cell-ref c))))
)' '(let c1 (make-compteur 0))' '(c1)' '(c1)' '(c1)'
1
2
3
make-compteur crée une cellule cc
c à chaque appelc est partagée entre la closure et l'environnement via le snapshot. set! c mute la cellule. cell-ref c la lit. Deux compteurs créés avec make-compteur ont des cellules indépendantes.Une closure peut recevoir un message (un symbole) pour déclencher un comportement :
;; style objet : (objet 'message) → résultat (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 'value)' '(c 'reset)' 101 101 0
Le symbole 'inc est passé comme argument. La closure le compare avec eq?. Pas de mot-clé this, pas de classe, juste une fermeture sur une cellule mutable.
list car cdr cons null?Les listes sont déjà là — ce sont les SEXPR_CONS de la bibliothèque. On ajoute des mots-clés pour les manipuler :
| 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 1 2 3)) | 1 |
(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? (cdr (list 1))) | 1 (vrai) |
carif (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; }
$ ./let '(list 10 20 30)' → (10 20 30) $ ./let '(car (list 10 20 30))' → 10 $ ./let '(cdr (list 10 20 30))' → (20 30) $ ./let '(null? (list))' → 1
Avec function, if et cons/car/cdr, on peut écrire des algorithmes sur listes :
;; longueur d'une liste (function len (lst) (if (null? lst) 0 (+ 1 (len (cdr lst))))) ;; map : applique f à chaque élément (function map (f lst) (if (null? lst) (list) (cons (f (car lst)) (map f (cdr lst))))) ;; filter : garde 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)))))
map et filter, c'est la réponse du LISP à « j'ai une liste et je veux tout transformer d'un coup ». En C, on écrit une boucle. En LISP, on écrit une blague de trois lignes.Notre langage est suffisamment complet pour être transpilé en Python :
transpile.cpython3(function fact (n) (if (eq? n 1) 1 (* n (fact (- n 1))))) (print (fact 5))
def fact(n): return 1 if n == 1 else n * fact(n - 1) print(fact(5))
let x v → x = v (ou (x := v) en expression),
lambda → lambda,
function → def,
if → ternaire,
list → [...],
car/cdr → [0]/[1:],
display/print → print(…, end='')/print(…).
Le transpileur est une fonction récursive transpile(Sexp *s, int in_expr) qui parcourt l'AST :
int main(int argc, char *argv[]) { for (int i = 1; i < argc; i++) { Sexp *s = sexp_parse(argv[i]); transpile(s, 0); /* 0 = statement */ printf("\n"); sexp_free(s); } }
Chaque argument de la ligne de commande est une expression LISP, transpilée indépendamment.
| LISP | Python |
|---|---|
(+ a b) | a + b |
(if c t e) | t if c else e |
(lambda (x) b) | lambda x: b |
(begin a b c) | a; b; return c (def)(a, b, c)[-1] (expr) |
(quote (a b)) | ['a', 'b'] |
my-fun? | my_fun_ (sanitized) |
# Transpiler une factorielle et l'exécuter $ ./transpile '(function fact (n) (if (eq? n 1) 1 (* n (fact (- n 1)))))' '(print (fact 5))' | python3 120 # map + filter $ ./transpile '(function map (f l) (if (null? l) (list) (cons (f (car l)) (map f (cdr l)))))' '(function even (x) (eq? 0 (% x 2)))' '(print (map even (list 1 2 3 4 5)))' | python3 [False, True, False, True, False] # range décroissant $ ./transpile '(function range (n) (if (eq? n 0) (list) (cons n (range (- n 1)))))' '(print (range 5))' | python3 [5, 4, 3, 2, 1]
./transpile '...' '...' > script.py && python3 script.py pour sauvegarder le code transpilé avant de l'exécuter.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 de "BOOM".(function pair? (x) ...) qui teste si x est une paire (cons non-vide). 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. Inspirez-vous du compteur.make-bank avec les messages 'deposit, 'withdraw, 'balance. (withdraw doit refuser si solde insuffisant (retourner 0). Ajoutez un message 'history qui retourne la liste des opérations.(function append (a b) ...) qui concatène deux listes. Testez : (append (list 1 2) (list 3 4)) → (1 2 3 4).
Vous avez transformé un simple parseur parenthésé
en un langage fonctionnel avec closures, cellules mutables,
objets par passage de messages, listes… et un transpileur Python.
make let-run · make transpile-run