aboutsummaryrefslogtreecommitdiff
path: root/docs/LEARN.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/LEARN.md')
-rw-r--r--docs/LEARN.md490
1 files changed, 362 insertions, 128 deletions
diff --git a/docs/LEARN.md b/docs/LEARN.md
index 676358a..8b0e1e6 100644
--- a/docs/LEARN.md
+++ b/docs/LEARN.md
@@ -1,6 +1,6 @@
# 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.
+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
@@ -13,12 +13,14 @@ 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:
+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 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
@@ -28,24 +30,28 @@ native c calls need the full native core. the raw syscall example needs linux on
- [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. `halt()` stops the machine.
+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.
-`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.
+`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() {
- put_s("hello, world!\n");
- return 0;
+ rt.put_s("hello, world!\n");
}
```
@@ -64,11 +70,12 @@ hello, world!
[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++; }
- put_n(n); put_c(10);
- return 0;
+ rt.put_n(n); rt.put_c(10);
}
```
@@ -78,6 +85,28 @@ output:
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.
@@ -85,11 +114,12 @@ output:
[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);
- put_n(n); put_c(10);
- return 0;
+ rt.put_n(n); rt.put_c(10);
}
```
@@ -106,11 +136,12 @@ output:
[example source](../examples/learn/17-switch.zc):
```zc
+namespace;
+include rt;
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;
+ 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);
}
```
@@ -129,10 +160,11 @@ with the typechecker enabled, `color` is its own type: members have that type, a
[example source](../examples/learn/04-enums.zc):
```zc
+namespace;
+include rt;
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;
+ rt.put_n(RED); rt.put_c(32); rt.put_n(GREEN); rt.put_c(32); rt.put_n(BLUE); rt.put_c(10);
}
```
@@ -148,19 +180,20 @@ output:
`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.
+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;
- put_n(a); put_c(10);
- put_n(sizeof([100]u8)); put_c(10);
- u8* px = new(u8, 4096);
+ 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;
- put_n(px[10]); put_c(10);
- return 0;
+ rt.put_n(px[10]); rt.put_c(10);
}
```
@@ -176,16 +209,17 @@ output:
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.
+`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
-fn put_f(x) { asm { x @ #1220 } return 0; }
+namespace;
+include rt;
+void put_f(float x) { asm { x @ #1220 } }
fn main() {
let a = 1.5;
- put_f(a + 2.5); put_c(10);
- return 0;
+ put_f(a + 2.5); rt.put_c(10);
}
```
@@ -197,19 +231,20 @@ output:
### 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.
+`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;
- 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;
+ 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);
}
```
@@ -221,20 +256,21 @@ output:
### 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.
+`"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`, 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.
+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;
- 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;
+ 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);
}
```
@@ -245,24 +281,48 @@ 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
-`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.
+`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.
-`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.
+`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 = 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;
+ 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);
}
```
@@ -274,18 +334,19 @@ output:
### 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.
+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 = new(int);
+ int* p = rt.new(int);
*p = 7;
- put_n(*p); put_c(10);
- return 0;
+ rt.put_n(*p); rt.put_c(10);
}
```
@@ -300,19 +361,22 @@ output:
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.
+- `.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.
-`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.
+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
-int*.u box(int v) { int* b = new(int); *b = v; return u*b; }
+namespace;
+include rt;
+int*.u box(int v) { int* b = rt.new(int); *b = v; return b; }
fn main() {
int* p = box(4);
- put_n(*p); put_c(10);
- return 0;
+ rt.put_n(*p); rt.put_c(10);
}
```
@@ -331,13 +395,14 @@ in this example, `a` owns `b` after `a.next = b`, and `c` borrows the child. ind
[example source](../examples/learn/12-aliases.zc):
```zc
+namespace;
+include rt;
struct node { int v; node* next; };
fn main() {
- node* a = new(node), b = new(node);
+ node* a = rt.new(node), b = rt.new(node);
a.v = 3; b.v = 4; a.next = b;
node* c = a.next;
- put_n(c.v); put_c(10);
- return 0;
+ rt.put_n(c.v); rt.put_c(10);
}
```
@@ -351,29 +416,30 @@ output:
`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 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 `free` quarantines an owned block until collection can safely reuse it. stale contents surviving quarantine are not permission to use a dead object.
+`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 = new(int);
+ int* p = rt.new(int);
*p = 1;
g = d*p;
- put_n(memreport());
- put_c(10);
- return 0;
+ rt.put_n(rt.memreport());
+ rt.put_c(10);
}
```
output:
```text
-leak: line 4 3 words
+leak: line 6 3 words
1
```
@@ -381,19 +447,19 @@ leak: line 4 3 words
### 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.
+`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 `Group.name(args)`. for example, `Text.itoa` writes a decimal integer into a caller-provided word-string buffer.
+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
-include "src/lib/std.zc";
+namespace;
+include rt, std;
fn main() {
- let s = readfile("tests/data/hi.txt");
- put_n(len_w(s)); put_c(10);
- free(d*s);
- return 0;
+ let s = rt.readfile("tests/data/hi.txt");
+ rt.put_n(rt.len_w(s)); rt.put_c(10);
+ rt.free(s);
}
```
@@ -405,18 +471,18 @@ output:
### 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.
+`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
-include "src/lib/std.zc";
+namespace;
+include rt, std;
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;
+ 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);
}
```
@@ -429,17 +495,17 @@ output:
### 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.
+`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.
-`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.
+`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 `<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";
+namespace;
+include rt, std;
fn main() {
- put_n(Host.now() > 0); put_c(10);
- return 0;
+ rt.put_n(std.Host.now() > 0); rt.put_c(10);
}
```
@@ -451,17 +517,17 @@ output:
### 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.
+`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
-include "src/lib/std.zc";
+namespace;
+include rt, std;
fn main() {
- put_n(Host.raw(39, 0, 0, 0, 0, 0, 0) > 0); put_c(10);
- return 0;
+ rt.put_n(std.Host.raw(39, 0, 0, 0, 0, 0, 0) > 0); rt.put_c(10);
}
```
@@ -473,14 +539,15 @@ output:
### 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.
+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() {
- put_c(get_c()); put_c(get_c()); put_c(10);
- return 0;
+ rt.put_c(rt.get_c()); rt.put_c(rt.get_c()); rt.put_c(10);
}
`abc
```
@@ -500,17 +567,18 @@ this example calls `sqrt`, then calls `qsort` with a zc comparison callback. nat
[example source](../examples/learn/23-c-abi.zc):
```zc
-fn put_f(x) { asm { x @ #1220 } return 0; }
+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)); put_c(10);
+ put_f(sqrt(2.0)); rt.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;
+ 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);
}
```
@@ -523,55 +591,158 @@ output:
## 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
-`[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.
+`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 = 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;
+ 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
+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<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.
+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. type arguments can be supplied explicitly or inferred 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
+namespace;
+include rt;
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>);
+ record<int, point>* r = rt.new(record<int, point>);
r.key = 9;
- put_n(keyof(r)); put_c(10);
- return 0;
+ rt.put_n(keyof(r)); rt.put_c(10);
}
```
@@ -590,6 +761,8 @@ an enum member can carry fields written as a block. a constructor such as `shape
[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) {
@@ -601,12 +774,11 @@ int area(shape* s) {
}
fn main() {
shape* s = shape.circle(3);
- put_n(area(s)); put_c(10);
- free(d*s);
+ rt.put_n(area(s)); rt.put_c(10);
+ rt.free(s);
shape* t = shape.rect(2, 5);
- put_n(area(t)); put_c(10);
- free(d*t);
- return 0;
+ rt.put_n(area(t)); rt.put_c(10);
+ rt.free(t);
}
```
@@ -623,20 +795,21 @@ output:
`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.
+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
-o3 int sum(fixed []int a) { int s = 0; for (int i = 0; i < len(a); i++) s += a[i]; return s; }
+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 = new(fixed []int, 5);
+ fixed int[?] a = rt.new(fixed int[?], 5);
for (int i = 0; i < 5; i++) a[i] = i * i;
- put_n(sum(a)); put_c(10);
+ 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++; }
- put_n(hits); put_c(10);
- return 0;
+ rt.put_n(hits); rt.put_c(10);
}
```
@@ -649,22 +822,25 @@ output:
## closures
-### capturing a value
+### shared captures
-`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.
+`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.
-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`.
+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; };
- put_n(add(1)); put_c(10);
- put_n(apply(add, 5)); put_c(10);
- return 0;
+ rt.put_n(add(1)); rt.put_c(10);
+ rt.put_n(apply(add, 5)); rt.put_c(10);
}
```
@@ -677,25 +853,26 @@ output:
### 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.
+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.
-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.
+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);
};
- put_n(outer(7)); put_c(10);
+ rt.put_n(outer(7)); rt.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;
+ 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);
}
```
@@ -704,7 +881,62 @@ output:
```text
109
1 5
-0
+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. a block comment immediately above a declaration provides hover documentation; `//:` marks an implementation note instead.
+
+[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
```
## the optional typechecker
@@ -716,3 +948,5 @@ output:
[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.