From 9fc04193d94d18fb116bceaafb369616341b9812 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 20:37:32 +0700 Subject: [PATCH] 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. --- BUILT.md | 14 ++++++++++++++ test/test_repl.ml | 37 ++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/BUILT.md b/BUILT.md index 865f2f8..bedf164 100644 --- a/BUILT.md +++ b/BUILT.md @@ -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 diff --git a/test/test_repl.ml b/test/test_repl.ml index 0615a4e..829b503 100644 --- a/test/test_repl.ml +++ b/test/test_repl.ml @@ -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;