Taxonomy of Lisp Form Shapes
What we did
Analyzed 10,455 source files across 31 open-source repositories to build a
data-driven taxonomy of what structural forms actually appear in
Lisp/Scheme/Racket/Clojure code.
Repositories analyzed
Common Lisp (12 repos, 2,926 files):
| Repo | Files | Domain |
|---|---|---|
| SBCL | 1,372 | Compiler |
| McCLIM | 538 | GUI framework |
| Mezzano | 330 | Lisp OS |
| Coalton | 207 | Typed FP on CL |
| Petalisp | 123 | Parallel computing |
| Serapeum | 88 | Utility library |
| CFFI | 83 | FFI |
| lparallel | 64 | Parallelism |
| numcl | 40 | Numerics |
| Hunchentoot | 31 | Web server |
| cl-ppcre | 25 | Regex |
| Caveman | 25 | Web framework |
Racket (3 repos, 5,424 files):
| Repo | Files | Domain |
|---|---|---|
| Racket | 3,136 | Language runtime |
| Typed Racket | 1,900 | Type system |
| Redex | 388 | Semantics modeling |
Clojure (16 repos, 2,080 files):
| Repo | Files | Domain |
|---|---|---|
| XTDB | 851 | Database |
| Pedestal | 278 | Web framework |
| Leiningen | 192 | Build tool |
| Clojure | 142 | Language core |
| Malli | 101 | Schema validation |
| Reagent | 95 | React wrapper |
| Ring | 82 | HTTP abstraction |
| DataScript | 70 | In-memory Datalog |
| re-frame | 54 | State management |
| Mount | 54 | State management |
| core.async | 50 | CSP channels |
| Meander | 49 | Pattern matching |
| HoneySQL | 29 | SQL DSL |
| Integrant | 13 | Lifecycle management |
| Compojure | 12 | Routing |
| Component | 8 | Dependency injection |
Methodology
- Mechanical extraction: Two Python parsers (one for CL/Racket/Scheme,
one for Clojure with[]/{}as data not forms) extracted all head
symbols from s-expressions, filtering against blacklists of ~200+ standard
forms per language and excluding calls where all arguments are atoms. - Agent classification: 517 haiku-level LLM agents (457 for CL/Racket,
60 for Clojure) each received a batch of source files plus candidate head
symbols. Each agent read the actual code and classified forms by name,
description, and shape, filtering out false positives. - Deduplication: Entries were merged by form name, keeping the best
description and tracking all source files. - Review: Opus-level agents reviewed each language's taxonomy, removing
standard builtins that leaked through, merging package-qualified
duplicates, and fixing shape templates. CL/Racket: 986 raw 737 clean.
Clojure: 220 raw 35 clean (aggressive filtering since most Clojure forms found were standard builtins from spec, core.async, etc.). - Merge: The cleaned taxonomies were merged into a single file with 772
entries tagged by language (CL, Racket, Clojure, or multi). - Categorization: Forms were categorized by pure structural shape
(argument layout), ignoring domain/purpose.
Results: Structural shapes
18 distinct argument-layout patterns cover all 772 forms. The 8 structurally
interesting ones (as opposed to "it's just a function call") are:
| Shape | Count | Template | What it is |
|---|---|---|---|
| Config-Group Then Body | 118 | (OP (config...) body...) |
Dynamic context establishment. The with-X pattern. All 3 languages. |
| Name, Param-List, Body | 72 | (OP name (params...) body...) |
Defining callable entities. The defun/defmacro pattern. |
| Binding-List Then Body | 47 | (OP ((var expr)...) body...) |
Variable binding. The let/for pattern. |
| Flat Alternating Pairs | 37 | (OP expr key1 val1 key2 val2 ...) |
Dispatching or rewriting by flat pattern/result pairs. Meander, case-enum, re-frame reg-sub. |
| Expr Then Clauses | 33 | (OP expr [pattern body...]...) |
Dispatching on shape with wrapped clauses. The match/case pattern. |
| Body Only | 35 | (OP body...) |
Wrapping execution context with no visible config. The progn/atomically pattern. |
| Expr Then Body | 31 | (OP expr body...) |
Guarded/conditional execution. The when/unless pattern. |
| Annotated Param-List, Body | 8 | (OP name :- type [param :- type ...] body...) |
Definition with inline type annotations. Typed Racket : and Malli :- forms. |
The "Flat Alternating Pairs" and "Annotated Param-List" shapes are new
findings from the Clojure analysis that were not present in the CL/Racket
data alone.
Flat Alternating Pairs deserves special attention: where CL/Racket
pattern-matching forms wrap each clause in a sublist —
(match expr (pat1 body1) (pat2 body2)) — Meander's m/match and
m/rewrite lay them flat: (m/match expr pat1 result1 pat2 result2).
This also appears in case-enum, zmatch, and re-frame's reg-sub
with :<- markers. It's structurally distinct from both "Expr Then Clauses"
(which has sublists) and "Variadic Args" (which has independent arguments).
The remaining shapes are less structurally distinctive:
| Shape | Count | Template | Notes |
|---|---|---|---|
| Variadic Args | 155 | (OP arg...) |
Catch-all for forms with variable positional args. Many are borderline function calls. |
| Fixed Positional Args | 117 | (OP arg1 arg2 [arg3]) |
2-4 positional arguments with no body or clauses. |
| Name Then Args | 105 | (OP name arg1 arg2 ...) |
Name followed by metadata/options. Definition forms without body. |
| Name Then Clauses | 5 | (OP name clause...) |
Named dispatch forms. |
| Name Then Body | 3 | (OP name body...) |
Named body forms (rare). |
| Misc / Special Syntax | 14 | varies | Non-s-expression syntax or unusual layouts. |
| Misc Compound | 3 | varies | Multi-section forms. |
| Bare Atom | 2 | SYMBOL |
Used as-is, not in a list. |
| Nullary | 1 | (OP) |
No arguments at all. |
Distribution
Results: Purpose categories
A separate categorization by purpose/domain (not purely structural) found 25+
categories. The top 12, now including Clojure-originated categories:
| Category | Count | What it covers |
|---|---|---|
| with-context-body | 151 | Resource management, parameter scoping, locks, GUI containers (all langs) |
| named-entity-definition | 78 | Domain-specific defs: VOPs, commands, interceptors, routes, states |
| module-import-export | 63 | require, provide, contract-out, for-label, etc. |
| ffi-foreign-interface | 57 | defcfun, define-alien-routine, cenum, cstruct, etc. |
| iteration-comprehension | 51 | for/list, for/fold, do-sequence, dolines, etc. |
| test-assertion | 47 | test, is, check-equal?, with-test, deftest, thrown-msg?, etc. |
| handler-registration | 39 | re-frame reg-*, Pedestal interceptors, Redex reductions (Clojure+Racket) |
| function-lambda-definition | 38 | defun/type, defpun, define-move-fun, mx/defn, r/defc, etc. |
| pattern-match-rewrite | 33 | match, m/match, m/rewrite, zmatch, case-enum, error-dispatch |
| contract-type-spec | 30 | ann, cast, inst, or/c, case->, provide/cond-contract, etc. |
| syntax-macro-definition | 25 | define-syntax-class, define-literal-set, define-conventions, etc. |
| concurrency-synchronization | 23 | atomically, with-mutex, place, future, barrier, etc. |
The handler-registration and pattern-match-rewrite categories are new
entries driven primarily by Clojure data:
- Handler registration (
reg-event-db,reg-sub,reg-fx,
definterceptor,defroutes,defstate) is the dominant macro pattern in
Clojure web/UI code. The shape is typically
(reg-X id [middleware...] handler-fn)— a name, optional interceptor
chain, and a handler function. This pattern barely appears in CL/Racket
because those ecosystems tend to use generic functions or method dispatch
instead of global registries. - Pattern-match-rewrite merges CL/Racket's
match/caseforms with
Meander'sm/match/m/rewriteand XTDB'szmatch. Notably, Meander's
forms use flat alternating pairs rather than wrapped clause sublists, which
is a structural difference from the Racket/Redex pattern-matching tradition.
Clojure-specific forms (35 entries)
The Clojure analysis added 35 domain-specific forms not found in CL/Racket.
Most standard Clojure forms (spec, core.async, test.check) were filtered as
builtins, leaving genuinely project-specific macros:
| Domain | Forms |
|---|---|
| re-frame | reg-event-db, reg-event-fx, reg-sub, reg-fx, reg-cofx, inject-cofx, ->interceptor, path, after, trim-v, debug, unwrap |
| Meander | m/match, m/rewrite, m/search, m/app, m/keyword |
| Pedestal | definterceptor, error-dispatch, defroutes, routes-from, from-cache |
| Mount | defstate |
| Malli | mx/defn (schema-annotated function definition) |
| XTDB | zmatch, case-enum, with-crash-log, walk-expr, util/with-tmp-dirs |
| Reagent | r/defc |
| Misc | do-template, defroute, defservice, with-tap-output |
Known gaps and open questions
Things this taxonomy might be missing or underrepresenting:
Structural patterns that may be underrepresented
- Form-composition / threading:
nestfrom Coalton takes N wrapper forms
and a base expression, composing them:(nest f g h x)=(f (g (h x))).
This was lumped into "Variadic Args" but is structurally distinct — the
arguments aren't independent, they compose right-to-left. Clojure's->,
->>,some->,cond->threading macros are the most famous version of
this pattern but were blacklisted as standard forms. The threading/composition
shape deserves its own structural category — it appeared in both CL (via
nest) and Clojure (via the threading macros) but was underrepresented
because we treated the Clojure versions as builtins. - Accumulation / collection forms:
collectanddo-sequenceappeared but
the "collector callback" pattern (where you pass a name that becomes a
function you call to accumulate results) is a distinctive shape we may have
too few examples of. - Continuation / delimited control:
call-with-continuation-prompt,
abort-current-continuation, etc. appeared but the structural pattern
(prompt tag + handler + body) is distinctive and may deserve its own shape. - Place/slot mutation forms:
(setf (accessor obj) value)and CL's
define-setf-expanderhave a distinctive shape where the "place" is itself
a compound form. This is hard to extract mechanically becausesetfwas
blacklisted. - Reader macros / dispatch characters:
#',#(,#|...|#etc. are
structural patterns at the reader level that our s-expression parser couldn't
see. - Loop-clause DSLs: CL's
loopmacro anditerate'siterhave rich
internal clause languages (for x in list,collecting,finally, etc.)
that are invisible to our analysis because we blacklistedloopand treated
iter's clauses as atoms. - Method combination / CLOS protocol forms:
define-method-combination,
:before/:after/:aroundqualifiers,call-next-method— the MOP has
structural patterns around method dispatch that may be underrepresented. - Condition system:
restart-casewith its named restarts and
handler-bindwith condition-type/handler pairs are distinctive structural
patterns that were blacklisted.
Repo coverage gaps
- No Emacs Lisp repos (would add
defcustom,defadvice,cl-loop,
pcase,rx, and interactive-command patterns). - No R7RS Scheme library code (would add
syntax-rulespatterns, SRFI forms). - Alexandria and other utility libraries aren't directly included (their forms
appear via downstream usage in McCLIM, serapeum, etc., but we may miss some).
Questions for review
- Are there common macro patterns in production CL code that don't appear
here because they're too project-specific to show up in library code? - Is the "Config-Group Then Body" vs "Binding-List Then Body" distinction
real, or are these the same shape with cosmetic differences? (In both
cases, the first arg is a list of pairs/specs followed by body forms.) - Are there important shapes in Scheme's
syntax-rules/syntax-case
pattern language that we're missing because we treated Racket's
define-syntax-ruleetc. as atomic? - Does the distinction between "Name Then Args" (105 forms) and "Variadic
Args" (155 forms) hold up, or is the first-arg-is-a-name distinction
not structural enough? - The "Flat Alternating Pairs" shape is distinctive to Clojure/Meander
butcondin all Lisps has the same structure (test expr test expr ...).
Is this actually a more general pattern that we undercounted because
condwas blacklisted?