aboutsummaryrefslogtreecommitdiff
path: root/docs/LEARN.md
blob: fd399e379fbad99dd16b19e7123a902b074f07ba (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
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
# 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. 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
```

### 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.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
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<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 = rt.new(record<int, point>);
	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
```

## 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.