Haunt can handle org-mode source blocks too! Kinda.

Tags:

One of the things I miss now that my blog has moved to Guile Haunt is the lack of source blocks. This post is a proof of concept for how I may go about handling them in the future.

For those who aren't aware, org-mode source blocks are a way for you to embed executable code and their result(s) in org-mode documents. They have a lot of features, but a basic source block would be

#+begin_src C
  printf("Hello world!\n");
#+end_src

#+RESULTS:
: Hello world!

I would talk about the implementation more, but the code is simple enough that I'll let it do the talking. If you're not familiar with SXML, the 12 word summary is "it's HTML, but instead of <p> it's (p and instead of </p> it's )".

Here is a barebones code block showing how by treating code as data, we can both output the code as a string and evaluate it.

(+ 1 1)
Results:
2

Interestingly, this method can work on itself. Here is (most of) the code used to make it happen.

(begin
  (define* (sxml-code-block
            code
            #:key
            (evaluator
              (cut eval <> (interaction-environment)))
            (lexer lex-scheme)
            (raw? #f))
    (let ((fmt (if raw? "~a" "~y")))
      `(div (@ (class "code-block-maybe-results"))
            (pre (code ,(highlights->sxml
                          (highlight lexer (format #f fmt code)))))
            ,@(if evaluator
                `("Results:"
                  (pre (code ,(format #f fmt (evaluator code)))))
                '()))))
  (sxml-code-block '(+ 1 1)))
Results:
(div (@ (class "code-block-maybe-results"))
     (pre (code ((span (@ (class "syntax-open")) "(")
                 (span (@ (class "syntax-symbol")) "+")
                 " "
                 (span (@ (class "syntax-symbol")) "1")
                 " "
                 (span (@ (class "syntax-symbol")) "1")
                 (span (@ (class "syntax-close")) ")")
                 "\n")))
     "Results:"
     (pre (code "2\n")))

One major disadvantage of this approach is that Scheme sexps do not preserve the input formatting. For example, '(foo bar) and '(foo       bar) are identical as soon as Scheme starts interpreting them. At present I am relying on Guile's "pretty-printer" to do the heavy lifting, but ideally I'd come up with a mechanism that let's me preserve the input format. This approach also does not allow for comments.

We can work around these issues by performing Fly Evaluation with eval-string instead of eval

(+ 1 ;ooooh a comment. Pretty.
   1)
Results:
2

But the editing experience is... unpleasant. After all, we're no longer writing code. Instead, we're writing a string of code, stripping away all of our fancy editing tools. This kind of defeats the entire goal of "code as data". Perhaps this can be made less painful using Skribe instead of raw SXML, but in another post I will instead try going down a different path; write a macro capable of taking a raw, "unquoted" sexp and converting it into a string and before callingsxml-code-block. This shouldn't be that hard... right?

One last tidbit for the road. As written, this awkward, not great source block evaluator can work with other languages, just as long as a Scheme evaluator function is written. As a very limited proof of concept, here's some Ruby code.

puts "Ah, General Kenobi. You are a bold one.".upcase
Results:
AH, GENERAL KENOBI. YOU ARE A BOLD ONE.

The code for this was:

(begin
  (define (eval-ruby code)
    (let* ((port (open-input-pipe
                   (string-append "ruby -e '" code "'")))
           (str (read-line port)))
      (close-pipe port)
      str))
  (sxml-code-block
    "puts \"Ah, General Kenobi. You are a bold one.\".upcase"
    #:evaluator
    eval-ruby
    #:raw?
    #t))

Now from here on out, every time I need to build my blog I will spin up a Ruby VM just to capitalize a string. Do you think God stays in heaven because he too lives in fear of what he's created?