The collector walks Map storage, arrays of strings or structs are map keys, map-keys and map-values are prelude generics, and spike/ is gone
This commit is contained in:
commit
78261fccda
38
TODO.org
38
TODO.org
@ -522,7 +522,7 @@ Func, Fnptr.
|
||||
CLOSED: [2026-09-25]
|
||||
Only a capturing =fn= that may outlive its frame gets a collector environment; one
|
||||
only called or passed down keeps its stack environment, as every handler does.
|
||||
Capture stays by value, and a =Map= of function values is refused. Rules out a
|
||||
Capture stays by value; a =Map= walks its values as a =Vec= does. Rules out a
|
||||
tag bit on the environment word and a heap environment for every closure.
|
||||
|
||||
** WAIT CFn and C's calling convention
|
||||
@ -911,16 +911,6 @@ there is a =Vec= to write real arena programs with, so whether the escapes that
|
||||
actually occur are lexical can now be answered. The next thing to look at, not the
|
||||
next thing to build.
|
||||
|
||||
** TODO A fixed array of structs or of strings is not a map key
|
||||
Refused by name, narrower than the spec's key set; a struct holding the array
|
||||
works. It needs the per-element walk a struct key gets, driven by a loop rather
|
||||
than a field list.
|
||||
|
||||
** TODO map-keys and map-values cannot be prelude functions
|
||||
Iteration is built; the remaining refusal is generics. A =defn= has to name its
|
||||
types and =(defn map-keys [m (Map K V)] (Vec K))= has no =K=. The loop is three
|
||||
lines at the call site, where =K= is known.
|
||||
|
||||
** DONE (vec-new [u8]) is refused
|
||||
CLOSED: [2026-09-25]
|
||||
The type positions of =vec-new= and =map-new= take a type expression: brackets, or
|
||||
@ -944,9 +934,10 @@ pairs. Rules out a type slot in =let=.
|
||||
CLOSED: [2026-09-25]
|
||||
=[const T]= and =(Ptr const T)=; a =[T]= or =(Ptr T)= converts at the top of a type or under another const one, never inside a writable one. The const is shallow: an element of a =[const [u8]]= and a Vec's buffer are writable. The address of read-only storage, a string's byte included, is a =(Ptr const T)=, and a C =const T *= parameter takes one.
|
||||
|
||||
** TODO (slice d 1) over a dyn string is refused where (at d i) works
|
||||
The typed and dyn spaces disagree about a spelling, which the standing rule
|
||||
forbids. A dyn slice should exist.
|
||||
** TODO (slice d 1) over a dyn vec traps where the typed Vec's works
|
||||
A text slices to a copy, which is the typed view's meaning because a text is
|
||||
immutable. A vec's slice has to share the vec's elements, so it needs a view
|
||||
object over a dyn vec; a copy would compute something else.
|
||||
|
||||
** TODO (slice "abc" 0 99) is not refused at compile time
|
||||
A string type carries no length, so there is nothing to compare the bound against
|
||||
@ -1085,13 +1076,6 @@ Every program in the corpus that compiles, has a =main= and terminates agrees wi
|
||||
the LLVM build down to stderr. =docs/BUILT.md=, "The hand-written x86 backend, and
|
||||
the four measurements behind it", is the account.
|
||||
|
||||
** NEXT Nothing pins the LLVM side at -O0 when the two backends are compared
|
||||
Decided 2026-09-25: the x86 parity survey compares against LLVM at -O0 only; -O2 is never its target. The acceptance suite's paired -O2/-O0 rows stay, being the check for undefined behaviour in emitted IR, which is a different question.
|
||||
The survey builds both sides at the default =-O2=, so a construct LLVM folds is
|
||||
compared as a constant rather than as a lowering. That is how the float =%= gap
|
||||
survived. Two things would close it: an =-O0= pass of the sweep, and something
|
||||
that walks the two backends' primitive match arms mechanically. Neither is queued.
|
||||
|
||||
** DONE Reading (uninit) before writing it is undefined behaviour
|
||||
CLOSED: [2026-09-25]
|
||||
Reading an =(uninit)= value before writing it is undefined behaviour, and the backends may differ on it. An exhausted match stays =ud2= on x86.
|
||||
@ -1447,12 +1431,14 @@ through a pointer into it is answerable; memcheck is told the same fact, so the
|
||||
same read is reported. The two stay two claims — different tools reaching
|
||||
different people.
|
||||
|
||||
** NEXT The leak question across the corpus
|
||||
** WAIT The leak question across the corpus
|
||||
Decided 2026-09-25: one pass over the whole corpus with LeakSanitizer and memcheck's leak check on. Memory an allocator holds by design is set aside; memory nothing owns is a leak and is fixed. The sweeps' default stays leak-checking off.
|
||||
Both sweeps run with leak checking off, because allocate-once-never-free is this
|
||||
runtime's design and a leak check produces a suppression list. A green sweep
|
||||
therefore says nothing about who frees the newly allocating =(bytes s)=. Worth
|
||||
asking on purpose one day, across the whole corpus and not one program.
|
||||
WAIT on the between-batches sweep slot: the switches are
|
||||
=ASAN_OPTIONS=detect_leaks=1 dune build @sanitize= and =FLAN_LEAKS=1 dune build @valgrind=.
|
||||
A 22-program LSan sample found no runtime leak; program leaks in map-remove and
|
||||
map-keys are fixed. Needs a decision: =(bytes s)= and =(clone slice)= with no
|
||||
allocator answer a =[T]= over a heap block nothing can free (bytes-copy.flan).
|
||||
Temp-allocator by default, as =i64->bytes= is, or a =(Vec T)= the caller frees?
|
||||
|
||||
** DONE trap_oom has no site
|
||||
CLOSED: [2026-09-25]
|
||||
|
||||
@ -64,7 +64,7 @@ claims the hardware masks to operand width; it masks to 63. `emit.ml:2041` masks
|
||||
`bits-1` explicitly (TODO.org, "A shift count is bounded two different ways", records
|
||||
this as the language's rule). Six
|
||||
confirmed divergences, e.g. `(<< x 32)` on i32: LLVM 1, x86 0; `(>> i8min 8)`: LLVM
|
||||
-128, x86 -1. Invisible because `spike/x86/survey.sh:80` never globs `spike/js/*.flan`,
|
||||
-128, x86 -1. Invisible because `test/survey-x86.sh:80` never globs `spike/js/*.flan`,
|
||||
where `p1-int-semantics.flan` already catches it — widen the glob in the same lane.
|
||||
Same wide-compute root, second divergence: float→int overflow under `--no-bounds-checks`
|
||||
gives 0 on x86 (64-bit `cvttsd2si` then truncate) vs INT_MIN on LLVM. Acknowledged-UB
|
||||
|
||||
@ -1265,8 +1265,8 @@ looked wrong.
|
||||
|
||||
### Proved by comparing output, never by reading bytes
|
||||
|
||||
`spike/x86/survey.sh` builds each program in `test/programs` and each probe in `spike/x86` twice — once default, once
|
||||
`--x86`, **with the same bounds-check setting on both sides** — runs both, and compares stdout, stderr and the exit
|
||||
`test/survey-x86.sh` builds each program in `test/programs`, the `x86-p*` probes among them, twice — once through LLVM
|
||||
at `-O0`, once `--x86`, **with the same bounds-check setting on both sides** — runs both, and compares stdout, stderr and the exit
|
||||
status. stderr is not a detail: every message the condition machinery produces goes there, each carrying a location
|
||||
this backend emits by hand as a `.rodata` label and a length in a register, and an exit status of 134 with the wrong
|
||||
text beside it is exactly the failure that reads as a match.
|
||||
@ -1374,7 +1374,7 @@ must not land in the middle of one; and the `flan_dev_reg_enable` constructor, w
|
||||
|
||||
**The corpus structurally cannot test this.** A dev build starts with every cell pointing at the body this build
|
||||
compiled, so it prints what a release build prints whether or not anything reads the cell — the property that makes
|
||||
the whole corpus a safe test of the cells is the property that makes it a useless one. `spike/x86/cells.sh` preloads
|
||||
the whole corpus a safe test of the cells is the property that makes it a useless one. `test/cells.sh` preloads
|
||||
a shared object whose constructor looks up `flan.cell.twice` with `dlsym` and stores a different body there: the one
|
||||
store a redefinition ends in, done from outside with no compiler involved. Four builds, and the two release rows are
|
||||
half the test — they answer `42 42` because there is no cell and `dlsym` finds nothing, which is what says the change
|
||||
@ -1400,7 +1400,7 @@ refused. What it does not emit is locals and types, and that is deliberate — a
|
||||
temporary whose lifetime this backend does not model, so there is nothing honest for a `DW_TAG_variable` to point
|
||||
at. A backtrace names files, functions and lines; `print x` says the name is not in the current context. A `flan
|
||||
dev --debug` session still takes LLVM's side, because `X86.redefinition` emits no line table.
|
||||
- **Code size and speed** are measured, in `spike/x86/COST.md`. This backend emits **3.84× the code LLVM does at
|
||||
- **Code size and speed** were measured in `spike/x86/COST.md`, since deleted with `spike/` and in git history. This backend emits **3.84× the code LLVM does at
|
||||
`-O2` and 1.92× what LLVM emits at `-O0`** — half the factor is the optimiser and not the backend. Of the five
|
||||
suspected costs, the frame-slot round trip on every intermediate is most of everything and is the one worth fixing;
|
||||
`rep movsb` is twenty cycles a copy and worth fixing cheaply; the bounds check's three temporaries cost 241 bytes
|
||||
@ -1562,7 +1562,7 @@ least two" — and lets the old caller fail at run time with a wrong-number-of-a
|
||||
count at run time to fail on, so the run-time half is a check it has to emit.
|
||||
|
||||
**A dev cell is three words**: `{ ptr body, i64 word, ptr text }`. The body is first, so a plain load of the cell is
|
||||
still the body and `spike/x86/cells.sh`'s store through `dlsym` still works. The word is a hash (FNV-1a) of the
|
||||
still the body and `test/cells.sh`'s store through `dlsym` still works. The word is a hash (FNV-1a) of the
|
||||
signature spelled the way a `defn` writes it — `[i64 i64] i64` — and the text is that spelling as a C string.
|
||||
`Emit.sig_text` and `Emit.sig_word` are the one definition both backends use. A redefinition module's installer
|
||||
stores the word and the text beside the body; a registry cell (`flan_dev_cell`) is three zeroed words until then.
|
||||
@ -1841,8 +1841,8 @@ the merged entry point uses to flush and park.
|
||||
#### What the embedding spike measured, and the three rules it left behind
|
||||
|
||||
The merge was taken on a spike run before any of it was built — `spike/embed/`, four shell scripts and sixteen small
|
||||
sources driving `ocamlfind` and `clang` by hand against the `flan.cmxa` dune already builds. Nothing under `spike/` is
|
||||
wired into the build. What it answered is why the shape above was safe to commit to.
|
||||
sources driving `ocamlfind` and `clang` by hand against the `flan.cmxa` dune already builds, deleted since and kept in
|
||||
git history. What it answered is why the shape above was safe to commit to.
|
||||
|
||||
**Linking.** `ocamlopt -output-complete-obj`, not `-output-obj`: it bundles the runtime, so there is no hunt for
|
||||
`libasmrun`. The final link needs `-lm -lpthread -ldl` and, on 5.x, **`-lzstd`** — the marshaller is compressed, and
|
||||
@ -1861,7 +1861,7 @@ specific capability the merged design needs.
|
||||
expected conflict does not exist. The reason is structural: OCaml 5 detects stack overflow with an explicit
|
||||
stack-limit check rather than with a guard page and a SIGSEGV handler. So the break loop can take `SIGSEGV` outright
|
||||
and does not have to install first or last. **This is an `x86_64-pc-linux-gnu` measurement only** — re-run
|
||||
`spike/embed/sig.sh` on macOS/arm64 before relying on it there. `flan_agent.c` needs nothing from it either way; it
|
||||
`spike/embed/sig.sh` (git history) on macOS/arm64 before relying on it there. `flan_agent.c` needs nothing from it either way; it
|
||||
sends with `MSG_NOSIGNAL` throughout.
|
||||
|
||||
**The GC and raw memory.** An 8 MiB arena filled with a checkable pattern, 64 raw interior pointers taken into it,
|
||||
@ -4575,8 +4575,13 @@ read of a local. So the prelude's `map`, `filter` and `reduce` are as cheap in a
|
||||
closure as in one that makes none: a map, filter and reduce loop measured 1.77G instructions at LLVM `-O2` with
|
||||
and without one unrelated escaping closure.
|
||||
|
||||
**What is refused.** A `Map` whose values hold a function value (`fn-in-map.flan`): a `Map`'s storage is not walked.
|
||||
A bare `Fn` field, global or array element is still refused for its zero; `(Option (Fn ...))` holds one.
|
||||
**A `Map`'s values are walked the same way** (`fn-in-map.flan`). A descriptor's `maps` table names each `Map` header
|
||||
and the value type's descriptor; `flan_rt.c` reports each `Map` block through the same hook, and the marker walks the
|
||||
full slots of a live block, reading the slot count from the header and the stride and value offset from the block's
|
||||
own head. It covers a dyn value as well as a closure, so `(Map K dyn)` holds dyn values the collector keeps. The hook
|
||||
is installed by any program holding a dyn, not only one making heap closures.
|
||||
|
||||
**What is refused.** A bare `Fn` field, global or array element, for its zero; `(Option (Fn ...))` holds one.
|
||||
|
||||
**A module that makes a heap closure is never unloaded**: the environment points at the module's descriptor and
|
||||
code, so making one counts toward the same gate a string literal does. A capturing `fn` typed at the dev prompt takes
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
> and nested inside `[$t]` or `(Option $t)`, and bare `t` only where a type's *name* is an argument in
|
||||
> expression position, as in `(vec-new t)` and the cast `(t x)`. `(Option t)` does not compile.
|
||||
> plan.org's Types section and spec-memory.md's Generics section are the current account.
|
||||
> `spike/`, which held every file this report names, was deleted on 2026-09-25; the files are in git history.
|
||||
|
||||
Milestone 5's parametric polymorphism, run early and deliberately out of order, as a spike rather than as a
|
||||
decision. **Feasible, and smaller than expected.** A generic function written in Flan goes through the ordinary
|
||||
|
||||
@ -76,7 +76,7 @@ because `load_loc` has already widened both operands according to their own sign
|
||||
backends used to diverge silently rather than both dying: x86 divided in 64 bits and truncated on the store, producing
|
||||
`-2147483648` for an `i32`, where LLVM emitted poison. `arith.flan` has an `i32` case for exactly that reason.
|
||||
|
||||
`spike/x86/survey.sh` is 101 MATCH / 0 DIFFER / 0 REFUSED, with the two programs this change adds among them.
|
||||
`test/survey-x86.sh` is 101 MATCH / 0 DIFFER / 0 REFUSED, with the two programs this change adds among them.
|
||||
|
||||
`arith.flan` carries an `i32` overflow case and an `f32` cast case on purpose, and neither is padding. The `i32`
|
||||
overflow is where the two backends disagreed *silently* rather than both dying, and it is the only thing that
|
||||
|
||||
@ -124,11 +124,11 @@ actually about is a **pointer reinterpretation**, which is not a checker arm.
|
||||
`defstruct`, every hand-written `declare-c` and every mapped constant against
|
||||
`raylib-5.5.h` and refuses to write when they disagree.
|
||||
- `bash web/examples/check.sh` green.
|
||||
- `spike/x86/survey.sh` on the finished tree: **103 MATCH, 0 DIFFER, 0 REFUSED** (38 skip
|
||||
- `test/survey-x86.sh` on the finished tree: **103 MATCH, 0 DIFFER, 0 REFUSED** (38 skip
|
||||
— 28 that do not compile on purpose, 8 with no main, 2 that run forever). Expected
|
||||
rather than surprising: nothing here is below the IR, and the one surveyed program that
|
||||
changed is `test/programs/raylib-codepoints.flan`. Run it detached — `setsid timeout
|
||||
2400 spike/x86/survey.sh > log 2>&1 </dev/null` — because a foreground run takes the
|
||||
2400 test/survey-x86.sh > log 2>&1 </dev/null` — because a foreground run takes the
|
||||
signal sent to its process group and returns a spurious 143.
|
||||
|
||||
## Still open
|
||||
|
||||
@ -98,9 +98,9 @@ should be reading on a green run.
|
||||
Baseline not regressed, all three measured after the change:
|
||||
|
||||
- `dune test --root .` — exit 0.
|
||||
- `spike/x86/survey.sh` — **101 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86**, 36 skipped (28 does-not-compile, 6 no-main,
|
||||
- `test/survey-x86.sh` — **101 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86**, 36 skipped (28 does-not-compile, 6 no-main,
|
||||
2 runs-forever).
|
||||
- `spike/x86/cells.sh` — **4/4**, exit 0.
|
||||
- `test/cells.sh` — **4/4**, exit 0.
|
||||
|
||||
One thing not to re-derive: a first attempt at the survey came back `SURVEY_EXIT=143` with a bare `Terminated`. That
|
||||
was the harness killing the process group of a long background command, not a survey failure and nothing to do with
|
||||
|
||||
@ -134,8 +134,8 @@ reason. The 600s watchdogs are what actually bound the run.
|
||||
|
||||
- `dune test --root .` — three runs, 232 checks, 0 failures, exit 0 each time.
|
||||
All three under sixteen busy loops; two of them alongside a running
|
||||
`spike/x86/survey.sh`.
|
||||
- `spike/x86/survey.sh` — 103 MATCH / 0 DIFFER / 0 REFUSED.
|
||||
`test/survey-x86.sh`.
|
||||
- `test/survey-x86.sh` — 103 MATCH / 0 DIFFER / 0 REFUSED.
|
||||
|
||||
## Open questions for the author
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Branch `dev-loop`, worktree `agent-a9d4028fa5752ba39`, from `f772162`.
|
||||
|
||||
`spike/x86/dump.sh` landed at `f772162` and prints four lowerings of one function side by side: the LLVM IR
|
||||
`tools/dump.sh` landed at `f772162` and prints four lowerings of one function side by side: the LLVM IR
|
||||
the frontend emits, what `llc` makes of it at `-O0` and at `-O2`, and what the hand-written x86 backend emits.
|
||||
Reading one against another is the only way to check a lowering by eye. A shell script that prints all four
|
||||
into a scrollback is not how anyone reads them, though: three of the four are noise on any given day, and the
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# The checks nobody runs
|
||||
|
||||
Twice now a script in this repository has been quietly wrong for weeks, and both
|
||||
times it was found by accident rather than by anything failing. `spike/x86/survey.sh`
|
||||
times it was found by accident rather than by anything failing. `test/survey-x86.sh`
|
||||
had two programs refused by name for about a month. `web/examples/check.sh` had been
|
||||
red since September 12th. This is the inventory of everything in the tree that
|
||||
asserts something and is not reached by `dune test`, what each one says today, and
|
||||
@ -18,8 +18,8 @@ Everything executable or fixture-shaped outside `dune test`, run rather than ass
|
||||
|---|---|---|---|
|
||||
| `web/examples/check.sh` | each `web/examples/*.flan` against the `.out` beside it, plus `flan shim` on shimdemo and a killed `--dev` build of breakdemo | now `@page` | **passes**, 21 checks |
|
||||
| `web/examples/quotes.sh` | every block on `index.html` that is not a program — usage text, refusal messages, the LLVM excerpt, the renderer's output, the Emacs keys — re-derived and looked for on the page | now `@page` | **was red, 11 of 42**; fixed, now passes |
|
||||
| `spike/x86/survey.sh` | stdout, stderr and exit status of every corpus program, x86 backend against LLVM | `@x86`, and by hand | **passes**, 103 match / 0 differ / 0 refused / 38 skip |
|
||||
| `spike/x86/cells.sh` | a `--dev` build really calls through its indirection cell: an `LD_PRELOAD`ed constructor stores a different body, four builds, `22 22` against `42 42` | nothing — now `@cells` | **passes**, 4 checks |
|
||||
| `test/survey-x86.sh` | stdout, stderr and exit status of every corpus program, x86 backend against LLVM | `@x86`, and by hand | **passes**, 103 match / 0 differ / 0 refused / 38 skip |
|
||||
| `test/cells.sh` | a `--dev` build really calls through its indirection cell: an `LD_PRELOAD`ed constructor stores a different body, four builds, `22 22` against `42 42` | nothing — now `@cells` | **passes**, 4 checks |
|
||||
| `tools/colon-to-dot.py --check` | every `.flan` uses the dot spelling for field labels | nothing | **reports 20 hits, all false.** See below — this one is a hazard, not a check |
|
||||
| `tools/unit-return.py --check` | every `defn` states a return type, `Unit` spelled `()` | nothing | **reports 30 hits, all false.** Same hazard |
|
||||
| `emacs/test-flan-dape.el` | dape driving lldb-dap sets a breakpoint from a `.flan` buffer and reports Flan frames | nothing loads it | **cannot run here** — `require 'dape` fails, dape is not installed. Its own header says it is out of `dune test` on purpose |
|
||||
@ -131,7 +131,7 @@ gone stale, not when the compiler has regressed, and a suite that goes red becau
|
||||
prose drifted teaches whoever runs it to skim past red. `dune test` should mean "the
|
||||
language broke". `@page` should mean "the page is lying".
|
||||
|
||||
**`@cells`** runs `spike/x86/cells.sh`, which was a real four-check pass/fail script
|
||||
**`@cells`** runs `test/cells.sh`, which was a real four-check pass/fail script
|
||||
that nothing in the tree ran. It is not folded into `@x86` because it asks a different
|
||||
question: the survey asks whether the backend agrees with LLVM about what a program
|
||||
prints, and no program can answer this one, because a dev build starts with every cell
|
||||
|
||||
@ -96,7 +96,7 @@ everything else explains itself. They are grouped and explained now, and
|
||||
|
||||
- `dune build --root .` exits 0.
|
||||
- `dune test --root . --force` reports 232 checks, 0 failures.
|
||||
- `spike/x86/survey.sh`, run detached, reports 103 MATCH, 0 DIFFER, 0 REFUSED,
|
||||
- `test/survey-x86.sh`, run detached, reports 103 MATCH, 0 DIFFER, 0 REFUSED,
|
||||
0 NOX86 and 38 SKIP — the baseline, unchanged.
|
||||
|
||||
The resolver reports sixteen unresolved citations and all sixteen are expected.
|
||||
|
||||
@ -149,9 +149,9 @@ merely intended, and these two crossed tests are what will catch a half-done ver
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| `spike/x86/survey.sh` | 103 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86 | **103 / 0 / 0 / 0** |
|
||||
| `test/survey-x86.sh` | 103 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86 | **103 / 0 / 0 / 0** |
|
||||
| skip breakdown | 28 does-not-compile / 8 no-main / 2 runs-forever | **28 / 8 / 2** |
|
||||
| `spike/x86/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `test/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `dune test --root .` | exit 0, 232 checks, 0 failures | **exit 0, 232 checks, 0 failures** |
|
||||
|
||||
The survey is the measurement that could have moved and did not, which is the point of running it: both
|
||||
@ -159,7 +159,7 @@ backends now emit a symbol into every `--dev` build, and the survey's default is
|
||||
so an unguarded marker would have shown up as 103 identical-but-different objects rather than as a wrong
|
||||
answer. It agrees byte-for-byte on what the programs print.
|
||||
|
||||
Run it detached — `setsid timeout 2400 spike/x86/survey.sh > log 2>&1 </dev/null` — or a signal to this
|
||||
Run it detached — `setsid timeout 2400 test/survey-x86.sh > log 2>&1 </dev/null` — or a signal to this
|
||||
harness's process group comes back as `SURVEY_EXIT=143`, which is not a result. The log is block-buffered
|
||||
through the redirect and stays empty until the end; that is not a hang.
|
||||
|
||||
|
||||
@ -138,9 +138,9 @@ together, the marker is what makes the choice checkable rather than merely inten
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| `spike/x86/survey.sh` | 99 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86 | **99 / 0 / 0 / 0** |
|
||||
| `test/survey-x86.sh` | 99 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86 | **99 / 0 / 0 / 0** |
|
||||
| skip breakdown | 28 does-not-compile / 6 no-main / 2 runs-forever | 28 / **8** / 2 |
|
||||
| `spike/x86/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `test/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `dune test --root .` | exit 0 | **exit 0** |
|
||||
|
||||
The `no-main` count moving from 6 to 8 is the two new fixtures existing: `survey.sh` globs
|
||||
|
||||
@ -19,7 +19,7 @@ compiler adds has a name where it appears and an explanation once, in a legend a
|
||||
| `lib/x86.ml` | the annotation machinery: a queue of pending comments on `buf`, the per-form hook in `lower`, `frame_map`, the bookkeeping `note`s, and the legend |
|
||||
| `bin/main.ml` | `emit --x86` annotates; `--no-annotate` is the bare spelling |
|
||||
| `spike/x86/annot.sh` | **new** — emits every program in the corpus both ways, assembles both, and compares every section of the two objects byte for byte, in all three of default, `--dev` and `--debug` |
|
||||
| `spike/x86/dump.sh` | its x86 section now shows the annotated listing *and* the disassembly of the object that listing assembles to — why beside what |
|
||||
| `tools/dump.sh` | its x86 section now shows the annotated listing *and* the disassembly of the object that listing assembles to — why beside what |
|
||||
|
||||
`lib/build.ml` was not touched. A `--x86` build emits exactly the assembly it emitted before.
|
||||
|
||||
@ -102,7 +102,7 @@ transfer exit and C's `main` carry a prose block each.
|
||||
|
||||
## A worked sample
|
||||
|
||||
`spike/x86/dump.sh small.flan twice`, on
|
||||
`tools/dump.sh small.flan twice`, on
|
||||
|
||||
```
|
||||
(defn twice [n i64] i64
|
||||
@ -205,14 +205,14 @@ reject corpus and the generic-milestone ones — and are the same set the survey
|
||||
`dune build @cells` both exit 0; `@cells` reports `x86 --dev: 22 22` and `x86 : 42 42`, which is the check
|
||||
that the indirection cells and the ABI marker still come out where they were.
|
||||
|
||||
**`spike/x86/survey.sh` has NOT been run on this work.** Two runs were started and both were invalidated by
|
||||
**`test/survey-x86.sh` has NOT been run on this work.** Two runs were started and both were invalidated by
|
||||
racing with a `dune build` that replaced `bin/main.exe` underneath them; a third was killed on instruction
|
||||
before it finished. **Whoever picks this up must not assume the 103 MATCH / 0 DIFFER / 0 REFUSED baseline
|
||||
still holds — it is the one check that matters and it is outstanding.** Run it detached and with the
|
||||
compiler pinned, so it cannot race a rebuild:
|
||||
|
||||
```
|
||||
FLAN=_build/default/bin/main.exe setsid timeout 2400 spike/x86/survey.sh > log 2>&1 </dev/null
|
||||
FLAN=_build/default/bin/main.exe setsid timeout 2400 test/survey-x86.sh > log 2>&1 </dev/null
|
||||
```
|
||||
|
||||
What makes an unwelcome result unlikely rather than impossible: the survey builds through `flan build --x86`,
|
||||
|
||||
@ -16,7 +16,7 @@ reason: it builds every corpus program twice and runs both, which is minutes whe
|
||||
dune build --root . @x86
|
||||
|
||||
It is a `(rule ...)` with no `(executable ...)` beside it, which is where it differs from its two neighbours. The
|
||||
check already exists — `spike/x86/survey.sh` is what every handoff quotes its counts from — and a second
|
||||
check already exists — `test/survey-x86.sh` is what every handoff quotes its counts from — and a second
|
||||
implementation of it in OCaml would be a second thing to drift, which is exactly the failure this alias is meant
|
||||
to prevent. So the rule runs the script, and the script grew three environment variables to make that possible:
|
||||
|
||||
|
||||
@ -212,8 +212,8 @@ multi-file case: its directory table has one entry and its file table two, `gene
|
||||
|
||||
| | before (`957ba07`) | after, on `957ba07` | after, rebased onto `eacf7c4` |
|
||||
|---|---|---|---|
|
||||
| `spike/x86/survey.sh` | 99 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86, skips 28 + 6 + 2 | **99 / 0 / 0 / 0**, skips 28 + 6 + 2 | **101 / 0 / 0 / 0**, skips 28 + 6 + 2 |
|
||||
| `spike/x86/cells.sh` | 4/4 ok | 4/4 ok | **4/4 ok** |
|
||||
| `test/survey-x86.sh` | 99 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86, skips 28 + 6 + 2 | **99 / 0 / 0 / 0**, skips 28 + 6 + 2 | **101 / 0 / 0 / 0**, skips 28 + 6 + 2 |
|
||||
| `test/cells.sh` | 4/4 ok | 4/4 ok | **4/4 ok** |
|
||||
| `dune test --root .` | exit 0 | exit 0 | **exit 0**, run without a pipe; acceptance reports **232 checks, 0 failures** |
|
||||
|
||||
The MATCH count went from 99 to 101 across the rebase and neither is this lane's doing: the
|
||||
|
||||
@ -221,9 +221,9 @@ Two things the numbers say that the headline does not:
|
||||
| | before (`f459352`) | after |
|
||||
|---|---|---|
|
||||
| `dune test --root .` | exit 0, 232 checks, 0 failures | **exit 0, 232 checks, 0 failures**, three runs |
|
||||
| `spike/x86/survey.sh` | 103 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86 | **103 / 0 / 0 / 0** |
|
||||
| `test/survey-x86.sh` | 103 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86 | **103 / 0 / 0 / 0** |
|
||||
| skip breakdown | 28 does-not-compile / 8 no-main / 2 runs-forever | 28 / **9** / 2 |
|
||||
| `spike/x86/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `test/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `bash web/examples/check.sh` | green | **green** |
|
||||
|
||||
The `no-main` count moving from 8 to 9 is `reload-v6.flan` existing: `survey.sh` globs
|
||||
@ -242,7 +242,7 @@ unchanged tree print nothing and exit 0 without having executed anything. If wha
|
||||
|
||||
Run `dune test --root .` **without a pipe** — piping to `tail` gives you `tail`'s exit status, and the
|
||||
`dev-robust` fixture puts `ld` and `clang` failure text in the output either way. Run the survey **detached**
|
||||
(`setsid timeout 2400 spike/x86/survey.sh > log 2>&1 </dev/null`) or a signal to the harness's process group
|
||||
(`setsid timeout 2400 test/survey-x86.sh > log 2>&1 </dev/null`) or a signal to the harness's process group
|
||||
comes back as 143, which is not a result. And do not run the survey while `dune test` is running: both take the
|
||||
`_build` lock, and the loser reports a lock error rather than a count.
|
||||
|
||||
@ -257,4 +257,4 @@ comes back as 143, which is not a result. And do not run the survey while `dune
|
||||
the client neither knows nor can say which backend a session uses — the flag is `flan dev`'s and the protocol
|
||||
above it is identical. The real editor path for *this* lane is `test_dev.ml`, which drives the daemon's own
|
||||
verbs and is where the case went. Worth revisiting only if `--x86` ever grows a spelling in the protocol.
|
||||
- Items 2–7 of `HANDOFF-x86-rt.md` §6, unchanged. And still worth doing: **run `spike/x86/survey.sh` in CI**.
|
||||
- Items 2–7 of `HANDOFF-x86-rt.md` §6, unchanged. And still worth doing: **run `test/survey-x86.sh` in CI**.
|
||||
|
||||
@ -19,7 +19,7 @@ and the caller that matters here is `guard`, the two-load-and-branch check emitt
|
||||
condition reads, in source terms: **a function that has a defer and makes no guarded call at all** — neither in the
|
||||
body nor in the defers, which are spliced onto the normal exit path and so are lowered as part of the body.
|
||||
|
||||
That is not an exotic shape. It is any leaf function with a defer. `spike/x86/p9-dead-defers.flan` is ten lines:
|
||||
That is not an exotic shape. It is any leaf function with a defer. `test/programs/x86-p9-dead-defers.flan` is ten lines:
|
||||
|
||||
```flan
|
||||
(defn quiet [n i32] i32
|
||||
@ -94,7 +94,7 @@ named `cleanup` is the innermost one, and anything that finds the channel set ag
|
||||
|
||||
**Is the block emitted?** Yes, whenever a defer body contains a guarded call in a function that can unwind. `used` is
|
||||
a `bool ref` set by `current_pad`, so a defer that only assigns — which is what `p6-transfer.flan`'s `middle` does —
|
||||
never emits it. `objdump -d` over `spike/x86/p10-defer-transfer.flan` built with `--x86` finds two references to
|
||||
never emits it. `objdump -d` over `test/programs/x86-p10-defer-transfer.flan` built with `--x86` finds two references to
|
||||
`flan_transfer_fail`.
|
||||
|
||||
**Does anything arrive there?** Yes. The thing that makes this hard to construct is that `lib/check.ml`'s
|
||||
@ -104,7 +104,7 @@ remaining case lives: "this is the one that reaches a function through a call, w
|
||||
So the probe puts the `invoke-restart` in an ordinary `defn` and has the defer call it. The checker has nothing to
|
||||
object to, because from where it stands `second` is a function like any other.
|
||||
|
||||
`spike/x86/p10-defer-transfer.flan` is that program. `outer` establishes two nested restart-cases and a handler-bind
|
||||
`test/programs/x86-p10-defer-transfer.flan` is that program. `outer` establishes two nested restart-cases and a handler-bind
|
||||
and calls `middle`; `deep` signals; the handler aims a transfer at `esc-one`; the transfer unwinds `deep` and then
|
||||
`middle`; `middle`'s transfer exit clears the channel and runs its defers; and the defer calls `second`, which aims a
|
||||
second transfer at `esc-two`. The restart frames are both still pushed at that moment — it is `outer`'s own pad that
|
||||
@ -115,7 +115,7 @@ is why the second restart-case has to be established *outside* the first.
|
||||
Both backends, verbatim and identical, at exit status 134:
|
||||
|
||||
```
|
||||
spike/x86/p10-defer-transfer.flan:39:7: a defer invoked a restart, which a defer may not do — it is the cleanup a transfer runs on its way out
|
||||
test/programs/x86-p10-defer-transfer.flan:39:7: a defer invoked a restart, which a defer may not do — it is the cleanup a transfer runs on its way out
|
||||
```
|
||||
|
||||
The location is `Loc.to_string fn.Tast.floc` — `middle`'s own `defer` form — on both sides, which is the part a
|
||||
@ -132,14 +132,14 @@ has for the case, and the corpus simply had no program that started a transfer f
|
||||
| file | state | what |
|
||||
|---|---|---|
|
||||
| `lib/x86.ml` — `emit_fn`, tail of the transfer exit | working | the item-5 refusal removed; one hunk, six lines out, a comment in. Nothing else in the file |
|
||||
| `spike/x86/p9-dead-defers.flan` | working, MATCH | a leaf function with a defer and no call. Was the refusal |
|
||||
| `spike/x86/p10-defer-transfer.flan` | working, MATCH at 134 | a defer that calls a function that invokes a restart, while a first transfer unwinds |
|
||||
| `test/programs/x86-p9-dead-defers.flan` | working, MATCH | a leaf function with a defer and no call. Was the refusal |
|
||||
| `test/programs/x86-p10-defer-transfer.flan` | working, MATCH at 134 | a defer that calls a function that invokes a restart, while a first transfer unwinds |
|
||||
|
||||
`lib/emit.ml`, `lib/check.ml` and `runtime/flan_rt.c` were not modified; no change to any of them was needed.
|
||||
|
||||
## The survey
|
||||
|
||||
`bash spike/x86/survey.sh p9 p10` — the two new probes, both sides, built and run:
|
||||
`bash test/survey-x86.sh p9 p10` — the two new probes, both sides, built and run:
|
||||
|
||||
```
|
||||
MATCH 2
|
||||
@ -153,7 +153,7 @@ Worth saying what that second row proves for `p10`, because it is not obvious: t
|
||||
hand-encoded backend that got the `.rodata` label or the length register wrong would still exit 134 and would still
|
||||
look like a match to anything comparing only exit statuses.
|
||||
|
||||
And the full run, `SURVEY_QUIET=1 spike/x86/survey.sh`, over `test/programs` and `spike/x86` together:
|
||||
And the full run, `SURVEY_QUIET=1 test/survey-x86.sh`, over `test/programs` and `spike/x86` together:
|
||||
|
||||
```
|
||||
MATCH 99
|
||||
|
||||
@ -118,7 +118,7 @@ new one. `macro_module` deletes its `.ll` unless `opts.keep`, so the `hidden:tru
|
||||
stands behind the new path is `nm -D` on the linked object above — three thunks and nothing else of Flan's,
|
||||
which is the property being claimed, read off the artefact rather than off the text that made it.
|
||||
|
||||
`spike/x86/survey.sh` was **not** run here — it is forty minutes and this lane changes no lowering. It will
|
||||
`test/survey-x86.sh` was **not** run here — it is forty minutes and this lane changes no lowering. It will
|
||||
report one more `runs-forever` when it next runs: `dev-macro.flan` waits on an agent that is not there, which
|
||||
is what `dev-loop.flan` and `dev-repl.flan` already do.
|
||||
|
||||
|
||||
@ -10,8 +10,8 @@ module publishes a new body into the host's cell, and the host's un-rebuilt call
|
||||
|
||||
| | before (`b1cc67b`) | after |
|
||||
|---|---|---|
|
||||
| `spike/x86/survey.sh` | 97 MATCH / 0 DIFFER / 0 refused | (see §"After", below) |
|
||||
| `spike/x86/cells.sh` | 4/4 ok | 4/4 ok |
|
||||
| `test/survey-x86.sh` | 97 MATCH / 0 DIFFER / 0 refused | (see §"After", below) |
|
||||
| `test/cells.sh` | 4/4 ok | 4/4 ok |
|
||||
| `dune test --root .` | exit 0 | exit 0 |
|
||||
|
||||
`dune test` prints two `ld` / `clang` failures from inside the `dev-robust` fixture and still exits 0. Check the
|
||||
@ -98,14 +98,14 @@ That is `C-c C-c` on an existing `defn`, which is the demo, and it is what `test
|
||||
path; add a fixture beside it.
|
||||
5. Items 2–7 of `HANDOFF-x86-rt.md` §6, unchanged.
|
||||
|
||||
Also still true and still worth doing: **run `spike/x86/survey.sh` in CI**.
|
||||
Also still true and still worth doing: **run `test/survey-x86.sh` in CI**.
|
||||
|
||||
## After, measured
|
||||
|
||||
| | before (`b1cc67b`) | after |
|
||||
|---|---|---|
|
||||
| `spike/x86/survey.sh` | 97 MATCH / 0 DIFFER / 0 refused | **97 / 0 / 0**, skips `28 does-not-compile / 6 no-main / 2 runs-forever` — identical |
|
||||
| `spike/x86/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `test/survey-x86.sh` | 97 MATCH / 0 DIFFER / 0 refused | **97 / 0 / 0**, skips `28 does-not-compile / 6 no-main / 2 runs-forever` — identical |
|
||||
| `test/cells.sh` | 4/4 ok | **4/4 ok** |
|
||||
| `dune test --root .` | exit 0 | **exit 0**, run without a pipe |
|
||||
|
||||
The skip breakdown is the line that matters beyond the MATCH count, because the no-`main` relaxation is the one
|
||||
|
||||
@ -49,7 +49,7 @@ repeated it. Verified with the survey: `Vec`/`Map` via `vec.flan`, `vec-of-vec.f
|
||||
|
||||
## 2. Survey counts, measured
|
||||
|
||||
`spike/x86/survey.sh`, unchanged in what it compares (stdout + stderr + exit status, same bounds-check setting both
|
||||
`test/survey-x86.sh`, unchanged in what it compares (stdout + stderr + exit status, same bounds-check setting both
|
||||
sides).
|
||||
|
||||
| | brief said | measured before | after |
|
||||
@ -63,7 +63,7 @@ sides).
|
||||
two refusals — `slice-from-ptr.flan` and `bounds.flan` — both reported as `x86: primitive with 2 arguments`. Fixing
|
||||
that is commit 1 and is reported separately so the "after" number is not misread as this lane's work.
|
||||
|
||||
Also measured, opt-in and new: `SURVEY_FLAGS=--dev spike/x86/survey.sh` → **97 MATCH / 0 DIFFER**.
|
||||
Also measured, opt-in and new: `SURVEY_FLAGS=--dev test/survey-x86.sh` → **97 MATCH / 0 DIFFER**.
|
||||
|
||||
## 3. What was built, file by file
|
||||
|
||||
@ -81,11 +81,11 @@ All **working and verified**; nothing in this list is unverified or reverted.
|
||||
| `lib/x86.ml` — `program` | working | now `~checks ?dev`; emits cells and the `flan_dev_reg_enable` ctor when `dev` |
|
||||
| `lib/x86.ml` — `layout_ctx` | working | now `~checks ~dev`; `Emit.m.dev` is no longer hardcoded `false` |
|
||||
| `lib/build.ml` | working | `--dev` removed from the `--x86` refusal list; `~dev:opts.dev` threaded to `X86.program` |
|
||||
| `spike/x86/survey.sh` | working | `SURVEY_FLAGS`, given to **both** sides. Default unchanged |
|
||||
| `spike/x86/p7-slice-from-ptr.flan` | working, MATCH | negative length through a parameter and a `restart-case` |
|
||||
| `spike/x86/p8-cell.flan` | working, MATCH | direct call + function value, for `cells.sh` |
|
||||
| `spike/x86/cell-override.c` | working | `dlsym("flan.cell.twice")` + store, in a constructor |
|
||||
| `spike/x86/cells.sh` | working, 4/4 ok | the only test of the cell that can exist |
|
||||
| `test/survey-x86.sh` | working | `SURVEY_FLAGS`, given to **both** sides. Default unchanged |
|
||||
| `test/programs/x86-p7-slice-from-ptr.flan` | working, MATCH | negative length through a parameter and a `restart-case` |
|
||||
| `test/programs/x86-p8-cell.flan` | working, MATCH | direct call + function value, for `cells.sh` |
|
||||
| `test/cell-override.c` | working | `dlsym("flan.cell.twice")` + store, in a constructor |
|
||||
| `test/cells.sh` | working, 4/4 ok | the only test of the cell that can exist |
|
||||
|
||||
`lib/emit.ml` was **not** modified. No change to it was needed.
|
||||
|
||||
@ -94,10 +94,10 @@ All **working and verified**; nothing in this list is unverified or reverted.
|
||||
Nothing fought for an hour. Four short false starts, all mine and all one-line:
|
||||
|
||||
- `handler-bind` clause syntax guessed as an `fn` literal:
|
||||
`spike/x86/p7-slice-from-ptr.flan:30:18: a handler-bind clause is (Type [name] body ...)`.
|
||||
`test/programs/x86-p7-slice-from-ptr.flan:30:18: a handler-bind clause is (Type [name] body ...)`.
|
||||
The form is `(BoundsError [c] body ...)`; `test/programs/bounds-condition.flan:112` is the model.
|
||||
- `(defn show [name [u8] ...])` for a literal argument:
|
||||
`spike/x86/p7-slice-from-ptr.flan:37:14: expected [u8], found string`. A string literal wants `string`, not `[u8]`,
|
||||
`test/programs/x86-p7-slice-from-ptr.flan:37:14: expected [u8], found string`. A string literal wants `string`, not `[u8]`,
|
||||
even though they are the same two words at the machine level.
|
||||
- `cmp_imm` takes `~dst` and an `int`, not `~reg` and an `Int64`.
|
||||
- `cell-override.c`: `error: 'NULL' undeclared` — needs `<stddef.h>` beside `<dlfcn.h>`.
|
||||
@ -112,7 +112,7 @@ fixtures did **not** fail here — `/tmp` had room throughout (6% used at start
|
||||
|
||||
**Yes, reached and tested.** And the test is the interesting part, because *the corpus cannot do it*: a dev build
|
||||
starts with every cell pointing at the body that build compiled, so it prints exactly what a release build prints
|
||||
whether or not anything reads the cell. `spike/x86/cells.sh` preloads a `.so` whose constructor `dlsym`s
|
||||
whether or not anything reads the cell. `test/cells.sh` preloads a `.so` whose constructor `dlsym`s
|
||||
`flan.cell.twice` (the cells are in `.dynsym` — a dev build is `-rdynamic`) and stores a different body there. Four
|
||||
builds; the two release rows are the control that says the effect is the indirection and not symbol interposition:
|
||||
|
||||
@ -162,5 +162,5 @@ returned a struct. `cells.sh` does not reach it: the body it installs is `(i64,
|
||||
bounds check, every intermediate in memory, `rep movsb` block copies, and now an extra load per call site in a dev
|
||||
build — which is the one item `emit.ml` pays too.
|
||||
|
||||
**Also worth doing and not a backend item: run `spike/x86/survey.sh` in CI.** The 2 refusals this lane found were a
|
||||
**Also worth doing and not a backend item: run `test/survey-x86.sh` in CI.** The 2 refusals this lane found were a
|
||||
month-old lane's new prim, and nothing noticed. A backend that refuses by name does not rot quietly, but it does rot.
|
||||
|
||||
@ -1052,7 +1052,7 @@ it is written in the compiler, so there is no file to open.
|
||||
`C-c C-l` on a name opens `*flan-lowering*`: the LLVM IR the frontend emits for
|
||||
that function, what `llc` makes of it at `-O0` and at `-O2`, and what the
|
||||
hand-written x86 backend emits, all narrowed to the one function. It is
|
||||
`spike/x86/dump.sh` with a buffer around it. Reading one against another is the
|
||||
`tools/dump.sh` with a buffer around it. Reading one against another is the
|
||||
only way to check a lowering by eye, and the reason the second backend is
|
||||
trustworthy is that the two agree.
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; `spike/x86/dump.sh' prints four lowerings of one function side by side --
|
||||
;; `tools/dump.sh' prints four lowerings of one function side by side --
|
||||
;; the LLVM IR the frontend emits, what `llc' makes of it at -O0 and at -O2,
|
||||
;; and what the hand-written x86 backend emits. Reading one against another is
|
||||
;; the only way to check a lowering by eye, and the whole reason the second
|
||||
|
||||
14
lib/build.ml
14
lib/build.ml
@ -690,11 +690,23 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () =
|
||||
built against, so repointing either must not serve a stale .o. *)
|
||||
let tflags = match tflags with Some f -> f | None -> target_flags opts in
|
||||
let cc = compiler opts in
|
||||
(* A source that includes the dyn header was compiled against it, so the
|
||||
header is part of what the object depends on. Without it a change to a
|
||||
struct the header declares served an object built against the old layout
|
||||
— test/dyn_ops.c read a descriptor's new fields past the end of its own. *)
|
||||
let header =
|
||||
let needle = "flan_dyn.h" in
|
||||
let n = String.length needle and m = String.length src in
|
||||
let rec has i =
|
||||
i + n <= m && (String.sub src i n = needle || has (i + 1))
|
||||
in
|
||||
if has 0 then Runtime_src.dyn_header else ""
|
||||
in
|
||||
let key =
|
||||
Digest.to_hex
|
||||
(Digest.string
|
||||
(String.concat "\000"
|
||||
[ name; src; stamp_of cc; opts.opt;
|
||||
[ name; src; header; stamp_of cc; opts.opt;
|
||||
String.concat " " (cflags opts);
|
||||
String.concat " " tflags;
|
||||
String.concat " " warn ]))
|
||||
|
||||
221
lib/check.ml
221
lib/check.ml
@ -3662,14 +3662,9 @@ let rec key_pair env loc (k : Types.t) : Tast.fnref * Tast.fnref =
|
||||
fail loc
|
||||
"%s is a union, and a union is not a map key — key on the member you \
|
||||
meant" n
|
||||
| Types.Array (_, e) ->
|
||||
(* A fixed array of a struct or of strings would need the same per-element
|
||||
walk a struct key gets, driven by a loop rather than by a field list.
|
||||
Nothing has wanted one, so it is refused by name rather than written
|
||||
untested — and refused with the shape that does work named beside it. *)
|
||||
fail loc
|
||||
"a fixed array of %s is not a map key — a struct holding the array is"
|
||||
(Types.to_string e)
|
||||
(* A bytewise array took the arm above; this is one whose elements need
|
||||
their own pair, a string's or a struct's. *)
|
||||
| Types.Array (n, e) -> array_key_pair env loc n e
|
||||
| Types.Float _ ->
|
||||
(* Not a milestone question, which is why it is said separately: NaN is not
|
||||
equal to itself, and 0.0 and -0.0 are equal while differing bytewise. A
|
||||
@ -3801,6 +3796,129 @@ and struct_key_pair env loc n =
|
||||
Tast.Flanfn hname, Tast.Flanfn ename
|
||||
end
|
||||
|
||||
(* The pair for a fixed array whose elements are not bytewise: the struct
|
||||
pair's shape, with the field list replaced by a loop over the elements, so
|
||||
[[64 string]] is one call site in a loop and not sixty-four. Each element is
|
||||
hashed and compared by its own pair, so an array of structs holding strings
|
||||
is served by the same recursion. *)
|
||||
and array_key_pair env loc n e =
|
||||
if Int64.compare n 0L <= 0 then
|
||||
fail loc
|
||||
"%s has no elements, so it is not a map key — every value of it would be \
|
||||
the same key" (Types.to_string (Types.Array (n, e)));
|
||||
let aty = Types.Array (n, e) in
|
||||
(* The type's printed form, with what a symbol cannot hold replaced. *)
|
||||
let tag =
|
||||
String.map
|
||||
(fun c -> match c with
|
||||
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '-' -> c
|
||||
| _ -> '_')
|
||||
(Types.to_string aty)
|
||||
in
|
||||
(* The mangle is many-to-one — [a+b] and [a_b] come out alike — so a digest
|
||||
of the printed type, which is an identity, keeps two such keys apart. *)
|
||||
let tag =
|
||||
tag ^ "/" ^ String.sub (Digest.to_hex (Digest.string (Types.to_string aty))) 0 12
|
||||
in
|
||||
let hname = "map/hash/array/" ^ tag and ename = "map/eq/array/" ^ tag in
|
||||
let known name =
|
||||
List.exists (fun (f : Tast.fn) -> f.Tast.name = name) env.lifted
|
||||
in
|
||||
if known hname then Tast.Flanfn hname, Tast.Flanfn ename
|
||||
else begin
|
||||
let pty = Types.Ptr (Types.Mut, aty) in
|
||||
let hparams = [ pty; hash_ty; Types.Int Types.I64 ] in
|
||||
let eparams = [ pty; pty; Types.Int Types.I64 ] in
|
||||
let placeholder name ret params =
|
||||
{ Tast.name; params; slots = Array.of_list params;
|
||||
snames = Array.make (List.length params) None;
|
||||
ret; body = []; fdefers = []; fenv = None; fparent = None; floc = loc }
|
||||
in
|
||||
env.lifted <-
|
||||
placeholder hname hash_ty hparams
|
||||
:: placeholder ename (Types.Int Types.I8) eparams
|
||||
:: env.lifted;
|
||||
let h, eq = key_pair env loc e in
|
||||
let call ret f args =
|
||||
match f with
|
||||
| Tast.Rtfn s -> rt loc ret (direct s) args
|
||||
| Tast.Flanfn s | Tast.Fnval s -> mk loc ret (Tast.Call (s, args))
|
||||
in
|
||||
let elem_addr p i =
|
||||
let target = mk loc aty (Tast.Deref (mk loc pty (Tast.Local p))) in
|
||||
mk loc (Types.Ptr (Types.Mut, e))
|
||||
(Tast.Addr (Tast.Pindex (target, [ mk loc index_ty (Tast.Local i) ])))
|
||||
in
|
||||
(* The counter and its loop, which carries no break and no continue — the
|
||||
condition tast.ml puts on a [While] the checker invents. *)
|
||||
let loop ctx body =
|
||||
let i = fresh_slot ~name:"i" ctx index_ty in
|
||||
let iv = mk loc index_ty (Tast.Local i) in
|
||||
let limit = mk loc index_ty (Tast.Int (n, Types.I32)) in
|
||||
let one = mk loc index_ty (Tast.Int (1L, Types.I32)) in
|
||||
let cond = mk loc Types.Bool (Tast.Prim (Tast.Lt, [ iv; limit ])) in
|
||||
let step =
|
||||
mk loc Types.Unit
|
||||
(Tast.Set (Tast.Plocal i,
|
||||
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; one ]))))
|
||||
in
|
||||
mk loc Types.Unit
|
||||
(Tast.Let ([ (i, mk loc index_ty (Tast.Int (0L, Types.I32))) ],
|
||||
[ mk loc Types.Unit (Tast.While (cond, [ body i ], [ step ])) ]))
|
||||
in
|
||||
let hctx = invented_ctx env hash_ty in
|
||||
let kp = fresh_slot ~name:"key" hctx pty in
|
||||
let seed = fresh_slot ~name:"seed" hctx hash_ty in
|
||||
ignore (fresh_slot ~name:"size" hctx (Types.Int Types.I64));
|
||||
let acc = fresh_slot ~name:"h" hctx hash_ty in
|
||||
let hbody =
|
||||
[ mk loc Types.Unit
|
||||
(Tast.Set (Tast.Plocal acc, mk loc hash_ty (Tast.Local seed)));
|
||||
loop hctx (fun i ->
|
||||
let one =
|
||||
call hash_ty h
|
||||
[ elem_addr kp i; mk loc hash_ty (Tast.Local seed);
|
||||
size_of loc e ]
|
||||
in
|
||||
mk loc Types.Unit
|
||||
(Tast.Set (Tast.Plocal acc,
|
||||
rt loc hash_ty "flan_hash_combine"
|
||||
[ mk loc hash_ty (Tast.Local acc); one ])));
|
||||
mk loc hash_ty (Tast.Local acc) ]
|
||||
in
|
||||
let ectx = invented_ctx env (Types.Int Types.I8) in
|
||||
let ap = fresh_slot ~name:"a" ectx pty in
|
||||
let bp = fresh_slot ~name:"b" ectx pty in
|
||||
ignore (fresh_slot ~name:"size" ectx (Types.Int Types.I64));
|
||||
let i8 v = mk loc (Types.Int Types.I8) (Tast.Int (v, Types.I8)) in
|
||||
let ebody =
|
||||
[ loop ectx (fun i ->
|
||||
let same =
|
||||
call (Types.Int Types.I8) eq
|
||||
[ elem_addr ap i; elem_addr bp i; size_of loc e ]
|
||||
in
|
||||
mk loc Types.Unit
|
||||
(Tast.If (mk loc Types.Bool (Tast.Prim (Tast.Eq, [ same; i8 0L ])),
|
||||
mk loc Types.Never (Tast.Return (Some (i8 0L))),
|
||||
unit_at loc)));
|
||||
i8 1L ]
|
||||
in
|
||||
let finish name ret params ctx body =
|
||||
{ Tast.name; params;
|
||||
slots = Array.of_list (List.rev ctx.slot_tys);
|
||||
snames = Array.of_list (List.rev ctx.slot_names);
|
||||
ret; body; fdefers = []; fenv = None; fparent = None; floc = loc }
|
||||
in
|
||||
env.lifted <-
|
||||
finish hname hash_ty hparams hctx hbody
|
||||
:: finish ename (Types.Int Types.I8) eparams ectx ebody
|
||||
:: List.filter
|
||||
(fun (f : Tast.fn) ->
|
||||
f.Tast.name <> hname && f.Tast.name <> ename)
|
||||
env.lifted;
|
||||
Tast.Flanfn hname, Tast.Flanfn ename
|
||||
end
|
||||
|
||||
(* The pair as two expressions, ready to be passed. Their Flan type is
|
||||
[(Ptr ())]: one opaque word, which is all the backend needs. *)
|
||||
let key_fns env loc k =
|
||||
@ -9460,22 +9578,33 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
(while (map-next m (addr cur) (addr k) (addr v))
|
||||
...))
|
||||
|
||||
It is *not* a generic (map-keys m): a Vec of them needs a signature naming
|
||||
K, and a prelude defn cannot be written at every K. That one is generics,
|
||||
not iteration, and it stays refused for that reason.
|
||||
The prelude's map-keys and map-values are this loop over a generic key.
|
||||
|
||||
No hash and no equality pair go with it — walking asks nothing about a
|
||||
key — so this is the one map entry point whose signature carries neither,
|
||||
and the sizes are still needed because the runtime is type-erased. *)
|
||||
(* (map-next m cur k) walks the keys alone. It is what lets a walk need no
|
||||
place for a value, which matters when the value is a function value: one
|
||||
cannot be zeroed to make the place, and a key never is one. *)
|
||||
| "map-next" ->
|
||||
arity ctx loc name 4 args;
|
||||
(match args with
|
||||
| [ target; cur; k; v ] ->
|
||||
| [ _; _; _ ] | [ _; _; _; _ ] -> ()
|
||||
| _ ->
|
||||
fail loc
|
||||
"map-next is (map-next m (addr cursor) (addr k) (addr v)) or, for \
|
||||
the keys alone, (map-next m (addr cursor) (addr k)) — given %d \
|
||||
arguments" (List.length args));
|
||||
(match args with
|
||||
| target :: cur :: k :: rest ->
|
||||
let target = check_target ctx target in
|
||||
let kt, vt = map_kv loc "map-next" target.Tast.ty in
|
||||
let cur = check ctx ~want:(Types.Ptr (Types.Mut, (Types.Int Types.I64))) cur in
|
||||
let k = check ctx ~want:(Types.Ptr (Types.Mut, kt)) k in
|
||||
let v = check ctx ~want:(Types.Ptr (Types.Mut, vt)) v in
|
||||
let vp = Types.Ptr (Types.Mut, vt) in
|
||||
let v = match rest with
|
||||
| [ v ] -> check ctx ~want:vp v
|
||||
| _ -> mk loc vp (Tast.Zero vp)
|
||||
in
|
||||
let found =
|
||||
rt loc (Types.Int Types.I8) "flan_map_next"
|
||||
[ target; cur; k; v; size_of loc kt; size_of loc vt; here loc ]
|
||||
@ -9894,6 +10023,20 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
(* A Vec leaves here: everything below is written around a length the
|
||||
compiler can see, and a Vec's is a word the runtime reads. *)
|
||||
| Types.Vec elem -> vec_slice ctx ~want loc target elem bounds
|
||||
(* A dyn leaves too, as [at] over one does: the bounds are dyn, like
|
||||
[at]'s index, and a missing [hi] is nil, which the runtime reads as
|
||||
the length. *)
|
||||
| Types.Dyn ->
|
||||
let bound b = check ctx ~want:Types.Dyn b in
|
||||
let nil () = rt loc Types.Dyn "flan_dyn_nil" [] in
|
||||
let lo, hi = match bounds with
|
||||
| [] -> box loc (mk loc dyn_i64 (Tast.Int (0L, Types.I64))), nil ()
|
||||
| [ lo ] -> bound lo, nil ()
|
||||
| [ lo; hi ] -> bound lo, bound hi
|
||||
| _ -> assert false
|
||||
in
|
||||
expect ctx loc ~want
|
||||
(rt loc Types.Dyn "flan_dyn_slice" [ target; lo; hi; here loc ])
|
||||
| _ ->
|
||||
(* A string slices to a string, not to a [u8]: the result views the
|
||||
same bytes and is read-only for the same reason the source is, and
|
||||
@ -13540,8 +13683,11 @@ let rec hidden_dyn p seen (t : Types.t) : Types.t option =
|
||||
| Types.Dyn -> None
|
||||
| Types.Array (_, e) -> hidden_dyn p seen e
|
||||
| Types.Vec e | Types.Option e -> under e
|
||||
(* A Map's values are walked through the value type's own descriptor, so a
|
||||
dyn there is found wherever that descriptor finds one. A key never holds
|
||||
one: dyn is not a key type. *)
|
||||
| Types.Map (k, v) ->
|
||||
if dyn_anywhere p seen k || dyn_anywhere p seen v then Some t else None
|
||||
if dyn_anywhere p seen k then Some t else hidden_dyn p seen v
|
||||
(* A pointer and a slice are views of storage something else roots; see the
|
||||
note above. What they point at is checked where it is declared. *)
|
||||
| Types.Ptr (_, e) | Types.Slice (_, e) -> hidden_dyn p seen e
|
||||
@ -13661,53 +13807,8 @@ let rec holds_fn p seen (t : Types.t) =
|
||||
| None -> false)
|
||||
| _ -> false
|
||||
|
||||
(* The first Map under this type whose values hold a function value. A
|
||||
closure's environment is found by walking the storage a function value
|
||||
sits in, and a Map's storage is not walked — so an (Fn ...) there would be
|
||||
one the collector frees under it. A Vec's is, which is the container to
|
||||
use; and a (CFn ...) carries no environment and may go in a Map freely. *)
|
||||
let rec map_of_fn p seen (t : Types.t) : Types.t option =
|
||||
match t with
|
||||
| Types.Map (k, v) when holds_fn p [] k || holds_fn p [] v -> Some t
|
||||
| Types.Array (_, e) | Types.Vec e | Types.Option e
|
||||
| Types.Ptr (_, e) | Types.Slice (_, e) -> map_of_fn p seen e
|
||||
| Types.Map (_, v) -> map_of_fn p seen v
|
||||
| Types.Named n when not (List.mem n seen) ->
|
||||
let seen = n :: seen in
|
||||
let fields =
|
||||
match List.find_opt (fun (s : Tast.structure) -> s.Tast.sname = n)
|
||||
p.Tast.structs with
|
||||
| Some s -> s.Tast.fields
|
||||
| None ->
|
||||
match List.find_opt (fun (u : Tast.data) -> u.Tast.dname = n)
|
||||
p.Tast.datas with
|
||||
| Some u -> List.concat_map (fun (c : Tast.variant) -> c.Tast.vfields) u.Tast.cases
|
||||
| None ->
|
||||
match List.find_opt (fun (u : Tast.structure) -> u.Tast.sname = n)
|
||||
p.Tast.unions with
|
||||
| Some u -> u.Tast.fields
|
||||
| None -> []
|
||||
in
|
||||
List.fold_left
|
||||
(fun acc (fl : Tast.field) ->
|
||||
match acc with Some _ -> acc | None -> map_of_fn p seen fl.Tast.fty)
|
||||
None fields
|
||||
| _ -> None
|
||||
|
||||
let dyn_descriptors (p : Tast.program) =
|
||||
let check loc what (t : Types.t) =
|
||||
(match map_of_fn p [] t with
|
||||
| Some at ->
|
||||
Loc.failk "check/fn-in-map" loc
|
||||
"%s is %s%s, a Map whose values are function values. A function \
|
||||
value's environment is found by walking the storage it sits in, \
|
||||
and a Map's storage is not walked, so the collector would free an \
|
||||
environment still in use. Keep the function values in a Vec, or \
|
||||
make them (CFn ...) if they capture nothing"
|
||||
what (Types.to_string t)
|
||||
(if Types.equal t at then ""
|
||||
else Printf.sprintf ", and holds %s" (Types.to_string at))
|
||||
| None -> ());
|
||||
(match hidden_dyn p [] t with
|
||||
| Some at ->
|
||||
Loc.failk "check/dyn-descriptor" loc
|
||||
|
||||
130
lib/emit.ml
130
lib/emit.ml
@ -81,7 +81,7 @@ let cellname n = "@" ^ quoted (Mangle.cell n)
|
||||
{ ptr body, i64 word, ptr text }
|
||||
|
||||
The body is first, so everything that only ever wanted the body — a load
|
||||
of the cell, [spike/x86/cells.sh]'s store through [dlsym] — reads the
|
||||
of the cell, [test/cells.sh]'s store through [dlsym] — reads the
|
||||
same address it always did.
|
||||
|
||||
The word is what makes a signature change installable. A redefinition that
|
||||
@ -459,23 +459,26 @@ let align_up x a = if a <= 1 then x else ((x + a - 1) / a) * a
|
||||
pointer is four bytes and [goff] would name the wrong word. *)
|
||||
type gcword = { goff : int; gpath : (string * string list) list }
|
||||
|
||||
(* Every word of an instance the collector follows, by kind — the three
|
||||
(* Every word of an instance the collector follows, by kind — the four
|
||||
tables of runtime/flan_dyn.h's [flan_desc]. [gvec] carries each Vec's
|
||||
element type, whose own descriptor the entry points at. *)
|
||||
element type and [gmap] each Map's value type, whose own descriptor the
|
||||
entry points at. *)
|
||||
type gclayout = {
|
||||
gdyn : gcword list;
|
||||
genv : gcword list;
|
||||
gvec : (gcword * Types.t) list;
|
||||
gmap : (gcword * Types.t) list;
|
||||
}
|
||||
|
||||
(* A descriptor this module has to write out: its symbol, the words, the
|
||||
instance size, and the symbol of each Vec entry's element descriptor in
|
||||
[gvec]'s order. *)
|
||||
[gvec]'s order and of each Map entry's value descriptor in [gmap]'s. *)
|
||||
type desc = {
|
||||
dsym : string;
|
||||
dlay : gclayout;
|
||||
dsize : int;
|
||||
dvecs : string list;
|
||||
dmaps : string list;
|
||||
}
|
||||
|
||||
(* ── Module-level state ────────────────────────────────────────────── *)
|
||||
@ -741,16 +744,12 @@ and dyn_offsets m (t : Types.t) : int list =
|
||||
— which is a run-time question a static descriptor cannot answer.
|
||||
Refused in [Check] rather than described wrongly here. *)
|
||||
| None -> acc)
|
||||
(* [Types.Option], [Types.Vec] and [Types.Map] fall through here with no
|
||||
arm of their own and answer no offsets, which is correct only because
|
||||
nothing reaches this function holding one with a dyn inside it:
|
||||
(* [Types.Option] and [Types.Vec] fall through here with no arm of their
|
||||
own and answer no offsets, which is correct only because nothing
|
||||
reaches this function holding one with a dyn inside it:
|
||||
[Check.hidden_dyn] refuses that at every global, parameter, return and
|
||||
frame slot first. If that gate is ever relaxed — the typed-container
|
||||
view the M2 queue's item 3 is building is exactly the kind of change
|
||||
that would relax it for [Vec]/[Map] — this arm has to grow alongside
|
||||
it, the way the array and struct arms above already walk their own
|
||||
storage; until then a silent [] here would be an unrooted dyn, not a
|
||||
refusal. *)
|
||||
frame slot first. A [Types.Map]'s dyn values are not words of the
|
||||
instance at all; [gc_layout] names the header in its [gmap] table. *)
|
||||
| _ -> acc
|
||||
in
|
||||
List.sort_uniq compare (go [] 0 t [])
|
||||
@ -794,7 +793,7 @@ let desc_mangle (t : Types.t) =
|
||||
symbol. *)
|
||||
let rec desc_of m (t : Types.t) : string option =
|
||||
let l = gc_layout m t in
|
||||
if l.gdyn = [] && l.genv = [] && l.gvec = [] then None
|
||||
if l.gdyn = [] && l.genv = [] && l.gvec = [] && l.gmap = [] then None
|
||||
else
|
||||
let key = Types.to_string t in
|
||||
match Hashtbl.find_opt m.descs key with
|
||||
@ -806,17 +805,19 @@ let rec desc_of m (t : Types.t) : string option =
|
||||
(* Claimed before the elements are asked for, so the counter a nested
|
||||
element's symbol takes cannot be this one's. *)
|
||||
Hashtbl.replace m.descs key
|
||||
{ dsym = sym; dlay = l; dsize = fst (lay m t); dvecs = [] };
|
||||
let dvecs =
|
||||
{ dsym = sym; dlay = l; dsize = fst (lay m t); dvecs = []; dmaps = [] };
|
||||
let elems what l =
|
||||
List.map
|
||||
(fun (_, e) ->
|
||||
match desc_of m e with
|
||||
| Some s -> s
|
||||
| None -> internal "a Vec entry whose element has no words")
|
||||
l.gvec
|
||||
| None -> internal "a %s entry whose element has no words" what)
|
||||
l
|
||||
in
|
||||
let dvecs = elems "Vec" l.gvec in
|
||||
let dmaps = elems "Map" l.gmap in
|
||||
Hashtbl.replace m.descs key
|
||||
{ dsym = sym; dlay = l; dsize = fst (lay m t); dvecs };
|
||||
{ dsym = sym; dlay = l; dsize = fst (lay m t); dvecs; dmaps };
|
||||
Some sym
|
||||
|
||||
(* ── The words the collector follows ─────────────────────────────────
|
||||
@ -845,7 +846,7 @@ let rec desc_of m (t : Types.t) : string option =
|
||||
at the same x86-64 offset and at different wasm32 ones, and marking a word
|
||||
twice costs nothing. *)
|
||||
and gc_layout m (t : Types.t) : gclayout =
|
||||
let dyn = ref [] and env = ref [] and vec = ref [] in
|
||||
let dyn = ref [] and env = ref [] and vec = ref [] and map = ref [] in
|
||||
let step ty idx path = path @ [ (ty, idx) ] in
|
||||
let rec go ~full seen off path (t : Types.t) =
|
||||
match t with
|
||||
@ -858,6 +859,13 @@ and gc_layout m (t : Types.t) : gclayout =
|
||||
[desc_of] claims before it recurses, is what closes that loop. *)
|
||||
| Types.Vec e when m.gcfn ->
|
||||
if reaches_fn m [] e then vec := ({ goff = off; gpath = path }, e) :: !vec
|
||||
(* A Map's values, when they hold a function value's environment or a
|
||||
dyn. The key never does: neither is a key type. The value type's own
|
||||
descriptor is what the entry points at, so a Map of Maps is walked
|
||||
through the inner one's. *)
|
||||
| Types.Map (_, v) ->
|
||||
if (m.gcfn && reaches_fn m [] v) || reaches_dyn m [] v then
|
||||
map := ({ goff = off; gpath = path }, v) :: !map
|
||||
| Types.Array (n, e) ->
|
||||
let s, _ = lay m e in
|
||||
for i = 0 to Int64.to_int n - 1 do
|
||||
@ -924,7 +932,8 @@ and gc_layout m (t : Types.t) : gclayout =
|
||||
in
|
||||
let uniq l = List.sort_uniq order l in
|
||||
{ gdyn = uniq !dyn; genv = uniq !env;
|
||||
gvec = List.sort_uniq (fun (a, _) (b, _) -> order a b) !vec }
|
||||
gvec = List.sort_uniq (fun (a, _) (b, _) -> order a b) !vec;
|
||||
gmap = List.sort_uniq (fun (a, _) (b, _) -> order a b) !map }
|
||||
|
||||
(* Whether an [(Fn ...)] is anywhere in a value's storage, a Vec's elements
|
||||
included. A type met again on the way contributes nothing more, which
|
||||
@ -933,7 +942,8 @@ and gc_layout m (t : Types.t) : gclayout =
|
||||
and reaches_fn m seen (t : Types.t) =
|
||||
match t with
|
||||
| Types.Fn _ -> true
|
||||
| Types.Array (_, e) | Types.Vec e | Types.Option e -> reaches_fn m seen e
|
||||
| Types.Array (_, e) | Types.Vec e | Types.Option e | Types.Map (_, e) ->
|
||||
reaches_fn m seen e
|
||||
| Types.Named nm when not (List.mem nm seen) ->
|
||||
let seen = nm :: seen in
|
||||
let fields =
|
||||
@ -950,17 +960,35 @@ and reaches_fn m seen (t : Types.t) =
|
||||
List.exists (fun (fl : Tast.field) -> reaches_fn m seen fl.Tast.fty) fields
|
||||
| _ -> false
|
||||
|
||||
(* Whether a dyn word is anywhere [gc_layout] records one: directly, in a
|
||||
fixed array, in a struct field, or in a Map's values. The other places a
|
||||
dyn could sit are refused by [Check.hidden_dyn]. *)
|
||||
and reaches_dyn m seen (t : Types.t) =
|
||||
match t with
|
||||
| Types.Dyn -> true
|
||||
| Types.Array (_, e) | Types.Map (_, e) -> reaches_dyn m seen e
|
||||
| Types.Named nm when not (List.mem nm seen) ->
|
||||
(match Hashtbl.find_opt m.structs nm with
|
||||
| Some st ->
|
||||
List.exists
|
||||
(fun (fl : Tast.field) -> reaches_dyn m (nm :: seen) fl.Tast.fty)
|
||||
st.Tast.fields
|
||||
| None -> false)
|
||||
| _ -> false
|
||||
|
||||
(* Whether the collector has anything to follow in a value of this type —
|
||||
the question every rooting decision asks. [dyn_offsets <> []] was that
|
||||
question until an [Fn] could hold an environment. *)
|
||||
let traced m (t : Types.t) =
|
||||
t = Types.Dyn
|
||||
|| (let l = gc_layout m t in l.gdyn <> [] || l.genv <> [] || l.gvec <> [])
|
||||
|| (let l = gc_layout m t in
|
||||
l.gdyn <> [] || l.genv <> [] || l.gvec <> [] || l.gmap <> [])
|
||||
|
||||
(* The words to clear before an instance at a pushed root can be marked, as
|
||||
x86-64 byte offsets of eight-byte words: each dyn word, each environment
|
||||
word, and each Vec header's pointer and length. The LLVM backend walks
|
||||
[gpath] instead; see [zero_words]. *)
|
||||
word, each Vec header's pointer and length, and each Map header's block
|
||||
pointer and capacity. The LLVM backend walks [gpath] instead; see
|
||||
[zero_words]. *)
|
||||
let gc_zero_offsets m (t : Types.t) : int list =
|
||||
if t = Types.Dyn then [ 0 ]
|
||||
else
|
||||
@ -968,6 +996,7 @@ let gc_zero_offsets m (t : Types.t) : int list =
|
||||
List.map (fun w -> w.goff) l.gdyn
|
||||
@ List.map (fun w -> w.goff) l.genv
|
||||
@ List.concat_map (fun (w, _) -> [ w.goff; w.goff + 8 ]) l.gvec
|
||||
@ List.concat_map (fun (w, _) -> [ w.goff; w.goff + 16 ]) l.gmap
|
||||
|
||||
(* A [gpath] as an LLVM constant expression over [base]: nested constant
|
||||
[getelementptr]s, one per step. Over [ptr null] and through [ptrtoint] it
|
||||
@ -4273,7 +4302,12 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
|
||||
(fun ((w : gcword), _) ->
|
||||
store "ptr null" (w.gpath @ [ ("%vec", [ "i32 0"; "i32 0" ]) ]);
|
||||
store "i64 0" (w.gpath @ [ ("%vec", [ "i32 0"; "i32 1" ]) ]))
|
||||
l.gvec
|
||||
l.gvec;
|
||||
List.iter
|
||||
(fun ((w : gcword), _) ->
|
||||
store "ptr null" (w.gpath @ [ ("%map", [ "i32 0"; "i32 0" ]) ]);
|
||||
store "i64 0" (w.gpath @ [ ("%map", [ "i32 0"; "i32 2" ]) ]))
|
||||
l.gmap
|
||||
end
|
||||
in
|
||||
let push base (ty : Types.t) =
|
||||
@ -4941,6 +4975,7 @@ declare i64 @flan_dyn_ge(i64, i64, ptr, i64)
|
||||
declare i64 @flan_dyn_eq(i64, i64)
|
||||
declare i64 @flan_dyn_len(i64)
|
||||
declare i64 @flan_dyn_at(i64, i64, ptr, i64)
|
||||
declare i64 @flan_dyn_slice(i64, i64, i64, ptr, i64)
|
||||
declare void @flan_dyn_set_at(i64, i64, i64, ptr, i64)
|
||||
declare void @flan_dyn_push(i64, i64, ptr, i64)
|
||||
declare void @flan_dyn_print(i64)
|
||||
@ -5095,7 +5130,7 @@ let uses_dyn (p : Tast.program) =
|
||||
let rec carries seen (t : Types.t) =
|
||||
match t with
|
||||
| Types.Dyn -> true
|
||||
| Types.Array (_, e) -> carries seen e
|
||||
| Types.Array (_, e) | Types.Map (_, e) -> carries seen e
|
||||
| Types.Named n when not (List.mem n seen) ->
|
||||
(match Hashtbl.find_opt structs n with
|
||||
| Some st ->
|
||||
@ -5129,12 +5164,13 @@ let emit_main m ?(startup = false) ?(gc = false) ?(dyn_globals = []) (fn : Tast.
|
||||
dyn global's initialiser runs in the startup function below, and the very
|
||||
first thing it does is allocate. *)
|
||||
if gc then Buffer.add_string b " call void @flan_gc_init()\n";
|
||||
(* Before anything can allocate a Vec block: a program that can make a
|
||||
collector-owned closure environment has flan_rt.c report every Vec block
|
||||
to the collector, which reads a Vec's elements only through a block it
|
||||
knows to be live (runtime/flan_dyn.c, "The Vec blocks a marker may
|
||||
read"). *)
|
||||
if m.gcfn then Buffer.add_string b " call void @flan_dyn_track_vecs()\n";
|
||||
(* Before anything can allocate a Vec or Map block: a program that can make
|
||||
a collector-owned closure environment, or that holds a dyn, has
|
||||
flan_rt.c report every such block to the collector, which reads a
|
||||
container's elements only through a block it knows to be live
|
||||
(runtime/flan_dyn.c, "The Vec blocks a marker may read"). *)
|
||||
if m.gcfn || gc then
|
||||
Buffer.add_string b " call void @flan_dyn_track_vecs()\n";
|
||||
(* The dyn globals, rooted here and never popped, which is the whole of what
|
||||
a global's extent means. They go on the stack *before* the startup
|
||||
function runs, because that function is what fills them and its first
|
||||
@ -5408,21 +5444,24 @@ let descriptors m =
|
||||
let l = d.dlay in
|
||||
let offs = table d.dsym "offs" "i64" (List.map word l.gdyn) in
|
||||
let envs = table d.dsym "envs" "i64" (List.map word l.genv) in
|
||||
let vecs =
|
||||
table d.dsym "vecs" "{ i64, ptr }"
|
||||
let pairs suffix words syms =
|
||||
table d.dsym suffix "{ i64, ptr }"
|
||||
(List.map2
|
||||
(fun ((w : gcword), _) e ->
|
||||
Printf.sprintf "{ i64, ptr } { i64 %s, ptr @\"%s\" }"
|
||||
(offset_const w) e)
|
||||
l.gvec d.dvecs)
|
||||
words syms)
|
||||
in
|
||||
let vecs = pairs "vecs" l.gvec d.dvecs in
|
||||
let maps = pairs "maps" l.gmap d.dmaps in
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf
|
||||
"@\"%s\" = private unnamed_addr constant \
|
||||
{ i64, i64, ptr, i64, ptr, i64, ptr } \
|
||||
{ i64 %d, i64 %d, ptr %s, i64 %d, ptr %s, i64 %d, ptr %s }\n"
|
||||
{ i64, i64, ptr, i64, ptr, i64, ptr, i64, ptr } \
|
||||
{ i64 %d, i64 %d, ptr %s, i64 %d, ptr %s, i64 %d, ptr %s, \
|
||||
i64 %d, ptr %s }\n"
|
||||
d.dsym d.dsize (List.length l.gdyn) offs (List.length l.genv)
|
||||
envs (List.length l.gvec) vecs));
|
||||
envs (List.length l.gvec) vecs (List.length l.gmap) maps));
|
||||
Buffer.contents b
|
||||
|
||||
(* The same table in the other backend's syntax. It lives here rather than in
|
||||
@ -5466,19 +5505,22 @@ let descriptors_asm m =
|
||||
in
|
||||
let offs = table "offs" (List.map (fun w -> string_of_int w.goff) l.gdyn) in
|
||||
let envs = table "envs" (List.map (fun w -> string_of_int w.goff) l.genv) in
|
||||
let vecs =
|
||||
table "vecs"
|
||||
let pairs suffix words syms =
|
||||
table suffix
|
||||
(List.concat
|
||||
(List.map2
|
||||
(fun ((w : gcword), _) e -> [ string_of_int w.goff; ".L" ^ e ])
|
||||
l.gvec d.dvecs))
|
||||
words syms))
|
||||
in
|
||||
let vecs = pairs "vecs" l.gvec d.dvecs in
|
||||
let maps = pairs "maps" l.gmap d.dmaps in
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf
|
||||
"\t.align\t8\n.L%s:\n\t.quad\t%d\n\t.quad\t%d\n\t.quad\t%s\n\
|
||||
\t.quad\t%d\n\t.quad\t%s\n\t.quad\t%d\n\t.quad\t%s\n"
|
||||
\t.quad\t%d\n\t.quad\t%s\n\t.quad\t%d\n\t.quad\t%s\n\
|
||||
\t.quad\t%d\n\t.quad\t%s\n"
|
||||
d.dsym d.dsize (List.length l.gdyn) offs (List.length l.genv) envs
|
||||
(List.length l.gvec) vecs))
|
||||
(List.length l.gvec) vecs (List.length l.gmap) maps))
|
||||
rows;
|
||||
Buffer.contents b
|
||||
|
||||
|
||||
@ -135,7 +135,7 @@
|
||||
|
||||
{1 Where this stops, and what the next lane picks up}
|
||||
|
||||
[spike/js/survey.sh] is the standing measurement: 24 MATCH, 0 DIFFER, 77
|
||||
[test/survey-js.sh] is the standing measurement: 24 MATCH, 0 DIFFER, 77
|
||||
refused by name, 0 that node would not run, over the corpus and this
|
||||
file's own two probes. What the refusals say about the order to work in:
|
||||
|
||||
|
||||
@ -554,16 +554,42 @@ let source = {flan|
|
||||
;; in. Owned by the caller: (free v), or let a (free-all a) take the region.
|
||||
;;
|
||||
;; This is the one that proves the containers and the generics compose. It
|
||||
;; allocates — (vec-new t), push, returns (Vec t) — and the type-erased Vec
|
||||
;; allocates — (vec-new $t), push, returns (Vec $t) — and the type-erased Vec
|
||||
;; runtime needed no change at all, because SizeOf and AlignOf are computed at
|
||||
;; the instantiation site, where the element type is concrete.
|
||||
(defn filter [s [const $t] keep? (Fn [$t] bool)] (Vec $t)
|
||||
(let [v (vec-new t)]
|
||||
(let [v (vec-new $t)]
|
||||
(dotimes [i (length s)]
|
||||
(when (keep? (at s i))
|
||||
(push v (at s i))))
|
||||
v))
|
||||
|
||||
;; A map's keys, and its values, as a new Vec the caller owns. In block order,
|
||||
;; which is the hash's and not the insertion's — sort what comes back if the
|
||||
;; order matters. A string key is copied as the view it is, so the Vec reads
|
||||
;; the map's own key bytes and is good for as long as they are.
|
||||
(defn map-keys [m (Map $k $v)] (Vec $k)
|
||||
{:where (hashable? $k)}
|
||||
(let [out (vec-new $k)
|
||||
cur (i64 0)
|
||||
key (the $k (zeroed))]
|
||||
(while (map-next m (addr cur) (addr key))
|
||||
(push out key))
|
||||
out))
|
||||
|
||||
(defn map-values [m (Map $k $v)] (Vec $v)
|
||||
{:where (hashable? $k)}
|
||||
;; Walked by key and read back with get, because a place to copy a value
|
||||
;; into would have to be zeroed first, and a function value cannot be.
|
||||
(let [out (vec-new $v)
|
||||
cur (i64 0)
|
||||
key (the $k (zeroed))]
|
||||
(while (map-next m (addr cur) (addr key))
|
||||
(match (get m key)
|
||||
(Some val) (push out val)
|
||||
None (do)))
|
||||
out))
|
||||
|
||||
;; ── The sign questions, over every numeric type at once ───────────────
|
||||
;;
|
||||
;; The family the whole of generics was asked for. Three questions about a
|
||||
@ -1925,15 +1951,6 @@ let source = {flan|
|
||||
;; that did not come with them, because it is one copy
|
||||
;; per *ordered pair* of types rather than per type,
|
||||
;; which is where a per-type family stops being honest.
|
||||
;; map-keys, map-values Generics — and the reason changed, which is the
|
||||
;; point of naming them separately. It used to be the
|
||||
;; missing Map iterator; `map-next` is that iterator
|
||||
;; and walking a map is expressible now. What a defn
|
||||
;; still cannot say is (defn map-keys [m {K V}] (Vec K)):
|
||||
;; a prelude function has to name its types, and there
|
||||
;; is no K. The loop is three lines at the call site,
|
||||
;; where K is known, and that is where it stays until
|
||||
;; there are generics.
|
||||
;;
|
||||
;; Builder Not refused — declined. strings.Builder in Odin
|
||||
;; wraps a [dynamic]u8; here the (Vec u8) *is* that and
|
||||
|
||||
13
lib/x86.ml
13
lib/x86.ml
@ -1,6 +1,6 @@
|
||||
(** Tast -> x86-64, by hand. The dev backend; LLVM stays the release one.
|
||||
|
||||
Grown out of [spike/backend/x86.ml], which proved the shape. What is new
|
||||
Grown out of a spike's [x86.ml], which proved the shape and is in git history. What is new
|
||||
here is everything the spike enumerated and did not do: aggregates, floats,
|
||||
globals, string literals, the transfer channel, and a whole program rather
|
||||
than one function.
|
||||
@ -4273,7 +4273,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
transfer exit are dead because no path names that exit. This used to be a
|
||||
refusal, on the theory that a function with a defer and no transfer exit
|
||||
was a sign the reasoning had gone wrong. It is not — it is every leaf
|
||||
function with a defer, and [spike/x86/p9-dead-defers.flan] is ten lines
|
||||
function with a defer, and [test/programs/x86-p9-dead-defers.flan] is ten lines
|
||||
of it. [emit.ml]'s [emit_fn] writes the whole exit under the same
|
||||
[if f.unwound], and so drops them too.
|
||||
|
||||
@ -4512,9 +4512,10 @@ let emit_main ?(ann = false) ?(startup = false) ?(gc = false)
|
||||
xor_rr b ~dst:rax ~src:rax;
|
||||
call_sym b "flan_gc_init"
|
||||
end;
|
||||
(* A program that can make a collector-owned closure environment has the
|
||||
collector told of every Vec block from here on; see [Emit.emit_main]. *)
|
||||
if md.Emit.gcfn then begin
|
||||
(* A program that can make a collector-owned closure environment, or holds
|
||||
a dyn, has the collector told of every Vec and Map block from here on;
|
||||
see [Emit.emit_main]. *)
|
||||
if md.Emit.gcfn || gc then begin
|
||||
xor_rr b ~dst:rax ~src:rax;
|
||||
call_sym b "flan_dyn_track_vecs"
|
||||
end;
|
||||
@ -5048,7 +5049,7 @@ let program ~checks ?(dev = false) ?(debug = false) ?(annotate = false)
|
||||
# installed while the process runs is reached by the next call.\n\
|
||||
#\n\
|
||||
# What is not here are mnemonics. The bytes are a blob so that every\n\
|
||||
# offset stays exactly known, and spike/x86/dump.sh puts objdump's\n\
|
||||
# offset stays exactly known, and tools/dump.sh puts objdump's\n\
|
||||
# disassembly of this same object beside this file: that one says what,\n\
|
||||
# and this one says why.\n";
|
||||
(* A numbered [.file] is what stops clang's integrated assembler from
|
||||
|
||||
2
plan.org
2
plan.org
@ -525,7 +525,7 @@ on.
|
||||
machine code directly and is selected with ~--x86~; it exists because ~llc~ is
|
||||
most of the 19ms above. It is a different route from the same typed IR to the
|
||||
same observable behaviour, not a different semantics, and what holds it to that
|
||||
is ~spike/x86/survey.sh~: every program in the corpus is built both ways and
|
||||
is ~test/survey-x86.sh~: every program in the corpus is built both ways and
|
||||
byte-compared on stdout, stderr and exit status. At the time of writing that is
|
||||
103 MATCH, 0 DIFFER, 0 refused by name. It handles conditions, bounds checks,
|
||||
indirection cells, redefinition modules and DWARF line tables; what it does not
|
||||
|
||||
@ -131,6 +131,8 @@ typedef uint64_t flan_dyn;
|
||||
* - [vecs]: a (Vec T) header whose elements hold words of their own, with the
|
||||
* element's descriptor. The marker reads the header's pointer and length
|
||||
* where they are, so a push that reallocated is seen.
|
||||
* - [maps]: a (Map K V) header whose values hold words of their own, with the
|
||||
* value's descriptor. Read the same way, and walked over the full slots.
|
||||
*
|
||||
* [size] is the stride of one instance as the compiler's element-size
|
||||
* arithmetic counts it, which is what a Vec's elements are laid out at. The
|
||||
@ -149,6 +151,8 @@ typedef struct flan_desc {
|
||||
const int64_t *envs;
|
||||
int64_t nvec;
|
||||
const flan_desc_vec *vecs;
|
||||
int64_t nmap;
|
||||
const flan_desc_vec *maps;
|
||||
} flan_desc;
|
||||
|
||||
#define DYN_QNAN 0xFFF8000000000000ULL
|
||||
@ -248,6 +252,38 @@ void flan_dyn_vec_hdr_layout(int64_t out[6]) {
|
||||
out[5] = (int64_t)offsetof(flan_dyn_vec_hdr, epoch);
|
||||
}
|
||||
|
||||
/* flan_map, restated for the same reason and read the same way: only
|
||||
* [data], [log2cap], [alloc] and [epoch]. With it, the three numbers of the
|
||||
* block's geometry the marker needs — flan_rt.c's FLAN_MAP_HEAD, _GROUP and
|
||||
* _ALIGN. If either file's table changes, change both. */
|
||||
typedef struct flan_dyn_map_hdr {
|
||||
void *data;
|
||||
int64_t len;
|
||||
int64_t log2cap;
|
||||
void *alloc;
|
||||
int64_t epoch;
|
||||
} flan_dyn_map_hdr;
|
||||
|
||||
#define DYN_MAP_HEAD 24
|
||||
#define DYN_MAP_GROUP 8
|
||||
#define DYN_MAP_ALIGN 64
|
||||
#define DYN_MAP_FULL 0x80
|
||||
|
||||
/* This mirror's numbers, compared against flan_rt.c's [flan_map_layout] by
|
||||
* test/dyn_ops.c's "layout" mode. */
|
||||
void flan_dyn_map_hdr_layout(int64_t out[10]) {
|
||||
out[0] = (int64_t)sizeof(flan_dyn_map_hdr);
|
||||
out[1] = (int64_t)offsetof(flan_dyn_map_hdr, data);
|
||||
out[2] = (int64_t)offsetof(flan_dyn_map_hdr, len);
|
||||
out[3] = (int64_t)offsetof(flan_dyn_map_hdr, log2cap);
|
||||
out[4] = (int64_t)offsetof(flan_dyn_map_hdr, alloc);
|
||||
out[5] = (int64_t)offsetof(flan_dyn_map_hdr, epoch);
|
||||
out[6] = DYN_MAP_HEAD;
|
||||
out[7] = DYN_MAP_GROUP;
|
||||
out[8] = DYN_MAP_ALIGN;
|
||||
out[9] = DYN_MAP_FULL;
|
||||
}
|
||||
|
||||
/* flan_allocator's prefix, far enough to read the one word a stale-container
|
||||
* check needs. The struct has more fields after [epoch]; this file never
|
||||
* touches them; and the alignment of a leading same-typed prefix is the same
|
||||
@ -1259,6 +1295,64 @@ typedef struct { char *p; int64_t n; const flan_desc *e; } vec_work;
|
||||
static vec_work *vstack;
|
||||
static int64_t vstack_n, vstack_cap;
|
||||
|
||||
/* Maps still to walk: a live block's control run, its slots, the slot count,
|
||||
* the stride and value offset the block's own head records, and the value's
|
||||
* descriptor. Queued for the Vec queue's reason. */
|
||||
typedef struct {
|
||||
const uint8_t *ctrl; char *slots; int64_t cap, stride, voff;
|
||||
const flan_desc *e;
|
||||
} map_work;
|
||||
static map_work *mapstack;
|
||||
static int64_t mapstack_n, mapstack_cap;
|
||||
|
||||
/* A live block, not reset since it was made: the checks a Vec's block and a
|
||||
* Map's share. The allocator header is never freed, so its epoch is always
|
||||
* readable. */
|
||||
static vblock *live_block(void *p) {
|
||||
vblock *b = vblock_find((uintptr_t)p);
|
||||
if (b == NULL) return NULL;
|
||||
if (b->alloc != NULL
|
||||
&& (int64_t)((flan_dyn_alloc_hdr *)b->alloc)->epoch != b->epoch)
|
||||
return NULL;
|
||||
return b;
|
||||
}
|
||||
|
||||
/* Queue the map whose header is at [h]. The slot count comes from the
|
||||
* header and everything else from the block, and the walk is bounded by the
|
||||
* block's recorded size, so a stale header copy naming a block another map
|
||||
* now owns reads nothing outside that block. */
|
||||
static void queue_map(const flan_dyn_map_hdr *h, const flan_desc *e) {
|
||||
vblock *b;
|
||||
const int64_t *head;
|
||||
int64_t cap, ctrl, stride, voff;
|
||||
if (e == NULL || h->data == NULL || h->log2cap <= 0 || h->log2cap > 40)
|
||||
return;
|
||||
b = live_block(h->data);
|
||||
if (b == NULL || b->bytes < DYN_MAP_HEAD) return;
|
||||
cap = (int64_t)1 << h->log2cap;
|
||||
ctrl = (DYN_MAP_HEAD + cap + (DYN_MAP_GROUP - 1) + (DYN_MAP_ALIGN - 1))
|
||||
& ~(int64_t)(DYN_MAP_ALIGN - 1);
|
||||
head = (const int64_t *)h->data;
|
||||
stride = head[1];
|
||||
voff = head[2];
|
||||
if (stride <= 0 || voff < 0 || voff + e->size > stride) return;
|
||||
if (ctrl > b->bytes || (b->bytes - ctrl) / stride < cap) return;
|
||||
if (mapstack_n == mapstack_cap) {
|
||||
int64_t c = mapstack_cap ? mapstack_cap * 2 : 16;
|
||||
map_work *m = (map_work *)realloc(mapstack, (size_t)c * sizeof *m);
|
||||
if (m == NULL) trap_oom(NULL, 0, c * (int64_t)sizeof *m);
|
||||
mapstack = m;
|
||||
mapstack_cap = c;
|
||||
}
|
||||
mapstack[mapstack_n].ctrl = (const uint8_t *)h->data + DYN_MAP_HEAD;
|
||||
mapstack[mapstack_n].slots = (char *)h->data + ctrl;
|
||||
mapstack[mapstack_n].cap = cap;
|
||||
mapstack[mapstack_n].stride = stride;
|
||||
mapstack[mapstack_n].voff = voff;
|
||||
mapstack[mapstack_n].e = e;
|
||||
mapstack_n++;
|
||||
}
|
||||
|
||||
/* The words [d] names inside the instance at [base]. A Vec entry is checked
|
||||
* against the live blocks above and queued; [mark_desc] drains the queue
|
||||
* before it returns. */
|
||||
@ -1266,17 +1360,17 @@ static void mark_words(char *base, const flan_desc *d) {
|
||||
int64_t j;
|
||||
for (j = 0; j < d->n; j++) mark_value(*(flan_dyn *)(base + d->offs[j]));
|
||||
for (j = 0; j < d->nenv; j++) mark_env(*(uintptr_t *)(base + d->envs[j]));
|
||||
for (j = 0; j < d->nmap; j++)
|
||||
queue_map((const flan_dyn_map_hdr *)(base + d->maps[j].off),
|
||||
d->maps[j].elem);
|
||||
for (j = 0; j < d->nvec; j++) {
|
||||
flan_dyn_vec_hdr *h = (flan_dyn_vec_hdr *)(base + d->vecs[j].off);
|
||||
const flan_desc *e = d->vecs[j].elem;
|
||||
vblock *b;
|
||||
int64_t n;
|
||||
if (e == NULL || e->size <= 0 || h->len <= 0) continue;
|
||||
b = vblock_find((uintptr_t)h->ptr);
|
||||
b = live_block(h->ptr);
|
||||
if (b == NULL) continue;
|
||||
if (b->alloc != NULL
|
||||
&& (int64_t)((flan_dyn_alloc_hdr *)b->alloc)->epoch != b->epoch)
|
||||
continue;
|
||||
n = b->bytes / e->size;
|
||||
if (h->len < n) n = h->len;
|
||||
if (vstack_n == vstack_cap) {
|
||||
@ -1295,10 +1389,17 @@ static void mark_words(char *base, const flan_desc *d) {
|
||||
|
||||
static void mark_desc(char *base, const flan_desc *d) {
|
||||
mark_words(base, d);
|
||||
while (vstack_n > 0) {
|
||||
vec_work w = vstack[--vstack_n];
|
||||
while (vstack_n > 0 || mapstack_n > 0) {
|
||||
int64_t i;
|
||||
for (i = 0; i < w.n; i++) mark_words(w.p + i * w.e->size, w.e);
|
||||
if (vstack_n > 0) {
|
||||
vec_work w = vstack[--vstack_n];
|
||||
for (i = 0; i < w.n; i++) mark_words(w.p + i * w.e->size, w.e);
|
||||
} else {
|
||||
map_work w = mapstack[--mapstack_n];
|
||||
for (i = 0; i < w.cap; i++)
|
||||
if (w.ctrl[i] & DYN_MAP_FULL)
|
||||
mark_words(w.slots + i * w.stride + w.voff, w.e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1355,7 +1456,8 @@ static void gc_sweep(void) {
|
||||
void *flan_dyn_env_new(int64_t size, const flan_desc *d) {
|
||||
flan_obj *o = gc_alloc(OBJ_ENV, size);
|
||||
o->len = size;
|
||||
o->u.env.desc = (d != NULL && (d->n > 0 || d->nenv > 0 || d->nvec > 0))
|
||||
o->u.env.desc = (d != NULL && (d->n > 0 || d->nenv > 0 || d->nvec > 0
|
||||
|| d->nmap > 0))
|
||||
? d : NULL;
|
||||
memset(o + 1, 0, (size_t)size);
|
||||
envset_put((uintptr_t)(o + 1));
|
||||
@ -1384,7 +1486,7 @@ void flan_dyn_root_push(flan_dyn *slot) { root_add(slot, NULL); }
|
||||
* compiler found no dyn in — but it still occupies an entry, because the count
|
||||
* is what the epilogue knows, and it is turned into an empty descriptor rather
|
||||
* than stored as NULL, which on this stack means something else. */
|
||||
static const flan_desc desc_empty = { 0, 0, NULL, 0, NULL, 0, NULL };
|
||||
static const flan_desc desc_empty = { 0, 0, NULL, 0, NULL, 0, NULL, 0, NULL };
|
||||
|
||||
void flan_dyn_root_push_desc(void *base, const flan_desc *d) {
|
||||
root_add(base, d == NULL ? &desc_empty : d);
|
||||
@ -2930,6 +3032,33 @@ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i, const uint8_t *loc,
|
||||
return o->u.v.items[k];
|
||||
}
|
||||
|
||||
/* (slice s lo) and (slice s lo hi) over a text; nil for [hi] is the length.
|
||||
* The typed slice of a string is a view, and this is a copy: a text is
|
||||
* immutable, so no program can tell the two apart. A vec's slice would have
|
||||
* to share its elements with the vec to mean what the typed one means, which
|
||||
* a copy does not, so a vec traps by type rather than answering differently. */
|
||||
flan_dyn flan_dyn_slice(flan_dyn v, flan_dyn lo, flan_dyn hi,
|
||||
const uint8_t *loc, int64_t loclen) {
|
||||
int64_t a, b, len;
|
||||
flan_obj *o;
|
||||
if (!is_text(v))
|
||||
trap2(loc, loclen, TYPE_TRAP, "slice", "only a text is sliced", v, lo);
|
||||
o = dyn_obj(v);
|
||||
len = o->len;
|
||||
a = need_index(loc, loclen, "slice", v, lo);
|
||||
b = flan_dyn_tag(hi) == FLAN_DYN_TAG_NIL
|
||||
? len : need_index(loc, loclen, "slice", v, hi);
|
||||
if (a < 0 || b < a || b > len) {
|
||||
char sv[SAY_MAX];
|
||||
say(sv, SAY_MAX, v);
|
||||
flan_say(loc, loclen,
|
||||
"dyn slice: [%lld %lld) is out of bounds for text of length %lld "
|
||||
"— %s", (long long)a, (long long)b, (long long)len, sv);
|
||||
flan_trap((const uint8_t *)"DynRange", 8);
|
||||
}
|
||||
return flan_dyn_from_bytes(obj_text_bytes(o) + a, b - a);
|
||||
}
|
||||
|
||||
void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x, const uint8_t *loc,
|
||||
int64_t loclen) {
|
||||
int64_t k;
|
||||
|
||||
@ -56,8 +56,11 @@ typedef uint64_t flan_dyn;
|
||||
* pointer-sized, holding a collector-allocated environment, null, or a
|
||||
* widened function's code address, told apart by the collector's own set of
|
||||
* environments and never by dereferencing — and [vecs], each a (Vec T)
|
||||
* header at [off] whose live elements are marked through [elem]. A descriptor
|
||||
* with only dyn words leaves the last four fields zero.
|
||||
* header at [off] whose live elements are marked through [elem] — and
|
||||
* [maps], each a (Map K V) header at [off] whose full slots' values are marked
|
||||
* through [elem]; a key never holds a word the collector follows, because no
|
||||
* such type is a key. A descriptor with only dyn words leaves the last six
|
||||
* fields zero.
|
||||
*
|
||||
* Nothing in this ABI ever writes a descriptor. See [flan_dyn_root_push_desc]
|
||||
* and [flan_dyn_env_new]. */
|
||||
@ -75,6 +78,8 @@ typedef struct flan_desc {
|
||||
const int64_t *envs;
|
||||
int64_t nvec;
|
||||
const flan_desc_vec *vecs;
|
||||
int64_t nmap;
|
||||
const flan_desc_vec *maps;
|
||||
} flan_desc;
|
||||
|
||||
/* ── Constructors ──────────────────────────────────────────────────── */
|
||||
@ -217,6 +222,11 @@ flan_dyn flan_dyn_len(flan_dyn v);
|
||||
flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i, const uint8_t *loc,
|
||||
int64_t loclen);
|
||||
|
||||
/* A copy of the text's bytes [lo, hi); nil for [hi] is the length. A vec, or
|
||||
* any other value, traps: see the definition. */
|
||||
flan_dyn flan_dyn_slice(flan_dyn v, flan_dyn lo, flan_dyn hi,
|
||||
const uint8_t *loc, int64_t loclen);
|
||||
|
||||
/* Vec only — a text is immutable and says so rather than being copied. */
|
||||
void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x, const uint8_t *loc,
|
||||
int64_t loclen);
|
||||
@ -498,6 +508,10 @@ void flan_gc_set_floor(int64_t bytes);
|
||||
* otherwise does. */
|
||||
void flan_dyn_vec_hdr_layout(int64_t out[6]);
|
||||
|
||||
/* flan_dyn.c's mirror of flan_map and of the block geometry the marker reads,
|
||||
* compared against flan_rt.c's [flan_map_layout] the same way. */
|
||||
void flan_dyn_map_hdr_layout(int64_t out[10]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -2561,13 +2561,14 @@ void flan_vec_region_only(flan_vec *v, const uint8_t *loc, int64_t loclen) {
|
||||
loc, loclen);
|
||||
}
|
||||
|
||||
/* Told of every Vec block this file allocates, moves or frees: the old block
|
||||
* (or NULL), the new one (or NULL), its size in bytes, and the allocator and
|
||||
* epoch it was made under. NULL unless flan_dyn.c's [flan_dyn_track_vecs] has
|
||||
* installed its own — a program that can make a collector-owned closure
|
||||
* environment, which may sit in a Vec, installs it so the collector never
|
||||
* reads a block a stale header copy still names. A pointer rather than a
|
||||
* call so this file names nothing in flan_dyn.c. */
|
||||
/* Told of every Vec block and every Map block this file allocates, moves or
|
||||
* frees: the old block (or NULL), the new one (or NULL), its size in bytes,
|
||||
* and the allocator and epoch it was made under. NULL unless flan_dyn.c's
|
||||
* [flan_dyn_track_vecs] has installed its own — a program that can make a
|
||||
* collector-owned closure environment or holds a dyn, either of which may sit
|
||||
* in a Vec or a Map, installs it so the collector never reads a block a stale
|
||||
* header copy still names. A pointer rather than a call so this file names
|
||||
* nothing in flan_dyn.c. */
|
||||
void (*flan_vec_block_hook)(void *old, void *fresh, int64_t bytes, void *alloc,
|
||||
int64_t epoch) = NULL;
|
||||
|
||||
@ -2824,6 +2825,22 @@ typedef struct flan_map {
|
||||
int64_t epoch;
|
||||
} flan_map;
|
||||
|
||||
/* The header's layout and the block geometry's constants, for the same check
|
||||
* [flan_vec_layout] exists for: flan_dyn.c restates both to walk a map's full
|
||||
* slots, and test/dyn_ops.c's "layout" mode compares the two. */
|
||||
void flan_map_layout(int64_t out[10]) {
|
||||
out[0] = (int64_t)sizeof(flan_map);
|
||||
out[1] = (int64_t)offsetof(flan_map, data);
|
||||
out[2] = (int64_t)offsetof(flan_map, len);
|
||||
out[3] = (int64_t)offsetof(flan_map, log2cap);
|
||||
out[4] = (int64_t)offsetof(flan_map, alloc);
|
||||
out[5] = (int64_t)offsetof(flan_map, epoch);
|
||||
out[6] = FLAN_MAP_HEAD;
|
||||
out[7] = FLAN_MAP_GROUP;
|
||||
out[8] = FLAN_MAP_ALIGN;
|
||||
out[9] = FLAN_CTRL_FULL;
|
||||
}
|
||||
|
||||
/* ── Hashing ──────────────────────────────────────────────────────────
|
||||
*
|
||||
* FNV-1a over the bytes, then a final avalanche. FNV alone leaves the low bits
|
||||
@ -3339,6 +3356,10 @@ static int8_t flan_map_rebuild(flan_map *m, int64_t log2cap, int64_t ksize,
|
||||
a->proc(a, FLAN_ALLOC_FREE, m->data,
|
||||
flan_map_block_size(ksize, vsize, old_cap), 0, FLAN_MAP_ALIGN);
|
||||
}
|
||||
if (flan_vec_block_hook)
|
||||
flan_vec_block_hook(m->data, fresh.data,
|
||||
flan_map_block_size(ksize, vsize, flan_map_cap(&fresh)),
|
||||
m->alloc, m->epoch);
|
||||
m->data = fresh.data;
|
||||
m->log2cap = fresh.log2cap;
|
||||
return 1;
|
||||
@ -3546,7 +3567,8 @@ int8_t flan_map_next(flan_map *m, int64_t *cursor, void *kout, void *vout,
|
||||
for (; i < cap; i++) {
|
||||
if (!(g.ctrl[i] & FLAN_CTRL_FULL)) continue;
|
||||
memcpy(kout, flan_map_k(&g, i), (size_t)ksize);
|
||||
memcpy(vout, flan_map_v(&g, i), (size_t)vsize);
|
||||
/* NULL from (map-next m cur k), the keys-only walk. */
|
||||
if (vout) memcpy(vout, flan_map_v(&g, i), (size_t)vsize);
|
||||
*cursor = i + 1;
|
||||
return 1;
|
||||
}
|
||||
@ -3586,6 +3608,8 @@ void flan_map_free(flan_map *m, int64_t ksize, int64_t vsize,
|
||||
m->alloc->proc(m->alloc, FLAN_ALLOC_FREE, m->data,
|
||||
flan_map_block_size(ksize, vsize, flan_map_cap(m)), 0,
|
||||
FLAN_MAP_ALIGN);
|
||||
if (m->data && flan_vec_block_hook)
|
||||
flan_vec_block_hook(m->data, NULL, 0, NULL, 0);
|
||||
m->data = NULL;
|
||||
m->len = 0;
|
||||
m->log2cap = 0;
|
||||
|
||||
@ -1,177 +0,0 @@
|
||||
(* The spike's harness: run the real frontend, lower the functions it produced
|
||||
with [X86], put the bytes in executable memory, call them, and compare with
|
||||
what the language says they should answer.
|
||||
|
||||
The comparison is the whole point. Reading the bytes proves nothing -- a
|
||||
disassembly that looks right and a program that returns the wrong number is
|
||||
the normal outcome of hand-encoding, which is why the oracle here is the
|
||||
arithmetic and not objdump. [oracle.sh] disassembles the same buffer, and
|
||||
that is a debugging aid, not the evidence. *)
|
||||
|
||||
external jit_alloc : int -> nativeint = "spike_jit_alloc"
|
||||
external jit_write : nativeint -> string -> unit = "spike_jit_write"
|
||||
external jit_protect : nativeint -> int -> unit = "spike_jit_protect"
|
||||
external call1 : nativeint -> int64 -> int64 = "spike_call1"
|
||||
external call2 : nativeint -> int64 -> int64 -> int64 = "spike_call2"
|
||||
external sym : string -> nativeint = "spike_sym"
|
||||
|
||||
let failures = ref 0
|
||||
let checks = ref 0
|
||||
|
||||
let check name got want =
|
||||
incr checks;
|
||||
if got = want then Printf.printf " ok %-28s = %Ld\n" name got
|
||||
else begin
|
||||
incr failures;
|
||||
Printf.printf " FAIL %-28s = %Ld, want %Ld\n" name got want
|
||||
end
|
||||
|
||||
(* One page per function, so that a function that runs off its own end lands in
|
||||
an unmapped page and segfaults at the fault rather than in the middle of the
|
||||
next function. This is the crudest possible version of the code-object
|
||||
question the whole exercise is really about. *)
|
||||
let page = 4096
|
||||
|
||||
let install (code : string) : nativeint =
|
||||
if String.length code > page then failwith "function exceeds one page";
|
||||
let p = jit_alloc page in
|
||||
jit_write p code;
|
||||
jit_protect p page;
|
||||
p
|
||||
|
||||
let run src =
|
||||
let decls =
|
||||
Flan.Load.program ~file:src (Flan.Parse.program_all (Flan.Reader.read_file src))
|
||||
in
|
||||
let prog = Flan.Check.program_all decls.Flan.Load.decls in
|
||||
Printf.printf "frontend: %d fns, %d globals, %d structs, %d externs\n"
|
||||
(List.length prog.Flan.Tast.fns) (List.length prog.Flan.Tast.globals)
|
||||
(List.length prog.Flan.Tast.structs) (List.length prog.Flan.Tast.externs);
|
||||
|
||||
(* Two passes, because [spike-calls] calls functions whose addresses are not
|
||||
known until they are installed. Pass one installs every function at a
|
||||
fixed page; pass two emits the real code into it. A real backend does this
|
||||
with relocations; the spike does it by emitting twice, which is the same
|
||||
answer with none of the machinery. *)
|
||||
let addrs : (string, nativeint) Hashtbl.t = Hashtbl.create 16 in
|
||||
let unsupported = ref [] in
|
||||
let lowerable =
|
||||
List.filter
|
||||
(fun (fd : Flan.Tast.fn) ->
|
||||
try
|
||||
ignore (X86.fn ~resolve:(fun _ -> 0L) fd);
|
||||
true
|
||||
with X86.Unsupported m ->
|
||||
unsupported := (fd.Flan.Tast.name, m) :: !unsupported;
|
||||
false)
|
||||
prog.Flan.Tast.fns
|
||||
in
|
||||
List.iter
|
||||
(fun (fd : Flan.Tast.fn) ->
|
||||
Hashtbl.replace addrs fd.Flan.Tast.name (jit_alloc page))
|
||||
lowerable;
|
||||
let resolve name =
|
||||
match Hashtbl.find_opt addrs name with
|
||||
| Some p -> Int64.of_nativeint p
|
||||
| None ->
|
||||
(* Not a Flan function: a runtime entry point, looked up the way a dev
|
||||
build already reaches the host's symbols -- through the dynamic symbol
|
||||
table, which --dev links with -rdynamic. *)
|
||||
Int64.of_nativeint (sym name)
|
||||
in
|
||||
let bytes = Hashtbl.create 16 in
|
||||
List.iter
|
||||
(fun (fd : Flan.Tast.fn) ->
|
||||
let code = X86.fn ~resolve fd in
|
||||
Hashtbl.replace bytes fd.Flan.Tast.name code;
|
||||
let p = Hashtbl.find addrs fd.Flan.Tast.name in
|
||||
jit_write p code;
|
||||
jit_protect p page)
|
||||
lowerable;
|
||||
|
||||
Printf.printf "lowered: %d of %d functions\n"
|
||||
(List.length lowerable) (List.length prog.Flan.Tast.fns);
|
||||
List.iter (fun (n, m) -> Printf.printf " skipped %-20s %s\n" n m)
|
||||
(List.rev !unsupported);
|
||||
Hashtbl.iter (fun n c -> Printf.printf " %-20s %4d bytes at %nx\n"
|
||||
n (String.length c) (Hashtbl.find addrs n)) bytes;
|
||||
|
||||
(* The bytes that actually ran, dumped where run.sh can objdump them.
|
||||
A debugging aid and not the evidence: a disassembly that reads correctly
|
||||
next to a function that answers 656 when it should answer 650 is the
|
||||
normal outcome of hand-encoding, which is why the checks below compare
|
||||
numbers. *)
|
||||
(match Sys.getenv_opt "SPIKE_DUMP" with
|
||||
| None -> ()
|
||||
| Some dir ->
|
||||
Hashtbl.iter
|
||||
(fun n c ->
|
||||
let oc = open_out_bin (Filename.concat dir (n ^ ".bin")) in
|
||||
output_string oc c; close_out oc)
|
||||
bytes);
|
||||
|
||||
(* ── The SysV boundary ──────────────────────────────────────────────
|
||||
Three synthetic functions, built as Tast by hand rather than written in
|
||||
Flan, because the surface language has no way to spell a call to an
|
||||
arbitrary C symbol with eight arguments. [Tast.Rt] is the node a runtime
|
||||
call already uses and the one a [declare-c] shim lands on, so this is the
|
||||
real path with a made-up callee. *)
|
||||
let loc = Flan.Loc.unknown in
|
||||
let i64 = Flan.Types.Int Flan.Types.I64 in
|
||||
let ex e = { Flan.Tast.e; ty = i64; loc } in
|
||||
let lit n = ex (Flan.Tast.Int (Int64.of_int n, Flan.Types.I64)) in
|
||||
let probe name params body =
|
||||
{ Flan.Tast.name; params; slots = Array.make (List.length params) i64;
|
||||
snames = Array.make (List.length params) None; ret = i64;
|
||||
body = [ body ]; fdefers = []; fparent = None; floc = loc }
|
||||
in
|
||||
let arg0 = ex (Flan.Tast.Local 0) in
|
||||
let probes = [
|
||||
(* Eight integers: six in registers and two on the stack, which is the case
|
||||
a register-only convention gets silently wrong. *)
|
||||
probe "abi-8" [ i64 ]
|
||||
(ex (Flan.Tast.Prim (Flan.Tast.Rt "spike_probe8",
|
||||
[ arg0; lit 2; lit 3; lit 4; lit 5; lit 6; lit 7; lit 8 ])));
|
||||
(* rsp % 16 == 0 at the call. The callee does an aligned 16-byte spill and
|
||||
answers -1 if it was entered misaligned. *)
|
||||
probe "abi-align" [ i64 ]
|
||||
(ex (Flan.Tast.Prim (Flan.Tast.Rt "spike_probe_align", [ arg0 ])));
|
||||
(* The same call, but underneath a binary operator -- so it is evaluated
|
||||
with the left operand spilled on the stack. This is the one that matters:
|
||||
alignment at a call site is not a property of the prologue, it is a
|
||||
property of how much the expression evaluator has pushed. *)
|
||||
probe "abi-align-nested" [ i64 ]
|
||||
(ex (Flan.Tast.Prim (Flan.Tast.Add,
|
||||
[ lit 0;
|
||||
ex (Flan.Tast.Prim (Flan.Tast.Rt "spike_probe_align", [ arg0 ])) ])));
|
||||
] in
|
||||
List.iter
|
||||
(fun (fd : Flan.Tast.fn) ->
|
||||
let code = X86.fn ~resolve fd in
|
||||
let p = jit_alloc page in
|
||||
jit_write p code; jit_protect p page;
|
||||
Hashtbl.replace addrs fd.Flan.Tast.name p)
|
||||
probes;
|
||||
|
||||
print_endline "results:";
|
||||
let at n = Hashtbl.find addrs n in
|
||||
check "spike-add 3 4" (call2 (at "spike-add") 3L 4L) 7L;
|
||||
check "spike-add -5 2" (call2 (at "spike-add") (-5L) 2L) (-3L);
|
||||
check "spike-arith 10 4" (call2 (at "spike-arith") 10L 4L) 19L;
|
||||
check "spike-let 6" (call1 (at "spike-let") 6L) 1332L;
|
||||
check "spike-if 1 2" (call2 (at "spike-if") 1L 2L) 1L;
|
||||
check "spike-if 9 2" (call2 (at "spike-if") 9L 2L) 7L;
|
||||
check "spike-calls 5" (call1 (at "spike-calls") 5L) 656L;
|
||||
check "abi-8 1" (call1 (at "abi-8") 1L) 87654321L;
|
||||
check "abi-align 10" (call1 (at "abi-align") 10L) 13L;
|
||||
check "abi-align-nested 10" (call1 (at "abi-align-nested") 10L) 13L;
|
||||
|
||||
Printf.printf "\n%d checks, %d failures\n" !checks !failures;
|
||||
exit (if !failures = 0 then 0 else 1)
|
||||
|
||||
(* The frontend's diagnostics printed rather than swallowed: a spike that says
|
||||
[Fatal error: exception Errors(_)] costs an hour. *)
|
||||
let () =
|
||||
try run Sys.argv.(1) with
|
||||
| Flan.Loc.Error d -> prerr_endline (Flan.Loc.report d); exit 2
|
||||
| Flan.Loc.Errors ds -> prerr_endline (Flan.Loc.report_all ds); exit 2
|
||||
@ -1,87 +0,0 @@
|
||||
(* Histogram of Tast expr_kind constructors over the reachable program. A
|
||||
measurement, not a backend: it answers "what would a whole-program x86
|
||||
build actually have to lower for this input", which is the question that
|
||||
decides whether whole-program coverage is reachable at all. *)
|
||||
let tbl : (string, int) Hashtbl.t = Hashtbl.create 64
|
||||
|
||||
let bump k =
|
||||
Hashtbl.replace tbl k (1 + (try Hashtbl.find tbl k with Not_found -> 0))
|
||||
|
||||
let name (k : Flan.Tast.expr_kind) =
|
||||
match k with
|
||||
| Int _ -> "Int" | Float _ -> "Float" | Bool _ -> "Bool" | Str _ -> "Str"
|
||||
| Unit -> "Unit" | Zero _ -> "Zero" | Uninit _ -> "Uninit"
|
||||
| Local _ -> "Local" | Global _ -> "Global" | Prim _ -> "Prim"
|
||||
| Call _ -> "Call" | FnAddr _ -> "FnAddr" | CallPtr _ -> "CallPtr"
|
||||
| Do _ -> "Do" | Let _ -> "Let" | If _ -> "If" | While _ -> "While"
|
||||
| Return _ -> "Return" | Break _ -> "Break" | Continue _ -> "Continue"
|
||||
| Set _ -> "Set" | Field _ -> "Field" | Addr _ -> "Addr" | Deref _ -> "Deref"
|
||||
| Make _ -> "Make" | MakeCase _ -> "MakeCase" | CaseField _ -> "CaseField"
|
||||
| Arr _ -> "Arr" | Some_ _ -> "Some" | None_ -> "None" | Match _ -> "Match"
|
||||
| UnwrapSome _ -> "UnwrapSome" | Signal _ -> "Signal" | Handled _ -> "Handled"
|
||||
| RestartCase _ -> "RestartCase" | WithAlloc _ -> "WithAlloc"
|
||||
| InvokeRestart _ -> "InvokeRestart"
|
||||
|
||||
let pname (p : Flan.Tast.prim) =
|
||||
match p with
|
||||
| Add -> "Add" | Sub -> "Sub" | Mul -> "Mul" | Div -> "Div" | Rem -> "Rem"
|
||||
| Eq -> "Eq" | Ne -> "Ne" | Lt -> "Lt" | Le -> "Le" | Gt -> "Gt" | Ge -> "Ge"
|
||||
| Not -> "Not" | BitAnd -> "BitAnd" | BitOr -> "BitOr" | BitXor -> "BitXor"
|
||||
| Shl -> "Shl" | Shr -> "Shr" | Len -> "Len" | At -> "At" | Slice -> "Slice"
|
||||
| Bytes -> "Bytes" | BytesToF64 -> "BytesToF64" | BytesToI64 -> "BytesToI64"
|
||||
| F64ToBytes -> "F64ToBytes" | I64ToBytes -> "I64ToBytes"
|
||||
| StrOfBytes -> "StrOfBytes" | U64ToBytes -> "U64ToBytes"
|
||||
| EscapeBytes -> "EscapeBytes" | WriteStdout -> "WriteStdout" | Exit -> "Exit"
|
||||
| Argv -> "Argv" | Rt s -> "Rt:" ^ s | SizeOf _ -> "SizeOf"
|
||||
| AlignOf _ -> "AlignOf" | AddrOf -> "AddrOf" | Cast _ -> "Cast"
|
||||
|
||||
let rec ex (e : Flan.Tast.expr) =
|
||||
bump (name e.e);
|
||||
match e.e with
|
||||
| Prim (p, xs) -> bump ("prim/" ^ pname p); List.iter ex xs
|
||||
| Call (_, xs) | Arr xs -> List.iter ex xs
|
||||
| Make (_, xs) | MakeCase (_, _, xs) -> List.iter ex xs
|
||||
| CallPtr (f, xs) -> ex f; List.iter ex xs
|
||||
| Do xs | Handled (_, xs) -> List.iter ex xs
|
||||
| Let (bs, body) -> List.iter (fun (_, x) -> ex x) bs; List.iter ex body
|
||||
| If (a, b, c) -> ex a; ex b; ex c
|
||||
| While (c, b, l) -> ex c; List.iter ex b; List.iter ex l
|
||||
| Return (Some x) | Some_ x | Deref x | UnwrapSome x | Field (x, _)
|
||||
| CaseField (x, _, _) | Signal (_, _, x) -> ex x
|
||||
| Set (p, x) -> pl p; ex x
|
||||
| Addr p -> pl p
|
||||
| Match (x, arms) ->
|
||||
ex x;
|
||||
List.iter (fun (a : Flan.Tast.arm) -> List.iter ex a.abody) arms
|
||||
| RestartCase (cs, x) ->
|
||||
List.iter (fun (c : Flan.Tast.rclause) -> List.iter ex c.rbody) cs; ex x
|
||||
| WithAlloc (a, b) -> ex a; List.iter ex b
|
||||
| InvokeRestart (_, _, xs, _, _, _) -> List.iter ex xs
|
||||
| _ -> ()
|
||||
|
||||
and pl (p : Flan.Tast.place) =
|
||||
match p with
|
||||
| Plocal _ -> bump "place/Plocal"
|
||||
| Pglobal _ -> bump "place/Pglobal"
|
||||
| Pfield (x, _) -> bump "place/Pfield"; ex x
|
||||
| Pindex (x, ys) -> bump "place/Pindex"; ex x; List.iter ex ys
|
||||
| Pderef x -> bump "place/Pderef"; ex x
|
||||
|
||||
let () =
|
||||
let src = Sys.argv.(1) in
|
||||
let l =
|
||||
Flan.Load.program ~file:src
|
||||
(Flan.Parse.program_all (Flan.Reader.read_file src))
|
||||
in
|
||||
let p = Flan.Check.program_all l.Flan.Load.decls in
|
||||
let p, _, _ = Flan.Reach.link l p in
|
||||
List.iter
|
||||
(fun (f : Flan.Tast.fn) -> List.iter ex f.body; List.iter ex f.fdefers)
|
||||
p.Flan.Tast.fns;
|
||||
List.iter (fun (g : Flan.Tast.global) -> ex g.Flan.Tast.ginit)
|
||||
p.Flan.Tast.globals;
|
||||
Printf.printf "%s: %d reachable fns\n" (Filename.basename src)
|
||||
(List.length p.Flan.Tast.fns);
|
||||
let rows = Hashtbl.fold (fun k v a -> (k, v) :: a) tbl [] in
|
||||
let rows = List.sort (fun (a, _) (b, _) -> compare a b) rows in
|
||||
List.iter (fun (k, v) -> Printf.printf " %-24s %d\n" k v) rows
|
||||
@ -1,100 +0,0 @@
|
||||
/* The three things OCaml cannot do for itself: get executable memory, put
|
||||
* bytes in it, and jump to them. Everything interesting is in x86.ml; this
|
||||
* file is deliberately dumb.
|
||||
*
|
||||
* Shaped after lib/dynload_stubs.c's rule, which spike/embed took verbatim for
|
||||
* the same reason: the boundary passes pointers and scalars, never an OCaml
|
||||
* [value] into foreign storage. Nothing here keeps anything.
|
||||
*
|
||||
* RW then mprotect to R+X, never RWX in one mmap: a hardened kernel may refuse
|
||||
* a writable-executable anonymous mapping outright, and a policy denial that
|
||||
* comes back as a null pointer reads exactly like an encoding bug. */
|
||||
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/memory.h>
|
||||
#include <caml/alloc.h>
|
||||
#include <caml/fail.h>
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
value spike_jit_alloc(value vlen) {
|
||||
size_t len = (size_t)Long_val(vlen);
|
||||
void *p = mmap(NULL, len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (p == MAP_FAILED) caml_failwith("spike_jit_alloc: mmap failed");
|
||||
return caml_copy_nativeint((intnat)p);
|
||||
}
|
||||
|
||||
value spike_jit_write(value vp, value vbytes) {
|
||||
char *p = (char *)Nativeint_val(vp);
|
||||
memcpy(p, String_val(vbytes), caml_string_length(vbytes));
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
value spike_jit_protect(value vp, value vlen) {
|
||||
void *p = (void *)Nativeint_val(vp);
|
||||
if (mprotect(p, (size_t)Long_val(vlen), PROT_READ | PROT_EXEC) != 0)
|
||||
caml_failwith("spike_jit_protect: mprotect failed");
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
/* Every Flan function's emitted signature is its parameters followed by the
|
||||
* transfer channel (emit.ml, [signature]), so the trampolines below all pass a
|
||||
* trailing pointer. Nothing in the spike transfers, so it is NULL. */
|
||||
typedef int64_t (*fn1)(int64_t, void *);
|
||||
typedef int64_t (*fn2)(int64_t, int64_t, void *);
|
||||
|
||||
value spike_call1(value vp, value a) {
|
||||
return caml_copy_int64(((fn1)Nativeint_val(vp))(Int64_val(a), NULL));
|
||||
}
|
||||
value spike_call2(value vp, value a, value b) {
|
||||
return caml_copy_int64(((fn2)Nativeint_val(vp))(Int64_val(a), Int64_val(b), NULL));
|
||||
}
|
||||
|
||||
value spike_sym(value vname) {
|
||||
void *h = dlsym(RTLD_DEFAULT, String_val(vname));
|
||||
if (h == NULL) caml_failwith("spike_sym: not found");
|
||||
return caml_copy_nativeint((intnat)h);
|
||||
}
|
||||
|
||||
/* ── The C side of the ABI probes ──────────────────────────────────── */
|
||||
|
||||
/* Eight integers: six in registers, two on the stack, which is the case a
|
||||
* register-only convention silently gets wrong. The answer is positional so a
|
||||
* swapped pair cannot pass. */
|
||||
int64_t spike_probe8(int64_t a, int64_t b, int64_t c, int64_t d,
|
||||
int64_t e, int64_t f, int64_t g, int64_t h) {
|
||||
return a * 1 + b * 10 + c * 100 + d * 1000 + e * 10000 + f * 100000
|
||||
+ g * 1000000 + h * 10000000;
|
||||
}
|
||||
|
||||
/* The alignment check, and it has to be done with an aligned load rather than
|
||||
* by reading rsp, because that is how raylib finds out: the SysV ABI promises
|
||||
* rsp % 16 == 0 at the call instruction, so on entry rsp+8 is aligned, and a
|
||||
* callee that spills an __m128 to its frame faults when it is not. -O2 is what
|
||||
* turns this into an actual movaps; without it the bug hides. */
|
||||
__attribute__((noinline))
|
||||
int64_t spike_probe_align(int64_t x) {
|
||||
volatile double v[2] __attribute__((aligned(16))) = { 1.0, 2.0 };
|
||||
/* Reading rsp as well, so a failure says which of the two it was. */
|
||||
uintptr_t sp;
|
||||
__asm__ volatile ("mov %%rsp, %0" : "=r"(sp));
|
||||
if ((sp % 16) != 8) return -1; /* entry rsp is call-site rsp minus 8 */
|
||||
return x + (int64_t)(v[0] + v[1]);
|
||||
}
|
||||
|
||||
/* No float probe either, for a plainer reason: this emitter has no SSE, so
|
||||
* there is nothing here that could call one. Floats are counted as work in
|
||||
* docs/BUILT.md, "Layout was already owned, and that is why the drift fear
|
||||
* was misplaced", rather than claimed as done.
|
||||
*
|
||||
* And no struct-by-value probe, and that is a finding rather than an
|
||||
* omission: check.ml rejects an aggregate in a [declare] signature and the
|
||||
* generated shim flattens every one, so no Flan-emitted call ever passes a
|
||||
* struct to C. The aggregate problem is real but it is on the Flan-to-Flan
|
||||
* side, which is measured in docs/BUILT.md, "The obstacle that was named
|
||||
* first, and dissolved", and not from here. */
|
||||
@ -1,24 +0,0 @@
|
||||
;; The spike's input. Ordinary Flan, run through the ordinary frontend --
|
||||
;; Reader, Parse, Load, Check -- so that what the emitter below lowers is the
|
||||
;; same Tast.fn the LLVM backend gets and not a literal someone typed to make
|
||||
;; the exercise come out.
|
||||
|
||||
(defn spike-add [a i64 b i64] i64
|
||||
(+ a b))
|
||||
|
||||
(defn spike-arith [a i64 b i64] i64
|
||||
(- (* a 3) (+ b 7)))
|
||||
|
||||
(defn spike-let [a i64] i64
|
||||
(let [x (* a a)
|
||||
y (+ x 1)]
|
||||
(* x y)))
|
||||
|
||||
(defn spike-if [a i64 b i64] i64
|
||||
(if (< a b) (- b a) (- a b)))
|
||||
|
||||
(defn spike-calls [a i64] i64
|
||||
(spike-add (spike-arith a 2) (spike-let a)))
|
||||
|
||||
(defn main [] i32
|
||||
0)
|
||||
@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# The spike, end to end: the real frontend produces a Tast, x86.ml turns it
|
||||
# into bytes, the bytes go into an mmap, and the mmap gets called.
|
||||
#
|
||||
# Driven by hand with ocamlfind and clang against the flan.cmxa dune already
|
||||
# builds, exactly as spike/embed does and for the same reason: nothing under
|
||||
# spike/ is wired into the build, so there is no dune file here and `dune test`
|
||||
# cannot see any of it.
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
cd "$root" || exit 1
|
||||
|
||||
dune build --root . lib/flan.cmxa 2>&1 | head -20
|
||||
|
||||
out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
|
||||
|
||||
# The C stubs. -O2 on purpose: spike_probe_align's aligned load only becomes a
|
||||
# real movaps with optimisation on, and an alignment bug that only shows up in
|
||||
# a release build is the one this is looking for.
|
||||
clang -O2 -c -I"$(ocamlopt -where)" "$here/jit_stubs.c" -o "$out/jit_stubs.o" || exit 1
|
||||
|
||||
ocamlfind ocamlopt -thread -package unix,threads.posix -linkpkg \
|
||||
-I "$root/_build/default/lib/.flan.objs/byte" \
|
||||
-I "$root/_build/default/lib/.flan.objs/native" \
|
||||
-I "$out" -I "$here" \
|
||||
-o "$out/spike" \
|
||||
"$root/_build/default/lib/flan.cmxa" \
|
||||
-cclib -rdynamic -ccopt -L"$root/_build/default/lib" \
|
||||
"$out/jit_stubs.o" \
|
||||
"$here/x86.ml" "$here/driver.ml" 2>&1 | head -40
|
||||
|
||||
test -x "$out/spike" || { echo "build failed"; exit 1; }
|
||||
|
||||
SPIKE_DUMP=$out "$out/spike" "$here/probe.flan"
|
||||
rc=$?
|
||||
|
||||
# Disassembly on request. objdump over the raw buffer, which is what to reach
|
||||
# for when a function answers the wrong number -- not what proves it answers
|
||||
# the right one.
|
||||
if [ "${SPIKE_DISASM:-}" = 1 ]; then
|
||||
for f in "$out"/*.bin; do
|
||||
echo; echo "== $(basename "$f" .bin)"
|
||||
objdump -D -b binary -m i386:x86-64 -M intel "$f" | tail -n +7
|
||||
done
|
||||
fi
|
||||
echo "exit: $rc"
|
||||
exit $rc
|
||||
@ -1,379 +0,0 @@
|
||||
(* A spike: Tast -> x86-64 machine code, in memory, called. Not a backend.
|
||||
The point is to find out what breaks, so the subset is deliberately tiny
|
||||
and every case it cannot do raises with the node that defeated it -- an
|
||||
honest [Unsupported] is the measurement, and a silently wrong answer is
|
||||
the one outcome that would waste the exercise.
|
||||
|
||||
Register allocation is the trivial one the brief allows: every slot is a
|
||||
stack slot at [rbp - 8*(i+1)], every value is computed into rax, and a
|
||||
binary operator pushes its left operand. Two registers are enough for
|
||||
everything below and nothing is kept live across a statement. That is what
|
||||
makes an instruction selector tractable in an afternoon; it is also why the
|
||||
code it produces is four times the size of clang -O0's.
|
||||
|
||||
Conventions, all of them SysV's, because raylib is called from this code:
|
||||
- integer arguments in rdi rsi rdx rcx r8 r9, then right-to-left on the
|
||||
stack; integer result in rax.
|
||||
- rsp % 16 == 0 at the [call] instruction. raylib spills xmm registers
|
||||
with movaps and faults far from the cause when this is wrong.
|
||||
- rbx rbp r12-r15 are callee-saved. This emitter touches none of them
|
||||
except rbp, which it saves.
|
||||
- every Flan function takes the transfer channel as a trailing ptr
|
||||
(emit.ml, [signature]), so a Flan function of n parameters is an n+1
|
||||
argument C function. *)
|
||||
|
||||
exception Unsupported of string
|
||||
|
||||
let unsupported fmt = Printf.ksprintf (fun s -> raise (Unsupported s)) fmt
|
||||
|
||||
(* ── Bytes ───────────────────────────────────────────────────────────── *)
|
||||
|
||||
type buf = { mutable bytes : Buffer.t }
|
||||
|
||||
let create () = { bytes = Buffer.create 256 }
|
||||
let len b = Buffer.length b.bytes
|
||||
let contents b = Buffer.contents b.bytes
|
||||
let u8 b n = Buffer.add_char b.bytes (Char.chr (n land 0xff))
|
||||
|
||||
let u32 b n =
|
||||
for i = 0 to 3 do u8 b ((n asr (i * 8)) land 0xff) done
|
||||
|
||||
let i32 b (n : int) =
|
||||
if n < -0x80000000 || n > 0x7fffffff then unsupported "displacement %d" n;
|
||||
u32 b n
|
||||
|
||||
let u64 b (n : int64) =
|
||||
for i = 0 to 7 do
|
||||
u8 b (Int64.to_int (Int64.logand (Int64.shift_right_logical n (i * 8)) 0xffL))
|
||||
done
|
||||
|
||||
(* ── Registers and modrm ─────────────────────────────────────────────── *)
|
||||
|
||||
(* The encoding order, not the ABI order: this numbering *is* the three bits
|
||||
the modrm byte wants, which is why rsp is 4 and rbp is 5 rather than
|
||||
anything more memorable. *)
|
||||
let rax = 0 and rcx = 1 and rdx = 2 and _rbx = 3
|
||||
let rsp = 4 and rbp = 5 and rsi = 6 and rdi = 7
|
||||
let r8 = 8 and r9 = 9
|
||||
|
||||
(* REX.W is always set: everything here is 64-bit. R extends the reg field and
|
||||
B the r/m field, which is the whole of what r8-r15 need. *)
|
||||
let rex b ~r ~m = u8 b (0x48 lor (if r >= 8 then 4 else 0) lor (if m >= 8 then 1 else 0))
|
||||
let modrm b ~md ~r ~m = u8 b ((md lsl 6) lor ((r land 7) lsl 3) lor (m land 7))
|
||||
|
||||
(* reg, reg *)
|
||||
let rr b op ~r ~m = rex b ~r ~m; u8 b op; modrm b ~md:3 ~r ~m
|
||||
|
||||
(* reg, [rbp + disp32]. Always disp32 rather than the shorter disp8 form: a
|
||||
frame can outgrow 128 bytes and a one-byte displacement that silently wraps
|
||||
is exactly the bug this spike would not find. *)
|
||||
let rm_rbp b op ~r ~disp =
|
||||
rex b ~r ~m:rbp; u8 b op; modrm b ~md:2 ~r ~m:rbp; i32 b disp
|
||||
|
||||
let mov_rr b ~dst ~src = rr b 0x89 ~r:src ~m:dst (* mov dst, src *)
|
||||
let mov_load b ~dst ~disp = rm_rbp b 0x8b ~r:dst ~disp (* mov dst, [rbp+d] *)
|
||||
let mov_store b ~src ~disp = rm_rbp b 0x89 ~r:src ~disp (* mov [rbp+d], src *)
|
||||
|
||||
let movabs b ~dst (n : int64) =
|
||||
rex b ~r:0 ~m:dst; u8 b (0xb8 lor (dst land 7)); u64 b n
|
||||
|
||||
let push b r = if r >= 8 then u8 b 0x41; u8 b (0x50 lor (r land 7))
|
||||
let pop b r = if r >= 8 then u8 b 0x41; u8 b (0x58 lor (r land 7))
|
||||
|
||||
let add_rr b ~dst ~src = rr b 0x01 ~r:src ~m:dst
|
||||
let sub_rr b ~dst ~src = rr b 0x29 ~r:src ~m:dst
|
||||
let and_rr b ~dst ~src = rr b 0x21 ~r:src ~m:dst
|
||||
let or_rr b ~dst ~src = rr b 0x09 ~r:src ~m:dst
|
||||
let xor_rr b ~dst ~src = rr b 0x31 ~r:src ~m:dst
|
||||
let imul_rr b ~dst ~src = (* 0f af /r *)
|
||||
rex b ~r:dst ~m:src; u8 b 0x0f; u8 b 0xaf; modrm b ~md:3 ~r:dst ~m:src
|
||||
let cmp_rr b ~a ~bb = rr b 0x39 ~r:bb ~m:a (* cmp a, b *)
|
||||
|
||||
let add_imm32 b ~dst n = rex b ~r:0 ~m:dst; u8 b 0x81; modrm b ~md:3 ~r:0 ~m:dst; i32 b n
|
||||
let sub_imm32 b ~dst n = rex b ~r:0 ~m:dst; u8 b 0x81; modrm b ~md:3 ~r:5 ~m:dst; i32 b n
|
||||
|
||||
let call_r b r = if r >= 8 then u8 b 0x41; u8 b 0xff; modrm b ~md:3 ~r:2 ~m:r
|
||||
let leave b = u8 b 0xc9
|
||||
let ret b = u8 b 0xc3
|
||||
let ud2 b = u8 b 0x0f; u8 b 0x0b
|
||||
|
||||
(* setcc al, then movzx rax, al -- a compare's result is a bool, which is one
|
||||
byte in Flan's layout (i1 in LLVM, and the ABI zero-extends it). *)
|
||||
let setcc b cc = u8 b 0x0f; u8 b (0x90 lor cc); modrm b ~md:3 ~r:0 ~m:rax
|
||||
let movzx_al b = u8 b 0x48; u8 b 0x0f; u8 b 0xb6; modrm b ~md:3 ~r:rax ~m:rax
|
||||
|
||||
(* jcc rel32 and jmp rel32, patched once the target is known. *)
|
||||
let jcc b cc = u8 b 0x0f; u8 b (0x80 lor cc); let at = len b in u32 b 0; at
|
||||
let jmp b = u8 b 0xe9; let at = len b in u32 b 0; at
|
||||
|
||||
let patch b ~at ~target =
|
||||
let rel = target - (at + 4) in
|
||||
let s = Buffer.contents b.bytes in
|
||||
let s = Bytes.of_string s in
|
||||
for i = 0 to 3 do
|
||||
Bytes.set s (at + i) (Char.chr ((rel asr (i * 8)) land 0xff))
|
||||
done;
|
||||
let nb = Buffer.create (Bytes.length s) in
|
||||
Buffer.add_bytes nb s;
|
||||
b.bytes <- nb
|
||||
|
||||
(* ── Lowering ────────────────────────────────────────────────────────── *)
|
||||
|
||||
type fnctx = {
|
||||
b : buf;
|
||||
nslots : int;
|
||||
(* How many 8-byte words this expression's evaluation has pushed since the
|
||||
prologue. rsp is 16-aligned at the end of the prologue, so [depth] even
|
||||
means rsp is aligned and [depth] odd means it is 8 out.
|
||||
|
||||
This counter is the answer to the one bug the ABI probe found. Alignment
|
||||
is not a property of the prologue: the evaluator spills the left operand
|
||||
across the right one's evaluation, so a call written in the right operand
|
||||
runs with one word outstanding. Deriving it from a count kept here is the
|
||||
only way that stays correct as the evaluator grows cases, and it is what
|
||||
clang's [sub rsp, 8] before a call is doing. *)
|
||||
mutable depth : int;
|
||||
(* A symbol the code calls, resolved to an absolute address by the driver
|
||||
before emission. movabs + call r is what a JIT does anyway: a rel32 call
|
||||
cannot reach an arbitrary mmap, and the 2-byte indirect call is cheaper
|
||||
than the relocation machinery a real backend would grow here. *)
|
||||
resolve : string -> int64;
|
||||
}
|
||||
|
||||
let slot_disp i = -8 * (i + 1)
|
||||
|
||||
(* Every stack movement goes through these two, so that nothing can move rsp
|
||||
without the counter noticing. *)
|
||||
let pushv f r = push f.b r; f.depth <- f.depth + 1
|
||||
let popv f r = pop f.b r; f.depth <- f.depth - 1
|
||||
|
||||
(* Every type this spike handles is one 8-byte integer register. Everything
|
||||
else is the real backend's problem and is enumerated in the verdict rather
|
||||
than guessed at here. *)
|
||||
let word_ty (t : Flan.Types.t) =
|
||||
match t with
|
||||
| Flan.Types.Int _ | Flan.Types.Bool | Flan.Types.Ptr _ -> true
|
||||
| _ -> false
|
||||
|
||||
let check_word what (t : Flan.Types.t) =
|
||||
if not (word_ty t) then
|
||||
unsupported "%s of type %s: not a single integer register" what
|
||||
(Flan.Types.to_string t)
|
||||
|
||||
let cc_of signed (p : Flan.Tast.prim) =
|
||||
match p, signed with
|
||||
| Flan.Tast.Eq, _ -> 0x4 | Flan.Tast.Ne, _ -> 0x5
|
||||
| Flan.Tast.Lt, true -> 0xc | Flan.Tast.Lt, false -> 0x2
|
||||
| Flan.Tast.Le, true -> 0xe | Flan.Tast.Le, false -> 0x6
|
||||
| Flan.Tast.Gt, true -> 0xf | Flan.Tast.Gt, false -> 0x7
|
||||
| Flan.Tast.Ge, true -> 0xd | Flan.Tast.Ge, false -> 0x3
|
||||
| _ -> assert false
|
||||
|
||||
let arg_regs = [| rdi; rsi; rdx; rcx; r8; r9 |]
|
||||
|
||||
(* Value into rax. Everything is a subexpression of something that will
|
||||
immediately consume rax, so nothing is kept live and no allocator is
|
||||
needed. *)
|
||||
let rec value f (e : Flan.Tast.expr) : unit =
|
||||
let b = f.b in
|
||||
match e.Flan.Tast.e with
|
||||
| Flan.Tast.Int (n, _) -> movabs b ~dst:rax n
|
||||
| Flan.Tast.Bool v -> movabs b ~dst:rax (if v then 1L else 0L)
|
||||
| Flan.Tast.Local i ->
|
||||
check_word "local" e.Flan.Tast.ty;
|
||||
if i >= f.nslots then unsupported "slot %d out of range" i;
|
||||
mov_load b ~dst:rax ~disp:(slot_disp i)
|
||||
| Flan.Tast.Do body -> block f body
|
||||
| Flan.Tast.Let (binds, body) ->
|
||||
List.iter
|
||||
(fun (i, e) ->
|
||||
value f e;
|
||||
check_word "binding" e.Flan.Tast.ty;
|
||||
mov_store b ~src:rax ~disp:(slot_disp i))
|
||||
binds;
|
||||
block f body
|
||||
| Flan.Tast.Set (Flan.Tast.Plocal i, rhs) ->
|
||||
value f rhs;
|
||||
check_word "assignment" rhs.Flan.Tast.ty;
|
||||
mov_store b ~src:rax ~disp:(slot_disp i)
|
||||
| Flan.Tast.If (c, t, e') -> emit_if f c t e'
|
||||
| Flan.Tast.Return (Some x) ->
|
||||
value f x;
|
||||
leave b; ret b
|
||||
| Flan.Tast.Return None -> leave b; ret b
|
||||
| Flan.Tast.Prim (p, args) -> prim f e p args
|
||||
| Flan.Tast.Call (name, args) -> call f (f.resolve name) args ~xfer:true
|
||||
| Flan.Tast.Unit -> ()
|
||||
| k -> unsupported "expression: %s" (node_name k)
|
||||
|
||||
and block f body =
|
||||
match body with
|
||||
| [] -> ()
|
||||
| [ last ] -> value f last
|
||||
| x :: rest -> value f x; block f rest
|
||||
|
||||
and prim f e (p : Flan.Tast.prim) args =
|
||||
let b = f.b in
|
||||
match p, args with
|
||||
| (Flan.Tast.Add | Flan.Tast.Sub | Flan.Tast.Mul
|
||||
| Flan.Tast.BitAnd | Flan.Tast.BitOr | Flan.Tast.BitXor), [ x; y ] ->
|
||||
check_word "arithmetic" x.Flan.Tast.ty;
|
||||
binop f x y;
|
||||
(* left in rax, right in rcx *)
|
||||
(match p with
|
||||
| Flan.Tast.Add -> add_rr b ~dst:rax ~src:rcx
|
||||
| Flan.Tast.Sub -> sub_rr b ~dst:rax ~src:rcx
|
||||
| Flan.Tast.Mul -> imul_rr b ~dst:rax ~src:rcx
|
||||
| Flan.Tast.BitAnd -> and_rr b ~dst:rax ~src:rcx
|
||||
| Flan.Tast.BitOr -> or_rr b ~dst:rax ~src:rcx
|
||||
| _ -> xor_rr b ~dst:rax ~src:rcx)
|
||||
| (Flan.Tast.Eq | Flan.Tast.Ne | Flan.Tast.Lt | Flan.Tast.Le
|
||||
| Flan.Tast.Gt | Flan.Tast.Ge), [ x; y ] ->
|
||||
let signed =
|
||||
match x.Flan.Tast.ty with
|
||||
| Flan.Types.Int k -> Flan.Types.signed k
|
||||
| Flan.Types.Bool -> false
|
||||
| t -> unsupported "comparison on %s" (Flan.Types.to_string t)
|
||||
in
|
||||
binop f x y;
|
||||
cmp_rr b ~a:rax ~bb:rcx;
|
||||
setcc b (cc_of signed p);
|
||||
movzx_al b
|
||||
| Flan.Tast.Rt sym, args -> call f (f.resolve sym) args ~xfer:false
|
||||
| _ -> unsupported "primitive in %s" (Flan.Types.to_string e.Flan.Tast.ty)
|
||||
|
||||
(* Left into rax, right into rcx, with the left spilled across the right's
|
||||
evaluation. Left-to-right, which emit.ml's [map_lr] is explicit about being
|
||||
required rather than a preference -- a call in either operand has effects.
|
||||
The push/pop pair keeps rsp 16-aligned in pairs, which matters only because
|
||||
[call] below re-derives alignment from a counter rather than tracking rsp. *)
|
||||
and binop f x y =
|
||||
let b = f.b in
|
||||
value f x;
|
||||
pushv f rax;
|
||||
value f y;
|
||||
mov_rr b ~dst:rcx ~src:rax;
|
||||
popv f rax
|
||||
|
||||
and emit_if f c t e =
|
||||
let b = f.b in
|
||||
value f c;
|
||||
(* cmp rax, 0: 48 83 f8 00 -- written out because the helper above takes
|
||||
registers only and a zero-compare is the one immediate form worth having. *)
|
||||
u8 b 0x48; u8 b 0x83; modrm b ~md:3 ~r:7 ~m:rax; u8 b 0x00;
|
||||
let to_else = jcc b 0x4 in (* je *)
|
||||
value f t;
|
||||
let to_end = jmp b in
|
||||
patch b ~at:to_else ~target:(len b);
|
||||
value f e;
|
||||
patch b ~at:to_end ~target:(len b)
|
||||
|
||||
(* A call, and this is the part that has to be exactly right.
|
||||
|
||||
[xfer] appends the transfer channel, which every Flan function's signature
|
||||
carries and a C entry point does not. The spike passes NULL: nothing here
|
||||
signals, and a real backend would pass the caller's own channel pointer.
|
||||
|
||||
Alignment: rsp is 16-aligned at function entry minus the 8 the [call]
|
||||
pushed, so after [push rbp] it is aligned again, and the frame is rounded to
|
||||
a multiple of 16. Every push here is paired with a pop before the next call
|
||||
can happen, so rsp is aligned at every call site by construction. Stack
|
||||
arguments are pushed in pairs to keep it that way -- an odd count gets a
|
||||
dummy push, which is what clang's [sub rsp, 8] is doing when you see it. *)
|
||||
and call f (addr : int64) args ~xfer =
|
||||
let b = f.b in
|
||||
let n = List.length args + (if xfer then 1 else 0) in
|
||||
(* Bring rsp to 16 first, so everything below can count in pairs. *)
|
||||
let pad = f.depth land 1 = 1 in
|
||||
if pad then (sub_imm32 b ~dst:rsp 8; f.depth <- f.depth + 1);
|
||||
let stacked = List.filteri (fun i _ -> i >= 6) args in
|
||||
let nstack = List.length stacked + (if xfer && n > 6 then 1 else 0) in
|
||||
(* The stack half, evaluated right to left so that the seventh argument ends
|
||||
up at [rsp] and the eighth above it. The transfer channel is the last
|
||||
argument of all, so it is pushed first. *)
|
||||
if nstack land 1 = 1 then (sub_imm32 b ~dst:rsp 8; f.depth <- f.depth + 1);
|
||||
if xfer && n > 6 then (movabs b ~dst:rax 0L; pushv f rax);
|
||||
List.iter (fun a -> value f a; pushv f rax) (List.rev stacked);
|
||||
(* The register half needs a spill of its own: rdi..r9 are argument registers
|
||||
and rax is where every value lands, so an earlier argument would be
|
||||
clobbered by a later one's evaluation. Push each, then pop them into their
|
||||
registers in reverse. *)
|
||||
let inreg = List.filteri (fun i _ -> i < 6) args in
|
||||
List.iter (fun a -> value f a; pushv f rax) inreg;
|
||||
let nreg = List.length inreg in
|
||||
List.iteri (fun i _ -> popv f arg_regs.(nreg - 1 - i)) inreg;
|
||||
if xfer && n <= 6 then movabs b ~dst:arg_regs.(nreg) 0L;
|
||||
(* al = the number of vector registers used. Required only for a variadic
|
||||
callee and set unconditionally because it is two bytes: a wrong al on a
|
||||
printf-shaped entry point -- raylib's TraceLog is one -- is a crash that
|
||||
looks like anything else. After the argument registers, since al is rax's
|
||||
low byte. *)
|
||||
u8 b 0xb0; u8 b 0x00; (* mov al, 0 *)
|
||||
(* r11 always, never r9: r11 is the scratch register SysV reserves and is the
|
||||
one register guaranteed not to be carrying an argument. Choosing the
|
||||
target conditionally is how a six-argument call gets quietly wrong. *)
|
||||
u8 b 0x49; u8 b 0xbb; u64 b addr; (* movabs r11, addr *)
|
||||
assert (f.depth land 1 = 0);
|
||||
call_r b 11;
|
||||
let back = 8 * (nstack + (nstack land 1)) in
|
||||
if back > 0 then (add_imm32 b ~dst:rsp back; f.depth <- f.depth - (back / 8));
|
||||
if pad then (add_imm32 b ~dst:rsp 8; f.depth <- f.depth - 1)
|
||||
|
||||
and node_name (k : Flan.Tast.expr_kind) =
|
||||
match k with
|
||||
| Flan.Tast.Int _ -> "Int" | Flan.Tast.Float _ -> "Float"
|
||||
| Flan.Tast.Bool _ -> "Bool" | Flan.Tast.Str _ -> "Str"
|
||||
| Flan.Tast.Unit -> "Unit" | Flan.Tast.Zero _ -> "Zero"
|
||||
| Flan.Tast.Uninit _ -> "Uninit" | Flan.Tast.Local _ -> "Local"
|
||||
| Flan.Tast.Global _ -> "Global" | Flan.Tast.Prim _ -> "Prim"
|
||||
| Flan.Tast.Call _ -> "Call" | Flan.Tast.FnAddr _ -> "FnAddr"
|
||||
| Flan.Tast.CallPtr _ -> "CallPtr" | Flan.Tast.Do _ -> "Do"
|
||||
| Flan.Tast.Let _ -> "Let" | Flan.Tast.If _ -> "If"
|
||||
| Flan.Tast.While _ -> "While" | Flan.Tast.Return _ -> "Return"
|
||||
| Flan.Tast.Break _ -> "Break" | Flan.Tast.Continue _ -> "Continue"
|
||||
| Flan.Tast.Set _ -> "Set" | Flan.Tast.Field _ -> "Field"
|
||||
| Flan.Tast.Addr _ -> "Addr" | Flan.Tast.Deref _ -> "Deref"
|
||||
| Flan.Tast.Make _ -> "Make" | Flan.Tast.MakeCase _ -> "MakeCase"
|
||||
| Flan.Tast.CaseField _ -> "CaseField" | Flan.Tast.Arr _ -> "Arr"
|
||||
| Flan.Tast.Some_ _ -> "Some" | Flan.Tast.None_ -> "None"
|
||||
| Flan.Tast.Match _ -> "Match" | Flan.Tast.UnwrapSome _ -> "UnwrapSome"
|
||||
| Flan.Tast.Signal _ -> "Signal" | Flan.Tast.Handled _ -> "Handled"
|
||||
| Flan.Tast.RestartCase _ -> "RestartCase"
|
||||
| Flan.Tast.WithAlloc _ -> "WithAlloc"
|
||||
| Flan.Tast.InvokeRestart _ -> "InvokeRestart"
|
||||
|
||||
(* ── A whole function ────────────────────────────────────────────────── *)
|
||||
|
||||
let fn ~resolve (fd : Flan.Tast.fn) : string =
|
||||
let b = create () in
|
||||
let nslots = Array.length fd.Flan.Tast.slots in
|
||||
let f = { b; nslots; resolve; depth = 0 } in
|
||||
push b rbp;
|
||||
mov_rr b ~dst:rbp ~src:rsp;
|
||||
(* Round the frame to 16 so that rsp is aligned at every call site. One
|
||||
extra word for the transfer channel's slot, which is not a Flan slot and
|
||||
has no index -- the spike never reads it, but a real backend must, and
|
||||
leaving no room for it is the kind of thing that is cheap now and
|
||||
expensive later. *)
|
||||
let frame = (nslots + 1) * 8 in
|
||||
let frame = (frame + 15) land lnot 15 in
|
||||
if frame > 0 then sub_imm32 b ~dst:rsp frame;
|
||||
(* Parameters arrive in registers and are stored into their slots at once,
|
||||
which is also emit.ml's rule: slots 0..n-1 are the parameters, in order. *)
|
||||
let np = List.length fd.Flan.Tast.params in
|
||||
if np > 6 then unsupported "more than six parameters";
|
||||
List.iteri
|
||||
(fun i ty ->
|
||||
check_word "parameter" ty;
|
||||
mov_store b ~src:arg_regs.(i) ~disp:(slot_disp i))
|
||||
fd.Flan.Tast.params;
|
||||
(* The transfer channel is the last argument and goes just past the slots. *)
|
||||
if np < 6 then mov_store b ~src:arg_regs.(np) ~disp:(slot_disp nslots);
|
||||
block f fd.Flan.Tast.body;
|
||||
leave b; ret b;
|
||||
(* Anything that falls off the end of a Never-returning body lands here and
|
||||
traps rather than running into the next function. LLVM's [unreachable] is
|
||||
undefined behaviour; ud2 is a defined SIGILL, and the difference is one of
|
||||
the audit's findings. *)
|
||||
ud2 b;
|
||||
contents b
|
||||
14
spike/embed/.gitignore
vendored
14
spike/embed/.gitignore
vendored
@ -1,14 +0,0 @@
|
||||
# Spike artifacts. run.sh rebuilds all of them from the sources beside it.
|
||||
*.o
|
||||
*.cmi
|
||||
*.cmx
|
||||
baseline
|
||||
spike1
|
||||
spike2
|
||||
spike3
|
||||
spike4
|
||||
spike5
|
||||
spike6
|
||||
spike5b
|
||||
spike5b_std
|
||||
stubs5b.o
|
||||
@ -1,4 +0,0 @@
|
||||
/* The floor: what a C binary with no OCaml in it weighs, so the delta the dev
|
||||
build actually pays can be stated honestly. */
|
||||
#include <stdio.h>
|
||||
int main(void) { printf("baseline\n"); return 0; }
|
||||
@ -1,149 +0,0 @@
|
||||
/* Loading a compiled macro into the compiler's own process.
|
||||
*
|
||||
* TODO.org, "The expander design: running a macro means dlopening it": there
|
||||
* is no interpreter, so running a macro means
|
||||
* compiling it and dlopening it. The reload primitive does exactly this
|
||||
* already, but its host is a running Flan program written in C; here the host
|
||||
* is the OCaml compiler, which has no dlopen of its own -- Dynlink loads
|
||||
* OCaml, not ELF. So the boundary needs stubs, and this is all of them.
|
||||
*
|
||||
* Two rules shape what is here:
|
||||
*
|
||||
* - Nothing but pointers and scalars crosses. A Flan `string`/slice is
|
||||
* {ptr,len} and a `Form` is {i32, [2 x i64]}, and LLVM's calling
|
||||
* convention for an aggregate passed or returned *by value* in hand-written
|
||||
* IR is not promised to be clang's C ABI for the equivalent struct. The
|
||||
* unions lane verified memory layout, so memory is the agreement we have:
|
||||
* every macro is reached through a thunk taking (ptr,i64,ptr,ptr) and
|
||||
* writing its result through the out pointer.
|
||||
*
|
||||
* - The macro module is self-contained: it links the runtime in and has no
|
||||
* undefined Flan symbols, so the OCaml executable needs no -rdynamic and
|
||||
* nothing in it has to be exported.
|
||||
*
|
||||
* The peek/poke family is how the marshaller writes a Form image into memory
|
||||
* the macro can read. OCaml cannot address raw memory, so the bytes are laid
|
||||
* out from here one field at a time.
|
||||
*/
|
||||
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/alloc.h>
|
||||
#include <caml/memory.h>
|
||||
#include <caml/fail.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
CAMLprim value flan_dl_open(value path) {
|
||||
CAMLparam1(path);
|
||||
void *h = dlopen(String_val(path), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!h) caml_failwith(dlerror());
|
||||
CAMLreturn(caml_copy_nativeint((intnat)h));
|
||||
}
|
||||
|
||||
CAMLprim value flan_dl_sym(value handle, value name) {
|
||||
CAMLparam2(handle, name);
|
||||
void *p = dlsym((void *)Nativeint_val(handle), String_val(name));
|
||||
if (!p) caml_failwith(dlerror());
|
||||
CAMLreturn(caml_copy_nativeint((intnat)p));
|
||||
}
|
||||
|
||||
CAMLprim value flan_dl_close(value handle) {
|
||||
dlclose((void *)Nativeint_val(handle));
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
/* The one call shape a macro is reached through. See the thunk Emit writes. */
|
||||
typedef void (*flan_macro_fn)(void *args, int64_t n, void *out, void *xfer);
|
||||
|
||||
CAMLprim value flan_macro_call(value fn, value args, value n, value out) {
|
||||
CAMLparam4(fn, args, n, out);
|
||||
/* The transfer channel every Flan signature carries (spec-conditions.md,
|
||||
section 6). A macro that signals a condition with nothing above it to
|
||||
handle it aborts inside the compiler, which is loud rather than silent;
|
||||
the channel still has to be a real, zeroed slot. */
|
||||
int64_t xfer[4] = { 0, 0, 0, 0 };
|
||||
((flan_macro_fn)Nativeint_val(fn))((void *)Nativeint_val(args),
|
||||
Int64_val(n),
|
||||
(void *)Nativeint_val(out), xfer);
|
||||
CAMLreturn(Val_unit);
|
||||
}
|
||||
|
||||
CAMLprim value flan_mem_alloc(value n) {
|
||||
CAMLparam1(n);
|
||||
/* Zeroed, because ZII is the language's rule and an unwritten Form field
|
||||
must read as the zero of its type rather than as whatever malloc had. */
|
||||
void *p = calloc((size_t)Long_val(n), 1);
|
||||
if (!p) caml_failwith("out of memory laying out a macro's arguments");
|
||||
CAMLreturn(caml_copy_nativeint((intnat)p));
|
||||
}
|
||||
|
||||
CAMLprim value flan_mem_free(value p) {
|
||||
free((void *)Nativeint_val(p));
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value flan_poke_i32(value p, value off, value x) {
|
||||
int32_t v = (int32_t)Int32_val(x);
|
||||
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 4);
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value flan_poke_i64(value p, value off, value x) {
|
||||
int64_t v = Int64_val(x);
|
||||
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 8);
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value flan_poke_f64(value p, value off, value x) {
|
||||
double v = Double_val(x);
|
||||
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 8);
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value flan_poke_ptr(value p, value off, value q) {
|
||||
void *v = (void *)Nativeint_val(q);
|
||||
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, sizeof v);
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value flan_poke_bytes(value p, value off, value s) {
|
||||
memcpy((char *)Nativeint_val(p) + Long_val(off), String_val(s),
|
||||
caml_string_length(s));
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value flan_peek_i32(value p, value off) {
|
||||
int32_t v;
|
||||
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 4);
|
||||
return caml_copy_int32(v);
|
||||
}
|
||||
|
||||
CAMLprim value flan_peek_i64(value p, value off) {
|
||||
int64_t v;
|
||||
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 8);
|
||||
return caml_copy_int64(v);
|
||||
}
|
||||
|
||||
CAMLprim value flan_peek_f64(value p, value off) {
|
||||
double v;
|
||||
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 8);
|
||||
return caml_copy_double(v);
|
||||
}
|
||||
|
||||
CAMLprim value flan_peek_ptr(value p, value off) {
|
||||
void *v;
|
||||
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), sizeof v);
|
||||
return caml_copy_nativeint((intnat)v);
|
||||
}
|
||||
|
||||
CAMLprim value flan_peek_bytes(value p, value off, value n) {
|
||||
CAMLparam3(p, off, n);
|
||||
CAMLlocal1(s);
|
||||
s = caml_alloc_string((mlsize_t)Long_val(n));
|
||||
memcpy((char *)Bytes_val(s), (char *)Nativeint_val(p) + Long_val(off),
|
||||
(size_t)Long_val(n));
|
||||
CAMLreturn(s);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
(* Step 6: the OCaml GC beside Flan's arenas.
|
||||
Allocate hard, then compact -- the most disruptive thing the collector does,
|
||||
since compaction is what actually moves blocks. C checks its arena after. *)
|
||||
|
||||
external note : nativeint -> unit = "spike_note_arena"
|
||||
|
||||
let () =
|
||||
Callback.register "spike_churn" (fun (rounds : int) ->
|
||||
let keep = ref [] in
|
||||
for i = 1 to rounds do
|
||||
(* Garbage, plus a little that survives, so the heap really grows. *)
|
||||
for _ = 1 to 2000 do ignore (Bytes.create 512) done;
|
||||
if i mod 10 = 0 then keep := Bytes.create 4096 :: !keep
|
||||
done;
|
||||
Gc.full_major ();
|
||||
Gc.compact ();
|
||||
let s = Gc.quick_stat () in
|
||||
Printf.sprintf
|
||||
"allocated %.0f words, %d major collections, %d compactions, heap %d words"
|
||||
s.Gc.minor_words s.Gc.major_collections s.Gc.compactions s.Gc.heap_words);
|
||||
ignore note
|
||||
@ -1,14 +0,0 @@
|
||||
/* A C main() that owns the process and starts the OCaml runtime underneath it. */
|
||||
#include <caml/callback.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
caml_startup(argv);
|
||||
const value *f = caml_named_value("spike_greet");
|
||||
if (!f) { fprintf(stderr, "spike: greet not registered\n"); return 1; }
|
||||
printf("%s\n", String_val(caml_callback(*f, Val_int(42))));
|
||||
printf("spike: C main still owns the process\n");
|
||||
return 0;
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
/* Step 2: the whole compiler inside a C binary, and what it costs to start.
|
||||
*
|
||||
* The startup number is measured around caml_startup itself, not with time(1)
|
||||
* on the process -- what a merged dev build would pay is the runtime coming up
|
||||
* and every module initialiser running, not exec and dynamic linking, which it
|
||||
* pays today anyway. */
|
||||
#include <caml/callback.h>
|
||||
#include <caml/alloc.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifndef FLANSRC
|
||||
#define FLANSRC "test/programs/edn.flan"
|
||||
#endif
|
||||
|
||||
static double ms_since(struct timespec a) {
|
||||
struct timespec b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
return (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6;
|
||||
}
|
||||
|
||||
static const value *need(const char *n) {
|
||||
const value *f = caml_named_value(n);
|
||||
if (!f) fprintf(stderr, "spike: %s not registered\n", n);
|
||||
return f;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct timespec t0;
|
||||
const char *src = argc > 1 ? argv[1] : FLANSRC;
|
||||
const value *f;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
caml_startup(argv);
|
||||
printf("caml_startup (runtime + every module initialiser): %.3f ms\n", ms_since(t0));
|
||||
|
||||
f = need("spike_footprint");
|
||||
if (f) printf("linked-module footprint: %s\n", String_val(caml_callback(*f, Val_unit)));
|
||||
|
||||
f = need("spike_compile");
|
||||
if (f) {
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
printf("%s\n", String_val(caml_callback(*f, caml_copy_string(src))));
|
||||
printf("first in-process compile (read+parse+check+emit): %.3f ms\n", ms_since(t0));
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
caml_callback(*f, caml_copy_string(src));
|
||||
printf("second, warm: %.3f ms\n", ms_since(t0));
|
||||
}
|
||||
|
||||
printf("spike: C main() still owns the process\n");
|
||||
return 0;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
/* Step 3: does -output-complete-obj carry the project's C stubs through? */
|
||||
#include <caml/callback.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const value *f;
|
||||
(void)argc;
|
||||
caml_startup(argv);
|
||||
f = caml_named_value("spike_stubs");
|
||||
if (!f) { fprintf(stderr, "spike: stubs not registered\n"); return 1; }
|
||||
printf("stubs reached from embedded runtime: %s\n", String_val(caml_callback(*f, Val_unit)));
|
||||
return 0;
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
/* Step 4: the macOS shape, and the discriminating test of the whole spike.
|
||||
*
|
||||
* main() is the game: it takes the thread the window needs and runs a loop it
|
||||
* never leaves until the compiler says stop. The OCaml runtime is started on a
|
||||
* pthread that C spawned -- exactly where vendor/agent/flan_agent.c already
|
||||
* puts its listener.
|
||||
*
|
||||
* Two separate claims get tested:
|
||||
* a. caml_startup works on a non-main, C-created thread at all.
|
||||
* b. a *different* C thread, one the runtime never created, can call into
|
||||
* OCaml after caml_c_thread_register().
|
||||
* (b) is the one that matters for the agent: its listener thread is spawned by
|
||||
* flan_agent_start and would have to be able to reach the compiler.
|
||||
*/
|
||||
#include <caml/callback.h>
|
||||
#include <caml/alloc.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/threads.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static char **g_argv;
|
||||
static const char *g_src = "test/programs/edn.flan";
|
||||
static atomic_int compiler_up = 0;
|
||||
static atomic_int quit = 0;
|
||||
static pthread_t main_tid;
|
||||
|
||||
static void nap(long ms) {
|
||||
struct timespec t = { ms / 1000, (ms % 1000) * 1000000L };
|
||||
nanosleep(&t, NULL);
|
||||
}
|
||||
|
||||
static const value *need(const char *n) {
|
||||
const value *f = caml_named_value(n);
|
||||
if (!f) fprintf(stderr, "spike: %s not registered\n", n);
|
||||
return f;
|
||||
}
|
||||
|
||||
/* The compiler thread: starts the OCaml runtime off the main thread. */
|
||||
static void *compiler_thread(void *unused) {
|
||||
const value *f;
|
||||
(void)unused;
|
||||
printf(" [compiler thread] is main thread? %s\n",
|
||||
pthread_equal(pthread_self(), main_tid) ? "YES (wrong)" : "no (correct)");
|
||||
caml_startup(g_argv);
|
||||
printf(" [compiler thread] caml_startup returned off the main thread\n");
|
||||
|
||||
f = need("spike_domains");
|
||||
if (f) printf(" [compiler thread] %s\n", String_val(caml_callback(*f, Val_unit)));
|
||||
|
||||
f = need("spike_thread_compile");
|
||||
if (f) printf(" [compiler thread] %s\n",
|
||||
String_val(caml_callback(*f, caml_copy_string(g_src))));
|
||||
|
||||
/* Hand the runtime over so another C thread can borrow it, and prove the
|
||||
main loop kept running throughout. */
|
||||
atomic_store(&compiler_up, 1);
|
||||
caml_release_runtime_system();
|
||||
nap(300);
|
||||
caml_acquire_runtime_system();
|
||||
atomic_store(&quit, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* A second C thread, like the agent's listener: never created by OCaml. */
|
||||
static void *listener_thread(void *unused) {
|
||||
const value *f;
|
||||
(void)unused;
|
||||
while (!atomic_load(&compiler_up)) nap(5);
|
||||
if (caml_c_thread_register() == 0) {
|
||||
printf(" [listener thread] caml_c_thread_register FAILED\n");
|
||||
return NULL;
|
||||
}
|
||||
caml_acquire_runtime_system();
|
||||
f = need("spike_thread_compile");
|
||||
if (f) printf(" [listener thread] %s\n",
|
||||
String_val(caml_callback(*f, caml_copy_string(g_src))));
|
||||
caml_release_runtime_system();
|
||||
caml_c_thread_unregister();
|
||||
printf(" [listener thread] registered, called OCaml, unregistered\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
pthread_t comp, lst;
|
||||
long frames = 0;
|
||||
g_argv = argv;
|
||||
if (argc > 1) g_src = argv[1];
|
||||
main_tid = pthread_self();
|
||||
|
||||
if (pthread_create(&comp, NULL, compiler_thread, NULL) != 0) return 1;
|
||||
if (pthread_create(&lst, NULL, listener_thread, NULL) != 0) return 1;
|
||||
|
||||
/* The game loop. This thread never calls into OCaml and never blocks on it --
|
||||
it is the window's thread, and on macOS it has to be this one. */
|
||||
while (!atomic_load(&quit)) { frames++; nap(1); }
|
||||
|
||||
pthread_join(comp, NULL);
|
||||
pthread_join(lst, NULL);
|
||||
printf(" [main thread] ran %ld frames without ever entering OCaml\n", frames);
|
||||
printf("spike: the game kept the main thread\n");
|
||||
return 0;
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
/* Step 5: who owns SIGSEGV.
|
||||
*
|
||||
* The OCaml runtime installs a SIGSEGV handler to turn a stack-guard-page hit
|
||||
* into the Stack_overflow exception. The break loop wants SIGSEGV for the
|
||||
* crash case. This is the one real collision, so it is measured in both
|
||||
* directions:
|
||||
*
|
||||
* a. what the disposition is before caml_startup, and after it;
|
||||
* b. whether a handler installed AFTER caml_startup actually receives a
|
||||
* genuine fault in program memory -- i.e. whether the break loop can have
|
||||
* what it wants by installing last.
|
||||
*
|
||||
* SIGPIPE is not probed: flan_agent.c sends with MSG_NOSIGNAL throughout and
|
||||
* does not rely on a disposition.
|
||||
*/
|
||||
#include <caml/callback.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <setjmp.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void describe(const char *when, int sig) {
|
||||
struct sigaction old;
|
||||
memset(&old, 0, sizeof old);
|
||||
sigaction(sig, NULL, &old);
|
||||
printf(" %-22s %-8s handler=%p flags=%#x %s%s\n", when,
|
||||
sig == SIGSEGV ? "SIGSEGV" : sig == SIGINT ? "SIGINT" : "SIGFPE",
|
||||
(old.sa_flags & SA_SIGINFO) ? (void *)old.sa_sigaction : (void *)old.sa_handler,
|
||||
(unsigned)old.sa_flags,
|
||||
(old.sa_flags & SA_ONSTACK) ? "ONSTACK " : "",
|
||||
old.sa_handler == SIG_DFL ? "(SIG_DFL)"
|
||||
: old.sa_handler == SIG_IGN ? "(SIG_IGN)" : "(custom)");
|
||||
}
|
||||
|
||||
static sigjmp_buf escape;
|
||||
static volatile sig_atomic_t ours_ran = 0;
|
||||
|
||||
static void our_segv(int sig, siginfo_t *info, void *ctx) {
|
||||
(void)sig; (void)ctx;
|
||||
ours_ran = 1;
|
||||
/* What a break loop would do here is stop and serve; the spike just proves
|
||||
the handler was reached, with the faulting address in hand. */
|
||||
printf(" our SIGSEGV handler ran, fault address = %p\n", info->si_addr);
|
||||
siglongjmp(escape, 1);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct sigaction sa, ocaml_segv;
|
||||
volatile int *bad = (int *)0x10;
|
||||
(void)argc;
|
||||
|
||||
printf("before caml_startup:\n");
|
||||
describe("before startup", SIGSEGV);
|
||||
describe("before startup", SIGINT);
|
||||
describe("before startup", SIGFPE);
|
||||
|
||||
caml_startup(argv);
|
||||
|
||||
printf("after caml_startup:\n");
|
||||
describe("after startup", SIGSEGV);
|
||||
describe("after startup", SIGINT);
|
||||
describe("after startup", SIGFPE);
|
||||
memset(&ocaml_segv, 0, sizeof ocaml_segv);
|
||||
sigaction(SIGSEGV, NULL, &ocaml_segv);
|
||||
|
||||
/* Now install ours last, the way the break loop would. */
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_sigaction = our_segv;
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGSEGV, &sa, NULL);
|
||||
printf("break loop installs last:\n");
|
||||
describe("after break loop", SIGSEGV);
|
||||
|
||||
if (sigsetjmp(escape, 1) == 0) {
|
||||
printf(" dereferencing %p ...\n", (void *)bad);
|
||||
*bad = 1;
|
||||
printf(" no fault -- UNEXPECTED\n");
|
||||
} else {
|
||||
printf(" recovered; a handler installed after caml_startup does receive "
|
||||
"a real fault: %s\n", ours_ran ? "yes" : "no");
|
||||
}
|
||||
|
||||
/* And the cost of taking it: OCaml's own handler is now displaced, so its
|
||||
stack-overflow detection is gone unless ours chains to the saved one. */
|
||||
printf(" OCaml's displaced SIGSEGV handler was %p -- chaining to it is what "
|
||||
"keeps Stack_overflow working\n",
|
||||
(void *)ocaml_segv.sa_sigaction);
|
||||
return 0;
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
/* Step 5b: the SIGSEGV question, asked properly.
|
||||
*
|
||||
* 5a read the disposition either side of caml_startup and found SIG_DFL both
|
||||
* times, which would mean no collision at all. That is too good, and it is
|
||||
* because OCaml 5 installs the handler per *domain*, on the domain's own
|
||||
* thread, not once during startup. So this asks at four moments, and then asks
|
||||
* the only question that decides anything: with the break loop holding SIGSEGV,
|
||||
* does an OCaml stack overflow still raise Stack_overflow, or does it become a
|
||||
* hard crash?
|
||||
*
|
||||
* Two ways of taking it are compared:
|
||||
* take_segv -- install ours and discard OCaml's, the naive thing;
|
||||
* chain_segv -- install ours, keep OCaml's, and forward to it.
|
||||
*/
|
||||
#include <caml/callback.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/memory.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static struct sigaction ocaml_segv;
|
||||
static int have_ocaml_segv = 0;
|
||||
|
||||
CAMLprim value spike_show_segv(value when) {
|
||||
struct sigaction cur;
|
||||
memset(&cur, 0, sizeof cur);
|
||||
sigaction(SIGSEGV, NULL, &cur);
|
||||
printf(" SIGSEGV %-46s handler=%p flags=%#x%s\n", String_val(when),
|
||||
(cur.sa_flags & SA_SIGINFO) ? (void *)cur.sa_sigaction
|
||||
: (void *)cur.sa_handler,
|
||||
(unsigned)cur.sa_flags,
|
||||
cur.sa_handler == SIG_DFL ? " (SIG_DFL)" : "");
|
||||
fflush(stdout);
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
/* The break loop's handler. It does not long-jump here -- the point is only to
|
||||
* see whether it is reached and whether OCaml still works around it. */
|
||||
static void break_segv(int sig, siginfo_t *info, void *ctx) {
|
||||
(void)sig;
|
||||
if (have_ocaml_segv && ocaml_segv.sa_sigaction &&
|
||||
ocaml_segv.sa_handler != SIG_DFL && ocaml_segv.sa_handler != SIG_IGN) {
|
||||
/* Chained: hand the fault to OCaml, which turns a guard-page hit into
|
||||
* Stack_overflow and re-raises anything else. */
|
||||
ocaml_segv.sa_sigaction(sig, info, ctx);
|
||||
return;
|
||||
}
|
||||
/* Taken outright: nothing below us. A real break loop would stop and serve;
|
||||
* here we can only abort, which is the honest cost of discarding OCaml's. */
|
||||
printf(" break loop caught SIGSEGV at %p with nothing to chain to\n",
|
||||
info->si_addr);
|
||||
fflush(stdout);
|
||||
_exit(9);
|
||||
}
|
||||
|
||||
static void install(int keep_old) {
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
memset(&ocaml_segv, 0, sizeof ocaml_segv);
|
||||
sigaction(SIGSEGV, NULL, &ocaml_segv);
|
||||
have_ocaml_segv = keep_old;
|
||||
sa.sa_sigaction = break_segv;
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGSEGV, &sa, NULL);
|
||||
}
|
||||
|
||||
/* A sweep, so "the OCaml runtime installs handlers" can be stated as a list
|
||||
* rather than a worry. Called from OCaml with the runtime and a domain up. */
|
||||
CAMLprim value spike_sweep(value u) {
|
||||
static const int sigs[] = { SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGINT, SIGTERM,
|
||||
SIGPIPE, SIGCHLD, SIGUSR1, SIGUSR2, SIGABRT,
|
||||
SIGALRM, SIGPROF, SIGVTALRM, SIGWINCH };
|
||||
static const char *names[] = { "SEGV", "BUS", "FPE", "ILL", "INT", "TERM",
|
||||
"PIPE", "CHLD", "USR1", "USR2", "ABRT",
|
||||
"ALRM", "PROF", "VTALRM", "WINCH" };
|
||||
struct sigaction c;
|
||||
unsigned i;
|
||||
(void)u;
|
||||
for (i = 0; i < sizeof sigs / sizeof *sigs; i++) {
|
||||
memset(&c, 0, sizeof c);
|
||||
sigaction(sigs[i], NULL, &c);
|
||||
if (c.sa_handler != SIG_DFL)
|
||||
printf(" SIG%-8s %s\n", names[i],
|
||||
c.sa_handler == SIG_IGN ? "SIG_IGN" : "custom handler");
|
||||
}
|
||||
printf(" (every signal not named above is SIG_DFL)\n");
|
||||
fflush(stdout);
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
CAMLprim value spike_take_segv(value u) { (void)u; install(0); return Val_unit; }
|
||||
CAMLprim value spike_chain_segv(value u) { (void)u; install(1); return Val_unit; }
|
||||
|
||||
#ifndef SPIKE_NO_MAIN
|
||||
int main(int argc, char **argv) {
|
||||
struct sigaction cur;
|
||||
(void)argc;
|
||||
memset(&cur, 0, sizeof cur);
|
||||
sigaction(SIGSEGV, NULL, &cur);
|
||||
printf(" SIGSEGV %-46s handler=%p%s\n", "before caml_startup",
|
||||
(void *)cur.sa_handler, cur.sa_handler == SIG_DFL ? " (SIG_DFL)" : "");
|
||||
/* Everything else runs from sig_ml.ml's module initialiser, so the readings
|
||||
* happen on the runtime's own thread at the moments that matter. */
|
||||
caml_startup(argv);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@ -1,61 +0,0 @@
|
||||
/* Step 6: does OCaml's collector touch memory it does not own?
|
||||
*
|
||||
* Flan's arenas, Vecs and Maps are plain malloc'd memory. The claim is that
|
||||
* OCaml never sees them, so a compaction cannot move or scribble on them. The
|
||||
* probe: fill an arena with a checkable pattern, hold raw interior pointers
|
||||
* into it across a full major collection AND a compaction, then verify every
|
||||
* byte and every pointer.
|
||||
*
|
||||
* What this proves is narrow and worth stating narrowly: OCaml traces its own
|
||||
* roots only. It does NOT license storing an OCaml `value` in this arena --
|
||||
* that would need caml_register_global_root, and is the way the assumption
|
||||
* actually breaks.
|
||||
*/
|
||||
#include <caml/callback.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/memory.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define ARENA (8u << 20) /* 8 MiB, the shape of a Flan arena */
|
||||
|
||||
static uint8_t *arena;
|
||||
static uint64_t *interior[64];
|
||||
|
||||
CAMLprim value spike_note_arena(value p) { (void)p; return Val_unit; }
|
||||
|
||||
static uint8_t pattern(size_t i) { return (uint8_t)(i * 31u + 7u); }
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const value *f;
|
||||
size_t i, bad = 0;
|
||||
uint8_t *before;
|
||||
(void)argc;
|
||||
|
||||
arena = malloc(ARENA);
|
||||
if (!arena) return 1;
|
||||
for (i = 0; i < ARENA; i++) arena[i] = pattern(i);
|
||||
for (i = 0; i < 64; i++) interior[i] = (uint64_t *)(arena + i * 4096);
|
||||
before = arena;
|
||||
|
||||
caml_startup(argv);
|
||||
|
||||
f = caml_named_value("spike_churn");
|
||||
if (!f) { fprintf(stderr, "spike: churn not registered\n"); return 1; }
|
||||
printf("%s\n", String_val(caml_callback(*f, Val_int(200))));
|
||||
|
||||
for (i = 0; i < ARENA; i++) if (arena[i] != pattern(i)) bad++;
|
||||
printf("arena base %s (%p -> %p)\n", before == arena ? "unmoved" : "MOVED",
|
||||
(void *)before, (void *)arena);
|
||||
printf("arena bytes altered by the GC: %zu of %u\n", bad, ARENA);
|
||||
|
||||
bad = 0;
|
||||
for (i = 0; i < 64; i++)
|
||||
if (interior[i] != (uint64_t *)(arena + i * 4096)) bad++;
|
||||
printf("raw interior pointers invalidated: %zu of 64\n", bad);
|
||||
printf("spike: %s\n", bad == 0 ? "foreign memory is invisible to the collector"
|
||||
: "FOREIGN MEMORY WAS DISTURBED");
|
||||
free(arena);
|
||||
return 0;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
(* Step 1: the smallest thing that proves OCaml code can be reached from a C
|
||||
[main]. One function, registered by name, called back from C. *)
|
||||
let () =
|
||||
Callback.register "spike_greet" (fun (n : int) ->
|
||||
Printf.sprintf "ocaml saw %d, unix says pid %d" n (Unix.getpid ()))
|
||||
@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Step 8: the thing the whole spike is really asking about -- ONE binary that
|
||||
# is both a compiled Flan program and the OCaml compiler, with clang doing the
|
||||
# final link.
|
||||
#
|
||||
# Everything before this proved a piece. This proves the shape: the Flan
|
||||
# program's own main() is renamed out of the way, a C main() takes the main
|
||||
# thread and runs the program there, and caml_startup happens on a side thread
|
||||
# beside it. That is exactly item 11's inversion, built for real.
|
||||
#
|
||||
# It is NOT the merged architecture -- nothing is wired up, the compiler and the
|
||||
# program do not talk. It is a link and a size and a startup number.
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
src=${1:-test/programs/edn.flan}
|
||||
cd "$root" || exit 1
|
||||
out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
|
||||
|
||||
FLAN=_build/default/bin/main.exe
|
||||
OCAMLLIB=$(ocamlopt -where)
|
||||
SYSLIBS="-lm -lpthread -ldl -lzstd"
|
||||
|
||||
echo "program: $src"
|
||||
|
||||
# 1. The Flan program, as it is built today, for the baseline sizes.
|
||||
"$FLAN" build "$src" -o "$out/rel" || exit 1
|
||||
"$FLAN" build "$src" --dev -o "$out/dev" || exit 1
|
||||
|
||||
# 2. The same program as an object, with its main renamed so a C main can own
|
||||
# the process. Emit writes @main literally; sed is enough to move it.
|
||||
"$FLAN" emit "$src" --dev > "$out/prog.ll" || exit 1
|
||||
sed -i 's/define i32 @main(/define i32 @flan_program_main(/' "$out/prog.ll"
|
||||
grep -q 'define i32 @flan_program_main(' "$out/prog.ll" || {
|
||||
echo "could not find @main in the emitted IR -- adjust the rename"; exit 1; }
|
||||
clang -c -x ir "$out/prog.ll" -o "$out/prog.o" || exit 1
|
||||
|
||||
# 3. The runtime the program needs, and the agent beside it.
|
||||
clang -c -O2 runtime/flan_rt.c -o "$out/rt.o" || exit 1
|
||||
clang -c -O2 runtime/flan_dev.c -o "$out/dev.o" || exit 1
|
||||
clang -c -O2 vendor/agent/flan_agent.c -o "$out/ag.o" || exit 1
|
||||
|
||||
# 4. The whole OCaml compiler as one object.
|
||||
ocamlfind ocamlopt -thread -package unix,threads.posix -linkpkg \
|
||||
-output-complete-obj \
|
||||
-I "$root/_build/default/lib/.flan.objs/byte" \
|
||||
-I "$root/_build/default/lib/.flan.objs/native" \
|
||||
-o "$out/compiler.o" "$root/_build/default/lib/flan.cmxa" \
|
||||
"$here/thread_ml.ml" || exit 1
|
||||
|
||||
# 5. One link. clang, as the project already does it.
|
||||
clang -I"$OCAMLLIB" "$here/merged_main.c" "$out/prog.o" "$out/rt.o" "$out/dev.o" \
|
||||
"$out/ag.o" "$out/compiler.o" -o "$out/merged" $SYSLIBS || exit 1
|
||||
|
||||
echo
|
||||
echo "sizes:"
|
||||
for f in rel dev merged; do
|
||||
printf ' %-30s %9d bytes\n' "$f" "$(stat -c%s "$out/$f")"
|
||||
done
|
||||
printf ' %-30s %9d bytes\n' "what the compiler adds to a dev build" \
|
||||
"$(( $(stat -c%s "$out/merged") - $(stat -c%s "$out/dev") ))"
|
||||
|
||||
echo
|
||||
echo "running the merged binary:"
|
||||
"$out/merged" "$src"
|
||||
echo "exit: $?"
|
||||
@ -1,77 +0,0 @@
|
||||
/* One process: the Flan program on the main thread, the OCaml compiler beside
|
||||
* it on a domain of its own.
|
||||
*
|
||||
* This is the shape item 11 settles on, and the reason it is written this way
|
||||
* round rather than the other: on macOS the window has to be on the main
|
||||
* thread, so the game keeps main() and the compiler moves to the side --
|
||||
* beside the listener vendor/agent/flan_agent.c already starts there.
|
||||
*
|
||||
* The program and the compiler do not talk to each other here. Wiring them up
|
||||
* is the real work; this only shows they can share an address space, a link,
|
||||
* and a process, with clang doing the final link. */
|
||||
#include <caml/callback.h>
|
||||
#include <caml/alloc.h>
|
||||
#include <caml/mlvalues.h>
|
||||
#include <caml/threads.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
/* The Flan program's entry point, renamed out of main's way by merged.sh. */
|
||||
extern int flan_program_main(int argc, char **argv);
|
||||
|
||||
static char **g_argv;
|
||||
static const char *g_src;
|
||||
/* The Flan program's main calls exit(), so the compiler has to be up before it
|
||||
* starts -- which is the honest ordering anyway: the image comes up and serves,
|
||||
* then the program runs, the way starting an SBCL image does. */
|
||||
static atomic_int compiler_ready = 0;
|
||||
|
||||
static double ms_since(struct timespec a) {
|
||||
struct timespec b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
return (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6;
|
||||
}
|
||||
|
||||
static void *compiler_side(void *unused) {
|
||||
struct timespec t0;
|
||||
const value *f;
|
||||
(void)unused;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
caml_startup(g_argv);
|
||||
printf("[compiler] up on a side thread in %.3f ms\n", ms_since(t0));
|
||||
f = caml_named_value("spike_thread_compile");
|
||||
if (f) {
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
printf("[compiler] %s\n", String_val(caml_callback(*f, caml_copy_string(g_src))));
|
||||
printf("[compiler] compiled the running program from inside it, in %.3f ms\n",
|
||||
ms_since(t0));
|
||||
}
|
||||
caml_release_runtime_system();
|
||||
atomic_store(&compiler_ready, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
pthread_t comp;
|
||||
int rc;
|
||||
g_argv = argv;
|
||||
g_src = argc > 1 ? argv[1] : "test/programs/edn.flan";
|
||||
|
||||
if (pthread_create(&comp, NULL, compiler_side, NULL) != 0) return 1;
|
||||
|
||||
while (!atomic_load(&compiler_ready)) {
|
||||
struct timespec t = { 0, 2000000L };
|
||||
nanosleep(&t, NULL);
|
||||
}
|
||||
|
||||
/* The main thread is the program's, and it never enters OCaml. */
|
||||
printf("[program] running on the main thread\n");
|
||||
rc = flan_program_main(argc, argv);
|
||||
printf("[program] returned %d\n", rc);
|
||||
|
||||
pthread_join(comp, NULL);
|
||||
printf("one process: a Flan program and the OCaml compiler, same binary\n");
|
||||
return 0;
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Spike: what it costs to link the OCaml compiler into a native Flan dev build.
|
||||
#
|
||||
# Deliberately NOT a dune target. The root `dune` only excludes old-ocaml/, so a
|
||||
# dune file here would land in @default and make the spike part of the build.
|
||||
# Instead this drives ocamlfind and clang by hand, against the flan.cmxa that
|
||||
# dune already produces. Run it from anywhere: bash spike/embed/run.sh
|
||||
set -u
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
cd "$here" || exit 1
|
||||
|
||||
OCAMLLIB=$(ocamlopt -where)
|
||||
CAMLINC="-I$OCAMLLIB"
|
||||
# OCaml 5.2's marshaller is compressed, so -output-complete-obj pulls in zstd.
|
||||
SYSLIBS="-lm -lpthread -ldl -lzstd"
|
||||
|
||||
step() { printf '\n=== %s ===\n' "$1"; }
|
||||
|
||||
# ---------------------------------------------------------------- 1. smallest
|
||||
step "1. smallest link: C main() -> caml_startup -> OCaml callback"
|
||||
ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \
|
||||
-o embed1.o hello_ml.ml || exit 1
|
||||
clang $CAMLINC harness1.c embed1.o -o spike1 $SYSLIBS || exit 1
|
||||
./spike1 || echo "spike1 FAILED"
|
||||
ls -l spike1 | awk '{print "spike1 size: " $5 " bytes"}'
|
||||
|
||||
# ------------------------------------------------------- 2. the real compiler
|
||||
step "2. link the whole flan compiler (flan.cmxa) into a C binary"
|
||||
CMXA="$root/_build/default/lib/flan.cmxa"
|
||||
if [ ! -f "$CMXA" ]; then
|
||||
echo "no $CMXA -- run 'dune build --root .' first"; exit 1
|
||||
fi
|
||||
ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \
|
||||
-I "$root/_build/default/lib/.flan.objs/byte" \
|
||||
-I "$root/_build/default/lib/.flan.objs/native" \
|
||||
-o embed2.o "$CMXA" whole_ml.ml || exit 1
|
||||
clang $CAMLINC harness2.c embed2.o -o spike2 $SYSLIBS || exit 1
|
||||
(cd "$root" && "$here/spike2" test/programs/edn.flan) || echo "spike2 FAILED"
|
||||
ls -l spike2 | awk '{print "spike2 size: " $5 " bytes"}'
|
||||
ls -l "$root/_build/default/bin/main.exe" | awk '{print "main.exe size: " $5 " bytes"}'
|
||||
clang $CAMLINC baseline.c -o baseline
|
||||
ls -l baseline | awk '{print "bare C baseline: " $5 " bytes"}'
|
||||
|
||||
# ------------------------------------------------------------- 3. with stubs
|
||||
step "3. the same, with the project's own C stubs compiled in"
|
||||
ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \
|
||||
-I "$root/_build/default/lib/.flan.objs/byte" \
|
||||
-I "$root/_build/default/lib/.flan.objs/native" \
|
||||
-o embed3.o "$CMXA" stubs_ml.ml dynload_stubs.c || exit 1
|
||||
clang $CAMLINC harness3.c embed3.o -o spike3 $SYSLIBS || exit 1
|
||||
./spike3 || echo "spike3 FAILED"
|
||||
|
||||
# ------------------------------------------ 4. threads: game owns main thread
|
||||
step "4. threads: C main runs the 'game loop', OCaml starts on another thread"
|
||||
ocamlfind ocamlopt -thread -package unix,threads.posix -linkpkg -output-complete-obj \
|
||||
-I "$root/_build/default/lib/.flan.objs/byte" \
|
||||
-I "$root/_build/default/lib/.flan.objs/native" \
|
||||
-o embed4.o "$CMXA" thread_ml.ml || exit 1
|
||||
clang $CAMLINC harness4.c embed4.o -o spike4 $SYSLIBS || exit 1
|
||||
(cd "$root" && "$here/spike4" test/programs/edn.flan) || echo "spike4 FAILED"
|
||||
|
||||
# ------------------------------------------------------------ 5. signals
|
||||
step "5. signals: who owns SIGSEGV across caml_startup"
|
||||
clang $CAMLINC harness5.c embed2.o -o spike5 $SYSLIBS || exit 1
|
||||
./spike5 || echo "spike5 exited nonzero"
|
||||
|
||||
# ------------------------------------------------------- 6. GC vs raw memory
|
||||
step "6. GC: does a compaction move or touch a C-owned arena"
|
||||
ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \
|
||||
-o embed6.o gc_ml.ml || exit 1
|
||||
clang $CAMLINC harness6.c embed6.o -o spike6 $SYSLIBS || exit 1
|
||||
./spike6 || echo "spike6 FAILED"
|
||||
|
||||
printf '\nspike: done\n'
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Step 5b on its own: the SIGSEGV question, embedded and standalone side by
|
||||
# side. The standalone build is the control -- if OCaml behaves the same in a
|
||||
# plain ocamlopt executable, then embedding changed nothing about signals.
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "$here" || exit 1
|
||||
OCAMLLIB=$(ocamlopt -where)
|
||||
SYSLIBS="-lm -lpthread -ldl -lzstd"
|
||||
|
||||
echo "=== 5b-embedded: caml_startup called from a C main() ==="
|
||||
ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \
|
||||
-o embed5b.o sig_ml.ml || exit 1
|
||||
clang -I"$OCAMLLIB" harness5b.c embed5b.o -o spike5b $SYSLIBS || exit 1
|
||||
./spike5b; echo "exit: $?"
|
||||
|
||||
echo
|
||||
echo "=== 5b-standalone: the same OCaml, as a plain ocamlopt executable ==="
|
||||
# The control. Same stubs, but OCaml owns main().
|
||||
clang -c -I"$OCAMLLIB" -DSPIKE_NO_MAIN harness5b.c -o stubs5b.o || exit 1
|
||||
ocamlfind ocamlopt -package unix -linkpkg -o spike5b_std sig_ml.ml stubs5b.o \
|
||||
-cclib -lzstd || exit 1
|
||||
./spike5b_std; echo "exit: $?"
|
||||
@ -1,34 +0,0 @@
|
||||
(* Step 5b: OCaml 5.2 installs its SIGSEGV handler per-domain, not once at
|
||||
startup, so "read the disposition after caml_startup" is not the whole
|
||||
question. This asks it at four moments, and then asks the thing that
|
||||
actually matters: does Stack_overflow still get raised once the break loop
|
||||
has taken SIGSEGV? *)
|
||||
|
||||
external show : string -> unit = "spike_show_segv"
|
||||
external take_segv : unit -> unit = "spike_take_segv"
|
||||
external chain_segv : unit -> unit = "spike_chain_segv"
|
||||
external sweep : unit -> unit = "spike_sweep"
|
||||
|
||||
let rec deep n = if n <= 0 then 0 else 1 + deep (n - 1) + (if n < 0 then deep n else 0)
|
||||
|
||||
let overflow_result () =
|
||||
try
|
||||
let n = deep 100_000_000 in
|
||||
Printf.sprintf "returned %d (no overflow)" n
|
||||
with Stack_overflow -> "Stack_overflow raised"
|
||||
|
||||
let () =
|
||||
show "at module init (main domain up)";
|
||||
let d = Domain.spawn (fun () -> show "inside a spawned domain") in
|
||||
Domain.join d;
|
||||
show "after Domain.join";
|
||||
print_endline "every signal the OCaml runtime is holding:";
|
||||
sweep ();
|
||||
Printf.sprintf "before touching SIGSEGV: %s" (overflow_result ()) |> print_endline;
|
||||
take_segv ();
|
||||
show "after the break loop takes SIGSEGV outright";
|
||||
Printf.sprintf "with SIGSEGV taken outright: %s" (overflow_result ())
|
||||
|> print_endline;
|
||||
chain_segv ();
|
||||
show "after the break loop chains to OCaml's handler";
|
||||
Printf.sprintf "with SIGSEGV chained: %s" (overflow_result ()) |> print_endline
|
||||
@ -1,26 +0,0 @@
|
||||
(* Step 3: the project's own C stubs, in the same link as the compiler.
|
||||
lib/dynload_stubs.c is taken verbatim from 9e0ae3a (the unmerged dlopen
|
||||
branch) -- it is the only C the compiler itself is built from, and it is the
|
||||
case that -output-complete-obj has to carry through. *)
|
||||
|
||||
external dl_open : string -> nativeint = "flan_dl_open"
|
||||
external dl_sym : nativeint -> string -> nativeint = "flan_dl_sym"
|
||||
external mem_alloc : int -> nativeint = "flan_mem_alloc"
|
||||
external mem_free : nativeint -> unit = "flan_mem_free"
|
||||
external poke_i64 : nativeint -> int -> int64 -> unit = "flan_poke_i64"
|
||||
external peek_i64 : nativeint -> int -> int64 = "flan_peek_i64"
|
||||
|
||||
let () =
|
||||
Callback.register "spike_stubs" (fun () ->
|
||||
(* peek/poke: the raw memory the marshaller lays a Form image out in. *)
|
||||
let p = mem_alloc 64 in
|
||||
poke_i64 p 8 0xfeedfacedeadbeefL;
|
||||
let got = peek_i64 p 8 in
|
||||
mem_free p;
|
||||
(* dlopen from inside the embedded runtime, on the process's own image. *)
|
||||
let h = dl_open "libm.so.6" in
|
||||
let s = dl_sym h "sqrt" in
|
||||
Printf.sprintf "peek/poke %s; dlopen+dlsym %s"
|
||||
(if got = 0xfeedfacedeadbeefL then "ok" else "WRONG")
|
||||
(if s <> 0n then "ok" else "WRONG"));
|
||||
ignore dl_open
|
||||
@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Step 7: the integration hazard nobody asks about until the link fails.
|
||||
#
|
||||
# Today runtime/flan_rt.c, runtime/flan_dev.c and vendor/agent/flan_agent.c are
|
||||
# compiled into the *program*, and lib/dynload_stubs.c into the *compiler*.
|
||||
# Merging the processes puts all four and the OCaml runtime in one link. This
|
||||
# checks, symbol by symbol, whether anything collides.
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
out=$(mktemp -d)
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
cd "$root" || exit 1
|
||||
|
||||
defs() { nm --defined-only "$@" 2>/dev/null | awk 'NF==3 {print $3}' | sort -u; }
|
||||
|
||||
clang -c runtime/flan_rt.c -o "$out/rt.o" || exit 1
|
||||
clang -c runtime/flan_dev.c -o "$out/dev.o" || exit 1
|
||||
clang -c vendor/agent/flan_agent.c -o "$out/ag.o" || exit 1
|
||||
clang -c -I"$(ocamlopt -where)" "$here/dynload_stubs.c" -o "$out/dl.o" || exit 1
|
||||
|
||||
defs "$out/rt.o" "$out/dev.o" "$out/ag.o" "$out/dl.o" > "$out/flan.syms"
|
||||
defs /home/joe/.opam/default/lib/ocaml/libasmrun.a > "$out/ml.syms" 2>/dev/null
|
||||
[ -s "$out/ml.syms" ] || defs "$(ocamlopt -where)/libasmrun.a" > "$out/ml.syms"
|
||||
|
||||
echo "Flan's own C defines $(wc -l < "$out/flan.syms") symbols;" \
|
||||
"libasmrun defines $(wc -l < "$out/ml.syms")."
|
||||
echo "collisions between Flan's C and the OCaml runtime:"
|
||||
if comm -12 "$out/flan.syms" "$out/ml.syms" | grep . ; then
|
||||
echo " ^^ those would have to be renamed"
|
||||
else
|
||||
echo " none"
|
||||
fi
|
||||
|
||||
echo "collisions among Flan's own four .c files:"
|
||||
for a in rt dev ag dl; do defs "$out/$a.o" > "$out/$a.syms"; done
|
||||
found=0
|
||||
for a in rt dev ag dl; do
|
||||
for b in rt dev ag dl; do
|
||||
[ "$a" \< "$b" ] || continue
|
||||
c=$(comm -12 "$out/$a.syms" "$out/$b.syms")
|
||||
[ -n "$c" ] && { echo " $a vs $b:"; echo "$c" | sed 's/^/ /'; found=1; }
|
||||
done
|
||||
done
|
||||
[ $found -eq 0 ] && echo " none"
|
||||
|
||||
echo "what Flan's C needs that the OCaml runtime also exports (shared libc etc):"
|
||||
nm --undefined-only "$out/rt.o" "$out/ag.o" 2>/dev/null | awk 'NF==2{print $2}' \
|
||||
| sort -u > "$out/need.syms"
|
||||
comm -12 "$out/need.syms" "$out/ml.syms" | sed 's/^/ /' | head -20
|
||||
@ -1,32 +0,0 @@
|
||||
(* Step 4: the macOS shape. The game owns the main thread; the compiler and the
|
||||
listener run beside it.
|
||||
|
||||
The question is NOT "can OCaml use threads" -- it is whether caml_startup can
|
||||
be called from a pthread that C spawned, while main() goes on to run a
|
||||
window loop it never returns from. That is the inversion item 11 settles on,
|
||||
and it is the one that has to be measured rather than assumed. *)
|
||||
|
||||
let compile file =
|
||||
let l = Flan.Load.program ~file (Flan.Parse.program (Flan.Reader.read_file file)) in
|
||||
let p = Flan.Check.program l.Flan.Load.decls in
|
||||
String.length (Flan.Emit.program ~dev:true p)
|
||||
|
||||
let () =
|
||||
Callback.register "spike_thread_compile" (fun (file : string) ->
|
||||
let tid = Thread.id (Thread.self ()) in
|
||||
match compile file with
|
||||
| n ->
|
||||
Printf.sprintf "compiled on OCaml thread %d: %d bytes of LLVM IR" tid n
|
||||
| exception e -> Printf.sprintf "FAILED: %s" (Printexc.to_string e));
|
||||
Callback.register "spike_domains" (fun () ->
|
||||
(* A second domain doing real work while the main thread is elsewhere --
|
||||
5.2's multicore runtime, which is the objection item 12 says has gone
|
||||
away. Confirmed rather than assumed. *)
|
||||
let d = Domain.spawn (fun () ->
|
||||
let s = ref 0 in
|
||||
for i = 1 to 5_000_000 do s := !s + i done;
|
||||
(Domain.self () :> int), !s)
|
||||
in
|
||||
let id, s = Domain.join d in
|
||||
Printf.sprintf "domain %d summed to %d; recommended_domain_count = %d"
|
||||
id s (Domain.recommended_domain_count ()))
|
||||
@ -1,40 +0,0 @@
|
||||
(* Step 2: reach enough of the compiler that the linker cannot drop it, and do
|
||||
real compiler work in-process so the measurement is of a working compiler
|
||||
rather than of dead code that happened to link.
|
||||
|
||||
The work is the driver's own path, the one bin/main.ml takes:
|
||||
read -> Parse.program -> Load.program -> Check.program -> Emit.program. That
|
||||
is the whole front end and the whole back end short of [llc]. Check.program
|
||||
prepends the prelude itself, so the prelude is in the measurement without
|
||||
being fed in twice. *)
|
||||
|
||||
let compile file =
|
||||
let l = Flan.Load.program ~file (Flan.Parse.program (Flan.Reader.read_file file)) in
|
||||
let p = Flan.Check.program l.Flan.Load.decls in
|
||||
let ir = Flan.Emit.program ~dev:true p in
|
||||
(List.length l.Flan.Load.decls, String.length ir)
|
||||
|
||||
(* Touched only so the linker keeps the modules a merged dev build would carry.
|
||||
Nothing here is called for its effect. *)
|
||||
let footprint () =
|
||||
String.concat ","
|
||||
[ Flan.Build.clang;
|
||||
string_of_int (String.length Flan.Shim.header);
|
||||
string_of_int (String.length Flan.Runtime_src.source);
|
||||
string_of_int (String.length Flan.Runtime_src.dev_source);
|
||||
string_of_int (List.length Flan.Session.externs);
|
||||
string_of_int (Flan.Render.max_span);
|
||||
string_of_int (String.length (Flan.Wire.ints [ 1; 2 ])) ]
|
||||
|
||||
let () =
|
||||
Callback.register "spike_compile" (fun (file : string) ->
|
||||
match compile file with
|
||||
| d, n -> Printf.sprintf "%s: %d decls, %d bytes of LLVM IR" file d n
|
||||
| exception e -> Printf.sprintf "FAILED: %s" (Printexc.to_string e));
|
||||
Callback.register "spike_footprint" footprint;
|
||||
(* Dev.start and Cimport are never run here, but naming them keeps the socket
|
||||
server and the C importer in the link -- a dev build pays for them. *)
|
||||
Callback.register "spike_unused" (fun () ->
|
||||
ignore (Flan.Dev.start : ?debug:bool -> file:string -> sock:string -> unit -> unit);
|
||||
ignore (Flan.Cimport.decl_source : Flan.Ast.decl -> string);
|
||||
"ok")
|
||||
@ -1,7 +0,0 @@
|
||||
(defn id [x $t] t x)
|
||||
|
||||
(defn main [] ()
|
||||
(println (id 3))
|
||||
(println (id 4.5))
|
||||
(println (id 7))
|
||||
(println (id true)))
|
||||
@ -1,171 +0,0 @@
|
||||
(* What redefining a generic function costs the dev loop, measured.
|
||||
|
||||
The question the spike exists to answer: C-c C-c on a concrete function is
|
||||
about 35 ms today, and a generic function that is redefined has to rebuild
|
||||
*every* instantiation. So the sweep is one generic called at N concrete
|
||||
types, N = 1..8, against the handwritten N-copies program it replaces, and
|
||||
the three things a C-c C-c actually pays for are timed separately:
|
||||
|
||||
check Check.program_with_env over the whole accumulated program —
|
||||
which is what Session.eval does on every evaluation, so this is
|
||||
paid whether the redefined function is generic or not.
|
||||
emit Emit.redefinition for the fns being installed.
|
||||
build llc + ld -shared, from Build.shared — the dominant term.
|
||||
|
||||
Nothing here modifies the session or the dev loop; it drives the real ones. *)
|
||||
|
||||
let tys = [| "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "f32" |]
|
||||
|
||||
let time f =
|
||||
let t0 = Unix.gettimeofday () in
|
||||
let x = f () in
|
||||
(x, (Unix.gettimeofday () -. t0) *. 1000.)
|
||||
|
||||
(* Best of k, because llc and the linker are processes and the machine is
|
||||
noisy; a median would hide a systematic cost and a mean would report the
|
||||
scheduler. *)
|
||||
let best k f =
|
||||
let rec go i acc = if i = 0 then acc else
|
||||
let _, ms = time f in go (i - 1) (min acc ms) in
|
||||
go k infinity
|
||||
|
||||
let generic_src n =
|
||||
let b = Buffer.create 1024 in
|
||||
Buffer.add_string b
|
||||
"(defn gswap [xs [$t] i i32 j i32] ()\n\
|
||||
\ (let [tmp (at xs i)]\n\
|
||||
\ (set (at xs i) (at xs j))\n\
|
||||
\ (set (at xs j) tmp)))\n\n\
|
||||
(defn gsort [s [$t] before? (Fn [$t $t] bool)] ()\n\
|
||||
\ (let [i 1]\n\
|
||||
\ (while (< i (length s))\n\
|
||||
\ (let [j i]\n\
|
||||
\ (while (and (> j 0) (before? (at s j) (at s (- j 1))))\n\
|
||||
\ (gswap s (- j 1) j)\n\
|
||||
\ (set j (- j 1))))\n\
|
||||
\ (set i (+ i 1)))))\n\n";
|
||||
for i = 0 to n - 1 do
|
||||
Buffer.add_string b (Printf.sprintf "(defonce xs-%s [8 %s])\n" tys.(i) tys.(i))
|
||||
done;
|
||||
Buffer.add_string b "\n(defn main [] ()\n";
|
||||
for i = 0 to n - 1 do
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf " (gsort (slice xs-%s 0 8) (fn [a b] (< a b)))\n" tys.(i))
|
||||
done;
|
||||
Buffer.add_string b " )\n";
|
||||
Buffer.contents b
|
||||
|
||||
(* The same program as it is written today: one copy of each function per
|
||||
element type, by hand. This is prelude.ml's shape. *)
|
||||
let mono_src n =
|
||||
let b = Buffer.create 1024 in
|
||||
for i = 0 to n - 1 do
|
||||
let t = tys.(i) in
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf
|
||||
"(defn mswap-%s [xs [%s] i i32 j i32] ()\n\
|
||||
\ (let [tmp (at xs i)]\n\
|
||||
\ (set (at xs i) (at xs j))\n\
|
||||
\ (set (at xs j) tmp)))\n\n\
|
||||
(defn msort-%s [s [%s] before? (Fn [%s %s] bool)] ()\n\
|
||||
\ (let [i 1]\n\
|
||||
\ (while (< i (length s))\n\
|
||||
\ (let [j i]\n\
|
||||
\ (while (and (> j 0) (before? (at s j) (at s (- j 1))))\n\
|
||||
\ (mswap-%s s (- j 1) j)\n\
|
||||
\ (set j (- j 1))))\n\
|
||||
\ (set i (+ i 1)))))\n\n"
|
||||
t t t t t t t);
|
||||
Buffer.add_string b (Printf.sprintf "(defonce xs-%s [8 %s])\n\n" t t)
|
||||
done;
|
||||
Buffer.add_string b "(defn main [] ()\n";
|
||||
for i = 0 to n - 1 do
|
||||
Buffer.add_string b
|
||||
(Printf.sprintf " (msort-%s (slice xs-%s 0 8) (fn [a b] (< a b)))\n"
|
||||
tys.(i) tys.(i))
|
||||
done;
|
||||
Buffer.add_string b " )\n";
|
||||
Buffer.contents b
|
||||
|
||||
let write path s =
|
||||
let oc = open_out path in output_string oc s; close_out oc
|
||||
|
||||
let dir =
|
||||
let d = Filename.concat (Filename.get_temp_dir_name ()) "flan-generics-spike" in
|
||||
(try Unix.mkdir d 0o700 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
|
||||
d
|
||||
|
||||
let decls_of path =
|
||||
(Flan.Load.program ~file:path
|
||||
(Flan.Parse.program (Flan.Reader.read_file path))).Flan.Load.decls
|
||||
|
||||
(* Every function the program ended up with whose name starts with one of the
|
||||
generic names — the instantiations, which is exactly what a redefinition of
|
||||
the generic would have to rebuild. *)
|
||||
let instantiations (p : Flan.Tast.program) =
|
||||
List.filter_map
|
||||
(fun (f : Flan.Tast.fn) ->
|
||||
let n = f.Flan.Tast.name in
|
||||
if String.length n > 6 && String.sub n 0 6 = "gswap-" then Some n
|
||||
else if String.length n > 6 && String.sub n 0 6 = "gsort-" then Some n
|
||||
else None)
|
||||
p.Flan.Tast.fns
|
||||
|
||||
let build_ms ir =
|
||||
let out = Filename.concat dir "redef.so" in
|
||||
best 3 (fun () ->
|
||||
ignore
|
||||
(Flan.Build.shared
|
||||
~opts:{ Flan.Build.default with dev = true } ~ir ~out ()))
|
||||
|
||||
let () =
|
||||
Printf.printf
|
||||
"n check-gen check-mono emit-1 emit-N build-1 build-N fns\n";
|
||||
(try
|
||||
for n = 1 to 8 do
|
||||
let gpath = Filename.concat dir (Printf.sprintf "gen%d.flan" n) in
|
||||
let mpath = Filename.concat dir (Printf.sprintf "mono%d.flan" n) in
|
||||
write gpath (generic_src n);
|
||||
write mpath (mono_src n);
|
||||
let gd = decls_of gpath and md = decls_of mpath in
|
||||
let check_gen = best 3 (fun () -> ignore (Flan.Check.program_with_env gd)) in
|
||||
let check_mono = best 3 (fun () -> ignore (Flan.Check.program_with_env md)) in
|
||||
let p, _ = Flan.Check.program_with_env gd in
|
||||
let insts = instantiations p in
|
||||
let one = [ List.hd insts ] in
|
||||
let ir_one =
|
||||
Flan.Emit.redefinition ~dev:true ~known:(fun _ -> true) p ~fns:one
|
||||
in
|
||||
let ir_all =
|
||||
Flan.Emit.redefinition ~dev:true ~known:(fun _ -> true) p ~fns:insts
|
||||
in
|
||||
let emit1 =
|
||||
best 3 (fun () ->
|
||||
ignore (Flan.Emit.redefinition ~dev:true ~known:(fun _ -> true) p ~fns:one))
|
||||
and emitn =
|
||||
best 3 (fun () ->
|
||||
ignore (Flan.Emit.redefinition ~dev:true ~known:(fun _ -> true) p ~fns:insts))
|
||||
in
|
||||
let b1 = build_ms ir_one and bn = build_ms ir_all in
|
||||
Printf.printf "%d %8.1f %10.1f %7.1f %7.1f %8.1f %8.1f %d\n%!"
|
||||
n check_gen check_mono emit1 emitn b1 bn (List.length insts)
|
||||
done
|
||||
with Flan.Loc.Error d -> prerr_endline (Flan.Loc.report d); exit 1);
|
||||
(* And what the session actually does when the generic itself is redefined.
|
||||
This is the real C-c C-c path — Session.eval on the form the editor sent
|
||||
— and what it reports is the finding, not the timing. *)
|
||||
let gpath = Filename.concat dir "gen4.flan" in
|
||||
let t, _ = Flan.Session.create ~file:gpath () in
|
||||
let form =
|
||||
"(defn gswap [xs [$t] i i32 j i32] ()\n\
|
||||
\ (let [tmp (at xs i)]\n\
|
||||
\ (set (at xs i) (at xs j))\n\
|
||||
\ (set (at xs j) tmp)))\n"
|
||||
in
|
||||
let c, ms = time (fun () -> Flan.Session.eval ~origin:gpath t form) in
|
||||
Printf.printf
|
||||
"\nSession.eval on the generic gswap itself: %.1f ms, installs=%b, \
|
||||
fns=[%s], names=[%s]\n"
|
||||
ms c.Flan.Session.installs
|
||||
(String.concat " " c.Flan.Session.fns)
|
||||
(String.concat " " c.Flan.Session.names)
|
||||
@ -1,51 +0,0 @@
|
||||
;; Which of prelude.ml's per-type families collapse as they are written, and
|
||||
;; which need their signature changed. Nothing here is installed in the
|
||||
;; prelude; it is the same bodies, over $t, checked and run.
|
||||
|
||||
(defn keep [s [$t] keep? (Fn [$t] bool)] (Vec $t)
|
||||
(let [v (vec-new t)]
|
||||
(dotimes [i (length s)]
|
||||
(when (keep? (at s i))
|
||||
(push v (at s i))))
|
||||
v))
|
||||
|
||||
(defn apply! [s [$t] f (Fn [$t] $t)] ()
|
||||
(dotimes [i (length s)]
|
||||
(set (at s i) (f (at s i)))))
|
||||
|
||||
(defn fold [s [$t] init $t f (Fn [$t $t] $t)] t
|
||||
(let [acc init]
|
||||
(dotimes [i (length s)]
|
||||
(set acc (f acc (at s i))))
|
||||
acc))
|
||||
|
||||
(defn flip! [s [$t]] ()
|
||||
(let [i 0
|
||||
j (- (length s) 1)]
|
||||
(while (< i j)
|
||||
(let [tmp (at s i)]
|
||||
(set (at s i) (at s j))
|
||||
(set (at s j) tmp))
|
||||
(set i (+ i 1))
|
||||
(set j (- j 1)))))
|
||||
|
||||
(defonce ns [5 i32])
|
||||
(defonce fs [5 f32])
|
||||
|
||||
(defn main [] ()
|
||||
(let [xs (slice ns 0 5)
|
||||
ys (slice fs 0 5)]
|
||||
(dotimes [i 5]
|
||||
(set (at xs i) (+ i 1))
|
||||
(set (at ys i) (f32 (* 2 (+ i 1)))))
|
||||
(apply! xs (fn [x] (* x 10)))
|
||||
(apply! ys (fn [x] (* x (f32 2))))
|
||||
(flip! xs)
|
||||
(flip! ys)
|
||||
(println (fold xs 0 (fn [a b] (+ a b))))
|
||||
(println (fold ys (f32 0) (fn [a b] (+ a b))))
|
||||
(let [evens (keep xs (fn [x] (= (% x 20) 0)))]
|
||||
(println (length evens))
|
||||
(free evens))
|
||||
(println (at xs 0))
|
||||
(println (at ys 0))))
|
||||
@ -1,4 +0,0 @@
|
||||
(defn add2 [a $t b $t] t (+ a b))
|
||||
|
||||
(defn main [] ()
|
||||
(println (add2 1 2)))
|
||||
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# The generics spike's measurement, driven by hand with ocamlfind against the
|
||||
# flan.cmxa dune already builds — the same arrangement spike/backend uses, and
|
||||
# for the same reason: nothing under spike/ is wired into the build, there is
|
||||
# no dune file here, and `dune test --root .` cannot see any of it.
|
||||
#
|
||||
# The three .flan programs beside this file are run with the ordinary driver:
|
||||
# dune exec --root . bin/main.exe -- run spike/generics/sort.flan
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
cd "$root" || exit 1
|
||||
|
||||
dune build --root . lib/flan.cmxa 2>&1 | head -20
|
||||
|
||||
out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
|
||||
|
||||
ocamlfind ocamlopt -thread -package unix,threads.posix -linkpkg \
|
||||
-I "$root/_build/default/lib/.flan.objs/byte" \
|
||||
-I "$root/_build/default/lib/.flan.objs/native" \
|
||||
-I "$out" -I "$here" \
|
||||
-o "$out/measure" \
|
||||
"$root/_build/default/lib/flan.cmxa" \
|
||||
-cclib -rdynamic -ccopt -L"$root/_build/default/lib" \
|
||||
"$here/measure.ml" 2>&1 | head -40
|
||||
|
||||
test -x "$out/measure" || { echo "build failed"; exit 1; }
|
||||
"$out/measure"
|
||||
@ -1,3 +0,0 @@
|
||||
(defn grow [x $t] ()
|
||||
(grow [x x]))
|
||||
(defn main [] () (grow 1))
|
||||
@ -1,25 +0,0 @@
|
||||
;; The shape prelude.ml's sort-i32-by! / sort-f32-by! pair would collapse into:
|
||||
;; one generic body, the comparison passed in as a function value because an
|
||||
;; unconstrained type variable has no < of its own.
|
||||
|
||||
(defn swap! [xs [$t] i i32 j i32] ()
|
||||
(let [tmp (at xs i)]
|
||||
(set (at xs i) (at xs j))
|
||||
(set (at xs j) tmp)))
|
||||
|
||||
(defn sort-by! [s [$t] before? (Fn [$t $t] bool)] ()
|
||||
(let [i 1]
|
||||
(while (< i (length s))
|
||||
(let [j i]
|
||||
(while (and (> j 0) (before? (at s j) (at s (- j 1))))
|
||||
(swap! s (- j 1) j)
|
||||
(set j (- j 1))))
|
||||
(set i (+ i 1)))))
|
||||
|
||||
(defn main [] ()
|
||||
(let [ns [5 3 9 1]
|
||||
fs [2.5 0.5 1.5]]
|
||||
(sort-by! (slice ns 0 4) (fn [a b] (< a b)))
|
||||
(sort-by! (slice fs 0 3) (fn [a b] (> a b)))
|
||||
(dotimes [i 4] (println (at ns i)))
|
||||
(dotimes [i 3] (println (at fs i)))))
|
||||
@ -1,19 +0,0 @@
|
||||
;; One generic function over one type variable, called at two concrete types
|
||||
;; in one program. The sigil binds ($t), a bare use reads it (t).
|
||||
|
||||
(defn swap! [xs [$t] i i32 j i32] ()
|
||||
(let [tmp (at xs i)]
|
||||
(set (at xs i) (at xs j))
|
||||
(set (at xs j) tmp)))
|
||||
|
||||
(defn main [] ()
|
||||
(let [ns [10 20 30]
|
||||
fs [1.5 2.5 3.5]]
|
||||
(swap! (slice ns 0 3) 0 2)
|
||||
(swap! (slice fs 0 3) 0 1)
|
||||
(swap! (slice ns 0 3) 1 2)
|
||||
(println (at ns 0))
|
||||
(println (at ns 1))
|
||||
(println (at ns 2))
|
||||
(println (at fs 0))
|
||||
(println (at fs 1))))
|
||||
@ -1,5 +0,0 @@
|
||||
(defn fst [a $t b $u] t a)
|
||||
(defn main [] ()
|
||||
(println (fst 1 2.5))
|
||||
(println (fst true (i64 9)))
|
||||
(println (fst 3 false)))
|
||||
@ -1,210 +0,0 @@
|
||||
# What the hand-written x86-64 backend costs
|
||||
|
||||
`survey.sh` has said for three handoffs that this backend agrees with LLVM on all 97 corpus programs it can build.
|
||||
Item 7 of `docs/handoffs/HANDOFF-x86-rt.md` is the other half of that sentence — nobody had a number for what the agreement
|
||||
costs — and it came with a list of suspects: a guard after every call, three frame temporaries per bounds check,
|
||||
every intermediate in memory, `rep movsb` block copies, and an extra load per call site in a dev build. This is
|
||||
the measurement. It does not change anything; two of the five suspects turn out not to matter, and the one that
|
||||
matters most is not on the list.
|
||||
|
||||
Produced by `spike/x86/cost.sh` (the corpus, for size) and `spike/x86/bench.sh` (four purpose-written programs,
|
||||
for speed). Both are documented in their own headers. Machine: 16-core x86-64, Fedora, clang as the assembler and
|
||||
linker on both sides, and other work running on it throughout — which is why every time below is the *minimum* of
|
||||
five or seven runs and why nothing here rests on a difference of a few percent.
|
||||
|
||||
The rows behind the tables are committed beside this file as `cost-corpus.tsv` and `cost-bench.tsv`, so a later
|
||||
lane can recompute a ratio rather than believe one.
|
||||
|
||||
## What is being compared, and against what
|
||||
|
||||
The interesting column is not the size of the executable. A Flan binary is mostly `flan_rt.o` and libc glue, the
|
||||
same object on both sides, and it drowns the signal: over the corpus the whole file is only **1.09×** bigger
|
||||
through this backend and `.text` only **1.18×**, which would be a reassuring number and a meaningless one.
|
||||
|
||||
So the measurement is the sum of the sizes of the defined symbols the compiler *named itself* — everything called
|
||||
`flan.<something>`. The runtime's C is `flan_<something>` with an underscore, so the two never collide, and a
|
||||
runtime symbol has the same size on both sides (`flan_map_clone`, `0x4b3` either way), which is the check that
|
||||
says the difference really is codegen and not a differently-linked runtime.
|
||||
|
||||
There is a third column, and it is what makes the second readable: **LLVM with `--debug`, which forces `-O0`**.
|
||||
This backend has no optimiser at all, so measuring it against LLVM at `-O2` charges it for the whole of mem2reg,
|
||||
inlining and constant folding. `-O0` is LLVM's instruction selection with none of that, which is the comparison
|
||||
that says something about *this* backend rather than about the absence of a middle end. (`--x86 --debug` is
|
||||
refused — the backend emits no DWARF — so the column exists on one side only. DWARF lands in `.debug_*` sections
|
||||
and not in `.text`, checked, so it does not contaminate the symbol sums.)
|
||||
|
||||
## The corpus: size
|
||||
|
||||
Ninety-seven programs — the same set `survey.sh` matches on, minus the two that run forever. Summed over all of
|
||||
them:
|
||||
|
||||
| | LLVM -O2 | LLVM -O0 | x86 |
|
||||
|---|---|---|---|
|
||||
| own code, all 97 programs | 221,608 | 444,504 | 851,638 |
|
||||
| against LLVM -O2 | 1.00× | 2.01× | **3.84×** |
|
||||
| against LLVM -O0 | | 1.00× | **1.92×** |
|
||||
|
||||
So the headline is two numbers, not one. **This backend emits 3.8× the code LLVM does at `-O2`, and half of that
|
||||
factor is the optimiser Flan ships with rather than anything about the backend; against LLVM with the optimiser
|
||||
off it is 1.9×.** Per program the second ratio is tight — median 2.21, quartiles 1.94 and 2.98, the whole range
|
||||
1.32 to 5.44 — which is itself a finding: the cost is not a few bad nodes, it is a constant tax on everything.
|
||||
|
||||
The ten largest programs, which are where the bytes actually are:
|
||||
|
||||
| program | LLVM -O2 | LLVM -O0 | x86 | x86 / -O2 | x86 / -O0 |
|
||||
|---|---|---|---|---|---|
|
||||
| `strings` | 13,395 | 22,681 | 33,265 | 2.48× | 1.47× |
|
||||
| `maps` | 12,833 | 19,850 | 40,445 | 3.15× | 2.04× |
|
||||
| `edn` | 12,057 | 34,556 | 48,532 | 4.03× | 1.40× |
|
||||
| `generics` | 10,137 | 20,427 | 39,241 | 3.87× | 1.92× |
|
||||
| `slurp` | 9,744 | 14,813 | 24,656 | 2.53× | 1.66× |
|
||||
| `into` | 8,277 | 12,345 | 21,320 | 2.58× | 1.73× |
|
||||
| `vec` | 8,039 | 12,089 | 22,770 | 2.83× | 1.88× |
|
||||
| `map-iter` | 7,211 | 10,374 | 21,715 | 3.01× | 2.09× |
|
||||
| `algorithms` | 6,776 | 16,616 | 30,875 | 4.56× | 1.86× |
|
||||
| `slices` | 2,393 | 9,057 | 17,753 | 7.42× | 1.96× |
|
||||
|
||||
And the two ends of the distribution, both of which are more interesting than the middle:
|
||||
|
||||
| program | LLVM -O2 | LLVM -O0 | x86 | x86 / -O2 | x86 / -O0 | why |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `bounds` | 363 | 1,951 | 4,039 | **11.13×** | 2.07× | LLVM at `-O2` proves the indices and deletes the checks |
|
||||
| `array-ctor` | 357 | 1,840 | 3,880 | 10.87× | 2.11× | the same, over a constructor's worth of stores |
|
||||
| `p2-loop-print` | 283 | 106 | 577 | 2.04× | **5.44×** | a `-O0` build *smaller* than `-O2`: LLVM unrolls the five-iteration loop and `-O0` does not |
|
||||
| `pkg-return` | 7,024 | 23,635 | 31,716 | 4.52× | 1.34× | mostly prelude, where `-O0` is already fat |
|
||||
|
||||
`bounds` is the clearest case in the table of why the `-O0` column had to exist. Eleven times is a shocking
|
||||
number and it is not about this backend at all: the program's whole point is indexing, LLVM at `-O2` can see the
|
||||
indices are in range and removes the check, and neither LLVM at `-O0` nor this backend can. Against the compiler
|
||||
that also keeps every check, `bounds` is 2.07× — a completely ordinary row.
|
||||
|
||||
## Where the size goes
|
||||
|
||||
Every one of these is from the disassembly of the benchmark programs, which are small enough to read whole.
|
||||
|
||||
**Every intermediate goes through the frame, and so does every constant.** This is the big one and it is not one
|
||||
feature, it is the shape of the whole backend. `(step acc 1)` in `b1-calls` compiles to:
|
||||
|
||||
movabs $0x1,%rax ; a 10-byte immediate ...
|
||||
mov %rax,-0x30(%rbp) ; ... stored to a frame slot ...
|
||||
mov -0x30(%rbp),%rsi ; ... and loaded back into the argument register
|
||||
|
||||
Three instructions and 24 bytes where LLVM writes `mov $1,%esi`, five. The loop bound gets the same treatment
|
||||
*every iteration* — `movabs $0x1312d00` into a slot, sign-extended out of it, compared — because nothing is
|
||||
hoisted. So does the loop condition: `cmp`/`setl`/`movzbq`/store a byte to the frame/reload it/`test`/`jne`,
|
||||
seven instructions for what is `cmp`/`jge` anywhere else. This is most of the 2× against `-O0` and essentially
|
||||
all of the difference on the programs at the bottom of the table, which have no calls, no bounds checks and no
|
||||
aggregates in them at all.
|
||||
|
||||
**The guard after every call is four instructions and one dependent load.**
|
||||
|
||||
call 4009f8 <flan.step>
|
||||
mov -0x18(%rbp),%r11 ; the condition frame, from its own slot
|
||||
mov 0x0(%r11),%r11 ; ... dereferenced
|
||||
test %r11,%r11
|
||||
jne <unwind>
|
||||
|
||||
Roughly 25 bytes per call site. Real, cheap, and third in size behind the two above it — on a program that is
|
||||
nothing *but* calls (`b1`) the whole backend is 2.8× LLVM `-O0`, and the guard is a minority of that.
|
||||
|
||||
**The bounds check is three frame temporaries, as suspected, and it costs code and not time.** The index is
|
||||
widened, stored, reloaded, stored again, the limit goes to a third slot, and then `cmp`/`jb` — with the failure
|
||||
path, its `.rodata` location string and its length, inline at the branch target. On `b2-bounds`, `flan.main` is
|
||||
`0x4b1` with checks and `0x3c0` without: **241 bytes, a quarter of the function.** In time it is 112.6ms against
|
||||
105.0ms over 20.5 million checked loads — **about 0.4ns, a cycle or two a check** — because the branch predicts
|
||||
perfectly and the loads were going to memory anyway. That contradicts the way the handoff's list reads. The three
|
||||
temporaries are a code-size item. They are not a speed item.
|
||||
|
||||
**`rep movsb` is real and it is the most expensive single instruction here.** A 64-byte struct copy lowers to
|
||||
`lea`/`lea`/`movabs $0x40,%rcx`/`rep movsb`, and `b4-copy` runs 2 million of them in 20.8ms against LLVM `-O0`'s
|
||||
7.2ms: **about 6.8ns of the difference per copy, some twenty cycles**, which is `rep movsb`'s startup cost and
|
||||
almost none of it the 64 bytes. It is also the one place where the backend loses to `-O0` by a factor (2.9×) it
|
||||
does not lose by on straight-line code, and the one item on the suspect list where a targeted fix — inline
|
||||
16-byte moves under some size threshold — would pay for itself.
|
||||
|
||||
## The corpus: speed, and why there is barely any
|
||||
|
||||
Almost nothing. **Every program in `test/programs` runs in about 2.5 milliseconds, nearly all of it `execve` and
|
||||
the dynamic loader**, and both backends produce the same 2.5 milliseconds. Best-of-five does not rescue a signal
|
||||
that is not there. There is exactly one corpus program whose own code is a measurable part of its runtime, and it
|
||||
is the right one:
|
||||
|
||||
| program | LLVM -O2 | LLVM -O0 | x86 | what it is |
|
||||
|---|---|---|---|---|
|
||||
| `recur` | ~1ms | 10ms | 60ms | a ten-million-iteration counting loop, written to prove `recur` is a jump |
|
||||
|
||||
Read that carefully, because the 25× against `-O2` is not a fact about this backend: LLVM folds the loop to its
|
||||
answer and runs nothing. Against `-O0`, which also runs ten million iterations, it is **6×** — about 6ns an
|
||||
iteration against 1ns, or roughly eighteen cycles for `i+1` and a compare. That is the frame-slot round trip
|
||||
above, four or five times over, and it is the honest number.
|
||||
|
||||
## The benchmarks
|
||||
|
||||
Four programs in `spike/x86/bench/`, each written so that one suspected cost is most of what the program does.
|
||||
They are in a subdirectory on purpose: `survey.sh` globs `spike/x86/*.flan` and a benchmark is not a case.
|
||||
Times are best-of-seven, in milliseconds.
|
||||
|
||||
| bench | what it is | LLVM -O2 | LLVM -O0 | x86 | x86 / -O0 | own code, -O0 → x86 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `b1-calls` | 20M calls of a one-instruction function | 2.1 | 36.8 | 104.3 | 2.8× | 200 → 807 |
|
||||
| `b2-bounds` | 20.5M bounds-checked array loads | 4.7 | 27.0 | 112.6 | 4.2× | 407 → 1390 |
|
||||
| `b3-spill` | 5M iterations of a six-deep arithmetic tree | 22.2 | 29.0 | 137.3 | 4.7× | 248 → 1117 |
|
||||
| `b4-copy` | 2M copies of a 64-byte struct | 2.0 | 7.2 | 20.8 | 2.9× | 348 → 877 |
|
||||
|
||||
`b2` with `--no-bounds-checks` on both sides: LLVM 4.5ms, x86 105.0ms — the 7.6ms the check costs over 20.5
|
||||
million of them, and the 241 bytes it costs in `flan.main`, are the whole of it.
|
||||
|
||||
`b3`'s ratio is the one to distrust slightly: its expression ends in a `%`, which is an `idiv`, and an `idiv` is
|
||||
twenty-odd cycles on every side. That is most of LLVM's own 22.2ms and a good part of its 29.0ms, so the
|
||||
denominator is largely a hardware latency this backend cannot do anything about. The absolute gap — 108ms over
|
||||
5 million iterations, about 21ns of extra work each — is the honest reading of that row.
|
||||
|
||||
|
||||
|
||||
`b1-calls` and `b3-spill` are the pair to read together, with the `idiv` caveat above in mind. `b1` is 20 million
|
||||
calls of a one-instruction function and lands at 2.8× `-O0`; `b3` has no calls at all and pays 21ns an iteration
|
||||
for six dependent arithmetic temporaries. **The backend is worse at arithmetic than it is at calling**, which is
|
||||
the opposite of what the suspect list implies, and it is because a call already costs enough that four extra
|
||||
instructions beside it disappear, while an add that should be one instruction costs five. `recur`, which is a
|
||||
counting loop and nothing else, says the same thing on a corpus program: 6× LLVM `-O0`.
|
||||
|
||||
The `-O2` column in `b1` and `b4` is 2ms — the loop is gone. That is a true fact about the toolchain Flan ships
|
||||
and a useless one about code generation, which is the whole reason the `-O0` column exists.
|
||||
|
||||
## `--dev`, which is the one axis both backends pay
|
||||
|
||||
`SURVEY_FLAGS=--dev` reported 97 MATCH for the lane before this one, so the comparison is available. The suspected cost was the extra
|
||||
load per call site — every cross-function call going through its indirection cell. **It is not measurable.** On
|
||||
`b1-calls`, 20 million calls, x86 release is 104.3ms and x86 `--dev` is 100.9ms: the same number, and the dev
|
||||
build is nominally the *faster* of the two, which is what a difference below the noise floor looks like. The load
|
||||
is from a `.data` cell that is in L1 after the first call and the machine was already waiting on the frame.
|
||||
|
||||
What a dev build actually costs is something else entirely, and both backends pay it. `b1-calls` is a program
|
||||
with two functions in it:
|
||||
|
||||
| build | own code |
|
||||
|---|---|
|
||||
| LLVM, release | 82 bytes |
|
||||
| x86, release | 807 bytes |
|
||||
| LLVM, `--dev` | 32,714 bytes |
|
||||
| x86, `--dev` | 83,018 bytes |
|
||||
|
||||
**A dev build emits the entire prelude**, because anything might be redefined and so nothing may be dropped. That
|
||||
is four hundred times the code for this program, and it dwarfs every item on the suspect list put together. It is
|
||||
also not a backend cost — LLVM pays a 400× of its own — so it is `Reach`'s business and not `x86.ml`'s. The
|
||||
backend's share of it is the same ~2.5× it charges everywhere else.
|
||||
|
||||
## What this says to the lane rewriting `lib/x86.ml`
|
||||
|
||||
Ranked by what the numbers actually support, and not by the order of the list in the handoff:
|
||||
|
||||
1. **Keep values in registers across a single expression.** Not a register allocator — just not routing every
|
||||
constant and every subexpression through a frame slot, and not re-materialising a loop bound every iteration.
|
||||
This is most of the 2× against `-O0` and most of `recur`'s 6×, and it is what `b3`'s 21ns an iteration buys.
|
||||
2. **Inline small aggregate copies** instead of `rep movsb`. One instruction, twenty cycles, on a copy that is
|
||||
four `movdqu` pairs.
|
||||
3. **`flan_dev_reg_note` in a release build** (item 2 of the old handoff's list) is worth doing and is small.
|
||||
4. **The call guard is fine.** Four instructions and 25 bytes, invisible in time. Leave it.
|
||||
5. **The bounds check is fine on time and fat on code.** If it is ever worth touching, it is worth touching for
|
||||
the 241 bytes — hoisting the failure path out of line would get most of that back without changing a cycle.
|
||||
6. **The dev call cell is free.** Whatever the redefinition emitter costs, it does not cost this.
|
||||
@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Does annotation change a single byte of what the backend emits?
|
||||
#
|
||||
# `flan emit --x86` annotates: a comment per Flan form, a frame map per
|
||||
# function, and a name for each piece of bookkeeping the compiler adds. All of
|
||||
# that is comments, plus the splitting of one long `.byte` directive into
|
||||
# several. Both are supposed to be invisible to the assembler -- and "supposed
|
||||
# to be" is exactly the kind of claim this repo measures rather than asserts,
|
||||
# because the whole licence for annotating at all is that the bytes are the
|
||||
# bytes.
|
||||
#
|
||||
# So: emit every program in the corpus both ways, assemble both, and compare
|
||||
# each section of the two objects byte for byte. No linking and no running --
|
||||
# survey.sh is what says the programs still behave, and this says nothing they
|
||||
# are built from moved.
|
||||
#
|
||||
# Three settings, because they are three different emitters. The default; --dev,
|
||||
# which adds the indirection cells and the ABI marker; and --debug, which adds
|
||||
# the line table, the labels its rows hang off, and the CFI directives. The
|
||||
# debug case is the sharp one: a `.debug_line` row is an address expressed as a
|
||||
# label, and annotation emits no labels precisely so that those cannot move.
|
||||
#
|
||||
# Usage: spike/x86/annot.sh [name-substring ...]
|
||||
set -u
|
||||
orig=$(pwd)
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
cd "$root" || exit 1
|
||||
|
||||
if [ -n "${FLAN:-}" ]; then
|
||||
case $FLAN in /*) flan=$FLAN;; *) flan=$orig/$FLAN;; esac
|
||||
else
|
||||
dune build --root . bin/main.exe 2>&1 | head -30
|
||||
flan=$root/_build/default/bin/main.exe
|
||||
fi
|
||||
test -x "$flan" || { echo "build failed"; exit 1; }
|
||||
|
||||
corpus=${SURVEY_CORPUS:-$root}
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/flan-annot.XXXXXX")
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
same=0; differ=0; skip=0
|
||||
|
||||
# Every section either object has, not a fixed list: a section that exists on
|
||||
# one side and not the other is itself a difference, and comparing a named list
|
||||
# would miss one that annotation invented.
|
||||
sections () {
|
||||
objdump -h "$1" | awk '$1 ~ /^[0-9]+$/ { print $2 }'
|
||||
}
|
||||
|
||||
check () {
|
||||
src=$1; shift
|
||||
name=$(basename "$src" .flan)
|
||||
tag="$name${*:+ $*}"
|
||||
a=$tmp/a.s; b=$tmp/b.s
|
||||
|
||||
if ! "$flan" emit --x86 "$@" "$src" > "$a" 2>"$tmp/err"; then
|
||||
skip=$((skip + 1)); echo "SKIP $tag"; return
|
||||
fi
|
||||
if ! "$flan" emit --x86 --no-annotate "$@" "$src" > "$b" 2>/dev/null; then
|
||||
skip=$((skip + 1)); echo "SKIP $tag"; return
|
||||
fi
|
||||
if ! as --64 -o "$tmp/a.o" "$a" 2>"$tmp/err"; then
|
||||
differ=$((differ + 1))
|
||||
echo "BADASM $tag -- the annotated listing does not assemble"
|
||||
head -3 "$tmp/err"; return
|
||||
fi
|
||||
if ! as --64 -o "$tmp/b.o" "$b" 2>/dev/null; then
|
||||
skip=$((skip + 1)); echo "SKIP $tag"; return
|
||||
fi
|
||||
|
||||
bad=
|
||||
for sec in $(sections "$tmp/a.o"; sections "$tmp/b.o"); do
|
||||
case " $bad " in *" $sec "*) continue;; esac
|
||||
objcopy -O binary --only-section="$sec" "$tmp/a.o" "$tmp/a.bin" 2>/dev/null
|
||||
objcopy -O binary --only-section="$sec" "$tmp/b.o" "$tmp/b.bin" 2>/dev/null
|
||||
cmp -s "$tmp/a.bin" "$tmp/b.bin" || bad="$bad $sec"
|
||||
done
|
||||
|
||||
if [ -z "$bad" ]; then
|
||||
same=$((same + 1)); echo "SAME $tag"
|
||||
else
|
||||
differ=$((differ + 1)); echo "DIFFER $tag --$bad"
|
||||
fi
|
||||
}
|
||||
|
||||
for src in "$corpus"/test/programs/*.flan "$corpus"/spike/x86/*.flan; do
|
||||
[ -f "$src" ] || continue
|
||||
if [ $# -gt 0 ]; then
|
||||
hit=
|
||||
for pat in "$@"; do case $src in *"$pat"*) hit=1;; esac; done
|
||||
[ -n "$hit" ] || continue
|
||||
fi
|
||||
check "$src"
|
||||
check "$src" --dev
|
||||
check "$src" --debug
|
||||
done
|
||||
|
||||
echo
|
||||
echo "$same SAME / $differ DIFFER / $skip SKIP"
|
||||
[ "$differ" -eq 0 ]
|
||||
@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# The speed half of cost.sh, on programs that are long enough to time.
|
||||
#
|
||||
# Why this exists beside cost.sh rather than inside it: every program in
|
||||
# test/programs runs in about two and a half milliseconds, of which nearly
|
||||
# all is fork, exec and the dynamic loader. Best-of-five does not rescue a
|
||||
# signal that is not there, and a table of 97 rows that all say "2.5ms vs
|
||||
# 2.6ms" would be a measurement of execve. So the corpus answers the size
|
||||
# question and these four answer the speed one, each written so that one
|
||||
# suspected cost is most of what the program does.
|
||||
#
|
||||
# Four builds of each, and the third column is the one to read:
|
||||
#
|
||||
# llvm as shipped, -O2. Frequently the loop is simply gone; that is a
|
||||
# true number about the toolchain and a useless one about codegen.
|
||||
# llvm -O0 via --debug, which forces it. LLVM's instruction selection with
|
||||
# its optimiser off -- the fair comparison for a backend that has
|
||||
# no optimiser.
|
||||
# x86 this backend.
|
||||
# x86 nbc --no-bounds-checks, for b2, where the difference is the check.
|
||||
#
|
||||
# And --dev on both sides, which is the one suspected cost the two backends
|
||||
# share: every cross-function call goes through an indirection cell, so it is
|
||||
# a load and an indirect call where a release build has a direct one. b1 is
|
||||
# where that has to show.
|
||||
#
|
||||
# Usage: spike/x86/bench.sh [name-substring ...]
|
||||
set -u
|
||||
orig=$(pwd)
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
cd "$root" || exit 1
|
||||
|
||||
if [ -n "${FLAN:-}" ]; then
|
||||
case $FLAN in /*) flan=$FLAN;; *) flan=$orig/$FLAN;; esac
|
||||
else
|
||||
dune build --root . bin/main.exe 2>&1 | head -30
|
||||
flan=$root/_build/default/bin/main.exe
|
||||
fi
|
||||
test -x "$flan" || { echo "build failed" >&2; exit 1; }
|
||||
|
||||
out=${COST_OUT:-${TMPDIR:-/tmp}/flan-bench.$$}
|
||||
mkdir -p "$out" || exit 1
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
|
||||
REPS=${COST_REPS:-5}
|
||||
|
||||
best () {
|
||||
min=
|
||||
for i in $(seq "$REPS"); do
|
||||
t0=$(date +%s%N)
|
||||
timeout 120 "$1" >/dev/null 2>&1 </dev/null
|
||||
t1=$(date +%s%N)
|
||||
d=$(( (t1 - t0) / 1000 ))
|
||||
if [ -z "$min" ] || [ "$d" -lt "$min" ]; then min=$d; fi
|
||||
done
|
||||
echo "$min"
|
||||
}
|
||||
|
||||
own () {
|
||||
nm --defined-only -S "$1" 2>/dev/null \
|
||||
| awk 'NF==4 && ($3=="T"||$3=="t") && $4 ~ /^flan\./ {n+=strtonum("0x"$2)} END{print n+0}'
|
||||
}
|
||||
|
||||
printf 'name\tllvm_us\to0_us\tx86_us\tllvm_nbc_us\tx86_nbc_us\tllvm_dev_us\tx86_dev_us\tllvm_own\to0_own\tx86_own\tx86_dev_own\n'
|
||||
|
||||
for src in "$root"/spike/x86/bench/*.flan; do
|
||||
name=$(basename "$src" .flan)
|
||||
if [ $# -gt 0 ]; then
|
||||
want=0
|
||||
for pat in "$@"; do case "$name" in *"$pat"*) want=1;; esac; done
|
||||
[ $want = 1 ] || continue
|
||||
fi
|
||||
"$flan" build "$src" -o "$out/l" >/dev/null 2>&1 || { echo "$name: llvm build failed" >&2; continue; }
|
||||
"$flan" build "$src" --debug -o "$out/d" >/dev/null 2>&1 || { echo "$name: -O0 build failed" >&2; continue; }
|
||||
"$flan" build "$src" --x86 -o "$out/x" >/dev/null 2>&1 || { echo "$name: x86 build failed" >&2; continue; }
|
||||
"$flan" build "$src" --no-bounds-checks -o "$out/ln" >/dev/null 2>&1
|
||||
"$flan" build "$src" --x86 --no-bounds-checks -o "$out/xn" >/dev/null 2>&1
|
||||
"$flan" build "$src" --dev -o "$out/lv" >/dev/null 2>&1
|
||||
"$flan" build "$src" --x86 --dev -o "$out/xv" >/dev/null 2>&1
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$name" \
|
||||
"$(best "$out/l")" "$(best "$out/d")" "$(best "$out/x")" \
|
||||
"$(best "$out/ln")" "$(best "$out/xn")" \
|
||||
"$(best "$out/lv")" "$(best "$out/xv")" \
|
||||
"$(own "$out/l")" "$(own "$out/d")" "$(own "$out/x")" "$(own "$out/xv")"
|
||||
done
|
||||
@ -1,20 +0,0 @@
|
||||
;;;; A hot loop that does nothing but call.
|
||||
;;;;
|
||||
;;;; The suspected cost is the guard this backend emits after every call --
|
||||
;;;; and, in a dev build, the load of the indirection cell before it. Neither
|
||||
;;;; is visible in the corpus, where a program's whole run is process startup.
|
||||
;;;; Here the call is the program: the body is one add, so whatever separates
|
||||
;;;; this from LLVM at -O0 is the call sequence and not the arithmetic.
|
||||
;;;;
|
||||
;;;; Not in spike/x86 proper, where survey.sh would pick it up: the survey's
|
||||
;;;; counts are quoted in three handoffs and a benchmark is not a case.
|
||||
|
||||
(defn step [a i64 b i64] i64
|
||||
(+ a b))
|
||||
|
||||
(defn main [] i32
|
||||
(let [acc (i64 0)]
|
||||
(dotimes [i 20000000]
|
||||
(set acc (step acc 1)))
|
||||
(print acc) (println ""))
|
||||
0)
|
||||
@ -1,20 +0,0 @@
|
||||
;;;; A hot loop that does nothing but index a bounds-checked array.
|
||||
;;;;
|
||||
;;;; The suspected cost is three frame temporaries per check. This one has an
|
||||
;;;; A/B that the others do not: --no-bounds-checks builds the same program
|
||||
;;;; with the check gone, on both sides, so the difference between the two
|
||||
;;;; x86 numbers is the check and nothing else, and the same difference on
|
||||
;;;; the LLVM side says what the check costs when a compiler is allowed to
|
||||
;;;; hoist it out of the loop.
|
||||
|
||||
(defonce xs [1024 i32])
|
||||
|
||||
(defn main [] i32
|
||||
(dotimes [i 1024]
|
||||
(set (at xs i) i))
|
||||
(let [acc (i64 0)]
|
||||
(dotimes [r 20000]
|
||||
(dotimes [i 1024]
|
||||
(set acc (+ acc (i64 (at xs i))))))
|
||||
(print acc) (println ""))
|
||||
0)
|
||||
@ -1,21 +0,0 @@
|
||||
;;;; A hot loop of arithmetic and nothing else: no calls, no arrays, no
|
||||
;;;; copies.
|
||||
;;;;
|
||||
;;;; The suspected cost is that every intermediate lives in a frame slot --
|
||||
;;;; this backend allocates no registers, so an expression tree becomes a
|
||||
;;;; chain of stores and reloads. The tree here is deliberately deep and
|
||||
;;;; entirely dependent, so a register allocator would keep all of it in
|
||||
;;;; registers and this backend cannot keep any of it.
|
||||
|
||||
(defn main [] i32
|
||||
(let [acc (i64 1)]
|
||||
(dotimes [i 5000000]
|
||||
(let [a (+ acc 3)
|
||||
b (* a 2)
|
||||
c (- b 1)
|
||||
d (bit-xor c 7)
|
||||
e (+ d (* a 5))
|
||||
f (- e (bit-and d 15))]
|
||||
(set acc (+ (% f 1000003) 1))))
|
||||
(print acc) (println ""))
|
||||
0)
|
||||
@ -1,20 +0,0 @@
|
||||
;;;; A hot loop of struct copies.
|
||||
;;;;
|
||||
;;;; The suspected cost is `rep movsb`: this backend copies an aggregate by
|
||||
;;;; block-moving bytes, where LLVM either keeps the thing in registers or
|
||||
;;;; emits a handful of wide moves. Eight i64 fields is 64 bytes -- big
|
||||
;;;; enough that a copy is a real copy, small enough that `rep movsb` is
|
||||
;;;; paying its setup cost on every one of them, which is the shape the
|
||||
;;;; instruction is worst at.
|
||||
|
||||
(defstruct Big [a i64 b i64 c i64 d i64 e i64 f i64 g i64 h i64])
|
||||
|
||||
(defn main [] i32
|
||||
(let [acc (i64 0)
|
||||
v (Big {.a 1 .b 2 .c 3 .d 4 .e 5 .f 6 .g 7 .h 8})]
|
||||
(dotimes [i 2000000]
|
||||
(let [w v]
|
||||
(set (.a v) (+ (.h w) 1))
|
||||
(set acc (+ acc (.a w)))))
|
||||
(print acc) (println ""))
|
||||
0)
|
||||
@ -1,5 +0,0 @@
|
||||
name llvm_us o0_us x86_us llvm_nbc_us x86_nbc_us llvm_dev_us x86_dev_us llvm_own o0_own x86_own x86_dev_own
|
||||
b1-calls 2108 36764 104266 1959 103336 26827 100874 82 200 807 83018
|
||||
b2-bounds 4711 27047 112567 4548 104951 4663 111295 355 407 1390 83596
|
||||
b3-spill 22184 28982 137347 22772 132053 22659 132913 168 248 1117 83323
|
||||
b4-copy 2043 7170 20792 1980 19559 2058 18497 140 348 877 83083
|
||||
|
@ -1,98 +0,0 @@
|
||||
name llvm_file llvm_text llvm_own o0_own x86_file x86_text x86_own llvm_us x86_us
|
||||
agent-longname 82224 42002 79 134 82264 42450 519 -1 -1
|
||||
agent-queue 82352 42306 340 567 86488 43698 1747 2491 2546
|
||||
agent 82336 42434 454 798 86472 44162 2218 2609 2496
|
||||
algorithms 76680 39282 6776 16616 101296 63234 30875 2499 2605
|
||||
allocators 67760 33650 1297 1719 71896 37490 5130 3170 3034
|
||||
array-ctor 67784 32722 357 1840 71920 36242 3880 2442 2664
|
||||
bounds-condition 76416 37074 4641 6963 84648 45858 13493 2673 2469
|
||||
bounds 67720 32738 363 1951 71856 36418 4039 3194 2654
|
||||
break 82296 42754 819 1182 86432 44482 2549 -1 -1
|
||||
bytes2 72120 37010 4576 10893 88544 52018 19654 2573 2510
|
||||
cleanup 68176 33986 1583 2077 72312 36946 4583 2351 2402
|
||||
conditions 68064 33586 1212 1209 68104 35346 2984 2436 2513
|
||||
debug-permuted 67720 32610 261 410 67760 33794 1433 2479 2442
|
||||
debug 67720 32610 261 429 67760 33794 1433 2494 2494
|
||||
defer-let 67928 34018 1625 1784 72064 36754 4396 2548 2543
|
||||
destructure 67792 33650 1290 4577 80120 40946 8583 2534 2523
|
||||
dev-break-bounds 82440 42546 588 648 86576 43762 1830 -1 -1
|
||||
dev-break 82368 42418 459 715 86504 43682 1755 -1 -1
|
||||
dev-globals 82456 42162 206 534 86584 43538 1614 -1 -1
|
||||
dev-inspect 82408 42610 652 704 86544 43698 1765 -1 -1
|
||||
dev-locals 82336 42418 465 623 86472 43650 1721 -1 -1
|
||||
dev-noagent 67720 32434 83 109 67760 32866 507 3116 2939
|
||||
dev-pause 82336 42066 112 244 82376 42802 877 -1 -1
|
||||
dev-ptr 86632 43954 1970 2859 90768 47490 5543 -1 -1
|
||||
dev-repl 82336 42066 112 244 82376 42802 877 -1 -1
|
||||
dev-robust 82336 42066 112 244 82376 42802 877 -1 -1
|
||||
edn 91208 44706 12057 34556 124016 80898 48532 3218 3165
|
||||
embed 67800 33330 977 2461 71936 37906 5550 2440 2650
|
||||
enum-compare 67752 32562 201 581 67792 33970 1611 2607 2805
|
||||
enum-convert 67752 33202 830 1522 71888 37042 4675 2737 2512
|
||||
error 67744 32498 131 163 67784 32946 591 2614 2587
|
||||
exhausted-unhandled 67688 33090 749 1156 67728 34850 2491 2442 2591
|
||||
exhausted 76192 35778 3415 5139 80336 42674 10321 2409 2469
|
||||
fn-values 68296 34226 1745 5124 80632 43026 10672 2405 2406
|
||||
format 76032 36802 4413 7076 84264 45394 13038 2449 2388
|
||||
frame-rollback 68440 34466 2020 3424 80768 39762 7402 2383 2394
|
||||
free-all-refused 67688 32370 28 28 67728 32658 299 2418 2497
|
||||
generics 81368 42786 10137 20427 114184 71602 39241 2474 2516
|
||||
handles 75880 35826 3482 6289 84112 46338 13981 2520 2470
|
||||
higher-order 76648 37154 4652 10329 88976 51314 18948 2413 2439
|
||||
into 80256 40658 8277 12345 92584 53682 21320 2428 2412
|
||||
loops 67736 33378 1016 1720 71872 37634 5269 2370 2315
|
||||
machine 67960 33186 821 1925 72096 37394 5042 2515 2456
|
||||
macro-unless 67720 32642 288 615 67760 34610 2243 2466 2457
|
||||
macros 67688 32802 461 793 67728 35250 2896 2532 2409
|
||||
map-exhausted 76152 36162 3805 5994 84392 44530 12171 2393 2506
|
||||
map-iter 76096 39586 7211 10374 92520 54082 21715 2541 2531
|
||||
map-stale-region 67688 33650 1307 2106 71824 36898 4537 2354 2407
|
||||
maps 88504 45266 12833 19850 117216 72802 40445 3162 3082
|
||||
math 67832 33986 1611 3288 72024 39362 7000 2384 2405
|
||||
math2 67784 33506 1148 2862 72024 39010 6644 2300 2482
|
||||
pkg-diamond 67880 32626 252 616 67928 34162 1801 2353 2227
|
||||
pkg-macro-idle 67688 32338 3 3 67728 32562 210 2440 2410
|
||||
pkg-macro 67800 32722 354 450 67840 33986 1626 2455 2429
|
||||
pkg-return 78416 39570 7024 23635 103032 64082 31716 2432 2375
|
||||
pkg-shadow 67848 32642 277 371 67888 33618 1258 2408 2236
|
||||
pkg-shared 74560 34066 1676 5593 86888 45650 13283 2422 2648
|
||||
pkg-unused 69704 32386 35 35 69744 34658 2298 2351 2271
|
||||
pool-stale-region 67688 33282 932 1675 71824 35746 3379 2389 2466
|
||||
printers 82464 42098 168 452 82504 43282 1348 -1 -1
|
||||
println 76256 36098 3737 7587 88584 50114 17753 2312 2460
|
||||
raylib-audio 79536 35522 2478 5610 87768 44178 11225 2574 2769
|
||||
raylib-ffi 85848 39538 6245 11506 102272 55650 22537 2607 2852
|
||||
raylib-font 79088 35538 2622 6260 83232 43186 10350 2661 2658
|
||||
raylib-image 80152 37698 4504 8285 88384 47058 13966 2684 2946
|
||||
raylib-imported 70344 33202 612 1003 74480 36658 4066 2688 2602
|
||||
reach-walk 67992 33170 774 1082 68032 34802 2443 2552 2308
|
||||
recur 67792 34594 2233 4070 76024 40674 8314 2468 62674
|
||||
registry 72008 35330 2940 4604 80240 41010 8608 2515 2329
|
||||
reload-generic 67944 32930 520 1390 67992 35442 3076 2341 2241
|
||||
restarts 77200 39010 6430 9268 89528 49442 17065 2356 2304
|
||||
rl-with 69704 32386 35 35 69744 34658 2298 2550 2461
|
||||
sand-headless 74728 34546 2139 6326 87056 47634 15269 2783 9172
|
||||
signedness 67688 32594 246 386 67728 33938 1576 2418 2411
|
||||
slice-from-ptr 67752 33106 755 2549 75984 37458 5101 2421 2367
|
||||
slices 68128 34802 2393 9057 88648 50114 17753 2504 2271
|
||||
slurp-unhandled 67688 33522 1173 1601 67728 35298 2945 2340 2354
|
||||
slurp 80288 42114 9744 14813 100808 57010 24656 2358 2408
|
||||
stale-region 67688 33490 1148 1681 71824 35842 3483 2332 2286
|
||||
string-of-bytes 67856 33490 981 1925 71992 36338 3817 2319 2353
|
||||
strings 88912 45890 13395 22681 109432 65618 33265 2455 2391
|
||||
text 72296 37122 4666 8007 84624 49378 17018 2310 2397
|
||||
unions 76016 36498 4131 17714 92440 55810 23456 2323 2413
|
||||
unit-main 67688 32386 33 33 67728 32674 307 2459 2377
|
||||
utf8 81216 39714 7170 21515 114024 73106 40745 2563 2380
|
||||
values 67720 32530 180 667 67760 34082 1719 2531 2378
|
||||
vec 80080 40402 8039 12089 92408 55122 22770 2370 2389
|
||||
virtual-controls-headless 70896 33666 1271 1295 75032 38962 6601 2319 3201
|
||||
web-files 67736 33058 706 1000 67776 34610 2246 2321 2382
|
||||
p1-exit 67688 32338 3 3 67728 32562 210 2436 2377
|
||||
p2-loop-print 67688 32626 283 106 67728 32930 577 2269 2364
|
||||
p3-fizz 67720 32530 169 225 67760 33282 928 2512 2359
|
||||
p4-convention 67824 32786 405 1005 67864 35138 2778 2436 2332
|
||||
p5-core 67792 33346 993 1507 71928 37218 4865 2346 2444
|
||||
p6-transfer 68096 34354 1944 2801 72232 37618 5252 2386 2310
|
||||
p7-slice-from-ptr 67840 33698 1336 1498 71984 35682 3318 2493 2392
|
||||
p8-cell 67752 32514 146 270 67800 33202 847 2402 2381
|
||||
|
@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# What does the hand-written backend cost, against LLVM, on the same programs?
|
||||
#
|
||||
# survey.sh answers "does it agree". This answers "what does agreeing cost",
|
||||
# which is item 7 of docs/handoffs/HANDOFF-x86-rt.md and the one thing about this backend
|
||||
# nobody had a number for. It builds each program the same two ways the
|
||||
# survey does, and for each records three sizes and a time:
|
||||
#
|
||||
# file the whole executable on disk. Mostly runtime and libc glue, and
|
||||
# the least interesting of the three -- it is here because it is
|
||||
# the number anybody looks at first, and it should be visible how
|
||||
# much of it is noise.
|
||||
# text the .text section, from `size -A`. Still contains flan_rt.o,
|
||||
# which is the same object on both sides.
|
||||
# own the sum of the sizes of the defined symbols named `flan.<name>`
|
||||
# -- the program's *own* code and nothing else. The runtime's C is
|
||||
# `flan_<name>` with an underscore, so the two do not collide, and
|
||||
# spot-checking a runtime symbol on both sides (flan_map_clone,
|
||||
# 0x4b3 either way) says the runtime really is byte-identical and
|
||||
# the difference in `own` is all codegen.
|
||||
#
|
||||
# The `own` column is the measurement; the other two are context.
|
||||
#
|
||||
# A fourth build, LLVM with --debug, is the reference that makes the number
|
||||
# readable. --debug forces -O0, so it is LLVM's codegen with its optimiser
|
||||
# switched off -- the closest thing available to what this backend is doing,
|
||||
# which has no optimiser at all. Without it every ratio silently blames the
|
||||
# backend for the whole of mem2reg and inlining. (--x86 --debug is refused,
|
||||
# so the column exists on one side only, and that is the point of it.)
|
||||
#
|
||||
# Time is best-of-N, not a mean: a mean measures the other tenants of the
|
||||
# machine. Even so, a corpus program is mostly process startup -- these are
|
||||
# milliseconds -- so read the time column only where it is tens of
|
||||
# milliseconds or more, and read the rest as size.
|
||||
#
|
||||
# Usage: spike/x86/cost.sh [name-substring ...] -> a TSV on stdout
|
||||
# COST_FLAGS=--dev extra flags, given to both sides, as SURVEY_FLAGS is
|
||||
# COST_REPS=5 timing repetitions
|
||||
# COST_O0=0 skip the LLVM -O0 reference column
|
||||
set -u
|
||||
orig=$(pwd)
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
cd "$root" || exit 1
|
||||
|
||||
if [ -n "${FLAN:-}" ]; then
|
||||
case $FLAN in /*) flan=$FLAN;; *) flan=$orig/$FLAN;; esac
|
||||
else
|
||||
dune build --root . bin/main.exe 2>&1 | head -30
|
||||
flan=$root/_build/default/bin/main.exe
|
||||
fi
|
||||
test -x "$flan" || { echo "build failed" >&2; exit 1; }
|
||||
|
||||
# Not mktemp under /tmp by default: this writes a few hundred executables of
|
||||
# a megabyte or two, and a full /tmp on this machine has already frozen one
|
||||
# session. The guard is cheap and a wedged run is not.
|
||||
# A directory of this run's own, made with a plain mkdir so that a second
|
||||
# copy of this script cannot land in the first one's: two runs sharing a
|
||||
# scratch directory overwrite each other's `l` and `x` between the build and
|
||||
# the timing, and the result is a row of numbers that belong to two different
|
||||
# programs. That happened once here and the numbers looked entirely ordinary.
|
||||
work=${COST_OUT:-${TMPDIR:-/tmp}/flan-cost}
|
||||
mkdir -p "$work" || exit 1
|
||||
out=$work/run.$$
|
||||
mkdir "$out" || exit 1
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
free=$(df -Pk "$out" | awk 'NR==2 {print $4}')
|
||||
[ "$free" -gt 2000000 ] || { echo "less than 2GB free at $out" >&2; exit 1; }
|
||||
|
||||
forever="dev-loop dev-watch"
|
||||
REPS=${COST_REPS:-5}
|
||||
read -r -a extra <<<"${COST_FLAGS:-}"
|
||||
o0=${COST_O0:-1}
|
||||
[ -z "${COST_FLAGS:-}" ] || o0=0
|
||||
|
||||
# The sum of the defined text symbols the compiler itself named. `nm -S`
|
||||
# prints value, size, type, name; a symbol with no size is not printed with
|
||||
# four fields at all, which is why the guard is on NF.
|
||||
own () {
|
||||
nm --defined-only -S "$1" 2>/dev/null \
|
||||
| awk 'NF==4 && ($3=="T"||$3=="t") && $4 ~ /^flan\./ {n+=strtonum("0x"$2)} END{print n+0}'
|
||||
}
|
||||
text () { size -A "$1" 2>/dev/null | awk '$1==".text" {print $2}'; }
|
||||
|
||||
# Best of REPS, in whole microseconds. A program that fails on one run and
|
||||
# not another would make this meaningless, so the exit status of the first
|
||||
# run is remembered and a run that disagrees with it poisons the row as -1.
|
||||
# A program that hits the timeout is not timed at all: several of the corpus
|
||||
# programs are agents or daemons that sit waiting for something that is not
|
||||
# there, and five repetitions of a twenty-second wait, twice, is most of an
|
||||
# afternoon spent measuring `timeout`.
|
||||
best () {
|
||||
exe=$1; min=; rc0=
|
||||
for i in $(seq "$REPS"); do
|
||||
t0=$(date +%s%N)
|
||||
( cd "$out" && timeout 20 "$exe" >/dev/null 2>&1 </dev/null )
|
||||
rc=$?
|
||||
t1=$(date +%s%N)
|
||||
[ -n "$rc0" ] || rc0=$rc
|
||||
[ "$rc" = "$rc0" ] || { echo "-1"; return; }
|
||||
d=$(( (t1 - t0) / 1000 ))
|
||||
[ "$d" -lt 15000000 ] || { echo "-1"; return; }
|
||||
if [ -z "$min" ] || [ "$d" -lt "$min" ]; then min=$d; fi
|
||||
done
|
||||
echo "$min"
|
||||
}
|
||||
|
||||
printf 'name\tllvm_file\tllvm_text\tllvm_own\to0_own\tx86_file\tx86_text\tx86_own\tllvm_us\tx86_us\n'
|
||||
|
||||
for src in "$root"/test/programs/*.flan "$root"/spike/x86/*.flan; do
|
||||
name=$(basename "$src" .flan)
|
||||
if [ $# -gt 0 ]; then
|
||||
want=0
|
||||
for pat in "$@"; do case "$name" in *"$pat"*) want=1;; esac; done
|
||||
[ $want = 1 ] || continue
|
||||
fi
|
||||
case " $forever " in *" $name "*) continue;; esac
|
||||
|
||||
rm -f "$out/l" "$out/x" "$out/d"
|
||||
"$flan" build "$src" "${extra[@]}" -o "$out/l" >/dev/null 2>&1 || continue
|
||||
# No main is a link failure, and it leaves nothing behind to measure.
|
||||
test -x "$out/l" || continue
|
||||
"$flan" build "$src" --x86 "${extra[@]}" -o "$out/x" >/dev/null 2>&1 || continue
|
||||
test -x "$out/x" || continue
|
||||
|
||||
d0=0
|
||||
if [ "$o0" = 1 ] && "$flan" build "$src" --debug -o "$out/d" >/dev/null 2>&1; then
|
||||
d0=$(own "$out/d")
|
||||
fi
|
||||
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$name" \
|
||||
"$(stat -c %s "$out/l")" "$(text "$out/l")" "$(own "$out/l")" "$d0" \
|
||||
"$(stat -c %s "$out/x")" "$(text "$out/x")" "$(own "$out/x")" \
|
||||
"$(best "$out/l")" "$(best "$out/x")"
|
||||
done
|
||||
@ -13,7 +13,7 @@
|
||||
*
|
||||
* A release build has no cells, dlsym answers NULL, and this does nothing --
|
||||
* which is the control: it shows the change below comes from the indirection
|
||||
* and not from ordinary symbol interposition. See spike/x86/cells.sh.
|
||||
* and not from ordinary symbol interposition. See test/cells.sh.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
@ -23,7 +23,7 @@
|
||||
# from ordinary symbol interposition.
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
root=$(cd "$here/.." && pwd)
|
||||
# FLAN overrides the compiler, and when it is set nothing is built here. The
|
||||
# @cells alias sets it, because a dune action that shells out to dune waits on a
|
||||
# lock it cannot get; main.exe is in that rule's deps instead. Resolved to an
|
||||
@ -47,7 +47,7 @@ test -x "$flan" || { echo "no compiler at $flan"; exit 1; }
|
||||
out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
|
||||
cc -shared -fPIC -o "$out/override.so" "$here/cell-override.c" || exit 1
|
||||
|
||||
src=$here/p8-cell.flan
|
||||
src=$here/programs/x86-p8-cell.flan
|
||||
fail=0
|
||||
|
||||
run() { # run <label> <expected> <build flags...>
|
||||
50
test/dune
50
test/dune
@ -112,11 +112,6 @@
|
||||
(alias corpus)
|
||||
(source_tree syntax)
|
||||
(file %{workspace_root}/conditions-play.flan)
|
||||
(glob_files %{workspace_root}/spike/backend/*.flan)
|
||||
(glob_files %{workspace_root}/spike/generics/*.flan)
|
||||
(glob_files %{workspace_root}/spike/js/*.flan)
|
||||
(glob_files %{workspace_root}/spike/x86/*.flan)
|
||||
(glob_files %{workspace_root}/spike/x86/bench/*.flan)
|
||||
(glob_files %{workspace_root}/web/examples/*.flan)
|
||||
(glob_files %{workspace_root}/web/examples/geom/*.flan)))
|
||||
|
||||
@ -139,13 +134,8 @@
|
||||
(deps
|
||||
(alias corpus)
|
||||
test_sanitize.exe
|
||||
; p13-dyn-collect.flan, which lives with the x86 probes because that is the
|
||||
; lane that wrote it, and is in this sweep because of what it does rather
|
||||
; than where it is: it is the only program anywhere that allocates past
|
||||
; flan_dyn.c's one-megabyte floor, so it is the only one under which a mark
|
||||
; and a sweep actually run. Every other Flan program here agrees with ASan
|
||||
; by never collecting at all.
|
||||
(glob_files %{workspace_root}/spike/x86/*.flan)
|
||||
; ASAN_OPTIONS=detect_leaks=1 asks the leak question; see test_sanitize.ml.
|
||||
(env_var ASAN_OPTIONS)
|
||||
; The dyn runtime's C main, which is the one thing in this sweep that is not
|
||||
; a Flan program: flan_dyn.c has no Flan spelling yet. It is also the one
|
||||
; translation unit here that frees the most, which is what makes it worth a
|
||||
@ -177,7 +167,9 @@
|
||||
test_valgrind.exe
|
||||
; The suppression file, which is all reasons and no suppressions; its own
|
||||
; header says why that is the finding rather than an oversight.
|
||||
(file valgrind.supp))
|
||||
(file valgrind.supp)
|
||||
; FLAN_LEAKS=1 turns memcheck's leak check on; see test_valgrind.ml.
|
||||
(env_var FLAN_LEAKS))
|
||||
(action (run ./test_valgrind.exe)))
|
||||
|
||||
; The corpus a fourth time, through the hand-written x86-64 backend, compared
|
||||
@ -189,12 +181,13 @@
|
||||
; second backend still lowers the language: it refuses by name rather than
|
||||
; miscompiling, so when another lane adds a primitive the backend says so
|
||||
; loudly, and nothing was listening. Two such refusals sat in the tree for a
|
||||
; month. Now they fail a build somebody can run.
|
||||
; month. Now they fail a build somebody can run. The LLVM side is built at
|
||||
; -O0, the level this backend corresponds to.
|
||||
;
|
||||
; dune build --root . @x86
|
||||
;
|
||||
; A rule with no executable beside it, unlike @sanitize and @valgrind: the
|
||||
; check already exists as spike/x86/survey.sh, which is what every handoff
|
||||
; check already exists as survey-x86.sh, which is what every handoff
|
||||
; quotes its counts from, and a second implementation in OCaml would be a
|
||||
; second thing to drift. SURVEY_STRICT=1 turns its report into an exit
|
||||
; status. FLAN is passed because the script otherwise runs `dune build` on
|
||||
@ -205,19 +198,13 @@
|
||||
(alias x86)
|
||||
(deps
|
||||
(alias corpus)
|
||||
(file %{workspace_root}/spike/x86/survey.sh)
|
||||
(glob_files %{workspace_root}/spike/x86/*.flan)
|
||||
; The js spike's programs are plain flan programs and the sweep reads them
|
||||
; now, so they have to be in the build tree the sweep runs from -- otherwise
|
||||
; the glob matches nothing, and nothing is exactly what it was reporting
|
||||
; while p1-int-semantics.flan sat there with a wrong shift in it.
|
||||
(glob_files %{workspace_root}/spike/js/*.flan)
|
||||
(file survey-x86.sh)
|
||||
(file %{workspace_root}/bin/main.exe))
|
||||
(action
|
||||
(setenv SURVEY_STRICT 1
|
||||
(setenv SURVEY_QUIET 1
|
||||
(setenv FLAN %{workspace_root}/bin/main.exe
|
||||
(run bash %{workspace_root}/spike/x86/survey.sh))))))
|
||||
(run bash survey-x86.sh))))))
|
||||
|
||||
; The reference page, checked against the compiler that is supposed to have
|
||||
; produced everything on it. Two scripts, one alias, because they are halves of
|
||||
@ -284,7 +271,7 @@
|
||||
(setenv FLAN %{workspace_root}/bin/main.exe
|
||||
(run sh %{workspace_root}/web/examples/quotes.sh)))))
|
||||
|
||||
; The indirection cell, driven from outside the language. spike/x86/cells.sh
|
||||
; The indirection cell, driven from outside the language. cells.sh
|
||||
; preloads a shared object whose constructor stores a different body into
|
||||
; flan.cell.twice with dlsym, and checks that a --dev build notices and a
|
||||
; release build does not -- 22 22 against 42 42, both backends, four builds.
|
||||
@ -300,13 +287,13 @@
|
||||
(rule
|
||||
(alias cells)
|
||||
(deps
|
||||
(file %{workspace_root}/spike/x86/cells.sh)
|
||||
(file %{workspace_root}/spike/x86/cell-override.c)
|
||||
(glob_files %{workspace_root}/spike/x86/*.flan)
|
||||
(file cells.sh)
|
||||
(file cell-override.c)
|
||||
(file programs/x86-p8-cell.flan)
|
||||
(file %{workspace_root}/bin/main.exe))
|
||||
(action
|
||||
(setenv FLAN %{workspace_root}/bin/main.exe
|
||||
(run bash %{workspace_root}/spike/x86/cells.sh))))
|
||||
(run bash cells.sh))))
|
||||
|
||||
; Everything that checks something and is not `dune test`, in one word.
|
||||
;
|
||||
@ -358,16 +345,15 @@
|
||||
; should say so and stop, not report zero of everything. And SURVEY_STRICT
|
||||
; means something narrower here: a refusal is a *decision* in a dialect, not
|
||||
; a gap, so only a DIFFER and a CRASH fail the alias. See the header of
|
||||
; spike/js/survey.sh.
|
||||
; survey-js.sh.
|
||||
(rule
|
||||
(alias js)
|
||||
(deps
|
||||
(alias corpus)
|
||||
(file %{workspace_root}/spike/js/survey.sh)
|
||||
(glob_files %{workspace_root}/spike/js/*.flan)
|
||||
(file survey-js.sh)
|
||||
(file %{workspace_root}/bin/main.exe))
|
||||
(action
|
||||
(setenv SURVEY_STRICT 1
|
||||
(setenv SURVEY_QUIET 1
|
||||
(setenv FLAN %{workspace_root}/bin/main.exe
|
||||
(run bash %{workspace_root}/spike/js/survey.sh))))))
|
||||
(run bash survey-js.sh))))))
|
||||
|
||||
@ -61,6 +61,7 @@ void flan_rt_init(int32_t argc, char **argv);
|
||||
void flan_vec_free(void *v, int64_t size, int64_t align, const uint8_t *loc,
|
||||
int64_t loclen);
|
||||
void flan_vec_layout(int64_t out[6]);
|
||||
void flan_map_layout(int64_t out[10]);
|
||||
|
||||
static int failures;
|
||||
|
||||
@ -597,6 +598,23 @@ static void layout(void) {
|
||||
fail(msg);
|
||||
}
|
||||
}
|
||||
{
|
||||
/* The Map header and its block geometry, restated in flan_dyn.c so the
|
||||
collector can walk a map's slots. Two copies, compared directly. */
|
||||
int64_t mrt[10], mdyn[10];
|
||||
static const char *const mnames[10] =
|
||||
{ "sizeof", "offset of data", "offset of len", "offset of log2cap",
|
||||
"offset of alloc", "offset of epoch", "head bytes", "group",
|
||||
"alignment", "full bit" };
|
||||
flan_map_layout(mrt);
|
||||
flan_dyn_map_hdr_layout(mdyn);
|
||||
for (i = 0; i < 10; i++)
|
||||
if (mrt[i] != mdyn[i]) {
|
||||
snprintf(msg, sizeof msg, "flan_map vs. flan_dyn_map_hdr's %s: %lld vs. %lld",
|
||||
mnames[i], (long long)mrt[i], (long long)mdyn[i]);
|
||||
fail(msg);
|
||||
}
|
||||
}
|
||||
printf(failures == 0 ? "layout ok\n" : "layout failed\n");
|
||||
}
|
||||
|
||||
|
||||
17
test/programs/dyn-slice.flan
Normal file
17
test/programs/dyn-slice.flan
Normal file
@ -0,0 +1,17 @@
|
||||
;; (slice d ...) over a dyn text, in the three spellings the typed slice has.
|
||||
;; The result is a text of its own; the source is untouched.
|
||||
;;
|
||||
;; With an argument, the last slice runs past the end and traps.
|
||||
|
||||
(defn main [args [string]] i32
|
||||
(let [d (the dyn "hello")]
|
||||
(println (slice d))
|
||||
(println (slice d 1))
|
||||
(println (slice d 1 3))
|
||||
(println (slice d 5))
|
||||
(println (length (slice d 2)))
|
||||
(println (= (slice d 0 2) "he"))
|
||||
(println d)
|
||||
(when (> (length args) 1)
|
||||
(println (slice d 2 9))))
|
||||
0)
|
||||
@ -1,9 +1,58 @@
|
||||
;; A closure's environment is found by walking the storage its function value
|
||||
;; sits in — a frame slot, a global, a struct, an Option, a Vec's elements —
|
||||
;; and a Map's storage is not walked. An (Fn ...) as a Map's value would hold
|
||||
;; an environment the collector cannot see and would free. A Vec holds them,
|
||||
;; and a (CFn ...) carries no environment and may go in a Map.
|
||||
;; A Map holding closures and a Map holding dyn values, both kept alive by the
|
||||
;; collector across enough allocation that it runs many times. A Map's block is
|
||||
;; walked like a Vec's: the full slots' values are marked through the value
|
||||
;; type's descriptor. A lost value is a use of freed memory here, not a wrong
|
||||
;; number.
|
||||
|
||||
(defn adder [n i32] (Fn [i32] i32)
|
||||
(fn [x] (+ x n)))
|
||||
|
||||
;; Garbage, and plenty of it: every pass boxes and drops a vector of four.
|
||||
(defn churn [n i32] ()
|
||||
(dotimes [i n]
|
||||
(let [junk (vec-new dyn)]
|
||||
(push junk i)
|
||||
(push junk "row")
|
||||
(push junk 2.5)
|
||||
(push junk true))))
|
||||
|
||||
(defn main [] i32
|
||||
(let [ops (map-new string (Fn [i32] i32))]
|
||||
(put ops "id" (fn [x] x))
|
||||
0))
|
||||
(let [ops (map-new string (Fn [i32] i32))
|
||||
many (map-new i32 (Fn [i32] i32))
|
||||
rows (map-new i32 dyn)]
|
||||
(put ops "one" (adder 1))
|
||||
(put ops "ten" (adder 10))
|
||||
(put ops "hundred" (adder 100))
|
||||
;; Each value a dyn vector the collector allocated, reachable from
|
||||
;; nothing but the map.
|
||||
(dotimes [i 64]
|
||||
(let [row (vec-new dyn)]
|
||||
(push row i)
|
||||
(push row "row")
|
||||
(put rows i row)))
|
||||
(churn 40000)
|
||||
;; Grown after the churn, so a rebuilt block is walked too.
|
||||
(dotimes [i 64]
|
||||
(put many i (adder i)))
|
||||
(churn 40000)
|
||||
(let [total 0]
|
||||
(dotimes [i 64]
|
||||
(set total (+ total (match (get many i) (Some f) (f 0) None 0))))
|
||||
(println total))
|
||||
(dotimes [i 3]
|
||||
(let [name (at ["one" "ten" "hundred"] i)]
|
||||
(println (match (get ops name) (Some f) (f 1) None -1))))
|
||||
(let [cur (i64 0)
|
||||
k 0
|
||||
v (the dyn nil)
|
||||
sum 0
|
||||
n 0]
|
||||
(while (map-next rows (addr cur) (addr k) (addr v))
|
||||
(set sum (+ sum (i32 (at v 0))))
|
||||
(set n (+ n 1)))
|
||||
(println n)
|
||||
(println sum))
|
||||
(free ops)
|
||||
(free many)
|
||||
(free rows))
|
||||
0)
|
||||
|
||||
30
test/programs/map-array-key.flan
Normal file
30
test/programs/map-array-key.flan
Normal file
@ -0,0 +1,30 @@
|
||||
;; A fixed array of strings, and of structs holding a string, as a map key.
|
||||
;; Each element is hashed and compared by its own pair, so two keys built from
|
||||
;; different storage with the same bytes are the same key.
|
||||
|
||||
(defstruct Tag [name string n i32])
|
||||
|
||||
(defn main [] i32
|
||||
(let [m (map-new [2 string] i32)
|
||||
a (the [2 string] ["ab" "cd"])
|
||||
b (the [2 string] [(slice "xab" 1) (slice "cdx" 0 2)])
|
||||
c (the [2 string] ["ab" "ce"])]
|
||||
(put m a 1)
|
||||
(put m c 3)
|
||||
(println (or-else (get m b) -1))
|
||||
(println (or-else (get m c) -1))
|
||||
(println (length m))
|
||||
(put m b 2)
|
||||
(println (length m))
|
||||
(println (or-else (get m a) -1))
|
||||
(free m))
|
||||
(let [t (map-new [3 Tag] i32)
|
||||
x (the [3 Tag] [(Tag "a" 1) (Tag "b" 2) (Tag "c" 3)])
|
||||
y (the [3 Tag] [(Tag "a" 1) (Tag "b" 2) (Tag "c" 4)])]
|
||||
(put t x 10)
|
||||
(put t y 20)
|
||||
(println (or-else (get t (the [3 Tag] [(Tag "a" 1) (Tag "b" 2) (Tag "c" 3)])) -1))
|
||||
(println (or-else (get t y) -1))
|
||||
(println (length t))
|
||||
(free t))
|
||||
0)
|
||||
59
test/programs/map-keys.flan
Normal file
59
test/programs/map-keys.flan
Normal file
@ -0,0 +1,59 @@
|
||||
;; map-keys and map-values: the prelude's two generic walks over a map. Block
|
||||
;; order is the hash's, so what comes back is sorted before it is printed.
|
||||
;;
|
||||
;; The three globals are named after the prelude's type variables. A generic's
|
||||
;; body names its element type as $t, $k or $v, never bare, because a bare
|
||||
;; name there is an expression and would find these.
|
||||
(defonce t i32 0)
|
||||
(defonce k i32 0)
|
||||
(defonce v i32 0)
|
||||
|
||||
(defn adder [n i32] (Fn [i32] i32)
|
||||
(fn [x] (+ x n)))
|
||||
|
||||
(defn main [] i32
|
||||
(let [m (map-new i32 i64)]
|
||||
(put m 30 (i64 300))
|
||||
(put m 10 (i64 100))
|
||||
(put m 20 (i64 200))
|
||||
(let [ks (map-keys m)
|
||||
vs (map-values m)]
|
||||
(sort (slice ks))
|
||||
(sort (slice vs))
|
||||
(dotimes [i (length ks)] (print (at ks i)) (print " "))
|
||||
(println "")
|
||||
(dotimes [i (length vs)] (print (at vs i)) (print " "))
|
||||
(println "")
|
||||
(free ks)
|
||||
(free vs))
|
||||
(free m))
|
||||
;; A string key, and a map that never allocated.
|
||||
(let [names (map-new string i32)
|
||||
none (map-new string i32)]
|
||||
(put names "b" 2)
|
||||
(put names "a" 1)
|
||||
(let [ks (map-keys names)
|
||||
nk (map-keys none)
|
||||
total 0]
|
||||
(dotimes [i (length ks)] (set total (+ total (or-else (get names (at ks i)) 0))))
|
||||
(println total)
|
||||
(println (length nk))
|
||||
(free ks)
|
||||
(free nk))
|
||||
(free names)
|
||||
(free none))
|
||||
;; Closures as the values: a function value cannot be zeroed, so the walk
|
||||
;; must not need a place for one.
|
||||
(let [fs (map-new i32 (Fn [i32] i32))]
|
||||
(put fs 1 (adder 1))
|
||||
(put fs 2 (adder 2))
|
||||
(let [ks (map-keys fs)
|
||||
vs (map-values fs)
|
||||
total 0]
|
||||
(dotimes [i (length vs)] (set total (+ total ((at vs i) 10))))
|
||||
(println (length ks))
|
||||
(println total)
|
||||
(free ks)
|
||||
(free vs))
|
||||
(free fs))
|
||||
0)
|
||||
@ -123,7 +123,8 @@
|
||||
(print (length t)) (println "") ; 150
|
||||
(match (get t 299) (Some v) (do (print v) (println "")) None (println "?")) ; 598
|
||||
(print (has-key? t 298)) (println ""))) ; false
|
||||
(free-all ar))
|
||||
(free-all ar)
|
||||
(arena-destroy ar))
|
||||
|
||||
;; (7) Churn at a steady size, checked against a plain array. Keys are drawn
|
||||
;; from 512 and the map holds about two thirds of them, so removals leave
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user