Pattern matching · Type safety · Native binaries
Zap takes the developer experience of functional programming — pattern matching, pipe operators, algebraic types — and strips away the overhead. No VM, no garbage collector, no interpreter. Your code compiles to Zig, and Zig compiles to a native binary.
Early stage. The core pipeline works and examples compile and run, but not everything described here is fully implemented yet.
Install Zig 0.15.2 or later.
Verify your installation:
zig versionClone the repository and build:
git clone https://github.com/trycog/zap.git
cd zap
zig buildThis produces the compiler binary at zig-out/bin/zap.
Create a file called hello.zap:
defmodule Greeter do
def hello(name :: String) :: String do
"Hello, " <> name <> "!"
end
end
def main() :: String do
Greeter.hello("World")
|> IO.puts()
endEvery Zap program needs a main function — that's your entry point. Functions declare their parameter types and return types at the boundary. The body is type-inferred.
./zig-out/bin/zap hello.zapZap compiles your code to Zig, then invokes the Zig compiler to produce a native binary. The output lands in zap-out/bin/:
./zap-out/bin/hello
# => Hello, World!That's it. Source code in, native binary out.
If you're curious what Zap produces under the hood:
./zig-out/bin/zap --emit-zig hello.zapThis prints the generated Zig source to stdout instead of compiling it.
Modules group related functions. Functions declare types at the boundary and infer everything inside.
defmodule Math do
def square(x :: i64) :: i64 do
x * x
end
def double(x :: i64) :: i64 do
x * 2
end
endChain function calls, passing the result of each step as the first argument to the next:
def double(x :: i64) :: i64 do
x * 2
end
def add_one(x :: i64) :: i64 do
x + 1
end
def main() do
5
|> double()
|> add_one()
endMultiple function clauses with the same name form an overload group. The compiler resolves which clause to call based on argument values and types.
def factorial(0 :: i64) :: i64 do
1
end
def factorial(n :: i64) :: i64 do
n * factorial(n - 1)
endThis works with atoms, integers, tuples, and wildcards:
defmodule Geometry do
type Shape = {:circle, f64} | {:rectangle, f64, f64}
def area({:circle, radius} :: Shape) :: f64 do
3.14159 * radius * radius
end
def area({:rectangle, w, h} :: Shape) :: f64 do
w * h
end
endTuple patterns destructure and bind in a single step. The tag atom selects the clause, the remaining elements bind to local variables.
Function clauses can carry guard conditions that participate in dispatch:
def classify(n :: i64) :: String if n > 0 do
"positive"
end
def classify(n :: i64) :: String if n < 0 do
"negative"
end
def classify(_ :: i64) :: String do
"zero"
endThe if clause runs after the type check passes. If the predicate fails, dispatch continues to the next clause.
Pattern matching inside function bodies:
def check(result) :: String do
case result do
{:ok, v} ->
v
{:error, e} ->
e
_ ->
"unknown"
end
endIf/else is an expression — it produces a value:
def abs(x :: i64) :: i64 do
if x < 0 do
-x
else
x
end
endTypes are declared at function boundaries. No implicit numeric coercion — all conversions are explicit.
| Category | Types |
|---|---|
| Signed integers | i8 i16 i32 i64 |
| Unsigned integers | u8 u16 u32 u64 |
| Floats | f16 f32 f64 |
| Platform-sized | usize isize |
| Primitives | Bool String Atom Nil |
| Bottom | Never |
| Compound | tuples, structs, enums |
Structs are top-level data definitions with named, typed fields:
defstruct User do
name :: String
email :: String
age :: i64
end
user = %{name: "Alice", email: "alice@example.com", age: 30} :: UserStructs support inheritance via extends, which copies fields from a parent:
defstruct Shape do
color :: String = "black"
end
defstruct Circle extends Shape do
radius :: f64
end
# Circle has: color, radiusClosed sets of named tags:
defenum Direction do
North
South
East
West
endLists are homogeneous — all elements must be the same type:
numbers = [1, 2, 3] # valid: [i64]
names = ["alice", "bob"] # valid: [String]Mixed-type collections use tuples instead:
mixed = {1, "two", :three} # valid: {i64, String, Atom}Source text goes in, Zig source comes out, Zig takes it the rest of the way to a native binary.
.zap source
│
▼
Lexer ─────────── tokenize with indent/dedent tracking
│
▼
Parser ────────── surface AST
│
▼
Collector ─────── register modules and functions
│
▼
Macro Expansion ─ AST→AST transforms to fixed point
│
▼
Type Checker ──── overload resolution + inference
│
▼
HIR Lowering ──── typed intermediate representation
│
▼
IR Lowering ───── lower-level IR closer to Zig semantics
│
▼
Code Gen ──────── emit Zig source
│
▼
Zig Compiler ──── native binary
The entire compiler is written in Zig.
zap [run] [flags] <file.zap> [zig-flags...]
| Command / Flag | Description |
|---|---|
run |
Compile and execute the program in one step |
--emit-zig |
Print generated Zig source to stdout instead of compiling |
--lib |
Compile as a library instead of an executable |
--strict-types |
Treat type warnings as errors |
Any additional flags after the .zap file are forwarded to the Zig build system.
# Run the full test suite
zig build test
# Compile and run an example
zig build run -- examples/factorial.zap
# See generated Zig for an example
zig build run -- --emit-zig examples/hello.zap