zezpaik

From an empty folder to a working program: every part of the language with a program that was run, and a cookbook.

The zezpaik tutorial

From an empty folder to a working program, with every part of the language shown by a program that runs. This is the document a new user reads first. It is written against zezpaik 0.39.0 on 64-bit Windows, the version in SPEC.md and in the compiler.

Every complete program below was compiled and run while this page was written, and the output shown is copied from the run. Where a rule has a refusal, the exact message the compiler prints is shown too. Nothing here is a fragment that has never been through the compiler.

Contents:

  1. Get a compiler and run a program
  2. The shape of a program
  3. Values and types
  4. Bindings and constants
  5. Printing
  6. Operators and arithmetic
  7. Control flow
  8. Functions
  9. Sequences: arrays, slices and for
  10. Records: struct
  11. Enums
  12. match
  13. Modules
  14. Files, arguments and directories
  15. Errors, traps and exit codes
  16. The standard library
  17. Projects and the SDK: zeko
  18. Windows and the C back end
  19. Cookbook
  20. What is not in the language (yet)
  21. Where to read more

1. Get a compiler and run a program

Two ways, and they run the same compiler.

call C:\path\to\zezpaik-sdk-full-0.37.0-win64\setup.cmd
zeko doctor
zeko new myapp
cd myapp
zeko run

From this repository (after make):

build\zezpaik.exe --interpret examples/hello.zk
build\zezpaik.exe examples/hello.zk -o hello.exe
hello.exe

The three ways to run one file:

commandwhat it doesneeds
zezpaik --interpret file.zkruns the reference implementationnothing but the compiler
zezpaik file.zk -o app.exeemits an object and links with gcca C linker on PATH
zezpaik --emit-c file.zkprints C to stdout insteadnothing; the C is the product

The interpreter is the reference: a compiled program must print the same bytes, report the same traps and exit with the same status. The corpus enforces that for hundreds of programs (tests/cli_runner.c).

One troubleshooting note. A native build and --emit-c stage a temporary file; if TMP and TEMP point somewhere unwritable, the compiler says cannot create a temporary file and exits 1. Point them at a folder you own and it works.

2. The shape of a program

// A line comment.
/*
 * A block comment.
 */

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let number: i32 = 100;
    let name: []u8 = "zezpaik";

    printf(fmt: "%s says %d\n", name, number);
    ret 0;
}

zezpaik says 100

3. Values and types

There are ten integer types plus i1, and nothing else. There is no bool, no float, no char, and no separate string type: text is a slice of bytes, []u8.

typebitsrange
i8 / u88-128..127 / 0..255
i16 / u1616-32768..32767 / 0..65535
i32 / u3232-2147483648..2147483647 / 0..4294967295
i64 / u6464about +/- 9.2e18 / 0..1.8e19
i128 / u128128native back end only (see below)
i110 or 1; what a comparison produces
extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let a: i8 = -128;
    let b: i16 = -32768;
    let c: i32 = -2147483648;
    let d: i64 = -9223372036854775808;

    let e: u8 = 255;
    let f: u16 = 65535;
    let g: u32 = 4000000000;
    let h: u64 = 18000000000000000000;

    let yes: i1 = 1 == 1;
    let hex: i32 = 0xFF;
    let bin: u8 = 0b1010_1010;
    let big: i32 = 1_000_000;

    printf(fmt: "i8 %d  i16 %d  i32 %d  i64 %lld\n", a, b, c, d);
    printf(fmt: "u8 %d  u16 %d  u32 %u  u64 %llu\n", e, f, g, h);
    printf(fmt: "i1 %d  hex %d  bin %d  big %d\n", yes, hex, bin, big);

    let widened: i32 = i32(a);
    let narrowed: u8 = u8(200);
    printf(fmt: "widened %d  narrowed %d\n", widened, narrowed);
    ret 0;
}

i8 -128  i16 -32768  i32 -2147483648  i64 -9223372036854775808
u8 255  u16 65535  u32 4000000000  u64 18000000000000000000
i1 1  hex 255  bin 170  big 1000000
widened -128  narrowed 200

4. Bindings and constants

extern def printf(fmt: *u8, ...) returns (i32);

const Language: *u8 = "zezpaik";   /* file scope: substituted at each use */
const Base: i32 = 6;
const Square: i32 = Base * Base;   /* arithmetic over constants is allowed */

def main() returns (i32)
{
    const Limit: i32 = 20;         /* a local constant */
    let name: []u8 = "immutable";  /* assigned once */
    let mut count: i32 = 0;        /* may be reassigned */

    count = count + 1;             /* `let mut` is required for this */
    printf(fmt: "%s %d %d %d\n", Language, Square, Limit, count);
    ret 0;
}

zezpaik 36 20 1

5. Printing

Most output goes through C's printf, declared once per file:

extern def printf(fmt: *u8, ...) returns (i32);

When the format string is a literal, the compiler checks it against the arguments and refuses a mismatch at compile time. The accepted conversions are:

conversionargument
%d %i %u %xan integer of 32 bits or fewer
%lld %lli %llu %llxa 64-bit integer
%sa *u8 or a []u8 (0.35.0)
%%nothing

Everything else - %f, %p, %c, %hd, %*d, a width or precision - is a compile error, because the compiler will not let a format string read the wrong bytes. The count of conversions must match the count of arguments exactly.

print(text) writes a []u8's bytes, exactly as many as its length says - including a NUL, which is where it differs from %s:

let greeting: []u8 = "hello";
print(greeting);              /* hello */
printf(fmt: " (%d bytes)\n", len(greeting));

print and %s are the two printing tools: printf for numbers and format, print for text.

6. Operators and arithmetic

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let sum: i32 = 7 + 3 * 2;              /* 13 */
    let quotient: i32 = -7 / 2;            /* -3: truncates toward zero */
    let remainder: i32 = -7 % 2;           /* -1: follows the dividend */
    let shifted: i32 = 1 << 4;             /* 16 */
    let anded: i32 = 12 & 10;              /* 8 */
    let ored: i32 = 12 | 10;               /* 14 */
    let xored: i32 = 12 ^ 10;              /* 6 */

    let wrapped: u8 = u8(255) +% u8(1);    /* 0 */
    let clamped: u8 = u8(200) +| u8(100);  /* 255 */
    let floor: u8 = u8(5) -| u8(10);       /* 0 */

    let less: i1 = 3 < 4;
    let logic: i1 = (3 < 4) && !(2 == 3);
    let either: i1 = (2 > 3) || (4 == 4);

    printf(fmt: "sum %d  div %d  rem %d  shift %d\n", sum, quotient, remainder, shifted);
    printf(fmt: "and %d  or %d  xor %d\n", anded, ored, xored);
    printf(fmt: "wrap %d  sat %d  floor %d\n", wrapped, clamped, floor);
    printf(fmt: "less %d  logic %d  either %d\n", less, logic, either);
    ret 0;
}

sum 13  div -3  rem -1  shift 16
and 8  or 14  xor 6
wrap 0  sat 255  floor 0
less 1  logic 1  either 1

Arithmetic has three answers to "the result does not fit", and the program writes which one it means:

/ and % truncate toward zero; the remainder follows the dividend. Division or remainder by zero traps, and so does T.min / -1. Shifts trap when the count is outside 0 <= count < bits. ^ is XOR, not exponentiation. There is no compound assignment and no ++/--.

Precedence, tightest first (the ladder is deliberately shorter than C's):

  1. unary - ! ~
  2. * / % *% *| & << >>
  3. + - +% -% +| -| | ^
  4. == != < <= > >=
  5. &&
  6. ||

So a & b == c is (a & b) == c and a << b + c is (a << b) + c, the opposite of C's famous surprises. Parentheses always group.

7. Control flow

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let mut n: i32 = 1;

    while n <= 15
    {
        if n % 15 == 0
        {
            printf(fmt: "FizzBuzz\n");
        }
        else if n % 3 == 0
        {
            printf(fmt: "Fizz\n");
        }
        else if n % 5 == 0
        {
            printf(fmt: "Buzz\n");
        }
        else
        {
            printf(fmt: "%d\n", n);
        }
        n = n + 1;
    }

    let mut i: i32 = 0;
    let mut total: i32 = 0;

    while 1 == 1
    {
        i = i + 1;
        if i > 20 { break; }
        if i % 2 == 0 { continue; }
        total = total + i;
    }
    printf(fmt: "odd sum 1..20: %d\n", total);
    ret 0;
}

The first loop prints 1 2 Fizz 4 Buzz ... FizzBuzz; the last line is:

odd sum 1..20: 100

8. Functions

extern def printf(fmt: *u8, ...) returns (i32);

def double(n: i32) returns (i32)
{
    ret n * 2;
}

def add(a: i32, b: i32) returns (i32)
{
    ret a + b;
}

def bump(mut n: i32) returns (i32)
{
    n = n + 1;                 /* its own copy; the caller is untouched */
    ret n;
}

def divmod(a: i32, b: i32) returns (i32, i32)
{
    ret (a / b, a % b);        /* two results */
}

def factorial(n: i32) returns (i32)
{
    if n <= 1 { ret 1; }
    ret n * factorial(n: n - 1);
}

def greet(name: []u8) returns ()
{
    printf(fmt: "hello, %s\n", name);
    ret;
}

def main() returns (i32)
{
    let start: i32 = 41;
    let after: i32 = bump(start);
    let (q: i32, r: i32) = divmod(a: 17, b: 5);

    printf(fmt: "double %d  add %d  bump %d (start %d)\n",
           double(21), add(a: 2, b: 3), after, start);
    printf(fmt: "17 / 5 = %d remainder %d\n", q, r);
    printf(fmt: "5! = %d\n", factorial(n: 5));
    greet("zezpaik");
    ret 0;
}

double 42  add 5  bump 42 (start 41)
17 / 5 = 3 remainder 2
5! = 120
hello, zezpaik

9. Sequences: arrays, slices and for

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let mut fixed: [5]i32 = [1, 2, 3, 4, 5];
    fixed[0] = 10;

    let mut page: [8]u8 = [65; 8];       /* 'A' eight times */
    let mut text: []u8 = "hello";
    text[0] = 72;                        /* "Hello" */

    let mut scratch: []u8 = buffer(16);  /* runtime-owned, zeroed */
    scratch[0] = 111;
    scratch[1] = 107;
    scratch[2] = 10;

    let words: [][]u8 = ["one", "two", "three"];

    printf(fmt: "fixed %d %d %d  len %d\n", fixed[0], fixed[1], fixed[4], len(fixed));
    print(page[0..8]);
    printf(fmt: " (page is %d bytes)\n", len(page[0..8]));
    print(text);
    printf(fmt: " (text is %d bytes)\n", len(text));
    print(scratch[0..3]);

    for word in words
    {
        print(word);
        print(" ");
    }
    printf(fmt: "(%d words)\n", len(words));

    let part: []i32 = fixed[1..3];
    printf(fmt: "part %d %d, len %d\n", part[0], part[1], len(part));
    ret 0;
}

fixed 10 2 5  len 5
AAAAAAAA (page is 8 bytes)
Hello (text is 5 bytes)
ok
one two three (3 words)
part 2 3, len 2
  let mut raw: []u8 = buffer(4);
  raw[0] = 255;
  let mut lanes: []i8 = view_i8(raw);
  lanes[0] = -1;              /* raw[0] is 255 again */

It is a slice for the lifetime rule, so a view of a local array cannot be returned; a view of a parameter, buffer, string literal or read_file/args/read_dir bytes can. The C back end refuses []i8 until it has a slice (D-41).

10. Records: struct

extern def printf(fmt: *u8, ...) returns (i32);

struct Point
{
    x: i32,
    y: i32,
}

struct Segment
{
    from: Point,
    to: Point,
}

def length_squared(s: Segment) returns (i32)
{
    let dx: i32 = s.to.x - s.from.x;
    let dy: i32 = s.to.y - s.from.y;
    ret dx * dx + dy * dy;
}

def main() returns (i32)
{
    let mut s: Segment = { from: { x: 0, y: 0 }, to: { x: 3, y: 4 } };
    printf(fmt: "before %d\n", length_squared(s: s));

    s.to.x = 6;
    printf(fmt: "after %d, to = %d,%d\n", length_squared(s: s), s.to.x, s.to.y);

    let p: Point = { x: 1, y: 2 };
    let q: Point = p;
    printf(fmt: "copy %d,%d\n", q.x, q.y);
    ret 0;
}

before 25
after 52, to = 6,4
copy 1,2

11. Enums

pub enum Scancode
{
    A: i32 = 4,
    P: i32 = 19,
    V: i32 = 25,
    W: i32 = 26,
}
  main.zk:5:12: error: an i32 is not a 'Scancode'; write a member of the enum

12. match

extern def printf(fmt: *u8, ...) returns (i32);

pub enum Light
{
    Red: i32 = 0,
    Amber: i32 = 1,
    Green: i32 = 2,
}

def next(light: Light) returns (Light)
{
    match light
    {
        Light::Red   { ret Light::Green; }
        Light::Amber { ret Light::Red; }
        Light::Green { ret Light::Amber; }
    }
}

def say(light: Light) returns ()
{
    match light
    {
        Light::Red   { printf(fmt: "red\n"); }
        Light::Amber { printf(fmt: "amber\n"); }
        Light::Green { printf(fmt: "green\n"); }
    }
    ret;
}

def main() returns (i32)
{
    let mut light: Light = Light::Red;
    let mut step: i32 = 0;

    while step < 5
    {
        say(light: light);
        light = next(light: light);
        step = step + 1;
    }
    ret 0;
}

red
green
amber
red
green

Two readings, and the matched value's type decides which:

matched valuedefaultrule
an enumforbiddenevery member must appear in exactly one arm
an i32requiredan open set cannot be exhausted
  main.zk:21:5: error: match is not complete: Scancode::A and Scancode::V are not handled

13. Modules

One file can be used from another. mathbox.zk:

pub const Answer: i32 = 42;

pub def double(n: i32) returns (i32)
{
    ret n * 2;
}

def secret() returns (i32)
{
    ret 7;
}

pub def revealed() returns (i32)
{
    ret secret();
}

main.zk:

import mathbox;

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    printf(fmt: "Answer %d\n", mathbox::Answer);
    printf(fmt: "double(21) %d\n", mathbox::double(n: 21));
    printf(fmt: "revealed %d\n", mathbox::revealed());
    ret 0;
}

Answer 42
double(21) 42
revealed 7

14. Files, arguments and directories

import io;
import args;

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let path: []u8 = "tutorial.tmp";

    io::write(path: path, text: "alpha\nbeta\n");
    let text: []u8 = io::read(path);
    print(text);
    printf(fmt: "bytes %d  lines %d\n", len(text), io::line_count(path));

    printf(fmt: "exists %d  is_dir %d  missing %d\n",
           exists(path), is_dir("."), exists("no-such-file.tmp"));

    let names: [][]u8 = read_dir(".");
    let mut found: i32 = 0;
    for name in names
    {
        if args::same_text(a: name, b: path) { found = 1; }
    }
    printf(fmt: "entries %d  wrote %d\n", len(names), found);
    ret 0;
}

alpha
beta
bytes 11  lines 2
exists 1  is_dir 1  missing 0
entries 1  wrote 1
  mkdir("objects");
  mkdir("objects/ab");     /* one level at a time: the parent came first */
  mkdir("objects");        /* already there: success */

  main.zk:3:5: error: cannot create 'objects/ab'

15. Errors, traps and exit codes

There are two very different failures:

whenoutputexit
compile errorbefore the program runs<file>:<line>:<col>: error: ... on stderr1
trapwhile it runs<file>:<line>:<col>: error: ... on stderr2

A trap is defined behaviour: the program stops at a known point with a sentence, instead of reading whatever memory happens to be there. These trap:

assert quotes the condition exactly as it was written:

assert(count == 0);

main.zk:5:5: error: assertion failed: count == 0

There are no exceptions, no try/catch, no error values and no recovery: a trap ends the program. A library function that should be loud uses assert; one that should answer quietly returns a flag (num::parse_int answers (value, ok)).

16. The standard library

The library is written in zezpaik, in lib/, and the compiler finds it beside itself. It allocates nothing: its functions view the caller's bytes, parse them, format into a caller's buffer, or print.

import text;
import num;
import csv;
import date;

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let (value: i32, ok: i1) = num::parse_int("-42");
    let (bad: i32, bad_ok: i1) = num::parse_int("4x");
    printf(fmt: "parse %d %d / %d %d\n", value, ok, bad, bad_ok);

    let mut number: [12]u8 = [0; 12];
    let mut buf: []u8 = number[0..12];
    let used: i32 = num::format_int(value: 12345, buf: buf);
    print(buf[0..used]);
    printf(fmt: " (%d bytes)\n", used);

    let line: []u8 = "sato,tanaka,,suzuki";
    printf(fmt: "fields %d\n", text::field_count(line: line, separator: 44));
    print(text::field(line: line, index: 1, separator: 44));
    printf(fmt: "\n");
    print(text::trim("  padded  "));
    printf(fmt: "\n");

    let row: []u8 = "a,\"b,c\",d";
    printf(fmt: "csv fields %d\n", csv::field_count(row: row));
    print(csv::field_at(row: row, index: 1));
    printf(fmt: "\n");

    printf(fmt: "2024 leap %d  Feb %d days  2023 leap %d\n",
           date::is_leap(2024), date::month_length(year: 2024, month: 2),
           date::is_leap(2023));
    ret 0;
}

parse -42 1 / 0 0
12345 (5 bytes)
fields 4
tanaka
padded
csv fields 3
"b,c"
2024 leap 1  Feb 29 days  2023 leap 0

The core modules a new program is likely to want:

modulewhat it is fora function to start with
iowhole files and storageio::read, io::write, io::copy
textwhat bytes mean: trim, fields, linestext::trim, text::field, text::line
numtext <-> i32num::parse_int, num::format_int
utf8walking UTF-8 textutf8::length, utf8::at
stringsbuilding text in a bufferstrings::put, strings::find
csvRFC 4180 quoting in both directionscsv::field_count, csv::field_at
sha256one hash into a 32-byte outputsha256::hash
datecivil dates and the packed stampdate::is_leap, date::days_from_civil
fieldan editable text field (caret, capacity)field::load, field::insert
byteshex, little/big-endian words, IEEE-754 as bitsbytes::hex_into, bytes::u32le
htmlbuilding HTML and SVGhtml::open, html::paragraph_text
httpwhat a request means and what an answer sayshttp::parse_request, http::status_name
mailRFC 5322 headers and bodiesmail::parse_headers, mail::unfold
nninteger-only inference: int8 matrices, layers, a bump arenann::layer, nn::forward
cnnconvolution and pooling over int8 lanes, integer-onlycnn::conv2d, cnn::max_pool2d
zkmthe ZKM1 model file: a checked header, a tensor table, a digestzkm::verify, zkm::find, zkm::body
bmpa 24/32-bit BMP as one grey byte per pixel, and a plane back as a BMPbmp::info, bmp::gray, bmp::write_gray

There are 88 modules in lib/, from ledger and worksheet to raytrace, mesh, ui, app, canvas, tone and wav. Every file starts with a comment that states its contract, and every public declaration is pub def/pub const/pub struct/pub enum. Read lib/ - the first thirty lines of a module are its manual.

The newest modules are cnn and zkm, the two halves of a picture model: cnn is the arithmetic - a filter that slides over a plane, a window reduced to its largest value or to its mean, all over int8 lanes - and zkm is the file that says which bytes are which. Neither knows what the other is; a program checks a model with zkm, views a tensor with view_i8 and runs it with cnn and nn:

import cnn;
import zkm;

if zkm::verify(data: model) != 0
{
    print("model: not a ZKM1 file\n");
    ret 1;
}
let w: []i8 = view_i8(zkm::body(data: model, index: zkm::find(data: model, want: "conv1.w")));

nn is the third: integer-only inference for a model converted to int8 outside the language. A Matrix is a row-major view of the model's own bytes, a Layer carries weights, bias and the requantise shift, Network runs two of them, and Arena bump-allocates activations in a pool the caller owns. There is no float and no allocator:

import nn;

let mut pool: []u8 = buffer(4096);
let mut arena: nn::Arena = nn::arena(pool);
let scratch: []u8 = nn::take(a: arena, n: 256);

The C back end refuses the slice paths (crefused); the interpreter and native build agree.

17. Projects and the SDK: zeko

A zezpaik project is a folder with a zeko.txt and a source file:

myapp/
    zeko.txt
    src/main.zk

zeko.txt:

name hello
source src/main.zk

src/main.zk:

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    printf(fmt: "hello from a zeko project\n");
    ret 0;
}

cd myapp
zeko run

zeko: hello: building src/main.zk
hello from a zeko project

zeko always rebuilds, on purpose. The language has no way to ask a file's age, and a binary built by last week's compiler is the bug the tool exists to prevent.

commandwhat it does
zeko new myappwrites a standard folder with src/main.zk and a .gitignore
zeko runbuilds, then runs the project's main program
zeko buildbuilds it
zeko checkbuilds, then runs every step and check line
zeko cleanremoves the object, the executable and the .actual files
zeko doctorasks every input a build needs and gives a verdict
zeko kit <folder>writes the portable folder (compiler, lib, shim)
zeko <task>builds, then runs a task line

18. Windows and the C back end

A console program needs nothing but the compiler and a linker. A window program links the platform layer in shim/ and SDL3 statically, so the output is still one self-contained executable. A minimal window program (the SDK's samples/window) is:

extern def printf(fmt: *u8, ...) returns (i32);
extern def ui_open(title: *u8, width: i32, height: i32) returns (*u8);
extern def ui_close(handle: *u8) returns ();
extern def ui_color(handle: *u8, red: i32, green: i32, blue: i32) returns ();
extern def ui_clear(handle: *u8) returns ();
extern def ui_rect(handle: *u8, x: i32, y: i32, width: i32, height: i32) returns ();
extern def ui_text(handle: *u8, x: i32, y: i32, text: []u8) returns (i32);
extern def ui_present(handle: *u8) returns ();
extern def ui_pump(handle: *u8, millis: i32) returns (i32);

def main() returns (i32)
{
    let win: *u8 = ui_open(title: "zezpaik", width: 640, height: 360);
    let title: []u8 = "hello from a window";

    ui_color(handle: win, red: 18, green: 18, blue: 22);
    ui_clear(handle: win);
    ui_color(handle: win, red: 235, green: 205, blue: 120);
    ui_rect(handle: win, x: 40, y: 40, width: 560, height: 280);
    ui_color(handle: win, red: 20, green: 20, blue: 20);
    ui_text(handle: win, x: 64, y: 72, text: title);
    ui_present(handle: win);

    let mut frame: i32 = 0;
    while frame < 3
    {
        ui_pump(handle: win, millis: 16);
        frame = frame + 1;
    }
    ui_close(handle: win);
    ret 0;
}

Its zeko.txt names the shim and the libraries:

name window
source src/main.zk
shim ..\..\shim\ui_sdl3.c ..\..\shim\screen.c
libs -Wl,-Bstatic -lSDL3 -lSDL3_image -Wl,-Bdynamic -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 -lole32 -loleaut32 -lversion -luuid -ladvapi32 -lsetupapi -lshell32 -ldinput8 -mwindows

With SDL_VIDEODRIVER=dummy and SDL_RENDER_DRIVER=software, zeko run builds and exits 0 with no display, which is how the project checks frames.

--emit-c is the third implementation. It prints C99 over <stdint.h> for the scalar core, struct, conversions, assert, enum and match. A construct it cannot carry is a compile error naming it, never a silent difference:

  main.zk:40:33: error: the C back end does not support the type '[]u8' yet:
                   use the native back end (-o) or --interpret

The emitted file compiles clean at -Wall -Wextra -Werror. --target linux-x86_64 emits an ELF object instead of a Windows one; a Linux cc links it.

19. Cookbook

wc - bytes, lines and words of a file

import io;
import text;

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let operands: [][]u8 = args();

    if len(operands) != 1
    {
        printf(fmt: "usage: wc <file>\n");
        ret 1;
    }

    let path: []u8 = operands[0];
    let body: []u8 = io::read(path);

    let mut words: i32 = 0;
    let mut inside: i1 = 0 == 1;

    for byte in body
    {
        if text::is_space(byte)
        {
            inside = 0 == 1;
        }
        else
        {
            if inside == (0 == 1)
            {
                words = words + 1;
                inside = 1 == 1;
            }
        }
    }
    printf(fmt: "%d bytes  %d lines  %d words  %s\n",
           len(body), io::line_count(path), words, path);
    ret 0;
}

> zezpaik --interpret wc.zk -- examples/hello.zk
1072 bytes  29 lines  171 words  examples/hello.zk

calc - arguments, parsing, and a dispatch

import num;
import args;

extern def printf(fmt: *u8, ...) returns (i32);

def main() returns (i32)
{
    let a: [][]u8 = args();

    if len(a) != 3
    {
        printf(fmt: "usage: calc <add|sub|mul> <a> <b>\n");
        ret 1;
    }

    let (left: i32, left_ok: i1) = num::parse_int(a[1]);
    let (right: i32, right_ok: i1) = num::parse_int(a[2]);

    if left_ok == (0 == 1) || right_ok == (0 == 1)
    {
        printf(fmt: "calc: both operands must be integers\n");
        ret 1;
    }

    let mut result: i32 = 0;

    if args::same_text(a: a[0], b: "add")      { result = left + right; }
    else if args::same_text(a: a[0], b: "sub") { result = left - right; }
    else if args::same_text(a: a[0], b: "mul") { result = left * right; }
    else
    {
        printf(fmt: "calc: unknown operation '%s'\n", a[0]);
        ret 1;
    }
    printf(fmt: "%d\n", result);
    ret 0;
}

> zezpaik --interpret calc.zk -- add 2 3
5
> zezpaik --interpret calc.zk -- mul 6 7
42

A self-checking test

A test in zezpaik is a program: compare, count failures, exit 1 if any failed. No framework is needed.

extern def printf(fmt: *u8, ...) returns (i32);

pub enum Command { Add: i32 = 0, Remove: i32 = 1, Quit: i32 = 2 }

def code(command: Command) returns (i32)
{
    match command
    {
        Command::Add    { ret 10; }
        Command::Remove { ret 11; }
        Command::Quit   { ret 13; }
    }
}

def expect(got: i32, want: i32, id: i32) returns (i32)
{
    if got != want
    {
        printf(fmt: "FAIL test %d: got %d, want %d\n", id, got, want);
        ret 1;
    }
    printf(fmt: "ok   test %d\n", id);
    ret 0;
}

def main() returns (i32)
{
    let mut failed: i32 = 0;
    failed = failed + expect(got: code(Command::Add), want: 10, id: 1);
    failed = failed + expect(got: code(Command::Remove), want: 11, id: 2);
    failed = failed + expect(got: code(Command::Quit), want: 13, id: 3);

    if failed != 0 { ret 1; }
    printf(fmt: "all tests passed\n");
    ret 0;
}

ok   test 1
ok   test 2
ok   test 3
all tests passed

20. What is not in the language (yet)

The absences are decisions, not gaps:

21. Where to read more

The shortest path from here: copy the hello program, change the text, run it; then copy calc, add an operation, run it with arguments; then write a struct and a match over an enum. The corpus and the examples are the reference for every step in between.