feat: persistent singly-linked lists with O(1) conj/cons prepend

Round 3 of the persistent-collections work. Lists were immutable Janet arrays,
so conj/cons-prepend was an O(n) copy (O(n^2) to build a list) — a large perf
gap vs Clojure's PersistentList.

Add src/jolt/plist.janet: an immutable cons-cell list (first/rest/count), same
algorithm as Clojure/CLJS/jank PersistentList. conj/cons onto a list now creates
an O(1) node that shares the existing list as its tail (no copy), with a cached
O(1) count. Repeated conj is O(n) total instead of O(n^2).

Hooked plist through first/rest/next/seq/count/peek/pop/nth/empty/empty?, the
predicates (list?/seq?/coll?/sequential?), realize-for-iteration, =, coll->cells
(concat/lazy), both printers, destructuring, and instance? tags. (list ...) and
quoted lists stay arrays; only conj/cons introduce plist nodes, so the surface
and risk stay small.

Verified: reduce-conj of 200k elements runs in ~0.4s (was effectively O(n^2)).
conformance 206/206, features 78/78 (+7 list regressions), jank 120 (+1).
This commit is contained in:
Yogthos 2026-06-04 19:20:27 -04:00
parent e43f39bc36
commit a0c9696900
9 changed files with 142 additions and 22 deletions

View file

@ -1,14 +1,16 @@
(use ../src/jolt/types)
(use ../src/jolt/pv)
(use ../src/jolt/plist)
(use ../src/jolt/reader)
(use ../src/jolt/evaluator)
(use ../src/jolt/core)
# Normalize jolt collection results to Janet tuples so Janet-level deep=/= can
# compare against tuple literals regardless of the (pvec) representation.
# compare against tuple literals regardless of the (pvec/plist) representation.
(defn norm [x]
(cond
(pvec? x) (tuple ;(map norm (pv->array x)))
(plist? x) (tuple ;(map norm (pl->array x)))
(tuple? x) (tuple ;(map norm x))
(array? x) (tuple ;(map norm x))
x))