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)
 2: 
 3: (executables
 4:  ((names     (plugin))
 5:   (libraries (bf_lib ecaml))
 6:   (preprocess (pps (ppx_here)))))
 7: 
 8: (rule (copy plugin.exe ecaml-bf.so))
 9: 
10: (alias
11:  ((name plugin)
12:   (deps (ecaml-bf.so))))
13: 
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
 2: 
 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:   Bf_lib.Program.run' ~program ~input
 7:   |> Value.of_utf8_bytes
 8: ;;
 9: 
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 Bf_lib.Program.run' 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:   Bf_lib.Program.run' ~program ~input
27: ;;
28: 
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"
43: 
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.

Footnotes:

1

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.

2

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