Cx is a compiled, GC-free systems programming language for game engines, tools, and systems programmers.
The goal of Cx is to give engine-facing code predictable memory behavior, explicit value movement, deterministic teardown, and low-level control without forcing every feature to become allocator plumbing.
Status: 0.2.0 — released 2026-06-05. Last tagged release: v0.2.0.
This README describes the current submain branch. The reference interpreter is the source of truth for language semantics; every code sample below was compiled and run against it.
Cx is not production-ready. It is a working compiler foundation under active development.
Engine and systems code often needs:
- predictable allocation
- explicit lifetime control
- stable data layout
- deterministic cleanup
- value movement that can be reasoned about frame to frame
- runtime behavior that does not depend on hidden garbage collection
Most teams end up mixing high-level ergonomics with low-level ownership rules by hand.
Cx is aimed at that gap.
It is designed around explicit memory behavior, stable handles, arena-oriented allocation, declared-width arithmetic, and a language model where unknown state is represented directly instead of being hidden behind ad hoc runtime conventions.
Source code moves through a full compiler pipeline:
source code
→ lexer/parser
→ AST
→ semantic analysis
→ IR
→ interpreter
→ Cranelift JIT
→ parity verification
The interpreter is the reference semantics backend — it defines what Cx programs mean.
The Cranelift JIT is the native execution backend.
A differential harness checks that every JIT-supported feature produces the same observable behavior as the interpreter.
You can inspect the IR for any program with the validate backend, which lowers to IR, checks it, and prints it:
cargo run -- --backend=validate examples/fibonacci.cxAs of v0.2.0:
- 243 unit tests passing (
cargo test) - 418 unit tests passing with the JIT enabled (
cargo test --features jit) - 230 verification fixtures
- JIT parity: 140 PASS / 90 SKIP / 0 PARITY_FAIL across all 230 fixtures
- zero Clippy errors
A fixture is SKIP when it exercises a language feature the JIT does not lower to native code yet (the interpreter still runs it). PARITY_FAIL means the interpreter and JIT disagree on observable behavior — that number must stay zero.
fnc: t64 sum_range(n: t64) {
total: t64 = 0
for i in 0..n {
total += i
}
total
}
print(sum_range(5)) // 10
fnc: t64 tier(n: t64) {
when n {
0 => 0,
1..=9 => 1,
10..=99 => 2,
_ => 3,
}
}
print(tier(7)) // 1
fnc: bool is_large(n: t64) {
n > 100
}
print(is_large(200)) // true
A note on the samples above:
- Declarations with a value are written
name: T = value. Barelet x;declares an uninitialized binding (assigned later); it does not take an initializer. - Both
ifandwhenare expressions: the chosen branch (or arm) is the value, so either can be returned or assigned directly. Usewhenfor multi-way matching andiffor a two-way choice —is_largeabove uses a bare trailing expression, the tersest form.ifandwhenalso work as plain statements when their value isn't used.
Each snippet here is a complete program; the inline comments show the exact output. Several are backed by files in examples/.
Integer types are t8, t16, t32, t64, t128 — all signed. Arithmetic wraps at the declared width, and an out-of-range literal is rejected at compile time (both signs).
x: t8 = 127
x += 1
print(x) // -128 (wraps at the t8 width)
x: t8 = 200 // compile error:
// integer literal 200 out of range for t8 (valid range: -128..127)
Enums declare named variants, referenced with ::. Since 0.2, an enum type can be used in parameter and return positions. See examples/enums.cx.
enum Light { Red, Green, Yellow }
fnc: Light next(cur: Light) {
when cur {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
_ => Light::Red,
}
}
c: Light = next(Light::Red)
when c { Light::Green => print(1), _ => print(0) } // 1
when matches integer literals, ranges with positive bounds, enum variants, and a _ catch-all. A when must be exhaustive: if it does not cover every case it needs a _ arm. See examples/when_match.cx.
size: str = when 7 {
0 => "none",
1..=9 => "small",
_ => "big",
}
print(size) // small
if produces a value, just like when — use it for a two-way choice as an implicit return, on the right of an assignment, or nested in a larger expression.
fnc: t64 grade(n: t64) {
if n >= 90 { 4 } else { 3 }
}
print(grade(95)) // 4
limit: t64 = 100
x: t64 = if limit > 50 { 10 } else { 20 }
print(x) // 10
Two rules for the value form: it must have an else (a consumed if always produces a value), and each branch is a single expression — the same shape as a when arm. As a plain statement, where the value is not used, if needs no else.
bool is three-state: true, false, and unknown, written with the literal ?. Because an unknown value cannot choose a branch, using it as an if condition is an error that directs you to when. See examples/tbool_uncertainty.cx.
let x;
x = ?
when x {
true => print("yes"),
false => print("no"),
unknown => print("maybe"),
}
// maybe
b: bool = ?
if b { print(1) } else { print(2) } // runtime error:
// `if` condition is unknown; an unknown TBool can't choose a branch —
// use `when` to handle true, false, and unknown explicitly
unknown is a when-pattern keyword, not a value. Writing it in value position is an error that points you at ?:
b: bool = unknown // parse error:
// `unknown` is a pattern keyword, not a value — use `?` for the unknown literal
A str literal interpolates bare variables in {...}:
name: str = "Cx"
n: t32 = 3
print("{name} v0.{n}") // Cx v0.3
Interpolation supports bare variable names only. A non-variable form (e.g. a call) is a compile-checked error rather than silent literal output:
print("{f(2)}") // error: string interpolation supports bare
// variables only; compute `{f(2)}` into a
// variable first
Concatenate strings with +. Get a string's byte length — or an array's element count — with len.
greeting: str = "Hello, " + "Cx"
print(greeting) // Hello, Cx
print(len(greeting)) // 9
len is a byte count, not a character count: len("é") is 2 (one character, two UTF-8 bytes). Mixing types in a concatenation is a compile error, not an implicit conversion — use interpolation instead:
n: t32 = 3
s: str = "v" + n // error: cannot concatenate `str` and `t32` with
// `+` — use string interpolation, e.g. "v{n}"
Fallible functions return Result<T> with Ok / Err, and the postfix ? operator propagates errors. See examples/error_handling.cx.
fnc: Result<t32> safe_divide(a: t32, b: t32) {
if b == 0 {
return Err("division by zero")
}
return Ok(a / b)
}
fnc: Result<t32> compute(a: t32, b: t32) {
let q;
q = safe_divide(a, b)?
return Ok(q * 2)
}
print(compute(10, 2)) // Ok(10)
print(compute(10, 0)) // Err(division by zero)
- signed integer types:
t8,t16,t32,t64,t128, with declared-width wrapping and compile-time range checking f64- three-state
bool(true/false/ unknown via?) char,str(with{var}interpolation of bare variables, and+concatenation)- enums — variants,
::access, and use in function signatures - structs and
implmethods - fixed-size arrays (
arr:[i]indexing) - generic structs and functions over types
- free functions; implicit last-expression return; explicit
return if/else,while,for, infiniteloop,break/continuewhenmatching (literals, positive ranges, enum variants, bool/TBool, catch-all) — usable as an expressionResult<T>withOk/Errand the?propagation operator- comparisons, logical short-circuiting, compound assignment, dot access
- built-ins:
print,println,printn,assert,assert_eq,read,input,exit,len
Cx includes the foundation for explicit value movement:
- stack-allocated structs
- stack-allocated arrays
- explicit copy semantics:
.copy,.copy.free,copy_into - no garbage collector
- no borrow checker
Longer-term memory features such as a richer Handle<T> surface, string arenas, and broader ownership tools are planned.
The tree-walking interpreter implements the full language surface above and is the comparison target for JIT parity.
The Cranelift JIT compiles a growing subset of Cx IR to native machine code. Currently JIT-lowered areas include:
- integer arithmetic and declared-width wrapping
- comparisons and logical AND/OR short-circuiting
- typed and inferred variable declarations
if/else,while,forranges, infiniteloop,break/continue- direct function calls and method dispatch
whenblocks for literal / range / bool / catch-all / TBool patterns- struct literals, field reads/writes; fixed-size arrays and element access
- unary negation, boolean NOT, integer and float casts
f64arithmetic and comparison- runtime intrinsics for print/assert; void returns and exit-code propagation
All currently JIT-lowered fixtures match interpreter behavior (0 PARITY_FAIL). A fixture whose feature is not yet lowered is reported as SKIP, not as a failure.
As of v0.2.0:
| Status | Count |
|---|---|
| PASS | 140 |
| SKIP | 90 |
| PARITY_FAIL | 0 |
| Total fixtures | 230 |
(Authoritative totals from the parity harness. Run cargo test --features jit jit_parity_by_feature -- --nocapture for the live per-category breakdown.)
These features work in the interpreter but are not yet lowered to the JIT (they show up as parity SKIP):
if-expression lowering (theif/elsestatement form is lowered; the expression form is interpreter-only for now)- enum IR lowering and
EnumVariantarms inwhen Result<T>/?operator lowering- string interpolation lowering
f64/t128native print formatting (ABI extension)WhileInsource-to-IR lowering- full TBool unknown propagation through arithmetic, comparison, and logical ops
These are not implemented in any backend yet:
- Cranelift AOT (object-file) compilation
- LLVM AOT backend
- richer
Handle<T>ownership surface and string arena - generic standard library
- Rust toolchain (stable works; the project also builds on nightly)
- The Cranelift JIT requires the
jitfeature
cargo build --features jitcargo run -- examples/hello.cxcargo run --features jit -- --backend=cranelift examples/fibonacci.cxcargo run -- --backend=validate examples/fibonacci.cxcargo test --features jitcargo test --features jit jit_parity_by_feature -- --nocaptureRunnable examples live in examples/:
| File | Shows |
|---|---|
hello.cx |
printing and string interpolation |
enums.cx |
enums, :: variants, enum types in signatures |
when_match.cx |
when literals/ranges/catch-all, when as an expression |
tbool_uncertainty.cx |
three-state bool, the ? literal, when over TBool |
structs_and_methods.cx |
structs and impl methods |
arrays_and_loops.cx |
fixed-size arrays, indexing, loops |
generics.cx |
generic structs over types |
error_handling.cx |
Result<T>, Ok / Err, the ? operator |
fibonacci.cx, fizzbuzz.cx |
classic small programs |
Run them all:
bash examples/run_all.shSome examples exercise interpreter features the JIT does not lower yet; use the parity harness rather than assuming every example is JIT-ready.
The Cx ABI target is x86-64.
Highlights:
- scalar sizes and alignments are defined
boolwire representation is fixed:false = 0,true = 1,unknown = 2- structs use C-compatible alignment with natural padding
- arrays use contiguous stack allocation through
ArrayAlloca - expression evaluation order is left-to-right
- current calling-convention target: C ABI / SystemV on Linux x86-64
See:
docs/backend/cx_abi_v0.1.md
Cx is early-stage and experimental.
Use it for:
- compiler development
- language design validation
- backend/JIT experimentation
- systems-language research
- test-driven expansion of the Cx feature set
Do not use it for production applications yet.
Near-term priorities:
- freeze and stabilize 0.2
- expand JIT lowering coverage toward the interpreter surface (enums,
Result/?, string interpolation,f64/t128printing) - broaden examples and documentation
- continue ownership and memory tooling
Longer-term goals:
- Cranelift AOT compilation
- LLVM AOT backend
- C FFI surface
- minimal standard library
- language server tooling
cx build,cx test, andcx check- game-engine-oriented runtime and memory tooling
Open an issue or PR to discuss language behavior, runtime semantics, backend work, or documentation.
Useful files:
src/tests/verification_matrix/— feature verification fixturesdocs/backend/cx_jit_parity_checklist.md— JIT parity statusdocs/backend/cx_abi_v0.1.md— ABI specificationCONTRIBUTING.md— branch policy and merge workflow