Zed is a statically-typed, compiled systems language that transpiles to C++17.
Inspired by C, Odin, Jai and Zig. File extension: .z. Namespace: ZedLang.
Pointer dereference operator: .* (Zig-style).
Note
Input-related documentation has not been added yet.
- Comments
- Primitive Types
- Literals
- Constants
- Variables
- Operators — arithmetic, bitwise, logical, ternary
?: - Type Casts —
cast,bit_cast - Pointers
- Arrays & Slices
- Dynamic Arrays
- Structs
- Unions
- Variants (Tagged Unions)
- Enums
- Procedures — basic, multi-return, named-return,
or_return,or_else - Named Return Values
- Proc Literals
- Control Flow — if, for (6 forms), labeled break/continue
- Defer
- Match
- When
- typeid
- Compile-time Assert
- String Types & Conversion
- Built-in Operations
- C Interop
- Multi-file Projects
- sizeof / alignof
-- single-line comment (no block comments)| Zed type | C equivalent |
|---|---|
i8 |
int8_t |
i16 |
int16_t |
i32 |
int32_t |
i64 |
int64_t |
u8 |
uint8_t |
u16 |
uint16_t |
u32 |
uint32_t |
u64 |
uint64_t |
f32 |
float |
f64 |
double |
bool |
bool |
cstr |
const char* |
string |
std::string |
void |
void |
42 -- decimal integer
0xFF -- hex
0b1010 -- binary
0o17 -- octal
3.14 -- float (f32 default; annotate type or cast for f64)
true false -- bool
nil -- null pointer
"hello" -- cstr (escape: \n \t \\ \")Declared with ::. Evaluated at compile time. Supports full integer expression folding across names.
MAX :: 100
HALF :: MAX / 2
CANVAS :: GRID * CELL_SIZE -- chains of const exprs work
PI :: 3.14159265-- Type-inferred declaration (:=)
x := 42
name := "zed" -- inferred as cstr
-- Explicit type, no init (zero-initialised)
count: i32
-- Explicit type with init
speed: f32 = 0.5
-- Multiple declaration from multi-return call
ok, val := parse_int(s)
-- Tuple literal declaration: declare and assign multiple variables at once
a, b, c := 1, "hello", 3.14
-- Tuple literal assignment: assign multiple existing variables at once
-- All RHS values are evaluated before any assignment — safe for swaps
x, y = y, x -- swap
p, q, r = r, p, q -- rotation
-- Discard with _
_, val2 := fallible_call()Global variables are declared at the top level (outside any proc).
Foreign C types declared globally are zero-initialised with = {}:
window: Window -- foreign C type, zero-init
score := 0 -- global, type inferredC keyword shadowing. Zed allows variable names that happen to be C/C++ reserved words (
double,float,int, …). The code generator automatically renames them with a_zl_prefix in the output so compilation is never affected. The names remain usable as-is in Zed source.
Arithmetic: + - * / %
Bitwise: & | ^ << >>
Comparison: == != < <= > >=
Logical: && || ! — aliases: and or not
Compound assign: += -= *= /= %= &= |= ^= <<= >>=
Ternary: cond ? then_expr : else_expr
Pointer arithmetic: ptr + n, ptr - n, ptr - ptr (→ i64)
Zed has no
++/--operators. Usex += 1/x -= 1.
The ternary operator is right-associative, allowing natural chaining:
grade := score >= 90 ? "A" : score >= 70 ? "B" : "C"Explicit only — no implicit numeric coercions.
cast(f32)(my_i32)
cast(i32)(3.7)
cast(*u8)(raw_ptr)bit_cast reinterprets the raw bits of a value as another type. Source and destination must have the same byte size. No conversion is performed — the bit pattern is preserved exactly.
f: f32 = 1.0
bits := bit_cast(u32)(f) -- u32 with same bits as f32 1.0 = 0x3F800000
back := bit_cast(f32)(bits) -- back to f32: 1.0x := 10
p := &x -- take address: *i32
p.* = 20 -- dereference and assign (.* postfix)
val := p.* -- dereference and read
-- Pointer indexing
arr_ptr[2] -- equivalent to (arr_ptr + 2).*
arr_ptr + 1 -- pointer arithmeticFixed arrays — size must be a compile-time integer literal, a :: constant, or any arithmetic expression (+, -, *, /, %) over those.
nums: [4]i32
nums[0] = 1
-- Aggregate initializer
verts: [3]Vec3 = { Vec3{x=0.0,y=0.0,z=0.0}, Vec3{x=1.0,y=0.0,z=0.0}, Vec3{x=0.0,y=1.0,z=0.0} }
-- Named constant as size
MAX_ITEMS :: 64
buf: [MAX_ITEMS]u8
-- Const-expr size (full arithmetic supported)
GRID :: 20
CELL_SIZE :: 4
cells: [GRID * GRID]i32 -- 400 elements
pixels: [GRID * GRID * CELL_SIZE]u8 -- 1600 elements
border: [(GRID + 2) * (GRID + 2)]i32 -- with paddingSlices — fat pointer (data + length).
s: []i32
sub := arr[1 ..< 4] -- exclusive upper bound [1, 4)
sub2 := arr[1 ..= 4] -- inclusive upper bound [1, 4]
full := arr[:] -- full sliceHeap-allocated, growable arrays backed by std::vector<T>. Declared with the [dynamic] prefix.
-- Declaration (empty, zero-length)
nums: [dynamic]i32
-- Append a value (pass pointer to array)
append(&nums, 10)
append(&nums, 20)
-- Length and capacity
n := len(nums) -- current element count → u64
cap_val := cap(nums) -- current capacity → u64
-- Pre-allocate capacity
-- Sets capacity to n; len stays 0. Reserved slots are zero-initialized.
reserve(&nums, 100)
-- Index (same syntax as fixed arrays)
first := nums[0]
nums[1] = 99
-- Clear the array (len → 0, retains allocation)
clear(&nums)
reservesemantics.reserve(&x, n)allocates backing memory for at leastnelements and zero-initializes all reserved slots.len(x)remains0;cap(x)returnsn. Elements in the reserved range[0, cap-1]read as zero and can be written directly by index.
len works on fixed arrays, slices, string, and dynamic arrays.
Soft keywords.
len,cap,append,reserve,clear,to_cstr,from_cstr,panic,free,copy, andenum_nameare recognised as built-in calls only when used as calls. They may freely be used as variable names in other positions (cap_val := cap(nums)orlen := 0both work).
Vec2 :: struct {
x: f32,
y: f32,
}
-- Literal
v := Vec2{ x = 1.0, y = 2.0 }
-- Field access
v.x = 3.0
-- Struct update (spread) — copy base, override named fields
v2 := Vec2{ ..v, x = 10.0 }
-- Pointer to struct
p := &v
p.*.x = 5.0Same syntax as structs but all fields share the same memory (C union semantics). Size equals the largest field. Fields may be separated by either , or ;.
Value :: union {
as_int: i64,
as_float: f64,
as_ptr: *u8,
}
v: Value
v.as_int = 42
x := v.as_float -- reinterprets the same bytesA variant is a tagged union — it pairs a data union with an automatic discriminant enum so you always know which field is active. Use it when a value can be one of several distinct types.
CircleData :: struct { radius: f32 }
RectData :: struct { width: f32, height: f32 }
Shape :: variant {
Circle: CircleData,
Rect: RectData,
}The compiler generates three C++ types automatically:
Shape_Tag— an enum class with one variant per fieldShape_Data— a raw union holding the payloadsShape— a wrapper struct with.tagand.datafields
Construction — set .tag and .data explicitly:
circle := Shape{
tag = Shape_Tag.Circle,
data = Shape_Data{ Circle = CircleData{ radius = 2.0 } },
}Pattern matching — use match with .Variant(binding) to extract the payload:
area :: proc(s: Shape) -> f32 {
match s {
case .Circle(c) { return 3.14159 * c.radius * c.radius }
case .Rect(r) { return r.width * r.height }
else { return 0.0 }
}
}The binding variable (c, r) is a reference to the active payload field — mutating it writes back into the variant.
Without binding — use .Variant alone when you don't need the payload:
match s {
case .Circle { printf("it's a circle\n") }
case .Rect { printf("it's a rect\n") }
}Direction :: enum {
North,
South,
East,
West,
}
-- Explicit values
Status :: enum {
Ok = 0,
Error = 1,
Retry = 2,
}
d := Direction.North-- Basic
add :: proc(a: i32, b: i32) -> i32 {
return a + b
}
-- Void return (no ->)
greet :: proc(name: cstr) {
printf("%s\n", name)
}
-- Multiple return values
min_max :: proc(a: i32, b: i32) -> (i32, i32) {
return a, b
}
lo, hi := min_max(3, 7)
-- Named return values (see §Named Return Values)
divide :: proc(a: i32, b: i32) -> (result: i32, ok: bool) {
if b == 0 { return }
result = a / b
ok = true
return
}
-- Proc type annotation (function pointer)
callback: proc(i32) -> bool
-- Variadic (C-style, must be last parameter, only usable via cimport)
printf :: proc(fmt: cstr, ..) -> i32 ---
-- Extern / no body
puts :: proc(s: cstr) -> i32 ---Postfix operator for propagating errors out of a proc. The callee must return (T, bool); if the bool is false, the enclosing proc immediately returns a zero-value T and false.
read_int :: proc(s: cstr) -> (i32, bool) { ... }
parse :: proc(input: cstr) -> (i32, bool) {
n := read_int(input) or_return -- on failure: returns (0, false)
return n * 2, true
}Like or_return but instead of propagating failure, yields a default value. Can be used in any context — no enclosing (T, bool) proc required.
n := read_int(input) or_else 0 -- returns 0 if parsing failed
s := find_key(map, key) or_else "missing"Return variables can be given names in the signature. They are zero-initialised automatically and a bare return commits whatever values they hold at that point.
divide :: proc(a: i32, b: i32) -> (result: i32, ok: bool) {
if b == 0 { return } -- result=0, ok=false (zero values)
result = a / b
ok = true
return
}
q, valid := divide(10, 2)Named returns are useful for documenting what each return slot means and for early-exit patterns where the zero-value is a sensible default.
Anonymous proc expressions. Capture semantics follow C++ lambda [=] (capture by copy).
-- Assigned to a proc-typed variable
double: proc(i32) -> i32 = proc(x: i32) -> i32 { return x * 2 }
-- Passed directly as an argument
apply :: proc(f: proc(i32) -> i32, v: i32) -> i32 { return f(v) }
result := apply(proc(x: i32) -> i32 { return x + 1 }, 10)if x > 0 {
-- ...
} else if x == 0 {
-- ...
} else {
-- ...
}-- Infinite loop
for { break }
-- While-style
for x > 0 { x -= 1 }
-- Range (exclusive)
for i in 0 ..< 10 { }
-- Range (inclusive) with optional step
for i in 0 ..= 100 step 5 { }
-- For-each: iterate over array, slice, [dynamic]T, or string
for item in nums { }
-- For-each with index
for i, item in nums { }All six forms support labels for break/continue targeting:
outer: for i in 0 ..< rows {
for j in 0 ..< cols {
if grid[i] == 0 { continue outer }
if found { break outer }
}
}Labels work on all six for forms. Place the label before the for keyword; break and continue can target any enclosing labelled loop.
i := 0
outer: for {
i += 1
j := 0
inner: for {
j += 1
if j == 3 { continue outer }
if i == 5 { break outer }
}
}return -- void proc
return 42 -- single value
return a, b -- multi-returnRuns at the end of the enclosing block (LIFO order). Works inside loops — defers fire each iteration.
open_file :: proc() {
f := fopen("data.bin", "rb")
defer fclose(f) -- runs when proc returns
-- ...
}
-- Inside a loop — fires every iteration
for not WindowShouldClose() {
BeginDrawing()
defer EndDrawing()
BeginMode2D(camera)
defer EndMode2D()
-- ...
}Pattern-match on any integer or enum value. else is the default case.
match direction {
case Direction.North { go_north() }
case Direction.South { go_south() }
else { stop() }
}When matching on an enum value the type name can be omitted — write .Variant and Zed infers the enum type from the match expression.
match direction {
case .North { go_north() }
case .South { go_south() }
case .East { go_east() }
case .West { go_west() }
}Compile-time conditional (like #if). The condition must be a compile-time constant expression. Branches that do not match are not emitted into the output.
DEBUG :: true
when DEBUG {
printf("debug mode\n")
} else {
printf("release mode\n")
}typeid(T) produces a compile-time u64 hash that uniquely identifies the type T. No runtime cost is incurred — the value folds away entirely at compile time.
id_i32 :: typeid(i32) -- u64 constant
#assert typeid(i32) != typeid(f32)typeid is most useful in when conditions to branch on type identity:
when typeid(T) == typeid(i32) {
-- branch emitted only when T is i32
} else when typeid(T) == typeid(f32) {
-- branch emitted only when T is f32
}#assert is valid both at the top level (outside any proc) and inside proc bodies. It emits a C++ static_assert with a file:line diagnostic message.
-- top-level
#assert GRID_SIZE > 0
#assert sizeof(i32) == 4
-- inside a proc
main :: proc() -> i32 {
#assert sizeof(i64) == 8
return 0
}Zed has two string types with distinct semantics:
| Type | Backing type | Mutable | Heap-allocated |
|---|---|---|---|
cstr |
const char* |
No | No (pointer) |
string |
std::string |
Yes | Yes |
String literals "..." always produce cstr. Assign to a string-typed variable to get a string (the compiler inserts the implicit conversion).
a : cstr = "hello" -- cstr (explicit)
b : string = "hello" -- string (cstr literal coerced to string)
c := "hello" -- cstr (inferred)
d :: "hello" -- cstr (const)
e := from_cstr("hello") -- string (explicit conversion)Conversion between the two always requires an explicit call:
s : string = "world"
cs: cstr = to_cstr(s) -- string → cstr (points into s's buffer)
s2: string = from_cstr(cs) -- cstr → string (copies)Assigning a runtime cstr variable directly to a string (or vice-versa) without a conversion call is a type error.
String concatenation uses +. At least one operand must be a string; the other may be string or cstr. The result is always string.
greeting: string = "Hello, "
name: string = "Zed"
full := greeting + name -- "Hello, Zed"
full2 := greeting + "World" -- string + cstr → stringString indexing returns the byte at position i as u8. Writing to string[i] is also valid — it mutates the byte in-place.
s: string = "abc"
b := s[0] -- u8 = 97 ('a')
s[0] = 90 -- mutate: s is now "Zbc"String for-each iterates over bytes.
for byte in s { } -- byte: u8
for i, byte in s { } -- i: i64, byte: u8These are soft keywords — usable as variable names outside a call position.
| Call | Description | Return type |
|---|---|---|
len(x) |
Element count of array, slice, [dynamic]T, or string |
u64 |
cap(x) |
Capacity of a [dynamic]T |
u64 |
append(&arr, val) |
Append element to dynamic array | void |
reserve(&arr, n) |
Pre-allocate capacity; len stays 0; slots zeroed | void |
clear(&arr) |
Set len to 0; retain allocation | void |
copy(dst, src) |
Copy min(cap(dst), len(src)) elements; returns count |
i64 |
free(ptr) |
Release heap memory obtained via C malloc / mem_alloc |
void |
panic(msg) |
Print message to stderr and abort — never returns | void |
to_cstr(s) |
string → cstr (points into string buffer) |
cstr |
from_cstr(cs) |
cstr → string (copies) |
string |
enum_name(val) |
Enum variant → its source name as cstr |
cstr |
min(a, b) |
Smaller of two numeric values | same as a |
max(a, b) |
Larger of two numeric values | same as a |
abs(x) |
Absolute value of a numeric | same as x |
swap(&a, &b) |
Swap two values in-place | void |
clamp(v, lo, hi) |
Clamp v to [lo, hi] |
same as v |
sizeof(T|x) |
Size in bytes of a type or variable | u64 |
alignof(T) |
Alignment in bytes | u64 |
typeid(T) |
Compile-time u64 hash unique to T |
u64 |
-- panic: runtime abort with message
if ptr == nil { panic("unexpected nil pointer") }
-- free: release raw allocation
buf := cast(*u8)(mem_alloc(1024))
mem_zero(buf, 1024)
free(buf)
-- copy: slice/dynamic array copy
src: [dynamic]i32
dst: [dynamic]i32
reserve(&dst, 5)
n := copy(dst, src) -- copies min(cap(dst), len(src)) elements
-- enum_name: get variant name at runtime
Dir :: enum { North, South, East, West }
d := Dir.East
printf("%s\n", enum_name(d)) -- prints "East"Pulls a C header into scope. All types, functions, and macros from that header become usable directly.
cimport "raylib.h"
cimport "math.h"
cimport "stdio.h"- C types (e.g.
Texture,Camera2D) are used by name as foreign types. - C macros (e.g.
WHITE,FLAG_VSYNC_HINT) pass through to the generated C++. - C functions are called directly with no wrapper needed.
tex := LoadTexture("sprite.png")
cosf(angle)
printf("%d\n", cast(i32)(score))DrawLine :: proc(x1: i32, y1: i32, x2: i32, y2: i32, color: Color) ---The --- body means "extern — defined in C".
Use import to bring another .z file's top-level declarations into scope.
import "math_utils" -- imports math_utils.z
import "engine/render" -- path relative to project src/- All top-level
proc,struct,union,enum,const, and globalvardeclarations from the imported file become visible. - When using
zed build --project, source files are discovered automatically viazed.toml; explicitimportis only needed for symbol visibility. - When using
zed build --fileor the low-level CLI, list all.zfiles on the command line in dependency order.
n := sizeof(Vec3) -- size in bytes → u64
n2 := sizeof(my_var) -- size of variable's type
a := alignof(i64) -- alignment in bytes → u64Emits C++ sizeof(...) / alignof(...) directly. Useful for malloc, memcpy, and layout calculations.
cimport "stdio.h"
Point :: struct { x: f32, y: f32 }
distance :: proc(a: Point, b: Point) -> f32 {
dx := a.x - b.x
dy := a.y - b.y
return sqrtf(dx*dx + dy*dy)
}
div_rem :: proc(a: i32, b: i32) -> (i32, i32) {
return a / b, a % b
}
main :: proc() -> i32 {
p1 := Point{ x = 0.0, y = 0.0 }
p2 := Point{ x = 3.0, y = 4.0 }
printf("dist = %f\n", cast(f64)(distance(p1, p2)))
q, r := div_rem(17, 5)
printf("%d / 5 = %d rem %d\n", 17, q, r)
return 0
} ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Zed Project Initialized ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
◆ <n>/
│
├── src/
│ │
│ └── main.z ⟵ entry point
│
├── .gitignore
│
├── README.md
│
└── zed.toml ⟵ build config
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
build:
zed build debug --project <n>/
zed build release --project <n>/
run:
zed run debug --project <n>/
zed run release --project <n>/
clean:
zed clean --project <n>/
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
▶▶ cd <n> && zed run debug --project .
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯