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:
|
**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
|
`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
|
has to move in the same commit as its reader. That half is still open and belongs with whoever next opens the
|
||||||
matching `.name`, so a field label is now unfontified where it used to be coloured. Neither is urgent; both belong
|
inspector. ~~And `flan-mode.el:61` font-locks `:name` as a constant with nothing matching `.name`, so a field label
|
||||||
with whoever next opens `emacs/`.
|
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;
|
**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
|
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.
|
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
|
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
|
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
|
reference~~; ~~`#_`~~; hex, binary and addresses on primitives in the inspector. **The indentation rewrite and `#_`
|
||||||
within this batch — it costs friction on every keystroke today.
|
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 —
|
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.
|
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
|
## Getting around
|
||||||
|
|
||||||
| Key | Does |
|
| Key | Does |
|
||||||
|
|||||||
@ -1,13 +1,51 @@
|
|||||||
;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
|
;;; 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
|
;; Derived from `prog-mode', borrowing `lisp-mode''s machinery for the parts
|
||||||
;; sexp motion, paren matching, `beginning-of-defun' and indentation all
|
;; that are simply s-expressions: sexp motion, paren matching and
|
||||||
;; already do the right thing. What is left is what the language actually adds
|
;; `beginning-of-defun' already do the right thing.
|
||||||
;; — its own literals and its own set of forms that indent like a body.
|
;;
|
||||||
|
;; 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:
|
;;; Code:
|
||||||
|
|
||||||
(require 'lisp-mode)
|
(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
|
;; For `imenu-generic-expression', which is set below and which would
|
||||||
;; otherwise be made buffer-local before its own defvar had run.
|
;; otherwise be made buffer-local before its own defvar had run.
|
||||||
(require 'imenu)
|
(require 'imenu)
|
||||||
@ -33,6 +71,8 @@
|
|||||||
(autoload 'flan-dev "flan-dev" nil t)
|
(autoload 'flan-dev "flan-dev" nil t)
|
||||||
(autoload 'flan-dev-quit "flan-dev" nil t)
|
(autoload 'flan-dev-quit "flan-dev" nil t)
|
||||||
(autoload 'flan-dev-restart-program "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
|
(defgroup flan nil
|
||||||
"Editing and evaluating Flan."
|
"Editing and evaluating Flan."
|
||||||
@ -59,6 +99,11 @@
|
|||||||
;; A keyword resolves against an enum at the call site, so it reads as a
|
;; A keyword resolves against an enum at the call site, so it reads as a
|
||||||
;; constant rather than as a string.
|
;; constant rather than as a string.
|
||||||
("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
|
("\\_<:\\(?:\\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.
|
;; 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\\)\\_>"
|
("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|Unit\\|Never\\|Ptr\\|Option\\)\\_>"
|
||||||
. font-lock-type-face)
|
. font-lock-type-face)
|
||||||
@ -160,25 +205,231 @@ line is off screen."
|
|||||||
(setq-local comment-add 1)
|
(setq-local comment-add 1)
|
||||||
(setq-local font-lock-defaults '(flan-font-lock-keywords))
|
(setq-local font-lock-defaults '(flan-font-lock-keywords))
|
||||||
(setq-local indent-line-function #'lisp-indent-line)
|
(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 lisp-indent-function #'flan-indent-function)
|
||||||
(setq-local outline-regexp ";;;;+[ \t]*")
|
(setq-local outline-regexp ";;;;+[ \t]*")
|
||||||
(setq-local imenu-generic-expression flan-imenu-generic-expression)
|
(setq-local imenu-generic-expression flan-imenu-generic-expression)
|
||||||
;; Buffer-locally, because this answers for Flan and nothing else.
|
;; Buffer-locally, because this answers for Flan and nothing else.
|
||||||
(add-hook 'which-func-functions #'flan-current-defun-name nil t))
|
(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)
|
(defun flan-indent-function (indent-point state)
|
||||||
"Indent like Lisp, with Flan's body forms as special forms.
|
"Indent a line inside a Flan form.
|
||||||
INDENT-POINT and STATE are as for `lisp-indent-function'."
|
INDENT-POINT and STATE are as for `lisp-indent-function'; the spec for the
|
||||||
(let ((open (elt state 1)))
|
enclosing form comes from `flan-indent-specs'. Returns nil to leave the
|
||||||
(or (and open
|
decision to `calculate-lisp-indent'."
|
||||||
(save-excursion
|
(goto-char (elt state 1))
|
||||||
(goto-char (1+ open))
|
(if (flan--data-form-p)
|
||||||
(let ((head (and (looking-at "\\(\\sw\\|\\s_\\)+")
|
;; A vector, a map, or a head that is not a symbol: align under the
|
||||||
(match-string 0))))
|
;; first element.
|
||||||
(when (member head '("defn" "let" "if" "while" "until"
|
(1+ (current-column))
|
||||||
"dotimes" "match" "do" "loop" "defer"))
|
(forward-char 1)
|
||||||
(+ (current-column) 1)))))
|
(let* ((name (thing-at-point 'symbol))
|
||||||
(lisp-indent-function indent-point state))))
|
(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
|
;;;###autoload
|
||||||
(add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode))
|
(add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode))
|
||||||
|
|||||||
@ -379,16 +379,18 @@
|
|||||||
(test-flan--check "and the number past the last is abort"
|
(test-flan--check "and the number past the last is abort"
|
||||||
(equal sent '(:op "abort"))))))
|
(equal sent '(:op "abort"))))))
|
||||||
|
|
||||||
;; The stack and its locals. Nothing produces this data today; the fixture is
|
;; The stack and its locals. The fixture is the shape `backtrace' and `locals'
|
||||||
;; the shape a `backtrace' verb would have to answer with, and building the
|
;; answer with, and both frames carry `:fetched t' because their locals are
|
||||||
;; buffer against it is how it is ready when one exists.
|
;; 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"
|
(let* ((state (list :condition "Missing"
|
||||||
:restarts '("retry")
|
:restarts '("retry")
|
||||||
:stack (list (list :fn "sim/settle" :loc "sand.flan:42:3"
|
:stack (list (list :fn "sim/settle" :loc "sand.flan:42:3"
|
||||||
|
:fetched t
|
||||||
:locals '(("i" "i32" "7")
|
:locals '(("i" "i32" "7")
|
||||||
("b" "Blob" "(Blob {:id 7})")))
|
("b" "Blob" "(Blob {:id 7})")))
|
||||||
(list :fn "sim/step" :loc "sand.flan:60:1"
|
(list :fn "sim/step" :loc "sand.flan:60:1"
|
||||||
:locals nil))))
|
:fetched t :locals nil))))
|
||||||
(buf (test-flan--cnr state))
|
(buf (test-flan--cnr state))
|
||||||
(text (with-current-buffer buf (buffer-string))))
|
(text (with-current-buffer buf (buffer-string))))
|
||||||
(test-flan--check "frames are numbered, innermost first"
|
(test-flan--check "frames are numbered, innermost first"
|
||||||
@ -430,7 +432,8 @@
|
|||||||
(flan-inspect-buffer " *test-inspect*"))
|
(flan-inspect-buffer " *test-inspect*"))
|
||||||
(with-current-buffer (test-flan--cnr
|
(with-current-buffer (test-flan--cnr
|
||||||
(list :condition "Missing" :restarts '("retry")
|
(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))
|
(goto-char (point-min))
|
||||||
(search-forward " 0: > f")
|
(search-forward " 0: > f")
|
||||||
(flan-cnr-toggle-frame)
|
(flan-cnr-toggle-frame)
|
||||||
@ -494,6 +497,9 @@
|
|||||||
(buffer-string))))
|
(buffer-string))))
|
||||||
(test-flan--check "the condition's name is what `layout' is asked for"
|
(test-flan--check "the condition's name is what `layout' is asked for"
|
||||||
(equal (plist-get (car (last asked)) :op) "break"))
|
(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"
|
(test-flan--check "and it is sent back verbatim, qualified as it came"
|
||||||
(equal (plist-get
|
(equal (plist-get
|
||||||
(seq-find (lambda (f)
|
(seq-find (lambda (f)
|
||||||
@ -539,6 +545,15 @@
|
|||||||
(or (test-flan--caught #'flan-cnr-show) ""))))
|
(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)
|
(message "\n%d checks, %d failures" test-flan--ran test-flan--failures)
|
||||||
(kill-emacs (if (> test-flan--failures 0) 1 0))
|
(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
|
Lisp's, because [,] is already whitespace here (see [is_delimiter]) and
|
||||||
every binding vector in the corpus relies on that.
|
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
|
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
|
symbol, so it cannot rot into a silently-wrong name the way quote would
|
||||||
have. *)
|
have. *)
|
||||||
@ -156,7 +167,7 @@ let wrap open_c items =
|
|||||||
| _ -> assert false
|
| _ -> assert false
|
||||||
|
|
||||||
let rec read_form st =
|
let rec read_form st =
|
||||||
skip_trivia st;
|
skip_ignorable st;
|
||||||
let loc = here st in
|
let loc = here st in
|
||||||
match peek st with
|
match peek st with
|
||||||
| '\000' -> Loc.fail loc "unexpected end of input"
|
| '\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
|
| ('-' | '+') when is_digit (peek2 st) -> read_number st
|
||||||
| _ -> read_symbol_or_keyword 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
|
(* 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
|
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. *)
|
character the reader saw rather than at the form after it. *)
|
||||||
@ -190,7 +219,10 @@ and read_seq st open_c loc =
|
|||||||
advance st;
|
advance st;
|
||||||
let want = closer open_c in
|
let want = closer open_c in
|
||||||
let rec go acc =
|
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
|
if at_end st then
|
||||||
Loc.fail loc "unclosed %C, expected %C" open_c want
|
Loc.fail loc "unclosed %C, expected %C" open_c want
|
||||||
else
|
else
|
||||||
@ -206,7 +238,9 @@ and read_seq st open_c loc =
|
|||||||
let read_all ~file src =
|
let read_all ~file src =
|
||||||
let st = of_string ~file src in
|
let st = of_string ~file src in
|
||||||
let rec go acc =
|
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)
|
if at_end st then List.rev acc else go (read_form st :: acc)
|
||||||
in
|
in
|
||||||
go []
|
go []
|
||||||
|
|||||||
@ -162,6 +162,35 @@ let () =
|
|||||||
reads "backtick ends a name" "(f a`b)" "(f a (quasiquote b))";
|
reads "backtick ends a name" "(f a`b)" "(f a (quasiquote b))";
|
||||||
reads "backtick in vec" "[`a ~b]" "[(quasiquote a) (unquote 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. *)
|
(* The whole class: no reader-significant character may end up inside a name. *)
|
||||||
let rec bad_names f =
|
let rec bad_names f =
|
||||||
let open Form in
|
let open Form in
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user