flan/emacs/flan-mode.el
Joseph Ferano 0c523cfe8b The program can be run again, in the process that is already there
You run a program under flan dev, it opens a raylib window, you close the
window, main returns — and there is no way to get another window short of
flan-dev-restart-program, which throws away the build, the session and every
global with it. In Common Lisp or Clojure the image outlives main, so you call
it again. The process here already outlived main: the exit hook flushed, closed
stdout and sat in for (;;) pause(). Nothing could wake it.

So main() is a loop. The hook records the status and longjmps back into a
setjmp in main() — there is no return available, since flan_exit is reached
from wherever the program happened to be — and the thread waits on a condition
variable until the new rerun op signals it. The main thread is the one that
runs main again: a window belongs to the thread that opened it, and on macOS to
the first thread of the process. A longjmp pops no frame, so the park first
empties the handler stack, the restart stack and the shadow frame chain, each
of which was a chain of allocas in stack the next run is about to write over.
Nothing else is reset; the second run reads whatever the first left in the
globals, which is the semantics that was asked for.

Closing stdout had to go with it. That was how the compiler learned the program
was done, but a pipe delivers EOF once, so the signal and the program's output
were the same resource and spending it left the second run with nowhere to
print. The descriptor hazard the old code reopened /dev/null for goes away with
the close that caused it. Liveness is asked for instead, through a weak symbol
in the same style as the agent's, and is now three states rather than two: Live,
Parked and Gone. Every guard branches on that before consulting the break
state, because the agent's listener answers "running" while the program is
parked and telling somebody whose program has finished that it is running is
worse than saying nothing. Only eval accepts a parked program — it queues and
waits for nothing, and the queued module installs at the first frame boundary
of the next run, so a body can be fixed while parked and the re-run executes
it. Everything else needs a frame boundary or a stopped stack, has neither, and
says which, naming the command that gets the program back.

A re-run while the program is running is refused rather than queued: the test
and the signal happen under one mutex, so two mains writing the same globals at
once never starts.

:parked rides on every reply beside :stopped, for the reason :stopped does —
finishing is as unannounced as stopping, more so when the way it happens is a
mouse click on a title bar. Emacs shows flan:parked in the modeline and binds
flan-rerun to C-c C-M-x.
2026-09-17 19:01:01 +07:00

468 lines
22 KiB
EmacsLisp

;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
;; 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-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)
;; Bound below, like the rest, and it was the one missing an autoload.
(autoload 'flan-disassemble "flan-dev" 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-dev" nil t)
(autoload 'flan-macroexpand-all "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" "print" "println")
"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)
;; 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\\|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)
;; 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-dev-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)
;; 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" . 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', `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