Your First Program
This page builds one small program, adding a language feature at a time. By the end you'll have touched functions, inference, classes, sealed hierarchies, and pattern matching — enough to read most Zena code.
Hello, world ​
export function main(): i32 {
console.log('Hello, world!');
return 0;
}
zena run main.zena
Four things are already worth noticing:
let, notfunction. Zena has arrow functions and nothing else. A function is a value bound withlet.export.mainis the entry point, so the module has to export it.- The return type is written. Parameter types and return types on exported functions are required; inside a function body, inference does the work.
Adding a function ​
let greet = (name: String): String => `Hello, ${name}!`;
export function main(): i32 {
console.log(greet('world'));
return 0;
}
An arrow function with an expression body returns that expression. Template
literals interpolate with ${}, the same as JavaScript.
Parameters can have defaults, and callers may omit trailing arguments:
let greet = (name: String, greeting: String = 'Hello'): String =>
`${greeting}, ${name}!`;
greet('world'); // "Hello, world!"
greet('world', 'Howdy'); // "Howdy, world!"
Types and inference ​
Zena infers types for local bindings, so annotations are for the places where they carry information:
let count = 42; // i32
let ratio = 0.5; // f64
let name = 'Zena'; // String
let flags = [1, 2, 3]; // ImmutableArray<i32>
var total = 0; // `var` because it changes
for (let n in flags) {
total += n;
}
Note what doesn't happen here. There is no implicit conversion between numeric types, so this is an error:
let n: i32 = 42;
let x: f64 = n; // error: i32 is not assignable to f64
let y: f64 = n as f64; // explicit conversion
And conditions must be boolean — there is no truthiness:
if (count) { } // error: i32 is not assignable to boolean
if (count != 0) { } // fine
These rules feel strict for about a day, and then they stop costing anything. See Types.
Structuring data ​
For plain data, use a record — a structural type with no declaration ceremony:
type Point = {x: f64, y: f64};
let origin: Point = {x: 0.0, y: 0.0};
let {x, y} = origin; // destructuring
For data with behaviour and identity, use a class:
class Circle {
radius: f64; // immutable field (the default)
var label: String; // mutable field
new(this.radius, this.label);
area(): f64 => 3.14159 * this.radius * this.radius;
}
let c = new Circle(2.0, 'small');
console.log(`${c.label}: ${c.area()}`); // needs `import {console} …`
The constructor's this.radius parameter assigns the field directly — the
Dart shorthand for what would otherwise be a line of boilerplate per field.
Matching on a closed set ​
When a value is one of a fixed set of shapes, use a sealed class. The
compiler then knows the full set and can check that a match handles all of it:
sealed class Shape {
case Circle(radius: f64)
case Rect(width: f64, height: f64)
case Triangle(base: f64, height: f64)
}
let area = (shape: Shape): f64 => match (shape) {
case Circle {radius}: 3.14159 * radius * radius
case Rect {width, height}: width * height
case Triangle {base, height}: 0.5 * base * height
};
Add a fourth case to Shape and this match stops compiling until you handle
it — which is the point. Guards work too:
let describe = (shape: Shape): String => match (shape) {
case Circle {radius} if radius > 10.0: 'a large circle'
case Circle: 'a circle'
case Rect {width, height} if width == height: 'a square'
case Rect: 'a rectangle'
case Triangle: 'a triangle'
};
Putting it together ​
sealed class Shape {
case Circle(radius: f64)
case Rect(width: f64, height: f64)
}
let area = (shape: Shape): f64 => match (shape) {
case Circle {radius}: 3.14159 * radius * radius
case Rect {width, height}: width * height
};
export function main(): i32 {
let shapes: FixedArray<Shape> = [
new Circle(1.0),
new Rect(2.0, 3.0),
new Circle(0.5),
];
var total = 0.0;
for (let shape in shapes) {
total += area(shape);
}
console.log(`Total area: ${total}`);
return 0;
}
Compiling to Wasm ​
zena build main.zena -o main.wasm --dce
--dce turns on dead-code elimination, which drops unused code and types from
the output. Inspect the result with wasm-tools:
wasm-tools print main.wasm | head -40
You'll see your classes as Wasm GC struct types and your functions as Wasm functions — no runtime, no allocator, no collector. That mapping is the whole point of the language; WebAssembly walks through it.