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.
This commit is contained in:
Joseph Ferano 2026-09-13 21:09:50 +07:00
parent 3dd9f61b7d
commit 9534d5c077
2 changed files with 27 additions and 1 deletions

View File

@ -429,4 +429,12 @@ let expand_step (f : Form.t) : Form.t * string option =
let expand_all (f : Form.t) : Form.t * string option = let expand_all (f : Form.t) : Form.t * string option =
match loaded_for [ f ] with match loaded_for [ f ] with
| None -> (f, None) | 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))

View File

@ -261,6 +261,24 @@ let () =
(* And the one typed at the editor a moment ago, which is the session's (* 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]. *) set rather than the file's: nothing on disk declares [thrice]. *)
expands "a macro defined at the editor, expanded" "(thrice 14)" "(* 14 3)"; 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 (* A form with no macro in it comes back as itself, and says so rather
than echoing and leaving the editor to diff. *) than echoing and leaving the editor to diff. *)
(let r = (let r =