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
- Why Clojure
- Questions
- Findings so far
- F1. The abstractions survived; the implementations did not
[E] - F2. Release tags start a third of the way through the work
[E] - F3. The construction order is not the designed order
[E], pilot for RQ3 - F4. Sequence and source separate at the git layer
[E] - F5. Commit prose is mostly free of implementation detail
[E] - F6. Primary sources disagree about when 1.0 shipped
[E] - F7. What froze inside may predict what travelled outside
[I]
- F1. The abstractions survived; the implementations did not
- Method in brief
- Open
- Threats
- Reproduction
- Updating this page
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
}
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.
- Findings are append-only. New observations get the next
Fnumber. Never renumber. A finding that turns out wrong keeps its number and gains aREFUTEDmarker with a pointer to the finding that replaced it. Negative results are the cheapest knowledge here and deleting one destroys the audit trail. - Every figure cites its instrument. If
scripts/reproduce-index.shdoes not produce a number, the number does not belong on this page. Adding a figure means adding the instrument first. - Update the header on every change.
#+LAST_MEASUREDcarries the date and the upstream commit the figures were taken against. Numbers drift as clojure/clojure moves; an undated figure is unfalsifiable. - 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. - 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.
- Status line changes when the claim table freezes. Before the freeze
nothing here is pre-registered. After it,
#+STATUSnames the tag and findings become citable as pre-registered. - 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. - 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.