flan/test/programs/dev-defstore.flan
Joseph Ferano 0c03550999 Evaluating a def assigns, because that is what defparameter means
The author edited (def colors [4 u32] [...]) in his running game, pressed
C-c C-c, and the colours did not change — the same complaint def was built
to answer, one step further in.

The reading behind it was that a re-evaluated def is a promise about the
next re-run, so the session republished the lifted global/<n> and stopped;
nothing called it. That is wrong for the reason the form is named after: def
is Common Lisp's defparameter, and evaluating a defparameter assigns. The
difference from defvar is not "one takes effect at restart", it is "one
takes effect, the other does not touch the value at all".

So a def now does both. The storage takes the new value at the next frame
boundary, carried by the thunk a redefinition module already has — one
Set per re-evaluated def, in the same flan_reload_call the class
registrations use, run after the bodies are published and on the game
thread. And the lifted initialiser is still republished, so the next re-run
runs the edited one; dev-rerun.flan pins that half unchanged.

A brand-new def gets its initialiser run too, which needed one thing from
each backend: a lifted global/<n> asked for by name is neither a sibling nor
one of the target's own lifted clauses, so it had no cell, and a dev call
goes through a cell. Both now give an unknown one a slot of the module's
own, filled from the registry by the installer.

An initialiser that signals leaves the old value alone — the value is
computed whole before it is stored — and offers abandon-evaluation like any
other thunk. A retype is refused first, by the pass that names both types.
defonce is untouched, which is its whole contract; defconst was already the
immediate one, through consts.
2026-09-21 11:03:30 +07:00

86 lines
3.6 KiB
Plaintext

;;;; What evaluating a [def] does to a program that is running right now.
;;;;
;;;; dev-rerun.flan is the other half of the same form: it asks what the
;;;; *next run* sees. This asks what *this* run sees, and the answer is that
;;;; a [def] is Common Lisp's [defparameter] — evaluating one assigns. The
;;;; author's report is the first global here, to the letter: a palette in a
;;;; native array, edited in a live game, with the colours expected to change
;;;; on the next frame and not on the next restart.
;;;;
;;;; One global per shape the store has to reach: a native array, a scalar, a
;;;; struct, a dyn, and one whose initialiser is a call rather than a literal.
;;;; [kept] is the control — a [defonce] whose whole contract is that
;;;; evaluating it again leaves the value alone.
;;;;
;;;; main runs for as long as any of this takes and does nothing else. Every
;;;; assertion is read back out of the program's own storage by the daemon,
;;;; because a printed line is what a run computed and the question here is
;;;; what the storage holds *now*.
(import agent "vendor:agent")
(defstruct Tuning [gain i32 bias i32])
(defstruct Boom [why i32])
;; A call, so that [computed]'s initialiser is lifted for a reason other than
;; the form — and so a brand-new def evaluated into this program has
;; something to call that the process was built with.
(defn twice [] i64 10)
;; What a failed initialiser is made of. The condition goes unhandled, so a
;; def whose initialiser calls this stops the program in a break loop rather
;; than storing anything.
(defn blow [] i64
(error (Boom {.why 1}))
0)
(def colors [4 u32] [0xE6B800FF 0xFF0000FF 0xA83232FF 0xCC6B1FFF])
(def speed i64 3)
(def knobs Tuning {.gain 1 .bias 2})
;; A map and not a dyn number: a number is an immediate word and would read
;; back the same whether or not the store reached the heap.
(def label dyn {:n 1})
(def computed i64 (twice))
(defonce kept i64 7)
;; A constant nothing consumes at compile time, so it is just bytes in the
;; program's memory and a re-evaluation can publish a new value into it. It
;; was the immediate one before [def] was, which is the fact this pins.
;;
;; A float rather than an integer, and that is the whole reason it is
;; written this way: the checker folds integer constants on the way in —
;; an array length has to be a number before any type resolves — and a
;; folded one is in the shape of the running program, where no store can
;; reach it. That one is refused by name, which is a different claim.
(defconst limit f64 40.0)
;; The one [def] spelling with nothing to run: [uninit] lifts no initialiser,
;; so re-evaluating this form stores nothing, and the byte main wrote stays.
(def scratch [4 u8] uninit)
;; For the one case where the thunk's two halves have to be in order. A
;; module carries one [flan_reload_call], and it holds both the class
;; registrations and the def stores; a def whose initialiser constructs an
;; instance of a class the same form redefined has to see the slot list the
;; registry now holds, so the registrations go first.
(defclass point [x y])
(defn main [] i32
(agent/start "/tmp/flan-dev-defstore-fallback.sock")
;; A byte of [scratch] with a value somebody chose, so that "the uninit
;; form stored nothing" is a claim about a number and not about whatever
;; the bytes happened to be.
(set (at scratch 0) (u8 77))
;; Long enough that the whole block below runs against a program that is
;; still running: every assertion here is about what a *running* program
;; holds, and a fixture that parked half way through would answer the
;; second half of them from the park instead.
(dotimes [i 120000]
(agent/wait 5))
0)