Files
ob-elixir/tasks/02-basic-execution.md

6.2 KiB

Task 02: Basic Code Execution

Phase: 1 - Core (MVP) Priority: Critical Estimated Time: 1-2 hours Dependencies: Task 01 (Project Setup)

Objective

Implement the core org-babel-execute:elixir function that can execute Elixir code blocks using external process (one-shot execution).

Prerequisites

  • Task 01 completed
  • Elixir installed and accessible via elixir command

Steps

Step 1: Add customization group and variables

Add to ob-elixir.el:

;;; 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.
Can be a full path or command name if in PATH."
  :type 'string
  :group 'ob-elixir
  :safe #'stringp)

Step 2: Add default header arguments

;;; Header Arguments

(defvar org-babel-default-header-args:elixir
  '((:results . "value")
    (:session . "none"))
  "Default header arguments for Elixir code blocks.")

Step 3: Register the language

;;; Language Registration

;; File extension for tangling
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))

;; Associate with elixir-mode for syntax highlighting (if available)
(with-eval-after-load 'org-src
  (add-to-list 'org-src-lang-modes '("elixir" . elixir)))

Step 4: Implement the execute function

;;; Execution

(defun org-babel-execute:elixir (body params)
  "Execute a block of Elixir code with org-babel.

BODY is the Elixir code to execute.
PARAMS is an alist of header arguments.

This function is called by `org-babel-execute-src-block'."
  (let* ((result-type (cdr (assq :result-type params)))
         (result-params (cdr (assq :result-params params)))
         (result (ob-elixir--execute 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))))))

Step 5: Implement the internal execute function

(defun ob-elixir--execute (body result-type)
  "Execute BODY as Elixir code.

RESULT-TYPE is either `value' or `output'.
For `value', wraps code to capture return value.
For `output', captures stdout directly.

Returns the result as a string."
  (let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
         (code (if (eq result-type 'value)
                   (ob-elixir--wrap-for-value body)
                 body)))
    (with-temp-file tmp-file
      (insert code))
    (let ((result (org-babel-eval
                   (format "%s %s"
                           ob-elixir-command
                           (org-babel-process-file-name tmp-file))
                   "")))
      (string-trim result))))

Step 6: Implement the value wrapper

(defconst ob-elixir--value-wrapper
  "result = (
%s
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
"
  "Wrapper template for capturing Elixir expression value.
%s is replaced with the user's code.")

(defun ob-elixir--wrap-for-value (body)
  "Wrap BODY to capture its return value.

The wrapper evaluates BODY, then prints the result using
`inspect/2` with infinite limits to avoid truncation."
  (format ob-elixir--value-wrapper body))

Step 7: Add tests

Add to test/test-ob-elixir.el:

(ert-deftest ob-elixir-test-elixir-available ()
  "Test that Elixir is available."
  (should (executable-find ob-elixir-command)))

(ert-deftest ob-elixir-test-simple-value ()
  "Test simple value evaluation."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--execute "1 + 1" 'value)))
    (should (equal "2" result))))

(ert-deftest ob-elixir-test-simple-output ()
  "Test simple output evaluation."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--execute "IO.puts(\"hello\")" 'output)))
    (should (equal "hello" result))))

(ert-deftest ob-elixir-test-multiline-value ()
  "Test multiline code value evaluation."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--execute "x = 10\ny = 20\nx + y" 'value)))
    (should (equal "30" result))))

(ert-deftest ob-elixir-test-list-result ()
  "Test list result."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--execute "[1, 2, 3]" 'value)))
    (should (equal "[1, 2, 3]" result))))

(ert-deftest ob-elixir-test-map-result ()
  "Test map result."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
    (should (string-match-p "%{a: 1, b: 2}" result))))

Step 8: Test in an org buffer

Create a test org file test.org:

* Test ob-elixir

** Basic arithmetic (value)

#+BEGIN_SRC elixir
1 + 1
#+END_SRC

** Output test

#+BEGIN_SRC elixir :results output
IO.puts("Hello, World!")
#+END_SRC

** List manipulation

#+BEGIN_SRC elixir
Enum.map([1, 2, 3], fn x -> x * 2 end)
#+END_SRC

Press C-c C-c on each block to test.

Acceptance Criteria

  • org-babel-execute:elixir function exists
  • Simple expressions evaluate correctly: 1 + 1 returns 2
  • :results value captures return value (default)
  • :results output captures stdout
  • Multiline code executes correctly
  • Lists and maps are returned in Elixir format
  • All tests pass: make test

Troubleshooting

"Cannot find elixir"

Ensure Elixir is in PATH:

which elixir
elixir --version

Or set the full path:

(setq ob-elixir-command "/usr/local/bin/elixir")

Results are truncated

The wrapper uses limit: :infinity to prevent truncation. If still truncated, check for very large outputs.

ANSI codes in output

We'll handle this in a later task. For now, output should be clean with the current approach.

Files Modified

  • ob-elixir.el - Add execution functions
  • test/test-ob-elixir.el - Add execution tests

References