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

281 lines
6.8 KiB
Markdown

# Task 13a: Fix Module Definition Blocks - Code.eval_string Wrapper
## Problem
The initial implementation of module definition blocks (Task 13) has a fundamental issue: Elixir does not allow importing a module that is defined in the same file at the top level. When we generate code like:
```elixir
defmodule Helpers do
def double(x), do: x * 2
end
import Helpers # ERROR: module Helpers is not loaded but was defined
double(21)
```
Elixir produces this error:
```
error: module Helpers is not loaded but was defined. This happens when you
depend on a module in the same context in which it is defined...
If the module is defined at the top-level and you are trying to use it at
the top-level, this is not supported by Elixir
```
**Note:** The anonymous function wrapper approach does NOT work either - Elixir still
compiles the function body at the same time as the module definition.
## Solution
Use `Code.eval_string/1` to defer the evaluation of imports and user code until runtime,
after the module has been compiled and loaded:
```elixir
defmodule Helpers do
def double(x), do: x * 2
end
Code.eval_string("""
import Helpers
result = (
double(21)
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
""")
```
This works because:
1. The `defmodule` is compiled and loaded first
2. `Code.eval_string/1` defers parsing and evaluation until runtime
3. By the time the string is evaluated, the module is fully available
## Implementation Plan
### Step 1: Add escape helper function
**File:** `ob-elixir.el` (before `ob-elixir--execute`)
**Add:**
```elisp
(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))
```
### Step 2: Modify `ob-elixir--execute`
**File:** `ob-elixir.el` (around line 968)
**New code assembly:**
```elisp
(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)))
```
### Step 3: Modify `ob-elixir--execute-with-deps`
**File:** `ob-elixir.el` (around line 610)
Apply the same Code.eval_string pattern as Step 2.
### Step 4: Modify `ob-elixir--evaluate-in-session`
**File:** `ob-elixir.el` (around line 736)
Apply the same Code.eval_string pattern as Step 2.
### Step 5: Modify `ob-elixir--evaluate-in-session-with-deps`
**File:** `ob-elixir.el` (around line 892)
Apply the same Code.eval_string pattern as Step 2.
### Step 6: Fix test expectations
**File:** `test/test-ob-elixir-modules.el`
The tests had incorrect expectations. When using Code.eval_string with modules:
- Scalar numbers come back as strings (e.g., `"42"` not `42`)
- String values come back with quotes (e.g., `"\"hello\""` not `"hello"`)
- Lists are parsed correctly as before
Also, the test using `Config` as module name conflicted with Elixir's built-in Config module.
Changed to `MyConfig`.
### Step 7: Run tests and verify
After making the changes:
```bash
make test
```
Expected: All 80 tests should pass, including the 6 that were previously failing:
- `ob-elixir-test-module-integration-basic`
- `ob-elixir-test-module-integration-merged`
- `ob-elixir-test-module-integration-multiple-modules`
- `ob-elixir-test-module-with-module-attributes`
- `ob-elixir-test-module-with-private-functions`
- `ob-elixir-test-module-without-import`
## Generated Code Examples
### Example 1: Basic module with import
**Input org file:**
```org
#+BEGIN_SRC elixir :module Helpers
def double(x), do: x * 2
#+END_SRC
#+BEGIN_IMPORTS elixir
import Helpers
#+END_IMPORTS
#+BEGIN_SRC elixir :results value
double(21)
#+END_SRC
```
**Generated Elixir code:**
```elixir
defmodule Helpers do
def double(x), do: x * 2
end
(fn ->
import Helpers
result = (
double(21)
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
end).()
```
### Example 2: Multiple modules
**Input org file:**
```org
#+BEGIN_SRC elixir :module ModA
def value, do: 10
#+END_SRC
#+BEGIN_SRC elixir :module ModB
def value, do: 20
#+END_SRC
#+BEGIN_IMPORTS elixir
alias ModA
alias ModB
#+END_IMPORTS
#+BEGIN_SRC elixir :results value
ModA.value() + ModB.value()
#+END_SRC
```
**Generated Elixir code:**
```elixir
defmodule ModA do
def value, do: 10
end
defmodule ModB do
def value, do: 20
end
(fn ->
alias ModA
alias ModB
result = (
ModA.value() + ModB.value()
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
end).()
```
### Example 3: Without imports (using full module name)
**Input org file:**
```org
#+BEGIN_SRC elixir :module Helpers
def greet, do: "hello"
#+END_SRC
#+BEGIN_SRC elixir :results value
Helpers.greet()
#+END_SRC
```
**Generated Elixir code:**
```elixir
defmodule Helpers do
def greet, do: "hello"
end
(fn ->
result = (
Helpers.greet()
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
end).()
```
### Example 4: No modules defined (no wrapper needed)
**Input org file:**
```org
#+BEGIN_IMPORTS elixir
import Enum
#+END_IMPORTS
#+BEGIN_SRC elixir :results value
map([1,2,3], &(&1 * 2))
#+END_SRC
```
**Generated Elixir code (no change from current behavior):**
```elixir
import Enum
result = (
map([1,2,3], &(&1 * 2))
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
```
## Summary of Changes
| File | Function | Change |
|------|----------|--------|
| `ob-elixir.el` | `ob-elixir--execute` | Wrap imports+code in anon fn when modules present |
| `ob-elixir.el` | `ob-elixir--execute-with-deps` | Wrap imports+code in anon fn when modules present |
| `ob-elixir.el` | `ob-elixir--evaluate-in-session` | Wrap imports+code in anon fn when modules present |
| `ob-elixir.el` | `ob-elixir--evaluate-in-session-with-deps` | Wrap imports+code in anon fn when modules present |
| `test/test-ob-elixir-modules.el` | Various tests | Adjust expectations if needed |
## Notes
- The anonymous function wrapper is ONLY used when `modules-string` is non-nil
- When no modules are defined, the code generation remains unchanged (backward compatible)
- The wrapper doesn't affect the return value semantics - `result = (...)` still works
- Session mode should work identically since modules persist after first evaluation