19 KiB
19 KiB
Elixir Integration Strategies
This document covers strategies for integrating Elixir with Emacs and org-babel, including execution modes, data conversion, and Mix project support.
Table of Contents
- Execution Modes
- One-Shot Execution
- IEx Session Management
- Mix Project Context
- Remote Shell (remsh)
- Data Type Conversion
- Error Handling
- elixir-mode Integration
- Performance Considerations
- References
Execution Modes
Overview of Execution Strategies
| Mode | Command | Use Case | Startup Time | State |
|---|---|---|---|---|
| One-shot | elixir -e |
Simple scripts | ~500ms | None |
| Script file | elixir file.exs |
Larger code | ~500ms | None |
| IEx session | iex |
Interactive | Once | Persistent |
| Mix context | iex -S mix |
Projects | Slower | Project |
| Remote shell | iex --remsh |
Production | Fast | Remote node |
Recommended Approach
For org-babel, we recommend:
- Default: Script file execution (reliable, predictable)
- With
:session: IEx via comint (persistent state) - With
:mix-project: Mix context execution
One-Shot Execution
Using elixir -e
For simple, single expressions:
(defun ob-elixir--eval-simple (code)
"Evaluate CODE using elixir -e."
(shell-command-to-string
(format "elixir -e %s"
(shell-quote-argument code))))
Pros: Simple, no temp files Cons: Limited code size, quoting issues
Using Script Files (Recommended)
More robust for complex code:
(defun ob-elixir--eval-script (code)
"Evaluate CODE using a temporary script file."
(let ((script-file (org-babel-temp-file "elixir-" ".exs")))
(with-temp-file script-file
(insert code))
(org-babel-eval
(format "elixir %s" (org-babel-process-file-name script-file))
"")))
Capturing Return Values vs Output
;; For :results output - capture stdout
(defun ob-elixir--eval-output (code)
"Execute CODE and capture stdout."
(ob-elixir--eval-script code))
;; For :results value - wrap to capture return value
(defconst ob-elixir--value-wrapper
"
_result_ = (
%s
)
IO.puts(inspect(_result_, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
"
"Wrapper to capture the return value of code.")
(defun ob-elixir--eval-value (code)
"Execute CODE and return its value."
(let ((wrapped (format ob-elixir--value-wrapper code)))
(string-trim (ob-elixir--eval-script wrapped))))
Handling Multiline Output
(defconst ob-elixir--value-wrapper-with-marker
"
_result_ = (
%s
)
IO.puts(\"__OB_ELIXIR_RESULT_START__\")
IO.puts(inspect(_result_, limit: :infinity, printable_limit: :infinity))
IO.puts(\"__OB_ELIXIR_RESULT_END__\")
"
"Wrapper with markers for reliable output parsing.")
(defun ob-elixir--extract-result (output)
"Extract result from OUTPUT between markers."
(when (string-match "__OB_ELIXIR_RESULT_START__\n\\(.*\\)\n__OB_ELIXIR_RESULT_END__"
output)
(match-string 1 output)))
IEx Session Management
Starting an IEx Session
(defvar ob-elixir-iex-buffer-name "*ob-elixir-iex*"
"Buffer name for the IEx process.")
(defvar ob-elixir-prompt-regexp "^iex\\([0-9]+\\)> "
"Regexp matching the IEx prompt.")
(defvar ob-elixir-continued-prompt-regexp "^\\.\\.\\.\\([0-9]+\\)> "
"Regexp matching the IEx continuation prompt.")
(defun ob-elixir--start-iex-session (session-name &optional params)
"Start an IEx session named SESSION-NAME."
(let* ((buffer-name (format "*ob-elixir-%s*" session-name))
(buffer (get-buffer-create buffer-name))
(mix-project (when params (cdr (assq :mix-project params))))
(iex-args (when params (cdr (assq :iex-args params)))))
(unless (comint-check-proc buffer)
(with-current-buffer buffer
;; Set up environment
(setenv "TERM" "dumb")
(setenv "IEX_WITH_WERL" nil)
;; Start the process
(apply #'make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
"iex"
nil
(append
(when mix-project
(list "-S" "mix"))
(when iex-args
(split-string iex-args))))
;; Wait for initial prompt
(ob-elixir--wait-for-prompt buffer 10)
;; Configure IEx for programmatic use
(ob-elixir--configure-iex-session buffer)))
buffer))
Configuring IEx for Non-Interactive Use
(defun ob-elixir--configure-iex-session (buffer)
"Configure IEx in BUFFER for non-interactive use."
(with-current-buffer buffer
(let ((config-commands
'("IEx.configure(colors: [enabled: false])"
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])"
"Application.put_env(:elixir, :ansi_enabled, false)")))
(dolist (cmd config-commands)
(ob-elixir--send-to-iex buffer cmd)
(ob-elixir--wait-for-prompt buffer 5)))))
Sending Code to IEx
(defvar ob-elixir-eoe-marker "__OB_ELIXIR_EOE__"
"End-of-evaluation marker.")
(defun ob-elixir--send-to-iex (buffer code)
"Send CODE to IEx process in BUFFER."
(with-current-buffer buffer
(goto-char (point-max))
(insert code)
(comint-send-input nil t)))
(defun ob-elixir--eval-in-session (session code)
"Evaluate CODE in SESSION, return output."
(let* ((buffer (ob-elixir--get-or-create-session session))
(start-marker nil))
(with-current-buffer buffer
(goto-char (point-max))
(setq start-marker (point-marker))
;; Send the code
(ob-elixir--send-to-iex buffer code)
(ob-elixir--wait-for-prompt buffer 30)
;; Send EOE marker to clearly delineate output
(ob-elixir--send-to-iex buffer (format "\"%s\"" ob-elixir-eoe-marker))
(ob-elixir--wait-for-prompt buffer 5)
;; Extract output
(ob-elixir--extract-session-output buffer start-marker))))
(defun ob-elixir--wait-for-prompt (buffer timeout)
"Wait for IEx prompt in BUFFER with TIMEOUT seconds."
(with-current-buffer buffer
(let ((end-time (+ (float-time) timeout)))
(while (and (< (float-time) end-time)
(not (ob-elixir--at-prompt-p)))
(accept-process-output (get-buffer-process buffer) 0.1)
(goto-char (point-max))))))
(defun ob-elixir--at-prompt-p ()
"Return t if point is at an IEx prompt."
(save-excursion
(beginning-of-line)
(looking-at ob-elixir-prompt-regexp)))
Cleaning Session Output
(defun ob-elixir--clean-iex-output (output)
"Clean OUTPUT from IEx session."
(let ((cleaned output))
;; Remove ANSI escape codes
(setq cleaned (ansi-color-filter-apply cleaned))
;; Remove prompts
(setq cleaned (replace-regexp-in-string
"^iex([0-9]+)> " "" cleaned))
(setq cleaned (replace-regexp-in-string
"^\\.\\.\\.([0-9]+)> " "" cleaned))
;; Remove EOE marker
(setq cleaned (replace-regexp-in-string
(regexp-quote (format "\"%s\"" ob-elixir-eoe-marker))
"" cleaned))
;; Remove trailing whitespace
(string-trim cleaned)))
Mix Project Context
Detecting Mix Projects
(defun ob-elixir--find-mix-project (dir)
"Find mix.exs file starting from DIR, searching up."
(let ((mix-file (locate-dominating-file dir "mix.exs")))
(when mix-file
(file-name-directory mix-file))))
(defun ob-elixir--in-mix-project-p ()
"Return t if current buffer is in a Mix project."
(ob-elixir--find-mix-project default-directory))
Executing in Mix Context
(defun ob-elixir--eval-with-mix (code project-dir &optional mix-env)
"Evaluate CODE in the context of Mix project at PROJECT-DIR."
(let* ((default-directory project-dir)
(script-file (org-babel-temp-file "elixir-" ".exs"))
(env-vars (when mix-env
(format "MIX_ENV=%s " mix-env))))
(with-temp-file script-file
(insert code))
(shell-command-to-string
(format "%smix run %s"
(or env-vars "")
(org-babel-process-file-name script-file)))))
Compiling Before Execution
(defun ob-elixir--ensure-compiled (project-dir)
"Ensure Mix project at PROJECT-DIR is compiled."
(let ((default-directory project-dir))
(shell-command-to-string "mix compile --force-check")))
(defun ob-elixir--eval-with-compilation (code project-dir)
"Compile and evaluate CODE in PROJECT-DIR."
(ob-elixir--ensure-compiled project-dir)
(ob-elixir--eval-with-mix code project-dir))
Using Mix Aliases
(defun ob-elixir--run-mix-task (task project-dir &optional args)
"Run Mix TASK in PROJECT-DIR with ARGS."
(let ((default-directory project-dir))
(shell-command-to-string
(format "mix %s %s" task (or args "")))))
Remote Shell (remsh)
Connecting to Running Nodes
(defun ob-elixir--start-remsh-session (node-name &optional cookie sname)
"Connect to remote Elixir node NODE-NAME.
COOKIE is the Erlang cookie (optional).
SNAME is the short name for the local node."
(let* ((local-name (or sname (format "ob_elixir_%d" (random 10000))))
(buffer-name (format "*ob-elixir-remsh-%s*" node-name))
(buffer (get-buffer-create buffer-name))
(args (append
(list "--sname" local-name)
(when cookie (list "--cookie" cookie))
(list "--remsh" node-name))))
(unless (comint-check-proc buffer)
(with-current-buffer buffer
(apply #'make-comint-in-buffer
(format "ob-elixir-remsh-%s" node-name)
buffer
"iex"
nil
args)
(ob-elixir--wait-for-prompt buffer 30)
(ob-elixir--configure-iex-session buffer)))
buffer))
Remote Evaluation
(defun ob-elixir--eval-remote (code node-name &optional cookie)
"Evaluate CODE on remote NODE-NAME."
(let ((session (ob-elixir--start-remsh-session node-name cookie)))
(ob-elixir--eval-in-session session code)))
Data Type Conversion
Elisp to Elixir
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir literal syntax."
(pcase value
('nil "nil")
('t "true")
((pred numberp) (number-to-string value))
((pred stringp) (ob-elixir--format-string value))
((pred symbolp) (ob-elixir--format-atom value))
((pred vectorp) (ob-elixir--format-tuple value))
((pred listp)
(cond
((ob-elixir--alist-p value) (ob-elixir--format-keyword-list value))
((ob-elixir--plist-p value) (ob-elixir--format-map value))
(t (ob-elixir--format-list value))))
(_ (format "%S" value))))
(defun ob-elixir--format-string (str)
"Format STR as Elixir string."
(format "\"%s\""
(replace-regexp-in-string
"\\\\" "\\\\\\\\"
(replace-regexp-in-string
"\"" "\\\\\""
(replace-regexp-in-string
"\n" "\\\\n"
str)))))
(defun ob-elixir--format-atom (sym)
"Format symbol SYM as Elixir atom."
(let ((name (symbol-name sym)))
(if (string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
(format ":%s" name)
(format ":\"%s\"" name))))
(defun ob-elixir--format-list (lst)
"Format LST as Elixir list."
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir lst ", ")))
(defun ob-elixir--format-tuple (vec)
"Format vector VEC as Elixir tuple."
(format "{%s}"
(mapconcat #'ob-elixir--elisp-to-elixir (append vec nil) ", ")))
(defun ob-elixir--format-keyword-list (alist)
"Format ALIST as Elixir keyword list."
(format "[%s]"
(mapconcat
(lambda (pair)
(format "%s: %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
alist ", ")))
(defun ob-elixir--format-map (plist)
"Format PLIST as Elixir map."
(let ((pairs '()))
(while plist
(push (format "%s => %s"
(ob-elixir--elisp-to-elixir (car plist))
(ob-elixir--elisp-to-elixir (cadr plist)))
pairs)
(setq plist (cddr plist)))
(format "%%{%s}" (mapconcat #'identity (nreverse pairs) ", "))))
Elixir to Elisp
(defun ob-elixir--parse-result (output)
"Parse Elixir OUTPUT into Elisp value."
(let ((trimmed (string-trim output)))
(cond
;; nil
((string= trimmed "nil") nil)
;; Booleans
((string= trimmed "true") t)
((string= trimmed "false") nil)
;; Numbers
((string-match-p "^-?[0-9]+$" trimmed)
(string-to-number trimmed))
((string-match-p "^-?[0-9]+\\.[0-9]+\\(e[+-]?[0-9]+\\)?$" trimmed)
(string-to-number trimmed))
;; Atoms (convert to symbols)
((string-match "^:\\([a-zA-Z_][a-zA-Z0-9_]*\\)$" trimmed)
(intern (match-string 1 trimmed)))
;; Strings
((string-match "^\"\\(.*\\)\"$" trimmed)
(match-string 1 trimmed))
;; Lists/tuples - use org-babel-script-escape
((string-match-p "^\\[.*\\]$" trimmed)
(org-babel-script-escape trimmed))
((string-match-p "^{.*}$" trimmed)
(org-babel-script-escape
(replace-regexp-in-string "^{\\(.*\\)}$" "[\\1]" trimmed)))
;; Maps - convert to alist
((string-match-p "^%{.*}$" trimmed)
(ob-elixir--parse-map trimmed))
;; Default: return as string
(t trimmed))))
(defun ob-elixir--parse-map (map-string)
"Parse Elixir MAP-STRING to alist."
;; Simplified - for complex maps, use JSON encoding
(let ((content (substring map-string 2 -1))) ; Remove %{ and }
(mapcar
(lambda (pair)
(when (string-match "\\(.+?\\) => \\(.+\\)" pair)
(cons (ob-elixir--parse-result (match-string 1 pair))
(ob-elixir--parse-result (match-string 2 pair)))))
(split-string content ", "))))
Using JSON for Complex Data
For complex nested structures, JSON is more reliable:
(defconst ob-elixir--json-wrapper
"
_result_ = (
%s
)
IO.puts(Jason.encode!(_result_))
"
"Wrapper that outputs result as JSON.")
(defun ob-elixir--eval-as-json (code)
"Evaluate CODE and parse result as JSON."
(let* ((wrapped (format ob-elixir--json-wrapper code))
(output (ob-elixir--eval-script wrapped)))
(json-read-from-string (string-trim output))))
Error Handling
Detecting Elixir Errors
(defconst ob-elixir-error-patterns
'("^\\*\\* (\\(\\w+Error\\))" ; ** (RuntimeError) ...
"^\\*\\* (\\(\\w+\\)) \\(.+\\)" ; ** (exit) ...
"^\\(CompileError\\)" ; CompileError ...
"^\\(SyntaxError\\)") ; SyntaxError ...
"Patterns matching Elixir error output.")
(defun ob-elixir--error-p (output)
"Return error info if OUTPUT contains an error, nil otherwise."
(catch 'found
(dolist (pattern ob-elixir-error-patterns)
(when (string-match pattern output)
(throw 'found (list :type (match-string 1 output)
:message output))))))
Signaling Errors to Org
(define-error 'ob-elixir-error "Elixir evaluation error")
(define-error 'ob-elixir-compile-error "Elixir compilation error" 'ob-elixir-error)
(define-error 'ob-elixir-runtime-error "Elixir runtime error" 'ob-elixir-error)
(defun ob-elixir--handle-error (output)
"Handle error in OUTPUT, signaling appropriate condition."
(when-let ((error-info (ob-elixir--error-p output)))
(let ((type (plist-get error-info :type))
(message (plist-get error-info :message)))
(cond
((member type '("CompileError" "SyntaxError" "TokenMissingError"))
(signal 'ob-elixir-compile-error (list message)))
(t
(signal 'ob-elixir-runtime-error (list message)))))))
Timeout Handling
(defcustom ob-elixir-timeout 30
"Default timeout in seconds for Elixir evaluation."
:type 'integer
:group 'ob-elixir)
(defun ob-elixir--eval-with-timeout (code timeout)
"Evaluate CODE with TIMEOUT seconds limit."
(with-timeout (timeout
(error "Elixir evaluation timed out after %d seconds" timeout))
(ob-elixir--eval-script code)))
elixir-mode Integration
Syntax Highlighting
;; Register with org-src for editing
(add-to-list 'org-src-lang-modes '("elixir" . elixir))
;; If elixir-mode isn't available, use a fallback
(unless (fboundp 'elixir-mode)
(add-to-list 'org-src-lang-modes '("elixir" . prog)))
Using elixir-mode Functions
(defun ob-elixir--format-code (code)
"Format CODE using mix format if available."
(when (and (executable-find "mix")
(> (length code) 0))
(with-temp-buffer
(insert code)
(let ((temp-file (make-temp-file "elixir-format" nil ".ex")))
(unwind-protect
(progn
(write-region (point-min) (point-max) temp-file)
(when (= 0 (call-process "mix" nil nil nil
"format" temp-file))
(erase-buffer)
(insert-file-contents temp-file)
(buffer-string)))
(delete-file temp-file))))))
Performance Considerations
Caching Compilation
(defvar ob-elixir--module-cache (make-hash-table :test 'equal)
"Cache of compiled Elixir modules.")
(defun ob-elixir--get-cached-module (code)
"Get cached module for CODE, or compile and cache."
(let ((hash (md5 code)))
(or (gethash hash ob-elixir--module-cache)
(let ((module-name (format "ObElixir_%s" (substring hash 0 8))))
(ob-elixir--compile-module module-name code)
(puthash hash module-name ob-elixir--module-cache)
module-name))))
Reusing Sessions
(defcustom ob-elixir-reuse-sessions t
"Whether to reuse sessions between evaluations.
When non-nil, sessions persist until explicitly killed."
:type 'boolean
:group 'ob-elixir)
Startup Time Optimization
;; Pre-start a session on package load (optional)
(defun ob-elixir-warm-up ()
"Pre-start an IEx session for faster first evaluation."
(interactive)
(ob-elixir--start-iex-session "warmup"))