A minimal, strongly-typed programming language that compiles to JavaScript.
If JavaScript allows something ambiguous, implicit, or surprising, ZZ must either forbid it or make it explicit in syntax.
ZZ uses symbols (#, ~, ?, @, ;) instead of keywords (const, let, if, while). This isn't about being cryptic—it's about information density.
- Visual structure: Code blocks become immediately apparent. A
?always starts a conditional,@always starts a loop,;always ends a block. - Reduced noise: Keywords compete with your variable names for attention. Symbols step out of the way.
- Forced consistency: There's only one way to write an if statement. No
if/else if/elsevs ternary debates.
Zsignifies functions. Lower case single letters are used for data types. Capital single letters are used for complex data types. All other keywords should be symbols.
In JavaScript, const is an afterthought. In ZZ, mutability is the afterthought.
- Explicit intent:
~screams "this will change."#is quiet, because unchanging values should be the norm. - Fewer bugs: Most variables don't need to change. When you're forced to opt-in to mutability, you think harder about state.
- Better reasoning: Reading
i#count = 10tells you this value is stable throughout its scope. No hunting for reassignments.
JavaScript is everywhere—browsers, servers, edge functions, embedded systems. By compiling to JS:
- Zero runtime: ZZ doesn't ship a runtime or standard library. The output is just JavaScript.
- Full interop: Call any npm package. Use any JavaScript API. ZZ code and JS code coexist.
- Readable output: The generated JavaScript is clean and debuggable, not minified soup.
- Proven foundation: JavaScript's semantics are well-understood (and its sharp edges are exactly what ZZ smooths over).
JavaScript has two "nothing" values: null and undefined. ZZ has one: _ (null).
s~value = _ // null, not undefined
?(value == _) // explicit null check
print("empty")
;
A value either exists or it's _. No typeof x === 'undefined', no x ?? y vs x || y confusion.
JavaScript's truthy/falsy coercion is a source of bugs. In ZZ, conditions must be boolean:
i#count = 0
?(count) // ❌ Compile error: condition must be boolean
?(count > 0) // ✅ Explicit comparison
s#name = ""
?(name) // ❌ Compile error
?(name != _) // ✅ Explicit null check
The !, &&, and || operators also require boolean operands—no !!value tricks.
In JavaScript, forgetting let/const creates a global variable. In ZZ, it's a compile error:
x = 5 // ❌ Compile error: Undeclared variable 'x'
i#x = 5 // ✅ Explicit declaration
Every variable must be declared with its type. Typos become compile errors, not silent bugs.
- Strong static typing with type inference
- Generics - Type-safe generic structs and functions with type inference and trait constraints
- Traits - Compile-time contracts for polymorphism without inheritance
- Immutable by default with explicit mutability
- Concise syntax using symbols instead of keywords
- Compiles to clean, readable JavaScript
npm install
npm run buildchmod +x install.sh
./install.sh# Compile to ./compiled/<filename>.js (default)
node dist/index.js yourfile.zz
# Compile and run immediately
node dist/index.js yourfile.zz --run
# Output to stdout instead of file
node dist/index.js yourfile.zz --stdout
# Compile to specific output file
node dist/index.js yourfile.zz --output path/to/output.js
# Show AST for debugging
node dist/index.js yourfile.zz --ast
# Start interactive REPL
node dist/index.js --replZZ includes an interactive REPL (Read-Eval-Print Loop) for experimenting with the language:
node dist/index.js --replZZ REPL v0.1.0
Type ZZ expressions. Commands: .help, .clear, .source, .exit
zz> i#x = 42
zz> print(x)
42
zz> S Point
... f#x
... f#y
... ;
zz> Point#p = Point(3.0, 4.0)
zz> print(p.x)
3
Features:
- Multi-line input with automatic block detection
- Persistent state across inputs (variables, structs, functions)
- Error recovery — bad input is rolled back, previous state preserved
- Full ZZ syntax support including structs, enums, traits, pattern matching
Commands:
.help— show available commands.clear— reset all state.source— show accumulated source code.exit— exit the REPL (also Ctrl+D)
| Prefix | Type | Example |
|---|---|---|
s |
string | s#name = "hello" |
i |
int | i#count = 42 |
f |
float | f#pi = 3.14 |
b |
bool | b#flag = true |
i[] |
int array | i[]#nums = [1, 2, 3] |
ti5 |
tuple of 5 ints | ti5#point = (1, 2, 3, 4, 5) |
tiN |
tuple (inferred) | tiN#vals = (1, 2, 3) |
Color |
enum type | Color#c = Color.Red |
J |
JSON-like object | J#config = { port: 8080 } |
Point[] |
struct array | Point[]#pts = [Point(1,2)] |
Color[] |
enum array | Color[]#cols = [Color.Red] |
J[] |
J object array | J[]#cfgs = [{ host: "a" }] |
ti3[] |
tuple array | ti3[]#coords = [(1,2,3)] |
| Symbol | Meaning | JavaScript |
|---|---|---|
# |
immutable | const |
~ |
mutable | let |
s#name = "Alice" // immutable string
i~counter = 0 // mutable int
counter = counter + 1 // OK - mutable
name = "Bob" // ERROR - immutable
print("Hello, World!")
print(42)
print(myVariable)
Arithmetic:
i#sum = 5 + 3 // 8
i#diff = 10 - 4 // 6
i#prod = 6 * 7 // 42
f#quot = 10 / 3 // 3.333... (division returns float)
i#mod = 10 % 3 // 1
i#pow = 2 ** 10 // 1024
Comparison:
b#eq = 5 == 5 // true
b#neq = 5 != 3 // true
b#gt = 5 > 3 // true
b#lt = 3 < 5 // true
b#gte = 5 >= 5 // true
b#lte = 3 <= 5 // true
Logical:
b#and = true && false // false
b#or = true || false // true
b#not = !true // false
Increment/Decrement:
i~count = 10
count++ // 11
count-- // 10
Compound Assignment:
i~x = 100
x += 10 // 110
x -= 30 // 80
x *= 2 // 160
x /= 4 // 40
i~y = 17
y %= 5 // 2
i~z = 2
z **= 8 // 256
s~text = "Hello"
text += " World" // "Hello World"
Use s"..." for interpolated strings:
s#name = "Alice"
i#age = 30
print(s"Hello, {name}! You are {age} years old.")
// Output: Hello, Alice! You are 30 years old.
| Function | Converts to |
|---|---|
s(expr) |
string |
i(expr) |
int (truncates) |
f(expr) |
float |
b(expr) |
bool |
tiN(expr) |
tuple of ints |
i[](expr) |
int array (from tuple) |
s#numStr = "42"
i#num = i(numStr) // 42
s#back = s(num) // "42"
i#truncated = i(3.7) // 3
// Tuple casts
i[]#arr = [1, 2, 3]
tiN#tup = tiN(arr) // array to tuple
i[]#back = i[](tup) // tuple to array
Use _ for null values:
s~value = _
?(value == _)
print("Value is null")
;
?(condition)
// if body
;
?(condition)
// if body
:?(other_condition)
// else if body
:
// else body
;
Example:
i#score = 85
?(score >= 90)
print("A")
:?(score >= 80)
print("B")
:?(score >= 70)
print("C")
:
print("F")
;
@(condition)
// body
;
Example:
i~count = 0
@(count < 5)
print(count)
count = count + 1
;
@(variable#start..end)
// body uses variable
;
Example:
// Count up: 1, 2, 3, 4, 5
@(i#1..5)
print(i)
;
// Count down: 5, 4, 3, 2, 1
@(i#5..1)
print(i)
;
// Using variables
i#from = 1
i#to = 10
@(n#from..to)
print(n)
;
@(variable#array)
// body uses variable
;
Example:
s[]#people = ["Sam", "Anna", "Ben"]
@(person#people)
print(person)
;
// Output: Sam, Anna, Ben
// Works with any array type
i[]#numbers = [10, 20, 30]
@(n#numbers)
print(s"Number: {n}")
;
>! // break - exit loop
>> // continue - skip to next iteration
Example:
@(i#1..10)
?(i == 5)
>> // skip 5
;
?(i == 8)
>! // stop at 8
;
print(i)
;
// Output: 1, 2, 3, 4, 6, 7
// Void function (no return)
Z functionName(type#param1 type#param2)
// body
;
// Function with return type
returnType Z functionName(type#param)
// body
returnExpression
;
Examples:
// Void function
Z greet(s#name)
print(s"Hello, {name}!")
;
// Function returning int
i Z add(i#a i#b)
a + b
;
// Function returning string
s Z format(s#name i#age)
s"{name} is {age} years old"
;
// Recursive function
i Z factorial(i#n)
i~result = 1
?(n > 1)
result = n * factorial(n - 1)
;
result
;
// Positional arguments
greet("Alice")
i#sum = add(3, 5)
// Named arguments
i#result = add(b=5, a=3) // same as add(3, 5)
// Dynamic arrays (size not specified)
i[]#numbers = [1, 2, 3, 4, 5] // immutable int array
s[]~names = ["Alice", "Bob"] // mutable string array
i[]#empty = [] // empty array
// Fixed-size arrays (size specified)
i[5]#fixed = [1, 2, 3, 4, 5] // fixed-size array of 5 ints
f[3]~coords = [0.0, 0.0, 0.0] // fixed-size mutable array
Fixed-size vs Dynamic arrays:
i[]- dynamic array, can grow/shrink withpush/popi[5]- fixed-size array of exactly 5 elements,push/popnot allowed
i[]#range = 1..5 // [1, 2, 3, 4, 5]
i[]#countdown = 5..1 // [5, 4, 3, 2, 1] (when used in for loop)
i[]#arr = [10, 20, 30]
print(arr[0]) // 10
print(arr[2]) // 30
i[]~arr = [1, 2, 3]
arr[0] = 100
print(arr[0]) // 100
i[]~arr = [1, 2, 3]
i#length = arr.len() // 3 - works on all arrays
arr.push(4) // arr is now [1, 2, 3, 4]
i#last = arr.pop() // returns 4, arr is now [1, 2, 3]
Note: push and pop only work on dynamic arrays (i[]). Fixed-size arrays (i[5]) will produce a compile error if you try to use these methods.
Arrays can contain structs, enums, J objects, and tuples:
// Struct arrays
S Point i#x i#y ;
Point[]~points = [Point(1, 2), Point(3, 4)]
points.push(Point(5, 6))
print(points[0].x)
// Enum arrays
E Color Red Green Blue ;
Color[]~colors = [Color.Red, Color.Green]
// J object arrays
J[]~configs = [{ host: "a" }, { host: "b" }]
configs.push({ host: "c" })
// Tuple arrays
ti3[]~coords = [(1, 2, 3), (4, 5, 6)]
coords.push((7, 8, 9))
Functions can take and return arrays of complex types:
// Complex array parameter
Z printPoints(Point[]#pts)
@(p#pts) print(s"({p.x}, {p.y})") ;
;
// Complex array return type
Point[] Z makePoints()
[Point(10, 20), Point(30, 40)]
;
Arrays can be nested to any depth:
// 2D array (matrix)
i[][]~matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(matrix[0][1]) // 2
matrix.push([10, 11, 12])
// 3D array
i[][][]#cube = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
print(cube[0][0][0]) // 1
// Works with complex types too
s[][]#words = [["hello", "world"], ["foo", "bar"]]
- Element types nest naturally:
i[][]is "array of array of int" - All array operations (
push,pop,len, indexing) work at each level - Type checking validates element types at every nesting level
i Z sum(i[]#numbers)
i~total = 0
@(n#numbers)
total += n
;
total
;
i[]#data = [1, 2, 3, 4, 5]
print(sum(data)) // 15
i[] Z makeRange(i#start i#end)
start..end
;
i[]#nums = makeRange(1, 5)
Tuples are fixed-length, immutable sequences of values of the same type. Unlike arrays, tuples cannot be resized or modified after creation.
// Fixed-length tuple (explicit size)
ti5#nums = (1, 2, 3, 4, 5) // tuple of 5 ints
ts3#names = ("a", "b", "c") // tuple of 3 strings
// Inferred-length tuple (use N)
tiN#values = (10, 20, 30) // inferred as ti3
tfN#coords = (1.0, 2.5, 3.7) // inferred as tf3
Syntax: t{type}{length} where:
t= tuple prefix- type =
i(int),f(float),s(string),b(bool) - length = explicit number or
Nfor inferred
Use parentheses with commas to create tuple literals:
ti3#point = (10, 20, 30)
tsN#words = ("hello", "world")
Note: A single value in parentheses is just a grouped expression, not a tuple. Use casting for single-element tuples:
i#value = (42) // just 42, not a tuple
tiN#single = tiN(42) // single-element tuple
ti5#data = (1, 2, 3, 4, 5)
print(data[0]) // 1
print(data[4]) // 5
print(data.len()) // 5
Convert between tuples and arrays:
// Array to tuple
i[]#arr = [1, 2, 3]
tiN#tup = tiN(arr) // freeze array into tuple
// Single value to tuple
tiN#single = tiN(42) // (42)
// Tuple to array
ti3#frozen = (1, 2, 3)
i[]#mutable = i[](frozen) // create mutable array copy
Tuples are always immutable. Using ~ (mutable) with tuples is a compile error:
ti3#ok = (1, 2, 3) // ✅ immutable
ti3~bad = (1, 2, 3) // ❌ Compile error: tuples must be immutable
ok[0] = 10 // ❌ Compile error: cannot modify tuple
When using explicit length, the tuple literal must match:
ti5#ok = (1, 2, 3, 4, 5) // ✅ 5 elements
ti5#wrong = (1, 2, 3) // ❌ Compile error: length mismatch
All tuple elements must be the same type:
ti3#nums = (1, 2, 3) // ✅ all ints
ti3#bad = (1, "two", 3) // ❌ Compile error: mixed types
Only len() is available on tuples (no push/pop since they're immutable):
ti5#data = (1, 2, 3, 4, 5)
print(data.len()) // 5
data.push(6) // ❌ Compile error
data.pop() // ❌ Compile error
Enums are named sets of variants for type-safe state representation.
E Color
Red
Green
Blue
;
E Status
Active
Inactive
Pending
;
Ekeyword starts declaration- Variants listed one per line
;terminates the declaration
Color#primary = Color.Red // immutable
Color~current = Color.Green // mutable
current = Color.Blue // reassign mutable
Color.Red // access variant
?(status == Color.Blue) // comparison
print("It's blue!")
;
// Enum parameter
Z printColor(Color#c)
?(c == Color.Red)
print("red")
:?(c == Color.Green)
print("green")
:
print("blue")
;
;
// Enum return type
Color Z getDefault()
Color.Green
;
printColor(Color.Red)
Color#def = getDefault()
E Color
Red
;
E Status
Active
;
Color#c = Status.Active // ❌ Type mismatch
Color#d = Color.Purple // ❌ Unknown variant
?(c == Status.Active) // ❌ Cannot compare different enums
Structs are user-defined types with fields and methods. They provide a foundation for object-oriented programming with encapsulated behavior.
S Point
i#x
i#y
s Z toString()
s"({x}, {y})"
;
i Z manhattanDistance()
x + y
;
;
Skeyword starts declaration- Fields use
type#namesyntax - Methods use standard function syntax inside the struct body
- Fields are accessible directly in methods (implicit self)
;terminates both methods and the struct declaration
// Positional arguments (in field order)
Point#p1 = Point(10, 20)
// Named arguments
Point#p2 = Point(x=5, y=15)
print(p1.x) // 10
print(p1.y) // 20
print(p1.toString()) // "(10, 20)"
print(p1.manhattanDistance()) // 30
Struct instances follow the same mutability rules as other types:
// Immutable instance - cannot modify fields
Point#immutable = Point(1, 2)
immutable.x = 5 // ❌ Compile error
// Mutable instance - can modify fields
Point~mutable = Point(1, 2)
mutable.x = 5 // ✅ OK
print(mutable.x) // 5
S Person
s#name
i#age
s Z greet()
s"Hello, I'm {name}!"
;
b Z isAdult()
age >= 18
;
;
Person#alice = Person("Alice", 30)
print(alice.greet()) // "Hello, I'm Alice!"
print(alice.isAdult()) // true
S Rectangle
i#width
i#height
i Z area()
width * height
;
b Z fitsInside(i#maxW i#maxH)
width <= maxW && height <= maxH
;
;
Rectangle#rect = Rectangle(10, 5)
print(rect.area()) // 50
print(rect.fitsInside(20, 20)) // true
print(rect.fitsInside(8, 8)) // false
// Struct parameter
Z printPoint(Point#p)
print(p.toString())
;
// Struct return type
Point Z origin()
Point(0, 0)
;
printPoint(Point(3, 4))
Point#o = origin()
S Point
i#x
i#y
;
S Size
i#w
i#h
;
Point#p = Size(10, 20) // ❌ Type mismatch
Point#q = Point("a", "b") // ❌ Field type mismatch
print(p.z) // ❌ Unknown field
Traits are compile-time contracts (interfaces) that structs can implement. They enable polymorphism without inheritance. Traits have no runtime representation — all checking is compile-time, no JavaScript is emitted for trait declarations.
Use the ZZ keyword to declare a trait:
ZZ Printable
s Z toString()
;
ZZ Comparable
i Z compareTo(Self#other)
;
ZZkeyword starts the declaration- Methods are signatures only — no body
Selfcan be used in parameter types to mean "the implementing type";terminates the declaration
Use : after the struct name to implement one or more traits:
S Point : Printable, Comparable
f#x
f#y
s Z toString()
s"({x}, {y})"
;
i Z compareTo(Point#other)
i(x + y) - i(other.x + other.y)
;
;
The compiler validates at compile time that:
- All required methods are present
- Parameter counts and types match
- Return types match
Selfis correctly substituted with the struct name
Traits can be used as parameter types, enabling polymorphism:
Z display(Printable#item)
print(item.toString())
;
Point#p = Point(3.0, 4.0)
display(p) // Works — Point implements Printable
Trait-typed variables accept any struct that implements the trait:
Printable#printable = Point(7.0, 8.0)
print(printable.toString()) // "(7, 8)"
ZZ Describable
Z describe()
;
S Cat : Describable
s#name
Z describe() print(name) ;
;
S Dog // Does NOT implement Describable
s#name
;
Z show(Describable#item) item.describe() ;
show(Cat("Whiskers")) // ✅ Cat implements Describable
show(Dog("Rex")) // ❌ Compile error: Dog doesn't implement Describable
Traits can be exported and imported across modules:
// shapes.zz
->ZZ Drawable
Z draw()
;
// main.zz
<- { Drawable } = "./shapes"
J objects are JSON-like, string-keyed objects whose values can be s, i, f, b, _ (null), nested J, or arrays of these types. They bridge the gap between rigid structs and raw JavaScript objects — typed and safe, but flexible in shape.
// Immutable J object
J#config = {
host: "localhost",
port: 8080,
ssl: false,
tags: ["web", "api"]
}
// Mutable J object
J~settings = { theme: "dark", fontSize: 14 }
J#→ immutable (compiles toObject.freeze({...}))J~→ mutable (plain JS object)
J#server = {
host: "api.example.com",
port: 443,
limits: {
maxConn: 100,
timeout: 30
}
}
print(server.host) // "api.example.com"
print(server.limits.maxConn) // 100
J objects support dot access for reading fields:
J#config = { host: "localhost", port: 8080 }
print(config.host) // "localhost"
print(config.port) // 8080
Dot access returns a dynamic (untyped) value at compile time.
J~settings = { theme: "dark", fontSize: 14 }
// Field assignment (mutable only)
settings.theme = "light"
print(settings.theme) // "light"
// set() method (mutable only)
settings.set("fontSize", 16)
print(settings.fontSize) // 16
Attempting to assign fields or call set() on an immutable J# is a compile error:
J#config = { x: 1 }
config.x = 2 // ❌ Compile error: immutable J object
config.set("x", 2) // ❌ Compile error: immutable J object
| Method | Returns | Description |
|---|---|---|
obj.has("key") |
b |
Check if key exists |
obj.get("key") |
dynamic | Get value by key |
obj.set("k", v) |
— | Set value (mutable J~ only) |
obj.len() |
i |
Number of keys |
J#config = { host: "localhost", port: 8080 }
print(config.has("host")) // true
print(config.has("missing")) // false
print(config.get("port")) // 8080
// J as return type
J Z makeConfig(s#host i#port)
{ host: host, port: port, ssl: true }
;
J#myConfig = makeConfig("example.com", 443)
// J as parameter
Z printHost(J#cfg)
print(cfg.host)
;
printHost(myConfig) // "example.com"
J#nullable = { name: "test", value: _ }
print(nullable.name) // "test"
print(nullable.value) // null
J objects support structural pattern matching with ??:
J#resp = { status: 200, body: "OK" }
??(resp)
| { status: 200, body: b } => print(s"Success: {b}")
| { status: s } => print(s"Status: {s}")
| _ => print("Unknown")
;
- Keys in the pattern check for existence (
"key" in obj) - Literal values match exactly (
status: 200) - Identifiers bind the field value (
body: bbindsb) - Nested J patterns are supported
ZZ supports exhaustive pattern matching with the ?? operator. Match arms use | for each pattern and => to separate the pattern from the body.
??(value)
| pattern1 => body1
| pattern2 => body2
| _ => defaultBody
;
E Color Red Green Blue ;
Z describeColor(Color#c)
??(c)
| Color.Red => print("Warm color: Red")
| Color.Green => print("Nature color: Green")
| Color.Blue => print("Cool color: Blue")
;
;
Enum matches are checked for exhaustiveness at compile time. If you miss a variant and don't include a wildcard _, you get a compile error.
Z describeNumber(i#n)
??(n)
| 0 => print("Zero")
| 1 => print("One")
| _ => print("Some other number")
;
;
Match on J object keys, with literal values and variable bindings:
J#resp = { status: 200, body: "OK" }
??(resp)
| { status: 200, body: b } => print(s"Success: {b}")
| { status: 404, body: b } => print(s"Not Found: {b}")
| { status: s } => print(s"Other: {s}")
| _ => print("Unknown")
;
status: 200— matches ifstatuskey exists and equals200body: b— matches ifbodykey exists, binds value tob- Nested J patterns work:
| { data: { id: id } } => ...
Match on struct fields, mixing literal values and variable bindings:
S Point i#x i#y ;
Z describePoint(Point#p)
??(p)
| Point(0, 0) => print("Origin")
| Point(0, y) => print(s"Y-axis at y={y}")
| Point(x, 0) => print(s"X-axis at x={x}")
| Point(x, y) => print(s"Point at ({x}, {y})")
;
;
0in a field position matches literallyx,yin a field position bind the field value to a variable
Add conditions to match arms with &:
Z classifyPoint(Point#p)
??(p)
| Point(x, y) & x > 0 && y > 0 => print("Quadrant I")
| Point(x, y) & x < 0 && y > 0 => print("Quadrant II")
| Point(x, y) & x < 0 && y < 0 => print("Quadrant III")
| Point(x, y) & x > 0 && y < 0 => print("Quadrant IV")
| _ => print("On an axis")
;
;
Pattern matching can return values. Declare a return type before Z and the last expression in each arm is the return value:
i Z scoreGrade(i#score)
??(score)
| 100 => 10
| _ => 0
;
;
i#bonus = scoreGrade(100) // 10
Capture the matched value into a variable, optionally with a guard:
Z processValue(i#n)
??(n)
| v & v > 100 => print(s"Large value: {v}")
| v & v > 50 => print(s"Medium value: {v}")
| v => print(s"Small value: {v}")
;
;
Pattern matching compiles to an IIFE with an if/else-if chain:
??(n)
| 0 => print("Zero")
| _ => print("Other")
;
Becomes:
(function () {
const __match = n;
if (__match === 0) {
console.log("Zero");
} else if (true) {
console.log("Other");
}
})();ZZ files are ZZ — no raw JavaScript works outside of injection blocks. When you need direct access to JavaScript APIs, use $js { ... } to inject raw JS into the compiled output.
$js {
// Any JavaScript code here
// Nested { braces } are handled correctly
}
The } closes the block — no trailing ; is needed.
s#name = "world"
$js {
console.log("Hello, " + name + "!");
}
Compiles to:
const name = "world";
console.log("Hello, " + name + "!");ZZ-defined variables are accessible inside $js blocks since they compile to regular JavaScript variables.
Nested {} in your JavaScript code work correctly — the compiler tracks brace depth:
$js {
const items = [1, 2, 3];
const mapped = items.map(x => {
return { value: x * 2 };
});
console.log(mapped);
}
- Accessing browser/Node.js APIs not exposed by ZZ
- Complex JS patterns (async/await, generators, etc.)
- Calling third-party JS libraries directly
- Performance-critical code that needs specific JS idioms
Code inside ${} is evaluated at compile time and substituted with literal values. This enables compile-time computation, environment-based configuration, and build metadata.
// Arithmetic evaluated at compile time
i#computed = ${2 + 3 * 4} // Compiles to: const computed = 14;
// String operations
s#greeting = ${"Hello" + ", World!"}
// Comparisons
b#isDebug = ${$env("DEBUG", "false") == "true"}
s#env = ${$env("NODE_ENV", "development")}
s#apiKey = ${$env("API_KEY")} // empty string if not set
b#hasKey = ${$defined("API_KEY")} // true if env var exists
s#buildDate = ${$date()} // ISO date: "2024-01-15"
s#buildTime = ${$time()} // ISO timestamp
i#lineNum = ${$line()} // Current line number
s#fileName = ${$file()} // Current file name
Define functions that execute at compile time:
$Z i factorial(i#n)
??(n)
| 0 => 1
| 1 => 1
| _ => n * factorial(n - 1)
;
;
i#fact5 = ${factorial(5)} // Compiles to: const fact5 = 120;
i#fact10 = ${factorial(10)} // Compiles to: const fact10 = 3628800;
Rules:
$Zfunctions can only call other$Zfunctions- Inside
$Zbody, calls to$Zfunctions are implicitly compile-time (no${}needed) - Runtime code must use
${}to call$Zfunctions $Zfunctions are NOT emitted to JS output
| Function | Description |
|---|---|
$read(path) |
Read file contents at compile time |
$env(name) |
Get environment variable |
$env(name, default) |
Get env var with default |
$defined(name) |
Check if env var exists (returns bool) |
$line() |
Current source line number |
$file() |
Current source file name |
$date() |
Compile date (ISO format) |
$time() |
Compile time (ISO format) |
- Literals, arithmetic, string operations
- Comparisons and logical operators
- Match expressions (pattern matching)
- Array, tuple, and J literals
- Calls to
$Zfunctions and built-in$functions
- Runtime variable references
- Runtime function calls (regular
Zfunctions) - Side effects:
print(),error() - Mutability: no
~variables in compile-time context
Strings have two built-in methods:
s#text = "Hello, World!"
// len() - get string length
i#length = text.len() // 13
// at(i) - get character at index
s#first = text.at(0) // "H"
s#last = text.at(length - 1) // "!"
ZZ supports UFCS: any function f(x, ...) can be called as x.f(...). This enables method chaining with library functions:
// Import string functions
<- { upper, trim } = std/string
// These are equivalent:
upper(str)
str.upper()
// Chaining works naturally:
s#name = " alice "
s#clean = name.trim().upper() // "ALICE"
UFCS works for any function where the first parameter matches the object's type. This means you can "extend" types with your own functions:
// Define a custom function
s Z shout(s#str)
str.upper() + "!"
;
s#msg = "hello"
print(msg.shout()) // "HELLO!"
ZZ includes a minimal standard library. Import functions and use them with UFCS for method-like syntax.
String manipulation functions:
<- { upper, lower, trim, split, has, find, starts, ends, slice, replace, repeat, padStart, padEnd, join } = std/string
| Function | Signature | Description |
|---|---|---|
upper(s) |
s → s |
Convert to uppercase |
lower(s) |
s → s |
Convert to lowercase |
trim(s) |
s → s |
Remove leading/trailing whitespace |
split(s, sep) |
s, s → s[] |
Split by separator |
has(s, sub) |
s, s → b |
Check if contains substring |
find(s, sub) |
s, s → i |
Find index of substring (-1 if not found) |
starts(s, pre) |
s, s → b |
Check if starts with prefix |
ends(s, suf) |
s, s → b |
Check if ends with suffix |
slice(s, start, end) |
s, i, i → s |
Extract substring |
replace(s, old, new) |
s, s, s → s |
Replace all occurrences |
repeat(s, n) |
s, i → s |
Repeat string n times |
padStart(s, len, pad) |
s, i, s → s |
Pad start to reach length |
padEnd(s, len, pad) |
s, i, s → s |
Pad end to reach length |
join(arr, sep) |
s[], s → s |
Join array with separator |
Example:
<- { upper, trim, split, has } = std/string
s#input = " hello, world "
// Method chaining with UFCS
s#cleaned = input.trim().upper()
print(cleaned) // "HELLO, WORLD"
// Search
?(input.has("world"))
print("Found it!")
;
// Split into array
s[]#words = input.trim().split(", ")
print(words[0]) // "hello"
print(words[1]) // "world"
Math functions for numerical operations:
<- { sqrt, floor, ceil, sin, cos, abs, min, max, random, PI } = std/math
| Function | Signature | Description |
|---|---|---|
abs(x) |
num → num |
Absolute value |
floor(x) |
num → i |
Round down |
ceil(x) |
num → i |
Round up |
round(x) |
num → i |
Round to nearest |
trunc(x) |
num → i |
Truncate decimals |
sign(x) |
num → i |
Sign (-1, 0, or 1) |
sqrt(x) |
num → f |
Square root |
cbrt(x) |
num → f |
Cube root |
pow(x, y) |
num, num → num |
Power |
exp(x) |
num → f |
e^x |
log(x) |
num → f |
Natural log |
log10(x) |
num → f |
Log base 10 |
log2(x) |
num → f |
Log base 2 |
sin(x) |
num → f |
Sine |
cos(x) |
num → f |
Cosine |
tan(x) |
num → f |
Tangent |
asin(x) |
num → f |
Arc sine |
acos(x) |
num → f |
Arc cosine |
atan(x) |
num → f |
Arc tangent |
atan2(y, x) |
num, num → f |
Two-argument arc tangent |
min(a, b) |
num, num → num |
Minimum of two values |
max(a, b) |
num, num → num |
Maximum of two values |
random() |
→ f |
Random float 0-1 |
randomInt(min, max) |
i, i → i |
Random int in range |
PI() |
→ f |
Pi constant (3.14159...) |
E() |
→ f |
Euler's number (2.71828...) |
Example:
<- { sqrt, floor, sin, PI } = std/math
f#x = 16.0
print(x.sqrt()) // 4
print(x.floor()) // 16
f#angle = PI() / 2
print(angle.sin()) // 1
Array utility functions:
<- { reverse, sort, includes, indexOf, sum, unique, first, last } = std/array
| Function | Signature | Description |
|---|---|---|
includes(arr, val) |
T[], T → b |
Check if array contains value |
indexOf(arr, val) |
T[], T → i |
Find index of value (-1 if not found) |
lastIndexOf(arr, val) |
T[], T → i |
Find last index of value |
reverse(arr) |
T[] → T[] |
Return reversed copy |
slice(arr, start, end) |
T[], i, i → T[] |
Extract sub-array |
concat(arr1, arr2) |
T[], T[] → T[] |
Concatenate arrays |
flat(arr) |
T[][] → T[] |
Flatten one level |
flatDeep(arr) |
T[][] → T[] |
Flatten all levels |
fill(arr, val) |
T[], T → T[] |
Fill array with value |
join(arr, sep) |
T[], s → s |
Join to string |
first(arr) |
T[] → T |
Get first element |
last(arr) |
T[] → T |
Get last element |
isEmpty(arr) |
T[] → b |
Check if empty |
sort(arr) |
num[] → num[] |
Sort numbers ascending |
sortDesc(arr) |
num[] → num[] |
Sort numbers descending |
sortStr(arr) |
s[] → s[] |
Sort strings alphabetically |
sum(arr) |
num[] → num |
Sum of elements |
product(arr) |
num[] → num |
Product of elements |
average(arr) |
num[] → num |
Average of elements |
minVal(arr) |
num[] → num |
Minimum value |
maxVal(arr) |
num[] → num |
Maximum value |
unique(arr) |
T[] → T[] |
Remove duplicates |
count(arr, val) |
T[], T → i |
Count occurrences |
Example:
<- { sort, sum, reverse, includes, unique } = std/array
i[]#nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(nums.sum()) // 31
print(nums.sort()) // [1, 1, 2, 3, 4, 5, 6, 9]
print(nums.unique().sort()) // [1, 2, 3, 4, 5, 6, 9]
print(nums.includes(5)) // true
print(nums.reverse()) // [6, 2, 9, 5, 1, 4, 1, 3]
HTTP server for building web applications. Uses ZZ's blocking model — no callbacks needed.
<- { createServer, nextRequest, respond, respondJson, getQuery, parseBody, closeServer, Request, Server } = std/server
| Function | Signature | Description |
|---|---|---|
createServer(port) |
i → Server |
Create and start HTTP server |
nextRequest(server) |
Server → Request |
Block until next request arrives |
respond(req, status, body) |
Request, i, s → void |
Send text response |
respondJson(req, status, data) |
Request, i, J → void |
Send JSON response |
respondWithHeaders(req, status, body, headers) |
Request, i, s, J → void |
Send response with custom headers |
closeServer(server) |
Server → void |
Graceful shutdown |
getQuery(req, key) |
Request, s → s |
Get query parameter |
getQueryAll(req) |
Request → J |
Get all query params as J object |
parseBody(req) |
Request → J |
Parse JSON body |
Structs:
Server—i#portRequest—s#method,s#path,s#body,J#headers,s#query,s#url
Example:
<- { createServer, nextRequest, respond, respondJson, Request, Server } = std/server
Server#server = createServer(3000)
print("Server listening on port 3000")
@(true)
Request#req = nextRequest(server)
??(req.method + " " + req.path)
| "GET /health" => respond(req, 200, "ok")
| "GET /users" => respondJson(req, 200, { users: ["alice", "bob"] })
| _ => respond(req, 404, "not found")
;
;
Concurrent handling with spawn:
Z handleRequest(Request#req)
??(req.method + " " + req.path)
| "GET /users" => respond(req, 200, "users")
| _ => respond(req, 404, "not found")
;
;
Server#server = createServer(3000)
@(true)
Request#req = nextRequest(server)
~> handleRequest(req)
;
?
// try body - code that might fail
:(errorVariable)
// catch body - handle error
;
Example:
s~result = "default"
?
result = "success"
:(err)
result = s"failed: {err}"
;
print(result)
Nested try-catch:
?
print("Outer try")
?
print("Inner try")
:(innerErr)
print(s"Inner catch: {innerErr}")
;
:(outerErr)
print(s"Outer catch: {outerErr}")
;
Use >X(expression) to throw an error:
>X("Something went wrong!")
With try-catch:
?
>X("Oops!")
:(e)
print(s"Caught: {e}")
;
Use error() to output to stderr (like console.error):
error("This goes to stderr")
print("This goes to stdout")
ZZ is synchronous by default. Every function call blocks until it completes — there are no callbacks, promises, or async/await at the language level.
All function calls block:
i#data = fetchData(url) // blocks until fetchData returns
processData(data) // runs after fetchData completes
Use ~> to spawn a function call that runs without blocking:
~> updateData(url, newData) // does not block — runs in background
print("continues immediately")
~> returns a Spawn struct. Use .onError() to handle errors:
Z handleError(s#e)
error(s"Failed: {e}")
;
~> riskyOperation(args).onError(handleError)
.onError() accepts a function name (not an inline function).
You can also capture the Spawn:
Spawn#task = ~> longRunning(args)
ZZ supports ES modules for code organization and reuse. There are two import modes: safe (<-) for ZZ modules with full type checking, and unsafe (<-!) for JavaScript/npm modules without type safety.
Use -> prefix to export functions and variables:
// math.zz
// Export a function
->i Z add(i#a i#b)
a + b
;
// Export a variable
->s#VERSION = "1.0.0"
->f#PI = 3.14159
// Private (not exported)
i Z helper(i#x)
x * 2
;
Use <- to import from other .zz modules. The compiler parses the imported file and resolves full type information — function signatures, variable types, struct definitions, and enums are all type-checked:
// Import from standard library (unquoted path)
<- { sqrt, floor, PI } = std/math
<- { upper, trim, split } = std/string
// Import from your own .zz modules (quoted path, relative to source file)
<- { add, subtract } = "./math"
// Import with alias
<- { add, pi=PI } = "./math"
// Import all as namespace
<- math = "./math"
Type safety with safe imports:
<- { upper } = std/string
<- { sqrt } = std/math
upper(42) // ❌ Compile error: expects string, got int
sqrt() // ❌ Compile error: missing argument
upper("hello") // ✅ Returns string
Auto-compilation: Safe imports automatically compile the imported .zz file to .js if the output is missing or stale (based on file timestamps). This means you don't need to manually compile dependencies — just run your main file.
ZZ supports a pkg/ directory for organizing reusable modules — like a local package system. Package imports use unquoted paths (same as std/), no quotes or .zz suffix needed:
// Import from a package
<- { greet, shout } = pkg/greeting
<- { Vec2, distance } = pkg/math_helpers
// Use imported symbols normally
print(greet("World")) // Hello, World!
Setting up packages:
- Create a
pkg/directory next to your.zzsource file - Add
.zzmodule files inside, exporting with->:
my-project/
├── main.zz # <- { greet } = pkg/greeting
├── pkg/
│ ├── greeting.zz # -> s Z greet(s#name) ...
│ └── utils.zz # -> i Z clamp(i#val i#lo i#hi) ...
└── compiled/ # Auto-generated
├── main.js
└── pkg/ # Package output (auto-generated)
├── greeting.js
└── utils.js
- Package modules are full ZZ files — they can import from
std/, use structs, enums, etc.:
// pkg/greeting.zz
<- { upper } = std/string
-> s Z greet(s#name)
s"Hello, {name}!"
;
-> s Z shout(s#name)
s"{upper(name)}!!!"
;
-> i Z add(i#a i#b)
a + b
;
How it works:
- The compiler resolves
pkg/moduletopkg/module.zznext to the source file - Full type checking — function signatures, structs, enums are all validated
- Auto-compilation — packages are compiled to
compiled/pkg/automatically - Stale detection — re-compiles only when the
.zzsource is newer than the.jsoutput
Current limitations:
- Packages are manual — create and manage the
pkg/directory yourself (no automatic fetching yet) - 1-level resolution only — transitive package imports are not auto-resolved
- No versioning or dependency management (yet)
Use <-! to import from JavaScript or npm modules. No type checking is performed — all imported bindings are treated as untyped:
// Import from a JS file
<-! { readFile } = "fs"
// Import from npm packages
<-! { fetch } = "node-fetch"
// Import from local JS files
<-! { helper } = "./lib/utils"
Use unsafe imports when:
- Importing npm packages
- Importing plain JavaScript files (no
.zzsource) - Working with Node.js built-in modules
- Standard library (
std/xxx): Unquoted. The compiler resolves the path to the built-instd/directory automatically. - Packages (
pkg/xxx): Unquoted. Resolves topkg/xxx.zznext to the source file. Compiled output goes tocompiled/pkg/. - Regular imports (
"./path"): Quoted. Paths are relative to the source.zzfile. The compiler adjusts them for the output location. .jsextension is optional — the compiler appends it automatically.- Only
std/andpkg/are valid unquoted import prefixes. Other unquoted paths produce a compile error.
// Use named imports directly
print(add(5, 3))
print(pi)
// Use namespace imports with dot notation
print(math.add(100, 200))
print(math.VERSION)
// Exports become:
export function add(a, b) {
return a + b;
}
export const VERSION = "1.0.0";
// Imports become:
import { add, subtract } from "./math.js";
import { PI as pi } from "./math.js";
import * as math from "./math.js";
import { sqrt, floor, PI } from "../../std/math.js";// FizzBuzz in ZZ
Z fizzbuzz(i#n)
@(i#1..n)
b#divisibleBy3 = i % 3 == 0
b#divisibleBy5 = i % 5 == 0
?(divisibleBy3 && divisibleBy5)
print("FizzBuzz")
:?(divisibleBy3)
print("Fizz")
:?(divisibleBy5)
print("Buzz")
:
print(i)
;
;
;
fizzbuzz(15)
| ZZ | JavaScript | Description |
|---|---|---|
s#x = "hi" |
const x = "hi" |
Immutable string |
i~x = 0 |
let x = 0 |
Mutable int |
print(x) |
console.log(x) |
|
?(cond) ... ; |
if (cond) { ... } |
If statement |
:?(cond) |
else if (cond) |
Else if |
: |
else |
Else |
@(cond) ... ; |
while (cond) { ... } |
While loop |
@(i#1..5) ... ; |
for (let i=1; i<=5; i++) |
For loop (range) |
@(x#arr) ... ; |
for (const x of arr) |
For-each loop |
>! |
break |
Break |
>> |
continue |
Continue |
Z fn() ... ; |
function fn() { ... } |
Void function |
i Z fn() ... ; |
function fn() { return ...; } |
Function with return |
? ... :(e) ... ; |
try { ... } catch(e) { ... } |
Try-catch |
>X(expr) |
throw expr |
Throw error |
error(x) |
console.error(x) |
Print to stderr |
s"...{x}..." |
`...${x}...` |
String interpolation |
i(x) |
Math.trunc(Number(x)) |
Cast to int |
_ |
null |
Null value |
1..5 |
[1,2,3,4,5] |
Range |
i[]#arr |
const arr = [...] |
Dynamic array |
i[5]#arr |
const arr = [...] |
Fixed-size array (no push/pop) |
ti5#tup |
Object.freeze([...]) |
Tuple (5 ints, immutable) |
tiN#tup |
Object.freeze([...]) |
Tuple (inferred length) |
(1, 2, 3) |
Object.freeze([1,2,3]) |
Tuple literal |
tiN(arr) |
Object.freeze([...arr]) |
Array to tuple cast |
i[](tup) |
[...tup] |
Tuple to array cast |
E Color Red Green ; |
const Color = Object.freeze({...}) |
Enum declaration |
Color#c = Color.Red |
const c = Color.Red |
Enum variable |
Color.Red |
Color.Red |
Enum access |
S Name ... ; |
class Name { ... } |
Struct declaration |
S Box<T> ... ; |
class Box { ... } |
Generic struct (erased) |
<T> T Z fn(T#x) |
function fn(x) |
Generic function (erased) |
<T: Trait> Z fn() |
function fn() |
Constrained generic (erased) |
S Name : Trait |
class Name { ... } |
Struct implementing trait |
Name#x = Name(...) |
const x = new Name(...) |
Immutable struct instance |
Name~x = Name(...) |
let x = new Name(...) |
Mutable struct instance |
ZZ Trait ... ; |
(nothing — erased) | Trait declaration |
Trait#x = ... |
const x = ... |
Trait-typed variable |
J#x = { k: v } |
const x = Object.freeze({k: v}) |
Immutable J object |
J~x = { k: v } |
let x = {k: v} |
Mutable J object |
x.has("k") |
("k" in x) |
J key existence check |
x.get("k") |
x["k"] |
J get value by key |
x.set("k", v) |
(x["k"] = v) |
J set value (mutable only) |
x.field |
x.field |
Field access |
x.method() |
x.method() |
Method call |
x++ |
x++ |
Increment |
x-- |
x-- |
Decrement |
x += 5 |
x += 5 |
Add assign |
x -= 5 |
x -= 5 |
Subtract assign |
x *= 5 |
x *= 5 |
Multiply assign |
x /= 5 |
x /= 5 |
Divide assign |
x %= 5 |
x %= 5 |
Modulo assign |
x **= 5 |
x **= 5 |
Power assign |
->i Z fn() |
export function fn() |
Export function |
->s#x = "hi" |
export const x = "hi" |
Export variable |
<- { a } = "./m" |
import { a } from "./m.js" |
Safe import (typed, .zz module) |
<-! { a } = "pkg" |
import { a } from "pkg" |
Unsafe import (untyped, JS) |
<- { a } = std/math |
import { a } from "../../std/math.js" |
Std library import |
<- m = "./m" |
import * as m from "./m.js" |
Namespace import |
??(val) | p => ; |
if/else-if chain (IIFE) |
Pattern matching |
| Color.Red => |
if (v === Color.Red) |
Enum pattern |
| 42 => |
if (v === 42) |
Literal pattern |
| Point(x, y) => |
destructure + bind fields | Struct pattern |
| { k: v } => |
if ("k" in obj && ...) |
J object pattern |
| _ => |
else |
Wildcard (catch-all) |
| v & v > 0 => |
if (true && (v > 0)) |
Guard condition |
$js { code } |
raw JS output verbatim | JS injection block |
str.len() |
str.length |
String length |
str.at(i) |
str.charAt(i) |
Character at index |
str.upper() |
upper(str) |
UFCS: calls imported function |
ZZ includes a built-in testing framework in std/test. Import the assertion functions you need and run your test file with --run.
<- { assert, assertEqual, assertEqualStr, assertApprox, assertFalse, assertContains, skip } = std/test
// Basic assertions
assert(true, "should be true")
assertEqual(2 + 2, 4, "addition works")
assertEqualStr("hello", "hello", "strings match")
assertApprox(3.14159, 3.14, 0.01, "pi approximation")
assertFalse(1 > 2, "1 is not greater than 2")
assertContains("hello world", "world", "contains substring")
// Skip a test
skip("not implemented yet")
| Function | Signature | Description |
|---|---|---|
assert(cond, msg) |
b, s → void |
Assert condition is true |
assertFalse(cond, msg) |
b, s → void |
Assert condition is false |
assertEqual(actual, expected, msg) |
i, i, s → void |
Compare two integers |
assertEqualStr(actual, expected, msg) |
s, s, s → void |
Compare two strings |
assertEqualBool(actual, expected, msg) |
b, b, s → void |
Compare two booleans |
assertApprox(actual, expected, epsilon, msg) |
f, f, f, s → void |
Compare floats within tolerance |
assertContains(str, substr, msg) |
s, s, s → void |
Assert string contains substring |
assertNull(val, msg) |
J, s → void |
Assert value is null |
assertNotNull(val, msg) |
J, s → void |
Assert value is not null |
assertLen(arr, expected, msg) |
i[], i, s → void |
Assert array has expected length |
fail(msg) |
s → void |
Immediately fail the test |
skip(reason) |
s → void |
Skip a test (prints skip message) |
# Run a test file
node dist/index.js tests/my_tests.zz --run
# Or with the global install
zz tests/my_tests.zz --runAll assertions throw on failure with a descriptive error message including expected vs actual values. A non-zero exit code is returned when any assertion fails.
ZZ has a full-featured VS Code extension with Language Server Protocol (LSP) support, located in editors/vscode/.
- Real-time diagnostics — type errors, syntax errors, and immutability violations shown as you type
- Autocomplete — keywords, functions, variables, struct fields, array methods
- Go-to-definition — Cmd/Ctrl+click to jump to declarations
- Hover information — type and kind information on hover
- Document Symbols — outline view showing structs, enums, traits, functions with their members
- Signature Help — parameter hints when typing function/method calls
- Find References — find all usages of variables, functions, structs, and other symbols
- Rename — rename symbols and all their references across the document
- Syntax highlighting — full TextMate grammar for
.zzfiles - Bracket matching — auto-closing pairs for
(),[],{},"" - Code folding — fold blocks terminated by
; - Comment toggling — toggle
//comments
The ZZ compiler must be built first:
cd /path/to/zz
npm install
npm run build- Open the
editors/vscodefolder in VS Code - Install dependencies and build:
cd editors/vscode npm install npm run compile - Press F5 to launch a new VS Code window with the extension loaded
cd editors/vscode
npm install
npm run compileThen copy to your extensions directory:
# macOS / Linux
cp -r . ~/.vscode/extensions/zz-language-0.2.0
# Windows
xcopy /E . %USERPROFILE%\.vscode\extensions\zz-language-0.2.0Restart VS Code.
npm install -g @vscode/vsce
cd editors/vscode
npm install
npm run compile
vsce package
code --install-extension zz-language-0.2.0.vsix- Language server not starting — make sure the ZZ compiler is built (
npm run buildin the project root). Check the Output panel (View > Output > "ZZ Language Server") for errors. - No autocomplete or diagnostics — ensure the file has a
.zzextension. Try reloading the window (Cmd/Ctrl+Shift+P > "Reload Window").
Syntax highlighting is available for multiple editors. See editors/README.md for full details including Emacs support.
mkdir -p ~/.vim/syntax ~/.vim/ftdetect
cp editors/vim/syntax/zz.vim ~/.vim/syntax/
cp editors/vim/ftdetect/zz.vim ~/.vim/ftdetect/
# For Neovim, use ~/.config/nvim/ instead of ~/.vim/# macOS
cp editors/sublime-text/* ~/Library/Application\ Support/Sublime\ Text/Packages/User/
# Linux
cp editors/sublime-text/* ~/.config/sublime-text/Packages/User/- Go to Settings → Editor → TextMate Bundles
- Click + and select the
editors/vscodefolder (uses the TextMate grammar) - Restart the IDE
If you're using AI coding agents (Claude Code, Cursor, GitHub Copilot, etc.) to write ZZ code, drop the ZZ_AGENT_GUIDE.md file into your project root or reference it in your agent's context.
The agent guide is a compact (~800 line) reference optimized for LLM consumption — no design philosophy, no editor setup, no generated JS examples. Instead it focuses on:
- Critical rules that cause compile errors (conditions must be boolean, every block ends with
;, etc.) - Complete syntax in scannable table/list format
- Common pitfalls mapping JS/TS/Python habits to ZZ equivalents
- What ZZ does NOT have — prevents agents from generating unsupported syntax
- Standard library API — function signatures at a glance
Copy ZZ_AGENT_GUIDE.md into any project that contains .zz files. Most AI agents will automatically pick it up from the project root. You can also:
- Claude Code: Reference it in your
CLAUDE.mdwith a note to readZZ_AGENT_GUIDE.mdfor ZZ syntax - Cursor: Add it to
.cursor/rules/or reference it in.cursorrules - GitHub Copilot: Place it in
.github/copilot-instructions.mdor include its contents there - Other agents: Add the file path to whatever context/instructions mechanism your agent supports
zz/
├── src/
│ ├── index.ts # CLI entry point
│ ├── lexer.ts # Tokenizer (100+ token types)
│ ├── parser.ts # Recursive descent parser
│ ├── ast.ts # AST node type definitions
│ ├── typechecker.ts # Static type validation
│ ├── evaluator.ts # Compile-time expression evaluator
│ ├── codegen.ts # JavaScript code generator
│ ├── repl.ts # Interactive REPL
│ └── errors.ts # Error formatting with source context
├── std/ # Standard library (.zz source, auto-compiled to .js)
│ ├── string.zz # String utilities
│ ├── math.zz # Math functions
│ ├── array.zz # Array utilities
│ ├── json.zz # JSON parsing and manipulation
│ ├── http.zz # HTTP client
│ ├── server.zz # HTTP server
│ ├── test.zz # Testing framework
│ ├── fs.zz # File system operations
│ ├── time.zz # Time and sleep utilities
│ ├── spawn.zz # Async spawn utilities
│ ├── rand.zz # Random number generation
│ ├── path.zz # Path manipulation
│ ├── os.zz # OS utilities
│ └── proc.zz # Process utilities
├── editors/ # Editor support
│ ├── vscode/ # VS Code extension with LSP
│ ├── vim/ # Vim/Neovim syntax highlighting
│ └── sublime-text/ # Sublime Text syntax highlighting
├── examples/ # Example ZZ programs
├── dist/ # Compiled TypeScript output
└── package.json
MIT