diff --git a/emacs/flan-inspect.el b/emacs/flan-inspect.el index 44cb63c..0e246d6 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -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 diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index f9a9ccc..f954c70 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -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 diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index fbc7d49..1506760 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -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 :n 1})"))) +(let* ((buf (test-flan--inspect "p" "(Node {.next .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") diff --git a/emacs/test-flan-mode.el b/emacs/test-flan-mode.el index f8d9c34..37e5e42 100644 --- a/emacs/test-flan-mode.el +++ b/emacs/test-flan-mode.el @@ -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 diff --git a/lib/render.ml b/lib/render.ml index 1346521..7de13dd 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -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 diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 660e507..8806fde 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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\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; diff --git a/test/test_dev.ml b/test/test_dev.ml index d64ecae..b708c94 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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 diff --git a/test/test_repl.ml b/test/test_repl.ml index b8c889f..f642277 100644 --- a/test/test_repl.ml +++ b/test/test_repl.ml @@ -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. *)