Introduction

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.

Same pseudocode, three engines. A program written for v0.3 runs unchanged on the native VM today — and the exact same source also runs in this website's playground.

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
Word-counting classic. Dicts + loop … in + contains is the bread and butter of X++ scripts — see the examples page.

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
Scoping is Python-like: a name assigned anywhere inside a function is local to it; every other name resolves to the global scope. The native VM recurses 20,000+ frames deep thanks to its flat dispatch loop.

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
Note: in prints its prompt inline, without a newline, the moment it reads — so both prompts above appear before the outputs. That's exactly what the native VM does in a terminal.
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.

BuiltinDoesExample
out a, b / print(a, b)print values, space-separated, then a newlineout "sum:", 5
in "prompt" / input()read one line (prints the prompt inline)name = in "name? "
len(x)length of string / list / dictlen("abc") → 3
str(x) · int(x) · float(x)convert typesint("42") → 42
push v to lst / push(lst, v) · appendappend to a listpush 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 & upsertget(d, "k", 0)
contains(x, v)membership in list / dict / stringcontains(s, "hi")
keys(d) · values(d)dict key / value listskeys(user)
range(n) / range(a, b[, s])list of ints (exclusive end)range(3) → [0, 1, 2]
sorted(lst) · min(…) · max(…) · sum(lst)sorting & aggregationsorted([3,1,2])
abs(x) · bool(x) · type(x)misc conversionstype(1.5) → float
read "path" · write(path, text)file I/Oread "data.txt"
clock()seconds since your program startedout clock()
exit(code)stop the programexit(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.

DirectiveEngineNeedsUse when
RNM=ZITRNative stack VM (default)xppvm (auto-built)Everyday running
RNM=ZCOMBytecode AOTxppvmYou want to inspect/port .xbc
RNM=ZJITNative AOT (fastest)xppvm + C++ compilerBenchmarks & hot loops
RNM=XCOM / RNM=XITRLegacy v0.3 enginesPythonOld programs
RNM=ITRAI intent compilerOpenRouter keyTurning 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.