flan/test/programs/reload.flan
Joseph Ferano a420bb1b1d The session: a program as a live thing
lib/session.ml holds the declarations a running process was built from plus
every change accepted since, which is what an editor needs and what a one-shot
compiler cannot have.

Transactionality came for free. Check.program builds a fresh environment from a
declaration list on every call, so a form that fails to check mutates nothing
and the accumulated list is simply not replaced - no scratch-environment
machinery, which is what I was about to build. Re-checking the whole program
each evaluation costs the frontend, under 10ms, less than the llc after it.
There is a test for the case that matters: a typo, then a good form, in the
same session.

Which names the process was built with comes from the checked program, not from
any accumulated AST, because Check.program prepends the prelude and no AST
contains it. Derive it from declarations and print-line reads as new, gets a
registry cell nobody publishes, and the first call jumps to null.

Three changes are refused with a reason rather than loaded. A function's
signature, because a cell is a bare ptr and every call site compiled before the
change still passes the old arguments through it. A global's type, because the
storage exists and has a shape - reusing it reads at the wrong offsets, and
replacing it discards the state the reload exists to preserve. A struct's
fields, because the values the process is holding have the old layout. Note
what the checker already catches on its own: change a parameter type and the
caller fails to type check first, loudly. These rules only get a turn on a
change the checker accepts, which is a name nothing else in the program uses -
exactly where the silent version lives. Hence an unused defvar and a C-called
defn in the fixtures.

The accumulated list is the post-Load one, so an evaluated import is spliced as
its expansion. Otherwise re-evaluating a file that imports something appends a
second import, Load expands it again, and the duplicate-name pass rejects it.
C-c C-k on sand.flan's own text is the test.

flan reload now takes a program and a file of changed forms rather than a list
of function names and a --new list: the session works out which names are new,
which is the thing a bare CLI could not.

Also fixed, found by running the agent test under load: the agent took SIGPIPE
when a sender read part of a reply and closed. Replies go out with
MSG_NOSIGNAL, per call rather than by installing a handler, because the signal
disposition belongs to the program the agent is embedded in.
2026-09-10 21:48:45 +07:00

29 lines
1.2 KiB
Plaintext

;;;; The reload primitive's fixture, v1 (NEXT.md, dev loop step 1 and 2).
;;;;
;;;; There is deliberately no [main]: the host is reload_host.c, which links
;;;; this program and then dlopens two rebuilt copies of [bump].
;;;;
;;;; [counter] is the state that has to survive a reload — a redefinition
;;;; module declares it [external], so the loaded object writes the host's copy
;;;; and not a new one. [outer] is the call site that has to *follow* a reload:
;;;; it is compiled once, into the host, and never rebuilt, so if a redefined
;;;; [bump] runs when the host calls [outer] then the cell is doing its job.
;;;; The string is not decoration either — a redefinition module has to carry
;;;; its own constants, and a one-function module usually has none.
(defvar counter i64)
;;; Unused, and that is the point: the session's compatibility rules only get a
;;; chance to speak about a change the *checker* accepts, and retyping a var
;;; something else reads is an ordinary type error long before it is a layout
;;; question.
(defvar spare i64)
(defn helper [x i64] i64 (* x 2))
(defn bump [] i64
(print-line "v1")
(set counter (+ counter 1))
(helper counter))
(defn outer [] i64 (bump))