The Centauri Language
A small, space-themed, statically-structured scripting language with a bytecode compiler and virtual machine written in Rust — shipping with a capability-based security model, a Runtime Application Self-Protection (RASP) engine, actor concurrency, OS-level multithreading, and a native compiler that bakes programs into standalone executables.
01Introduction #
What Centauri is, and the mental model to keep while reading the rest of this reference.
Centauri is a compact scripting language built around a single idea: code should be expressive like Python yet safe by construction. Every program is lexed, parsed, analyzed for capability violations, compiled to bytecode, and run on a virtual machine that enforces security at three layers. The vocabulary is deliberately space-themed — you launch a program, transmit output, group code in a system, define classes as capsules, and loop with orbit.
Secure by default
Filesystem, network, and process access are denied unless a permit grants them. Tainted data is scanned by the RASP engine before crossing any I/O boundary.
Compiled & fast
Source compiles to compact bytecode for a register-light VM, or all the way down to a standalone native executable.
Concurrent
An M:N actor scheduler runs every program; the proc module adds real OS threads, pools, mutexes, and atomics.
Batteries included
Pixel graphics with live windows, sound synthesis, a math library, HTTP networking, and a full OS toolkit (LaunchControl).
Your first program
Every Centauri program begins life inside a system block, and execution starts at its launch() function. Here is the canonical hello-world:
// hello.cnt — the smallest complete Centauri program.
// A `system` is the program container; `launch()` is the entry point.
system Main {
launch() {
transmit("hello, Centauri") // transmit() prints a value + newline
}
}▸ output: hello, Centauri
Reading tip. Features introduced in later updates are tagged inline: Phase 8 Phase 9 Phase 10. Everything else is part of the core language.
02Running Programs #
Two ways to go from a .cnt file to a running program.
Centauri source files use the .cnt extension. The command-line tool offers two verbs — one to interpret immediately, one to produce a native binary:
Centauri launch <file.cnt> # Interpret and run immediately
Centauri compile <file.cnt> [output] # Compile to a standalone native binaryWhat each verb does
launch walks the full pipeline and runs the result through the scheduler: it lexes the source into tokens, parses them into an AST, analyzes the program for capability violations, compiles it to bytecode, and finally executes it on the VM.
compile performs the same front-end work, then serializes the bytecode and links it with the runtime into a single native executable you can ship and run with no interpreter present.
# Run a script directly while you iterate
Centauri launch examples/bounce.cnt
# Produce a distributable binary called "bounce"
Centauri compile examples/bounce.cnt bounce
./bounce # runs with no interpreter installedPipeline at a glance: lex → parse → analyze (capability checks) → bytecode → VM scheduler. The capability analysis happens before any code runs, so an unpermitted file or network access is rejected at compile time, not mid-execution.
03Program Structure #
The top-level declarations a Centauri file may contain.
A Centauri file is a sequence of top-level declarations and statements. You will mix and match these building blocks:
| Declaration | Purpose |
|---|---|
system Name { … } | The conventional program container. Holds function definitions, the launch() entry point, and permit capability declarations. |
capsule Name { … } | A class definition — fields and methods. |
fn name(params) { … } Phase 8 | A top-level named function, usable from anywhere below it. |
chamber "path" | A file-level module import. |
| any statement / expression | Statements and expressions may also appear directly at the top level. |
Execution always begins at the launch() function of a system.
A more complete skeleton
This example shows top-level functions, a capsule, and a system living together in one file — the typical shape of a real program:
chamber "stdlib/math" // (1) imports come first
// (2) top-level helper functions — visible to everything below
fn greet(name) {
return "Greetings, " + name + "!"
}
// (3) a class
capsule Crew {
name
role
badge() { return self.name + " — " + self.role }
}
// (4) the program container; execution starts in launch()
system Main {
launch() {
transmit(greet("Centauri")) // Greetings, Centauri!
captain = Crew { name: "Ada", role: "Pilot" }
transmit(captain.badge()) // Ada — Pilot
}
}▸ output: Greetings, Centauri! / Ada — Pilot
04Values & Types #
The eight value kinds Centauri works with, and how truthiness is decided.
Centauri is dynamically typed at the value level. There is exactly one numeric type — a 64-bit float — which keeps arithmetic simple and predictable.
| Type | Description | Literal examples |
|---|---|---|
Float | 64-bit floating point; the only numeric type | 42, 3.14, -8 |
String | UTF-8 text | "hello" |
Boolean | truth value | true, false |
Vacuum | the absence of a value (like null / None) | Vacuum |
List | ordered, growable sequence | [1, 2, 3], [] |
Capsule | an instance of a capsule class | Dog { name: "Rex" } |
Closure | a first-class function value | fn(x) { return x + 1 } |
Reference | a ref-wrapped value | ref x |
Working with each type
system Main {
launch() {
// Float — the sole numeric type (integers are just floats)
age = 42
pi = 3.14159
transmit(age) // 42
transmit(age / 8) // 5.25 (no integer division surprises)
// String — UTF-8 text, concatenated with +
name = "Centauri"
transmit("Hello, " + name)
// Boolean
ready = true
transmit(ready) // true
// Vacuum — the explicit "nothing" value
nothing = Vacuum
transmit(nothing) // Vacuum
// List — ordered and growable
crew = ["Ada", "Alan", "Grace"]
transmit(crew) // [Ada, Alan, Grace]
transmit(crew[0]) // Ada
// Closure — a function stored in a variable
inc = fn(x) { return x + 1 }
transmit(inc(9)) // 10
}
}Truthiness
In any boolean context — if, orbit, and, or, not — a value is falsy if it is false, the number 0, or Vacuum. Everything else is truthy, including empty strings and empty lists.
system Main {
launch() {
// Falsy values: false, 0, Vacuum
if (0) { transmit("never") } else { transmit("0 is falsy") }
if (Vacuum) { transmit("never") } else { transmit("Vacuum is falsy") }
// Truthy values: every other number, all strings, all lists
if ("") { transmit("empty string is TRUTHY") } // prints!
if ([]) { transmit("empty list is TRUTHY") } // prints!
if (-1) { transmit("any nonzero number is truthy") }
}
}▸ output: 0 is falsy / Vacuum is falsy / empty string is TRUTHY / empty list is TRUTHY / any nonzero number is truthy
Coming from Python or JavaScript? Note that empty strings and empty lists are truthy in Centauri. To test for emptiness, compare lengths explicitly: if (len(xs) == 0) { … }.
05Operators #
Arithmetic, comparison, logical, and assignment — with full precedence rules.
Arithmetic
| Operator | Meaning | Notes |
|---|---|---|
+ | addition / concatenation | numbers add; strings and lists concatenate Phase 8 |
- | subtraction | |
* | multiplication | |
/ | division | division by zero is catchable via explore / divert |
% Phase 8 | modulo | follows the sign of the dividend; modulo by zero is catchable |
-x Phase 8 | unary negation |
system Main {
launch() {
transmit(3 + 4 * 2) // 11 — * binds tighter than +
transmit(17 % 5) // 2 — modulo
transmit(-8) // -8 — unary negation
transmit(7 / 2) // 3.5 — true division, always
// + is overloaded: it concatenates strings AND lists (Phase 8)
transmit("foo" + "bar") // foobar
transmit([1, 2] + [3, 4]) // [1, 2, 3, 4]
}
}Comparison
The comparison operators are ==, !=, <, >, <=, and >=. Ordering comparisons (< > <= >=) require two numbers; equality (== / !=) works across all value types.
system Main {
launch() {
transmit(10 >= 10) // true
transmit(3 != 4) // true
transmit("a" == "a") // true — == spans every type
transmit(Vacuum == 0) // false — different value kinds
}
}Logical Phase 8
| Operator | Alias | Meaning |
|---|---|---|
and | && | short-circuit logical AND |
or | || | short-circuit logical OR |
not | ! | logical NOT |
and / or short-circuit: the right-hand side is not evaluated once the result is already determined by the left-hand side.
fn fetch() {
transmit("...fetching from network...") // side effect we want to skip
return true
}
system Main {
launch() {
x = 5
done = false
// both forms are equivalent; pick whichever reads better
if (x > 0 and not done) { transmit("go") } // go
if (x > 0 && !done) { transmit("go (alias)") } // go (alias)
// short-circuit: because `cached` is true, fetch() never runs
cached = true
if (cached or fetch()) { transmit("have data") } // have data
}
}Precedence
From lowest to highest binding strength:
or < and < not < comparison < + - < * / % < unarySo a or b and c parses as a or (b and c), and not x == y parses as (not x) == y. When in doubt, add parentheses.
Assignment
= binds a value to a variable (creating it if it does not exist) or sets a capsule field:
system Main {
launch() {
count = 0 // creates the variable
count = count + 1 // re-binds it
point = Point { x: 0, y: 0 }
point.x = 10 // sets a capsule field
transmit(point.x) // 10
}
}
capsule Point { x y }06Control Flow #
Conditionals, loops, pattern matching, and error handling.
if / else / else if Phase 9
Standard conditional branching. else if chains were added in Phase 9 so you can ladder conditions cleanly:
fn grade(score) {
if (score >= 90) {
return "A"
} else if (score >= 80) { // chained branch (Phase 9)
return "B"
} else if (score >= 70) {
return "C"
} else {
return "try again"
}
}
system Main {
launch() {
transmit(grade(95)) // A
transmit(grade(83)) // B
transmit(grade(50)) // try again
}
}orbit — the while loop
Centauri's orbit keeps looping as long as its condition is truthy. (Think of a satellite orbiting until told to stop.)
system Main {
launch() {
// countdown from 5 to 1
n = 5
orbit (n > 0) {
transmit(n)
n = n - 1
}
transmit("liftoff!")
// orbit also drives event/render loops (see Graphics)
// orbit (gfx.is_open()) { ...draw a frame... }
}
}▸ output: 5 / 4 / 3 / 2 / 1 / liftoff!
for … in — iteration Phase 8
Iterates over the elements of a list, or the characters of a string. Pair it with range() for counted loops.
system Main {
launch() {
// iterate a list directly
for item in [10, 20, 30] {
transmit(item) // 10, 20, 30
}
// counted loop with range(n) -> 0,1,2,3,4
for i in range(5) {
transmit("step " + str(i))
}
// iterate the characters of a string
for ch in "abc" {
transmit(ch) // a, b, c
}
}
}clip — break
clip exits the nearest enclosing orbit or for loop immediately.
system Main {
launch() {
// stop as soon as we reach 5
for i in range(100) {
if (i == 5) { clip } // break out of the loop
transmit(i) // 0, 1, 2, 3, 4
}
}
}return
Returns from the current function, optionally carrying a value back to the caller. A function with no return yields Vacuum.
fn double(x) { return x * 2 }
fn classify(n) {
if (n < 0) { return "negative" } // early return
if (n == 0) { return "zero" }
return "positive"
}
system Main {
launch() {
transmit(double(21)) // 42
transmit(classify(-3)) // negative
}
}match — pattern matching
Patterns may be: a numeric literal, a string literal, Vacuum, a capsule type name (a capitalized identifier), a binding name (a lowercase identifier that binds the subject), or _ (the wildcard). The first matching arm runs.
capsule Dog { name }
fn describe(value) {
match value {
0 => { return "the number zero" } // numeric literal
"hi" => { return "a greeting" } // string literal
Vacuum => { return "nothing at all" } // the Vacuum pattern
Dog => { return "it's a dog" } // capsule TYPE match
other => { return "bound to: " + str(other) } // binding (lowercase)
_ => { return "fallthrough" } // wildcard
}
}
system Main {
launch() {
transmit(describe(0)) // the number zero
transmit(describe("hi")) // a greeting
transmit(describe(Dog { name: "Rex"})) // it's a dog
transmit(describe(99)) // bound to: 99
}
}Because a lowercase identifier binds the subject and always matches, place it (or _) last — any arm after it is unreachable.
explore / divert / jettison — try / catch / throw
jettison raises an error with an optional payload. explore runs a block; if anything inside it jettisons — including runtime faults such as division or modulo by zero — control transfers to divert, which binds the error value.
fn risky(n) {
if (n < 0) { jettison "negative input!" } // throw with a payload
return n * 2
}
system Main {
launch() {
// 1) catching a manual jettison
explore {
transmit(risky(10)) // 20
transmit(risky(-1)) // jettisons here
transmit("unreached")
} divert(err) {
transmit("caught: " + err) // caught: negative input!
}
// 2) runtime faults are catchable too — e.g. divide by zero
explore {
transmit(10 / 0)
} divert(e) {
transmit("math fault: " + str(e))
}
}
}▸ output: 20 / caught: negative input! / math fault: …
07Functions #
Top-level functions, system methods, and first-class closures.
Top-level functions Phase 8
Define reusable functions outside any system block with fn. They are called by name and fully support recursion.
fn square(x) {
return x * x
}
fn factorial(n) {
if (n <= 1) { return 1 }
return n * factorial(n - 1) // self-recursion is always allowed
}
system Main {
launch() {
transmit(square(9)) // 81
transmit(factorial(5)) // 120
}
}Ordering rule. A function must be defined before another function calls it. Self-recursion is always fine. Defining all your helper fns above system Main satisfies this in every case.
System functions
Functions declared inside a system block behave identically and are the classic way to organize a program. launch() is simply the one the runtime calls first.
system Main {
helper(x) { return x + 1 } // a system function
launch() {
transmit(helper(41)) // 42
}
}Closures
fn(params) { body } with no name is a first-class function value. It captures variables from the scope where it was created, so it can carry state around.
fn make_adder(base) {
// the returned closure captures `base`
return fn(x) { return x + base }
}
system Main {
launch() {
// 1) a closure capturing an outer variable
base = 100
add = fn(x) { return x + base }
transmit(add(5)) // 105
// 2) closures as return values — a function factory
add10 = make_adder(10)
add50 = make_adder(50)
transmit(add10(1)) // 11
transmit(add50(1)) // 51
// 3) closures as arguments (see map/filter/reduce later)
nums = [1, 2, 3]
transmit(nums.map(fn(n) { return n * n })) // [1, 4, 9]
}
}08Built-in Functions #
The always-available global functions. Phase 8
These functions are available everywhere without any import. A user-defined function with the same name takes precedence over the built-in, so you can always shadow one if you need different behavior.
| Function | Description |
|---|---|
transmit(v) | print a value (lists and capsules render readably) |
len(x) | length of a list or string |
str(v) | convert any value to its string form |
range(n) | list [0, 1, …, n-1] |
range(a, b) | list [a, …, b-1] |
range(a, b, step) | list from a toward b by step |
push(list, v) | append v to list (in place); returns the list |
pop(list) | remove and return the last element |
system Main {
launch() {
// transmit — prints anything, including structured values
transmit([1, 2, 3]) // [1, 2, 3]
// len — works on lists and strings
transmit(len("Centauri")) // 8
transmit(len([10, 20, 30])) // 3
// str — convert and concatenate
transmit(str(42) + "!") // 42!
// range — three forms
transmit(range(5)) // [0, 1, 2, 3, 4]
transmit(range(2, 6)) // [2, 3, 4, 5]
transmit(range(0, 10, 2)) // [0, 2, 4, 6, 8]
// push / pop — grow and shrink a list in place
xs = [1, 2, 3]
push(xs, 4) // xs is now [1, 2, 3, 4]
last = pop(xs) // last == 4, xs == [1, 2, 3]
transmit(last) // 4
transmit(len(xs)) // 3
}
}Shadowing a built-in
// Define your own len(); it wins over the built-in.
fn len(x) { return "I refuse to count" }
system Main {
launch() {
transmit(len([1, 2, 3])) // I refuse to count
}
}09Capsules (Classes) #
Centauri's object model: fields, methods, inheritance, and dispatch.
A capsule is a class. It declares fields (bare names) and methods (functions). extends enables single inheritance, self refers to the receiving instance, and super.method(...) calls the parent's implementation.
Defining and using a capsule
capsule Animal {
name // a field
speak() { return "..." } // a method
describe() {
return self.name + " says " + self.speak() // self = the receiver
}
}
capsule Dog extends Animal { // single inheritance
speak() { return "woof" } // overrides Animal.speak
}
system Main {
launch() {
// Construct with `ClassName { field: value, ... }`
d = Dog { name: "Rex" }
transmit(d.describe()) // Rex says woof
}
}Instances are created with constructor syntax: ClassName { field: value, ... }. Any field you omit defaults to Vacuum.
Inheritance with super
Use super.method(...) to extend — rather than replace — the parent's behavior.
capsule Ship {
name
status() { return "Ship " + self.name }
}
capsule Cruiser extends Ship {
guns
status() {
// call the parent, then add to it
return super.status() + " [armed: " + str(self.guns) + "]"
}
}
system Main {
launch() {
c = Cruiser { name: "Aurora", guns: 8 }
transmit(c.status()) // Ship Aurora [armed: 8]
}
}Capsules as data + behavior
Capsules combine state and the operations on it. Here a small counter shows fields being read and mutated through methods:
capsule Counter {
value
bump() { self.value = self.value + 1 return self.value }
reset() { self.value = 0 }
}
system Main {
launch() {
c = Counter { value: 0 }
transmit(c.bump()) // 1
transmit(c.bump()) // 2
c.reset()
transmit(c.value) // 0
}
}10Dictionaries #
String-keyed maps with indexed get/set. Phase 9
A dictionary is a string-keyed map. Number keys are accepted but stored by their string form. Create one with curly braces and "key": value pairs.
system Main {
launch() {
d = {"name": "Rex", "age": 3}
transmit(d["name"]) // Rex — index to read
d["age"] = 4 // indexed assignment (existing key)
d["breed"] = "collie" // indexed assignment (new key)
transmit(d["age"]) // 4
transmit(len(d)) // 3 — number of entries
}
}Dictionary methods
has(key), get(key), keys(), values(), and remove(key). Both keys() and values() return lists in key-sorted order.
system Main {
launch() {
scores = {"ada": 91, "alan": 85, "grace": 99}
transmit(scores.has("ada")) // true
transmit(scores.get("alan")) // 85
transmit(scores.keys()) // [ada, alan, grace] (sorted)
transmit(scores.values()) // [91, 85, 99] (by sorted key)
scores.remove("alan")
transmit(scores.keys()) // [ada, grace]
// iterate a dictionary via its keys
for k in scores.keys() {
transmit(k + " -> " + str(scores[k]))
}
}
}Indexed assignment also works on lists (xs[i] = v), and strings support read indexing (s[0] → the first character).
11String & List Methods #
Dot-syntax methods on any string or list value. Phase 9
String methods
| Method | Result |
|---|---|
upper() / lower() | change case |
trim() | strip leading/trailing whitespace |
length() | character count |
contains(s) | does it contain substring s? |
starts_with(s) / ends_with(s) | prefix / suffix test |
replace(from, to) | replace all occurrences |
repeat(n) | concatenate the string n times |
index_of(s) | position of s, or -1 |
char_at(i) | character at index i |
to_number() | parse to a Float |
split(sep) | split into a list |
system Main {
launch() {
transmit("Hello".upper()) // HELLO
transmit(" spaced ".trim()) // spaced
transmit("starship".contains("ship")) // true
transmit("file.cnt".ends_with(".cnt")) // true
transmit("a-b-a".replace("a", "X")) // X-b-X
transmit("ab".repeat(3)) // ababab
transmit("42".to_number() + 8) // 50 (now a number)
// split + chain
transmit("a,b,c".split(",").join(" | ")) // a | b | c
}
}List methods
| Method | Result |
|---|---|
contains(v) | membership test |
index_of(v) | position of v, or -1 |
reverse() | reversed list |
sort() | sorted list |
slice(a, b) | sublist from a up to (not including) b |
join(sep) | join elements into a string |
push(v) / pop() | append / remove-last |
system Main {
launch() {
xs = [3, 1, 2]
xs.sort()
transmit(xs) // [1, 2, 3]
transmit(xs.reverse()) // [3, 2, 1]
transmit(xs.contains(2)) // true
transmit(xs.index_of(3)) // 0 (after sort)
// slice(a, b) — half-open range
transmit([10, 20, 30, 40].slice(1, 3)) // [20, 30]
// join turns a list into a string
transmit(["a", "b", "c"].join("-")) // a-b-c
}
}12Functional List Operations #
map, filter, and reduce with closures. Phase 9
These three methods take closures and can be chained together into data pipelines. map transforms every element, filter keeps elements that pass a test, and reduce folds the list down to a single accumulated value.
system Main {
launch() {
nums = [1, 2, 3, 4, 5]
// map — transform each element
doubled = nums.map(fn(x) { return x * 2 })
transmit(doubled) // [2, 4, 6, 8, 10]
// filter — keep elements that pass the predicate
evens = nums.filter(fn(x) { return x % 2 == 0 })
transmit(evens) // [2, 4]
// reduce — fold to a single value (acc starts at 0)
total = nums.reduce(fn(acc, x) { return acc + x }, 0)
transmit(total) // 15
}
}Chaining into a pipeline
Because each call returns a list, you can chain them — read top to bottom like a transformation pipeline:
system Main {
launch() {
// square each of 1..5, then keep the ones greater than 5
result = range(1, 6)
.map(fn(x) { return x * x }) // [1, 4, 9, 16, 25]
.filter(fn(x) { return x > 5 }) // [9, 16, 25]
transmit(result) // [9, 16, 25]
// real-world flavour: clean a CSV of names
names = "ada,alan,grace"
.split(",")
.map(fn(n) { return n.upper() }) // ["ADA", "ALAN", "GRACE"]
transmit(names.join(" | ")) // ADA | ALAN | GRACE
}
}13Math Module #
Numeric functions via math.*, with common ones also available bare. Phase 9
Call functions as math.<fn>(...). The most common ones are also available as bare built-ins: abs, min, max, sqrt, pow, floor, ceil, round, and random.
Full roster: sqrt, pow, floor, ceil, round, trunc, sign, abs, min, max, sin, cos, tan, atan2, log/ln, log10, exp, random.
system Main {
launch() {
transmit(math.sqrt(144)) // 12
transmit(math.pow(2, 10)) // 1024
transmit(math.floor(3.9)) // 3
transmit(math.ceil(3.1)) // 4
// the common ones work without the math. prefix
transmit(abs(-9)) // 9
transmit(max(3, 7)) // 7
transmit(min(3, 7)) // 3
// random() returns 0.0 <= r < 1.0
r = math.random()
transmit(r >= 0 and r < 1) // true
// trig — handy for graphics (angles in radians)
transmit(math.sin(0)) // 0
transmit(math.cos(0)) // 1
}
}A practical example: distance
fn distance(x1, y1, x2, y2) {
dx = x2 - x1
dy = y2 - y1
return math.sqrt(dx * dx + dy * dy)
}
system Main {
launch() {
transmit(distance(0, 0, 3, 4)) // 5 — classic 3-4-5 triangle
}
}14Modules #
Importing other files with chamber, and registering system modules with dock.
chamber — file imports
chamber imports another .cnt file, compiles it, and makes its functions available. Diamond dependencies are de-duplicated (a file imported by two others is only compiled once), and circular imports are rejected.
chamber "stdlib/math" // string path, relative to this file
chamber stdlib.math // dotted form → stdlib/math
chamber "utils" as u // optional alias
system Main {
launch() {
// functions from the imported files are now callable
transmit("modules loaded")
}
}dock
dock registers a built-in system module handle — for example to bring the fs, net, time, or proc namespaces into scope.
Use chamber for your own .cnt files and dock for the language's built-in system modules.
15Concurrency #
Actor message-passing and real OS threads.
Actors
Centauri runs every program on an M:N actor scheduler. You spawn named functions as independent actors and communicate by exchanging messages — there is no shared mutable state to race on.
fn worker(start) {
msg = receive // block until a message arrives
transmit("worker got: " + msg)
}
system Main {
launch() {
pid = spawn worker(42) // start `worker` as a new actor → yields a PID
send "ping" to pid // enqueue a message to that actor
}
}Message boundary rule. Only Float, String, Boolean, and Vacuum may cross actor boundaries — these are the safely-copyable value kinds. Lists, capsules, and closures stay within their owning actor.
proc — OS threads & parallelism
The proc module exposes real OS threading primitives for CPU-bound parallel work, including thread counts, raw threads, thread pools, parallel map/for, mutexes, atomics, and barriers.
| Primitive | Calls |
|---|---|
| Introspection | proc.cpu_count() |
| Raw threads | proc.thread(...), proc.thread_join(...) |
| Thread pools | proc.thread_pool, proc.pool_submit, proc.pool_await, proc.pool_shutdown |
| Parallel helpers | proc.parallel_map(list, "fn"), proc.parallel_for(n, "fn") |
| Synchronization | mutexes, atomics, barriers |
fn heavy(x) { return x * x }
system Main {
launch() {
transmit("cores: " + str(proc.cpu_count()))
// parallel_map runs `heavy` across the input list on multiple threads.
// The function is passed by NAME (a string).
results = proc.parallel_map([1, 2, 3, 4], "heavy")
transmit(results) // [1, 4, 9, 16]
}
}16Standard Modules #
time, fs, and net — note the capability gates.
time
Clock and calendar utilities. time.format tokens include %Y %m %d %H %M %S %A %B and more.
system Main {
permit time.sleep // sleeping is capability-gated (DoS guard)
launch() {
now = time.now() // current timestamp
transmit(time.format(now, "%Y-%m-%d")) // e.g. 2026-06-18
a = time.now()
time.sleep(1) // pause 1 second
b = time.now()
transmit("elapsed: " + str(time.diff(a, b)))
// also: time.parse(iso8601), time.components(ts)
}
}fs — file I/O capability-gated
Reads require a permit fs.read; writes require a permit fs.write. The four core calls:
| Call | Action | Requires |
|---|---|---|
fs.airlock(path) | read a file's contents | permit fs.read |
fs.exists(path) | does the path exist? | permit fs.read |
fs.write(path, content) | write a file | permit fs.write |
fs.delete(path) | delete a file | permit fs.write |
system Main {
permit fs.read("/tmp") // grant read access under /tmp
permit fs.write("/tmp") // grant write access under /tmp
launch() {
fs.write("/tmp/log.txt", "mission start\n")
if (fs.exists("/tmp/log.txt")) {
transmit(fs.airlock("/tmp/log.txt")) // read it back
}
}
}net — networking capability-gated
net.fetch(url), net.post(url, body), plus listen / accept / recv / send / close. All require a permit net.transmit.
system Main {
permit net.transmit // authorizes all net.* calls
launch() {
body = net.fetch("https://example.com/status")
transmit(body)
}
}For the broader, modern OS + HTTP toolkit (downloads, arbitrary verbs, process execution), see LaunchControl.
17Graphics #
A pixel framebuffer with live, interactive windows. Phase 9
The gfx module gives you a pixel framebuffer. On a desktop, gfx.open pops up a real, interactive window; on a headless machine the same drawing calls still work and can be exported to a PNG with gfx.save. Colours are r, g, b in the range 0–255.
| Call | Purpose |
|---|---|
gfx.open(w, h, "Title") | create the framebuffer / window |
gfx.clear(r, g, b) | fill the whole frame with a colour |
gfx.pixel(x, y, r, g, b) | set one pixel |
gfx.rect(x, y, w, h, r, g, b) | filled rectangle |
gfx.line(x0, y0, x1, y1, r, g, b) | line segment |
gfx.circle(cx, cy, radius, r, g, b) | filled circle |
gfx.update() | blit to window + pump events → Boolean (window open?) |
gfx.is_open() | Boolean — is the window still open? |
gfx.key("left") | Boolean — arrows, space, escape, enter, a–z, 0–9 |
gfx.mouse() | current pointer as [x, y] |
gfx.width() / gfx.height() | frame dimensions |
gfx.save("out.png") | export the framebuffer as a PNG |
Generative art → PNG (works headless)
system Main {
launch() {
w = 240
h = 180
gfx.open(w, h, "Centauri Art")
// vertical colour gradient background, drawn line by line
for y in range(h) {
shade = math.floor(y * 255 / h)
gfx.line(0, y, w - 1, y, shade / 3, shade / 2, shade)
}
// a ring of circles placed with sin/cos
cx = w / 2
cy = h / 2
count = 12
for i in range(count) {
angle = i * 6.2831853 / count
x = cx + math.cos(angle) * 70
y = cy + math.sin(angle) * 50
red = math.floor(128 + 127 * math.sin(angle))
grn = math.floor(128 + 127 * math.cos(angle))
gfx.circle(x, y, 12, red, grn, 200)
}
gfx.circle(cx, cy, 18, 255, 255, 255)
gfx.save("centauri_art.png") // export — no display needed
transmit("Wrote centauri_art.png")
}
}A live, interactive render loop
The pattern for anything interactive: open a window, then loop while it is open — clear, draw, read input, and update() once per frame.
system Main {
launch() {
w = 480
h = 360
gfx.open(w, h, "Centauri — Bounce")
x = 100 y = 80
dx = 4 dy = 3
r = 16
orbit (gfx.is_open()) {
// physics
x = x + dx
y = y + dy
if (x < r or x > w - r) { dx = 0 - dx } // bounce off walls
if (y < r or y > h - r) { dy = 0 - dy }
// arrow keys nudge the ball; ESC quits
if (gfx.key("left")) { x = x - 6 }
if (gfx.key("right")) { x = x + 6 }
if (gfx.key("escape")) { clip }
// draw the frame
gfx.clear(15, 15, 30)
gfx.circle(x, y, r, 90, 200, 255)
gfx.rect(0, h - 6, w, 6, 40, 40, 80) // floor
gfx.update()
}
transmit("done")
}
}The interactive window opens on a desktop OS (e.g. macOS, using the native windowing system). On a server / headless host the window simply can't open — but drawing and gfx.save still produce correct images.
18Sound #
Generate and play audio with no external dependencies. Phase 9
The audio module synthesizes tones and plays sound files. audio.tone writes a sine-wave WAV directly; audio.play hands a file to the OS's audio player.
| Call | Result |
|---|---|
audio.tone(path, freq, secs) | write a secs-long sine WAV at freq Hz |
audio.duration(path) | length in seconds (reads a WAV header) |
audio.play(path) | play, blocking until finished |
audio.play_async(path) | play without blocking |
system Main {
launch() {
// note name → frequency (Hz)
notes = {"C": 261.63, "D": 293.66, "E": 329.63, "G": 392.0, "A": 440.0}
tune = ["E", "D", "C", "D", "E", "E", "E"] // "Mary Had a Little Lamb"
for name in tune {
freq = notes[name]
audio.tone("/tmp/note.wav", freq, 0.4) // synthesize the note
transmit("playing " + name + " (" + str(freq) + " Hz)")
audio.play("/tmp/note.wav") // blocks until it ends
}
transmit("samples per note: " + str(audio.duration("/tmp/note.wav")))
}
}audio.play hands the file to the operating system's player, so it plays whatever the OS supports — including mp3, wav, m4a, and aac (macOS uses the built-in afplay; Linux uses ffplay / aplay / paplay / mpg123 if installed). Use play_async for sound effects you don't want to wait on — perfect for games.
19LaunchControl #
Mission control for the host OS: files, paths, environment, processes, HTTP. Phase 10
LaunchControl is Centauri's interface to the host operating system. Method names are capitalized (LaunchControl.Move, LaunchControl.GetCurrentDirectory). Dangerous operations are gated by the capability system, exactly like fs.*.
| Capability | Authorizes |
|---|---|
os.write | Move, Copy, Delete, DeleteDir, MakeDir, WriteFile, AppendFile, Rename, SetCurrentDirectory |
os.read | ReadFile |
os.exec | Run, RunArgs, Shell |
os.net | Get, Post, Put, Download, Upload, Request |
Metadata, path helpers, and environment/process reads need no permit. Every path argument also passes through the RASP engine (check_path), so traversal, sensitive system files, and glob injection are blocked even when a permit is held.
Filesystem & OS operations
system Main {
permit os.write("/tmp")
permit os.read("/tmp")
permit os.exec("echo")
launch() {
// free reads — no permit needed for metadata / env / paths
transmit("Platform: " + LaunchControl.Platform()) // macos/linux/windows
transmit("User: " + LaunchControl.Username())
transmit("Home: " + LaunchControl.HomeDir())
transmit("CWD: " + LaunchControl.GetCurrentDirectory())
// build a working directory under the system temp dir
dir = LaunchControl.JoinPath(LaunchControl.TempDir(), "centauri_demo")
LaunchControl.MakeDir(dir) // needs os.write
// write three files, then list them
for i in range(3) {
path = LaunchControl.JoinPath(dir, "file" + str(i) + ".txt")
LaunchControl.WriteFile(path, "contents of file " + str(i))
}
files = LaunchControl.ListDir(dir)
transmit("Files: " + str(files.sort()))
// read one back; report its size
first = LaunchControl.JoinPath(dir, "file0.txt")
transmit("file0 size: " + str(LaunchControl.FileSize(first)) + " bytes")
transmit("file0 text: " + LaunchControl.ReadFile(first)) // needs os.read
// run an external command (needs os.exec)
transmit("echo -> " + LaunchControl.Run("echo launched"))
// clean up
LaunchControl.DeleteDir(dir)
transmit("cleaned up: " + str(LaunchControl.Exists(dir) == false))
}
}HTTP networking
Networking goes over the system's curl, so HTTPS/TLS, redirects, and uploads work out of the box. The URL passes through RASP check_url (SSRF to localhost / private / metadata addresses and credentials-in-URL are blocked even with a permit). Response bodies are RASP-scanned and marked tainted — a page containing active content like <script> is rejected; use Download to save raw bytes.
| Call | Result |
|---|---|
Get(url) | response body (string) |
Post(url, body) | response body |
Put(url, body) | response body |
Request(url, method, body) | response body — any verb (DELETE, PATCH, …) |
Download(url, path) | saves the URL to a file |
Upload(url, path) | multipart file upload → response body |
system Main {
permit os.net("https://api.example.com")
permit os.write("/tmp")
launch() {
// GET a document
json = LaunchControl.Get("https://api.example.com/status")
transmit(json)
// download a file to disk (needs os.write for the destination)
LaunchControl.Download("https://api.example.com/report.csv", "/tmp/report.csv")
transmit("saved " + str(LaunchControl.FileSize("/tmp/report.csv")) + " bytes")
// POST a body and read the reply
reply = LaunchControl.Post("https://api.example.com/events", "{\"ok\":true}")
transmit(reply)
}
}Full function reference
Files & directories: Move, Rename, Copy, Delete, DeleteDir (recursive), MakeDir (recursive), WriteFile, AppendFile, ReadFile, Exists, IsFile, IsDir, ListDir, FileSize.
Paths: GetCurrentDirectory, SetCurrentDirectory, AbsolutePath, BaseName, DirName, Extension, JoinPath, HomeDir, TempDir.
Environment & process: GetEnv, SetEnv, GetArgs, Platform, Hostname, Username, ProcessId.
Running programs: Run(cmd) → stdout, RunArgs([prog, a, b, …]) → stdout, Shell(cmd) → exit code, Exit(code).
20Security Model #
Three defensive layers: capabilities, taint tracking, and the RASP engine.
Centauri enforces security at three layers that work together. The first decides whether a sensitive operation is allowed at all; the second tracks untrusted data as it flows through the program; the third inspects that data right before it leaves the program.
1 · Capabilities
Sensitive operations are denied unless the enclosing system declares a matching permit.
2 · Taint tracking
Data from the outside world is marked tainted and stays tracked through every operation.
3 · RASP rules
Tainted data is normalized and matched against an attack rule set before any I/O.
Capabilities — permit
Grants are prefix-matched, so permit fs.read("/var") authorizes reading /var/log/app.log. If you call a gated operation without the matching permit, the program is rejected during analysis — before any code runs.
system Main {
permit fs.read("/tmp") // grant: read anything under /tmp
launch() {
transmit(fs.airlock("/tmp/data.txt")) // allowed
// fs.airlock("/etc/passwd") // would be DENIED — outside the grant
}
}| Capability | Authorizes |
|---|---|
fs.read | fs.airlock, fs.exists |
fs.write | fs.write, fs.delete |
net.transmit | all net.* calls |
time.sleep | time.sleep (DoS guard) |
os.read | LaunchControl.ReadFile |
os.write | LaunchControl file mutations (Move, Delete, WriteFile, …) |
os.exec | LaunchControl.Run / RunArgs / Shell |
os.net | LaunchControl.Get / Post / Put / Download / Upload / Request |
Taint tracking
Every string that enters the program from the outside world — file contents, network responses, command output — is marked tainted. Taint propagates through operations on those strings, including + concatenation Phase 8, so any value derived from untrusted input remains tracked all the way to its destination.
RASP rules
Before any tainted string crosses an I/O boundary, it is normalized (iterative URL-decoding, Unicode fold, entity decode, whitespace collapse) and matched against an extensive rule set. A match blocks the operation.
The rule set covers SQL injection, XSS, command injection, path traversal, SSRF, LDAP, XXE, SSTI, header/log injection, and many WAF-bypass variants. Normalization happens first specifically to defeat encoding tricks that try to slip past naive pattern matching.
21Runtime Limits #
Guardrails that keep a program from exhausting host resources.
To prevent a runaway program from taking down the host, the VM enforces hard limits and turns what would be a crash into a clean, catchable runtime error.
| Limit | Value | Behavior on breach |
|---|---|---|
| Call depth | ≤ 100,000 nested call frames | raises a clean "Call stack overflow" error instead of crashing the host |
range() size | ≤ 10,000,000 elements | refuses to generate a larger list |
fn forever(n) { return forever(n + 1) } // unbounded recursion
system Main {
launch() {
// the call-depth guard turns this into a catchable error,
// not a host crash
explore {
forever(0)
} divert(e) {
transmit("safely caught: " + str(e)) // safely caught: Call stack overflow
}
}
}22Keyword Glossary #
Every reserved word, at a glance.
| Keyword | Role |
|---|---|
system | program container |
launch | entry-point function |
fn | function / closure |
capsule | class definition |
extends | class inheritance |
self, super | method receiver / parent dispatch |
permit | capability grant |
if, else, else if | conditional (with chaining) Phase 9 |
orbit | while loop |
for, in | for-each loop Phase 8 |
clip | break |
return | return from function |
match, => | pattern matching |
explore, divert | try / catch |
jettison | throw an error |
and, or, not | logical operators Phase 8 |
spawn, send, to, receive | actor concurrency |
chamber, as, dock | imports / modules |
ref | make a reference |
transmit | |
true, false, Vacuum | literals |
23Full Programs #
Complete, runnable programs that combine many features at once.
Data pipeline — dictionaries, functional ops, strings, math
A single program touching dictionaries, a functional pipeline, string methods, and the math module:
fn is_even(n) { return n % 2 == 0 }
system Main {
launch() {
// Dictionaries
scores = {"ada": 91, "alan": 85, "grace": 99}
scores["linus"] = 88
transmit("students: " + str(scores.keys()))
transmit("grace -> " + str(scores["grace"]))
// Functional pipeline over a range
nums = range(1, 11)
evens = nums.filter(fn(n) { return is_even(n) })
squares = evens.map(fn(n) { return n * n })
total = squares.reduce(fn(a, b) { return a + b }, 0)
transmit("even squares: " + str(squares))
transmit("sum: " + str(total))
// String methods
csv = "ada,alan,grace"
names = csv.split(",").map(fn(n) { return n.upper() })
transmit(names.join(" | "))
// Math
transmit("hypotenuse: " + str(math.sqrt(math.pow(3,2) + math.pow(4,2))))
transmit("max score: " + str(max(scores["grace"], scores["ada"])))
}
}Galactic Pong — graphics, audio, AI, state (excerpt)
The flagship example: a complete arcade game rendered with gfx, sound effects synthesized at runtime with audio.tone, a CPU opponent, a 7-segment scoreboard, and a particle trail. Below are representative pieces — the helper functions and the core game loop. (The full file lives in examples/GalacticPong.cnt.)
// Clamp a value into [lo, hi].
fn clamp(v, lo, hi) {
if (v < lo) { return lo }
if (v > hi) { return hi }
return v
}
// CPU paddle AI: chase the ball when it is incoming, else drift to centre.
fn cpu_move(cur, ball_y, ball_vx, is_right, spd, H, pl) {
active = (is_right and ball_vx > 0) or (not is_right and ball_vx < 0)
target = H / 2 - pl / 2
if (active) { target = ball_y - pl / 2 }
d = target - cur
if (math.abs(d) <= spd) { return clamp(target, 0, H - pl) }
if (d > 0) { return clamp(cur + spd, 0, H - pl) }
return clamp(cur - spd, 0, H - pl)
}
// A glowing paddle: dim halo, solid body, bright core stripe.
fn draw_paddle(x, y, w, h, r, g, b) {
gfx.rect(x - 3, y - 3, w + 6, h + 6, r * 0.35, g * 0.35, b * 0.35)
gfx.rect(x, y, w, h, r, g, b)
gfx.rect(x + w / 3, y + 5, w / 3, h - 10,
clamp(r + 50, 0, 255), clamp(g + 50, 0, 255), clamp(b + 50, 0, 255))
}
// Audio helpers — wrapped so a machine with no audio player never crashes.
fn safe_tone(path, freq, secs) {
explore { audio.tone(path, freq, secs) } divert(e) { }
}
fn play_sound(path) {
explore { audio.play_async(path) } divert(e) { }
}system Main {
launch() {
W = 900 H = 560
gfx.open(W, H, "Galactic Pong")
// pre-generate sound effects once
safe_tone("/tmp/gp_hit.wav", 700, 0.05)
safe_tone("/tmp/gp_wall.wav", 480, 0.04)
safe_tone("/tmp/gp_score.wav", 300, 0.16)
// ball + paddle state
bx = W / 2 by = H / 2
bvx = 6 bvy = math.random() * 6 - 3
ly = H / 2 ry = H / 2
pl = 96 pspeed = 9
br = 9
orbit (gfx.is_open()) {
if (gfx.key("escape")) { clip }
// move the human paddle with the arrow keys
if (gfx.key("up")) { ly = clamp(ly - pspeed, 0, H - pl) }
if (gfx.key("down")) { ly = clamp(ly + pspeed, 0, H - pl) }
// advance the ball; bounce off the top/bottom walls
bx = bx + bvx
by = by + bvy
if (by - br < 0) { by = br bvy = -bvy play_sound("/tmp/gp_wall.wav") }
if (by + br > H) { by = H - br bvy = -bvy play_sound("/tmp/gp_wall.wav") }
// render the frame
gfx.clear(8, 8, 22)
draw_paddle(24, ly, 14, pl, 255, 215, 0) // gold = you
gfx.circle(bx, by, br, 255, 228, 120) // the ball
gfx.update()
}
transmit("thanks for playing Galactic Pong!")
}
}This one program exercises nearly the whole language: top-level functions, closures, orbit loops, match-free branching, lists as buffers, the math / gfx / audio modules, and explore / divert for graceful audio fallback. A great file to read end-to-end once the basics click.