xxc

可编译可解释的系统级语言 - 确定性 ARC 无 GC / 全自举工具链 / 三后端逐字节一致

📂 开发工具#编程语言#编译器#自举#跨平台#无GC#并发#开发工具📅 2026-10-9

Language Guide

📅 2026-10-9

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 ·

> Concurrency Guide ·

> Implementation & Bootstrap ·

> 中文版:语言指南

---

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: spawn runs 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 struct deep-copies it (nested structs included).

Mutating a copy never affects the original.

  • Assigning a class shares 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).
  • -> T declares 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 when expr is 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
  • struct receivers receive a copy; class receivers 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 bare Point.
  • match dispatches 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, y returns the tuple (x, y).
  • a, b := expr destructures; 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

Standard Library Reference.

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.