21 KiB
Org Babel Language Implementation Guide
This document provides a comprehensive guide to implementing org-babel support for a new programming language, specifically targeting Elixir.
Table of Contents
- Architecture Overview
- Required Components
- Optional Components
- Header Arguments
- Result Handling
- Session Management
- Variable Handling
- Utility Functions
- Complete Implementation Template
- References
Architecture Overview
How Org Babel Executes Code
User presses C-c C-c on source block
│
▼
┌─────────────────────────────┐
│ org-babel-execute-src-block │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Parse header arguments │
│ Get block body │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ org-babel-execute:LANG │ ◄── Your implementation
│ (language-specific) │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Process and format results │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Insert results in buffer │
└─────────────────────────────┘
Source Block Structure
#+NAME: block-name
#+HEADER: :var x=5
#+BEGIN_SRC elixir :results value :session my-session
# This is the body
x * 2
#+END_SRC
#+RESULTS: block-name
: 10
Key Data Structures
params alist - Association list passed to execute function:
((:results . "value")
(:session . "my-session")
(:var . ("x" . 5))
(:var . ("y" . 10))
(:colnames . no)
(:rownames . no)
(:result-params . ("value" "replace"))
(:result-type . value)
...)
Required Components
1. The Execute Function
This is the only strictly required function. It must be named exactly org-babel-execute:LANG:
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel.
BODY is the code to execute.
PARAMS is an alist of header arguments.
This function is called by `org-babel-execute-src-block'."
(let* ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params))))
;; Execute and return results
(org-babel-reassemble-table
(org-babel-result-cond result-params
;; For :results output - return raw output
(ob-elixir--execute full-body session)
;; For :results value - parse as elisp data
(ob-elixir--table-or-string
(ob-elixir--execute full-body session)))
(org-babel-pick-name (cdr (assq :colname-names params))
(cdr (assq :colnames params)))
(org-babel-pick-name (cdr (assq :rowname-names params))
(cdr (assq :rownames params))))))
2. Default Header Arguments
Define language-specific defaults:
(defvar org-babel-default-header-args:elixir
'((:results . "value")
(:session . "none"))
"Default header arguments for Elixir code blocks.")
3. Language Registration
Enable the language for evaluation:
;; Add to tangle extensions (for org-babel-tangle)
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
;; Add to language mode mapping (for syntax highlighting)
(add-to-list 'org-src-lang-modes '("elixir" . elixir))
Optional Components
Language-Specific Header Arguments
Define what header arguments your language supports:
(defconst org-babel-header-args:elixir
'((mix-project . :any) ; Path to mix project
(mix-env . :any) ; MIX_ENV value
(iex-args . :any) ; Extra args for iex
(timeout . :any) ; Execution timeout
(async . ((yes no)))) ; Async execution
"Elixir-specific header arguments.")
Initiate Session Function
For persistent REPL sessions:
(defun org-babel-elixir-initiate-session (&optional session params)
"Create or return an Elixir session buffer.
SESSION is the session name (string or nil).
PARAMS are the header arguments.
Returns the session buffer, or nil if SESSION is \"none\"."
(unless (string= session "none")
(let ((session-name (or session "default")))
(ob-elixir--get-or-create-session session-name params))))
Prep Session Function
Prepare a session with variables before execution:
(defun org-babel-prep-session:elixir (session params)
"Prepare SESSION according to PARAMS.
Injects variables into the session before code execution."
(let ((session-buffer (org-babel-elixir-initiate-session session params))
(var-lines (org-babel-variable-assignments:elixir params)))
(when session-buffer
(org-babel-comint-in-buffer session-buffer
(dolist (var-line var-lines)
(insert var-line)
(comint-send-input nil t)
(org-babel-comint-wait-for-output session-buffer))))
session-buffer))
Load Session Function
Load code into session without executing:
(defun org-babel-load-session:elixir (session body params)
"Load BODY into SESSION without executing.
Useful for setting up definitions to be used later."
(save-window-excursion
(let ((buffer (org-babel-prep-session:elixir session params)))
(with-current-buffer buffer
(goto-char (process-mark (get-buffer-process buffer)))
(insert (org-babel-chomp body)))
buffer)))
Variable Assignments Function
Convert Elisp values to language-specific variable assignments:
(defun org-babel-variable-assignments:elixir (params)
"Return list of Elixir statements to assign variables from PARAMS."
(mapcar
(lambda (pair)
(format "%s = %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
(org-babel--get-vars params)))
Expand Body Function
Customize how the code body is expanded (optional, generic works for most):
(defun org-babel-expand-body:elixir (body params)
"Expand BODY according to PARAMS.
Add variable bindings, prologue, epilogue."
(let ((vars (org-babel-variable-assignments:elixir params))
(prologue (cdr (assq :prologue params)))
(epilogue (cdr (assq :epilogue params))))
(concat
(when prologue (concat prologue "\n"))
(mapconcat #'identity vars "\n")
(when vars "\n")
body
(when epilogue (concat "\n" epilogue)))))
Header Arguments
Standard Header Arguments (Always Available)
| Argument | Values | Description |
|---|---|---|
:results |
value, output | What to capture |
:session |
name, "none" | Session to use |
:var |
name=value | Variable binding |
:exports |
code, results, both, none | Export behavior |
:file |
path | Save results to file |
:dir |
path | Working directory |
:prologue |
code | Code to run before |
:epilogue |
code | Code to run after |
:noweb |
yes, no, tangle | Noweb expansion |
:cache |
yes, no | Cache results |
:tangle |
yes, no, filename | Tangle behavior |
Result Types
;; Accessing result configuration
(let* ((result-params (cdr (assq :result-params params)))
(result-type (cdr (assq :result-type params))))
;; result-type is 'value or 'output
;; result-params is a list like ("value" "replace" "scalar")
(cond
((eq result-type 'output)
;; Capture stdout
(ob-elixir--execute-output body))
((eq result-type 'value)
;; Return last expression value
(ob-elixir--execute-value body))))
Parsing Header Arguments
(defun ob-elixir--parse-params (params)
"Parse PARAMS into a structured format."
(let ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(dir (cdr (assq :dir params)))
(vars (org-babel--get-vars params))
;; Custom header args
(mix-project (cdr (assq :mix-project params)))
(mix-env (cdr (assq :mix-env params)))
(timeout (cdr (assq :timeout params))))
(list :session session
:result-type result-type
:result-params result-params
:dir dir
:vars vars
:mix-project mix-project
:mix-env mix-env
:timeout (or timeout ob-elixir-default-timeout))))
Result Handling
The org-babel-result-cond Macro
This handles result formatting based on :results header:
(org-babel-result-cond result-params
;; This form is used for :results output/scalar/verbatim
(ob-elixir--raw-output body)
;; This form is used for :results value (default)
;; Should return elisp data for possible table conversion
(ob-elixir--parsed-value body))
Converting Results to Tables
(defun ob-elixir--table-or-string (result)
"Convert RESULT to an Emacs table or string.
If RESULT looks like a list/table, parse it.
Otherwise return as string."
(let ((res (org-babel-script-escape result)))
(if (listp res)
(mapcar (lambda (el)
(if (eq el 'nil)
org-babel-elixir-nil-to
el))
res)
res)))
(defvar org-babel-elixir-nil-to 'hline
"Value to use for nil in Elixir results.
When nil values appear in lists/tables, convert to this.")
Using org-babel-reassemble-table
Restore column/row names to tables:
(org-babel-reassemble-table
result ; The result data
(org-babel-pick-name ; Column names
(cdr (assq :colname-names params))
(cdr (assq :colnames params)))
(org-babel-pick-name ; Row names
(cdr (assq :rowname-names params))
(cdr (assq :rownames params))))
File Results
For :results file:
(defun ob-elixir--handle-file-result (result params)
"Handle file output for RESULT based on PARAMS."
(let ((file (cdr (assq :file params))))
(when file
(with-temp-file file
(insert result))
file))) ; Return filename for link creation
Session Management
Using Comint for Sessions
Org-babel provides utilities for managing comint-based REPLs:
(require 'ob-comint)
(defvar ob-elixir-prompt-regexp "^iex([0-9]+)> "
"Regexp matching the IEx prompt.")
(defun ob-elixir--get-or-create-session (name params)
"Get or create an IEx session named NAME."
(let ((buffer-name (format "*ob-elixir-%s*" name)))
(if (org-babel-comint-buffer-livep (get-buffer buffer-name))
(get-buffer buffer-name)
(ob-elixir--start-session buffer-name params))))
(defun ob-elixir--start-session (buffer-name params)
"Start a new IEx session in BUFFER-NAME."
(let* ((iex-cmd (or (cdr (assq :iex-command params)) "iex"))
(mix-project (cdr (assq :mix-project params)))
(cmd (if mix-project
(format "cd %s && iex -S mix"
(shell-quote-argument mix-project))
iex-cmd)))
(with-current-buffer (get-buffer-create buffer-name)
(make-comint-in-buffer "ob-elixir" (current-buffer)
shell-file-name nil "-c" cmd)
;; Wait for prompt
(sit-for 1)
;; Configure IEx for non-interactive use
(ob-elixir--configure-session (current-buffer))
(current-buffer))))
(defun ob-elixir--configure-session (buffer)
"Configure IEx session in BUFFER for programmatic use."
(org-babel-comint-in-buffer buffer
(insert "IEx.configure(colors: [enabled: false])")
(comint-send-input nil t)
(org-babel-comint-wait-for-output buffer)))
Evaluating in Session
(defun ob-elixir--evaluate-in-session (session body)
"Evaluate BODY in SESSION buffer, return output."
(let ((session-buffer (ob-elixir--get-or-create-session session nil))
(eoe-indicator ob-elixir-eoe-indicator))
(org-babel-comint-with-output
(session-buffer ob-elixir-prompt-regexp t eoe-indicator)
(insert body)
(comint-send-input nil t)
;; Send EOE marker
(insert (format "\"%s\"" eoe-indicator))
(comint-send-input nil t))))
Key Comint Utilities
;; Check if buffer has live process
(org-babel-comint-buffer-livep buffer)
;; Execute forms in comint buffer context
(org-babel-comint-in-buffer buffer
(insert "code")
(comint-send-input))
;; Wait for output with timeout
(org-babel-comint-wait-for-output buffer)
;; Capture output until EOE marker
(org-babel-comint-with-output (buffer prompt t eoe-marker)
body...)
Variable Handling
Getting Variables from Params
;; org-babel--get-vars returns list of (name . value) pairs
(let ((vars (org-babel--get-vars params)))
;; vars = (("x" . 5) ("y" . "hello") ("data" . ((1 2) (3 4))))
(dolist (var vars)
(let ((name (car var))
(value (cdr var)))
;; Process each variable
)))
Converting Elisp to Elixir
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir literal syntax."
(cond
;; nil -> nil
((null value) "nil")
;; t -> true
((eq value t) "true")
;; Numbers stay as-is
((numberp value) (number-to-string value))
;; Strings get quoted
((stringp value) (format "\"%s\"" (ob-elixir--escape-string value)))
;; Symbols become atoms
((symbolp value) (format ":%s" (symbol-name value)))
;; Lists become Elixir lists
((listp value)
(if (ob-elixir--alist-p value)
;; Association list -> keyword list or map
(ob-elixir--alist-to-elixir value)
;; Regular list
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir value ", "))))
;; Fallback: convert to string
(t (format "%S" value))))
(defun ob-elixir--escape-string (str)
"Escape special characters in STR for Elixir."
(replace-regexp-in-string
"\\\\" "\\\\\\\\"
(replace-regexp-in-string
"\"" "\\\\\""
(replace-regexp-in-string
"\n" "\\\\n" str))))
(defun ob-elixir--alist-p (list)
"Return t if LIST is an association list."
(and (listp list)
(cl-every (lambda (el)
(and (consp el) (atom (car el))))
list)))
(defun ob-elixir--alist-to-elixir (alist)
"Convert ALIST to Elixir keyword list or map."
(format "[%s]"
(mapconcat
(lambda (pair)
(format "%s: %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
alist
", ")))
Handling Tables (hlines)
(defvar org-babel-elixir-hline-to ":hline"
"Elixir representation for org table hlines.")
(defun ob-elixir--table-to-elixir (table)
"Convert org TABLE to Elixir list of lists."
(format "[%s]"
(mapconcat
(lambda (row)
(if (eq row 'hline)
org-babel-elixir-hline-to
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir row ", "))))
table
", ")))
Utility Functions
From ob.el / ob-core.el
;; Execute external command with body as stdin
(org-babel-eval command body)
;; Returns stdout as string
;; Create temporary file
(org-babel-temp-file "prefix-")
;; Returns temp file path
;; Read file contents
(org-babel-eval-read-file filename)
;; Process file path for use in scripts
(org-babel-process-file-name filename)
;; Parse script output as Elisp data
(org-babel-script-escape output)
;; Parses "[1, 2, 3]" -> (1 2 3)
;; Remove trailing newlines
(org-babel-chomp string)
;; Expand generic body with vars
(org-babel-expand-body:generic body params var-lines)
Wrapper Method Pattern
For :results value, wrap code to capture return value:
(defconst ob-elixir-wrapper-method
"
result = (fn ->
%s
end).()
result
|> inspect(limit: :infinity, printable_limit: :infinity)
|> IO.puts()
"
"Template for wrapping Elixir code to capture value.
%s is replaced with the user's code.")
(defun ob-elixir--wrap-for-value (body)
"Wrap BODY to capture its return value."
(format ob-elixir-wrapper-method body))
Complete Implementation Template
Here's a minimal but functional template:
;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Your Name
;; Author: Your Name <email@example.com>
;; Version: 1.0.0
;; Package-Requires: ((emacs "27.1") (org "9.4"))
;; Keywords: literate programming, elixir
;; URL: https://github.com/user/ob-elixir
;;; Commentary:
;; Org Babel support for evaluating Elixir code blocks.
;;; Code:
(require 'ob)
(require 'ob-eval)
;;; Customization
(defgroup ob-elixir nil
"Org Babel support for Elixir."
:group 'org-babel
:prefix "ob-elixir-")
(defcustom ob-elixir-command "elixir"
"Command to execute Elixir code."
:type 'string
:group 'ob-elixir)
;;; Header Arguments
(defvar org-babel-default-header-args:elixir
'((:results . "value")
(:session . "none"))
"Default header arguments for Elixir code blocks.")
(defconst org-babel-header-args:elixir
'((mix-project . :any))
"Elixir-specific header arguments.")
;;; File Extension
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
;;; Execution
(defun org-babel-execute:elixir (body params)
"Execute Elixir BODY according to PARAMS."
(let* ((result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params)))
(result (ob-elixir--execute full-body result-type)))
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(org-babel-script-escape result))
(org-babel-pick-name (cdr (assq :colname-names params))
(cdr (assq :colnames params)))
(org-babel-pick-name (cdr (assq :rowname-names params))
(cdr (assq :rownames params))))))
(defun ob-elixir--execute (body result-type)
"Execute BODY and return result based on RESULT-TYPE."
(let* ((tmp-file (org-babel-temp-file "elixir-" ".exs"))
(wrapper (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body)))
(with-temp-file tmp-file
(insert wrapper))
(org-babel-eval
(format "%s %s" ob-elixir-command
(org-babel-process-file-name tmp-file))
"")))
(defconst ob-elixir--value-wrapper
"result = (\n%s\n)\nIO.puts(inspect(result, limit: :infinity))"
"Wrapper to capture return value.")
(defun ob-elixir--wrap-for-value (body)
"Wrap BODY to capture its return value."
(format ob-elixir--value-wrapper body))
;;; Variables
(defun org-babel-variable-assignments:elixir (params)
"Return Elixir code to assign variables from PARAMS."
(mapcar
(lambda (pair)
(format "%s = %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
(org-babel--get-vars params)))
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir syntax."
(cond
((null value) "nil")
((eq value t) "true")
((numberp value) (number-to-string value))
((stringp value) (format "\"%s\"" value))
((symbolp value) (format ":%s" (symbol-name value)))
((listp value)
(format "[%s]" (mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
(t (format "%S" value))))
(provide 'ob-elixir)
;;; ob-elixir.el ends here