Cours 5

S-expressions : environnements,

closures & objets en C

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. »

1

🗺️ Plan du cours

Aujourd'hui, votre parseur de parenthèses devient un langage de programmation. Comme un tournevis qui deviendrait un couteau suisse. Sauf que c'est vous qui tenez le manche.
2

1. S-expressions — rappel

Une S-expression (ou sexp) est une notation pour représenter des données arborescentes :

Syntaxe minimale

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)

Types C

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;
Les S-expressions, c'est la réponse des programmeurs LISP à la question « et si on remplaçait toute la syntaxe par des parenthèses ? ». La réponse était : « oui, et on va kiffer ». Ils n'avaient pas tort.
3

1.2 Les limites de l'évaluation simple

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
📄 Expression
🧮 evaluer()
✅ Résultat

Problème : chaque appel repart de zéro. Pas de mémoire, pas d'effet de bord.

4

2. Environnements chaînés

Un environnement est une table d'association symbole → valeur, organisée en chaîne de portées :

Structure C

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).

Chaîne d'envs

[2] ((x 10) (y 20))
↑ parent
[1] ((z 100))
↑ parent
[0] () ← globale

La recherche remonte la chaîne : on trouve x au niveau [2].

Les environnements chaînés, c'est comme les tiroirs de votre cuisine : vous regardez d'abord dans celui des couverts (portée locale), puis celui des ustensiles (parent), et si rien n'est trouvé, vous commandez une pizza (erreur).
5

2.1 Recherche dans la chaîne

env_lookup

static 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é */
}

Exemple

/* 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;
}
6

3. let — lier un nom

(let x 42) crée une nouvelle portée avec la liaison x = 42 et retourne la valeur.

Code

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);
}

Ce qui se passe

Avant : env global ()
↓ let x 42
Après : env [ (x 42) ← () ]

Le nouvel env devient le courant. Les expressions suivantes voient x.

Astuce : une copie (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.
7

3.1 Exécution pas à pas

$ ./let '(let x 42)' '(+ x 1)' '(let y (* x 2))'
État initial env: [0] ()
(let x 42)
État après let env: [0] (x 42) parent: [1] ()
(+ x 1)
Résultat : 43 env inchangé
(let y (* x 2))
Nouvelle liaison env: [0] (y 84) parent: [1] (x 42) parent: [2] ()
Chaque 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.
8

4. set! — la mutation

(set! x 43) modifie la liaison existante la plus proche dans la chaîne d'environnements.

Principe

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é */
}

Deux cas

Cas 1 — valeur ordinaire : la liaison est remplacée dans l'alist.
Cas 2 — cellule mutable : le contenu de la Cell est modifié in-situ. Toutes les références à cette cellule voient la modification.
⚠️ Attention : 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.
9

4.1 Exemple interactif

$ ./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 → 100
set! 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.
10

4.2 Cellules mutables — cell / cell-ref

Une cellule est une boîte mutable sur le tas, partageable entre plusieurs environnements :

Structure

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.

Compteur de références

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.

Avantage : deux closures peuvent partager la même cellule. Une mutation set! via la première closure est immédiatement visible par la seconde. C'est la base des objets.
11

5. Fermetures lexicales (closures)

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))
🧩 Paramètres (x)
+
📝 Corps (+ x 1)
+
📸 Snapshot d'env
🎁 Closure

Code C (détection)

static int is_closure(Sexp *v) {
    return v && v->type == SEXPR_CONS
        && v->car->type == SEXPR_SYM
        && strcmp(v->car->symbole, "fun") == 0;
}

Accès aux champs

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; }
Une closure, c'est comme un colis Amazon qui contient non seulement l'objet, mais aussi une photo de votre salon au moment de la commande. L'objet, c'est la fonction. La photo, c'est l'environnement.
12

5.1 Snapshot d'environnement

Quand on crée une closure, on fige la chaîne d'environnements :

env_snapshot

static 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_snapshot

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;
}
Conséquence clé : les cellules mutables sont partagées entre la closure et l'environnement courant. Une mutation via la closure modifie aussi ce que voit l'appelant. C'est exactement le comportement attendu pour les objets.
13

6. lambda — des fonctions anonymes

(lambda (x) (+ x 1)) crée une closure anonyme et la retourne :

Code

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 */
}

Application

$ ./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. »
14

6.1 function — nommer une closure

(function f (x) (+ x 1)) crée une closure et l'enregistre dans l'environnement sous le nom f :

Code

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);
}

Fonction récursive

$ ./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.

Attention à l'ordre : la closure est créée avant d'être liée à 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.
15

5.2 Exemple concret : compteur

$ ./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 c
↓ retourne une closure qui référence c
La closure incrémente et lit c à chaque appel
Magie : la cellule c 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.
16

7. Objets par passage de messages

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)))))

Utilisation

$ ./let '(let c (make-compteur 100))'
'(c 'inc)' '(c 'value)' '(c 'reset)'
101
101
0

Comment ça marche

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.

Les objets à la lambda, c'est la version hipster du développeur : « une classe ? Non merci, j'ai une closure et une cellule mutable. Et du café équitable. »
17

8. Listes : 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 :

FormeComportementExempleRé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)

Code pour car

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;
}

Test interactif

$ ./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
18

8.1 Manipuler des listes avec des fonctions

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.
19

9. Transpiler LISP → Python

Notre langage est suffisamment complet pour être transpilé en Python :

📄 S-expression
⚙️ transpile.c
🐍 Python
🚀 python3

Entrée LISP

(function fact (n)
  (if (eq? n 1) 1
    (* n (fact (- n 1)))))
(print (fact 5))

Sortie Python

def fact(n):
    return 1 if n == 1 else n * fact(n - 1)
print(fact(5))
Transformation : let x vx = v (ou (x := v) en expression), lambdalambda, functiondef, if → ternaire, list[...], car/cdr[0]/[1:], display/printprint(…, end='')/print(…).
20

9.1 Architecture du transpileur

Le transpileur est une fonction récursive transpile(Sexp *s, int in_expr) qui parcourt l'AST :

Boucle principale

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.

Quelques correspondances

LISPPython
(+ 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)
Le transpileur, c'est le traducteur automatique des langages : là où un interprète dit « je joue la partition », le transpileur dit « voilà la partition en Python, débrouillez-vous avec ». Étonnamment, ça marche.
21

9.2 Exemples d'utilisation

# 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]
Astuce : utilisez ./transpile '...' '...' > script.py && python3 script.py pour sauvegarder le code transpilé avant de l'exécuter.
22

Exercices

Niveau ★☆☆

✏️ Ex. 1 — double tout (★) : Écrivez une fonction 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)).
✏️ Ex. 2 — compte à rebours (★) : Écrivez une fonction récursive countdown qui prend n et affiche les nombres de n à 0 avec display, puis print de "BOOM".

Niveau ★★☆

✏️ Ex. 3 — pair? (★★) : Écrivez (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.
✏️ Ex. 4 — accumulateur (★★) : Implémentez make-accumulator : (let a (make-accumulator 10)) (a 5) → 15 (a 3) → 18. Inspirez-vous du compteur.

Niveau ★★★

✏️ Ex. 5 — bank object (★★★) : Créez 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.
✏️ Ex. 6 — append (★★★) : Implémentez (function append (a b) ...) qui concatène deux listes. Testez : (append (list 1 2) (list 3 4)) → (1 2 3 4).
23

Félicitations — cours 5 terminé !

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.

📄 Fichier clé
cours5-envs.html
📚 Concept du jour
Environnements chaînés + closures
🛠️ Commandes
make let-run · make transpile-run
Le C, c'est comme un kit de construction : on peut faire un tabouret (factorielle), une étagère (VM), ou une maison entière avec alarme et caméras (interprète LISP). Vous êtes maintenant architecte.
« Les parenthèses ne sont pas un fardeau — ce sont les poutres porteuses de votre édifice logiciel. »
24