flan/vendor/agent/flan_agent.web.c
Joseph Ferano ce346dd972 sand.flan opens in a browser: the sheet is embedded and the agent is a stub
Three things stood between the flagship program and the web target, and each
is answered here rather than worked around.

The brush was a path. (rl/load-texture "brush.png") hands raylib a filename to
open, and a bare relative path means nothing on a target with no filesystem.
It is (embed "brush.png") now, decoded through a new binding —
LoadImageFromMemory, declared (Ptr u8) plus an explicit count because the shim
generator refuses a slice parameter and says so, with a Flan wrapper taking
the slice apart exactly as collision-point-poly? and load-font-ex already do.
One decode now serves both textures: the unflipped upload first, then
ImageFlipHorizontal in place, then the mirrored one. load-texture and
load-image lose their only call site in this repository; that is deliberate,
because a path-based load is the thing that cannot work here.

A package's C may now be addressed to one target, the way a link line already
could. A .c file may carry a tag before its extension — flan_agent.web.c — and
on that target it is compiled and *replaces* the untagged file of the same
base name. Replacement rather than plain tagging, so that teaching a package
about a new target is additive: the file that was right on three targets is
not renamed to say so. Selection is in Build and not in Load, for the reason
select_lflags gives.

The dev agent on the web is a no-op, and the reasoning is written at length in
vendor/agent/flan_agent.web.c. Short version: the agent is a socket server and
a browser has no sockets, so the missing <sys/time.h> was the surface and not
the cause. Refusing vendor:agent on a web target was the other candidate and
is ruled out by arithmetic — Flan has no conditional compilation, sand.flan
calls agent/start unconditionally, Reach cannot prune a package something
reachable calls into, so a refusal means the program does not build for the
browser at all. This does not contradict the `barf` decision made earlier
today. `barf` is asked to make something durable, and a no-op returns success
to a program that now believes bytes are on disk. The agent is asked to accept
redefinitions, and on the web there is no editor, no socket and no session —
--dev is refused by name on every wasm target — so there is nothing to lose.
sand.flan already says the same of a native release build at the call site.

test/test_web.ml builds sand.flan for the browser and reads the module for
brush.png's own bytes, whole. Not "IHDR": stb_image carries that string itself,
linked in from raylib, so it would pass on a build where the embed emitted
nothing. It is not run — node has no DOM, so main reaches InitWindow and dies
inside glfwInit on `window is not defined`, which says the module is live and
nothing about whether the canvas paints.

test/dune gains brush.png, because an embed is read by the checker and the
headless case reaches sand.flan through ../../ from a sandboxed _build.
test_session's C-c C-k case now passes ~origin, which is what both editor
paths already send; omitting it was testing a request nobody makes.

dune test is green. Docs follow in the next commit.
2026-09-12 12:07:16 +07:00

70 lines
3.6 KiB
C

/* The dev agent in a browser: the three calls, doing nothing, and saying so.
*
* Build selects this file over flan_agent.c on --target=web, and only there
* (see Build.select_csrcs). The reason is not the compile error that led here
* — emscripten's headers do not pull <sys/time.h> in transitively, so
* flan_agent.c fails on `struct timeval` at line 426 — because an #include
* would have fixed that and fixed nothing real. The reason is structural:
*
* THE AGENT IS A SOCKET SERVER, AND A BROWSER HAS NO SOCKETS.
*
* flan_agent_start binds an AF_UNIX socket and hands it to a listener thread.
* There is no such address family under emscripten, nothing to listen on, and
* nothing that could connect if there were. Adding the include produces an
* agent that compiles, links, starts, and can never accept a connection.
*
* ── Why a no-op here, when `barf` on the web signals instead ────────────
*
* NEXT.md decision 2 rejected a silent no-op for `barf` in as many words: a
* no-op write is how a save file disappears with nothing said. The two look
* contradictory and are not, and the difference is what the caller loses.
*
* `barf` is asked to make something durable. A no-op returns success to a
* program that now believes the bytes are on disk; the loss is real, it is the
* user's, and it is discovered later or never. So the web signals a condition
* and the program decides.
*
* The agent is asked to accept redefinitions from an editor. On the web there
* is no editor, no socket, and no session — `--dev` is refused by name on
* every wasm target, so a web build has no cells to install a redefinition
* into even if one arrived. There is nothing to lose because there was never
* anything there. sand.flan already says the same thing about a *native*
* release build, at the call site:
*
* "Building without --dev is fine — nothing has cells to install into, so a
* module is refused on the listener thread and the loop never notices."
*
* A web build reaches that same outcome by a shorter route. The no-op is not
* hiding a failure; it is the truthful implementation of "nothing is
* available here", which is the case where Odin's own `.Unsupported` stubs are
* right and the case decision 2 was careful to say it was not.
*
* ── Why not refuse vendor:agent on a web target ─────────────────────────
*
* It was the other candidate and it is ruled out by arithmetic, not taste.
* Flan has no conditional compilation, so a program cannot say "skip this on
* web". sand.flan calls (agent/start ...) unconditionally, Reach cannot prune
* a package something reachable calls into, and a build-time refusal would
* therefore mean sand.flan does not build for the browser at all without being
* edited into a second program. Refusing is only honest when the caller has a
* way to not ask; here it has none.
*
* ── What the return values say ──────────────────────────────────────────
*
* start returns -1, which is the same "could not listen" flan_agent.c returns
* for a path it cannot bind, so a caller that checks gets the answer it
* already knows how to read. poll and wait return 0 — no redefinitions
* installed — which is the truth and is what the native build returns on every
* frame that nothing arrived on. */
#include <stdint.h>
int32_t flan_agent_start(const uint8_t *path, int64_t len) {
(void)path; (void)len;
return -1;
}
int32_t flan_agent_poll(void) { return 0; }
int32_t flan_agent_wait(int32_t ms) { (void)ms; return 0; }