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 ## 1. What is `i` supposed to inspect in the stack section?
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?
Raised as a question about intent, and it deserves one, because the current answer may be wrong rather than merely Raised as a question about intent, and it deserves one, because the current answer may be wrong rather than merely
undocumented. 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 rooted; refuse `i` outside the innermost frame; or make the inspector frame-aware. Worth settling before anyone relies
on it. 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 The compiler should interleave the originating Flan expression into both the emitted LLVM IR and the disassembly
listing. 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 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. 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). 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 `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. 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 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 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 with the planned managed classes, which are explicitly the *opposite* direction — identity and metadata rather than plain
layout. 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 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 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 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. 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 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. 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 — 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. 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 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 `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 `NEXT.md` records the argument; decision 4 there took the cheaper parent-link route for now and explicitly left real
inheritance possible later. inheritance possible later.
## 10. Watching variables ## 8. Watching variables
Raised while designing item 1, and deliberately separated from it. 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 sharing, which plan.org rules out by name because shared structure destroys the clear ownership that is the whole
reason there is no collector. 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 ## Blocked and unfinished
Everything below was found, decided or half-built and then stopped. Each says what blocks it. Nothing here is a 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. ;; over a mutable scan position, which is what a while loop is.
(defn settle [row i32 col i32] (defn settle [row i32 col i32]
(let [vel (+ gravity (at velocity row col)) (let [vel (+ gravity (at velocity row col))
some-point (rl/Vector2 {:x 15.0 :y 12})
y (min (- rows 1) (+ row (i32 vel)))] y (min (- rows 1) (+ row (i32 vel)))]
(while (> y row) (while (> y row)
(when (empty-at? y col) (when (empty-at? y col)
@ -108,8 +109,8 @@
(return)) (return))
(let [left? (and (> col 0) (empty-at? y (- col 1))) (let [left? (and (> col 0) (empty-at? y (- col 1)))
right? (and (< col (- cols 1)) (empty-at? y (+ col 1)))] right? (and (< col (- cols 1)) (empty-at? y (+ col 1)))]
(pause)
(when (or left? right?) (when (or left? right?)
(pause)
(let [side (cond (let [side (cond
(not left?) 1 (not left?) 1
(not right?) -1 (not right?) -1