Language reference

Docs

Every section is a runnable example with the minimum prose needed to read it. Click Run on any block to execute it inline — no install. Reach for the Internals doc to see how the language is implemented.

Getting started

ROT runs on Python 3.9 or newer. Two ways to grab it:

# clone the repo (no PyPI package yet)
git clone https://github.com/omkarxpatel/ROT.git
cd ROT

# run a program
python -m rot examples/fizzbuzz.rot

# start the REPL
python -m rot

# trace the pipeline (tokens + AST + timings)
python -m rot --trace examples/hello.rot

The fastest sandbox is the browser playground — no install, Pyodide runs the real Python package in WASM.

Hello world

coutln prints with a trailing newline; cout doesn't. Click Run on the block below to see it.

The smallest ROT program.
hello.rot
coutln("hello, world")

Variables

Bare = walks the scope chain to find an existing binding — that's how closures mutate enclosing state. let name = expr always declares a fresh local in the current scope.

Bare assignment vs. let: see what the inner function returns.
vars.rot
funct outer() {
    z = 1
    funct inner() {
        z = z + 1     // chain-walks; mutates outer's z
        let z = 99    // creates a NEW local z that shadows
        return z      // returns the local (99), not the outer
    }
    coutln(inner())
    coutln(z)         // outer's z was bumped to 2 by inner
}

outer()
Why two forms?

Many languages tie scope to declaration syntax (let / var /const). ROT splits it differently: a bare assignment finds an existing binding; let creates one. Closures mutate enclosing state by default — no Python-stylenonlocal declaration needed.

Builtins are frozen

pi = 3.0 raises at runtime; the builtin layer rejects re-binding. Shadow locally with let pi = 3.0 if you really need it.

Literals

Numbers, strings, booleans, lists, and dicts. Note the | separator inside collections — ROT inherits Python's comma usage everywhere else.

literals.rot
n = 42
f = 3.14
s = "hello"
t = 'world'
ok = true
nothing = null

xs = [1 | 2 | 3]
d = {"name": "ada" | "age": 36}

coutln(n)
coutln(f)
coutln(f"{s}, {t}")
coutln(xs)
coutln(d)
coutln(ok)
coutln(nothing)
Why `|` and not `,`?

Commas are used in argument lists and statements; the language uses | as the list / dict / param separator to keep parsing dead-simple and unambiguous. There's no precedence-juggling between commas in different contexts.

Operators

Familiar surface, no integer division (yet).

// arithmetic
a + b   a - b   a * b   a / b   a % b

// comparison
a == b   a != b   a < b   a <= b   a > b   a >= b

// logical
a and b   a or b   not a

// compound assignment
x += 1   x -= 1   x *= 2   x /= 2   x %= 3

// unary
-x   not flag

Control flow

elseif and the two-word else if both work. break and continue are lexically scoped to loops in the same function body — they can't escape across a call.

Branches, while + continue/break, for-in.
control_flow.rot
x = 0
if (x > 0) {
    coutln("positive")
} elseif (x < 0) {
    coutln("negative")
} else {
    coutln("zero")
}

i = 0
while (i < 6) {
    if (i == 3) { i += 1 continue }
    if (i == 5) { break }
    coutln(i)
    i += 1
}

for word in ["a" | "b" | "c"] {
    coutln(word)
}
Step through it visually

Open this in the playground and switch to Animate mode to step through every iteration, seeing the env update each time around the loop.

Functions

funct declares a function. Parameters are separated by |. Closures capture by reference — they can mutate enclosing scope unless you use let to shadow.

A function and a closure that remembers its own counter.
functions.rot
funct add(a | b) {
    return a + b
}

coutln(add(2 | 3))

funct make_counter() {
    count = 0
    funct tick() {
        count += 1
        return count
    }
    return tick
}

c = make_counter()
coutln(c())
coutln(c())
coutln(c())
How does the closure remember `count`?

When make_counter runs, it builds a function value that captures a reference to count in its enclosing scope. Every time you call c(), it walks the scope chain, finds the same count, and updates it. See it happen live in the playground (Animate mode shows the scope chain growing).

Classes

class declares a class. init is the constructor. The method receiver is this, not self. Inheritance is not yet supported — super is reserved and produces a clear error.

A small mutable Counter. Watch the field update on each tick.
classes.rot
class Counter {
    init(start) {
        this.count = start
    }
    tick() {
        this.count += 1
        return this.count
    }
    to_string() {
        return f"Counter({this.count})"
    }
}

c = Counter(0)
c.tick()
c.tick()
coutln(c)
`this`, not `self`

ROT uses C++/Java-style this as the implicit receiver. It's bound automatically inside method bodies — no self parameter to declare.

No inheritance yet

Classes are single-level. super is reserved (so when inheritance lands, syntax won't shift), but using it today errors with a clear message. Compose instead: hold one instance inside another.

Error handling

try / catch / finally with the expected semantics — finally runs even when return, break, continue, or a re-throw fires inside the try. throw can carry any value.

`finally` always runs — even when the try body returns.
errors.rot
funct parse(s) {
    try {
        return num(s)
    } catch (e) {
        coutln(f"bad input: {e}")
        return null
    } finally {
        coutln("done")
    }
}

parse("not a number")

// throw any value — dicts, instances, primitives
try {
    throw {"code": 42 | "msg": "boom"}
} catch (err) {
    coutln(err["msg"])
}
Throws carry values, not types

Unlike Java's typed exception hierarchy, a throw in ROT carries any value (number, string, dict, instance) and catch (e) binds it. Pattern-match on its shape with regular if-statements.

Imports

import "path" evaluates another .rot file in a fresh module scope and binds the resulting namespace to the file's basename. Paths are relative to the importing file. Modules are cached, so circular imports terminate.

// math_utils.rot
funct square(x) { return x * x }
PI = 3.14159

// main.rot
import "math_utils"
coutln(math_utils.square(5))
coutln(math_utils.PI)
CLI-only

Imports need a real filesystem with a second .rot file, so they only work from the CLI. The browser playground doesn't expose virtual-filesystem writes yet.

F-strings

Prefix a string with f to interpolate expressions inside {...}. The full Python format-spec mini-language is supported because the desugaring routes through Python's built-in format().

fstrings.rot
name = "ada"
coutln(f"hello, {name}")

pi = 3.14159
coutln(f"pi rounded: {pi:.2f}")

n = 7
coutln(f"[{n:>5}]")
coutln(f"hex {255:x}")
coutln(f"bin {10:08b}")

xs = [1 | 2 | 3 | 4 | 5]
coutln(f"first three: {xs[:3]}")

Slicing

Strings and lists support s[a:b] and s[a:b:c]. Negative bounds wrap from the end, out-of-range bounds clamp, the step controls direction ([::-1] reverses). Dicts can't be sliced and report a clean error.

slicing.rot
s = "hello, world"
coutln(s[7:])
coutln(s[:5])
coutln(s[::-1])

xs = [10 | 20 | 30 | 40 | 50]
coutln(xs[1:4])
coutln(xs[::2])
coutln(xs[-2:])

Builtins reference

About 35 builtins ship with the interpreter, organized by purpose. The full set is defined in rot/builtins.py.

I/O

NameArityDescriptionExample
cout1+Print without trailing newline.cout("hi ")
coutln1+Print with trailing newline.coutln("hello")
input0-1Read a line from stdin (optional prompt).input("name? ")
read_file1Read a text file as a string (UTF-8).read_file("notes.txt")
write_file2Write a string to a file (UTF-8).write_file("out.txt" | "ok")

Conversion

NameArityDescriptionExample
str1Convert any value to its rot-style string form.str(42)
num1Parse a string into a number; raises on bad input.num("3.14")
chr1Codepoint integer to a single-char string.chr(65)
ord1Single-char string to its codepoint integer.ord("A")

Math

NameArityDescriptionExample
abs1Absolute value.abs(-3)
min1+Minimum of arguments / iterable.min(1 | 2 | 3)
max1+Maximum of arguments / iterable.max(xs)
pow2Power.pow(2 | 10)
sqrt1Square root.sqrt(16)
floor1Round down to integer.floor(3.7)
ceil1Round up to integer.ceil(3.2)
round1-2Round to nearest (optional ndigits).round(3.14159 | 2)
piconstantThe constant pi.pi
econstantThe constant e.e

Collections

NameArityDescriptionExample
len1Length of a string, list, or dict.len(xs)
range1-3Integer range as a list.range(1 | 10)
append2Append to a list in place; returns the list.append(xs | 4)
pop1-2Pop and return last (or given index).pop(xs)
sum1Sum a list of numbers.sum([1 | 2 | 3])
sorted1Return a new sorted copy.sorted(xs)
reversed1Return a reversed copy.reversed(xs)
keys1Dict keys as a list.keys(d)
values1Dict values as a list.values(d)
items1Dict items as a list of pairs.items(d)

Type introspection

NameArityDescriptionExample
type1Type name as a string.type(42) // "num"
is_num1True if value is a number.is_num(3.14)
is_str1True if value is a string.is_str("hi")
is_list1True if value is a list.is_list([1])
is_dict1True if value is a dict.is_dict({})
is_bool1True if value is a boolean.is_bool(true)
is_null1True if value is null.is_null(null)
is_func1True if value is a function.is_func(coutln)

Random

NameArityDescriptionExample
rand_int2Random integer in [lo, hi].rand_int(1 | 6)
rand_float0-2Random float (default [0, 1)).rand_float()
seed1Seed the RNG for reproducibility.seed(42)

Control

NameArityDescriptionExample
assert1-2Raise if false; optional message.assert(x > 0 | "must be positive")
exit0-1Exit the program (optional code).exit(1)

The REPL

python -m rot with no file starts the REPL. Multi-line input is supported (open braces and strings keep accepting more lines). History persists in ~/.rot_history. Type exit, quit, or :q to leave.

$ python -m rot
ROT 2.25.x  |  Ctrl-D / exit / :q to quit
> x = 5
> x * x
25
> funct fact(n) {
...     if (n <= 1) { return 1 }
...     return n * fact(n - 1)
... }
> fact(6)
720
> :q

Error messages

Runtime errors carry source coordinates and render in a rustc style — the offending source line, a caret, and a hint when the failure looks like a Python-ism. Click Run below to trigger one.

Try common Python-isms — the error block calls them out by name.
errors.rot
// 'print' is Python's stdout function — ROT uses cout / coutln.
print("hello")
What you'll see

The formatted block prints with the offending source line, a caret under the failing identifier, and a note suggestingcout. The same shape applies to arity mismatches, attribute lookups, and type errors.

Reserved words

The full keyword table lives in rot/keywords.py.

KeywordRole
functDeclare a function.
letDeclare a fresh local binding (opt out of chain-walking).
returnReturn a value from a function.
ifConditional.
elseifElse-if (one-word form).
elseFallthrough branch (also pairs with `if` as two-word form).
whileWhile loop.
forFor-in loop.
inPairs with `for`.
breakBreak out of the nearest loop in the current function.
continueSkip to the next iteration.
classDeclare a class.
thisImplicit receiver inside method bodies.
tryBegin a try block.
catchCatch a thrown value.
finallyAlways-runs block.
throwThrow any value.
importImport another .rot file.
andLogical AND.
orLogical OR.
notLogical NOT.
trueBoolean true.
falseBoolean false.
nullNull value.
coutPrint without newline.
coutlnPrint with newline.
superReserved for inheritance (not yet implemented).

Examples

Seven runnable programs live in examples/ alongside golden .expected outputs. Each card below deep-links to the playground with the example pre-loaded.