Skip to content

Language Reference

The complete semantics behind the Syntax tour.

No import needed. print writes without a newline, println adds one, input prints an optional prompt and reads a line (empty string on EOF).

print("name: ")
name := input()
println("Hello, {name}!")
answer := input("Continue? (y/n) ")
Type Description Example
int 64-bit signed integer 42, -1
float 64-bit IEEE 754 float 3.14, -0.5
bool Boolean true, false
str UTF-8 string "hello"
unit Empty tuple / void ()

Homogeneous, dynamically-sized arrays and insertion-ordered dicts:

nums: [int] = [1, 2, 3]
names: [str] = ["Ada", "Grace"]
ages: {str: int} = {"Ada": 36, "Grace": 85}

Structs are named records with typed fields and field access:

struct Point { x: int, y: int }
struct Rect { origin: Point, w: int, h: int }
p := Point{ x: 1, y: 2 }
r := Rect{ origin: p, w: 10, h: 20 }
println(p.x) // 1

Fields are mutable through the binding, and structs compose across modules:

p := Point{ x: 1, y: 2 }
p.x = 10

Two built-in variant types use dot-prefixed literals:

x := .some(42)
y := .none
a := .ok("success")
b := .err("failed")

int() and float() parse strings into Option (.none on failure):

count := input("How many? ") |> int() ?? 1

:= infers from the right-hand side; : Type = checks against it:

x := 42 // int
pi := 3.14 // float
y: int = 42 // OK
// y: int = 3.14 // ERROR: expected int, found float

Return types are inferred from the body when omitted, and generic parameters are inferred at the call site:

func add(a: int, b: int) { a + b } // -> int, inferred
func identity<T>(x: T) -> T { x }
result := identity(42) // T = int
func min<T: Ord>(a: T, b: T) -> T {
if a < b { a } else { b }
}
println(min(3, 5)) // 3
println(min(2.5, 1.5)) // 1.5

The standard library builds on this — vec.min_val, math.clamp and math.min_arr are all generic over ordered types.

Closures capture their environment:

mult := 3
triple := |x: int| x * mult
println(map(range(1, 4), triple)) // [3, 6, 9]

Parameter types can be omitted when inference can see through:

zz eval "range(1, 6) |> map(|x| x * 2) |> str"
[2, 4, 6, 8, 10]
Operator Operands Result
+ - * / % int, int int (division truncates: 7 / 2 == 3)
+ - * / float, float float
+ int, float float
+ str, str str (concatenation)
+ [T], [T] [T] (concatenation)
** int, int int (power)
== != any, same type bool
< > <= >= numbers bool
&& || bool bool
?? any, T T — unwraps Option, passes anything else through
|> a, f f(a) — with _ choosing the slot

Ranges come from range() in one-, two- and three-argument form, including negative steps:

range(5) // [0, 1, 2, 3, 4]
range(10, 15) // [10, 11, 12, 13, 14]
range(0, 20, 5) // [0, 5, 10, 15]
range(10, 0, -2) // [10, 8, 6, 4, 2]

Only false and .none are falsy — 0, "" and [] are truthy, so compare them explicitly:

if count == 0 {
println("empty")
}

When the checker hits a type error it marks the value instead of spraying follow-on errors:

x := undefined_var // ERROR: undefined variable `undefined_var`
y := x + 1 // no additional error

See Error Handling for turning these into great diagnostics.