TD 3

Des fermetures

à let.c — le langage complet

8 exercices pour ajouter cellules, objets, natives, bibliothèque et persistance

Laurent Thiry — Programmation en C

« Au début, on écrit printf("Hello World").
À la fin, on écrit son propre langage de programmation.
Entre les deux, il y a du café, des crashes, et des révélations. »

1 / 12

🗺️ Plan de la séance

  1. Cellules mutablescell, cell-ref, mutation
  2. Objets par messages — compteur, compte bancaire
  3. prolog.lisp — la bibliothèque standard
  4. Fonctions nativessqrt, random en C
  5. save / load — persistance
  6. Mini projet — devinez le nombre
  7. Aller plus loin — défis supplémentaires
  8. Sans filet — à vous de jouer
Aujourd'hui, on ajoute la mutabilité, la persistance, et le contact avec le monde extérieur (les fonctions C). Votre langage devient un vrai caméléon : il peut tout faire, mais il change de couleur selon l'humeur.
2 / 12

1. Cellules mutables ★★☆

Le type SEXPR_CELL encapsule un entier modifiable.

a) Création et lecture

  1. Utilisez sexp_cell pour créer une cellule
  2. Utilisez sexp_cell_ref pour lire sa valeur
  3. Testez : (cell-ref (cell 42)) → 42

b) Mutation

  1. Utilisez sexp_cell_set pour modifier
  2. Créez une cellule, modifiez-la, vérifiez la nouvelle valeur

c) swap !

  1. Écrivez une fonction (swap! a b) qui échange le contenu de deux cellules
  2. Testez : (let a (cell 1)) (let b (cell 2)) (swap! a b) (cell-ref a) → 2
🐍 Voir la solution
/* a) + b) cellules en C */
Sexp *cell = sexp_cell(42);
printf("%lld\n", sexp_cell_ref(cell));  /* 42 */
sexp_cell_set(cell, 100);
printf("%lld\n", sexp_cell_ref(cell));  /* 100 */

/* c) swap! en LISP via let.c */
;; (let swap!
;;   (lambda (a b)
;;     (let tmp (cell-ref a))
;;     (set-cell! a (cell-ref b))
;;     (set-cell! b tmp)))
💡 Cellule vs int : Une cellule est une boîte mutable. Un entier est immuable. (set! x 42) change ce que x désigne ; (set-cell! c 42) change le contenu de la boîte.
3 / 12

2. Objets par passage de messages ★★★

Un objet est une fermeture qui reçoit des messages et retourne des valeurs.

a) Compteur simple

  1. Créez (make-compteur) qui renvoie un objet compteur
  2. Messages : (compteur 'inc), (compteur 'dec), (compteur 'val)
  3. Le compteur utilise une cellule pour stocker sa valeur

b) Compte bancaire

  1. Créez (make-compte solde-initial)
  2. Messages : 'depot montant, 'retrait montant, 'solde
  3. Un retrait ne doit pas passer le solde en négatif

c) Historique des transactions

  1. Ajoutez un message 'historique au compte bancaire
  2. Stockez chaque transaction dans une liste
  3. (compte 'historique)((depot 100) (retrait 30) (depot 50))
🐍 Voir la solution
;; a) compteur
(let make-compteur
  (lambda ()
    (let compteur (cell 0))
    (lambda (msg)
      (if (eq? msg 'inc) (set-cell! compteur (+ (cell-ref compteur) 1)))
      (if (eq? msg 'dec) (set-cell! compteur (- (cell-ref compteur) 1)))
      (if (eq? msg 'val) (cell-ref compteur)))))

;; b) + c) compte bancaire avec historique
(let make-compte
  (lambda (solde-initial)
    (let solde (cell solde-initial))
    (let hist '())
    (lambda (msg . args)
      (if (eq? msg 'depot)
        (begin
          (set-cell! solde (+ (cell-ref solde) (car args)))
          (set! hist (cons (cons 'depot args) hist))))
      (if (eq? msg 'retrait)
        (if (>= (cell-ref solde) (car args))
          (begin
            (set-cell! solde (- (cell-ref solde) (car args)))
            (set! hist (cons (cons 'retrait args) hist)))
          (display "Solde insuffisant\n")))
      (if (eq? msg 'solde) (cell-ref solde))
      (if (eq? msg 'historique) hist))))
Les objets par messages, c'est exactement comme les hipsters : tout le monde en parle, personne ne sait vraiment ce que c'est, mais ça a l'air cool.
4 / 12

3. prolog.lisp — la bibliothèque standard ★☆☆

Découvrez le fichier prolog.lisp qui contient 21 fonctions prêtes à l'emploi.

a) Chargement et exploration

  1. Lancez ./let et tapez (load "prolog.lisp")
  2. Testez les fonctions : (map square '(1 2 3)), (filter odd? '(1 2 3 4))
  3. Utilisez range pour générer une liste : (range 1 10)

b) Fonction prime?

  1. Écrivez (prime? n) qui teste si n est premier
  2. Indice : un nombre est premier si aucun entier de 2 à sqrt(n) ne le divise
  3. Utilisez range, filter et length

c) Tous les nombres premiers

  1. Affichez la liste de tous les nombres premiers ≤ 100
  2. Utilisez filter avec votre fonction prime?
🐍 Voir la solution
;; a) chargement
(load "prolog.lisp")

;; b) prime?
(let prime?
  (lambda (n)
    (if (< n 2) 0
      (= 0 (length
        (filter (lambda (d) (= 0 (% n d)))
          (range 2 (sqrt n))))))))

;; c) tous les premiers ≤ 100
(filter prime? (range 2 100))
💡 Fonctions disponibles : map, filter, reduce, range, odd?, even?, square, cube, abs, max, min, length, reverse, append, nth, take, drop, member?, zip, flatten, range.
5 / 12

4. Fonctions natives C → LISP ★★★

Ajoutez des fonctions implémentées en C directement accessibles depuis LISP.

a) sqrt depuis C

  1. Implémentez native_sqrt en C : signature type
  2. Utilisez sexp_native pour l'enregistrer
  3. Testez : (sqrt 144) → 12.0

b) random

  1. Ajoutez native_random qui retourne un entier aléatoire entre 0 et n-1
  2. Utilisez rand() de la bibliothèque C

c) time et sleep

  1. Ajoutez (time) qui retourne l'horodatage Unix (secondes depuis 1970)
  2. Ajoutez (sleep n) qui suspend l'exécution pendant n secondes
  3. Utile pour créer des animations ou mesurer des performances
🐍 Voir la solution
/* a) sqrt native */
#include <math.h>
#include <time.h>
#include <unistd.h>

Sexp *native_sqrt(Sexp *args, void *env) {
    double v = (double)sexp_car(args)->entier;
    return sexp_float(sqrt(v));
}

/* b) random */
Sexp *native_random(Sexp *args, void *env) {
    int n = sexp_car(args)->entier;
    return sexp_int(rand() % n);
}

/* c) time + sleep */
Sexp *native_time(Sexp *args, void *env) {
    return sexp_int(time(NULL));
}
Sexp *native_sleep(Sexp *args, void *env) {
    sleep(sexp_car(args)->entier);
    return NULL;
}

/* enregistrement */
env_bind(env, sexp_sym("sqrt"),
         sexp_native(&native_sqrt));
env_bind(env, sexp_sym("random"),
         sexp_native(&native_random));
env_bind(env, sexp_sym("time"),
         sexp_native(&native_time));
env_bind(env, sexp_sym("sleep"),
         sexp_native(&native_sleep));
Les fonctions natives, c'est le super-pouvoir de votre langage : tout ce que vous pouvez faire en C, vous pouvez le faire en LISP. Sauf les pointeurs. Eux, ils restent en C. C'est mieux pour tout le monde.
6 / 12

5. save / load — persistance ★★☆

Ajoutez la capacité de sauvegarder et restaurer l'état de votre environnement.

a) Sauvegarde basique

  1. Créez (save "etat.lisp") qui écrit tous les bindings dans un fichier
  2. Formatez en LISP : (let x 42) par ligne

b) Chargement

  1. Créez (load "etat.lisp") qui lit et évalue chaque ligne
  2. Vérifiez que load préserve l'ordre des définitions

c) Auto-save

  1. Ajoutez un compteur de modifications
  2. Sauvegardez automatiquement dans .let-autosave.lisp toutes les 10 modifications
🐍 Voir la solution
/* a) save */
void native_save(Env *env, const char *path) {
    FILE *f = fopen(path, "w");
    for (Sexp *b = env->bindings; b; b = b->cdr) {
        Sexp *pair = b->car;
        fprintf(f, "(let %s %lld)\n",
                pair->car->symbole,
                pair->cdr->entier);
    }
    fclose(f);
}

/* b) load : évalue chaque ligne */
Sexp *native_load(Sexp *args, void *env) {
    FILE *f = fopen(sexp_car(args)->symbole, "r");
    if (!f) { perror("load"); return NULL; }
    char line[1024];
    while (fgets(line, sizeof(line), f)) {
        eval(sexp_parse(line), env);
    }
    fclose(f);
    return NULL;
}
⚠️ Attention : save écrit uniquement les valeurs entières. Si vous avez des fermetures ou des listes, il faudrait les sérialiser récursivement. Challenge accepté ?
7 / 12

6. Mini projet — devinez le nombre ★★★

Écrivez un jeu complet : l'ordinateur choisit un nombre, le joueur doit le deviner.

a) Version basique

  1. Utilisez (random 100) pour générer un nombre entre 0 et 99
  2. Boucle : lire l'entrée, comparer, répondre "trop grand" ou "trop petit"
  3. Terminer quand le joueur trouve le nombre

b) Compteur de tentatives

  1. Affichez le nombre de tentatives à la fin
  2. Ajoutez un message personnalisé : "Bravo !" (1-3 essais), "Pas mal" (4-7), "Enfin !" (8+)

c) Mode deux joueurs

  1. Le joueur 1 choisit un nombre, le joueur 2 doit le deviner
  2. Utilisez (display) et (read) pour les entrées/sorties
🐍 Voir la solution
;; a) + b) jeu basique avec compteur
(load "prolog.lisp")
(let mystere (random 100))
(let tentatives 0)
(display "Devinez le nombre (0-99)\n")
(let fini 0)
(while (not fini)
  (let essai (read))
  (set! tentatives (+ tentatives 1))
  (if (= essai mystere)
    (begin
      (display "Gagné !\n")
      (display (concat "Tentatives : " tentatives "\n"))
      (set! fini 1))
    (if (< essai mystere)
      (display "Trop petit\n")
      (display "Trop grand\n"))))

;; c) mode deux joueurs
(display "J1 : choisissez un nombre\n")
(let mystere (read))
(display "J2 : devinez\n")
...
Vous venez d'écrire un jeu complet dans votre propre langage. C'est exactement comme créer une calculatrice : inutile, mais terriblement satisfaisant. Et ça marche mieux que le jeu du snake sur Nokia 3310.
8 / 12

7. Aller plus loin ★★★

Défis supplémentaires pour enrichir votre langage. Solutions fournies.

📦 display / print

Ajoutez (display val) et (print val) pour afficher depuis LISP.

display affiche sans retour à la ligne, print avec.

🧵 and / or / not

Ajoutez les opérateurs logiques avec court-circuit.

(and (> 3 2) (< 5 10)) → vrai

📝 quote et eq?

(quote (1 2 3)) retourne la liste sans l'évaluer.

(eq? 'a 'a) compare des symboles.

🔢 Nombre d'or

Calculez le nombre d'or φ par itération : φ = 1 + 1/(1 + 1/(1 + ...))

Implémentez une fonction (phi n) avec n itérations.

🐍 Voir la solution
;; display / print (Côté C)
Sexp *native_display(Sexp *args, void *env) {
    sexp_print(sexp_car(args));
    return NULL;
}

;; and / or en C (court-circuit)
if (!strcmp(op, "and")) {
    Sexp *cur = args;
    while (cur) {
        if (!eval(cur->car, env)) return sexp_int(0);
        cur = cur->cdr;
    }
    return sexp_int(1);
}

;; nombre d'or en LISP
(let phi
  (lambda (n)
    (let f (lambda (acc k)
      (if (= k 0) acc
        (f (+ 1 (/ 1 acc)) (- k 1)))))
    (f 1 n)))
💡 Le nombre d'or : Plus il y a d'itérations, plus on s'approche de φ ≈ 1.618. Avec 20 itérations, la précision est déjà de l'ordre de 10⁻⁶.
9 / 12

8. Sans filet ★★★

⚠️ Pas de solution fournie. Ces exercices sont conçus pour vous faire voler de vos propres ailes. Regardez let.c si vous êtes bloqué.

🏷️ Système de types

Ajoutez une fonction (type-of val) qui retourne le type d'une valeur :

  • (type-of 42)'integer
  • (type-of 3.14)'float
  • (type-of '(1 2))'pair
  • (type-of (lambda (x) x))'closure

Indice

🎨 Pretty-printer

Écrivez une fonction qui affiche les S-expressions formatées avec indentation.

Exemple :

(let fib
  (lambda (n)
    (if (< n 2) n
      (+ (fib (- n 1))
         (fib (- n 2))))))

Chaque niveau d'imbrication ajoute 2 espaces.

✏️ Défi bonus : Implémentez la sérialisation JSON en LISP : (json->string '((a . 1) (b . (2 3))))'{"a":1,"b":[2,3]}'. Gérer les entiers, symboles (→ strings), listes (→ arrays) et paires (→ objects).
10 / 12

🗺️ De zéro à λ en C — le chemin parcouru

Ce que vous avez construit, du premier caractère au langage complet :

TD1
Hello World
Parseur
Évaluateur
TD2
Variables
Fermetures
Listes
TD3
Cellules
Objets
Natives
Persistance

📦 Les pièces du puzzle

parseur → sexp_parse env → chaînage closure → make_closure cell → mutation native → C→LISP save/load → fichier

🔮 Et après ?

Le cours 6 explore la suite : compilateur calc.c, transpileur LISP→Python, macros, continuation passing style…

Fichiers concernés :
tabulator.c, evalenv.c, table.c, mapk.c, filterk.c, project.c, calc.c, transpile.c

11 / 12

TD 3 — Terminé !

Votre langage est complet : cellules mutables, objets par messages,
bibliothèque standard, fonctions natives, persistance.

📄 Fichier clé
let.c — un interpréteur LISP complet en C
📚 Suite
Cours 6 — Compilation, transpilation, macros
🔑 Mots-clés
cellule, message, native, persistance, langage complet
« On a commencé par printf("Hello World"). On finit avec un langage de programmation. Entre les deux, il y a eu du café, des segfaults, et des révélations. »
« Le C, c'est comme un couteau suisse : ça coupe, ça visse, ça ouvre des boîtes. Et si on fait une bêtise, ça coupe aussi les doigts. Mais quel outil. »
12 / 12