diff --git a/NEXT.md b/NEXT.md index da43f24..7c0c8fc 100644 --- a/NEXT.md +++ b/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 diff --git a/lib/load.ml b/lib/load.ml index a9ee176..f4dc86c 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -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; } diff --git a/lib/session.ml b/lib/session.ml index a4d2121..ddfb794 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -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