flan/emacs/flan-mode.el
Joseph Ferano 93231e8c9e println, the structural printer, shared with the REPL
session.ml already had this: a compile-time walk over a Tast type that
emits the calls to print a value of it, handling every concrete type the
language has. It was dev-build-only and went to flan_dev_emit, and
prelude.ml justified the per-type print-* functions by saying a real
println had to wait for milestone 5 and generics. It did not. plan.org
specifies println as compiler-provided and per concrete type, which is
not overloading: there is nothing to dispatch on at run time and no
user-supplied printer to choose between, so no type variables appear.

The walk moves to render.ml, parameterised on an emitter and a slot
allocator. The emitter is five functions rather than five extern names
because the two sides are not both extern calls -- the REPL's are, and
stdout's compose a conversion with a write. The slot allocator differs
too: the REPL builds a thunk's frame, println takes slots from the
enclosing function being checked, once per call site.

Two runtime shims, both only reachable from the walk. flan_u64_to_bytes,
because routing u64 through the signed printer makes 0xFFFF...F read as
-1, which is the one way println could disagree with the REPL about a
value both can hold. flan_escape_bytes, so a string nested in a printed
structure is quoted and escaped -- same table as flan_dev_emit_str, noted
in both, because the REPL and println must not disagree about what a
struct looks like.

A string at top level prints raw and nested prints quoted. Not a conflict:
(println "hello") has to print hello, and a struct's string field has to
be distinguishable from the punctuation around it. The split is top-level
vs nested, so it lives in check.ml and not in the walk.

Found on the way: a field of an Option had no gep in emit.ml, so the
walk's Option arm had never run -- the REPL would have failed on one too.
Option is { i8, T } with no declared name, so its layout is now spelled
out. Nothing in the surface language reaches a field of an Option; the
printer does, to read the tag without unwrapping a None.

The print-* functions stay. They print without a newline, which println
cannot express -- slices.flan's show prints elements separated by spaces
-- and they are raw where print is structural.

println.flan covers every arm at -O0 and -O2: the u64, the raw/quoted
split, both Option arms, the depth and span caps, and the slice arm's
loop twice over plus once inside a dotimes, which is where per-call-site
slot allocation would show if it were per-iteration.
2026-09-12 04:55:42 +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" "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)
;; 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