The refused shape is defmacro at C-x C-e, and the union's boundary is defn's

The head dispatch already names it; test_repl pins it beside the defvar case,
and the editor-typed defmacro moved below the describe assertion that says an
evaluation changes nothing -- that one does, on purpose. BUILT.md gains the
boundary of the shape: the set only grows, a deleted defmacro still expands,
and that is exactly what a deleted defn already does.
This commit is contained in:
Joseph Ferano 2026-09-13 20:37:32 +07:00
parent b9f5b5c44c
commit 9fc04193d9
2 changed files with 38 additions and 13 deletions

View File

@ -5453,6 +5453,13 @@ since.** Applied to macros, that is both halves of the fix and neither is option
this path — it parses to a `Defn` and installs a body like any other. The only thing missing was the session
remembering that the name is a macro.
**The boundary of that shape, written down rather than discovered:** the set only ever grows. Delete
`(defmacro tenfold ...)` from the buffer, reload, and `(tenfold 7)` still expands, because a union never removes.
That is not a leak to fix later — it is exactly what `defn` does. `Session.eval` keeps `kept @ added`, so a
function the file no longer declares is still in the session and still callable, and the running process still has
its body loaded. A session is what the program was built from plus every change *accepted*, and a deletion is not
a change anything sent. The one operation that forgets is restarting the session, which is what it is for.
**The session does not re-read the file, and that was the live alternative.** It has `~origin`, the buffer's own
path, and re-reading would pick up macros the session has never been told about. It would also read whatever is
*saved*, while the buffer on screen is whatever is *typed* — so an expansion would silently use a body the reader
@ -5486,6 +5493,13 @@ The names stay unqualified. A buffer writes its own macro's bare name, so that i
answer to; a file inside a package the program also imports ends up holding both, the bare one from here and
`alias/name` from `Load`, which is what the two call sites each need.
**One shape stays refused, and its refusal was already right.** `(defmacro ...)` at `C-x C-e` is a declaration,
and `Parse.expr`'s head dispatch already names `defmacro` first among the heads it refuses — "defmacro is a
top-level declaration, not an expression". It matters more now than when it was written: before this, nobody would
type a `defmacro` at `C-x C-e`, because the session forgot it either way. Now that one typed at the editor means
something, reaching for `C-x C-e` on it is a reflex, and what comes back says what it is rather than complaining
about an unknown function. `test_repl` pins it beside the `defvar` case.
Both editor paths work and they are different wraps — `Parse.expr`'s for `C-x C-e` and `Parse.decl`'s for
`C-c C-c`. `test/test_session.ml` covers both, plus a `defmacro` the file never had followed by a call to it,
plus editing that macro and calling it again; the assertions are on the IR and not on the absence of an

View File

@ -168,19 +168,6 @@ let () =
same read that built the program. Over a real socket, because that is
the path a person is on; test_session has the in-process halves. *)
value "the file's own macro" "(tenfold 7)" "70";
(* A macro the file never had, typed at the editor and then called. This
is the shape the fix chose: a [defmacro] evaluated into a session
*joins* it, exactly as a [defn] does, and the next evaluation can call
it. Two round trips, because that is the whole of the claim. *)
(let r =
request c
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/buf.flan\")"
(quote "(defmacro thrice [args] `(* ~(at args 0) 3))"))
in
if status r <> "ok" then
fail "evaluating a defmacro over the socket: %s"
(Option.value ~default:(status r) (field r "message")));
value "a macro defined at the editor" "(thrice 14)" "42";
(* The one that proves it ran inside the process: the program increments
[ticks] every frame, so two evaluations of it must disagree. A copy
@ -215,12 +202,36 @@ let () =
refuses "a declaration inside an expression" "(do 1 (defn f [] i32 1))"
"top-level declaration";
refuses "an unknown name" "no-such-name" "unknown name";
(* [defmacro] is in that same head list, and it is the shape that stays
refused now that a [defmacro] typed at the editor means something: a
person will reach for C-x C-e on one by reflex, and what they get is
the sentence naming it a declaration rather than an arity complaint
about an unknown function. C-c C-c is where a declaration goes, which
is the case below. *)
refuses "a defmacro at C-x C-e" "(defmacro m [args] args)"
"top-level declaration";
(* And the session is untouched by all of it: an evaluation is not a
declaration, so nothing named eval/N accumulates in the program. *)
let r = request c "(:op \"describe\")" in
if status r <> "ok" then fail "describe after evaluating: %s" (status r);
(* Below the line above deliberately, because this one *does* change the
session: a macro the file never had, typed at the editor and then
called. It is the shape the fix chose a [defmacro] evaluated into a
session joins it, exactly as a [defn] does, and the next evaluation
can call it. Two round trips, because that is the whole of the
claim. *)
(let r =
request c
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/buf.flan\")"
(quote "(defmacro thrice [args] `(* ~(at args 0) 3))"))
in
if status r <> "ok" then
fail "evaluating a defmacro over the socket: %s"
(Option.value ~default:(status r) (field r "message")));
value "a macro defined at the editor" "(thrice 14)" "42";
ignore (request c "(:op \"close\")");
Unix.close c
end;