dev log ยท 31 October 2026

How could a Nim REPL work?

Prior art, a design where each input is a module, a service any front end can use, and a marimo-style notebook.

research ยท rendered with nimib

Summary

A usable Nim REPL is within reach by treating each input as a Nim module, loaded into a long-running process with hot code reloading (HCR), with incremental compilation (IC, Nim 2.4) keeping each compile cheap. This is the direction Araq suggested, and both pieces work on Nim 2.4.0 in a quick test (see What I measured below). The next post, A working PoC with Hot Code Reloading and Incremental Compilation, shows that test in detail.

Where the idea came from

A REPL for Nim is an idea probably as old as Nim itself. It resurfaced recently in Pietro's forum thread uv add nimlang and other scientific Nim experiments (2026-10-18), where two replies point to two different designs.

Prior art

Every existing Nim REPL either re-runs everything, runs in the VM, or depends on a second compiler. The two building blocks for doing better, HCR and IC, are in the compiler, but neither is a REPL by itself.

Approach Example How state is kept Strengths Weaknesses
Re-run INim, jupyternim Not kept: each input is appended to a buffer file, which is recompiled and re-run with nim c -r, old output hidden Full Nim, FFI works, simple Cost grows with the session; I/O, randomness and long computations run again every time
VM nim secret, NimScript In the compiler VM Instant, real persistent state No FFI, so many stdlib modules fail; ptr, ref ref and multimethods are shaky; slow for numeric work
JIT nlvm nlvm r LLVM ORCv2 JIT in the process Real REPL at native speed Pins its own Nim version; ignores header, emit, importcpp; parts of stdlib unavailable; heavy LLVM build
Hot code reloading --hotCodeReloading:on Each module is a .so; globals survive reloads Native code, keeps state, per-module Main module not reloadable; type changes can't reload; old libraries leak memory under orc; tested only with --threads:off; no arm64 glibc
Incremental compilation nim ic (Nim 2.4) A cache, not a REPL One-line edit rebuild 0.45 s vs 22 s on Araq's benchmark Cold builds can be slower (Atlas: 11.6 s vs 4.1 s, Sept 2026); the 2.4.0 release shipped without the nifler tool ic:on needs

jupyternim's own TODO list says it: "find a better way to fake a repl than re running prior code and discarding output (we have HCR! Buggy though)". IC numbers are from Araq's progress thread; the 2.4.0 packaging bug is in thread 14255.

What I measured

Nim 2.2.6 (the version the nimlang package installs from PyPI), gcc, Linux:

Nim 2.4.0 built from source (the version-2-4 branch still equals the v2.4.0 tag on 2026-10-31), gcc, Linux:

REPL design: each input is a module

This design follows Araq's suggestion. Each input becomes a module, a long-running process holds the state, and earlier inputs never run again.

  1. Each input (cell) becomes a module cell_N.nim. It imports and re-exports the earlier cells. Its top-level declarations are rewritten to be exported (x becomes x*), using the compiler's parser so the rewrite is syntax-aware.
  2. A long-running host process is built with HCR. Its reloadable module imports all the cells. Adding a cell means recompiling and calling performCodeReload(). HCR loads the new .so, runs its initialisation and keeps the earlier globals.
  3. IC keeps each step cheap. The stdlib and earlier cells are already cached, so only the new module needs checking and its C code compiling.
  4. Redefinitions such as let x = 1 followed later by let x = "a" would be ambiguous across modules. The REPL keeps a symbol table and generates import cell_K except x for names that a later cell redefines.
  5. Fallback: when HCR can't apply a change (a changed type, or an edit to an earlier Jupyter cell), the REPL restarts the host and re-runs everything, as INim does.
  6. Showing values: >>> x should print the value and its type. This can reuse INim's trick: compile the bare expression, and when the compiler says it has to be discarded, wrap it in echo.

Library and service architecture

The REPL should be its own project, not part of nimlang: an engine library, a server binary that any tool can drive, and thin front ends. Rust's evcxr is the closest example for a compiled language. It is one library crate with a separate terminal REPL and Jupyter kernel, and each cell is compiled to a dylib and loaded into a child process that holds the state. Clojure's nREPL (one server, many editors), xeus (a C++ library for writing Jupyter kernels), LSP (JSON-RPC over stdio) and Nim's own nimsuggest show how to run the shared part as a service.

Front ends stay thin: one server drives the engine and the workerTerminal REPLin your shellJupyter kerneltranslates Jupyter messagesEditor pluginsend code to the REPLPython clientnimlang, notebook magicsnimrepld serverJSON-RPC 2.0 over stdio by default, or a Unix / TCP socketa socket lets several front ends share one sessionEngine library (pure Nim)session: eval, complete, inspect, interrupt, resetRe-run backend (v0)HCR cells as modulesnlvm JIT (maybe)pipeWorker processruns the user code and holds its state; killed and restarted on a crash or interrupt

The user's code runs in a separate worker, so a crash or hang never takes down the server. Stdio is the default because a front end just launches the server, with no ports or auth to manage. A TCP socket would need a token.

Protocol sketch (JSON-RPC requests with an id, output streamed as notifications):

Build order: the library with the re-run backend, the stdio server and a terminal client first. Then the Jupyter adapter, then the HCR backend.

A reactive notebook, marimo style

A reactive notebook fits Nim well if each cell is a module that imports only the cells it reads from, so that the dependency graph is the import graph.

What marimo does (Python). Each cell is a function in a plain .py file. Static analysis finds which globals each cell defines and which it reads, which gives a dependency graph. Editing a cell re-runs every cell that depends on it. Deleting a cell deletes its variables, so there is no hidden state. Each global is defined in exactly one cell. UI elements are values in the graph, so moving a slider re-runs the cells that use it.

Pluto.jl (Julia) is the closer model for a compiled language. It works the same way, but the notebook is saved as a valid .jl script in dependency order, code runs in a separate worker process, and a redefined struct moves the notebook to a fresh module.

How it maps onto Nim:

Open questions and next steps

Open questions:

Next steps:

Sources

Forum threads were read through tomd: https://tomd.pietroppeter.xyz/https://forum.nim-lang.org/t/<id> returns a thread as Markdown.