@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.
- package/CHANGELOG.md +146 -1
- package/LogN.d.ts +107 -0
- package/LogN.js +1031 -13
- package/README.md +150 -19
- package/llms.txt +91 -10
- 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.
|
|
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
|
+
}
|