The printed struct moves to the dot, with the reader that parses it

render.ml's output and emacs/flan-inspect.el's parser are the two ends of one
wire format, which is why the printer was left on the colon when the rest of
the corpus moved: shifting it alone would have broken inspection in the dev
loop without breaking a test that said so. They move together here.

The field list in the inspector is labelled with the dot too, which is the
spelling flan-inspect-step-expr already used to build `(.x b)' — the label and
the expression it stands for now read the same.

One case needed a guard the colon never did: `...' also begins with a dot and
is the renderer saying it stopped, not a field called `..'. A field name never
starts with a second dot, so one character of lookahead separates them.

The colon is not gone from the rendered grammar. An enum member is `:green' and
is a *value*, so the two are now told apart by the character alone, which is
the only thing that distinguishes them.

Also font lock, handed over with the same change: `:name' was the rule that
drew field labels, and with the colon belonging to keywords every label in the
corpus was left unfontified. `.name' is drawn as a constant, in both the places
it appears — the label in `{.x 1.0}' and the accessor in `(.x v)', which are
the same name.
This commit is contained in:
Joseph Ferano 2026-09-12 15:29:34 +07:00
parent a0610cecd5
commit 0f3b633449
8 changed files with 110 additions and 57 deletions

View File

@ -60,12 +60,12 @@ reply without a daemon behind them, and so that this file names
;;; Reading what the renderer wrote
;; The value comes back as a string, and that string is very nearly an
;; s-expression — `(Blob {:id 7 :pos (V {:x 1.5 :y 0})})'. Nearly, because
;; s-expression — `(Blob {.id 7 .pos (V {.x 1.5 .y 0})})'. Nearly, because
;; Emacs' `read' has no `{', so it is parsed here instead. The grammar is
;; small and fixed by `Session.render' (lib/session.ml), and every case below
;; names the line of it that produces it.
;;
;; (Name {:f V :f V}) a struct, with ` ...' before the `}' if the walk hit
;; (Name {.f V .f V}) a struct, with ` ...' before the `}' if the walk hit
;; its span bound of 8 fields
;; [ V V V] an array or a slice, ` ...' likewise
;; (some V) / none an option
@ -153,7 +153,7 @@ reply without a daemon behind them, and so that this file names
:children (list (cons "some" (car r))))
(if (and (< k (length s)) (eq (aref s k) ?\))) (1+ k) k))))
(t
;; `(Name {' then `:field VALUE' pairs, then `})'.
;; `(Name {' then `.field VALUE' pairs, then `})'.
(setq j (flan-inspect--skip-space s j))
(when (and (< j (length s)) (eq (aref s j) ?\{)) (setq j (1+ j)))
(let ((kids nil) (more nil) (done nil))
@ -162,7 +162,18 @@ reply without a daemon behind them, and so that this file names
(cond
((>= j (length s)) (setq done t))
((eq (aref s j) ?\}) (setq j (1+ j)) (setq done t))
((eq (aref s j) ?:)
;; A dot, which is how a struct literal is written in the
;; source and now how `Render.render' prints one. The colon
;; belongs to keywords — an enum member renders as `:green', and
;; one of those is a *value* here, never a field label — so the
;; two cannot be told apart by anything but this character.
;; `...' also begins with a dot and is the renderer saying it
;; stopped, not a field called `..'. A field name never starts
;; with a second dot, so one character of lookahead separates
;; them.
((and (eq (aref s j) ?.)
(< (1+ j) (length s))
(not (eq (aref s (1+ j)) ?.)))
(let ((start j))
(while (and (< j (length s)) (not (memq (aref s j) '(?\s ?\}))))
(setq j (1+ j)))
@ -253,8 +264,11 @@ reply without a daemon behind them, and so that this file names
(defvar-local flan-inspect--node nil "Its parsed value.")
(defun flan-inspect--label (child)
"How CHILD is named in the list: `0.' for an element, `.x' for a field.
A field is written with the dot it is written with in the source, which is also
what `flan-inspect-step-expr' puts in the expression it sends."
(let ((k (car child)))
(if (integerp k) (format "%d." k) (format ":%s" k))))
(if (integerp k) (format "%d." k) (format ".%s" k))))
;;; What a leaf is, beyond its decimal

View File

@ -31,10 +31,10 @@
;; - field access is `(.x v)', an ordinary call whose head happens to begin
;; with a dot. `.' is a symbol constituent in the syntax table below, so
;; nothing special is needed for it to read as a head.
;; - field labels are moving from `:x' to `.x', so `{.x 1.0}' is a struct
;; literal and not a map with symbol keys. The indenter is correct for
;; both spellings *because* it never looks at the key: a brace aligns under
;; its first element whatever that element is spelled like.
;; - a field label is a dot: `{.x 1.0}' is a struct literal, and the colon
;; now belongs to keywords, which a `Map' will use as keys. The indenter
;; is correct for both *because* it never looks at the key: a brace aligns
;; under its first element whatever that element is spelled like.
;;; Code:
@ -99,10 +99,11 @@
;; A keyword resolves against an enum at the call site, so it reads as a
;; constant rather than as a string.
("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
;; A field, in both of the spellings that exist while the corpus moves from
;; `{:x 1}' to `{.x 1}': the label in a struct literal and the accessor
;; `(.x v)' are the same name and are drawn the same way, which is also
;; what the keyword rule above did for the spelling being replaced.
;; A field. The label in a struct literal — `{.x 1.0}' — and the accessor
;; `(.x v)' are the same name and are drawn the same way. Without this
;; rule every field label in the corpus is unfontified, which is what the
;; colon-to-dot change left behind: the keyword rule above used to cover
;; them and no longer does, because the colon belongs to keywords now.
("\\_<\\.\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
;; The machine types, which are ordinary symbols but never anything else.
("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|Unit\\|Never\\|Ptr\\|Option\\)\\_>"
@ -306,9 +307,9 @@ Point is on the opening delimiter.
is an argument and there is no first argument to align under. Aligning under
the first *element* instead is what makes a binding vector line its names up
name-under-name, and by the same rule it lines up `defn' parameter lists,
`restart-case' clause parameters, `handler-bind' clause vectors, and both
spellings of a struct literal `{:x 1}' and `{.x 1}' with no case for any
of them, because the rule is about the bracket and never about what is
`restart-case' clause parameters, `handler-bind' clause vectors, and a brace
whatever its keys are spelled like `{.x 1}' and `{:north 0}' with no case
for any of them, because the rule is about the bracket and never about what is
written inside it.
A head that is not a symbol is the third case: `((f x) y)' has nothing to look

View File

@ -44,8 +44,8 @@
(message "\nreading what the renderer wrote")
;; (V {:x 1.5 :y 0}) — Types.Named, lib/session.ml.
(let ((n (flan-inspect-parse "(V {:x 1.5 :y 0})")))
;; (V {.x 1.5 .y 0}) — Types.Named, lib/session.ml.
(let ((n (flan-inspect-parse "(V {.x 1.5 .y 0})")))
(test-flan--check "a struct is a struct" (eq (plist-get n :kind) 'struct))
(test-flan--check "with its type name" (equal (plist-get n :type) "V"))
(test-flan--check "and its fields in order"
@ -56,7 +56,7 @@
;; The whole of NEXT.md's worked example, nested two deep with a string that
;; has escaped quotes in it and a slice at the end.
(let* ((src "(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})")
(let* ((src "(Blob {.id 7 .name \"sandy \\\"quoted\\\"\" .pos (V {.x 1.5 .y 0}) .tags [ 0 42 0]})")
(n (flan-inspect-parse src))
(kids (plist-get n :children)))
(test-flan--check "every field of a nested struct"
@ -102,7 +102,7 @@
;; A struct with a `...' where a field would be: span truncation *inside* a
;; struct, which is a different position in the grammar from a sequence's.
(let ((n (flan-inspect-parse "(Wide {:a 1 :b 2 ...})")))
(let ((n (flan-inspect-parse "(Wide {.a 1 .b 2 ...})")))
(test-flan--check "a struct's span bound is truncation, not a field"
(and (equal (mapcar #'car (plist-get n :children)) '("a" "b"))
(plist-get n :truncated))))
@ -134,7 +134,7 @@
(and why (string-match-p (regexp-quote (cadr case)) why)))))
(test-flan--check "a struct with fields does not refuse"
(null (flan-inspect-refusal (flan-inspect-parse "(V {:x 1 :y 2})"))))
(null (flan-inspect-refusal (flan-inspect-parse "(V {.x 1 .y 2})"))))
(test-flan--check "and a pointer says the address is not there either"
(string-match-p "does not write the address"
@ -195,14 +195,14 @@
(string-match-p "0xFF 0b1111_1111" text))))
(let ((flan-inspect-request-function
(lambda (_) '(:status "ok" :value "(Mask {:bits 255 :name \"all\"})")))
(lambda (_) '(:status "ok" :value "(Mask {.bits 255 .name \"all\"})")))
(flan-inspect-buffer " *test-inspect*"))
(let ((text (with-current-buffer (save-window-excursion (flan-inspect "m"))
(buffer-string))))
(test-flan--check "and a numeric field shows them in the field list"
(string-match-p ":bits +255 +0xFF 0b1111_1111" text))
(string-match-p "\\.bits +255 +0xFF 0b1111_1111" text))
(test-flan--check "while a string field is left alone"
(string-match-p ":name +\"all\"\n" text))))
(string-match-p "\\.name +\"all\"\n" text))))
;;; The inspector buffer
@ -218,13 +218,13 @@
(save-window-excursion (flan-inspect--show expr nil))))
(let* ((buf (test-flan--inspect
"b" "(Blob {:id 7 :name \"sandy\" :pos (V {:x 1.5 :y 0})})"))
"b" "(Blob {.id 7 .name \"sandy\" .pos (V {.x 1.5 .y 0})})"))
(text (with-current-buffer buf (buffer-string))))
(test-flan--check "the expression is at the top" (string-match-p "\\`b\n" text))
(test-flan--check "and the type under it" (string-match-p "a Blob" text))
(test-flan--check "the fields are listed" (string-match-p ":id.*7" text))
(test-flan--check "the fields are listed" (string-match-p "\\.id.*7" text))
(test-flan--check "a nested struct is summarised, not expanded"
(string-match-p ":pos +(V … 2 fields)" text))
(string-match-p "\\.pos +(V … 2 fields)" text))
(test-flan--check "and the keys are shown" (string-match-p "RET inspect" text))
;; Every field line carries the step that reaches it.
(with-current-buffer buf
@ -276,8 +276,8 @@
(push (plist-get form :code) asked)
(list :status "ok"
:value (if (equal (plist-get form :code) "(.pos b)")
"(V {:x 1.5 :y 0})"
"(Blob {:id 7 :pos (V {:x 1.5 :y 0})})"))))
"(V {.x 1.5 .y 0})"
"(Blob {.id 7 .pos (V {.x 1.5 .y 0})})"))))
(flan-inspect-buffer " *test-inspect*"))
(when (get-buffer " *test-inspect*") (kill-buffer " *test-inspect*"))
(save-window-excursion
@ -304,7 +304,7 @@
;; RET on something that cannot be entered refuses there, rather than sending
;; an expression the program would reject.
(let* ((buf (test-flan--inspect "p" "(Node {:next <ptr> :n 1})")))
(let* ((buf (test-flan--inspect "p" "(Node {.next <ptr> .n 1})")))
(with-current-buffer buf
(goto-char (point-min))
(flan-inspect-next)
@ -321,7 +321,7 @@
;; The span bound is drawn, because a field list that silently stops is a lie
;; about the value.
(let ((text (with-current-buffer
(test-flan--inspect "w" "(Wide {:a 1 :b 2 ...})") (buffer-string))))
(test-flan--inspect "w" "(Wide {.a 1 .b 2 ...})") (buffer-string))))
(test-flan--check "span truncation is drawn, not dropped"
(string-match-p "span bound of 8" text)))
@ -456,7 +456,7 @@
:stack (list (list :fn "sim/settle" :loc "sand.flan:42:3"
:fetched t
:locals '(("i" "i32" "7")
("b" "Blob" "(Blob {:id 7})")))
("b" "Blob" "(Blob {.id 7})")))
(list :fn "sim/step" :loc "sand.flan:60:1"
:fetched t :locals nil))))
(buf (test-flan--cnr state))
@ -496,7 +496,7 @@
(let ((asked nil))
(let ((flan-inspect-request-function
(lambda (form) (push (plist-get form :code) asked)
(list :status "ok" :value "(Blob {:id 7})")))
(list :status "ok" :value "(Blob {.id 7})")))
(flan-inspect-buffer " *test-inspect*"))
(with-current-buffer (test-flan--cnr
(list :condition "Missing" :restarts '("retry")

View File

@ -81,7 +81,7 @@
;; survive a `{' in the middle of it.
(test-flan-mode--check
"and still does with a struct literal in a value"
"(let [tip (rl/Vector2 {:x 15.0 :y 12.0})
"(let [tip (rl/Vector2 {.x 15.0 .y 12.0})
vel (+ gravity 1.0)]
(draw tip vel))")
@ -90,17 +90,21 @@
;; element is — and this is the test that keeps it that way while the corpus
;; moves from `:x' to `.x'.
(test-flan-mode--check
"a struct literal aligns under its first field, keyword spelling"
"(rl/Rectangle {:x 0.0 :y 0.0
:width (f32 screen-width)
:height (f32 screen-height)})")
(test-flan-mode--check
"and identically in the dot spelling it is moving to"
"a struct literal aligns under its first field"
"(rl/Rectangle {.x 0.0 .y 0.0
.width (f32 screen-width)
.height (f32 screen-height)})")
;; The colon spelling is gone from the language — the parser refuses it — but a
;; map literal will be written this way, and the indenter must not have learned
;; the difference: it aligns under the first element of a brace and never looks
;; at what that element is spelled like.
(test-flan-mode--check
"and a brace of keyword keys aligns exactly the same way"
"(counts {:north 0 :south 1
:east 2
:west 3})")
;; `defn' carries a return type between the parameters and the body, and it is
;; optional. Both spellings indent the body by two, which is why the spec is
;; `:defn' and not a count: a count would have to know whether the type is
@ -204,5 +208,39 @@
c 3]
(print c))")
;;; Font lock
;; The colon-to-dot change left every field label in the corpus unfontified:
;; the keyword rule used to cover them, and the colon belongs to keywords now.
(defun test-flan-mode--face-at (text needle)
"The face on the first character of NEEDLE in TEXT, under `flan-mode'."
(with-temp-buffer
(insert text)
(flan-mode)
(font-lock-ensure)
(goto-char (point-min))
(search-forward needle)
(get-text-property (- (point) (length needle)) 'face)))
(message "\n-- font lock")
(dolist (case '(("(rl/Vector2 {.x 1.0 .y 2.0})" ".x" "a struct literal's field label")
("(rl/Vector2 {.x 1.0 .y 2.0})" ".y" "and the second one")
("(set total (.bytes c))" ".bytes" "a field accessor")
;; The keyword rule is still there: an enum member is one.
("(rl/mouse-button-down? :left)" ":left" "an enum member")))
(test-flan--check (format "%s is a constant" (nth 2 case))
(eq (test-flan-mode--face-at (nth 0 case) (nth 1 case))
'font-lock-constant-face)))
;; A dot inside a number is not a label, and neither is the dot in a name that
;; has one in the middle.
;; A dot in the middle of a name is not a label: `\_<' anchors the rule to the
;; start of a symbol, and `a.b' is one symbol starting at `a'.
(test-flan--check "a dot inside a name does not start a label"
(null (test-flan-mode--face-at "(f alpha.beta)" ".beta")))
(provide 'test-flan-mode)
;;; test-flan-mode.el ends here

View File

@ -147,11 +147,12 @@ 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 " " ])
(* 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 ^ " ") ]
(* A dot, matching the source spelling a struct literal
is written in. This printed form is a wire format --
emacs/flan-inspect.el parses it back to build the field
list -- so the two moved together; see that file's
[flan-inspect--read-struct]. *)
@ [ 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,12 +775,11 @@ let () =
let want =
[ ("n", "i64", "3");
("label", "string", "\"hello\"");
(* Colons, deliberately: this is the *renderer's* output, not
source. [render.ml] still prints [{:x ...}] because
[flan-inspect.el] parses that text and the two move together
or not at all see NEXT.md. Converting this line with the
rest was wrong and the suite caught it. *)
("p", "Point", "(Point {:x 1.5 :y 2.5})");
(* The *renderer's* output, not source — and it is a dot now.
[render.ml] and [emacs/flan-inspect.el] are the two ends of
one wire format, and they moved together, which is what the
note here used to say was still owed. *)
("p", "Point", "(Point {.x 1.5 .y 2.5})");
("xs", "[3 i32]", "[ 10 20 30]");
("flag", "bool", "true") ]
in

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. *)