;;; 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 <monnier@iro.umontreal.ca>
;; 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 <http://www.gnu.org/licenses/>.

;;; 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)
    ;; the following line breaks using / in identifiers
    ;; (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]*:[^.]*\\<type\\>[ \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
  "\\_<?\\(\\sw+\\)\\_>")

(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-*" ":"
       "\\([^.]*\\_<type\\_>\\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