Files
ob-elixir/docs/04-elixir-integration-strategies.md

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

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

For org-babel, we recommend:

  1. Default: Script file execution (reliable, predictable)
  2. With :session: IEx via comint (persistent state)
  3. 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

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"))

References