A persistent warm Nim session for interactive use from Emacs org-babel and Jupyter.
Nim is fast — but cold-starting a script that imports datamancer or arraymancer means paying generic instantiation and macro expansion costs on every single run. Interactive data exploration becomes painful when each tweak takes seconds to compile.
nimteractive solves this by keeping a session's growing script (imports + procs + history) warm against a persistent, shared Nim build cache, so only the newest block's delta needs to be recompiled.
┌──────────────────────────────────────────────────────────────────┐
│ SESSION (imports + procs + history, replayed on every eval) │
│ │
│ Each eval block appends to history and is compiled for real: │
│ write .nim → `nim c --app:lib` against a persistent nimcache │
│ (keyed by sha256(imports | nim version | nimble.lock)) → │
│ dlopen the .so (its NimMain runs as a POSIX constructor, │
│ executing all top-level statements and producing stdout) → │
│ dlclose immediately │
└──────────────────────────────────────────────────────────────────┘
Because the whole script (imports+procs+history+new code) compiles and
runs as one program each time, cross-block let/var state works
naturally — no VM, no typed value store, no cross-library ABI. The
trade-off: eval pays a real compile+link, not an instant VM step —
sub-second once the nimcache is warm for your imports, not zero.
:precompile blocks front-load the expensive part (datamancer/
arraymancer-style generic instantiation and C compilation) once;
regular eval blocks only pay for their own delta.
cd nimteractive
nim c -o:nimteractive src/nimteractive.nim
# or via nimble:
nimble buildPut the resulting nimteractive binary somewhere on your PATH (e.g. ~/.nimble/bin/).
With use-package and the built-in :vc fetcher (Emacs 30+):
(use-package ob-nimteractive
:vc (:url "~/nimteractive" :lisp-dir ".")
:config
(ob-nimteractive-setup))Or load the file directly:
(load "~/nimteractive/ob-nimteractive.el")
(ob-nimteractive-setup)ob-nimteractive-setup registers nim with org-babel and pins *Nimteractive:* buffers to a bottom side window (like ESS does for *R*).
Three block types, selected by header arguments:
1. Precompile — run once per session. Compiles your imports and caches the result.
#+begin_src nim :session analysis :precompile t
import strutils
#+end_src
Output: [session ready — 4200ms] on first run, [session ready — 80ms, cached] thereafter.
2. Procs — define reusable procedures. Re-execute to redefine (this clears accumulated eval history, same as re-running :precompile).
#+begin_src nim :session analysis :procs t
proc shout(s: string): string =
s.toUpperAscii & "!"
#+end_src
3. Eval — interactive code. State accumulates across blocks.
#+begin_src nim :session analysis
let msg = shout("hello, nim")
echo msg
#+end_src
Each session gets a *Nimteractive:<name>* comint buffer — a live REPL backed by the same process that org-babel uses.
| Key | Action |
|---|---|
C-c C-v z |
Jump to (or open) the session buffer |
RET |
Send current input as an eval request |
With a prefix argument (C-u C-c C-v z) you are prompted for a session name — useful when working with multiple named sessions.
When you open a nim source block with C-c ' (org-edit-src), the session buffer is automatically displayed alongside the edit buffer, mirroring the ESS workflow.
;; Show session buffer (interactive)
M-x ob-nimteractive-show-session
;; Clean shutdown (sends exit op)
M-x ob-nimteractive-exit-session
;; Hard kill
M-x ob-nimteractive-kill-session;; Path to binary (auto-detected from PATH / ~/.nimble/bin/)
(setq ob-nimteractive-binary "/path/to/nimteractive")
;; Prompt string (must match what the server emits)
(setq ob-nimteractive-prompt "nim> ")
;; Seconds to wait for a response before timing out
(setq ob-nimteractive-timeout 120)The server communicates over stdin/stdout using line-delimited JSON. You can drive it from any editor or script.
→ {"op": "eval", "id": "1", "code": "echo 1+1"}
← {"op": "result", "id": "1", "stdout": "2\n", "value": ""}
→ {"op": "precompile", "id": "2", "imports": ["strutils"]}
← {"op": "compiling", "id": "2"}
← {"op": "ready", "id": "2", "cache_hit": false, "elapsed_ms": 4200}
→ {"op": "load_procs", "id": "3", "code": "proc shout(s: string): string = s.toUpperAscii & \"!\""}
← {"op": "compiling", "id": "3"}
← {"op": "reloaded", "id": "3", "elapsed_ms": 1100}
→ {"op": "complete", "id": "4", "code": "df[\"", "cursor": 4}
← {"op": "completions","id": "4", "items": []}
→ {"op": "exit", "id": "5"}
← {"op": "result", "id": "5", "stdout": "bye", "value": ""}
Cache key: sha256(sorted_imports | nim_version | nimble.lock)
Stored under ~/.cache/nimteractive/<hash>/:
| Path | Contents |
|---|---|
nimcache/ |
Nim's incremental build cache — persists across evals, so a datamancer-heavy delta compile stays fast |
tmp/<tag>.nim, tmp/<tag>.so |
per-eval generated source and library, removed right after each dlclose |
The cache invalidates automatically when you upgrade Nim or change package versions.
- Nim ≥ 2.2.0
- Emacs ≥ 29.1, Org ≥ 9.6 (for the Emacs integration)
MIT