aboutsummaryrefslogtreecommitdiff
path: root/docs/history/13-gap-fixes/notes.md
blob: f7e5ab5854d811cc763ede559d5db8fec095f5f0 (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
# gap fixes

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

## message 13

> gaps
> 1. when there's a var state change to d, mark that var as dead by default for any other scope that can access it afterwards. u is fine since it just returns
> 2. any suggestions for a cheap fix?
> 3. it's fine, rare occurrence (probably?), suggest a fix
> 4. fix that
> 5. suggest a fix
> 6. fix
> 7. fix

**summary:** the numbers followed the gap list from my previous chat message. here's what each one got.

**1. joins.** a variable that became d, or was given away, on any path is dead after the join, and fixup code frees it on the paths that still own it. a loop's exit state is the join of its breaks and its normal exit. giving something away on a path that loops back is still an error. u and borrows don't get the dead treatment.

**2, 3 and 5.** suggestions only, now under open ends: a quarantine on reuse plus "borrowed from" marks on pointer-typed values for aliases, a stack-depth opcode for the collector, and page-indexed invalidation plus per-slot install tracking for the jit.

**4. heap.** a buddy allocator.

**6. stack checks.** all three stacks are checked: data and return at every block exit on all backends, and frames in every prologue.

**7. the small stuff.** error recovery with up to 20 reported errors, line numbers carried in token lists, a signature prescan over the whole unit so prototypes are optional, storing into a borrowed object moving the object automatically, `?:`, and go-style `switch`. on the side, a 4-token lookahead fixed `**` declarations.

## message 14

> how costly is it if we add a counter to each block to see how many times it runs, then run the optimizer on it if it's a hot path?

> continue

**summary:** answered without building anything. about 4 instructions per block entry while a block is being counted, and nothing once its optimized translation replaces it. the optimizing pass would reuse the enhancement lifter, optimizer and register allocator. lifting whole blocks would need jz side exits and barriers at calls and i/o. compiling a block that way takes microseconds, against milliseconds for llvm, and keeping values in registers instead of the memory stack likely gives 2 to 4x on tight loops. it's listed under open ends. then the gap fixes continued.

## working notes

the join rule from message 1 needed a place to put frees, because the merged state is only known after the last path has compiled. for if/else, the then branch already jumps over the else, so that jump now lands on a small fixup block after the else when it has frees to do, and on the join otherwise. if without else does its then-fixups inline, since the skip path's state (the state before the branch) is known in advance. loops record the states at every break in an exit record, and after the loop each break gets its own fixup stub, or a direct jump when it has nothing to free. the normal exit's fixups come last and fall into the end. switch is built on the same loop frames, with continue skipping past them to the real loop.

error recovery without exceptions: the vm can't unwind its own stacks, so a syntax error returns normally and sets panic. the error consumes the token it happened on, which guarantees every loop makes progress, and statement and top-level parsing skip ahead until panic clears. the risky part was code after an error that used a bad lookup result (sval[-1] and the like), which on the jit would read outside memory. those spots return safe defaults now.

the prescan reads the whole unit first, packed 4 chars to a word (a char per word would have needed 400k words of heap for the compiler's own source), then lexes it once with the analyzer and errors muted. signatures it finds are marked as prescanned, so a real definition quietly overrides a wrong guess.

bugs on the way: deleting a one-line function with a "find the next closing brace" helper also deleted the function after it. the variable d bit again in new code, since u, m and d are reserved. and a rename helper turned die into sdie when it was supposed to produce serr.

the buddy heap: freed blocks merge with their buddy while it's free, the same size, and inside the same top-level chunk. the sweep walks the heap by block sizes, and a merge can swallow the block that comes next, so the sweep continues from the end of the merged block. a merge also clears the absorbed header, so no stale header can look like a free block later.

numbers: the genome is 71014 words. the per-prologue frame check costs about 12 words per function, which accounts for most of the growth.