Centauri
Language Docs
/

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.

📄 Source extension .cnt ⚙ Bytecode VM 🛡 Capability security ⇄ Actor concurrency 🚀 Native compiler

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
// 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:

shell
Centauri launch  <file.cnt>            # Interpret and run immediately
Centauri compile <file.cnt> [output]   # Compile to a standalone native binary

What 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.

shell
# 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 installed
🧭

Pipeline 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:

DeclarationPurpose
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 8A top-level named function, usable from anywhere below it.
chamber "path"A file-level module import.
any statement / expressionStatements 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:

structure.cnt
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.

TypeDescriptionLiteral examples
Float64-bit floating point; the only numeric type42, 3.14, -8
StringUTF-8 text"hello"
Booleantruth valuetrue, false
Vacuumthe absence of a value (like null / None)Vacuum
Listordered, growable sequence[1, 2, 3], []
Capsulean instance of a capsule classDog { name: "Rex" }
Closurea first-class function valuefn(x) { return x + 1 }
Referencea ref-wrapped valueref x

Working with each type

types.cnt
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.

truthiness.cnt
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

OperatorMeaningNotes
+addition / concatenationnumbers add; strings and lists concatenate Phase 8
-subtraction
*multiplication
/divisiondivision by zero is catchable via explore / divert
% Phase 8modulofollows the sign of the dividend; modulo by zero is catchable
-x Phase 8unary negation
arithmetic.cnt
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.

comparison.cnt
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

OperatorAliasMeaning
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.

logical.cnt
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:

precedence
or  <  and  <  not  <  comparison  <  + -  <  * / %  <  unary

So 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:

assignment.cnt
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:

if_else.cnt
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.)

orbit.cnt
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.

for_in.cnt
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.

clip.cnt
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.

return.cnt
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.

match.cnt
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.

explore.cnt
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.

functions.cnt
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_fns.cnt
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.

closures.cnt
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.

FunctionDescription
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
builtins.cnt
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

shadow.cnt
// 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

capsules.cnt
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.

super.cnt
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:

counter.cnt
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.

dicts.cnt
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.

dict_methods.cnt
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

MethodResult
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
string_methods.cnt
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

MethodResult
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
list_methods.cnt
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.

functional.cnt
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:

pipeline.cnt
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.

math.cnt
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

distance.cnt
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.

imports.cnt
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.

actors.cnt
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.

PrimitiveCalls
Introspectionproc.cpu_count()
Raw threadsproc.thread(...), proc.thread_join(...)
Thread poolsproc.thread_pool, proc.pool_submit, proc.pool_await, proc.pool_shutdown
Parallel helpersproc.parallel_map(list, "fn"), proc.parallel_for(n, "fn")
Synchronizationmutexes, atomics, barriers
proc.cnt
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.

time.cnt
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:

CallActionRequires
fs.airlock(path)read a file's contentspermit fs.read
fs.exists(path)does the path exist?permit fs.read
fs.write(path, content)write a filepermit fs.write
fs.delete(path)delete a filepermit fs.write
fs.cnt
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.

net.cnt
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.

CallPurpose
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)

art.cnt
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.

bounce.cnt
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.

CallResult
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
melody.cnt
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.*.

CapabilityAuthorizes
os.writeMove, Copy, Delete, DeleteDir, MakeDir, WriteFile, AppendFile, Rename, SetCurrentDirectory
os.readReadFile
os.execRun, RunArgs, Shell
os.netGet, 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

os_tools.cnt
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.

CallResult
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
net_demo.cnt
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.

permit.cnt
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
    }
}
CapabilityAuthorizes
fs.readfs.airlock, fs.exists
fs.writefs.write, fs.delete
net.transmitall net.* calls
time.sleeptime.sleep (DoS guard)
os.readLaunchControl.ReadFile
os.writeLaunchControl file mutations (Move, Delete, WriteFile, …)
os.execLaunchControl.Run / RunArgs / Shell
os.netLaunchControl.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.

LimitValueBehavior on breach
Call depth≤ 100,000 nested call framesraises a clean "Call stack overflow" error instead of crashing the host
range() size≤ 10,000,000 elementsrefuses to generate a larger list
limits.cnt
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.

KeywordRole
systemprogram container
launchentry-point function
fnfunction / closure
capsuleclass definition
extendsclass inheritance
self, supermethod receiver / parent dispatch
permitcapability grant
if, else, else ifconditional (with chaining) Phase 9
orbitwhile loop
for, infor-each loop Phase 8
clipbreak
returnreturn from function
match, =>pattern matching
explore, diverttry / catch
jettisonthrow an error
and, or, notlogical operators Phase 8
spawn, send, to, receiveactor concurrency
chamber, as, dockimports / modules
refmake a reference
transmitprint
true, false, Vacuumliterals

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:

data_demo.cnt
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.)

GalacticPong.cnt — helpers
// 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) { }
}
GalacticPong.cnt — game loop excerpt
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.