Architecture — how the interpreter works
TON618 is a classic tree-walking interpreter, made of three stages that run in a straight pipeline:
source text (.ton file)
│
▼
┌───────────────┐ turns characters into a flat list of Tokens
│ Lexer │ (numbers, strings, identifiers, keywords, operators)
└───────┬───────┘
▼ tokens
┌───────────────┐ recursive-descent parsing: groups Tokens into an
│ Parser │ Abstract Syntax Tree (a tree of Stmt/Expr nodes)
└───────┬───────┘ according to the language's grammar
▼ AST
┌───────────────┐ walks the AST directly and executes it — no bytecode,
│ Interpreter │ no separate compilation step; each node is evaluated
└───────────────┘ the moment it's reached
ton618/
├── Makefile
├── include/ # headers (.hpp) — declarations and design notes
│ ├── Token.hpp — the language's vocabulary (TokenType, Token)
│ ├── Lexer.hpp — text -> tokens
│ ├── Ast.hpp — the AST node shapes (Expr, Stmt)
│ ├── Parser.hpp — tokens -> AST
│ ├── Value.hpp — runtime value representation
│ ├── Environment.hpp — variable scopes (name -> Value, chained)
│ ├── Interpreter.hpp — walks the AST, runs the program
│ ├── Debugger.hpp — breakpoints / step mode
│ ├── LocalServer.hpp — raw-socket HTTP server (ton.serve/get/post)
│ ├── HttpClient.hpp — raw-socket HTTP client (the ton.requests module)
│ ├── JsonParser.hpp — JSON text -> Value (ton.json's json_parse)
│ ├── Platform.hpp — OS/Termux/architecture auto-detection
│ └── Version.hpp — the interpreter's own version string
├── src/ # implementations (.cpp), one per header above,
│ plus main.cpp (CLI entry point: reads a file, runs the
│ Lexer -> Parser -> Interpreter pipeline, and wires up
│ --debug / --break / --version / --update / --uninstall-ton / install / uninstall)
├── exemples/ # example .ton scripts, including one that exercises most
│ of the language (test_new_features.ton), plus
│ documentation.html, playground.html, and ton618-lite.js
│ (a JS reimplementation used only by the playground)
├── scripts/ # install.sh/.ps1/.bat, uninstall.sh/.ps1/.bat, rebuild.sh,
│ zig-strip.sh, and doc_templates/ (see `make docs` above)
└── modules/ # shared .ton modules (see the module guide above)
Every header starts with a short comment block explaining its role in the pipeline and, where relevant, exactly what to touch first if you want to extend that part of the language — read those before diving into the .cpp files.
Two design choices worth calling out because they shape almost every other file:
- AST/Value nodes are "fat structs", not a class hierarchy.
ExprandStmt(Ast.hpp) andValue(Value.hpp) each hold every field any of their variants might need, with atypetag saying which ones apply. This trades a bit of memory for a codebase whereevaluate()/execute()are singleswitchstatements instead of a scattered visitor pattern — much easier to read end-to-end for a language this size. Some node kinds reuse another kind's fields (documented next to theenum) instead of adding new ones, to keep the structs from growing unbounded —SWITCH, for instance, reusesIF'scondition/elseBranchfields for its subject expression and default block. - Control flow uses C++ exceptions.
break,continue, andreturnare implemented as tiny exception types (BreakException,ContinueException,ReturnException, see Interpreter.hpp) thrown fromexecute()and caught by the nearest enclosing loop,switch, or function call. This mirrors, almost one-to-one, how those constructs unwind nested blocks in the AST — no manual "did we break?" flag needs to be threaded through every recursive call.
A third thing worth knowing if you're touching the standard library: valuesEqual() (a free function near the top of src/Interpreter.cpp) is the single source of truth for what "equal" means — the ==/!= operators, switch/case matching, and the contains()/indexOf() natives all call it, so a fix or a new value kind only needs handling in one place.
Internals since beta-2.0.0, for contributors:
break/continue/returnare no longer C++ exceptions:Interpreter::execute()returns aSignal(NONE,BREAK,CONTINUE,RETURN) that loops and calls check. Only real errors use exceptions (LineErrorfor interpreter errors,ThrownValueforthrow).- Names are interned into integers (
SymbolsinEnvironment.hpp) and scopes are small flat vectors, with a hash index only for big scopes such as the globals. A block that declares nothing doesn't get its own scope at all. - Classes are
ClassObjand objectsInstanceObj(Value.hpp); method calls go throughInterpreter::callMethod, which also implements uniform call syntax. - The standard library added in 2.0 lives in
src/Stdlib.cpp(defineNativesV2()). main.cppruns scripts on a thread with a 512 MB stack, so deep recursion reaches the interpreter's own depth limit (10,000 calls) instead of crashing.tests/*.tonwithtests/*.expectedis the regression suite:make test(also run by CI).
Together these make typical scripts about 6x faster than beta-1.x (a fib(25) plus a 1,000,000-iteration loop: 2.0 s → 0.3 s).