Two questions became decisions, so they move to what is left

This commit is contained in:
Joseph Ferano 2026-09-12 13:30:15 +07:00
parent 244f83fe53
commit f4f61ae8c9
3 changed files with 37 additions and 53 deletions

View File

@ -8,51 +8,7 @@ Settled decisions live in `NEXT.md`. Reasons for what already exists live in `BU
---
## 1. Inspecting globals, separately, in the break buffer
Locals are readable now; globals are not shown anywhere. They are arguably the more useful half in this language today,
because a Flan game keeps most of its state in top-level `defvar`s — `sand.flan` holds its entire grid that way.
What already exists: the daemon's `describe` op returns the program's globals by name, and `Session.render` walks a
concrete type to a printed value. The `eval-expr` path already renders a global by name inside the running program. So
the machinery is there and the question is mostly one of presentation — its own section in the break buffer, folded like
the stack, or a separate buffer.
The real question is **which** globals. A program has hundreds and a listing of all of them is the backtrace problem
again: the thing you want is buried by the thing you don't.
**Direction: one Globals section, scoped to the stack.** Per-frame was considered and dropped. The reason is that a
global is not part of a frame — it is program state the frame happened to touch — so nesting it under one implies an
ownership that is not there, and the same name then appears once per frame that reads it.
So: a section of its own, whose contents are the **union of the globals every frame on the current stack references**.
That keeps the compiler doing the choosing (the reference set per function is already known) without repeating anything,
and without falling back to listing all of a program's globals, which is the backtrace problem again.
Two refinements that came out of the same conversation:
- **Annotate each entry with which frames touch it.** This recovers what per-frame would have told you — "the whole
chain is reading this" reads differently from "only the innermost one is" — at no cost in duplication.
- **Order by the innermost frame that touches it.** A deep stack makes the union large again, and proximity to the
error is the ordering that puts the likely culprit on top.
What this deliberately does not solve: a global that nothing on the stack names but that you still want visible — the
sand grid while stopped in a helper that never mentions it. That is watching, and it is item 10 rather than part of
this.
## 2. The break buffer should appear by itself
Today a condition stops the program and the buffer only opens when `C-c C-b` is typed. It should `pop-to-buffer` the
moment the program stops.
What already exists: `flan-dev--absorb` inspects every reply for `:stopped`, and there is a poll for the case where no
reply is pending — the client already *knows* the moment it stops, and already updates the mode line from it. So this is
a hook at a point that exists, not new plumbing.
To decide: whether it steals focus or only displays; whether it should also fire for a `(pause)`, which is a deliberate
stop and arguably always wants the window; and what happens when it stops while point is mid-edit in another buffer.
## 3. What is `i` supposed to inspect in the stack section?
## 1. What is `i` supposed to inspect in the stack section?
Raised as a question about intent, and it deserves one, because the current answer may be wrong rather than merely
undocumented.
@ -67,7 +23,7 @@ frame-accurate and the inspector may not be. Options: root the inspector at the
rooted; refuse `i` outside the innermost frame; or make the inspector frame-aware. Worth settling before anyone relies
on it.
## 4. Annotating the IR and the disassembly with the source
## 2. Annotating the IR and the disassembly with the source
The compiler should interleave the originating Flan expression into both the emitted LLVM IR and the disassembly
listing.
@ -80,7 +36,7 @@ already; what is missing is the interleaving.
The IR half is nearly free. The disassembly half is more interesting and more valuable — it is what would let you see
what one line of Flan actually costs, including the indirection cell a dev build puts on every cross-function call.
## 5. `def`, `defvar`, `defconst`
## 3. `def`, `defvar`, `defconst`
There is no `def`. There is `defvar` (mutable global) and `defconst` (compile-time constant, folded).
@ -93,7 +49,7 @@ accumulating.
`defconst` under redefinition is the unclear one, as noted — it is folded into its use sites, so changing one is closer to
a recompile than to an assignment.
## 6. Structural typing, row polymorphism, anonymous structs
## 4. Structural typing, row polymorphism, anonymous structs
Worth noting before this is scoped: `types.ml` already does **structural equality** on resolved types, and a Flan struct
is exactly its C layout with no header or tag word. So the representation is already structural; what is nominal is the
@ -104,7 +60,7 @@ spelling; how this interacts with the FFI, where the shim generates a C typedef
with the planned managed classes, which are explicitly the *opposite* direction — identity and metadata rather than plain
layout.
## 7. A performant JS transpiler
## 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
@ -114,7 +70,7 @@ The hard parts are the ones the wasm target got for free from clang: the memory
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.
## 8. C interop as seamless as Zig's
## 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
@ -128,7 +84,7 @@ trades an explicit, checkable list for an implicit surface. Worth weighing again
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.
## 9. Where `defclass` stands
## 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
@ -151,7 +107,7 @@ Also open and related: whether a *condition* can be a class, which decides wheth
`NEXT.md` records the argument; decision 4 there took the cheaper parent-link route for now and explicitly left real
inheritance possible later.
## 10. Watching variables
## 8. Watching variables
Raised while designing item 1, and deliberately separated from it.

27
NEXT.md
View File

@ -318,6 +318,33 @@ algorithmic cleverness. **jank is not the model** — it is Clojure, so its maps
sharing, which plan.org rules out by name because shared structure destroys the clear ownership that is the whole
reason there is no collector.
## Decided in discussion, queued
**Globals in the break buffer — one section, scoped to the stack.** Locals are readable; globals are not shown anywhere,
and in this language they are arguably the more useful half: a game keeps most of its state in top-level `defvar`s and
`sand.flan` holds its entire grid that way.
Not per frame. A global is not part of a frame — it is program state the frame happened to touch — so nesting it under
one implies an ownership that is not there and repeats the name once per frame that reads it. Instead: its own section,
whose contents are the **union of the globals every frame on the current stack references**, which keeps the compiler
doing the choosing (the per-function reference set is already known) without listing all of a program's globals.
Two refinements decided with it: **annotate each entry with which frames touch it**, which recovers what per-frame would
have told you at no cost in duplication; and **order by the innermost frame that touches it**, since a deep stack makes
the union large and proximity to the error is the ordering that puts the likely culprit on top.
The daemon already has the pieces — `describe` returns the globals, `Session.render` walks a concrete type to a printed
value, and the `layout` op established that a qualified name is an identity the daemon can resolve.
**The break buffer opens by itself when the program stops.** Today a condition stops the program and the buffer appears
only when `C-c C-b` is typed. `flan-dev--absorb` already inspects every reply for `:stopped` and a poll covers the case
where no reply is pending, so the client already knows the moment it happens and already moves the mode line from it —
this is a hook at a point that exists, not new plumbing.
Three things to settle while building it: whether it takes focus or only displays; whether `(pause)` should always take
the window, being a deliberate stop rather than a failure; and what it does when the program stops while point is
mid-edit in another buffer.
## Blocked and unfinished
Everything below was found, decided or half-built and then stopped. Each says what blocks it. Nothing here is a

View File

@ -101,6 +101,7 @@
;; over a mutable scan position, which is what a while loop is.
(defn settle [row i32 col i32]
(let [vel (+ gravity (at velocity row col))
some-point (rl/Vector2 {:x 15.0 :y 12})
y (min (- rows 1) (+ row (i32 vel)))]
(while (> y row)
(when (empty-at? y col)
@ -108,8 +109,8 @@
(return))
(let [left? (and (> col 0) (empty-at? y (- col 1)))
right? (and (< col (- cols 1)) (empty-at? y (+ col 1)))]
(pause)
(when (or left? right?)
(pause)
(let [side (cond
(not left?) 1
(not right?) -1