A small language with a long memory

Lisp, with a database in it.

EELisp is a compact Lisp where tables, records, forms and an agenda are part of the language rather than a library you bolt on. Define a table, insert a record, ask a question, get a table back. It runs anywhere Rust runs, and it fits in one binary.

;; A table is a form, not a schema file
(deftable books (title:string author:string year:number))

(insert books {:title "The Mythical Man-Month" :author "Brooks" :year 1975})
(insert books {:title "Thinking Forth"         :author "Brodie" :year 1984})

(query books :where "year < ?" :params (list 1980) :order "year")
┌────┬────────────────────────┬────────┬──────┐
│ id │ title                  │ author │ year │
├────┼────────────────────────┼────────┼──────┤
│ 1  │ The Mythical Man-Month │ Brooks │ 1975 │
└────┴────────────────────────┴────────┴──────┘

Three ideas, one language

λ

The Lisp

Lexer, reader, environments and a tail-call-optimising evaluator, with macros and quasiquote. Around 95 builtins, plus a prelude written in EELisp itself. Proper tail calls mean loop/recur runs forever without growing the stack.

The database — dBASE III, revisited

deftable, insert, query, update, delete, on real SQLite. Field types, defaults, required fields and choices are stored with the schema, so a table survives a reload intact. browse and defform give you a grid and a form, not a printout.

The agenda — Lotus Agenda, reborn

Items with dates, priorities and notes; hierarchical categories; templates; views; and a rules engine whose conditions are ordinary EELisp, stored as source and evaluated against each item. Type in your own words and let the rules do the filing.

The rules engine is the point

Most organisers make you file things. Lotus Agenda, in 1988, inverted it: you write in plain language, and rules decide where it belongs. EELisp keeps that, and because a rule's condition is just EELisp held as source and re-evaluated, the whole language is available inside it.

(defcategory work/calls)

(add "call Bob tomorrow !!")          ; date and priority parsed out of the sentence

(defrule calls
  :when   (str-matches text "call|phone|ring")
  :assign "work/calls")

(apply-rules)                        ; → 1
(items :category "work/calls")

Inside a condition the item is in scope — text, notes, categories, props — along with get, has-category, overdue? and match for regex captures.

Get it

EELisp is one Rust crate that builds a library and a CLI. You need a Rust toolchain:

git clone https://github.com/santacroce-tech/eelisp-rs
cd eelisp-rs
cargo build --release --bin eelisp

./target/release/eelisp             # a REPL
./target/release/eelisp script.eelisp   # run a file
./target/release/eelisp --serve     # JSON-line RPC on stdin/stdout

Or use it inside an application: EEditor is a notes editor built on this engine, and the whole language is available from its REPL and from its keybindings.

Embedding it in your own Rust program takes about five lines — see embedding.

Where it came from

EELisp began as the scripting layer of a Mac editor written in Swift. This is a clean-break rewrite in Rust, done to fix the things that were wrong in the original — no tail calls, broken macro rest-parameters, no transactions, order-dependent behaviour — and to make the engine reusable rather than welded to one app.

The result is deliberately portable: no threads and no I/O in the library itself, no platform types in the value representation. One engine backs a desktop app, a phone app and a command line, and the same code is meant to reach WebAssembly.

MIT licensed. 74 tests. Source at github.com/santacroce-tech/eelisp-rs.