Everything you need to know to start writing FezLang. Takes about 15 minutes.
One command on macOS or Linux:
curl -sSL https://fezlang.org/install.sh | sh
Verify:
$ fez --version
fez 1.0.0
See Install for manual download, build from source, and Windows support.
Create hello.fez:
io.print("hello, world")
Run it:
$ fez run hello.fez
hello, world
No imports needed for io — it's available everywhere. No main function, no boilerplate.
Variables are mutable by default with type inference. Use const for immutability.
// Type inference — the compiler knows the types
name = "FezLang" // str
version = 1 // int
pi = 3.14159 // f64
active = true // bool
// Explicit types when you want them
count: int = 0
ratio: f64 = 0.5
// Constants — cannot be reassigned
const MAX_SIZE = 1024
const APP_NAME = "MyApp"
// Mutable — reassignment is fine
x = 10
x = 20 // OK
Primitive types: int, f64, str, bool, byte, err.
Declared with fn. Arguments are typed. Return types come after ->.
fn greet(name: str) -> str {
return "hello, {name}!"
}
// Multiple return values
fn divide(a: f64, b: f64) -> f64, err {
if b == 0.0 {
return 0.0, error("division by zero")
}
return a / b, nil
}
// Calling functions
msg = greet("Cal")
io.print(msg)
result, err = divide(10.0, 3.0)
if err {
io.print(err.message)
} else {
io.print("result: {result}")
}
Functions are first-class values — you can pass them as arguments and return them from other functions.
Modules organize code. They hold functions, constants, and nested modules. They do not hold mutable state.
module math_utils {
const PI = 3.14159
fn circle_area(r: f64) -> f64 {
return PI * r * r
}
// Modules can nest
module convert {
fn deg_to_rad(deg: f64) -> f64 {
return deg * math_utils.PI / 180.0
}
}
}
// Access with dot syntax — always a path
area = math_utils.circle_area(5.0)
rad = math_utils.convert.deg_to_rad(90.0)
io.print("area: {area}, rad: {rad}")
The dot is always a module path or struct field access. Never a method call.
Structs are plain data containers. No methods, no constructors, no inheritance.
struct Point {
x: f64
y: f64
}
struct Rect {
origin: Point
width: f64
height: f64
}
// Instantiate with named fields
p = Point { x: 10.0, y: 20.0 }
io.print("({p.x}, {p.y})")
// Nested structs
r = Rect {
origin: Point { x: 0.0, y: 0.0 },
width: 100.0,
height: 50.0,
}
// Mutation — structs are mutable by default
p.x = 30.0
Functions operate on structs — structs don't have methods. This keeps data and behavior separate.
Simple enumerated types with implicit integer values.
enum Color {
Red // 0
Green // 1
Blue // 2
}
enum Direction {
North
South
East
West
}
c = Color.Red
if c == Color.Red {
io.print("It's red!")
}
d = Direction.East
io.print("heading: {d}") // 2
No exceptions. Functions that can fail return an err alongside their result.
// Create errors with error()
fn parse_age(s: str) -> int, err {
n = math.parse_int(s)
if n < 0 {
return 0, error("age cannot be negative")
}
return n, nil
}
// Check errors
age, err = parse_age("25")
if err {
io.print("Error: {err.message}")
}
// Ignore errors explicitly with _
_ = file.delete("temp.txt")
// Propagation pattern — check and re-wrap
fn load_user(id: int) -> User, err {
data, err = db.query("SELECT * FROM users WHERE id = ?", id)
if err {
return User{}, error("load_user failed: {err.message}")
}
return parse_user(data)
}
Standard if/else, for loops over arrays/ranges/maps, while, break, continue.
// If/else
if x > 10 {
io.print("big")
} else if x > 5 {
io.print("medium")
} else {
io.print("small")
}
// For over array
items = ["apple", "banana", "cherry"]
for item in items {
io.print(item)
}
// For with index
for i, item in items {
io.print("{i}: {item}")
}
// For over range
for i in 0..10 {
io.print("{i}") // 0 through 9
}
// For over map
ages = {"cal": 30, "luna": 5}
for key, val in ages {
io.print("{key} is {val}")
}
// While
count = 0
while count < 5 {
io.print("{count}")
count = count + 1
}
// Break and continue
for i in 0..100 {
if i % 2 == 0 { continue }
if i > 10 { break }
io.print("{i}")
}
Lambdas use |params| expr syntax. They capture variables by copy.
// Lambda syntax
double = |x| x * 2
io.print(double(5)) // 10
// Multi-param
add = |a, b| a + b
io.print(add(3, 4)) // 7
// Pass to higher-order functions
numbers = [1, 2, 3, 4, 5]
evens = filter(numbers, |n| n % 2 == 0)
// Function factories
fn make_multiplier(factor: int) -> fn(int) -> int {
return |x| x * factor // captures factor by copy
}
triple = make_multiplier(3)
io.print(triple(7)) // 21
Closures capture by copy — the original variable is never modified through the closure.
Spawn lightweight tasks. Communicate through channels. Wait for completion.
// Spawn a concurrent task
handle = spawn {
time.sleep(1000)
io.print("done!")
}
handle.wait()
// Channels for communication
ch = channel(1)
spawn {
ch <- "hello from spawned task"
}
msg = <-ch
io.print(msg)
// Parallel work pattern
fn fetch_all(urls: []str) -> []str {
ch = channel(len(urls))
for url in urls {
spawn {
resp, _ = http.get(url)
ch <- resp.body
}
}
results = []
for _ in urls {
results = append(results, <-ch)
}
return results
}
Spawned tasks capture variables by copy. The compiler enforces this — no shared mutable state.
By default, arguments are passed by value. Use ref for in-place mutation — required at both declaration and call site.
fn increment(counter: ref int) {
counter = counter + 1
}
x = 0
increment(ref x)
io.print(x) // 1
// Ref is explicit at both sites — mutation is always visible
fn swap(a: ref int, b: ref int) {
temp = a
a = b
b = temp
}
p = 5
q = 10
swap(ref p, ref q)
io.print("{p} {q}") // 10 5
Use {expr} inside double-quoted strings. Backtick strings are raw (no interpolation).
name = "Cal"
age = 30
// Interpolation with {}
io.print("Hello, {name}! You are {age} years old.")
io.print("Next year: {age + 1}")
// Raw strings with backticks — no interpolation
pattern = `\d+\.\d+`
io.print(pattern) // \d+\.\d+
// Escape sequences in double-quoted strings
io.print("line 1\nline 2")
io.print("tab\there")
io.print("quote: \"hello\"")
Defer runs a statement when the current function returns. Multiple defers execute in LIFO (last-in, first-out) order.
fn process() {
f = file.open("data.txt")
defer file.close(f) // runs when process() returns
lock = acquire_lock()
defer release_lock(lock) // runs before file.close
// Work with f and lock...
// Both are cleaned up automatically
}
// Common pattern: open/close, lock/unlock, connect/disconnect
fn query_db(sql: str) -> []Row, err {
conn, err = db.connect("postgres://localhost/app")
if err { return [], err }
defer db.close(conn)
return db.query(conn, sql)
}
Let's build a CLI TODO app that uses everything we've learned — structs, modules, arrays, while loop, and user input.
struct Task {
id: int
text: str
done: bool
}
module todo {
next_id = 1
tasks = []
fn add(text: str) -> Task {
task = Task { id: next_id, text: text, done: false }
tasks = append(tasks, task)
next_id = next_id + 1
return task
}
fn complete(id: int) -> bool {
for i, task in tasks {
if task.id == id {
tasks[i].done = true
return true
}
}
return false
}
fn list() {
if len(tasks) == 0 {
io.print(" No tasks yet.")
return
}
for task in tasks {
mark = if task.done { "x" } else { " " }
io.print(" [{mark}] #{task.id}: {task.text}")
}
}
}
io.print("FezTodo — type 'help' for commands")
while true {
input = io.read("> ")
parts = str.split(str.trim(input), " ")
cmd = parts[0]
if cmd == "quit" { break }
else if cmd == "help" {
io.print(" add — add a task")
io.print(" done — mark task complete")
io.print(" list — show all tasks")
io.print(" quit — exit")
}
else if cmd == "add" {
text = str.join(parts[1:], " ")
task = todo.add(text)
io.print(" Added #{task.id}: {task.text}")
}
else if cmd == "done" {
id = math.parse_int(parts[1])
if todo.complete(id) {
io.print(" Completed #{id}")
} else {
io.print(" Task not found")
}
}
else if cmd == "list" { todo.list() }
else { io.print(" Unknown command. Type 'help'.") }
}
Save as todo.fez and run with fez run todo.fez. That's a complete interactive application in under 60 lines — structs for data, a module for organization, functions for behavior.
Next: Language Reference — the complete guide to every FezLang feature.