Learn X++ in 15 minutes
X++ reads like the pseudocode you already write in a notebook — and it runs natively. This page teaches the whole language. Every snippet below has a ▶ playground button: run it in your browser, edit it, break it.
What is X++? #
X++ is an intent-driven language: you write strict pseudocode, type one command, and it runs. Under the hood it's a real engine — a zero-dependency C++17 virtual machine (ZITR), a bytecode compiler (ZCOM), and a native AOT backend that compiles your program through C++ to machine code (ZJIT).
The design rule that shapes everything: no types to annotate, no pointers, no manual memory management, no build files. The VM garbage-collects automatically.
Your first program #
Install X++ (see the home page), open any editor, and write:
RNM=ZITR out "hello world"
Then run it with one command:
x run hello.xp
hello world
The first line, RNM=ZITR, is a directive that picks the engine. It's optional — x auto-detects it and defaults to the native VM. Everything after a # is a comment.
Printing — out #
out prints values separated by spaces, one line at a time:
RNM=ZITR
out "hello", "world"
out 1, 2, "three"
out [1, 2, 3]
out {"name": "xpp", "version": "0.4.1"}
hello world
1 2 three
[1, 2, 3]
{"name": xpp, "version": 0.4.1}
(Yes — string values inside lists and dicts print without quotes. That's the native VM's display style, faithfully reproduced here.)
Variables & values #
Assign with =. No declarations, no types. Reading a variable that was never set gives nil:
RNM=ZITR name = "Aagastya" version = 0.4 awesome = true missing = nothing_here out name, version, awesome, missing out type(name), type(version), type(awesome), type(missing)
Aagastya 0.4 true nil string float true nil
- Ints and floats — 7 / 2 is 3.5 (true division); 2 + 2 stays an int.
- Booleans — true / false.
- nil — the “nothing” value; it is falsy, and it propagates through arithmetic (nil + 1 is nil).
- Empty things are falsy: 0, "", [], {}, nil.
Numbers & operators #
RNM=ZITR
out 7 + 3, 7 - 3, 7 * 3
out 7 / 2 # true division -> float
out 7 % 3 # modulo (Python floor semantics)
out 2 ** 10 # power
out 2 ** 0.5
out -7 % 3
out 1 < 2, 2 <= 2, 3 != 4
out min(4, 2, 9), max(4, 2, 9)
out abs(-5), int("42"), float("2.5")
10 4 21 3.5 1 1024 1.4142135623731 2 true true true 2 9 5 42 2.5
and / or short-circuit and return the deciding operand (like Python):
RNM=ZITR out true and 5 out false or "fallback" out not nil, not 0, not ""
5 fallback true true true
Strings #
Double or single quotes, + to join, * to repeat, indexing with negatives:
RNM=ZITR s = "hello" out s + " world" out s * 2 out s[0], s[-1] out len(s) out contains(s, "ell") out str(42) + "!"
hello world hellohello h o 5 true 42!
Escapes work as expected: \n, \t, \\, \".
Lists #
Lists are ordered, growable, and hold anything:
RNM=ZITR nums = [1, 2, 3] push 4 to nums # append nums[0] = 99 out nums out nums[0], nums[-1] out len(nums), sum(nums) out nums + [7, 8] # concatenate out [1, 2] * 2 # repeat out sorted([3, 1, 2]) out contains(nums, 99) out pop(nums) # removes & returns the last item out nums
[99, 2, 3, 4] 99 4 4 108 [99, 2, 3, 4, 7, 8] [1, 2, 1, 2] [1, 2, 3] true 4 [99, 2, 3]
Dicts #
Dicts map string keys to values and keep insertion order. Use ["key"] or dot access:
RNM=ZITR
user = {"name": "Aagastya", "lang": "X++"}
user.version = "0.4.1" # dot assignment
user["os"] = "Windows" # bracket assignment
out user
out user.name # dot read
out user["lang"] # bracket read
out len(user)
out keys(user)
out values(user)
out contains(user, "os")
out get(user, "missing") # nil if absent
out get(user, "missing", "nope") # with a default
{"name": Aagastya, "lang": X++, "version": 0.4.1, "os": Windows}
Aagastya
X++
4
[name, lang, version, os]
[Aagastya, X++, 0.4.1, Windows]
true
nil
nope
Decisions — if / elif / else #
Blocks open with : and close with end. Indentation is for humans — the VM doesn't require it.
RNM=ZITR
fn classify(n):
if n < 0:
return "negative"
elif n == 0:
return "zero"
elif n < 10:
return "small"
else:
return "big"
end
end
loop n in [-5, 0, 7, 99]:
out n, "->", classify(n)
end
-5 -> negative 0 -> zero 7 -> small 99 -> big
Loops #
Three loop flavours, plus break and continue:
RNM=ZITR
# 1) counting loop — inclusive of both ends, optional step
loop i from 1 to 10 step 3:
out i
end
# 2) for-each over a list (or a string!)
loop fruit in ["apple", "banana", "cherry"]:
out "I like", fruit
end
# 3) while — with break and continue
i = 0
while true:
i = i + 1
if i > 6:
break
end
if i % 2 == 0:
continue
end
out i, "is odd"
end
1 4 7 10 I like apple I like banana I like cherry 1 is odd 3 is odd 5 is odd
Negative steps count down: loop i from 10 to 0 step -2: gives 10, 8, 6, 4, 2, 0.
Functions #
Define with fn, return with return (returns nil if you don't). Functions are available everywhere — even before their definition — and recursion is a first-class citizen:
RNM=ZITR
out shout("x++") # called before the definition below
fn shout(word):
return word + "!"
end
fn factorial(n):
if n <= 1:
return 1
end
return n * factorial(n - 1)
end
out factorial(10)
# mutual recursion works too
fn is_even(n):
if n == 0:
return true
end
return is_odd(n - 1)
end
fn is_odd(n):
if n == 0:
return false
end
return is_even(n - 1)
end
out is_even(10), is_odd(7)
x++! 3628800 true true
Errors — safe / fail #
Wrap risky code in safe: … fail e: … end. Errors propagate out of nested function calls until a safe catches them:
RNM=ZITR
fn risky(n):
return 100 / n # division by zero is a runtime error
end
loop n in [4, 0]:
safe:
out "100 /", n, "=", risky(n)
fail e:
out "caught:", e
end
end
100 / 4 = 25 caught: division by zero
Uncaught errors stop the program with X++ [ZITR] error: ….
Input & files #
in reads one line (optionally with a prompt); read loads a whole file; write(…) saves one:
RNM=ZITR # in the playground, `in` reads from the Input box name = in "What is your name? " n = int(in "Favourite number? ") out "Hello,", name if n % 2 == 0: out n, "is even" else: out n, "is odd" end
What is your name? Favourite number? Hello, Aagastya 42 is even
RNM=ZITR
# in the browser this uses a sandboxed in-memory disk —
# natively it is your real filesystem
write("notes.txt", "X++ makes pseudocode real")
out read "notes.txt"
X++ makes pseudocode real
Builtins reference #
Every builtin can be called like a function; a few (out, push … to, in, read) also have friendly statement forms.
| Builtin | Does | Example |
|---|---|---|
out a, b / print(a, b) | print values, space-separated, then a newline | out "sum:", 5 |
in "prompt" / input() | read one line (prints the prompt inline) | name = in "name? " |
len(x) | length of string / list / dict | len("abc") → 3 |
str(x) · int(x) · float(x) | convert types | int("42") → 42 |
push v to lst / push(lst, v) · append | append to a list | push 4 to nums |
pop(lst[, i]) | remove & return item (default: last) | pop(nums) |
get(x, k[, d]) · set(x, k, v) | safe read / write with defaults & upsert | get(d, "k", 0) |
contains(x, v) | membership in list / dict / string | contains(s, "hi") |
keys(d) · values(d) | dict key / value lists | keys(user) |
range(n) / range(a, b[, s]) | list of ints (exclusive end) | range(3) → [0, 1, 2] |
sorted(lst) · min(…) · max(…) · sum(lst) | sorting & aggregation | sorted([3,1,2]) |
abs(x) · bool(x) · type(x) | misc conversions | type(1.5) → float |
read "path" · write(path, text) | file I/O | read "data.txt" |
clock() | seconds since your program started | out clock() |
exit(code) | stop the program | exit(0) |
Run modes & RNM #
The RNM=… directive at the top of a file picks an engine. Missing or unknown? x auto-detects and defaults to ZITR, the native VM.
| Directive | Engine | Needs | Use when |
|---|---|---|---|
RNM=ZITR | Native stack VM (default) | xppvm (auto-built) | Everyday running |
RNM=ZCOM | Bytecode AOT | xppvm | You want to inspect/port .xbc |
RNM=ZJIT | Native AOT (fastest) | xppvm + C++ compiler | Benchmarks & hot loops |
RNM=XCOM / RNM=XITR | Legacy v0.3 engines | Python | Old programs |
RNM=ITR | AI intent compiler | OpenRouter key | Turning English steps into code |
x run app.xp # native VM (auto ZITR) x run app.xp --mode ZJIT # native AOT — fastest x compile app.xp --emit-xbc app.xbc x disasm app.xbc # see the bytecode xppvm zitr app.xp # run with no Python at all
The web playground #
The playground runs the same strict-pseudocode language entirely in your browser — a faithful port of the ZITR VM semantics, quirks included. A few honest differences:
- It's an interpreter, not the native VM — great for learning, ~100× slower than ZJIT for hot loops.
- Recursion depth is limited to roughly 1,500 frames (the native VM handles 20,000+ with its flat dispatch loop).
- read / write use a sandboxed in-memory disk instead of your filesystem.
- Runs time out after 8 seconds to keep infinite loops from freezing the page.
When your program outgrows the sandbox, install the real thing ↗ — same language, real speed.