Skip to content

Language Reference

This page is the language at a glance — every form Tulpar’s parser accepts, in one place. The Language Guide walks through each topic with examples; this is the spec-ish summary for when you already know what you’re looking for.

A .tpr file is a sequence of top-level statements: imports, function declarations, struct (type) declarations, and any other expression or control-flow statement that runs immediately when the program starts. There is no main() requirement — top-level statements execute in source order.

import "wings"; // import (top-level only)
type Point { // struct declaration
int x;
int y;
}
func origin(): Point { // function declaration
Point p = { x: 0, y: 0 };
return p;
}
print(origin()); // top-level expression — runs at startup
TypeLiteral exampleNotes
int42, -1, 0xff64-bit signed.
float3.14, 2.0, -0.564-bit IEEE 754.
booltrue, false
str"hi", t"x={n}"UTF-8. t"..{expr}.." interpolates; + coerces.
json{"k": 1}, [1, 2, 3]Tagged-union for objects/arrays/scalars at runtime.
array<T>[1, 2, 3]Tint / float / str / bool / json.
voidFunction return only — no values of type void.
<TypeName>Point p = { x: 1, y: 2 };User-declared via type.

var (and its alias let, untyped) and null are also keywords — var declares a binding the type-inferer fills in, null is the absence value for json slots and unset variables.

String interpolation. t"total: {n} adet" is a t-string: each {expr} is evaluated and stitched in. It always yields a str (even t"{n}"); write a literal brace as \{ / \}. Plain + also works and coerces — "n=" + 5 is "n=5", no toString() needed.

int x = 10; // typed, with initializer
str name = "Hamza";
bool flag; // typed, default-initialised (0 / "" / false / null)
var n = 7; // type inferred from initializer
let count = 0; // synonym for `var`

let and var are interchangeable — let is conventional for “won’t change” but the language doesn’t enforce immutability.

CategoryOperators
Arithmetic+, -, *, /, %, unary -
Comparison==, !=, <, <=, >, >=
Logical&&, ||, unary !
Assignment=, +=, -=, *=, /=
Incrementx++, x-- (statement form only — not an expression)
Subscriptarr[i], obj["key"], obj.key (sugar for ["key"])
Callf(args), mod.func(args) (mod-qualified import)

Precedence (highest first):

  1. Postfix: call (...), subscript [...], member ., increment ++/--
  2. Unary: -, !
  3. *, /, %
  4. +, -
  5. <, <=, >, >=
  6. ==, !=
  7. &&
  8. ||
  9. Assignment: =, +=, …
if (cond) { ... }
if (cond) { ... } else if (other) { ... } else { ... }
while (cond) { ... }
for (int i = 0; i < n; i = i + 1) { ... }
// `i = i + 1` is also valid as `i++` or `i += 1`
// for-each — iterate an array, a string's chars, or range(n).
// Parens are required; the loop variable is inferred (no type, no `var`).
for (x in [10, 20, 30]) { print(x); }
for (i in range(n)) { print(i); }
break; // exit innermost loop
continue; // jump to next iteration
return; // exit function (void)
return expr; // exit function with value
// match — Rust-style; an *expression* (yields a value).
str grade = match score {
90 => "A", // literal
1 | 2 | 3 => "low", // alternatives
10 .. 20 => "mid", // inclusive range
_ => "other" // wildcard
};
// Also valid in statement position with block arms:
match cmd { "go" => { run(); }, _ => { stop(); } }
// Destructuring — a bare identifier BINDS, a literal CONSTRAINS, `_`
// ignores, `..rest` captures the array tail. Patterns nest arbitrarily.
match arr {
[] => "empty",
[0, second] => "leading zero", // 0 constrains, second binds
[head, ..tail] => "head + tail",
};
match user { // json object / struct fields
{role: "admin", name} => "admin " + name,
{name} => "user " + name,
};
match shape { // typed-struct variant patterns
Circle{r: 0} => "point",
Rect{w, h} => w * h,
};
match req { {user: {id}, path} => handle(id, path), _ => skip() }; // nested

match evaluates its subject once and takes the first matching arm. Patterns are scalar literals (int / string / bool), |-alternatives, lo .. hi ranges, and destructuring of arrays ([a, b], [head, ..tail]), JSON objects / struct fields ({role: "admin", name}), and typed-struct variants (Circle{r}) — which nest. _ is the catch-all.

// Positional parameters; type before name.
func add(int a, int b): int {
return a + b;
}
// Return type omitted = void (or json if you return one).
func greet(str name) {
print("Hello, " + name);
}
// Recursion is supported (forward references resolved automatically).
func fib(int n): int {
if (n <= 1) { return n; }
return fib(n - 1) + fib(n - 2);
}

Function-by-name dispatch (call("name")) looks up the symbol at runtime; useful for routers / tables-of-handlers.

// Lambda: `=>` arrow, expression or block body.
var add = (int a, int b) => a + b; // add(2, 3) == 5
var sq = (int x) => { return x * x; }; // sq(5) == 25
// Closures capture enclosing parameters/locals (heap env), so factories
// and mutable counters work. Captures are independent per instance.
func make_adder(int n) { return (int x) => n + x; }
var add10 = make_adder(10); // add10(5) == 15
func make_counter() {
int c = 0;
return () => { c = c + 1; return c; };
}
var tick = make_counter(); // tick() -> 1, 2, 3, ...

Closures are fully supported on the AOT path (default tulpar run / build).

type Point {
int x;
int y;
}
// Literal initialisation.
Point p = { x: 3, y: 4 };
print(p.x); // 3
// Pass by value; modify a copy.
func translate(Point p, int dx, int dy): Point {
Point q = { x: p.x + dx, y: p.y + dy };
return q;
}

Struct fields are accessed with . (sugar for ["field"]). == on structs is currently field-by-field (json behaviour).

json o = { "name": "Hamza", "age": 24, "tags": ["admin", "user"] };
print(o["name"]); // "Hamza"
print(o.tags[0]); // "admin"
o["age"] = 25; // mutate
o["new_field"] = true; // add
push(o.tags, "verified"); // helper builtins
// Arrays:
array<int> nums = [1, 2, 3];
push(nums, 4);
int n = length(nums); // 4

json is the runtime tagged-union — the same value can be an object, array, string, number, or null.

import "wings"; // brings every top-level func into scope
import "wings" as w; // namespaced — call as `w.listen(...)` or `w__listen(...)`
import "./local/util.tpr"; // path import (resolves relative to source)

Resolution order: literal path → path.tprtulpar_modules/<name>/<name>.tprtulpar_modules/<name>.tpr → embedded stdlib. See Package Manager for the lockfile + version specs.

try {
risky();
} catch (e) {
print("oh no: " + toString(e));
} finally {
cleanup();
}
throw "error message";
throw {"code": 500, "msg": "boom"};

catch (e) binds the thrown value to e (a json) regardless of its shape. finally always runs, even on uncaught throws.

async func fetch(int id) { // returns a promise immediately
await sleep_async(20); // non-blocking; other tasks run meanwhile
return id * 2;
}
var p = fetch(21); // queued, not run yet
int r = await p; // drive it → 42
// gather: await many concurrently, results in argument order
var all = await gather(fetch(1), fetch(2));

async funcs are stackful coroutines on a single-threaded event loop; calling one queues a task and returns a promise, await drives it. An uncaught throw rejects the promise and await re-raises it, so ordinary try / catch catches async errors. Up to 16 parameters. See the Async / Await guide for the full model, including the non-blocking HTTP client.

// Single line — runs to EOL.
/* Block comment — does not nest. */
async await break catch continue do
else false finally for func if
import in let match null return
throw true try type var void
while

Type-name keywords (int, float, str, bool, json, array) are also reserved when used as types but can appear inside identifiers via underscore (int_value, array_size).

CommandEffect
tulpar foo.tprAOT-compile + run, fall back to VM on AOT error.
tulpar --vm foo.tprBytecode VM — faster startup, slower steady-state.
tulpar build foo.tpr [out]Standalone native binary.
tulpar --replInteractive prompt (VM-backed).
tulpar fmt foo.tpr [-w]Source formatter.
tulpar pkg <subcmd>Package manager — see Package Manager.
tulpar --lspLSP server on stdio (used by editor extensions).
tulpar typecheck foo.tprStandalone type-checker (also runs as a build pre-pass).
tulpar update [--check]Self-update from the official release.

The full CLI reference, including environment variables, lives at CLI Reference.

The Language Guide covers the same surface with runnable examples and motivation; this page is the lookup table you keep open in another tab. If something here looks wrong, the guide pages and the source (src/parser/parser.cpp + src/lexer/lexer.cpp) are the authoritative answer.