Say what a package is now, since the answer changed three times
NEXT.md described a packaging system with no visibility, no nesting and a link that ignored the program, and explained sand's two files by it. All four are now wrong. The Packages section says what the rules are; a new section says how the link is decided and why the pruning has to take the functions as well as the flags; and the sand section keeps the part that still stands — the headless test needs no window on any target, which is a reason for two entry points and never was a reason for two files. The comments in load.ml and session.ml that used sim.flan to explain package qualification now use vendor/agent, which is the package left with a defn in it.
This commit is contained in:
parent
259cf3b3e2
commit
f78c935b95
131
NEXT.md
131
NEXT.md
@ -146,7 +146,6 @@ reader ✅ → parse ✅ → load ✅ → check ✅ → emit ✅ → clang ✅
|
||||
| `vendor/raylib/` | **the raylib package: `raylib.flan`, `shim.c`, `link`** |
|
||||
| `vendor/agent/` | **the dev agent: a socket, a loader thread, install at a frame boundary** |
|
||||
| `emacs/` | **`flan-mode.el`, `flan-dev.el`, `flan-repl.el`: the editor half of the dev loop** |
|
||||
| `sand-sim/` | **the falling-sand simulation, with no raylib in it** |
|
||||
| `bin/main.ml` | `flan read \| parse \| check \| emit \| build \| run \| reload \| dev` |
|
||||
| `test/test_flan.ml` | reader, parser and checker |
|
||||
| `test/test_acceptance.ml` | expression/result pairs + whole programs + the traps |
|
||||
@ -340,64 +339,100 @@ slash.
|
||||
|
||||
A package may also carry the C it binds to: every `.c` file in the directory is
|
||||
compiled into the build, and a file named `link` lists extra linker arguments.
|
||||
Whether those reach the build at all is decided *after* checking — see below.
|
||||
|
||||
This is not a module system yet. No visibility (hence `rl/get-color-raw` being
|
||||
callable), no cycle detection, and a package cannot import another one.
|
||||
**A package may be a single `.flan` file** named outright, rather than a
|
||||
directory. That is for the program that is also a library: `sand.flan` shares
|
||||
the repository root with three other loose programs, so naming its directory
|
||||
would import all four. A file carries no `.c` and no `link` file; those belong
|
||||
to a directory.
|
||||
|
||||
## sand.flan is two programs
|
||||
**A package may import a package.** The qualification flattens to the *inner*
|
||||
alias — raylib imported by a package that is itself imported is still `rl/…`,
|
||||
never `sand/rl/…` — because a directory reached along two routes has to arrive
|
||||
under one set of names or the checker sees every declaration twice. A directory
|
||||
is keyed by its real path and read once, which is also what ends a cycle: a
|
||||
package that imports itself meets its own entry and contributes nothing the
|
||||
second time, and the namespace being flat, mutually dependent packages simply
|
||||
work. The same directory under two *different* aliases is refused.
|
||||
|
||||
**The stated reason below is narrower than it reads, and the real fix is a
|
||||
one-line change to `Load` rather than two files.** Two claims got run together:
|
||||
**Visibility is one rule: `main` is not exported.** A package carrying one
|
||||
would collide with the importer's the moment anything imported it, so a program
|
||||
could never be a package; and `main` is a reachability root, so an imported one
|
||||
would keep everything it calls alive. Writing `sand/main` is refused at the
|
||||
line that wrote it, with the reason — left to the checker it would be "unknown
|
||||
name", which is true and useless.
|
||||
|
||||
- *raylib does not work on wasm* — false. It works through emscripten, which
|
||||
plan.org says itself. What is true is that it does not work on the **wasi**
|
||||
path, which is what the headless table targets, and which has no GL and no
|
||||
browser.
|
||||
- *a game loop cannot be expressed on wasm* — false. The browser cannot be
|
||||
blocked, so a web build drives the loop with `emscripten_set_main_loop`
|
||||
instead of a `while`. That is a different `main`, not a different program,
|
||||
and nothing about it requires the simulation to live in its own package.
|
||||
Still missing: a package-private marker for anything other than `main`, which
|
||||
is why `rl/get-color-raw` is callable.
|
||||
|
||||
What actually justifies the split is smaller and stands on its own: **the
|
||||
headless test needs no window and no input on any target.** `sand.flan` could
|
||||
not be that test even natively — with no mouse the grid stays empty and
|
||||
`settle` and `move-grain` never run on real data.
|
||||
## The link follows the program
|
||||
|
||||
What makes the split *mandatory* rather than chosen is the packaging
|
||||
limitation: **`Load` collects a package's C sources and link flags whether or
|
||||
not anything references the package.** Make that conditional and the two files
|
||||
become a preference. That is the thing to fix, and it is not large.
|
||||
|
||||
|
||||
|
||||
plan.org wants sand tested twice — interactive at 120 fps, and headless over N
|
||||
frames with the grid hashed, the version CI runs on native *and* wasm32. Those
|
||||
cannot be one binary: `Load` collects a package's C sources and linker
|
||||
arguments unconditionally, so anything importing the raylib package links
|
||||
libraylib on every target regardless of what its `main` does, and on wasm32
|
||||
`Load` used to hand a package's `.c` files and `link` arguments to the build
|
||||
the moment it was imported, whatever the importing program did with them. So
|
||||
anything naming `vendor:raylib` linked libraylib on every target, and on wasm32
|
||||
that link cannot succeed.
|
||||
|
||||
So the simulation moved to `sand-sim/`, which imports nothing. `sand.flan`
|
||||
imports it as `sim/` and adds the window, the mouse and the drawing;
|
||||
`test/programs/sand-headless.flan` imports it and adds a seed, four
|
||||
deterministic clouds, 40 frames and an FNV-1a hash. One copy of the physics.
|
||||
`lib/reach.ml` answers it from the checked program instead. Start at `main` and
|
||||
at the globals that run before it, follow every call — including the `Handled`
|
||||
frames, where a lifted handler clause is reached by address and by nothing else
|
||||
— and keep what is reached. A package none of whose externs survive contributes
|
||||
no C and no linker argument.
|
||||
|
||||
The headless case is what actually *verifies* milestone 4 — running the
|
||||
interactive build only proves it enters its loop, because with no mouse input
|
||||
the grid stays empty and `paint-at`, `settle` and `move-grain` never execute on
|
||||
real data. Measured through the probe: 168 grains painted around row 4–8, still
|
||||
168 after 40 frames, lowest occupied row 68. Grains fall, and none are lost.
|
||||
Dropping the flags alone would only move the failure: the bodies that called
|
||||
into raylib would still be emitted and `wasm-ld` would fail on the symbols
|
||||
rather than on the argument list. So the same walk prunes **functions and
|
||||
externs** from the program. Only those. Globals, structs and unions stay,
|
||||
because an unreferenced global is bytes in BSS and a dropped one is a silently
|
||||
different program.
|
||||
|
||||
**Three edits were made to sand.flan's own text**, and they are language
|
||||
decisions rather than fixes:
|
||||
**Dev builds are not pruned.** What a REPL may redefine next is not a function
|
||||
of what has been called so far.
|
||||
|
||||
The filtering happens at the call sites — `bin/main.ml`, the tests — because
|
||||
`Build.executable` receives `csrcs` and `lflags` from its caller and never sees
|
||||
the import list. `Reach.link` returns the pruned program and its C and linker
|
||||
arguments together, so a caller cannot take one without the other.
|
||||
|
||||
## sand.flan is one program
|
||||
|
||||
It was two files, and only ever for the reason above: the headless run is the
|
||||
one CI does on native *and* wasm32, and a program that imported raylib linked
|
||||
libraylib whatever its `main` did. So the simulation lived in `sand-sim/` and
|
||||
both drivers imported it.
|
||||
|
||||
Now `sand.flan` holds the simulation *and* the raylib front-end, and
|
||||
`test/programs/sand-headless.flan` imports `sand.flan` itself — window, raylib
|
||||
bindings, dev agent and all — and still builds for wasm32. Nothing it calls
|
||||
reaches raylib; `sand.flan`'s `main` is not exported, so the only `main` is the
|
||||
headless one; and the hash is unchanged on both targets at `-O2` and `-O0`,
|
||||
which is the point. A refactor that moved that number would have moved the
|
||||
simulation.
|
||||
|
||||
What still justifies *two entry points* is smaller and stands on its own: **the
|
||||
headless test needs no window and no input on any target.** `sand.flan` could
|
||||
not be that test even natively — with no mouse the grid stays empty and
|
||||
`settle` and `move-grain` never run on real data. Measured through the probe:
|
||||
168 grains painted around row 4–8, still 168 after 40 frames, lowest occupied
|
||||
row 68. Grains fall, and none are lost.
|
||||
|
||||
Two claims that got run together in an earlier note, for the record:
|
||||
|
||||
- *raylib does not work on wasm* — false. It works through emscripten. What is
|
||||
true is that it does not work on the **wasi** path, which is what the
|
||||
headless table targets, and which has no GL and no browser.
|
||||
- *a game loop cannot be expressed on wasm* — false. The browser cannot be
|
||||
blocked, so a web build drives the loop with `emscripten_set_main_loop`
|
||||
instead of a `while`. That is a different `main`, not a different program.
|
||||
|
||||
**Three edits were made to sand.flan's own text** when it was ported, and they
|
||||
are language decisions rather than fixes:
|
||||
|
||||
- `(defconst gravity 0.05)` → `(defconst gravity f32 0.05)`. An untyped float
|
||||
constant is `f64`, `velocity` is `[f32]`, and there is no implicit widening.
|
||||
- `(defvar current-color u32)` → `i32`. It is an index into `colors`, and
|
||||
`(len colors)` is an `i32`.
|
||||
- The file was split as above, so its body now says `sim/rows` and so on.
|
||||
|
||||
`(defn main [])` is unchanged — the short form, as plan.org says.
|
||||
- `(defn main [])` is unchanged — the short form, as plan.org says.
|
||||
|
||||
Painting is on **hold left mouse button** rather than on space, since the mouse
|
||||
bindings exist now. Space is still what cycles the colour, on release, which is
|
||||
@ -503,7 +538,7 @@ enforces, and a later change could quietly drop it.
|
||||
from the folding pass's value, because a global's initialiser has to be a
|
||||
compile-time constant and only that pass knows this one is. Its range check
|
||||
is therefore its own call to `in_range`; there is a regression test.
|
||||
- A `let` binding takes no type annotation, which is why `sand-sim` names its
|
||||
- A `let` binding takes no type annotation, which is why `sand.flan` names its
|
||||
FNV constants instead of writing them inline.
|
||||
- `(defn f [] f65 0.0)` still says *unknown name* rather than *did you mean
|
||||
f64*: with a single body form the parser cannot tell a return type from the
|
||||
@ -804,8 +839,8 @@ all the folding back; `Tast.global.gfolded` is what tells the two apart, because
|
||||
nothing downstream of the checker could.
|
||||
|
||||
**A form typed into a file that is imported as a package is qualified the way
|
||||
the import qualified it.** `settle` in `sand-sim/sim.flan` becomes `sim/settle`,
|
||||
and its call to `move-grain` becomes `sim/move-grain` — through `Load`'s own
|
||||
the import qualified it.** `poll` in `vendor/agent/agent.flan` becomes
|
||||
`agent/poll`, and its call to `poll-raw` becomes `agent/poll-raw` — through `Load`'s own
|
||||
`qualify_decl`, so the rule cannot drift from the one used at import time.
|
||||
Without this the form spliced as a brand-new unrelated name: the evaluation
|
||||
answered `ok`, and the running program went on calling the `sim/settle` it
|
||||
@ -1596,7 +1631,7 @@ The tests assert on the reason, not just on the failure.
|
||||
|
||||
`calc-me` and `sand`, the executables `flan build` drops beside their sources,
|
||||
are now in `.gitignore` — anchored (`/calc-me`, `/sand`) so the patterns cannot
|
||||
also match `sand-sim/` or anything nested.
|
||||
match anything nested.
|
||||
|
||||
`old-ocaml/` — the pre-rewrite menhir/ocamllex frontend, kept as reference and
|
||||
excluded from the build by the root `dune` file. Its contents are also in git
|
||||
|
||||
@ -43,8 +43,8 @@ type t = {
|
||||
(* Which alias each package's directory was imported under, and the names it
|
||||
owns. A file on disk does not say what it is called from outside — the
|
||||
*importer* chooses that — so this is the only place the answer exists, and
|
||||
a REPL editing a package's source needs it to know that [settle] typed in
|
||||
sand-sim/sim.flan means [sim/settle] to the running program. *)
|
||||
a REPL editing a package's source needs it to know that [poll] typed in
|
||||
vendor/agent/agent.flan means [agent/poll] to the running program. *)
|
||||
pkgs : pkg list;
|
||||
}
|
||||
|
||||
|
||||
@ -67,8 +67,8 @@ let create ~file =
|
||||
|
||||
(* Which package a file being edited belongs to, if any.
|
||||
|
||||
A form typed into sand-sim/sim.flan declares [settle], but the running
|
||||
program only ever knew it as [sim/settle]: the alias is chosen by whatever
|
||||
A form typed into vendor/agent/agent.flan declares [poll], but the running
|
||||
program only ever knew it as [agent/poll]: the alias is chosen by whatever
|
||||
imported the directory, and is written nowhere in the file itself. Without
|
||||
this the form splices as a brand-new unrelated name, the evaluation reports
|
||||
success, and nothing changes — the exact failure this whole design is meant
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user