Keep the printed struct a colon; it is a wire format Emacs reads back

render.ml's output is parsed by emacs/flan-inspect.el, which hard-codes
the colon when it reads a field out of a rendered struct. Moving the
printer on its own would break inspection in the dev loop without
breaking a test that says so, so the printer waits and moves with its
reader, in the Emacs lane.

The sweep could not tell a rendered *expectation* from a Flan *source*
snippet -- both are strings in a test -- so it converted both. The suite
named every one it got wrong, and those are back.

emacs/test-flan-dev.el:415 is the one edit inside emacs/: Flan source sent
to the daemon for eval, which the parser now refuses in the old spelling.
One label, in a fixture.
This commit is contained in:
Joseph Ferano 2026-09-12 14:55:47 +07:00
parent 8e47356592
commit cb757868b4
6 changed files with 26 additions and 8 deletions

View File

@ -412,7 +412,7 @@ is written instead — the real `message' call the real command makes."
;; that errors stops it on its own game thread, in a frame of its own — not
;; inside anything this client asked for. Nothing tells Emacs.
(flan-dev--eval
"(defn step [] i64 (restart-case (do (error (Missing {:id 7})) 0) (use-placeholder [] -1)))"
"(defn step [] i64 (restart-case (do (error (Missing {.id 7})) 0) (use-placeholder [] -1)))"
"form")
(let ((deadline (+ (float-time) 20)))
(while (and (not flan-dev--stopped) (< (float-time) deadline))

View File

@ -147,7 +147,11 @@ let rec render c depth (e : Tast.expr) : Tast.expr list =
(fun i (f : Tast.field) ->
let v = { Tast.e = Tast.Field (e, i); ty = f.Tast.fty; loc } in
(if i = 0 then [] else [ lit " " ])
@ [ lit ("." ^ f.Tast.fname ^ " ") ]
(* Still a colon, deliberately. This printed form is a wire
format: emacs/flan-inspect.el parses it back, and it
hard-codes the colon. Moving the printer to the dot has
to land with that reader, in the Emacs lane. *)
@ [ lit (":" ^ f.Tast.fname ^ " ") ]
@ render c (depth + 1) v)
shown)
in

View File

@ -150,15 +150,15 @@ let () =
let println_out =
"plain string\nplain bytes\n42\n-7\n5\n18446744073709551615\n3.5\n\
-0.25\ntrue\nfalse\n()\n:green\n:red\n<ptr>\n(some 0)\nnone\n\
(Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y -2}) .tags [ 0 42 0]})\n\
(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y -2}) :tags [ 0 42 0]})\n\
[ 0 0 9 0]\n[ 0 0 0 0 0 0 0 0 ...]\n[ 0 9 0]\n\
(D1 {.d (D2 {.d (D3 {.d (D4 {.d (D5 {.n ...})})})})})\n[ 0 0]\n\
(D1 {:d (D2 {:d (D3 {:d (D4 {:d (D5 {:n ...})})})})})\n[ 0 0]\n\
[ 0 0]\n[ 9 0]\n"
(* The escape buffer is 1024 and the input is 1100 x's, so this is the
truncation: the ellipsis goes *inside* the quotes, and the count is
spelled out rather than pasted so that a change to the buffer or to
the reserve shows up here as a number and not as a wall of x. *)
^ "(Long {.s \"" ^ String.make 1014 'x' ^ "...\"})\n"
^ "(Long {:s \"" ^ String.make 1014 'x' ^ "...\"})\n"
^ "abc\n"
in
outputs "println, every arm" "programs/println.flan" println_out;

View File

@ -775,7 +775,7 @@ let () =
let want =
[ ("n", "i64", "3");
("label", "string", "\"hello\"");
("p", "Point", "(Point {.x 1.5 .y 2.5})");
("p", "Point", "(Point {:x 1.5 :y 2.5})");
("xs", "[3 i32]", "[ 10 20 30]");
("flag", "bool", "true") ]
in

View File

@ -618,6 +618,12 @@ let () =
rejects_check "field given twice"
(cursor ^ "(defn f [s [u8]] Cursor (Cursor {.pos 0 .pos 1}))")
~needle:"given twice";
(* The old spelling is refused rather than quietly accepted, and the refusal
names the new one. Two accepted spellings is how two spellings become
permanent, and the colon is wanted for keys. *)
rejects_check "a field label written with a colon"
(cursor ^ "(defn f [s [u8]] Cursor (Cursor {:src s}))")
~needle:"a field label is written .src, not :src";
accepts "field through a pointer auto-derefs"
(cursor ^ "(defn f [c (Ptr Cursor)] i32 (.pos c))");
accepts "set through a pointer"
@ -957,6 +963,14 @@ let () =
rejects_check "a pattern with no field name after it"
(pt ^ "(defn f [p Point] i32 (let [{a b} p] 0))")
~needle:"expected .field after a";
(* The same refusal on the destructuring side: a field is a field wherever it
is named, so the rule is not half-applied. :keys is the one that keeps its
colon, and the test below it says so. *)
rejects_check "a destructured field written with a colon"
(pt ^ "(defn f [p Point] i32 (let [{a :x} p] a))")
~needle:"a field label is written .x, not :x";
accepts ":keys keeps its colon, naming no field"
(pt ^ "(defn f [p Point] i32 (let [{:keys [x y]} p] (+ x y)))");
(* Clojure's other map-destructuring keys. Each is refused by its own name:
"unexpected form" would leave the author guessing which of the four they

View File

@ -96,9 +96,9 @@ let () =
this would otherwise come back as -1. *)
value "u64 at its maximum" "big" "18446744073709551615";
(* A struct, nested, with a fixed array inside it. *)
value "a struct" "(.pos b)" "(V {.x 1.5 .y 0})";
value "a struct" "(.pos b)" "(V {:x 1.5 :y 0})";
value "a nested struct" "b"
"(Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y 0}) .tags [ 0 42 0]})";
"(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})";
value "a fixed array" "arr" "[ 0 0 9 0]";
(* A slice's length is not known until it runs, so this one renders
through a loop rather than by unrolling. *)