# Let's discuss Open questions, raised and deliberately not answered yet. Nothing here is a decision or a task. Each entry is the question as asked, plus what is already known in this repo that bears on it — so the investigation starts from what exists rather than from scratch. Settled decisions live in `NEXT.md`. Reasons for what already exists live in `BUILT.md`. --- ## 1. `i`, the inspector, and the frame it cannot see Two things got conflated here and they should be separated. **What the inspector already does.** Most of what was asked for is built. `flan-inspect` opens its own buffer, lays a value's fields one per line, `RET` walks into one, `l` comes back, `g` re-reads. The renderer bounds its walk at depth 4 and span 8, and entering a field renders *that field* from depth 0 — so the elision moves with you rather than truncating permanently. It is CIDER's inspector adapted, and the file says what the adaptation changed. **What is actually missing** is detail on the leaves: a number shows in decimal only, with no hex and no binary, and a pointer does not show its address. Purely additive, small, and worth doing. **The real problem, and it is sharper than "a bug".** The locals listing renders from each frame's own slot addresses, so it is frame-accurate. The inspector is built on a stack of **expressions** — going into a field means sending a different expression (`(.pos b)` where the last was `b`), and `l` works by popping back to the previous one. That design is forced: a Flan value has no header, the thunk that rendered it is `dlclose`d as soon as it returns, and there is no heap to retain anything in, so nothing can be held server-side the way CIDER holds a JVM object. The consequence is that `i` evaluates a name wherever the evaluator stands, **not in the frame being looked at**. On the innermost frame that happens to be right. On any other it may resolve to a global, to a different binding, or fail — with nothing saying so. **An earlier suggestion in this conversation — "root the inspector at the slot's address" — does not work**, and the reason is worth keeping: an address is not an expression, so the first `RET` has nothing to build the next expression from and navigation dies at step one. Recorded because it is the obvious fix and it is wrong. So the options are genuinely three, and none is free: 1. **Teach the program to evaluate an expression relative to a frame.** The most useful and the most work: the frame's slots would have to be in scope for a compiled thunk, which means the daemon building a thunk whose free names bind to that frame's addresses. It would also fix `C-x C-e` while stopped, which has the same blindness. 2. **Give the inspector a second rooting mode** — an address root that can still walk, by carrying a type alongside the address and stepping to a field's address rather than to a sub-expression. Navigation then works, but the two modes have different capabilities and `l` has to cross between them. 3. **Refuse `i` outside the innermost frame**, honestly and by name. Cheapest, and it gives up the feature exactly where it is most wanted, since the innermost frame is the one already fully visible. ## 2. Annotating the IR and the disassembly with the source **The IR half is nearly free and should just be done.** `emit.ml` writes `.ll` as text, so a comment costs nothing and cannot break anything, and every typed IR node already carries a `Loc.t`. **The disassembly half, with the optimisation question settled.** `objdump` already interleaves source into a listing when DWARF is present (`-S`), and the daemon already shells out to objdump — so the `-O0` case is close to a flag. Settled in conversation: **`-O0` is expected to follow the source and gets the full annotation; `-O2` is not expected to and gets either nothing or whatever best-effort mapping falls out.** That removes what looked like the blocking question. `--debug` forcing `-O0` is therefore fine and does not need decoupling for this. **How SBCL does it, since it came up.** SBCL does *not* shell out. `sb-disassem` is its own disassembler, written in Lisp, that knows the instruction encodings directly. What that buys is annotation from the inside: it labels constants the function references, names the functions being called, marks entry points, and shows its own calling conventions — because it compiled the code object and still holds the metadata. The relevant observation is that **this project is closer to SBCL's position than the objdump route suggests.** The daemon owns the build and holds the metadata too; it simply is not feeding much of it into the listing yet. Naming the function behind an indirection cell, or a constant by its source name, needs no instruction decoder — only the information the daemon already has. Writing a disassembler is not the interesting part and should stay off the table; richer annotation of objdump's output is cheap and is where SBCL's advantage actually comes from. ## 3. `def`, `defvar`, `defconst` — three roles, currently two words There is no `def`. `defvar` is a mutable global that is always re-initialised on redefinition; `defconst` is a compile-time constant, folded into its use sites. The problem in practice: `C-c C-k` re-evaluates every top-level form against a *running* program, so today it wipes state you may have spent a session accumulating. **Settled in conversation — three roles, three words:** - **`def`** — always re-initialised. The tweakable: some number you are adjusting and do not care about preserving. - **`defvar`** — re-initialised *only if it would come out different*. For heavier initialisers: an N×M grid that should recompute only when the rows or columns actually changed. - **`defconst`** — folded, and reserved for what genuinely cannot change for the life of the program. `rows`/`cols` in `sand.flan` are honest `defconst`s: the program does not support resizing on the fly and changing them means resetting everything anyway. Note this **`defvar` is not Common Lisp's**, which is "assign only if unbound". This one is value-dependent, which suits a live-edited game better and is the harder of the two to implement. **The open question is what "different" means**, and the grid example is what makes it sharp. If the grid is sized by `rows` and `cols`, the initialising expression's *text* is unchanged when `rows` changes — only its value is. So: 1. **Compare the expression** (textual or AST). Cheap, and does not catch the case above. 2. **Compare the expression plus everything it depends on.** Catches it. Needs dependency tracking. 3. **Evaluate and compare the result.** Correct, and defeats the entire purpose whenever the initialiser is expensive — which is the only case this feature exists for. Option 2 is what is wanted. **The reason to do it: it is the same machinery `defconst` needs.** A folded constant cannot be tuned live, because its value is baked into every function that used it — so the values you most want to tweak while the game runs are exactly the ones you cannot. Fixing that means knowing which functions depend on which constants, and rebuilding those. That is the same dependency tracking `defvar`'s "if different" requires. Built once, it buys live-tunable constants *and* the conditional re-initialisation. Neither is worth building alone; together they are clearly worth it. ## 4. Structural typing, row polymorphism, anonymous structs The goal as stated: make this feel Clojure-like while still being typed. **The representation is already structural.** `types.ml` does structural equality on resolved types, and a Flan struct is exactly its C layout with no header and no tag word. What is nominal is the *checking*, not the data — so this is a front-end question, not a data-model change. **What it buys, in rough order of value here:** 1. **Your destructuring already looks like this, and that is the strongest argument.** Clojure's `{:keys [x y]}` works in binding position today. Structural typing makes the same notation work in *parameter* position — the pattern you write to pull fields out becomes the signature saying what you accept. One notation, two places, no separate declaration. That is precisely the "Clojure-like but typed" feel being asked for, and it needs no new syntax, only a new meaning for syntax that exists. 2. **Functions over "anything with these fields".** Magnitude over anything with an `x` and a `y` — a position, a velocity, an enemy, a bullet. In a game this recurs constantly because everything has a position. 3. **Ad-hoc returns.** Returning a hit flag and a point without declaring a type for the pair. The Clojure habit of returning a map, typed and free. 4. **The FFI stops needing conversions.** raylib's `Vector2` and a locally-defined vector are byte-identical and are today two nominal types. Structurally they are one. **Two costs, and the second is the real work:** - **It cuts against the project's standing instinct.** This language has repeatedly chosen explicit over convenient: no implicit numeric conversions, a bare integer cannot become an enum, `:spcae` must fail at the call site. Structural typing says things are compatible when they happen to line up, which is the opposite reflex. Worth deciding deliberately rather than drifting into — and worth asking whether structural compatibility should be *written* at the site that relies on it, the way `(GamepadAxis n)` made an enum conversion explicit without making it implicit. - **Field order is layout, and that is the hard part.** `{x f32, y f32}` and `{y f32, x f32}` have the same fields and different memory. So either field order becomes significant — surprising, since nothing else about a structural type should be — or matching a differently-ordered type requires a copy, which breaks the zero-cost property the whole data model rests on. This is the question to settle first; the type checking is comparatively easy. Also unresolved: the shim generates a C typedef per *named* struct, and an anonymous struct has no name to generate one from. Either anonymous structs cannot cross the FFI, or the generator learns to name them. ## 5. A performant JS transpiler Asked as a feasibility question. Bear in mind the web target already exists and ships real machine code via wasm, so this is not the only route to a browser and the case for it needs stating: smaller artifacts, no wasm toolchain, debuggability in browser devtools, or something else. The hard parts are the ones the wasm target got for free from clang: the memory model (Flan is pointers and explicit layout; JS is not), the FFI, and the fact that the whole raylib layer is C. A JS backend that cannot run raylib is a different product from the one that can. ## 6. C interop as seamless as Zig's Today: `declare-c` names one C function per line, and the compiler generates the wrapper, the typedefs and the flattened declaration. 175 of them for raylib. **No header is ever read, deliberately** — which means nothing can check that a declaration matches the real signature, and that is written down as trusted rather than guaranteed. The proposal is to read the header, prefix a namespace, and get `rl/InitWindow` for free — plus possibly automatic kebab-casing to `rl/init-window`. This is a large change in kind, not just in size: it means a C parser or a libclang dependency in the build, and it trades an explicit, checkable list for an implicit surface. Worth weighing against what `declare-c` already buys, which is that a binding is one line and the wrapper is generated. The auto-kebab-case question is separable and much smaller — and note the FFI currently keeps C's own spelling on purpose, so the mapping would need to be reversible. ## 7. Where `defclass` stands Specified in `plan.org` and **deliberately not started** — its own last line says nothing happens until ordinary `struct`, `Handle` and reload semantics work. Those have largely landed since that was written, so the gate may be closer than the document assumes. What it is meant to add: identity, runtime shape metadata, an implementation-defined representation, generic-function dispatch, and live schema change with an explicit migration at a frame boundary — which is the answer to the one thing redefinition still cannot do, changing a struct's layout while instances exist. Three findings from an earlier review, recorded in `NEXT.md` and not yet in `plan.org`: - **A generic function is a cell.** Adding a method from a later module is the same problem indirection cells already solve, so the expensive half is built and tested. - **The pool is not one storage option among three.** `migrate-instances` has to enumerate live instances, which a pool behind a generational `Handle` gives by construction and the other two options do not. - **Every layout version must stay resolvable** for as long as any instance holds it — the same rule as "nothing is ever `dlclose`d". Also open and related: whether a *condition* can be a class, which decides whether handler matching has one path or two. `NEXT.md` records the argument; decision 4 there took the cheaper parent-link route for now and explicitly left real inheritance possible later. ## 8. Watching variables Raised while designing item 1, and deliberately separated from it. Item 1 shows globals *per frame*, chosen by what the code in that frame references. That is the right default and it cannot cover the case where the thing you care about is not named by the frame you are standing in — stopped deep in a helper, still wanting to see the grid. So: a way to say "always show me this", surviving a resume and the next break. Questions it opens: - **What can be watched.** A global is easy — it has a name and an address that does not move. A *local* is harder: it belongs to a frame that is gone as soon as you resume, so "watch `y`" either means a different `y` every time or means nothing. A watched expression — `(at velocity 40 12)` — is the most useful and the most expensive, since it has to be compiled and run in the program each time, which is what `eval-expr` already does. - **Where the list lives.** Per project, per session, or in the file. A watch list that vanishes when Emacs restarts is one you stop using; one in the repo is one you accidentally commit. - **When it updates.** Only on entering a break is cheap and probably enough. Live-updating while the program runs is a different feature — closer to a HUD than a debugger — and worth not conflating. - **Whether it belongs in the break buffer at all**, or in its own window that is useful while the program is *running*, which is arguably where a game developer wants it.