*flan-output* is gone. The program's output lands in the daemon's buffer always — renamed *flan-dev* to *flan* — and at the REPL when one is open, inserted above the prompt, output first and the value after it. A rejection puts its message in *flan-diagnostics*, which now pops up, and leaves one line at the prompt pointing there; the diagnostics buffer got a major mode of its own, read-only with n/p/RET, and the memory sites from flan-check-memory render into it as one section below the errors, replaced whole on every ask. Two clears at the REPL, on CIDER's keys: C-c C-o for the last send's output, C-c M-o for the transcript. No daemon changes: output already rides every reply's :output, so both destinations are editor-side routing.
623 lines
31 KiB
EmacsLisp
623 lines
31 KiB
EmacsLisp
;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
|
|
|
|
;; Author: Joseph Ferano <joseph@ferano.io>
|
|
;; Version: 0.1.0
|
|
;; Package-Requires: ((emacs "29.1"))
|
|
;; Keywords: languages, lisp, tools
|
|
|
|
;; The headers above are what make this directory installable. M-x
|
|
;; package-install-file on it reads them, and a file with no Version: is not a
|
|
;; package as far as package.el is concerned -- until now the client was
|
|
;; reachable only by adding it to load-path by hand, which is a thing to
|
|
;; explain to every person who wants to try it.
|
|
;;
|
|
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
|
;; against, not because some function here is known to need it. dape, which
|
|
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
|
;; reached through declare-function, so the rest of the client loads and works
|
|
;; without it and it is deliberately not listed above. The compiler this talks
|
|
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
|
;; says what has to be on PATH.
|
|
|
|
;; 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.
|
|
;; - 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:
|
|
|
|
(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)
|
|
|
|
;; The keymap binds them, but do not load the client merely to edit a file.
|
|
;; These must be real autoloads, not just `declare-function`s: otherwise a
|
|
;; user who has loaded only flan-mode cannot invoke M-x flan at all.
|
|
(autoload 'flan-eval-defun "flan" nil t)
|
|
(autoload 'flan-eval-buffer "flan" nil t)
|
|
(autoload 'flan-eval-last-sexp "flan" nil t)
|
|
(autoload 'flan-connect "flan" nil t)
|
|
(autoload 'flan-disconnect "flan" nil t)
|
|
(autoload 'flan-describe "flan" nil t)
|
|
(autoload 'flan-repl "flan-repl" nil t)
|
|
(autoload 'flan-repl-clear "flan-repl" nil t)
|
|
(autoload 'flan-repl-clear-output "flan-repl" nil t)
|
|
(autoload 'flan-break "flan" nil t)
|
|
;; The two CIDER-shaped buffers. They reach the daemon through an indirection
|
|
;; of their own so that fixtures can drive them, so autoloading is all the
|
|
;; wiring they need.
|
|
(autoload 'flan-inspect "flan-inspect" nil t)
|
|
(autoload 'flan-cnr-show "flan-cnr" nil t)
|
|
(autoload 'flan-doc "flan" nil t)
|
|
(autoload 'flan "flan" nil t)
|
|
(autoload 'flan-quit "flan" nil t)
|
|
(autoload 'flan-restart-program "flan" nil t)
|
|
;; And the cheap counterpart, which needs an autoload for the reason above and
|
|
;; more than most: it is the command someone reaches for the moment a window
|
|
;; closes, which can be the first thing they ever ask the client to do.
|
|
(autoload 'flan-rerun "flan" nil t)
|
|
;; Bound below, like the rest, and it was the one missing an autoload.
|
|
(autoload 'flan-disassemble "flan" nil t)
|
|
;; The other question about the same function, and the reason it is a second
|
|
;; command rather than a fifth argument to the first: `flan-disassemble' asks
|
|
;; the running program, and this compiles the file.
|
|
(autoload 'flan-lowering "flan-lower" nil t)
|
|
;; C-c C-m and the half of it that is findable by name rather than by a
|
|
;; modifier. Real autoloads for the reason stated above: a `declare-function'
|
|
;; would leave M-x with nothing to load.
|
|
(autoload 'flan-macroexpand "flan" nil t)
|
|
(autoload 'flan-macroexpand-all "flan" nil t)
|
|
|
|
(defgroup flan nil
|
|
"Editing and evaluating Flan."
|
|
:group 'languages
|
|
:prefix "flan-")
|
|
|
|
;; Both lists are read off `Parse.decl' and `Parse.form' (lib/parse.ml), which
|
|
;; are where the two questions are actually decided: a head `decl' has an arm
|
|
;; for introduces a name, and a head `form' has an arm for is never a call.
|
|
;; Kept whole rather than topped up one name at a time — the list that is
|
|
;; patched only when somebody notices a gap is the list that is always a
|
|
;; release behind the parser.
|
|
(defconst flan--definers
|
|
'("defn" "defmacro" "defvar" "defconst" "defstruct" "defdata" "defunion"
|
|
"defenum" "defalias"
|
|
;; The object and dispatch heads (lib/parse.ml:1277-1334).
|
|
"defclass" "defgeneric" "defmulti" "defmethod"
|
|
"declare" "declare-c" "import" "package")
|
|
"Forms that introduce a top-level name.")
|
|
|
|
(defconst flan--special
|
|
'("quote" "let" "if" "when" "cond" "and" "or" "do" "while" "until" "dotimes"
|
|
"loop" "recur" "break" "continue" "match" "set" "return" "fn" "array"
|
|
"defer" "some" "none" "try" "signal" "error"
|
|
"handler-bind" "handler-case" "restart-case" "invoke-restart"
|
|
"zeroed" "uninit" "slice" "at" "len" "addr"
|
|
"bytes" "cast" "true" "false" "nil" "print" "println")
|
|
"Forms with meaning to the checker.
|
|
|
|
The heads `Parse.form' dispatches on, plus the literals and the handful of
|
|
builtins that are never anything else. Two groups of real heads are
|
|
deliberately left out: `quasiquote', `unquote' and `unquote-splicing', which
|
|
nobody writes as words — the reader makes them out of \\=`, ~ and ~@, and the
|
|
sigils are not symbols for a keyword rule to reach — and `find-restart',
|
|
`compute-restarts', `errdefer' and `await', which the parser recognises only
|
|
in order to refuse them. Drawing those four as keywords would advertise four
|
|
forms that cannot be used.")
|
|
|
|
(defvar flan-font-lock-keywords
|
|
`((,(concat "(" (regexp-opt flan--definers t) "\\_>"
|
|
"[ \t]*\\(\\(?:\\sw\\|\\s_\\)+\\)?")
|
|
(1 font-lock-keyword-face)
|
|
(2 font-lock-function-name-face nil t))
|
|
(,(concat "(" (regexp-opt flan--special t) "\\_>") 1 font-lock-keyword-face)
|
|
;; 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. 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\\)\\_>"
|
|
. font-lock-type-face)
|
|
("\\_<\\(?:0x[0-9a-fA-F]+\\|-?[0-9]+\\(?:\\.[0-9]+\\)?\\)\\_>"
|
|
. font-lock-constant-face))
|
|
"Font lock for `flan-mode'.")
|
|
|
|
(defconst flan--name-re "\\(\\(?:\\sw\\|\\s_\\)+\\)"
|
|
"A Flan name, as one group.
|
|
Written in terms of the syntax table rather than as a character class, so
|
|
that the characters a name may contain are stated in one place — the table
|
|
below — and not again here.")
|
|
|
|
;; Anchored at the start of a line, which is where a top-level form is: a
|
|
;; `defn' nested inside a `let' is not a definition of anything, and a match
|
|
;; that ignored the column would offer one.
|
|
(defvar flan-imenu-generic-expression
|
|
`(("Functions" ,(concat "^(defn\\s-+" flan--name-re) 1)
|
|
("Types" ,(concat "^(def\\(?:struct\\|data\\|union\\|enum\\|alias\\)\\s-+"
|
|
flan--name-re)
|
|
1)
|
|
("Variables" ,(concat "^(def\\(?:var\\|const\\)\\s-+" flan--name-re) 1)
|
|
;; A forward declaration is not a definition, and a file with both would
|
|
;; otherwise show the same name twice with nothing to tell them apart.
|
|
("Declared" ,(concat "^(declare\\s-+" flan--name-re) 1))
|
|
"Imenu index for `flan-mode', by what each form introduces.")
|
|
|
|
(defun flan-current-defun-name ()
|
|
"The name of the top-level definition point is in, or nil.
|
|
For `which-func-functions': a long file scrolled into the middle of a
|
|
function is the case this exists for, and it is the case where the header
|
|
line is off screen."
|
|
(save-excursion
|
|
(ignore-errors
|
|
(beginning-of-defun)
|
|
(and (looking-at (concat "(" (regexp-opt flan--definers t) "\\_>\\s-+"
|
|
flan--name-re))
|
|
(match-string-no-properties 2)))))
|
|
|
|
(defvar flan-mode-syntax-table
|
|
(let ((table (make-syntax-table lisp-mode-syntax-table)))
|
|
;; Flan's own punctuation in names: a name may contain - ? > / and .
|
|
(modify-syntax-entry ?? "_" table)
|
|
(modify-syntax-entry ?! "_" table)
|
|
(modify-syntax-entry ?/ "_" table)
|
|
(modify-syntax-entry ?. "_" table)
|
|
(modify-syntax-entry ?- "_" table)
|
|
;; [ ] and { } are brackets, not symbol characters: every binding list and
|
|
;; every type is written with them.
|
|
(modify-syntax-entry ?\[ "(]" table)
|
|
(modify-syntax-entry ?\] ")[" table)
|
|
(modify-syntax-entry ?{ "(}" table)
|
|
(modify-syntax-entry ?} "){" table)
|
|
table)
|
|
"Syntax table for `flan-mode'.")
|
|
|
|
;;; The discard reader macro
|
|
|
|
;; `#_' reads the form after it and throws it away (lib/reader.ml:16-24,
|
|
;; 202-262), which is how a form is commented out without counting its closing
|
|
;; parens. The table above knows nothing about it — it comes from
|
|
;; `lisp-mode-syntax-table', and Common Lisp has no such construct — so a
|
|
;; discarded form used to render as ordinary code, which is the one thing it
|
|
;; must not look like.
|
|
;;
|
|
;; The fix is `clojure-mode's, which has the same reader macro and the same
|
|
;; problem: a `syntax-propertize-function' that gives the span the comment
|
|
;; syntax class. Font lock then draws it as a comment with no rule of its own,
|
|
;; and every other thing that reads syntax classes — `forward-comment',
|
|
;; `comment-only-p', a package that skips comments — agrees for free.
|
|
;;
|
|
;; Generic comment fences (the `!' class) rather than a line comment, because a
|
|
;; discarded form is a region and may be one line or twenty. The `#' is the
|
|
;; opening fence and the last character of the form is the closing one, so
|
|
;; everything in between is inside a comment and is no longer read as
|
|
;; structure. That is the reader's own answer, and it leaves the *enclosing*
|
|
;; list's parens alone: only the discarded form's own last delimiter changes
|
|
;; class, so `(a #_(b c) d)' still balances.
|
|
|
|
(defun flan--discard-end (pos)
|
|
"Where the discard whose form starts at POS ends, or nil.
|
|
|
|
The reader's rule rather than a count: a discard reads *a form*, and reading
|
|
one skips leading discards of its own, so `#_#_ a b' throws away both a and b
|
|
with nothing ever counting to two (lib/reader.ml:202-262). Written as the
|
|
loop that recursion amounts to — every `#_' seen owes one more form — since
|
|
the walk over the buffer is the same either way.
|
|
|
|
Nil when a form is unfinished, which is every buffer halfway through being
|
|
typed: nothing is propertized then, rather than `#_(' greying out the rest of
|
|
the file while the parenthesis is still open."
|
|
(let ((owed 1)
|
|
;; The scan must not consult the properties it is being run to
|
|
;; compute: with `parse-sexp-lookup-properties' on, `forward-sexp'
|
|
;; would ask for the syntax of positions this pass has not reached.
|
|
;; The table alone is enough — the nesting is counted here, and
|
|
;; strings and escapes are the table's own business.
|
|
(parse-sexp-lookup-properties nil))
|
|
(ignore-errors
|
|
(save-excursion
|
|
(goto-char pos)
|
|
(while (> owed 0)
|
|
(forward-comment (buffer-size))
|
|
(if (looking-at-p "#_")
|
|
(progn (forward-char 2) (setq owed (1+ owed)))
|
|
(forward-sexp)
|
|
(setq owed (1- owed))))
|
|
;; `forward-sexp' at the end of the buffer stays put rather than
|
|
;; signalling, so a trailing `#_' with nothing after it would otherwise
|
|
;; come back as a span covering only itself — a two-character comment
|
|
;; drawn over the reader's one real discard error. It is unfinished
|
|
;; like any other, and answers nil like any other.
|
|
(and (> (point) pos) (point))))))
|
|
|
|
(defun flan--syntax-propertize (start end)
|
|
"Mark every `#_' discard between START and END as a comment.
|
|
For `syntax-propertize-function'."
|
|
(goto-char start)
|
|
(while (search-forward "#_" end t)
|
|
;; Both ends of the match are read off point before anything else runs, and
|
|
;; the `syntax-ppss' below is wrapped: it moves point and clobbers the
|
|
;; match data, so asking it first and reading the match afterwards is how
|
|
;; this would come to scan from the `#' instead of from the form — and how
|
|
;; a `#_' it declined would be found again, forever, at the same place.
|
|
(let* ((beg (- (point) 2))
|
|
(from (point))
|
|
;; `#_' written inside a string, or inside a `;' comment, is text
|
|
;; and not a reader macro. One inside a span an earlier discard
|
|
;; already covers is not reached at all: that discard's own scan
|
|
;; consumed it, and point jumps past the whole span below.
|
|
(quoted (save-excursion (nth 8 (syntax-ppss beg))))
|
|
(fin (and (not quoted) (flan--discard-end from))))
|
|
(when fin
|
|
(put-text-property beg (1+ beg)
|
|
'syntax-table (string-to-syntax "!"))
|
|
(put-text-property (1- fin) fin
|
|
'syntax-table (string-to-syntax "!"))
|
|
;; So that a change *inside* a discarded form re-propertizes from the
|
|
;; `#_' rather than from the line it was typed on; the other half is
|
|
;; `syntax-propertize-multiline' in the mode body.
|
|
(put-text-property beg fin 'syntax-multiline t))
|
|
;; Past the span when there was one, and past the `#_' when there was
|
|
;; not. Either way forward, which is the whole of why this terminates.
|
|
(goto-char (or fin from)))))
|
|
|
|
(defvar flan-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
;; Autoloaded from flan.el, so the client loads on first use.
|
|
(define-key map (kbd "C-c C-c") #'flan-eval-defun)
|
|
;; The same command on the binding SLIME and CIDER put it on. Emacs binds
|
|
;; C-M-x to eval-defun only in `emacs-lisp-mode-map', so a mode derived
|
|
;; from `lisp-mode' inherits nothing and the key is undefined — which
|
|
;; reads as the client being broken rather than as the key being free.
|
|
(define-key map (kbd "C-M-x") #'flan-eval-defun)
|
|
(define-key map (kbd "C-c C-k") #'flan-eval-buffer)
|
|
(define-key map (kbd "C-x C-e") #'flan-eval-last-sexp)
|
|
(define-key map (kbd "C-c C-z") #'flan-connect)
|
|
(define-key map (kbd "C-c C-q") #'flan-disconnect)
|
|
(define-key map (kbd "C-c C-d") #'flan-describe)
|
|
;; The REPL's two clears, reachable from the file being edited: results
|
|
;; land in *flan-repl* whichever buffer the send came from, so the keys
|
|
;; that take them down belong here too. CIDER's pair: C-c C-o for the
|
|
;; last send's output, C-c M-o for the whole transcript.
|
|
(define-key map (kbd "C-c C-o") #'flan-repl-clear-output)
|
|
(define-key map (kbd "C-c M-o") #'flan-repl-clear)
|
|
(define-key map (kbd "C-c C-r") #'flan-repl)
|
|
;; C-c C-b opens the buffer rather than the minibuffer prompt: it shows the
|
|
;; same restarts plus the condition and the stack, and — the reason it
|
|
;; replaces rather than joins — it is the one that refuses a *shadowed*
|
|
;; restart instead of silently invoking a different frame's. flan-break is
|
|
;; still there under C-c C-M-b for the one-key path.
|
|
(define-key map (kbd "C-c C-b") #'flan-cnr-show)
|
|
(define-key map (kbd "C-c C-M-b") #'flan-break)
|
|
(define-key map (kbd "C-c C-i") #'flan-inspect)
|
|
;; Help on the name at point. C-c C-d is taken by `flan-describe', which
|
|
;; is about the session rather than about a name, and renaming a key that
|
|
;; is already documented costs more than it is worth. Not C-c C-h either:
|
|
;; C-h after a prefix is how anyone finds out what is under C-c, and a
|
|
;; binding there takes that away.
|
|
(define-key map (kbd "C-c C-v") #'flan-doc)
|
|
;; The code the running program is calling for a name, as amd64 or as the
|
|
;; IR it was built from. C-u for the IR rather than a second key: it is
|
|
;; the same question asked of the same body.
|
|
(define-key map (kbd "C-c C-a") #'flan-disassemble)
|
|
;; Every lowering of the name at point -- the IR, `llc' at -O0 and at -O2,
|
|
;; and the hand-written x86 backend -- in one buffer of folding sections.
|
|
;; Beside C-c C-a and not under it: that one answers for the program that
|
|
;; is running, this one for the file on disk, and no prefix argument can
|
|
;; make one of those into the other.
|
|
(define-key map (kbd "C-c C-l") #'flan-lowering)
|
|
;; What the macro call before point expands to. CIDER's key, and `C-u' for
|
|
;; the fixpoint rather than a second one — the same question asked of the
|
|
;; same form, which is the rule `C-c C-a' above already follows. One step
|
|
;; is the bare key because it is the one that can name the macro that ran:
|
|
;; a full expansion is stamped with the outermost name only.
|
|
(define-key map (kbd "C-c C-m") #'flan-macroexpand)
|
|
;; The way out when a reload is refused: rebuild, relaunch, reconnect.
|
|
(define-key map (kbd "C-c C-x") #'flan-restart-program)
|
|
;; And beside it the cheap one, which is the same question — "run this
|
|
;; program" — asked of a process that is already there: `main' again, with
|
|
;; the globals as the finished run left them. The pairing is `C-c C-b'
|
|
;; and `C-c C-M-b' above: the modifier is what distinguishes two commands
|
|
;; about one subject, and the heavier of the pair keeps the bare key
|
|
;; because it is the one that works from any state.
|
|
(define-key map (kbd "C-c C-M-x") #'flan-rerun)
|
|
map)
|
|
"Keymap for `flan-mode'.")
|
|
|
|
;;;###autoload
|
|
(define-derived-mode flan-mode prog-mode "Flan"
|
|
"Major mode for editing Flan.
|
|
|
|
\\{flan-mode-map}"
|
|
:syntax-table flan-mode-syntax-table
|
|
(setq-local comment-start ";")
|
|
(setq-local comment-start-skip ";+ *")
|
|
(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)
|
|
;; `#_' — see the section above the syntax table.
|
|
(setq-local syntax-propertize-function #'flan--syntax-propertize)
|
|
;; A discarded form spans as many lines as it likes, so the region handed to
|
|
;; the propertizer has to be widened back to the `#_' that owns it.
|
|
(add-hook 'syntax-propertize-extend-region-functions
|
|
#'syntax-propertize-multiline nil t)
|
|
;; 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' is the other way round — the body first and the clause
|
|
;; vector after it — but the entry is the same number, because what it
|
|
;; says is that one argument is special and the rest indent as a body,
|
|
;; and that is true of both orders.
|
|
("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 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
|
|
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 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', `defdata', `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))
|
|
|
|
(provide 'flan-mode)
|
|
;;; flan-mode.el ends here
|