This post is part 4 of a series (prev). The full code is available on GitHub.

In previous parts, we:

  1. figured out how to compile OCaml code into an Emacs plugin
  2. wrote a brainfuck interpreter library
  3. explored features of the Ecaml library

Now, it’s time to put everything together. Our final plugin will provide us a way to execute brainfuck code in a buffer without leaving Emacs, and with the benefit of the type safety and performance of OCaml.

1 Setup

First, we need to modify our jbuild file from part 1.

 1: (jbuild_version 1)
 3: (executables
 4:  ((names     (plugin))
 5:   (libraries (bf_lib ecaml))
 6:   (preprocess (pps (ppx_here)))))
 8: (rule (copy plugin.exe
10: (alias
11:  ((name plugin)
12:   (deps (
14: (alias
15:  ((name runtest)
16:   (deps ((alias plugin)))
17:   (action (run emacs -Q -L . --batch --eval "(require 'ecaml-bf)"))))

Only the executables rule was modified, adding bf_lib (our interpreter library) to the list of required libraries and adding a preprocess rule for ppx_here, which provides the [%here] syntax we’ll be using with defun.

2 Plugin

2.1 Basic function

To begin with, we’re just going to allow Emacs to evaluate brainfuck code through our interpreter, non-interactively (i.e., from Lisp code). We’ll define a function named bf-eval that just wraps our library code:

 1: open Ecaml
 3: let eval_program program input =
 4:   let program = Value.to_utf8_bytes_exn program in
 5:   let input   = Value.to_utf8_bytes_exn input   in
 6:' ~program ~input
 7:   |> Value.of_utf8_bytes
 8: ;;
10: let () =
11:   defun [%here] (Symbol.intern "bf-eval")
12:     ~docstring:"evaluate [program], a brainfuck program, given [input]"
13:     ~args:[ Symbol.intern "program"
14:           ; Symbol.intern "input" ]
15:     (function
16:       | [| program; input |] -> eval_program program input
17:       | _ -> invalid_arg "wrong arity")
18: ;;

I split up the definition into two parts. eval_program wraps' and does the work of converting Emacs values (in this case, strings) into OCaml values and vice versa.

The second part calls defun to register the function with Emacs, providing a docstring and argument names and checking the number of arguments passed.

We can try running it:

jbuilder build @plugin
emacs -Q -L _build/default/src --batch --eval "(require 'ecaml-bf)" \
      --eval '(print (bf-eval ",+." "A"))'
# B

The program ,+. simply inputs a byte, increments it, and prints the result, so it transforms "A" into "B".

If we load the plugin into a normal (non-batch mode) Emacs, we wouldn’t be able to call the function as an M-x command, since we haven’t marked it as an interactive command yet.

2.2 Interacting with Emacs

Next, let’s write a function that evaluates the brainfuck code in the current buffer. bf-eval-buffer will be an interactive command, so it can be called via M-x bf-eval-buffer and will prompt the user for a string to serve as the input to the program.

20: let eval_current_buffer input =
21:   let program =
22:     Current_buffer.contents ()
23:     |> Text.to_utf8_bytes
24:   in
25:   let input = Value.to_utf8_bytes_exn input in
26:' ~program ~input
27: ;;
29: let () =
30:   defun [%here] (Symbol.intern "bf-eval-buffer")
31:     ~docstring:"evaluate the current buffer as brainfuck code"
32:     ~interactive:"sInput: "
33:     ~args:[ Symbol.intern "input" ]
34:     (function
35:       | [| input |] ->
36:         let output = eval_current_buffer input in
37:         messagef "Output: %s" output;
38:         Value.nil
39:       | _ -> invalid_arg "wrong arity")
40: ;;

The argument to interactive, "sInput: ", causes the command to prompt the user for a string as its first argument (the minibuffer will contain the prompt Input:).

2.3 Major mode and key bindings

Next, we’ll define a major mode for this new feature, as well as a key binding so that users can easily access the command without using M-x.

42: let mode_name = Symbol.intern "bf-mode"
44: let () =
45:   (* define [bf-mode] as a major mode *)
46:   let mode =
47:     Major_mode.define_derived_mode ~parent:Major_mode.fundamental
48:       [%here]
49:       ~change_command:mode_name
50:       ~docstring:"Major mode for interacting with brainfuck code."
51:       ~initialize:ignore
52:       ~mode_line:"brainfuck"
53:   in
54:   (* bind [C-x C-e] to [bf-eval-buffer] *)
55:   let keymap = Major_mode.keymap mode in
56:   Keymap.define_key keymap (Key_sequence.create_exn "C-x C-e")
57:     (Command (Command.of_value_exn (Value.intern "bf-eval-buffer")))
58: ;;

Our major mode doesn’t need anything to be initialized, so we simply pass ignore to that argument. change_command determines the name of the mode, i.e., the name of the command that sets the major mode, in this case, bf-mode. docstring provides the documentation that is displayed by describe-mode (C-h m), while mode_line sets the display name of the mode that appears, unsurprisingly, in the mode line.

Additionally, we bind the sequence C-x C-e to bf-eval-buffer in the keymap of our major mode, so that when the mode is activated, pressing that key sequence will trigger the command bf-eval=buffer.

2.4 Automatically starting bf-mode

Ecaml’s Auto_mode_alist module provides a clean interface for manipulating the auto-mode-alist variable, which determines how Emacs decides which major mode to use when you open a file, like starting c-mode when you open a file named foo.c. We’ll register bf-mode for *.b files:

60: let () =
61:   (* automatically start [bf-mode] upon opening a [*.b] file *)
62:   Auto_mode_alist.add Auto_mode_alist.Entry.(
63:     [ { delete_suffix_and_recur = false
64:       ; filename_match = Regexp.of_pattern "\\.b\\'"
65:       ; function_ = Some mode_name } ])
66: ;;

The delete_suffix_and_recur field allows an entry to defer to another mode when the file may have more than one extension.1 filename_match specifies the regular expression that determines which filenames activate the major mode. Finally, function_ specifies what mode to activate when the file name matches filename_match2.

2.5 Finishing up

Last but not least, we declare to Emacs that our plugin is done initializing and provides the feature ecaml-bf that it so kindly requested:

68: let () = provide (Symbol.intern "ecaml-bf")

3 Demo

(You may need to open the video in full screen in order to read anything, sorry!)

4 Hooray!!!

jbuilder build @plugin
emacs -Q -L _build/default/src --eval "(require 'ecaml-bf)"

Try it out for yourself!

All of the code’s on GitHub. Bug reports and patches welcome.



For example, you could have an entry that is activated by opening a file with a .gz suffix, such as foo.c.gz. It might decompress the file and then recursively activate the correct major mode based on the remainder of the filename.


It actually doesn’t have to be a mode change command; it can just be any function.