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.rotThe 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.
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.
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()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.
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.
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)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 flagControl 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.
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)
}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.
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())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.
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)ROT uses C++/Java-style this as the implicit receiver. It's bound automatically inside method bodies — no self parameter to declare.
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.
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"])
}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)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().
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.
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
| Name | Arity | Description | Example |
|---|---|---|---|
| cout | 1+ | Print without trailing newline. | cout("hi ") |
| coutln | 1+ | Print with trailing newline. | coutln("hello") |
| input | 0-1 | Read a line from stdin (optional prompt). | input("name? ") |
| read_file | 1 | Read a text file as a string (UTF-8). | read_file("notes.txt") |
| write_file | 2 | Write a string to a file (UTF-8). | write_file("out.txt" | "ok") |
Conversion
| Name | Arity | Description | Example |
|---|---|---|---|
| str | 1 | Convert any value to its rot-style string form. | str(42) |
| num | 1 | Parse a string into a number; raises on bad input. | num("3.14") |
| chr | 1 | Codepoint integer to a single-char string. | chr(65) |
| ord | 1 | Single-char string to its codepoint integer. | ord("A") |
Math
| Name | Arity | Description | Example |
|---|---|---|---|
| abs | 1 | Absolute value. | abs(-3) |
| min | 1+ | Minimum of arguments / iterable. | min(1 | 2 | 3) |
| max | 1+ | Maximum of arguments / iterable. | max(xs) |
| pow | 2 | Power. | pow(2 | 10) |
| sqrt | 1 | Square root. | sqrt(16) |
| floor | 1 | Round down to integer. | floor(3.7) |
| ceil | 1 | Round up to integer. | ceil(3.2) |
| round | 1-2 | Round to nearest (optional ndigits). | round(3.14159 | 2) |
| pi | constant | The constant pi. | pi |
| e | constant | The constant e. | e |
Collections
| Name | Arity | Description | Example |
|---|---|---|---|
| len | 1 | Length of a string, list, or dict. | len(xs) |
| range | 1-3 | Integer range as a list. | range(1 | 10) |
| append | 2 | Append to a list in place; returns the list. | append(xs | 4) |
| pop | 1-2 | Pop and return last (or given index). | pop(xs) |
| sum | 1 | Sum a list of numbers. | sum([1 | 2 | 3]) |
| sorted | 1 | Return a new sorted copy. | sorted(xs) |
| reversed | 1 | Return a reversed copy. | reversed(xs) |
| keys | 1 | Dict keys as a list. | keys(d) |
| values | 1 | Dict values as a list. | values(d) |
| items | 1 | Dict items as a list of pairs. | items(d) |
Type introspection
| Name | Arity | Description | Example |
|---|---|---|---|
| type | 1 | Type name as a string. | type(42) // "num" |
| is_num | 1 | True if value is a number. | is_num(3.14) |
| is_str | 1 | True if value is a string. | is_str("hi") |
| is_list | 1 | True if value is a list. | is_list([1]) |
| is_dict | 1 | True if value is a dict. | is_dict({}) |
| is_bool | 1 | True if value is a boolean. | is_bool(true) |
| is_null | 1 | True if value is null. | is_null(null) |
| is_func | 1 | True if value is a function. | is_func(coutln) |
Random
| Name | Arity | Description | Example |
|---|---|---|---|
| rand_int | 2 | Random integer in [lo, hi]. | rand_int(1 | 6) |
| rand_float | 0-2 | Random float (default [0, 1)). | rand_float() |
| seed | 1 | Seed the RNG for reproducibility. | seed(42) |
Control
| Name | Arity | Description | Example |
|---|---|---|---|
| assert | 1-2 | Raise if false; optional message. | assert(x > 0 | "must be positive") |
| exit | 0-1 | Exit 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
> :qError 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.
// 'print' is Python's stdout function — ROT uses cout / coutln.
print("hello")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.
| Keyword | Role |
|---|---|
| funct | Declare a function. |
| let | Declare a fresh local binding (opt out of chain-walking). |
| return | Return a value from a function. |
| if | Conditional. |
| elseif | Else-if (one-word form). |
| else | Fallthrough branch (also pairs with `if` as two-word form). |
| while | While loop. |
| for | For-in loop. |
| in | Pairs with `for`. |
| break | Break out of the nearest loop in the current function. |
| continue | Skip to the next iteration. |
| class | Declare a class. |
| this | Implicit receiver inside method bodies. |
| try | Begin a try block. |
| catch | Catch a thrown value. |
| finally | Always-runs block. |
| throw | Throw any value. |
| import | Import another .rot file. |
| and | Logical AND. |
| or | Logical OR. |
| not | Logical NOT. |
| true | Boolean true. |
| false | Boolean false. |
| null | Null value. |
| cout | Print without newline. |
| coutln | Print with newline. |
| super | Reserved 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.