UUUMエンジニアブログ

UUUMのエンジニアによる技術ブログです

EmacsLispでBrainfuckのMajor Modeとインタプリタを作る

こんにちは、最近 Ubuntu 20.04 から Manjaro 19.0 に乗り換えた @takeokunn です。

非常にサクサク動くようになって快適になりました。Linux詳しくなりたいです。

はじめに

brainfuckは実行できる命令が8種類しかないシンプルな言語です。brainfuckの記事は世の中に転がっているのでそちらを参照ください。

知り合いのエンジニアとガストで喋ってたときに「emacs詳しくなりたいならbrainfuckとか良いんじゃね?作ろうよー」と言われたので作ってみました。

できたもの

repo: https://github.com/takeokunn/brainfuck.el

f:id:bararararatty:20200324160118p:plain

  • syntax highlightできるようにした
  • tokenのdocをmodelineに出すようにした
  • interpreterを作ってbuffer内のstringを取得して結果を出すようにした

Major Mode

(defvar bf-syntax-table
  (let ((table (make-syntax-table)))
    (modify-syntax-entry ?\" "." table)
    table))

;;;###autoload
(define-derived-mode brainfuck-mode prog-mode "Brainfuck"
  :syntax-table bf-syntax-table
  (bf--add-keywords)
  (bf--help-doc-fun))

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

(defvar brainfuck-mode-map nil "Keymap for brainfuck-mode.")

(defun bf--add-keywords ()
  (font-lock-add-keywords
   nil
   (list (cons (rx (any "[" "]")) font-lock-keyword-face)
         (cons (rx (any ">" "<" "+" "-" "." ",")) font-lock-function-name-face)
         (cons (rx (not (any "[" "]" ">" "<" "+" "-" "." ","))) font-lock-comment-face))))

今回は prog-mode から派生させてみました。

token 8種類をハイライトするだけなので、 keyword を正規表現でmatchさせて [ ] はkeyword, 他6種類のtokenは function, それ以外の文字列は comment として表示するように書いてみました。

DocをModeLineに表示

(defun bf--help-sym-called-at-point ()
  (unless (eobp)
    (buffer-substring-no-properties (point) (1+ (point)))))

(defun bf--help-lookup-doc (sym)
  "Return document string for SYM."
  (pcase sym
    (">" "Increment the pointer.")
    ("<" "Decrement the pointer.")
    ("+" "Increment the value indicated by the pointer.")
    ("-" "Decrement the value indicated by the pointer.")
    ("." "Print the value indicated by the pointer.")
    ("," "Read one byte from input and store it in the indicated value.")
    ("[" "Jump to the matching `]' if the indicated value is zero.")
    ("]" "Jump to the matching `[' if the indicated value is not zero.")))

(defun bf--help-summerize-doc (sym doc)
  (concat sym " : " (car (split-string doc "[\n\r]+"))))

(defun bf-help-minibuffer-help-string ()
  (interactive)
  (let* ((sym (bf--help-sym-called-at-point))
         (doc (when sym (bf--help-lookup-doc sym))))
    (when doc (bf--help-summerize-doc sym doc))))

(defun bf--help-doc-fun ()
  (make-local-variable 'eldoc-documentation-function)
  (setq eldoc-documentation-function
        'bf-help-minibuffer-help-string))

自分の現在のカーソルの文字を取得し、それにあたるdocを取得、 eldoc に流し込むように実装しました。

インタープリタと実行処理

(defun bf-interpreter (input)
  (interactive)
  (let* ((input-list (-map #'char-to-string (coerce input 'list)))
         (ptr 0)
         (mem (make-vector 30000 0))
         (braces (make-vector (length input-list) 0))
         (braces-stack '()))
    (dotimes (outer (length input-list))
      (if (string-equal (nth outer input-list) "[")
          (let ((cnt 0))
            (progn
              (do ((inner 0 (1+ inner)))
                  ((< (length (nthcdr outer input-list)) inner))
                (cond ((string-equal (nth (+ outer inner) input-list) "[") (push t braces-stack))
                      ((string-equal (nth (+ outer inner) input-list) "]") (pop braces-stack)))
                (if (zerop (length braces-stack))
                    (setq inner (length (nthcdr outer input-list)))
                    (incf cnt)))
              (aset braces outer (+ outer cnt))
              (aset braces (+ outer cnt) outer)))))
    (do ((index 0 (1+ index)))
        ((< (length input-list) index))
      (pcase (nth index input-list)
        (">" (incf ptr))
        ("<" (decf ptr))
        ("+" (aset mem ptr (incf (aref mem ptr))))
        ("-" (aset mem ptr (decf (aref mem ptr))))
        ("." (princ (char-to-string (aref mem ptr))))
        ("," (aset mem ptr (read-char)))
        ("[" (if (zerop (aref mem ptr)) (setq index (incf (aref braces index)))))
        ("]" (unless (zerop (aref mem ptr)) (setq index (aref braces index))))))))

(defun bf-exec ()
  (interactive)
  (let ((str (buffer-string)))
    (bf-interpreter str)))

Loopの処理(括弧の対応)が非常に大変だったが、 JavascriptでBrainfuckのインタプリタを実装してみた。 を参考にしたらいけた。

M-x bf-exec と叩くとバッファ内の文字列を取得し、インタープリタで処理をし、結果を吐き出すようにしました。

参考サイト

終わりに

次はEmacsLispでC compilerを作りたいです。

弊社ではemacsのpluginを作れるプログラマを募集しています。

www.wantedly.com