flan/emacs/flan-mode.el
Joseph Ferano 6ed413a232 An inspector and a conditions buffer, and a restart nobody could take
Two CIDER-shaped buffers: C-c C-i navigates a value, C-c C-b shows the
condition, the restarts and the stack. C-c C-M-b keeps the old one-key prompt.

The inspector needs no protocol change at all. eval-expr already answers a
rendered string, and Session.render writes exactly seven shapes, so that string
is a grammar. Navigation is a stack of *expressions* rather than of handles:
going into :pos sends (.pos b), into element 2 sends (at (.tags b) 2) - both
ordinary Flan a person could type. CIDER keeps its stack on the server because
a JVM value can be retained; nothing here can, since a Flan value has no header
and the render thunk is dlclosed the moment it returns. The view is therefore
never stale, where CIDER's shows the object as it was when you pushed. What it
buys over C-x C-e is the depth-4 span-8 bound: a field past it comes back as
... and nothing recovers it from the echo area, and re-rooting renders it from
depth 0.

SBCL decided the order - condition, restarts, then stack - because invoke-debugger
prints the condition and show-restarts and then stops; the backtrace is a command
you type. The restarts are the decision and the stack is the explanation for it.

And SBCL found a live bug. show-restarts omits the bracket on a name already
used further in, which is not decoration: §4 takes the first frame offering a
name, so a second frame offering retry is real, is on the list, and cannot be
chosen. The old prompt showed retry twice and sent the string either way, and
the inner frame took it silently. restarts.flan's own nested function has been
that counterexample since the transfer landed. The buffer draws the shadowed row
unbracketed and refuses RET on it by name, with a test asserting nothing was
sent - which stops the lie without restoring the choice. Taking a restart by
index is the fix and is recorded as such.

Sections that cannot be filled are drawn saying why rather than omitted: a
missing section cannot be told from an empty one, and only one of those is a
fact about the program.
2026-09-12 04:17:41 +07:00

188 lines
8.4 KiB
EmacsLisp

;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
;; Derived from lisp-mode, which is most of the work: Flan is s-expressions, so
;; sexp motion, paren matching, `beginning-of-defun' and indentation all
;; already do the right thing. What is left is what the language actually adds
;; — its own literals and its own set of forms that indent like a body.
;;; Code:
(require 'lisp-mode)
;; 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-dev at all.
(autoload 'flan-eval-defun "flan-dev" nil t)
(autoload 'flan-eval-buffer "flan-dev" nil t)
(autoload 'flan-eval-last-sexp "flan-dev" nil t)
(autoload 'flan-connect "flan-dev" nil t)
(autoload 'flan-disconnect "flan-dev" nil t)
(autoload 'flan-describe "flan-dev" nil t)
(autoload 'flan-show-output "flan-dev" nil t)
(autoload 'flan-repl "flan-repl" nil t)
(autoload 'flan-break "flan-dev" 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-dev" nil t)
(autoload 'flan-dev "flan-dev" nil t)
(autoload 'flan-dev-quit "flan-dev" nil t)
(autoload 'flan-dev-restart-program "flan-dev" nil t)
(defgroup flan nil
"Editing and evaluating Flan."
:group 'languages
:prefix "flan-")
(defconst flan--definers
'("defn" "defvar" "defconst" "defstruct" "defunion" "defenum" "defalias"
"declare" "import" "package")
"Forms that introduce a top-level name.")
(defconst flan--special
'("let" "if" "do" "while" "until" "dotimes" "loop" "match" "set" "return"
"defer" "some" "none" "try" "zeroed" "uninit" "slice" "at" "len" "addr"
"bytes" "cast" "true" "false" "nil")
"Forms with meaning to the checker.")
(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)
;; 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\\|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'.")
(defvar flan-mode-map
(let ((map (make-sparse-keymap)))
;; Autoloaded from flan-dev.el, so the client loads on first use.
(define-key map (kbd "C-c C-c") #'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)
(define-key map (kbd "C-c C-o") #'flan-show-output)
(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)
;; The way out when a reload is refused: rebuild, relaunch, reconnect.
(define-key map (kbd "C-c C-x") #'flan-dev-restart-program)
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)
(setq-local lisp-indent-function #'flan-indent-function)
(setq-local outline-regexp ";;;;+[ \t]*")
(setq-local imenu-generic-expression flan-imenu-generic-expression)
;; Buffer-locally, because this answers for Flan and nothing else.
(add-hook 'which-func-functions #'flan-current-defun-name nil t))
(defun flan-indent-function (indent-point state)
"Indent like Lisp, with Flan's body forms as special forms.
INDENT-POINT and STATE are as for `lisp-indent-function'."
(let ((open (elt state 1)))
(or (and open
(save-excursion
(goto-char (1+ open))
(let ((head (and (looking-at "\\(\\sw\\|\\s_\\)+")
(match-string 0))))
(when (member head '("defn" "let" "if" "while" "until"
"dotimes" "match" "do" "loop" "defer"))
(+ (current-column) 1)))))
(lisp-indent-function indent-point state))))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode))
(provide 'flan-mode)
;;; flan-mode.el ends here