Architecture technique

Ce chapitre s'adresse aux développeurs et contributeurs qui veulent comprendre comment AlgoLab fonctionne en interne.

Pipeline d'exécution

Quand vous lancez algolab mon_programme.algo, voici ce qui se passe :

Code source (.algo)
       │
       ▼
   ┌─────────┐
   │ main.py │  Lecture du fichier, parsing des arguments CLI
   └────┬────┘
        │
        ▼
   ┌──────────┐
   │ parser.py│  Lark transforme le code source en arbre syntaxique (AST)
   └────┬─────┘
        │   utilise grammar.lark (grammaire LALR)
        │   utilise lexer.py (mode contextuel)
        │   applique des heuristiques d'erreur si le parsing échoue
        ▼
   ┌──────────────┐
   │interpreter.py│  Parcours récursif de l'arbre Lark, exécution noeud par noeud
   └────┬─────────┘
        │   utilise environment.py pour les variables
        │   utilise errors.py pour les erreurs pédagogiques
        ▼
     Sortie (Ecrire → stdout)

Modules

main.py — Point d'entrée CLI

Le point d'entrée du programme. Il gère deux modes d'exécution : depuis un fichier (algolab fichier.algo) et en inline (algolab -c 'code'). Il instancie un Interpreter et appelle run(). Les erreurs AlgoLabError et FileNotFoundError sont capturées et affichées proprement (sans stack trace Python).

parser.py — Analyse syntaxique

Ce module crée un parser Lark à partir de la grammaire grammar.lark et transforme le code source en un arbre syntaxique Lark (Tree).

Le chargement de la grammaire est robuste : il essaie d'abord importlib.resources (installation pip), puis le chemin relatif au fichier (développement), puis le chemin PyInstaller _MEIPASS (binaires compilés).

Quand le parsing échoue, au lieu de remonter l'erreur Lark brute, parse_source() applique une série d'heuristiques pour produire des messages d'erreur utiles. Ces heuristiques analysent le contexte de l'erreur (lignes précédentes, tokens attendus) et formulent un diagnostic humain. Les heuristiques couvrent : le mot-clé Alors manquant après Si, le mot-clé Faire manquant après TantQue ou Pour, la valeur manquante après Pas, les arguments manquants après Lire et Ecrire, les opérandes manquantes après un opérateur, et les blocs non fermés.

La détection des blocs non fermés fonctionne avec une pile : chaque Si, TantQue, Pour empile le mot-clé de fermeture attendu. Chaque FinSi, FinTantQue, FinPour dépile. Si la pile n'est pas vide à la fin, l'erreur indique quel bloc est resté ouvert et à quelle ligne.

grammar.lark — Grammaire formelle

La source de vérité syntaxique. C'est une grammaire Lark en format EBNF qui définit : la structure d'un programme (préambule + Debut + instructions + Fin), les déclarations de variables et fonctions, les instructions (affectation, si, pour, tantque, lire, ecrire, appel de fonction), les expressions (arithmétiques, booléennes, chaînes), et les tokens (mots-clés, opérateurs, littéraux).

Les mots-clés sont définis avec des regex insensibles à la casse (flag /i) et une priorité élevée (.2) pour éviter les conflits avec les identifiants. Les mots-clés composés acceptent un underscore optionnel (SINON_?SI, FIN_?SI, etc.).

Le parser utilise le mode LALR avec le lexer contextuel de Lark, ce qui donne un parsing efficace et une bonne gestion des ambiguïtés.

Voir la référence complète de la grammaire pour les détails.

lexer.py — Configuration du lexer

Un module minimaliste qui centralise la constante LEXER_MODE = "contextual". AlgoLab utilise le lexer contextuel de Lark (intégré au parser LALR), pas un lexer séparé. Ce module existe pour maintenir une surface d'import stable si un lexer dédié est ajouté dans le futur.

interpreter.py — Exécution

Le coeur d'AlgoLab. L'interpréteur parcourt l'arbre Lark de manière récursive avec un pattern visiteur : pour chaque noeud de type xxx, il appelle la méthode visit_xxx().

L'Interpreter est initialisé avec trois dépendances injectables : un Environment (gestion mémoire), un reader (fonction de lecture, par défaut input), et un writer (fonction d'écriture, par défaut print). Cette injection permet de tester l'interpréteur sans I/O réel.

Les fonctions sont stockées dans un dictionnaire self.functions sous forme de FunctionDef (dataclass contenant le nom, les paramètres, le type de retour, le corps et l'expression de retour). À chaque appel de fonction, un nouveau scope local est créé avec Environment(parent=previous_env).

L'évaluation des expressions arithmétiques utilise _eval_infix(), une méthode générique qui parse une liste alternée [valeur, opérateur, valeur, opérateur, valeur...] et applique les opérations de gauche à droite. La priorité des opérateurs est gérée par la grammaire (les terme sont évalués avant les expression_arithmetique).

La gestion des erreurs enrichit automatiquement les exceptions avec les informations de position (ligne, colonne) extraites des métadonnées Lark.

environment.py — Gestion mémoire

Ce module gère les variables et les scopes. Les concepts clés sont :

  • TypeSpec : représentation d'un type (base + taille optionnelle pour les tableaux), dataclass immuable.
  • Variable : associe un TypeSpec à une valeur.
  • Environment : dictionnaire de variables avec scope parent optionnel.

La résolution des variables remonte la chaîne des scopes parents (lookup chain). La déclaration est toujours locale (pas de redéclaration dans le même scope).

Le typage est vérifié à chaque affectation via _coerce_value(). Cette méthode vérifie la compatibilité de type et effectue les conversions implicites (entier → réel). Les booléens Python (True/False) sont explicitement rejetés pour le type Entier (car bool est une sous-classe de int en Python).

Les tableaux sont implémentés comme des listes Python de taille fixe, initialisées à None. L'indexation est convertie de base-1 (AlgoLab) à base-0 (Python) par _to_zero_based().

errors.py — Système d'erreurs

Trois classes d'erreurs, toutes héritant de AlgoLabError :

  • SyntaxErrorAlgoLab : préfixe "Erreur Syntaxique", levée par le parser.
  • SemanticErrorAlgoLab : préfixe "Erreur Semantique", levée par l'environnement ou l'interpréteur pour les incohérences de types/scopes.
  • RuntimeErrorAlgoLab : préfixe "Erreur d execution", levée pendant l'exécution (division par zéro, variable non initialisée...).

Chaque erreur porte optionnellement une ligne et une colonne, formatées automatiquement : Erreur [Type] (Ligne X, Colonne Y) : [Message].

Structure des fichiers

AlgoLab/
├── src/algolab/           # Code source principal
│   ├── __init__.py        # Package marker
│   ├── grammar.lark       # Grammaire formelle (source de vérité)
│   ├── lexer.py           # Configuration du lexer
│   ├── parser.py          # Parsing + heuristiques d'erreur
│   ├── interpreter.py     # Exécution de l'arbre
│   ├── environment.py     # Variables, types, scopes
│   ├── errors.py          # Classes d'erreurs pédagogiques
│   └── main.py            # Point d'entrée CLI
├── tests/                 # Suite de tests (pytest)
│   ├── conftest.py        # Configuration pytest + path setup
│   ├── test_lexer.py      # Tests du lexer
│   ├── test_parser.py     # Tests du parser
│   ├── test_interpreter.py# Tests de l'interpréteur
│   ├── test_types.py      # Tests de typage
│   ├── test_errors.py     # Tests des messages d'erreur
│   └── test_edge_cases.py # Tests de cas limites
├── examples/              # 13 programmes d'exemple
├── vscode-extension/      # Extension VS Code
│   ├── extension.js       # Logique de l'extension
│   ├── package.json       # Manifeste et configuration
│   ├── syntaxes/          # Grammaire TextMate (coloration)
│   ├── snippets/          # Snippets de code
│   └── images/            # Icônes
├── scripts/               # Scripts utilitaires
│   ├── build_deb.sh       # Build du paquet .deb
│   ├── ast_viewer.py      # Visualiseur d'AST (debug)
│   ├── validate_grammar.py# Validation de la grammaire
│   ├── run.py / run.sh    # Raccourcis d'exécution
├── docs/                  # Documentation technique
├── .github/               # CI/CD, templates, config GitHub
│   ├── workflows/         # GitHub Actions
│   └── ISSUE_TEMPLATE/    # Templates d'issues
├── pyproject.toml         # Configuration du projet Python
├── algolab.spec           # Configuration PyInstaller
└── README.md              # README principal

CI/CD

Le workflow GitHub Actions (build-release.yml) exécute les étapes suivantes à chaque push sur main :

  1. Quality : lint avec Ruff, tests avec pytest (couverture), smoke test CLI, build du wheel Python
  2. Build : compilation PyInstaller sur 3 OS (Ubuntu → .deb, Windows → .exe, macOS → binaire)
  3. Build Extension : packaging de l'extension VS Code en .vsix
  4. Release : upload des artefacts sur la release GitHub (uniquement lors d'un événement release)

Tests

La suite de tests couvre 24 cas répartis en 6 fichiers. Les tests utilisent une technique d'injection : un Interpreter est créé avec un reader et un writer personnalisés pour capturer les sorties et simuler les entrées sans I/O réel :

def _run_program(source, inputs=None):
    values = iter(inputs or [])
    outputs = []
    interpreter = Interpreter(
        reader=lambda: next(values),
        writer=lambda value: outputs.append(str(value))
    )
    interpreter.run(source)
    return outputs

Les tests couvrent le lexer (reconnaissance des tokens, insensibilité à la casse), le parser (programme minimal, erreurs guidées), l'interpréteur (expressions, fonctions, division par zéro), le typage (coercition, incompatibilités), les erreurs (format des messages, guidage), et les cas limites (types mixtes, récursion, chaînage de fonctions, tableaux hors bornes).