From 157048e5f7f264ffd744901b9e2591761619c7dc Mon Sep 17 00:00:00 2001 From: Ryan Kavanagh Date: Tue, 21 May 2024 11:19:45 -0400 Subject: locally ship beluga-mode.el --- dot_emacs.d/lisp/beluga/beluga-mode.el | 1113 ++++++++++++++++++++++++++++++++ 1 file changed, 1113 insertions(+) create mode 100644 dot_emacs.d/lisp/beluga/beluga-mode.el (limited to 'dot_emacs.d/lisp') diff --git a/dot_emacs.d/lisp/beluga/beluga-mode.el b/dot_emacs.d/lisp/beluga/beluga-mode.el new file mode 100644 index 0000000..8c04209 --- /dev/null +++ b/dot_emacs.d/lisp/beluga/beluga-mode.el @@ -0,0 +1,1113 @@ +;;; beluga-mode.el --- Major mode for Beluga source code -*- coding: utf-8; lexical-binding:t -*- + +;; Copyright (C) 2009-2018 Free Software Foundation, Inc. + +;; Author: Stefan Monnier +;; Maintainer: beluga-dev@cs.mcgill.ca +;; Version: 0 +;; Keywords: + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; BUGS + +;; - Indentation thinks "." can only be the termination marker of +;; an LF declaration. This can mess things up badly. +;; - Indentation after curried terms like "fn x => fn y =>" is twice that +;; after "fn x y =>". + +;;; Code: + +(eval-when-compile (require 'cl-lib)) +(require 'smie) + +(require 'ansi-color) +(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter) + +(provide 'beluga-unicode-input-method) +(require 'quail) + +(defconst beluga-input-method-name + "beluga-unicode" + "The name of the Beluga unicode input method.") + +(quail-define-package + beluga-input-method-name ;; name + "UTF-8" ;; language + "B" ;; mode-line "title" + t ;; guidance + "Beluga unicode input method: actually replaces keyword strings with +a single unicode character instead of merely representing the keywords +in unicode using Font Lock mode." + nil nil nil nil nil nil nil nil nil nil t) +;; This very final t is the SIMPLE flag of quail-define-package, and +;; causes Quail to not affect the meanings of C-{f,b,n,p} or TAB. + +(quail-define-rules + ;; Greek letters + ("\\alpha" ["α"]) + ("\\Alpha" ["Α"]) + ("\\beta" ["β"]) + ("\\Beta" ["Β"]) + ("\\gamma" ["γ"]) + ("\\Gamma" ["Γ"]) + ("\\delta" ["δ"]) + ("\\Delta" ["Δ"]) + ("\\epsilon" ["ε"]) + ("\\Epsilon" ["Ε"]) + ("\\zeta" ["ζ"]) + ("\\Zeta" ["Ζ"]) + ("\\eta" ["η"]) + ("\\Eta" ["Η"]) + ("\\theta" ["θ"]) + ("\\Theta" ["Θ"]) + ("\\iota" ["ι"]) + ("\\Iota" ["Ι"]) + ("\\kappa" ["κ"]) + ("\\Kappa" ["Κ"]) + ("\\lambda" ["λ"]) + ("\\Lambda" ["Λ"]) + ("\\lamda" ["λ"]) + ("\\Lamda" ["Λ"]) + ("\\mu" ["μ"]) + ("\\Mu" ["Μ"]) + ("\\nu" ["ν"]) + ("\\Nu" ["Ν"]) + ("\\xi" ["ξ"]) + ("\\Xi" ["Ξ"]) + ("\\omicron" ["ο"]) + ("\\Omicron" ["Ο"]) + ("\\pi" ["π"]) + ("\\Pi" ["Π"]) + ("\\rho" ["ρ"]) + ("\\Rho" ["Ρ"]) + ("\\sigma" ["σ"]) + ("\\Sigma" ["Σ"]) + ("\\tau" ["τ"]) + ("\\Tau" ["Τ"]) + ("\\upsilon" ["υ"]) + ("\\Upsilon" ["Υ"]) + ("\\phi" ["φ"]) + ("\\Phi" ["Φ"]) + ("\\chi" ["χ"]) + ("\\Chi" ["Χ"]) + ("\\psi" ["ψ"]) + ("\\Psi" ["Ψ"]) + ("\\omega" ["ω"]) + ("\\Omega" ["Ω"]) + + ;; Common logical symbol + ("\\conj" ["∧"]) + ("\\disj" ["∨"]) + ("\\imp" ["⊃"]) + ("\\top" ["⊤"]) + ("\\bot" ["⊥"]) + + ("\\equiv" ["≣"]) + + ;; Arrows + ("->" ["→"]) + ("<-" ["←"]) + ("=>" ["⇒"]) + + ;;LF + ("|-" ["⊢"]) + ("\\not" ["¬"]) + ("::" ["∷"]) + ("FN" ["Λ"]) +) + +(defgroup beluga-mode () + "Editing support for the Beluga language." + :group 'languages) + +(defvar beluga-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "\C-c\C-c" 'compile) + (define-key map "\C-c\C-l" 'beluga-highlight-holes) + (define-key map "\C-c\C-e" 'beluga-erase-holes) + (define-key map "\C-c\C-x" 'beluga-run-command) + (define-key map "\C-c\C-t" 'beluga-get-type) + (define-key map "\C-c\C-s" 'beluga-split-hole) + (define-key map "\C-c\C-i" 'beluga-intro-hole) + (define-key map "\C-c\C-j" 'beluga-hole-jump) + (define-key map "\C-c\C-p" 'beluga-hole-info) + map)) + +(defvar beluga-mode-syntax-table + (let ((st (make-syntax-table))) + (modify-syntax-entry ?% "< 14" st) + (modify-syntax-entry ?\{ "(}2 b" st) + (modify-syntax-entry ?\} "){3 b" st) + (modify-syntax-entry ?\n ">" st) + (modify-syntax-entry ?/ "$/" st) + ;; For application of dependent arguments "exp A < ctx . term >", we'd want + ;; <..> to match, but that breaks ->, <-, and other things. + ;; (modify-syntax-entry ?< "(>" st) + ;; (modify-syntax-entry ?> ")<" st) + ; see https://emacs.stackexchange.com/a/4149 for a possible solution + (modify-syntax-entry ?# "'" st) + (modify-syntax-entry ?< "." st) + (modify-syntax-entry ?> "." st) + (modify-syntax-entry ?- "." st) + (modify-syntax-entry ?| "." st) + (modify-syntax-entry ?= "." st) + (modify-syntax-entry ?\' "_" st) + st)) + +(defun beluga--proc-live-p (process) + "Return non-nil if PROCESS is alive. +A process is considered alive if its status is `run', `open', + `listen', `connect' or `stop'." + (and (not (eq process nil)) + (memq (process-status process) + '(run open listen connect stop)))) + +(defun beluga-font-lock-compose-symbol (alist) + "Compose a sequence of ascii chars into a symbol. +Regexp match data 0 points to the chars." + ;; Check that the chars should really be composed into a symbol. + (let* ((start (match-beginning 0)) + (end (match-end 0)) + (syntaxes (cond + ((eq (char-syntax (char-after start)) ?w) '(?w)) + ;; Special case for the . used for qualified names. + ((and (eq (char-after start) ?\.) (= end (1+ start))) + '(?_ ?\\ ?w)) + (t '(?_ ?\\)))) + sym-data) + (if (or (memq (char-syntax (or (char-before start) ?\ )) syntaxes) + (memq (char-syntax (or (char-after end) ?\ )) syntaxes) + (memq (get-text-property start 'face) + '(font-lock-doc-face font-lock-string-face + font-lock-comment-face)) + (and (consp (setq sym-data (cdr (assoc (match-string 0) alist)))) + (let ((pred (cadr sym-data))) + (setq sym-data (car sym-data)) + (funcall pred start)))) + ;; No composition for you. Let's actually remove any composition + ;; we may have added earlier and which is now incorrect. + (remove-text-properties start end '(composition)) + ;; That's a symbol alright, so add the composition. + (compose-region start end sym-data))) + ;; Return nil because we're not adding any face property. + nil) + +(defun beluga-font-lock-symbols-keywords () + (when (and (fboundp 'compose-region) beluga-font-lock-symbols) + (let ((alist nil)) + (dolist (x beluga-font-lock-symbols-alist) + (when (and (if (fboundp 'char-displayable-p) + (char-displayable-p (if (consp (cdr x)) (cadr x) (cdr x))) + t) + (not (assoc (car x) alist))) ;Not yet in alist. + (push x alist))) + (when alist + `((,(regexp-opt (mapcar #'car alist) t) + (0 (beluga-font-lock-compose-symbol ',alist) + ;; In Emacs-21, if the `override' field is nil, the face + ;; expressions is only evaluated if the text has currently + ;; no face. So force evaluation by using `keep'. + keep))))))) + +(defconst beluga-syntax-id-re + "[[:alpha:]_][[:alnum:]_']*" + "A regexp for matching a Beluga identifier.") + +(defconst beluga-syntax-fundec-re + (regexp-opt '("rec" "proof") 'symbols) + "A regexp for matching a function declaration. +Note that this will also match the 'and' keyword!") + +(defvar beluga-imenu-generic-expression + `(("Schemas" + ,(concat "^[ \t]*schema[ \t\n]+\\(" beluga-syntax-id-re "\\)") 1) + ("Constructors" + ,(concat "^\\(" beluga-syntax-id-re "\\)[ \t\n]*:") 1) + ("Type Constructors" + ,(concat "^\\(?:inductive[ \t]+\\(" beluga-syntax-id-re + "\\)\\|\\(?1:" beluga-syntax-id-re + "\\)[ \t\n]*:[^.]*\\[ \t\n]*.\\)") + 1) + ("Functions" + ,(concat beluga-syntax-fundec-re "[ \t\n]+\\(" beluga-syntax-id-re "\\)") 1))) + +(define-obsolete-variable-alias 'beluga-interpreter-path + ;; A "path" is a list of file names, as in $PATH, $MANPATH. + 'beluga-interpreter-name "Sep-2010") +(defcustom beluga-interpreter-name "beluga" + "Name of the interpreter executable." + :type 'string) + +;;---------------------------- Interactive mode ----------------------------;; + +(define-error 'beluga-interactive-error "Beluga interactive error") + +(defun beluga-interactive-error (&rest data) + (signal 'beluga-interactive-error data)) + +;; ------ process management ----- ;; + +(defvar beluga--proc () + "Contain the process running beli.") +(make-variable-buffer-local 'beluga--proc) + +(defvar beluga--output-wait-time + 0.025 + "How long to wait for output from Beluga on each iteration.") + +(defvar beluga--output-timeout + 1.0 + "How long to wait in total for output before giving up.") + +(defvar beluga--output-timer 0.0) + +(defun beluga--proc () + (unless (beluga--proc-live-p beluga--proc) (beluga-start)) + beluga--proc) + +(defun beluga-start () + "Start an inferior Beluga Interactive process with the -emacs option. +The process is put into a buffer called \"*beluga*\"." + (interactive) + (unless (beluga--proc-live-p beluga--proc) + (setq beluga--proc + (get-buffer-process + (make-comint "beluga" + beluga-interpreter-name + nil "-I" "-emacs" )))) + beluga--proc) + +(defun beluga-quit () + "Stop the Beluga interactive process by sending the quit command. +This is a graceful termination." + (interactive) + (beluga--rpc "quit")) + +(defun beluga-stop () + "Stop the beli process." + (interactive) + (when (processp 'beluga--proc) + (kill-process 'beluga--proc))) + +;; ----- Stuff for hole overlays ----- ;; + +(defvar beluga--holes-overlays () + "Will contain the list of hole overlays so that they can be resetted.") +(make-variable-buffer-local 'beluga--holes-overlays) + +(defun beluga-sorted-holes () + (cl-labels + ((hole-comp (a b) + (let* ((s1 (overlay-start a)) + (s2 (overlay-start b))) + (< s1 s2)))) + (sort beluga--holes-overlays #'hole-comp))) + +(defface beluga-holes + '((t :background "cyan")) ;; :foreground "white" + "Face used to highlight holes in Beluga mode.") + +(defun beluga--pos (line bol offset) + ;; According to http://caml.inria.fr/mantis/view.php?id=5159, + ;; `line' can refer to line numbers in various source files, + ;; whereas `bol' and `offset' refer to "character" (byte?) positions within + ;; the actual parsed stream. + ;; So if there might be #line directives, we need to do: + ;; (save-excursion + ;; (goto-char (point-min)) + ;; (forward-line (1- line)) ;Lines count from 1 :-( + ;; (+ (point) (- offset bol)))) + ;; But as long as we know there's no #line directive, we can ignore all that + ;; and use the more efficient code below. When #line directives can appear, + ;; we will need to make further changes anyway, such as passing the file-name + ;; to select the appropriate buffer. + ;; Emacs considers the first character in the file to be at index 1, + ;; but the Beluga lexer starts counting at zero, so we need to add + ;; one here. + (+ (point-min) offset)) + +(defun beluga--create-overlay (pos) + "Create an overlay at the position described by POS (a Loc.to_tuple)." + (let* (;; (file-name (nth 0 pos)) + (start-line (nth 1 pos)) + (start-bol (nth 2 pos)) + (start-off (nth 3 pos)) + (stop-line (nth 4 pos)) + (stop-bol (nth 5 pos)) + (stop-off (nth 6 pos)) + (ol + (make-overlay (beluga--pos start-line start-bol start-off) + (beluga--pos stop-line stop-bol stop-off)))) + (overlay-put ol 'face 'beluga-holes) + ol)) + +(defun beluga-erase-holes () + (interactive) + (mapc #'delete-overlay beluga--holes-overlays) + (setq beluga--holes-overlays nil)) + +;; ----- Interaction with the interactive mode ----- ;; + +;; Sending and receiving strings from the inferior process. +;; Ultimately, all you really need to use is beluga--rpc to send a +;; command to the interactive mode and get back a string response. + +(defun beluga--wait (proc) + (assert (eq (current-buffer) (process-buffer proc))) + (setq beluga--output-timer 0.0) + (while (progn + (goto-char comint-last-input-end) + (not (re-search-forward ".*?;" nil t))) + (accept-process-output proc beluga--output-wait-time) + (setq beluga--output-timer (+ beluga--output-timer beluga--output-wait-time)) + (when (> beluga--output-timer beluga--output-timeout) + (error "Beluga command didn't produce complete output")))) + +(defun beluga--chomp (str) + "Chomp leading and tailing whitespace from STR." + (replace-regexp-in-string (rx (or (: bos (* (any " \t\n"))) + (: (* (any " \t\n")) eos))) + "" + str)) +(defun beluga--trim (str) + (let ((str2 (beluga--chomp str))) + (substring str2 0 (1- (length str2))))) + +(defun beluga--send (cmd) + "Send CMD to beli." + ; (interactive) + (let ((proc (beluga--proc))) + (with-current-buffer (process-buffer proc) + ;; We could also just use `process-send-string', but then we wouldn't + ;; have the input text in the buffer to separate the various prompts. + (goto-char (point-max)) + (insert (concat "%:" cmd)) + (comint-send-input) + (beluga--wait proc)))) + +(defun beluga--receive () + "Read the last output of beli." + (let ((proc (beluga--proc))) + (with-current-buffer (process-buffer proc) + (beluga--wait proc) + (beluga--trim (buffer-substring-no-properties comint-last-input-end (point-max)))))) + +(defun beluga--rpc (cmd) + (beluga--send cmd) + (beluga--receive)) + +(defun beluga--is-response-error (resp) + "Determine whether a Beluga RPC response (RESP) is an error." + (string= "-" (substring resp 0 1))) + +(defun beluga--rpc! (cmd) + "Variant of beluga--rpc that signals an error if CMD fails." + (let ((resp (beluga--rpc cmd))) + (when (beluga--is-response-error resp) + (beluga-interactive-error (list (format "%s" (substring resp 2))))) + resp)) + +(defvar beluga--last-load-time + '(0 0 0 0) + "The last time the file was loaded into the Beluga interpreter. +This variable is updated by `beluga--maybe-save-load-current-buffer'.") +(make-variable-buffer-local 'beluga--last-load-time) + +(defun beluga--should-reload-p () + "Decide whether the current buffer should be reloaded into beli. +The `visited-file-modtime' is compared to `beluga--last-load-time'. +If the former is greater than the latter, then the former is +returned. Else, nil is returned." + (let ((mtime (visited-file-modtime))) + (when (> (float-time mtime) (float-time beluga--last-load-time)) + mtime))) + +;; ----- Beluga Interactive basic functions ----- ;; + +;; Each of these functions directly corresponds to a command in src/core/command.ml + +;; Since the construction of these functions is totally tedious and +;; straightforward, we define a macro to do the real work. +;; This macro takes: +;; * the name of the function to define, e.g. beluga--printhole +;; * the real name of the command to invoke, e.g. "printhole" +;; * the arguments of the function, e.g. '((hole . "%s")) +;; +;; Note: the type of each argument is specified as a printf-style +;; format code. That's because the construction of the command will +;; construct a string formatting routine using these codes. +;; +;; What about constant portions of the string? +;; If you want to supply a constant portion in the format string, +;; simply provide a string as-is in the argument list. +;; e.g. ((hole . "%s") "with" (exp . "%s")) +;; +;; Two separate functions are generated by the macro invocation. The +;; first uses exactly the given function name, but the second appends +;; "!". This bang-variant will use beluga--rpc! under the hood, so you +;; get an exception-raising variant of every function for free. + +(defun beluga--generate-format-string (args) + "Construct the format string from the ARGS." + (cons + "%s" + (mapcar + (lambda (x) + (if (stringp x) + x + (cdr x))) + args))) + +(defun beluga--generate-arg-list (args) + "Construct a list of symbols representing the function arguments from ARGS." + (mapcar 'car (cl-remove-if 'stringp args))) + +(defun beluga--define-command (rpc name realname args) + (let ((arglist (beluga--generate-arg-list args)) + (fmt (beluga--generate-format-string args))) + `(defun ,name ,arglist + (,rpc + (format + ;; construct the format string + ,(mapconcat 'identity fmt " ") + ;; construct the argument list + ,realname ,@arglist))))) + +(defmacro beluga-define-command (name realname args) + `(progn + ,(beluga--define-command 'beluga--rpc name realname args) + ,(beluga--define-command 'beluga--rpc! (intern (concat (symbol-name name) "!")) realname args))) + +(beluga-define-command beluga--basic-chatteron "chatteron" nil) +(beluga-define-command beluga--basic-chatteroff "chatteroff" nil) +(beluga-define-command beluga--basic-load "load" ((path . "%s"))) +(beluga-define-command beluga--basic-clearholes "clearholes" nil) +(beluga-define-command beluga--basic-countholes "countholes" nil) +(beluga-define-command beluga--basic-lochole "lochole" ((hole . "%s"))) +(beluga-define-command beluga--basic-lochole-n "lochole" ((hole . "%d"))) +(beluga-define-command beluga--basic-printhole "printhole" ((hole . "%s"))) +(beluga-define-command beluga--basic-printhole-n "printhole" ((hole . "%d"))) +(beluga-define-command beluga--basic-types "types" nil) +(beluga-define-command beluga--basic-constructors-lf "constructors" ((type . "%s"))) +(beluga-define-command beluga--basic-fill "fill" ((hole . "%s") "with" (exp . "%s"))) +(beluga-define-command beluga--basic-split "split" ((hole . "%s") (var . "%s"))) +(beluga-define-command beluga--basic-intro "intro" ((hole . "%s"))) +(beluga-define-command beluga--basic-constructors "constructors-comp" ((type . "%s"))) +(beluga-define-command beluga--basic-signature "fsig" ((fun . "%s"))) +(beluga-define-command beluga--basic-printfun "fdef" ((fun . "%s"))) +(beluga-define-command beluga--basic-query "query" ((expected . "%s") (tries . "%s") (type . "%s"))) +(beluga-define-command beluga--basic-get-type "get-type" ((line . "%d") (col . "%d"))) +(beluga-define-command beluga--basic-reset "reset" nil) +(beluga-define-command beluga--basic-quit "quit" nil) +(beluga-define-command beluga--basic-lookuphole "lookuphole" ((hole . "%s"))) + +;; ----- Higher-level commands ----- ;; + +;; These build off the basic commands, and will typically do something +;; like parse the response or display it in a message. + +(defun beli () + "Start beli mode." + (interactive) + (beluga-start)) + +(defun beluga-run-command (cmd) + "Run CMD in beli." + (interactive "MCommand: ") + (message "%s" (beluga--rpc cmd))) + +(defun beluga--maybe-save () + (if (buffer-modified-p) + (if (y-or-n-p "Save current file? ") + (save-buffer) + ()))) + +(defun beluga-get-type () + "Get the type at the current cursor position (if it exists)." + (interactive) + (message "%s" (beluga--basic-get-type (count-lines 1 (point)) (current-column)))) + +(defun beluga--load-current-buffer (&optional mtime) + "Load the current buffer in Beluga Interactive. +This command signals an error if loading fails. +The optional MTIME parameter, if given, will be written to `beluga--last-load-time'." + (let ((resp (beluga--basic-load! (buffer-file-name)))) + (when mtime + (setq beluga--last-load-time mtime)) + resp)) + +(defun beluga--maybe-save-load-current-buffer () + "Load the current buffer if it has either never been loaded, or +modified since the last time it was loaded. +This will update `beluga--last-load-time' if a load is performed." + ;; prompt the user to save the buffer if it is modified + (beluga--maybe-save) + ;; retrieve the last time the file was written to disk, checking + ;; whether a reload should occur + (let ((mtime (beluga--should-reload-p))) + (when mtime ;; mtime non-nil means we must reload + (beluga--load-current-buffer mtime)))) + +(defun beluga--lochole-n! (hole-num) + (read (beluga--basic-lochole-n! hole-num))) + +(defun beluga--countholes! () + "Get the number of holes in the file." + (string-to-number (beluga--basic-countholes!))) + +(defun beluga--lookup-hole! (hole) + "Look up a hole number by its name (HOLE)." + (string-to-number (beluga--basic-lookuphole! hole))) + +(defun beluga--highlight-holes () + "Create overlays for each of the holes and color them." + (beluga-erase-holes) + (let ((numholes (beluga--countholes!))) + (dotimes (i numholes) + (let* ((pos (beluga--lochole-n! i)) + (ol (beluga--create-overlay pos)) + (info (beluga--basic-printhole-n! i))) + (overlay-put ol 'help-echo info) + (push ol beluga--holes-overlays) + )))) + +(defun beluga--get-hole-overlay! (hole) + "Get the overlay associated with HOLE." + (nth (beluga--lookup-hole! hole) (beluga-sorted-holes))) + +(defun beluga--apply-quail-completions (str) + (if (string= current-input-method beluga-input-method-name) + (replace-regexp-in-string "=>" "⇒" + (replace-regexp-in-string "|-" "⊢" str)) + str)) + +(defun beluga--insert-formatted (str start) + (goto-char start) + (insert (beluga--apply-quail-completions str)) + (indent-region start (+ start (length str)))) + +(defun beluga--error-no-such-hole (n) + (message "Couldn't find hole %s - make sure the file is loaded" n)) + +(defconst beluga-named-hole-re + "\\_") + +(defun beluga-named-hole-at-point () + "Retrieve the name of the hole at point, if any. +If e.g. point is over \"?abracadabra\", then this function returns +\"abracadabra\". Else, if point is not over a valid hole, then this +function returns nil." + (let ((thing (thing-at-point 'symbol))) + (when (and thing (string-match beluga-named-hole-re thing)) + (match-string 1 thing)))) + +(defun beluga--prompt-with-hole-at-point (prompt) + "Prompt the user to specify a hole, giving the named hole at point +as the default if any." + (let ((name (beluga-named-hole-at-point))) + (if name + (read-string (format "%s (%s): " prompt name) + nil nil name) + (read-string (format "%s: " prompt))))) + +(defun beluga--begin-command () + "Perform necessary setup to begin a compound Beluga Interactive command. +Specifically, the following are performed, if necessary: + - Starting the Beluga inferior process. + - Saving the current buffer. + - Loading the current buffer into Beluga. + - Highlighting holes." + (beluga-start) + (beluga--maybe-save-load-current-buffer) + (beluga--highlight-holes)) + +(defun beluga--prompt-string (prompt) + "Prompts the user to input a string." + (read-string (format "%s: " prompt))) + +;; ----- Top-level commands ----- ;; + +(defun beluga-load () + "Load the current file in Beluga Interactive. +This command will start the interactive mode if necessary and +prompt for saving." + (interactive) + (beluga-start) + (let ((r (beluga--maybe-save-load-current-buffer))) + (if r + (message "%s" r) + (message "Buffer does not require reload.")))) + +(defun beluga-highlight-holes () + "Create overlays for each of the holes and color them." + (interactive) + (beluga--begin-command)) + +(defun beluga-hole-info (hole) + (interactive + (list + (beluga--prompt-with-hole-at-point "Hole to query"))) + (beluga--begin-command) + (let ((resp (beluga--basic-printhole! hole))) + (message resp))) + +(defun beluga-split-hole (hole var) + "Split on HOLE." + (interactive + (list + (beluga--prompt-with-hole-at-point "Hole to split at") + (beluga--prompt-string "Variable to split"))) + (beluga--begin-command) + (let ((resp (beluga--basic-split! hole var)) + (ovr (beluga--get-hole-overlay! hole))) + (if ovr + (let ((start (overlay-start ovr)) + (end (overlay-end ovr))) + (delete-overlay ovr) + (delete-region start end) + (beluga--insert-formatted (format "(%s)" resp) start) + (save-buffer) + (beluga--load-current-buffer) + (beluga--highlight-holes)) + (beluga--error-no-such-hole hole)))) + +(defun beluga-intro-hole (hole) + "Introduce variables into HOLE" + (interactive + (list + (beluga--prompt-with-hole-at-point "Hole to introduce variables into"))) + (beluga--begin-command) + (let ((resp (beluga--basic-intro! hole)) + (ovr (beluga--get-hole-overlay! hole))) + (if ovr + (let ((start (overlay-start ovr)) + (end (overlay-end ovr))) + (delete-overlay ovr) + (delete-region start end) + (beluga--insert-formatted resp start) + (save-buffer) + (beluga--load-current-buffer) + (beluga--highlight-holes)) + (beluga--error-no-such-hole hole)))) + +(defun beluga-hole-jump (hole) + (interactive "sHole to jump to: ") + (beluga--begin-command) + (let ((ovr (beluga--get-hole-overlay! hole))) + (if ovr + (goto-char (+ 1 (overlay-start ovr))) + (beluga--error-no-such-hole hole)))) +(define-obsolete-function-alias 'hole-jump #'beluga-hole-jump "Jul-2018") + +;; ----- Beluga indentation and navigation via SMIE ----- ;; + +(defconst beluga-syntax-pragma-re + "--\\(\\(name\\|query\\|infix\\|prefix\\|assoc\\).*?\\.\\|\\w+\\)" + "A regexp for matching a Beluga pragma. +Long pragmas continue until a `.` is found, e.g. `--name oft D.`. +Short pragmas consist of only one word, e.g. `--nostrengthen`. +It's easy to use this regex to check which type of pragma (long or short) +was matched: simply check whether `match-string' 2 is non-nil. In that case, +a long pragma was matched and the result is the name of the long pragma. +Otherwise, `match-string' 1 will contain the name of the matched short.") + +(defconst beluga-slim-arrows + '("->" "<-" "→" "←") + "A list of the slim arrow strings.") + +(defconst beluga-fat-arrows + '("=>" "⇒") + "A list of the fat arrow strings.") + +(defconst beluga-punct-re + (regexp-opt `(,@beluga-fat-arrows ,@beluga-slim-arrows "\\" "." "<" ">" "," ";" ".."))) + +(defconst beluga-syntax-keyword-re + (regexp-opt + '("FN" "and" "block" "case" "inductive" "LF" "coinductive" + "stratified" "else" "ffalse" "fn" "if" "in" "impossible" "let" + "mlam" "of" "rec" "proof" "schema" "some" "then" "type" "ctype" "ttrue" + "typedef" + "module" "struct" "end" "#stratified" "#positive" "fun") + 'symbols) + "A regular expression to match any beluga keyword.") + +(defface beluga-font-lock-annotation + '((t :inherit italic)) + "Face used for Beluga's /.../ annotations.") + +(defconst beluga-font-lock-keywords + `((,beluga-syntax-pragma-re . ,font-lock-warning-face) + + ,beluga-syntax-keyword-re + + ("/\\s-*\\_<\\(total\\)\\_>.*?/" + (0 'beluga-font-lock-annotation) + (1 'font-lock-keyword-face prepend)) + + (,(concat + "^\\(" beluga-syntax-id-re "\\)" + "\\s-*" ":" + "\\([^.]*\\_\\s-*.\\)?") + ;; This is a regexp that can span multiple lines, so it may not + ;; always highlight properly. `font-lock-multiline' tries to help. + (0 (if (match-end 2) '(face nil font-lock-multiline t))) + (1 (if (match-end 2) + font-lock-type-face font-lock-variable-name-face))) + + (,(concat "^\\(?:schema\\|inductive\\|coinductive\\|LF\\|stratified\\)" "\\s-+" + "\\(" beluga-syntax-id-re "\\)") + (1 font-lock-type-face)) + + (,(concat beluga-syntax-fundec-re "\\s-+\\(" beluga-syntax-id-re "\\)") + (2 font-lock-function-name-face)) + )) + +(defcustom beluga-indent-basic 4 + "Basic amount of indentation." + :type 'integer) + +(defun beluga-smie-forward-token () + ; skip all whitespace and comments + (forward-comment (point-max)) + (cond + ((looking-at "\\.[ \t]*$") + ;; One of the LF-terminating dots. + (progn (forward-char 1) ";.")) + ((looking-at beluga-syntax-pragma-re) + ;; move point to the end of the long-pragma name if a long-pragma was matched. + ;; otherwise, move it to the end of the short pragma name. + (goto-char (or (match-end 2) (match-end 1))) + ;; set to non-nil by beluga-syntax-pragma-re if the pragma is a long-form pragma + (if (match-string 2) + "--longpragma" + "--shortpragma")) + (t + ; otherwise, we return a chunk of text that starts at point and + ; ends according to the following `cond` check. + (buffer-substring-no-properties + (point) + (progn + (cond + ((looking-at beluga-punct-re) (goto-char (match-end 0))) + ((not (zerop (skip-syntax-forward "w_'")))) + ;; In case of non-ASCII punctuation. + ((not (zerop (skip-syntax-forward "."))))) + (point)))))) + +(defun beluga-short-pragma-before-p () + "Decide whether there is a short pragma before point. +Return the starting position of the short pragma; else, nil." + (save-excursion + ;; idea: move backward across word-characters, and then check if + ;; there are two dashes before point. + (skip-syntax-backward "w") + (save-match-data + (when (looking-back "--" (- (point) 2)) + (match-beginning 0))))) + +(defun beluga-smie-backward-token () + (forward-comment (- (point-max))) + (cond + ((and (eq ?\. (char-before)) + (looking-at "[ \t]*$") ;; "[ \t]*\\(?:$\\|[0-9]\\(\\)\\)" + (not (looking-back "\\.\\." (- (point) 2)))) + ;; Either an LF-terminating dot, or a projection-dot. + (progn (forward-char -1) ";.")) + ((setq pos (beluga-short-pragma-before-p)) + (goto-char pos) + "--shortpragma") + (t + (buffer-substring-no-properties + (point) + (progn + (cond + ((looking-back beluga-punct-re (- (point) 2) 'greedy) + (goto-char (match-beginning 0))) + ((not (zerop (skip-syntax-backward "w_'")))) + ;; In case of non-ASCII punctuation. + ((not (zerop (skip-syntax-backward "."))))) + (point)))))) + +(defun beluga-smie-grammar (bnf resolvers precs) + (smie-prec2->grammar + (smie-merge-prec2s + (apply #'smie-bnf->prec2 bnf resolvers) + (smie-precs->prec2 precs)))) + +(defconst beluga-smie-grammar + ;; The "." used for terminating LF declarations is syntactically completely + ;; different from the "." used in the binding forms. Conflating the two + ;; leads here to a lot of precedence conflicts, so we try and guess the two + ;; based on a heuristic in the tokenizing code. + (beluga-smie-grammar + ;; FIXME: without this dummy, "=>" is marked as "open paren" because it + ;; can only bind to `atom' on the left. + '((atom ("--dummy--")) + + (def (exp "=" exp) (atom ":" exp)) + + (decl + (atom ":" type) + (datatypes) + ("schema" sdef) + ("let" def) + ("--shortpragma") + ("--longpragma" atom) + (recs)) + + (datatypes + ("inductive" datatype-def) + ("coinductive" datatype-def) + ("LF" datatype-def) + ("stratified" datatype-def) + (datatypes "and" datatypes)) + + (simpletype + (simpletype "->" simpletype) + (simpletype "→" simpletype) + (simpletype "←" simpletype) + (simpletype "<-" simpletype)) + + (recs + ("rec" def) + ("proof" def) + (recs "and" recs)) + + (decls + (decl) + (decls ";" decls) + (decls ";." decls)) + + ;; FIXME: only allow simple types here, otherwise we get nasty + ;; precedence conflicts between "." and ",". In practice, this seems to + ;; be sufficient. + (sdecl + (atom ":" type)) + + (sdecls + (sdecl) + (sdecls "," sdecls)) + + (dotted-type + (sdecls "|-" type) + (sdecls "⊢" type)) + + (type + (simpletype) + ("\\" atom "." type) ;dotted-type + ("block" sdecls "." type) ;dotted-type + ;; ("{" blabla "}" type) ; FIXME! + ;; FIXME: the projection via "." creates precedence conflicts. + ;; (type "." atom) + ) + + (sdef + (atom "=" schema)) + + (schema + (type) + ;; Not sure if it's correct, and create precedence conflicts. + ;; ("some" sdecls "block" sdecls "." schema) + ) + + (datatype-name + (atom ":" type)) + + (datatype-def + (datatype-name "=" datatype-branches)) + + (datatype-branches + (datatype-branches "|" datatype-branches) + (atom ":" type)) + + (exp + ("if" exp "then" exp "else" exp) + (type) + ("let" def "in" exp) + ("fun" atom "=>" exp) + ("fun" atom "⇒" exp) + ("fn" atom "=>" exp) + ("fn" atom "⇒" exp) + ("FN" atom "=>" exp) + ("FN" atom "⇒" exp) + ("mlam" atom "=>" exp) + ("mlam" atom "⇒" exp) + ("<" dotted-type ">") + ("case" exp "of" cases)) + + (exps + (exps ";" exps) + (exp)) + + ;; Separate cases/branch so that "|" is recognized as associative. + (cases + (branch) + (cases "|" cases)) + + (branch + (atom "=>" exp) + (atom "⇒" exp))) + + ; resolvers + `(((assoc ";" ";.")) + ((assoc ,@beluga-slim-arrows)) + ((assoc ",")) + ((assoc "and")) + ((nonassoc "of") (assoc "|")) ; Trailing | ambiguity. + ;; '((nonassoc "\\") (nonassoc ".")) ; Trailing . ambiguity. + ;; '((nonassoc "block") (nonassoc ".")) ; Trailing . ambiguity. + ) + + ;; The above BNF grammar should cover this already, so this ends up only + ;; useful to check that the BNF entails the expected precedences. + ; precs + `((assoc ";") + (assoc ",") + (left ":") + (assoc ,@beluga-slim-arrows) + (nonassoc " -dummy- ")))) ;Bogus anchor at the end. + +(defconst beluga-pattern-fat-arrow + `(or ,@beluga-fat-arrows) + "A pattern for use in pcase that matches any fat arrow string.") + +(defun beluga-rule-parent-p (parents) + `(smie-rule-parent-p ,@parents)) + +(defun beluga-smie-indent-rules (method token) + (pcase `(,method . ,token) + (`(:elem . basic) + beluga-indent-basic) + + ; describe the tokens that introduce lists (with no separators) + (`(:list-intro . ,(or ":" "fn" "fun" "FN" "mlam")) + 't) + + ; if the token is a pipe preceded by an '=' or 'of', then we + ; indent by adding the basic offset + (`(:before . ,(and "|" (guard (smie-rule-prev-p "=" "of" "fun")))) + 2) + + ; if the token is a pipe, (and the preceding check didn't pass, so + ; it isn't the first pipe in a sequence) then we consider it a + ; separator + (`(method . ,"|") + (smie-rule-separator method)) + + (`(:after . ,"of") + beluga-indent-basic) + + (`(:after . ,"in") + (when (smie-rule-hanging-p) + (smie-rule-parent))) + + (`(:after . ,(and "=" (guard (smie-rule-parent-p "rec")))) + (smie-rule-parent)) + + (`(:after . ,(and "=" (guard (smie-rule-parent-p "proof")))) + (smie-rule-parent)) + + ; if the token is a form that will use => eventually but is + ; preceded by a =>, then this is a chain, e.g. + ; mlam G => mlam Q => fn x => fn y => + ; (code should be here, not indented 8 characters) + (`(:before + . + ,(and + (or "case" "fn" "FN" "mlam" "fun") + (guard `(smie-rule-prev-p ,@beluga-fat-arrows)))) + (smie-rule-parent)) + + (`(:after . ".") + (smie-rule-parent)) + + (`(:after . ,(and ":" (guard (smie-rule-parent-p "{")))) + (smie-rule-parent 1)) + + (`(:after . ,(or ":" "let" "if")) + (smie-rule-parent beluga-indent-basic)) + + (`(:before + . + ,(and "=" (guard `(smie-rule-parent-p ,@beluga-type-declaration-keywords)))) + (smie-rule-parent)) + )) + +;;---------------------------- Loading of the mode ----------------------------;; + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.s?bel\\'" . beluga-mode)) + +(defconst beluga-type-declaration-keywords + '("inductive" "coinductive" "LF" "stratified") + "The different keywords that introduce a type definition.") + +(defconst beluga-type-declaration-keywords-re + (regexp-opt beluga-type-declaration-keywords 'symbols) + "A regular expression that matches any type definition keyword.") + +;;;###autoload +(define-derived-mode beluga-mode prog-mode "Beluga" + "Major mode to edit Beluga source code." + (set (make-local-variable 'imenu-generic-expression) + beluga-imenu-generic-expression) + (set (make-local-variable 'outline-regexp) + (concat beluga-syntax-fundec-re "\\|^(inductive\\|coinductive\\|LF\\|stratified)\\_>")) + (set (make-local-variable 'require-final-newline) t) + (when buffer-file-name + (set (make-local-variable 'compile-command) + ;; Quite dubious, but it's the intention that counts. + (concat beluga-interpreter-name + " " + (shell-quote-argument buffer-file-name)))) + (set (make-local-variable 'comment-start) "%") + (set (make-local-variable 'comment-start-skip) "%[%{]*[ \t]*") + (set (make-local-variable 'comment-end-skip) "[ \t]*\\(?:\n\\|}%\\)") + (comment-normalize-vars) + (set (make-local-variable 'electric-indent-chars) + (append '(?|) (if (boundp 'electric-indent-chars) + electric-indent-chars + '(?\n)))) + ;QUAIL + (add-hook 'beluga-mode-hook + (lambda () (set-input-method "beluga-unicode"))) + + ;Turn off hilighting + (setq input-method-highlight-flag nil) + + (smie-setup + beluga-smie-grammar + 'beluga-smie-indent-rules + :forward-token #'beluga-smie-forward-token + :backward-token #'beluga-smie-backward-token) + + (set (make-local-variable 'parse-sexp-ignore-comments) t) + + (set + (make-local-variable 'font-lock-defaults) + '(beluga-font-lock-keywords + nil + nil + () + nil + (font-lock-syntactic-keywords . nil))) + + (message "beluga-mode loaded")) + +(provide 'beluga-mode) +;;; beluga-mode.el ends here -- cgit v1.2.3