Skip to content

Fe — Embedded Lisp by rxi

Fe is a Lisp-family scripting language by rxi (same author as lite text editor, microui immediate-mode UI, json.lua, and a dozen other “small focused thing in a tiny implementation” projects). The implementation is ~800 source lines of ANSI C with no dependencies and no malloc — all allocations come from a single fixed-size memory arena the embedder provides at init time.

The dialect is intentionally minimal:

  • Numbers, symbols, strings, pairs, lambdas, macros
  • Lexical scoping with first-class closures
  • Tail-call optimization (TCO)
  • Mark-and-sweep GC over the embedder-provided arena
  • Embeddable via a small C API (fe_open, fe_eval, fe_pushnumber, fe_call, etc.)
  • No standard library beyond core primitives — the embedder provides whatever I/O, FFI, or domain primitives the application needs

The deliberate omissions: no file I/O (embedder supplies), no FFI scaffolding (embedder supplies), no module system, no exceptions (errors call a handler the embedder set up). Everything not in the 800 lines is the embedder’s job.

Per ADR-0004: the KN-86 cart runtime needed an embeddable bytecode-or-tree-walker Lisp with arena memory discipline (no malloc, hard memory budget per cart), deterministic teardown (arena reset = cart unload, nothing leaks), small code footprint (the runtime ships on a 32 GB SD card and runs on a Pi Zero 2 W; every megabyte counts), and C embedding friendliness (NoshAPI per ADR-0005 is 57+ C primitives exposed to cart Lisp as builtins). Fe hit every requirement.

The alternatives considered in ADR-0004:

  • MicroPython / CircuitPython — too big, runtime cost too high, GC behavior too opaque for memory budgeting
  • Lua — bigger than Fe (~10× LoC), great C embedding, but the not-quite-Lisp dialect doesn’t carry the cartridge-as-Lisp-program identity KN-86 wants (ADR-0001)
  • mal — Make a Lisp (mal.md) — pedagogical, not embedding-grade; valuable as the curriculum for learning Fe-style implementation, not as the runtime itself
  • Scheme (S7, TinyScheme, Chibi) — production-grade but each carries 5-30× more LoC than Fe, and the SRFI / call-cc machinery is overkill
  • Janet — modern, beautiful, but larger and with its own runtime expectations that don’t fit the Fe-style arena-reset model
  • Custom from scratch — building a Lisp from scratch (build-your-own-lisp.md is the canonical book) is months of work to reach Fe’s stability; not justified when Fe is sitting on the shelf

Fe won on every axis that mattered, and the rxi authorship signals long-term not aiming for feature creep — the project is stable because the author is famously disciplined about scope (lite is ~2k LoC; microui is ~1.5k; the pattern holds).

PathRole
kn86-emulator/src/fe/Vendored Fe source — fe.c, fe.h, plus the KN-86 embedder glue.
kn86-emulator/src/nosh.cNoshAPI C primitive registrations into the Fe VM.
kn86-emulator/src/cartridge.cCart loader — opens a cart’s .kn86 container (ADR-0006), allocates the arena, loads the Lisp source + static data (tree-walked on load; no bytecode), runs the cart’s init thunk.
kn86-emulator/src/cart_unload.cCart teardown — fe_close + arena reset; deterministic, no leaks possible.

The runtime budget per cart is set per the cart’s .kn86 manifest; typical carts get ~64 KB arena which is enormous relative to Fe’s overhead (~4 KB for the VM itself).

What we borrow from the rxi project that’s not Fe specifically

Section titled “What we borrow from the rxi project that’s not Fe specifically”

The rxi authoring philosophy is worth borrowing wholesale:

  1. Small enough to vendor. Fe lives in our source tree (kn86-emulator/src/fe/) as vendored copies, not as a submodule, not as a package dependency. We can read it, understand it, modify it if we have to. The 800 LoC is small enough that this is practical.
  2. No GitHub Actions / npm / CMake complexity. Fe builds with a single cc fe.c -c -o fe.o. Our build chain handles it as one more source file.
  3. No “framework” — just a library. Fe doesn’t impose application structure. NoshAPI gets to be NoshAPI; Fe doesn’t have opinions about what FFI primitives the embedder exposes.
  4. The README is the spec. rxi’s docs are minimal-and-correct rather than exhaustive-and-aspirational. KN-86’s own runtime docs should match this posture.
  • Not modifying Fe upstream. Fe-the-language is stable. If KN-86 needs something Fe doesn’t have (additional core primitive, different number type), we add it in our embedder layer (nosh.c), not by forking Fe. This keeps Fe upgradeable from rxi’s upstream if a security fix or perf improvement lands.
  • Not adopting the full rxi library set. lite is a text editor, not a runtime substrate; microui is an immediate-mode UI library that overlaps awkwardly with our 80×25-grid model. We borrow Fe specifically.
  • lite — minimal text editor in C+Lua. Pattern: tiny C core + Lua scripted UI. Architecturally interesting; not adopted.
  • microui — immediate-mode UI library, ~1500 LoC C. Pattern: tiny scope, frame-by-frame UI rebuild. The IM-style isn’t a fit for KN-86’s 80×25 grid + cell-state model, but the discipline is exemplary.
  • json.lua — JSON parser in 300 LoC of Lua. Pattern: one job, done right.
  • Cross-link ADR-0001 — the decision to use Lisp for cart authoring.
  • Cross-link ADR-0004 — the decision to use Fe specifically.
  • Cross-link ADR-0005 — NoshAPI: the FFI surface exposed to cart Lisp.
  • Cross-link ADR-0006 — the .kn86 container format that ships Lisp source + static data (AOT bytecode deferred).
  • Cross-link ADR-0012 — the Lisp slot table widening for the 14-Lisp-primitive function block.
  • Cross-link build-your-own-lisp.md + mal.md** — pedagogical references; useful as background reading for anyone trying to understand what Fe is by understanding what implementing a Lisp looks like.
  • License: MIT. Permissive; commercial use fine; no attribution-in-binary requirement (though we credit rxi anyway in _archive/ and ADR-0004).