Expand description
lispexp — a pure-Rust reader (lexer + parser) for S-expression syntax across many Lisp dialects.
The crate is deliberately reader-only: it does not evaluate, expand
macros, or interpret the numeric tower. It reads source text into data — the
shape, positions, and reader-macro structure needed to statically analyze
Lisp code — and accepts a superset of what any one implementation’s reader
would, so it is a substrate for tools (linters, indexers, formatters), not a
validator (ADR-0030). See docs/design.md and docs/adr/ for the design and
the decisions behind it.
§Quick start
use lispexp::{parse, Options};
let parsed = parse("(define (square x) (* x x))", &Options::scheme());
assert!(parsed.errors.is_empty());
assert_eq!(parsed.data[0].head_symbol(), Some("define"));
assert_eq!(parsed.data[0].items().unwrap().len(), 3);The reader is fault-tolerant — a malformed form loses only itself and
recovery resumes at the next top-level form (ADR-0004) — so always inspect
Parsed::errors alongside Parsed::data. parsed.errors.is_empty() is a
usable “structurally clean” check.
§Choosing a dialect
There is one reader; a Dialect selects a preset of Options (ADR-0003).
lispexp never infers a dialect across files — pick one per input, e.g. by file
extension:
use lispexp::{Dialect, Options};
let options = match "core.clj".rsplit('.').next() {
Some("clj" | "cljs" | "cljc" | "edn") => Options::clojure(),
Some("scm" | "ss") => Options::scheme_superset(),
Some("el") => Options::emacs_lisp(),
_ => Options::for_dialect(Dialect::Scheme),
};Presets are a starting point: adjust individual fields by assignment afterwards (the settings are orthogonal, ADR-0006).
§Two layers
Both layers sit over the same Options (ADR-0015):
parse— builds theParseddatum tree. The common entry point.lex/Lexer— a linear token stream that tiles the input (every byte is covered), for consumers like a parinfer backend that need lexical state, not a tree. The tree drops comments and whitespace, so a trivia-sensitive tool reads those here and correlates by byteSpan.
The lexer’s EOF contract: tokens always tile the input, and an unterminated
construct at end-of-input is reported as one TokenKind::Unterminated token
carrying the lexical state it was in, rather than an error or a truncated
token stream.
§Static-analysis utilities
Built on the tree, each opt-in and reader-only:
walk— a pruning visitor that classifies each node asClass::CodeorClass::Data, so a tool descends into code and skips quoted data;walk_regionsrefinesDatainto prunableRegion::SealedDatavs. porousRegion::PorousDataso aSkipnever drops quasiquoted code, andcode_nodesis a fixed-policy pre-order iterator over just the code nodes (ADR-0026).annotate— tags definition forms (name, arglist, docstring, body, method dispatch) across dialects, from a bundled per-dialect core plus a spec harvester that learns a project’s own def-macros (ADR-0019/0020, ADR-0031/0032).indent— harvests Emacs Lisp indent specs into asymbol → IndentSpectable (ADR-0022).detect— opt-in, content-aware dialect detection (extension registry +#lang/shebang/structural signals) that picks anOptionsfor you; the reader itself stays passive (ADR-0034, ADR-0012).parse_form_at— reads exactly one top-level form at a byte offset, for incremental re-validation after an edit (ADR-0023).LineIndex— maps byte offsets to 1-based (line, byte-column) (ADR-0024).
Modules§
- annotate
- Definition-form annotation (ADR-0019).
- detect
- Opt-in, content-aware dialect detection (ADR-0034).
- indent
- Indent specs: a first-class
symbol → IndentSpectable (ADR-0022).
Structs§
- Block
Comment - A block-comment delimiter pair (ADR-0007).
- Char
Roles - The per-dialect table of reader-macro prefix glyphs (ADR-0016).
- Code
Nodes - A pre-order iterator over the
Class::Codenodes of a datum forest, created bycode_nodes. - Datum
- A single parsed unit of S-expression syntax, annotated with its source span
and 1-based start line. Borrows
&'a strslices from the source (ADR-0008). - FormAt
- One top-level form read at or after a byte offset (ADR-0023).
- Lexer
- The lexer. Implements
IteratoroverTokens. - Line
Index - A precomputed line/column index over a source string (ADR-0024).
- Options
- Reader/lexer configuration. Construct via a preset such as
Options::schemeorOptions::clojure, then adjust fields if needed. - Parse
Dialect Error - The error returned by
Dialect’sFromStrimpl when the input names no known dialect. - Parse
Error - A non-fatal parse diagnostic. The reader is fault-tolerant (ADR-0004): it returns a partial tree plus a list of these, resynchronizing at the next top-level form.
- Parsed
- The result of reading a source string. Borrows the source (ADR-0008).
- Span
- A byte range into the source string:
[start, end). - Token
- One lexeme. Tokens tile the input — every byte belongs to exactly one token, whitespace and comments included (ADR-0015). Carries only a span; text is recovered by slicing the source.
Enums§
- Char
Syntax - How character literals are introduced.
- Class
- Whether a subtree is executable code or inert data (ADR-0026).
- Datum
Kind - The shape of a
Datum. - Delim
- Delimiter shape. The reader records shape; the consumer assigns meaning.
- Delim
Role - The role of a bracket pair
[]or{}in a dialect. - Dialect
- A named dialect. Presets are constructed via
Options. - Error
Kind - A structured classification of a parse diagnostic (ADR-0023).
- Hash
Bracket - What
#[opens in a dialect. The single#[dispatch has several mutually-exclusive meanings across dialects; this enum makes the choice explicit (likeHashParenfor#() instead of leaving it to the implicit order of competing flags. Distinct from the bare-[delimiter role (Options::square), which still applies when this isHashBracket::None(e.g. Racket/Emacs#[...]hash-vectors). - Hash
Paren - What
#(means in a dialect. - Notation
- Whether a reader-macro form appeared in shorthand or long-hand call form.
- Prefix
- The role of a reader-macro prefix. The glyph that triggers each is a per-dialect table (ADR-0016).
- Region
- A refinement of
Classthat splitsDataby whether it is safe to prune (ADR-0026). - Terminator
- The line-terminator kind at the end of a line (ADR-0024). Breaks are
\nand\r\nonly, so this is a closed set — matched exhaustively, likecrate::Class. - Token
Kind - The classification of a
Token. - Unterminated
Kind - The lexical state an
TokenKind::Unterminatedtoken was in when input ran out. - Walk
- What the visitor callback asks the walker to do next.
Functions§
- code_
nodes - Iterate the
Class::Codenodes ofdatain pre-order — the read-only, fixed-policy counterpart towalk. It always prunes sealed data and descends porous quasiquote templates, so nested unquoted code is reached but quoted data is never yielded. - lex
- Lex
sourceunderoptions, yielding a token stream that tiles the input.sourcemust be at mostu32::MAXbytes (Spanstoresu32offsets). - parse
- Parse
sourceunderoptionsinto a datum tree. Never panics — including on pathologically nested input: list/hash nesting deeper than a fixed depth cap (ADR-0028) stops descending, reportsErrorKind::DepthLimitExceededonce, and skips the too-deep subtree (ADR-0004), keeping prior siblings.sourcemust be at mostu32::MAXbytes (Spanstoresu32offsets). - parse_
form_ at - Read exactly one top-level form at or after byte offset
start(ADR-0023). - walk
- Walk each top-level datum in
data, invokingvisit(datum, class)in pre-order. When the callback returnsWalk::Skip, that datum’s children are pruned;Walk::Descendrecurses;Walk::Stopaborts the whole walk. Top-level data start asClass::Code. - walk_
regions - Like
walk, but the callback receives a three-wayRegioninstead of the binaryClass, so pruning is safe:Region::is_prunabletells you whetherWalk::Skipwould lose code.