function definitions outside modules
This commit is contained in:
280
tasks/13a-module-definition-blocks-fix.md
Normal file
280
tasks/13a-module-definition-blocks-fix.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user