flan/test/programs/dev-watch.flan
Joseph Ferano bdbb6e094c The watch window pushes, because a poll cannot answer a stopped program
Ported from the author's Clojure watch.el. Three of its decisions survive
unchanged — the program decides what is shown, the request is async, and the
paint is replace-buffer-contents so point survives every tick. The transport
is the part that had to turn round.

An eval here compiles a module and dlopens it, so the planned fix of
compiling the render thunk once and re-invoking it per tick was still a poll,
and a poll has a defect caching does not touch: a thunk runs at a frame
boundary and a stopped program has no more of those. So the program writes
into a table in flan_dev.c from its own loop and Emacs reads the table, which
is memory. The values are then as fresh as the last frame whatever the
repaint interval is, and they are still there while the program is stopped.

The frame thread's constraints decide the storage: no allocation, so names
are fixed arrays rather than strdup'd; no lock, because the reader is the
listener thread; and not the result buffer, which is written once per C-x C-e
and would be overwritten sixty times a second by watch traffic. One seqlock
per slot rather than one for the table, so a reader retries one slot instead
of having to catch the gap between two frames' writes; a snapshot from
adjacent frames is what a HUD looks like anyway.

Sixty-four slots, and past that a name is dropped rather than fatal — killing
the program because somebody watched a 65th value is the diagnostic shooting
the patient. Reported as a flag and not a count: the only number the write
path could keep is of write attempts, which at frame rate says "3847 names"
about one name.

Nothing writes the table until a watch buffer is open, so a watch call in a
program nobody is debugging is a load and a branch that is not taken — the
same number in a release build, since flan_dev.c is linked into both.

Scalars work today through declare-c against four runtime entry points, which
is why this needed no compiler change. A struct or a slice needs a walk over
its type, which is one arm in check.ml beside print; BUILT.md writes it out
rather than reaching into a file another lane holds. Ghost text turns out to
be gated on the same arm, for a different reason: nothing in the table
carries a source location, and a hand-written declare-c call cannot supply
one that does not drift when the line moves.
2026-09-12 23:16:44 +07:00

39 lines
1.6 KiB
Plaintext

;;;; A program that pushes values into the watch table from its own loop.
;;;;
;;;; The point of the case is that this needs no compiler change: the watch
;;;; entry points are ordinary C functions, so a program reaches them through
;;;; [declare-c] the same way it reaches anything else in the runtime. That is
;;;; deliberate — a [(watch "hp" hp)] form would be an arm in the checker, and
;;;; a scalar does not need one.
;;;;
;;;; The values are written every iteration and are *not* read back from here.
;;;; What reads them is the daemon's [watch] op, over the agent, while this
;;;; program is still running — which is the whole design: the program pushes
;;;; at frame rate and the editor reads memory.
(import agent "vendor:agent")
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
(declare-c watch-f64 [name string x f64] i32 "flan_dev_watch_f64")
(declare-c watch-str [name string s string] i32 "flan_dev_watch_str")
(defvar ticks i64)
(defn step [] i64
(set ticks (+ ticks 1))
;; Three types, because the table stores *rendered text* and the rendering
;; is per type: an i64 and an f64 do not print the same way, and a string is
;; quoted and escaped so that a newline in one cannot become a second row.
(watch-i64 "ticks" ticks)
(watch-f64 "half" (/ (f64 ticks) 2.0))
(watch-str "label" "sand")
ticks)
(defn main [] i32
(agent/start "/tmp/flan-dev-watch-fallback.sock")
;; A watch call costs a load and a not-taken branch until somebody opens a
;; watch buffer, so spinning here writes nothing until the test arms it.
(while (= (agent/wait 20) 0) (step))
(step)
(while (= (agent/wait 20) 0) (step))
0)