Xia is an ahead-of-time compiled programming language with Pythonic indentation-based syntax, automatic reference counting (ARC) instead of a garbage collector, and a zero-cost C FFI. The compiler is written in Rust and emits native machine code through LLVM 18.
# Strings and arrays are heap-allocated and managed by ARC — no GC.
extern fn printf(fmt: str, ...) -> int
fn greet(name: str) -> str:
return "hello, " + name + "!"
fn fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
fn main() -> int:
print(greet("world"))
for i in range(11):
printf("fib(%lld) = %lld\n", i, fib(i))
let langs = ["xia", "c"]
push(langs, "rust")
for name in langs:
print(name)
return 0
$ xia run examples/strings.xia
hello, world!
...
- Lexing (
src/lexer.rs) — alogostokenizer plus an indentation stack (Vec<usize>) that emitsINDENT/DEDENTtokens, Python-style, with implicit line joining inside brackets. - Parsing (
src/parser.rs) — a hand-rolled recursive descent parser producing the AST insrc/ast.rs;elifchains desugar to nested if/else. - Semantic analysis (
src/sema.rs) — scoped symbol tables (a stack ofHashMaps), bottom-up type inference, and call checking against both Xia andexternsignatures. - ARC insertion (
src/arc.rs) — rewrites the typed AST withretain/releaseso every heap value's refcount balances at scope boundaries: aliases retain, returns transfer ownership to the caller,break/continue/returnrelease eagerly along their paths. - Code generation (
src/codegen.rs) — aninkwellvisitor lowers the AST to LLVM IR. The ARC/string runtime (xia_retain,xia_release,xia_str_concat,xia_str_dup,xia_str_eq) is built directly in IR; a string's refcount header sits atptr - 8, so every Xiastrdoubles as achar*for the FFI. - Backend & linking (
src/backend.rs,src/linker.rs) — LLVMTargetMachineobject emission for any target triple, the standarddefault<O3>/default<Oz>pass pipelines plus symbol stripping for release builds, thenlld-link(Windows) orcc(Unix) links directly against libc.
int(i64),float(f64),bool(i1) are plain values.- Every heap block starts with a
[i64 kind][i64 refcount]header and the value points just past it. A negative refcount marks immortal data (string literals live in constant globals and are never freed). str(kind 0) is[header][bytes][NUL]; the value points at the bytes, so every Xia string doubles as achar*for the FFI.[T]arrays (kind 1, or 2 for heap elements) are[header][len][cap][data ptr]handles over a growable buffer of 8-byte words. Indexing is bounds-checked (out of bounds prints a diagnostic and exits with code 1). Arrays retain their heap elements; releasing the last reference releases every element before freeing the buffer.- A
structis[header][field0][field1]...with one 8-byte word per field. Structs with no heap fields use kind 0; structs that own heap data store the address of a generated destructor (xia_drop_<Name>) in the kind word, soxia_releasedispatches to it and releases each heap field before freeing. - An
enumis a tagged union[header][i64 tag][payload...], sized to the constructed variant; the value points at the tag and payload fieldjsits at+8 + j*8. Like structs, a scalar-only enum uses kind 0, while one whose variants own heap data storesxia_drop_<Name>in the kind word — that destructor switches on the tag and releases the live variant's heap fields. Self-referential enums (e.g. a cons list) are released recursively. - The compiler inserts all retain/release calls; there is nothing to call
manually and no GC pause. Function arguments are borrowed, returns are +1,
and
strresults fromexternfunctions are copied into Xia-owned memory.
xia build <file.xia> [--release | --opt-size] [--target <triple>]
[--emit ir|obj|exe] [-o <out>]
xia run <file.xia> [--release]
xia check <file.xia>
--target accepts any LLVM triple — the same source emits ELF, Mach-O, or
PE/COFF objects (--emit obj; cross-linking needs a linker for that target).
Requires Rust and an LLVM 18.1 build with llvm-config and static libraries.
On Windows, official installers don't ship those; grab a dev package from
c3lang/win-llvm and point llvm-sys at
it:
$env:LLVM_SYS_181_PREFIX = "C:\path\to\llvm-18.1.8-windows-amd64-msvc17-msvcrt"
cargo build --release
cargo test # 115 unit/IR tests + 18 end-to-end binary testsOn Linux the distro packages work directly (see .github/workflows/ci.yml,
which runs the suite on every push):
sudo apt-get install llvm-18-dev libpolly-18-dev libzstd-dev zlib1g-dev
LLVM_SYS_181_PREFIX=/usr/lib/llvm-18 cargo testLinking on Windows uses lld-link from the same LLVM package against the
MSVC / Windows SDK import libraries (Visual Studio Build Tools required); on
Linux/macOS it uses the system cc.
-
Types:
int,float,bool,str,[T]arrays (including nested[[T]]),structs, andenums; functions may return nothing (unit). -
struct Name:followed by an indentedfield: typeper line declares a product type. Construct with positional arguments (Name(a, b)), read and assign fields with.(p.x,p.x = 5). Struct types resolve regardless of declaration order. Seeexamples/structs.xia. -
Methods take an explicit receiver before the name —
fn (p: Point) area() -> int:— and are called asp.area(args). The receiver is borrowed (like any parameter) and the result is owned by the caller; calls compile to a direct call to aStruct.methodsymbol with the receiver passed as the implicit first argument (no vtables). Different structs may share a method name. -
enum Name:followed by indented variant lines declares a tagged union; a variant is a bare name (Nil) or carries a positional payload (Cons(int, IntList),Some(str)). Variant names are global and unique, so they construct without qualification:Some(x),Nil. Enum types resolve regardless of declaration order and may be self-referential. Take a value apart withmatch:match shape: Circle(r): return r * r * 3 Rect(w, h): return w * h Nothing: return 0Each arm names a variant and binds its payload positionally (in scope only within that arm), or is the catch-all
_. Amatchmust be exhaustive or end in a_arm; duplicate and unknown-variant arms are rejected. Seeexamples/enums.xia. -
let x = expr(inferred) orlet x: type = expr; assignment with=. An empty array literal needs an annotation:let xs: [int] = []. -
if/elif/else,while,break,continue— blocks by indentation, no braces. -
for i in range(end):/for i in range(start, end):countsstart(inclusive) toend(exclusive);for x in xs:iterates an array's elements.continuealways advances the loop. -
Operators:
+ - * / %, comparisons,and/or/not(short-circuit);+concatenates strings,==/!=compare them by value.xs[i]indexes (bounds-checked);xs[i] = vassigns in place. Indexing chains for nested arrays:grid[r][c],grid[r][c] = v(seeexamples/matrix.xia). -
Builtins:
print(x)for any printable type;len(s)/len(xs);push(xs, v)appends (the buffer grows by doubling). -
extern fn name(types...) -> retdeclares a C symbol;...marks varargs (e.g.printf). Calls have zero wrapper overhead — they are direct calls.