flan/emacs/flan-mode.el

628 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 package half of a qualified name — the `rl/' of `rl/draw-text'.
;; Drawn as a type the way clojure-mode draws a namespace, so the eye can
;; split the package from the name without reading either. The leading
;; letter keeps `:foo/bar' keywords and a bare `/' out of it.
("\\_<\\([a-zA-Z][a-zA-Z0-9!?*+=<>._-]*/\\)" 1 font-lock-type-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