@zakkster/lite-logn 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,131 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-09-20
10
+
11
+ ### Added
12
+
13
+ - **Treap** -- the fifth member and the family's balanced BST: a randomized,
14
+ self-balancing binary search tree that is ALSO an order-statistic tree (an AUGMENTED
15
+ ordered map key -> value). A BST order on `_key` x a MAX-HEAP order on a per-node
16
+ random priority `_prio` gives EXPECTED O(log n) height, and a subtree-size column
17
+ `_size` (maintained in the SAME pass as every link rewrite) adds O(log n) order
18
+ statistics + set surgery. Surface: `get` / `has` / `set` (updates the value in place
19
+ on an existing key) / `delete` (idempotent) / `rank(x)` (count of keys STRICTLY less
20
+ than x) / `select(k)` (the k-th smallest key, 0-based) / `successor` (strictly
21
+ greater) / `predecessor` (strictly less) / `rangeIter(lo, hi)` (a VERSION-STAMPED
22
+ iterator over `[lo, hi]` inclusive, ascending; `+-Infinity` bounds allowed,
23
+ structural OR value mutation mid-iteration throws) / `forEach` / `clear` / `split`,
24
+ the static `Treap.merge(a, b)`, and `size` / `capacity` getters. Keys and values are
25
+ finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
26
+ fail closed with a `[lite-logn]` throw).
27
+ - **Pointer-free, zero-GC, second bind of the shared NodePool.** Nodes are slot
28
+ INDICES in six flat columns (`_key` / `_value` Float64; `_left` / `_right` / `_prio`
29
+ / `_size` Uint32, `NIL = 0`) over the SAME private free-list (`NodePool`) SkipList
30
+ ships -- design-parity, not a fork or a runtime dep (decisions/0007-treap.md). The
31
+ conservation invariant `activeSlots + freeListLength === capacity` holds after every
32
+ op. `TR_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1: slot indices + subtree counts fit a
33
+ `Uint32`). Rotations rewrite one child link pair + two `_size` cells; `set` / `delete`
34
+ / `split` / `merge` recurse over slot indices on the native CALL STACK (not the GC
35
+ heap), so every hot op is 0 B/op.
36
+ - **Priority via the repo LCG.** One instance-local Numerical-Recipes LCG draw per
37
+ inserted node; a fixed seed replays an identical structure, ties break by key, so the
38
+ tree shape is a deterministic function of the (key, priority) set.
39
+ - **split / merge are O(log n) EXPECTED (arena-sharing).** `split(key)` returns
40
+ `[left (keys < key), right (keys >= key)]` by rewiring in place, so the two treaps
41
+ SHARE the source's backing arena and the source is CONSUMED (left empty);
42
+ `Treap.merge(a, b)` requires `a` / `b` to share an arena (all keys of a < all of b)
43
+ and consumes both. Fails closed on non-Treap inputs, cross-arena treaps, or an
44
+ overlapping key range.
45
+ - **EXPECTED, not worst-case.** A hot op is EXPECTED O(log n) (the randomized
46
+ priority heap); the MAX single insert (rotation chain) is DISCLOSED by the witness,
47
+ never gated -- the same honesty contract as SkipList.
48
+ - **Witness: one more log line.** `test/witness.mjs` gains `Treap.get` (a BST descent)
49
+ against a linear-scan O(n) foil. Measured on this machine (shared, FROZEN R^2 floor
50
+ 0.958, the four prior members' bands UNTOUCHED): `get` R^2 ~ 0.988, slope ~ 4.03
51
+ ns/level, in its OWN band `[2.55, 5.95]` = median-of-15 fit-runs (median 4.25) x
52
+ `[0.6, 1.4]`, centered on the median (ADR-0004). A treap descent touches one node per
53
+ level, so its per-level slope is lower than SkipList.get's (~8.78) -- expected, which
54
+ is why only the R^2 floor is shared. The linear-scan foil MISSES the floor
55
+ (R^2 ~ 0.79). All EIGHT gated op-rows are ON-LINE; MAX single insert disclosed
56
+ (~54 us on the cold shuffled build trace).
57
+ - **Types + docs.** `LogN.d.ts` gains the `Treap` ambient block; `llms.txt` gains the
58
+ Treap roster entry + full export surface; `decisions/0007-treap.md` records D-06/D-07
59
+ (the balanced-BST pick, the augmentation, the NodePool reuse, the arena-sharing
60
+ split/merge, and the recursion-depth disclosure).
61
+
62
+ ### Verified
63
+
64
+ - Torture: 0 B/op on every Treap hot lane (get / set / delete / rank / select /
65
+ successor / forEach / rangeIter) + the mixed steady-state churn; `gc major = 0`;
66
+ leak `size 0/0`; the private-pool conservation invariant after every soak cycle; a
67
+ 32 B/op control lane proving the instrument has teeth. Prior four members still green.
68
+ - `test/perf/PerfGate.test.mjs`: four Treap scenarios (get / set / delete / rank-select-
69
+ successor mix) at 0 scavenges, backing buffers fixed (the `grows` counter reads 0).
70
+ - `test/Treap.test.mjs`: a >= 1e5 mixed-op differential fuzz vs a Map + sorted-array
71
+ oracle (0 divergences), the three treap invariants checked throughout (BST order,
72
+ heap order, subtree-size correctness), rank/select/split/merge correctness, the
73
+ fail-closed doors ([lite-logn] tag pinned), determinism, and conservation.
74
+ - `LogN.js`: the prior four classes are BYTE-IDENTICAL; only the `VERSION` const and
75
+ the appended `Treap` section changed.
76
+
77
+ ## [0.4.0] - 2026-09-17
78
+
79
+ ### Added
80
+
81
+ - **SkipList** -- the fourth member and the family's FIRST randomized, pointer-based
82
+ member: a pointer-free ordered map (key -> value) whose `get` / `set` / `delete` /
83
+ `successor` / `predecessor` are EXPECTED O(log n) via a probabilistic tower of
84
+ forward links stored as slot INDICES in a SINGLE flat `Uint32Array` of
85
+ `columns * (capacity + 1)` cells (stride-indexed `lvl*(capacity + 1) + slot`,
86
+ `NIL = 0`, slot 0 the head sentinel) over a private free-list (`NodePool`) -- never
87
+ a heap object per op. Surface: `get` / `set` (updates the value in place on an
88
+ existing key) / `delete` (idempotent) / `successor` (strictly greater) /
89
+ `predecessor` (strictly less) / `rangeIter(lo, hi)` (a VERSION-STAMPED iterator
90
+ over `[lo, hi]` inclusive, ascending; `+-Infinity` bounds allowed, mutation
91
+ mid-iteration throws) / `forEach` / `clear`, and `size` / `capacity` getters. Keys
92
+ are finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN /
93
+ +-Infinity fail closed with a `[lite-logn]` throw); values are finite numbers.
94
+ Level generation is ONE step of the repo's Numerical-Recipes LCG whose HIGH bits
95
+ draw a geometric height (`1 + clz32(word)`), instance-local seed, deterministic (a
96
+ fixed seed replays an identical structure; the low bits of the LCG are periodic, so
97
+ the high bits are used -- validated by a deterministic chi-square test, df = 15,
98
+ p > 0.001, over ~1e6 levels). `SL_MAX_CAPACITY = 0x03FFFFFF` (2^26 - 1: slot
99
+ indices fit a `Uint32`, `NIL = 0` reserves slot 0, and the column stride stays an
100
+ addressable length). Level columns are sized to `ceil(log2 cap) + 1` up front (NOT
101
+ grown lazily), so `_next` never reallocates -- trivially 0 B/op. `get` / `set` /
102
+ `delete` / `successor` / `predecessor` allocate zero bytes after construction.
103
+ Verified: torture 0 B/op on every hot lane (+ a 32 B/op control lane proving the
104
+ instrument has teeth), the private-pool conservation invariant `activeSlots +
105
+ freeListLength === capacity` after every soak cycle, leak `size 0/0`,
106
+ `gc major = 0`.
107
+ - **Witness: two more log lines.** `test/witness.mjs` gains SkipList's `get` and
108
+ `set` entries. Measured on this machine (shared R^2 floor 0.958): `get` R^2 ~
109
+ 0.97-0.99, slope ~ 9 ns/level (band `[5.27, 12.30]`, median 8.78 x [0.6, 1.4]),
110
+ gated over `[2^11, 2^17]` for dynamic range; `set` R^2 ~ 0.97-0.99, slope ~ 14
111
+ ns/level (band `[8.36, 19.50]`, median 13.93 x [0.6, 1.4]), gated over the
112
+ cache-resident `[2^9, 2^14]` so the fit sees the structural level count, not DRAM
113
+ latency (each op is measured where its logarithm is visible, not where the cache
114
+ wall is). Both O(n) foils leave the line: the linear-scan search foil (O(n) per
115
+ search) and the sorted-array insert foil (O(n) shift), each below the floor.
116
+ Because the member is EXPECTED (not worst-case) O(log n), the witness ALSO prints
117
+ the MAX single insert over a realistic randomized build trace -- the unlucky-tower
118
+ tail a mean hides.
119
+ - **ADR.** [`decisions/0006-skiplist.md`](./decisions/0006-skiplist.md) (D-06):
120
+ binds D-01 to an IN-FILE PRIVATE `NodePool` as DESIGN-PARITY with lite-o1's private
121
+ pools (identical free-list contract + conservation invariant), NOT a runtime dep on
122
+ lite-o1 (rejected: zero-runtime-deps law; SlotPool never shipped); records the PRNG
123
+ choice (NR LCG, high bits for the level), the randomized-honesty note (MAX
124
+ single-op + chi-square df), and the level-column memory decision (size up front,
125
+ never grow lazily, to protect the 0-B/op gate).
126
+
127
+ ### Unchanged
128
+
129
+ - **BinaryHeap, Fenwick and SegmentTree are byte-identical.** The v0.1.0 / v0.2.0 /
130
+ v0.3.0 member class bodies are untouched; only the file header roster, the
131
+ `VERSION` const, and the appended SkipList block (plus the `_lcgNext` / `NodePool`
132
+ / `_levelCap` helpers) changed in `LogN.js`.
133
+
9
134
  ## [0.3.0] - 2026-09-17
10
135
 
11
136
  ### Added
@@ -125,7 +250,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
125
250
  [`decisions/0003-pack.md`](./decisions/0003-pack.md) (D-07: `files[]` ships the
126
251
  six files only; `test/`, `benchmark/`, `decisions/`, `demo/` are repo-only).
127
252
 
128
- [Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.3.0...HEAD
253
+ [Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.4.0...HEAD
254
+ [0.4.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.3.0...v0.4.0
129
255
  [0.3.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.2.0...v0.3.0
130
256
  [0.2.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...v0.2.0
131
257
  [0.1.0]: https://github.com/PeshoVurtoleta/lite-logn/releases/tag/v0.1.0
package/LogN.d.ts CHANGED
@@ -130,3 +130,99 @@ export class SegmentTree {
130
130
  /** O(n) bottom-up bulk build from a finite-number array-like and a fold kind. */
131
131
  static build(values: ArrayLike<number>, kind: 'min' | 'max' | 'sum' | 'gcd'): SegmentTree;
132
132
  }
133
+
134
+ /**
135
+ * A skip list: a pointer-free ordered map (key -> value) whose get / set / delete /
136
+ * successor / predecessor are EXPECTED O(log n) via a probabilistic tower of forward
137
+ * links stored as slot INDICES in flat Uint32Array columns over a private free-list
138
+ * (no heap objects per op). Keys are finite numbers (typeof-guarded before coercion;
139
+ * Symbol / BigInt / NaN / +-Infinity fail closed); values are finite numbers. set on
140
+ * an existing key updates the value in place. An unsupplied seed defaults to a fixed
141
+ * constant; a fixed seed replays an identical structure. Fixed capacity: a full pool
142
+ * throws. Every hot op allocates zero bytes.
143
+ */
144
+ export class SkipList {
145
+ /** @param capacity exact max live entries; integer in [1, 2^26-1].
146
+ * @param seed PRNG seed; unsigned 32-bit integer (default fixed). */
147
+ constructor(capacity: number, seed?: number);
148
+
149
+ /** Live entry count. */
150
+ readonly size: number;
151
+ /** The fixed capacity this list was sized for. */
152
+ readonly capacity: number;
153
+
154
+ /** The value under key, or undefined if absent (no throw). Non-finite key throws. */
155
+ get(key: number): number | undefined;
156
+ /** Insert key -> value, or update the value in place if key exists. Non-finite
157
+ * key/value throws; a full pool throws. */
158
+ set(key: number, value: number): this;
159
+ /** Remove key; true if it was present, false if absent (idempotent). Non-finite key throws. */
160
+ delete(key: number): boolean;
161
+ /** The smallest key strictly greater than key, or undefined. Non-finite key throws. */
162
+ successor(key: number): number | undefined;
163
+ /** The largest key strictly less than key, or undefined. Non-finite key throws. */
164
+ predecessor(key: number): number | undefined;
165
+ /** A version-stamped iterator over keys in [lo, hi] inclusive, ascending. Bounds
166
+ * may be +-Infinity (unbounded ends); NaN or lo > hi throws; mutation during
167
+ * iteration throws. */
168
+ rangeIter(lo: number, hi: number): IterableIterator<number>;
169
+ /** Visit every (key, value) pair in ascending key order. */
170
+ forEach(fn: (key: number, value: number, list: SkipList) => void): void;
171
+ /** Empty the list, keeping capacity (resets the PRNG to its initial seed). */
172
+ clear(): this;
173
+ }
174
+
175
+ /**
176
+ * A treap: a randomized, self-balancing BST that is also an order-statistic tree (an
177
+ * AUGMENTED ordered map key -> value). BST order on keys x max-heap order on a per-node
178
+ * random priority gives EXPECTED O(log n) height; a subtree-size column adds O(log n)
179
+ * rank / select / split / merge. Nodes are slot INDICES in flat typed-array columns
180
+ * over a private free-list (no heap object per op). Keys and values are finite numbers
181
+ * (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed). set
182
+ * on an existing key updates the value in place. EXPECTED, not worst-case (an unlucky
183
+ * priority draw can spike one op; the MAX single insert is disclosed, not gated). Fixed
184
+ * capacity: a full pool throws. Every hot op allocates zero bytes.
185
+ */
186
+ export class Treap {
187
+ /** @param capacity exact max live entries; integer in [1, 2^31-1].
188
+ * @param seed PRNG seed; unsigned 32-bit integer (default fixed). */
189
+ constructor(capacity: number, seed?: number);
190
+
191
+ /** Live entry count. */
192
+ readonly size: number;
193
+ /** The fixed capacity this treap was sized for. */
194
+ readonly capacity: number;
195
+
196
+ /** The value under key, or undefined if absent (no throw). Non-finite key throws. */
197
+ get(key: number): number | undefined;
198
+ /** True iff key is currently stored. Non-finite key throws. */
199
+ has(key: number): boolean;
200
+ /** Insert key -> value, or update the value in place if key exists. Non-finite
201
+ * key/value throws; a full pool throws. */
202
+ set(key: number, value: number): this;
203
+ /** Remove key; true if it was present, false if absent (idempotent). Non-finite key throws. */
204
+ delete(key: number): boolean;
205
+ /** Count of stored keys strictly less than x (its rank), in [0, size]. Non-finite x throws. */
206
+ rank(x: number): number;
207
+ /** The k-th smallest key (0-based), or undefined if k is out of [0, size). Non-integer k throws. */
208
+ select(k: number): number | undefined;
209
+ /** The smallest key strictly greater than key, or undefined. Non-finite key throws. */
210
+ successor(key: number): number | undefined;
211
+ /** The largest key strictly less than key, or undefined. Non-finite key throws. */
212
+ predecessor(key: number): number | undefined;
213
+ /** A version-stamped iterator over keys in [lo, hi] inclusive, ascending. Bounds
214
+ * may be +-Infinity (unbounded ends); NaN or lo > hi throws; mutation during
215
+ * iteration throws. */
216
+ rangeIter(lo: number, hi: number): IterableIterator<number>;
217
+ /** Visit every (key, value) pair in ascending key order. */
218
+ forEach(fn: (key: number, value: number, treap: Treap) => void): void;
219
+ /** Empty the treap, keeping capacity (resets the PRNG to its initial seed). */
220
+ clear(): this;
221
+
222
+ /** Split at key into [left (keys < key), right (keys >= key)]; CONSUMES this and
223
+ * returns two treaps SHARING this treap's backing arena. Non-finite key throws. */
224
+ split(key: number): [Treap, Treap];
225
+ /** Merge two arena-sharing treaps where every key of a < every key of b, CONSUMING
226
+ * both. Non-Treap inputs, different arenas, or an overlapping range throw. */
227
+ static merge(a: Treap, b: Treap): Treap;
228
+ }