// ELISPRS — ENGINEERING REPORT

elisprs v0.1.2 · Emacs Lisp in Rust · Lisp-2 + lexical & dynamic binding · elisp-correct reader · compiled to fusevm bytecode (JIT/AOT) · MIT

Docs GitHub
// Color scheme

>_ENGINEERING REPORT

elisprs is an Emacs Lisp runtime in Rust that runs .el outside Emacs as a fusevm frontend: it lowers elisp to fusevm::Chunk and executes on the shared bytecode VM. This report describes the architecture, the object heap, the lowering pipeline, and the dependency posture. Numbers below are facts about the current tree, not aspirational metrics.

fusevm
frontend · lowers to Chunk
234
subrs · 22 special forms
Lisp-2
value + function cells
MIT
license · free / OSS

Summary

Emacs Lisp has historically been inseparable from its host editor. elisprs separates the two: it runs .el as standalone programs and is the fifth consumer of fusevm — alongside zshrs, stryke, awkrs, and vimlrs.

elisprs is a pure fusevm frontend with no bespoke VM or JIT: an elisp-correct reader, a Lisp-2 obarray with lexical and dynamic binding, 22 compiler-recognized special forms with macros, and a 234-subr standard library plus an elisp prelude. The compiler lowers each form to a fusevm::Chunk; fusevm executes it with its three-tier Cranelift JIT, and --aot/--aot-exe emit native objects/binaries. The elisp object heap rides through the VM as Value::Obj handles. The --lsp and --dap servers, regexp engine, and rkyv bytecode cache are all implemented.


The value model & object heap

Emacs Lisp is a Lisp-2 (separate value and function cells per symbol) with both lexical and dynamic scope. elisprs owns those semantics directly on top of fusevm's value type — there is no rust_lisp dependency. The cons/symbol/vector/closure/hash-table cells live in an ElispHost arena and travel through the VM as opaque handles:

Value = fusevm::Value

elisprs re-exports fusevm::Value. Immediates (ints, floats, strings, bool, nil) are native VM values; every compound elisp object is a heap cell referenced as Value::Obj(u32) into the host arena.

The reader

reader.rs is a hand-written elisp-correct reader: 1+/1-, #', ?c, keywords, dotted pairs (a . b), and backquote (`/,/,@) all parse and decompose to ordinary forms.

The obarray

Every symbol (an Obj::Symbol) gets a value cell and a function cell; resolution follows function-cell aliases. Subrs and user closures are both heap function objects in the function cell.

Lexical + dynamic binding

Closures capture a chain of Scope frames (lexical, indefinite extent, mutable via setq); symbols marked special via defvar/defconst bind dynamically through a specstack that unwinds on scope exit.


The lowering pipeline

elisprs compiles, it does not interpret. lib.rs::eval_forms reads, macro-expands, lowers each form with compiler::compile_top, and runs the resulting chunk with host::run_chunk:

.el  →  reader  →  Value (Value::Obj heap handles)  →  macroexpand
                          ↘  compiler.rs → fusevm::Chunk  →  fusevm VM + Cranelift JIT / AOT

No fork of the value enum

elisp cells are not new fusevm::Value variants. They live in the ElispHost arena and ride through the VM as Value::Obj(u32) handles, resolved by the extension handler — fusevm stays language-agnostic.

Extension ops

The compiler emits an elisp Op::Extended(id, arg) range (CALL, GETVAR/SETVAR, FSET, SPECBIND/LETBIND/UNBIND, MAKE_CLOSURE, TRUTHY) dispatched into the host for funcall, variable cells, and dynamic binding.

Sub-chunks for lambdas

Each top-level form lowers through a ChunkBuilder; lambda/closure bodies become their own fusevm::Chunk, so calling a closure runs that chunk on a nested VM. Hot numeric ops lower to native fusevm arithmetic for the JIT.

JIT + AOT

Lowered elisp inherits fusevm's three-tier Cranelift JIT, the persistent on-disk JIT cache, and aot::compile_object — the same engine vimlrs, zshrs, and awkrs run on. --aot-exe links a standalone native binary.


Component status

ComponentStateNotes
reader.rsWorkingElisp-correct: 1+/#'/?c/:kw, dotted pairs, backquote (`/,/,@).
obarray + lexical & dynamic bindingWorkingValue + function cells; Scope frames + specstack unwind.
special forms (22) + macrosWorkinglet/cond/while/condition-case/defmacro/… lowered in compiler.rs.
subr library (234) + elisp preludeWorkingLists, numbers, strings, predicates, IO, functional; prelude.rs for derived surface.
elisp CLI + REPLWorkingFile / -e / REPL; --version/--help.
compiler.rs → fusevm::ChunkWorkingcompile_top/compile_program; runs via host::run_chunk.
--aot / --aot-exeWorkingcompiler + fusevm::aot::compile_object; native object or standalone exe (aot.rs).
--lsp / --dapWorkingFull stdio servers — completion/hover/signature/symbols (lsp.rs); breakpoints/stepping (dap.rs).
dotted pairs · backquote · lexical · setcarWorkingImplemented in the reader / host / builtins.
regexp engine · rkyv bytecode cacheWorkingregexp.rs (fancy-regex); cache.rs at ~/.elisprs/scripts.rkyv.

Dependency posture

elisprs is a focused frontend over fusevm plus a handful of protocol/support crates — nine direct dependencies, all pulling their weight:

CrateRole
fusevmLanguage-agnostic bytecode VM + three-tier Cranelift JIT + AOT (jit-disk-cache, aot features); the execution engine elisp lowers onto
lsp-server / lsp-typesLSP transport + protocol types for --lsp (src/lsp.rs)
serde / serde_jsonJSON-RPC for LSP/DAP; serialization of the AOT heap image
rkyv / bincodeThe ~/.elisprs/scripts.rkyv bytecode cache — zero-copy outer shard, bincode per-form chunks
fancy-regexBacktracking regexp engine behind string-match / replace-regexp-in-string (src/regexp.rs)
libcDAP debuggee stdout capture (pipe + dup2) so program output streams as output events

Honesty & longevity

Loud failures

The only unlowered forms — save-excursion/save-current-buffer/save-restriction (the buffer milestone) — error explicitly at compile time rather than silently misbehaving.

Real elisp semantics

A genuine Lisp-2 with both lexical and dynamic binding, so behaviour tracks Emacs Lisp rather than a Scheme approximation.

Shared engine

By lowering to fusevm::Chunk, elisprs reuses the same VM, JIT, AOT, and on-disk cache as the sibling languages instead of maintaining a one-off interpreter.

Tested

cargo test covers the reader, the lowering/eval pipeline end-to-end, and error handling, including recursion and macros.


Links