diff --git a/docs/rfc/0001-language-specification.md b/docs/rfc/0001-language-specification.md new file mode 100644 index 0000000..06222e7 --- /dev/null +++ b/docs/rfc/0001-language-specification.md @@ -0,0 +1,179 @@ +# RFC 0001 — A Specification for the Clojure Language + +- **Status**: Draft +- **Champions**: jolt maintainers +- **Created**: 2026-06-10 + +## Summary + +Produce a normative, implementation-independent specification of the Clojure +language — the reader, the evaluation model, the special forms, the data types +and their equality/hashing/ordering contracts, sequences and laziness, and the +`clojure.core` library — to the standard set by R7RS Scheme and the Racket +reference. The specification is developed *in this repository*, validated +continuously by jolt's executable conformance suite, and intended to be useful +to every alternative implementation (ClojureScript, jank, babashka/sci, +Basilisp, ClojureCLR, jolt). + +## Motivation + +Clojure has no specification. The language is defined by: + +1. the reference JVM implementation's source, +2. docstrings (frequently silent on edge cases), +3. community folklore (ClojureDocs examples, mailing-list threads), +4. each alternative implementation's reverse-engineering effort. + +Every alternative implementation independently re-derives answers to the same +questions — *what does `(nth coll nil)` do? is `(first "")` an error? does +`conj` on `nil` produce a list or vector? in what order does `reduce-kv` visit +a map?* — and they routinely diverge. The cross-dialect +[clojure-test-suite](https://github.com/jank-lang/clojure-test-suite) exists +precisely because these divergences are real and frequent: it currently +encodes hundreds of edge-case assertions that no normative document captures. + +Building jolt's self-hosted compiler forced us to answer these questions +one at a time (the conformance harness runs every behavior through three +independent execution paths and demands agreement). That work product — over +300 three-way-validated conformance assertions, ~1,500 behavioral spec cases, +and a frozen catalog of which forms are language vs. host — is the seed of a +specification, currently trapped in test files. This RFC proposes promoting it +into prose with normative force. + +### Why us / why now + +A useful spec needs an implementation that can *afford* to be strict. The +reference implementation can't adopt a spec retroactively without breaking +changes; an alternative implementation chasing drop-in compatibility can't +deviate from the reference even where the reference is accidental. jolt's +goals (self-hosted, minimal seed, multiple execution paths that must agree) +already require us to decide, for every form, *what the contract is* — we are +writing the spec anyway, in test form. The marginal cost of writing it down +properly is small; the value to the ecosystem is large. + +## Goals + +1. **Normative core**: reader grammar, evaluation model, all special forms, + data types with equality/hashing/ordering contracts, seq/laziness + contracts, namespaces/vars, and per-var entries for the portable + `clojure.core` surface. +2. **Executable**: every normative statement is paired with at least one + conformance test. The spec and the suite are maintained together; a spec + claim without a test is marked `unverified`. +3. **Host classification**: every `clojure.core` var is classified + **portable** (specified normatively), **host-dependent** (interface + specified, behavior host-defined — e.g. `slurp`, `*out*`), or + **JVM-specific** (documented as outside the portable language — e.g. + `bases`, `definline`, agents/STM as currently scoped). +4. **Versioned against reference Clojure**: each spec edition states the + reference version it describes (initially 1.12) and records *deliberate* + divergences (e.g. where reference behavior is accidental — these become + labeled "implementation-defined" with the reference behavior noted). +5. **Useful to other implementations**: no jolt-specific concepts in + normative text. jolt appears only in conformance-suite references. + +## Non-goals + +- Specifying the JVM interop surface (`proxy`, `gen-class`, `.`-forms beyond + their syntax), agents, STM refs, or the Java class hierarchy mapping. + These are catalogued as host/JVM surface, not specified. +- Specifying `clojure.spec`, `core.async`, or other contrib libraries + (candidates for later, separate documents). +- Changing the language. The spec describes Clojure as it is; divergence + decisions document reality, they don't invent semantics. +- Replacing clojure-test-suite — we contribute to it and cite it. + +## The specification document + +Lives in `docs/spec/`. Shape (mirroring R7RS chapters): + +| § | Document | Content | +|---|---|---| +| 0 | `00-front-matter.md` | conformance terms (RFC 2119), entry format, host classification | +| 1 | `01-evaluation.md` | evaluation model: forms, environments, vars, macroexpansion order | +| 2 | `02-reader.md` | lexical syntax: formal grammar, all reader macros, reader conditionals | +| 3 | `03-special-forms.md` | the special forms, one normative entry each | +| 4 | `04-data-types.md` | nil/booleans/numbers/strings/chars/keywords/symbols/colls; equality, hashing, ordering | +| 5 | `05-sequences.md` | the seq abstraction, laziness contract, realization boundaries | +| 6 | `06-namespaces-vars.md` | namespaces, vars, dynamic binding, resolution | +| 7 | `07-polymorphism.md` | protocols, records/types, multimethods, hierarchies | +| 8 | `08-macros.md` | defmacro, syntax-quote/hygiene, `&env`/`&form` | +| 9 | `09-core-library.md` | normative per-var entries for the portable surface | +| A | `coverage.md` | generated status dashboard: 694 vars × {specified, tested, implemented, classification} | + +### The normative entry format + +Every special form and library var gets an entry with these fields +(exemplars in `03-special-forms.md` and `09-core-library.md`): + +``` +### name +Signature(s), since-version +1. Semantics — numbered MUST/SHOULD statements +2. Edge cases — nil, empty, bounds, wrong-type behavior (normative) +3. Errors — what MUST throw, and when error type is implementation-defined +4. Examples — executable, drawn from ClojureDocs where community-validated +5. Conformance — test IDs that verify each numbered statement +``` + +### Evidence sources, in priority order + +1. **Differential testing** against reference Clojure 1.12 (the ground truth + for behavior questions). +2. **clojure-test-suite** (cross-dialect agreement = portable semantics; + dialect splits = host-dependent candidates). +3. **ClojureDocs export** (`clojuredocs-export.edn`, 694 core vars, 648 with + community examples) — examples become spec examples after verification. +4. **jank's language test corpus** (~800 per-form tests under + `test/jank/{form,call,metadata,reader-macro,syntax-quote,var}`) — the + per-construct granularity model for §2–§3 conformance. +5. Reference implementation source — last resort, for intent. + +## Current baseline (measured 2026-06-10) + +- ClojureDocs inventory: **694** `clojure.core` vars (648 with examples). +- jolt implements **572**; **373 (66%)** are exercised by the behavioral + spec/conformance suites; 139 implemented-but-untested. +- Initial classification of the 182 unimplemented: ~31 dynamic vars, ~20 + agents/taps, ~11 STM, ~15 special-form docs, ~105 to adjudicate + (genuinely-portable gaps spotted already: `compare`, `any?`, `update-keys`, + `update-vals`, `parse-long`, `parse-double`, `parse-boolean`, + `partitionv`, `splitv-at`, `macroexpand`, `time`, `with-redefs`). +- Conformance: 302 assertions × 3 execution paths; ~1,500 behavioral cases; + clojure-test-suite ≥ 4081/4707 assertions. + +## Process + +1. **Section by section**, in chapter order. §2 (reader) and §3 (special + forms) first — they are the smallest closed sets and jank's corpus gives + per-construct conformance shape immediately. +2. Each PR that adds/edits normative text MUST add or cite the conformance + tests for every numbered statement, and update `coverage.md`. +3. Divergences from reference Clojure discovered during writing get filed, + then either fixed in jolt or recorded as a labeled divergence — never + silently spec'd to jolt's behavior. +4. Editions: spec snapshots versioned independently of jolt releases + (`Clojure Language Specification, Draft N`). +5. When a chapter stabilizes, solicit review from other implementations + (jank, babashka, Basilisp maintainers) before marking it Stable. + +## Alternatives considered + +- **Contribute prose to clojure-test-suite instead**: the suite is the right + *conformance* home but tests can't express rationale, classification, or + grammar; both are needed and they cross-reference. +- **Spec only what jolt implements**: rejected — the host classification of + the *full* 694-var surface is half the value. +- **EDN/data-format spec only** (edn already has a loose spec): far too + narrow; the evaluation model and core library are where divergence lives. + +## Open questions + +1. Numerics: the reference has longs/doubles/ratios/BigInt with promotion + rules; CLJS has JS numbers; jolt has Janet numbers. Likely answer: specify + an integer/float core with a host-numeric-tower extension point — needs + its own design note in §4. +2. Where do `*print-length*`-style dynamic vars land — host-dependent + interface or portable with defaults? +3. License/venue if the spec outgrows this repo (likely CC-BY; separate repo + once §1–§3 stabilize). diff --git a/docs/spec/00-front-matter.md b/docs/spec/00-front-matter.md new file mode 100644 index 0000000..61a70c2 --- /dev/null +++ b/docs/spec/00-front-matter.md @@ -0,0 +1,87 @@ +# Clojure Language Specification — Front Matter + +**Edition**: Draft 1 · **Describes**: Clojure 1.12 (reference) · **Status**: in progress + +This document specifies the Clojure programming language independently of any +implementation. See `docs/rfc/0001-language-specification.md` for motivation, +process, and scope. + +## 1. Conformance terminology + +The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** +are to be interpreted as described in RFC 2119. + +- A statement marked **MUST** is normative: a conforming implementation + exhibits exactly this behavior, and the conformance suite tests it. +- **implementation-defined** marks behavior a conforming implementation must + document but may choose (e.g. the concrete error type thrown where the + reference throws a JVM exception class). +- **host-defined** marks behavior delegated to the host platform (e.g. what + `slurp` accepts as a source). +- **⚠ reference-divergence** marks a place where this spec deliberately + differs from observed reference behavior, with rationale; the reference + behavior is always recorded alongside. + +## 2. Classification of the core surface + +Every `clojure.core` var carries exactly one classification (dashboard: +`coverage.md`): + +| Class | Meaning | Spec treatment | +|---|---|---| +| **portable** | semantics independent of host | full normative entry (§9) | +| **host-dependent** | portable *interface*, host-defined behavior | interface entry; behavior host-defined | +| **JVM-specific** | meaningful only on the JVM | catalogued in Appendix; not specified | + +Initial classifications are mechanical and reviewable; reclassification is an +ordinary spec change. + +## 3. The normative entry format + +Each special form (§3) and portable var (§9) is specified as: + +``` +### name — since +(signature ...) (signature ...) + +Semantics + S1. + S2. ... +Edge cases + E1. +Errors + X1. +Examples + +Conformance + S1 → /; E1 → ... (statements without a test: UNVERIFIED) +``` + +The **Conformance** field is load-bearing: every numbered statement names the +test(s) that verify it. A normative statement with no test is labeled +`UNVERIFIED` and is a defect in the spec. + +## 4. Evidence and verification + +Behavioral questions are settled in this order: differential testing against +the reference implementation → cross-dialect agreement in clojure-test-suite +→ ClojureDocs community examples (verified before inclusion) → reference +source (for intent). Conformance tests live in this repository +(`test/integration/conformance-test.janet` runs each assertion through three +independent execution paths) and in the cross-dialect clojure-test-suite. + +## 5. Chapter plan + +| § | File | Status | +|---|---|---| +| 1 | `01-evaluation.md` | planned | +| 2 | `02-reader.md` | planned (first: jank's reader corpus gives conformance shape) | +| 3 | `03-special-forms.md` | **exemplars written** (`if`, `let*`); catalog complete | +| 4 | `04-data-types.md` | planned (numeric-tower design note required) | +| 5 | `05-sequences.md` | planned (laziness contract from jolt Phase-5 work) | +| 6 | `06-namespaces-vars.md` | planned | +| 7 | `07-polymorphism.md` | planned | +| 8 | `08-macros.md` | planned | +| 9 | `09-core-library.md` | **exemplars written** (`first`, `reduce`, `parse-uuid`) | +| A | `coverage.md` | **generated** (regenerate: `python3 tools/spec_coverage.py`) | diff --git a/docs/spec/03-special-forms.md b/docs/spec/03-special-forms.md new file mode 100644 index 0000000..7f46804 --- /dev/null +++ b/docs/spec/03-special-forms.md @@ -0,0 +1,143 @@ +# §3 Special Forms + +**Status**: catalog complete; normative exemplars for `if` and `let*`; the +remaining entries follow the same format (tracked in `coverage.md`). + +A *special form* is a form whose head symbol is evaluated by rule rather than +by function application or macroexpansion. The special forms of Clojure are: + +> `def` · `if` · `do` · `let*` · `fn*` · `loop*` · `recur` · `quote` · `var` +> · `throw` · `try`/`catch`/`finally` · `set!` · `monitor-enter` · +> `monitor-exit` (host) · the interop forms `.` and `new` (host) + +`let`, `fn`, `loop`, `and`, `or`, `when`, … are **macros** over these (§8); +implementations MUST treat them as redefinable macros, not additional special +forms. `monitor-enter`/`monitor-exit`, `.` and `new` are host forms: their +syntax is specified here, their behavior is host-defined. + +Special-form head symbols are not shadowable: a binding named `if` does not +change the meaning of `(if ...)` in operator position. ⚠ This matches the +reference; it differs from Scheme. + +--- + +### if — since 1.0 + +``` +(if test then) +(if test then else) +``` + +**Semantics** + +- S1. `test` MUST be evaluated first, exactly once. +- S2. Every value other than `nil` and `false` is *logically true*. If the + value of `test` is logically true, `then` MUST be evaluated and its value + returned; otherwise `else` (or `nil` when absent) MUST be evaluated and its + value returned. +- S3. The branch not taken MUST NOT be evaluated. +- S4. `if` MUST be usable in tail position with respect to `recur` (§3 + `recur`): an `if` whose branch is a `recur` form is a valid recur target + path. + +**Edge cases** + +- E1. `(if test then)` with a logically false `test` evaluates to `nil`. +- E2. The empty collections (`()`, `[]`, `{}`, `#{}`), the number `0`, and + the empty string `""` are logically **true** (only `nil`/`false` are + false). ⚠ This differs from several Lisps and is a frequent divergence + source in alternative implementations. + +**Errors** + +- X1. `(if)` and `(if test)` with fewer than two argument forms, or more + than three, MUST be a compile-time error. + +**Examples** + +```clojure +(if 0 :t :f) ;=> :t +(if "" :t :f) ;=> :t +(if nil :t :f) ;=> :f +(if false :t) ;=> nil +``` + +**Conformance** + +S1–S3, E1–E2 → jolt `forms-spec` "if/do/def" group; truthiness group in +`truthiness-spec`; clojure-test-suite `core_test/if.cljc`. S4 → `forms-spec` +fn/loop recur cases. X1 → UNVERIFIED (arity-error case to add). + +--- + +### let* — since 1.0 + +``` +(let* [sym₁ init₁ … symₙ initₙ] body…) +``` + +`let*` is the primitive sequential-binding form. The user-facing `let` macro +adds destructuring and expands to `let*` (§8); `let*` itself accepts **only +simple symbols** in binding positions. + +**Semantics** + +- S1. Each `initᵢ` MUST be evaluated in order, exactly once, in an + environment where `sym₁…symᵢ₋₁` are bound to their values (sequential + scope, as Scheme `let*`). +- S2. The body forms MUST be evaluated in order with all bindings in scope; + the value of the last body form is the value of the `let*` form. An empty + body evaluates to `nil`. +- S3. A later binding MAY rebind the same symbol; each binding creates a new + lexical binding visible from the next init onward (no mutation of the + earlier binding is implied). +- S4. Bindings are lexical and immutable: there is no form that assigns to a + `let*`-bound local. (Closures capture bindings by value; see §3 `fn*`.) +- S5. The binding vector MUST be a vector literal with an even number of + forms. + +**Edge cases** + +- E1. `(let* [] body)` is valid and equivalent to `(do body…)`. +- E2. Binding a symbol that names a var shadows the var for the lexical + extent of the body; `(var sym)` within that extent still denotes the var. + +**Errors** + +- X1. An odd number of binding forms MUST be a compile-time error. +- X2. A non-symbol in a binding position (e.g. a destructuring pattern) MUST + be a compile-time error for `let*` — destructuring belongs to the `let` + macro. ("Bad binding form, expected symbol" in the reference.) + +**Examples** + +```clojure +(let* [a 1 b (+ a 1)] (* a b)) ;=> 2 +(let* [x 1 x (inc x)] x) ;=> 2 +(let* [] 42) ;=> 42 +``` + +**Conformance** + +S1–S3, E1 → jolt `forms-spec` let group; clojure-test-suite +`core_test/let.cljc`; jank corpus `form/let/*`. X2 → jolt +`destructuring-spec` "primitives reject patterns". S4, X1 → UNVERIFIED +(cases to add). + +--- + +## Remaining entries (format above; status in coverage.md) + +| Form | Notes for the entry author | +|---|---| +| `def` | var creation vs re-binding; metadata on the name; `(def x)` unbound; return value is the var | +| `do` | empty `(do)` → nil; top-level `do` splices for compilation units (important and under-documented) | +| `fn*` | arities, variadic `&`, closure capture, self-name, simple-symbol params only, recur target | +| `loop*` | recur arity must match bindings; recur rebinds in place | +| `recur` | tail-position rule (normative definition of tail position needed), across `if`/`do`/`let*`/`try` interactions | +| `quote` | self-evaluation table: which literals are self-evaluating unquoted | +| `var` | `#'` reader sugar; resolution at compile time | +| `throw` | any value vs Throwable — host question; jolt/cljs allow data, reference requires Throwable → classification needed | +| `try/catch/finally` | catch dispatch order, `:default`-style catch-all is a dialect extension (⚠ divergence note), finally evaluation guarantees, value of try | +| `set!` | host-dependent (dynamic vars + host fields) | +| `.` / `new` | syntax only; behavior host-defined | diff --git a/docs/spec/09-core-library.md b/docs/spec/09-core-library.md new file mode 100644 index 0000000..778c274 --- /dev/null +++ b/docs/spec/09-core-library.md @@ -0,0 +1,174 @@ +# §9 The Core Library + +**Status**: entry format fixed; exemplars for `first`, `reduce`, `parse-uuid`. +The full portable surface (≈500 vars after classification, dashboard in +`coverage.md`) is filled in chapter-by-chapter using this format. + +Entries specify *behavioral contracts*, not implementations. Performance +characteristics are specified only where the language community relies on +them (e.g. vector `nth` is "effectively constant time" — SHOULD-level). + +--- + +### first — since 1.0 + +``` +(first coll) +``` + +**Semantics** + +- S1. MUST return the first element of `(seq coll)`. +- S2. If `(seq coll)` is `nil` (i.e. `coll` is empty or `nil`), MUST return + `nil`. +- S3. MUST accept anything *seqable* (§5): seqs, lists, vectors, maps + (yielding map entries), sets, strings (yielding characters), `nil`. +- S4. On a lazy sequence, MUST realize at most the first element (§5 + laziness contract). + +**Edge cases** + +- E1. `(first nil)` ⇒ `nil`; `(first [])` ⇒ `nil`; `(first "")` ⇒ `nil`. +- E2. A `nil` or `false` first *element* is returned as-is — callers cannot + distinguish "empty" from "first element is nil" via `first` alone (that is + what `seq` is for). +- E3. On a map, the element is a map entry; on an unordered collection (map, + set) *which* element is first is implementation-defined but MUST be + consistent with that collection's seq order for the same collection value. + +**Errors** + +- X1. A non-seqable argument (e.g. a number) MUST throw. + +**Examples** + +```clojure +(first [1 2 3]) ;=> 1 +(first '()) ;=> nil +(first "ab") ;=> \a +(first {:a 1}) ;=> [:a 1] +(first [nil 2]) ;=> nil +``` + +**Conformance** + +S1–S3, E1–E2 → jolt `sequences-spec` "seq / access"; clojure-test-suite +`core_test/first.cljc`. S4 → jolt `lazy-seqs-spec` counter cases. X1 → +clojure-test-suite `core_test/first.cljc` (throwing cases). + +--- + +### reduce — since 1.0 + +``` +(reduce f coll) +(reduce f init coll) +``` + +**Semantics** + +- S1. With `init`: MUST return `init` if `(seq coll)` is nil; otherwise MUST + return `(f … (f (f init e₁) e₂) … eₙ)`, applying `f` left-to-right over the + elements, exactly once each. +- S2. Without `init`: if `coll` is empty, MUST return `(f)` (f called with + no arguments); if `coll` has one element, MUST return that element + *without calling `f`*; otherwise as S1 with `init = e₁` over `e₂…eₙ`. +- S3. **Reduced short-circuit**: if any intermediate result is a `reduced` + value, iteration MUST stop and the dereferenced value MUST be returned + immediately; `f` MUST NOT be called again. +- S4. `reduce` is eager: it MUST fully realize the consumed portion of a + lazy `coll` (to the end, or to the `reduced` point). + +**Edge cases** + +- E1. `(reduce f nil)` ⇒ `(f)`; `(reduce f init nil)` ⇒ `init`. +- E2. A `reduced` value as the *initial* `init` is NOT unwrapped before the + first call in the reference — ⚠ under-documented; differential result to + pin down and test before this entry is marked verified. +- E3. Visit order over maps is entry order of the map's seq; + over vectors/lists/seqs it is sequential order (normative). + +**Errors** + +- X1. Without `init`, on an empty coll, if `f` has no zero-arg arity the + call `(f)` MUST throw (arity error). + +**Examples** + +```clojure +(reduce + [1 2 3 4]) ;=> 10 +(reduce + 10 [1 2 3 4]) ;=> 20 +(reduce + []) ;=> 0 ; (+) is 0 +(reduce + [5]) ;=> 5 ; f not called +(reduce (fn [a x] (if (> a 2) (reduced a) (+ a x))) 0 [1 2 3 4 5]) ;=> 3 +``` + +**Conformance** + +S1–S3, E1 → jolt `sequences-spec` "map filter reduce" group + +`transducers-spec` "reduce honors reduced"; clojure-test-suite +`core_test/reduce.cljc`. S2 (single-element, f-not-called) → jolt conformance +"reduce single no init". E2 → UNVERIFIED (differential test to add). S4 → +`lazy-seqs-spec`. + +--- + +### parse-uuid — since 1.11 + +``` +(parse-uuid s) +``` + +**Semantics** + +- S1. If `s` is a string in canonical UUID form — five groups of hex digits + of lengths 8, 4, 4, 4, 12 separated by `-` — MUST return a UUID value `u` + such that `(uuid? u)` is true and `(str u)` is the lowercase form of `s`. +- S2. Parsing MUST be case-insensitive and equality on the results + case-insensitive: `(= (parse-uuid s) (parse-uuid (upper-case s)))` is true. +- S3. If `s` is a string not in canonical form, MUST return `nil`. + ⚠ reference-divergence: reference Clojure (java.util.UUID) additionally + accepts non-canonical forms like `"0-0-0-0-0"`; ClojureScript and other + dialects are strict. This spec adopts **strict** (the cross-dialect + behavior); the reference's permissiveness is recorded as host leniency. +- S4. UUID values MUST support value equality, hashing (usable as map keys + and set members), `str` (lowercase canonical form), and print as the + tagged literal `#uuid "…"` such that the printed form reads back equal + (§2 tagged literals). + +**Edge cases** + +- E1. `""`, over-long, truncated, non-hex characters, and misplaced dashes + ⇒ `nil`. + +**Errors** + +- X1. A non-string argument MUST throw. + +**Examples** + +```clojure +(parse-uuid "b6883c0a-0342-4007-9966-bc2dfa6b109e") ;=> #uuid "b6883c0a-…" +(uuid? *1) ;=> true +(parse-uuid "df0993") ;=> nil +(parse-uuid 1000) ;; throws +``` + +**Conformance** + +S1–S4, E1, X1 → jolt `uuid-spec` (30 cases) + 6 three-path conformance +cases; clojure-test-suite `core_test/parse_uuid.cljc`, +`core_test/uuid_qmark.cljc`, `core_test/random_uuid.cljc`. + +--- + +## Authoring notes + +- Source examples from the ClojureDocs export (`clojuredocs-export.edn`, + 648 core vars have community examples) — but every example is verified + against the reference before inclusion. +- When writing an entry surfaces a behavior question, settle it by + differential test first; if dialects split, that's a classification + decision (host-dependent / divergence note), not a coin flip. +- An entry is **Verified** when no field carries UNVERIFIED; `coverage.md` + tracks per-var status. diff --git a/docs/spec/README.md b/docs/spec/README.md new file mode 100644 index 0000000..7c710f2 --- /dev/null +++ b/docs/spec/README.md @@ -0,0 +1,52 @@ +# The Clojure Language Specification (Draft) + +A normative, implementation-independent specification of the Clojure +language, developed alongside jolt's self-hosted compiler and validated by +its executable conformance suites. **Why**: Clojure has no spec — every +alternative implementation re-derives semantics from the reference +implementation and folklore. See the RFC for motivation, scope, evidence +sources, and process: [`../rfc/0001-language-specification.md`](../rfc/0001-language-specification.md). + +## Documents + +| Doc | Content | Status | +|---|---|---| +| [`00-front-matter.md`](00-front-matter.md) | conformance terms, entry format, host classification | drafted | +| `01-evaluation.md` … `08-macros.md` | see chapter plan in front matter | planned | +| [`03-special-forms.md`](03-special-forms.md) | special-form catalog + normative exemplars (`if`, `let*`) | exemplars | +| [`09-core-library.md`](09-core-library.md) | per-var entry format + exemplars (`first`, `reduce`, `parse-uuid`) | exemplars | +| [`coverage.md`](coverage.md) | generated dashboard over the 694-var surface | generated | + +Regenerate the dashboard after surface changes: +`python3 tools/spec_coverage.py` (requires `clojuredocs-export.json` in the +repo root and a working jolt checkout). + +## Current numbers (2026-06-10) + +Of the 694 `clojure.core` vars in the ClojureDocs inventory: + +- **380** implemented in jolt *and* exercised by the behavioral suites +- **154** implemented but not directly tested — each gets a test with its spec entry +- **35** portable but missing from jolt (`parse-long`/`parse-double`/ + `parse-boolean`, `update-keys`/`update-vals`, `macroexpand`, `time`, + `partitionv`/`partitionv-all`/`splitv-at`, `with-redefs`, `with-open`, + reader fns, ns-introspection stragglers, …) — tracked as implementation gaps +- **22** resolvable in code but invisible to ns introspection + (`resolve`/`ns-publics` can't see seed-fallback names like `compare`, + `gensym`, `type`) — a conformance finding in its own right +- the rest classified host/JVM/concurrency (see dashboard) + +## How this connects to the test suites + +- `test/integration/conformance-test.janet` — 302 assertions, each run + through three independent execution paths (interpreter, bootstrap + compiler, self-hosted compiler) that must agree. Spec entries cite these. +- `test/spec/*.janet` — ~1,500 behavioral cases organized by topic. +- `vendor/clojure-test-suite` — the cross-dialect suite (≥4081 assertions + passing); dialect splits there are classification evidence. +- jank's per-construct corpus (`~/src/jank/compiler+runtime/test/jank`) is + the granularity model for §2/§3 conformance. + +The invariant: **every numbered normative statement names its conformance +test**, or is marked UNVERIFIED. The spec cannot drift from the +implementations that check it. diff --git a/docs/spec/coverage.md b/docs/spec/coverage.md new file mode 100644 index 0000000..3f919c2 --- /dev/null +++ b/docs/spec/coverage.md @@ -0,0 +1,721 @@ +# Appendix A — Coverage Dashboard (generated) + +Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand. + +Surface: **694** clojure.core vars (ClojureDocs export; 648 with +community examples). jolt interns 534 of them. + +| Status | Count | Meaning | +|---|---|---| +| implemented+tested | 380 | in jolt and exercised by spec/conformance | +| implemented-untested | 154 | in jolt, no direct test — spec entries will add them | +| resolvable-not-interned | 22 | works in code but invisible to ns introspection (conformance finding) | +| missing-portable | 35 | portable semantics, jolt lacks it — implementation gap | +| special-form | 13 | specified in §3, not a library var | +| dynamic-var | 29 | classification needed: portable default vs host-dependent | +| agents-taps | 22 | out of scope pending concurrency design note | +| stm-refs | 11 | out of scope pending concurrency design note | +| jvm-specific | 50 | catalogued, not specified | + +Classifications are initial and mechanical — reclassifying is an ordinary +spec change. A var is *Verified* only when its §9 entry exists and carries no +UNVERIFIED field; that column will be added as entries land. + +## Per-var status + +| Var | Status | ClojureDocs examples | +|---|---|---| +| `*` | implemented+tested | ✓ | +| `*'` | implemented-untested | ✓ | +| `*1` | implemented-untested | ✓ | +| `*2` | implemented-untested | ✓ | +| `*3` | implemented-untested | ✓ | +| `*agent*` | dynamic-var | ✓ | +| `*allow-unresolved-vars*` | dynamic-var | ✓ | +| `*assert*` | dynamic-var | ✓ | +| `*clojure-version*` | implemented-untested | ✓ | +| `*command-line-args*` | dynamic-var | ✓ | +| `*compile-files*` | dynamic-var | ✓ | +| `*compile-path*` | dynamic-var | ✓ | +| `*compiler-options*` | dynamic-var | ✓ | +| `*data-readers*` | dynamic-var | ✓ | +| `*default-data-reader-fn*` | dynamic-var | ✓ | +| `*e` | implemented-untested | ✓ | +| `*err*` | implemented-untested | ✓ | +| `*file*` | dynamic-var | ✓ | +| `*flush-on-newline*` | dynamic-var | | +| `*fn-loader*` | dynamic-var | | +| `*in*` | dynamic-var | | +| `*math-context*` | dynamic-var | | +| `*ns*` | dynamic-var | ✓ | +| `*out*` | implemented-untested | ✓ | +| `*print-dup*` | dynamic-var | ✓ | +| `*print-length*` | dynamic-var | ✓ | +| `*print-level*` | dynamic-var | ✓ | +| `*print-meta*` | dynamic-var | ✓ | +| `*print-namespace-maps*` | dynamic-var | ✓ | +| `*print-readably*` | dynamic-var | ✓ | +| `*read-eval*` | dynamic-var | ✓ | +| `*reader-resolver*` | dynamic-var | | +| `*repl*` | dynamic-var | | +| `*source-path*` | dynamic-var | ✓ | +| `*suppress-read*` | dynamic-var | | +| `*unchecked-math*` | implemented-untested | ✓ | +| `*use-context-classloader*` | dynamic-var | ✓ | +| `*verbose-defrecords*` | dynamic-var | | +| `*warn-on-reflection*` | dynamic-var | ✓ | +| `+` | implemented+tested | ✓ | +| `+'` | implemented-untested | ✓ | +| `-` | implemented+tested | ✓ | +| `-'` | implemented-untested | ✓ | +| `->` | implemented+tested | ✓ | +| `->>` | implemented+tested | ✓ | +| `->ArrayChunk` | jvm-specific | | +| `->Eduction` | implemented-untested | | +| `->Vec` | jvm-specific | | +| `->VecNode` | jvm-specific | | +| `->VecSeq` | jvm-specific | | +| `-cache-protocol-fn` | jvm-specific | | +| `-reset-methods` | jvm-specific | | +| `.` | implemented-untested | ✓ | +| `..` | implemented-untested | ✓ | +| `/` | implemented-untested | ✓ | +| `<` | implemented+tested | ✓ | +| `<=` | implemented+tested | ✓ | +| `=` | implemented+tested | ✓ | +| `==` | implemented+tested | ✓ | +| `>` | implemented+tested | ✓ | +| `>=` | implemented+tested | ✓ | +| `EMPTY-NODE` | jvm-specific | | +| `Inst` | jvm-specific | | +| `NaN?` | implemented+tested | ✓ | +| `PrintWriter-on` | jvm-specific | ✓ | +| `StackTraceElement->vec` | jvm-specific | ✓ | +| `Throwable->map` | jvm-specific | ✓ | +| `abs` | implemented+tested | ✓ | +| `accessor` | jvm-specific | ✓ | +| `aclone` | implemented-untested | ✓ | +| `add-classpath` | jvm-specific | ✓ | +| `add-tap` | agents-taps | ✓ | +| `add-watch` | implemented+tested | ✓ | +| `agent` | agents-taps | ✓ | +| `agent-error` | agents-taps | ✓ | +| `agent-errors` | agents-taps | | +| `aget` | implemented+tested | ✓ | +| `alength` | implemented+tested | ✓ | +| `alias` | missing-portable | ✓ | +| `all-ns` | implemented-untested | ✓ | +| `alter` | stm-refs | ✓ | +| `alter-meta!` | implemented+tested | ✓ | +| `alter-var-root` | implemented+tested | ✓ | +| `amap` | jvm-specific | ✓ | +| `ancestors` | implemented+tested | ✓ | +| `and` | implemented+tested | ✓ | +| `any?` | implemented-untested | ✓ | +| `apply` | implemented+tested | ✓ | +| `areduce` | jvm-specific | ✓ | +| `array-map` | implemented-untested | ✓ | +| `as->` | implemented+tested | ✓ | +| `aset` | implemented+tested | ✓ | +| `aset-boolean` | implemented-untested | ✓ | +| `aset-byte` | implemented-untested | ✓ | +| `aset-char` | implemented-untested | ✓ | +| `aset-double` | implemented-untested | ✓ | +| `aset-float` | implemented-untested | ✓ | +| `aset-int` | implemented-untested | ✓ | +| `aset-long` | implemented-untested | ✓ | +| `aset-short` | implemented-untested | ✓ | +| `assert` | implemented+tested | ✓ | +| `assoc` | implemented+tested | ✓ | +| `assoc!` | implemented+tested | ✓ | +| `assoc-in` | implemented+tested | ✓ | +| `associative?` | implemented+tested | ✓ | +| `atom` | implemented+tested | ✓ | +| `await` | agents-taps | ✓ | +| `await-for` | agents-taps | ✓ | +| `await1` | agents-taps | | +| `bases` | jvm-specific | ✓ | +| `bean` | implemented-untested | ✓ | +| `bigdec` | implemented+tested | ✓ | +| `bigint` | implemented+tested | ✓ | +| `biginteger` | implemented-untested | ✓ | +| `binding` | implemented+tested | ✓ | +| `bit-and` | implemented+tested | ✓ | +| `bit-and-not` | implemented-untested | ✓ | +| `bit-clear` | implemented+tested | ✓ | +| `bit-flip` | implemented-untested | ✓ | +| `bit-not` | implemented-untested | ✓ | +| `bit-or` | implemented+tested | ✓ | +| `bit-set` | implemented+tested | ✓ | +| `bit-shift-left` | implemented+tested | ✓ | +| `bit-shift-right` | implemented+tested | ✓ | +| `bit-test` | implemented+tested | ✓ | +| `bit-xor` | implemented+tested | ✓ | +| `boolean` | implemented+tested | ✓ | +| `boolean-array` | implemented-untested | ✓ | +| `boolean?` | implemented+tested | ✓ | +| `booleans` | implemented-untested | ✓ | +| `bound-fn` | missing-portable | ✓ | +| `bound-fn*` | missing-portable | ✓ | +| `bound?` | missing-portable | ✓ | +| `bounded-count` | implemented+tested | ✓ | +| `butlast` | implemented+tested | ✓ | +| `byte` | implemented-untested | ✓ | +| `byte-array` | implemented-untested | ✓ | +| `bytes` | implemented-untested | ✓ | +| `bytes?` | implemented-untested | ✓ | +| `case` | implemented+tested | ✓ | +| `cast` | jvm-specific | ✓ | +| `cat` | implemented-untested | ✓ | +| `catch` | special-form | ✓ | +| `char` | implemented+tested | ✓ | +| `char-array` | implemented-untested | ✓ | +| `char-escape-string` | implemented-untested | ✓ | +| `char-name-string` | implemented-untested | ✓ | +| `char?` | implemented+tested | ✓ | +| `chars` | implemented-untested | ✓ | +| `chunk` | implemented-untested | ✓ | +| `chunk-append` | implemented-untested | ✓ | +| `chunk-buffer` | implemented-untested | ✓ | +| `chunk-cons` | implemented-untested | ✓ | +| `chunk-first` | implemented-untested | ✓ | +| `chunk-next` | implemented-untested | ✓ | +| `chunk-rest` | implemented-untested | ✓ | +| `chunked-seq?` | implemented+tested | ✓ | +| `class` | implemented-untested | ✓ | +| `class?` | jvm-specific | ✓ | +| `clear-agent-errors` | agents-taps | | +| `clojure-version` | implemented-untested | ✓ | +| `coll?` | implemented+tested | ✓ | +| `comment` | implemented+tested | ✓ | +| `commute` | stm-refs | ✓ | +| `comp` | implemented+tested | ✓ | +| `comparator` | implemented+tested | ✓ | +| `compare` | implemented+tested | ✓ | +| `compare-and-set!` | implemented+tested | ✓ | +| `compile` | implemented-untested | ✓ | +| `complement` | implemented+tested | ✓ | +| `completing` | implemented+tested | ✓ | +| `concat` | implemented+tested | ✓ | +| `cond` | implemented+tested | ✓ | +| `cond->` | implemented+tested | ✓ | +| `cond->>` | implemented+tested | ✓ | +| `condp` | implemented+tested | ✓ | +| `conj` | implemented+tested | ✓ | +| `conj!` | implemented+tested | ✓ | +| `cons` | implemented+tested | ✓ | +| `constantly` | implemented+tested | ✓ | +| `construct-proxy` | implemented-untested | ✓ | +| `contains?` | implemented+tested | ✓ | +| `count` | implemented+tested | ✓ | +| `counted?` | implemented+tested | ✓ | +| `create-ns` | implemented+tested | ✓ | +| `create-struct` | jvm-specific | ✓ | +| `cycle` | implemented+tested | ✓ | +| `dec` | implemented+tested | ✓ | +| `dec'` | implemented-untested | ✓ | +| `decimal?` | implemented+tested | ✓ | +| `declare` | implemented+tested | ✓ | +| `dedupe` | implemented+tested | ✓ | +| `def` | special-form | ✓ | +| `default-data-readers` | jvm-specific | ✓ | +| `definline` | jvm-specific | | +| `definterface` | implemented-untested | ✓ | +| `defmacro` | implemented+tested | ✓ | +| `defmethod` | implemented+tested | ✓ | +| `defmulti` | implemented+tested | ✓ | +| `defn` | implemented+tested | ✓ | +| `defn-` | implemented+tested | ✓ | +| `defonce` | implemented+tested | ✓ | +| `defprotocol` | implemented+tested | ✓ | +| `defrecord` | implemented+tested | ✓ | +| `defstruct` | jvm-specific | ✓ | +| `deftype` | implemented+tested | ✓ | +| `delay` | implemented+tested | ✓ | +| `delay?` | implemented-untested | ✓ | +| `deliver` | implemented+tested | ✓ | +| `denominator` | implemented+tested | ✓ | +| `deref` | implemented+tested | ✓ | +| `derive` | implemented+tested | ✓ | +| `descendants` | implemented+tested | ✓ | +| `destructure` | implemented-untested | ✓ | +| `disj` | implemented+tested | ✓ | +| `disj!` | implemented+tested | ✓ | +| `dissoc` | implemented+tested | ✓ | +| `dissoc!` | implemented+tested | ✓ | +| `distinct` | implemented+tested | ✓ | +| `distinct?` | implemented+tested | ✓ | +| `do` | special-form | ✓ | +| `doall` | implemented+tested | ✓ | +| `dorun` | implemented+tested | ✓ | +| `doseq` | implemented+tested | ✓ | +| `dosync` | stm-refs | ✓ | +| `dotimes` | implemented+tested | ✓ | +| `doto` | implemented+tested | ✓ | +| `double` | implemented-untested | ✓ | +| `double-array` | implemented-untested | ✓ | +| `double?` | implemented+tested | ✓ | +| `doubles` | implemented-untested | ✓ | +| `drop` | implemented+tested | ✓ | +| `drop-last` | implemented+tested | ✓ | +| `drop-while` | implemented+tested | ✓ | +| `eduction` | implemented+tested | ✓ | +| `empty` | implemented+tested | ✓ | +| `empty?` | implemented+tested | ✓ | +| `ensure` | stm-refs | ✓ | +| `ensure-reduced` | implemented-untested | ✓ | +| `enumeration-seq` | implemented-untested | ✓ | +| `error-handler` | agents-taps | ✓ | +| `error-mode` | agents-taps | ✓ | +| `eval` | implemented+tested | ✓ | +| `even?` | implemented+tested | ✓ | +| `every-pred` | implemented+tested | ✓ | +| `every?` | implemented+tested | ✓ | +| `ex-cause` | implemented+tested | ✓ | +| `ex-data` | implemented+tested | ✓ | +| `ex-info` | implemented+tested | ✓ | +| `ex-message` | implemented+tested | ✓ | +| `extend` | implemented-untested | ✓ | +| `extend-protocol` | implemented+tested | ✓ | +| `extend-type` | implemented+tested | ✓ | +| `extenders` | missing-portable | ✓ | +| `extends?` | implemented-untested | ✓ | +| `false?` | implemented+tested | ✓ | +| `ffirst` | implemented+tested | ✓ | +| `file-seq` | missing-portable | ✓ | +| `filter` | implemented+tested | ✓ | +| `filterv` | implemented+tested | ✓ | +| `finally` | special-form | ✓ | +| `find` | implemented+tested | ✓ | +| `find-keyword` | missing-portable | ✓ | +| `find-ns` | implemented+tested | ✓ | +| `find-protocol-impl` | jvm-specific | | +| `find-protocol-method` | implemented-untested | | +| `find-var` | implemented+tested | ✓ | +| `first` | implemented+tested | ✓ | +| `flatten` | implemented+tested | ✓ | +| `float` | implemented-untested | ✓ | +| `float-array` | implemented-untested | ✓ | +| `float?` | implemented+tested | ✓ | +| `floats` | implemented-untested | ✓ | +| `flush` | implemented-untested | ✓ | +| `fn` | implemented+tested | ✓ | +| `fn?` | implemented+tested | ✓ | +| `fnext` | implemented+tested | ✓ | +| `fnil` | implemented+tested | ✓ | +| `for` | implemented+tested | ✓ | +| `force` | implemented+tested | ✓ | +| `format` | implemented+tested | ✓ | +| `frequencies` | implemented+tested | ✓ | +| `future` | implemented+tested | ✓ | +| `future-call` | implemented-untested | ✓ | +| `future-cancel` | implemented+tested | ✓ | +| `future-cancelled?` | implemented+tested | ✓ | +| `future-done?` | implemented+tested | ✓ | +| `future?` | implemented+tested | ✓ | +| `gen-class` | jvm-specific | ✓ | +| `gen-interface` | jvm-specific | ✓ | +| `gensym` | implemented+tested | ✓ | +| `get` | implemented+tested | ✓ | +| `get-in` | implemented+tested | ✓ | +| `get-method` | implemented+tested | ✓ | +| `get-proxy-class` | implemented-untested | ✓ | +| `get-thread-bindings` | missing-portable | ✓ | +| `get-validator` | implemented+tested | ✓ | +| `group-by` | implemented+tested | ✓ | +| `halt-when` | implemented-untested | ✓ | +| `hash` | implemented-untested | ✓ | +| `hash-combine` | implemented-untested | ✓ | +| `hash-map` | implemented+tested | ✓ | +| `hash-ordered-coll` | implemented-untested | ✓ | +| `hash-set` | implemented+tested | ✓ | +| `hash-unordered-coll` | implemented-untested | ✓ | +| `ident?` | implemented+tested | ✓ | +| `identical?` | implemented+tested | ✓ | +| `identity` | implemented+tested | ✓ | +| `if` | special-form | ✓ | +| `if-let` | implemented+tested | ✓ | +| `if-not` | implemented+tested | ✓ | +| `if-some` | implemented+tested | ✓ | +| `ifn?` | implemented+tested | ✓ | +| `import` | implemented+tested | ✓ | +| `in-ns` | implemented+tested | ✓ | +| `inc` | implemented+tested | ✓ | +| `inc'` | implemented-untested | ✓ | +| `indexed?` | implemented+tested | ✓ | +| `infinite?` | implemented+tested | ✓ | +| `init-proxy` | implemented-untested | ✓ | +| `inst-ms` | implemented-untested | ✓ | +| `inst-ms*` | missing-portable | | +| `inst?` | implemented-untested | ✓ | +| `instance?` | implemented+tested | ✓ | +| `int` | implemented+tested | ✓ | +| `int-array` | implemented-untested | ✓ | +| `int?` | implemented+tested | ✓ | +| `integer?` | implemented+tested | ✓ | +| `interleave` | implemented+tested | ✓ | +| `intern` | implemented+tested | ✓ | +| `interpose` | implemented+tested | ✓ | +| `into` | implemented+tested | ✓ | +| `into-array` | implemented-untested | ✓ | +| `ints` | implemented-untested | ✓ | +| `io!` | stm-refs | ✓ | +| `isa?` | implemented+tested | ✓ | +| `iterate` | implemented+tested | ✓ | +| `iteration` | jvm-specific | ✓ | +| `iterator-seq` | implemented-untested | ✓ | +| `juxt` | implemented+tested | ✓ | +| `keep` | implemented+tested | ✓ | +| `keep-indexed` | implemented+tested | ✓ | +| `key` | implemented+tested | ✓ | +| `keys` | implemented+tested | ✓ | +| `keyword` | implemented+tested | ✓ | +| `keyword?` | implemented+tested | ✓ | +| `last` | implemented+tested | ✓ | +| `lazy-cat` | implemented+tested | ✓ | +| `lazy-seq` | implemented+tested | ✓ | +| `let` | implemented+tested | ✓ | +| `letfn` | implemented+tested | ✓ | +| `line-seq` | implemented-untested | ✓ | +| `list` | implemented+tested | ✓ | +| `list*` | implemented+tested | ✓ | +| `list?` | implemented+tested | ✓ | +| `load` | jvm-specific | ✓ | +| `load-file` | jvm-specific | ✓ | +| `load-reader` | jvm-specific | ✓ | +| `load-string` | implemented-untested | ✓ | +| `loaded-libs` | jvm-specific | ✓ | +| `locking` | implemented+tested | ✓ | +| `long` | implemented-untested | ✓ | +| `long-array` | implemented-untested | ✓ | +| `longs` | implemented-untested | ✓ | +| `loop` | implemented+tested | ✓ | +| `macroexpand` | missing-portable | ✓ | +| `macroexpand-1` | implemented+tested | ✓ | +| `make-array` | implemented-untested | ✓ | +| `make-hierarchy` | implemented+tested | ✓ | +| `map` | implemented+tested | ✓ | +| `map-entry?` | implemented+tested | ✓ | +| `map-indexed` | implemented+tested | ✓ | +| `map?` | implemented+tested | ✓ | +| `mapcat` | implemented+tested | ✓ | +| `mapv` | implemented+tested | ✓ | +| `max` | implemented+tested | ✓ | +| `max-key` | implemented+tested | ✓ | +| `memfn` | implemented-untested | ✓ | +| `memoize` | implemented+tested | ✓ | +| `merge` | implemented+tested | ✓ | +| `merge-with` | implemented+tested | ✓ | +| `meta` | implemented+tested | ✓ | +| `method-sig` | jvm-specific | ✓ | +| `methods` | implemented+tested | ✓ | +| `min` | implemented+tested | ✓ | +| `min-key` | implemented+tested | ✓ | +| `mix-collection-hash` | jvm-specific | | +| `mod` | implemented+tested | ✓ | +| `monitor-enter` | special-form | | +| `monitor-exit` | special-form | | +| `munge` | implemented-untested | ✓ | +| `name` | implemented+tested | ✓ | +| `namespace` | implemented+tested | ✓ | +| `namespace-munge` | implemented+tested | ✓ | +| `nat-int?` | implemented+tested | ✓ | +| `neg-int?` | implemented+tested | ✓ | +| `neg?` | implemented+tested | ✓ | +| `new` | special-form | ✓ | +| `newline` | missing-portable | ✓ | +| `next` | implemented+tested | ✓ | +| `nfirst` | implemented-untested | ✓ | +| `nil?` | implemented+tested | ✓ | +| `nnext` | implemented+tested | ✓ | +| `not` | implemented+tested | ✓ | +| `not-any?` | implemented+tested | ✓ | +| `not-empty` | implemented+tested | ✓ | +| `not-every?` | implemented+tested | ✓ | +| `not=` | implemented+tested | ✓ | +| `ns` | implemented+tested | ✓ | +| `ns-aliases` | implemented-untested | ✓ | +| `ns-imports` | implemented-untested | ✓ | +| `ns-interns` | implemented-untested | ✓ | +| `ns-map` | implemented-untested | ✓ | +| `ns-name` | implemented+tested | ✓ | +| `ns-publics` | missing-portable | ✓ | +| `ns-refers` | missing-portable | ✓ | +| `ns-resolve` | implemented+tested | ✓ | +| `ns-unalias` | missing-portable | ✓ | +| `ns-unmap` | implemented-untested | ✓ | +| `nth` | implemented+tested | ✓ | +| `nthnext` | implemented+tested | ✓ | +| `nthrest` | implemented+tested | ✓ | +| `num` | implemented+tested | ✓ | +| `number?` | implemented+tested | ✓ | +| `numerator` | implemented+tested | ✓ | +| `object-array` | implemented+tested | ✓ | +| `odd?` | implemented+tested | ✓ | +| `or` | implemented+tested | ✓ | +| `parents` | implemented+tested | ✓ | +| `parse-boolean` | missing-portable | ✓ | +| `parse-double` | missing-portable | ✓ | +| `parse-long` | missing-portable | ✓ | +| `parse-uuid` | implemented+tested | ✓ | +| `partial` | implemented+tested | ✓ | +| `partition` | implemented+tested | ✓ | +| `partition-all` | implemented+tested | ✓ | +| `partition-by` | implemented+tested | ✓ | +| `partitionv` | missing-portable | | +| `partitionv-all` | missing-portable | | +| `pcalls` | jvm-specific | ✓ | +| `peek` | implemented+tested | ✓ | +| `persistent!` | implemented+tested | ✓ | +| `pmap` | jvm-specific | ✓ | +| `pop` | implemented+tested | ✓ | +| `pop!` | implemented+tested | ✓ | +| `pop-thread-bindings` | implemented-untested | | +| `pos-int?` | implemented+tested | ✓ | +| `pos?` | implemented+tested | ✓ | +| `pr` | implemented+tested | ✓ | +| `pr-str` | implemented+tested | ✓ | +| `prefer-method` | implemented+tested | ✓ | +| `prefers` | implemented-untested | ✓ | +| `primitives-classnames` | jvm-specific | ✓ | +| `print` | implemented+tested | ✓ | +| `print-ctor` | jvm-specific | ✓ | +| `print-dup` | implemented-untested | ✓ | +| `print-method` | implemented-untested | ✓ | +| `print-simple` | jvm-specific | ✓ | +| `print-str` | implemented+tested | ✓ | +| `printf` | implemented+tested | ✓ | +| `println` | implemented+tested | ✓ | +| `println-str` | implemented+tested | ✓ | +| `prn` | implemented+tested | ✓ | +| `prn-str` | implemented+tested | ✓ | +| `promise` | implemented+tested | ✓ | +| `proxy` | implemented-untested | ✓ | +| `proxy-call-with-super` | implemented-untested | | +| `proxy-mappings` | implemented-untested | ✓ | +| `proxy-name` | jvm-specific | | +| `proxy-super` | implemented-untested | ✓ | +| `push-thread-bindings` | implemented-untested | | +| `pvalues` | jvm-specific | ✓ | +| `qualified-ident?` | implemented+tested | ✓ | +| `qualified-keyword?` | implemented+tested | ✓ | +| `qualified-symbol?` | implemented+tested | ✓ | +| `quot` | implemented+tested | ✓ | +| `quote` | special-form | ✓ | +| `rand` | implemented+tested | ✓ | +| `rand-int` | implemented+tested | ✓ | +| `rand-nth` | implemented+tested | ✓ | +| `random-sample` | implemented-untested | ✓ | +| `random-uuid` | implemented+tested | ✓ | +| `range` | implemented+tested | ✓ | +| `ratio?` | implemented+tested | ✓ | +| `rational?` | implemented+tested | ✓ | +| `rationalize` | implemented+tested | ✓ | +| `re-find` | implemented+tested | ✓ | +| `re-groups` | implemented-untested | ✓ | +| `re-matcher` | implemented-untested | ✓ | +| `re-matches` | implemented+tested | ✓ | +| `re-pattern` | implemented+tested | ✓ | +| `re-seq` | implemented+tested | ✓ | +| `read` | missing-portable | ✓ | +| `read+string` | missing-portable | ✓ | +| `read-line` | missing-portable | ✓ | +| `read-string` | implemented+tested | ✓ | +| `reader-conditional` | implemented-untested | ✓ | +| `reader-conditional?` | implemented+tested | ✓ | +| `realized?` | implemented+tested | ✓ | +| `record?` | implemented+tested | ✓ | +| `recur` | special-form | ✓ | +| `reduce` | implemented+tested | ✓ | +| `reduce-kv` | implemented+tested | ✓ | +| `reduced` | implemented+tested | ✓ | +| `reduced?` | implemented+tested | ✓ | +| `reductions` | implemented+tested | ✓ | +| `ref` | stm-refs | ✓ | +| `ref-history-count` | stm-refs | ✓ | +| `ref-max-history` | stm-refs | | +| `ref-min-history` | stm-refs | ✓ | +| `ref-set` | stm-refs | ✓ | +| `refer` | implemented-untested | ✓ | +| `refer-clojure` | implemented-untested | ✓ | +| `reify` | implemented+tested | ✓ | +| `release-pending-sends` | agents-taps | ✓ | +| `rem` | implemented+tested | ✓ | +| `remove` | implemented+tested | ✓ | +| `remove-all-methods` | implemented+tested | ✓ | +| `remove-method` | implemented+tested | ✓ | +| `remove-ns` | implemented+tested | ✓ | +| `remove-tap` | agents-taps | | +| `remove-watch` | implemented+tested | ✓ | +| `repeat` | implemented+tested | ✓ | +| `repeatedly` | implemented+tested | ✓ | +| `replace` | implemented+tested | ✓ | +| `replicate` | implemented+tested | ✓ | +| `require` | implemented+tested | ✓ | +| `requiring-resolve` | jvm-specific | ✓ | +| `reset!` | implemented+tested | ✓ | +| `reset-meta!` | implemented-untested | ✓ | +| `reset-vals!` | implemented+tested | ✓ | +| `resolve` | implemented+tested | ✓ | +| `rest` | implemented+tested | ✓ | +| `restart-agent` | agents-taps | ✓ | +| `resultset-seq` | jvm-specific | ✓ | +| `reverse` | implemented+tested | ✓ | +| `reversible?` | implemented+tested | ✓ | +| `rseq` | implemented+tested | ✓ | +| `rsubseq` | implemented+tested | ✓ | +| `run!` | implemented+tested | ✓ | +| `satisfies?` | implemented+tested | ✓ | +| `second` | implemented+tested | ✓ | +| `select-keys` | implemented+tested | ✓ | +| `send` | agents-taps | ✓ | +| `send-off` | agents-taps | ✓ | +| `send-via` | agents-taps | ✓ | +| `seq` | implemented+tested | ✓ | +| `seq-to-map-for-destructuring` | implemented-untested | ✓ | +| `seq?` | implemented+tested | ✓ | +| `seqable?` | implemented+tested | ✓ | +| `seque` | implemented-untested | ✓ | +| `sequence` | implemented+tested | ✓ | +| `sequential?` | implemented+tested | ✓ | +| `set` | implemented+tested | ✓ | +| `set!` | special-form | ✓ | +| `set-agent-send-executor!` | agents-taps | ✓ | +| `set-agent-send-off-executor!` | agents-taps | ✓ | +| `set-error-handler!` | agents-taps | ✓ | +| `set-error-mode!` | agents-taps | ✓ | +| `set-validator!` | implemented+tested | ✓ | +| `set?` | implemented+tested | ✓ | +| `short` | implemented-untested | ✓ | +| `short-array` | implemented-untested | ✓ | +| `shorts` | implemented-untested | ✓ | +| `shuffle` | implemented+tested | ✓ | +| `shutdown-agents` | agents-taps | ✓ | +| `simple-ident?` | implemented+tested | ✓ | +| `simple-keyword?` | implemented+tested | ✓ | +| `simple-symbol?` | implemented+tested | ✓ | +| `slurp` | implemented-untested | ✓ | +| `some` | implemented+tested | ✓ | +| `some->` | implemented+tested | ✓ | +| `some->>` | implemented+tested | ✓ | +| `some-fn` | implemented+tested | ✓ | +| `some?` | implemented+tested | ✓ | +| `sort` | implemented+tested | ✓ | +| `sort-by` | implemented+tested | ✓ | +| `sorted-map` | implemented+tested | ✓ | +| `sorted-map-by` | implemented+tested | ✓ | +| `sorted-set` | implemented+tested | ✓ | +| `sorted-set-by` | implemented+tested | ✓ | +| `sorted?` | implemented+tested | ✓ | +| `special-symbol?` | implemented-untested | ✓ | +| `spit` | implemented-untested | ✓ | +| `split-at` | implemented+tested | ✓ | +| `split-with` | implemented+tested | ✓ | +| `splitv-at` | missing-portable | | +| `str` | implemented+tested | ✓ | +| `stream-into!` | jvm-specific | | +| `stream-reduce!` | jvm-specific | | +| `stream-seq!` | jvm-specific | | +| `stream-transduce!` | jvm-specific | | +| `string?` | implemented+tested | ✓ | +| `struct` | implemented-untested | ✓ | +| `struct-map` | jvm-specific | ✓ | +| `subs` | implemented+tested | ✓ | +| `subseq` | implemented+tested | ✓ | +| `subvec` | implemented+tested | ✓ | +| `supers` | implemented-untested | ✓ | +| `swap!` | implemented+tested | ✓ | +| `swap-vals!` | implemented+tested | ✓ | +| `symbol` | implemented+tested | ✓ | +| `symbol?` | implemented+tested | ✓ | +| `sync` | stm-refs | | +| `tagged-literal` | implemented+tested | ✓ | +| `tagged-literal?` | implemented+tested | | +| `take` | implemented+tested | ✓ | +| `take-last` | implemented+tested | ✓ | +| `take-nth` | implemented+tested | ✓ | +| `take-while` | implemented+tested | ✓ | +| `tap>` | agents-taps | ✓ | +| `test` | implemented-untested | ✓ | +| `the-ns` | implemented+tested | ✓ | +| `thread-bound?` | missing-portable | ✓ | +| `throw` | special-form | ✓ | +| `time` | missing-portable | ✓ | +| `to-array` | implemented-untested | ✓ | +| `to-array-2d` | implemented+tested | ✓ | +| `trampoline` | implemented+tested | ✓ | +| `transduce` | implemented+tested | ✓ | +| `transient` | implemented+tested | ✓ | +| `tree-seq` | implemented+tested | ✓ | +| `true?` | implemented+tested | ✓ | +| `try` | implemented+tested | ✓ | +| `type` | implemented-untested | ✓ | +| `unchecked-add` | implemented-untested | ✓ | +| `unchecked-add-int` | implemented-untested | ✓ | +| `unchecked-byte` | implemented-untested | ✓ | +| `unchecked-char` | implemented-untested | | +| `unchecked-dec` | implemented-untested | ✓ | +| `unchecked-dec-int` | implemented-untested | ✓ | +| `unchecked-divide-int` | implemented-untested | ✓ | +| `unchecked-double` | implemented-untested | ✓ | +| `unchecked-float` | implemented-untested | ✓ | +| `unchecked-inc` | implemented-untested | ✓ | +| `unchecked-inc-int` | implemented-untested | ✓ | +| `unchecked-int` | implemented-untested | ✓ | +| `unchecked-long` | implemented-untested | ✓ | +| `unchecked-multiply` | implemented-untested | ✓ | +| `unchecked-multiply-int` | implemented-untested | | +| `unchecked-negate` | implemented-untested | ✓ | +| `unchecked-negate-int` | implemented-untested | ✓ | +| `unchecked-remainder-int` | implemented-untested | | +| `unchecked-short` | implemented-untested | ✓ | +| `unchecked-subtract` | implemented-untested | ✓ | +| `unchecked-subtract-int` | implemented-untested | ✓ | +| `underive` | implemented-untested | ✓ | +| `unquote` | jvm-specific | ✓ | +| `unquote-splicing` | jvm-specific | ✓ | +| `unreduced` | implemented+tested | ✓ | +| `unsigned-bit-shift-right` | implemented-untested | ✓ | +| `update` | implemented+tested | ✓ | +| `update-in` | implemented+tested | ✓ | +| `update-keys` | missing-portable | ✓ | +| `update-proxy` | implemented-untested | ✓ | +| `update-vals` | missing-portable | ✓ | +| `uri?` | implemented-untested | ✓ | +| `use` | implemented+tested | ✓ | +| `uuid?` | implemented+tested | ✓ | +| `val` | implemented+tested | ✓ | +| `vals` | implemented+tested | ✓ | +| `var` | special-form | ✓ | +| `var-get` | implemented+tested | ✓ | +| `var-set` | implemented+tested | ✓ | +| `var?` | implemented+tested | ✓ | +| `vary-meta` | implemented+tested | ✓ | +| `vec` | implemented+tested | ✓ | +| `vector` | implemented+tested | ✓ | +| `vector-of` | jvm-specific | ✓ | +| `vector?` | implemented+tested | ✓ | +| `volatile!` | implemented+tested | ✓ | +| `volatile?` | implemented+tested | ✓ | +| `vreset!` | implemented+tested | ✓ | +| `vswap!` | implemented+tested | ✓ | +| `when` | implemented+tested | ✓ | +| `when-first` | implemented+tested | ✓ | +| `when-let` | implemented+tested | ✓ | +| `when-not` | implemented+tested | ✓ | +| `when-some` | implemented+tested | ✓ | +| `while` | implemented+tested | ✓ | +| `with-bindings` | missing-portable | ✓ | +| `with-bindings*` | missing-portable | ✓ | +| `with-in-str` | missing-portable | ✓ | +| `with-loading-context` | jvm-specific | | +| `with-local-vars` | missing-portable | ✓ | +| `with-meta` | implemented+tested | ✓ | +| `with-open` | missing-portable | ✓ | +| `with-out-str` | implemented+tested | ✓ | +| `with-precision` | missing-portable | ✓ | +| `with-redefs` | missing-portable | ✓ | +| `with-redefs-fn` | missing-portable | ✓ | +| `xml-seq` | implemented-untested | ✓ | +| `zero?` | implemented+tested | ✓ | +| `zipmap` | implemented+tested | ✓ | diff --git a/tools/spec_coverage.py b/tools/spec_coverage.py new file mode 100644 index 0000000..b42d8e7 --- /dev/null +++ b/tools/spec_coverage.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Generate docs/spec/coverage.md — the spec status dashboard. + +Cross-references three sources: + 1. clojuredocs-export.json — the clojure.core var inventory (the surface) + 2. jolt's interned clojure.core mappings (via janet) + 3. symbols exercised by test/spec/* + the 3-path conformance suite + +Run from the repo root: python3 tools/spec_coverage.py +""" +import json, re, subprocess, glob, datetime +from collections import Counter + +# --- 1. the surface -------------------------------------------------------- +data = json.load(open('clojuredocs-export.json')) +core = sorted(v['name'] for v in data['vars'] if v['ns'] == 'clojure.core') +examples = {v['name'] for v in data['vars'] + if v['ns'] == 'clojure.core' and v.get('examples')} + +# --- 2. what jolt provides --------------------------------------------------- +# Two notions: INTERNED (in clojure.core's mappings — visible to ns +# introspection) and RESOLVABLE (usable in code; some seed fns resolve through +# fallback paths without being interned — itself a conformance finding). +janet_prog = '''(use ./src/jolt/api) (use ./src/jolt/types) (use ./src/jolt/reader) +(def ctx (init)) +(def core (ctx-find-ns ctx "clojure.core")) +(each n (sort (keys (core :mappings))) (print "I " n)) +(def names (string/split "\\n" (slurp "/tmp/spec-surface-names.txt"))) +(each n names + (when (> (length n) 0) + # value-position probe: seed fns resolve through the core fallback even + # when not interned (and jolt resolve can't see them — a finding itself). + (def r (protect (do (def form (parse-string n)) + (when (and (struct? form) (= :symbol (form :jolt/type))) + (eval-string ctx n) true)))) + (when (and (r 0) (= true (r 1))) (print "R " n))))''' +open('/tmp/spec-surface-names.txt','w').write('\n'.join(core)) +out = subprocess.run(['janet', '-e', janet_prog], capture_output=True, text=True) +interned = {l[2:] for l in out.stdout.splitlines() if l.startswith('I ')} +resolvable = {l[2:] for l in out.stdout.splitlines() if l.startswith('R ')} +jolt = interned | resolvable + +# --- 3. what the tests exercise -------------------------------------------- +tested = set() +sym = re.compile(r'\(([a-zA-Z*+!?<>=_-][a-zA-Z0-9*+!?<>=_./-]*)') +for f in glob.glob('test/spec/*.janet') + ['test/integration/conformance-test.janet']: + tested |= set(sym.findall(open(f).read())) + +# --- classification --------------------------------------------------------- +SPECIAL = {'catch','finally','do','def','defmacro','fn','if','let','loop','quote', + 'recur','throw','try','var','new','set!','monitor-enter','monitor-exit'} +AGENTS = {'agent','send','send-off','send-via','await','await-for','await1', + 'agent-error','agent-errors','clear-agent-errors','error-handler', + 'error-mode','set-agent-send-executor!','set-agent-send-off-executor!', + 'restart-agent','shutdown-agents','release-pending-sends','add-tap', + 'tap>','remove-tap','set-error-handler!','set-error-mode!'} +STM = {'dosync','ref','ref-set','alter','commute','ensure','ref-history-count', + 'ref-max-history','ref-min-history','sync','io!'} +JVM = {'class','class?','cast','bases','supers','compile','add-classpath', + 'definline','bean','accessor','create-struct','defstruct','struct', + 'struct-map','amap','areduce','memfn','enumeration-seq','iterator-seq', + 'resultset-seq','print-ctor','print-dup','print-method','print-simple', + 'primitives-classnames','vector-of','PrintWriter-on', + 'StackTraceElement->vec','Throwable->map','Inst','->ArrayChunk','->Vec', + '->VecNode','->VecSeq','-cache-protocol-fn','-reset-methods','EMPTY-NODE', + 'method-sig','proxy-name','gen-class','gen-interface','find-protocol-impl', + 'find-protocol-method','with-loading-context','load','load-file', + 'load-reader','loaded-libs','requiring-resolve','default-data-readers', + '..','.','pcalls','pmap','pvalues','stream-into!','stream-reduce!', + 'stream-seq!','stream-transduce!','mix-collection-hash','iteration', + 'unquote','unquote-splicing'} + +def classify(n): + if n in jolt: + return 'implemented+tested' if n in tested else 'implemented-untested' + if re.match(r'^\*.*\*$', n): return 'dynamic-var' + if n in SPECIAL: return 'special-form' + if n in AGENTS: return 'agents-taps' + if n in STM: return 'stm-refs' + if n in JVM: return 'jvm-specific' + return 'missing-portable' + +cls = {n: classify(n) for n in core} +counts = Counter(cls.values()) +stamp = datetime.date.today().isoformat() + +rows = [] +for n in core: + rows.append(f"| `{n}` | {cls[n]} | {'✓' if n in examples else ''} |") + +md = f"""# Appendix A — Coverage Dashboard (generated) + +Generated {stamp} by `tools/spec_coverage.py` — do not edit by hand. + +Surface: **{len(core)}** clojure.core vars (ClojureDocs export; {len(examples)} with +community examples). jolt interns {len(jolt & set(core))} of them. + +| Status | Count | Meaning | +|---|---|---| +| implemented+tested | {counts['implemented+tested']} | in jolt and exercised by spec/conformance | +| implemented-untested | {counts['implemented-untested']} | in jolt, no direct test — spec entries will add them | +| resolvable-not-interned | {len((resolvable - interned) & set(core))} | works in code but invisible to ns introspection (conformance finding) | +| missing-portable | {counts['missing-portable']} | portable semantics, jolt lacks it — implementation gap | +| special-form | {counts['special-form']} | specified in §3, not a library var | +| dynamic-var | {counts['dynamic-var']} | classification needed: portable default vs host-dependent | +| agents-taps | {counts['agents-taps']} | out of scope pending concurrency design note | +| stm-refs | {counts['stm-refs']} | out of scope pending concurrency design note | +| jvm-specific | {counts['jvm-specific']} | catalogued, not specified | + +Classifications are initial and mechanical — reclassifying is an ordinary +spec change. A var is *Verified* only when its §9 entry exists and carries no +UNVERIFIED field; that column will be added as entries land. + +## Per-var status + +| Var | Status | ClojureDocs examples | +|---|---|---| +{chr(10).join(rows)} +""" +open('docs/spec/coverage.md', 'w').write(md) +print(f"wrote docs/spec/coverage.md — {len(core)} vars") +for k, v in counts.most_common(): print(f" {k}: {v}")