@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 +127 -1
- package/LogN.d.ts +96 -0
- package/LogN.js +1062 -11
- package/README.md +169 -9
- package/llms.txt +83 -6
- package/package.json +7 -3
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.
|
|
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
|
+
}
|