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.
188 lines
8.4 KiB
EmacsLisp
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
|