7 Commits

Author SHA1 Message Date
259cf3b3e2 sand is one program again
The simulation was in a package of its own for one reason: importing raylib
linked libraylib on every target, so the headless run could not name the
package the interactive one needs. That reason is gone, and the split was
never anything else — the physics is the same code either way.

So sim.flan is back inside sand.flan, and test/programs/sand-headless.flan
imports sand.flan itself: window, raylib bindings, dev agent and all. It builds
for wasm32 anyway. Nothing it calls reaches raylib, so no shim is compiled, no
-lraylib is passed, and the front-end's functions are never emitted; sand.flan's
main is not exported, so the only main is the headless one. The hash is
unchanged on both targets at both optimisation levels, which is the point —
a refactor that moved the number would have moved the simulation.

The new cases cover what made it possible rather than only the result: a
package nothing calls into, native and wasm32; raylib reached both directly and
through sand.flan and read once; and the three refusals — sand/main, one
directory under two aliases, and two mains.

test_session's package-qualification case moves to vendor/agent, which is now
the package in the tree with a defn in it.
2026-09-11 20:11:01 +07:00
05676f3181 The two bindings nothing was calling, found by listing rather than by reading
An audit over every public name in raylib.flan against every file that calls
one turned up draw-circle-v and load-texture-from-image with no call site at
all — bound, linked, and never once executed, which is the state the parent
commit already made a rule about. Reading the diff had not caught either.

load-texture-from-image now has the only call site it can have: sand.flan
loads brush.png a second time as an Image, mirrors it in RAM, and uploads
that. The two badges sit side by side, so a flip that did nothing or an
upload that took the unedited buffer shows as two identical sprites rather
than as nothing.

draw-circle-v fills the dot at the world cursor's centre, beside the pixel
that was already there — both Vector2 forms, so both land where the ring's
centre is rather than where an integer cast would have put them.

Still uncalled and not this lane's to invent a use for: key-released? and
mouse-button-pressed?.
2026-09-11 19:46:44 +07:00
a90badbcd5 A camera nothing looked through was a camera nothing tested
begin-mode-2d and end-mode-2d have been bound since Camera2D went in and
called by nothing, which is the same as not having bound them. The grid now
draws through a camera the arrow keys pan and comma and period zoom, and
paint has to undo that transform with get-screen-to-world-2d — so a camera
plumbed in wrongly is visible at once as grains landing somewhere other than
the cursor, rather than as nothing at all.

The shapes, the text and the timing come with it, and none of them can be
asserted: every one needs a GL context, and measure-text needs init-window
too — the default font is loaded there and nowhere else, so headless it
answers 0 for every string. Measured against libraylib.so.550, not assumed,
which is why it is absent from the acceptance table despite looking exactly
like a call that belongs in it.

So the HUD is built to be looked at instead: each shape binding appears once
and each is asymmetric enough that crossed arguments show. The ellipse is
wider than it is tall, the ring's sweep comes from get-time, the triangle has
its counter-clockwise winding with an outline over it as a control, and the
panel is sized by measure-text rather than by a guess.

draw-rectangle-rounded-lines takes no thickness in raylib 5.5 — it moved to
the -ex form, and both are here. The 5.1 header on this machine still shows
the five-argument version; nm -D on the library is what settled it.

Font loading stays unbound and says so: a Font carries a Texture2D, a
Rectangle* and a GlyphInfo*, and a GlyphInfo carries an Image.
2026-09-11 19:44:25 +07:00
01603843c0 A sprite in sand, because running it is the only test there is
sand.flan now loads a 16x8 sheet of two 8x8 brush frames and draws it four
ways: the frame under the cursor through draw-texture-rec, and three badges in
the corner through draw-texture, draw-texture-v and draw-texture-ex. That is
not decoration — it is one call site per binding that the acceptance table
cannot reach, and without it draw-texture-v and draw-texture-ex would be code
nobody had ever executed.

A failed load says so by name. LoadTexture on a missing file returns an id of
0, and every draw with that texture silently does nothing, so the program
would look like it had a drawing bug rather than a missing file. texture-valid?
is asked once at load and the answer is both printed and remembered, so the
sand still runs with the cursor off.

brush.png is generated rather than drawn — two circles, one ring and one
filled, 102 bytes — so the repository gains an asset nobody has to keep.

What this was checked by: xvfb-run, a screenshot of the running window, and
the badges counted in it. Also with brush.png moved away, which is how the
refusal path above is known to fire rather than merely to compile.
2026-09-11 17:56:03 +07:00
23a1b6c6fb The agent: a redefinition arriving in a program that is running
vendor/agent/ is a package like any other - agent.flan declares three calls,
flan_agent.c implements them, link asks for -lpthread. start listens on a unix
socket, poll installs whatever arrived and says how many, wait does the same
after waiting for something.

The split between poll and the listener is the whole design. dlopen relocates a
module and takes the loader lock, which is milliseconds and unbounded, so it
happens on the listener thread. flan_reload_install is one store per function
and must not land while a redefined function is on the stack, so it happens on
the game thread at the top of the frame, when the program asks. A ring and two
atomics connect them; the game thread never blocks on the loader.

wait exists for tests. A test that races the frame rate fails on a loaded
machine, so test/programs/agent.flan waits for the reload rather than sleeping
past it. It also sends a junk path first: the daemon is a separate process and
can send anything, and a bad path must be refused rather than take down the
program it was sent to.

Two things came out of running it. The reply goes out before the module is
queued, because the other way round the game thread can install and the program
can exit between the two, and the answer reaches the sender as a connection
reset instead of as ok. And ok means queued, not installed - the sender does
not get to know when the swap happened, since only the program knows when it is
between frames.

sand.flan now polls at the top of its loop, which is what this step was for.
Under Xvfb, one line on the socket and 455 consecutive frames drew from a
game-draw that did not exist when the process started. Building without --dev
still works: there are no cells, so a module is refused on the listener thread
and the loop never notices.

flan reload builds one module the way the daemon will. --new names what the
host was not built with, which is the one thing the command cannot work out for
itself and exactly what the session will track.
2026-09-10 21:41:27 +07:00
60a1928ee3 Raylib runs, Heckin yeah 2026-09-10 18:55:55 +07:00
e9cdbb321b Lisp based flan 2026-09-10 14:40:34 +07:00