TD 1

Du Hello World

au parseur S-expression

9 exercices pour construire un évaluateur d'expressions arithmétiques

Laurent Thiry — Programmation en C

« Chaque expert a commencé par écrire Hello World.
Puis il a oublié un point-virgule. Et ça a marché quand même… enfin, non. »

1 / 12

🗺️ Plan de la séance

  1. Hello World — Écrire, compiler, exécuter
  2. Variables & boucles — Calculer n!
  3. Fonctions — Extraire factorielle
  4. Bibliothèque Sexp — Découvrir les S-expressions
  5. Parser — Lire une expression depuis la ligne de commande
  6. Évaluateur — Calculer (+ 2 (* 3 4))
  7. Bonus — REPL interactif
  8. Aller plus loin — Défis supplémentaires
  9. Sans filet — À vous de jouer
2 heures. 9 exercices. Un seul objectif : arriver à la machine à café en ayant un parseur qui marche. La machine à café, elle, n'aura pas de parseur. Normal.
2 / 12

1. Hello World ★☆☆

Créez un fichier hello.c qui affiche "Hello World".

a) Premier programme

  1. Écrivez le programme avec #include <stdio.h>, main(), printf
  2. Compilez avec gcc
  3. Exécutez ./hello

b) Personnalisation

  1. Modifiez le programme pour qu'il affiche "Hello <votre_prénom>"
  2. Utilisez argv[1] pour passer le prénom en argument
  3. Si aucun argument n'est fourni, affichez "Hello World" par défaut

c) Boucle d'affichage

  1. Affichez tous les arguments : "Hello Alice, Bob, Charlie!"
  2. Gérez le cas où il n'y a qu'un seul argument : "Hello Alice!"
🐍 Voir la solution
/* hello.c — solution complète */
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
    /* b) argument unique */
    if (argc == 2) {
        printf("Hello %s!\n", argv[1]);
        return 0;
    }
    /* c) arguments multiples */
    if (argc > 2) {
        printf("Hello");
        for (int i = 1; i < argc; i++) {
            printf("%s%s", i == 1 ? " " : ", ", argv[i]);
        }
        printf("!\n");
        return 0;
    }
    /* a) défaut */
    printf("Hello World!\n");
    return 0;
}
💡 Compilation : gcc -Wall -Wextra -std=c99 -o hello hello.c. L'option -Wall active tous les warnings. Le C ne vous tient pas la main, mais -Wall vous offre un gilet de sauvetage.
3 / 12

2. Variables & boucles — factorielle ★☆☆

Calculez la factorielle d'un nombre entier.

a) Version itérative

  1. Demandez un nombre au clavier
  2. Calculez n! avec une boucle for ou while
  3. Affichez le résultat
  4. Testez avec 0!, 5!, 10!

b) Version récursive

  1. Réécrivez factorielle sous forme récursive
  2. Comparez les deux versions : quelle est la limite récursive chez vous ?

c) Suite de Fibonacci

  1. Affichez les n premiers termes de la suite de Fibonacci
  2. F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)
  3. Testez avec n=10, n=20
🐍 Voir la solution
/* a) itératif */
long long factorielle_iter(int n) {
    long long res = 1;
    for (int i = 2; i <= n; i++) res *= i;
    return res;
}

/* b) récursif */
long long factorielle_rec(int n) {
    if (n <= 1) return 1;
    return n * factorielle_rec(n - 1);
}

/* c) Fibonacci */
void fibonacci(int n) {
    long long a = 0, b = 1;
    for (int i = 0; i < n; i++) {
        printf("%lld ", a);
        long long tmp = a + b;
        a = b; b = tmp;
    }
    printf("\n");
}
21! dépasse les 64 bits. Le C ne vous préviendra pas — il continuera comme si de rien n'était, en vous donnant un résultat faux. C'est ce qu'on appelle une confiance aveugle.
4 / 12

3. Fonctions ★☆☆

Extrayez le calcul dans des fonctions réutilisables.

a) Fonction factorielle

  1. Écrivez une fonction factorielle
  2. Appelez-la depuis le main pour plusieurs valeurs
  3. Ajoutez la gestion d'erreur si n < 0

b) Fonction puissance

  1. Écrivez long long puissance(int a, int b) qui calcule ab
  2. Utilisez l'exponentiation rapide : O(log b) multiplications
  3. Testez : 210=1024, 35=243

c) Combinaison

  1. Écrivez long long combinaison(int n, int k) = n! / (k! × (n-k)!)
  2. Testez : C(5,2)=10, C(10,5)=252
🐍 Voir la solution
/* a) factorielle */
long long factorielle(int n) {
    if (n < 0) return -1;
    long long res = 1;
    for (int i = 2; i <= n; i++) res *= i;
    return res;
}

/* b) exponentiation rapide */
long long puissance(int a, int b) {
    if (b == 0) return 1;
    long long moitie = puissance(a, b / 2);
    return (b % 2 == 0) ? moitie * moitie : moitie * moitie * a;
}

/* c) combinaison */
long long combinaison(int n, int k) {
    if (k < 0 || k > n) return 0;
    if (k > n - k) k = n - k;  /* optimisation */
    long long res = 1;
    for (int i = 1; i <= k; i++) {
        res = res * (n - k + i) / i;
    }
    return res;
}
💡 Makefile : N'oubliez pas d'écrire un Makefile avec une tabulation, pas des espaces. Le Makefile est aussi exigeant que votre prof de C.
5 / 12

4. Bibliothèque Sexp ★★☆

Découvrez sexpression.h — la bibliothèque qui permet de manipuler des S-expressions.

a) Premières manipulations

  1. Créez sexplore.c qui inclut "sexpression.h"
  2. Utilisez sexp_int pour créer un entier, sexp_print pour l'afficher
  3. Créez une liste avec sexp_cons
  4. Compilez avec sexpression.c

b) Arbre d'expressions

  1. Créez une S-expression qui représente (+ 2 (* 3 4))
  2. Affichez-la avec sexp_print — le résultat doit être (+ 2 (* 3 4))
  3. Utilisez sexp_sym pour les opérateurs

c) Compter les nœuds

  1. Écrivez une fonction int compte_ndeuds(Sexp *e) qui compte récursivement les nœuds d'une S-expression
  2. Testez sur (+ 2 (* 3 4)) : combien de nœuds ?
🐍 Voir la solution
#include "sexpression.h"
#include <stdio.h>

/* c) compteur récursif */
int compte_ndeuds(Sexp *e) {
    if (!e) return 0;
    if (e->type == SEXPR_CONS)
        return 1 + compte_ndeuds(e->car) + compte_ndeuds(e->cdr);
    return 1;  /* atom */
}

int main() {
    /* a) entier + liste */
    Sexp *e = sexp_int(42);
    sexp_print(e); printf("\n");
    Sexp *lst = sexp_cons(sexp_int(1),
                sexp_cons(sexp_int(2),
                    sexp_cons(sexp_int(3), NULL)));
    sexp_print(lst); printf("\n");

    /* b) arbre (+ 2 (* 3 4)) */
    Sexp *arbre = sexp_cons(sexp_sym("+"),
        sexp_cons(sexp_int(2),
            sexp_cons(sexp_cons(sexp_sym("*"),
                sexp_cons(sexp_int(3),
                    sexp_cons(sexp_int(4), NULL))),
            NULL)));
    sexp_print(arbre); printf("\n");
    printf("Nœuds : %d\n", compte_ndeuds(arbre));

    sexp_free(e); sexp_free(lst); sexp_free(arbre);
    return 0;
}
💡 Types : Sexp peut être SEXPR_INT, SEXPR_SYM, SEXPR_CONS, SEXPR_FLOAT, SEXPR_CELL ou SEXPR_NATIVE. Le champ e->type permet de savoir à quoi on a affaire.
6 / 12

5. Parser une expression ★★☆

Utilisez sexp_parse pour transformer une chaîne en S-expression.

a) Premier parse

  1. Créez parseur.c qui lit une expression depuis la ligne de commande
  2. Utilisez sexp_parse pour la parser
  3. Affichez le résultat parsé
  4. Testez avec : "(+ 2 (* 3 4))", "(lambda (x) x)"

b) Gestion d'erreurs

  1. Testez "(+ 2 (* 3 4)" (parenthèse manquante)
  2. Affichez un message d'erreur clair si sexp_parse retourne NULL
  3. Testez avec "" (chaîne vide)

c) Parse depuis un fichier

  1. Lisez une expression depuis un fichier texte passé en second argument
  2. Utilisez fgets/fread pour récupérer le contenu
  3. Testez : créez expr.txt contenant (+ 10 20 30)
🐍 Voir la solution
#include <stdio.h>
#include <stdlib.h>
#include "sexpression.h"

/* c) lire un fichier */
char *lit_fichier(const char *path) {
    FILE *f = fopen(path, "r");
    if (!f) return NULL;
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    rewind(f);
    char *buf = malloc(sz + 1);
    fread(buf, 1, sz, f);
    buf[sz] = 0;
    fclose(f);
    return buf;
}

int main(int argc, char **argv) {
    const char *input;
    char *buf = NULL;

    if (argc >= 3) {         /* c) depuis fichier */
        buf = lit_fichier(argv[2]);
        input = buf ? buf : "";
    } else if (argc > 1) { /* a) depuis argv */
        input = argv[1];
    } else {
        input = "(+ 2 3)";
    }

    Sexp *expr = sexp_parse(input);
    /* b) gestion d'erreur */
    if (!expr) {
        fprintf(stderr, "Erreur : expression invalide\n");
        free(buf);
        return 1;
    }
    printf("Parsé : ");
    sexp_print(expr);
    printf("\n");
    sexp_free(expr);
    free(buf);
    return 0;
}
Les parenthèses, c'est comme les fraises Tagada : si vous en avez trop, ça colle. Si vous en avez pas assez, c'est triste. L'équilibre, voilà la clé.
7 / 12

6. Évaluateur arithmétique ★★★

Évaluez récursivement une expression arithmétique parsée : (+ 2 (* 3 4)) → 14

a) Évaluateur de base

  1. Écrivez une fonction eval qui parcourt l'arbre
  2. Si SEXPR_INT → retourner sa valeur
  3. Si SEXPR_CONS → c'est (op arg1 arg2 ...) : évaluer chaque arg, appliquer l'opérateur
  4. Gérez + , - , * , /

b) Division par zéro

  1. Ajoutez une vérification : si le diviseur vaut 0, affichez "Erreur : division par zéro"
  2. Retournez 0 dans ce cas (ou mieux : exit(1))

c) Opérateurs supplémentaires

  1. Ajoutez l'opérateur % (modulo)
  2. Ajoutez l'opérateur ^ (puissance) — utilisez votre fonction puissance
  3. Testez : (% 10 3) → 1, (^ 2 10) → 1024
🐍 Voir la solution
long long eval(Sexp *e) {
    if (e->type == SEXPR_INT)
        return e->entier;

    char *op = e->car->symbole;
    Sexp *args = e->cdr;

    long long val = eval(args->car);
    args = args->cdr;

    while (args) {
        long long v = eval(args->car);
        if (op[0] == '+') val += v;
        else if (op[0] == '-') val -= v;
        else if (op[0] == '*') val *= v;
        else if (op[0] == '/') {
            if (v == 0) { /* b) */
                fprintf(stderr, "Division par zéro\n");
                return 0;
            }
            val /= v;
        }
        else if (op[0] == '%') val %= v;   /* c) modulo */
        else if (op[0] == '^') val = puissance(val, v); /* c) puissance */
        args = args->cdr;
    }
    return val;
}
💡 Pensez-y : une liste (+ 2 (* 3 4)) est représentée en interne comme cons(+, cons(2, cons(cons(*, cons(3, cons(4, NULL))), NULL))). Le car du CONS est le premier élément, le cdr est le reste.
8 / 12

7. Bonus — REPL interactif ★★☆

Enveloppez votre parseur + évaluateur dans une boucle interactive.

a) Boucle minimale

  1. Boucle infinie qui : affiche un prompt, lit une ligne, parse, évalue, affiche le résultat
  2. Ajoutez la commande exit pour sortir
  3. Testez : (+ 1 2)(* 3 4)(- 10 5)

b) Historique

  1. Stockez les 10 dernières expressions dans un tableau circulaire
  2. Ajoutez la commande history pour les afficher
  3. Ajoutez !n pour ré-exécuter la n-ième entrée de l'historique

c) Mode batch

  1. Si un argument est passé, traitez-le comme un fichier .lisp
  2. Lisez le fichier ligne par ligne, évaluez chaque expression, affichez les résultats
  3. Testez : créez test.lisp avec (+ 1 2), (* 3 4)
🐍 Voir la solution
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "sexpression.h"

/* b) historique circulaire */
#define HIST_SIZE 10
char *history[HIST_SIZE];
int hist_idx = 0, hist_count = 0;

void hist_add(const char *line) {
    free(history[hist_idx]);
    history[hist_idx] = strdup(line);
    hist_idx = (hist_idx + 1) % HIST_SIZE;
    if (hist_count < HIST_SIZE) hist_count++;
}

const char *hist_get(int n) {
    if (n < 0 || n >= hist_count) return NULL;
    int idx = (hist_idx - hist_count + n) % HIST_SIZE;
    return history[idx];
}

int main(int argc, char **argv) {
    /* c) mode batch */
    if (argc > 1) {
        FILE *f = fopen(argv[1], "r");
        if (!f) { perror(argv[1]); return 1; }
        char line[1024];
        while (fgets(line, sizeof(line), f)) {
            line[strcspn(line, "\n")] = 0;
            if (line[0] == 0 || line[0] == ';') continue;
            Sexp *e = sexp_parse(line);
            if (e) { printf("> %lld\n", eval(e)); sexp_free(e); }
        }
        fclose(f);
        return 0;
    }

    /* a) REPL interactif */
    char ligne[1024];
    while (1) {
        printf("λ > ");
        if (!fgets(ligne, sizeof(ligne), stdin)) break;
        ligne[strcspn(ligne, "\n")] = 0;
        if (strcmp(ligne, "exit") == 0) break;
        if (strcmp(ligne, "history") == 0) {
            for (int i = 0; i < hist_count; i++)
                printf("%d: %s\n", i, hist_get(i));
            continue;
        }
        if (ligne[0] == '!') {
            int n = atoi(ligne + 1);
            const char *prev = hist_get(n);
            if (prev) { strcpy(ligne, prev); printf("→ %s\n", ligne); }
            else { printf("Pas d'entrée %d\n", n); continue; }
        }
        if (ligne[0] == 0) continue;
        hist_add(ligne);
        Sexp *expr = sexp_parse(ligne);
        if (!expr) { printf("Erreur de syntaxe\n"); continue; }
        long long r = eval(expr);
        printf("= %lld\n", r);
        sexp_free(expr);
    }
    for (int i = 0; i < HIST_SIZE; i++) free(history[i]);
    return 0;
}
Félicitations, vous venez d'écrire votre premier interpréteur interactif. Les ingénieurs de Lisp Machines Inc. pleurent de joie quelque part.
9 / 12

8. Aller plus loin ★★★

Défis supplémentaires pour les plus rapides. Solutions fournies pour vérification.

🔢 Triangle de Pascal

Affichez les n premières lignes du triangle de Pascal en utilisant votre fonction combinaison.

Exemple pour n=5 :

    1
   1 1
  1 2 1
 1 3 3 1
1 4 6 4 1

📊 Mini wc

Écrivez mini-wc.c qui compte les lignes, mots et caractères d'un fichier (comme wc sous Unix).

Utilisez fgetc et un automate à états pour compter les mots.

🐍 Voir la solution
/* Triangle de Pascal */
void pascal(int n) {
    for (int i = 0; i < n; i++) {
        for (int s = 0; s < n - i - 1; s++) printf(" ");
        for (int j = 0; j <= i; j++)
            printf("%lld ", combinaison(i, j));
        printf("\n");
    }
}

/* mini-wc */
int main(int argc, char **argv) {
    FILE *f = argc > 1 ? fopen(argv[1], "r") : stdin;
    int lignes = 0, mots = 0, carac = 0, dans_mot = 0, c;
    while ((c = fgetc(f)) != EOF) {
        carac++;
        if (c == '\n') lignes++;
        if (c == ' ' || c == '\n' || c == '\t') { dans_mot = 0; }
        else if (!dans_mot) { mots++; dans_mot = 1; }
    }
    printf("%d %d %d %s\n", lignes, mots, carac, argc > 1 ? argv[1] : "");
    return 0;
}
Le triangle de Pascal a une infinité de propriétés. Celle que vous venez d'implémenter : il calcule des combinaisons sans faire de factorielles. Pas mal pour un triangle.
10 / 12

9. Sans filet ★★★

⚠️ Pas de solution fournie. Ces exercices sont conçus pour vous faire voler de vos propres ailes. Le code let.c et calc.c peuvent vous servir d'inspiration.

🔬 Vérificateur de parenthèses

Écrivez un programme qui lit une chaîne et vérifie si les parenthèses sont correctement équilibrées.

Exemples :

  • "(+ 2 (* 3 4))" → OK
  • "(+ 2 (* 3 4)" → Erreur (parenthèse ouvrante non fermée)
  • ")( 1 2 (" → Erreur (parenthèse fermante avant ouvrante)

Indice

🔄 Opérateurs de comparaison

Ajoutez les opérateurs =, <, > à l'évaluateur arithmétique du TD1.

Ils doivent retourner 0 (faux) ou 1 (vrai).

Testez : (= (+ 2 3) 5) → 1, (> 10 (* 2 3)) → 1

✏️ Défi bonus : Écrivez un programme qui affiche un damier de taille n×n avec des # et des . alternés, sans utiliser de tableau.
11 / 12

TD 1 — Terminé !

En 2 heures, vous êtes passés de printf("Hello World")
à un évaluateur d'expressions arithmétiques S-expressions.

📄 Fichiers créés
hello.c → factorielle.c → sexplore.c → parseur.c → repl.c
📚 Prochaine séance
TD 2 — Environnements & fermetures lexicales
🔑 Mots-clés
compilation, récursivité, S-expression, parseur, REPL
Un interpréteur qui marche, c'est comme un chat qui ronronne : c'est rare, c'est beau, et ça vous regarde avec condescendance.
« On ne devient pas magicien en un jour. Mais on peut parser en une soirée. »
12 / 12