STEP 1Your first program
A program is one or more .cy files. There is no required main: you name the function to run, and its parameters come from --arg. A run shows the function's return value, and print(v) writes a line to standard output.
-- hello.cy: the sum of the squares 1..n
pub fn main(n: u32) -> u64
mut total = u64(0)
mut i = u32(1)
while i <= n do
total = total + u64(i * i)
i = i + u32(1)
end
return total
end
$ century hello.cy --jit main --arg 10
hello.cy: ok - 0 types, 1 functions, 52 tokens, 26 nodes, 46 interned
jit main: 256 bytes (module 256)
jit main = 385
Manual: Quick start →
STEP 2A program you can hand out
--build compiles once into a standalone program, the way a C or Rust compiler does. It starts at main, takes each word on the command line as one parameter, and checks each one's type and range. On Windows it needs no DLL beyond Windows' own; on macOS and Linux it is named without .exe.
$ century hello.cy --build hello.exe
hello.cy: ok - 0 types, 1 functions, 52 tokens, 26 nodes, 46 interned
built hello.exe: 256 bytes of machine code, starting at main
$ hello.exe 10
385
$ hello.exe ten
hello.exe: `ten` is not a u32 for n (a whole number from 0 to 4294967295)
usage: hello.exe <n: u32>
Manual: Building a program →
STEP 3Types never convert by themselves
Both sides of an operator must already have the same type. There is no promotion, so you write the conversion. Integer division or remainder by zero gives 0 and does not crash.
mut a = u32(7)
mut b = u64(5)
c = u64(a) + b -- fine: one side converted
d = a + b -- refused
error: arithmetic needs the same numeric type on both sides, found u32 and u64
Manual: Operators →
STEP 4mut and the assignment rule
mut name = e always declares a fresh binding. name = e declares an immutable one when the name is new, and assigns when it names an existing mut binding. A binding can also name its type: mut x: u32 = 0.
mut x = 5 -- declares a NEW mutable binding
y = 10 -- y is not in scope: declares a NEW immutable binding
x = x + 1 -- x is in scope and mutable: assigns to it
y = 3 -- error: y is immutable
Manual: Bindings →
STEP 5Structs and sum types
A type is either a struct with fields or a sum with case lines. A match must handle every variant; there is no default case, so adding a variant later makes every match that misses it a compile error.
type Shape
case Circle(r: f64)
case Rect(w: f64, h: f64)
case Empty
end
fn area(s: Shape) -> f64
match s
case Circle(r) then
return 3.14159 * r * r
case Rect(w, h) then
return w * h
case Empty then
return 0.0
end
end
Manual: Sum types and match →
STEP 6Every core with cpu do
The loop inside cpu do runs on every CPU thread at once. The compiler proves every iteration independent or refuses the program, so it can never race. Here each iteration reads what the one before wrote:
-- prefix.cy
pub fn prefix(n: u32) -> u32
mut a = array[u32](n)
cpu do
mut i = u32(1)
while i < n do
a[i] = a[i - u32(1)] + i
i = i + u32(1)
end
end
return a[n - u32(1)]
end
prefix.cy:4:0: error: this `cpu` block cannot be run wide: an array is read at one element and written at another
4 | cpu do
| ^
= help: every access to an array written here has to use the same subscript, or one iteration could read what another is writing
Manual: Parallel loops on the CPU →
STEP 7The GPU with gpu do
Mark a loop nest whose iterations are independent and it runs on the GPU. Data moves between CPU and GPU only when needed, and the answer is bit for bit what the same loop gives on the CPU. A tiled 2048 × 2048 matrix product using on-chip shared memory ran in 6.4 ms on an RTX 4060 Laptop GPU.
gpu do
mut i = u32(0)
while i < n do
mut old = atomic_add(hist, i % u32(16), u32(1))
i = i + u32(1)
end
end
Manual: Parallel loops on the GPU →
STEP 8Error budgets with ~
Without ~, floating-point arithmetic happens in exactly the order written. With it, the compiler keeps sixteen f64 partial sums going at once, but only when the loop's length times the type's precision fits your budget. Otherwise the loop runs as written.
mut acc ~ 1e-6 = 0.0 -- any order, within 1e-6
mut i = u32(0)
while i < len(a) do
acc = acc + a[i] * a[i]
i = i + u32(1)
end
Over a million f64 values: 0.43 ms in source order, 0.15 ms with the budget.
Manual: Floating-point sums →
STEP 9Calling the system
extern declares a function that lives in a system library. The compiler finds it when it compiles, so a misspelt name is an error at once. On macOS, name /usr/lib/libSystem.B.dylib; on Windows, the DLL.
extern "kernel32.dll" fn GetTickCount64() -> u64
extern "kernel32.dll" fn Sleep(ms: u32)
extern "user32.dll" fn MessageBoxA(owner: u64, text: string, title: string, kind: u32) -> i32
pub fn demo() -> u32
Sleep(u32(100))
r = MessageBoxA(u64(0), "Hello from Century", "Century", u32(0))
return u32(r)
end
Manual: Calling the operating system →
STEP 10The compiler as a library
A program can require the compiler, compile Century source text while it runs, and call the result in the same process. No compiler process is started. Two thousand compiles in one process took about 0.11 ms each, with the call.
mut C = require("../Century/CenturyWindows/Source/century.cy")
pub fn main(z: u32) -> u32
hp = C.lib_init()
j = C.lib_compile(hp, bytes("pub fn sq(x: i64) -> i64\n return x * x\nend\n"), bytes("sq.cy"), i32(1))
if not C.lib_ok(j) then
return u32(1)
end
C.lib_use(hp, j)
mut args = array[u64](0)
push(args, u64(12))
return u32(C.lib_callfn(hp, j, C.lib_fn(hp, j, bytes("sq")), args))
end
Manual: Compiling inside a running program →