(defgroup brainfuck nil
"Brainfuck compiler/interpreter."
:prefix "brainfuck-"
:group 'programming)
(defconst brainfuck-data-size
10000
"Initial program data side in bytes.")
(defconst brainfuck-keywords-commands-regexp
"[][><+-.,]"
"Brainfuck commands.")
(defconst brainfuck-keywords-comments-regexp
"[^][><+-.,]"
"Brainfuck comments.")
(defconst brainfuck-keywords-commands
'(?> ?< ?+ ?- ?. ?, ?\[ ?\])
"Brainfuck commands.")
(defconst brainfuck-mode-font-lock-keywords
`(("[><]" 0 font-lock-builtin-face)
("[+-]" 0 nil)
("[.,]" 0 font-lock-keyword-face)
("[][]" 0 font-lock-variable-name-face)
(,brainfuck-keywords-comments-regexp 0 font-lock-comment-face))
"Brainfuck keywords used by font-lock.")
(defconst brainfuck-output-buffer-name
"*brainfuck-output*"
"Buffer name to use for Brainfuck output.")
(defvar brainfuck-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-i") 'brainfuck-interpret-run)
(define-key map (kbd "C-c C-c") 'brainfuck-run)
(define-key map (kbd "C-c C-r") 'brainfuck-run)
map)
"Mode map used for `brainfuck-mode'.")
(defun brainfuck-mode ()
"Major mode to edit, compile, and execute Brainfuck programs."
(interactive)
(kill-all-local-variables)
(setq major-mode 'brainfuck-mode)
(setq mode-name "Brainfuck")
(use-local-map brainfuck-mode-map)
(setq font-lock-defaults '((brainfuck-mode-font-lock-keywords) nil t))
(setq case-fold-search t)
(run-hooks 'brainfuck-mode-hook))
(defvar brainfuck-parse-grammar-char-type
`(,(cons (string-to-char ">") :greater-than)
,(cons (string-to-char "<") :less-than)
,(cons (string-to-char "+") :plus)
,(cons (string-to-char "-") :minus)
,(cons (string-to-char ".") :period)
,(cons (string-to-char ",") :comma)
,(cons (string-to-char "[") :left-bracket)
,(cons (string-to-char "]") :right-bracket))
"List of characters and their types.")
(defvar brainfuck-parse-grammar-char-type-hash
(make-hash-table)
"Map of characters to their types.")
(dolist (item brainfuck-parse-grammar-char-type)
(setf (gethash (car item) brainfuck-parse-grammar-char-type-hash)
(cdr item)))
(defvar brainfuck-parse-grammar-type-char-hash
(make-hash-table)
"Map of types to their characters.")
(dolist (item brainfuck-parse-grammar-char-type)
(let ((entry (gethash (cdr item) brainfuck-parse-grammar-type-char-hash)))
(if entry
(if (listp entry)
(setf (gethash (cdr item) brainfuck-parse-grammar-type-char-hash)
(append entry (list (car item))))
(setf (gethash (cdr item) brainfuck-parse-grammar-type-char-hash)
(list entry (car item))))
(setf (gethash (cdr item) brainfuck-parse-grammar-type-char-hash)
(car item)))))
(defun brainfuck-parse-statement (statement)
"Parse Brainfuck STATEMENT into a list of tokens."
(let (tokens current (jump 0)) (cl-loop for char across statement
do (let ((type (gethash char brainfuck-parse-grammar-char-type-hash)))
(when type
(cond ((eq type :left-bracket)
(setq jump (1+ jump)))
((eq type :right-bracket)
(setq jump (1- jump))))
(push type tokens)
(when (< jump 0)
(error "Parsed ']' command without a matching '['")))))
(when (> jump 0)
(error "Parsed '[' command without a matching ']'"))
(nreverse tokens)))
(defun brainfuck-parse (&optional start end)
"Parse current buffer or region and return a list of tokens."
(let ((start (or start (point-min)))
(end (or end (point-max))))
(brainfuck-parse-statement (buffer-substring-no-properties start end))))
(defun brainfuck-interpret (tokens &optional input)
"Interpret TOKENS with optional INPUT and run Brainfuck program."
(let ((data (make-vector brainfuck-data-size 0)) (data-pointer 0) jump-stack output (input (if (stringp input) (nconc (string-to-list input) (list -1))
input)))
(while tokens
(let ((token (pop tokens)))
(cond ((eq token :greater-than) (cl-incf data-pointer)
(when (= data-pointer (length data))
(setq data (vconcat data (make-vector brainfuck-data-size 0)))))
((eq token :less-than) (cl-decf data-pointer)
(when (minusp data-pointer)
(error "Data pointer cannot go below zero")))
((eq token :plus) (cl-incf (aref data data-pointer)))
((eq token :minus) (cl-decf (aref data data-pointer)))
((eq token :period) (push (aref data data-pointer) output))
((eq token :comma) (aset data data-pointer
(progn
(unless input (setq input (string-to-list (read-from-minibuffer "? "))))
(if input
(pop input) -1)))) ((eq token :left-bracket) (if (zerop (aref data data-pointer))
(let ((js 1))
(while (plusp js)
(setq token (pop tokens))
(cond ((eq token :left-bracket)
(cl-incf js))
((eq token :right-bracket)
(cl-decf js)))))
(push (cons token tokens) jump-stack)))
((eq token :right-bracket) (setq tokens (pop jump-stack))))))
(concat (nreverse output))))
(defun brainfuck-interpret-run (&optional input buffer)
"Interpret a Brainfuck program in BUFFER with optional INPUT.
\nBUFFER defaults to `current-buffer'."
(interactive)
(let ((buffer (or buffer (current-buffer)))
output-buffer)
(save-current-buffer
(set-buffer buffer)
(condition-case err
(let ((output-buffer (get-buffer-create brainfuck-output-buffer-name)) (tokens (brainfuck-parse))) (delete-other-windows)
(split-window-vertically)
(other-window 1)
(set-buffer output-buffer)
(switch-to-buffer output-buffer)
(setq buffer-read-only nil)
(erase-buffer)
(condition-case err
(insert (brainfuck-interpret tokens input))
('error
(insert (format "Compiler Error: %s" err)))))
('error
(insert (format "Parser Error: %s" err)))
(setq buffer-read-only t)))))
(defun brainfuck-compile (tokens)
"Compile TOKENS and return a block of Emacs Lisp code."
(cl-labels ((while-loop ()
(let (done code)
(while (and (not done) tokens) (let ((token (pop tokens)))
(cond ((eq token :greater-than)
(push `(funcall data-pointer-incr) code))
((eq token :less-than)
(push `(funcall data-pointer-decr) code))
((eq token :plus)
(push `(funcall data-incr) code))
((eq token :minus)
(push `(funcall data-decr) code))
((eq token :period)
(push `(funcall data-output) code))
((eq token :comma)
(push `(funcall data-input) code))
((eq token :left-bracket) (push `(while (not (zerop (aref data data-pointer))) ,@(while-loop)) code))
((eq token :right-bracket) (setq done t)))))
(nreverse code))))
`(lambda (&optional input)
(let ((data (make-vector brainfuck-data-size 0)) (data-pointer 0) jump-stack output (input (if (stringp input) (nconc (string-to-list input) (list -1))
input)))
(let ((data-pointer-incr (lambda ()
(cl-incf data-pointer)
(when (= data-pointer (length data))
(setq data (vconcat data (make-vector brainfuck-data-size 0))))))
(data-pointer-decr (lambda ()
(cl-decf data-pointer)
(when (minusp data-pointer)
(error "Data pointer cannot go below zero"))))
(data-incr (lambda ()
(cl-incf (aref data data-pointer))))
(data-decr (lambda ()
(cl-decf (aref data data-pointer))))
(data-output (lambda ()
(push (aref data data-pointer) output)))
(data-input (lambda ()
(aset data data-pointer
(progn
(unless input (setq input (string-to-list (read-from-minibuffer "? "))))
(if input
(pop input) -1)))))) ,@(while-loop)
(concat (nreverse output)))))))
(defun brainfuck-run (&optional input buffer)
"Compile and run a Brainfuck program in BUFFER with optional INPUT.
\nBUFFER defaults to `current-buffer'."
(interactive)
(let ((buffer (or buffer (current-buffer)))
output-buffer)
(save-current-buffer
(set-buffer buffer)
(condition-case err
(let ((output-buffer (get-buffer-create brainfuck-output-buffer-name)) (tokens (brainfuck-parse))) (delete-other-windows)
(split-window-vertically)
(other-window 1)
(set-buffer output-buffer)
(switch-to-buffer output-buffer)
(setq buffer-read-only nil)
(erase-buffer)
(condition-case err
(let ((code (brainfuck-compile tokens)))
(message "Brainfuck compiled code: %S" code)
(insert (funcall code input)))
('error
(insert (format "Compiler Error: %s" err)))))
('error
(insert (format "Parser Error: %s" err)))
(setq buffer-read-only t)))))
(provide 'brainfuck)