From c0bf4ac34c783e85776bb089f048012b4f719bbf Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 12:09:08 +0700 Subject: [PATCH] Say where sand.flan lands for the browser, and how to open it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUILT.md gains the section: why the path had to become an embed, the rule for a target-tagged .c file, why the agent is a no-op there and why that is not the barf decision reversed, the exact commands including the .html output name and the server a wasm module needs, and the four things only a human opening it can settle. NEXT.md strikes web blocker 1 and rewrites blocker 3 — nothing has been opened in a browser is still true, and is now the only thing left. --- BUILT.md | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ NEXT.md | 33 ++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/BUILT.md b/BUILT.md index d970c86..656de17 100644 --- a/BUILT.md +++ b/BUILT.md @@ -1795,3 +1795,118 @@ and node runs the emitted JS and gets `ok`. For raylib it builds `core-basic-win module for the two things that would be false if the mechanism were wrong: an `asyncify_start_unwind` export, and a `glViewport` import that can only have come from raylib's web platform. Import and export names are plain strings in the binary, so this needs no wasm reader. + +## sand.flan in a browser + +**The flagship program builds for the browser and the artifact opens.** Three things were between it and the target, +and none of them was the `#include` the earlier note named. + +**The brush was a path, and a path is what cannot work.** `(rl/load-texture "brush.png")` hands raylib a filename to +open; a bare relative path has no meaning where there is no filesystem, so raylib would have opened nothing and the +cursor would simply have been missing. It is `(embed "brush.png")` now, decoded by a new binding — +`LoadImageFromMemory`, which completes the chain embedded bytes -> `Image` -> `Texture2D` that `LoadTextureFromImage` +already had the other half of. The declaration is `(Ptr u8)` plus an explicit `i32` count, because `shim.ml` refuses a +slice parameter and its refusal says exactly that; the Flan wrapper beside it takes the slice apart, which is the +idiom `collision-point-poly?` and `load-font-ex` already established. One decode serves both textures now: the +unflipped upload first, then `ImageFlipHorizontal` in place, then the mirrored upload — **the order is load-bearing**, +and if the two badges look the same it was swapped. + +`load-texture` and `load-image` lose their only call site in this repository by this change. That is deliberate rather +than an accident of editing: a path-based load is the one shape the browser cannot have, and the bindings stay for the +desktop programs that will want them. + +The embedded path resolves **relative to the file the form is written in**, which is why +`test/programs/sand-headless.flan` still works: it reaches `sand.flan` through `../../` out of a sandboxed `_build`, +and the PNG is found beside `sand.flan` and not beside the working directory. `test/dune` therefore lists `brush.png` +as a dependency of every stanza that builds sand — an embed is read by the *checker*, so it is a build input and not a +run-time one. The same fact reached `test_session`'s `C-c C-k` case, which re-evaluates sand.flan's whole text: it now +passes `~origin`, which is the buffer path both editor paths already send, because the default `` origin would +resolve the embed against the working directory instead. + +**A package's C may be addressed to one target, the way a `link` line already could.** `Load` collects a package's +`.c` files by listing the directory, and there is nowhere in a directory listing to put a tag except the name, so the +tag goes there, before the extension: + +``` +vendor/agent/flan_agent.c compiled everywhere, unless displaced +vendor/agent/flan_agent.web.c compiled for the browser, and displaces the above +``` + +One rule: **a tagged file is compiled only on its own target, and there it replaces the untagged file of the same base +name.** Untagged is the default and every existing package is untagged, so nothing that did not opt in changed. +Replacement rather than the pure tagging Go's `_windows.go` and Odin's `file_js.odin` use, and the difference is the +point — pure tagging would mean renaming `flan_agent.c` to `flan_agent.native.c` to teach the package about a target +it had never heard of, and this way a package gains a target by gaining a file. The selection is in `Build` and not in +`Load`, for the reason `select_lflags` gives: `Load` resolves imports before a target is chosen. + +**The dev agent on the web is a no-op, and it is not the `barf` decision being contradicted.** The compile error that +led here — `struct timeval` incomplete, because emscripten's headers do not pull `` in transitively — is +the surface. Underneath: *the agent is a socket server and a browser has no sockets*. Adding the include produces an +agent that compiles, links, starts and can never accept a connection. + +Refusing `vendor:agent` on a web target was the other candidate and is ruled out by arithmetic, not taste. Flan has no +conditional compilation, `sand.flan` calls `(agent/start ...)` unconditionally, and `Reach` cannot prune a package +something reachable calls into — so a build-time refusal means the flagship program does not build for the browser at +all without being edited into a second program. **A refusal is only honest when the caller has a way to not ask.** + +The two decisions 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, and the loss is +real, is the user's, and is discovered later or never. 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. **Nothing is lost because there was never anything +there.** `sand.flan` already says the same 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 outcome by a shorter route. `start` returns `-1`, which is what `flan_agent.c` +returns for a path it cannot bind; `poll` and `wait` return 0, which is what the native build returns on every frame +nothing arrived on. The whole argument is written at the top of `vendor/agent/flan_agent.web.c`, where the next reader +will meet it. + +### Building it and opening it + +The raylib archive is built once and is not in the tree. From the repository root: + +```sh +sh vendor/raylib/build-web.sh # clones raylib 5.5 and compiles it with emcc +export FLAN_RAYLIB_WEB=$PWD/vendor/raylib/web/libraylib-5.5.a +``` + +`build-web.sh` prints that `export` line itself. Then: + +```sh +flan build sand.flan --target=web -o sand.html +``` + +**The output must be named `.html`.** `--shell-file` is passed only when it is, because emcc accepts and ignores it +otherwise — so `-o sand` produces a module with no shell, no canvas, and a page that looks like it built fine and +paints nothing. Three files land beside it, in whatever directory `-o` names: `sand.html`, `sand.js`, `sand.wasm`. + +**A `file://` URL will not work.** The page fetches `sand.wasm`, and a browser refuses that from the filesystem. Serve +the directory holding the three files: + +```sh +python3 -m http.server 8000 +``` + +and open **`http://localhost:8000/sand.html`**. Left mouse paints; the keys are the ones the native build has. + +### What only a human opening it can settle + +Verified headlessly, by `test/test_web.ml`: the three files exist, the module carries `asyncify_start_unwind` and +`glViewport`, and **brush.png's own bytes are in the module, whole** — the assertion that keeps the embed from +rotting. Not `IHDR`: stb_image, linked in from raylib, carries that string itself, so an `IHDR` check would pass on a +build where the embed emitted nothing. + +Not verified, and not verifiable here. `node sand.js` instantiates the module, runs `main`, and dies inside `glfwInit` +on `window is not defined` — which says the module is live and says nothing about the canvas. + +- **Whether it paints at all.** Nothing in CI has ever seen a pixel of this. +- **Audio.** `start-audio` generates a tone, `ExportWave`s it to `/tmp/flan-sand-tone.wav` and loads it back as a + music stream. That is raylib's own `fopen`, not Flan's `barf`, so the `#ifdef` in `flan_rt.c` does not cover it and + emscripten's MEMFS may well give it a writable `/tmp`. Every use is behind `music-ok`/`tone-ok`, so a failure is + silence and not a crash. Separately, browsers suspend the audio context until a user gesture, so `audio-ok` may be + true while nothing is heard until the first click. +- **The loop never exits.** `WindowShouldClose()` on `PLATFORM_WEB` is an `emscripten_sleep` that returns false, so + `until` never terminates and **none of `main`'s `defer`s ever run** — no `CloseWindow`, no `UnloadTexture`. That is + correct for a page, which is torn down by the tab closing, and it is worth knowing before reading anything into it. +- **Canvas size against `screen-width`/`screen-height`.** The shell is a string in `Build` and its canvas is not sized + from the program, so 900x600 may be letterboxed or cropped. diff --git a/NEXT.md b/NEXT.md index deeff41..71da5c8 100644 --- a/NEXT.md +++ b/NEXT.md @@ -365,6 +365,29 @@ array with a struct element, a 2-D struct array, and `[N string]` as both `defco "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 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: + +- The `#include` was never the fix. **The agent is a socket server and a browser has no sockets**, so an agent that + compiles there is an agent that can never accept a connection. `vendor/agent/flan_agent.web.c` is three no-ops, and + `Build` selects it over `flan_agent.c` on `--target=web` and nowhere else. +- **Refusing `vendor:agent` on web was the honest-looking option and is ruled out by arithmetic.** There is no + conditional compilation, `sand.flan` calls `agent/start` unconditionally, `Reach` cannot prune a package something + reachable calls into — so a refusal means the flagship program does not build for the browser at all. A refusal is + only honest when the caller has a way to not ask. This does **not** reverse decision 2 above: `barf`'s no-op loses a + file the program believed it wrote, and there is nothing for the agent to lose because `--dev` is already refused by + name on every wasm target. The argument is written out at the top of `flan_agent.web.c`. +- **A package's `.c` files can now be addressed to a target**, by a tag in the name before the extension, and a tagged + file *replaces* the untagged file of the same base name on that target. This is the C-source half of the + `@native`/`@wasi`/`@web` link-line mechanism decision 2 pointed at for per-package target isolation. + +The brush is `(embed "brush.png")` decoded through a new `LoadImageFromMemory` binding. `load-texture` and +`load-image` now have no call site anywhere in this repository — deliberately, because a path-based load is the one +shape the browser cannot have, and said here so it is not read later as an accident. + +The original entry follows. + **1. `sand.flan` has no web build, and the cause is one missing `#include`.** `vendor/agent/flan_agent.c` does not compile under emcc: *variable has incomplete type 'struct timeval'* at line 426, because emscripten's headers do not pull `` in transitively the way glibc's do. `sand.flan`'s `main` calls `(agent/start ...)` @@ -393,6 +416,16 @@ The original text follows. `sand.flan` does `(rl/load-texture "brush.png")` agai all. Answering this means either giving a single-file program a way to carry build arguments, or making assets their own declaration rather than a linker flag. No flag was invented for it here. +**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. 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. + +The original entry follows. + **3. Nothing has been opened in a browser.** The test is headless and permanently so: it asserts the artifact's shape, the `asyncify_start_unwind` export and the `glViewport` import, and that node runs the emitted JS. Whether the canvas actually paints is unverified by anything in CI, and a human should look once.