Compare commits
No commits in common. "6ac11be5b1d58f73d04d54ed24f80fdfc1d92866" and "9dddcac821fc3c3a1bdd623cc45e612ba8148db0" have entirely different histories.
6ac11be5b1
...
9dddcac821
16
.gitignore
vendored
16
.gitignore
vendored
@ -50,18 +50,7 @@ old-ocaml/
|
||||
/sand.html
|
||||
/sand.js
|
||||
/sand.wasm
|
||||
|
||||
# The agent harness: its settings, its transcripts and the worktrees it
|
||||
# registers all live here, and none of them are this repository's business.
|
||||
.claude/
|
||||
|
||||
# A stale unix socket left behind by `flan dev`. The daemon writes one beside
|
||||
# the program it is serving and unlinks it on a clean exit; a killed daemon
|
||||
# does not, and the leftover is a socket, not a file worth keeping.
|
||||
.flan-dev.sock
|
||||
|
||||
# Hand-run scratch: a `probe` executable and the C it was compiled from, from
|
||||
# poking at codegen by hand rather than through a test.
|
||||
probe
|
||||
probe.c
|
||||
|
||||
@ -79,10 +68,5 @@ test/web-files-out.txt
|
||||
# Python bytecode from the tools directory
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Two more hand-run leftovers: a shared object built beside a form experiment,
|
||||
# and the history directory a spike driver writes when it is run from the
|
||||
# source tree. Anchored where they appear, and nothing in the build makes
|
||||
# either one.
|
||||
/forms.so
|
||||
_hist/
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Two examples, `text_codepoints_loading` and `textures_image_processing`, ported into
|
||||
`examples/`. This file is the running record: what is done, what was decided without being
|
||||
able to ask, and what is still open. `../PORTING.md` has the findings that outlive the port —
|
||||
able to ask, and what is still open. `PORTING.md` has the findings that outlive the port —
|
||||
its last section, "Porting the raylib examples — the last two" — and this file has the
|
||||
state of the work.
|
||||
|
||||
@ -12,7 +12,7 @@ state of the work.
|
||||
- [x] `examples/textures-image-processing.flan` — builds
|
||||
- [x] `test/programs/raylib-codepoints.flan` and `test/programs/raylib-image-processing.flan`,
|
||||
both in the acceptance table, both gated on libraylib, both run plain and at `-O0`
|
||||
- [x] `../PORTING.md` section for this round
|
||||
- [x] `PORTING.md` section for this round
|
||||
- [x] `dune test --root .` green
|
||||
|
||||
Four commits, each one a working state:
|
||||
@ -39,7 +39,7 @@ Four commits, each one a working state:
|
||||
reported", *"a `(Ptr T)` where the header says `T *` and the hand-written line chose
|
||||
something more specific"*. The function implements the enum-against-i32 arm and nothing
|
||||
else. That missing arm is the only thing standing between this round's two refused
|
||||
bindings and ordinary binding work — see `../PORTING.md` §A.1 and §A.2, which have the
|
||||
bindings and ordinary binding work — see `PORTING.md` §A.1 and §A.2, which have the
|
||||
exact declarations and the exact error. **Not built: it is a compiler change and the
|
||||
freeze is on.** It is small and it is well specified, and it is the thing to do first if
|
||||
these two ports are ever revisited.
|
||||
@ -61,7 +61,7 @@ So a reference to a symbol the object does not define goes through the GOT. Meas
|
||||
`call_flan`'s `` `Cell `` target loaded **one** level: `mov r11, cell@GOTPCREL(%rip)` then `call *%r11`. That
|
||||
reads as "call through the cell" in a disassembly and in fact *calls the cell*. The GOT slot holds the cell's
|
||||
**address**; reading what the cell holds is two loads, not one. Found by the segfault, not by reading — which is
|
||||
../DISCUSS.md item 15's claim, now with a sixth instance. `load_sym` is the fix and is the only helper that
|
||||
DISCUSS.md item 15's claim, now with a sixth instance. `load_sym` is the fix and is the only helper that
|
||||
double-loads; `Lgot` gets it right for free because `lmem` already had the shape.
|
||||
|
||||
## Scope, deliberately narrower than `Emit.redefinition`
|
||||
@ -1,7 +1,7 @@
|
||||
# Handoff — the x86 backend's last two correctness items
|
||||
|
||||
Branch: `dev-loop`, worktree `agent-af9064091c0602dc8`. Three commits plus this one; nothing is half-written and
|
||||
nothing is reverted. The long-form report is **../DISCUSS.md item 18**; this file is the operational version.
|
||||
nothing is reverted. The long-form report is **DISCUSS.md item 18**; this file is the operational version.
|
||||
|
||||
## 1. The `flan_vec_as_slice` answer — got it, and it is complete
|
||||
|
||||
200
NEXT.md
200
NEXT.md
@ -7,7 +7,7 @@ of taste, and it is worth your reading of it: `runtime/flan_rt.c:70-86` allocate
|
||||
`restart-case` that offers it, on that frame's own stack**, so the runtime cannot push one on a program's
|
||||
behalf. Offering `use-value` at a division would mean an alloca plus a push and pop emitted at every
|
||||
division in every checked build — which is the identical cost `lib/prelude.ml:70-76` already refuses, in
|
||||
prose and on the record, for indexing. `docs/handoffs/HANDOFF-arith.md` is the full write-up.
|
||||
prose and on the record, for indexing. `HANDOFF-arith.md` is the full write-up.
|
||||
|
||||
So the question back to you: **is signal-and-die-with-a-location enough for arithmetic, the way it is for
|
||||
`BoundsError`, or do you want `saturate` on the cast arm** — which is the one case where clamping is
|
||||
@ -26,14 +26,14 @@ Nothing is running and nothing is half-built. Read this first.
|
||||
`spike/x86/survey.sh` builds every program in `test/programs` and every probe in `spike/x86` both
|
||||
ways and compares **stdout, stderr and exit status**: **97 MATCH, 0 DIFFER, 0 refused by name.** That
|
||||
is up from 41/1/41 at the start of the day. Conditions, bounds checks, `Fnval`'s indirection cell and
|
||||
`--x86 --dev` all landed. `docs/DISCUSS.md` items 16, 17 and 18 are the three reports.
|
||||
`--x86 --dev` all landed. `DISCUSS.md` items 16, 17 and 18 are the three reports.
|
||||
|
||||
**`docs/handoffs/HANDOFF-x86-rt.md` at the root has the remaining seven items in order.** Item 1 is the substantial
|
||||
**`HANDOFF-x86-rt.md` at the root has the remaining seven items in order.** Item 1 is the substantial
|
||||
one and is the reason the backend exists:
|
||||
|
||||
> **A redefinition emitter in `lib/x86.ml`** — the counterpart to `Emit.redefinition`, producing a
|
||||
> `.so`. Until it exists the backend builds whole programs and cannot serve a single `C-c C-c`, which
|
||||
> is where `docs/DISCUSS.md` item 13's ~21ms of a 35ms redefinition actually lives.
|
||||
> is where `DISCUSS.md` item 13's ~21ms of a 35ms redefinition actually lives.
|
||||
|
||||
**And the constraint the last lane found, which is the thing not to rediscover:** publishing a cell an
|
||||
LLVM-built module can store into puts an edge on `lib/x86.ml`'s own licence that *"a dev build is
|
||||
@ -78,7 +78,7 @@ request and `Session.eval` commits below the checker.
|
||||
|
||||
## Queued, nothing blocked on a decision
|
||||
|
||||
1. `docs/handoffs/HANDOFF-x86-rt.md`'s seven items, item 1 first.
|
||||
1. `HANDOFF-x86-rt.md`'s seven items, item 1 first.
|
||||
2. `text_codepoints_loading` and `textures_image_processing` — ordinary porting now that
|
||||
`slice-from-ptr` exists.
|
||||
3. Generic structs and `$n` array lengths — decided, unblocked, sections below. **Stopped once
|
||||
@ -101,7 +101,7 @@ something that is not memory. Their old entries stand; do not schedule either.
|
||||
**Built.** `flan_slice_promise_error` is its own function in `runtime/flan_rt.c`, shaped like the two
|
||||
beside it — it signals `BoundsError`, walks the handlers, offers the break loop, and only then falls
|
||||
through to a message and a status. The sentence names the promise and the number that broke it, and a
|
||||
second line says what is *not* checked. See [`docs/BUILT.md`](docs/BUILT.md), "`slice-from-ptr` refuses in its
|
||||
second line says what is *not* checked. See [`BUILT.md`](BUILT.md), "`slice-from-ptr` refuses in its
|
||||
own words". The original entry follows.
|
||||
|
||||
Small, and known rough at the time it shipped. `(slice-from-ptr p n)` with a **computed** negative
|
||||
@ -127,10 +127,10 @@ is the whole reason it was left. Say what the caller promised and what was passe
|
||||
**Named here because it is not on the list below and would otherwise be discovered late.**
|
||||
|
||||
Finishing items 1-6 below gives a second **whole-program** compiler. `--x86` is refused with `--dev`
|
||||
today, deliberately, and `docs/DISCUSS.md` item 16 says it has no cells. So at the end of that list the
|
||||
today, deliberately, and `DISCUSS.md` item 16 says it has no cells. So at the end of that list the
|
||||
backend can build `sand.flan` and cannot serve a single `C-c C-c`.
|
||||
|
||||
What the backend was for is `docs/DISCUSS.md` item 13's measurement: `llc` and `ld` are **about 21ms of a
|
||||
What the backend was for is `DISCUSS.md` item 13's measurement: `llc` and `ld` are **about 21ms of a
|
||||
35ms redefinition**, and removing them is the only thing that touches that number. Collecting it means
|
||||
emitting a **redefinition module** — one function, compiled and installed into a running process
|
||||
through an indirection cell — which is what `Build.shared` does on the LLVM path.
|
||||
@ -150,13 +150,13 @@ Concretely, and none of it is scoped yet:
|
||||
- **DWARF.** `flan dev --debug` exists and `--x86` refuses `--debug`. A dev backend that cannot be
|
||||
debugged is a narrower tool than the one it replaces.
|
||||
- **Then measure, because the whole case rests on it.** If a hand-written backend does not actually
|
||||
beat 35ms end to end, the argument for it was wrong and that is worth knowing. `docs/DISCUSS.md` item 13
|
||||
beat 35ms end to end, the argument for it was wrong and that is worth knowing. `DISCUSS.md` item 13
|
||||
says the backend is decided at step 4 on a measurement taken at step 3, and this is that
|
||||
measurement finally being takeable.
|
||||
|
||||
## The backend, in the order that finishes it
|
||||
|
||||
`docs/DISCUSS.md` item 16 is the report and its verdict is the sequencing: *"the wiring is done and it was
|
||||
`DISCUSS.md` item 16 is the report and its verdict is the sequencing: *"the wiring is done and it was
|
||||
the easy half. What is left is conditions, and the measurement moved them from 'first obstacle' to
|
||||
'the only obstacle'."* 40 of 111 programs lower today; the other 40 are refused by name.
|
||||
|
||||
@ -211,7 +211,7 @@ question 4 seriously now that there are two backends that can disagree.
|
||||
policy that distinguishes it from `two_process`. `HANDOFF-f1.md` is gone with it.
|
||||
- ~~**The memcheck half of the registry**~~ — done. `flan_arena_proc`'s `free-all` issues memcheck's
|
||||
`MAKE_MEM_UNDEFINED` over the arena, vendored rather than included; it is a control in
|
||||
`test_valgrind.ml` now, and `docs/BUILT.md` carries the cost. The registry answer and the memcheck
|
||||
`test_valgrind.ml` now, and `BUILT.md` carries the cost. The registry answer and the memcheck
|
||||
answer stay two claims.
|
||||
|
||||
## Not in the freeze, because it is already decided and unblocked
|
||||
@ -236,7 +236,7 @@ everything below it is the standing queue and the decision record.
|
||||
- **`pause` marking, both halves.** `C-u` before an eval marks the innermost form, `C-u C-u` the
|
||||
top-level one.
|
||||
- **`spy-num`.** A watch slot keeps count, min, max, last, mean, windowed to the editor's last tick.
|
||||
- **Frame rollback**, `docs/PORTING.md` Tier 1 item 6. `restore` goes in the `continue` clause, not a defer.
|
||||
- **Frame rollback**, `PORTING.md` Tier 1 item 6. `restore` goes in the `continue` clause, not a defer.
|
||||
- **The dev allocation registry is finished.** `M-x flan-inspect-address`, `M-x flan-allocations`,
|
||||
`M-x flan-leaks`.
|
||||
- **15 raylib examples**, an idiomatic layer over the bindings, and `vendor/raylib/vector.flan`.
|
||||
@ -252,7 +252,7 @@ everything below it is the standing queue and the decision record.
|
||||
|
||||
~~**`worktree-agent-a859480edc827ab73` — macros importable from a package.**~~ **Finished.** The suite
|
||||
is green, the acceptance wiring asserts both halves of the rule, and the dev loop is covered. See
|
||||
docs/BUILT.md, "A package may declare a macro, and its name is the package's".
|
||||
BUILT.md, "A package may declare a macro, and its name is the package's".
|
||||
|
||||
The finding is worth keeping: the old refusal claimed collecting a package's macros needed a second
|
||||
import resolver at the Form level. It did not. The file being compiled is parsed before `Load` runs
|
||||
@ -260,7 +260,7 @@ too, so no shape of the feature could have left import resolution where it was
|
||||
forms now and uses the one resolver that always existed.
|
||||
|
||||
~~One gap left, and it is older than this work: **`C-x C-e` expands no macros at all.**~~ **Done** —
|
||||
see [`docs/BUILT.md`](docs/BUILT.md), "`C-x C-e` expands too, and a declaration is not an expression". It was
|
||||
see [`BUILT.md`](BUILT.md), "`C-x C-e` expands too, and a declaration is not an expression". It was
|
||||
the wrap `Parse.decl` already had, and the decision it was waiting on came out as: an expression that
|
||||
expands to a declaration is **refused by name**, in `Parse.expr`'s head dispatch, so a `defn` nested
|
||||
anywhere in the expansion and a `defvar` typed by hand get the same sentence. The spin refusal fires
|
||||
@ -269,7 +269,7 @@ expands is the prelude's macros and the imported packages' — and, since the la
|
||||
the file's own as well.
|
||||
|
||||
Two things left behind it, neither this lane's. ~~**A session expands no macro the buffer itself
|
||||
declares**~~ — **done**, see [`docs/BUILT.md`](docs/BUILT.md), "A session expands the buffer's own macros". The
|
||||
declares**~~ — **done**, see [`BUILT.md`](BUILT.md), "A session expands the buffer's own macros". The
|
||||
session seeds its file's own `defmacro`s in `Session.create`, from the same read that produced
|
||||
`decls`, and an evaluated `defmacro` joins the set so the next evaluation can call it — the shape
|
||||
`defn` already has. Re-reading the file was the alternative and was refused: it would put
|
||||
@ -303,7 +303,7 @@ the Emacs side". **Superseded** — the registry lane built that properly. Delet
|
||||
|
||||
## For siam-farmer
|
||||
|
||||
`docs/PORTING.md`'s verdict is unconditional now: the game's state is **fixed arrays with counts**, so it
|
||||
`PORTING.md`'s verdict is unconditional now: the game's state is **fixed arrays with counts**, so it
|
||||
fits in `defvar` globals, nothing is move-only, and generics is off its critical path. Tier 0 and Tier 1
|
||||
items 4, 5 and 6 are all done. Nothing blocks writing it.
|
||||
|
||||
@ -407,7 +407,7 @@ allocation**, which is Odin's `Small_Array` and a good fit for a game that refus
|
||||
(defvar enemies (Small-Array 64 Enemy))
|
||||
```
|
||||
|
||||
`docs/PORTING.md`'s recommendation for the game's state was fixed arrays with counts held in `defvar` globals, kept
|
||||
`PORTING.md`'s recommendation for the game's state was fixed arrays with counts held in `defvar` globals, kept
|
||||
deliberately away from `Vec` so nothing is move-only. `(Small-Array $n $t)` is that pattern with a type behind it
|
||||
instead of two variables kept in step by hand. That is the argument for building this, and it is stronger than
|
||||
"Odin has it".
|
||||
@ -436,7 +436,7 @@ separate question and should be answered deliberately rather than falling out of
|
||||
|
||||
## Decided 2026-09-13: generics by monomorphisation, checked abstractly, with `where` predicates
|
||||
|
||||
The spike answered it (`docs/SPIKE-GENERICS.md`, on `worktree-agent-afcd2406f3660629b`): **it runs**, the whole feature
|
||||
The spike answered it (`SPIKE-GENERICS.md`, on `worktree-agent-afcd2406f3660629b`): **it runs**, the whole feature
|
||||
is `lib/check.ml` and nothing else in `lib/`, and instantiation is under the noise floor at 1.8ms whole-program
|
||||
re-check. The bill is `llc`+`ld` at **+1.7ms per extra body**, so a generic used at three types adds ~7ms to a
|
||||
redefinition. `plan.org` is updated; what follows is the decision and the reasoning that does not belong there.
|
||||
@ -615,13 +615,13 @@ why they are here and not in a binding list.
|
||||
check now accepts an enum where the header says `int`, symmetrically, and still refuses
|
||||
anything that is not four bytes. `Camera3D.projection` is a `CameraProjection` again and the
|
||||
`rl/camera-projection` helper is gone; `.projection :perspective` resolves at the construction
|
||||
site, so the keyword half of the problem went away with it. See docs/BUILT.md.
|
||||
site, so the keyword half of the problem went away with it. See BUILT.md.
|
||||
|
||||
3. ~~**The header check does not reach `defconst` or `defenum`.**~~ **Closed.** It reaches both.
|
||||
`bindings` gained `enum`, `const` and `constant` lines that say what a Flan constant is called in
|
||||
C; every mapped name is compared by value, and a name the mapping cannot find, a rule that
|
||||
reaches nothing, and a `defenum` with no line at all are each reported rather than skipped. All
|
||||
eight raylib enums and all 16 `ConfigFlags` bits check out against 5.5. See docs/BUILT.md.
|
||||
eight raylib enums and all 16 `ConfigFlags` bits check out against 5.5. See BUILT.md.
|
||||
|
||||
4. ~~**raymath is `static inline`, so there is no symbol to bind.**~~ **Closed for the vector half.**
|
||||
`Clamp`, `Vector2Add`, `Remap` and the rest exist only in the header and `declare-c` has nothing
|
||||
@ -662,7 +662,7 @@ tip themselves. A lane that did not notice would produce plausible work against
|
||||
|
||||
**Built.** All seven items landed: the `eval_expr` pause path, `flan-dev--eval` sending the position,
|
||||
`flan-eval-defun` taking `C-u`, the overlay and its face, `test/programs/dev-pause.flan`, the `test_dev.ml` block and
|
||||
the `emacs/test-flan-dev.el` one. `HANDOFF-f2.md` is deleted; the reasoning is in `docs/BUILT.md`, "A breakpoint is a
|
||||
the `emacs/test-flan-dev.el` one. `HANDOFF-f2.md` is deleted; the reasoning is in `BUILT.md`, "A breakpoint is a
|
||||
function call, and the editor only says where".
|
||||
|
||||
### ~~From `HANDOFF-f1.md`~~ — the socket flake is fixed, and everything that followed it is done
|
||||
@ -730,7 +730,7 @@ actually worth having. The timers are 30s, down from a minute, because the build
|
||||
|
||||
### ~~From `HANDOFF-f3.md` — the watch is built; two tests are not~~
|
||||
|
||||
`spy-num` landed and `docs/PORTING.md` Tier 1 item 5 is closed. The `spy` half already existed — the finding that
|
||||
`spy-num` landed and `PORTING.md` Tier 1 item 5 is closed. The `spy` half already existed — the finding that
|
||||
reframed the task is that the table, the agent commands, the buffer and the ghost text were all there at `344e571`,
|
||||
and only the numeric accumulator was missing. A slot keeps count, min, max, last and mean; the window is since the
|
||||
editor's last tick rather than cumulative, because cumulative min/max reach the session's extremes within seconds of
|
||||
@ -745,19 +745,19 @@ stopped program does not sample, so an epoch-aware reader would blank the watch
|
||||
is exactly when the numbers are being read. What was actually wrong was in the editor — `flan-watch--tick` sent
|
||||
`:reset t` five times a second at a program that could not answer it — and it is now guarded on `flan-dev--stopped`,
|
||||
with the read still going out every tick. `emacs/test-flan-watch.el` pins both halves. `HANDOFF-f3.md` is deleted;
|
||||
the reasoning is in `docs/BUILT.md`, "A hot loop keeps five numbers, and the window is the editor's".
|
||||
the reasoning is in `BUILT.md`, "A hot loop keeps five numbers, and the window is the editor's".
|
||||
|
||||
### Not started: five more raylib core examples
|
||||
|
||||
Ten are in `examples/`. Five more were queued and the lane was stopped before it wrote anything, so there is nothing
|
||||
to pick up — only the brief. Pick for what they exercise of the bindings rather than for how they look, since the
|
||||
point of the corpus is finding gaps in `vendor/raylib/raylib.flan`. Upstream is
|
||||
`vendor/raylib/web/raylib-5.5/examples/core/`. `docs/PORTING.md` §1's rule binds: a raylib function on a real per-frame
|
||||
`vendor/raylib/web/raylib-5.5/examples/core/`. `PORTING.md` §1's rule binds: a raylib function on a real per-frame
|
||||
path is hand-written and header-checked, not left to the opt-in `FLAN_RAYLIB_H` import, because the default build
|
||||
has no `FLAN_RAYLIB_H` and still has to draw. An enum value has to be added by hand whatever happens — the importer
|
||||
generates functions and only functions.
|
||||
|
||||
### Landed: frame rollback, `docs/PORTING.md` Tier 1 item 6
|
||||
### Landed: frame rollback, `PORTING.md` Tier 1 item 6
|
||||
|
||||
`test/programs/frame-rollback.flan`, with three acceptance rows beside `bounds-condition.flan`'s (plain, `-O0`,
|
||||
dev). Nothing was added to the language, which was the premise: `snapshot` is `(set grid-backup grid)` and
|
||||
@ -820,7 +820,7 @@ written in `vendor/raylib` now, and have not been.
|
||||
|
||||
## ~~Queued: a restart is not a transaction, and the docs must say so~~ — **landed**
|
||||
|
||||
See [`docs/BUILT.md`](docs/BUILT.md), "A restart is not a transaction". **This entry was stale** — all three places already
|
||||
See [`BUILT.md`](BUILT.md), "A restart is not a transaction". **This entry was stale** — all three places already
|
||||
carried the note when it was re-read: `conditions.org` under *Gotchas*, `spec-conditions.md` §5, and
|
||||
`web/index.html`'s restart gotcha list, which was rewritten in place rather than gaining a second bullet beside the
|
||||
existing one. Nothing was left to write.
|
||||
@ -841,7 +841,7 @@ skip a frame and carry on rather than die — exactly the case where a non-idemp
|
||||
|
||||
## ~~Queued: a dev-build allocation registry~~ — **landed; one item left, and it is not a registry item**
|
||||
|
||||
Items 1 to 5 are built and the test is written. `docs/BUILT.md`'s *"An address answers with a type"* is the account of the
|
||||
Items 1 to 5 are built and the test is written. `BUILT.md`'s *"An address answers with a type"* is the account of the
|
||||
table, the note and the inspector's pointer arm; *"An address you have in your hand"* is the account of the reader —
|
||||
the address root, the breakdown, the leak report, and what "at exit" turned out to mean.
|
||||
|
||||
@ -861,7 +861,7 @@ do not need the registry. This is for plain structs, `Vec`, `Map` and pool stora
|
||||
|
||||
**On cost, as built.** One insert per allocation, always on in a dev build, no opt-out — the author's instruction,
|
||||
followed literally. Nothing was built per-region, no range recording, no per-allocator opt-out. Revisit only if a real
|
||||
program shows a problem, and `docs/BUILT.md` still names **two** places a release build is not quite free: the readers
|
||||
program shows a problem, and `BUILT.md` still names **two** places a release build is not quite free: the readers
|
||||
added since are functions nothing in a release build calls, and the exit report is registered by `atexit` from inside
|
||||
`flan_dev_reg_enable` rather than by a file-scope destructor, precisely so that it is not a third.
|
||||
|
||||
@ -871,14 +871,14 @@ Three things, in order. The first two are one line each and unblock a real game.
|
||||
|
||||
**1. `DrawTexturePro` is not bound — DONE.** It is `draw-texture-pro` in `vendor/raylib/raylib.flan` now, hand-written
|
||||
beside `draw-texture-rec` and read off a raylib header rather than remembered. `image-from-image` and `window-ready?`
|
||||
went in with it. The rule the gap exposed is written down in `docs/BUILT.md` and `docs/PORTING.md` §1: *a raylib function on a
|
||||
went in with it. The rule the gap exposed is written down in `BUILT.md` and `PORTING.md` §1: *a raylib function on a
|
||||
game's per-frame path is hand-written and header-checked, not left to the opt-in import* — the import widens the
|
||||
surface and must not be load-bearing, because the default build has no `FLAN_RAYLIB_H` and still has to draw.
|
||||
|
||||
**2. `Key` has no `left-shift` — DONE.** `left-shift 340`, and nothing else: `docs/PORTING.md` §5 checked every other enum
|
||||
**2. `Key` has no `left-shift` — DONE.** `left-shift 340`, and nothing else: `PORTING.md` §5 checked every other enum
|
||||
value the game touches and they were all already right.
|
||||
**1. `DrawTexturePro` — done, and not by a hand-written line.** It was the one true blocker for `siam-farmer`
|
||||
(see `docs/PORTING.md`: every tile in both implementations goes through it, and neither `DrawTextureRec` nor
|
||||
(see `PORTING.md`: every tile in both implementations goes through it, and neither `DrawTextureRec` nor
|
||||
`DrawTextureEx` substitutes). It was reachable only through the opt-in `FLAN_RAYLIB_H` import. The bindings are
|
||||
committed now, so `rl/draw-texture-pro` is in `vendor/raylib/generated.flan` and a default build has it. **Nothing
|
||||
should add it by hand** — a second `declare-c` for the same C symbol is refused for the whole program.
|
||||
@ -892,7 +892,7 @@ still a hand edit: the importer generates functions and only functions, so no `d
|
||||
the break loop, and tail into the old `flan_bounds_fail` message and status only if nothing answered. `Vec`'s two
|
||||
checks are plumbed the same way, since `(at v i)` and `(at arr i)` are one form in the source.
|
||||
|
||||
**No restart is established at the failing index**, and `docs/BUILT.md` has the argument: `retry` exists for allocation and
|
||||
**No restart is established at the failing index**, and `BUILT.md` has the argument: `retry` exists for allocation and
|
||||
for files because those attempts are *repeatable*, and nothing a handler can do makes index 51 valid for a length-50
|
||||
array. `use-value` for the index would cost every indexing operation a restart frame and buy a silently different
|
||||
element. What answers a bad index is the restart the program already had — a frame loop's `continue`, `sand.flan`'s
|
||||
@ -912,22 +912,22 @@ resumes with the session intact.
|
||||
different kind of failure — the region the container lived in was released — and there is no frame to go back to that
|
||||
would not read freed memory. The map path was left alone rather than converted half-way.
|
||||
|
||||
### What is left on `docs/PORTING.md`'s list
|
||||
### What is left on `PORTING.md`'s list
|
||||
|
||||
Tier 0 is finished, and so is item 5. **The watch for a running program is done** — the `spy` half was already
|
||||
built (a pushed table, the watch buffer, inline ghost text) and the `spy-num` half landed 2026-09-13: a hot-loop slot
|
||||
keeps count/min/max/last/mean, the write path does no formatting, and the window is since the editor's last tick
|
||||
rather than cumulative, which is a deliberate divergence from `watch.clj` argued in `docs/BUILT.md`, "A hot loop keeps
|
||||
rather than cumulative, which is a deliberate divergence from `watch.clj` argued in `BUILT.md`, "A hot loop keeps
|
||||
five numbers, and the window is the editor's". **Item 6 landed the same day**:
|
||||
`test/programs/frame-rollback.flan` is the worked example — `snapshot` at the top of the frame, `restore` in the
|
||||
`continue` clause — and `bounds-condition.flan`'s half-written abandoned frame is the thought it finishes. That was
|
||||
the last Tier 1 item anyone was going to move. Items 7 (`drop`), 8 (generics) and 9 (`(read-edn T bytes)`) are still
|
||||
on that list and still deferred with reasons written beside each; none is a blocker for this game.
|
||||
|
||||
**What `docs/PORTING.md` says NOT to build, with evidence:** escaping closures (one capture site, fixed by one parameter),
|
||||
**What `PORTING.md` says NOT to build, with evidence:** escaping closures (one capture site, fixed by one parameter),
|
||||
`Handle`/pools, `Result`/`try`, `handler-case`, `loop`/`recur` and tail calls, user allocators, structural typing —
|
||||
**none has a customer in that code**. (`Handle` and the pool were built anyway, and on the other reason: they are the
|
||||
gate on classes. The finding stands and is why they were built small — see [`docs/BUILT.md`](docs/BUILT.md). `loop`/`recur` was
|
||||
gate on classes. The finding stands and is why they were built small — see [`BUILT.md`](BUILT.md). `loop`/`recur` was
|
||||
built too, and the finding stands there as well: what it is not is **tail calls**, which are still not built and still
|
||||
have no customer.) And **generics is not the blocker** there either: the element-changing maps are
|
||||
five-line load-time loops. That last one hangs on a design decision the report states flatly — whether the game's
|
||||
@ -940,9 +940,9 @@ byte-identical between 5.5 and the vendored 6.0.
|
||||
|
||||
Both lanes committed their main work and died on trailing polish; both are merged and the suite is green.
|
||||
|
||||
- ~~**`pause` marking from Emacs was not built.**~~ **Built**, both halves. See `docs/BUILT.md`, "A breakpoint is a
|
||||
- ~~**`pause` marking from Emacs was not built.**~~ **Built**, both halves. See `BUILT.md`, "A breakpoint is a
|
||||
function call, and the editor only says where".
|
||||
- ~~**Ghost text** for the watch window — values shown inline at the code they belong to — is noted and not designed.~~ **Built.** See `docs/BUILT.md`, "Ghost text finds its anchor in the buffer, not in the table".
|
||||
- ~~**Ghost text** for the watch window — values shown inline at the code they belong to — is noted and not designed.~~ **Built.** See `BUILT.md`, "Ghost text finds its anchor in the buffer, not in the table".
|
||||
- `tools/unit-return.py` is re-runnable; run it over any `.flan` file a lane wrote before the conversion landed.
|
||||
|
||||
# Where this is
|
||||
@ -951,11 +951,11 @@ Both lanes committed their main work and died on trailing polish; both are merge
|
||||
|
||||
**Branch `dev-loop`, 199 commits, working tree clean, `dune test` green.**
|
||||
|
||||
**[`docs/DISCUSS.md`](docs/DISCUSS.md) is what has been *asked* and not answered** — open questions with the repo context that
|
||||
**[`DISCUSS.md`](DISCUSS.md) is what has been *asked* and not answered** — open questions with the repo context that
|
||||
bears on each, so an investigation starts from what exists. Nothing in it is a decision or a task; when one becomes
|
||||
either, it moves here.
|
||||
|
||||
**`NEXT.md` is what is left. [`docs/BUILT.md`](docs/BUILT.md) is why the existing parts are the shape they are** — the reload
|
||||
**`NEXT.md` is what is left. [`BUILT.md`](BUILT.md) is why the existing parts are the shape they are** — the reload
|
||||
primitive, cells, the agent, the session, the daemon, the Emacs client, conditions, the FFI shim, the layout, and the
|
||||
order it all got built in. This file was half build log until it was split; do not let it become one again. When a
|
||||
track here finishes, its explanation moves there and its entry here goes away.
|
||||
@ -994,7 +994,7 @@ The expander is written and the exit criterion plan.org set for milestone 5 is m
|
||||
unchanged. Running `test/programs/macro-unless.flan` means the compiler built a shared object, `dlopen`ed it into
|
||||
itself and called a Flan function to find out what `(unless c a b)` means.
|
||||
|
||||
The full explanation is in [`docs/BUILT.md`](docs/BUILT.md), "Macros: the compiler dlopens the program". Four things worth
|
||||
The full explanation is in [`BUILT.md`](BUILT.md), "Macros: the compiler dlopens the program". Four things worth
|
||||
knowing before touching any of it, because each cost something to find:
|
||||
|
||||
- **A call inside a quasiquote is output, not a compile-order dependency.** A macro body that *calls* another macro
|
||||
@ -1017,10 +1017,10 @@ difference being a cached `.so`; and a hello-world carries eight bytes of it, be
|
||||
|
||||
### Landed — a C header is read, so a binding is checked instead of trusted
|
||||
|
||||
`lib/cimport.ml`, `lib/cjson.ml`, a `headers` file beside `link`. Full reasoning in `docs/BUILT.md`, "The header is read
|
||||
now"; docs/DISCUSS.md item 6 is rewritten down to the two decisions left, both the author's.
|
||||
`lib/cimport.ml`, `lib/cjson.ml`, a `headers` file beside `link`. Full reasoning in `BUILT.md`, "The header is read
|
||||
now"; DISCUSS.md item 6 is rewritten down to the two decisions left, both the author's.
|
||||
|
||||
The gap closed is the one `docs/BUILT.md` recorded as *trusted*: `declare-c` generates the wrapper, the typedefs and the
|
||||
The gap closed is the one `BUILT.md` recorded as *trusted*: `declare-c` generates the wrapper, the typedefs and the
|
||||
prototype from one declaration, so they agree with each other by construction and only the library could disagree —
|
||||
and nothing had a second opinion to disagree with. Now clang is asked for a JSON AST dump of the header (shelled out,
|
||||
never libclang — the dependency plan.org rejected; Zig has since left it too, for Aro) and both halves are compared
|
||||
@ -1030,14 +1030,14 @@ against it.
|
||||
all 172 hand-written `declare-c` agree exactly.** Against the 5.1-dev header also installed on this machine, ten real
|
||||
differences — nine functions that version lacks and one that gained a parameter — so picking the wrong header is
|
||||
loud. Both comparisons run at build time and stop the build; verified by permuting `Texture2D` and by putting `f64`
|
||||
where raylib says `float`, which is the hazard `docs/BUILT.md` names and says only a test can catch.
|
||||
where raylib says `float`, which is the hazard `BUILT.md` names and says only a test can catch.
|
||||
|
||||
**Costs, measured, because they decide the remaining question.** Release build +4ms warm — `Reach.link` already drops
|
||||
a wrapper nothing reachable calls, confirmed on the wasm32 case it exists for with 256 extra declarations in play.
|
||||
Redefinition 31.0ms → 46.5ms. Dev build +333ms cold, once per session, since `Build.shared` compiles no C. Reading the
|
||||
header is cached (64ms → 17ms), keyed like the object cache; the cache was built against a measurement, not a guess.
|
||||
|
||||
**Re-measured, and the 15.5ms was misattributed** — see docs/BUILT.md, "Where that 15.5ms actually is". A `C-c C-c` reads
|
||||
**Re-measured, and the 15.5ms was misattributed** — see BUILT.md, "Where that 15.5ms actually is". A `C-c C-c` reads
|
||||
no header: `Session.eval` puts the forms through `Load`, and forms with no `(import …)` in them touch no package. The
|
||||
15.5ms is `flan reload`'s, and `flan reload` is a fresh process — ~14.5ms of it is session startup and ~4ms of *that*
|
||||
is the header. What a redefinition really pays for an imported package is **+3.6ms per eval** in `Check` and in
|
||||
@ -1057,7 +1057,7 @@ Worth knowing before touching it:
|
||||
172 are already bound by hand and left alone. Widening the binding is a `defstruct`, not a list edit.
|
||||
- **No `defstruct` is generated, and that is load-bearing.** Generate them and the header becomes the authority on
|
||||
layout, and checking the package's layouts against it would be comparing the header with itself — which is exactly
|
||||
why `docs/BUILT.md` rejected a `_Static_assert` as circular. Keeping them hand-written is what makes the check a second
|
||||
why `BUILT.md` rejected a `_Static_assert` as circular. Keeping them hand-written is what makes the check a second
|
||||
source.
|
||||
- **A refusal is a demotion, not a drop** — Zig's `failDecl`, which `Load.refuse_hidden` already implemented for
|
||||
`main`. `rl/get-gamepad-name` is a name that exists, cannot be had, and says why at the use site.
|
||||
@ -1066,7 +1066,7 @@ Worth knowing before touching it:
|
||||
- **`test/headers/sample.h`** is the importer's table — one function per decision, committed, no raylib needed. The
|
||||
raylib acceptance case skips without `FLAN_RAYLIB_H`; that one does not.
|
||||
|
||||
Two things that are *not* done, and are 6a and 6b in docs/DISCUSS.md: whether the header stays a build-time read or becomes
|
||||
Two things that are *not* done, and are 6a and 6b in DISCUSS.md: whether the header stays a build-time read or becomes
|
||||
a committed generator (`flan import-c` already prints the lines, so it costs nothing more to switch), and whether the
|
||||
172 hand-written lines migrate. Neither is blocked on correctness. The argument for the first is weaker than it
|
||||
looked — committing the generated lines would save ~4ms of session startup and none of the +3.6ms per redefinition,
|
||||
@ -1081,7 +1081,7 @@ sibling of a call that worked did not — by excluding them and hand-writing the
|
||||
|
||||
### Landed 2026-09-12 — six tracks, one session
|
||||
|
||||
Six agents in parallel worktrees. Kept short on purpose; the reasoning that outlives the change is in `docs/BUILT.md` or in
|
||||
Six agents in parallel worktrees. Kept short on purpose; the reasoning that outlives the change is in `BUILT.md` or in
|
||||
the commit that made it.
|
||||
|
||||
1. **`nth` removed**, an alias of `at` that was asymmetric — `check.ml` aliased them but `parse.ml` and
|
||||
@ -1140,7 +1140,7 @@ is written down rather than left out; see the unsettled list under the build ord
|
||||
The critical path, and the thing NEXT.md said was the only one standing between this and writing a game. Steps 1, 2 and
|
||||
3 of the build order below are struck; `Map` is step 4 and is untouched. `Allocator` is a builtin opaque type and
|
||||
needed nothing from milestone 5, which was the whole bet. Three amendments to a **frozen** `spec-memory.md`, made
|
||||
deliberately and stated as amendments in [`docs/BUILT.md`](docs/BUILT.md): `free-all` is retain-capacity with `arena-destroy`
|
||||
deliberately and stated as amendments in [`BUILT.md`](BUILT.md): `free-all` is retain-capacity with `arena-destroy`
|
||||
beside it; `context/allocator` is a dynamic variable rather than a literal calling-convention parameter; and the `Vec`
|
||||
header is six words in every build rather than four in release. One addition the spec does not have: a budget on the
|
||||
allocator, because `retry` needs a handler that can make the *same* request succeed.
|
||||
@ -1150,7 +1150,7 @@ allocator, because `retry` needs a handler that can make the *same* request succ
|
||||
`--sanitize` is a build flag beside `--debug`; `dune build --root . @sanitize` builds twenty-eight programs twice, plain
|
||||
and sanitized, and compares output and exit status. Its own alias and not `dune test`, because the sweep is about nine
|
||||
minutes. The checked sweep is **clean**. How ASan and UBSan reach a language whose IR is written by hand, and why the
|
||||
flag does not force `-O0` when `--debug` does, is in [`docs/BUILT.md`](docs/BUILT.md).
|
||||
flag does not force `-O0` when `--debug` does, is in [`BUILT.md`](BUILT.md).
|
||||
|
||||
Two defects came out of it, both found by reading rather than by the tools, both fixed with a regression case:
|
||||
`flan_bytes_to_i64`/`flan_bytes_to_f64` clamped a slice length with `(size_t)n` and so read 63 or 511 bytes off the end
|
||||
@ -1182,7 +1182,7 @@ it *would* have written.
|
||||
control that must report: index 3 of a `Vec` with len 2 and cap 4, with `--track-origins` naming the
|
||||
`aligned_alloc` in `flan_vec_push`. Two more controls pin a heap overrun and a padded `Map` key. `test/valgrind.supp`
|
||||
holds **no suppressions** — nothing false came up to suppress. Details, and the measured fact that memcheck catches
|
||||
0 of `bounds.flan`'s 6 cases where ASan catches 3, in [`docs/BUILT.md`](docs/BUILT.md).
|
||||
0 of `bounds.flan`'s 6 cases where ASan catches 3, in [`BUILT.md`](BUILT.md).
|
||||
|
||||
**The arena half of the hole is closed; the interior half cannot be.** `free-all` is retain-capacity, so the pages
|
||||
stay and `malloc` never learns the storage died — but memcheck can be told directly, and now is: `flan_arena_proc`
|
||||
@ -1192,7 +1192,7 @@ it *would* have written.
|
||||
control. Cost outside valgrind is five no-op instructions plus the request block — 23 on the `free-all` path only,
|
||||
about 1ns per reset — and the macro is vendored rather than `#include`d because the machine that runs the sweep
|
||||
has `valgrind` and not `valgrind-devel`, so a guarded include would silently compile to nothing exactly here. See
|
||||
[`docs/BUILT.md`](docs/BUILT.md).
|
||||
[`BUILT.md`](BUILT.md).
|
||||
|
||||
**Interior overruns remain invisible**, for a structural reason no client request fixes — an arena is one `malloc`,
|
||||
and the `Map`'s `keys | values | hashes | scratch` is one allocation too, so "probe overrun at high load" is not
|
||||
@ -1253,7 +1253,7 @@ Either way it is an amendment to a **frozen** `spec-conditions.md`, not a gap in
|
||||
program, at that program's next frame boundary. Verified against sand: an unsaved buffer edit to `game-draw`, and 240
|
||||
consecutive frames drew it.
|
||||
|
||||
Steps 1, 2 and 3 are done — see *The reload primitive* in `docs/BUILT.md`. A list of top-level forms can be recompiled and installed
|
||||
Steps 1, 2 and 3 are done — see *The reload primitive* in `BUILT.md`. A list of top-level forms can be recompiled and installed
|
||||
into a running process; call sites compiled before they existed follow them, and a `defn` or `defvar` the process was
|
||||
never built with can be added and then redefined again. That is the whole of `C-c C-c`, minus an editor: sand.flan takes
|
||||
a redefinition over a socket and installs it between frames.
|
||||
@ -1263,7 +1263,7 @@ running process was built with, and speaks a protocol an editor can talk to.
|
||||
|
||||
Milestone 4 is done: **sand.flan builds, links raylib and runs**, and its simulation has a headless acceptance case that
|
||||
runs on the `dune test` path at `-O0` and `-O2`. Milestones 2 and 3 are behind it (`calc-me.flan` compiles and runs; the
|
||||
interpreter was dropped — open decision #7, settled — see "Why there is no interpreter" in `docs/BUILT.md`).
|
||||
interpreter was dropped — open decision #7, settled — see "Why there is no interpreter" in `BUILT.md`).
|
||||
|
||||
```
|
||||
reader ✅ → parse ✅ → load ✅ → check ✅ → emit ✅ → clang ✅
|
||||
@ -1322,7 +1322,7 @@ $ flan run sand.flan # a window, 120 fps, hold space
|
||||
Five questions were put and answered in one sitting. Each is a decision, not a preference — build against them, and
|
||||
reopen one only with a reason rather than a taste.
|
||||
|
||||
~~**1. Assets are embedded at compile time, one file or one directory.**~~ **Built** — `(embed "p")`, `(embed "p" string)`, `(embed-dir "d")`. See docs/BUILT.md, "Assets are baked in". Odin's answer, and the reason it is the right
|
||||
~~**1. Assets are embedded at compile time, one file or one directory.**~~ **Built** — `(embed "p")`, `(embed "p" string)`, `(embed-dir "d")`. See BUILT.md, "Assets are baked in". Odin's answer, and the reason it is the right
|
||||
one here: it is a *compiler* feature, so it needs no build flags, no linker arguments and no per-target packaging, and
|
||||
it works identically on desktop and web. That matters more here than it does for Odin, because `Load` gives link flags
|
||||
only to directory packages — the single file doing `(rl/load-texture "brush.png")` is structurally the one file with
|
||||
@ -1331,7 +1331,7 @@ no link channel, which is what stopped the web lane from inventing a flag. Embed
|
||||
`--preload-file` stays available later for assets that should load lazily rather than be baked in; the `@web` link
|
||||
line already carries it if wanted.
|
||||
|
||||
~~**2. Reading a file works everywhere; writing is desktop-only and signals on web.**~~ **Built** — `barf` on the web signals `FileError` with reason `file-unsupported`, and `test/test_web.ml` runs it under node rather than asserting the artifact's shape. See docs/BUILT.md, "slurp, barf, and the two ways they fail". Odin stubs its whole file API on
|
||||
~~**2. Reading a file works everywhere; writing is desktop-only and signals on web.**~~ **Built** — `barf` on the web signals `FileError` with reason `file-unsupported`, and `test/test_web.ml` runs it under node rather than asserting the artifact's shape. See BUILT.md, "slurp, barf, and the two ways they fail". Odin stubs its whole file API on
|
||||
js/wasm — every operation returns `.Unsupported`, and `core/os/file_js.odin`'s own comment says the stubs exist only
|
||||
so importing `core:os` "panics cleanly". Take the restriction and not the mechanism. **Flan has no conditional
|
||||
compilation** — nothing in `parse.ml` or `check.ml` reads the target — so "isolate this code to desktop" is not
|
||||
@ -1340,7 +1340,7 @@ it is how a save file disappears with nothing said. So `barf` on web signals a c
|
||||
program decides. This is the language having something Odin does not; use it. Per-package target isolation, if a
|
||||
whole desktop-only package is ever wanted, is the `@native`/`@wasi`/`@web` link-line tagging the web lane built.
|
||||
|
||||
**3. Build the shadow stack.** ~~Not yet built.~~ **Built**, both halves — see docs/BUILT.md. Kept here as the decision it
|
||||
**3. Build the shadow stack.** ~~Not yet built.~~ **Built**, both halves — see BUILT.md. Kept here as the decision it
|
||||
was, with the measurement it asked for: +33% on call-heavy code over globals for the frames, +61% with the slot table,
|
||||
and 0.06% of a 60fps frame.
|
||||
|
||||
@ -1371,7 +1371,7 @@ length is not known until the file is read and therefore cannot exist before an
|
||||
refusal names the new one. `:keys` kept its colon — it names no field, so leaving it alone is what lets the dot mean
|
||||
exactly one thing. 681 labels across 45 `.flan` files including `vendor/`, plus 94 more in the Flan embedded in
|
||||
`lib/prelude.ml` and the tests. `Map` is now free to take `{:key value}` without colliding with struct literals. See
|
||||
docs/BUILT.md, "The colon belongs to keys".
|
||||
BUILT.md, "The colon belongs to keys".
|
||||
|
||||
**What it left for the Emacs lane, both verified.** `render.ml` still *prints* a struct with colons, deliberately:
|
||||
`emacs/flan-inspect.el:165` parses that output and hard-codes the colon when it reads a field out, so the printer
|
||||
@ -1408,7 +1408,7 @@ reason there is no collector.
|
||||
|
||||
**Globals in the break buffer — built.** One section under the stack, holding the union of the globals every frame on
|
||||
the current stack references, each entry annotated with the frames that touch it and ordered by the innermost one. It
|
||||
is `(:op "globals")` in `dev.ml` and `flan-cnr--insert-globals` in the break buffer. See docs/BUILT.md, "Globals of a
|
||||
is `(:op "globals")` in `dev.ml` and `flan-cnr--insert-globals` in the break buffer. See BUILT.md, "Globals of a
|
||||
stopped stack". ~~The one hole left open — the redefinition check is a fingerprint over a body's *slots*, so a new body
|
||||
that names different globals while binding the same locals is not caught.~~ **Closed**: `Reach.ref_fingerprint` is a
|
||||
second fingerprint over the set of globals a body names, carried beside the slot one in `%fninfo` and checked the same
|
||||
@ -1428,7 +1428,7 @@ mid-edit in another buffer.
|
||||
that nothing starts on managed classes "until ordinary `struct`, `Handle`, and reload semantics are working". All
|
||||
three now hold: structs work fully; reload works with one known hole (a changed signature is refused rather than
|
||||
versioned); and `(Handle T)` and `(Pool T)` exist, with the enumeration primitive `migrate-instances` was blocked on.
|
||||
See [`docs/BUILT.md`](docs/BUILT.md), "`(Handle T)` and the pool, which is what a stale reference answers with".
|
||||
See [`BUILT.md`](BUILT.md), "`(Handle T)` and the pool, which is what a stale reference answers with".
|
||||
|
||||
It was not an incidental precondition. `migrate-instances` has to *enumerate* live instances, and a pool behind a
|
||||
generational `(Handle T)` gives that by construction while a world arena and an owned region do not. plan.org presents
|
||||
@ -1482,11 +1482,11 @@ acquisitions against releases at that boundary and report what is still held at
|
||||
annotation, nothing running at a distance — it does not change how code is written, it reports when something was
|
||||
forgotten.
|
||||
|
||||
## ~~Before the batch below: read `docs/DISCUSS.md`'s "NEXT SESSION STARTS HERE"~~ — **answered, and being unwound**
|
||||
## ~~Before the batch below: read `DISCUSS.md`'s "NEXT SESSION STARTS HERE"~~ — **answered, and being unwound**
|
||||
|
||||
The architectural question raised at the end of 2026-09-12 — **putting the compiler inside the running program's
|
||||
process** — was researched (`docs/DISCUSS.md` §14), then built: `flan dev` is one binary and one process, and the editor
|
||||
socket did not move. See "One process" in [`docs/BUILT.md`](docs/BUILT.md).
|
||||
process** — was researched (`DISCUSS.md` §14), then built: `flan dev` is one binary and one process, and the editor
|
||||
socket did not move. See "One process" in [`BUILT.md`](BUILT.md).
|
||||
|
||||
What it reopened is now being deleted one piece at a time, each with its own green run:
|
||||
|
||||
@ -1516,7 +1516,7 @@ whether an in-process JIT or a hand-written backend is needed at all.
|
||||
|
||||
## ~~Queued: a second tier of the standard library, after macros~~ — **landed**
|
||||
|
||||
See [`docs/BUILT.md`](docs/BUILT.md), "The prelude's second tier". The diagnosis here was right and the prelude had 44
|
||||
See [`BUILT.md`](BUILT.md), "The prelude's second tier". The diagnosis here was right and the prelude had 44
|
||||
allocation-free functions because there was nothing to allocate from; there are 24 more now, and the "Refused, by
|
||||
name" block at the foot of `prelude.ml` is down from eight entries to four, each with a *different* reason rather
|
||||
than the one shared sentence.
|
||||
@ -1534,14 +1534,14 @@ Tests: `programs/strings.flan`, `programs/format.flan`, `programs/algorithms.fla
|
||||
them wants a language decision.
|
||||
|
||||
- ~~**`Map` keys and values.**~~ **Iteration is built** — `flan_map_next` and the `map-next!` builtin, exactly the
|
||||
shape this described. See [`docs/BUILT.md`](docs/BUILT.md), "`map-next!`, the one thing a Map could not do".
|
||||
shape this described. See [`BUILT.md`](BUILT.md), "`map-next!`, the one thing a Map could not do".
|
||||
`map-keys`/`map-values` as *prelude functions* stay refused, and the reason is now generics rather than the
|
||||
iterator: a `defn` has to name its types and `(defn map-keys [m {K V}] (Vec K))` has no `K`. The loop is three
|
||||
lines at the call site, where `K` is known.
|
||||
|
||||
- ~~**`map`, `filter`, `reduce`, and a sort taking a comparator.**~~ **All four are in the prelude.** The diagnosis
|
||||
was right and is now evidenced: **function values, not generics** — they arrived with no generics at all. See
|
||||
[`docs/BUILT.md`](docs/BUILT.md), "Function values, with no capture". They are one copy per element type (i32 and f32), which
|
||||
[`BUILT.md`](BUILT.md), "Function values, with no capture". They are one copy per element type (i32 and f32), which
|
||||
is the half generics would remove, and a `map` that *changes* the element type is the one shape that did not come
|
||||
with them — one copy per ordered pair of types rather than per type.
|
||||
|
||||
@ -1563,7 +1563,7 @@ them wants a language decision.
|
||||
Two smaller findings, both written down beside the code that ran into them:
|
||||
|
||||
- ~~**The prelude is never macro-expanded.**~~ **Fixed, and the diagnosis above was wrong in both halves** — see
|
||||
[`docs/BUILT.md`](docs/BUILT.md), "A prelude function may call a prelude macro". The prelude *does* reach the expander; the
|
||||
[`BUILT.md`](BUILT.md), "A prelude function may call a prelude macro". The prelude *does* reach the expander; the
|
||||
arity error came from the `Check.program` *inside* `Macro.compile`, where expansion is off. It is a cycle and not
|
||||
an ordering — a macro module is compiled from the prelude — so moving the prepend would have changed nothing. What
|
||||
fixed it is `Macro.reduce`, which makes the prelude smaller for that one build, plus dropping the prelude's own
|
||||
@ -1642,7 +1642,7 @@ destination is always honest about what it is.
|
||||
|
||||
Drop Clojure's `:eduction` branch — that is the pass-around case, and the one part that would need runtime machinery.
|
||||
|
||||
**Done.** See *`into`, which fuses at compile time because it is a macro* in [`docs/BUILT.md`](docs/BUILT.md). It is a prelude
|
||||
**Done.** See *`into`, which fuses at compile time because it is a macro* in [`BUILT.md`](BUILT.md). It is a prelude
|
||||
`defmacro` over a plain `defn` that walks the transforms in reverse, and all four of the macro limits bit without
|
||||
blocking anything: the three refusals are names nothing defines, `into-wrap` uses only special forms so
|
||||
`Macro.reduce` does not drop it, the quasiquotes are all single-level, and `into` lives in the prelude because a
|
||||
@ -1664,7 +1664,7 @@ this, and it does not exist.
|
||||
## ~~Queued: `loop`/`recur` (the return type is done)~~ — **landed**
|
||||
|
||||
~~**1. A `defn` must always state its return type, and unit is written `()`.**~~ **Done.** See *The return type is
|
||||
the slot, and unit is `()`* in [`docs/BUILT.md`](docs/BUILT.md).
|
||||
the slot, and unit is `()`* in [`BUILT.md`](BUILT.md).
|
||||
|
||||
The slot after the parameters is unconditionally a type, the pre-pass that collected a file's type names is gone
|
||||
along with `is_type_form`, `qualified_type`, `types_in`, `declared_types` and `prelude_types`, and `Parse.decl` no
|
||||
@ -1690,10 +1690,10 @@ python3 tools/unit-return.py --in-html web/index.html
|
||||
|
||||
`-v` logs every `defn` it saw and what it decided; `--check` changes nothing. It is re-runnable, and on this tree it
|
||||
reports exactly six sites, all in `test_flan.ml`, which spell the refused forms *on purpose* so the refusals can be
|
||||
tested. Read the diff of every non-`.flan` file — docs/BUILT.md lists what the script can and cannot see.
|
||||
tested. Read the diff of every non-`.flan` file — BUILT.md lists what the script can and cannot see.
|
||||
|
||||
~~**2. `loop` and `recur`.**~~ **Done.** See *`loop` and `recur`, and why `recur` is better than tail calls and not
|
||||
only cheaper* in [`docs/BUILT.md`](docs/BUILT.md). `emit.ml` is untouched: a loop is a `let`, a `While` whose condition is
|
||||
only cheaper* in [`BUILT.md`](BUILT.md). `emit.ml` is untouched: a loop is a `let`, a `While` whose condition is
|
||||
`true`, and two jumps, and the barrier question `recur` asks is the one labelled `break` already answered.
|
||||
|
||||
Three things the plan did not anticipate, each written up there. **Tail position is a permission that is withdrawn**
|
||||
@ -1718,7 +1718,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
backtrace line never carried it, and `Dev.locals` compared slot counts and nothing else — four of the five
|
||||
hand-offs were never written, and printing both sides of the comparison could not have found it because there was
|
||||
no comparison. The mechanism was sound and stayed: it hashes slot *names* as well as types, so it does see a
|
||||
rename. See docs/BUILT.md, "Locals of a stopped frame".
|
||||
rename. See BUILT.md, "Locals of a stopped frame".
|
||||
|
||||
2. ~~**The colon-to-dot change.**~~ **Done**, and `Map` is unblocked: `{:key value}` is free. The sweep is
|
||||
`tools/colon-to-dot.py`, kept rather than thrown away, because the lanes that branched before it wrote Flan in the
|
||||
@ -1726,7 +1726,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
`--in-strings` for a `test/*.ml` that embeds Flan.
|
||||
|
||||
3. ~~**`Map`, and the `defer` relaxation.**~~ **Both done.** See *`(Map K V)`, which is Odin's map* and *`defer` may be
|
||||
written in a `let`* in [`docs/BUILT.md`](docs/BUILT.md). The `defer` relaxation **amends `spec-memory.md`**, which said
|
||||
written in a `let`* in [`BUILT.md`](BUILT.md). The `defer` relaxation **amends `spec-memory.md`**, which said
|
||||
`(defer (free v))` for a `let`-bound `v` was not expressible; it is now. `Map` restricts one thing the spec does not:
|
||||
a fixed array is a key only when its elements compare bytewise, so an array of structs or of strings is refused by
|
||||
name. The measured answer to "is this another Python dict" is six times quicker cache-resident and *slower* at a
|
||||
@ -1743,7 +1743,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
5. ~~**Union values**~~, then the macro expander, then `Result`/`try`. Promoted above `Handle` on the author's call —
|
||||
macros are the thing most worth wanting, and unions are the only thing between here and them.
|
||||
|
||||
**Union values are done.** See *Unions, and the tag they carry* in [`docs/BUILT.md`](docs/BUILT.md). The diagnosis was right:
|
||||
**Union values are done.** See *Unions, and the tag they carry* in [`BUILT.md`](BUILT.md). The diagnosis was right:
|
||||
`Option` is a two-case union wearing a special coat, so `Tast.arm`'s `acase` and `binds` already were union shape
|
||||
and `check_match` grew a second subject rather than a second path. A union is `Types.Named` exactly as a struct is,
|
||||
so every path that merely carries a type learned nothing.
|
||||
@ -1761,7 +1761,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
needs no import and no `load.ml` change. Verified by declaring one there and matching it from a program.
|
||||
|
||||
**Macros landed on top of this** and needed no `load.ml` change for `Form`, exactly as this said. See
|
||||
[`docs/BUILT.md`](docs/BUILT.md), "Macros: the compiler dlopens the program", and the short list of what is left of them
|
||||
[`BUILT.md`](BUILT.md), "Macros: the compiler dlopens the program", and the short list of what is left of them
|
||||
below.
|
||||
|
||||
Macros are what buy `with-drawing` and `with-mode-2d` over raylib's begin/end pairs, the hiccup DSL if a JS backend
|
||||
@ -1778,7 +1778,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
a C-shaped callback and somewhere to put a `flan_allocator`, neither of which is a type parameter.
|
||||
|
||||
6. ~~**`Handle` and the pool.**~~ **Built.** A reference to something that can die, that reports that it died rather
|
||||
than silently resolving to whatever reused the slot. See [`docs/BUILT.md`](docs/BUILT.md), "`(Handle T)` and the pool, which
|
||||
than silently resolving to whatever reused the slot. See [`BUILT.md`](BUILT.md), "`(Handle T)` and the pool, which
|
||||
is what a stale reference answers with". A handle is one `i64` — slot index low, generation high — so it copies,
|
||||
zeroes and compares like an integer and owns nothing; a live slot's generation is odd, which makes a zeroed handle
|
||||
resolve to nothing rather than to slot 0; and a generation that would wrap retires its slot instead, because "rare"
|
||||
@ -1800,7 +1800,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
is consumed, because there is no borrowing parameter — a pre-existing `Vec` gap, not a pool one.
|
||||
|
||||
7. ~~**`break` and `continue`, with loop labels.**~~ **Built.** Labels are Odin's in the head position, both blockers
|
||||
are answered, and the refusals name the construct they refuse for. See docs/BUILT.md, "`break` and `continue`, and the
|
||||
are answered, and the refusals name the construct they refuse for. See BUILT.md, "`break` and `continue`, and the
|
||||
rule that replaced a blanket refusal". What was settled in conversation before it was built, kept:
|
||||
|
||||
**Labels, Odin-style but in the head position.** A keyword names a loop and `break` takes it:
|
||||
@ -1822,7 +1822,7 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
hang.
|
||||
|
||||
8. ~~**Errors: a structured value with spans and notes, and more than one per compile.**~~ **Built.** See
|
||||
*An error is a value, and there is more than one of them* in [`docs/BUILT.md`](docs/BUILT.md). `Loc.Error` carries a
|
||||
*An error is a value, and there is more than one of them* in [`BUILT.md`](BUILT.md). `Loc.Error` carries a
|
||||
`diag` — a stable `kind`, a *span*, `notes` that each have their own span and severity, and the macro expansion
|
||||
the error came out of — and `flan check`/`flan build` print the source line with the offending span underlined,
|
||||
in the GNU format `compilation-mode` already parses. No editor work was needed and none was done.
|
||||
@ -1880,7 +1880,7 @@ debug tracking allocator, which is the leak safety net and a good candidate when
|
||||
|
||||
## Decided in discussion — the array constructor and the module system
|
||||
|
||||
**`(array 4 rl/Vector2)` makes a fixed array; `[4 T]` stays the type syntax. Built** — see docs/BUILT.md, "`(array 4 rl/Vector2)`, and the one position with no type slot". The problem it solved: a `let`
|
||||
**`(array 4 rl/Vector2)` makes a fixed array; `[4 T]` stays the type syntax. Built** — see BUILT.md, "`(array 4 rl/Vector2)`, and the one position with no type slot". The problem it solved: a `let`
|
||||
binding takes no type, so `(let [pts [4 rl/Vector2]] ...)` reads `[4 rl/Vector2]` as a two-element array *literal* and
|
||||
fails with *unknown name rl/Vector2*. It cost 32 hand-written `Vector2`s in one raylib example.
|
||||
|
||||
@ -1938,7 +1938,7 @@ package to *mark* a name private, and adding one means a parser change.
|
||||
|
||||
## Decided in discussion — three more, two now built
|
||||
|
||||
~~**A watch window, ported from the author's Clojure one.**~~ **Built.** See `docs/BUILT.md`, "The watch window, and why
|
||||
~~**A watch window, ported from the author's Clojure one.**~~ **Built.** See `BUILT.md`, "The watch window, and why
|
||||
it is the only listing that is pushed", and `emacs/MANUAL.md` under "Looking at values". Three of the original's
|
||||
decisions were kept unchanged — the program decides what is shown, the request is async, and the paint is
|
||||
`replace-buffer-contents` so point and scroll survive every tick.
|
||||
@ -1954,7 +1954,7 @@ could not have done at any price.
|
||||
|
||||
**What was not built, deliberately: the `(watch "hp" hp)` form.** Scalars work today through `declare-c` against four
|
||||
runtime entry points, which needs no compiler change at all. A struct or a slice needs a compile-time walk over its
|
||||
type — one arm in `check.ml` beside `print`, which `docs/BUILT.md` writes out in full — and that file is held by another
|
||||
type — one arm in `check.ml` beside `print`, which `BUILT.md` writes out in full — and that file is held by another
|
||||
lane, so it was left alone rather than reached into.
|
||||
|
||||
~~**Ghost text is gated on that same arm**, which is the finding worth keeping.~~ **Wrong, and ghost text is built.**
|
||||
@ -1965,10 +1965,10 @@ conclusion did not follow: the call site is in the *buffer*, and the name in the
|
||||
so the anchor is searched for rather than reported. Nothing new is asked of the daemon. The two open questions
|
||||
`flan-watch.el` recorded are answered rather than solved — overlays are replaced wholesale every repaint, so an edit
|
||||
has nothing to invalidate, and a watch in a loop shows the last value written exactly as the buffer does, because
|
||||
every better answer is the query UI this design exists to avoid. See `docs/BUILT.md`, "Ghost text finds its anchor in the
|
||||
every better answer is the query UI this design exists to avoid. See `BUILT.md`, "Ghost text finds its anchor in the
|
||||
buffer, not in the table".
|
||||
|
||||
~~**The inspector gets a second way to start: an address and a type.**~~ **Built.** See `docs/BUILT.md`, "Two ways to root
|
||||
~~**The inspector gets a second way to start: an address and a type.**~~ **Built.** See `BUILT.md`, "Two ways to root
|
||||
a walk, and why neither subsumes the other". It went in as a frame and a slot *index* rather than an address and a
|
||||
type — the daemon holds both and an index is the thing the listing can hand back, while an address is not something an
|
||||
editor should be holding. The one prediction that did not survive contact: `l` crossing between the two modes was
|
||||
@ -2042,11 +2042,11 @@ Ranked by how often they were hit, top two first because they are walls rather t
|
||||
list head — because `(Key n)` is a *value* now, and putting `Key` in `types` would make a body starting with one
|
||||
be eaten as a return type.
|
||||
3. ~~**`break` is not implemented.**~~ **Built, with `continue` and loop labels.** Both blockers are answered: the
|
||||
`in_frames` rule became a relative one rather than a blanket one, and `Tast.While` grew a latch. See docs/BUILT.md,
|
||||
`in_frames` rule became a relative one rather than a blanket one, and `Tast.While` grew a latch. See BUILT.md,
|
||||
"`break` and `continue`, and the rule that replaced a blanket refusal".
|
||||
4. ~~**A `let` binding takes no type annotation**~~ — still true, and **no longer the blocker it was**: `(array 4
|
||||
rl/Vector2)` is built and is the answer to the case that raised it. The reasoning below is kept because it is what
|
||||
chose between the three surfaces, and the first of them is not what was taken — see docs/BUILT.md, "`(array 4
|
||||
chose between the three surfaces, and the first of them is not what was taken — see BUILT.md, "`(array 4
|
||||
rl/Vector2)`, and the one position with no type slot". The original entry:
|
||||
|
||||
A fixed array is either a top-level `defvar` or a literal with
|
||||
@ -2089,11 +2089,11 @@ array with a struct element, a 2-D struct array, and `[N string]` as both `defco
|
||||
|
||||
### The web target: what it does not reach yet
|
||||
|
||||
`flan build --target=web` works, a raylib example builds unchanged and `test/test_web.ml` is green — see docs/BUILT.md,
|
||||
`flan build --target=web` works, a raylib example builds unchanged and `test/test_web.ml` is green — see BUILT.md,
|
||||
"The browser is the third target", for the mechanism and why asyncify rather than `emscripten_set_main_loop`. Four
|
||||
things it does not cover.
|
||||
|
||||
~~**1. `sand.flan` has no web build, and the cause is one missing `#include`.**~~ **Built. It opens.** See docs/BUILT.md,
|
||||
~~**1. `sand.flan` has no web build, and the cause is one missing `#include`.**~~ **Built. It opens.** See BUILT.md,
|
||||
"sand.flan in a browser", for the whole of it. Three summary lines, because the diagnosis below was right about the
|
||||
structure and wrong about the cause:
|
||||
|
||||
@ -2147,7 +2147,7 @@ The original text follows. `sand.flan` does `(rl/load-texture "brush.png")` agai
|
||||
**3. Nothing has been opened in a browser.** *Still true, and now it is the only thing left between here and
|
||||
"someone played with it".* `sand.flan` builds for the web, the module carries asyncify, raylib's GL imports and
|
||||
brush.png's own bytes whole, and `node sand.js` gets as far as `glfwInit` before dying on `window is not defined` —
|
||||
which proves the module is live and proves nothing about the canvas. docs/BUILT.md carries the exact commands to serve and
|
||||
which proves the module is live and proves nothing about the canvas. BUILT.md carries the exact commands to serve and
|
||||
open it, and the list of what only a human will discover: whether it paints, whether the audio round trip through
|
||||
MEMFS survives, and the canvas size. The `until` loop never exits on the web, so none of `main`'s `defer`s run —
|
||||
expected, and worth knowing before reading anything into it.
|
||||
@ -2167,7 +2167,7 @@ message.
|
||||
### `break`, and why it was not built — **it is built now**
|
||||
|
||||
Kept as written, because everything in it held and the two blockers at the end are the two things the build had to
|
||||
rule on. Both are ruled on in docs/BUILT.md, "`break` and `continue`, and the rule that replaced a blanket refusal":
|
||||
rule on. Both are ruled on in BUILT.md, "`break` and `continue`, and the rule that replaced a blanket refusal":
|
||||
the `in_frames` precedent was replaced by a barrier on the loop stack, which refuses a *crossing* rather than
|
||||
everything, and `Tast.While` grew the latch. The original note:
|
||||
|
||||
@ -2197,7 +2197,7 @@ plan.org's single line on it (831) names a `for` the language does not have and
|
||||
|
||||
1. ~~**Allocators, then `Vec` and `Map`.**~~ **Steps 1, 2 and 3 are done** — the allocator, the arena, `(Vec T)`,
|
||||
`StorageExhausted` and `retry`. `Map` is step 4 and is what is left of this item. See *Allocators, `(Vec T)` and
|
||||
`StorageExhausted`* in [`docs/BUILT.md`](docs/BUILT.md) for the shape, the three amendments to a frozen `spec-memory.md` and
|
||||
`StorageExhausted`* in [`BUILT.md`](BUILT.md) for the shape, the three amendments to a frozen `spec-memory.md` and
|
||||
the one addition. The claim below held: **`Vec` does not need generics** — that was wrong and is worth un-learning: Odin's containers are compiler
|
||||
builtins over a *type-erased* runtime (`base/runtime/dynamic_array_internal.odin`), where `$T` appears only in thin
|
||||
wrappers producing `size_of`/`align_of` at the call site, and per-key hash and equality are compiler-emitted
|
||||
@ -2237,7 +2237,7 @@ plan.org's single line on it (831) names a `for` the language does not have and
|
||||
|
||||
**Steps 1, 2 and 3 are built; 4 to 8 are what is left.** The reasoning is kept because it is what the remaining steps
|
||||
rest on, and because the escape it describes was tested rather than assumed — see *Allocators, `(Vec T)` and
|
||||
`StorageExhausted`* in [`docs/BUILT.md`](docs/BUILT.md).
|
||||
`StorageExhausted`* in [`BUILT.md`](BUILT.md).
|
||||
|
||||
The dependency nobody had written down, and the reason it looked worse than it is. `spec-memory.md` defines an
|
||||
allocator as "a procedure plus an opaque data pointer" — a function value. `check.ml` refuses function values four
|
||||
@ -2280,7 +2280,7 @@ expander last, on 6's unions.
|
||||
~~1. **`Allocator` and the arena.**~~ **Done.** The builtin opaque type, the four operations with `size` and `align`,
|
||||
the capability set read off the allocator value, `with-allocator`, `context/allocator`, `context/temp` and the epoch
|
||||
counter. `free-all` was decided as retain-capacity with `arena-destroy` beside it; the context is a dynamic variable
|
||||
rather than a literal calling-convention parameter; both are stated as amendments in `docs/BUILT.md`. A user-written
|
||||
rather than a literal calling-convention parameter; both are stated as amendments in `BUILT.md`. A user-written
|
||||
allocator is refused by name with milestone 5 as the reason.
|
||||
|
||||
~~2. **`(Vec T)`**~~ **Done**, over the type-erased runtime, with `push`, `reserve`, `at`, `len`, `as-slice`, `free` and
|
||||
@ -2340,7 +2340,7 @@ expander last, on 6's unions.
|
||||
not the next thing to build.
|
||||
- ~~**The operation table may be one operation short.**~~ **Decided.** `free-all` is retain-capacity and
|
||||
`arena-destroy` hands the pages back — two names rather than the mode parameter, so the table the spec froze at four
|
||||
operations did not grow. `docs/BUILT.md` states it as the amendment it is.
|
||||
operations did not grow. `BUILT.md` states it as the amendment it is.
|
||||
- **The `Vec` header is six words in release too, and should not stay that way.** The 32-byte layout the spec fixes is
|
||||
blocked on one thing: a redefinition module is built by `llc` and `ld` against a host built separately, and nothing
|
||||
makes the two agree on a struct size. Give the reload path a way to carry the build flags and this falls out.
|
||||
@ -2372,7 +2372,7 @@ expander last, on 6's unions.
|
||||
- ~~`web/examples/breakdemo.out` is stale and `check.sh` fails on it.~~ **Fixed.** Commit `4a6a8fa` made the break
|
||||
banner number its restarts and the `.out` was never repinned. Nothing had to drive the socket in the end:
|
||||
`check.sh` already builds this one `--dev` and runs it under `timeout 5`, keeping what it printed before it
|
||||
stopped, so the repin was the `.out` plus the two prose copies of the banner — `web/index.html` and `docs/BUILT.md` —
|
||||
stopped, so the repin was the `.out` plus the two prose copies of the banner — `web/index.html` and `BUILT.md` —
|
||||
and a sentence on the page saying what the numbers are for, since a restart is taken by position.
|
||||
|
||||
- ~~A shadowed restart is offered and cannot be taken.~~ **Fixed.** A restart is taken by *position* now:
|
||||
@ -2483,7 +2483,7 @@ memcheck sweep (`@valgrind`), whose alarm is looser at 5400s because memcheck is
|
||||
now exists** — `Session.render_locals` is exactly that thunk, rooted at an address the program supplies — so what is
|
||||
left is the first: keep the pointer, and give the agent a verb that hands it back. The type is already known: it is
|
||||
the `condition_name` the break loop reports, which `layout` already resolves.
|
||||
- **The type identity is settled, and it is the qualified name** — `layout` is in, see docs/BUILT.md. `Load` qualifies
|
||||
- **The type identity is settled, and it is the qualified name** — `layout` is in, see BUILT.md. `Load` qualifies
|
||||
every declaration at import, so the names in `Tast.structs` are a flat namespace where two packages' `Missing` are
|
||||
`a/Missing` and `b/Missing`; a bare name is refused with the candidates rather than resolved. `condition` inherits
|
||||
it for free: the string the break loop already reports *is* that name, because `Emit.struct_name_of` writes
|
||||
@ -2491,7 +2491,7 @@ memcheck sweep (`@valgrind`), whose alarm is looser at 5400s because memcheck is
|
||||
reads is not qualified by anything.
|
||||
- ~~**`(:op "backtrace")` is blocked** on frame metadata.~~ **Built**, and not out of DWARF: decision 3's shadow
|
||||
stack carries the name and the location on the frame itself, so a backtrace needs no debug information at all. See
|
||||
docs/BUILT.md, "The shadow stack, and `backtrace`", for what it costs. **Locals landed with it** — the pointer-rooted
|
||||
BUILT.md, "The shadow stack, and `backtrace`", for what it costs. **Locals landed with it** — the pointer-rooted
|
||||
render thunk turned out to be `Render.render` over a `Deref` of a slot's address, and one new arm in the backend.
|
||||
See "Locals of a stopped frame" for the four things it refuses. Restart source locations and arity are still blocked — `flan_restart` carries `prev`, `name_id`, `name` and `namelen`, so both need a new
|
||||
field in the frame, which means the compiler emitting it.
|
||||
@ -2614,7 +2614,7 @@ left of the item is visibility, which is listed above and needs a marker the par
|
||||
## Macros — landed; what is left of them
|
||||
|
||||
**The expander works and `unless` is a prelude `defmacro`.** How all of it fits together is in
|
||||
[`docs/BUILT.md`](docs/BUILT.md), "Macros: the compiler dlopens the program" — the image format, the thunk ABI, why quasiquote
|
||||
[`BUILT.md`](BUILT.md), "Macros: the compiler dlopens the program" — the image format, the thunk ABI, why quasiquote
|
||||
runs before the walk, the two different ways expansion fails to terminate, `-linkall`, and the three cost numbers.
|
||||
What follows is only the part that is still missing.
|
||||
|
||||
@ -2684,7 +2684,7 @@ tests assert on the reason, not just on the failure.
|
||||
## Handoff: the shadow stack lane, stopped mid-repair
|
||||
|
||||
Two commits landed and are green: the shadow stack with `(:op "backtrace")`, and `(:op "locals" :frame N)`. See
|
||||
docs/BUILT.md's two new sections for the design and the measurements. A third commit was **half-built and its own test
|
||||
BUILT.md's two new sections for the design and the measurements. A third commit was **half-built and its own test
|
||||
left red** on purpose; **it is finished now** — see the struck item 1 above — and the rest of this section is kept
|
||||
because the parts of it that were true are still worth having.
|
||||
|
||||
@ -2698,7 +2698,7 @@ wrote.
|
||||
|
||||
**Not obvious from the diff.** Two things cost a day between them. The linked-list frame beat an array-with-a-stack-
|
||||
pointer on both benchmarks, which is the opposite of what the escaping-alloca argument predicts, and the measurement
|
||||
that first said otherwise was comparing a 40-frame binary with a 600-frame one; every number in docs/BUILT.md is now a
|
||||
that first said otherwise was comparing a 40-frame binary with a 600-frame one; every number in BUILT.md is now a
|
||||
minimum of nine runs for that reason. And `redefinition`'s transient rule (`m.nstr = 0`) silently stops every module
|
||||
carrying a string literal from ever being unloaded — the frame descriptors go through their own counter, `m.nfi`, for
|
||||
that reason, and a locals thunk passes `~retains:false` because everything it emits is memcpy'd into the result buffer.
|
||||
|
||||
@ -860,7 +860,7 @@ load a file out of `resources/` and neither file is in this tree.
|
||||
- `DotGothic16-Regular.ttf` is SIL OFL 1.1 and therefore redistributable, but it is 2 MB,
|
||||
and whether a compiler repository whose only binary asset is a 1 KB PNG should grow a 2 MB
|
||||
font is a call about the repository rather than about this port. Left to the author;
|
||||
`handoffs/HANDOFF-raylib-ports.md` carries it as the open question. The example looks for it under
|
||||
`HANDOFF-raylib-ports.md` carries it as the open question. The example looks for it under
|
||||
`examples/resources/`, says on screen when it is not there, and runs either way — raylib
|
||||
answers a missing path with the default font, whose glyphs are ASCII, so the kana draw as
|
||||
boxes and the program explains itself rather than looking broken.
|
||||
@ -214,8 +214,6 @@ Then in Emacs: `M-x flan-dev`, or `C-c C-z` to attach to the one you just starte
|
||||
- [`web/index.html`](web/index.html) — the language reference, including a table of
|
||||
everything that is *not* implemented and the exact words the compiler uses to
|
||||
refuse it
|
||||
- [`docs/`](docs/) — the reports and the history, with an index saying which of
|
||||
them are still true
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
@ -109,7 +109,7 @@ let sanitize_flag = "--sanitize"
|
||||
let two_process_flag = "--two-process"
|
||||
|
||||
(* The hand-written x86-64 backend (lib/x86.ml) instead of LLVM. The dev
|
||||
backend from docs/DISCUSS.md item 15, off by default and named explicitly:
|
||||
backend from DISCUSS.md item 15, off by default and named explicitly:
|
||||
LLVM stays the release path and the default one. It covers a subset of the
|
||||
IR and refuses the rest by name, so a build that succeeds is one it really
|
||||
compiled. *)
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
# What is in here, and what is still true
|
||||
|
||||
These are the documents that are written once and read occasionally: the reasons behind the code, the
|
||||
reports from finished investigations, and the record of individual work sessions. The documents that are
|
||||
edited every day stay at the repository root, because source comments cite them by bare filename from
|
||||
dozens of places and a path that moves is a path that rots. So `plan.org`, `NEXT.md`, `spec-memory.md`
|
||||
and `spec-conditions.md` are one directory up, and everything here points back at them.
|
||||
|
||||
A note on how to read the citations below: a document in this directory that says `plan.org` means the
|
||||
one at the repository root. Nothing in `docs/` is named for a file at the root, so there is no ambiguity,
|
||||
and rewriting a hundred prose mentions into `../plan.org` would have cost more in readability than it
|
||||
bought in precision.
|
||||
|
||||
## If you have thirty seconds
|
||||
|
||||
Read **`BUILT.md`**. It is by far the largest document here and it is the one that pays: it holds the
|
||||
reason behind every part of the compiler that exists, from why nothing is ever `dlclose`d to why the
|
||||
printer is a compile-time walk over a type rather than a runtime function. It is current, it is
|
||||
maintained, and deleting it would mean deriving all of it again. If you want to know why a thing is the
|
||||
shape it is, the answer is in here.
|
||||
|
||||
After that, **`../NEXT.md`** at the root for what is actually in flight, and **`REFERENCES.md`** here for
|
||||
where the evidence comes from — the reference clones on this machine, what each one answers, and the
|
||||
standing rule that a claim in these notes was read out of a clone rather than recalled.
|
||||
|
||||
## The current documents
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `BUILT.md` | Why the parts that exist are shaped the way they are. The longest and the most load-bearing document in the repository. **Current.** |
|
||||
| `DISCUSS.md` | Open questions, raised and deliberately not answered. Nothing in it is a decision or a task; entries that have since been answered say so and point at where the answer landed. **Current**, though it is half archive by now. |
|
||||
| `PORTING.md` | What the author's game, `siam-farmer`, needs from Flan that Flan does not have yet, measured against the code that exists rather than against a plan. It is the requirements document the language is actually steering by. **Current.** |
|
||||
| `REFERENCES.md` | The reference clones under `~/Repositories`, what each one is consulted for, and the specific files and lines already cited from them. **Current.** |
|
||||
|
||||
## The reports
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `SPIKE-GENERICS.md` | Milestone 5's parametric polymorphism, run early and out of order as a spike. The spike succeeded and generics landed, so this is the report of finished work rather than a live plan — but its findings about where the cost falls are still the reason the implementation looks the way it does. **Historical, findings still stand.** |
|
||||
| `overview.md` | The first brainstorm, from before the language had S-expressions. It says in its own first line that it is superseded and no longer accurate. Kept for history only; its table of what changed is the only part worth reading. **Superseded.** |
|
||||
|
||||
## `handoffs/`
|
||||
|
||||
One report per work session, each written by whoever held the branch at the time. They are the
|
||||
operational record: what was attempted, what was measured, what was decided without being able to ask,
|
||||
and what was left open for the next lane. They are all historical the moment they are written — a handoff
|
||||
describes a session that has ended — but they are cited by name from source comments and from `NEXT.md`,
|
||||
because the reasoning behind a guard or a calling convention is often only written down once and this is
|
||||
where it was written.
|
||||
|
||||
Six of them are the hand-written x86-64 backend, read in the order the work happened:
|
||||
`handoffs/HANDOFF-x86-rt.md` set the remaining-items list that the four after it close, `handoffs/HANDOFF-x86-redef.md`
|
||||
built the redefinition emitter, `handoffs/HANDOFF-x86-aggregates.md` took that across the struct boundary,
|
||||
`handoffs/HANDOFF-x86-guards.md` settled the two guards nothing reaches, `handoffs/HANDOFF-x86-debug.md` added debug
|
||||
information, and `handoffs/HANDOFF-x86-cost.md` measured what the backend costs and set the survey running on its
|
||||
own so a refusal cannot sit unnoticed again.
|
||||
|
||||
The other three sessions are unrelated to each other. `handoffs/HANDOFF-arith.md` is why a divide by zero is a condition
|
||||
rather than a `SIGFPE`. `handoffs/HANDOFF-raylib-ports.md` is the running record of the last two raylib example
|
||||
ports, whose lasting findings were folded into `PORTING.md`. `handoffs/HANDOFF-devtest-noise.md` is the linker
|
||||
error `dune test` used to print on every run. `handoffs/HANDOFF-tidy.md` is this reorganisation.
|
||||
@ -1,116 +0,0 @@
|
||||
# Tidying the repository root
|
||||
|
||||
The root had thirty-odd files in it, nine of them session reports sitting at the
|
||||
same level as the language specification, and a README is about to point a
|
||||
newcomer at that. This is the plan and, below it, what actually happened.
|
||||
|
||||
## The plan
|
||||
|
||||
1. `git mv` the nine `HANDOFF-*.md` into `docs/handoffs/`, and `../DISCUSS.md`,
|
||||
`../BUILT.md`, `../PORTING.md`, `../SPIKE-GENERICS.md`, `../REFERENCES.md` and
|
||||
`../overview.md` into `docs/`. These are written once and read occasionally.
|
||||
2. Leave `plan.org`, `spec-memory.md`, `spec-conditions.md`, `NEXT.md`, the
|
||||
`.flan` scratch programs and the build files where they are: they are cited
|
||||
by bare filename from source comments in dozens of places, and moving them
|
||||
would be a large rewrite for no gain.
|
||||
3. Rewrite every citation of a moved file, zone by zone — a flat substitution is
|
||||
wrong, because a citation inside `docs/` should stay bare while the same
|
||||
citation in `lib/` needs the `docs/` prefix.
|
||||
4. Verify by resolving, not by grepping: for every cited path in the repository,
|
||||
check it exists relative to the citing file or to the root.
|
||||
5. Write `docs/README.md` saying which of these documents are still true.
|
||||
|
||||
## What happened
|
||||
|
||||
**The moves.** Fifteen files, all with `git mv` so `git log --follow` still
|
||||
reaches their history. Nothing else moved.
|
||||
|
||||
**The citations.** 238 of them across 52 files, rewritten by zone. The zones
|
||||
matter and a repository-wide substitution would have corrupted two of them:
|
||||
|
||||
| Citing file lives in | `DISCUSS.md` and its siblings | `HANDOFF-*.md` |
|
||||
|---|---|---|
|
||||
| the root, `lib/`, `test/`, `emacs/`, `spike/`, `examples/`, `vendor/` | `docs/DISCUSS.md` | `docs/handoffs/HANDOFF-x.md` |
|
||||
| `docs/` | unchanged — they are siblings | `handoffs/HANDOFF-x.md` |
|
||||
| `docs/handoffs/` | `../DISCUSS.md` | unchanged — siblings again |
|
||||
|
||||
Most of the citations are prose inside a source comment rather than a link;
|
||||
there is not a single markdown link among the fifteen moved documents. So the
|
||||
rewrite is a rewrite of comments, and in `lib/x86.ml`, `lib/build.ml` and
|
||||
`test/test_reload.ml` — which another lane is editing at the same time — every
|
||||
touched line is a comment line.
|
||||
|
||||
**The verification.** Grep cannot tell you a rewrite is finished, because a grep
|
||||
for `DISCUSS.md` matches `docs/DISCUSS.md` too. So the check resolves instead:
|
||||
every path-shaped citation in the repository is looked up relative to the file
|
||||
that wrote it and relative to the root, and anything that resolves to neither is
|
||||
reported. 817 citations checked. The only failures are seven mentions of
|
||||
`HANDOFF-f1.md`, `HANDOFF-f2.md` and `HANDOFF-f3.md` in `../../NEXT.md`, which were
|
||||
already dangling before this work — the prose around each one says the file was
|
||||
deleted — and one false positive where the pattern caught `"$root/NEXT.md"` out
|
||||
of a shell variable.
|
||||
|
||||
### The judgement calls
|
||||
|
||||
**`MY-NOTES.org` and `conditions.org` stay at the root.** `MY-NOTES.org` is the
|
||||
author's scratch — questions to himself about the standard library, in the
|
||||
second person. `conditions.org` is a live cheatsheet, not a report, and it is
|
||||
the entry point to `conditions-play.flan` beside it. Neither is a document that
|
||||
belongs in a directory of finished reports, and personal files are best left
|
||||
where their owner put them.
|
||||
|
||||
**`conditions-play.flan` stays at the root.** The brief said to move it to
|
||||
`spike/` if nothing referenced it. Three things do: `conditions.org:5` tells the
|
||||
reader to run it, `NEXT.md:1306` lists it as the program to poke conditions
|
||||
with, and `.gitignore` has an anchored `/conditions-play` for the executable it
|
||||
builds to. It is also not shaped like the rest of `spike/`, which is four
|
||||
self-contained experiment directories with their own runners, not a home for a
|
||||
loose `.flan` file. It is a demonstration that belongs next to the cheatsheet
|
||||
that points at it.
|
||||
|
||||
**`.flan-dev.sock` is not in this worktree.** The stale one is in the author's
|
||||
main checkout, which a worktree-isolated lane cannot reach. The ignore pattern
|
||||
is added here so the next one never shows up in `git status`; the existing
|
||||
socket is the author's to remove.
|
||||
|
||||
**`.gitignore`.** Five entries at the bottom of the file — `.claude/`, `probe`,
|
||||
`probe.c`, `/forms.so`, `_hist/` — were uncommented stragglers in a file where
|
||||
everything else explains itself. They are grouped and explained now, and
|
||||
`.flan-dev.sock` is added. No pattern was removed or changed in meaning.
|
||||
|
||||
### Verification
|
||||
|
||||
- `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,
|
||||
0 NOX86 and 38 SKIP — the baseline, unchanged.
|
||||
|
||||
The resolver reports sixteen unresolved citations and all sixteen are expected.
|
||||
Seven are the deleted `HANDOFF-f1.md`, `HANDOFF-f2.md` and `HANDOFF-f3.md` in
|
||||
`../../NEXT.md`, which the prose around them already calls deleted. One is a
|
||||
false positive where the pattern caught `"$root/NEXT.md"` out of a shell
|
||||
variable in `web/examples/quotes.sh`. The other eight are in this file: the
|
||||
`HANDOFF-x.md` placeholders in the zone table above, and `DISCUSS.md` quoted
|
||||
bare to explain why a grep cannot verify this work. None of them is a path.
|
||||
|
||||
### The branch clutter — reported, not acted on
|
||||
|
||||
`git branch --list 'worktree-agent-*'` returns **113** branches. **111** are
|
||||
fully merged into `dev-loop`. **89** of them have a worktree currently
|
||||
registered under `.claude/worktrees/`, and deleting one of those would break the
|
||||
harness. The two unmerged branches are the two lanes running right now.
|
||||
|
||||
That leaves **24** branches that are both merged and have no live worktree, and
|
||||
those are the only ones safe to delete:
|
||||
|
||||
```sh
|
||||
comm -23 \
|
||||
<(git branch --merged dev-loop --format='%(refname:short)' --list 'worktree-agent-*' | sort) \
|
||||
<(git worktree list --porcelain | sed -n 's|^branch refs/heads/||p' | sort) \
|
||||
| xargs -r git branch -d
|
||||
```
|
||||
|
||||
`git branch -d` rather than `-D` on purpose: it refuses anything that turns out
|
||||
not to be merged after all. The 89 with live worktrees want
|
||||
`git worktree prune` and a decision about the directories first, which is the
|
||||
author's call and not a cleanup to run blind.
|
||||
@ -1,179 +0,0 @@
|
||||
# Handoff — the ABI marker symbol, so a crossed pair is refused at dlopen
|
||||
|
||||
Branch `dev-loop`, from `682cb74`. This closes the serious finding of `HANDOFF-x86-aggregates.md`: a
|
||||
redefinition module built by one backend, dlopened into a host built by the other, links and loads and then
|
||||
dies with SIGSEGV at the first call into a redefined function that takes or returns a struct. Nothing refused
|
||||
it, and `flan build game.flan --x86 --dev` followed by `flan reload game.flan changed.flan` builds exactly that
|
||||
pair.
|
||||
|
||||
**It is refused now, by the loader, before any of the new code runs, with a sentence that says why.** Both
|
||||
directions are in `dune test`.
|
||||
|
||||
## What was built
|
||||
|
||||
| file | what |
|
||||
|---|---|
|
||||
| `lib/x86.ml` | `abi_marker = "flan.abi.x86"`. `program` defines it in `.data`, in a dev build only; `redefinition` emits a `.quad` against it |
|
||||
| `lib/emit.ml` | `abi_marker = "flan.abi.llvm"`, the same two halves — a definition inside `program`'s existing `if dev` block, and an `external` plus a hidden pointer to it in `redefinition`'s |
|
||||
| `vendor/agent/flan_agent.c` | `abi_mismatch`, which turns a `flan.abi.` `dlopen` failure into a sentence; the reload handler uses it |
|
||||
| `test/reload_host.c` | the same function, duplicated rather than shared, and the same use at its `dlopen` |
|
||||
| `test/test_reload.ml` | `agg_cross`, run both ways, asserting a nonzero exit *and* the sentence *and* which marker was missing |
|
||||
|
||||
`lib/build.ml` was not touched. Its option-record guard is the other half of this answer and is unchanged; the
|
||||
comment there already says what it cannot catch, which is what the marker is for.
|
||||
|
||||
## The mechanism, and why it is a datum and not a call
|
||||
|
||||
A dev build defines a marker symbol naming the backend that built it. A redefinition module emits a
|
||||
pointer-sized datum holding the address of the marker it was itself built for.
|
||||
|
||||
That datum is the whole of it. A pointer in `.data` is a relocation the dynamic loader has to resolve while it
|
||||
maps the object, whatever it does about lazy binding of calls, so a host that does not define the marker fails
|
||||
the `dlopen` outright. A call into an absent function would do as well under `RTLD_NOW`, which is what both
|
||||
loaders in this repo pass, but the datum does not depend on that and costs eight bytes.
|
||||
|
||||
Both sides are gated on `dev`, for two reasons that happen to agree. A release build has no cells and nothing
|
||||
to load into one, so the marker would be dead weight; and `Build.executable` passes `-rdynamic` only for a dev
|
||||
build, so a release build could not export the symbol even if it emitted it. Gating also keeps a release
|
||||
build's output byte-for-byte what it was, which `x86.ml`'s `.Ldwtext` comment says the repo cares about.
|
||||
Measured on `test/programs/cleanup.flan`:
|
||||
|
||||
```
|
||||
x86 --dev : 00000000004280f0 D flan.abi.x86
|
||||
llvm --dev : 000000000041d3f0 B flan.abi.llvm
|
||||
x86 release : (nothing)
|
||||
```
|
||||
|
||||
Macro modules are unaffected and were checked: `Build.macro_module` calls `Emit.program` without `~dev`, so a
|
||||
macro module neither defines a marker nor requires one, and the OCaml-side `dlopen` in `lib/dynload_stubs.c` —
|
||||
which is the *only* other `dlopen` of a Flan-built object anywhere — never sees one. It therefore got no arm,
|
||||
deliberately.
|
||||
|
||||
`lib/dev.ml` got no arm either, and this is worth stating because it looks like an omission. The daemon builds
|
||||
the host itself and sends every module to the agent in the game process; it has no `dlopen` of a redefinition
|
||||
module of its own, and `--x86` has no spelling anywhere in `dev.ml` or `session.ml`, so a `flan dev` session
|
||||
builds host and modules both through LLVM and is matched by construction. The mismatch is only reachable
|
||||
through `flan build --x86 --dev` plus a separately built module, and that module is loaded by the agent, which
|
||||
is where the sentence lives.
|
||||
|
||||
## What the crossed pair does now
|
||||
|
||||
Verbatim, from the two new cases in `test/test_reload.ml`, captured from the host's stderr:
|
||||
|
||||
```
|
||||
flan: the module and this host were built by different backends: the module came from LLVM and needs
|
||||
flan.abi.llvm, which an --x86 host does not define. The two backends pass every struct differently. Rebuild
|
||||
the host without --x86.
|
||||
```
|
||||
|
||||
```
|
||||
flan: the module and this host were built by different backends: the module came from the x86 dev backend and
|
||||
needs flan.abi.x86, which this host does not define. The two backends pass every struct differently. Rebuild
|
||||
the host with --x86.
|
||||
```
|
||||
|
||||
Exit 1 in both cases, from the host refusing to install. Before this it was exit 139 — SIGSEGV, at a call site,
|
||||
after `a1` and `host 54063108` had already been printed.
|
||||
|
||||
The agent's wording is longer than the test host's, because its reader is a person in an editor rather than a
|
||||
test: it names the running program rather than "this host", says the pair would die at the first call into a
|
||||
redefined function taking or returning a struct, and — in the LLVM-module direction — says why the program is
|
||||
the half that has to move.
|
||||
|
||||
The agent's two sentences have no unit test of their own, so they were checked the one way that matters: that
|
||||
they are in a binary a user would actually run. `vendor/agent/flan_agent.c` reaches a program through the
|
||||
vendor package's C sources rather than through an embedded-as-a-string module like `Runtime_src`, so an edit to
|
||||
it lands without regenerating anything — but that had to be confirmed rather than assumed, because a repo that
|
||||
embeds one C file that way can embed another.
|
||||
|
||||
```
|
||||
flan build test/programs/agent.flan --dev -o ahost ; strings ahost | grep -c "built by different backends" → 2
|
||||
flan build test/programs/agent.flan --x86 --dev -o ahost ; strings ahost | grep -c "built by different backends" → 2
|
||||
```
|
||||
|
||||
Two, in both configurations: one sentence per direction.
|
||||
|
||||
Two things about the matching that are deliberate. It matches on the *marker's name*, not on `"undefined
|
||||
symbol"`, which is glibc's phrasing and glibc's to change. And `dlerror` is one-shot with a buffer the next
|
||||
`dl` call may clobber, so the pointer is taken once and used for both the test and the reply; the agent's
|
||||
existing code called it once and still does.
|
||||
|
||||
## Why it is in `dune test` now, when the segfault was not
|
||||
|
||||
The aggregate lane measured the crossed pair and deliberately left it out: it was undefined behaviour, what it
|
||||
printed was a property of whichever LLVM was installed, and a test pinning it would have been pinning the shape
|
||||
of a crash. That is no longer true. The refusal happens in the loader, at a fixed point, before a single
|
||||
instruction of the new body runs, so it is deterministic and is asserted the way the retyped-global and
|
||||
registry-overflow cases already were: on the exit status *and* on the message.
|
||||
|
||||
Both directions, because a marker only one backend emitted would refuse in one direction and say nothing in the
|
||||
other — and the direction with no test is the direction that quietly stops working.
|
||||
|
||||
The matched pairs are unchanged and still print the transcript the aggregate lane derived:
|
||||
|
||||
```
|
||||
a1 / host 54063108 / a1 / after1 54063108 / a2 / after2 104337044 / counter 12
|
||||
```
|
||||
|
||||
from `Emit.redefinition` + `Build.shared` and from `X86.redefinition` + `Build.shared_x86` alike.
|
||||
|
||||
And "a matched pair is unaffected" is checked through the agent as well as through `reload_host.c`, which is
|
||||
worth saying because the two are different code paths and only one of them is what a user meets. The daemon
|
||||
tests in `dune test` — `dev-globals`, `dev-repl`, `dev-watch`, `dev-pause`, `dev-loop` — start a real `flan
|
||||
dev` session, build a real host, and send real redefinition modules to the agent in it over the socket. Every
|
||||
one of those `dlopen`s now has to resolve `flan.abi.llvm`, and every one of those tests passes.
|
||||
|
||||
## `flan reload --x86` did not land, and should not have
|
||||
|
||||
The brief left this to judgement. It is item 3 of `HANDOFF-x86-redef.md`, not a flag.
|
||||
|
||||
`flan reload` does not build a module directly; it runs a `Session` over the program and asks
|
||||
`Session.eval` for one, and `Session.change` holds a single `ir : string` field filled by
|
||||
`Emit.redefinition ~consts`. Three things would have to move together:
|
||||
|
||||
- `Session.change` would need a backend-tagged payload rather than an `ir` string, and every one of the six
|
||||
`Emit.redefinition` call sites in `session.ml` would have to choose.
|
||||
- `X86.redefinition` raises `Unsupported` on `~consts`, on `~call`, and on any name the host was not built
|
||||
with. `Session.eval` passes `~consts` as a matter of course and the expression path passes `~call`. So the
|
||||
x86 path would refuse most of what a session legitimately sends, and the refusals would surface as "this
|
||||
works in LLVM and not in x86" rather than as anything a user could act on.
|
||||
- The daemon is the real caller of all of this. Giving `flan reload` a flag the daemon does not have would
|
||||
leave the two commands disagreeing about what a session can do.
|
||||
|
||||
So the deliverable here is the refusal, which is the part that matters: a silent segfault is the bug. The
|
||||
remedy the messages name is the one that actually exists today — rebuild the host to match the module — rather
|
||||
than a flag that does not. When item 3 does land, the marker is what makes the choice checkable rather than
|
||||
merely intended, and these two crossed tests are what will catch a half-done version of it.
|
||||
|
||||
## Baseline
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| `spike/x86/survey.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** |
|
||||
| `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
|
||||
backends now emit a symbol into every `--dev` build, and the survey's default is a release build on both sides,
|
||||
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
|
||||
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.
|
||||
|
||||
## What remains
|
||||
|
||||
- Item 3 of `HANDOFF-x86-redef.md`: `flan dev` and `flan reload` choosing host and module backend together.
|
||||
The marker is now the thing that makes that checkable.
|
||||
- A per-shape crossed measurement, still unmeasured and still depended on by nothing. It is *harder* to get
|
||||
now, not easier: the marker refuses the pair before any of the four `step` functions runs, so anyone who
|
||||
wants the answer has to build the crossed module with the marker suppressed on purpose.
|
||||
- Items 1, 2 and 5 of `HANDOFF-x86-redef.md`, untouched.
|
||||
- A seam worth knowing about before anyone renames a marker. The two strings live in OCaml — `X86.abi_marker`
|
||||
and `Emit.abi_marker` — and the two `abi_mismatch` functions match the same literals in C, with nothing
|
||||
linking the four. Rename one and the refusal still fires, because the symbol is still missing; it just stops
|
||||
being a sentence and reverts to the loader's bare "undefined symbol", which is the failure this lane was
|
||||
about. The crossed tests would catch it — they assert on the marker's name — so the seam is guarded, but it
|
||||
is a seam.
|
||||
@ -1,7 +1,7 @@
|
||||
# Flan in Emacs
|
||||
|
||||
A manual for the editor side. It assumes you know Emacs and nothing about how
|
||||
Flan's dev loop is built — if you want that, `docs/BUILT.md` has it.
|
||||
Flan's dev loop is built — if you want that, `BUILT.md` has it.
|
||||
|
||||
The short version: you start a program, you keep it running, and you change it
|
||||
while it runs. Everything below is a variation on that.
|
||||
|
||||
@ -1409,7 +1409,7 @@ START and END, when given, are the region it came from, flashed on success.
|
||||
PAUSE, when given, is (BEG . END): the bounds of the form inside CODE the
|
||||
program should stop at. Only BEG goes on the wire — the daemon matches it
|
||||
against the location the reader attached to that form — and END is what the
|
||||
mark is drawn over here. Nothing is inserted in the buffer; see docs/DISCUSS.md
|
||||
mark is drawn over here. Nothing is inserted in the buffer; see DISCUSS.md
|
||||
§9."
|
||||
(let ((reply
|
||||
(flan-dev--request
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
;;
|
||||
;; Scalars only, so far. A struct or a slice needs a compile-time walk over
|
||||
;; its type — a `(watch "hp" hp)' form in the checker — and that is a file this
|
||||
;; change does not own. See docs/BUILT.md.
|
||||
;; change does not own. See BUILT.md.
|
||||
|
||||
;;; Code:
|
||||
|
||||
@ -533,7 +533,7 @@ Tears down both consumers. `flan-watch--drop' is the way to stop one of them."
|
||||
;; Still genuinely blocked on the checker, and unchanged by any of this: a
|
||||
;; **struct or a slice**. Rendering one is a compile-time walk over its type,
|
||||
;; the runtime has scalar entry points only, and no amount of searching the
|
||||
;; buffer produces a value the table does not hold. See docs/BUILT.md.
|
||||
;; buffer produces a value the table does not hold. See BUILT.md.
|
||||
|
||||
(provide 'flan-watch)
|
||||
;;; flan-watch.el ends here
|
||||
|
||||
@ -811,7 +811,7 @@ is written instead — the real `message' call the real command makes."
|
||||
|
||||
;; ── Marking a form with (pause) ───────────────────────────────────────
|
||||
;;
|
||||
;; docs/DISCUSS.md §9: `C-u' before an evaluation marks a form so the program
|
||||
;; DISCUSS.md §9: `C-u' before an evaluation marks a form so the program
|
||||
;; stops when it runs, and the buffer is never edited — the position goes on
|
||||
;; the wire beside the code and the daemon splices the call in after parsing.
|
||||
;;
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
;;;; is the same struct going into the call that actually draws with it.
|
||||
;;;;
|
||||
;;;; The hundred buildings are a `defvar` of fixed arrays rather than a Vec:
|
||||
;;;; a global cannot hold a Vec (docs/PORTING.md §3) and does not need to here,
|
||||
;;;; a global cannot hold a Vec (PORTING.md §3) and does not need to here,
|
||||
;;;; because the count is a constant in the C too. `(array n T)` is the zeroed
|
||||
;;;; fixed array, and the C's `= { 0 }` is exactly that.
|
||||
;;;;
|
||||
;;;; get-random-value comes from the generated half of the bindings. It is on
|
||||
;;;; no frame's path — it runs once, before the loop — and its Flan face is
|
||||
;;;; the C one, so docs/PORTING.md §1's rule does not reach it.
|
||||
;;;; the C one, so PORTING.md §1's rule does not reach it.
|
||||
;;;;
|
||||
;;;; One deliberate difference from the C: `(- camera.rotation 1.0)` and the
|
||||
;;;; clamp after it are written with the prelude's `clamp` macro rather than
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
;;;;
|
||||
;;;; examples/core/core_scissor_test.c. Needed begin-scissor-mode and
|
||||
;;;; end-scissor-mode, now hand-written in vendor/raylib/raylib.flan: they are
|
||||
;;;; a begin/end pair inside a frame, which is the class docs/PORTING.md §1 says
|
||||
;;;; a begin/end pair inside a frame, which is the class PORTING.md §1 says
|
||||
;;;; belongs in the hand-written half and not in the importer's. get-mouse-x
|
||||
;;;; and get-mouse-y come from the generated half — their Flan face is the C
|
||||
;;;; one, two plain i32s, and there is nothing for a hand-written line to say
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
;;;; window-state?, set-window-state, clear-window-state, toggle-fullscreen,
|
||||
;;;; minimize-window, maximize-window and restore-window all come from the
|
||||
;;;; generated half. That is the line drawn in vendor/raylib/bindings and it
|
||||
;;;; is worth saying why it falls here: the rule in docs/PORTING.md §1 exists so
|
||||
;;;; is worth saying why it falls here: the rule in PORTING.md §1 exists so
|
||||
;;;; that a build with no header set can still draw, and generated.flan is
|
||||
;;;; committed, so it can. What a hand-written line adds on top of that is a
|
||||
;;;; Flan face the C signature does not have — a Key instead of an int, a
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
;;;; Hand-written in raylib.flan rather than generated, and why each:
|
||||
;;;;
|
||||
;;;; begin-mode-3d / end-mode-3d a begin/end pair inside a frame, the
|
||||
;;;; class docs/PORTING.md §1 names, and taken
|
||||
;;;; class PORTING.md §1 names, and taken
|
||||
;;;; together the way begin-mode-2d is
|
||||
;;;; draw-cube / draw-cube-wires / draw-grid drawn every frame
|
||||
;;;; update-camera the Flan face differs: (Ptr Camera3D),
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
;;;; zeroed global standing in for the null pointer that means "the default
|
||||
;;;; ASCII set" — written for exactly this call, before anything called it.
|
||||
;;;;
|
||||
;;;; **Two things Flan would not do, both written up in docs/PORTING.md.**
|
||||
;;;; **Two things Flan would not do, both written up in PORTING.md.**
|
||||
;;;;
|
||||
;;;; 1. `GetCodepointPrevious` cannot be called at all. It reads BACKWARDS
|
||||
;;;; from the pointer it is handed, and a Flan `string` crosses to C as a
|
||||
@ -176,7 +176,7 @@
|
||||
;; Whether the TTF was there, asked once. A `defvar` and not the call itself
|
||||
;; in the draw loop: path-file? is a stat, and a syscall per frame to answer a
|
||||
;; question whose answer cannot change while the program runs is exactly what
|
||||
;; the per-frame rule in docs/PORTING.md is about.
|
||||
;; the per-frame rule in PORTING.md is about.
|
||||
(defvar font-present bool)
|
||||
(defvar show-font-atlas bool)
|
||||
(defvar cursor i32)
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
;;;; u8]` globals here. The dimensions are compile-time constants in the C
|
||||
;;;; too (the #defines and the two assignments right after), so nothing is
|
||||
;;;; lost by fixing them, and a global cannot hold a Vec in any case
|
||||
;;;; (docs/PORTING.md §3). 375 is 25*15, written out because Flan's array length
|
||||
;;;; (PORTING.md §3). 375 is 25*15, written out because Flan's array length
|
||||
;;;; must be a literal.
|
||||
|
||||
(import rl "vendor:raylib")
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
;;;; and because the picture is a demonstration of the filter rather than the
|
||||
;;;; point of the program.
|
||||
;;;;
|
||||
;;;; One gap, written up in docs/PORTING.md and worked around in `reload-texture`
|
||||
;;;; One gap, written up in PORTING.md and worked around in `reload-texture`
|
||||
;;;; below: LoadImageColors answers a (Ptr Color) and UpdateTexture takes a
|
||||
;;;; (Ptr u8), because the header spells its parameter `const void *` and Flan
|
||||
;;;; has no cast between pointer types. The address of the first field of the
|
||||
@ -143,7 +143,7 @@
|
||||
;; takes a (Ptr u8), the header having spelled that parameter `const void *`;
|
||||
;; and there is no cast between pointer types in Flan. What there is, is the
|
||||
;; address of the red channel of pixel zero, which is the address of the
|
||||
;; buffer said the long way round. See docs/PORTING.md.
|
||||
;; buffer said the long way round. See PORTING.md.
|
||||
(defn reload-texture [] ()
|
||||
(rl/unload-image im-copy)
|
||||
(set im-copy (rl/image-copy im-origin))
|
||||
|
||||
@ -189,7 +189,7 @@ let declared_name (d : decl) =
|
||||
|
||||
(* ── Instrumenting a form with (pause) ─────────────────────────────── *)
|
||||
|
||||
(* [C-u C-c C-c] marks a form so the program stops when it runs — docs/DISCUSS.md
|
||||
(* [C-u C-c C-c] marks a form so the program stops when it runs — DISCUSS.md
|
||||
§9. The mark travels beside the source as a position and is applied *here*,
|
||||
to the AST, rather than being spliced into the text the editor sends: text
|
||||
would shift every line and column after the insertion, and the error
|
||||
|
||||
@ -388,7 +388,7 @@ let wasm_main_source =
|
||||
(* ── The browser: the main loop, the shell, and the link ─────────────
|
||||
The mechanism, and why it is not the one the old note predicted.
|
||||
|
||||
docs/BUILT.md says a web build "drives the loop with [emscripten_set_main_loop]
|
||||
BUILT.md says a web build "drives the loop with [emscripten_set_main_loop]
|
||||
instead of a [while]. That is a different [main], not a different program."
|
||||
The first half is right about the browser and wrong about what it costs
|
||||
here: [emscripten_set_main_loop] wants the loop body as a callback, so every
|
||||
@ -916,7 +916,7 @@ let shared ?(opts = default) ~ir ~out () : timing =
|
||||
host. [flan reload] is precisely that caller. The complete answer is a
|
||||
marker symbol the host defines and a module references, so the loader
|
||||
refuses the pair at dlopen rather than the processor refusing it at a
|
||||
call. See docs/handoffs/HANDOFF-x86-aggregates.md. *)
|
||||
call. See HANDOFF-x86-aggregates.md. *)
|
||||
if opts.x86 then
|
||||
failwith
|
||||
"--x86: Build.shared is the LLVM redefinition path, and an --x86 host \
|
||||
|
||||
@ -1742,7 +1742,7 @@ and var ctx loc ~want name =
|
||||
(* spec-memory.md puts the allocator in the calling convention as
|
||||
[context/allocator] and [context/temp]. They read as names rather than
|
||||
calls because that is how the spec writes them, and they are dynamic
|
||||
variables at run time rather than extra parameters — see docs/BUILT.md for why
|
||||
variables at run time rather than extra parameters — see BUILT.md for why
|
||||
the literal reading of "calling convention" is deferred. *)
|
||||
| "context/allocator" ->
|
||||
expect loc ~want
|
||||
@ -3493,7 +3493,7 @@ and named_call ctx ~want loc name args =
|
||||
expect loc ~want
|
||||
(mk loc Types.Alloc (Tast.Prim (Tast.Rt "flan_arena_new", [ cap ])))
|
||||
(* Hands the pages back, which [free-all] deliberately does not — see
|
||||
docs/BUILT.md, "free-all is retain-capacity". *)
|
||||
BUILT.md, "free-all is retain-capacity". *)
|
||||
| "arena-destroy" ->
|
||||
arity loc name 1 args;
|
||||
let a = check ctx ~want:Types.Alloc (List.hd args) in
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
[declare-c] closed half the gap: the wrapper, the typedefs and the
|
||||
prototype are generated, so they cannot disagree with each other. The half
|
||||
it left open is the one docs/BUILT.md records as *trusted* — that the signature
|
||||
it left open is the one BUILT.md records as *trusted* — that the signature
|
||||
somebody typed is the function's real signature. Nothing checked it,
|
||||
because no header was ever read. This reads one.
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
package's [defstruct]s against the header's records — would be comparing
|
||||
the header with itself. Keeping the layouts hand-written is what makes
|
||||
[check_structs] an independent second source, and that check is the
|
||||
cheapest real closure of docs/BUILT.md's trusted-not-guaranteed gap: a
|
||||
cheapest real closure of BUILT.md's trusted-not-guaranteed gap: a
|
||||
[_Static_assert] was rejected there as circular for exactly this reason,
|
||||
and this is not circular, because the two sides have different authors.
|
||||
|
||||
@ -810,7 +810,7 @@ let of_dump ~env ~taken ~bound_syms ~config (d : dump) : imported =
|
||||
|
||||
(* The point of reading a header that the generator does not otherwise need.
|
||||
|
||||
docs/BUILT.md rejected a [_Static_assert] on [sizeof]/[offsetof] as circular:
|
||||
BUILT.md rejected a [_Static_assert] on [sizeof]/[offsetof] as circular:
|
||||
both sides would have come from the same field list. This is not circular.
|
||||
The [defstruct] was written by hand and the record comes from the library's
|
||||
own header, so a disagreement is real information — and it is the failure
|
||||
@ -868,7 +868,7 @@ let check_structs ~env ~(structs : (string * Ast.field list) list) (d : dump) =
|
||||
| None -> None)
|
||||
in
|
||||
(* Names and widths both. Order is what a permuted [defstruct] gets wrong and
|
||||
what docs/BUILT.md says only a test can catch; width is the other half of the
|
||||
what BUILT.md says only a test can catch; width is the other half of the
|
||||
same hazard and the one it calls out by name — [f64] where the library
|
||||
says [float] lays out eight bytes where there are four, and every field
|
||||
after it moves. Comparing the rendered Flan type rather than the C
|
||||
@ -1268,7 +1268,7 @@ let header ~loc ~header:h ~flags ~known_structs ~known_enums ~taken ~bound_syms
|
||||
|
||||
(* ── Printing a declaration back as source ─────────────────────────── *)
|
||||
|
||||
(* Which makes the third option in docs/DISCUSS.md item 6 available at no extra
|
||||
(* Which makes the third option in DISCUSS.md item 6 available at no extra
|
||||
cost: generate the declarations from the header, *commit the result*, and
|
||||
regenerate when the library moves. That trade — explicit in the source,
|
||||
checked against reality, no header read at build time — is a real one, and
|
||||
@ -1309,7 +1309,7 @@ let decl_source (d : Ast.decl) =
|
||||
something more specific for a reason it recorded.
|
||||
|
||||
What is left after those is a real disagreement about a width, an arity or a
|
||||
direction — which is exactly the class of bug docs/BUILT.md warns about, where
|
||||
direction — which is exactly the class of bug BUILT.md warns about, where
|
||||
[f64] against the library's [float] reads as garbage rather than as a link
|
||||
error. *)
|
||||
|
||||
@ -1378,7 +1378,7 @@ let diff_bound ~env ~(bound : (Ast.fn * string) list) (d : dump) =
|
||||
|
||||
(* Generate once, commit the result, regenerate when the library moves.
|
||||
|
||||
What that buys is in docs/DISCUSS.md item 6 and it is not caching — the dump is
|
||||
What that buys is in DISCUSS.md item 6 and it is not caching — the dump is
|
||||
already cached on disk and in memory, so a build that reads the header pays
|
||||
for it once either way. It is that no header is needed by *anybody*: the
|
||||
declarations are in the repository, so they are greppable, they diff when
|
||||
@ -1398,7 +1398,7 @@ let diff_bound ~env ~(bound : (Ast.fn * string) list) (d : dump) =
|
||||
they come from, because the one function that writes the file is this one
|
||||
and it refuses when the two disagree. A regeneration that quietly rewrote
|
||||
the bindings against a header the library does not match would produce
|
||||
exactly the failure docs/BUILT.md warns about — a permuted struct read as five
|
||||
exactly the failure BUILT.md warns about — a permuted struct read as five
|
||||
plausible numbers rather than as a link error — and it would produce it in a
|
||||
committed file that looks reviewed.
|
||||
|
||||
|
||||
@ -1017,7 +1017,7 @@ let locals t ~frame =
|
||||
refused) ]))
|
||||
|
||||
(* [(:op "inspect" :frame N :slot I :path (...))] — the inspector's second
|
||||
rooting mode. [docs/BUILT.md]'s "Two ways to root a walk" says what each root
|
||||
rooting mode. [BUILT.md]'s "Two ways to root a walk" says what each root
|
||||
can and cannot do; this is the half that names a frame.
|
||||
|
||||
[i] in the break buffer used to send a local's *name* to be evaluated as an
|
||||
@ -1080,7 +1080,7 @@ let inspect t ~frame ~slot ~path =
|
||||
|
||||
(* ── The allocation registry, read from this end ───────────────────── *)
|
||||
|
||||
(* [docs/BUILT.md]'s "An address answers with a type" is what the table is and why.
|
||||
(* [BUILT.md]'s "An address answers with a type" is what the table is and why.
|
||||
What follows is the reader: three verbs that ask the agent for what is
|
||||
recorded, and one of them turns a recorded *name* back into a type.
|
||||
|
||||
@ -2530,7 +2530,7 @@ let two_process ?(debug = false) ~file ~sock () =
|
||||
(* Everything above this line works the same either way. What follows is the
|
||||
merged build: one executable that is the compiled Flan program *and* holds
|
||||
the whole OCaml compiler, with the editor's socket served from a thread
|
||||
inside it. docs/DISCUSS.md item 14 is the spike this is built from.
|
||||
inside it. DISCUSS.md item 14 is the spike this is built from.
|
||||
|
||||
The shape, and it is this way round for a reason:
|
||||
|
||||
|
||||
@ -164,7 +164,7 @@ CAMLprim value flan_peek_bytes(value p, value off, value n) {
|
||||
*
|
||||
* The runtime system is released across the call. A delivery does a [dlopen],
|
||||
* which is milliseconds of relocation and the loader lock, and holding OCaml's
|
||||
* lock through it stalls every other OCaml thread for no reason — docs/DISCUSS.md
|
||||
* lock through it stalls every other OCaml thread for no reason — DISCUSS.md
|
||||
* §14's third cost, in the one place this lane creates it. Nothing the agent
|
||||
* does touches an OCaml value, so there is nothing to keep alive across it. */
|
||||
extern char *flan_agent_request(const char *line, uint64_t *len)
|
||||
|
||||
33
lib/emit.ml
33
lib/emit.ml
@ -78,17 +78,6 @@ let struct_name_of (t : Types.t) =
|
||||
let cellptr n = "@" ^ quoted ("flan.cellp." ^ n)
|
||||
let globalptr n = "@" ^ quoted ("flan.gp." ^ n)
|
||||
|
||||
(* Which backend built this image. A dev build defines its own marker and a
|
||||
redefinition module emits a data relocation against the one it was built
|
||||
for, so a crossed pair — an LLVM module in an [--x86] host, or the reverse —
|
||||
is refused by the loader at [dlopen] instead of running until the first call
|
||||
into a redefined function that takes or returns a struct, which is where the
|
||||
two conventions disagree and where the crossed pair was measured dying with
|
||||
SIGSEGV. See [X86.abi_marker], which is the same mechanism spelled for the
|
||||
other backend, and docs/handoffs/HANDOFF-x86-abi-marker.md. *)
|
||||
let abi_marker = "flan.abi.llvm"
|
||||
let abi_marker_sym = "@" ^ quoted abi_marker
|
||||
|
||||
(* ── Types ─────────────────────────────────────────────────────────── *)
|
||||
|
||||
let rec ll (t : Types.t) =
|
||||
@ -1045,7 +1034,7 @@ and value_at f (e : Tast.expr) : string =
|
||||
same name would do, at the point the *address* is taken rather than at the
|
||||
call. What that does not give is a value taken before a redefinition and
|
||||
called after it: that one is still the old body, because there is nothing
|
||||
left to re-resolve once the address is in a slot. Named in docs/BUILT.md rather
|
||||
left to re-resolve once the address is in a slot. Named in BUILT.md rather
|
||||
than papered over with a trampoline. *)
|
||||
| Tast.FnAddr (Tast.Fnval n) -> body_of f n
|
||||
| Tast.Addr p -> fst (place f p)
|
||||
@ -2132,7 +2121,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||
only [Rt] symbols that can transfer, so they are the only ones that take
|
||||
the channel and the only ones guarded — everything else in this family
|
||||
is arithmetic over a container header and cannot reach a handler. A Vec
|
||||
is checked inside the runtime rather than in emitted IR (docs/BUILT.md), so
|
||||
is checked inside the runtime rather than in emitted IR (BUILT.md), so
|
||||
this is where (at v i) gets what (at arr i) gets from [check_at]. *)
|
||||
let signals =
|
||||
String.equal sym "flan_vec_at" || String.equal sym "flan_vec_as_slice"
|
||||
@ -2925,13 +2914,6 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
|
||||
Nothing has been redefined yet, so a dev build starts out behaving exactly
|
||||
like a release one — the indirection is the only difference. *)
|
||||
if dev then begin
|
||||
(* The ABI marker, defined here so a redefinition module can bind against
|
||||
it, and only in a dev build: a release build has no cells and nothing to
|
||||
load into one, so it keeps exactly the module text it had before this
|
||||
existed. [-rdynamic] is what puts it in the executable's dynamic symbol
|
||||
table, and a dev build is the only build that gets that either. *)
|
||||
Buffer.add_string m.out
|
||||
(Printf.sprintf "%s = global i64 0\n" abi_marker_sym);
|
||||
List.iter
|
||||
(fun (fn : Tast.fn) ->
|
||||
Buffer.add_string m.out
|
||||
@ -3047,17 +3029,6 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
|
||||
(globalptr g.Tast.gname)))
|
||||
p.Tast.globals;
|
||||
if dev then begin
|
||||
(* The host's ABI marker, and a pointer-sized datum holding its address.
|
||||
That datum is a relocation the loader has to resolve while it maps the
|
||||
object, so a host built by the other backend — which defines
|
||||
[flan.abi.x86] and not this — fails the [dlopen] outright, rather than
|
||||
loading and then dying at the first call into a redefined function that
|
||||
takes or returns a struct. Hidden, so this module's own copy can never
|
||||
be interposed by another loaded module's; the relocation against the
|
||||
host's marker is the only part that matters. *)
|
||||
Buffer.add_string m.out
|
||||
(Printf.sprintf "%s = external global i64\n%s = hidden global ptr %s\n\n"
|
||||
abi_marker_sym ("@" ^ quoted "flan.abi.require") abi_marker_sym);
|
||||
(* The cells are the host's, like the globals. Referencing one is how a
|
||||
redefined function reaches its siblings, and storing into one is how it
|
||||
replaces itself. A name the host lacks gets a slot instead, filled by
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
(** Macro expansion: the pass between the reader and [Parse].
|
||||
|
||||
There is no interpreter and there is not going to be one (docs/BUILT.md, "Why
|
||||
There is no interpreter and there is not going to be one (BUILT.md, "Why
|
||||
there is no interpreter"), so running a macro at compile time means
|
||||
compiling it and loading it into this process. Every piece of that is
|
||||
already built and measured — [Emit.macro_thunk], [Build.macro_module],
|
||||
|
||||
@ -341,7 +341,7 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
|
||||
[(Form.Sym {.s "begin"})] whose name is a *string in an argument* rather
|
||||
than a name anything would rename. That is not an accident to work around:
|
||||
it is the property [Macro]'s walk depends on, the one that makes a
|
||||
quasiquoted call output rather than a compile-order dependency (docs/BUILT.md,
|
||||
quasiquoted call output rather than a compile-order dependency (BUILT.md,
|
||||
"A call inside a quasiquote is output, not a dependency"). It is also
|
||||
exactly what puts the name out of a rename's reach.
|
||||
|
||||
@ -421,7 +421,7 @@ let rec rename_form owned alias bound (f : Form.t) : Form.t =
|
||||
Desugared on the way out, because [Macro.program] is handed forms that
|
||||
[Parse.parse_forms] has already run [Expand.quasiquote] over and its rounds
|
||||
read them with that assumed. An un-desugared one would make a quasiquoted
|
||||
call look like a real one, which is the false ring docs/BUILT.md records the
|
||||
call look like a real one, which is the false ring BUILT.md records the
|
||||
first cycle test walking into. *)
|
||||
let qualify_macro owned alias (f : Form.t) : Form.t option =
|
||||
match f.Form.v with
|
||||
@ -704,7 +704,7 @@ let words line =
|
||||
nothing if it is not".
|
||||
|
||||
That marker is what lets a package offer the check without requiring it.
|
||||
[vendor/raylib] builds today against a shared library alone — docs/BUILT.md's "no
|
||||
[vendor/raylib] builds today against a shared library alone — BUILT.md's "no
|
||||
raylib headers are needed", which is a real property: a build needs
|
||||
libraylib linkable and not raylib-devel installed. A required header would
|
||||
take that away from everyone in order to give the check to the people who
|
||||
@ -969,7 +969,7 @@ let rec import ~seen ~open_ ~loc alias dir =
|
||||
the header is the package's own claim about what it binds; a
|
||||
[defstruct] that disagrees with it lays fields out in the
|
||||
wrong order and reads as five plausible numbers rather than as
|
||||
a link error, which is the failure docs/BUILT.md says only a test
|
||||
a link error, which is the failure BUILT.md says only a test
|
||||
can catch. Continuing past a known-wrong layout to produce a
|
||||
program that will read garbage is the shape the house rule
|
||||
against swallowing things exists to prevent.
|
||||
|
||||
@ -1466,7 +1466,7 @@ let source = {flan|
|
||||
;; site attached to what a macro produces", and it is what the queued
|
||||
;; structured-error work will read.
|
||||
;;
|
||||
;; Case order is the tag order (docs/BUILT.md, unions), so this list is a layout
|
||||
;; Case order is the tag order (BUILT.md, unions), so this list is a layout
|
||||
;; contract with lib/expand.ml's marshaller and may not be reordered.
|
||||
(defunion Form
|
||||
[(Sym [s string])
|
||||
|
||||
@ -87,7 +87,7 @@ let rec same_const (a : Tast.expr) (b : Tast.expr) =
|
||||
desugars a package's macro on its way out for the same reason — but
|
||||
[Parse.imported_macros] is read by [Macro.program] directly, past that map.
|
||||
An undesugared body still has its quasiquote in it, which makes a
|
||||
quasiquoted call look like a real one: the false ring docs/BUILT.md records the
|
||||
quasiquoted call look like a real one: the false ring BUILT.md records the
|
||||
first cycle test walking into.
|
||||
|
||||
Unqualified, and that is the point: a buffer writes its own macro's bare
|
||||
@ -345,7 +345,7 @@ type change = {
|
||||
}
|
||||
|
||||
(* [pause] is [C-u C-c C-c]: the position, in the source just sent, of the form
|
||||
the program should stop at — docs/DISCUSS.md §9. It arrives as a separate field
|
||||
the program should stop at — DISCUSS.md §9. It arrives as a separate field
|
||||
rather than spliced into [src], because splicing text would move every
|
||||
location after it, and it is applied below to the *declarations*, once
|
||||
parsing has attached those locations and [Load] has qualified the names.
|
||||
|
||||
59
lib/x86.ml
59
lib/x86.ml
@ -7,7 +7,7 @@
|
||||
|
||||
{1 The internal calling convention}
|
||||
|
||||
The spike's report (docs/DISCUSS.md item 15) called the internal convention the
|
||||
The spike's report (DISCUSS.md item 15) called the internal convention the
|
||||
sharpest obstacle, because LLVM's answer for a first-class struct is an
|
||||
implementation detail discoverable only by disassembly — a 24-byte struct
|
||||
comes back in [rax]:[rdx]:[rcx], and [rcx] is a register SysV never uses
|
||||
@ -408,28 +408,6 @@ let gsym n = asm_sym ("flan." ^ n)
|
||||
Byte-for-byte or the link fails and the piece served nothing. *)
|
||||
let csym n = asm_sym ("flan.cell." ^ n)
|
||||
|
||||
(* The marker that says which backend built an image, and it is the whole of
|
||||
the answer to the one way these two backends can be mixed and be wrong.
|
||||
[emit.ml] and this file agree on every scalar and disagree on every
|
||||
aggregate — this file passes a struct by pointer with a hidden [sret] and
|
||||
LLVM classifies per SysV — so a redefinition module from one backend
|
||||
dlopened into a host from the other links, loads, and then dies at the first
|
||||
call into a redefined function that takes or returns a struct. That was
|
||||
measured as SIGSEGV; see HANDOFF-x86-aggregates.md.
|
||||
|
||||
A dev build defines its own marker and a redefinition module emits a data
|
||||
relocation against the marker it was itself built for. A matched pair binds
|
||||
it and notices nothing. A crossed pair has no such symbol to bind, and the
|
||||
loader refuses the module at [dlopen] — before a single instruction of the
|
||||
new body runs, and with the missing symbol naming the backend in the
|
||||
message. That is the property: the mismatch is caught by the loader rather
|
||||
than by the processor, at load rather than at a call.
|
||||
|
||||
[Emit.abi_marker] is the same string for the LLVM half. The two must stay
|
||||
distinct and neither may ever be defined by both backends, or the refusal
|
||||
quietly stops refusing. *)
|
||||
let abi_marker = "flan.abi.x86"
|
||||
|
||||
(* ── Debug information ───────────────────────────────────────────────── *)
|
||||
|
||||
(* DWARF, written out as bytes, for the same reason the instructions are — and
|
||||
@ -767,7 +745,7 @@ let addr_sym f ~dst s =
|
||||
holds the cell's *address*. Collapsing them was this lane's one real bug,
|
||||
and it looked exactly right in the disassembly: [mov r11, cell@GOTPCREL(%rip)]
|
||||
beside [call *%r11] reads as "call through the cell" and in fact calls the
|
||||
cell. docs/DISCUSS.md item 15 said this is how hand-encoding fails. *)
|
||||
cell. DISCUSS.md item 15 said this is how hand-encoding fails. *)
|
||||
let load_sym f ~dst s =
|
||||
if f.ext s then begin
|
||||
load_int f.b ~dst ~mm:(Got s) ~size:8 ~signed:false;
|
||||
@ -1140,7 +1118,7 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
| Tast.None_ -> zero_value f dst t
|
||||
(* Reading an uninitialised value gives whatever the slot held: stable
|
||||
garbage rather than LLVM's [poison]. The one construct where the two
|
||||
backends are meant to differ — docs/DISCUSS.md item 15, question 4. *)
|
||||
backends are meant to differ — DISCUSS.md item 15, question 4. *)
|
||||
| Tast.Uninit _ -> ()
|
||||
| Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ ->
|
||||
let src = lvalue f e in
|
||||
@ -1899,9 +1877,9 @@ and check_slice f (base : loc) (ty : Types.t) (loc : Loc.t) (lo : Tast.expr)
|
||||
bounds_call f "flan_slice_error" loc [ a; b; c ];
|
||||
lbl f.b ok)
|
||||
|
||||
(* [emit.ml]'s [check_div] and [check_cast], item 3 of docs/handoffs/HANDOFF-x86-rt.md's
|
||||
(* [emit.ml]'s [check_div] and [check_cast], item 3 of HANDOFF-x86-rt.md's
|
||||
list, and the one item on it that was blocked on a language decision rather
|
||||
than on code. That decision is in docs/handoffs/HANDOFF-arith.md: a divide or remainder by
|
||||
than on code. That decision is in HANDOFF-arith.md: a divide or remainder by
|
||||
zero, the one division that overflows, and a float to integer cast whose
|
||||
value does not fit all signal ArithError, exactly as a bad index signals
|
||||
BoundsError.
|
||||
@ -2134,7 +2112,7 @@ and call_c f ~sym ~args ~rty dst =
|
||||
[Rt] symbols that can transfer, so they are the only ones that take the
|
||||
channel and the only ones guarded — everything else in this family is
|
||||
arithmetic over a container header and cannot reach a handler. A Vec is
|
||||
checked inside the runtime rather than in emitted code (docs/BUILT.md), so this
|
||||
checked inside the runtime rather than in emitted code (BUILT.md), so this
|
||||
is where [(at v i)] gets what [(at arr i)] gets from [check_at]. *)
|
||||
and rt_signals sym =
|
||||
String.equal sym "flan_vec_at" || String.equal sym "flan_vec_as_slice"
|
||||
@ -3288,19 +3266,6 @@ let program ~checks ?(dev = false) ?(debug = false) (p : Tast.program) : string
|
||||
(Printf.sprintf "\t.section\t.init_array,\"aw\",@init_array\n\t.align\t8\n%s\
|
||||
\t.quad\t%s\n\n"
|
||||
(if dev then "\t.quad\tflan_dev_reg_enable\n" else "") init_sym);
|
||||
(* The ABI marker, and only in a dev build: it exists for redefinition
|
||||
modules to bind against, a release build has no cells to load one into,
|
||||
and gating it here is what keeps a release build's assembly byte-for-byte
|
||||
what it was. [.globl] and default visibility, for the reason the cells
|
||||
have them — a dlopened object has to be able to see it, which is also why
|
||||
[Build.executable] passes [-rdynamic] for a dev build and nothing else. *)
|
||||
if dev then
|
||||
Buffer.add_string out
|
||||
(Printf.sprintf
|
||||
"\t.data\n\t.globl\t%s\n\t.align\t8\n\t.type\t%s, @object\n\
|
||||
\t.size\t%s, 8\n%s:\n\t.quad\t0\n\n"
|
||||
(asm_sym abi_marker) (asm_sym abi_marker) (asm_sym abi_marker)
|
||||
(asm_sym abi_marker));
|
||||
if dev then Buffer.add_string out (emit_cells p);
|
||||
Buffer.add_string out (emit_globals_data md p.Tast.globals);
|
||||
Buffer.add_string out "\n\t.section\t.rodata\n";
|
||||
@ -3458,18 +3423,6 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
||||
"\t.size\tflan_reload_install, . - flan_reload_install\n\n";
|
||||
let out = Buffer.create 8192 in
|
||||
Buffer.add_buffer out text;
|
||||
(* The ABI marker this module requires of its host. A pointer-sized datum
|
||||
holding the host's marker is a relocation the loader has to resolve while
|
||||
it maps the object, whatever it does about lazy binding of calls, so a
|
||||
host that does not define [flan.abi.x86] fails the [dlopen] outright. A
|
||||
call would do as well under [RTLD_NOW], which is what both loaders here
|
||||
pass, but a datum does not depend on that and costs eight bytes.
|
||||
|
||||
The label is local: nothing outside this module names it, and only the
|
||||
relocation against the marker matters. *)
|
||||
Buffer.add_string out
|
||||
(Printf.sprintf "\n\t.data\n\t.align\t8\n%s:\n\t.quad\t%s\n"
|
||||
(asm_sym "flan.abi.require") (asm_sym abi_marker));
|
||||
Buffer.add_string out "\n\t.section\t.rodata\n";
|
||||
Buffer.add_buffer out rodata;
|
||||
Buffer.add_string out "\n\t.section\t.note.GNU-stack,\"\",@progbits\n";
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
This was the first brainstorm. It is kept for history and is **no longer
|
||||
accurate**. Read instead:
|
||||
|
||||
- `../plan.org` — the design, the build sequence, and the open decisions
|
||||
- `../spec-memory.md` — ownership, containers, places, generics, function values
|
||||
- `../spec-conditions.md` — conditions and restarts, operational semantics
|
||||
- `../sand.flan` — the first acceptance program
|
||||
- `../syntax-sketch.flan` — the syntax, annotated with the decisions above
|
||||
- `plan.org` — the design, the build sequence, and the open decisions
|
||||
- `spec-memory.md` — ownership, containers, places, generics, function values
|
||||
- `spec-conditions.md` — conditions and restarts, operational semantics
|
||||
- `sand.flan` — the first acceptance program
|
||||
- `syntax-sketch.flan` — the syntax, annotated with the decisions above
|
||||
|
||||
## What changed since
|
||||
|
||||
@ -608,7 +608,7 @@ void flan_dev_watch_end(void) {
|
||||
* four [flan_dev_watch_emit_*] above are the emitter it would be pointed at,
|
||||
* shaped exactly like the [print] arm's. What is missing is the
|
||||
* [(watch "hp" hp)] arm in check.ml that joins the two, which is a file this
|
||||
* change does not own. docs/BUILT.md says what that arm is. */
|
||||
* change does not own. BUILT.md says what that arm is. */
|
||||
int32_t flan_dev_watch_i64(const char *name, int64_t x) {
|
||||
if (!flan_dev_watch_begin(name)) return 0;
|
||||
flan_dev_watch_emit_i64(x);
|
||||
@ -642,7 +642,7 @@ int32_t flan_dev_watch_str(const char *name, const char *s) {
|
||||
/* The scalar watch above keeps one value per name, and from a hot inner loop
|
||||
* that is nearly useless: you see whichever of the 91,200 cells happened to
|
||||
* run last. This is the other half — the port of [spy-num] from the author's
|
||||
* watch.clj, which exists for exactly that reason and is the piece docs/PORTING.md
|
||||
* watch.clj, which exists for exactly that reason and is the piece PORTING.md
|
||||
* calls the least obvious and the most valuable.
|
||||
*
|
||||
* **What a slot keeps: count, min, max, last, mean.** Five numbers, and the
|
||||
@ -1053,7 +1053,7 @@ static int flan_reg_full; /* something found no slot */
|
||||
* flan_rt.c are called unconditionally and begin with this load and a
|
||||
* not-taken branch, because flan_dev.c is linked into every build and a
|
||||
* second version of the allocator gated on a build flag is worse than a
|
||||
* branch. That is a real cost and not zero; docs/BUILT.md says so rather than
|
||||
* branch. That is a real cost and not zero; BUILT.md says so rather than
|
||||
* repeating the claim that a release build carries nothing. */
|
||||
static void flan_reg_report(void); /* the exit report, at the bottom */
|
||||
|
||||
@ -1378,7 +1378,7 @@ int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts,
|
||||
* Registered by [flan_dev_reg_enable] and therefore only in a dev build,
|
||||
* which is the point: a file-scope destructor would run in *every* build,
|
||||
* because this file is compiled into every build, and that would be a third
|
||||
* place a release build is not free. docs/BUILT.md names two and only two.
|
||||
* place a release build is not free. BUILT.md names two and only two.
|
||||
*
|
||||
* Off unless FLAN_DEV_LEAKS is set, and that is not timidity. The acceptance
|
||||
* table reads programs/registry.flan's output with stderr folded in, so a
|
||||
|
||||
@ -774,7 +774,7 @@ static int flan_over_budget(flan_allocator *a, int64_t size) {
|
||||
* an address, or a range of them. So this half is unconditional and calls into
|
||||
* flan_dev.c, which is linked into every build and begins each of these with a
|
||||
* load of a flag that only a dev build ever sets. A release build pays a load
|
||||
* and a not-taken branch per free, which is not nothing, and docs/BUILT.md says so.
|
||||
* and a not-taken branch per free, which is not nothing, and BUILT.md says so.
|
||||
*/
|
||||
void flan_dev_reg_note(void *base, int64_t bytes, int64_t elem,
|
||||
const char *type, int64_t typelen);
|
||||
@ -905,7 +905,7 @@ static flan_allocator flan_heap = {
|
||||
/* -- The arena: one fixed backing buffer and a bump offset. ----------
|
||||
*
|
||||
* `free-all` is retain-capacity: offset = 0, the pages stay. That is an
|
||||
* announced amendment to spec-memory.md's operation table (see docs/BUILT.md) and
|
||||
* announced amendment to spec-memory.md's operation table (see BUILT.md) and
|
||||
* it is what Odin's arena_free_all already does in effect. Handing the pages
|
||||
* back is `arena-destroy`, a separate operation, because a frame arena reset
|
||||
* every frame must not return memory only to ask for it again.
|
||||
@ -998,7 +998,7 @@ static void *flan_arena_proc(flan_allocator *a, int32_t mode, void *p,
|
||||
* signature. The spec calls it part of the calling convention; taking that
|
||||
* literally would touch every function signature, the FFI shim, the dev
|
||||
* trampolines and the reload ABI, for the same observable behaviour. The
|
||||
* literal reading is deferred and docs/BUILT.md says so.
|
||||
* literal reading is deferred and BUILT.md says so.
|
||||
*
|
||||
* There are no threads in Flan, so a plain global is the whole of it. */
|
||||
|
||||
@ -1133,7 +1133,7 @@ _Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
||||
*
|
||||
* ptr len cap allocator the release layout spec-memory.md fixes
|
||||
* gen bumped on every reallocation — the stale-slice
|
||||
* word. It has no reader yet; see docs/BUILT.md.
|
||||
* word. It has no reader yet; see BUILT.md.
|
||||
* epoch the allocator's epoch when this Vec last
|
||||
* touched it. Any operation on a container whose
|
||||
* recorded epoch has moved traps.
|
||||
@ -1142,7 +1142,7 @@ _Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
||||
* is not laziness: a redefinition module is built by llc and ld against a host
|
||||
* that was built separately, and nothing makes the two agree on a struct size.
|
||||
* A layout that changes with a build flag is a layout that can disagree across
|
||||
* that boundary silently. Dropping them in release is deferred and docs/BUILT.md
|
||||
* that boundary silently. Dropping them in release is deferred and BUILT.md
|
||||
* says what it is blocked on.
|
||||
*
|
||||
* Every entry point returns int8_t 1/0 for "did it fit", and never reports
|
||||
@ -1243,7 +1243,7 @@ static int8_t flan_vec_grow(flan_vec *v, int64_t want, int64_t size,
|
||||
v->ptr = p;
|
||||
v->cap = cap;
|
||||
/* Any slice taken before this points at storage that may have moved. The
|
||||
* word is bumped here and read nowhere yet; see docs/BUILT.md. */
|
||||
* word is bumped here and read nowhere yet; see BUILT.md. */
|
||||
v->gen++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -95,4 +95,4 @@ int64_t spike_probe_align(int64_t x) {
|
||||
* 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/DISCUSS.md item 15 and not from here. */
|
||||
* side, which is measured in DISCUSS.md item 15 and not from here. */
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 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
|
||||
Item 7 of `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
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# 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
|
||||
# which is item 7 of 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:
|
||||
#
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# Does the hand-written backend agree with LLVM?
|
||||
#
|
||||
# The only honest test of a hand-encoded backend is what the program prints and
|
||||
# what it exits with -- docs/DISCUSS.md item 15 and item 16 both say so, and both
|
||||
# what it exits with -- DISCUSS.md item 15 and item 16 both say so, and both
|
||||
# say it after a disassembly that read perfectly beside a wrong answer. So this
|
||||
# builds every program in test/programs twice, runs both, and diffs stdout,
|
||||
# stderr and the exit status. objdump is for after a program already has the
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
;;;; with it and the session is the thing the project is built around never
|
||||
;;;; having to restart. And the route in is the most ordinary one there is: a
|
||||
;;;; grid indexed from a mouse position is out of bounds the first time the
|
||||
;;;; pointer leaves the window (docs/PORTING.md, §3).
|
||||
;;;; pointer leaves the window (PORTING.md, §3).
|
||||
;;;;
|
||||
;;;; So a failed bounds check signals BoundsError with `error`, the same way a
|
||||
;;;; failed allocation signals StorageExhausted, and dies with the old message
|
||||
@ -28,7 +28,7 @@
|
||||
;;;;
|
||||
;;;; 1. The frame is abandoned and the program carries on. `frames` counts
|
||||
;;;; the ones that finished and `skipped` the ones that did not.
|
||||
;;;; 2. **Defers run.** A trap ran none, which docs/BUILT.md recorded as following
|
||||
;;;; 2. **Defers run.** A trap ran none, which BUILT.md recorded as following
|
||||
;;;; from the noreturn-then-unreachable shape rather than as a decision.
|
||||
;;;; The shape changed, so the question had to be answered rather than
|
||||
;;;; inherited: an *answered* bounds failure leaves through the same
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
;;;; editor picks up a stopped program with the stack, the locals and the
|
||||
;;;; globals readable and the restarts on offer.
|
||||
;;;;
|
||||
;;;; That is the claim `docs/PORTING.md` said was missing, and it is worth testing
|
||||
;;;; That is the claim `PORTING.md` said was missing, and it is worth testing
|
||||
;;;; end to end rather than reasoning about: before this, a bad index called
|
||||
;;;; exit(134), and with `flan dev` running as one process that took the
|
||||
;;;; compiler and the session with it.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
;;;; Frame rollback: the half of the frame loop that `continue` does not do.
|
||||
;;;;
|
||||
;;;; docs/PORTING.md Tier 1 item 6. Not a language feature — restart-case, struct
|
||||
;;;; PORTING.md Tier 1 item 6. Not a language feature — restart-case, struct
|
||||
;;;; assignment and fixed-arrays-as-values are all built. This is the worked
|
||||
;;;; example, the way sand.flan is the worked example for the loop itself.
|
||||
;;;;
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
;;;; It is all one function because a Pool is move-only exactly as a Vec is,
|
||||
;;;; so passing one to a helper *consumes* it — there is no borrowing
|
||||
;;;; parameter in the language yet. That is not a pool question and this
|
||||
;;;; program does not work around it; see docs/BUILT.md.
|
||||
;;;; program does not work around it; see BUILT.md.
|
||||
|
||||
(defstruct Enemy [hp i32 kind i32])
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
;;;;
|
||||
;;;; Nothing here introduces a name the host was not built with. X86.redefinition
|
||||
;;;; refuses those by name — that is the registry path, and it is item 1 of
|
||||
;;;; docs/handoffs/HANDOFF-x86-redef.md's "what remains" rather than anything to do with
|
||||
;;;; HANDOFF-x86-redef.md's "what remains" rather than anything to do with
|
||||
;;;; aggregates.
|
||||
|
||||
(defstruct Pair [a i64 b i64])
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
;;;; The aggregate case across the reload boundary, v1 (docs/handoffs/HANDOFF-x86-redef.md,
|
||||
;;;; The aggregate case across the reload boundary, v1 (HANDOFF-x86-redef.md,
|
||||
;;;; item 4).
|
||||
;;;;
|
||||
;;;; `reload.flan' proves that a redefined body is reached; every signature in
|
||||
|
||||
@ -34,7 +34,6 @@
|
||||
#include <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* The Flan symbols the executable itself defines. Flan names contain
|
||||
@ -63,43 +62,11 @@ static double now_ms(void) {
|
||||
return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
/* The same sentence [vendor/agent/flan_agent.c] says, for the same failure.
|
||||
* Duplicated rather than shared: the agent is vendored to be dropped into a
|
||||
* user's game and carries no header of its own, and this host is a test
|
||||
* fixture that links against neither it nor the runtime's dev half.
|
||||
*
|
||||
* A dev build defines a marker naming the backend that built it, and a
|
||||
* redefinition module holds a pointer to the marker it was itself built for.
|
||||
* The two backends agree on scalars and disagree on every aggregate, so a
|
||||
* crossed pair would run until the first call into a redefined function that
|
||||
* takes or returns a struct and then die with SIGSEGV. The marker turns that
|
||||
* into a relocation the loader cannot resolve. What it says then is
|
||||
* "undefined symbol: flan.abi.x86", so the marker's name is matched — not the
|
||||
* loader's phrasing, which is libc's to change — and the reason is stated. */
|
||||
static const char *abi_mismatch(const char *err) {
|
||||
if (err == NULL) return NULL;
|
||||
if (strstr(err, "flan.abi.x86") != NULL)
|
||||
return "the module and this host were built by different backends: the "
|
||||
"module came from the x86 dev backend and needs flan.abi.x86, "
|
||||
"which this host does not define. The two backends pass every "
|
||||
"struct differently. Rebuild the host with --x86.";
|
||||
if (strstr(err, "flan.abi.llvm") != NULL)
|
||||
return "the module and this host were built by different backends: the "
|
||||
"module came from LLVM and needs flan.abi.llvm, which an --x86 "
|
||||
"host does not define. The two backends pass every struct "
|
||||
"differently. Rebuild the host without --x86.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int install(const char *path) {
|
||||
double t0 = now_ms();
|
||||
void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||
if (h == NULL) {
|
||||
/* [dlerror] is one-shot, so the pointer is taken once and used twice. */
|
||||
const char *err = dlerror();
|
||||
const char *why = abi_mismatch(err);
|
||||
if (why != NULL) fprintf(stderr, "flan: %s\n", why);
|
||||
else fprintf(stderr, "dlopen %s: %s\n", path, err);
|
||||
fprintf(stderr, "dlopen %s: %s\n", path, dlerror());
|
||||
return 0;
|
||||
}
|
||||
install_fn f = (install_fn)(uintptr_t)dlsym(h, "flan_reload_install");
|
||||
|
||||
@ -1053,7 +1053,7 @@ let () =
|
||||
out of 54 is a fact about the Iroha that a lost byte anywhere in that
|
||||
chain would change. The two walk rows are what stands in for
|
||||
GetCodepointPrevious, which cannot be called from Flan at all — see
|
||||
docs/PORTING.md, and the file's own header for what each row fails on. *)
|
||||
PORTING.md, and the file's own header for what each row fails on. *)
|
||||
let raylib_codepoints_out =
|
||||
"codepoints 54\n\
|
||||
unique 49\n\
|
||||
@ -2799,7 +2799,7 @@ ERR@7 unexpected token: not the kind the caller was reading
|
||||
offset its own alignment allows, so the offset of field 1 in
|
||||
{ i8, T } *is* alignof(T). Reading [2 x i64] out of the emitted type and
|
||||
concluding 8 would be asserting the layout against itself, which is the
|
||||
circularity docs/BUILT.md already rejected for _Static_assert. *)
|
||||
circularity BUILT.md already rejected for _Static_assert. *)
|
||||
let llvm_align ir sname =
|
||||
let sty = Printf.sprintf "%%\"%s\"" sname in
|
||||
let b = Buffer.create 512 in
|
||||
|
||||
@ -1768,7 +1768,7 @@ let () =
|
||||
The redefinition binds a local, deliberately, because that is what
|
||||
the detector can see. [Emit.slot_fingerprint] hashes a body's
|
||||
*slots*, so a new body with the same slots and different global
|
||||
references is not caught — the hole is stated in docs/BUILT.md, and a
|
||||
references is not caught — the hole is stated in BUILT.md, and a
|
||||
test that asserted otherwise would be asserting a mechanism that is
|
||||
not there. *)
|
||||
let r =
|
||||
@ -2421,7 +2421,7 @@ let () =
|
||||
|
||||
(* ── Marking a form with (pause), from the editor's side ───────── *)
|
||||
|
||||
(* docs/DISCUSS.md §9: C-u before an evaluation marks a form so the program
|
||||
(* DISCUSS.md §9: C-u before an evaluation marks a form so the program
|
||||
stops when it runs. The mark travels as a *position* beside the code
|
||||
rather than spliced into it — splicing text would move every line and
|
||||
column after the insertion, and the error overlays, the break loop's
|
||||
|
||||
@ -1632,7 +1632,7 @@ let () =
|
||||
(* The struct check, which is the point of reading a header the generator
|
||||
does not otherwise need: the defstruct and the header's record have
|
||||
different authors, so a disagreement is real information. A
|
||||
_Static_assert was rejected in docs/BUILT.md as circular for want of exactly
|
||||
_Static_assert was rejected in BUILT.md as circular for want of exactly
|
||||
that. *)
|
||||
let structs_of ds =
|
||||
List.filter_map
|
||||
@ -1642,7 +1642,7 @@ let () =
|
||||
in
|
||||
check "a defstruct that matches the header is not reported"
|
||||
(Cimport.check_structs ~env ~structs:(structs_of fixture_ds) dump = []);
|
||||
(* Permuted: the failure docs/BUILT.md says only a test can catch, because every
|
||||
(* Permuted: the failure BUILT.md says only a test can catch, because every
|
||||
field still reads as a plausible number. *)
|
||||
check "a permuted defstruct is reported"
|
||||
(match
|
||||
@ -1651,7 +1651,7 @@ let () =
|
||||
with
|
||||
| [ ("Pair", why) ] -> contains why "field order"
|
||||
| _ -> false);
|
||||
(* Widened: the other half of the same hazard and the one docs/BUILT.md names —
|
||||
(* Widened: the other half of the same hazard and the one BUILT.md names —
|
||||
f64 where the library says float lays out eight bytes where there are
|
||||
four, and every field after it moves. *)
|
||||
check "a widened field is reported"
|
||||
@ -1684,7 +1684,7 @@ let () =
|
||||
~structs:(structs_of (program "(defstruct Feel [mood i32 n i32])\n")) dump
|
||||
= []);
|
||||
(* And the whole point of the check survives it. The tolerance is for a
|
||||
32-bit integer and nothing else, so the width hazard docs/BUILT.md names — f64
|
||||
32-bit integer and nothing else, so the width hazard BUILT.md names — f64
|
||||
where the library says float — still fails, in the very struct whose
|
||||
other field is an enum. *)
|
||||
check "a widened field beside an enum field is still reported"
|
||||
|
||||
@ -201,7 +201,7 @@ let () =
|
||||
fail "reload\n got: %S (exit %d)\n wanted: %S" text code want;
|
||||
|
||||
(* The same thing again, compiled by the dev backend end to end (x86.ml's
|
||||
header, docs/handoffs/HANDOFF-x86-rt.md item 1). Both halves, host and module, because
|
||||
header, HANDOFF-x86-rt.md item 1). Both halves, host and module, because
|
||||
the two backends' conventions agree on every scalar and disagree on
|
||||
every aggregate: an LLVM-built module dlopened into an --x86 host would
|
||||
be correct until the first redefined function took or returned a struct.
|
||||
@ -213,7 +213,7 @@ let () =
|
||||
flan_dev_global lookups that needs are refused there by name.
|
||||
|
||||
Read by running, not by reading. A disassembly reads correctly beside a
|
||||
wrong answer often enough (docs/DISCUSS.md item 15) that only the printed
|
||||
wrong answer often enough (DISCUSS.md item 15) that only the printed
|
||||
transcript settles it: [outer] is compiled once into the host and never
|
||||
rebuilt, so "after2 1204" can only mean its call site followed a body
|
||||
that this backend emitted, published through a cell it reached via the
|
||||
@ -335,88 +335,37 @@ let () =
|
||||
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ())
|
||||
[ h; m1; m2; o; e ]
|
||||
in
|
||||
let agg_llvm_mod q name =
|
||||
agg_run "llvm" dev (fun q name ->
|
||||
let o = tmp name in
|
||||
let ir = Emit.redefinition ~dev:true ~known:agg_known q ~fns:agg_fns in
|
||||
ignore (Build.shared ~opts:dev ~ir ~out:o ());
|
||||
o
|
||||
in
|
||||
let agg_x86_mod q name =
|
||||
o);
|
||||
agg_run "x86" x86 (fun q name ->
|
||||
let o = tmp name in
|
||||
let asm =
|
||||
X86.redefinition ~checks:true ~dev:true ~known:agg_known q ~fns:agg_fns
|
||||
in
|
||||
ignore (Build.shared_x86 ~opts:x86 ~asm ~out:o ());
|
||||
o
|
||||
in
|
||||
agg_run "llvm" dev agg_llvm_mod;
|
||||
agg_run "x86" x86 agg_x86_mod;
|
||||
|
||||
(* The same measurement run crossed, which is what the marker symbol is
|
||||
for. Before it, an --x86 host given LLVM-built modules loaded them and
|
||||
then died with SIGSEGV on the first call into a redefined aggregate
|
||||
body:
|
||||
o);
|
||||
(* The mismatch, which is the same measurement run crossed. An --x86 host
|
||||
given LLVM-built modules dies with SIGSEGV on the first call into a
|
||||
redefined aggregate body — measured, not argued:
|
||||
|
||||
got: "a1\nhost 54063108\na1\n" (exit 139)
|
||||
|
||||
That could not be asserted. It was undefined behaviour and what it
|
||||
printed was a property of whichever LLVM happened to be installed; a
|
||||
test pinning it would have been pinning the shape of a crash.
|
||||
That is not asserted here. It is undefined behaviour and what it prints
|
||||
is a property of whichever LLVM is installed; a test that pins it would
|
||||
be pinning the shape of a crash.
|
||||
|
||||
It is deterministic now, which is why it is here. A dev build defines a
|
||||
marker naming the backend that built it — [flan.abi.x86] or
|
||||
[flan.abi.llvm] — and a redefinition module holds a pointer to the one
|
||||
it was itself built for. That pointer is a relocation the loader has to
|
||||
resolve while it maps the object, so a crossed pair fails the [dlopen]
|
||||
outright, before a single instruction of the new body runs. Both
|
||||
directions, because a marker only one of the two backends emitted would
|
||||
refuse in one direction and say nothing in the other.
|
||||
|
||||
Asserted on the message as well as the exit status, the way the
|
||||
retyped-global and registry-overflow cases below are: a nonzero exit is
|
||||
not by itself this refusal, and the point of the exercise is that what
|
||||
reaches a user names the reason rather than repeating the loader's
|
||||
"undefined symbol". *)
|
||||
let agg_cross label opts mkmod wants =
|
||||
let h = tmp ("agg-xhost-" ^ label) in
|
||||
ignore
|
||||
(Build.executable ~opts ~csrcs:[ "reload_host.c" ] ~lflags:[ "-ldl" ]
|
||||
a1 ~out:h);
|
||||
let m1 = mkmod a1 ("agg-cross-" ^ label ^ "-1.so") in
|
||||
let o = tmp ("agg-xout-" ^ label) and e = tmp ("agg-xerr-" ^ label) in
|
||||
let code =
|
||||
Sys.command
|
||||
(Printf.sprintf "%s %s > %s 2> %s" (Filename.quote h)
|
||||
(Filename.quote m1) (Filename.quote o) (Filename.quote e))
|
||||
in
|
||||
let said = In_channel.with_open_bin e In_channel.input_all in
|
||||
if code = 0 then
|
||||
fail "%s: a crossed pair loaded and ran (exit 0)" label;
|
||||
if not (has said "built by different backends") then
|
||||
fail "%s: a crossed pair was refused without naming the reason: %S"
|
||||
label said;
|
||||
(* Which marker is missing is which backend built the module, so this is
|
||||
also what says the refusal fired for the right direction rather than
|
||||
for the other one. *)
|
||||
if not (has said wants) then
|
||||
fail "%s: the refusal named the wrong marker (wanted %s): %S" label
|
||||
wants said;
|
||||
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ())
|
||||
[ h; m1; o; e ]
|
||||
in
|
||||
(* An --x86 host handed an LLVM module: the pair the CLI can build today,
|
||||
since [flan reload] has no --x86 spelling. *)
|
||||
agg_cross "x86-host-llvm-module" x86 agg_llvm_mod "flan.abi.llvm";
|
||||
(* And the reverse, which no command spells but [X86.redefinition] does. *)
|
||||
agg_cross "llvm-host-x86-module" dev agg_x86_mod "flan.abi.x86";
|
||||
|
||||
(* The option-record guard, which is the older and narrower half of the
|
||||
same answer: [Build.opts] is where the backend choice lives, so a
|
||||
builder handed the *other* backend's option record refuses by name. It
|
||||
catches a caller holding one option record and reaching for the wrong
|
||||
builder. It cannot catch a caller holding two — the crossed runs above
|
||||
pass both of these refusals — which is what the marker is for. See
|
||||
docs/handoffs/HANDOFF-x86-aggregates.md and docs/handoffs/HANDOFF-x86-abi-marker.md. *)
|
||||
What is asserted is narrower, and the gap between the two is the finding
|
||||
rather than a caveat on it: [Build.opts] is where the backend choice
|
||||
lives, so a builder handed the *other* backend's option record refuses.
|
||||
The crossed run above passes both refusals — it hands [Build.shared] an
|
||||
LLVM record and never calls [Build.shared_x86] at all — and it still
|
||||
segfaults with this guard in place, re-measured after it landed. The
|
||||
guard catches a caller holding one option record; it cannot catch a
|
||||
caller holding two, and [flan reload] is exactly that caller. See
|
||||
HANDOFF-x86-aggregates.md. *)
|
||||
(match Build.shared ~opts:x86 ~ir:"" ~out:(tmp "never.so") () with
|
||||
| _ -> fail "Build.shared accepted an --x86 option record"
|
||||
| exception Failure m when has m "--x86" -> ()
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
* settles it. Memcheck answers both, and needs no instrumentation whatever:
|
||||
* it works on the binary, so hand-written IR, clang-compiled C and libc all
|
||||
* arrive on the same footing. Nothing in [Emit] has to cooperate, which is
|
||||
* the exact opposite of the sanitize_address attribute story in docs/BUILT.md.
|
||||
* the exact opposite of the sanitize_address attribute story in BUILT.md.
|
||||
*
|
||||
* The two tools are not ordered, they are complementary, and the measurement
|
||||
* that shows it is bounds.flan: ASan catches three of its six deliberate
|
||||
|
||||
@ -181,7 +181,7 @@ let () =
|
||||
end;
|
||||
|
||||
(* ── raylib in the browser ────────────────────────────────────────
|
||||
The claim docs/BUILT.md left open. core-basic-window.flan is built for the
|
||||
The claim BUILT.md left open. core-basic-window.flan is built for the
|
||||
web unchanged — no edit to its `until` loop, which is the whole point
|
||||
of choosing asyncify over emscripten_set_main_loop — and the module is
|
||||
then read for the two things that prove the claim rather than assert
|
||||
@ -237,7 +237,7 @@ let () =
|
||||
no DOM and no WebGL, so the module instantiates, main runs, and
|
||||
InitWindow dies inside glfwInit on `window is not defined` — which
|
||||
says the module is live but says nothing about whether the canvas
|
||||
paints. Only a human opening it can say that; docs/BUILT.md carries the
|
||||
paints. Only a human opening it can say that; BUILT.md carries the
|
||||
commands. *)
|
||||
let out = Filename.concat scratch "flan-web-sand.html" in
|
||||
(match web_build "../sand.flan" out with
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
# malloc(cap), so every byte in it — handed out, padding, or still past
|
||||
# the offset — is one allocation to memcheck. It never learns that a
|
||||
# sub-object ended, so it has nothing to complain about and equally
|
||||
# nothing to catch. Recorded as a coverage ceiling in docs/BUILT.md, not as a
|
||||
# nothing to catch. Recorded as a coverage ceiling in BUILT.md, not as a
|
||||
# clean bill of health.
|
||||
#
|
||||
# What *is* caught is definedness rather than addressability, and that
|
||||
|
||||
49
vendor/agent/flan_agent.c
vendored
49
vendor/agent/flan_agent.c
vendored
@ -674,49 +674,6 @@ static void emit(sink *o, const void *p, size_t n) {
|
||||
|
||||
static void reply(sink *o, const char *s) { emit(o, s, strlen(s)); }
|
||||
|
||||
/* A dlopen failure the compiler's two backends are responsible for, turned
|
||||
* into a sentence that says so.
|
||||
*
|
||||
* Flan has two native backends. They agree about every scalar and disagree
|
||||
* about every aggregate — the x86 dev backend passes a struct by pointer with
|
||||
* a hidden sret, LLVM classifies it per the SysV psABI — so a redefinition
|
||||
* module built by one and loaded into a host built by the other links, loads,
|
||||
* and then dies with SIGSEGV at the first call into a redefined function that
|
||||
* takes or returns a struct. A dev build therefore defines a marker naming its
|
||||
* backend and a module holds a pointer to the marker it was built for, which
|
||||
* is a relocation the loader must resolve while it maps the object. A crossed
|
||||
* pair has no such symbol and is refused here, before any of the new code
|
||||
* runs.
|
||||
*
|
||||
* What the loader says at that point is "undefined symbol: flan.abi.x86",
|
||||
* which is true and tells nobody anything. So the marker's name is matched —
|
||||
* the name, not the loader's phrasing, which is libc's to change — and the
|
||||
* reason is stated instead. Which marker is missing says which backend built
|
||||
* the module, and the host is necessarily the other one.
|
||||
*
|
||||
* Returns NULL for a failure that is about something else, which is then
|
||||
* passed through as the loader wrote it. */
|
||||
static const char *abi_mismatch(const char *err) {
|
||||
if (err == NULL) return NULL;
|
||||
if (strstr(err, "flan.abi.x86") != NULL)
|
||||
return "the module and the running program were built by different "
|
||||
"backends: the module came from the x86 dev backend and needs "
|
||||
"flan.abi.x86, which this program does not define. The two "
|
||||
"backends pass every struct differently, so the pair would die at "
|
||||
"the first call into a redefined function that takes or returns "
|
||||
"one. Rebuild the program with --x86 so that both halves agree.";
|
||||
if (strstr(err, "flan.abi.llvm") != NULL)
|
||||
return "the module and the running program were built by different "
|
||||
"backends: the module came from LLVM and needs flan.abi.llvm, "
|
||||
"which an --x86 program does not define. The two backends pass "
|
||||
"every struct differently, so the pair would die at the first call "
|
||||
"into a redefined function that takes or returns one. Rebuild the "
|
||||
"program without --x86: there is no --x86 spelling for building a "
|
||||
"redefinition module yet, so the program is the half that has to "
|
||||
"move.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* One line, one answer, one module. This is the whole of what the agent is
|
||||
* asked, and it is reached two ways: from the socket below, and — in a build
|
||||
* where the compiler is a thread in this same process — by being called. The
|
||||
@ -1163,12 +1120,8 @@ static void handle_line(char *line, sink *o) {
|
||||
}
|
||||
void *h = dlopen(line, RTLD_NOW | RTLD_LOCAL);
|
||||
if (h == NULL) {
|
||||
/* [dlerror] is one-shot and the next dl call may clobber what it returned,
|
||||
* so the pointer is taken once and used for both the test and the reply. */
|
||||
const char *err = dlerror();
|
||||
const char *why = abi_mismatch(err);
|
||||
reply(o, "err ");
|
||||
reply(o, why != NULL ? why : err);
|
||||
reply(o, dlerror());
|
||||
reply(o, "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
2
vendor/raylib/bindings
vendored
2
vendor/raylib/bindings
vendored
@ -67,7 +67,7 @@ name IsAudioStreamProcessed audio-stream-processed?
|
||||
|
||||
# Hand-written in raylib.flan, so excluded here: a second declare-c for one C
|
||||
# symbol is refused for the whole program. These three are on a game's
|
||||
# per-frame path (docs/PORTING.md), and a hand-written line is what the signature
|
||||
# per-frame path (PORTING.md), and a hand-written line is what the signature
|
||||
# check has to compare against -- generated output agrees with the header by
|
||||
# construction, so it can only check the hand-written half.
|
||||
exclude DrawTexturePro
|
||||
|
||||
4
vendor/raylib/build-web.sh
vendored
4
vendor/raylib/build-web.sh
vendored
@ -12,7 +12,7 @@
|
||||
#
|
||||
# The output is a plain static archive plus nothing else: no headers are
|
||||
# installed, because the generated FFI shim declares the prototypes it uses
|
||||
# (see docs/BUILT.md, "No raylib headers are needed").
|
||||
# (see BUILT.md, "No raylib headers are needed").
|
||||
#
|
||||
# sh vendor/raylib/build-web.sh
|
||||
#
|
||||
@ -54,7 +54,7 @@ else
|
||||
|
||||
# GRAPHICS_API_OPENGL_ES2 is what WebGL is. PLATFORM_WEB makes rcore.c
|
||||
# include platforms/rcore_web.c, whose WindowShouldClose() is an
|
||||
# emscripten_sleep that returns false — see docs/BUILT.md on why that is the whole
|
||||
# emscripten_sleep that returns false — see BUILT.md on why that is the whole
|
||||
# reason a Flan `until` loop needs no rewriting for the browser.
|
||||
#
|
||||
# rglfw.c is not in the list: the web platform uses emscripten's own GLFW
|
||||
|
||||
2
vendor/raylib/modes.flan
vendored
2
vendor/raylib/modes.flan
vendored
@ -39,7 +39,7 @@
|
||||
;;;;
|
||||
;;;; The obvious fix is `defer`, and it is not available here. `defer` is a
|
||||
;;;; compile-time construct — the forms are copied into the function's exit
|
||||
;;;; paths — so docs/BUILT.md refuses it inside a loop or a branch, and a
|
||||
;;;; paths — so BUILT.md refuses it inside a loop or a branch, and a
|
||||
;;;; begin/end pair lives inside the game loop essentially always. Were it
|
||||
;;;; permitted there it would be wrong in the worse direction: one EndDrawing
|
||||
;;;; at function exit for N BeginDrawings. A macro expanding to a defer would
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user