RASP mode

^home ^projects

@Gitlab

rasp-mode is an Emacs major mode for RASP, a language for understanding transformers. It supports syntax highlighing, commenting, indentation and simple auto-completion.

I originally wrote rasp-mode because I wanted to learn how to write an Emacs major mode (also, I found the paper on RASP pretty cool). It's simple, but it works!

Since this website is powered by Emacs's org-mode, I can highlight rasp code blocks with rasp-mode. Here is an example:

def _with_bos_selector_width(s) {
    s = s or select(indices, 0, ==);
    return round((1/aggregate(s, indicator(indices == 0)))) - 1;
}

Since it is short, here is the entire source code of rasp-mode:

;;; rasp.el --- A Major Mode for writing RASP        -*- lexical-binding: t; -*-

;; Copyright (C) 2021  Arthur Amalvy

;; Author: Arthur Amalvy
;; URL: https://gitlab.com/Aethor/rasp-mode
;; Package-Requires: ((emacs "24.3"))
;; Keywords: languages
;; Version: 0.0.1

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; `rasp-mode' is a major mode for RASP (Restricted Access Sequence
;; Processing Language), a language for understanding transformers.

;;; Code:


;;; Customisation

(defgroup rasp ()
  "Major mode for editing RASP files."
  :group 'languages)

(defcustom rasp-mode-indent-offset 4
  "Number of indentation spaces in `rasp-mode'."
  :type 'integer
  :group 'rasp)


;;; auto-mode-alist

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


;;; Constant definitions

(defconst rasp-keywords '("not" "and" "or" "def" "return" "if" "else" "for" "True" "False")
  "Keywords for `rasp-mode'.")

(defconst rasp-builtins '("select" "aggregate" "selector_width" "zip" "len" "range" "indicator" "round")
  "Builtins functions for `rasp-mode'.")

(defconst rasp-constants '("tokens" "indices" "length")
  "Constants for `rasp-mode'.")

(defconst rasp--function-regex (rx line-start (0+ space) "def" (1+ space) (group (1+ (or word ?_))))
  "Match a rasp function.
Match group 1 can be used to retrieve the name of the function.")

(defconst rasp--variable-declaration-regex (rx line-start (0+ space) (group (1+ (or word ?_))) (0+ space) ?=)
  "Match a rasp variable declaration.
Match group 1 can be used to retrieve the name of the variable.")


;;; Font lock

(defconst rasp-font-lock-keywords
  (append
   ;; keywords
   (mapcar (lambda (k) (cons (rx symbol-start (literal k) symbol-end) font-lock-keyword-face)) rasp-keywords)
   ;; builtins functions
   (mapcar (lambda (k) (cons (rx symbol-start (literal k) symbol-end) font-lock-builtin-face)) rasp-builtins)
   ;; constants
   (mapcar (lambda (k) (cons (rx symbol-start (literal k) symbol-end) font-lock-constant-face)) rasp-constants)
   `(
     ;; function names
     (,rasp--function-regex (1 font-lock-function-name-face))
     ;; variable declaration
     (,rasp--variable-declaration-regex (1 font-lock-variable-name-face))))
  "Fontification for `rasp-mode'.")


;;; Syntax table

(defvar rasp-mode-syntax-table
  (let ((table (make-syntax-table)))
    (modify-syntax-entry ?# "<" table)
    (modify-syntax-entry ?\n ">" table)
    table)
  "Syntax table for `rasp-mode'.")


;;; Indentation

(defun rasp--at-an-unfinished-statement-p ()
  "Check if the current line is an unfinished statement."
  (interactive)
  (and
   ;; beginning or end of function
   (not (looking-at "^.*[{}][ \t]*\\(#.*\\)?$"))
   ;; statement ending in ;
   (not (looking-at "^.*;[ \t]*\\(#.*\\)?$"))
   ;; empty line
   (not (looking-at "^[\t ]*\\(#.*\\)?$"))))

(defun rasp-indent-line ()
  "Indent current line of rasp code."
  (interactive)
  (beginning-of-line)
  (let ((line-indent (current-indentation))
        (is-indented nil))
    (save-excursion
      ;; look backward to figure indentation. When exiting the while,
      ;; `line-indent' will be set to the needed indent level (except
      ;; for some special cases handled at the end of the function).
      (while (not is-indented)
        (forward-line -1)
        (beginning-of-line)
        (cond
         ;; we see the beginning of a function: indent to beginning of
         ;; function + 1 since we are in its body
         ((looking-at "^.*{[ \t]*\\(#.*\\)?$")
          (setq line-indent (+ (current-indentation) rasp-mode-indent-offset))
          (setq is-indented t))
         ;; we see the end of function or the beginning of the file:
         ;; indent to the indentation of the closing brace or 0
         ((or (looking-at "^.*}[ \t]*\\(#.*\\)?$")
              (bobp))
          (setq line-indent (current-indentation))
          (setq is-indented t)))))
    (cond
     ;; exception: we were at the closing brace of a function. In that
     ;; case, we must remove 1 to the indent level (we saw the
     ;; beginning of the function earlier, but the closing brace should
     ;; be indented at one less level as the function's body)
     ((looking-at "^.*}[ \t]*\\(#.*\\)?$")
      (indent-line-to (- line-indent rasp-mode-indent-offset)))
     ;; exception: unfinished statement
     ((save-excursion (forward-line -1)
                      (rasp--at-an-unfinished-statement-p))
      (indent-line-to (+ line-indent rasp-mode-indent-offset)))
     ;; standard case
     (t (indent-line-to line-indent)))))


;;; Completion

(defun rasp--buffer-matchs (regexp &optional group)
  "Return all matchs of regex REGEXP in current buffer.
If GROUP is specified, get all matchs for regexp group GROUP."
  (let ((group (or group 0))
        (matchs '()))
    (save-excursion
      (goto-char 1)
      (while (search-forward-regexp regexp nil t 1)
        (push (match-string-no-properties group) matchs)))
    matchs))

(defun rasp-buffer-functions ()
  "Return the names of all rasp functions in the current buffer."
  (rasp--buffer-matchs rasp--function-regex 1))

(defun rasp-buffer-variables ()
  "Return all rasp variables declared in the current buffer."
  (rasp--buffer-matchs rasp--variable-declaration-regex 1))

(defun rasp-completion-at-point ()
  "Completion function for `rasp-mode'.
Supports rasp builtins, constants, keywords, in-buffer declared
variables and functions."
  (let ((bounds (bounds-of-thing-at-point 'word)))
    (when bounds
      (list (car bounds)
            (cdr bounds)
            (append (rasp-buffer-variables)
                    (rasp-buffer-functions)
                    rasp-builtins rasp-constants rasp-keywords)))))


;;; Menu Bar

(defvar rasp-mode-map (make-sparse-keymap) "Keymap for RASP mode.")

(easy-menu-define rasp-menu rasp-mode-map "`rasp-mode' menu."
  `("RASP"
    ["Indent Line" rasp-indent-line]))


;;; Mode declaration

;;;###autoload
(define-derived-mode rasp-mode prog-mode "Rasp"
  "Major mode for editing RASP code.
\\{rasp-mode-map}"
  :group 'rasp
  ;; font lock
  (setq-local font-lock-defaults '(rasp-font-lock-keywords nil nil nil))
  ;; comments
  (setq-local comment-start "#")
  (setq-local comment-start-skip "#+ *")
  ;; indentation
  (setq-local indent-line-function #'rasp-indent-line)
  ;; completion
  (add-hook 'completion-at-point-functions #'rasp-completion-at-point nil 'local))

(provide 'rasp)
;;; rasp.el ends here