aboutsummaryrefslogblamecommitdiffstats
path: root/editors/emacs/sol-mode.el
blob: 0b9579da44e434a48a4604d827c7dea7daa65325 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
















                                                                                          



                                                                        




































































































































































































































                                                                                                                     







                                                      


                                                      







                                                                    























                                                                                 

                                                    




































                                                                          
;;; sol-mode.el --- Major mode for the Sol relational language -*- lexical-binding: t; -*-

;; Author: Marcos Magueta
;; Keywords: languages, logic, relational
;; Version: 0.2.0
;; Package-Requires: ((emacs "26.1"))

;;; Commentary:

;; A major mode for Sol, a total relational language with Smalltalk-like
;; message syntax.  It provides:
;;
;;   - Syntax highlighting, one face per token category: directives,
;;     logical combinators, reserved labels, generic labels, relation
;;     heads, variables, references (#Name), literals, delimiters.
;;   - Navigation: imenu over Relations, Rules and Queries;
;;     beginning/end-of-defun over top-level forms.
;;   - An inferior Sol REPL (`run-sol'); `sol-load-file' (C-c C-l) loads
;;     the current file into the session, `sol-send-line' (C-c C-e)
;;     sends the statement at point, `sol-run-buffer' (C-c C-k) runs the
;;     file in a compilation buffer.
;;   - Eglot wiring: `sol lsp' serves diagnostics, so admission errors
;;     (totality, schemas, integrity) appear while typing.
;;
;; Installation:
;;
;;   (add-to-list 'load-path "/path/to/sol2/editors/emacs")
;;   (require 'sol-mode)
;;
;; Files ending in .sol open in `sol-mode' automatically.

;;; Code:

(require 'comint)

(defgroup sol nil
  "Major mode for the Sol relational language."
  :group 'languages
  :prefix "sol-")

(defcustom sol-indent-offset 2
  "Number of columns to indent per bracket nesting level."
  :type 'integer
  :group 'sol)

(defcustom sol-program "sol"
  "Path to the sol executable, used by the REPL, the runner and eglot."
  :type 'string
  :group 'sol)

;;; ----------------------------------------------------------------------
;;; Faces -- one colour per token category.
;;; ----------------------------------------------------------------------

(defface sol-keyword-face
  '((((background dark))  (:foreground "#c678dd" :weight bold))
    (((background light)) (:foreground "#a626a4" :weight bold)))
  "Face for top-level directives: Relation, Rule, Query."
  :group 'sol)

(defface sol-combinator-face
  '((((background dark))  (:foreground "#56b6c2" :weight bold))
    (((background light)) (:foreground "#0184bc" :weight bold)))
  "Face for the logical combinators :and, :or and :not."
  :group 'sol)

(defface sol-builtin-label-face
  '((((background dark))  (:foreground "#e5c07b"))
    (((background light)) (:foreground "#b07d00")))
  "Face for structural labels: named:, schema:, head:, body:, and kin."
  :group 'sol)

(defface sol-label-face
  '((((background dark))  (:foreground "#61afef"))
    (((background light)) (:foreground "#4078f2")))
  "Face for generic attribute labels, e.g. value:, left:, age:."
  :group 'sol)

(defface sol-relation-face
  '((((background dark))  (:foreground "#98c379" :weight bold))
    (((background light)) (:foreground "#50a14f" :weight bold)))
  "Face for relation heads -- the first name after `['."
  :group 'sol)

(defface sol-variable-face
  '((((background dark))  (:foreground "#e06c75"))
    (((background light)) (:foreground "#e45649")))
  "Face for logical variables -- capitalised identifiers in term position."
  :group 'sol)

(defface sol-reference-face
  '((((background dark))  (:foreground "#d19a66" :weight bold))
    (((background light)) (:foreground "#986801" :weight bold)))
  "Face for relation references, #Name."
  :group 'sol)

(defface sol-number-face
  '((((background dark))  (:foreground "#ff79c6"))
    (((background light)) (:foreground "#c41a8d")))
  "Face for integer and rational literals."
  :group 'sol)

(defface sol-bracket-face
  '((((background dark))  (:foreground "#828997"))
    (((background light)) (:foreground "#9ca0a4")))
  "Face for delimiters and the cons bar |."
  :group 'sol)

;;; ----------------------------------------------------------------------
;;; Syntax table.
;;; ----------------------------------------------------------------------

(defvar sol-mode-syntax-table
  (let ((table (make-syntax-table)))
    ;; Line comments start with % and run to end of line.
    (modify-syntax-entry ?% "<" table)
    (modify-syntax-entry ?\n ">" table)
    ;; Strings.
    (modify-syntax-entry ?\" "\"" table)
    (modify-syntax-entry ?\\ "\\" table)
    ;; The delimiter law: [] propositions, {} records, () grouping,
    ;; #( ... ) sequences.  All pair for navigation.
    (modify-syntax-entry ?\[ "(]" table)
    (modify-syntax-entry ?\] ")[" table)
    (modify-syntax-entry ?\{ "(}" table)
    (modify-syntax-entry ?\} "){" table)
    (modify-syntax-entry ?\( "()" table)
    (modify-syntax-entry ?\) ")(" table)
    ;; # prefixes references and sequences.
    (modify-syntax-entry ?# "'" table)
    ;; Identifier constituents.
    (modify-syntax-entry ?_ "_" table)
    ;; Punctuation used as token glue.
    (modify-syntax-entry ?: "." table)
    (modify-syntax-entry ?| "." table)
    (modify-syntax-entry ?/ "." table)
    table)
  "Syntax table for `sol-mode'.")

;;; ----------------------------------------------------------------------
;;; Font lock.  Order matters: earlier entries win.
;;; ----------------------------------------------------------------------

(defconst sol-font-lock-keywords
  (list
   ;; 1. Top-level directives.
   '("^[[:space:]]*\\(Relation\\|Rule\\|Query\\)\\_>" 1 'sol-keyword-face)
   ;; 2. Logical combinators.
   '("\\(:and\\|:or\\|:not\\)\\_>" 1 'sol-combinator-face)
   ;; 3. Relation references, #Name.
   '("\\(#[A-Za-z][A-Za-z0-9_]*\\)" 1 'sol-reference-face)
   ;; 4. Structural labels.
   '("\\_<\\(named\\|schema\\|name\\|head\\|body\\|expression\\|domain\\|variable\\|quantifier\\):"
     1 'sol-builtin-label-face)
   ;; 5. Relation head: the first name after `['.
   '("\\[[[:space:]]*\\([A-Za-z][A-Za-z0-9_]*\\)" 1 'sol-relation-face)
   ;; 6. Generic attribute labels.
   '("\\_<\\([A-Za-z_][A-Za-z0-9_]*\\):" 1 'sol-label-face)
   ;; 7. Logical variables: capitalised identifiers in term position.
   '("\\_<\\([A-Z][A-Za-z0-9_]*\\)\\_>" 1 'sol-variable-face)
   ;; 8. Numeric literals: integers and rationals.
   '("\\_<-?[0-9]+\\(?:/[0-9]+\\)?\\_>" 0 'sol-number-face)
   ;; 9. Delimiters.
   '("[][}{|]" 0 'sol-bracket-face))
  "Font-lock highlighting for `sol-mode'.")

(defvar sol-prettify-symbols-alist
  '((":and" . ?∧)
    (":or"  . ?∨)
    (":not" . ?¬))
  "Symbol prettification for `sol-mode'.")

;;; ----------------------------------------------------------------------
;;; Navigation.
;;; ----------------------------------------------------------------------

(defconst sol-defun-regexp
  "^[[:space:]]*\\(?:Relation\\|Rule\\|Query\\)\\_>\\|^[[:space:]]*\\["
  "Regexp matching the start of a top-level Sol form: a directive or a fact.")

(defvar sol-imenu-generic-expression
  (list
   (list "Relation"
         "\\_<Relation\\_>[[:space:]]+named:[[:space:]]*\\([A-Za-z][A-Za-z0-9_]*\\)" 1)
   (list "Rule"
         "\\_<Rule\\_>[[:space:]]+name:[[:space:]]*\"\\([^\"]+\\)\"" 1)
   (list "Query"
         "\\_<Query\\_>[[:space:]]+expression:[[:space:]]*\\(?:(\\|\\[\\)[[:space:]]*\\([A-Za-z][A-Za-z0-9_]*\\)" 1))
  "Imenu configuration for `sol-mode'.")

(defun sol-beginning-of-defun (&optional arg)
  "Move backward to the start of a top-level Sol form.
With ARG, repeat that many times.  Returns non-nil on success."
  (setq arg (or arg 1))
  (let ((found nil))
    (while (> arg 0)
      (setq found (re-search-backward sol-defun-regexp nil t))
      (setq arg (1- arg)))
    (when found
      (goto-char (match-beginning 0))
      (back-to-indentation))
    found))

(defun sol-end-of-defun ()
  "Move to the end of the current top-level Sol form."
  (when (looking-at-p sol-defun-regexp)
    (forward-char 1))
  (if (re-search-forward sol-defun-regexp nil 'move)
      (progn
        (goto-char (match-beginning 0))
        (skip-chars-backward " \t\n"))
    (goto-char (point-max))))

;;; ----------------------------------------------------------------------
;;; Indentation: bracket-depth based, conservative.
;;; ----------------------------------------------------------------------

(defun sol-indent-line ()
  "Indent the current line by its bracket nesting depth."
  (let* ((bol   (line-beginning-position))
         (depth (car (syntax-ppss bol))))
    (save-excursion
      (back-to-indentation)
      (when (and (> depth 0) (looking-at-p "[])}]"))
        (setq depth (1- depth))))
    (if (> depth 0)
        (indent-line-to (* depth sol-indent-offset))
      'noindent)))

;;; ----------------------------------------------------------------------
;;; Inferior Sol: the REPL, through comint.
;;; ----------------------------------------------------------------------

(defvar inferior-sol-buffer "*sol*"
  "Buffer name of the inferior Sol process.")

(define-derived-mode inferior-sol-mode comint-mode "Inferior Sol"
  "Major mode for the Sol REPL."
  (setq comint-prompt-regexp "^sol> ")
  (setq comint-prompt-read-only t))

;;;###autoload
(defun run-sol ()
  "Start the Sol REPL, or pop to it if it is already running."
  (interactive)
  (unless (comint-check-proc inferior-sol-buffer)
    (with-current-buffer (make-comint "sol" sol-program)
      (inferior-sol-mode)))
  (pop-to-buffer inferior-sol-buffer))

(defun sol--send (statement)
  "Send STATEMENT to the REPL, starting it if needed."
  (run-sol)
  (with-current-buffer inferior-sol-buffer
    (goto-char (point-max))
    (insert statement)
    (comint-send-input)))

(defun sol-send-line ()
  "Send the current line (one statement) to the REPL."
  (interactive)
  (sol--send (string-trim (or (thing-at-point 'line t) ""))))

(defun sol-load-file ()
  "Load the current file into the REPL session with :load.
The load is atomic: a rejected file changes nothing in the session."
  (interactive)
  (when (buffer-modified-p) (save-buffer))
  (sol--send (format ":load %s" buffer-file-name)))

(defun sol-run-buffer ()
  "Run the current file with sol, showing answers in a compilation buffer."
  (interactive)
  (when (buffer-modified-p) (save-buffer))
  (compile (format "%s %s" sol-program (shell-quote-argument buffer-file-name))))

;;; ----------------------------------------------------------------------
;;; Eglot: diagnostics from `sol lsp'.
;;; ----------------------------------------------------------------------

(defvar eglot-server-programs)

(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               `(sol-mode . (,sol-program "lsp"))))

;;; ----------------------------------------------------------------------
;;; Keymap.
;;; ----------------------------------------------------------------------

(defvar sol-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-z") #'run-sol)
    (define-key map (kbd "C-c C-l") #'sol-load-file)
    (define-key map (kbd "C-c C-e") #'sol-send-line)
    (define-key map (kbd "C-c C-k") #'sol-run-buffer)
    (define-key map (kbd "C-c C-j") #'imenu)
    map)
  "Keymap for `sol-mode'.")

;;; ----------------------------------------------------------------------
;;; Mode definition.
;;; ----------------------------------------------------------------------

;;;###autoload
(define-derived-mode sol-mode prog-mode "Sol"
  "Major mode for editing Sol relational programs.

\\{sol-mode-map}"
  :syntax-table sol-mode-syntax-table
  (setq-local font-lock-defaults '(sol-font-lock-keywords))
  ;; Comments.
  (setq-local comment-start "% ")
  (setq-local comment-end "")
  (setq-local comment-start-skip "%+[ \t]*")
  ;; Navigation.
  (setq-local beginning-of-defun-function #'sol-beginning-of-defun)
  (setq-local end-of-defun-function #'sol-end-of-defun)
  (setq-local imenu-generic-expression sol-imenu-generic-expression)
  (setq-local imenu-case-fold-search nil)
  ;; Indentation.
  (setq-local indent-line-function #'sol-indent-line)
  (setq-local indent-tabs-mode nil)
  ;; Eye-candy.
  (setq-local prettify-symbols-alist sol-prettify-symbols-alist))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.sol\\'" . sol-mode))

(provide 'sol-mode)

;;; sol-mode.el ends here