Indentation ported from clojure-mode, and #_ discards a form
This commit is contained in:
commit
5ad429d69f
17
NEXT.md
17
NEXT.md
@ -286,9 +286,11 @@ BUILT.md, "The colon belongs to keys".
|
||||
|
||||
**What it left for the Emacs lane, both verified.** `render.ml` still *prints* a struct with colons, deliberately:
|
||||
`emacs/flan-inspect.el:165` parses that output and hard-codes the colon when it reads a field out, so the printer
|
||||
has to move in the same commit as its reader. And `flan-mode.el:61` font-locks `:name` as a constant with nothing
|
||||
matching `.name`, so a field label is now unfontified where it used to be coloured. Neither is urgent; both belong
|
||||
with whoever next opens `emacs/`.
|
||||
has to move in the same commit as its reader. That half is still open and belongs with whoever next opens the
|
||||
inspector. ~~And `flan-mode.el:61` font-locks `:name` as a constant with nothing matching `.name`, so a field label
|
||||
is now unfontified where it used to be coloured.~~ **The font-lock half is done:** a field is drawn as a constant in
|
||||
both of the spellings that exist while the corpus moves, so `{.x 1}` and the accessor `(.x v)` read alike, and the
|
||||
keyword rule stayed where it was because the colon still means an enum member and a map key.
|
||||
|
||||
**7. `Map` follows Odin's implementation.** Read `base/runtime/dynamic_map_internal.odin` before writing any of it;
|
||||
the checkout is at `~/Repositories/Odin`. Three properties are the ones worth copying, and they are stated in its own
|
||||
@ -423,9 +425,12 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them.
|
||||
whole of the resource-cleanup answer.
|
||||
|
||||
4. **The Emacs batch. Disjoint from the compiler, so it runs in parallel with anything above.** Globals in the break
|
||||
buffer; the buffer opening itself when the program stops; the indentation rewrite with `clojure-mode` as the
|
||||
reference; `#_`; hex, binary and addresses on primitives in the inspector. The indentation one is worth doing first
|
||||
within this batch — it costs friction on every keystroke today.
|
||||
buffer; the buffer opening itself when the program stops; ~~the indentation rewrite with `clojure-mode` as the
|
||||
reference~~; ~~`#_`~~; hex, binary and addresses on primitives in the inspector. **The indentation rewrite and `#_`
|
||||
are done.** The indenter is ported from `clojure-mode`'s source rather than derived from it — `flan-mode` still
|
||||
requires nothing outside stock Emacs — and it aligns a binding vector name-under-name, which is the bug that cost
|
||||
friction on every keystroke. `defn` parameter lists and `restart-case` clause parameters were the same shape and
|
||||
came with it. What remains in this batch is the break buffer and the inspector, and they are independent.
|
||||
|
||||
5. **Union values, then the macro expander, then `Result`/`try`.** Promoted above `Handle` on the author's call —
|
||||
macros are the thing most worth wanting, and unions are the only thing between here and them.
|
||||
|
||||
@ -263,6 +263,49 @@ the inner binding.
|
||||
|
||||
---
|
||||
|
||||
## Writing it
|
||||
|
||||
`TAB` indents the line, and `C-M-q` the form under point. The rules are ported
|
||||
from `clojure-mode`'s, because Clojure has the shapes Flan has and Emacs Lisp
|
||||
does not — vectors that bind, bracket variety, and keys inside braces.
|
||||
|
||||
**A binding vector lines up name under name.** The second and later bindings of a
|
||||
`let` sit under the first one's *name*, not under its value:
|
||||
|
||||
```
|
||||
(let [vel (+ gravity (at velocity row col))
|
||||
y (min (- rows 1) (+ row (i32 vel)))]
|
||||
…)
|
||||
```
|
||||
|
||||
The same rule draws `defn` parameter lists, `restart-case` and `handler-bind`
|
||||
clause parameters, and both spellings of a struct literal — they are all a vector
|
||||
or a brace read in pairs, so they are all indented as one.
|
||||
|
||||
**A body indents two.** `let`, `if`, `when`, `while`, `match`, `restart-case`,
|
||||
`handler-bind`, `defn` and the `def…` forms all put their body two columns in
|
||||
from the head. What differs between them is only how many forms come *before*
|
||||
the body and stay on the head's line — a `let`'s binding vector, an `if`'s test,
|
||||
a `restart-case`'s protected form — and the indenter knows that count per form.
|
||||
|
||||
A form it has no entry for is treated as a call: the arguments line up under the
|
||||
first argument, not two in. That is the fallback, and it is what you want for
|
||||
`(rl/draw-rectangle x y w h)` and for every function you write.
|
||||
|
||||
`defn` carries a return type between the parameter vector and the body, and it is
|
||||
optional. The indenter does not need to know which — everything after the head
|
||||
indents two, which is the right answer for the name, the parameters, a return type
|
||||
if one is written, and the body alike.
|
||||
|
||||
**A field is drawn as a constant**, in the accessor `(.x v)` and as a label in
|
||||
`{.x 1.0}`. The colon is still a constant too; it means an enum member, `:green`,
|
||||
and a key in a map.
|
||||
|
||||
Nothing here needs a running program. Indentation and colouring are the major
|
||||
mode's, so they work in a file you have only opened.
|
||||
|
||||
---
|
||||
|
||||
## Getting around
|
||||
|
||||
| Key | Does |
|
||||
|
||||
@ -1,13 +1,51 @@
|
||||
;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
|
||||
|
||||
;; Derived from lisp-mode, which is most of the work: Flan is s-expressions, so
|
||||
;; sexp motion, paren matching, `beginning-of-defun' and indentation all
|
||||
;; already do the right thing. What is left is what the language actually adds
|
||||
;; — its own literals and its own set of forms that indent like a body.
|
||||
;; Derived from `prog-mode', borrowing `lisp-mode''s machinery for the parts
|
||||
;; that are simply s-expressions: sexp motion, paren matching and
|
||||
;; `beginning-of-defun' already do the right thing.
|
||||
;;
|
||||
;; Indentation is the part that does not, and it is ported from
|
||||
;; `clojure-mode''s source rather than from `lisp-mode''s. That is a
|
||||
;; deliberate change of reference and it is worth being exact about what it
|
||||
;; means: `clojure-mode' is neither an ancestor nor a dependency here — this
|
||||
;; mode ships in the Flan repository and requires nothing outside stock Emacs —
|
||||
;; it is the file whose rules were read and written out again below.
|
||||
;;
|
||||
;; The reason is that Emacs Lisp has none of the shapes Flan is written in. It
|
||||
;; has no vectors-as-bindings, no maps, no bracket variety, so
|
||||
;; `lisp-indent-function' treats `[a 1 b 2]' as a function call and aligns
|
||||
;; continuation lines under `1' — the first *argument* — instead of under `a',
|
||||
;; the first *binding*. That was the reported bug in `sand.flan''s `settle',
|
||||
;; and it was never one missing rule: every rule has to be added by hand when
|
||||
;; the base language does not have the shape. Clojure's rules already cover
|
||||
;; brackets-mean-binding, pairs-align, maps and `#_'.
|
||||
;;
|
||||
;; Where Flan diverges from Clojure it diverges on purpose, and each divergence
|
||||
;; is written down at the place that handles it:
|
||||
;;
|
||||
;; - `defn' carries a **return type between the parameter vector and the
|
||||
;; body**, and it is optional — `(defn show [x f32] …)' has none. Both are
|
||||
;; handled by not caring: the spec is `:defn', so everything after the head
|
||||
;; indents two, which is right for the name, the parameters, a return type
|
||||
;; that is there and a body whether or not one preceded it.
|
||||
;; - 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.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'lisp-mode)
|
||||
;; `thing-at-point', which the indenter reads the enclosing form's head with.
|
||||
(require 'thingatpt)
|
||||
|
||||
;; Bound by `calculate-lisp-indent' around the call to `lisp-indent-function',
|
||||
;; and declared in `lisp-mode' without a `defvar', so say so here rather than
|
||||
;; let the byte-compiler call it a free variable.
|
||||
(defvar calculate-lisp-indent-last-sexp)
|
||||
;; For `imenu-generic-expression', which is set below and which would
|
||||
;; otherwise be made buffer-local before its own defvar had run.
|
||||
(require 'imenu)
|
||||
@ -33,6 +71,8 @@
|
||||
(autoload 'flan-dev "flan-dev" nil t)
|
||||
(autoload 'flan-dev-quit "flan-dev" nil t)
|
||||
(autoload 'flan-dev-restart-program "flan-dev" nil t)
|
||||
;; Bound below, like the rest, and it was the one missing an autoload.
|
||||
(autoload 'flan-disassemble "flan-dev" nil t)
|
||||
|
||||
(defgroup flan nil
|
||||
"Editing and evaluating Flan."
|
||||
@ -59,6 +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.
|
||||
("\\_<\\.\\(?:\\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\\)\\_>"
|
||||
. font-lock-type-face)
|
||||
@ -160,25 +205,231 @@ line is off screen."
|
||||
(setq-local comment-add 1)
|
||||
(setq-local font-lock-defaults '(flan-font-lock-keywords))
|
||||
(setq-local indent-line-function #'lisp-indent-line)
|
||||
;; Spaces. The whole corpus is written with them, and alignment that is
|
||||
;; correct here is alignment under a specific *column* — a tab makes that
|
||||
;; depend on a setting the file cannot carry.
|
||||
(setq-local indent-tabs-mode nil)
|
||||
(setq-local lisp-indent-function #'flan-indent-function)
|
||||
(setq-local outline-regexp ";;;;+[ \t]*")
|
||||
(setq-local imenu-generic-expression flan-imenu-generic-expression)
|
||||
;; Buffer-locally, because this answers for Flan and nothing else.
|
||||
(add-hook 'which-func-functions #'flan-current-defun-name nil t))
|
||||
|
||||
;;; Indentation
|
||||
|
||||
;; Ported from `clojure-mode', as the header says. Three pieces, and the
|
||||
;; middle one is where the reported bug lived.
|
||||
|
||||
(defconst flan-indent-specs
|
||||
;; A number N: the first N arguments are *special* and the rest are a body.
|
||||
;; `:defn': everything after the head is a body. These are the same two
|
||||
;; values `clojure-mode' uses, and they mean the same thing here.
|
||||
'(;; Binding forms. The vector is the one special argument; the body
|
||||
;; follows it. This is the entry that makes `let' correct, but note that
|
||||
;; it is *not* what fixed the reported bug — alignment inside the vector is
|
||||
;; `flan--data-form-p' below, and it would be right even with no entry
|
||||
;; here.
|
||||
("let" . 1)
|
||||
("loop" . 1)
|
||||
("dotimes" . 1)
|
||||
;; The clause vector, then the protected body. Same shape as `let'.
|
||||
("handler-bind" . 1)
|
||||
("handler-case" . 1)
|
||||
;; Test first, body after.
|
||||
("if" . 1)
|
||||
("when" . 1)
|
||||
("unless" . 1)
|
||||
("while" . 1)
|
||||
("until" . 1)
|
||||
("match" . 1)
|
||||
("with-allocator" . 1)
|
||||
;; The protected form, then the clauses. It has to be 1 rather than 0:
|
||||
;; with 0 the clauses are ordinary arguments, and a protected form written
|
||||
;; on the head's line — `(restart-case (middle n)' — would drag every
|
||||
;; clause out to align under it.
|
||||
("restart-case" . 1)
|
||||
;; All body.
|
||||
("do" . 0)
|
||||
("cond" . 0)
|
||||
("defer" . 0)
|
||||
("try" . 0)
|
||||
;; `defn' is `:defn' rather than a count because the return type between
|
||||
;; the parameters and the body is optional; a count would have to know
|
||||
;; whether one is there, and `:defn' does not care.
|
||||
("defn" . :defn)
|
||||
;; `(declare-c NAME [params] RET "CSymbol")'. The name is the one special
|
||||
;; argument; everything after it is written down the page in one column.
|
||||
("declare" . 1)
|
||||
("declare-c" . 1))
|
||||
"How each form indents, by name.
|
||||
Anything not named here that begins with `def' is treated as `:defn' by
|
||||
`flan-indent-function'; anything else indents as a function call.")
|
||||
|
||||
(defun flan--indent-spec (name)
|
||||
"The indent spec for the form called NAME, or nil."
|
||||
(and name (cdr (assoc name flan-indent-specs))))
|
||||
|
||||
(defun flan--non-logical-sexp-p ()
|
||||
"Non-nil if what follows point is read but produces no form.
|
||||
Today that is only `#_', the discard reader macro — see `lib/reader.ml'. A
|
||||
discarded form is skipped rather than counted, so `(let [#_a b 1] …)' still
|
||||
aligns as the pairs it will be once the reader is done with it."
|
||||
(looking-at-p "#_"))
|
||||
|
||||
(defun flan--forward-sexp (&optional n)
|
||||
"Move forward over N sexps, skipping discarded ones."
|
||||
(setq n (or n 1))
|
||||
(let ((forward-sexp-function nil))
|
||||
(while (> n 0)
|
||||
(while (flan--non-logical-sexp-p) (forward-sexp 1))
|
||||
(forward-sexp 1)
|
||||
(setq n (1- n)))))
|
||||
|
||||
(defun flan--backward-sexp (&optional n)
|
||||
"Move backward over N sexps, skipping discarded ones."
|
||||
(setq n (or n 1))
|
||||
(let ((forward-sexp-function nil))
|
||||
(while (> n 0)
|
||||
(backward-sexp 1)
|
||||
(while (and (not (bobp))
|
||||
(ignore-errors
|
||||
(save-excursion (backward-sexp 1)
|
||||
(flan--non-logical-sexp-p))))
|
||||
(backward-sexp 1))
|
||||
(setq n (1- n)))))
|
||||
|
||||
(defun flan--data-form-p ()
|
||||
"Non-nil if the form at point is data rather than a call.
|
||||
Point is on the opening delimiter.
|
||||
|
||||
**This is the fix.** A `[' or a `{' is not a function call, so nothing in it
|
||||
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
|
||||
written inside it.
|
||||
|
||||
A head that is not a symbol is the third case: `((f x) y)' has nothing to look
|
||||
a spec up under."
|
||||
(or (memq (char-after) '(?\[ ?\{))
|
||||
(not (looking-at ".\\(?:\\sw\\|\\s_\\)"))))
|
||||
|
||||
(defun flan--normal-indent (last-sexp)
|
||||
"Align with the argument above, as an ordinary call does.
|
||||
Point is just after the open paren of the enclosing form; LAST-SEXP is where
|
||||
the sexp before the one being indented starts."
|
||||
(goto-char last-sexp)
|
||||
(forward-sexp 1)
|
||||
(flan--backward-sexp 1)
|
||||
(let ((last-sexp-start nil))
|
||||
(if (ignore-errors
|
||||
;; Back up until we reach a sexp that starts its own line: that is
|
||||
;; the one every Lisp aligns under.
|
||||
(while (string-match "[^[:blank:]]"
|
||||
(buffer-substring (line-beginning-position)
|
||||
(point)))
|
||||
(setq last-sexp-start (prog1 (point) (forward-sexp -1))))
|
||||
t)
|
||||
(current-column)
|
||||
;; Nothing above but the head itself, so there are two cases and Flan
|
||||
;; answers them differently from Clojure's default.
|
||||
(if (and last-sexp-start (< last-sexp-start (line-end-position)))
|
||||
;; An argument shares the head's line. Align under it — this is the
|
||||
;; alignment every Lisp agrees on.
|
||||
(progn (goto-char last-sexp-start) (current-column))
|
||||
;; The head is alone on its line. Clojure's default would align the
|
||||
;; arguments under the *head*; the Flan corpus indents them by a body
|
||||
;; instead, which is `clojure-indent-style''s `align-arguments' and is
|
||||
;; what every hand-written call in the tree does:
|
||||
;;
|
||||
;; (rl/draw-rectangle-lines-ex
|
||||
;; (rl/Rectangle {.x 0.0 .y 0.0})
|
||||
;; (f32 2.0) (rl/get-color 0x303030FF))
|
||||
(+ (current-column) lisp-body-indent -1)))))
|
||||
|
||||
(defun flan--clause-form-p ()
|
||||
"Non-nil if the form at point is a clause: `(name [params] body…)'.
|
||||
Point is just after the open paren, on the head.
|
||||
|
||||
This is `defn' with the name left off, and it is how `handler-bind',
|
||||
`handler-case' and `restart-case' all write their clauses. Their heads are
|
||||
condition classes and restart names — things a program invents — so no table
|
||||
here could ever list them; the shape is what can be recognised."
|
||||
(save-excursion
|
||||
(ignore-errors
|
||||
(flan--forward-sexp 1) ; over the head
|
||||
(skip-chars-forward " \t\n\r,")
|
||||
(eq (char-after) ?\[))))
|
||||
|
||||
(defun flan--count-indent (method indent-point last-sexp head-column)
|
||||
"Indent inside a form whose first METHOD arguments are special.
|
||||
INDENT-POINT, LAST-SEXP and HEAD-COLUMN are as in `flan-indent-function';
|
||||
point is just after the open paren."
|
||||
(let ((pos -1))
|
||||
(condition-case nil
|
||||
(while (and (<= (point) indent-point) (not (eobp)))
|
||||
(flan--forward-sexp 1)
|
||||
(setq pos (1+ pos)))
|
||||
;; Past the last sexp in the form: count as if one more were here, which
|
||||
;; is what indenting an empty line at the end of a form means.
|
||||
(scan-error (setq pos (1+ pos))))
|
||||
(cond
|
||||
;; The first argument that is body rather than special.
|
||||
((= pos (1+ method)) (+ lisp-body-indent head-column))
|
||||
;; Further body arguments line up with the one above.
|
||||
((> pos (1+ method)) (flan--normal-indent last-sexp))
|
||||
;; Still in the special arguments. Clojure indents these to twice the
|
||||
;; body indent so they cannot be mistaken for body; Flan uses one, because
|
||||
;; the corpus is written that way throughout —
|
||||
;;
|
||||
;; (handler-bind
|
||||
;; [(StorageExhausted [c] …)]
|
||||
;; (load-all))
|
||||
;;
|
||||
;; — and there is nothing to confuse: the special arguments come first, so
|
||||
;; a reader never has to tell them apart by column.
|
||||
(t (+ lisp-body-indent head-column)))))
|
||||
|
||||
(defun flan-indent-function (indent-point state)
|
||||
"Indent like Lisp, with Flan's body forms as special forms.
|
||||
INDENT-POINT and STATE are as for `lisp-indent-function'."
|
||||
(let ((open (elt state 1)))
|
||||
(or (and open
|
||||
(save-excursion
|
||||
(goto-char (1+ open))
|
||||
(let ((head (and (looking-at "\\(\\sw\\|\\s_\\)+")
|
||||
(match-string 0))))
|
||||
(when (member head '("defn" "let" "if" "while" "until"
|
||||
"dotimes" "match" "do" "loop" "defer"))
|
||||
(+ (current-column) 1)))))
|
||||
(lisp-indent-function indent-point state))))
|
||||
"Indent a line inside a Flan form.
|
||||
INDENT-POINT and STATE are as for `lisp-indent-function'; the spec for the
|
||||
enclosing form comes from `flan-indent-specs'. Returns nil to leave the
|
||||
decision to `calculate-lisp-indent'."
|
||||
(goto-char (elt state 1))
|
||||
(if (flan--data-form-p)
|
||||
;; A vector, a map, or a head that is not a symbol: align under the
|
||||
;; first element.
|
||||
(1+ (current-column))
|
||||
(forward-char 1)
|
||||
(let* ((name (thing-at-point 'symbol))
|
||||
(method (flan--indent-spec name))
|
||||
(last-sexp calculate-lisp-indent-last-sexp)
|
||||
(head-column (1- (current-column))))
|
||||
(cond
|
||||
((integerp method)
|
||||
(flan--count-indent method indent-point last-sexp head-column))
|
||||
((eq method :defn) (+ lisp-body-indent head-column))
|
||||
;; No spec. Anything else spelled `def…' is a definition and indents
|
||||
;; like one, which covers `defstruct', `defunion', `defenum', `defvar',
|
||||
;; `defconst' and `defalias' without naming them.
|
||||
((and name (string-match-p "\\`def" name))
|
||||
(+ lisp-body-indent head-column))
|
||||
;; A clause: `(name [params] body…)'. `handler-bind', `handler-case'
|
||||
;; and `restart-case' all write their clauses this way, and the head is
|
||||
;; a condition class or a restart name — something the program invented,
|
||||
;; so it can never be in a table here. What can be recognised is the
|
||||
;; *shape*, which is `defn' with the name left off: a parameter vector
|
||||
;; where the first argument goes, and a body after it.
|
||||
;;
|
||||
;; `clojure-mode' reaches the same clauses by backtracking out to the
|
||||
;; enclosing form and reading a nested spec off it. That machinery buys
|
||||
;; generality this language has no other use for — these three forms are
|
||||
;; the whole of it — and the shape is unambiguous on its own.
|
||||
((flan--clause-form-p) (flan--count-indent 1 indent-point last-sexp
|
||||
head-column))
|
||||
(t (flan--normal-indent last-sexp))))))
|
||||
|
||||
;;;###autoload
|
||||
(add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode))
|
||||
|
||||
@ -379,16 +379,18 @@
|
||||
(test-flan--check "and the number past the last is abort"
|
||||
(equal sent '(:op "abort"))))))
|
||||
|
||||
;; The stack and its locals. Nothing produces this data today; the fixture is
|
||||
;; the shape a `backtrace' verb would have to answer with, and building the
|
||||
;; buffer against it is how it is ready when one exists.
|
||||
;; The stack and its locals. The fixture is the shape `backtrace' and `locals'
|
||||
;; answer with, and both frames carry `:fetched t' because their locals are
|
||||
;; already here: without it `flan-cnr-toggle-frame' goes and asks the daemon
|
||||
;; for them, and there is no daemon behind these tests on purpose.
|
||||
(let* ((state (list :condition "Missing"
|
||||
:restarts '("retry")
|
||||
:stack (list (list :fn "sim/settle" :loc "sand.flan:42:3"
|
||||
:fetched t
|
||||
:locals '(("i" "i32" "7")
|
||||
("b" "Blob" "(Blob {:id 7})")))
|
||||
(list :fn "sim/step" :loc "sand.flan:60:1"
|
||||
:locals nil))))
|
||||
:fetched t :locals nil))))
|
||||
(buf (test-flan--cnr state))
|
||||
(text (with-current-buffer buf (buffer-string))))
|
||||
(test-flan--check "frames are numbered, innermost first"
|
||||
@ -430,7 +432,8 @@
|
||||
(flan-inspect-buffer " *test-inspect*"))
|
||||
(with-current-buffer (test-flan--cnr
|
||||
(list :condition "Missing" :restarts '("retry")
|
||||
:stack (list (list :fn "f" :locals '(("b" "Blob" "…"))))))
|
||||
:stack (list (list :fn "f" :fetched t
|
||||
:locals '(("b" "Blob" "…"))))))
|
||||
(goto-char (point-min))
|
||||
(search-forward " 0: > f")
|
||||
(flan-cnr-toggle-frame)
|
||||
@ -494,6 +497,9 @@
|
||||
(buffer-string))))
|
||||
(test-flan--check "the condition's name is what `layout' is asked for"
|
||||
(equal (plist-get (car (last asked)) :op) "break"))
|
||||
;; By op rather than by position: `flan-cnr-show' asks three things now —
|
||||
;; `break', `layout', `backtrace' — and which of them is last is not what
|
||||
;; this is testing.
|
||||
(test-flan--check "and it is sent back verbatim, qualified as it came"
|
||||
(equal (plist-get
|
||||
(seq-find (lambda (f)
|
||||
@ -539,6 +545,15 @@
|
||||
(or (test-flan--caught #'flan-cnr-show) ""))))
|
||||
|
||||
|
||||
;; `flan-mode' itself — indentation, which is a function from text to text and
|
||||
;; so belongs with the other fixture-driven checks rather than with anything
|
||||
;; that needs a daemon. Loaded rather than run separately because
|
||||
;; `emacs/*.el' is already a dependency of test/dune's stanza, so a file here
|
||||
;; needs no build change to be run.
|
||||
(load (expand-file-name "test-flan-mode.el"
|
||||
(file-name-directory load-file-name))
|
||||
nil t)
|
||||
|
||||
(message "\n%d checks, %d failures" test-flan--ran test-flan--failures)
|
||||
(kill-emacs (if (> test-flan--failures 0) 1 0))
|
||||
|
||||
|
||||
208
emacs/test-flan-mode.el
Normal file
208
emacs/test-flan-mode.el
Normal file
@ -0,0 +1,208 @@
|
||||
;;; test-flan-mode.el --- Indentation, from written-out shapes -*- lexical-binding: t; -*-
|
||||
|
||||
;; Loaded by test-flan-cider.el, which runs under `dune test'. It is not a
|
||||
;; file of its own in test/dune deliberately: `emacs/*.el' is already a
|
||||
;; dependency of that stanza, so a new `.el' here needs no build change.
|
||||
;;
|
||||
;; Every case is a piece of Flan written the way the corpus writes it, with
|
||||
;; every line's indentation thrown away and put back. That is the whole test:
|
||||
;; if what comes out is what went in, the indenter agrees with the code that
|
||||
;; already exists. A case that is *not* in the corpus is written from
|
||||
;; `clojure-mode''s behaviour and says so.
|
||||
;;
|
||||
;; The cases come from the bug report and from reading the tree for the shapes
|
||||
;; that share it: a `let' binding vector, `defn' parameters with and without a
|
||||
;; return type, `handler-bind' and `restart-case' clause parameters, and struct
|
||||
;; literals in both the `:x' and the `.x' spelling — the two must agree,
|
||||
;; because a lane is converting the corpus from one to the other and the
|
||||
;; indenter is not allowed to notice.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'flan-mode)
|
||||
|
||||
;; The harness, which is test-flan-cider.el's: it counts and it reports, and
|
||||
;; this file is loaded from there.
|
||||
(declare-function test-flan--check "test-flan-cider" (name ok))
|
||||
|
||||
(defun test-flan-mode--reindent (text)
|
||||
"Strip TEXT's indentation and let `flan-mode' put it back."
|
||||
;; Quiet: `indent-region' reports progress, and in batch that lands in the
|
||||
;; middle of the line a failure is being printed on.
|
||||
(let ((inhibit-message t))
|
||||
(with-temp-buffer
|
||||
(insert text)
|
||||
(flan-mode)
|
||||
(goto-char (point-min))
|
||||
;; Leading whitespace off every line but the first: the first line's column
|
||||
;; is the buffer's, not the indenter's, and a form is indented relative to
|
||||
;; where it starts. `eobp' rather than `forward-line''s return value,
|
||||
;; which is 0 for a move to the end of a buffer that has no final newline
|
||||
;; and would take the whole last line with it.
|
||||
(forward-line 1)
|
||||
(while (not (eobp))
|
||||
(skip-chars-forward " \t")
|
||||
(delete-region (line-beginning-position) (point))
|
||||
(forward-line 1))
|
||||
(indent-region (point-min) (point-max))
|
||||
(buffer-substring-no-properties (point-min) (point-max)))))
|
||||
|
||||
(defun test-flan-mode--check (name text)
|
||||
"Check that TEXT is what `flan-mode' indents it to."
|
||||
(let ((got (test-flan-mode--reindent text)))
|
||||
(test-flan--check name (equal got text))
|
||||
(unless (equal got text)
|
||||
;; Line by line, and quoted: the difference is always a count of leading
|
||||
;; spaces, which is exactly what two blocks of text printed as-is make
|
||||
;; hardest to see.
|
||||
(let ((want (split-string text "\n")) (had (split-string got "\n")))
|
||||
(while (or want had)
|
||||
(unless (equal (car want) (car had))
|
||||
(message " want %S\n got %S" (car want) (car had)))
|
||||
(setq want (cdr want) had (cdr had)))))))
|
||||
|
||||
(message "\n-- indentation")
|
||||
|
||||
;; The bug, as reported: sand.flan's `settle'. The second and later bindings
|
||||
;; went one column too far, because the enclosing open is `[' and the symbol
|
||||
;; after it is the first binding's *name*, so the old check fell through to
|
||||
;; `lisp-indent-function', which treated the vector as a call and aligned under
|
||||
;; the first argument — `(+ gravity …)' — instead of under `vel'.
|
||||
(test-flan-mode--check
|
||||
"a let's bindings align name under name"
|
||||
"(defn settle [row i32 col i32]
|
||||
(let [vel (+ gravity (at velocity row col))
|
||||
y (min (- rows 1) (+ row (i32 vel)))]
|
||||
(while (> y row)
|
||||
(set y (- y 1)))))")
|
||||
|
||||
;; The same shape one level in, which is where the report was actually hit: a
|
||||
;; binding whose value is a struct literal, so the vector's alignment has to
|
||||
;; 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})
|
||||
vel (+ gravity 1.0)]
|
||||
(draw tip vel))")
|
||||
|
||||
;; The two spellings must be indented identically. Nothing in the indenter
|
||||
;; looks at the key — a brace aligns under its first element whatever that
|
||||
;; 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"
|
||||
"(rl/Rectangle {.x 0.0 .y 0.0
|
||||
.width (f32 screen-width)
|
||||
.height (f32 screen-height)})")
|
||||
|
||||
;; `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
|
||||
;; there.
|
||||
(test-flan-mode--check
|
||||
"a defn with a return type indents its body by two"
|
||||
"(defn look [n i64 label string] i64
|
||||
(print label)
|
||||
n)")
|
||||
|
||||
(test-flan-mode--check
|
||||
"and a defn without one indents it the same"
|
||||
"(defn show-trim [s string]
|
||||
(print s)
|
||||
(println \"\"))")
|
||||
|
||||
;; A parameter list that wraps is the binding-vector rule again: it is a
|
||||
;; vector, so it aligns under its first element and not under its second.
|
||||
(test-flan-mode--check
|
||||
"a wrapped parameter list aligns under the first parameter"
|
||||
"(defn move-grain [row i32 col i32
|
||||
to-row i32 to-col i32
|
||||
vel f32]
|
||||
(set moved true))")
|
||||
|
||||
;; `handler-bind' clauses: the vector is the special argument, and each clause
|
||||
;; inside it is `(Name [params] body…)' — `defn' with the name left off. The
|
||||
;; body indents by two from the clause's own paren, which is the rule the head
|
||||
;; cannot be looked up for: `StorageExhausted' is a name the program invented.
|
||||
(test-flan-mode--check
|
||||
"a handler-bind clause indents its body under its own paren"
|
||||
"(handler-bind
|
||||
[(StorageExhausted [c]
|
||||
(set failures (+ failures 1))
|
||||
(invoke-restart 'retry))]
|
||||
(load-all))")
|
||||
|
||||
(test-flan-mode--check
|
||||
"and two clauses in one vector line up with each other"
|
||||
"(handler-bind [(AssetMissing [c] (set seen (+ seen 10)))
|
||||
(Corrupt [c] (set other (+ other 1)))]
|
||||
(load-all))")
|
||||
|
||||
;; `restart-case' is 1 rather than 0: with 0 a protected form written on the
|
||||
;; head's line would drag every clause out to align under it.
|
||||
(test-flan-mode--check
|
||||
"restart-case clauses indent by two under a protected form on the head's line"
|
||||
"(defn fetch [n i32] i32
|
||||
(restart-case (middle n)
|
||||
(use-placeholder [] -1)
|
||||
(retry [] 7)))")
|
||||
|
||||
(test-flan-mode--check
|
||||
"and by two when the protected form is on its own line"
|
||||
"(restart-case
|
||||
(do (agent/poll)
|
||||
(game-update))
|
||||
(continue [] (do)))")
|
||||
|
||||
;; `declare-c' writes its parameters, return type and C symbol down the page in
|
||||
;; one column.
|
||||
(test-flan-mode--check
|
||||
"declare-c puts everything after the name in one column"
|
||||
"(declare-c mouse-button-pressed?
|
||||
[button MouseButton] bool
|
||||
\"IsMouseButtonPressed\")")
|
||||
|
||||
;; An ordinary call, in the two cases every Lisp separates.
|
||||
(test-flan-mode--check
|
||||
"a call with an argument on the head's line aligns under that argument"
|
||||
"(rl/draw-text \"hello\"
|
||||
10 20 30)")
|
||||
|
||||
(test-flan-mode--check
|
||||
"a call whose head is alone on its line indents its arguments by a body"
|
||||
"(rl/draw-rectangle-lines-ex
|
||||
(rl/Rectangle {.x 0.0 .y 0.0})
|
||||
(f32 2.0) (rl/get-color 0x303030FF))")
|
||||
|
||||
;; A `do' is all body, so its second form aligns under its first.
|
||||
(test-flan-mode--check
|
||||
"a do aligns its forms with the first one"
|
||||
"(do (agent/poll)
|
||||
(game-update))")
|
||||
|
||||
;; Field access is an ordinary call whose head begins with a dot. `.' is a
|
||||
;; symbol constituent, so nothing special is needed — but if it ever stopped
|
||||
;; being one, `(.x v)' would start indenting as data and this is what would
|
||||
;; say so.
|
||||
(test-flan-mode--check
|
||||
"field access indents as the call it is"
|
||||
"(set total
|
||||
(+ (.bytes c)
|
||||
(.align c)))")
|
||||
|
||||
;; `#_' discards the form after it, so the reader sees pairs either way and the
|
||||
;; alignment must not shift. See `lib/reader.ml'.
|
||||
(test-flan-mode--check
|
||||
"a discarded binding does not disturb the pairs around it"
|
||||
"(let [#_a #_1 b 2
|
||||
c 3]
|
||||
(print c))")
|
||||
|
||||
(provide 'test-flan-mode)
|
||||
;;; test-flan-mode.el ends here
|
||||
@ -13,6 +13,17 @@
|
||||
Lisp's, because [,] is already whitespace here (see [is_delimiter]) and
|
||||
every binding vector in the corpus relies on that.
|
||||
|
||||
[#_] discards the form after it, as in Clojure: it is read and thrown away,
|
||||
so commenting out a form does not mean counting its closing parens. Repeated
|
||||
([#_#_]) discards that many following forms, which falls out of the
|
||||
recursion rather than being counted — the discard reads *a form*, and the
|
||||
form it reads may itself begin with a discard.
|
||||
|
||||
It is a property of [read_form] rather than of the sequence readers, so it
|
||||
works in every position a form can appear: at the top level, inside a list
|
||||
or a vector or a map, and after a quote. A trailing [#_] with nothing after
|
||||
it is the one error, and it is the same error an unterminated form gives.
|
||||
|
||||
Not handled yet: metadata ([^:async]). It is rejected rather than read as a
|
||||
symbol, so it cannot rot into a silently-wrong name the way quote would
|
||||
have. *)
|
||||
@ -156,7 +167,7 @@ let wrap open_c items =
|
||||
| _ -> assert false
|
||||
|
||||
let rec read_form st =
|
||||
skip_trivia st;
|
||||
skip_ignorable st;
|
||||
let loc = here st in
|
||||
match peek st with
|
||||
| '\000' -> Loc.fail loc "unexpected end of input"
|
||||
@ -177,6 +188,24 @@ let rec read_form st =
|
||||
| ('-' | '+') when is_digit (peek2 st) -> read_number st
|
||||
| _ -> read_symbol_or_keyword st
|
||||
|
||||
(* Whitespace, comments, and forms that are read only to be thrown away.
|
||||
[#_] is handled here rather than in [read_form]'s match so that it is gone
|
||||
before *anything* looks at what comes next: a discard is not a form, so a
|
||||
sequence must not count it as an element and a top-level loop must not stop
|
||||
on it.
|
||||
|
||||
[#_#_ a b c] discards [a] and [b] with no counting. The outer discard reads
|
||||
one form; that read is [read_form], which sees the inner [#_], discards [a]
|
||||
and returns [b]; the outer discard then throws [b] away. What is left is
|
||||
[c]. *)
|
||||
and skip_ignorable st =
|
||||
skip_trivia st;
|
||||
if peek st = '#' && peek2 st = '_' then begin
|
||||
advance st; advance st;
|
||||
ignore (read_form st : Form.t);
|
||||
skip_ignorable st
|
||||
end
|
||||
|
||||
(* One sigil character, then the form it applies to, wrapped in a name. The
|
||||
name's location is the sigil's, so an error inside the wrapper points at the
|
||||
character the reader saw rather than at the form after it. *)
|
||||
@ -190,7 +219,10 @@ and read_seq st open_c loc =
|
||||
advance st;
|
||||
let want = closer open_c in
|
||||
let rec go acc =
|
||||
skip_trivia st;
|
||||
(* [skip_ignorable], not [skip_trivia]: a discard just before the closer —
|
||||
[(a #_b)] — has to be gone before the closer is looked for, or the
|
||||
sequence would try to read a form and find [)]. *)
|
||||
skip_ignorable st;
|
||||
if at_end st then
|
||||
Loc.fail loc "unclosed %C, expected %C" open_c want
|
||||
else
|
||||
@ -206,7 +238,9 @@ and read_seq st open_c loc =
|
||||
let read_all ~file src =
|
||||
let st = of_string ~file src in
|
||||
let rec go acc =
|
||||
skip_trivia st;
|
||||
(* Same reason as in [read_seq]: a file ending in [#_(defn …)] has read
|
||||
everything there is to read, and must not then be asked for a form. *)
|
||||
skip_ignorable st;
|
||||
if at_end st then List.rev acc else go (read_form st :: acc)
|
||||
in
|
||||
go []
|
||||
|
||||
@ -162,6 +162,35 @@ let () =
|
||||
reads "backtick ends a name" "(f a`b)" "(f a (quasiquote b))";
|
||||
reads "backtick in vec" "[`a ~b]" "[(quasiquote a) (unquote b)]";
|
||||
|
||||
(* ── Discard ───────────────────────────────────────────────────── *)
|
||||
(* [#_] reads the next form and throws it away, so commenting out a form does
|
||||
not mean counting its closing parens. Clojure's spelling and Clojure's
|
||||
semantics, including the repeated form. *)
|
||||
reads "discard in a call" "(f #_a b)" "(f b)";
|
||||
reads "discard the last" "(f a #_b)" "(f a)";
|
||||
reads "discard the first" "(#_f g a)" "(g a)";
|
||||
reads "discard a list" "(f #_(g x) b)" "(f b)";
|
||||
reads "discard in a vector" "[a #_b c]" "[a c]";
|
||||
reads "discard in a map" "{:a 1 #_:b #_2 :c 3}" "{:a 1 :c 3}";
|
||||
(* Two discards drop two forms, and that is the recursion rather than a
|
||||
count: the outer discard reads one form, and the form it reads is itself a
|
||||
discard that returns the one after. *)
|
||||
reads "two discards" "(f #_#_a b c)" "(f c)";
|
||||
reads "three discards" "(f #_#_#_a b c d)" "(f d)";
|
||||
(* Every position a form can appear in. *)
|
||||
reads "discard at top level" "#_(defn a [] 1) (defn b [] 2)" "(defn b [] 2)";
|
||||
reads "discard a whole file" "#_(defn a [] 1)" "";
|
||||
reads "discard before quote" "(f #_a 'b)" "(f (quote b))";
|
||||
reads "discard of a quote" "(f #_'a b)" "(f b)";
|
||||
(* Nested, which the recursive read gives for free. *)
|
||||
reads "discard inside a discarded form" "(f #_(g #_h i) j)" "(f j)";
|
||||
(* A name may still contain '#' — it is only a discard at the start of a
|
||||
form, after trivia. *)
|
||||
reads "hash inside a name" "(f a#_b)" "(f a#_b)";
|
||||
(* Nothing to discard is an error, not a silent nothing. *)
|
||||
rejects ~needle:"end of input" "discard at end of input" "(f a #_";
|
||||
rejects ~needle:"unbalanced" "discard of a closing paren" "(f a #_)";
|
||||
|
||||
(* The whole class: no reader-significant character may end up inside a name. *)
|
||||
let rec bad_names f =
|
||||
let open Form in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user