aboutsummaryrefslogtreecommitdiff
path: root/docs/LEARN.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/LEARN.md')
-rw-r--r--docs/LEARN.md718
1 files changed, 718 insertions, 0 deletions
diff --git a/docs/LEARN.md b/docs/LEARN.md
new file mode 100644
index 0000000..676358a
--- /dev/null
+++ b/docs/LEARN.md
@@ -0,0 +1,718 @@
+# 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
+bin/zy < examples/learn/01-hello.zc
+```
+
+`bin/zy` is the native core: it uses the arm64 jit where available and otherwise interprets. `bin/zym` is the portable microcore, `bin/zc0` is the bootstrap compiler, and `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 `include` and `readfile` are resolved from the process's working directory. `tests/drivers/learn.py` checks the examples and verifies that the code blocks here match their source files:
+
+```sh
+python3 tests/drivers/learn.py bin/zy
+```
+
+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)
+- [optimized regions](#optimized-regions)
+- [closures](#closures)
+- [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. `halt()` stops the machine.
+
+`put_s(s)` prints a string, `put_n(n)` prints an integer, and `put_c(c)` prints a character code. `put_c(10)` prints a newline.
+
+[example source](../examples/learn/01-hello.zc):
+
+```zc
+fn main() {
+ put_s("hello, world!\n");
+ return 0;
+}
+```
+
+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. 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
+fn main() {
+ let i = 0, n = 0;
+ while (i < 4) { n = n + i; i++; }
+ put_n(n); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+6
+```
+
+### 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
+fn main() {
+ int n = 0, i = 0;
+ do { i++; if (i == 2) continue; n = n + i; } while (i < 5);
+ put_n(n); put_c(10);
+ return 0;
+}
+```
+
+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
+fn main() {
+ int k = 2;
+ switch (k) { case 1: put_s("one"); break; case 2: put_s("two"); break; default: put_s("other"); }
+ put_c(10);
+ return 0;
+}
+```
+
+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
+enum color { RED, GREEN = 5, BLUE };
+fn main() {
+ put_n(RED); put_c(32); put_n(GREEN); put_c(32); put_n(BLUE); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+0 5 6
+```
+
+## 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 `[100]u8` 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
+fn main() {
+ u8 a = 250; a = a + 10;
+ put_n(a); put_c(10);
+ put_n(sizeof([100]u8)); put_c(10);
+ u8* px = new(u8, 4096);
+ px[10] = 300;
+ put_n(px[10]); put_c(10);
+ return 0;
+}
+```
+
+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.
+
+`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
+fn put_f(x) { asm { x @ #1220 } return 0; }
+fn main() {
+ let a = 1.5;
+ put_f(a + 2.5); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+4.000000
+```
+
+### byte addresses
+
+`addr_b(p)` converts a word address to a byte address; `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
+fn main() {
+ let w = 0;
+ st8(addr_b(&w), 0x11);
+ st8(addr_b(&w) + 1, 0x22);
+ put_n(ld8(addr_b(&w))); put_c(32); put_n(w); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+17 8721
+```
+
+### packed byte strings
+
+`"text"` is a word string: one character per word, followed by a zero word. `b"text"` is a packed byte string: a length word, then eight characters per word and a terminating zero byte. write the `b` directly against the quote.
+
+for byte strings, `len`, indexing, and `put_s` use the packed representation. 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. `s_to_n` parses an integer; `n_to_s` creates a byte string containing its decimal form.
+
+[example source](../examples/learn/08-byte-strings.zc):
+
+```zc
+fn main() {
+ let name = b"zc";
+ let hi = b"hello, " + name;
+ put_s(hi); put_c(10);
+ put_n(len(hi)); put_c(32);
+ put_n(s_to_n(b"40") + 2); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+hello, zc
+9 42
+```
+
+## ownership and the heap
+
+### shared cells
+
+`cons(a, b)` creates an immutable pair, read with `car` and `cdr`. `triple` and `tget` do the same for three values. several pointers may share a cell. `share(p)` publishes an existing object, and `is_shared(p)` reports whether it is shared.
+
+`free` leaves shared cells intact. `gc()` reclaims them once no root points to them, including cycles. `scrub()` clears unused stack words that would otherwise look like roots. here, `left` and `right` share the same tail.
+
+[example source](../examples/learn/09-shared.zc):
+
+```zc
+fn main() {
+ let tail = cons(1, 2);
+ let left = cons(tail, 3);
+ let right = cons(tail, 4);
+ put_n(car(car(left))); put_c(32);
+ put_n(car(left) == car(right)); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+1 1
+```
+
+### owned objects
+
+binding a fresh `new` result to a local makes that local its owner. the analyzer inserts a free when the scope ends and reports an owned object that is never used. you normally do not write the free yourself.
+
+`int*` is a pointer to an integer. `*p` accesses that integer.
+
+[example source](../examples/learn/10-owned.zc):
+
+```zc
+fn main() {
+ int* p = new(int);
+ *p = 7;
+ put_n(*p); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+7
+```
+
+### moving ownership
+
+tags describe where an owned object goes:
+
+- `.m` keeps it in the current block.
+- `.d` hands it down to a callee or container; the previous owner cannot use it afterward.
+- `.u` hands it up through a return, block value, or assignment to an enclosing scope.
+
+`m*p`, `d*p`, and `u*p` recast the tag. a plain parameter lends the object without transferring ownership; `.m` on a parameter adds a checked borrow. `free(d*p)` hands the object to `free`. an owned result can be declared with `.u` or inferred from its return.
+
+[example source](../examples/learn/11-tags.zc):
+
+```zc
+int*.u box(int v) { int* b = new(int); *b = v; return u*b; }
+fn main() {
+ int* p = box(4);
+ put_n(*p); put_c(10);
+ return 0;
+}
+```
+
+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
+struct node { int v; node* next; };
+fn main() {
+ node* a = new(node), b = new(node);
+ a.v = 3; b.v = 4; a.next = b;
+ node* c = a.next;
+ put_n(c.v); put_c(10);
+ return 0;
+}
+```
+
+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. `memreport()` lists live allocations made while debugging was on. the allocation line below is the line containing `new`; the reported size is the buddy block's payload capacity.
+
+`memdebug scrub;` also fills freed payloads with `0xDEAD`; `memdebug off;` disables debugging. ordinary `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
+memdebug on;
+int* g;
+fn main() {
+ int* p = new(int);
+ *p = 1;
+ g = d*p;
+ put_n(memreport());
+ put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+leak: line 4 3 words
+1
+```
+
+## libraries and the host
+
+### including a library
+
+`include "src/lib/std.zc";` reads and compiles that file at the include. place it before code that uses the library. the path is relative to the process's working directory.
+
+the library groups related helpers in structs and calls them as `Group.name(args)`. for example, `Text.itoa` writes a decimal integer into a caller-provided word-string buffer.
+
+[example source](../examples/learn/14-include.zc):
+
+```zc
+include "src/lib/std.zc";
+fn main() {
+ let s = readfile("tests/data/hi.txt");
+ put_n(len_w(s)); put_c(10);
+ free(d*s);
+ return 0;
+}
+```
+
+output:
+
+```text
+6
+```
+
+### text and number helpers
+
+`Text` provides word-string parsing, formatting, copying, search, and case conversion. `Num` provides helpers such as `gcd`, `pow`, `isqrt`, and `clamp`. `Bytes` provides packed-string helpers. `Host`, `File`, `Arena`, and `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
+include "src/lib/std.zc";
+fn main() {
+ put_n(Num.gcd(12, 18)); put_c(32); put_n(Num.pow(2, 10)); put_c(32); put_n(Num.isqrt(50)); put_c(10);
+ put_n(Text.atoi("123") + 1); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+6 1024 7
+124
+```
+
+### host services
+
+`syscall(op, a, b, c)` passes a request through 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. `Host.now()` wraps the clock request.
+
+`getenvs(name)` returns a fresh word string or 0. the library also supplies `argc`, `argv`, `envc`, and `envp`; a program read on stdin has `<eval>` as its first argument. gate calls must be made from ordinary code, outside an `o3` region.
+
+[example source](../examples/learn/21-host.zc):
+
+```zc
+include "src/lib/std.zc";
+fn main() {
+ put_n(Host.now() > 0); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+1
+```
+
+### raw linux syscalls
+
+`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
+include "src/lib/std.zc";
+fn main() {
+ put_n(Host.raw(39, 0, 0, 0, 0, 0, 0) > 0); put_c(10);
+ return 0;
+}
+```
+
+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 `get_c`. `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
+fn main() {
+ put_c(get_c()); put_c(get_c()); put_c(10);
+ return 0;
+}
+`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
+fn put_f(x) { asm { x @ #1220 } return 0; }
+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)); put_c(10);
+ qsort(a, 4, 8, cfunc(cmp));
+ put_n(a[0]); put_c(32); put_n(a[1]); put_c(32); put_n(a[2]); put_c(32); put_n(a[3]); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+1.414214
+1 3 5 9
+```
+
+## types and containers
+
+### arrays and methods
+
+`[n]T` is an array stored in place. `[]T` is a growable handle that owns its items, created with `new([]T)` or `new([]T, n)`. `push`, `pop`, `resize`, `len`, and `cap` operate on it. `fixed []T` has a length that cannot change.
+
+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
+struct cnt {
+ int n;
+ static int make() { return 3; }
+ int get() { return this.n; }
+};
+fn main() {
+ []int xs = new([]int);
+ push(xs, 4); push(xs, 5);
+ put_n(len(xs)); put_c(10);
+ put_n(pop(xs)); put_c(10);
+ put_n(cnt.make()); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+2
+5
+3
+```
+
+### generic functions and structs
+
+type parameters follow the name: `record<K, V>` and `keyof<K, V>`. the compiler creates a distinct function instance for each set of concrete types. you can supply type arguments explicitly or infer them from the call.
+
+`record<int, point>` 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
+struct record<K, V> { K key; V val; };
+struct point { int x; int y; };
+K keyof<K, V>(record<K, V>* r) { return r.key; }
+fn main() {
+ record<int, point>* r = new(record<int, point>);
+ r.key = 9;
+ put_n(keyof(r)); put_c(10);
+ return 0;
+}
+```
+
+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
+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);
+ put_n(area(s)); put_c(10);
+ free(d*s);
+ shape* t = shape.rect(2, 5);
+ put_n(area(t)); put_c(10);
+ free(d*t);
+ return 0;
+}
+```
+
+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 `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
+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 = new(fixed []int, 5);
+ for (int i = 0; i < 5; i++) a[i] = i * i;
+ put_n(sum(a)); put_c(10);
+ o3 int hits = 0;
+ o3 { for (int i = 0; i < 1000; i++) if (i % 7 == 3) hits++; }
+ put_n(hits); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+30
+143
+```
+
+## closures
+
+### capturing a value
+
+`fn(x) { ... }` in an expression creates an owned closure. it copies the variables it uses from the surrounding function when it is created. calls use ordinary function syntax.
+
+bind a closure to a local before lending it to a plain parameter. to return an owned closure, use an owned return such as `return u*f;`; the result tag can be inferred or declared as `.u`.
+
+[example source](../examples/learn/25-closures.zc):
+
+```zc
+fn apply(f, x) { return f(x); }
+fn main() {
+ let k = 10;
+ let add = fn(x) { return x + k; };
+ put_n(add(1)); put_c(10);
+ put_n(apply(add, 5)); put_c(10);
+ return 0;
+}
+```
+
+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 values needed by the next one.
+
+captures are by value, but a closure can change its own copy. `tick` keeps its counter between calls; the `count` in `main` stays 0. changing closure state does not write back into the function that made it.
+
+[example source](../examples/learn/26-closure-state.zc):
+
+```zc
+fn main() {
+ let base = 2;
+ let outer = fn(x) {
+ let inner = fn(y) { return y + base + x; };
+ return inner(100);
+ };
+ put_n(outer(7)); put_c(10);
+ let count = 0;
+ let tick = fn(d) { count = count + d; return count; };
+ put_n(tick(1)); put_c(32); put_n(tick(4)); put_c(10);
+ put_n(count); put_c(10);
+ return 0;
+}
+```
+
+output:
+
+```text
+109
+1 5
+0
+```
+
+## 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.