All docs

Internals

How ROT actually works. A nine-line program — counting from 1 to 3 with a Fizz on the third — walked through every stage of the pipeline, end to end.

Watch the pipeline

Every stage that this page describes — read, parse, run — is what you see below. The animation loops through fourteen statement-executions of a tiny FizzBuzz, captured from the same snapshot model the playground exposes.

Live demostep 1/14
Source
1i = 1
2while (i <= 3) {
3 if (i == 3) {
4 coutln("Fizz")
5 } else {
6 coutln(i)
7 }
8 i = i + 1
9}
Output
(waiting…)
Readtokens
i=1
Parseast node
Assign(i, 1)
Runeffect
i←1

Source — characters

The whole program is just text. Whitespace matters only inside strings; ROT uses C-style braces, not Python-style indentation.

demo.rot
i = 1
while (i <= 3) {
    if (i == 3) {
        coutln("Fizz")
    } else {
        coutln(i)
    }
    i = i + 1
}

Nothing is happening yet. The file is bytes on disk. Two things will turn it into something runnable: a lexer that groups characters into tokens, and a parser that groups tokens into a tree.

Lexer — tokens

The lexer reads characters one at a time. When it sees a digit it consumes more digits until something stops being a digit, and emits a NUMBER token. When it sees a letter it consumes an identifier, then checks the keyword table to decide if it's WHILE or just IDENT. Every token carries its source line and column.

Lexerlive
characters
coutln("hi")
tokens
A char-by-char tokenizer. Groups characters into typed tokens that carry line + column.
tokens (abbreviated)
IDENT(i) OP(=) NUMBER(1)
WHILE LPAREN IDENT(i) LE NUMBER(3) RPAREN LBRACE
IF LPAREN IDENT(i) EQ NUMBER(3) RPAREN LBRACE
IDENT(coutln) LPAREN STRING("Fizz") RPAREN
RBRACE ELSE LBRACE
IDENT(coutln) LPAREN IDENT(i) RPAREN
RBRACE
IDENT(i) OP(=) IDENT(i) OP(+) NUMBER(1)
RBRACE

Source: rot/lexer.py — around 360 lines, no regex, no parser-generator. Just a big while loop and a character-dispatch table.

Parser — AST

The parser turns the flat token stream into a tree of typed nodes — an abstract syntax tree. Statements use recursive descent (one function per grammar rule). Expressions use Pratt parsing, which handles operator precedence cleanly without a separate precedence table.

Parserlive
tokens
1+2*3
ast
(waiting…)
Recursive descent for statements, Pratt for expressions. Operator precedence falls out naturally.
AST for the demo
Program
└── statements
    ├── Assign(target=Var("i"), value=Lit(1))
    └── WhileStmt(
        cond = BinaryOp("<=", Var("i"), Lit(3)),
        body = Block([
            IfStmt(
                cond   = BinaryOp("==", Var("i"), Lit(3)),
                then   = Block([Call(Var("coutln"), [Lit("Fizz")])]),
                else_  = Block([Call(Var("coutln"), [Var("i")])])
            ),
            Assign(
                target = Var("i"),
                value  = BinaryOp("+", Var("i"), Lit(1))
            )
        ])
    )

Source: rot/syntax.py. Every node is a @dataclass with line and column fields, so runtime errors can point back to the exact source position.

Interpreter — snapshots

The tree-walking interpreter visits each AST node in order, and executes it. Statements like Assign mutate the environment; Call pushes a new frame onto the scope chain; If evaluates its condition and dispatches.

For the playground, the interpreter also yields a snapshot after every statement — a frozen view of the scope chain, accumulated stdout, and the source position. That snapshot list is what the “Step” button walks through.

Source: rot/interpreter.py — about 1,100 lines. The fast path (execute()) is the default; the snapshot path (iter_execute()) is opt-in and powers the playground.

Bytecode — opcodes

ROT also ships an opt-in bytecode compiler and stack VM. The compiler lowers the same AST into a flat array of 38 opcodes; the VM executes them with a value stack and a frame stack — the same model CPython, Lua, and the JVM use, just smaller.

VMlive
bytecode
  1. 0 LOAD_CONST 42
  2. 1 STORE_NAME i
  3. 2 LOAD_NAME i
  4. 3 RETURN
stack
empty
env
(empty)
Bytecode runs on a stack machine — same shape as CPython, Lua, or the JVM, just smaller.
compiled chunk (excerpt)
# the i = 1 line compiles to:
0   LOAD_CONST    1
2   STORE_NAME    i

# while (i <= 3) { ... } compiles to:
4   LOAD_NAME     i
6   LOAD_CONST    3
8   LE
9   JUMP_IF_FALSE  <end-of-loop>
...

Source: rot/codegen.py and rot/vm.py. Try it from the CLI with python -m rot --vm examples/fizzbuzz.rot.

Output — stdout

cout and coutln write to a captured buffer that the playground streams back to the browser, one chunk at a time. There's no magic: the implementation is print(...) into a StringIO.

Where to look next