Language Reference

A complete reference for every construct in FezLang. For a guided introduction, see Getting Started.

Lexical Structure

Comments

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

Keywords

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

Operators

Arithmetic, comparison, logical, assignment, and special operators:

+   -   *   /   %               // arithmetic
==  !=  <   >   <=  >=          // comparison
&&  ||  !                       // logical
=   +=  -=  *=  /=  %=         // assignment
..                              // range
<-  ->                          // channel / return type

Identifiers

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 System

Primitive types

TypeDescription
int64-bit signed integer
f6464-bit IEEE 754 floating-point
strUTF-8 encoded string
boolBoolean — true or false
byte8-bit unsigned integer
errError value, or nil for no error

Arrays

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

Maps are unordered key-value collections. Declared with {key_type: value_type}.

ages: {str: int} = {"cal": 30, "luna": 5}
config = {"debug": true, "verbose": false}

Type inference

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)

Type conversion

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

nil is only valid for the err type. It represents the absence of an error. Other types cannot be nil.

Implicit generics

Containers (arrays, maps, channels) are generic without explicit syntax. The compiler tracks element types internally. There is no user-facing generics syntax.

Variables

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

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

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

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

Functions are declared with fn. Parameters are typed. Return types follow ->.

Basic declaration

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}")
}

Multiple return values

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)
}

First-class functions

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

Lambdas

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)

Ref parameters

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

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)
}

Control Flow

if / else

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")
}

for...in

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}")
}

while

count = 0
while count < 5 {
  io.print(count)
  count += 1
}

break and continue

for i in 0..100 {
  if i % 2 == 0 { continue }  // skip even numbers
  if i > 15 { break }         // stop after 15
  io.print(i)
}

Error Handling

FezLang has no exceptions. Functions that can fail return an err value alongside their result. Errors are values that you check explicitly.

Creating errors

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
}

Checking errors

age, err = validate_age(-5)
if err {
  io.print("invalid: {err.message}")
}

// Ignore an error explicitly with _
_ = file.delete("temp.txt")

Error propagation

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
}

Concurrency

FezLang provides lightweight concurrency with spawn and communication via channels. The compiler enforces that spawned tasks capture variables by copy, preventing shared mutable state.

Spawning tasks

// 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

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

Capture by copy

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

Rules

  • No shared mutable state between tasks.
  • All data crossing task boundaries is copied.
  • Use channels for communication, not shared variables.
  • Use handle.wait() to synchronize completion.

Strings

Double-quoted strings

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\"")

Escape sequences

SequenceMeaning
\nNewline
\tTab
\rCarriage return
\\Backslash
\"Double quote
\0Null byte

Raw strings

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

Imports

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.

Operator Precedence

From highest to lowest precedence:

LevelOperatorsDescription
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.

Grammar

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 ;