Language Guide
xxc Language Guide
> This guide documents the implemented, gate-verified behavior of the xxc
> toolchain (interpreter, bytecode VM, and the native compilers for Windows
> PE, Linux x86-64 ELF, and Linux aarch64 ELF). Build-feature requirements
> are marked inline.
>
> Related pages: Quick Start ·
> Standard Library Reference ·
> Implementation & Bootstrap ·
> 中文版:语言指南
- Overview
- Getting Started
- Lexical Structure
- Types
- Memory Model
- Variables and Assignment
- Functions and Closures
- Control Flow
- Error Handling
- Methods and Interfaces
- Generics
- Enums and Pattern Matching
- Tuples
- Maps
- String Methods
- Array Methods
- Modules
- Operators
- Native Compiler
- Self-Hosting
---
Overview
xxc is a cross-platform, full-stack language that can be **interpreted or
compiled**. Its design goals:
- Go-like ergonomics: simple syntax, batteries included, channels + tasks.
- No garbage collector: deterministic reference counting (ARC) for
reference types, plain values for everything else.
- Multi-core:
spawnruns tasks in parallel on N OS threads — in the
interpreter and in compiled binaries alike.
- Interpretable: the tree-walking interpreter runs any program with no
build step; a bytecode VM runs the same programs faster; the native
compiler emits direct machine code through xxc's own assembler and linkers.
Three sequential execution backends (one frontend, byte-exact against each
other):
| Command | Backend | Notes |
|---|---|---|
| xxc run file.xxc | Tree-walking interpreter | Full language; the semantic oracle |
| xxc vm file.xxc | Flat-stack bytecode VM | Full sequential language, ~2.8× faster |
| xxc build file.xxc | Direct native code (x86-64 PE/ELF, aarch64 ELF) | Own assembler + linkers; no gcc/binutils |
Install
# Linux x64 / arm64
curl -fsSL https://magicdo.top/install/xxc.sh | bash
# Windows (PowerShell)
irm https://magicdo.top/install/xxc.ps1 | iex
Or grab the executable for your platform from the
download page. xxc is a single-file distribution with
zero runtime dependencies (the Linux builds are static ELF), drop it on
your PATH and go.
Three backends
xxc run hello.xxc # tree-walking interpreter — the semantic oracle
xxc vm hello.xxc # bytecode VM — ~2.8x faster
xxc build hello.xxc # native compiler → ./hello runs directly
All three share one frontend; the differential gate matrix keeps their
outputs byte-identical.
Lexical Structure
- Source is UTF-8.
//line comments and/* ... */block comments. - Newlines end statements (Go-style); a binary operator must sit on the same
line as its left operand (a +\nb continues; a\n+ b does not).
- Identifiers:
[A-Za-z_][A-Za-z0-9_]*. - Keywords: `fn struct class interface enum if else while for in return
defer break continue true false nil or import spawn select case default
match`.
Literals
42 int (decimal i64)
3.14 float (f64)
0xFF int, hexadecimal
0b1010 int, binary
0o777 int, octal
"hello" string
"n = {n}" interpolated string (see below)
true false bool
nil the nil value
String interpolation
Inside a string literal, {expr} evaluates expr and inserts its display
form (str(expr)). Escapes: \n \t \r \" \\ \0 \{ \} — use \{ \} for
literal braces:
print("fib({n}) = {fib(n)}") // fib(10) = 55
print("brace: \{ not interpolation\}")
Types
| Type | Description |
|---|---|
| int | 64-bit signed integer |
| float | 64-bit float |
| bool | true / false |
| string | UTF-8 string (immutable, shared) |
| nil | absence |
| array | Vec<Value> (reference-shared, mutable) |
| struct T | value type — copied on bind |
| class T | reference type — ARC-managed |
| map | dynamic object used as a dictionary |
| tuple | (v1, v2, ...) — immutable, value-copied |
| enum | tagged union |
| chan | channel |
| function | first-class closures |
Typing is gradual: annotations are checked statically where known;
unannotated code is dynamic. The checker reports only definite errors.
Memory Model
Two categories, chosen by declaration:
struct Point { x: int; y: int } // VALUE type
class Box { value: int } // REFERENCE type (ARC)
- Assigning/binding a
structdeep-copies it (nested structs included).
Mutating a copy never affects the original.
- Assigning a
classshares the instance under deterministic ARC:
reference counts are maintained with atomic operations (x86 lock
instructions / aarch64 exclusive load-store pairs), and when a count
reaches zero the runtime releases the block immediately — no tracing
GC, no pause, and reclamation timing is exactly the last-reference point.
The free-at-zero path is exercised by the gate-open differential
(XXC_ARC_FREE=1) and protected by the allocation runaway guard
(XXC_ALLOC_LIMIT, strict base-10 bytes, default 2 GiB).
- Arrays and strings are shared; strings are immutable so sharing is
invisible. Array aliasing is observable (b := a; push(b, 1) affects a).
p := Point{x: 1, y: 2}
q := p // independent copy
q.x = 99 // p.x is still 1
c := Box{value: 10}
d := c // same instance, refcount +1
d.value = 77 // c.value is now 77
refcount(v) reports the approximate strong count (for demonstration).
Breaking cycles with weak
weak(obj) returns a weak reference to a class instance that does not keep
it alive. Field access and method calls transparently upgrade it; touching a
dead weak reference is a runtime error.
class Node { value: int; next: int; prev: int }
head.next = tail
tail.prev = weak(head) // back-reference without a cycle
Print line-atomicity
A print statement's parts (strings, ints, floats) are appended to a
per-line buffer under a statement-level owner-token lock and flushed as a
single write at the newline — concurrent tasks on any backend can never
interleave mid-line. The property is verified by the stress multiset gates
in the conc ladder.
Variables and Assignment
x := 10 // infer type
y: int = 20 // annotated
y = 30 // assign
a, b := pair() // destructure a tuple-producing expression
c, d := (10, 20) // or a tuple literal
Declaration forms: name := expr, name: Type = expr. Assignment targets:
variables, fields (obj.f = v), indexes (arr[i] = v, m["k"] = v).
Functions and Closures
fn add(a: int, b: int) -> int {
return a + b
}
fn fallible(path: string) -> string ! { // may return an error
...
}
- Parameters may be annotated (
a: int) or dynamic (a). -> Tdeclares the return type;!marks a fallible function.- Functions are first-class: assign, pass, return.
Function literals and closures
double := fn(x) { return x * 2 }
apply := fn(f, x) { return f(x) }
makeAdder := fn(n) {
return fn(x) { return x + n } // captures n
}
add10 := makeAdder(10)
print(add10(5)) // 15
A closure captures its enclosing scope; captured variables stay live for the
closure's lifetime.
Control Flow
if x < 0 { ... } else if x == 0 { ... } else { ... }
while cond { ... }
for x in arr { ... } // arrays (values)
for c in "héllo" { ... } // strings (per codepoint)
for i in range(10) { ... } // 0..9
for i in range(2, 5) { ... }// 2..4
break
continue
Error Handling
Errors are ordinary values. Three constructs:
fn inv(x: int) -> int ! {
if x == 0 { return error("divide by zero") }
return 100 / x
}
a := inv(5)? // propagate: inv's error returns from here
b := inv(0) or { -1 } // recover: the block's last expr is the value
print(inv(0)) // bare error-valued statements propagate
f(...)?— if the call yields an error, return it from the enclosing
function (or blocks catch propagated errors too).
expr or { ... }— run the block whenexpris an error.error(msg)constructs an error value; every error carries source position
for diagnostics.
defer
fn work() {
f := open(path)?
defer f.close() // runs when work() exits — normal or error
...
}
Deferred statements run in reverse registration order.
Methods and Interfaces
struct Rect { w: int; h: int }
fn (self: Rect) area() -> int { // value receiver (copy)
return self.w * self.h
}
fn (self: Rect) scale(k: int) -> Rect { // returns a new value
return Rect{w: self.w * k, h: self.h * k}
}
class Stack { items: array }
fn (self: Stack) push(v) { push(self.items, v) } // reference receiver
structreceivers receive a copy;classreceivers share the instance.- Call syntax:
r.area(),s.push(3).
Structural interfaces — any type with the required methods satisfies the
interface; no declaration needed:
interface Shape { fn area(self) -> int }
fn describe(s: Shape) { print(s.area()) } // accepts Rect, Box, ...
Generics
fn identity<T>(x: T) -> T { return x }
fn first<T>(arr: array<T>) -> T { return arr[0] }
fn max<T>(a: T, b: T) -> T { if a > b { return a } return b }
struct Pair<A, B> { first: A; second: B }
fn (self: Stack<T>) push(v: T) { ... } // generic method
Type parameters are erased at runtime; the type checker infers them at call
sites (max(3, 7) → T = int, result int; max("a", "b") → T = string).
Enums and Pattern Matching
enum Shape {
Circle(float),
Rect(float, float),
Point
}
fn area(s) -> float {
return match s {
Circle(r) => { 3.14159 * r * r }
Rect(w, h) => { w * h }
Point => { 0.0 }
}
}
- Variants carry typed data fields. Unit variants need no parentheses.
- Construct with
Circle(5.0)or a barePoint. matchdispatches on the variant name, binds its data
(Circle(r) binds r), and its arm body yields the match's value.
- A
_arm matches anything. A non-matching value with no_arm is a
runtime error.
Tuples
fn divmod(a: int, b: int) { // returns (int, int) implicitly
return a / b, a % b
}
q, r := divmod(17, 5) // destructure: q=3, r=2
point := (10, 20)
print(point.0, point.1) // index with .0 .1 ...
lo, hi := minmax(arr) // any tuple-returning call
return x, yreturns the tuple(x, y).a, b := exprdestructures; element counts must match.- Tuples are immutable and value-copied.
Maps
map() creates a dynamic object usable as a dictionary; keys are strings.
m := map()
m["name"] = "xxc"
m["version"] = 1.0
print(m["name"]) // xxc
print(m["missing"]) // nil
print(m.contains("name")) // true
print(m.keys()) // ["name", "version"]
print(m.values())
print(m.length) // 2
// word frequency
freq := map()
for w in text.split(" ") {
if freq.contains(w) { freq[w] = freq[w] + 1 }
else { freq[w] = 1 }
}
Any class value supports index access; map() is the empty dynamic one.
Key iteration order is unspecified.
String Methods
s.length element count
s.contains(sub) bool
s.starts_with(sub) bool
s.ends_with(sub) bool
s.index_of(sub) first index or -1
s.slice(start, end) substring [start, end)
s.replace(old, new) all occurrences
s.trim() strip whitespace
s.upper() s.lower()
s.split(sep) -> array
Array Methods
arr.length
arr.push(v) append in place
arr.pop() remove + return last
arr.contains(v) bool
arr.index_of(v) first index or -1
Builtins: len(x), push(arr, v), range(n) / range(a, b).
Concurrency and async I/O
spawn, channels (buffered and rendezvous), select, the worker-pool
runtime, deadlock detection, and the async I/O modules (std/fs,
net/tcp, net/http) are documented in the dedicated
Concurrency Guide — the same
semantics run in the interpreter and in compiled binaries.
Standard library
Every module (std/time, std/json, std/random, std/os,
std/regex, std/crypto, std/base64, std/uri, std/fs, net/tcp,
net/http, db/sqlite, gui, js, webview) plus the full operator
precedence table live in the
Modules
Source modules
import "mathlib" // loads mathlib.xxc (same dir), merges its decls
Relative to the importing file; .xxc extension optional; cycles and
duplicate loads are handled.
Native modules
import time "std/time" // alias -> native module
print(time.now())
Operators
Precedence (highest to lowest):
| Prec | Operators | Category |
|---|---|---|
| highest | fn(...) call, . field/index, [...] index, postfix ? | postfix |
| — | unary - ! ~ | unary |
| 7 | * / % | multiplicative |
| 6 | + - | additive |
| 5 | << >> & \| ^ | bitwise |
| 4 | < <= > >= | comparison |
| 3 | == != | equality |
| 2 | && | and |
| 1 | \|\| | or |
| 0 | = (assignment), ch <- v (send) | assignment |
&&/|| short-circuit. + adds numbers and concatenates strings
(string + non-string is a type error; convert with str()). Bitwise ops
work on int (& | ^ also on bool). Integer division truncates;
x / 0 and x % 0 on ints are runtime errors.
Native Compiler
xxc build file.xxc compiles the program **directly to native machine
code** — there is no C compiler, no assembler, and no linker in the tool
chain: xxc carries its own in-process assembler (assembler.rs) and object
linkers (pelinker.rs for Windows PE, elflinker.rs for static Linux ELF).
Targets:
| Host | Output | Notes |
|---|---|---|
| Windows x86-64 | PE executable | VEH-based diagnostics, guard pages |
| Linux x86-64 | static ELF64 | pure syscalls, no libc, no PT_INTERP — self-contained like a Go binary |
| Linux aarch64 | ELF64 aarch64 | the same generated code translated by the a64 backend (a64.rs) |
fn fib(n: int) -> int {
if n < 2 { return n }
return fib(n - 2) + fib(n - 1)
}
fn main() {
print("fib(40) = {fib(40)}")
}
$ xxc build examples/bench.xxc && ./bench
fib(40) = 102334155
The full sequential language compiles: structs (value) and classes (ARC),
maps, arrays, strings, closures, errors (?/or/!), defer, match,
tuples, generics, and — with --features concurrency — spawn, channels,
and select, with the same worker-pool semantics as the interpreter. The
runtime (rt/runtime.s, canonical) provides deterministic ARC with atomic
counts, the free-at-zero release gate, the allocation runaway guard, and
line-atomic printing.
Cross-compilation: XXC_TARGET=linux xxc build foo.xxc on Windows produces
a Linux ELF; an aarch64 Linux host builds native AArch64 executables by
default.
Self-Hosting
The entire toolchain is written in xxc: `selfhost/lexer.xxc →
parser.xxc → typeck → gen.xxc → xas.xxc → xld`, merged into
selfhost/xxcc_full.xxc. The chain compiles itself, and every refresh
verifies the fixpoint: the chain's output on its own source is
byte-identical to the seed-built compiler's output
(selfhost/build_selfhost.sh), under the ARC gate closed and open.
$ bash selfhost/build_selfhost.sh
...
CHAIN REFRESH OK / FIXPOINT OK
The Rust seed (src/) plays the role of Go's "previous compiler": language
extensions land there first, and the fixpoint check proves the self-hosted
compiler stays semantically equivalent on everything else. The self-hosted
x86 assembler (xas.xxc) and linker (xld) are validated byte-exact against
the Rust assembler.rs/pelinker.rs outputs.