examples: lisp — perm/trans split, promote-on-define, slice free
Two bump arenas. arena_reset_trans() runs between top-level forms; top-level define / set! deep-copy the bound value graph into perm via Cheney-style forwarding (pin = -1 + stashed fwd pointer in .car/.val) so no perm cell ever points into trans. Args slice in eval's apply path also gets explicit os.free per dispatch — without that the rt_ensure page-per-call leak dominated and masked the reset. test_huge peaks at ~2.6 MB under massif --pages-as-heap=yes, down from ~525 MB pre-arena (~200x).
This commit is contained in:
@@ -9,7 +9,7 @@ not as a reference Lisp implementation.
|
||||
eval, apply, printer, REPL. Everything the entry
|
||||
point and the test driver consume is `export`-ed.
|
||||
- `lisp.ww` entry point; `use lispcore;` + `main()`.
|
||||
- `lisp_test.ww` in-process test driver (66 probes). Built as a
|
||||
- `lisp_test.ww` in-process test driver (72 probes). Built as a
|
||||
standalone binary, exec'd directly — `ww test`
|
||||
drops `-I` in single-file mode, so the Makefile
|
||||
runs the binary itself.
|
||||
@@ -89,6 +89,17 @@ same bug class shows up in any new code that hits the same pattern.
|
||||
Integer compound assigns work fine, so `acc += i` on `i64`
|
||||
stays as-is.
|
||||
|
||||
9. **`let r = call(); foreign_call(); return r?;` corrupts `r` when
|
||||
the call returned a tagged union.** The (`tag`, `payload1`,
|
||||
`payload2`) triple sits in AX/DX/CX after the call, and the
|
||||
foreign call between capture and `?`-unwrap clobbers at least one
|
||||
register before the cgen has spilled it to the local slot.
|
||||
Symptom: a `(*value | rterror)` whose `rterror` carries a string
|
||||
literal prints with a `str.len` of tens of thousands. Workaround:
|
||||
`match` the union inline before the foreign call and let each arm
|
||||
return its own typed result. See eval's BUILTIN apply path
|
||||
(where we free the args slice after `apply_builtin`).
|
||||
|
||||
If a new function "should work but acts weird", the bug is almost
|
||||
always one of the above and shows up under valgrind/gdb the same
|
||||
way it did the first time: silently dropped store, missing field
|
||||
@@ -115,17 +126,22 @@ env where `od?` is unbound; the tie-back only adds the self-binding.
|
||||
|
||||
## Interpreter limitations (design, not bug)
|
||||
|
||||
- **No GC.** Cons / value / env cells come from a single bump-
|
||||
pointer arena (`arena_alloc`, 64 KiB chunks chained via `os.alloc`).
|
||||
The arena never reclaims — long REPL sessions still leak — but
|
||||
cells pack tightly instead of one cell per 4 KiB mmap. test_huge
|
||||
peaks at ~253 MB under `--pages-as-heap=yes`, down from ~525 MB
|
||||
pre-arena. The remaining bulk is `append(xs, av)` in eval's arg
|
||||
loop: every apply allocates a fresh `[]*value` through `rt_ensure`,
|
||||
which still hits `rt_alloc` page-per-call. Per-top-level-form
|
||||
arena reset (perm/trans split + Cheney promote on define/set!) is
|
||||
the next step — see the closing note in `repl()` for the hook
|
||||
point.
|
||||
- **Arena, not GC.** Cells live in two bump-pointer arenas: `perm`
|
||||
(top-level definitions and the value graph each one pins) and
|
||||
`trans` (parse cells, intermediate evals, the form's printed
|
||||
result). `repl()` calls `arena_reset_trans()` between top-level
|
||||
forms; a Cheney-style `promote_v` deep-copies the value graph at
|
||||
every top-level `define`/`set!` boundary so no perm cell ever
|
||||
points into trans. Forwarding markers (`pin = -1` plus the new
|
||||
perm ptr stashed in `.car`/`.val`) break cycles. Detached trans
|
||||
chunks go onto a per-arena free list, so peak virtual address
|
||||
space is bounded by the largest form's working set. Builtin
|
||||
args-slice headers in eval's apply path are released with
|
||||
`os.free` per dispatch — without that, `rt_ensure`'s page-per-
|
||||
call allocation dominates the profile. test_huge peaks at ~2.6 MB
|
||||
under `--pages-as-heap=yes`, down from ~525 MB pre-arena (~200×).
|
||||
No real GC inside a single form, so a pathological one-shot like
|
||||
`(fib 25)` would still grow trans linearly until the form returns.
|
||||
- **No bigints.** `i64` wraps silently on overflow. `(fact 21)`
|
||||
rolls over.
|
||||
- **Float printing is fixed `%.6f`.** `1.0` prints as `1.000000`.
|
||||
@@ -155,7 +171,7 @@ env where `od?` is unbound; the tie-back only adds the self-binding.
|
||||
|
||||
```
|
||||
make # build ./lisp
|
||||
make test # build + run lisp_test (66 in-process probes)
|
||||
make test # build + run lisp_test (72 in-process probes)
|
||||
make demo # cat each test_*.lisp through ./lisp
|
||||
make clean
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user