Bring the docs in line with the actual implementation now that Chez is the sole substrate. Deleted the migration/spike/handoff artifacts that only documented the Janet era or the port effort: the port plan, phase-0 and foundational-runtime spike writeups (+ the stray root-level copy), the self-hosting design notes, the architecture-refactor plan, and spike/chez/RESULTS.md. Rewrote the current reference docs against the Chez facts: building-and-deps and tools-deps (no jpm/build step — bin/joltc off the checked-in seed, deps via jolt.deps into ~/.jolt/gitlibs), libraries (SQLite is built-in jdbc.core over libsqlite3, not a Janet driver), the conformance/spec test-flow docs (the Chez corpus runner + certify, no .janet harnesses), and the transient / type-hint / seed-overlay design notes (Chez representations: mutable transients, flat copy-on-write vectors, HAMT maps, the seed/overlay twin). Fixed the README collections line (vectors aren't 32-way tries) and added the ffi/transient gate targets. rfc 0001's numerics open-question is resolved (the Scheme tower). Renamed the built-in HTTP adapter to jolt.http.server only (dropped the ring-janet.adapter alias — a Janet-era name).
108 lines
5.4 KiB
Markdown
108 lines
5.4 KiB
Markdown
# RFC 0003: Transients — semantics and the Chez mutable backing
|
|
|
|
Status: accepted (design note)
|
|
|
|
This note pins down what transients *are* in Jolt, where their behavior
|
|
deviates from JVM Clojure and why, and how the transient machinery is
|
|
represented in the Chez runtime. It exists so the design doesn't revisit
|
|
transients every round.
|
|
|
|
## What a transient is in Jolt
|
|
|
|
A transient is a Chez record (`jolt-transient`, `host/chez/transients.ss`)
|
|
wrapping *true mutable* host backing, snapshotted to the immutable collection on
|
|
`persistent!`. The backing is per kind:
|
|
|
|
- transient vector — a growable Scheme vector (a capacity buffer plus a fill
|
|
count `n`). `conj!`/`pop!` are in-place, amortized O(1); the buffer doubles on
|
|
growth.
|
|
- transient map — a Chez hashtable keyed by `key-hash` / `jolt=`
|
|
(value-equality, nil-safe). Hashing by value keeps collection keys comparing
|
|
across representations.
|
|
- transient set — a Chez hashtable of elements.
|
|
- `cow` — a copy-on-write fallback for anything else (e.g. a sorted coll).
|
|
|
|
`transient` accepts pvecs, pmaps, psets, and the exotic colls handled by the
|
|
`cow` path. Each kind copies its source into the matching mutable backing once.
|
|
|
|
The bang ops (`conj!`, `assoc!`, `dissoc!`, `disj!`, `pop!`) mutate that backing
|
|
in place and return the transient — O(1) per op (amortized for the vector push).
|
|
`persistent!` snapshots a persistent value from the backing (folding the
|
|
hashtable into a pmap/pset, handing off the buffer as a pvec) and invalidates the
|
|
transient (the record's active flag clears; any further bang op or a second
|
|
`persistent!` throws "transient used after persistent!", matching Clojure's
|
|
invalidation contract).
|
|
|
|
Read ops work on an active transient where Clojure supports them: `get`,
|
|
`contains?`, `count`, and `nth` (vector kind) see through the transient.
|
|
`seq` on a transient is not supported, as in Clojure.
|
|
|
|
## Deviations from JVM Clojure (deliberate)
|
|
|
|
**O(n) edges, O(1) middle.** Clojure's `(transient v)` is O(1) — the transient
|
|
*shares* the persistent trie and marks nodes editable; `persistent!` is O(1)
|
|
too. Jolt's `transient` copies the source into a mutable buffer/hashtable (O(n))
|
|
and `persistent!` snapshots back (O(n)). The bang ops in between are host-mutable
|
|
O(1), which is *faster* per-op than trie editing. So the asymptotics of the usual
|
|
pattern
|
|
|
|
(persistent! (reduce conj! (transient []) coll))
|
|
|
|
are identical (O(n) total either way) with a better constant in the loop and a
|
|
worse constant at the two edges. The pattern transients exist for — batch
|
|
construction — is fully served. What is NOT served is transient-editing a
|
|
*large* collection to change a few keys: that's O(n) in Jolt vs O(log n) in
|
|
Clojure, because `transient` copies the source into a growable Scheme vector /
|
|
Chez hashtable and `persistent!` snapshots it back.
|
|
|
|
**No thread-ownership check.** JVM Clojure ≥1.7 also dropped the owner-thread
|
|
assertion (for fork/join), keeping only "don't use after persistent!", which
|
|
Jolt enforces. A transient handed across threads is a data race exactly as in
|
|
Clojure — documented, not checked, same as the JVM.
|
|
|
|
**`(conj!)` / `(conj! t)` arities** follow Clojure's transducer-era contract:
|
|
zero args makes a fresh `(transient [])`, one arg returns it untouched.
|
|
`assoc!` tolerates a dangling final key (treated as `k nil`), matching the
|
|
lenient kvs walk of Jolt's `assoc`.
|
|
|
|
**No transient sorted variants** — same as Clojure. One leniency: Clojure
|
|
throws on `(transient '(1))`, but Jolt routes a list through the `cow` fallback
|
|
path, yielding a transient. Harmless but non-Clojure; tighten if it ever
|
|
bites.
|
|
|
|
## Why transients live in the host
|
|
|
|
Transients are part of the value/representation layer in the Chez runtime
|
|
(`host/chez/transients.ss`), not the portable `clojure.core` overlay, on three
|
|
grounds:
|
|
|
|
1. **They are the mutation kernel.** A transient's entire value is direct
|
|
mutation of a host buffer/hashtable. The overlay has no mutation seam of its
|
|
own. Re-expressing the bang ops in Clojure would mean either growing the host
|
|
surface one-for-one (a host-vector-push, a host-hashtable-put, …, i.e. moving
|
|
the same code behind more indirection) or simulating mutation over persistent
|
|
values (defeating the point of transients).
|
|
|
|
2. **They sit under the collection dispatch.** `conj`/`assoc`/`get`/`count`/
|
|
`contains?` see through a transient. Hoisting the transient ops above that
|
|
dispatch would put a compiled-Clojure call inside the hottest paths for no
|
|
semantic gain — transients have no semantics to *fix*.
|
|
|
|
3. **The value layer is the host's job.** The persistent collections and, with
|
|
them, their mutable scratch counterparts, live in the Chez runtime alongside
|
|
the value model. Transients are representation, not library.
|
|
|
|
What lives in the overlay: anything *derived* — e.g. `into`'s transient-using
|
|
fast path, or `update!`-style conveniences — is plain Clojure over
|
|
`transient`/bang-ops/`persistent!`.
|
|
|
|
## Future work
|
|
|
|
- The persistent map/set are a bitmap HAMT with structural sharing
|
|
(`host/chez/collections.ss`), so Clojure-style O(1) `transient`/`persistent!`
|
|
via editable nodes is a real option there — an internal change behind the same
|
|
surface, not a semantics change. The persistent vector is a flat
|
|
copy-on-write Scheme vector rather than a trie, so the transient surface for
|
|
it stays the copy-to-growable-vector path.
|
|
- `transient?` (Jolt extension, useful in tests) stays; Clojure has no public
|
|
predicate, so it must not leak into portability-sensitive code.
|