# Porting siam-farmer What the author's game needs from Flan that Flan does not have yet, measured against the code that exists today rather than against a plan. `~/Development/siam-farmer` holds two implementations of the same game. The Clojure one (`src/*.clj`, ~1500 lines) is the complete thought. The Common Lisp one (`src/engine.lisp`, `src/game.lisp`, `src/tilemap-blob.lisp`) is the author working the same design through a compiled, non-lazy, manually-managed host. **Where the two differ is the most reliable evidence in this report**: every place the Lisp port abandoned a Clojure construct is a place Flan does not have to supply one, and the author has already paid the cost of finding that out. **Coverage.** I read, in full: `src/engine.clj`, `src/game.clj`, `src/rl.clj`, `src/sprite_atlas.clj`, `src/tilemap_blob.clj`, `src/util.clj`, `src/check.clj`, `src/watch.clj`, `src/editor.clj`, and the whole Lisp port — `src/engine.lisp`, `src/game.lisp`, `src/tilemap-blob.lisp`. On the Flan side: `vendor/raylib/raylib.flan` and its `link`/`headers`/`build-web.sh`, `sand.flan`, `lib/prelude.ml`'s export surface, `lib/check.ml`'s container and global rules, `runtime/flan_rt.c`'s trap path, `lib/dev.ml`'s request gating, and the relevant `test/programs/*.flan`. I did **not** read `MEMORY.org` or `TODO.org` in siam-farmer, its Emacs side (`watch.el`, `cider-error.el`) beyond what the Clojure sources say about them, or most of Flan's `plan.org`/`BUILT.md` outside the FFI sections. Everything below points at a file. Nothing below is hypothetical. --- ## 1. The one thing that cannot be written at all ### `DrawTexturePro` is not bound Every tile in this game is drawn by one call: - `src/game.clj`, `draw-tile!` — `rl/draw-texture-pro!*` with a source rect into the atlas, a destination rect on the grid, an origin, a rotation and a tint. - `src/game.lisp`, `draw-tile` — the same call, same six arguments. - `src/game.clj`, `draw-game` — again for the palette overlay, once per palette cell. It is not in `vendor/raylib/raylib.flan`. I grepped for both spellings across `vendor/`, `lib/`, `test/` and `tools/`; there is no `DrawTexturePro` and no `draw-texture-pro` anywhere in the tree. Nothing else in the hand-written 172 does the job. `DrawTextureRec` takes a source rect and a position but no scale, and this game draws 16px tiles at 4x. `DrawTextureEx` takes a scale but no source rect, and this game draws one cell out of an atlas. The two together are exactly the two halves `DrawTexturePro` puts in one call, and neither half is usable alone. **Blocker.** Not "awkward" — the renderer of this game cannot be expressed. The grid draw, the palette, and the tile riding the cursor are all of the drawing there is. **Cost to fix: one line.** The function's signature mentions only `Texture2D`, `Rectangle`, `Vector2` and `Color`, all four of which `raylib.flan` already describes, so it is a `declare-c` beside `draw-texture-rec` and nothing else. **Why it did not already land.** It is importable — with `FLAN_RAYLIB_H` exported it comes in with the other 256. But `vendor/raylib/headers` says the header is opt-in on purpose, so that a build needs libraylib linkable and *not* raylib-devel installed. That property is worth keeping, and it means the default build of this game has no renderer. The general rule this exposes is worth more than the one line: **a function the game calls every frame should be hand-written and checked against the header, not left to the import.** The import widens the surface; it should not be load-bearing. Three more the same way, none of them blockers, all one line each: | Missing | Used at | Verdict | |---|---|---| | `ImageFromImage` | `sprite_atlas.clj` `auto-select-tiles`; `tilemap-blob.lisp` `tile-subimages` | Annoyance — see §3 | | `IsWindowReady` | `engine.clj` `run-game!`, `engine.lisp` `run-game` — the "window already open" guard | Annoyance; a `defvar bool` does the same thing | | `SetTextureFilter` | declared in `rl.clj`, called nowhere | Not needed. Point is raylib's default | | `UpdateTexture` | declared in `rl.clj`, called nowhere | Not needed | | `SetClipboardText` | `sprite_atlas.clj`, in a `comment` block only | Not needed | | `GetMouseWheelMoveV` | declared in `rl.clj`; the atlas tool uses the scalar form, which *is* bound | Not needed | And one enum member, not a function: **`raylib.flan`'s `Key` has no `left-shift`.** Both `game.clj`'s `handle-game-input` and `game.lisp`'s do shift+1..5 to pick the tilemap, via `KEY_LEFT_SHIFT` (340). The `defenum` carries a deliberate subset — the keys `sand.flan` uses — and this is one member short of what this game needs. One entry. --- ## 2. What looks like a gap and is not This is the half of the report that should stop work rather than start it. Each of these is a Clojure habit the Lisp port already dropped, or a Flan answer that is simply spelled differently. ### Persistent maps as game state — already abandoned `game.clj` threads an immutable map through `update-as->` (`util.clj`), `cond->`, `assoc`, `dissoc`, `reduce`-with-`reduced`. `check.clj` exists only to police the shape of that map at runtime, with `malli`, including `{:optional true}` keys because `dissoc :editor-mode` is how the editor closes. `game.lisp` threw all of it away. The state is a `defclass` with fixed slots; `update-game` mutates through accessors and returns nothing; `drag-mode` is a slot holding `nil` rather than a key that comes and goes. In Flan that is a `defstruct` mutated through a `(Ptr Game)`. `check.clj` does not get ported — it disappears, because the compiler is already doing it and does it earlier. No feature is needed. ### `reduce` with `reduced` for early exit — `break` covers it Three sites: `set-random-tile!`'s weighted pick, `handle-game-input`'s number-key scan, `update-game`'s palette hit test. All three are "walk until something matches, then stop". Flan has `break` with loop labels. `game.lisp` had already rewritten all three as `loop … return`. **`loop`/`recur` and tail calls are not needed by this game.** Nothing here recurses. ### Escaping closures and capture — needed once, and trivially avoided I checked every function-valued thing in both implementations: - `run-game!`'s `:init`/`:update`/`:draw`/`:unload`/`:watch-fn` — `#'` vars in Clojure, bare symbols in Lisp. Top-level names, no environment. Flan's `(Fn [...] R)` takes them as-is, and `fn-values.flan` shows exactly this shape. - `do-neighbors!`'s `do-fn` — non-escaping, called and dropped. - `auto-tile!` passes `do-neighbors!` a literal `(fn [l r c] … game …)`. **That one captures `game`.** `fn-capture.flan` refuses it by name. The fix is to give the callback the parameter: `(Fn [(Ptr Game) i32 i32 i32])`, and `do-neighbors` forwards the game it was handed. One extra parameter. Note also that `game.lisp`'s `do-neighbors` is currently dead code — `auto-tile` calls `tile-bitset` directly — so even the one capture is in a path the port has not revived. **Escaping closures are not on this game's path.** Nothing stores a function with an environment; nothing needs to. ### `Result`/`try` — nothing uses that discipline Neither implementation returns errors as values. Both use the host's exception or condition system throughout. Porting this game exercises `signal`/`error`/`restart-case` and never wants a `Result`. ### `handler-case` — one site, and `handler-bind` already covers it `engine.clj`'s `reload-config!` is `(try … (catch Exception e (println e)))`. That is "log it and carry on", which is what a `handler-bind` clause returning normally does. ### `Handle` and pools — nothing to pool The texture cache is five entries keyed by path (`engine.clj` `load-texture-once`, `engine.lisp` `*texture-cache*`). There are no entities yet, no spawning, no freeing mid-frame. Handles and pools have no customer in this code. ### User-written allocators, structural typing — no customer either Nothing in either implementation parameterises over allocation, and nothing depends on a type's shape rather than its name. ### Reading `.edn` / `.data` files at runtime — `vendor:edn` already does it `engine.lisp` `read-data`, `game.lisp` `load-tileset` and `read-bitmask` read plists out of `assets/*.data` at startup, and `reload-config!` re-reads the config while the game runs. `vendor/edn/edn.flan` is a 561-line tokenizer with `int-of` / `text=?` / `keyword=?` / `expect`, and `test/programs/edn.flan` includes a hand-written struct reader as the worked example. So this is writable today. It costs a hand-written reader per schema — two here, the tileset (`:texture-path`, `:selected-cells`) and the bitmask table — because `(read-edn T bytes)` is not built. Call it ~80 lines, once, at load time. See §3 for whether it should be written at all. --- ## 3. Real friction, with what the workaround costs Ordered by how much of the game it touches. ### Globals cannot hold a `Vec` or a `Map` `lib/check.ml:4039`, `no_move_only_global`. Both hosts keep the texture cache in a global (`engine.clj`'s `texture-cache` atom, `engine.lisp`'s `*texture-cache*` hash table), and `engine.lisp` keeps `*buffers*` there too. `sand.flan`'s idiom — everything in `defvar` fixed arrays, the loop functions taking no state — works because nothing it holds is move-only. This game's state is not like that: `tilesets`, `src-rects`, `dst-rects` and the bitmask table all want a `Vec` or a `Map`. **Annoyance.** Two workarounds, both cheap, and the second is better than what either host does: 1. Hold the state in a `let` in `main` and pass `(Ptr Game)` to update and draw. Costs one parameter on three functions. 2. For the texture cache specifically, a `[8 CacheEntry]` fixed array with a count beats a `(Map string Texture2D)` outright — the game loads five textures, and a linear scan of five path comparisons at load time is not worth a hash table. This is a case where the restriction pushes toward the right answer. ### A `Vec` cannot hold a move-only element, and a `Map` cannot hold a move-only value `lib/check.ml:412` and `:317`. `game.lisp`'s shape is `tilesets` = a vector of `tileset` structs, each of which owns `tiles`, `src-rects` and `dst-rects` vectors. That is `(Vec Tileset)` where `Tileset` owns three `Vec`s, and it is refused — recursive teardown waits on `drop`, which `NEXT.md:484` records as deferred, not built. **Annoyance.** The fix is at the *inner* level, not the outer one: making the outer container a fixed array changes nothing, because `[4 Tileset]` where `Tileset` owns `Vec`s is still move-only transitively (`lib/check.ml:3831`). Make `tiles`, `src-rects` and `dst-rects` fixed arrays with counts, and then nothing in the state is move-only at all. This game has exactly one tileset and about twenty palette cells. Cost: the palette's capacity becomes a constant instead of growing. That is not a loss at this size. Flag it anyway, because the next thing this game grows — several tilesets, several atlases — grows straight into it. ### Generics: an annoyance here, not the blocker the plan says it is I looked for every place this game changes element type, because that is the case the project has queued generics for. - `game.clj` `compute-tileset-rects` / `game.lisp` the same — `[r c]` pairs to `Rectangle`s. Twenty elements, once, at load. - `game.clj` `init-game`'s `mapv` over four `[keyword path]` pairs to textures. Four elements, once. - The prelude is monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so anything over `[r c]` pairs has no helper at all. All three are `dotimes` with a `push`. Five lines each, at load time, run once. **The reusable-engine argument does not survive either.** `engine.clj` is shared by `game.clj` and `sprite_atlas.clj` with two different state types, which looks like it forces `run-game` to be generic over the state. It does not: with the state in the game's own storage and the engine taking only `(Fn [] Unit)` callbacks, the engine never names the state type. `sand.flan` is already written that way. The frame rollback is the one piece that needs to touch the state, and the game can supply `snapshot`/`restore` as two more callbacks. **This verdict is conditional, and the condition is one decision.** Both it and the globals workaround above hang on the same pivot, so it is worth stating once, flatly: *nothing in this game's state needs to be move-only.* Make the inner collections fixed arrays with counts, and then the state fits in `defvar` globals, the engine takes only `(Fn [] Unit)` callbacks and never names a state type, and generics stays off the critical path. Keep `Vec`s in a let-bound `Game` struct passed as `(Ptr Game)` instead, and `run-game` names `Game`, two programs with two state types need two engines, and generics is back on the critical path immediately. The recommendation is the first branch. **On that branch: generics is worth building, and it is not what is standing between Flan and this game.** Saying otherwise would send the work in the wrong direction. If the goal is siam-farmer running, generics is not the next thing. ### A bad index ends the process, and the frame loop is built on surviving one This is the finding with the widest consequences, and it is about the dev loop rather than the language. Both hosts survive a bad frame and keep the window open: - `engine.clj` `run-game!` snapshots every `IntGrid` at the top of the frame, catches `Throwable`, restores the grids, and parks in a draw-only loop until the error is cleared. - `engine.lisp` `run-frame` wraps update and draw in a `restart-case` offering `retry-frame`, `skip-frame` and `reinit`, each of which rolls the grids *and* a MOP shallow-copy of the state object back. Both files carry a deliberate blow-up on the `E` key (`game.clj` `handle-game-input`, `game.lisp` the same) whose only purpose is exercising that path. Flan has the shape already — `sand.flan`'s main loop is `restart-case` around `(agent/poll)` and `(game-update)` with a `continue` restart, which is `skip-frame` under another name. Two things are missing behind it: 1. **The rollback.** `sand.flan`'s `continue` abandons the frame but restores nothing. This game's grids are mutated in place by `set-tile`/`delete-tile`, so an abandoned frame leaves half-written state. In Flan this is *easier* than in either host — fixed arrays are values, so `(set backup grid)` is the whole of `snapshot!` and there is no `sb-mop:class-slots` walk to write. It just has to be written. 2. **A bounds failure is not a condition.** `runtime/flan_rt.c` `flan_bounds_fail` prints and calls `rt_die`, which is `exit(134)`. No handler runs, no restart is offered, and `lib/dev.ml` answers every subsequent request with "the program exited; restart flan dev". That is not theoretical for this game. `game.clj`'s `update-game` computes `row` and `col` straight from the mouse position and indexes the grid with them, with no bounds check anywhere. **`game.lisp` added `in-bounds-p` and calls it in `update-drag`** — the author hit this and fixed it in the port. But the Clojure version survived the bug because a bad frame was recoverable, and under Flan the same bug is the end of the session. **Not a blocker for writing the game** — the game can be written, and the check can be written. **It is a hole in "never restarting"**, and this game walks into it by the most ordinary route there is: a mouse coordinate one pixel outside the window. Worth separating honestly: the `(+ 1 :asdf)` blow-up both hosts carry is a *type* error, and Flan rejects it at compile time. Part of that rollback machinery is answering a dynamic-language problem and is not a gap. The out-of-bounds path is the part that survives the port, and it is the part that matters. ### The watch buffer has no equivalent `watch.clj` is a pull-based watch: the game drops a snapshot of labelled values into an atom once per frame, and Emacs polls it on its own timer, so the watch rate is decoupled from the frame rate and nothing is pushed over nREPL. `spy` records one value per label; `spy-num` keeps count/min/max/last/mean in a `double-array` so it survives being called thousands of times a frame without allocating. `game.clj`'s `watch-game` publishes the mouse position and the whole state minus the three big collections every frame. Flan's inspector is **stopped-only**: `lib/dev.ml` `globals_op` refuses a running program by name — "globals are read against a stopped stack, and the stack is what decides which of them to show". That is the right design for a break loop and it is not what this is. Watching a value while the game plays at 30fps is a different tool. **Annoyance, and a heavily-used one.** The author built this deliberately (the docstring argues the design), built the Emacs side for it, and built a second numeric variant for hot loops. A game that stops to be inspected is not the same workflow as a game that shows you a number while you drag the mouse. Whether `flan dev`'s `eval` against a running program could serve it — it does not gate on `Running` the way `globals_op` does — I did not test, and should not be assumed. ### Offline image tooling `sprite_atlas.clj` `auto-select-tiles` carves the atlas into 16x16 subimages and keeps the ones that are not fully transparent. `tilemap_blob.clj` / `tilemap-blob.lisp` carve a tileset the same way and sample eight edge pixels per tile to compute an autotiling bitmask. Both go through `ImageFromImage`, which is not bound. **Not a gap, mostly a rewrite.** `GetImageColor` *is* bound, and `Image.data` is a `(Ptr u8)`, so both jobs read better against the whole image with computed coordinates than against a subimage per tile — the bitmask sampler in particular is eight `get-image-color` calls at offsets derived from the tile's origin, and never needs a subimage at all. `ImageCrop` is bound but mutates in place and there is no `ImageCopy`, so the literal transcription is the one that does not work. `sprite_atlas.clj` also opens a `java.awt.FileDialog` to choose where to save. Nothing in Flan or in raylib 6.0 offers that. Workaround: write to a fixed path under `assets/`. Tool-only, and the tool is run by the author. ### String formatting `sprite_atlas.clj`'s HUD draws `(format " X: %d" (int mx))` every frame. Flan has `append-i64!`, `concat`, `join` and `format-f64`, and no printf. Cost: a few lines and a reusable buffer per HUD string, since doing it naively allocates per frame. Small, and only the atlas tool draws text today. ### `reload-config!` may not want porting at all `engine.clj` re-reads `game-config.edn` from disk while the game runs so the config can be edited live. Flan's answer is compile-time embedding plus `flan dev` recompiling into the running process, which reaches the same place with no file watcher, no runtime parser, and no window where the file on disk and the value in memory disagree. **This is a case where porting the mechanism would be porting the workaround.** --- ## 4. What Flan does better than either host Worth recording, because these are places the game gets shorter rather than longer. - **Conditions and restarts, natively.** `engine.clj` reaches them by printing a `SIAM-REPORT-EXCEPTION` marker line, stashing the throwable in an atom, and having `cider-error.el` watch the REPL output and rethrow it over nREPL so it lands in a stacktrace buffer. That is a marker line, an atom and an Emacs Lisp handler simulating what `engine.lisp` gets from `restart-case` in eight lines. Flan is on the Lisp side of that, with typed restarts, and `dev-break.flan` shows the editor driving them. - **Frame snapshot is an assignment.** `engine.lisp` walks `sb-mop:class-slots` to shallow-copy the state object, with a comment explaining that it copies *into* the existing object so a REPL reference stays live. In Flan the state struct and the fixed arrays are values; `(set backup state)` is the whole thing, and there is no aliasing question to answer. - **`check.clj` does not get ported.** 56 lines of malli schema, a `:closed true` map, and a compile-time flag to switch it off in release — all to catch a typo'd key. A struct field is checked by the compiler, earlier, for free, and the release flag has nothing to switch off. - **Asset paths stop being a hazard.** `engine.lisp` `resource-path` exists because raylib resolves paths against the process working directory, which under SLY is wherever the inferior Lisp started, and a miss is silent — `LoadTexture` hands back a texture with id 0. The function resolves against the ASDF system directory and `probe-file`s it, and `load-texture-once` checks for id 0 afterwards anyway. Flan's compile-time embedding of a file or a directory, plus the bound `LoadImageFromMemory` and `LoadTextureFromImage`, removes the failure mode instead of detecting it. - **`num-keys` indexing.** `game.lisp`'s key loop reads `(aref *num-keys* n)` over a literal vector of keywords. Flan's `defenum Key` resolves a keyword against its members at compile time and a typo is an error there. --- ## 5. The raylib binding, checked against what the game actually links siam-farmer vendors raylib **6.0** (`vendor/raylib-6.0/include/raylib.h`, `RAYLIB_VERSION "6.0"`). Flan pins **5.5** — `vendor/raylib/link` names `libraylib.so.550` and `build-web.sh` pins the web archive to tag 5.5, both with comments explaining that two targets built from different raylibs would disagree about layouts silently. **I diffed every struct on this game's path against the 6.0 header: `Vector2`, `Color`, `Rectangle`, `Texture`/`Texture2D`, `Image`, `Font`, `GlyphInfo`. All seven are byte-for-byte identical to `raylib.flan`'s `defstruct`s.** Field order, field count and scalar widths all match. Of the C names `rl.clj` declares, I checked nine directly against the vendored header — `DrawTexturePro`, `ImageFromImage`, `GetImageColor`, `CheckCollisionPointRec`, `DrawTextureEx`, `SetTextureFilter`, `IsWindowReady`, `SetClipboardText` and `GetMouseWheelMove` — and all nine exist in 6.0 under the same name with the same signature. **I did not sweep the full ~50.** Those nine are the ones on the game's own path; the remainder is unchecked and should not be read as verified. **Enum values, the other half of what `link` says `raylib.flan` carries.** `KEY_ZERO`=48 through `KEY_NINE`=57, `KEY_SPACE`=32, `KEY_E`=69, `KEY_LEFT_SHIFT`=340, `MOUSE_BUTTON_LEFT`/`RIGHT`/`MIDDLE`=0/1/2, and `LOG_WARNING` as the fifth `TraceLogLevel` member (4) are all unchanged in 6.0 and all match `raylib.flan` — except that `left-shift` is absent from the `Key` `defenum` entirely, which is the §1 note, not a skew. So the version skew is not a correctness problem for this game today. It is still worth closing, because the whole point of `headers` is that a silent disagreement is the failure mode, and "I checked by hand once" is exactly the state `headers` was built to replace. The binding's real gap against `rl.clj` is §1: `DrawTexturePro`, then `ImageFromImage` and `IsWindowReady`. --- ## 6. Ranked: what to build next Split into two tiers, because a one-line binding fix and a month of language work should not compete for the same slot. ### Tier 0 — bindings. Hours, not weeks. Do these first. 1. **`declare-c draw-texture-pro`.** Unblocks the entire renderer. Without it this game has no draw call. One line, plus an acceptance case that makes raylib compute with the source rect so a permuted `Rectangle` goes red. 2. **`left-shift` in the `Key` `defenum`.** Shift+1..5 picks the tilemap in both implementations, and the member is not there. One entry, and without it that input is unwritable. 3. **`declare-c image-from-image` and `window-ready?`.** The atlas tool and the engine's reentrancy guard. Two lines. 4. **Decide the rule the first item exposes.** A function the game calls every frame should be hand-written and header-checked, not left to the opt-in import. Worth writing down, because the next game-shaped program will find the next `DrawTexturePro`. ### Tier 1 — language and tooling, in the order that unblocks the most of this game 4. **A bounds failure should stop the program, not end it.** Route `flan_bounds_fail` through the condition machinery so it reaches the break loop with the restarts that are on the stack, instead of `exit(134)`. This is the single largest gap between what Flan promises and what this game would experience, and the game reaches it through the most ordinary path in it — a mouse coordinate outside the window, which `game.lisp` had to add `in-bounds-p` to survive. Everything else here is a workaround with a known cost; this one ends the session. 5. **A watch for a running program.** `watch.clj` + `watch.el` + `spy` + `spy-num` is a tool the author built deliberately and uses constantly, and the stopped-stack inspector is a different tool for a different moment. Given that the dev loop is the priority, this outranks every language feature below it. The numeric accumulator for hot loops is the part that is least obvious and most valuable. 6. **Frame rollback in the engine pattern.** Not a language feature — the pieces are all there (`restart-case`, struct assignment, fixed arrays as values). What is missing is the worked example showing `snapshot`/`restore` callbacks alongside the `continue` restart, the way `sand.flan` is the worked example for the loop. A page of code and a test program, and the "never restarting" claim gets materially stronger. 7. **`drop`, or recursive teardown.** Unblocks `(Vec T)` where `T` owns a `Vec`, which is `game.lisp`'s tileset shape exactly. Fixed arrays sidestep it at this size, so this is about the version of the game with several atlases, not this one. `NEXT.md:484` already has it deferred with reasons; nothing here argues with that, only records a customer. 8. **Generics.** Real, and worth building — but for this game it is five-line loops at load time, and the reusable-engine argument for it dissolves once the state lives in the game rather than in the engine. **Do not sequence it ahead of items 4–6 on this game's account.** 9. **`(read-edn T bytes)`.** `vendor:edn` already makes the asset readers writable; this removes ~80 lines of hand-written cursor walking for two schemas. Convenience, and it competes with compile-time embedding, which may be the better answer for both files anyway. **Not ranked, because this game does not need them:** escaping closures and capture (one site, fixed by one parameter), `Handle` and pools (nothing to pool), `Result`/`try` (neither host uses that discipline), `handler-case` (one site, `handler-bind` covers it), `loop`/`recur` and tail calls (nothing recurses), user-written allocators, structural typing.