aboutsummaryrefslogtreecommitdiff
path: root/docs/LEARN.md
blob: 676358a522fa87355d2772de9b54582ff4e23627 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
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.