# 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](#execution-modes) - [One-Shot Execution](#one-shot-execution) - [IEx Session Management](#iex-session-management) - [Mix Project Context](#mix-project-context) - [Remote Shell (remsh)](#remote-shell-remsh) - [Data Type Conversion](#data-type-conversion) - [Error Handling](#error-handling) - [elixir-mode Integration](#elixir-mode-integration) - [Performance Considerations](#performance-considerations) - [References](#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: 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: ```elisp (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: ```elisp (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 ```elisp ;; 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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```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: ```elisp (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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp ;; 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 ```elisp (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 ```elisp (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 ```elisp (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 ```elisp ;; 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 - [Elixir Documentation](https://elixir-lang.org/docs.html) - [IEx Documentation](https://hexdocs.pm/iex/IEx.html) - [Mix Documentation](https://hexdocs.pm/mix/Mix.html) - [Erlang Distribution Protocol](https://www.erlang.org/doc/reference_manual/distributed.html) - [elixir-mode GitHub](https://github.com/elixir-editors/emacs-elixir) - [inf-elixir for REPL](https://github.com/J3RN/inf-elixir) - [Elixir LS (Language Server)](https://github.com/elixir-lsp/elixir-ls)