281 lines
6.8 KiB
Markdown
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
|