Stable language core

Lisp-shaped, value-oriented, ECMAScript-bound.

Eliscript keeps the programmable shape of Emacs Lisp while adding persistent data, explicit modules, open protocols, and semantics designed for modern JavaScript runtimes.

Value model

EliscriptECMAScript
nilnull
undefinedundefined
t / falsetrue / false
numbers and stringsECMAScript numbers and strings
evaluated keywordsinterned immutable Keyword values
vector expressionspersistent immutable Vectors
list expressions and quoted listspersistent linked Lists
map and set literalspersistent HAMT Maps and Sets
#queue [...]persistent FIFO Queue
(js-array ...) / (js-object ...)explicit native containers

Truthiness is Lisp-like: only false, null, and undefined are false. Zero and empty strings remain true.

Implemented forms

defvar / defconst

Top-level mutable and immutable bindings.

defun / defn

Named functions with single or argument-count-dispatched multi-arity bodies.

defportable

Worker entries with a statically checked immutable dependency closure.

defmulti / defmethod

Named multimethods and source-ordered method definitions over the portable multimethod library.

defprotocol / extend-*

Open protocols with exact-type, host-category, and default implementations over the canonical protocol library.

lambda / fn

First-class anonymous functions with fixed and variadic multi-arity clauses.

letfn

Lexically scoped local functions with direct, mutual, multi-arity, portable, and async recursion.

let / let*

Parallel and sequential lexical bindings.

Compile-time macros

(defmacro once (form)
  `(let ((value$ ,form)) value$))

(defun load-once (value)
  (once (+ value 1)))

defmacro definitions are processed in source order and removed before emission. Backquote, comma, &rest, and &body are supported. gensym and trailing-$ template symbols create deterministic private bindings, while ordinary template symbols deliberately resolve at the call site. Every expanded form is checked by the lexical analyzer.

Both compiler generations interpret the same deterministic macro subset. Unlisted host functions, editor state, files, environment variables, and processes are unavailable unless a future explicit compiler capability exposes them.

ECMAScript modules

(import "react" :default React useState)
(import "library" :as Library)
(import "side-effect-only")

(export Component helper)
(export-default Component)

One Eliscript source file emits one ESM file. The project builder recursively resolves root-contained relative source imports, preserves their directory tree, rewrites them to .mjs, emits Source Maps, and records deterministic graph identity. Package and JavaScript specifiers remain host-defined.

Persistent collections

ValueImplementation
ListSingly linked nodes with O(1) front operations and exact suffix sharing
Vector32-way bit-partitioned trie with a tail and O(log32 n) indexed updates
Map / SetHAMT with bitmap, dense array, collision nodes, and path copying
QueuePersistent front/rear Vectors with iterative million-value behavior
Sorted Map / SetStructurally shared AVL trees with custom comparators and range queries

All collection families participate in value equality, hashing, metadata, canonical data text, generic lookup, reduction, construction, stack, and reversible traversal protocols where applicable. Records extend persistent Map behavior without surrendering their declared type identity.

Owner-token Transient Vector, Map, and Set builders support efficient isolated construction. persistent! completes the builder once; later mutation or reuse is rejected, and static compiler analysis prevents transient ownership from escaping trusted construction scopes.

Portable sequences

(import "../../stdlib/sequence.eli" map filter reduce range)

(map (lambda (value) (* value 2)) (range 1 5))

The standard library is written in Eliscript and includes persistent hash and AVL-sorted collections, protocols, replayable and memoized lazy sequences, pull transduction, immutable tree rewriting and zipper editing, text, numeric, Result, JSON, state, and host-interop modules without adding application-framework dependencies.

The maintained stdlib/core/seq.eli algorithms are also written in Eliscript. They accept persistent, native, and externally extended reducible values through collection protocols, return persistent Vectors, add remove, and use exact early termination for bounded transforms and searches. Adjacent Eliscript modules expose protocol, collection, transducer, and transient APIs while the optimized dispatch substrate remains in JavaScript.

Portable text

(import "../../stdlib/text.eli" contains? strip-prefix trim)

(trim (strip-prefix "#" "# article "))

Thirteen functions provide literal matching, end-exclusive slicing, prefix and suffix removal, ASCII-boundary trimming, blank checks, joining, and repetition. They are written in portable Eliscript without regular expressions or host string methods. Indices follow ECMAScript UTF-16 code units rather than locale-aware grapheme clusters.

Immutable objects

(import "../../stdlib/object.eli" assoc pick)
(import "../../stdlib/data.eli" index-by)

(pick (assoc (object :name "Eliscript") :runtime "JavaScript")
      [:name :runtime])

Eleven object functions enumerate own keys, test own properties, associate without mutation, remove and merge keys, transform values, and select fields. The data module adds keyed lookup, grouping, and counting by importing object primitives through explicit import-portable edges. Graph-aware portable builds verify those targets and prune every generated module.

The runtime-backed stdlib/core/data.eli surface returns value-semantic persistent Maps and persistent Vector groups, adds frequencies, and uses transient builders for efficient final construction.

Recursion and constant stack

(defun sum-to (n total)
  (if (= n 0)
      total
    (recur (1- n) (+ total n))))

recur is checked at compile time and lowers self recurrence or a lexical loop to an iterative transfer with constant JavaScript stack use. The standard-library trampoline runs returned zero-argument thunks and supports open or mutual recursion with a fixed stack.

Ordinary calls in tail position remain ordinary JavaScript calls. General self and mutual tail-call optimization is recorded as Draft specification 0180 and receives no current implementation credit.

Intentional differences

  • Lexical scope is mandatory; dynamic scope is not inherited.
  • Numbers follow ECMAScript behavior rather than Emacs integer semantics.
  • Lists, Vectors, Maps, Sets, Queues, and Records are distinct persistent value categories.
  • false is distinct from nil.
  • Modules, objects, promises, and browser APIs are explicit language concerns.

Compatibility is a starting point, not a ceiling. Later versions may add more expressive forms when Emacs Lisp syntax or semantics would hold the language back.