diff --git a/BUILT.md b/BUILT.md index 1fe5847..d81170e 100644 --- a/BUILT.md +++ b/BUILT.md @@ -242,7 +242,9 @@ 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. +`emscripten_set_main_loop` instead of a `while`. ~~That is a different `main`, not a different program.~~ **The premise +held and the conclusion did not.** It is the same `main` and the same program: `-sASYNCIFY` answers the same browser +fact without cutting anything in half. See "The browser is the third target" below. **Three edits were made to sand.flan's own text** when it was ported, and they are language decisions rather than fixes: @@ -1221,5 +1223,70 @@ an `ExperimentalWarning` to stderr on every run and the harness compares combine one (same reason), and `flan run --target=` (a `.wasm` is not something this host execs — build it and point a runtime at it). -Still open: raylib on wasm, which plan.org wants through emscripten and its own sysroot. wasi-sdk is right for the -headless table; it is not necessarily right for the eventual game build. +~~Still open: raylib on wasm, which plan.org wants through emscripten and its own sysroot.~~ **Done — and it is a third +target, not a mode of this one.** wasi-sdk is right for the headless table and was never going to be right for the game +build. See the next section. + +## The browser is the third target + +`flan build --target=web` produces a page, its JS and a `.wasm`, and a raylib example opens in a browser from source +that was not touched. The two wasm targets share the word and almost nothing else, so `is_wasi` and `is_web` are +separate predicates and `is_wasm` is their union — the union is exactly the set of facts about the *machine* (32-bit +pointers, no `dlopen`), which is what the refusals are about, and nothing else is shared. + +**The compiler is `emcc`, not `clang`, and that is the whole of the sysroot story.** Everything the wasi target has to +find by hand — a sysroot, a builtins archive, a shadow resource directory, the `__main_argc_argv` shim — is what emcc +*is*. `target_flags` for `web` is the empty list; the only thing checked is that emcc exists, refused by name where the +reason can say so. The one fact that had to be true for any of this: **emcc takes a `.ll` on its command line**, which +it does, so `Emit`'s output needs no change and the IR stays target-independent. The object cache keys on the compiler +binary's path, size and mtime as it always did — now of *whichever* compiler the target uses, so an emcc `flan_rt.o` +and a clang one cannot collide. + +**The main loop: `-sASYNCIFY`, not `emscripten_set_main_loop`.** The older note above had the browser fact right — +it cannot be blocked — and drew the wrong conclusion from it. `emscripten_set_main_loop` wants the loop body as a +callback, so every one of the eleven examples that writes + +```lisp +(until (rl/window-should-close?) ...) +``` + +would have to be split by hand into an init and a tick, and the web program would stop being the native program. +Asyncify rewrites the module so a call can suspend across a return to the event loop, and raylib's web platform is +built for precisely that: `WindowShouldClose()` on `PLATFORM_WEB` is an `emscripten_sleep(16)` that then returns false +(raylib 5.5, `platforms/rcore_web.c`, read rather than assumed). So the loop yields once a frame at a call it already +makes, and **no example changed a character**. The price is real and is paid by every web build: asyncify instruments +the whole module, roughly doubling code size. It is not applied per-program because "does this program block" is not a +question `Build` can answer, and a flag set that varies per program is a cache key that varies per program. + +**`link` lines can be addressed to a target.** `vendor/raylib/link` named `libraylib.so.550`, which exists on the host +and nowhere else. A line may now carry `@native`, `@wasi` or `@web`, an untagged line applies everywhere — which is +what every existing `link` file already is — and `${NAME}` expands from the environment. The selection happens in +`Build` and not in `Load`, which is where the file is read, because **`Load` resolves imports before a target is +chosen**: the same program is built for both, and a package's linker arguments arrive here as a flat list of strings. +`Load`'s part in this is to pass the lines through untouched, which it already did. + +**raylib for the browser is built, not installed.** No emscripten port provides it (`emcc --show-ports`: there is +`contrib.glfw3` and no raylib), so `vendor/raylib/build-web.sh` clones raylib at the **5.5** tag — the one whose +`.so.550` the host links, because `raylib.flan` carries raylib's struct layouts and enum values and two targets built +from different raylibs would disagree about them in silence — and compiles the seven modules with +`-DPLATFORM_WEB -DGRAPHICS_API_OPENGL_ES2` into one archive under `vendor/raylib/web/` (gitignored). `rglfw.c` is not +among them: the web platform uses emscripten's own GLFW port, which is why `link` carries `@web -sUSE_GLFW=3`. No +headers are installed, for the reason the host build needs none — the generated shim declares its own prototypes. + +**The HTML shell is a string in `Build`, not a file in the tree**, for the same reason `Runtime_src` is: it has to be +wherever the compiler is, and a build that cannot find its own shell fails for a reason nobody spelled. It is a canvas +and a `Module.print` that puts stdout on the page; `FLAN_WEB_SHELL` replaces it. `--shell-file` is passed only when the +output is a `.html`, because emcc accepts and ignores it otherwise. + +**Refused by name, inherited whole:** `--dev`, `--debug`, `Build.shared` and `flan run --target=` are refused for +`web` exactly as for `wasm32`, each naming `web` rather than `wasm32` in the message. `--sanitize` is refused too, but +the web half of that refusal is weaker than the wasi half and says so: emscripten *does* ship an ASan, and nothing here +has ever run it. A sanitizer that has never been run is one whose silence means nothing. + +**What the test can honestly check.** `test/test_web.ml` is headless and permanently so. It probes — emscripten may not +be installed, and the raylib archive is not in the tree — and skips with the reason rather than going red. What it +asserts: the three files exist, the module starts with `\0asm`, the page references its own JS and carries the canvas, +and node runs the emitted JS and gets `ok`. For raylib it builds `core-basic-window.flan` unchanged and then reads the +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. diff --git a/NEXT.md b/NEXT.md index 4f2b851..565cfd3 100644 --- a/NEXT.md +++ b/NEXT.md @@ -295,6 +295,44 @@ crosses as a parameter — a C function that returns one returns something Flan the shape the language already has. Three constructs unexercised anywhere else in the repo worked first try: a fixed array with a struct element, a 2-D struct array, and `[N string]` as both `defconst` and mutable `defvar`. +### The web target: what it does not reach yet + +`flan build --target=web` works, a raylib example builds unchanged and `test/test_web.ml` is green — see BUILT.md, +"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`.** `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 ...)` +unconditionally, so `Reach` cannot prune the package, so the flagship program stops at that error — even without +`--dev`. Beneath the include is a structural fact worth deciding rather than patching around: **the agent is a socket +server and the browser has no sockets**, which is the same family as the `--dev` refusal. So the two fixes are not +equivalent — add the include and the agent compiles into a web build that can never accept a connection, or refuse +`vendor:agent` by name on a web target the way `--dev` is refused. The second is the honest one. Neither was taken +here: `vendor/agent/` belonged to another lane this session. + +**2. Assets are two questions and only one of them is about emscripten.** `sand.flan` does +`(rl/load-texture "brush.png")` against a bare relative path. + +- The easy half: a bare relative path has no meaning on a target with no filesystem. emscripten's answer is + `--embed-file` or `--preload-file` into MEMFS, and both are *linker arguments*, so they are already expressible as an + `@web` line in a package's `link` file. No new mechanism is needed for a package. +- The hard half, and the actual design question: **the file that needs the asset is structurally the one file that + cannot declare it.** `Load` hands out `lflags` only for a directory package (`one_file` → `[]`), and `main` is not + exported, so a program can never be a package. The program doing the `load-texture` therefore has no link channel at + 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.** 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. + +**4. Unmeasured and untested.** Asyncify's cost is quoted from emscripten's documentation (roughly a doubling of code +size) and not measured here, and no frame time on web has been taken at all. raylib's audio and any use of threads on +the web target are untried. And a **wasi** build that reaches raylib now fails on undefined symbols rather than on a +missing `-l:libraylib.so.550`, because that line is tagged `@native` — the same error one step later, and a worse +message. + ### `break`, and why it was not built Settled, so the next attempt is cheap rather than a rediscovery: