On lit du LISP, on l'évalue, on le compile en bytecode
Laurent Thiry — Programmation en C
« Les pointeurs, c'est comme les adresses : si vous perdez le papier,
vous ne retrouvez plus la maison. Ni le résultat de votre expression. »
On part d'une chaîne comme "(+ 2 (* 3 4))" et on arrive à :
calc.c (414 lignes) fait tout : parse, évalue, compile, REPL.
Un pointeur est une variable qui contient une adresse mémoire.
int x = 42; int *p = &x; // & = "adresse de" printf("%d", *p); // * = "valeur pointée" → 42 *p = 99; // x vaut maintenant 99
| Opérateur | Nom | Exemple |
|---|---|---|
&x | Adresse de x | int *p = &x; |
*p | Déréférencement | int v = *p; |
p->x | Accès membre via pointeur | n->valeur (= (*n).valeur) |
int *p = NULL;) ne pointe nulle part. Déréférencer NULL → crash immédiat. Toujours vérifier !Regrouper des variables de types différents dans une structure :
typedef struct Node { int type; // 0 = nombre, 1 = opérateur int valeur; // pour les nombres char op; // pour les opérateurs : + - * / struct Node *gauche; struct Node *droite; } Node;
typedef crée un alias : on écrit Node au lieu de struct Node.
Node *n = malloc(sizeof(Node)); n->type = 0; // -> = accès membre via pointeur n->valeur = 42;
char *nom à la structure. Quand faut-il allouer la chaîne ? Quand la libérer ?Tableau de char terminé par '\0' :
char s[] = "hello"; // s = ['h','e','l','l','o','\0'] strlen(s); // → 5 strcmp(a,b); // compare strcpy(d,s); // copie
Allouer sur le tas (heap) :
int *p = malloc(10 * sizeof(int)); if (!p) { /* échec */ } p[0] = 42; free(p); // libérer p = NULL; // bonne pratique
Une fonction qui s'appelle elle-même — avec une condition d'arrêt.
long long fact(int n) { if (n <= 1) return 1; return n * fact(n - 1); }
int evaluer(Node *n) { if (n->type == 0) return n->valeur; int vg = evaluer(n->gauche); int vd = evaluer(n->droite); // combiner vg et vd selon l'op }
| Récursivité | Itération (boucle) |
|---|---|
| Plus élégante (code = spé) | Plus économique (pas de pile d'appels) |
| Naturelle pour les arbres | Naturelle pour les tableaux |
| Risque de stack overflow | Pas de limite théorique |
Première étape : découper la chaîne en tokens (unités lexicales).
static char *pos; static char token[64]; void token_suivant(void) { while (*pos && isspace(*pos)) pos++; // skip espaces if (*pos == '(' || *pos == ')') { // parenthèse token[0] = *pos++; token[1] = '\0'; return; } if (strchr("+-*/", *pos)) { // opérateur token[0] = *pos++; token[1] = '\0'; return; } // nombre (avec chiffres) int i = 0; while (isdigit((unsigned char)*pos)) token[i++] = *pos++; token[i] = '\0'; }
( + 2 ( * 3 4 ) )Grammaire : expr → '(' op expr expr ')' | nombre
Node *parse_expr(void) { if (strcmp(token, "(") == 0) { // liste → opérateur token_suivant(); // consommer '(' char op = token[0]; // lire l'opérateur token_suivant(); // passer à l'opérande Node *g = parse_expr(); // enfant gauche Node *d = parse_expr(); // enfant droit token_suivant(); // consommer ')' return nouvel_operateur(op, g, d); } // atome → nombre int v = atoi(token); token_suivant(); return nouveau_nombre(v); }
Il s'appelle lui-même pour les sous-expressions → récursif descendant.
Pour (+ 2 (* 3 4)) :
| Token | Action | Résultat |
|---|---|---|
( | Début expression | ← opérateur |
+ | Opérateur | op = '+' |
2 | Nombre | gauche = 2 |
( | Début sous-expression | ← récursion |
* | Opérateur | op = '*' |
3 | Nombre | gauche = 3 |
4 | Nombre | droite = 4 |
) | Fin sous-expression | (* 3 4) |
) | Fin expression | (+ 2 (* 3 4)) ✓ |
2 + 2), le parseur affiche une erreur et exit. C'est sévère mais efficace.L'AST est la structure en mémoire de l'expression, avec des pointeurs gauche et droite.
'+'2'*'34Affichage indenté (depth-first) :
opérateur '+'
nombre 2
opérateur '*'
nombre 3
nombre 4
Parcours postfixé (post-order) : on évalue d'abord les enfants, puis le parent.
int evaluer(Node *n) { if (n->type == 0) // nombre return n->valeur; int vg = evaluer(n->gauche); // ← gauche d'abord int vd = evaluer(n->droite); // ← droite ensuite switch (n->op) { case '+': return vg + vd; case '*': return vg * vd; } }
evaluer('+')
→
evaluer(2)=2
→
evaluer('*')
→
evaluer(3)=3; evaluer(4)=4; 3*4=12
→
2+12=14
Même parcours postfixé, mais au lieu de calculer, on émet des instructions.
void compiler_expr(Bytecode *bc, Node *n) { if (n->type == 0) { emettre(bc, PUSH); emettre(bc, n->valeur); return; } compiler_expr(bc, n->gauche); compiler_expr(bc, n->droite); switch (n->op) { case '+': emettre(bc, ADD); break; case '*': emettre(bc, MUL); break; } }
PUSH 2 PUSH 3 PUSH 4 MUL ADD PRINT HALT
./calc -vm '(+ 2 (* 3 4))'
./calc -vm '(+ 2 (* 3 4))' -o prog.c
Le compilateur émet dans un tableau dynamique (Bytecode) :
typedef struct { int *code; int taille; int capacite; } Bytecode; void emettre(Bytecode *bc, int v) { if (bc->taille >= bc->capacite) { bc->capacite *= 2; bc->code = realloc(bc->code, bc->capacite * sizeof(int)); } bc->code[bc->taille++] = v; }
Le compilateur peut aussi écrire un fichier .C contenant le bytecode :
// Fichier généré par : ./calc -vm '(+ 2 (* 3 4))' -o out.c int bytecode[] = { PUSH, 2, PUSH, 3, PUSH, 4, MUL, ADD, PRINT, HALT }; int taille_bytecode = sizeof(bytecode) / sizeof(bytecode[0]);
On peut même #include le fichier dans vm.c. C'est de la génération de code : un programme qui écrit un autre programme.
Read → Eval → Print → Loop :
> (+ 2 (* 3 4)) => 14 > (* (+ 2 3) (- 5 2)) => 15 > quit Au revoir !
Le REPL utilise fgets pour lire l'entrée, parse_expr + evaluer pour le calcul :
void repl(void) { char ligne[1024]; printf("> "); while (fgets(ligne, sizeof(ligne), stdin)) { traiter_expression(ligne, 0, NULL); printf("> "); } }
calc.c accepte plusieurs modes via la ligne de commande :
| Commande | Action |
|---|---|
./calc | REPL interactif |
./calc "(+ 1 2)" | Évaluer et afficher le résultat |
./calc -vm "(+ 1 2)" | Compiler en bytecode VM (affiche le désassemblage) |
./calc -vm "expr" -o prog.c | Écrire le bytecode dans un fichier .C |
./calc -f entree.txt | Évaluer une expression depuis un fichier |
./calc -vm -f e -o f.c | Compilation depuis fichier vers fichier |
make calc-run pour le REPL, puis testez ./calc -vm '(* 2 (+ 3 4))' pour voir le bytecode.LISP (1958) est le 2e plus vieux langage. Sa grande idée : code et données ont la même forme.
| Concept | LISP | Notre C |
|---|---|---|
| Notation | (+ 2 (* 3 4)) | 2 + 3 * 4 |
| Structure | Listes chaînées (cons cells) | struct Node + pointeurs |
| Parsing | Intégré au langage | Parseur récursif manuel |
| Évaluation | Fonction eval | Fonction evaluer |
| Compilation | Fonction compile | Fonction compiler_expr |
| Concept | À retenir |
|---|---|
| Pointeur | Variable contenant une adresse mémoire (& pour l'obtenir, * pour la suivre) |
| Structure | Regroupement de variables de types différents |
| malloc/free | Allocation/libération sur le tas — ne pas oublier free |
| Chaîne C | Tableau de char terminé par '\0' |
| Récursivité | Fonction qui s'appelle elle-même (avec condition d'arrêt) |
| Tokeniseur | Texte → jetons (unités lexicales) |
| Parseur | Tokens → arbre (AST) |
| Évaluateur | Arbre → valeur (parcours postfixé) |
| Compilateur | Arbre → bytecode VM (parcours postfixé aussi) |
| REPL | Boucle interactive Read-Eval-Print |
L'évaluateur et le compilateur font le même parcours d'arbre. L'un calcule, l'autre génère du code. C'est la même recette, mais avec des ingrédients différents.
Tokeniseur → Parseur → AST → Évaluateur / Compilateur
make calc && make calc-run./calc -vm "(+ 2 3)"