Architecture — comment fonctionne l'interpréteur

TON618 est un interpréteur à parcours d'arbre classique, composé de trois étapes formant un pipeline direct :

texte source (fichier .ton)
        │
        ▼
┌───────────────┐   transforme les caractères en une liste plate de Tokens
│     Lexer     │   (nombres, chaînes, identifiants, mots-clés, opérateurs)
└───────┬───────┘
        ▼  tokens
┌───────────────┐   parsing par descente récursive : groupe les Tokens en un
│     Parser    │   arbre syntaxique abstrait (un arbre de nœuds Stmt/Expr)
└───────┬───────┘   selon la grammaire du langage
        ▼  AST
┌───────────────┐   parcourt l'AST directement et l'exécute — pas de bytecode,
│  Interpreteur │   pas d'étape de compilation séparée ; chaque nœud est évalué
└───────────────┘   au moment où il est atteint
ton618/
├── Makefile
├── include/                 # en-têtes (.hpp) — déclarations et notes de conception
│   ├── Token.hpp               — le vocabulaire du langage (TokenType, Token)
│   ├── Lexer.hpp                 — texte -> tokens
│   ├── Ast.hpp                     — la forme des nœuds AST (Expr, Stmt)
│   ├── Parser.hpp                    — tokens -> AST
│   ├── Value.hpp                       — représentation des valeurs à l'exécution
│   ├── Environment.hpp                   — scopes de variables (nom -> Value, chaînés)
│   ├── Interpreter.hpp                     — parcourt l'AST, exécute le programme
│   ├── Debugger.hpp                          — points d'arrêt / mode pas-à-pas
│   ├── LocalServer.hpp                         — serveur HTTP en sockets bruts (ton.serve/get/post)
│   ├── HttpClient.hpp                            — client HTTP en sockets bruts (module ton.requests)
│   ├── JsonParser.hpp                              — texte JSON -> Value (json_parse de ton.json)
│   ├── Platform.hpp                                  — détection auto OS/Termux/architecture
│   └── Version.hpp                                     — la version de l'interpréteur lui-même
├── src/                     # implémentations (.cpp), une par en-tête ci-dessus,
│                              plus main.cpp (point d'entrée CLI : lit un fichier, lance
│                              le pipeline Lexer -> Parser -> Interpreter, et câble
│                              --debug / --break / --version / --update / --uninstall-ton / install / uninstall)
├── exemples/                # scripts .ton d'exemple, dont un qui exerce la majorité
│                              du langage (test_new_features.ton), plus documentation.html,
│                              playground.html, et ton618-lite.js (une réimplémentation JS
│                              utilisée seulement par le playground)
├── scripts/                 # install.sh/.ps1/.bat, uninstall.sh/.ps1/.bat, rebuild.sh,
│                              zig-strip.sh, et doc_templates/ (voir `make docs` plus haut)
└── modules/                 # modules .ton partagés (voir le guide de module plus haut)

Chaque en-tête commence par un court bloc de commentaire expliquant son rôle dans le pipeline et, si pertinent, ce qu'il faut toucher en premier pour étendre cette partie du langage — lis-les avant de plonger dans les fichiers .cpp.

Deux choix de conception qui valent la peine d'être mentionnés car ils façonnent presque tous les autres fichiers :

  • Les nœuds AST/Value sont des "fat structs", pas une hiérarchie de classes. Expr et Stmt (Ast.hpp) et Value (Value.hpp) contiennent chacun tous les champs dont n'importe laquelle de leurs variantes pourrait avoir besoin, avec une étiquette type disant lesquels s'appliquent. Ça sacrifie un peu de mémoire pour un code où evaluate()/execute() sont de simples switch plutôt qu'un visiteur dispersé — bien plus facile à lire de bout en bout pour un langage de cette taille. Certains types de nœuds réutilisent les champs d'un autre type (documenté à côté de l'enum) plutôt que d'en ajouter de nouveaux, pour empêcher les structs de grossir sans limite — SWITCH, par exemple, réutilise les champs condition/elseBranch de IF pour son expression sujet et son bloc par défaut.
  • Les structures de contrôle utilisent les exceptions C++. break, continue, et return sont implémentés comme de petits types d'exception (BreakException, ContinueException, ReturnException, voir Interpreter.hpp) levés depuis execute() et attrapés par la boucle, le switch, ou l'appel de fonction englobant le plus proche. Ça reproduit, presque un-à-un, la façon dont ces structures remontent à travers les blocs imbriqués de l'AST — aucun drapeau manuel "a-t-on break ?" n'a besoin d'être propagé à travers chaque appel récursif.

Une troisième chose à savoir si tu touches à la bibliothèque standard : valuesEqual() (une fonction libre près du haut de src/Interpreter.cpp) est la seule source de vérité pour ce que signifie "égal" — les opérateurs ==/!=, la correspondance switch/case, et les natives contains()/indexOf() l'appellent tous, donc un correctif ou un nouveau type de valeur n'a besoin d'être géré qu'à un seul endroit.

Fonctionnement interne depuis beta-2.0.0, pour les contributeurs :

  • break/continue/return ne sont plus des exceptions C++ : Interpreter::execute() renvoie un Signal (NONE, BREAK, CONTINUE, RETURN) que les boucles et les appels vérifient. Seules les vraies erreurs utilisent des exceptions (LineError pour les erreurs de l'interpréteur, ThrownValue pour throw).
  • Les noms sont internés en entiers (Symbols dans Environment.hpp) et les scopes sont de petits vecteurs plats, avec un index haché seulement pour les gros scopes comme les globales. Un bloc qui ne déclare rien n'a pas de scope du tout.
  • Les classes sont des ClassObj et les objets des InstanceObj (Value.hpp) ; les appels de méthode passent par Interpreter::callMethod, qui implémente aussi la syntaxe d'appel uniforme.
  • La bibliothèque standard ajoutée en 2.0 est dans src/Stdlib.cpp (defineNativesV2()).
  • main.cpp exécute les scripts sur un thread avec une pile de 512 Mo, donc une récursion profonde atteint la limite de l'interpréteur (10 000 appels) au lieu de planter.
  • tests/*.ton avec tests/*.expected forment la suite de non-régression : make test (aussi lancé par la CI).

Ensemble, ces changements rendent les scripts typiques environ 6 fois plus rapides qu'en beta-1.x (un fib(25) plus une boucle de 1 000 000 d'itérations : 2,0 s → 0,3 s).

Modifier cette page sur GitHub