From 9534d5c0778f9ed9747624c3f56a7677b4b1fa15 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 21:09:50 +0700 Subject: [PATCH] The printed form round-trips, and the macro is named before the walk replaces it Two things the first pass left on unspecified ground. Form.to_source is the only field in the protocol carrying arbitrary literal data -- a macro may build any literal at all -- and Wire.quote escapes only the quote and the backslash, on the stated ground that both readers take everything else as itself. test_repl checks that ground now: every escape the reader knows, both byte-literal spellings to_string would have written raw, and a whole float, each sent through the printer and the socket and back. And expand_all read the macro's name out of a tuple beside the walk that replaces it. OCaml does not promise which half runs first; the failure would have been the wrong macro named, never an error. --- lib/macro.ml | 10 +++++++++- test/test_repl.ml | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/macro.ml b/lib/macro.ml index 4ffb065..7be9038 100644 --- a/lib/macro.ml +++ b/lib/macro.ml @@ -429,4 +429,12 @@ let expand_step (f : Form.t) : Form.t * string option = let expand_all (f : Form.t) : Form.t * string option = match loaded_for [ f ] with | None -> (f, None) - | Some l -> with_module l (fun () -> (expand_form l f, head_macro l f)) + | Some l -> + with_module l (fun () -> + (* The name is read off the *input*, so it has to be taken before the + walk replaces it. Bound rather than written as a tuple: OCaml does + not promise the order a tuple's components are evaluated in, and + [Dev.serve] already carries a comment about the one place that bit. + Here it would be silent — the wrong macro named, never an error. *) + let name = head_macro l f in + (expand_form l f, name)) diff --git a/test/test_repl.ml b/test/test_repl.ml index e5c9a03..9486d49 100644 --- a/test/test_repl.ml +++ b/test/test_repl.ml @@ -261,6 +261,24 @@ let () = (* And the one typed at the editor a moment ago, which is the session's set rather than the file's: nothing on disk declares [thrice]. *) expands "a macro defined at the editor, expanded" "(thrice 14)" "(* 14 3)"; + (* Every escape the reader knows, through the printer and then through + the wire, and back out as the same text. This is the one field in the + protocol that carries arbitrary literal data — a macro may build any + literal at all, which is why [Form.to_source] exists beside + [Form.to_string] — and [Wire.quote] escapes only the quote and the + backslash, on the stated ground that both readers take everything + else as itself. This is that ground, checked. *) + expands "every string escape survives the printer and the wire" + "\"a\\nb\\0c\\\"d\\\\e\\tf\\rg\"" "\"a\\nb\\0c\\\"d\\\\e\\tf\\rg\""; + (* And the byte literals, where [to_string] would have written a NUL and + a carriage return into the middle of the line. *) + expands "a byte literal is written as a name the reader has" + "[\\nul \\return \\space \\tab \\newline \\A]" + "[\\nul \\return \\space \\tab \\newline \\A]"; + (* A float that is a whole number, which [to_string]'s %g writes as an + integer — and an integer is what it would read back as. *) + expands "a whole float keeps its point" "[1.0 0.5 -2.0]" "[1.0 0.5 -2.0]"; + (* A form with no macro in it comes back as itself, and says so rather than echoing and leaving the editor to diff. *) (let r =