@zakkster/lite-logn 0.1.0 → 0.4.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.
Files changed (6) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/LogN.d.ts +107 -0
  3. package/LogN.js +1031 -13
  4. package/README.md +150 -19
  5. package/llms.txt +91 -10
  6. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,148 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-09-17
10
+
11
+ ### Added
12
+
13
+ - **SkipList** -- the fourth member and the family's FIRST randomized, pointer-based
14
+ member: a pointer-free ordered map (key -> value) whose `get` / `set` / `delete` /
15
+ `successor` / `predecessor` are EXPECTED O(log n) via a probabilistic tower of
16
+ forward links stored as slot INDICES in a SINGLE flat `Uint32Array` of
17
+ `columns * (capacity + 1)` cells (stride-indexed `lvl*(capacity + 1) + slot`,
18
+ `NIL = 0`, slot 0 the head sentinel) over a private free-list (`NodePool`) -- never
19
+ a heap object per op. Surface: `get` / `set` (updates the value in place on an
20
+ existing key) / `delete` (idempotent) / `successor` (strictly greater) /
21
+ `predecessor` (strictly less) / `rangeIter(lo, hi)` (a VERSION-STAMPED iterator
22
+ over `[lo, hi]` inclusive, ascending; `+-Infinity` bounds allowed, mutation
23
+ mid-iteration throws) / `forEach` / `clear`, and `size` / `capacity` getters. Keys
24
+ are finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN /
25
+ +-Infinity fail closed with a `[lite-logn]` throw); values are finite numbers.
26
+ Level generation is ONE step of the repo's Numerical-Recipes LCG whose HIGH bits
27
+ draw a geometric height (`1 + clz32(word)`), instance-local seed, deterministic (a
28
+ fixed seed replays an identical structure; the low bits of the LCG are periodic, so
29
+ the high bits are used -- validated by a deterministic chi-square test, df = 15,
30
+ p > 0.001, over ~1e6 levels). `SL_MAX_CAPACITY = 0x03FFFFFF` (2^26 - 1: slot
31
+ indices fit a `Uint32`, `NIL = 0` reserves slot 0, and the column stride stays an
32
+ addressable length). Level columns are sized to `ceil(log2 cap) + 1` up front (NOT
33
+ grown lazily), so `_next` never reallocates -- trivially 0 B/op. `get` / `set` /
34
+ `delete` / `successor` / `predecessor` allocate zero bytes after construction.
35
+ Verified: torture 0 B/op on every hot lane (+ a 32 B/op control lane proving the
36
+ instrument has teeth), the private-pool conservation invariant `activeSlots +
37
+ freeListLength === capacity` after every soak cycle, leak `size 0/0`,
38
+ `gc major = 0`.
39
+ - **Witness: two more log lines.** `test/witness.mjs` gains SkipList's `get` and
40
+ `set` entries. Measured on this machine (shared R^2 floor 0.958): `get` R^2 ~
41
+ 0.97-0.99, slope ~ 9 ns/level (band `[5.27, 12.30]`, median 8.78 x [0.6, 1.4]),
42
+ gated over `[2^11, 2^17]` for dynamic range; `set` R^2 ~ 0.97-0.99, slope ~ 14
43
+ ns/level (band `[8.36, 19.50]`, median 13.93 x [0.6, 1.4]), gated over the
44
+ cache-resident `[2^9, 2^14]` so the fit sees the structural level count, not DRAM
45
+ latency (each op is measured where its logarithm is visible, not where the cache
46
+ wall is). Both O(n) foils leave the line: the linear-scan search foil (O(n) per
47
+ search) and the sorted-array insert foil (O(n) shift), each below the floor.
48
+ Because the member is EXPECTED (not worst-case) O(log n), the witness ALSO prints
49
+ the MAX single insert over a realistic randomized build trace -- the unlucky-tower
50
+ tail a mean hides.
51
+ - **ADR.** [`decisions/0006-skiplist.md`](./decisions/0006-skiplist.md) (D-06):
52
+ binds D-01 to an IN-FILE PRIVATE `NodePool` as DESIGN-PARITY with lite-o1's private
53
+ pools (identical free-list contract + conservation invariant), NOT a runtime dep on
54
+ lite-o1 (rejected: zero-runtime-deps law; SlotPool never shipped); records the PRNG
55
+ choice (NR LCG, high bits for the level), the randomized-honesty note (MAX
56
+ single-op + chi-square df), and the level-column memory decision (size up front,
57
+ never grow lazily, to protect the 0-B/op gate).
58
+
59
+ ### Unchanged
60
+
61
+ - **BinaryHeap, Fenwick and SegmentTree are byte-identical.** The v0.1.0 / v0.2.0 /
62
+ v0.3.0 member class bodies are untouched; only the file header roster, the
63
+ `VERSION` const, and the appended SkipList block (plus the `_lcgNext` / `NodePool`
64
+ / `_levelCap` helpers) changed in `LogN.js`.
65
+
66
+ ## [0.3.0] - 2026-09-17
67
+
68
+ ### Added
69
+
70
+ - **SegmentTree** -- the third member: an associative range-query AND a
71
+ point-update, BOTH O(log n), over a SINGLE flat `Float64Array(2 * length)`
72
+ (leaves at `n .. 2n-1`, `_t[0]` unused) via iterative bottom-up walks -- no
73
+ nodes, no pointers, no recursion on the hot path. The fold is chosen ONCE at
74
+ construction (`'min'` / `'max'` / `'sum'` / `'gcd'`) and cached as a small-int
75
+ `_k` combined by an INLINE switch in the hot body (no function ref, no closure,
76
+ no megamorphic call site). Surface: `query(lo, hi)` (INCLUSIVE both ends,
77
+ matching `Fenwick.rangeSum`) / `update(i, value)` (ABSOLUTE leaf set + ancestor
78
+ fix) / `at(i)` (O(1) leaf read), `length` / `kind` getters, `clear`, `forEach`,
79
+ and a static `SegmentTree.build(values, kind)` O(n) bottom-up bulk build (seed
80
+ leaves, then fold each internal node once deepest-first -- not n incremental
81
+ updates). The fold identity fills query accumulators and cleared / fresh leaves
82
+ (`sum -> 0`, `min -> +Infinity`, `max -> -Infinity`, `gcd -> 0`); it is a legal
83
+ RESULT but never a legal INPUT -- the value door rejects user `NaN` /
84
+ `+-Infinity` (and, for the `gcd` kind, negatives + non-integers), typeof-guarded
85
+ before coercion, with a `[lite-logn]` throw. `SEGTREE_MAX = 2^30 - 1` (HALF of
86
+ Fenwick's ceiling: the `2n` layout must keep `2n` a positive int32). `query` /
87
+ `update` / `at` allocate zero bytes after construction. Verified: torture 0 B/op
88
+ on every hot lane (+ a 32 B/op control lane proving the instrument has teeth),
89
+ leak `size 0/0`, `gc major = 0`.
90
+ - **Witness: two more straight log lines.** `test/witness.mjs` gains SegmentTree's
91
+ `update` and `query` entries. Measured on this machine (shared R^2 floor 0.958):
92
+ update R^2 ~ 0.99, slope ~ 3.2 ns/level (band `[2.29, 5.35]`, median 3.83 x
93
+ [0.6, 1.4]); query R^2 ~ 0.99, slope ~ 7 ns/level (band `[4.30, 10.04]`,
94
+ median 7.17 x [0.6, 1.4]). The gated sweep is pinned to EXACT powers of two in
95
+ `[2^10, 2^16]` (a segment-tree op touches a node per level spread across the
96
+ `2n` array, so above ~2^16 the tree leaves the steady cache band; exact powers
97
+ keep the range decomposition a regular node count). Both O(n) foils leave the
98
+ line: the whole-tree rebuild (O(n) per update, R^2 ~ 0.85) and the scan-fold
99
+ (O(n) per query, R^2 ~ 0.75), each below the floor.
100
+ - **ADR.** [`decisions/0005-segtree.md`](./decisions/0005-segtree.md) (D-05):
101
+ scope is point-update + range-query ONLY (no lazy propagation, no caller-supplied
102
+ fold) for v0.3.0; the fold is injected via a ctor-cached `_k` inline switch, not
103
+ a function ref; and the iterative `2n` layout is order-agnostic, so it is correct
104
+ ONLY for commutative + associative folds -- a future non-commutative fold is
105
+ routed to a pow2 layout instead.
106
+
107
+ ### Unchanged
108
+
109
+ - **BinaryHeap and Fenwick are byte-identical.** The v0.1.0 and v0.2.0 member class
110
+ bodies are untouched; only the file header roster, the `VERSION` const, and the
111
+ appended SegmentTree block changed in `LogN.js`.
112
+
113
+ ## [0.2.0] - 2026-09-17
114
+
115
+ ### Added
116
+
117
+ - **Fenwick** (Binary Indexed Tree) -- the second member: BOTH point-update AND
118
+ prefix-sum in O(log n) over a single flat `Float64Array`, via the lowest-set-bit
119
+ walk (`i & -i`). Surface: `update(i, delta)` / `prefix(i)` / `rangeSum(lo, hi)` /
120
+ `at(i)` / `set(i, value)`, a `length` getter, `clear`, `forEach`, and a static
121
+ `Fenwick.build(values)` O(n) LINEAR bulk build (each cell adds itself to its
122
+ parent in one forward pass -- not n incremental updates). Public indices are
123
+ 0-based in `[0, length)`; internally 1-based (`_t[0]` the unused identity
124
+ sentinel). `prefix(-1) === 0` is the empty-prefix base case; `rangeSum` and `at`
125
+ are pairs of inlined prefix walks. Values are finite numbers (negatives
126
+ allowed); NaN / +-Infinity / non-number fail closed (typeof-guarded before
127
+ coercion) with a `[lite-logn]` throw. `update` / `prefix` / `rangeSum` / `at` /
128
+ `set` allocate zero bytes after construction. `FENWICK_MAX = 2^31 - 1` (the
129
+ `i & -i` walk relies on signed-int32 two's complement, so indices stay in that
130
+ range). Verified: torture 0 B/op on every hot lane (+ a 32 B/op control lane
131
+ proving the instrument has teeth), leak `size 0/0`, `gc major = 0`.
132
+ - **Witness: two straight log lines.** `test/witness.mjs` gains Fenwick's `update`
133
+ and `prefix` entries. Measured on this machine (shared R^2 floor 0.958): update
134
+ R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level (band `[1.84, 4.30]`, median 3.07 x
135
+ [0.6, 1.4]); prefix R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level (band `[1.76, 4.10]`,
136
+ median 2.93 x [0.6, 1.4]). Both O(n) foils leave the line: the prefix-array
137
+ rebuild (O(n) per update) and the naive re-sum (O(n) per query) each fit at
138
+ R^2 ~ 0.76, below the floor.
139
+ - **ADR.** [`decisions/0004-witness-band.md`](./decisions/0004-witness-band.md)
140
+ (D-08): the R^2 floor (0.958) is frozen family-wide; each member calibrates its
141
+ OWN per-op slope band = median-of-15 fit-runs x [0.6, 1.4] (the same procedure
142
+ that set BinaryHeap's band). A cheaper op having a lower slope is expected, not
143
+ a regression.
144
+
145
+ ### Unchanged
146
+
147
+ - **BinaryHeap is byte-identical.** The v0.1.0 member's class body is untouched;
148
+ only the file header roster, the `VERSION` const, and the appended Fenwick block
149
+ changed in `LogN.js`.
150
+
9
151
  ## [0.1.0] - 2026-09-17
10
152
 
11
153
  ### Added
@@ -40,5 +182,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
40
182
  [`decisions/0003-pack.md`](./decisions/0003-pack.md) (D-07: `files[]` ships the
41
183
  six files only; `test/`, `benchmark/`, `decisions/`, `demo/` are repo-only).
42
184
 
43
- [Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...HEAD
185
+ [Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.4.0...HEAD
186
+ [0.4.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.3.0...v0.4.0
187
+ [0.3.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.2.0...v0.3.0
188
+ [0.2.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...v0.2.0
44
189
  [0.1.0]: https://github.com/PeshoVurtoleta/lite-logn/releases/tag/v0.1.0
package/LogN.d.ts CHANGED
@@ -64,3 +64,110 @@ export class BinaryHeap {
64
64
  capacity: number,
65
65
  ): BinaryHeap;
66
66
  }
67
+
68
+ /**
69
+ * A Fenwick tree (Binary Indexed Tree): BOTH point-update AND prefix-sum in
70
+ * O(log n) over a single flat Float64Array via the lowest-set-bit walk (i & -i).
71
+ * Public indices are 0-based in [0, length); internally 1-based (_t[0] the unused
72
+ * identity sentinel). Values are finite numbers (negatives allowed); NaN /
73
+ * Infinity / non-number fail closed. Every hot op allocates zero bytes.
74
+ */
75
+ export class Fenwick {
76
+ /** @param length exact element count; integer in [1, 2^31-1]. */
77
+ constructor(length: number);
78
+
79
+ /** Element count this tree was sized for. */
80
+ readonly length: number;
81
+
82
+ /** Add delta at 0-based index i. O(log n). Non-finite delta / out-of-range i throws. */
83
+ update(i: number, delta: number): this;
84
+ /** Sum of [0, i] inclusive (prefix(-1) === 0). O(log n). Out-of-range i throws. */
85
+ prefix(i: number): number;
86
+ /** Sum of [lo, hi] inclusive = prefix(hi) - prefix(lo-1). O(log n). lo > hi throws. */
87
+ rangeSum(lo: number, hi: number): number;
88
+ /** The single element at i = prefix(i) - prefix(i-1). O(log n). Out-of-range i throws. */
89
+ at(i: number): number;
90
+ /** Set the element at i to value (absolute). O(log n). Non-finite value throws. */
91
+ set(i: number, value: number): this;
92
+ /** Zero every element in place, keeping capacity. */
93
+ clear(): this;
94
+ /** Visit every element as (value, index, fenwick) in ascending index order. */
95
+ forEach(fn: (value: number, index: number, fenwick: Fenwick) => void): void;
96
+
97
+ /** O(n) linear bulk build from a finite-number array-like. */
98
+ static build(values: ArrayLike<number>): Fenwick;
99
+ }
100
+
101
+ /**
102
+ * A segment tree: an associative range-query AND a point-update, BOTH O(log n),
103
+ * over a single flat Float64Array(2n) (leaves at n..2n-1, _t[0] unused). The fold
104
+ * (min / max / sum / gcd) is chosen once at construction and cached. query(lo, hi)
105
+ * is INCLUSIVE both ends; update(i, value) sets an ABSOLUTE leaf value. Values are
106
+ * finite numbers (nonnegative integers for the gcd kind); NaN / +-Infinity / out-
107
+ * of-domain values fail closed. Every hot op allocates zero bytes.
108
+ */
109
+ export class SegmentTree {
110
+ /** @param length exact element count; integer in [1, 2^30-1].
111
+ * @param kind the frozen associative fold. */
112
+ constructor(length: number, kind: 'min' | 'max' | 'sum' | 'gcd');
113
+
114
+ /** Element count this tree was sized for. */
115
+ readonly length: number;
116
+ /** The frozen associative fold. */
117
+ readonly kind: 'min' | 'max' | 'sum' | 'gcd';
118
+
119
+ /** Folded value over [lo, hi] inclusive both ends. O(log n). Throws on OOB or lo > hi. */
120
+ query(lo: number, hi: number): number;
121
+ /** Set leaf i to value (absolute), fixing ancestors. O(log n). Non-finite / OOB throws. */
122
+ update(i: number, value: number): this;
123
+ /** The single element at leaf i. O(1). Out-of-range i throws. */
124
+ at(i: number): number;
125
+ /** Reset every element to the fold identity, keeping capacity. */
126
+ clear(): this;
127
+ /** Visit every element as (value, index, tree) in ascending leaf order. */
128
+ forEach(fn: (value: number, index: number, tree: SegmentTree) => void): void;
129
+
130
+ /** O(n) bottom-up bulk build from a finite-number array-like and a fold kind. */
131
+ static build(values: ArrayLike<number>, kind: 'min' | 'max' | 'sum' | 'gcd'): SegmentTree;
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
+ }