868 lines
44 KiB
EmacsLisp
868 lines
44 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-step-defun "flan" 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" "defn-" "defmacro" "def" "defonce" "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" "do" "let" "if" "when" "cond" "and" "or"
|
|
"while" "until" "break" "continue" "return" "set"
|
|
"array" "array-fill" "array-gen" "the" "match" "fn" "dotimes" "loop" "recur"
|
|
"defer" "some" "try" "signal" "error"
|
|
"handler-bind" "handler-case" "restart-case" "invoke-restart")
|
|
"The heads `Parse.form' dispatches on — the forms with a meaning of their own.
|
|
|
|
Not functions, which is the line this list draws: `and' does not evaluate its
|
|
second argument unless it has to and `quote' evaluates none of its, while
|
|
everything in `flan--builtins' below is an ordinary call. They used to be one
|
|
list and were drawn alike, which said they were the same kind of thing.
|
|
|
|
Two groups of real heads are deliberately left out. `quasiquote', `unquote'
|
|
and `unquote-splicing' are never written as words — the reader makes them out
|
|
of \=`, ~ and ~@, and the sigils are not symbols for a keyword rule to reach.
|
|
`find-restart', `compute-restarts', `errdefer' and `await' the parser
|
|
recognises only in order to refuse them, and drawing those as keywords would
|
|
advertise four forms that cannot be used.
|
|
|
|
`until' is a macro in the prelude and not a head of `Parse.form'. It stays
|
|
here because it is control flow a reader takes for `while', which is what this
|
|
face says.")
|
|
|
|
(defconst flan--builtins
|
|
'(;; arithmetic, comparison, bits
|
|
"+" "-" "*" "/" "%" "=" "!=" "<" "<=" ">" ">=" "not"
|
|
"bit-and" "bit-or" "bit-xor" "bit-not" "&&" "||" "^^" "<<" ">>"
|
|
"rotate-left" "rotate-right" "popcount" "leading-zeros" "trailing-zeros"
|
|
"min" "max"
|
|
;; the fill patterns
|
|
"zeroed" "filled" "dead-beef"
|
|
;; allocators
|
|
"make-allocator" "allocator-from" "allocator" "heap-allocator"
|
|
"arena-new" "arena-destroy" "free-all" "free-temp" "can-free?" "can-free-all?"
|
|
"alloc-epoch" "alloc-id" "alloc-budget" "set-alloc-budget"
|
|
"alloc-live-blocks" "with-allocator"
|
|
;; Vec
|
|
"vec-new" "push" "reserve" "free" "clone"
|
|
;; String
|
|
"string-new" "bytes->string" "append" "insert" "remove" "runes" "rune-count"
|
|
;; Map
|
|
"map-new" "put" "get" "map-remove" "map-next" "has-key?"
|
|
;; dyn
|
|
"class-of" "type-of" "keyword"
|
|
;; compile time
|
|
"embed" "embed-dir" "compile-error"
|
|
;; files
|
|
"slurp" "barf" "delete-file" "make-directory" "rename-file"
|
|
;; containers and memory
|
|
"length" "at" "slice" "slice-from" "addr" "deref"
|
|
;; options, bytes, the host
|
|
"Some" "bytes" "bytes-view" "str" "char"
|
|
"bytes->f64" "bytes->i64" "f64->bytes" "i64->bytes"
|
|
"write-stdout" "print" "println" "exit" "argv")
|
|
"The functions the compiler provides, from `lib/check.ml''s `builtins' table.
|
|
|
|
Ordinary calls — nothing here is special to the parser — so they are drawn as
|
|
builtins and not as keywords. `str' and `char' are in this list and in the
|
|
type rule below and mean a different thing in each: `(str b)' converts and a
|
|
bare `str' names a type, which the rules tell apart by the paren.
|
|
|
|
`destructure~nth' is in the table and not here: the compiler writes it into a
|
|
destructuring `let' and nobody types it.
|
|
|
|
The randomness functions, the string and sequence functions and everything
|
|
else in `lib/prelude.ml' are deliberately absent. They are ordinary Flan
|
|
written in Flan, the running program answers for them by name, and listing
|
|
them here would be a second copy of the prelude to keep in step.")
|
|
|
|
(defconst flan--constants
|
|
'("true" "false" "nil" "None" "context/allocator" "context/temp" "uninit")
|
|
"Names that stand for themselves rather than being called.
|
|
|
|
Matched as bare symbols, which is how they are written: nobody types `(true)',
|
|
so a paren-anchored rule would never see one. `uninit' is here for the same
|
|
reason and is the odd one — it is legal only as the last item of a `def' or a
|
|
`defonce', where it says the storage is left as it was found.")
|
|
|
|
(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)
|
|
;; The builtins are deliberately not drawn. clojure-mode leaves `map',
|
|
;; `filter' and the rest of the core library plain and spends
|
|
;; `font-lock-builtin-face' only on the earmuffed dynamic variables, and a
|
|
;; buffer reads better for it: `at', `push' and `println' are the ordinary
|
|
;; vocabulary of a program, and colouring the ordinary leaves nothing for
|
|
;; the unusual. `flan--builtins' is still the list completion and the
|
|
;; dynamic rules check against, so nothing else has to know this.
|
|
;;
|
|
;; Ahead of the qualified-name rule below, which would otherwise take
|
|
;; `context/' in `context/allocator' for a package alias. It is not one:
|
|
;; there is no package called `context', and the slash is part of the name.
|
|
(,(concat "\\_<" (regexp-opt flan--constants t) "\\_>")
|
|
1 font-lock-constant-face)
|
|
;; A keyword resolves against an enum at the call site, so it reads as a
|
|
;; constant rather than as a string. `:where', the one key a `defn''s
|
|
;; constraint map accepts, is covered by the same rule.
|
|
("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
|
|
;; A field — the label in `{.x 1.0}' and the accessor in `(.x v)' — is left
|
|
;; plain, for the reason the builtins are: it is part of reading the
|
|
;; program, not part of reading the language. clojure-mode draws Java
|
|
;; interop the same way.
|
|
;;
|
|
;; 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. It sits
|
|
;; after the constants rule above on purpose: `context/allocator' has a
|
|
;; slash and is not a qualified name — there is no package called
|
|
;; `context' — and font-lock leaves text that is already drawn alone.
|
|
("\\_<\\([a-zA-Z][a-zA-Z0-9!?*+=<>._-]*/\\)" 1 font-lock-type-face)
|
|
;; The types the compiler knows without being told: every primitive in
|
|
;; `Types.primitive_names', plus the four applied ones the checker
|
|
;; resolves and the two function types, `Fn' and `CFn'. `dyn' is
|
|
;; lowercase on purpose — it is a primitive beside `i64' and `bool', not
|
|
;; a container over something.
|
|
;; `int' and `float' are builtin aliases for `i32' and `f32'. `const'
|
|
;; is the reserved word of the read-only slice type, `[const u8]', and is
|
|
;; drawn as part of the type it spells.
|
|
;;
|
|
;; `Unit' is deliberately absent, though `Types.primitive_names' has it.
|
|
;; The resolver answers to the name because `Cimport' builds one for C's
|
|
;; void, but nothing anyone writes reaches that: `Parse.texpr' refuses the
|
|
;; word outright — unit is spelled `()'. Drawing it as a valid type would
|
|
;; advertise a spelling the parser rejects, which is the same reason
|
|
;; `find-restart' and `await' are left out of `flan--special'.
|
|
("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|char\\|str\\|dyn\\|const\\|int\\|float\\|Never\\|Allocator\\|String\\|Ptr\\|Option\\|Vec\\|Map\\|C?Fn\\)\\_>"
|
|
. font-lock-type-face)
|
|
;; A type variable, `$t', which is what a generic `defn' names its
|
|
;; parameter types with and what `{:where (ordered? $t)}' constrains.
|
|
("\\_<\\$\\(?:\\sw\\|\\s_\\)*" . font-lock-type-face))
|
|
;; A number is not drawn. clojure-mode has no rule for one either: a
|
|
;; literal is already unmistakable by shape, and a colour on every integer
|
|
;; in a program full of indices is noise.
|
|
"Font lock for `flan-mode'.
|
|
Every rule here is about the language itself, so a file that has never been
|
|
near a running program is drawn completely. What a *particular program*
|
|
defines is a separate question, and `flan.el' answers it — see
|
|
`flan-font-lock-dynamically' — by adding rules after these ones. After, and
|
|
never over: a name that is a special form or a builtin keeps the face these
|
|
rules gave it whatever the program happens to call its own functions.")
|
|
|
|
(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
|
|
;; `defgeneric', `defmulti' and `defmethod' are here with `defn': all four
|
|
;; introduce something you call, and which of them declared a name is not
|
|
;; the question an index is being asked. A method is listed by the generic
|
|
;; it implements, which is the name in the same place, so a file of several
|
|
;; methods shows that name several times — the honest answer, and better
|
|
;; than listing none of them as it did.
|
|
`(("Functions" ,(concat "^(def\\(?:n-?\\|generic\\|multi\\|method\\)\\s-+"
|
|
flan--name-re)
|
|
1)
|
|
;; Its own heading rather than a second `Functions' entry: a macro runs at
|
|
;; compile time and a function at run time, and an index that drew them
|
|
;; alike would be hiding the one difference that matters about them.
|
|
("Macros" ,(concat "^(defmacro\\s-+" flan--name-re) 1)
|
|
;; `defclass' with the other type declarations: it names a shape, and a
|
|
;; reader looking for where `Sprite' is defined does not first have to
|
|
;; decide whether it was spelled as a struct or as a class.
|
|
("Types"
|
|
,(concat "^(def\\(?:struct\\|data\\|union\\|enum\\|alias\\|class\\)\\s-+"
|
|
flan--name-re)
|
|
1)
|
|
;; `def', `defonce' and `defconst'. `def' has to be matched as itself —
|
|
;; `\_>' keeps it from swallowing every other definer's prefix.
|
|
("Variables" ,(concat "^(def\\(?:once\\|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.
|
|
;; `declare-c' as well as `declare': it declares a name the same way and
|
|
;; differs only in generating the C shim that reaches it.
|
|
("Declared" ,(concat "^(declare\\(?:-c\\)?\\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)
|
|
;; `$t' is a type variable and `&' is the rest marker in an array pattern.
|
|
;; The reader's rule is that a name is anything up to a delimiter (see
|
|
;; `is_delimiter' in `lib/reader.ml'), and neither of these is one, so both
|
|
;; are part of the name they sit in and `M-.' on `$t' should not stop at
|
|
;; the sigil.
|
|
(modify-syntax-entry ?$ "_" table)
|
|
(modify-syntax-entry ?& "_" table)
|
|
;; A comma is whitespace, exactly as it is in Clojure and for the same
|
|
;; reason: `lib/reader.ml' skips it wherever a space would go, so `[a 1, b
|
|
;; 2]' and `[a 1 b 2]' are the same vector. Saying so here is what lets
|
|
;; sexp motion and the indenter step over one without a case for it.
|
|
(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)))))
|
|
|
|
;; The keys both syntaxes share: everything that talks to the running program
|
|
;; about a name, a value or the session rather than about a piece of the text.
|
|
;; The keys that pick text out of the buffer -- which form C-c C-c means --
|
|
;; are each child mode's own, because what a form is differs between them.
|
|
(defvar flan-base-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-k") #'flan-eval-buffer)
|
|
;; The stepper: the defn at point, installed to stop before each form.
|
|
(define-key map (kbd "C-c C-s") #'flan-step-defun)
|
|
(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 every Flan source buffer, `flan-mode' and `flan-fln-mode'.")
|
|
|
|
(defvar flan-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(set-keymap-parent map flan-base-mode-map)
|
|
(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-x C-e") #'flan-eval-last-sexp)
|
|
map)
|
|
"Keymap for `flan-mode'.")
|
|
|
|
;; The parent of both source modes. Everything the dev loop asks of a buffer
|
|
;; -- is this Flan, set up eldoc and completion, draw the program's names,
|
|
;; paint watched values -- asks it of this mode, so a .fln buffer gets it the
|
|
;; same way a .flan buffer does. What each syntax reads as a form is its
|
|
;; child's business.
|
|
(define-derived-mode flan-base-mode prog-mode "Flan"
|
|
"Parent mode of the Flan source modes, `flan-mode' and `flan-fln-mode'."
|
|
(setq-local comment-start ";")
|
|
(setq-local comment-start-skip ";+ *")
|
|
(setq-local comment-add 1)
|
|
;; 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. In a .fln file a tab in the
|
|
;; indentation is an error besides.
|
|
(setq-local indent-tabs-mode nil))
|
|
|
|
;;;###autoload
|
|
(define-derived-mode flan-mode flan-base-mode "Flan"
|
|
"Major mode for editing Flan.
|
|
|
|
\\{flan-mode-map}"
|
|
:syntax-table flan-mode-syntax-table
|
|
(setq-local font-lock-defaults '(flan-font-lock-keywords))
|
|
(setq-local indent-line-function #'lisp-indent-line)
|
|
(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)
|
|
;; An anonymous function: the parameter vector, then the body. `defn'
|
|
;; without the name, and it indents like `let' rather than like `defn'
|
|
;; because there is no optional return type to be vague about.
|
|
("fn" . 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)
|
|
;; The condition, then the struct literal that carries its fields.
|
|
("signal" . 1)
|
|
("error" . 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)
|
|
("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.
|
|
A qualified name falls back to the entry for its unqualified part. Anything
|
|
still unnamed that begins with `def' (but not `default') or `with-' indents as
|
|
`:defn' in `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.
|
|
A qualified name falls back to its unqualified part, so `rl/when' would find
|
|
the entry for `when' — `clojure--get-indent-method' does the same."
|
|
(and name
|
|
(cdr (or (assoc name flan-indent-specs)
|
|
(and (string-match "/\\([^/]+\\)\\'" name)
|
|
(assoc (match-string 1 name) flan-indent-specs))))))
|
|
|
|
(defun flan--definer-p (name)
|
|
"Non-nil if NAME indents as a definition or a `with-' form.
|
|
Either may be qualified: `rl/with-drawing' is a `with-' form. This is
|
|
`clojure-indent-function''s fallback for a head with no spec, regexp and all;
|
|
`default…' is excluded there because it is not a definer, and here too."
|
|
(and name
|
|
(string-match "\\`\\(?:\\S +/\\)?\\(def[a-z]*\\|with-\\)" name)
|
|
(not (string-match-p "\\`default" (match-string 1 name)))))
|
|
|
|
(defconst flan--labelled-forms '("dotimes" "while" "until")
|
|
"Loops that may carry a label, which `break' and `continue' name.
|
|
`loop' is deliberately not here: it refuses a label, because a loop answers
|
|
with the value of its body and there is nothing for a jump out of one to
|
|
give. See `lib/parse.ml'.")
|
|
|
|
(defun flan--label-p (name)
|
|
"Non-nil if the form called NAME, at point, carries a label.
|
|
Point is on the head. A label is a keyword written where the binding vector
|
|
or the test would otherwise go — `(dotimes :outer [i 3] …)' — and it pushes
|
|
everything after it along by one, so the count of special arguments has to
|
|
know about it. Without this a labelled loop indented its body under its own
|
|
binding vector, which is where `test/programs/loops.flan' would have said so
|
|
if anything had been indenting it."
|
|
(and (member name flan--labelled-forms)
|
|
(save-excursion
|
|
(ignore-errors
|
|
(flan--forward-sexp 1) ; over the head
|
|
(skip-chars-forward " \t\n\r")
|
|
(eq (char-after) ?:)))))
|
|
|
|
(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 (if (flan--label-p name) (1+ method) 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', `defonce', `defconst' and `defalias' without naming
|
|
;; them. A `with-' form is a body too — `rl/with-drawing',
|
|
;; `rl/with-mode-2d camera' — whatever it takes before the body.
|
|
((flan--definer-p 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))
|
|
;; The indented syntax's mode lives in its own file; a buffer of it is the first
|
|
;; thing that loads it.
|
|
;;;###autoload
|
|
(autoload 'flan-fln-mode "flan-fln-mode" nil t)
|
|
;;;###autoload
|
|
(add-to-list 'auto-mode-alist '("\\.fln\\'" . flan-fln-mode))
|
|
|
|
;;; The other Flan buffers under Evil
|
|
|
|
;; Evil's normal state binds most single keys — the digits, RET, TAB, n, p, q,
|
|
;; g — above any major mode's map, so in a buffer of Flan's own a key its mode
|
|
;; binds would move point, start a count or record a macro instead. Each such
|
|
;; buffer's mode gives the keys its own map binds to Evil's normal and motion
|
|
;; states, so they do the same thing under Evil as without it. Only those
|
|
;; keys: the maps inherit `special-mode-map', and making one an overriding map
|
|
;; would carry h, SPC, < and - over from there as well. Every key a mode does
|
|
;; not bind itself stays Evil's.
|
|
|
|
(declare-function evil-define-key* "evil-core" (state keymap key def &rest bindings))
|
|
|
|
(defun flan-evil-own-keys (name)
|
|
"Give the keys the map named NAME binds itself to Evil's states.
|
|
Normal and motion. Takes effect when Evil is loaded, or at once if it
|
|
already is.
|
|
|
|
The map is passed by name because `eval-after-load' drops a function
|
|
`equal' to one it already holds, and two maps with the same bindings are
|
|
`equal': given the maps, only the first of them would be set up."
|
|
(with-eval-after-load 'evil
|
|
(when (fboundp 'evil-define-key*)
|
|
;; Collected first, and without the parent's bindings, because
|
|
;; `evil-define-key*' writes into the map being walked.
|
|
(let ((map (symbol-value name))
|
|
(own nil))
|
|
(map-keymap-internal (lambda (key def)
|
|
(when (commandp def) (push (cons key def) own)))
|
|
map)
|
|
(dolist (b own)
|
|
(evil-define-key* '(normal motion) map (vector (car b)) (cdr b)))))))
|
|
|
|
(provide 'flan-mode)
|
|
;;; flan-mode.el ends here
|