Files
ob-elixir/tasks/13-module-definition-blocks.md

28 KiB

Task 13: Add Module Definition Blocks Support

Problem

When working with Elixir in org-mode, users often want to define reusable functions that can be called from multiple code blocks. Currently, users must either:

  1. Define full defmodule wrappers in each block
  2. Use sessions and manually manage module definitions
  3. Duplicate function code across blocks

This is cumbersome and doesn't align with the literate programming paradigm where functions should be defined once and used throughout the document.

Desired Behavior

Users can define Elixir functions in code blocks with a :module header argument. These blocks:

  1. Define functions but don't execute any code
  2. Merge when multiple blocks share the same module name
  3. Are automatically wrapped in defmodule when a regular block is executed
  4. Require explicit imports via the #+BEGIN_IMPORTS elixir block to use

Example

#+BEGIN_SRC elixir :module Helpers
def greet(name), do: "Hello, #{name}!"
def double(x), do: x * 2
#+END_SRC

#+BEGIN_SRC elixir :module Helpers
# This merges with the above block
def triple(x), do: x * 3
#+END_SRC

#+BEGIN_SRC elixir :module Math
def square(x), do: x * x
@moduledoc "Math utilities"
#+END_SRC

#+BEGIN_IMPORTS elixir
import Helpers
import Math
#+END_IMPORTS

#+BEGIN_SRC elixir
greet("World") <> " - " <> to_string(square(5))
#+END_SRC

When the last block executes, the generated code is:

defmodule Helpers do
  def greet(name), do: "Hello, #{name}!"
  def double(x), do: x * 2
  def triple(x), do: x * 3
end

defmodule Math do
  def square(x), do: x * x
  @moduledoc "Math utilities"
end

import Helpers
import Math

result = (
greet("World") <> " - " <> to_string(square(5))
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))

Scope

  • :module ModuleName header marks a block as a function definition block
  • Definition blocks don't execute - they return a confirmation message
  • Multiple blocks with the same :module value have their bodies merged
  • Only blocks before the current position are gathered (consistent with imports/deps)
  • Works with both session and non-session modes
  • Works alongside deps blocks and imports blocks
  • Supports all Elixir module constructs (def, defp, @moduledoc, use, etc.)

Implementation Plan

Step 1: Add function to find all module definition blocks

File: ob-elixir.el (after ob-elixir--find-imports-for-position, around line 453)

Add: A function to scan the buffer for all elixir src blocks with :module header:

(defun ob-elixir--find-all-module-blocks (pos)
  "Find all module definition blocks before POS.

Scans the buffer for Elixir source blocks with a :module header argument.
Returns an alist of (MODULE-NAME . BODY-STRING) with merged bodies
for blocks sharing the same module name.

Blocks are processed in document order, so later blocks with the same
module name have their content appended to earlier blocks."
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (let ((modules (make-hash-table :test 'equal)))
        (org-element-map (org-element-parse-buffer) 'src-block
          (lambda (src-block)
            (when (and (< (org-element-property :begin src-block) pos)
                       (string= (org-element-property :language src-block) "elixir"))
              (let* ((params (org-babel-parse-header-arguments
                              (or (org-element-property :parameters src-block) "")))
                     (module-name (cdr (assq :module params))))
                (when module-name
                  (let* ((body (org-element-property :value src-block))
                         (existing (gethash module-name modules "")))
                    (puthash module-name
                             (if (string-empty-p existing)
                                 (string-trim body)
                               (concat existing "\n\n" (string-trim body)))
                             modules)))))))
        ;; Convert hash table to alist
        (let (result)
          (maphash (lambda (k v) (push (cons k v) result)) modules)
          (nreverse result))))))

Step 2: Add function to generate module definitions

File: ob-elixir.el (after ob-elixir--find-all-module-blocks)

Add: A function to convert the modules alist to Elixir defmodule code:

(defun ob-elixir--generate-module-definitions (modules-alist)
  "Generate Elixir module definitions from MODULES-ALIST.

Each entry in MODULES-ALIST is (MODULE-NAME . BODY-STRING).
Returns a string with all defmodule definitions separated by blank lines,
or nil if MODULES-ALIST is empty."
  (when modules-alist
    (mapconcat
     (lambda (entry)
       (format "defmodule %s do\n%s\nend" (car entry) (cdr entry)))
     modules-alist
     "\n\n")))

Step 3: Modify org-babel-execute:elixir to detect :module blocks

File: ob-elixir.el (in org-babel-execute:elixir function, around line 946)

Change: Add early return when :module header is present:

Current code:

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

Proposed change:

(defun org-babel-execute:elixir (body params)
  "Execute a block of Elixir code with org-babel.
..."
  (let ((module-name (cdr (assq :module params))))
    (if module-name
        ;; This is a module definition block - don't execute
        (format "Module %s: functions defined" module-name)
      ;; Normal execution path
      (let* ((session (cdr (assq :session params)))
             ...))))

Step 4: Modify ob-elixir--execute to accept modules-string

File: ob-elixir.el (around line 919)

Current signature:

(defun ob-elixir--execute (body result-type &optional imports-string)

Proposed signature:

(defun ob-elixir--execute (body result-type &optional imports-string modules-string)

Changes to function body:

(defun ob-elixir--execute (body result-type &optional imports-string modules-string)
  "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.
IMPORTS-STRING, if provided, contains import/alias/require statements.
MODULES-STRING, if provided, contains module definitions to prepend.

Code is assembled in this order:
1. Module definitions (MODULES-STRING)
2. Imports (IMPORTS-STRING)
3. User code (BODY, wrapped for value if needed)

Returns the result as a string.
May signal `ob-elixir-error' if execution fails and
`ob-elixir-signal-errors' is non-nil."
  (let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
         (wrapped (if (eq result-type 'value)
                      (ob-elixir--wrap-for-value body)
                    body))
         (code (concat
                (when modules-string (concat (string-trim modules-string) "\n\n"))
                (when imports-string (concat (string-trim imports-string) "\n\n"))
                wrapped)))
    (with-temp-file tmp-file
      (insert code))
    (let ((result (with-temp-buffer
                    (call-process ob-elixir-command nil t nil
                                  (org-babel-process-file-name tmp-file))
                    (buffer-string))))
      (ob-elixir--process-result result))))

Step 5: Modify ob-elixir--execute-with-deps to accept modules-string

File: ob-elixir.el (around line 561)

Current signature:

(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string)

Proposed signature:

(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string modules-string)

Changes to function body:

(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string modules-string)
  "Execute BODY with dependencies from DEPS-STRING.

RESULT-TYPE is `value' or `output'.
IMPORTS-STRING, if provided, is prepended after module definitions.
MODULES-STRING, if provided, contains module definitions to prepend first."
  (let* ((project-dir (ob-elixir--get-deps-project deps-string))
         (default-directory project-dir)
         (tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
         (wrapped (if (eq result-type 'value)
                      (ob-elixir--wrap-for-value body)
                    body))
         (code (concat
                (when modules-string (concat (string-trim modules-string) "\n\n"))
                (when imports-string (concat (string-trim imports-string) "\n\n"))
                wrapped)))
    
    ;; Write code to temp file
    (with-temp-file tmp-file
      (insert code))
    
    ;; Execute with mix run
    (let ((command (format "%s run --no-compile %s 2>&1"
                           ob-elixir-mix-command
                           (shell-quote-argument tmp-file))))
      (ob-elixir--process-result
       (shell-command-to-string command)))))

Step 6: Modify ob-elixir--evaluate-in-session to accept modules-string

File: ob-elixir.el (around line 685)

Current signature:

(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string)

Proposed signature:

(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string modules-string)

Changes to function body:

(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string modules-string)
  "Evaluate BODY in SESSION, return result.

RESULT-TYPE is `value' or `output'.
IMPORTS-STRING, if provided, is prepended after module definitions.
MODULES-STRING, if provided, contains module definitions to prepend first."
  (let* ((buffer (org-babel-elixir-initiate-session session nil))
         (wrapped (if (eq result-type 'value)
                      (ob-elixir--session-wrap-for-value body)
                    body))
         (code (concat
                (when modules-string (concat (string-trim modules-string) "\n\n"))
                (when imports-string (concat (string-trim imports-string) "\n\n"))
                wrapped))
         (eoe-indicator ob-elixir--eoe-marker)
         (full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
         output)
    (unless buffer
      (error "Failed to create Elixir session: %s" session))

    (setq output
          (org-babel-comint-with-output
              (buffer eoe-indicator t full-body)
            (ob-elixir--send-command buffer full-body)))

    (ob-elixir--clean-session-output output result-type)))

Step 7: Modify ob-elixir--evaluate-in-session-with-deps to accept modules-string

File: ob-elixir.el (around line 839)

Current signature:

(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string)

Proposed signature:

(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string modules-string)

Changes to function body:

(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string modules-string)
  "Evaluate BODY in SESSION with DEPS-STRING context.

RESULT-TYPE is `value' or `output'.
IMPORTS-STRING, if provided, is prepended after module definitions.
MODULES-STRING, if provided, contains module definitions to prepend first."
  (let* ((buffer (ob-elixir--get-or-create-session-with-deps session deps-string))
         (wrapped (if (eq result-type 'value)
                      (ob-elixir--session-wrap-for-value body)
                    body))
         (code (concat
                (when modules-string (concat (string-trim modules-string) "\n\n"))
                (when imports-string (concat (string-trim imports-string) "\n\n"))
                wrapped))
         (eoe-indicator ob-elixir--eoe-marker)
         (full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
         output)
    
    (unless buffer
      (error "Failed to create Elixir session with deps: %s" session))
    
    (setq output
          (org-babel-comint-with-output
              (buffer eoe-indicator t full-body)
            (ob-elixir--send-command buffer full-body)))
    
    (ob-elixir--clean-session-output output result-type)))

Step 8: Update org-babel-execute:elixir to gather and pass modules

File: ob-elixir.el (in org-babel-execute:elixir, the normal execution path)

Changes: Add module gathering and pass to all execution functions:

(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 ((module-name (cdr (assq :module params))))
    (if module-name
        ;; This is a module definition block - don't execute
        (format "Module %s: functions defined" module-name)
      ;; Normal execution path
      (let* ((session (cdr (assq :session params)))
             (result-type (cdr (assq :result-type params)))
             (result-params (cdr (assq :result-params params)))
             ;; Find deps for this block's position
             (deps-string (ob-elixir--find-deps-for-position (point)))
             ;; Find imports for this block's position
             (imports-string (ob-elixir--find-imports-for-position (point)))
             ;; Find module definitions for this block's position
             (modules-alist (ob-elixir--find-all-module-blocks (point)))
             (modules-string (ob-elixir--generate-module-definitions modules-alist))
             ;; Expand body with variable assignments
             (full-body (org-babel-expand-body:generic
                         body params
                         (org-babel-variable-assignments:elixir params)))
             (result (condition-case err
                         (cond
                          ;; Session mode with deps
                          ((and session (not (string= session "none")) deps-string)
                           (ob-elixir--evaluate-in-session-with-deps
                            session full-body result-type deps-string imports-string modules-string))
                          ;; Session mode without deps
                          ((and session (not (string= session "none")))
                           (ob-elixir--evaluate-in-session session full-body result-type imports-string modules-string))
                          ;; Non-session with deps
                          (deps-string
                           (ob-elixir--execute-with-deps full-body result-type deps-string imports-string modules-string))
                          ;; Plain execution
                          (t
                           (ob-elixir--execute full-body result-type imports-string modules-string)))
                       (ob-elixir-error
                        ;; Return error message so it appears in buffer
                        (cadr err)))))
        (org-babel-reassemble-table
         (org-babel-result-cond result-params
           ;; For output/scalar/verbatim - return as-is
           result
           ;; For value - parse into Elisp data
           (ob-elixir--table-or-string 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 9: Create test file for module functionality

File: test/test-ob-elixir-modules.el (new file)

;;; test-ob-elixir-modules.el --- Module definition block tests -*- lexical-binding: t; -*-

;;; Commentary:

;; Tests for the module definition block functionality.
;; Tests the :module header argument and related functions.

;;; Code:

(require 'ert)
(require 'ob-elixir)
(require 'org)
(require 'org-element)

;;; Module Block Detection Tests

(ert-deftest ob-elixir-test-module-block-detection ()
  "Test that :module header argument is detected in params."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module MyModule\ndef foo, do: :bar\n#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "#+BEGIN_SRC")
    (let* ((info (org-babel-get-src-block-info))
           (params (nth 2 info))
           (module-name (cdr (assq :module params))))
      (should module-name)
      (should (string= module-name "MyModule")))))

(ert-deftest ob-elixir-test-module-block-no-execute ()
  "Test that blocks with :module don't execute Elixir code."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module TestMod\ndef foo, do: raise \"should not run\"\n#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      ;; Should return a message, not execute the code
      (should (stringp result))
      (should (string-match-p "Module.*TestMod.*defined" result)))))

;;; Module Block Gathering Tests

(ert-deftest ob-elixir-test-find-module-blocks-single ()
  "Test finding a single module block."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Helpers\ndef greet(name), do: name\n#+END_SRC\n")
    (insert "#+BEGIN_SRC elixir\n:ok\n#+END_SRC\n")
    (goto-char (point-max))
    (let ((modules (ob-elixir--find-all-module-blocks (point))))
      (should modules)
      (should (= 1 (length modules)))
      (should (string= "Helpers" (caar modules)))
      (should (string-match-p "def greet" (cdar modules))))))

(ert-deftest ob-elixir-test-find-module-blocks-multiple-same-name ()
  "Test that multiple blocks with same module name are merged."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Helpers\ndef foo, do: 1\n#+END_SRC\n")
    (insert "#+BEGIN_SRC elixir :module Helpers\ndef bar, do: 2\n#+END_SRC\n")
    (insert "#+BEGIN_SRC elixir\n:ok\n#+END_SRC\n")
    (goto-char (point-max))
    (let ((modules (ob-elixir--find-all-module-blocks (point))))
      (should modules)
      (should (= 1 (length modules)))
      (should (string= "Helpers" (caar modules)))
      (let ((body (cdar modules)))
        (should (string-match-p "def foo" body))
        (should (string-match-p "def bar" body))))))

(ert-deftest ob-elixir-test-find-module-blocks-different-names ()
  "Test finding multiple modules with different names."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module ModA\ndef a, do: 1\n#+END_SRC\n")
    (insert "#+BEGIN_SRC elixir :module ModB\ndef b, do: 2\n#+END_SRC\n")
    (insert "#+BEGIN_SRC elixir\n:ok\n#+END_SRC\n")
    (goto-char (point-max))
    (let ((modules (ob-elixir--find-all-module-blocks (point))))
      (should modules)
      (should (= 2 (length modules)))
      (should (assoc "ModA" modules))
      (should (assoc "ModB" modules)))))

(ert-deftest ob-elixir-test-find-module-blocks-position-scoped ()
  "Test that only blocks before current position are found."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Before\ndef before, do: 1\n#+END_SRC\n")
    (let ((middle-pos (point)))
      (insert "#+BEGIN_SRC elixir :module After\ndef after, do: 2\n#+END_SRC\n")
      (let ((modules (ob-elixir--find-all-module-blocks middle-pos)))
        (should modules)
        (should (= 1 (length modules)))
        (should (assoc "Before" modules))
        (should-not (assoc "After" modules))))))

(ert-deftest ob-elixir-test-find-module-blocks-no-modules ()
  "Test that nil/empty is returned when no module blocks exist."
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC\n")
    (goto-char (point-max))
    (let ((modules (ob-elixir--find-all-module-blocks (point))))
      (should (null modules)))))

;;; Module Definition Generation Tests

(ert-deftest ob-elixir-test-generate-module-definitions-single ()
  "Test generating a single module definition."
  (let* ((modules '(("MyMod" . "def foo, do: :bar")))
         (result (ob-elixir--generate-module-definitions modules)))
    (should result)
    (should (string-match-p "defmodule MyMod do" result))
    (should (string-match-p "def foo, do: :bar" result))
    (should (string-match-p "end" result))))

(ert-deftest ob-elixir-test-generate-module-definitions-multiple ()
  "Test generating multiple module definitions."
  (let* ((modules '(("ModA" . "def a, do: 1") ("ModB" . "def b, do: 2")))
         (result (ob-elixir--generate-module-definitions modules)))
    (should result)
    (should (string-match-p "defmodule ModA do" result))
    (should (string-match-p "defmodule ModB do" result))))

(ert-deftest ob-elixir-test-generate-module-definitions-empty ()
  "Test that nil is returned for empty modules list."
  (should (null (ob-elixir--generate-module-definitions nil)))
  (should (null (ob-elixir--generate-module-definitions '()))))

;;; Integration Tests

(ert-deftest ob-elixir-test-module-integration-basic ()
  "Test full integration: define module, import, call function."
  (skip-unless (executable-find ob-elixir-command))
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Helpers\n")
    (insert "def double(x), do: x * 2\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_IMPORTS elixir\n")
    (insert "import Helpers\n")
    (insert "#+END_IMPORTS\n\n")
    (insert "#+BEGIN_SRC elixir :results value\n")
    (insert "double(21)\n")
    (insert "#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "double(21)")
    (beginning-of-line)
    (search-backward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      (should result)
      (should (equal result 42)))))

(ert-deftest ob-elixir-test-module-integration-merged ()
  "Test that merged module functions are all available."
  (skip-unless (executable-find ob-elixir-command))
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Math\n")
    (insert "def double(x), do: x * 2\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_SRC elixir :module Math\n")
    (insert "def triple(x), do: x * 3\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_IMPORTS elixir\n")
    (insert "import Math\n")
    (insert "#+END_IMPORTS\n\n")
    (insert "#+BEGIN_SRC elixir :results value\n")
    (insert "double(10) + triple(10)\n")
    (insert "#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "double(10)")
    (beginning-of-line)
    (search-backward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      (should result)
      (should (equal result 50)))))

(ert-deftest ob-elixir-test-module-integration-multiple-modules ()
  "Test multiple different modules."
  (skip-unless (executable-find ob-elixir-command))
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module ModA\n")
    (insert "def value, do: 10\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_SRC elixir :module ModB\n")
    (insert "def value, do: 20\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_IMPORTS elixir\n")
    (insert "alias ModA\nalias ModB\n")
    (insert "#+END_IMPORTS\n\n")
    (insert "#+BEGIN_SRC elixir :results value\n")
    (insert "ModA.value() + ModB.value()\n")
    (insert "#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "ModA.value()")
    (beginning-of-line)
    (search-backward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      (should result)
      (should (equal result 30)))))

(ert-deftest ob-elixir-test-module-with-private-functions ()
  "Test that defp (private functions) work in modules."
  (skip-unless (executable-find ob-elixir-command))
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Helpers\n")
    (insert "def public(x), do: private_helper(x)\n")
    (insert "defp private_helper(x), do: x * 2\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_IMPORTS elixir\n")
    (insert "import Helpers\n")
    (insert "#+END_IMPORTS\n\n")
    (insert "#+BEGIN_SRC elixir :results value\n")
    (insert "public(21)\n")
    (insert "#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "public(21)")
    (beginning-of-line)
    (search-backward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      (should result)
      (should (equal result 42)))))

(ert-deftest ob-elixir-test-module-with-module-attributes ()
  "Test that module attributes work in modules."
  (skip-unless (executable-find ob-elixir-command))
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Config\n")
    (insert "@default_value 42\n")
    (insert "def get_default, do: @default_value\n")
    (insert "#+END_SRC\n\n")
    (insert "#+BEGIN_IMPORTS elixir\n")
    (insert "import Config\n")
    (insert "#+END_IMPORTS\n\n")
    (insert "#+BEGIN_SRC elixir :results value\n")
    (insert "get_default()\n")
    (insert "#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "get_default()")
    (beginning-of-line)
    (search-backward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      (should result)
      (should (equal result 42)))))

(ert-deftest ob-elixir-test-module-without-import ()
  "Test that modules are defined but require import to use directly."
  (skip-unless (executable-find ob-elixir-command))
  (with-temp-buffer
    (org-mode)
    (insert "#+BEGIN_SRC elixir :module Helpers\n")
    (insert "def greet, do: \"hello\"\n")
    (insert "#+END_SRC\n\n")
    ;; No imports block - must use full module name
    (insert "#+BEGIN_SRC elixir :results value\n")
    (insert "Helpers.greet()\n")
    (insert "#+END_SRC\n")
    (goto-char (point-min))
    (search-forward "Helpers.greet()")
    (beginning-of-line)
    (search-backward "#+BEGIN_SRC")
    (let ((result (org-babel-execute-src-block)))
      (should result)
      (should (equal result "hello")))))

(provide 'test-ob-elixir-modules)
;;; test-ob-elixir-modules.el ends here

Step 10: Update test loader

File: test/test-ob-elixir.el

Add: Require statement for the new test file (with other requires):

(require 'test-ob-elixir-modules)

Summary of Changes

File Change
ob-elixir.el Add ob-elixir--find-all-module-blocks function
ob-elixir.el Add ob-elixir--generate-module-definitions function
ob-elixir.el Modify org-babel-execute:elixir - early return for :module blocks
ob-elixir.el Modify ob-elixir--execute - add modules-string parameter
ob-elixir.el Modify ob-elixir--execute-with-deps - add modules-string parameter
ob-elixir.el Modify ob-elixir--evaluate-in-session - add modules-string parameter
ob-elixir.el Modify ob-elixir--evaluate-in-session-with-deps - add modules-string parameter
ob-elixir.el Modify org-babel-execute:elixir - gather modules and pass to execution
test/test-ob-elixir-modules.el New file with module definition tests
test/test-ob-elixir.el Require new test file

Code Assembly Order

When a regular (non-module) block is executed, code is assembled in this order:

  1. Module definitions (defmodule ... end for each gathered module)
  2. Imports (from #+BEGIN_IMPORTS elixir block)
  3. Variable assignments (from :var headers)
  4. User code (the block body, wrapped for value capture if needed)

Verification

After implementation, test with this org file:

#+BEGIN_SRC elixir :module Helpers
def greet(name), do: "Hello, #{name}!"
def double(x), do: x * 2
#+END_SRC

#+BEGIN_SRC elixir :module Helpers
def triple(x), do: x * 3
#+END_SRC

#+BEGIN_SRC elixir :module Math
def square(x), do: x * x
@doc "Adds two numbers"
def add(a, b), do: a + b
#+END_SRC

#+BEGIN_IMPORTS elixir
import Helpers
import Math
#+END_IMPORTS

#+BEGIN_SRC elixir :results value
{greet("World"), double(5), triple(5), square(4), add(10, 20)}
#+END_SRC

Expected result: {"Hello, World!", 10, 15, 16, 30}

Notes

  • Module redefinition warnings may appear in sessions (this is expected Elixir behavior)
  • Only blocks before the current position are gathered (consistent with imports/deps behavior)
  • The :module header value should be a valid Elixir module name (e.g., MyModule, My.Nested.Module)
  • All standard Elixir module constructs are supported (def, defp, @moduledoc, @doc, use, require, import, alias, etc.)