A complete reference for every construct in FezLang. For a guided introduction, see Getting Started.
Line comments start with // and extend to the end of the line. Block comments are delimited by /* and */.
// This is a line comment
/* This is a
block comment */
x = 42 // inline comment
The following identifiers are reserved:
module fn struct enum const
if else for in while
return ref import spawn channel
defer break continue true false
nil
Arithmetic, comparison, logical, assignment, and special operators:
+ - * / % // arithmetic
== != < > <= >= // comparison
&& || ! // logical
= += -= *= /= %= // assignment
.. // range
<- -> // channel / return type
Identifiers begin with a letter or underscore, followed by any combination of letters, digits, and underscores. Identifiers are case-sensitive.
name // valid
_count // valid
item2 // valid
MAX_SIZE // valid (convention: constants are UPPER_SNAKE_CASE)
| Type | Description |
|---|---|
int | 64-bit signed integer |
f64 | 64-bit IEEE 754 floating-point |
str | UTF-8 encoded string |
bool | Boolean — true or false |
byte | 8-bit unsigned integer |
err | Error value, or nil for no error |
Arrays are ordered, dynamically-sized sequences of a single type. Declared with []type.
numbers: []int = [1, 2, 3, 4, 5]
names: []str = ["alice", "bob"]
empty = [] // type inferred from first insertion
Maps are unordered key-value collections. Declared with {key_type: value_type}.
ages: {str: int} = {"cal": 30, "luna": 5}
config = {"debug": true, "verbose": false}
The compiler infers types from the right-hand side of assignments. Explicit annotations are optional but allowed.
x = 42 // int (inferred)
y: f64 = 42 // f64 (explicit)
Convert between types with built-in conversion functions:
n = 42
f = f64(n) // 42.0
s = str(n) // "42"
b = byte(65) // 65 (ASCII 'A')
i = int(3.14) // 3 (truncates)
nil is only valid for the err type. It represents the absence of an error. Other types cannot be nil.
Containers (arrays, maps, channels) are generic without explicit syntax. The compiler tracks element types internally. There is no user-facing generics syntax.
Variables are mutable by default. Use const for immutable bindings. All variables are block-scoped.
// Mutable by default
x = 10
x = 20 // OK — reassignment
// Immutable with const
const MAX = 100
// MAX = 200 // compile error: cannot reassign const
// Type inference
name = "FezLang" // str
count = 0 // int
pi = 3.14159 // f64
active = true // bool
// Explicit type annotation
ratio: f64 = 0.5
label: str = "none"
// Block scoping
if true {
inner = 42
io.print(inner) // OK
}
// io.print(inner) // compile error: inner not in scope
Structs are plain data containers with named, typed fields. They have no methods, no constructors, and no inheritance. Functions operate on structs externally.
// Declaration
struct Point {
x: f64
y: f64
}
struct Circle {
center: Point
radius: f64
}
// Instantiation with named fields
p = Point { x: 10.0, y: 20.0 }
// Field access
io.print(p.x) // 10.0
// Nested structs
c = Circle {
center: Point { x: 0.0, y: 0.0 },
radius: 5.0,
}
io.print(c.center.x) // 0.0
// Mutation — fields are mutable by default
p.x = 30.0
// Passed by value (copied) by default
fn move_point(pt: Point, dx: f64, dy: f64) -> Point {
pt.x = pt.x + dx
pt.y = pt.y + dy
return pt // returns a modified copy
}
// Use ref for in-place mutation
fn move_in_place(pt: ref Point, dx: f64, dy: f64) {
pt.x = pt.x + dx
pt.y = pt.y + dy
}
move_in_place(ref p, 1.0, 2.0)
io.print("({p.x}, {p.y})") // (31.0, 22.0)
Enums define a set of named integer constants. Values are implicitly assigned starting at 0.
enum Color {
Red // 0
Green // 1
Blue // 2
}
enum Direction {
North
South
East
West
}
// Usage — always qualified with the enum name
c = Color.Red
d = Direction.East
// Comparison
if c == Color.Red {
io.print("red")
}
if d == Direction.North {
io.print("heading north")
}
Modules group related functions and constants. Access members with dot syntax.
module geometry {
const PI = 3.14159
fn circle_area(r: f64) -> f64 {
return PI * r * r
}
fn circle_circumference(r: f64) -> f64 {
return 2.0 * PI * r
}
// Nested modules
module convert {
fn deg_to_rad(deg: f64) -> f64 {
return deg * geometry.PI / 180.0
}
fn rad_to_deg(rad: f64) -> f64 {
return rad * 180.0 / geometry.PI
}
}
}
// Access via dot path
area = geometry.circle_area(5.0)
rad = geometry.convert.deg_to_rad(90.0)
io.print("area: {area}")
io.print("radians: {rad}")
Modules hold functions and constants. They are intended to be stateless containers for organizing logic.
Note: The spec states that modules should not hold mutable state. In practice, module-level variables are allowed by the compiler, but keeping modules free of mutable state is the recommended convention.
Functions are declared with fn. Parameters are typed. Return types follow ->.
fn greet(name: str) -> str {
return "hello, {name}!"
}
fn add(a: int, b: int) -> int {
return a + b
}
// No return type
fn log(msg: str) {
io.print("[LOG] {msg}")
}
Functions can return multiple values, commonly used for the value-plus-error pattern.
fn divide(a: f64, b: f64) -> f64, err {
if b == 0.0 {
return 0.0, error("division by zero")
}
return a / b, nil
}
result, err = divide(10.0, 3.0)
if err {
io.print("failed: {err.message}")
} else {
io.print(result)
}
Functions are values. They can be assigned to variables and passed as arguments.
fn apply(x: int, f: fn(int) -> int) -> int {
return f(x)
}
fn double(n: int) -> int {
return n * 2
}
result = apply(5, double)
io.print(result) // 10
// Assign to variable
op = double
io.print(op(7)) // 14
Anonymous functions use |params| expr syntax. They capture variables from the enclosing scope by copy.
square = |x| x * x
add = |a, b| a + b
io.print(square(4)) // 16
io.print(add(2, 3)) // 5
// Capture by copy
base = 10
offset = |x| x + base
base = 999
io.print(offset(5)) // 15 — captured original base (10)
Use ref in both the declaration and at the call site for in-place mutation.
fn increment(n: ref int) {
n = n + 1
}
count = 0
increment(ref count)
io.print(count) // 1
defer schedules a statement to run when the enclosing function returns. Multiple defers execute in LIFO order.
fn read_file(path: str) -> str, err {
f, err = file.open(path)
if err { return "", err }
defer file.close(f)
return file.read_all(f)
}
Conditions do not use parentheses. Braces are required.
if x > 10 {
io.print("big")
} else if x > 5 {
io.print("medium")
} else {
io.print("small")
}
Iterate over arrays, maps, and ranges.
// Array
items = ["apple", "banana", "cherry"]
for item in items {
io.print(item)
}
// Array with index
for i, item in items {
io.print("{i}: {item}")
}
// Range (0 up to but not including 10)
for i in 0..10 {
io.print(i)
}
// Map
ages = {"cal": 30, "luna": 5}
for key, val in ages {
io.print("{key} is {val}")
}
count = 0
while count < 5 {
io.print(count)
count += 1
}
for i in 0..100 {
if i % 2 == 0 { continue } // skip even numbers
if i > 15 { break } // stop after 15
io.print(i)
}
FezLang has no exceptions. Functions that can fail return an err value alongside their result. Errors are values that you check explicitly.
fn validate_age(age: int) -> int, err {
if age < 0 {
return 0, error("age cannot be negative")
}
if age > 150 {
return 0, error("age is unrealistic")
}
return age, nil // nil means no error
}
age, err = validate_age(-5)
if err {
io.print("invalid: {err.message}")
}
// Ignore an error explicitly with _
_ = file.delete("temp.txt")
There is no automatic propagation. Check the error, optionally wrap it, and return it.
fn load_config(path: str) -> Config, err {
data, err = file.read(path)
if err {
return Config{}, error("load_config: {err.message}")
}
config, err = parse_config(data)
if err {
return Config{}, error("load_config: {err.message}")
}
return config, nil
}
FezLang provides lightweight concurrency with spawn and communication via channels. The compiler enforces that spawned tasks capture variables by copy, preventing shared mutable state.
// Spawn a block
handle = spawn {
io.print("running in background")
}
// Spawn a function call
handle2 = spawn do_work(arg1, arg2)
// Wait for completion
handle.wait()
handle2.wait()
Channels are typed, buffered queues for communicating between tasks.
// Create a buffered channel
ch = channel(10)
// Send with <-
spawn {
ch <- "hello"
ch <- "world"
}
// Receive with <-
msg1 = <-ch
msg2 = <-ch
io.print("{msg1}, {msg2}") // hello, world
Spawned blocks and functions capture all referenced variables by copy. The compiler enforces this to prevent data races.
counter = 0
spawn {
// counter here is a copy — modifying it does not
// affect the outer counter
counter = counter + 1
io.print("inner: {counter}") // inner: 1
}
io.print("outer: {counter}") // outer: 0
handle.wait() to synchronize completion.Support interpolation with {expr} and escape sequences.
name = "Cal"
age = 30
// Interpolation
greeting = "Hello, {name}! You are {age} years old."
io.print("Next year: {age + 1}")
// Escape sequences
io.print("line 1\nline 2")
io.print("col1\tcol2")
io.print("she said \"hello\"")
| Sequence | Meaning |
|---|---|
\n | Newline |
\t | Tab |
\r | Carriage return |
\\ | Backslash |
\" | Double quote |
\0 | Null byte |
Backtick-delimited strings have no interpolation and no escape processing.
// No interpolation, no escapes
pattern = `\d+\.\d+`
io.print(pattern) // \d+\.\d+
path = `C:\Users\cal\docs`
io.print(path) // C:\Users\cal\docs
Use import to bring in standard library modules or local files.
// Import a standard library module
import "http"
import "json"
import "math"
// Import a local file (relative path)
import "./utils"
import "./models/user"
// Use imported modules
resp, err = http.get("https://example.com")
data = json.parse(resp.body)
io.print(math.sqrt(144.0))
Standard library modules like io, str, and math are available without an explicit import in simple scripts. For larger projects, explicit imports are recommended for clarity.
From highest to lowest precedence:
| Level | Operators | Description |
|---|---|---|
| 1 | () [] . | Grouping, indexing, member access |
| 2 | ! - | Unary not, unary negate |
| 3 | * / % | Multiplicative |
| 4 | + - | Additive |
| 5 | .. | Range |
| 6 | == != < > <= >= | Comparison |
| 7 | && | Logical AND |
| 8 | || | Logical OR |
| 9 | <- | Channel send / receive |
| 10 | = += -= *= /= %= | Assignment |
Use parentheses to override precedence when intent is unclear.
A condensed EBNF grammar for FezLang.
program = { declaration } ;
declaration = module_decl
| fn_decl
| struct_decl
| enum_decl
| import_decl
| statement ;
module_decl = "module" IDENT "{" { declaration } "}" ;
fn_decl = "fn" IDENT "(" [ param_list ] ")" [ "->" type_list ] block ;
param_list = param { "," param } ;
param = [ "ref" ] IDENT ":" type ;
type_list = type { "," type } ;
struct_decl = "struct" IDENT "{" { field_decl } "}" ;
field_decl = IDENT ":" type ;
enum_decl = "enum" IDENT "{" { IDENT } "}" ;
import_decl = "import" STRING ;
statement = const_decl
| assign_stmt
| if_stmt
| for_stmt
| while_stmt
| return_stmt
| defer_stmt
| spawn_stmt
| break_stmt
| continue_stmt
| expr_stmt ;
const_decl = "const" IDENT "=" expression ;
assign_stmt = target_list assign_op expression ;
target_list = target { "," target } ;
target = IDENT | IDENT "." IDENT | IDENT "[" expression "]" ;
assign_op = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
if_stmt = "if" expression block { "else" "if" expression block } [ "else" block ] ;
for_stmt = "for" IDENT [ "," IDENT ] "in" expression block ;
while_stmt = "while" expression block ;
return_stmt = "return" [ expression { "," expression } ] ;
defer_stmt = "defer" expression ;
spawn_stmt = "spawn" ( block | expression ) ;
break_stmt = "break" ;
continue_stmt = "continue" ;
expr_stmt = expression ;
block = "{" { statement } "}" ;
expression = or_expr ;
or_expr = and_expr { "||" and_expr } ;
and_expr = compare_expr { "&&" compare_expr } ;
compare_expr = range_expr { ( "==" | "!=" | "<" | ">" | "<=" | ">=" ) range_expr } ;
range_expr = add_expr [ ".." add_expr ] ;
add_expr = mul_expr { ( "+" | "-" ) mul_expr } ;
mul_expr = unary_expr { ( "*" | "/" | "%" ) unary_expr } ;
unary_expr = ( "!" | "-" ) unary_expr | postfix_expr ;
postfix_expr = primary { "." IDENT | "[" expression "]" | "(" [ arg_list ] ")" } ;
arg_list = [ "ref" ] expression { "," [ "ref" ] expression } ;
primary = INT_LIT | FLOAT_LIT | STRING | RAW_STRING | "true" | "false" | "nil"
| IDENT
| "(" expression ")"
| "[" [ expression { "," expression } ] "]"
| "{" [ map_entry { "," map_entry } ] "}"
| lambda
| "channel" "(" expression ")"
| "<-" expression ;
lambda = "|" [ IDENT { "," IDENT } ] "|" expression ;
map_entry = expression ":" expression ;
type = "int" | "f64" | "str" | "bool" | "byte" | "err"
| "[]" type
| "{" type ":" type "}"
| "fn" "(" [ type_list ] ")" [ "->" type_list ]
| IDENT ;