# learning zc zc is a small C-like language for a stack machine with 64-bit words. this guide starts with ordinary programs, then covers memory, types, native calls, and metaprogramming. every zc code block is a complete file in [examples/learn/](../examples/learn/). the output below it comes from the matching `.out` file. ## setup build from the repository root, then run a source file on stdin: ```sh make build/bin/zy < examples/learn/01-hello.zc ``` `build/bin/zy` is the native core: it uses the arm64 jit where available and otherwise interprets. `build/bin/zym` is the portable microcore, `build/bin/zc0` is the bootstrap compiler, and `build/bin/zclsp` checks source for the editor. the interactive runners are `./tinyvm.sh` on linux and `tinyvm.bat` on windows. run from the root throughout this guide. paths in `rt.readfile` are resolved from the process's working directory, and so are those in `include` when nothing nearer has the file. `tests/drivers/learn.py` checks the examples and verifies that the code blocks here match their source files: ```sh python3 tests/drivers/learn.py build/bin/zy ``` example 23 uses the native C ABI and requires host FFI support. the microcore does not provide that support, so skip example 23 when running the guide on a micro-only host; the learning driver does not currently select examples by backend capability. native c calls need the full native core. the raw syscall example needs linux on x86-64. the other examples use ordinary language features; availability of host services still depends on the port. ## contents - [programs and control flow](#programs-and-control-flow) - [numbers and strings](#numbers-and-strings) - [ownership and the heap](#ownership-and-the-heap) - [libraries and the host](#libraries-and-the-host) - [types and containers](#types-and-containers) - [inferred call types](#inferred-call-types) - [const fixed arrays](#const-fixed-arrays) - [optimized regions](#optimized-regions) - [closures](#closures) - [editor hints](#editor-hints) - [further reading](#further-reading) ## programs and control flow ### the first program a unit is read to eof or a backtick outside a comment or literal. after compiling the unit, the runtime calls `main`. an ordinary function returns one word; falling off its end returns 0. the return value of `main` is not the process exit status. `rt.halt()` stops the machine. `rt.put_s(s)` prints a string, `rt.put_n(n)` prints an integer, and `rt.put_c(c)` prints a character code. `rt.put_c(10)` prints a newline. [example source](../examples/learn/01-hello.zc): ```zc namespace; include rt; fn main() { rt.put_s("hello, world!\n"); } ``` output: ```text hello, world! ``` ### variables and while `let` infers a variable's type from its initializer. an integer literal stays a plain word. `int i = 0;` writes the type explicitly. a local must be assigned on every path before it is read (locals are not zeroed); passing `&x` counts as initializing `x`, and an array or struct needs a full `{...}` initializer or `&agg`. the lifetime analyzer runs by default; the optional typechecker is enabled separately with `typecheck on;`. `while` tests before entering the body. `i++` increments the variable, and `break` leaves the loop. [example source](../examples/learn/02-loops.zc): ```zc namespace; include rt; fn main() { let i = 0, n = 0; while (i < 4) { n = n + i; i++; } rt.put_n(n); rt.put_c(10); } ``` output: ```text 6 ``` ### file namespaces and multiple includes `namespace app;` scopes declarations in this file. `include rt, std;` makes both namespaces available here; an included library keeps its own namespace and does not import its dependencies into `app`. `let` infers the word-string type from `"21"`, and the qualified standard-library call parses it. [example source](../examples/learn/27-namespaces.zc): ```zc namespace app; include rt, std; int twice(int n) { return n * 2; } fn main() { let text = "21"; rt.put_n(twice(std.Text.atoi(text))); rt.put_c(10); } ``` output: ```text 42 ``` ### do and continue `do` runs the body once before testing the condition. in a `do` loop, `continue` jumps to the condition; in a `for` loop, it jumps to the increment step. this example skips adding 2. [example source](../examples/learn/03-do-continue.zc): ```zc namespace; include rt; fn main() { int n = 0, i = 0; do { i++; if (i == 2) continue; n = n + i; } while (i < 5); rt.put_n(n); rt.put_c(10); } ``` output: ```text 13 ``` ### switch `switch` selects the matching `case`; `default` handles the rest. cases do not fall through, so `break` is optional at the end of an arm. [example source](../examples/learn/17-switch.zc): ```zc namespace; include rt; fn main() { int k = 2; switch (k) { case 1: rt.put_s("one"); break; case 2: rt.put_s("two"); break; default: rt.put_s("other"); } rt.put_c(10); } ``` output: ```text two ``` ### plain enums members without an explicit value increase by one, starting at 0. `GREEN = 5` makes `BLUE` equal 6. members can also be written as `color.BLUE` and used as case labels. with the typechecker enabled, `color` is its own type: members have that type, an enum value can become an `int`, and an arbitrary integer does not become a `color`. [example source](../examples/learn/04-enums.zc): ```zc namespace; include rt; enum color { RED, GREEN = 5, BLUE }; fn main() { rt.put_n(RED); rt.put_c(32); rt.put_n(GREEN); rt.put_c(32); rt.put_n(BLUE); rt.put_c(10); } ``` output: ```text 0 5 6 ``` ### defer `defer expr;`, `defer { statements }` or any other single statement (`defer if (c) a; else b;`, a loop, a `switch`) runs code when the enclosing block ends: after the last statement, and also on `return`, `break` and `continue` out of it. defers in a block run last registered first. the deferred code is compiled at each exit, so it sees the variables as they are when it runs, and `return e;` computes `e` before any defer runs. `defer rt.free(p);` hands the owner `p` over at that point: `p` stays usable until the block ends and is not freed twice. `return`, `break` and `continue` are not allowed inside a deferred statement. [example source](../examples/learn/34-defer.zc): ```zc namespace; include rt; fn say(n) { rt.put_n(n); rt.put_c(32); return 0; } int work(int n) { int* p = rt.new(int); defer rt.free(p); defer say(100); *p = n * 2; say(*p); if (n > 2) return *p + 1; say(1); return 0; } fn main() { for (let i = 0; i < 4; i++) { defer { say(i); rt.put_c(10); } if (i == 1) continue; say(work(i)); } } ``` output: ```text 0 1 100 0 0 1 4 1 100 0 2 6 100 7 3 ``` ## numbers and strings ### sized integers `u8`, `i8`, `u16`, `i16`, `u32`, and `i32` narrow on assignment. adding 10 to an unsigned byte holding 250 leaves 4. arrays and pointers of these types use packed storage. addresses normally count words. `sizeof` also counts words, so `u8[100]` occupies 13 words. an `int*` points to word-sized integers; `p + 1` advances one word. a sized pointer such as `u8*` indexes packed elements. [example source](../examples/learn/05-small-ints.zc): ```zc namespace; include rt; fn main() { u8 a = 250; a = a + 10; rt.put_n(a); rt.put_c(10); rt.put_n(sizeof(u8[100])); rt.put_c(10); u8* px = rt.new(u8, 4096); px[10] = 300; rt.put_n(px[10]); rt.put_c(10); } ``` output: ```text 4 13 44 ``` ### floating point a literal such as `1.5` is an ieee binary64 value in one word. `float` and `f64` name that type; `f32` rounds on store. integer operands are promoted when mixed with floats in arithmetic. `rt.put_n` prints integers. the helper `put_f` below calls the vm's float printer with encoded instruction `#1220`. division by zero gives `inf`; `0.0 / 0.0` gives `nan`. both `0.0` and `-0.0` are false in conditions. [example source](../examples/learn/06-floats.zc): ```zc namespace; include rt; void put_f(float x) { asm { x @ #1220 } } fn main() { let a = 1.5; put_f(a + 2.5); rt.put_c(10); } ``` output: ```text 4.000000 ``` ### byte addresses `rt.addr_b(p)` converts a word address to a byte address; `rt.addr_w(b)` converts back. `ld8`, `ld16`, and `ld32` load little-endian values; `st8`, `st16`, and `st32` store them. byte addresses name the same memory as word addresses. this example stores `0x11` in the low byte and `0x22` in the next byte. the resulting word is `0x2211`, or 8721. [example source](../examples/learn/07-bytes.zc): ```zc namespace; include rt; fn main() { let w = 0; rt.st8(rt.addr_b(&w), 0x11); rt.st8(rt.addr_b(&w) + 1, 0x22); rt.put_n(rt.ld8(rt.addr_b(&w))); rt.put_c(32); rt.put_n(w); rt.put_c(10); } ``` output: ```text 17 8721 ``` ### packed byte strings `"text"` is a word string of type `char*`: one character per word, followed by a zero word. `b"text"` is a packed byte string of type `bstr`: a length word, then eight characters per word and a terminating zero byte. write the `b` directly against the quote. for byte strings, `len` and indexing use the packed representation, and `rt.put_s` writes the packed bytes. indexing reads a byte and cannot be assigned through. `+` concatenates strings, `s[i:j]` makes a slice with an exclusive end, and comparisons order unsigned bytes. `rt.s_to_n` parses an integer; `rt.n_to_s` creates a byte string containing its decimal form. [example source](../examples/learn/08-byte-strings.zc): ```zc namespace; include rt; fn main() { let name = b"zc"; let hi = b"hello, " + name; rt.put_s(hi); rt.put_c(10); rt.put_n(len(hi)); rt.put_c(32); rt.put_n(rt.s_to_n(b"40") + 2); rt.put_c(10); } ``` output: ```text hello, zc 9 42 ``` ### inferred string types `let` preserves the distinct types of the two string literal forms: `"..."` infers `char*`, while `b"..."` infers `bstr`. the standard-library groups accept the matching type, so each length call below uses the correct representation. [example source](../examples/learn/28-string-inference.zc): ```zc namespace; include rt, std; fn main() { let word_text = "word string"; let packed_text = b"packed bytes"; rt.put_n(std.Text.len(word_text)); rt.put_c(32); rt.put_n(std.Bytes.len(packed_text)); rt.put_c(10); } ``` output: ```text 11 12 ``` ## ownership and the heap ### shared cells `rt.cons(a, b)` creates an immutable pair, read with `rt.car` and `rt.cdr`. `rt.triple` and `rt.tget` do the same for three values. several pointers may share a cell. `rt.share(p)` publishes an existing object, and `rt.is_shared(p)` reports whether it is shared. `rt.free` leaves shared cells intact. `rt.gc()` reclaims them once no root points to them, including cycles. `rt.scrub()` clears unused stack words that would otherwise look like roots. in this example, `left` and `right` share the same tail. [example source](../examples/learn/09-shared.zc): ```zc namespace; include rt; fn main() { let tail = rt.cons(1, 2); let left = rt.cons(tail, 3); let right = rt.cons(tail, 4); rt.put_n(rt.car(rt.car(left))); rt.put_c(32); rt.put_n(rt.car(left) == rt.car(right)); rt.put_c(10); } ``` output: ```text 1 1 ``` ### owned objects binding a fresh `rt.new` result to a local makes that local its owner. the analyzer inserts `rt.free` when the scope ends and reports an owned object that is never used. explicit `rt.free` calls are usually unnecessary for owned locals. `int*` is a pointer to an integer. `*p` accesses that integer. [example source](../examples/learn/10-owned.zc): ```zc namespace; include rt; fn main() { int* p = rt.new(int); *p = 7; rt.put_n(*p); rt.put_c(10); } ``` output: ```text 7 ``` ### moving ownership tags describe where an owned object goes: - `.m` keeps it in the current block. - `.d` transfers it to a callee or container; the previous owner cannot use it afterward. - `.u` transfers it through a return, block value, or assignment to an enclosing scope. assigning an owner to a global transfers it to the collector automatically; the local cannot be used afterward, and a borrowed reference or a child of a container is rejected. `m*p`, `d*p`, and `u*p` remain available for explicit low-level recasts. a plain parameter lends the object without transferring ownership; `.m` on a parameter adds a checked borrow. a `.d` parameter and `rt.free` consume an owned argument automatically, and returning an owned local transfers it to the caller without `u*`. a fresh object passed to a proved borrower, such as `rt.put_s(message())`, is held through the call and freed afterward. an owned result can be declared with `.u` or inferred from its return. [example source](../examples/learn/11-tags.zc): ```zc namespace; include rt; int*.u box(int v) { int* b = rt.new(int); *b = v; return b; } fn main() { int* p = box(4); rt.put_n(*p); rt.put_c(10); } ``` output: ```text 4 ``` ### aliases a pointer derived from an owner is an alias. pointer arithmetic, pointer fields, and pointer elements retain the relationship to that owner. an alias can be copied or passed to a call, but it does not free the object at scope end. using it after its owner is handed away is an error. in this example, `a` owns `b` after `a.next = b`, and `c` borrows the child. indexing an untyped word array is not tracked as a pointer operation. [example source](../examples/learn/12-aliases.zc): ```zc namespace; include rt; struct node { int v; node* next; }; fn main() { node* a = rt.new(node), b = rt.new(node); a.v = 3; b.v = 4; a.next = b; node* c = a.next; rt.put_n(c.v); rt.put_c(10); } ``` output: ```text 4 ``` ### runtime memory checks `unchecked fn` and `unchecked { }` disable lifetime analysis for their scope. allocations there rely on the collector. checked variables cannot be accessed from an unchecked block. `memdebug on;` adds runtime checks for heap loads and stores, use after free, and double free. `rt.memreport()` lists live allocations made while debugging was on. the allocation line below is the line containing `rt.new`; the reported size is the buddy block's payload capacity. `memdebug scrub;` also fills freed payloads with `0xDEAD`; `memdebug off;` disables debugging. ordinary `rt.free` quarantines an owned block until collection can safely reuse it. stale contents surviving quarantine are not permission to use a dead object. [example source](../examples/learn/13-memdebug.zc): ```zc namespace; include rt; memdebug on; int* g; fn main() { int* p = rt.new(int); *p = 1; g = d*p; rt.put_n(rt.memreport()); rt.put_c(10); } ``` output: ```text leak: line 6 1 words 1 ``` ## libraries and the host ### including a library `include std;` compiles the standard library at that point. it is part of the compiler, so no file is read, and it must appear before code that uses it. the library uses the `std` namespace, so call its helpers as `std.Text.itoa(...)` or `std.Host.now()`. each file has its own namespace and imports: an included file does not import its dependencies into the caller. `include rt, std;` imports both namespaces into the current file. `include "path";` reads and compiles another file the same way. the file is looked for next to the file that has the include (for a program on stdin, that step is skipped), then in the workspace folder when an editor gives one, then relative to the process's working directory. the library groups related helpers in structs and calls them as `std.Group.name(args)`. for example, `std.Text.itoa` writes a decimal integer into a caller-provided word-string buffer. [example source](../examples/learn/14-include.zc): ```zc namespace; include rt, std; fn main() { let s = rt.readfile("tests/data/hi.txt"); rt.put_n(rt.len_w(s)); rt.put_c(10); rt.free(s); } ``` output: ```text 6 ``` ### text and number helpers `std.Text` provides word-string parsing, formatting, copying, search, and case conversion. `std.Num` provides helpers such as `gcd`, `pow`, `isqrt`, and `clamp`. `std.Bytes` provides packed-string helpers. `std.Host`, `std.Arena`, and `std.Buf` cover host services, files, allocation arenas, and byte buffers. check the signatures in `src/lib/std.zc` before passing a buffer: many text functions write into storage supplied by the caller. [example source](../examples/learn/20-text-num.zc): ```zc namespace; include rt, std; fn main() { rt.put_n(std.Num.gcd(12, 18)); rt.put_c(32); rt.put_n(std.Num.pow(2, 10)); rt.put_c(32); rt.put_n(std.Num.isqrt(50)); rt.put_c(10); rt.put_n(std.Text.atoi("123") + 1); rt.put_c(10); } ``` output: ```text 6 1024 7 124 ``` ### terminal input and output `std.Io` groups the terminal: `print(s)` and `println(s)` write a string, `nl()` a line break, and `put_c`, `put_s`, `put_n`, `put_hex`, `put_bs` and `put_f` write a character, a string, a decimal or hexadecimal integer, a packed byte string and a float. `get_c()`, `eof()`, `read(buf, n)` and `getline(buf, n)` read the program input; `getline` drops the line break and returns -1 at the end of the input. `print` and `println` take any number of arguments of any printable type, such as `std.Io.println("total: ", n, " avg: ", x);`; the compiler picks the writer for each argument's type (string, byte string, integer or float), and a fresh owned string argument is freed after it is written. `println()` alone writes a line break. other functions have no varargs or overloading. the writers go through `rt.put_c`, so `std.capture` sees them, and the `rt` names (`rt.put_n` and the rest) keep working. [example source](../examples/learn/35-io.zc): ```zc namespace; include std; fn main() { std.Io.print("total: "); std.Io.put_n(6 * 7); std.Io.nl(); std.Io.print("ratio: "); std.Io.put_f(1.0 / 4.0); std.Io.nl(); std.Io.println("done"); } ``` output: ```text total: 42 ratio: 0.250000 done ``` ### host services `rt.syscall(op, a, b, c)` calls the runtime interface to the vm's syscall gate. the runtime defines names such as `SYS_OPEN`, `SYS_READ`, `SYS_WRITE`, `SYS_GETENV`, and `SYS_TIME`. unsupported operations return -1. `std.Host.now()` wraps the clock request. `std.Host.open(path, mode)` opens a file (0 to read, 1 to write) and returns an owned `std.File`, or null. `f.write(s)` and `f.read(buf, n)` go through its descriptor, and its drop closes it: the file is closed when its owner's block ends, when it is freed, or when the collector reclaims it, with no `close` to call. `std.Host.env(name, dst, n)` writes an environment value into a word-string buffer and returns its length, or -1 when it is unset. the library also supplies `std.argc`, `std.argv`, `std.envc`, and `std.envp`; a program read on stdin has `` as its first argument. gate calls must be made from ordinary code, outside an `o3` region. [example source](../examples/learn/21-host.zc): ```zc namespace; include rt, std; fn main() { rt.put_n(std.Host.now() > 0); rt.put_c(10); } ``` output: ```text 1 ``` ### raw linux syscalls `std.Host.raw` uses the kernel's own syscall number and argument convention. it returns the result or a negated errno. the syscall numbers differ by architecture: this example uses x86-64 linux `getpid`, number 39. this path bypasses the vm's syscall checks. pointer arguments must use the host addresses required by that syscall. the example does not take pointer arguments and is skipped on other hosts. [example source](../examples/learn/22-raw-x86.zc): ```zc namespace; include rt, std; fn main() { rt.put_n(std.Host.raw(39, 0, 0, 0, 0, 0, 0) > 0); rt.put_c(10); } ``` output: ```text 1 ``` ### input after the source a backtick outside a comment or literal ends the source unit. bytes after it are the program's input, read with `rt.get_c`. `rt.eof()` reports the end. this complete file includes both the program and the input `abc`; the program reads only the first two characters. [example source](../examples/learn/24-backtick.zc): ```zc namespace; include rt; fn main() { rt.put_c(rt.get_c()); rt.put_c(rt.get_c()); rt.put_c(10); } `abc ``` output: ```text ab ``` ### calling c `extern` declares a native function with its c types. `cfunc(f)` produces a native function pointer so c can call back into zc. use `word*` for a pointer to vm words, `char*` for the byte-string convention of a native c signature, and `double` for a zc float. this example calls `sqrt`, then calls `qsort` with a zc comparison callback. native calls require the full native core; the microcore and javascript runtime do not implement them. struct arguments have additional abi restrictions described in the reference. [example source](../examples/learn/23-c-abi.zc): ```zc namespace; include rt; void put_f(float x) { asm { x @ #1220 } } extern double sqrt(double); extern void qsort(word *base, long n, long size, void *cmp); int cmp(int* a, int* b) { return *a - *b; } fn main() { let a[4]; a[0] = 5; a[1] = 3; a[2] = 9; a[3] = 1; put_f(sqrt(2.0)); rt.put_c(10); qsort(a, 4, 8, cfunc(cmp)); rt.put_n(a[0]); rt.put_c(32); rt.put_n(a[1]); rt.put_c(32); rt.put_n(a[2]); rt.put_c(32); rt.put_n(a[3]); rt.put_c(10); } ``` output: ```text 1.414214 1 3 5 9 ``` ## types and containers ### inferred call types the compiler propagates type constraints through calls and returned values until they stop changing. a typed callee can determine a caller's parameter and return types, including through chains of untyped functions declared before that callee. recursive calls can use a typed callee as an anchor. when callers disagree on an argument type, the compiler leaves the shared parameter unresolved; an unknown reassignment also discards the previous initializer's type evidence. owned raw allocation results retain the `.u` return tag while their inferred storage type remains `word`. [example source](../examples/learn/32-inferred-calls.zc): ```zc namespace; include rt; fn layer0(n) { return seed(n); } fn layer1(n) { return layer0(n); } fn layer2(n) { return layer1(n); } int seed(int n) { return n; } fn countdown(n) { if (n == 0) return seed(7); return countdown(n - 1); } .u make_list(cap) { let list = rt.new(cap + 1); list[0] = cap; return list; } .u pass_list(n) { let list = make_list(n); return list; } fn main() { let list = pass_list(layer2(41)); rt.put_n(list[0]); rt.put_c(32); rt.put_n(countdown(3)); rt.put_c(10); } ``` output: ```text 41 7 ``` ### const fixed arrays `fixed const T[n]` and `const fixed T[n]` both declare read-only fixed arrays. the qualifier applies to the array cells and to aliases of the array. element assignment, increment, compound assignment, and rebinding a `const fixed T[?]` handle are rejected, even when `typecheck off;` is active. a read-only array reference cannot be converted to a mutable reference. a formal parameter declared `const fixed T[n]` accepts either a read-only or mutable array. copying an element into an ordinary numeric variable remains allowed. [example source](../examples/learn/33-const-fixed.zc): ```zc namespace; include rt; int first(const fixed int[2] values) { return values[0]; } fn main() { const fixed int[2] a = {7, 8}; fixed const int[2] b = {2, 3}; fixed int[2] c = {5, 6}; int copy = a[0]; copy++; rt.put_n(first(a)); rt.put_c(32); rt.put_n(first(b)); rt.put_c(32); rt.put_n(first(c)); rt.put_c(32); rt.put_n(copy); rt.put_c(10); } ``` output: ```text 7 2 5 8 ``` ### arrays and methods `T[n]` is an array stored in place. `T[]` infers a fixed length from its initializer. `int[] a = {1, [5] = 9, 7};` has seven elements: the unspecified elements are zero, index 5 is 9, and index 6 is 7. a trailing comma is allowed. `int[2][3]` has two rows of three elements. character and byte arrays also accept string initializers, with an inferred bound including the terminating zero. nested arrays accept nested initializer lists or flat lists with braces omitted. designators can chain indices, as in `[1][0] = 9`, and later entries resume after that element. repeated designators overwrite the earlier value. `(int[]){1, 2, 3}` is a compound literal that can be passed to a function taking `int*` or `int[]`; the storage belongs to the surrounding scope. bare `{1, 2, 3}` in an expression infers an integer array. `T[?]` is a growable handle that owns its items, created with `rt.new(T[?])` or `rt.new(T[?], n)`. `push`, `pop`, `resize`, `len`, and `cap` also have receiver forms such as `a.push(x)` and `a.len()`. `fixed T[n]` and `fixed T[?]` support `len()` and `cap()` but not the mutators `push()`, `pop()`, and `resize()`. `include std;` registers read-only receiver algorithms for growable arrays and mutable fixed arrays. support for const-qualified arrays has not been verified. `find(value)` and `find_if(predicate)` return the first matching index or -1; `contains(value)`, `any(predicate)`, and `all(predicate)` return 1 or 0; `count(predicate)` returns the number of matches. `any` is false and `all` is true for an empty input. `map(transform)` and `filter(predicate)` return new growable handles, and `reduce(initial, combine)` folds from left to right, returning `initial` for an empty input. callback parameter types follow the element type, `map` infers its result type from the callback, `filter` preserves the element type, and `reduce` uses the initial value's type for its accumulator. algorithms snapshot the input length and visit indices in order; callbacks should not change that input's length. map and filter produce independent handles but shallow-share reference elements with the collector. aggregate-by-value elements are not supported by `push`, `pop`, `map`, or `filter`; use pointers to records instead. struct methods receive a hidden `this` parameter: `p.get()` passes `p`. static functions belong to the struct and are called as `cnt.make()`. freeing a container also frees its owned children. [example source](../examples/learn/19-growable.zc): ```zc namespace; include rt; struct cnt { int n; static int make() { return 3; } int get() { return this.n; } }; fn main() { int[?] xs = rt.new(int[?]); xs.push(4); xs.push(5); rt.put_n(xs.len()); rt.put_c(32); rt.put_n(xs.cap()); rt.put_c(10); rt.put_n(xs.pop()); rt.put_c(10); xs.resize(4); rt.put_n(xs.len()); rt.put_c(10); int[3] fixed = {1, 2, 3}; rt.put_n(fixed.len()); rt.put_c(32); rt.put_n(fixed.cap()); rt.put_c(10); rt.put_n(cnt.make()); rt.put_c(10); } ``` output: ```text 2 4 5 4 3 3 3 ``` the standard-library methods also accept mutable fixed arrays as read-only inputs. const-qualified array support has not been verified. `map` infers its callback parameter from the element type, `filter` preserves that type, and `reduce` combines elements in index order. [example source](../examples/learn/31-array-algorithms.zc): ```zc namespace; include rt, std; fn main() { int[?] values = {1, 2, 3, 4, 5}; let doubled = values.map(fn(e) { return e * 2; }); let large = doubled.filter(fn(e) { return e > 4; }); rt.put_n(values.find(3)); rt.put_c(32); rt.put_n(values.find_if(fn(e) { return e > 4; })); rt.put_c(32); rt.put_n(large.reduce(0, fn(total, e) { return total + e; })); rt.put_c(10); } ``` output: ```text 2 4 24 ``` ### generic functions and structs type parameters follow the name: `record` and `keyof`. the compiler creates a distinct function instance for each set of concrete types. type arguments can be supplied explicitly or inferred from the call. `record` stores its `point` field in place. inside an instantiated function, `sizeof` uses the concrete type. a generic body is checked when it is instantiated, so an unused template can contain a mistake that has not been reported yet. [example source](../examples/learn/15-generics.zc): ```zc namespace; include rt; struct record { K key; V val; }; struct point { int x; int y; }; K keyof(record* r) { return r.key; } fn main() { record* r = rt.new(record); r.key = 9; rt.put_n(keyof(r)); rt.put_c(10); } ``` output: ```text 9 ``` ### tagged enums and matching an enum member can carry fields written as a block. a constructor such as `shape.circle(3)` returns an owned `shape*`. word 0 holds the member tag; the remaining words hold its payload. `case circle(r):` matches the tag and binds `r` only in that arm. a switch that misses a member without a `default` produces a warning. release or return the value according to the ordinary ownership rules. [example source](../examples/learn/16-tagged-enums.zc): ```zc namespace; include rt; enum shape { circle { int r; }, rect { int w; int h; }, dot }; int area(shape* s) { switch (s) { case circle(r): return r * r * 3; case rect(w, h): return w * h; case dot: return 0; } return -1; } fn main() { shape* s = shape.circle(3); rt.put_n(area(s)); rt.put_c(10); rt.free(s); shape* t = shape.rect(2, 5); rt.put_n(area(t)); rt.put_c(10); rt.free(t); } ``` output: ```text 27 10 ``` ## optimized regions ### requesting native optimization `o3` can mark a function, block, or variable. the compiler emits a region for the full core to optimize and compile into native code. the microcore and javascript runtime execute the ordinary bytecode fallback. inside a region, locals and parameters must be typed. promoted variables have no address, so `&x` is not allowed for one. gate calls and closures are restricted. an untyped `rt.new(n)` is zeroed without a type stamp. the fixed array below keeps its length, while the optimized block counts 143 matching integers. [example source](../examples/learn/18-o3.zc): ```zc namespace; include rt; o3 int sum(fixed int[?] a) { int s = 0; for (int i = 0; i < len(a); i++) s += a[i]; return s; } fn main() { fixed int[?] a = rt.new(fixed int[?], 5); for (int i = 0; i < 5; i++) a[i] = i * i; rt.put_n(sum(a)); rt.put_c(10); o3 int hits = 0; o3 { for (int i = 0; i < 1000; i++) if (i % 7 == 3) hits++; } rt.put_n(hits); rt.put_c(10); } ``` output: ```text 30 143 ``` ## closures ### shared captures `fn(x) { ... }` in an expression creates an owned closure. it shares the variables it captures with the surrounding function and other closures. calls use ordinary function syntax. write `fn:type(args)` to declare the result type, as in `fn:float(int x) { return x; }`; each `return` is converted and checked against it, and hovers show the declared result instead of an inferred one. captured locals and parameters use shared cells. a counter closure and its enclosing function see the same updates, and an escaping closure keeps those cells alive. a fresh closure can be passed to a checked borrowing parameter directly. returning an owned closure transfers it to the caller; its owned result can be inferred or declared as `.u`. [example source](../examples/learn/25-closures.zc): ```zc namespace; include rt; fn apply(f, x) { return f(x); } fn main() { let k = 10; let add = fn(x) { return x + k; }; rt.put_n(add(1)); rt.put_c(10); rt.put_n(apply(add, 5)); rt.put_c(10); } ``` output: ```text 11 15 ``` ### nested captures and mutable state a nested closure can capture a variable from more than one enclosing function. each enclosing closure carries the shared cells needed by the next one. captured variables use shared storage. `tick` keeps its counter between calls, and `count` in `main` sees those changes. sibling and nested closures see the same updates; an escaping closure keeps its cells alive. [example source](../examples/learn/26-closure-state.zc): ```zc namespace; include rt; fn main() { let base = 2; let outer = fn(x) { let inner = fn(y) { return y + base + x; }; return inner(100); }; rt.put_n(outer(7)); rt.put_c(10); let count = 0; let tick = fn(d) { count = count + d; return count; }; rt.put_n(tick(1)); rt.put_c(32); rt.put_n(tick(4)); rt.put_c(10); rt.put_n(count); rt.put_c(10); } ``` output: ```text 109 1 5 5 ``` ### typed closures and returned ownership `fn:int(int n) { ... }` declares the closure's result and parameter types. the returned closure is an owned heap object; `word.u` marks the factory result as transferring that object to its caller. its captured `factor` remains available after the factory returns. [example source](../examples/learn/30-typed-closure.zc): ```zc namespace; include rt; word.u multiplier(int factor) { let scale = fn:int(int n) { return n * factor; }; return u*scale; } fn main() { let scale = multiplier(3); rt.put_n(scale(14)); rt.put_c(10); } ``` output: ```text 42 ``` ## editor hints the language server reports informational hints for likely mistakes. by default, `hints on;` shows blue informational squiggles. `hints small;` keeps the same hints but displays compact dots; it does not reduce their number or kinds. `hints off;` suppresses hints after the directive. each setting applies from its source position. an inline block comment immediately before a numeric literal marks that literal as intentional and suppresses its magic-number hint. a comment before another operand does not suppress the hint. block comments are for this kind of place, between code on one line; a comment on lines of its own is written with `//`. the `//` comment immediately above a declaration is its hover documentation. [example source](../examples/learn/29-block-comments.zc): ```zc namespace; include rt; // adds a memory offset to an address. int offset_address(int address) { return address + /* offset in memory */ 0x40ff; } fn main() { rt.put_n(offset_address(1)); rt.put_c(10); } ``` output: ```text 16640 ``` ## hold blocks actors share nothing by default: each one runs on its own copy of the program's globals and its own heap, so a global an actor changes is changed only for that actor. state that every actor sees is declared `shared` inside a top-level `hold` block, a monitor. only the functions written in the block can use its shared variables, and each call to one of them holds the block's lock for the whole call, so everything such a function does is atomic. a call from one of the block's functions to another doesn't lock again. using a shared variable anywhere else is an error, and so is using it in a closure, which could run after the lock is released. shared variables hold numbers or arrays of numbers; a pointer would point into one actor's heap. when a program has several hold blocks, a function of one block may call into a block declared after it, but not back into an earlier one while it holds a later one: that order rules out deadlocks between blocks, and breaking it stops the program. [example source](../examples/learn/36-hold.zc): ```zc namespace; include rt, actor; typecheck on; // one counter for every actor. only the functions written in the block can touch it, // and each call holds the block's lock, so a read and the write after it can't interleave. hold { shared let hits = 0; fn hit() { let seen = hits; hits = seen + 1; return seen; } fn total() { return hits; } } int work(int n) { let i = 0; while (i < n) { hit(); i++; } return n; } fn main() { actor.Actor* a = rt.new(actor.Actor), b = rt.new(actor.Actor); a.spawn(work); b.spawn(work); a.send(1000); b.send(1000); a.recv(fn(int n) { return 0; }); b.recv(fn(int n) { return 0; }); a.await(); b.await(); rt.put_n(total()); rt.put_c(10); a.close(); b.close(); return 0; } ``` output: ```text 2000 ``` ## the optional typechecker `typecheck on;` enables type checking for the declarations that follow; `typecheck off;` disables it. errors report the expected and actual types. the lifetime analyzer runs independently of this switch. a literal 0 can be a null pointer, `word` matches any type, and arrays can decay to pointers. ## further reading [README.md](../README.md) is the full reference. `tests/` contains runnable feature checks, `tests/errors/` contains rejected lifetime examples, and `tests/typeerrors/` contains rejected type examples. `docs/design/` explains implementation decisions; `docs/history/` records earlier versions. use `make check` while editing and `make test` for the full available suite. enable `ZYTRACE=1` to inspect native translation, or `ZYHOT=1` on an llvm build to compile each block as early as possible. `//:` marks an implementation comment that never becomes hover documentation. use `/* ... */` or ordinary comments for declaration descriptions, and `//:` for section overviews and internal notes.