Three roles want three words, and two of them want the same dependency tracking

This commit is contained in:
Joseph Ferano 2026-09-12 13:51:40 +07:00
parent f1223cde2e
commit a8a032e340

View File

@ -68,18 +68,41 @@ function behind an indirection cell, or a constant by its source name, needs no
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`
## 3. `def`, `defvar`, `defconst` — three roles, currently two words
There is no `def`. There is `defvar` (mutable global) and `defconst` (compile-time constant, folded).
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 proposal: a `def` that is mutable, where `defvar` only rewrites the value if the variable is *new* — Common Lisp's
actual `defvar` semantics, where re-evaluating a `defvar` deliberately does not clobber a value you have been building up
at runtime. That distinction matters much more here than in most languages, because `C-c C-k` on a whole buffer
re-evaluates every top-level form against a *running* program, and today that resets state you may have spent a session
accumulating.
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.
`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.
**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