Rebuilding Clojure from behavior
A graded clean-room study of the abstractions that outlasted twenty years of implementation churn

Table of Contents

What this is

A research initiative. The instrument is a Clojure reimplementation; the output is a paper.

Clojure's source has been under version control since 2006-03-24, three years before its first stable release. Twenty years of that history is public, and it separates cleanly into two populations: a small set of abstractions that were right early and never moved, and a larger body of implementation that has been rewritten continuously underneath them. This project rebuilds the first population, under a protocol that forbids reading the second, to find out what forced their shape.

Not a goal: a working Clojure. Several exist and all are better than this one will be. A complete Clojure obtained by reading core.clj is a null result reported as a success, and it is the only outcome that would make the whole thing worthless.

Siblings

Two, and the distinction matters because "what survived" means different things at different scales.

  • What survived: Clojure asks which of Clojure's ideas the industry absorbed without adopting the language: immutable-by-default data, REPL-first workflow, data-as-interface and EDN, spec contracts, managed state. Diffusion outward. This page asks which of Clojure's own abstractions survived its own churn. Survival inward. F7 argues the two are connected and says how to find out if they are not.
  • History as retrieval context surveys the literature this measures against. One bibliography, kept there.

Why Clojure

Four properties, and few systems have all of them.

  • Long public history with an early start. Version control predates the first public release by nineteen months. The construction period is visible, not just the shipping period.
  • An explicit kernel/library boundary. Java core plus clojure/core.clj. Most of what a user touches is written in the language itself, so the boundary is a real seam rather than an analytical convenience.
  • Runnable artifacts at every release. A behavioral oracle exists for each milestone without reconstructing a build.
  • A small special-form set. The irreducible core is roughly fourteen forms. Bounded enough that "did we get it" is answerable.

The fourth is what makes the study finishable and the second is what makes it interesting.

Questions

id question status
RQ1 What share of the public surface is reconstructible from a running artifact alone? open
RQ2 Which features require which grade of source access, and is the ranking stable? open
RQ3 Does a history-derived construction order differ from a designed one? pilot observed
RQ4 What does commit-level sequence buy over release-level? partial
RQ5 How much does the operator's prior exposure to Clojure contaminate RQ1 and RQ2? open

RQ3 and RQ5 are answerable before any language code exists, and both are cheap. A paper carrying only those two, the method, and the threats section is still a paper. An implementation without them is a hobby.

Findings so far

All figures below measured 2026-08-16 against github.com/clojure/clojure at HEAD 2026-07-28, via a metadata-only clone. Epistemic labels per the house scale: [E] measured here, [P] sourced, [H] hypothesis, [I] inference.

F1. The abstractions survived; the implementations did not [E]

The central observation. Deliberately not the page's title: an initiative URL named after a day-one finding locks in the conclusion, which is the same failure the claim-table freeze exists to prevent. The URL states the method; findings keep their own assertion-titled pages underneath.

population files representative
src Java/Clojure at HEAD 229  
first seen before 2008 77 33.6% of the surviving source
born pre-2008, untouched since 2015 22 almost entirely interfaces

The frozen set: Sequential, Named, IPersistentStack, IPersistentList, IMapEntry, IObj, IPersistentCollection, IRef, IPersistentVector, Associative, IndexedSeq, Binding, Box.

Against the same-vintage implementation files, by lifetime touch count:

file touches span
Compiler.java 585 2006-09-25 → 2025-07-03
RT.java 340 2006-06-11 → 2026-07-21
LispReader.java 138 2006-06-12 → 2024-04-12
Var.java 82 2006-06-11 → 2022-06-09
PersistentHashMap 73 2007-06-18 → 2022-06-09
PersistentVector 66 2007-07-05 → 2023-08-24
Sequential.java 2 2006-08-04 → 2008-12-14

Two touches against five hundred and eighty-five, over the same two decades. Clojure's stated preference for abstractions over implementations is usually read as a design argument; it is also a measurable property of its own repository [I].

Caveats, checked rather than assumed. The 2008-12-14 terminal date shared by many interfaces is commit cad4fcf1, the Eclipse Public License move, which touched 95 files in one go [E]. It is a header change, so the true freeze date for those files is earlier and F1 is stronger than the raw dates suggest. Separately, src/jvm/clojure/asm/** is vendored ASM rather than Clojure and must be excluded from any churn ranking; its uniformly low touch counts are dependency bumps.

Refutation. If, after excluding vendored paths and bulk commits above the 50-file threshold, the interface population's median touch count is within a factor of three of the implementation population's, F1 is refuted and the project's object dissolves. [H] that it will not be; the observed gap is two orders of magnitude.

F2. Release tags start a third of the way through the work [E]

boundary commits before share of history
first public release, 2007-10-17 479 13.1%
first downloadable tarball, 20081217 1,162 31.8%
first git tag, 1.0 @ 2009-05-04 1,316 36.0%

Total history is 3,659 commits spanning 2006-03-24 to 2026-07-28 [E]. Activity peaks inside the window the release artifacts cannot see: 588 commits in 2008 against 261 in 2006.

Consequence for method: a release-tag sequence is blind to the construction period. A commit-level sequence is not, and holding it costs nothing in source access. See F4.

F3. The construction order is not the designed order [E], pilot for RQ3

Clojure's first two weeks, from commit subjects alone:

2006-03-24  created IDEA project
2006-03-25  added AMap
2006-03-25  added AFn, RT
2006-03-27  implemented IFn for attr access
2006-03-28  first cut of numbers
2006-03-29  added Keyword
2006-03-29  added keyword and global ns
2006-04-02  Initial commit of CLI runtime

Data structures and the function protocol first. Keywords and namespaces in week one. Execution last.

The build plan for this project, written before the history was consulted, put the reader first, because that is the order interpreter tutorials use. The inversion was found by accident, which is itself evidence about how readily the pedagogical order is mistaken for the natural one [I].

RQ3 formalizes this: propose a build order cold, then measure rank correlation against the actual commit order. Refuted if τ > 0.8, which would mean the history told us nothing we would not have guessed.

F4. Sequence and source separate at the git layer [E]

A git clone --filter=blob:none --no-checkout of clojure/clojure is 4.0 MB and contains every commit, tree, and tag with zero file contents. Holding the full history does not imply holding the source.

This falsifies the assumption two of the project's early decisions rested on, and both reverse. It is the same fact/claim separation that governs any provenance index: metadata is deterministic and cheap, content is where the leak lives.

F5. Commit prose is mostly free of implementation detail [E]

Applying a leak filter (internal type names, plus a CamelCase test for Java type mentions) to all 3,659 commit subjects flags 17.0% (12.5% by lexicon, 6.6% by CamelCase, ORed), measured 2026-08-16 via tools/leak_grade.py. The remaining 83.0% carry sequence and intent without naming an internal.

Mean subject length is 51.2 characters [E]. The early messages are terse to the point of being nearly pure sequence signal.

The pre-public era runs about three points hotter than post-1.0, which is inconvenient: it is the era the sequence is most wanted for. The filter is a keyword regex with unmeasured recall, so F5 is provisional pending a hand-labelled sample [H].

F6. Primary sources disagree about when 1.0 shipped [E]

source date
git tag 1.0 creator date 2009-05-04
Wikipedia version table 2009-05-04
clojure.org/releases/downloads_older Jul 2009

Two agree; one does not. Most likely a file timestamp or a re-upload rather than the release event [I], but the point stands regardless: the sequence had no error bar and was never labelled with one.

Standing rule: every date and ordering claim on this site cites which of the three sources it came from. Where they conflict, prefer the tag graph, as the only one written at the time by the process that made the thing.

F7. What froze inside may predict what travelled outside [I]

The frozen set in F1 is not an arbitrary pile of interfaces. Sequential, Associative, IPersistentCollection, IPersistentVector, IMapEntry, IObj, Named, and IndexedSeq are the layer that lets any collection be seq'd, assoc'd, counted, and annotated without a caller knowing its representation. That layer is data-as-interface, which the sibling essay names as one of the Clojure ideas that travelled furthest outside the language.

Conjecture: internal stability and external diffusion are one property observed at two scales. An abstraction that never needed to change is one that was right about a problem other people also had.

The counterexample is what makes this falsifiable instead of tautological. REPL-first workflow travelled very widely and its implementation never stabilized at all: Compiler.java took 585 touches and RT.java 340 over the same two decades, measured 2026-08-16. If the correspondence were tight, the most-diffused idea would sit on the most-frozen code. It sits on the two churniest files in the repository.

Refutation. Rank Clojure's ideas by external adoption, using whatever evidence the sibling essay settles on, and rank its subsystems by internal churn stability via tools/survival.py. Test rank correlation. Refuted if τ ≤ 0.3, or if discordant pairs outnumber concordant ones. [H] that it lands in between, with workflow ideas as the systematic exception, because a workflow diffuses as a practice rather than as an interface and practices carry no code to freeze.

Inherited threat. F7 takes on the weaknesses of both pages. External adoption has no clean measure, so whichever proxy the sibling essay adopts becomes F7's independent variable and its softest point. Do not report F7 without naming that proxy.

Method in brief

The contribution is intended to be the protocol, independent of the Clojure result. Sources form a seven-grade ladder, and the grades are separated by tooling rather than by intention.

grade content rule
G0 the running artifact: values, arities, docstrings, exception classes free
G1 commit dates, paths, tags, diffstat free
G2 filter-passed commit prose free; filter is committed
G3 design rationale free; cited
G4 filter-flagged prose, commit bodies, tickets logged before reading
G5 implementation exposition waiver
G6 source at tag T waiver, minimal

The oracle is a release jar with every .clj file stripped out. Clojure boots from its AOT'd class files, so (doc map) still answers and (source map) has nothing to read. The boundary is a zip operation, not a promise.

The G5 line runs through the secondary literature too, not only the source. The HOPL IV history is G3 throughout except §3.4.1 on persistent data structures, which names hash array mapped tries, the branching factor of 32, path copying, and System.arraycopy, and §3.6.2 on the STM, which gives MVCC and the ensure mechanism for write skew [P]. Both are enough to build from and both sit behind a waiver.

The waiver log is the primary output. Each entry names a feature that black-box probing could not reach and how close it got. That log is RQ2's answer directly. A run with no waivers means the budgets were too generous; a run that is all waivers refutes the method. Neither is failure. The failure is a run where the waivers happened and were not written down.

digraph structure {
  rankdir=LR
  bgcolor=white
  node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10]
  edge [fontname="Helvetica", fontsize=9, color="#334155", fontcolor="#334155"]

  H   [label="clojure/clojure\nblob-less clone", shape=cylinder, fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"]
  J   [label="stripped release jar",             shape=cylinder, fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"]
  SEQ [label="sequence:\nwhat to build next", fillcolor="#f1f5f9", color="#334155", fontcolor="#334155"]
  ORC [label="oracle:\nis it right",          fillcolor="#f1f5f9", color="#334155", fontcolor="#334155"]
  B   [label="rebuild the survivors", fillcolor="#dcfce7", color="#15803d", fontcolor="#15803d"]
  R   [label="results table", shape=cylinder, fillcolor="#fef9c3", color="#a16207", fontcolor="#a16207"]
  W   [label="waiver log",    shape=cylinder, fillcolor="#fee2e2", color="#b91c1c", fontcolor="#b91c1c"]
  P   [label="paper", fillcolor="#ede9fe", color="#6b21a8", fontcolor="#6b21a8"]

  H   -> SEQ [label="G1"]
  J   -> ORC [label="G0"]
  SEQ -> B
  ORC -> B
  B   -> R [label="gate passes"]
  B   -> W [label="budget exhausted"]
  W   -> R
  R   -> P
}

structure.png

Open

  • RQ5 cold-probe baseline. The operator and the model in the loop have both read Clojure. Unenforceable, so it must be measured rather than assumed. Highest priority; it conditions the interpretability of everything else.
  • Hand-labelled sample for the F5 filter, to put precision and recall on it.
  • Churn ranking recomputed with vendored paths and bulk commits excluded, to test F1 properly.
  • Claim table freeze. Until it is frozen, no result here is pre-registered and should not be cited as though it were.

Threats

Stated before results rather than after, where they act as a design constraint instead of a defence.

id threat residual
T1 the model in the loop has read Clojure's source large
T2 single target, single language large
T5 single operator, no blinding, no inter-rater check large
T3 tree-walking evaluator: compilation-entangled features read as unreconstructible when they are merely un-targeted medium
T4 Python host: JVM-typed vars leave the comparable subset medium
T6 the leak filter is a keyword regex of unmeasured recall medium
T7 waivers fire on operator-declared budget exhaustion, so the rate partly measures patience medium

T1 and T5 together mean the honest framing is a case study: one operator with prior exposure, one target, under a graded protocol. Saying so in the title costs nothing and forecloses the obvious objection.

Reproduction

Every figure on this page is produced by an instrument in the repo, not quoted. The code gate recomputes them; a hand-typed number fails it.

# Regenerates every measurement on the initiative index.
set -euo pipefail
SEQ="${SEQ:-vendor/sequence/clojure}"

[ -d "$SEQ" ] || tools/clone_sequence.sh "$SEQ"

echo "== F2/F4: history extent and clone size =="
du -sh "$SEQ/.git"
git -C "$SEQ" rev-list --count HEAD
for d in 2007-10-17 2008-12-17 2009-05-04; do
  printf '  before %s: %s\n' "$d" "$(git -C "$SEQ" rev-list --count --before="$d" HEAD)"
done

echo "== F3: construction order, first two weeks =="
git -C "$SEQ" log --reverse --format='%ad  %s' --date=short | head -12

echo "== F5: G2/G4 split =="
python3 tools/leak_grade.py "$SEQ"

echo "== F1: survival and churn =="
python3 tools/survival.py "$SEQ" --exclude 'src/jvm/clojure/asm/' --max-files-per-commit 50

echo "== F6: sequence triangulation =="
git -C "$SEQ" for-each-ref --sort=creatordate \
  --format='%(creatordate:short) %(refname:short)' refs/tags | head -3

tools/survival.py does not exist yet. Writing it, with the vendored-path and bulk-commit exclusions applied, is the first task that changes a number on this page.

Updating this page

For whoever grinds on this next, agent or otherwise. This page is the initiative's front door and its running state; it is not a snapshot.

  1. Findings are append-only. New observations get the next F number. Never renumber. A finding that turns out wrong keeps its number and gains a REFUTED marker with a pointer to the finding that replaced it. Negative results are the cheapest knowledge here and deleting one destroys the audit trail.
  2. Every figure cites its instrument. If scripts/reproduce-index.sh does not produce a number, the number does not belong on this page. Adding a figure means adding the instrument first.
  3. Update the header on every change. #+LAST_MEASURED carries the date and the upstream commit the figures were taken against. Numbers drift as clojure/clojure moves; an undated figure is unfalsifiable.
  4. Epistemic labels are mandatory. [E] only for something measured by a committed instrument on a recorded date. Reasoning about what the data would show is [I], not [E], however confident it feels.
  5. Refutation conditions go on findings, not on the page. A finding without one is an observation; say "observation" and move on rather than dressing it as a result.
  6. Status line changes when the claim table freezes. Before the freeze nothing here is pre-registered. After it, #+STATUS names the tag and findings become citable as pre-registered.
  7. Do not add a fact here that is not in spec.org. The spec governs; this page is derived and public. Where they disagree, the spec wins and this page is the bug.
  8. Findings that touch the sibling essay get cross-linked both ways. F7 is the current one. A claim that lives on one page and is assumed on the other is how two documents drift into contradicting each other, and neither author notices.

Assertion-titled findings live at /research/2026-rebuilding-clojure-from-behavior/<claim>/ in the house convention: the page states the claim and the URL states the claim, while the initiative URL stays method-shaped so it survives the answer moving. That is why this page is not at /research/2026-what-survived-clojure/, which belongs to the diffusion essay and to a different question at a different scale.