Cours 4

S-expressions, Tables

& Mini langage Template

Des listes au chatbot en 5 étapes

Laurent Thiry — Programmation en C

« Les templates, c'est comme les sandwichs : vous préparez le pain et la garniture. »

1

🗺️ Plan du cours

La différence entre ce cours et les précédents ? Les parenthèses. Plein de parenthèses.
2

1. De la liste au chatbot — le fil rouge

📋 Listes simples
mapk / filterk
📊 Tables
filter / project
📝 Templates
publipostage
💬 Chatbot
pattern + substitution
zip + cherche + evaluer

Chaque étape ajoute une couche sans changer le moteur fondamental.

On commence par multiplier des entiers. On finit par faire du chatbot. Comme dans la vie.
3

2.1 Le type Sexp — le bloc Lego

typedef struct Sexp {
    int    type;      // SEXPR_INT, SEXPR_SYM, SEXPR_CONS
    int    entier;
    char  *symbole;
    struct Sexp *car;
    struct Sexp *cdr;
} Sexp;

Trois visages

  • Entiersexp_int(42)
  • Symbolesexp_sym("x")
  • Paire (cons)sexp_cons(a, b)

Exemple

(1 2 3) = cons(1, cons(2, cons(3, NULL)))
C'est comme un couteau suisse avec 3 lames. Les 3 suffisent à construire une fusée.
4

2.2 Le parseur — des parenthèses au C

static Sexp *parse_depuis(Parseur *p) {
    if (strcmp(p->token, "(") == 0) {
        prochain_token(p);
        Sexp *tete = NULL, *cur = NULL;
        while (strcmp(p->token, ")") != 0) {
            Sexp *item = parse_depuis(p);
            Sexp *cell = sexp_cons(item, NULL);
            if (!tete) { tete = cell; cur = cell; }
            else        { cur->cdr = cell; cur = cell; }
        }
        prochain_token(p);
        return tete;
    }
    return est_nombre(tok) ? sexp_int(atoi(tok))
                               : sexp_sym(tok);
}

sexp_parse("(+ 2 (* 3 4))") → arbre Sexp

Un parseur récursif, c'est comme un miroir : il se regarde lui-même.
5

2.3 zip — fusionner deux listes

Sexp *zip(Sexp *a, Sexp *b) {
    if (!a || !b) return NULL;
    Sexp *ca = a->car, *cb = b->car;
    Sexp *ac = (ca->type == SEXPR_INT) ? sexp_int(ca->entier)
                                       : sexp_sym(ca->symbole);
    Sexp *bc = (cb->type == SEXPR_INT) ? sexp_int(cb->entier)
                                       : sexp_sym(cb->symbole);
    return sexp_cons(
        sexp_cons(ac, sexp_cons(bc, NULL)),
        zip(a->cdr, b->cdr));
}

zip((x y z), (1 2 3)) → ((x 1) (y 2) (z 3))

Copie défensive : les atomes sont copiés → indépendance mémoire.
Variation sans copie : on partage les pointeurs → économie de mémoire, mais attention aux doubles free !
6

2.4 cherche et evaluer

Sexp *cherche(Sexp *key, Sexp *alist) {
    while (alist && alist->type == SEXPR_CONS) {
        Sexp *e = alist->car;
        if (e->car->type == SEXPR_SYM
            && key->type == SEXPR_SYM
            && strcmp(e->car->symbole,
                      key->symbole) == 0)
            return e->cdr->car;
        alist = alist->cdr;
    }
    return NULL;
}
  • cherche(x, ((x 1)(y 2)))1
  • evaluer((+ x y), env) → évalue récursivement
  • Moteur complet en ~50 lignes : zip + cherche + evaluer

    7

    3.1 mapk — multiplier une liste

    int main(int argc, char **argv) {
        int k = atoi(argv[1]);
        Sexp *liste = sexp_parse("(1 2 3 4 5 6 7 8 9 10)");
        Sexp *res = NULL, *last = NULL;
        Sexp *cur = liste;
    
        while (cur) {
            Sexp *nv = sexp_int(cur->car->entier * k);
            Sexp *cell = sexp_cons(nv, NULL);
            if (!res)  { res = cell; last = cell; }
            else       { last->cdr = cell; last = cell; }
            cur = cur->cdr;
        }
        sexp_print(res);
        sexp_free(res);
        return 0;
    }

    ./mapk 3(3 6 9 12 15 18 21 24 27 30)

    ✏️ Exercice 1 — filterk : Écrire filterk.c qui garde les éléments < k. Modifiez-le pour accepter <, >, = via le 2ème argument.
    8

    3.2 Association lists — le dictionnaire du pauvre

    // alist.c : zip + cherche
    Sexp *cles   = sexp_parse("(x y z)");
    Sexp *vleurs = sexp_parse("(10 20 30)");
    Sexp *env    = zip(cles, vleurs);
    // env = ((x 10) (y 20) (z 30))
    
    // Chercher la valeur de y
    Sexp *val = cherche(sexp_sym("y"), env);
    // val->entier == 20

    C'est notre environnement. Toute la suite repose là-dessus.

    Un dictionnaire, c'est juste une liste de paires. 40 ans de recherche en bases de données pour en arriver là.
    9

    3.3 evalenv — l'environnement en action

    // evalenv.c
    Sexp *env = zip(sexp_parse("(x y)"),
                    sexp_parse("(10 20)"));
    Sexp *expr = sexp_parse(argv[1]);
    Sexp *res  = evaluer(expr, env);
    sexp_print(res);  // (+ x y) → 30

    Usage : ./evalenv '(+ x y)'30

    Variation : Ajoutez % (modulo) et expt (puissance) à evaluer. Combien de lignes ? Juste 2.
    10

    4.1 Représentation d'une table

    Une table = liste de listes :

    ((x y z))
    ((1 2 3)
    (4 5 6)
    (7 8 9))
    C'est moins joli qu'un DataFrame Pandas. Mais ça compile en 0.3 secondes.
    11

    4.2 filter_table — WHERE en s-expressions

    Sexp *filter_table(Sexp *table, Sexp *col,
                        Sexp *op, Sexp *val) {
        Sexp *headers = table->car;
        Sexp *result = NULL, *last = NULL;
    
        for (Sexp *r = table->cdr; r; r = r->cdr) {
            Sexp *env = zip(headers, r->car);
            // Construit l'expression (op col val)
            Sexp *expr = sexp_parse("...");
            Sexp *v = evaluer(expr, env);
            if (v->entier != 0) { /* garder la ligne */ }
            sexp_free(env);
        }
        return result;
    }
    ✏️ Exercice 2 : Ajoutez la possibilité de filtrer avec and/or. Ex : filter_table(t, "(and (> x 1) (< y 5))")
    12

    4.3 project_table — expressions calculées

    // project.c
    Sexp *exprs = sexp_parse("((+ x y) (- x y))");
    
    for (chaque ligne) {
        Sexp *env = zip(headers, row);
        for (chaque expression dans exprs) {
            Sexp *val = evaluer(expr, env);
            // val = resultat calcule
        }
    }

    Résultat : une table sans noms de colonnes.

    Variation : Et si on passait ((somme (+ x y)) (diff (- x y))) ? On aurait des colonnes nommées. C'est exactement ce qu'on va faire avec les templates.
    13

    5.1 Principe du template

    Template : motif avec des expressions à évaluer.

    (x vaut (x) et xy vaut (+ x y))

    Pour chaque ligne de la table, on remplit le motif :

    table : ((x y) (1 1) (2 1) (3 2) ...)
    résultat : ((x vaut 1 et xy vaut 2)
        (x vaut 2 et xy vaut 3)
        (x vaut 3 et xy vaut 5) ...)
    C'est du publipostage (mail merge). Microsoft Word facture 150 € pour ça. Nous, on a make template-run.
    14

    5.2 paires_template — découper le motif

    while (tmpl) {
        Sexp *nom = tmpl->car;     // x
        tmpl = tmpl->cdr;            // saute nom
        tmpl = tmpl->cdr;            // saute 'vaut'
        Sexp *expr = tmpl->car;     // (x) ou (+ x y)
    
        // (x) → x  pour references aux variables
        if (expr->type == SEXPR_CONS
            && !expr->cdr
            && expr->car->type == SEXPR_SYM)
            expr = expr->car;
    
        // Copie defensive !!
        sexp_cons(sexp_copie(nom),
                  sexp_cons(sexp_copie(expr), NULL));
    
        // saute 'et' entre deux clauses
        if (tmpl->car->type == SEXPR_SYM
            && strcmp(..., "et") == 0)
            tmpl = tmpl->cdr;
    }

    Résultat : ((x (x)) (xy (+ x y)))

    15

    5.3 eval_template — le moteur

    Sexp *eval_template(Sexp *table, Sexp *tmpl) {
        Sexp *paires = paires_template(tmpl);
        Sexp *result = NULL;
    
        for (chaque ligne) {
            Sexp *env = zip(headers, row);
            Sexp *row_out = NULL;
    
            for (chaque paire (nom . expr)) {
                Sexp *val = evaluer(expr, env);
                // ajoute (nom vaut val) + 'et'
            }
            sexp_free(env);
            result = ...;
        }
        return result;
    }
    ✏️ Exercice 3 — template conditionnel : Ajoutez un mot-clé si : (x vaut (x) si (> x 2)). La clause n'est produite que si la condition est vraie.
    16

    6.1 Untemplate — extraire les valeurs

    Principe : un masque avec des (placeholders) extrait les valeurs.

    (x vaut (v1) et xy vaut (v2))
    +
    (x vaut 1 et xy vaut 2)

    v1 = 1, v2 = 2

    Collecté sur toutes les lignes → table :

    ((v1 v2) (1 2) (2 3) (3 5) (1 4) ...)

    Identité : template ∘ untemplate = renommage de colonnes.

    Comme déballer un cadeau : on retire le papier, on note où étaient les étiquettes, et on range tout dans un tableau.
    17

    6.2 Détection des placeholders

    while (tmpl) {
        if (tmpl->car->type == SEXPR_CONS     // liste
            && !tmpl->car->cdr            // 1 element
            && tmpl->car->car->type == SEXPR_SYM) {
            // C'est un placeholder !
            sexp_copie(tmpl->car->car);
        }
        tmpl = tmpl->cdr;
    }

    Puis on parcourt masque et ligne en parallèle pour extraire.

    Variation wildcard : Ajoutez _ comme symbole « joker » qui matche n'importe quelle valeur sans l'extraire. Utile pour (je veux _ s il vous plait).
    18

    7.1 Du template au chatbot

    Ingrédients :

    1. Une base de connaissances : table de paires (pattern, réponse)
    2. Un algorithme :
      • Pattern matching via inverse_template
      • Génération via eval_template
    PatternRéponse
    (bonjour je m appelle (n))(bonjour (n) !)
    (quel age as tu)(je suis né en 2026)
    (au revoir)(a bientôt (n) !)
    C'est pas ChatGPT. Mais ELIZA (1966) faisait pareil avec des regex. On a juste mis des parenthèses.
    19

    7.2 Architecture du chatbot

    while (1) {
        printf("> ");
        fgets(buf, ...);
        Sexp *entree = sexp_parse(buf);
    
        for (chaque regle (pattern . reponse)) {
            Sexp *vars = inverse_template(entree, pattern);
            if (vars) {  // match !
                Sexp *answer =
                    eval_template(vars, reponse);
                sexp_print(answer);
                break;
            }
        }
    }

    15 lignes de boucle, le reste est dans les templates.

    ✏️ Exercice 4 — chatbot minimal : Implémentez la boucle REPL complète avec 3 règles + une règle par défaut (_) → (je n ai pas compris).
    20

    7.3 Variations autour du chatbot

    Cascade de templates : Enchaînez 3 templates : normalisation → classification → réponse. Exactement l'architecture des chatbots modernes (Rasa, Dialogflow), mais avec des s-expressions au lieu de réseaux de neurones.
    Templates récursifs : Un template peut en générer un autre. Ex : (vous avez dit (x) ? repondez : (x)) — répète l'entrée. Utile pour le small talk.
    Stockage fichier : Lire les règles depuis un fichier .sexp. Plus besoin de recompiler. fread + sexp_parse = 3 lignes.
    Fonctions utilisateur : Étendez evaluer pour supporter (defun double (x) (* x 2)). Stockez les fonctions dans un environnement global.
    On a commencé par factorielle(5) = 120. On termine avec une architecture de chatbot. Le C, c'est comme les Legos.
    21

    8.1 Exercices — niveau ★☆☆

    ✏️ Ex. 1 — more ops (★) : Ajoutez pow, abs, min, max à evaluer. Testez avec evalenv.c.
    ✏️ Ex. 2 — templates chaînés (★) : Écrire un programme qui applique deux templates à la suite. La sortie du premier sert d'entrée au second.
    ✏️ Ex. 3 — negate filter (★) : Ajoutez un flag --not à filterk.c. ./filterk --not 5 → garde les éléments ≥ 5.

    Niveau ★★☆

    ✏️ Ex. 4 — JOIN (★★) : Implémentez join_tables(t1, col, t2) qui combine deux tables sur une colonne commune (équivalent SQL de JOIN).
    ✏️ Ex. 5 — templates multi-lignes (★★) : Modifiez eval_template pour qu'une clause puisse contenir plusieurs expressions : (stats (min (+ x y)) (max (+ x y))).
    ✏️ Ex. 6 — csv2sexp (★★) : Lire un CSV et produire une table Sexp. Bonus : sexp2csv pour l'inverse.
    22

    8.2 Exercices — niveau ★★★

    ✏️ Ex. 7 — chatbot complet (★★★) :
    Implémentez le chatbot complet avec :
    ✏️ Ex. 8 — mini SQL (★★★) :
    Écrire un interpréteur de requêtes minimal : ./sql 'SELECT (+ x y) AS sum FROM t WHERE (> x 2)'. Traduisez en appels à filter_table + project_table.
    ✏️ Ex. 9 — fonctions utilisateur (★★★) :
    Étendez evaluer pour supporter (defun double (x) (* x 2)). Stockez les fonctions dans un environnement global.
    23

    De la factorielle au chatbot

    3 types de données. 6 fonctions. Des pages de possibilités.

    📄 Fichiers
    sexpression.h/.c · template.c · untemplate.c
    table.c · project.c · mapk.c · filterk.c
    evalenv.c · alist.c
    📚 Cours
    cours1-introduction.html
    cours2-vm.html
    cours3-parseur.html
    cours4-template.html
    « Les s-expressions, c'est comme les maths :
    avec suffisamment de parenthèses, tout devient possible »
    24