Sauvegarder/charger l'environnement, écrire une bibliothèque fonctionnelle en LISP (prolog.lisp), appeler du C depuis LISP, et appeler LISP depuis le C.
Le cours 5 nous a donné let.c : un interprète LISP complet
avec environnements chaînés, closures, cellules mutables, objets par
passage de messages, listes et transpilation Python.
main() charge prolog.lisp au démarrage et
enregistre quelques fonctions natives.
Mais il manque encore des briques essentielles pour un langage pratique :
save & load
Les formes spéciales (save "fichier") et
(load "fichier") permettent respectivement de
sérialiser l'environnement courant sur le disque et de
recharger un fichier de code LISP.
save — sérialiser l'environnement
La fonction file_save (let.c:730) parcourt la chaîne
d'environnements de la portée globale à la portée locale et écrit
chaque liaison sous forme d'expression LISP lisible :
;; si l'env contient : [x 42] → [y (cell 10)] → [fact (fun ...)] ;; save génère : (let x 42) (let y (cell 10)) (function fact (n) (if (eq? n 1) 1 (* n (fact (- n 1)))))
static void file_save(const char *filename, Env *env) { FILE *f = fopen(filename, "w"); /* collecte tous les niveaux */ Env *frames[1024]; int nf = 0; for (Env *e = env; e && nf < 1024; e = e->parent) frames[nf++] = e; /* écrit du plus externe au plus interne */ for (int i = nf - 1; i >= 0; i--) { ... } fclose(f); }
Le fichier produit est du LISP pur et dur : chaque variable est
écrite avec let, chaque fermeture avec
function, chaque cellule avec cell.
Le résultat est à la fois exécutable et lisible par un humain.
load — lire et exécuter un fichier
La fonction file_load (let.c:647) lit un fichier texte,
le découpe en S-expressions équilibrées en gérant les parenthèses
et les commentaires ;, puis les évalue une par une :
static Sexp *file_load(const char *filename, Env **env) { FILE *f = fopen(filename, "r"); /* lit tout le fichier en mémoire */ fseek(f, 0, SEEK_END); long fsize = ftell(f); fseek(f, 0, SEEK_SET); char *buf = malloc((size_t)fsize + 2); fread(buf, 1, (size_t)fsize, f); buf[n] = '\n'; buf[n + 1] = '\0'; fclose(f); /* découpage en sexps équilibrées */ while (buf[i]) { /* saute blancs et commentaires */ /* repère une sexp ( ... ) ou un atome */ Sexp *expr = sexp_parse(buf + start); result = evaluer_env(expr, env); sexp_free(expr); } return result; }
file_load ne se contente pas
de parser — il exécute chaque expression au fur et à mesure.
Cela signifie qu'un fichier LISP peut contenir des définitions de
fonctions, des calculs, des affichages… comme un script shell.
$ ./let '(let x 42)' '(let y (cell 10))' '(save "session.lisp")'
; → 1
$ cat session.lisp
(let x 42)
(let y (cell 10))
$ ./let '(load "session.lisp")' '(print x)' '(print (cell-ref y))'
-> 42
-> 10
function sont sérialisées correctement. Le
snapshot d'environnement n'est pas conservé — au
rechargement, la closure sera recréée dans l'environnement courant.
C'est un choix délibéré (simplicité > fidélité).
save et load, c'est votre sauvegarde de
partie dans un jeu vidéo. Sauf que le jeu, c'est votre environnement
de programmation, et que le « Game Over » c'est un
segfault. Sauvegardez souvent.
prolog.lisp — la bibliothèque standard
Au démarrage, let charge automatiquement
prolog.lisp s'il existe (let.c:844-847). Ce fichier
est notre bibliothèque standard : un ensemble de
fonctions écrites en LISP qui étendent le langage.
main(){
FILE *f = fopen("prolog.lisp", "r");
if (f) { fclose(f);
file_load("prolog.lisp", &env); }
}
Comme un #include <stdlib.h> en C, ou comme
le noyau d'un OS qui fournit des appels système. prolog.lisp
est la première chose chargée — une API de base pour tout programme
LISP qui suit.
;; fact — version flottante pour grands nombres (function fact (n) (if (< n 2) 1.0 (* n (fact (- n 1))))) ;; puissance : x^n (function puissance (x n) (if (eq? n 0) 1 (* x (puissance x (- n 1)))))
;; cos(x) par série de Taylor (10 termes) (function cos-iter (x k n sign) (if (> k n) 0 (+ (* sign (/ (puissance x (* 2 k)) (fact (* 2 k)))) (cos-iter x (+ k 1) n (* -1 sign))))) (function cos (x) (cos-iter x 0 10 1))
double, pas de
bibliothèque math — juste des S-expressions et de la récursion.
;; range : [start, stop] avec un pas (function range (start step stop) (if (> start stop) (quote ()) (cons start (range (+ start step) step stop)))) ;; iota : [0, 1, ..., n-1] (function iota (n) (range 0 1 (- n 1))) ;; repeat : n copies de x (function repeat (x n) (if (eq? n 0) (quote ()) (cons x (repeat x (- n 1)))))
;; longueur d'une liste (function length (xs) (if (null? xs) 0 (+ 1 (length (cdr xs))))) ;; take : les n premiers (function take (n xs) (if (or (eq? n 0) (null? xs)) (quote ()) (cons (car xs) (take (- n 1) (cdr xs))))) ;; drop : enlève les n premiers (function drop (n xs) (if (or (eq? n 0) (null? xs)) xs (drop (- n 1) (cdr xs)))) ;; reverse : inversion (function reverse (xs) (if (null? xs) (quote ()) (append (reverse (cdr xs)) (list (car xs))))) ;; append : concaténation (function append (xs ys) (if (null? xs) ys (cons (car xs) (append (cdr xs) ys))))
;; map : applique f à chaque élément (function map (f vs) (if (null? vs) (quote ()) (cons (f (car vs)) (map f (cdr vs))))) ;; filter : garde les éléments qui satisfont pred (function filter (pred xs) (if (null? xs) (quote ()) (if (pred (car xs)) (cons (car xs) (filter pred (cdr xs))) (filter pred (cdr xs))))) ;; zip : combine deux listes en paires (function zip (vs ws) (if (or (null? vs) (null? ws)) (quote ()) (cons (list (car vs) (car ws)) (zip (cdr vs) (cdr ws))))) ;; foldl : réduction gauche (function foldl (f init xs) (if (null? xs) init (foldl f (f init (car xs)) (cdr xs)))) ;; foldr : réduction droite (function foldr (f init xs) (if (null? xs) init (f (car xs) (foldr f init (cdr xs))))) ;; sum / product : raccourcis (function sum (xs) (foldl + 0 xs)) (function product (xs) (foldl * 1 xs))
;; compose : (f ∘ g)(x) = f(g(x)) (function compose (f g) (lambda (x) (f (g x)))) ;; deriv : dérivée numérique (function deriv (f h) (lambda (x) (/ (- (f (+ x h)) (f (- x h))) (* 2 h)))) ;; prédicats : any / all (function any (pred xs) (if (null? xs) 0 (if (pred (car xs)) 1 (any pred (cdr xs))))) (function all (pred xs) (if (null? xs) 1 (if (pred (car xs)) (all pred (cdr xs)) 0)))
init.lisp ou un
.bashrc. Il définit les primitives de base sur
lesquelles tout le reste s'appuie. C'est exactement le modèle
des systèmes d'exploitation : un micro-noyau (l'interprète)
et des services système (prolog.lisp).
prolog.lisp, c'est comme le dossier /etc
de votre OS LISP : au démarrage, tout est chargé, tout est prêt.
Sauf qu'au lieu de configurer des démons, on définit
map, filter et foldl.
C'est plus propre, et ça consomme moins de RAM.
Parfois, le LISP pur ne suffit pas : calculs trigonométriques, nombres aléatoires, opérations système… Pour ces cas, on a besoin d'appeler du C depuis LISP. C'est le rôle des fonctions natives.
SEXPR_NATIVE
La bibliothèque sexpression.h définit un nouveau type :
enum { SEXPR_INT, SEXPR_SYM, SEXPR_CONS, SEXPR_CELL, SEXPR_FLOAT, SEXPR_NATIVE }; typedef struct Sexp { int type; /* ... */ void *native_fn; /* SEXPR_NATIVE */ } Sexp;
Une fonction native est une fonction C qui a cette signature :
typedef Sexp *(*NativeFn)(Sexp *args, Env **env);
Elle reçoit les arguments (non évalués) et l'environnement courant, et retourne une S-expression. C'est aussi simple que ça.
La fonction register_native crée une S-expression
de type SEXPR_NATIVE et l'ajoute aux bindings :
static void register_native(Env *env, const char *name, NativeFn fn) { env->bindings = sexp_cons( sexp_cons(sexp_sym(name), sexp_cons( sexp_native(name, (void*)fn), NULL)), env->bindings); }
sinstatic Sexp *native_sin(Sexp *args, Env **env) { if (!args || args->cdr) { fprintf(stderr, "ERREUR: sin attend " "1 argument\n"); exit(1); } Sexp *v = evaluer_env( args->car, env); double x = (v->type == SEXPR_FLOAT) ? v->reel : (double)v->entier; sexp_free(v); return sexp_float(sin(x)); }
sqrtstatic Sexp *native_sqrt(Sexp *args, Env **env) { if (!args || args->cdr) { fprintf(stderr, "ERREUR: sqrt attend " "1 argument\n"); exit(1); } Sexp *v = evaluer_env( args->car, env); double x = (v->type == SEXPR_FLOAT) ? v->reel : (double)v->entier; sexp_free(v); return sexp_float(sqrt(x)); }
randomstatic Sexp *native_random(Sexp *args, Env **env) { (void)args; (void)env; return sexp_float( (double)rand() / (double)RAND_MAX); }
main()int main(int argc, char *argv[]) { Env *env = env_new(NULL); /* enregistre les fonctions natives */ register_native(env, "random", native_random); register_native(env, "sqrt", native_sqrt); register_native(env, "sin", native_sin); /* charge prolog.lisp s'il existe */ FILE *f = fopen("prolog.lisp", "r"); if (f) { fclose(f); file_load("prolog.lisp", &env); } ... }
| Native | Signature LISP | Implémentation C | Notes |
|---|---|---|---|
random | (random) | rand() / RAND_MAX | Retourne un float [0, 1) |
sqrt | (sqrt x) | sqrt(x) (math.h) | Racine carrée, toujours float |
sin | (sin x) | sin(x) (math.h) | Sinus, toujours float |
Quand l'évaluateur rencontre un symbole qui correspond à une
fonction native, il appelle fn->native_fn(args, env) :
/* dans evaluer_env */ Sexp *fn = env_lookup(*env, op); if (fn != (Sexp*)-1 && fn != NULL) { if (is_closure(fn)) return apply_closure(fn, args, env); if (is_native(fn)) return ((NativeFn)fn->native_fn)(args, env); }
is_closure / is_native).
C'est la beauté du polymorphisme de type en C avec une union
(ou plutôt un void*).
sin
comme si c'était une fonction LISP — mais derrière, c'est du C
pur, rapide, sans récursion. Le meilleur des deux mondes,
comme un croissant fourré au Nutella.
La réciproque existe aussi : lorsque les traitements s'expriment plus simplement en LISP, on peut embarquer l'interprète dans un programme C. C'est l'embedding.
tabulator.c — un tabulateur en C + LISP
Le fichier tabulator.c (48 lignes) montre comment
un programme C peut déléguer un calcul à l'interprète LISP
via popen :
int main(int argc, char *argv[]) { /* Construit une commande let avec range + map */ int n = snprintf(cmd, sizeof(cmd), "./let '(map (lambda (x) (cons x %s))" " (range %s %s %s))'", argv[1], argv[2], argv[3], argv[4]); /* Exécute let et lit le résultat */ FILE *fp = popen(cmd, "r"); /* Extrait les lignes "→ ..." */ while (fgets(buf, sizeof(buf), fp)) { char *p = strstr(buf, " -> "); if (p) fputs(p + 4, stdout); } ... }
$ ./tabulator '(+ (* x x) (* 3 x))' -2 0.5 2 (-2 . -2) (-1.5 . -2.25) (-1 . -2) (-0.5 . -1.25) (0 . 0) (0.5 . 0.75) (1 . 4) (1.5 . 6.75) (2 . 10)
range
et map./let via popen(range -2 0.5 2)(lambda (x) (cons x (+ (* x x) (* 3 x))))((x . y) …)
Une autre approche (plus élégante) consiste à lier statiquement
l'interprète LISP dans le programme C, en utilisant directement
les fonctions de sexpression.h/.c :
#include "sexpression.h" int main() { // Construire une expression LISP en C Sexp *expr = sexp_parse("(+ 1 2)"); // L'évaluer avec evaluer (sans env) Sexp *res = evaluer(expr, NULL); sexp_print(res); // → 3 sexp_free(res); sexp_free(expr); return 0; }
Cette approche est utilisée par plusieurs programmes du cours :
evalenv.c, mapk.c, filterk.c,
table.c, etc. Tous lient sexpression.o
et manipulent les S-expressions directement depuis le C.
| Fichier | Utilisation de LISP depuis C |
|---|---|
tabulator.c | Appelle ./let via popen |
evalenv.c | Lie sexpression.o, utilise evaluer |
mapk.c | Crée des S-expressions en C, appelle evaluer |
filterk.c | Filtre une liste avec un prédicat LISP |
table.c | Zip + eval pour filtrer une table |
project.c | Projection d'expressions LISP |
map, filter, combinateurs)popen
crée un sous-processus à chaque appel. C'est lent. Pour des appels
fréquents, préférez la liaison directe via sexpression.o.
Le tabulator s'en sort parce qu'il ne fait qu'un seul
appel — mais pour une boucle chaude, ce serait désastreux.
Avec ces quatre briques (persistance, bibliothèque, FFI, embedding), l'architecture de notre environnement LISP forme une pile cohérente :
./let, définissez quelques variables avec let
et function, puis (save "test.lisp").
Quittez, relancez, faites (load "test.lisp") et vérifiez
que les variables existent toujours.
(map (lambda (x) (* x x)) (iota 10)). Que retourne
(foldl + 0 (iota 100)) ? Et (cos 3.14159) ?
(random) plusieurs fois. Calculez
(sqrt 144) et (sin 0).
Que se passe-t-il avec (sqrt -1) ?
deriv de prolog.lisp pour calculer la dérivée
de (lambda (x) (* x x)) en x = 3. La valeur exacte
est 6. L'approximation avec h = 0.001 donne quoi ?
./let (sans tabulator), générez une table
de valeurs pour sin(x) sur [0, π] avec un pas de 0.1.
Utilisez range et map.
cos à let.c (déjà dans
math.h). Enregistrez-la dans main() et comparez ses
résultats avec la version LISP de prolog.lisp. Laquelle est la
plus précise ? Laquelle est la plus rapide ?
(integral f a b n) qui calcule
l'intégrale de f sur [a, b] avec n rectangles
(méthode des rectangles). Utilisez range,
map et sum. Testez avec
(integral (lambda (x) (* x x)) 0 1 1000)
(≈ 0.333).
let.c pour que le REPL sauvegarde
automatiquement l'environnement dans .let_session
à chaque let ou function, et le
recharge au démarrage. Gère le cas où le fichier n'existe pas.
(define-native nom args body) qui
permet de définir une fonction native à l'exécution (sans
recompiler). Le corps serait une chaîne C compilée avec
libtcc ou similaire. (Difficile — nécessite
un compilateur JIT.)
Ce cours marque un tournant : on passe de la construction d'un langage à l'industrialisation de l'environnement. Voici des pistes pour continuer :
floor,
ceil, abs, exp,
log, cos, tan,
atan2, system (appel shell)file_save : sérialisez
aussi les snapshots d'environnement des closures (collection de
garbage collector)SEXPR_VECTOR avec accès O(1)libreadline)calc.c le fait, mais pour le langage complet
du cours 5./let '(save "f.lisp")' ·
./let '(load "f.lisp")' ·
./tabulator '(+ x x)' 0 1 10