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.
ExpretStmt(Ast.hpp) etValue(Value.hpp) contiennent chacun tous les champs dont n'importe laquelle de leurs variantes pourrait avoir besoin, avec une étiquettetypedisant lesquels s'appliquent. Ça sacrifie un peu de mémoire pour un code oùevaluate()/execute()sont de simplesswitchplutô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 champscondition/elseBranchdeIFpour son expression sujet et son bloc par défaut. - Les structures de contrôle utilisent les exceptions C++.
break,continue, etreturnsont implémentés comme de petits types d'exception (BreakException,ContinueException,ReturnException, voir Interpreter.hpp) levés depuisexecute()et attrapés par la boucle, leswitch, 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/returnne sont plus des exceptions C++ :Interpreter::execute()renvoie unSignal(NONE,BREAK,CONTINUE,RETURN) que les boucles et les appels vérifient. Seules les vraies erreurs utilisent des exceptions (LineErrorpour les erreurs de l'interpréteur,ThrownValuepourthrow).- Les noms sont internés en entiers (
SymbolsdansEnvironment.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
ClassObjet les objets desInstanceObj(Value.hpp) ; les appels de méthode passent parInterpreter::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.cppexé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/*.tonavectests/*.expectedforment 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).