function definitions outside modules

This commit is contained in:
2026-01-25 09:49:44 +01:00
parent 4e07ffa70c
commit 6ad1c86b95
7 changed files with 1576 additions and 63 deletions

View File

@@ -451,6 +451,55 @@ Returns the imports content as a string, or nil if no imports block found."
(setq found (match-string-no-properties 1))))
found)))
;;; Module Definition Block Parsing
(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))))))
(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")))
(defun ob-elixir--normalize-deps (deps-string)
"Normalize DEPS-STRING for consistent hashing.
@@ -558,20 +607,36 @@ Returns the project directory path."
;;; Execution with Dependencies
(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string)
(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 to the code before execution."
IMPORTS-STRING, if provided, is prepended after module definitions.
MODULES-STRING, if provided, contains module definitions to prepend first.
When modules are defined, imports and user code are wrapped in Code.eval_string.
This is required because Elixir cannot import a module defined in the same file
at the top level - Code.eval_string defers evaluation until runtime."
(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 (if imports-string
(concat (string-trim imports-string) "\n\n" wrapped)
wrapped)))
(code (if modules-string
;; When modules are defined, wrap imports + code in Code.eval_string
(let ((eval-body (concat
(when imports-string (concat (string-trim imports-string) "\n"))
wrapped)))
(concat
(string-trim modules-string) "\n\n"
"Code.eval_string(\"\"\"\n"
(ob-elixir--escape-for-eval-string eval-body)
"\n\"\"\")"))
;; Normal path without modules
(concat
(when imports-string (concat (string-trim imports-string) "\n\n"))
wrapped))))
;; Write code to temp file
(with-temp-file tmp-file
@@ -682,18 +747,34 @@ IO.puts(\"__ob_value_end__\")
"Wrap BODY to capture its value in session mode."
(format ob-elixir--session-value-wrapper body))
(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string)
(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 to the code before execution."
IMPORTS-STRING, if provided, is prepended after module definitions.
MODULES-STRING, if provided, contains module definitions to prepend first.
When modules are defined, imports and user code are wrapped in Code.eval_string.
This is required because Elixir cannot import a module defined in the same file
at the top level - Code.eval_string defers evaluation until runtime."
(let* ((buffer (org-babel-elixir-initiate-session session nil))
(wrapped (if (eq result-type 'value)
(ob-elixir--session-wrap-for-value body)
body))
(code (if imports-string
(concat (string-trim imports-string) "\n\n" wrapped)
wrapped))
(code (if modules-string
;; When modules are defined, wrap imports + code in Code.eval_string
(let ((eval-body (concat
(when imports-string (concat (string-trim imports-string) "\n"))
wrapped)))
(concat
(string-trim modules-string) "\n\n"
"Code.eval_string(\"\"\"\n"
(ob-elixir--escape-for-eval-string eval-body)
"\n\"\"\")"))
;; Normal path without modules
(concat
(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)
@@ -836,18 +917,34 @@ DEPS-STRING contains the dependencies specification."
(puthash name deps-hash ob-elixir--session-deps)
buffer))))
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string)
(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 to the code before execution."
IMPORTS-STRING, if provided, is prepended after module definitions.
MODULES-STRING, if provided, contains module definitions to prepend first.
When modules are defined, imports and user code are wrapped in Code.eval_string.
This is required because Elixir cannot import a module defined in the same file
at the top level - Code.eval_string defers evaluation until runtime."
(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 (if imports-string
(concat (string-trim imports-string) "\n\n" wrapped)
wrapped))
(code (if modules-string
;; When modules are defined, wrap imports + code in Code.eval_string
(let ((eval-body (concat
(when imports-string (concat (string-trim imports-string) "\n"))
wrapped)))
(concat
(string-trim modules-string) "\n\n"
"Code.eval_string(\"\"\"\n"
(ob-elixir--escape-for-eval-string eval-body)
"\n\"\"\")"))
;; Normal path without modules
(concat
(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)
@@ -916,13 +1013,32 @@ The wrapper evaluates BODY, then prints the result using
`inspect/2` with infinite limits to avoid truncation."
(format ob-elixir--value-wrapper body))
(defun ob-elixir--execute (body result-type &optional imports-string)
(defun ob-elixir--escape-for-eval-string (str)
"Escape STR for use inside Elixir Code.eval_string heredoc.
Escapes backslashes and double quotes."
(let ((result str))
(setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
(setq result (replace-regexp-in-string "\"" "\\\\\"" result))
result))
(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, is prepended to the code before execution.
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) - wrapped in Code.eval_string if modules present
3. User code (BODY, wrapped for value if needed)
When modules are defined, imports and user code are wrapped in Code.eval_string.
This is required because Elixir cannot import a module defined in the same file
at the top level - Code.eval_string defers evaluation until runtime.
Returns the result as a string.
May signal `ob-elixir-error' if execution fails and
@@ -931,9 +1047,20 @@ May signal `ob-elixir-error' if execution fails and
(wrapped (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body))
(code (if imports-string
(concat (string-trim imports-string) "\n\n" wrapped)
wrapped)))
(code (if modules-string
;; When modules are defined, wrap imports + code in Code.eval_string
(let ((eval-body (concat
(when imports-string (concat (string-trim imports-string) "\n"))
wrapped)))
(concat
(string-trim modules-string) "\n\n"
"Code.eval_string(\"\"\"\n"
(ob-elixir--escape-for-eval-string eval-body)
"\n\"\"\")"))
;; Normal path without modules
(concat
(when imports-string (concat (string-trim imports-string) "\n\n"))
wrapped))))
(with-temp-file tmp-file
(insert code))
(let ((result (with-temp-buffer
@@ -950,45 +1077,53 @@ 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* ((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)))
;; 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))
;; Session mode without deps
((and session (not (string= session "none")))
(ob-elixir--evaluate-in-session session full-body result-type imports-string))
;; Non-session with deps
(deps-string
(ob-elixir--execute-with-deps full-body result-type deps-string imports-string))
;; Plain execution
(t
(ob-elixir--execute full-body result-type imports-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))))))
(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))))))))
(provide 'ob-elixir)
;;; ob-elixir.el ends here