aboutsummaryrefslogtreecommitdiff
path: root/docs/history/17-o3-boundaries/notes.md
blob: d2f27b464527c33a99d3fe79640a2961eb571c93 (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
# generics after the name, leaving o3, registers by scope, cached fixed arrays

code/ next to this file is the tree at the end of this stage.

## message 17

> generics should go after fn name and before parens
> we also need some machinery for converting values to/from o3. to is pretty easy, we just hand over the data, and o3 ignores the fat metadata. from is a bit harder, there needs to be a system to get type hints and fatten the metadata for the packed data
> variables just get the latest free register, it's fine for now
> add the fixed array once per region thing
>
> also if you have any tips on how to make the memory system better (aka avoiding gc and leaks/uaf as much as possible) do tell

## working notes

**generics after the name.** the reason they had moved in front last stage was that `T first<T>(...)` uses T before declaring it. the fix: while a top-level return type is read, an unknown name becomes a provisional type parameter (numbered by first use), and the `<...>` after the name renumbers them with subst. a provisional one that the list doesn't confirm, or a global whose type used one, is "T: isn't a type". the prescan learned that `NAME<...>(` is a function too: at depth 0 it skips a balanced `<...>` and checks for `(`, which also steps over `list<T>` in return types harmlessly.

**registers by scope.** the compiler keeps a free list of the 7 registers per region. a declaration takes the one freed last, or the next unused one; block_close gives back the registers of its scope's o3 locals. so the descriptor became (slot, register) pairs, several slots can share a register, and the core spills a register to every slot it stands for (harmless, the dead ones are never read again) and loads it from the first. the bits live in ltag (8..10), next to the o3 flag (16) and the tag (low 4).

**cached fixed arrays.** two hidden o3 locals per `fixed []T` variable (`#items`, `#len`, names a program can't write), created when the variable is declared in o3 code, or at the start of the region for parameters and for o3 variables from outside an o3 block. refreshed after every assignment to the array variable in o3 code and at region start. indexing compares against the len variable and adds to the items variable. careful with o3 function parameters: their caches can't be created in the parameter loop, since the prologue stores parameter i to slot i and a hidden local there would shift them; they're made after the `jmp lo`, inside the region.

**fattening.** "packed data" here is o3's new_raw object: header, no type, no zeroing. the hint that survives is the size asked for, kept as -(n + 1) in the type word (typeof_obj reads that as 0). everything else comes from the compiler's static types at the boundary. the boundaries picked: stores of pointer-typed values anywhere but an o3 local, arguments to functions not known to be o3 (F_O3, also set by the prescan when `o3` comes before a function), returns from o3 code, push, container stores (after own), and the end of an o3 block for outer o3 variables. fatten is in compiler.zc, not rt.zc, because it walks type records with tnfields/tftype/tfoff. it stops at fat objects, so walks are cheap after the first, and dyn handles (always fat) are walked with a depth limit.

zc0 bug found on the way: `fatin(a + tfoff(t, i), tftype(t, i), dl)` (calls nested in an argument list next to arithmetic) made zc0 report "undefined function dl". rewritten with temporaries rather than fixing zc0. also `d` is reserved in zc0 too (`d*x`), so a local named d is a parse error there.

**numbers** after this stage (ms per run, start-up removed):

```
backend        kernel     plain ms      o3 ms  speedup
interp         collatz      324.90     240.61     1.4x
interp         sieve         47.08      21.71     2.2x
interp         digits       160.89     100.82     1.6x
interp         fib            3.48       4.09     0.8x
llvm           collatz       47.03       3.34    14.1x
llvm           sieve         10.77       0.28    38.9x
llvm           digits        18.93       1.45    13.1x
llvm           fib            0.87       1.01     0.9x
arm64 (qemu)   collatz      323.89      13.04    24.8x
arm64 (qemu)   sieve         56.84       1.74    32.6x
arm64 (qemu)   digits       147.06       4.17    35.3x
arm64 (qemu)   fib            5.19       5.71     0.9x
```

the sieve got about 2x on arm64 from the cache (3.85 -> 1.74 ms). fib is flat: each call exits and re-enters the region, and that costs what the body saves. the earlier "3.9x" for fib was noise; bench.sh now grows the run count until a timing difference is over a second.

## memory tips (answered in chat, not built)

arenas per block for m objects that provably don't escape; borrowed-from marks on derived pointers so aliases are checked; a quarantine for freed blocks plus a debug mode that checks the header on every deref; allocation sites in a debug header so leaks report where they came from; explicit shared ownership (a counted tag) instead of the collector for the cases that really need sharing; and making unchecked code the only way into the collector.