hashrope 0.1.0__py3-none-any.whl

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.
hashrope/__init__.py ADDED
@@ -0,0 +1,64 @@
1
+ """
2
+ hashrope -- Hash rope data structure with algebraic content addressing.
3
+
4
+ A balanced binary tree (BB[2/7]) augmented with polynomial hash metadata
5
+ at every node. Supports O(log w) concat, split, and substring hashing,
6
+ plus O(log q) repetition encoding via RepeatNode and geometric accumulator.
7
+
8
+ All arithmetic is performed modulo a Mersenne prime using bit-shift
9
+ reduction (no expensive division).
10
+ """
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ from hashrope.polynomial_hash import (
15
+ PolynomialHash,
16
+ phi,
17
+ mersenne_mod,
18
+ mersenne_mul,
19
+ MERSENNE_61,
20
+ MERSENNE_127,
21
+ )
22
+ from hashrope.rope import (
23
+ Leaf,
24
+ Internal,
25
+ RepeatNode,
26
+ Node,
27
+ rope_concat,
28
+ rope_split,
29
+ rope_repeat,
30
+ rope_substr_hash,
31
+ rope_len,
32
+ rope_hash,
33
+ rope_from_bytes,
34
+ rope_to_bytes,
35
+ validate_rope,
36
+ )
37
+ from hashrope.sliding import SlidingWindow
38
+
39
+ __all__ = [
40
+ # Polynomial hash
41
+ "PolynomialHash",
42
+ "phi",
43
+ "mersenne_mod",
44
+ "mersenne_mul",
45
+ "MERSENNE_61",
46
+ "MERSENNE_127",
47
+ # Rope nodes
48
+ "Leaf",
49
+ "Internal",
50
+ "RepeatNode",
51
+ "Node",
52
+ # Rope operations
53
+ "rope_concat",
54
+ "rope_split",
55
+ "rope_repeat",
56
+ "rope_substr_hash",
57
+ "rope_len",
58
+ "rope_hash",
59
+ "rope_from_bytes",
60
+ "rope_to_bytes",
61
+ "validate_rope",
62
+ # Sliding window
63
+ "SlidingWindow",
64
+ ]
@@ -0,0 +1,241 @@
1
+ """
2
+ Polynomial hash over Z/pZ with Mersenne prime arithmetic.
3
+
4
+ Implements Definitions 1-3, Lemmas 1-2, 13-14, Theorems 1-3, 5
5
+ from the compressed-domain hashing framework.
6
+
7
+ All arithmetic is performed modulo a Mersenne prime p = 2^61 - 1
8
+ using bit-shift reduction (Lemma 14) to avoid expensive division.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Constants
15
+ # ---------------------------------------------------------------------------
16
+
17
+ MERSENNE_61: int = (1 << 61) - 1 # 2^61 - 1
18
+ MERSENNE_127: int = (1 << 127) - 1 # 2^127 - 1 (recommended for high security, Corollary 5)
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Mersenne prime arithmetic (Lemma 14)
23
+ # ---------------------------------------------------------------------------
24
+
25
+ def mersenne_mod(a: int, p: int = MERSENNE_61) -> int:
26
+ """
27
+ Reduce a non-negative integer a modulo a Mersenne prime p = 2^k - 1.
28
+
29
+ Uses the identity: a mod (2^k - 1) = (a >> k) + (a & ((1 << k) - 1))
30
+ with at most one conditional subtraction.
31
+
32
+ Proof (Lemma 14): Write a = q·2^k + r. Then a = q·(p+1) + r = q·p + (q+r),
33
+ so a ≡ q + r (mod p). Since q + r < 2^(k+1), at most one subtraction suffices.
34
+ """
35
+ # Determine k from p: p = 2^k - 1, so k = p.bit_length()
36
+ k = p.bit_length()
37
+ # Repeated folding for very large values
38
+ while a >= (1 << (2 * k)):
39
+ a = (a >> k) + (a & p)
40
+ # Final fold
41
+ a = (a >> k) + (a & p)
42
+ # At most one more fold needed
43
+ a = (a >> k) + (a & p)
44
+ # Conditional subtraction
45
+ if a >= p:
46
+ a -= p
47
+ return a
48
+
49
+
50
+ def mersenne_mul(a: int, b: int, p: int = MERSENNE_61) -> int:
51
+ """
52
+ Compute (a * b) mod p for Mersenne prime p.
53
+
54
+ Python's arbitrary-precision integers make this straightforward:
55
+ multiply natively, then reduce via mersenne_mod.
56
+ """
57
+ return mersenne_mod(a * b, p)
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Geometric accumulator Φ (Definition 3, Theorem 3)
62
+ # ---------------------------------------------------------------------------
63
+
64
+ def phi(q: int, alpha: int, p: int = MERSENNE_61) -> int:
65
+ """
66
+ Compute Φ(q, α) = Σ_{i=0}^{q-1} α^i (mod p) via inverse-free
67
+ repeated doubling.
68
+
69
+ Recurrence (Theorem 3):
70
+ Φ(0, α) = 0
71
+ Φ(1, α) = 1
72
+ Φ(2k, α) = Φ(k, α) · (1 + α^k)
73
+ Φ(2k+1, α) = Φ(2k, α) · α + 1
74
+
75
+ Handles the degenerate case α ≡ 1 (mod p) correctly,
76
+ yielding Φ(q, 1) = q (mod p) (Lemma 3).
77
+
78
+ Time: O(log q) multiplications in Z/pZ.
79
+
80
+ Implementation note: We scan q's bits left-to-right (MSB to LSB).
81
+ We maintain alpha_power = α^n where n is the current accumulated
82
+ value of q built so far. At each step:
83
+ - Doubling (n → 2n): alpha_power squares (α^n → α^(2n))
84
+ - Odd step (n → n+1): alpha_power multiplies by α (α^n → α^(n+1))
85
+ This ensures the correct α^k is used in each doubling step.
86
+ """
87
+ if q == 0:
88
+ return 0
89
+ if q == 1:
90
+ return 1
91
+
92
+ bits = q.bit_length()
93
+
94
+ phi_val = 1 # Φ(1, α) = 1
95
+ alpha_power = alpha # α^1 — tracks α^(current n)
96
+
97
+ # Scan from the second-most-significant bit down to bit 0
98
+ for i in range(bits - 2, -1, -1):
99
+ # Even step: Φ(2k, α) = Φ(k, α) · (1 + α^k)
100
+ # alpha_power currently holds α^k
101
+ phi_val = mersenne_mul(phi_val, (1 + alpha_power) % p, p)
102
+ # α^k → α^(2k)
103
+ alpha_power = mersenne_mul(alpha_power, alpha_power, p)
104
+
105
+ # If current bit is 1: odd step (2k → 2k+1)
106
+ if (q >> i) & 1:
107
+ # Φ(2k+1, α) = Φ(2k, α) · α + 1
108
+ phi_val = (mersenne_mul(phi_val, alpha, p) + 1) % p
109
+ # α^(2k) → α^(2k+1)
110
+ alpha_power = mersenne_mul(alpha_power, alpha, p)
111
+
112
+ return phi_val
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Polynomial Hash (Definition 2, Theorems 1-2, 5)
117
+ # ---------------------------------------------------------------------------
118
+
119
+ class PolynomialHash:
120
+ """
121
+ Polynomial hash function over Z/pZ.
122
+
123
+ H(s) = Σ_{i=0}^{n-1} (s_i + 1) · x^(n-1-i) (mod p)
124
+
125
+ The +1 offset (Definition 2) ensures no byte maps to zero in Z/pZ,
126
+ preventing the zero-padding collision (Lemma 1).
127
+
128
+ Parameters
129
+ ----------
130
+ prime : int
131
+ A Mersenne prime. Default: 2^61 - 1.
132
+ base : int
133
+ The hash base x, chosen from {2, ..., p-1}.
134
+ """
135
+
136
+ __slots__ = ("_p", "_x", "_power_cache")
137
+
138
+ def __init__(self, prime: int = MERSENNE_61, base: int = 131) -> None:
139
+ if base < 2 or base >= prime:
140
+ raise ValueError(f"Base must be in [2, p-1], got {base}")
141
+ self._p = prime
142
+ self._x = base
143
+ self._power_cache: dict[int, int] = {0: 1, 1: base}
144
+
145
+ @property
146
+ def prime(self) -> int:
147
+ return self._p
148
+
149
+ @property
150
+ def base(self) -> int:
151
+ return self._x
152
+
153
+ def power(self, n: int) -> int:
154
+ """
155
+ Compute x^n mod p with caching.
156
+
157
+ Uses Python's built-in pow(x, n, p) which implements
158
+ binary exponentiation (Lemma 13).
159
+ """
160
+ if n in self._power_cache:
161
+ return self._power_cache[n]
162
+ result = pow(self._x, n, self._p)
163
+ self._power_cache[n] = result
164
+ return result
165
+
166
+ def hash(self, data: bytes) -> int:
167
+ """
168
+ Compute H(data) = Σ (data[i] + 1) · x^(n-1-i) (mod p).
169
+
170
+ For the empty string, returns 0 (Definition 2).
171
+
172
+ Parameters
173
+ ----------
174
+ data : bytes
175
+ The byte string to hash.
176
+
177
+ Returns
178
+ -------
179
+ int
180
+ Hash value in [0, p-1].
181
+ """
182
+ if len(data) == 0:
183
+ return 0
184
+
185
+ p = self._p
186
+ x = self._x
187
+ h = 0
188
+ for byte in data:
189
+ # h = h · x + (byte + 1)
190
+ h = mersenne_mod(h * x + byte + 1, p)
191
+ return h
192
+
193
+ def hash_concat(self, h_a: int, len_b: int, h_b: int) -> int:
194
+ """
195
+ Compute H(A ‖ B) from H(A), |B|, H(B) via Theorem 1.
196
+
197
+ H(A ‖ B) = H(A) · x^|B| + H(B) (mod p)
198
+ """
199
+ x_pow = self.power(len_b)
200
+ return mersenne_mod(h_a * x_pow + h_b, self._p)
201
+
202
+ def hash_repeat(self, h_s: int, d: int, q: int) -> int:
203
+ """
204
+ Compute H(S^q) from H(S) and |S| = d via Theorem 2.
205
+
206
+ H(S^q) = H(S) · Φ(q, x^d) (mod p)
207
+ """
208
+ x_d = self.power(d)
209
+ phi_val = phi(q, x_d, self._p)
210
+ return mersenne_mul(h_s, phi_val, self._p)
211
+
212
+ def hash_overlap(self, h_p: int, d: int, l: int, h_prefix: int) -> int:
213
+ """
214
+ Compute H(W) for overlapping back-reference via Theorem 5.
215
+
216
+ W = P^q ‖ P[0..r-1] where q = ⌊l/d⌋, r = l mod d.
217
+
218
+ H(W) = H(P) · Φ(q, x^d) · x^r + H(P[0..r-1]) (mod p)
219
+
220
+ Parameters
221
+ ----------
222
+ h_p : int
223
+ H(P), hash of the pattern of length d.
224
+ d : int
225
+ Pattern length.
226
+ l : int
227
+ Total back-reference length (d < l for overlapping).
228
+ h_prefix : int
229
+ H(P[0..r-1]) where r = l mod d. Pass 0 if r = 0.
230
+ """
231
+ q, r = divmod(l, d)
232
+ x_d = self.power(d)
233
+ x_r = self.power(r)
234
+
235
+ # H(P) · Φ(q, x^d)
236
+ phi_val = phi(q, x_d, self._p)
237
+ h_repeated = mersenne_mul(h_p, phi_val, self._p)
238
+
239
+ # · x^r + H(P[0..r-1])
240
+ result = mersenne_mod(h_repeated * x_r + h_prefix, self._p)
241
+ return result
hashrope/rope.py ADDED
@@ -0,0 +1,601 @@
1
+ """
2
+ Hash rope data structure for compressed-domain hashing.
3
+
4
+ Implements Part III of the framework:
5
+ - Definition 6: Leaf, Internal, RepeatNode
6
+ - Definition 7: Invariants I1-I9 (BB[2/7] weight balance)
7
+ - Lemma 4: Hash correctness (structural induction)
8
+ - Lemma 5: Height bound O(log w)
9
+ - Lemma 6: Rotations preserve hash invariant
10
+ - Lemma 7: RepeatNode splittability
11
+ - Lemma 8: Join complexity O(k · |h_L - h_R|)
12
+ - Lemma 8': RepeatNode bisection for rebalancing
13
+ - Theorem 6: Join (full specification)
14
+ - Theorem 7: Split in O(k · log w)
15
+ - Theorem 8: Repeat via RepeatNode in O(k · log q), O(1) space
16
+ - Theorem 9: Allocation-free SubstrHash in O(k · log w)
17
+ - Theorem 10: Concat = Join
18
+
19
+ All nodes are immutable (Invariant I9). Operations return new trees
20
+ with structural sharing.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Optional
27
+
28
+ from hashrope.polynomial_hash import (
29
+ PolynomialHash,
30
+ phi,
31
+ mersenne_mod,
32
+ mersenne_mul,
33
+ MERSENNE_61,
34
+ )
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Balance parameter (Definition 7, Invariant I8)
38
+ # ---------------------------------------------------------------------------
39
+
40
+ # BB[α] with α = 2/7 (Nievergelt-Reingold condition).
41
+ # A node is balanced if each child's weight is at least α * parent weight.
42
+ ALPHA_NUM = 2
43
+ ALPHA_DEN = 7
44
+
45
+ # Type alias
46
+ Node = Optional["Leaf | Internal | RepeatNode"]
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Definition 6: Node types
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class Leaf:
56
+ """
57
+ Leaf node storing a byte sequence (Definition 6a).
58
+
59
+ Invariant I1: hash_val = H(data).
60
+ Weight is always 1.
61
+ """
62
+
63
+ data: bytes
64
+ hash_val: int
65
+ len: int
66
+ weight: int
67
+
68
+ def __init__(self, data: bytes, h: PolynomialHash) -> None:
69
+ if len(data) == 0:
70
+ raise ValueError("Leaf cannot be empty")
71
+ object.__setattr__(self, "data", bytes(data))
72
+ object.__setattr__(self, "len", len(data))
73
+ object.__setattr__(self, "hash_val", h.hash(data))
74
+ object.__setattr__(self, "weight", 1)
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class Internal:
79
+ """
80
+ Internal node joining two subtrees (Definition 6b).
81
+
82
+ Invariant I2: len = left.len + right.len
83
+ Invariant I3: hash_val = left.hash_val · x^(right.len) + right.hash_val
84
+ Invariant I4: weight = left.weight + right.weight
85
+ """
86
+
87
+ left: Leaf | Internal | RepeatNode
88
+ right: Leaf | Internal | RepeatNode
89
+ hash_val: int
90
+ len: int
91
+ weight: int
92
+
93
+ def __init__(
94
+ self,
95
+ left: Leaf | Internal | RepeatNode,
96
+ right: Leaf | Internal | RepeatNode,
97
+ h: PolynomialHash,
98
+ ) -> None:
99
+ object.__setattr__(self, "left", left)
100
+ object.__setattr__(self, "right", right)
101
+ object.__setattr__(self, "len", left.len + right.len)
102
+ object.__setattr__(self, "weight", left.weight + right.weight)
103
+ # I3: hash = left.hash · x^(right.len) + right.hash
104
+ hv = h.hash_concat(left.hash_val, right.len, right.hash_val)
105
+ object.__setattr__(self, "hash_val", hv)
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class RepeatNode:
110
+ """
111
+ Repeat node representing child^reps (Definition 6c).
112
+
113
+ Invariant I5: len = child.len * reps
114
+ Invariant I6: hash_val = child.hash_val · Φ(reps, x^(child.len))
115
+ Invariant I7: weight = child.weight * reps
116
+ """
117
+
118
+ child: Leaf | Internal | RepeatNode
119
+ reps: int
120
+ hash_val: int
121
+ len: int
122
+ weight: int
123
+
124
+ def __init__(
125
+ self,
126
+ child: Leaf | Internal | RepeatNode,
127
+ reps: int,
128
+ h: PolynomialHash,
129
+ ) -> None:
130
+ if reps < 2:
131
+ raise ValueError(f"RepeatNode reps must be >= 2, got {reps}")
132
+ object.__setattr__(self, "child", child)
133
+ object.__setattr__(self, "reps", reps)
134
+ object.__setattr__(self, "len", child.len * reps)
135
+ object.__setattr__(self, "weight", child.weight * reps)
136
+ # I6: hash = child.hash · Φ(reps, x^d)
137
+ x_d = h.power(child.len)
138
+ phi_val = phi(reps, x_d, h.prime)
139
+ hv = mersenne_mul(child.hash_val, phi_val, h.prime)
140
+ object.__setattr__(self, "hash_val", hv)
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Accessors
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ def rope_len(node: Node) -> int:
149
+ """Total byte length of the string represented by node."""
150
+ return 0 if node is None else node.len
151
+
152
+
153
+ def rope_hash(node: Node) -> int:
154
+ """Hash of the string represented by node."""
155
+ return 0 if node is None else node.hash_val
156
+
157
+
158
+ def _weight(node: Node) -> int:
159
+ return 0 if node is None else node.weight
160
+
161
+
162
+ def _height(node: Node) -> int:
163
+ """Approximate height for join decisions."""
164
+ if node is None:
165
+ return 0
166
+ if isinstance(node, Leaf):
167
+ return 1
168
+ if isinstance(node, RepeatNode):
169
+ return _height(node.child) + 1
170
+ # Internal
171
+ return 1 + max(_height(node.left), _height(node.right))
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Balance helpers (Invariant I8)
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ def _is_balanced(left: Node, right: Node) -> bool:
180
+ """Check BB[2/7] condition for a potential Internal(left, right)."""
181
+ total = _weight(left) + _weight(right)
182
+ if total <= 2:
183
+ return True
184
+ wl = _weight(left)
185
+ # α ≤ wl/total ≤ 1-α ⟺ α·total ≤ wl ≤ (1-α)·total
186
+ # With α = 2/7: 2·total ≤ 7·wl and 7·wl ≤ 5·total
187
+ return ALPHA_NUM * total <= ALPHA_DEN * wl <= (ALPHA_DEN - ALPHA_NUM) * total
188
+
189
+
190
+ def _rotate_right(node: Internal, h: PolynomialHash) -> Leaf | Internal | RepeatNode:
191
+ """Right rotation: Internal(Internal(A, B), C) → Internal(A, Internal(B, C))."""
192
+ u = node.left
193
+ if not isinstance(u, Internal):
194
+ return node # can't rotate
195
+ a, b, c = u.left, u.right, node.right
196
+ new_right = Internal(b, c, h)
197
+ return Internal(a, new_right, h)
198
+
199
+
200
+ def _rotate_left(node: Internal, h: PolynomialHash) -> Leaf | Internal | RepeatNode:
201
+ """Left rotation: Internal(A, Internal(B, C)) → Internal(Internal(A, B), C)."""
202
+ u = node.right
203
+ if not isinstance(u, Internal):
204
+ return node
205
+ a, b, c = node.left, u.left, u.right
206
+ new_left = Internal(a, b, h)
207
+ return Internal(new_left, c, h)
208
+
209
+
210
+ def _rebalance(left: Leaf | Internal | RepeatNode,
211
+ right: Leaf | Internal | RepeatNode,
212
+ h: PolynomialHash) -> Leaf | Internal | RepeatNode:
213
+ """
214
+ Create a balanced Internal node from left and right.
215
+ Applies single or double rotations as needed (Lemma 6).
216
+ """
217
+ node = Internal(left, right, h)
218
+ wl, wr = _weight(left), _weight(right)
219
+ total = wl + wr
220
+
221
+ if total <= 2 or _is_balanced(left, right):
222
+ return node
223
+
224
+ if wl * ALPHA_DEN < ALPHA_NUM * total:
225
+ # Left too light → rotate left
226
+ if isinstance(right, Internal):
227
+ # Check if double rotation needed
228
+ if _weight(right.left) * ALPHA_DEN > (ALPHA_DEN - ALPHA_NUM) * wr:
229
+ # Double: rotate right child right, then rotate left
230
+ new_right = _rotate_right(right, h)
231
+ node = Internal(left, new_right, h)
232
+ return _rotate_left(node, h)
233
+ return node
234
+
235
+ if wr * ALPHA_DEN < ALPHA_NUM * total:
236
+ # Right too light → rotate right
237
+ if isinstance(left, Internal):
238
+ if _weight(left.right) * ALPHA_DEN > (ALPHA_DEN - ALPHA_NUM) * wl:
239
+ new_left = _rotate_left(left, h)
240
+ node = Internal(new_left, right, h)
241
+ return _rotate_right(node, h)
242
+ return node
243
+
244
+ return node
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Theorem 6 / Lemma 8: Join (Concat)
249
+ # ---------------------------------------------------------------------------
250
+
251
+
252
+ def rope_concat(
253
+ left: Node, right: Node, h: PolynomialHash
254
+ ) -> Node:
255
+ """
256
+ Join two ropes (Theorem 6, Theorem 10).
257
+
258
+ Returns a valid hash rope representing left's string ‖ right's string.
259
+ Time: O(k · |h_L - h_R|).
260
+ """
261
+ if left is None:
262
+ return right
263
+ if right is None:
264
+ return left
265
+ return _join(left, right, h)
266
+
267
+
268
+ def _join(
269
+ left: Leaf | Internal | RepeatNode,
270
+ right: Leaf | Internal | RepeatNode,
271
+ h: PolynomialHash,
272
+ ) -> Leaf | Internal | RepeatNode:
273
+ """Core join with rebalancing."""
274
+ if _is_balanced(left, right):
275
+ return Internal(left, right, h)
276
+
277
+ wl, wr = _weight(left), _weight(right)
278
+
279
+ if wl > wr:
280
+ # Left is heavier — descend right spine of left
281
+ if isinstance(left, Internal):
282
+ new_right = _join(left.right, right, h)
283
+ return _rebalance(left.left, new_right, h)
284
+ elif isinstance(left, RepeatNode):
285
+ # Split the repeat node in half, join right half with right
286
+ mid = left.len // 2
287
+ ll, lr = rope_split(left, mid, h)
288
+ new_right = _join(lr, right, h)
289
+ return _join(ll, new_right, h)
290
+ else:
291
+ # Leaf — just make Internal
292
+ return Internal(left, right, h)
293
+ else:
294
+ # Right is heavier — descend left spine of right
295
+ if isinstance(right, Internal):
296
+ new_left = _join(left, right.left, h)
297
+ return _rebalance(new_left, right.right, h)
298
+ elif isinstance(right, RepeatNode):
299
+ mid = right.len // 2
300
+ rl, rr = rope_split(right, mid, h)
301
+ new_left = _join(left, rl, h)
302
+ return _join(new_left, rr, h)
303
+ else:
304
+ return Internal(left, right, h)
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Theorem 7: Split
309
+ # ---------------------------------------------------------------------------
310
+
311
+
312
+ def rope_split(
313
+ node: Node, pos: int, h: PolynomialHash
314
+ ) -> tuple[Node, Node]:
315
+ """
316
+ Split rope at byte position pos (Theorem 7).
317
+
318
+ Returns (left, right) where left has pos bytes, right has the rest.
319
+ Time: O(k · log w).
320
+ """
321
+ if node is None:
322
+ return None, None
323
+ if pos <= 0:
324
+ return None, node
325
+ if pos >= node.len:
326
+ return node, None
327
+ return _split(node, pos, h)
328
+
329
+
330
+ def _split(
331
+ node: Leaf | Internal | RepeatNode,
332
+ pos: int,
333
+ h: PolynomialHash,
334
+ ) -> tuple[Node, Node]:
335
+ """Core split. 0 < pos < node.len guaranteed."""
336
+
337
+ if isinstance(node, Leaf):
338
+ left = Leaf(node.data[:pos], h)
339
+ right = Leaf(node.data[pos:], h)
340
+ return left, right
341
+
342
+ if isinstance(node, Internal):
343
+ ll = node.left.len
344
+ if pos == ll:
345
+ return node.left, node.right
346
+ if pos < ll:
347
+ l1, l2 = _split(node.left, pos, h)
348
+ return l1, rope_concat(l2, node.right, h)
349
+ else:
350
+ r1, r2 = _split(node.right, pos - ll, h)
351
+ return rope_concat(node.left, r1, h), r2
352
+
353
+ if isinstance(node, RepeatNode):
354
+ return _split_repeat(node, pos, h)
355
+
356
+ raise TypeError(f"Unknown node type: {type(node)}")
357
+
358
+
359
+ def _split_repeat(
360
+ node: RepeatNode, pos: int, h: PolynomialHash
361
+ ) -> tuple[Node, Node]:
362
+ """
363
+ Split a RepeatNode at byte position pos (Lemma 7).
364
+
365
+ Let d = child.len, m = pos // d, r = pos % d.
366
+ """
367
+ d = node.child.len
368
+ m, r = divmod(pos, d)
369
+ q = node.reps
370
+
371
+ if r == 0:
372
+ # Split falls on repetition boundary
373
+ left = _make_repeat(node.child, m, h)
374
+ right = _make_repeat(node.child, q - m, h)
375
+ return left, right
376
+ else:
377
+ # Split falls within a repetition
378
+ child_left, child_right = _split(node.child, r, h)
379
+
380
+ # Left = Repeat(child, m) ‖ child_left
381
+ left_rep = _make_repeat(node.child, m, h)
382
+ left = rope_concat(left_rep, child_left, h)
383
+
384
+ # Right = child_right ‖ Repeat(child, q - m - 1)
385
+ right_rep = _make_repeat(node.child, q - m - 1, h)
386
+ right = rope_concat(child_right, right_rep, h)
387
+
388
+ return left, right
389
+
390
+
391
+ def _make_repeat(
392
+ child: Leaf | Internal | RepeatNode, reps: int, h: PolynomialHash
393
+ ) -> Node:
394
+ """Create a Repeat with proper handling of reps 0 and 1."""
395
+ if reps == 0:
396
+ return None
397
+ if reps == 1:
398
+ return child
399
+ return RepeatNode(child, reps, h)
400
+
401
+
402
+ # ---------------------------------------------------------------------------
403
+ # Theorem 8: Repeat
404
+ # ---------------------------------------------------------------------------
405
+
406
+
407
+ def rope_repeat(
408
+ node: Leaf | Internal | RepeatNode,
409
+ q: int,
410
+ h: PolynomialHash,
411
+ ) -> Node:
412
+ """
413
+ Create a RepeatNode representing node^q (Theorem 8).
414
+
415
+ Time: O(k · log q) for the Φ computation. O(1) new nodes.
416
+ """
417
+ if q == 0:
418
+ return None
419
+ if q == 1:
420
+ return node
421
+ return RepeatNode(node, q, h)
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Theorem 9: SubstrHash (allocation-free)
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ def rope_substr_hash(
430
+ node: Node,
431
+ start: int,
432
+ length: int,
433
+ h: PolynomialHash,
434
+ ) -> int:
435
+ """
436
+ Compute H(S[start..start+length-1]) without allocating nodes (Theorem 9).
437
+
438
+ Time: O(k · log w). Space: O(log w) stack.
439
+ """
440
+ if node is None or length == 0:
441
+ return 0
442
+ return _hash_range(node, start, length, h)
443
+
444
+
445
+ def _hash_range(
446
+ node: Leaf | Internal | RepeatNode,
447
+ start: int,
448
+ length: int,
449
+ h: PolynomialHash,
450
+ ) -> int:
451
+ """Recursive hash range query."""
452
+ p = h.prime
453
+
454
+ if isinstance(node, Leaf):
455
+ return h.hash(node.data[start:start + length])
456
+
457
+ if isinstance(node, Internal):
458
+ ll = node.left.len
459
+ if start + length <= ll:
460
+ # Entirely in left
461
+ return _hash_range(node.left, start, length, h)
462
+ if start >= ll:
463
+ # Entirely in right
464
+ return _hash_range(node.right, start - ll, length, h)
465
+ # Spanning
466
+ l_len = ll - start
467
+ r_len = length - l_len
468
+ h_l = _hash_range(node.left, start, l_len, h)
469
+ h_r = _hash_range(node.right, 0, r_len, h)
470
+ return h.hash_concat(h_l, r_len, h_r)
471
+
472
+ if isinstance(node, RepeatNode):
473
+ d = node.child.len
474
+ # Map into the repeating pattern
475
+ first_copy = start // d
476
+ start_in_copy = start % d
477
+
478
+ end = start + length - 1
479
+ last_copy = end // d
480
+ end_in_copy = end % d
481
+
482
+ if first_copy == last_copy:
483
+ # Entirely within one copy
484
+ return _hash_range(node.child, start_in_copy, length, h)
485
+
486
+ # Three parts: tail of first copy, full copies, head of last copy
487
+ # 1. Tail of first copy: child[start_in_copy .. d-1]
488
+ tail_len = d - start_in_copy
489
+ h_tail = _hash_range(node.child, start_in_copy, tail_len, h)
490
+
491
+ # 2. Full copies in the middle
492
+ full_copies = last_copy - first_copy - 1
493
+ if full_copies > 0:
494
+ x_d = h.power(d)
495
+ phi_val = phi(full_copies, x_d, p)
496
+ h_full = mersenne_mul(node.child.hash_val, phi_val, p)
497
+ else:
498
+ h_full = 0
499
+ full_len = full_copies * d
500
+
501
+ # 3. Head of last copy: child[0 .. end_in_copy]
502
+ head_len = end_in_copy + 1
503
+ h_head = _hash_range(node.child, 0, head_len, h)
504
+
505
+ # Combine: tail ‖ full ‖ head
506
+ # H(tail ‖ full ‖ head) = H(tail) · x^(full_len + head_len) + H(full) · x^(head_len) + H(head)
507
+ result = mersenne_mod(
508
+ h.power(full_len + head_len) * h_tail
509
+ + h.power(head_len) * h_full
510
+ + h_head,
511
+ p,
512
+ )
513
+ return result
514
+
515
+ raise TypeError(f"Unknown node type: {type(node)}")
516
+
517
+
518
+ # ---------------------------------------------------------------------------
519
+ # Reconstruction: rope → bytes
520
+ # ---------------------------------------------------------------------------
521
+
522
+
523
+ def rope_to_bytes(node: Node) -> bytes:
524
+ """Reconstruct the byte string from a rope (for testing)."""
525
+ if node is None:
526
+ return b""
527
+ parts: list[bytes] = []
528
+ _collect_bytes(node, parts)
529
+ return b"".join(parts)
530
+
531
+
532
+ def _collect_bytes(node: Leaf | Internal | RepeatNode, parts: list[bytes]) -> None:
533
+ if isinstance(node, Leaf):
534
+ parts.append(node.data)
535
+ elif isinstance(node, Internal):
536
+ _collect_bytes(node.left, parts)
537
+ _collect_bytes(node.right, parts)
538
+ elif isinstance(node, RepeatNode):
539
+ for _ in range(node.reps):
540
+ _collect_bytes(node.child, parts)
541
+
542
+
543
+ # ---------------------------------------------------------------------------
544
+ # Construction helper
545
+ # ---------------------------------------------------------------------------
546
+
547
+
548
+ def rope_from_bytes(data: bytes, h: PolynomialHash) -> Node:
549
+ """Create a rope from a byte string. Returns None for empty."""
550
+ if len(data) == 0:
551
+ return None
552
+ return Leaf(data, h)
553
+
554
+
555
+ # ---------------------------------------------------------------------------
556
+ # Validation (for testing)
557
+ # ---------------------------------------------------------------------------
558
+
559
+
560
+ def validate_rope(node: Node, h: PolynomialHash | None = None) -> None:
561
+ """
562
+ Validate all rope invariants (Definition 7). Raises AssertionError on violation.
563
+ """
564
+ if node is None:
565
+ return
566
+
567
+ if isinstance(node, Leaf):
568
+ assert node.weight == 1, f"Leaf weight must be 1, got {node.weight}"
569
+ assert node.len == len(node.data), "Leaf len mismatch"
570
+ return
571
+
572
+ if isinstance(node, Internal):
573
+ # I2
574
+ assert node.len == node.left.len + node.right.len, "Internal len mismatch"
575
+ # I4
576
+ assert node.weight == node.left.weight + node.right.weight, "Internal weight mismatch"
577
+ # I8: balance
578
+ total = node.weight
579
+ wl = node.left.weight
580
+ if total > 2:
581
+ assert ALPHA_NUM * total <= ALPHA_DEN * wl, (
582
+ f"Left child too light: {wl}/{total}"
583
+ )
584
+ assert ALPHA_DEN * wl <= (ALPHA_DEN - ALPHA_NUM) * total, (
585
+ f"Left child too heavy: {wl}/{total}"
586
+ )
587
+ # Recurse
588
+ validate_rope(node.left, h)
589
+ validate_rope(node.right, h)
590
+ return
591
+
592
+ if isinstance(node, RepeatNode):
593
+ # I5
594
+ assert node.len == node.child.len * node.reps, "RepeatNode len mismatch"
595
+ # I7
596
+ assert node.weight == node.child.weight * node.reps, "RepeatNode weight mismatch"
597
+ assert node.reps >= 2, f"RepeatNode reps must be >= 2, got {node.reps}"
598
+ validate_rope(node.child, h)
599
+ return
600
+
601
+ raise TypeError(f"Unknown node type: {type(node)}")
hashrope/sliding.py ADDED
@@ -0,0 +1,210 @@
1
+ """
2
+ Sliding window hash rope for bounded-memory streaming hash computation.
3
+
4
+ Adapted from UHC's SlidingRopeState (Definition 10, Theorem 11).
5
+ Provides a generic sliding window over a hash rope without any
6
+ compression-format dependencies.
7
+
8
+ The window maintains a prefix hash and a bounded rope, evicting old
9
+ data as new data arrives. At any point, the full-sequence hash
10
+ H(T[0..pos-1]) can be computed in O(1) from the prefix hash and
11
+ the window rope hash.
12
+
13
+ Invariant I_slide (holds after every operation):
14
+ (1) h_prefix = H(T[0 .. l_prefix - 1])
15
+ (2) r_window represents T[l_prefix .. pos - 1]
16
+ (3) rope_len(r_window) = pos - l_prefix <= W
17
+ (4) H(T[0..pos-1]) = h_prefix * x^(pos - l_prefix) + rope_hash(r_window)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from hashrope.polynomial_hash import PolynomialHash, MERSENNE_61
23
+ from hashrope.rope import (
24
+ Leaf,
25
+ Node,
26
+ rope_concat,
27
+ rope_split,
28
+ rope_repeat,
29
+ rope_len,
30
+ rope_hash,
31
+ )
32
+
33
+
34
+ class SlidingWindow:
35
+ """
36
+ Bounded-memory sliding window over a hash rope.
37
+
38
+ Maintains a window of at most W = d_max + m_max bytes as a rope,
39
+ plus a prefix hash covering all evicted bytes. Supports two
40
+ operations:
41
+
42
+ - append_bytes(data): append literal bytes
43
+ - append_copy(offset, length): append a copy from within the window
44
+
45
+ At any point, current_hash() returns H(T[0..pos-1]) in O(1).
46
+
47
+ Parameters
48
+ ----------
49
+ d_max : int
50
+ Maximum copy-back distance. The window retains at least d_max
51
+ bytes after eviction so that future copies can find their source.
52
+ m_max : int
53
+ Maximum copy length. Together with d_max, determines the
54
+ maximum window extent W = d_max + m_max.
55
+ prime : int
56
+ Mersenne prime for hash arithmetic. Default: 2^61 - 1.
57
+ base : int
58
+ Hash base. Default: 131.
59
+ """
60
+
61
+ __slots__ = (
62
+ "h", "d_max", "m_max", "W",
63
+ "h_prefix", "l_prefix", "r_window", "pos",
64
+ )
65
+
66
+ def __init__(
67
+ self,
68
+ d_max: int = 32768,
69
+ m_max: int = 258,
70
+ prime: int = MERSENNE_61,
71
+ base: int = 131,
72
+ ) -> None:
73
+ if d_max < 1:
74
+ raise ValueError(f"d_max must be >= 1, got {d_max}")
75
+ if m_max < 1:
76
+ raise ValueError(f"m_max must be >= 1, got {m_max}")
77
+
78
+ self.h = PolynomialHash(prime=prime, base=base)
79
+ self.d_max = d_max
80
+ self.m_max = m_max
81
+ self.W = d_max + m_max
82
+
83
+ # State (Definition 10)
84
+ self.h_prefix: int = 0 # H(T[0..l_prefix-1])
85
+ self.l_prefix: int = 0 # evicted byte count
86
+ self.r_window: Node = None # rope for T[l_prefix..pos-1]
87
+ self.pos: int = 0 # total bytes ingested
88
+
89
+ @property
90
+ def window_len(self) -> int:
91
+ """Current window size in bytes."""
92
+ return rope_len(self.r_window)
93
+
94
+ def current_hash(self) -> int:
95
+ """
96
+ Compute H(T[0..pos-1]) from sliding state via Theorem 1:
97
+ H(T) = H_prefix * x^(window_len) + H(window)
98
+ """
99
+ wl = self.window_len
100
+ wh = rope_hash(self.r_window) if self.r_window else 0
101
+ return self.h.hash_concat(self.h_prefix, wl, wh)
102
+
103
+ def final_hash(self) -> int:
104
+ """
105
+ FinalHash() -- Theorem 11.
106
+
107
+ H(T[0..N-1]) = H_prefix * x^(R_window.len) + R_window.hash
108
+ """
109
+ return self.current_hash()
110
+
111
+ def append_bytes(self, data: bytes) -> None:
112
+ """
113
+ Append literal bytes to the stream.
114
+
115
+ Parameters
116
+ ----------
117
+ data : bytes
118
+ One or more bytes to append.
119
+ """
120
+ if not data:
121
+ return
122
+ leaf = Leaf(data, self.h)
123
+ self.r_window = rope_concat(self.r_window, leaf, self.h)
124
+ self.pos += len(data)
125
+ self._evict()
126
+
127
+ def append_copy(self, offset: int, length: int) -> None:
128
+ """
129
+ Append a copy-from-window reference.
130
+
131
+ Copies `length` bytes starting `offset` bytes back from the
132
+ current end of the window. Handles both non-overlapping
133
+ (offset >= length) and overlapping (offset < length) cases.
134
+
135
+ Parameters
136
+ ----------
137
+ offset : int
138
+ Distance back from the current end of the window.
139
+ Must be <= pos and <= window_len.
140
+ length : int
141
+ Number of bytes to copy. For overlapping copies
142
+ (offset < length), the pattern of length `offset`
143
+ is repeated as needed.
144
+
145
+ Raises
146
+ ------
147
+ ValueError
148
+ If offset exceeds decoded length or window size.
149
+ """
150
+ win_len = self.window_len
151
+
152
+ if offset > self.pos:
153
+ raise ValueError(
154
+ f"Invalid copy: offset {offset} exceeds "
155
+ f"decoded length {self.pos}"
156
+ )
157
+
158
+ if offset > win_len:
159
+ raise ValueError(
160
+ f"Copy offset {offset} exceeds window size {win_len}. "
161
+ f"Increase d_max (currently {self.d_max}) to at least {offset}."
162
+ )
163
+
164
+ start = win_len - offset
165
+
166
+ if offset >= length:
167
+ # Non-overlapping
168
+ _, tmp = rope_split(self.r_window, start, self.h)
169
+ source, _ = rope_split(tmp, length, self.h)
170
+ self.r_window = rope_concat(self.r_window, source, self.h)
171
+ else:
172
+ # Overlapping: extract pattern of length `offset`, repeat
173
+ _, tmp = rope_split(self.r_window, start, self.h)
174
+ pattern, _ = rope_split(tmp, offset, self.h)
175
+
176
+ q, r = divmod(length, offset)
177
+ rep: Node = rope_repeat(pattern, q, self.h) if q >= 1 else None
178
+ if r > 0:
179
+ partial, _ = rope_split(pattern, r, self.h)
180
+ rep = rope_concat(rep, partial, self.h)
181
+ self.r_window = rope_concat(self.r_window, rep, self.h)
182
+
183
+ self.pos += length
184
+ self._evict()
185
+
186
+ def _evict(self) -> None:
187
+ """
188
+ Evict excess bytes from the window.
189
+
190
+ After eviction: rope_len(r_window) <= d_max, ensuring that
191
+ future copies of distance <= d_max find their source.
192
+
193
+ Correctness (Lemma 12): The prefix hash update follows from
194
+ Theorem 1 (concatenation composability).
195
+ """
196
+ win_len = rope_len(self.r_window)
197
+ if win_len <= self.W:
198
+ return
199
+
200
+ # Evict down to d_max bytes remaining
201
+ excess = win_len - self.d_max
202
+ r_old, r_keep = rope_split(self.r_window, excess, self.h)
203
+
204
+ # Fold evicted bytes into h_prefix via Theorem 1
205
+ old_len = rope_len(r_old)
206
+ old_hash = rope_hash(r_old)
207
+ self.h_prefix = self.h.hash_concat(self.h_prefix, old_len, old_hash)
208
+
209
+ self.l_prefix += excess
210
+ self.r_window = r_keep
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: hashrope
3
+ Version: 0.1.0
4
+ Summary: Hash rope: a BB[2/7] balanced binary tree with polynomial hash metadata for O(log n) concat, split, substring hashing, and O(log q) repetition encoding.
5
+ Project-URL: Homepage, https://github.com/jemsbhai/hashrope
6
+ Project-URL: Repository, https://github.com/jemsbhai/hashrope
7
+ Project-URL: Issues, https://github.com/jemsbhai/hashrope/issues
8
+ Author-email: Muntaser Syed <muntaser@ieee.org>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: balanced-tree,content-addressing,data-structure,hash,mersenne-prime,polynomial-hash,rope
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # hashrope
31
+
32
+ A BB[2/7] weight-balanced binary tree augmented with polynomial hash metadata at every node.
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/hashrope)](https://pypi.org/project/hashrope/)
35
+ [![Python 3.10+](https://img.shields.io/pypi/pyversions/hashrope)](https://pypi.org/project/hashrope/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
37
+ [![Tests](https://img.shields.io/badge/tests-88%20passing-brightgreen)](https://github.com/jemsbhai/hashrope)
38
+
39
+ **Zero dependencies. Pure Python. Python 3.10+.**
40
+
41
+ ## What it does
42
+
43
+ hashrope is a rope data structure where every node carries a polynomial hash. This enables:
44
+
45
+ - **O(k log w) concat and split** with strict BB[2/7] rebalancing via recursive join
46
+ - **O(k log q) repetition encoding** via RepeatNode -- represents "repeat this subtree q times" without materializing the data
47
+ - **O(k log w) substring hashing** without allocating new nodes
48
+ - **O(1) whole-sequence hash** at any point during streaming ingestion
49
+ - **Bounded-memory sliding window** that evicts old data while maintaining a running hash of everything seen
50
+
51
+ All arithmetic uses Mersenne prime modular reduction (bit-shift, no division).
52
+
53
+ ## Install
54
+
55
+ ```
56
+ pip install hashrope
57
+ ```
58
+
59
+ ## Quick start
60
+
61
+ ```python
62
+ from hashrope import PolynomialHash, Leaf, rope_concat, rope_split, rope_substr_hash, rope_hash
63
+
64
+ h = PolynomialHash()
65
+
66
+ # Build a rope
67
+ a = Leaf(b"hello ", h)
68
+ b = Leaf(b"world", h)
69
+ ab = rope_concat(a, b, h)
70
+
71
+ # Whole-string hash matches direct computation
72
+ assert rope_hash(ab) == h.hash(b"hello world")
73
+
74
+ # Split at any byte position
75
+ left, right = rope_split(ab, 6, h)
76
+
77
+ # Substring hash without allocation
78
+ sub_h = rope_substr_hash(ab, 0, 5, h)
79
+ assert sub_h == h.hash(b"hello")
80
+ ```
81
+
82
+ ### RepeatNode
83
+
84
+ Encode repetitions in O(log q) time and O(1) space:
85
+
86
+ ```python
87
+ from hashrope import PolynomialHash, Leaf, rope_repeat, rope_hash
88
+
89
+ h = PolynomialHash()
90
+ pattern = Leaf(b"ab", h)
91
+ repeated = rope_repeat(pattern, 1000, h) # "ab" x 1000
92
+
93
+ # Hash computed in O(log 1000), not O(2000)
94
+ assert rope_hash(repeated) == h.hash(b"ab" * 1000)
95
+ ```
96
+
97
+ ### SlidingWindow
98
+
99
+ Bounded-memory streaming hash with copy-from-window support:
100
+
101
+ ```python
102
+ from hashrope import PolynomialHash, SlidingWindow
103
+
104
+ h = PolynomialHash()
105
+ sw = SlidingWindow(d_max=1024, m_max=256)
106
+
107
+ # Stream data in
108
+ sw.append_bytes(b"hello ")
109
+ sw.append_bytes(b"world")
110
+
111
+ # Hash of everything seen so far
112
+ assert sw.current_hash() == h.hash(b"hello world")
113
+
114
+ # Copy-from-window (like LZ77 back-references)
115
+ sw.append_copy(offset=5, length=5) # copies "world" again
116
+ assert sw.final_hash() == h.hash(b"hello worldworld")
117
+ ```
118
+
119
+ ## API reference
120
+
121
+ ### Polynomial hash
122
+
123
+ | Function / Class | Description |
124
+ |---|---|
125
+ | `PolynomialHash(prime, base)` | Hash function over Z/pZ. Default: p = 2^61 - 1, base = 131 |
126
+ | `PolynomialHash.hash(data)` | H(data) with +1 byte offset to prevent zero collisions |
127
+ | `PolynomialHash.hash_concat(h_a, len_b, h_b)` | H(A \|\| B) from H(A), \|B\|, H(B) in O(1) |
128
+ | `PolynomialHash.hash_repeat(h_s, d, q)` | H(S^q) from H(S) and \|S\| in O(log q) |
129
+ | `PolynomialHash.hash_overlap(h_p, d, l, h_prefix)` | H(P^q \|\| P[0..r-1]) for overlapping references |
130
+ | `phi(q, alpha, p)` | Geometric accumulator: sum of alpha^i for i in [0, q) in O(log q) |
131
+ | `mersenne_mod(a, p)` | Bit-shift reduction mod Mersenne prime |
132
+ | `mersenne_mul(a, b, p)` | Modular multiply via mersenne_mod |
133
+ | `MERSENNE_61` | 2^61 - 1 (default prime) |
134
+ | `MERSENNE_127` | 2^127 - 1 (high-security prime) |
135
+
136
+ ### Rope nodes
137
+
138
+ | Class | Description |
139
+ |---|---|
140
+ | `Leaf(data, h)` | Immutable leaf storing a byte sequence. Weight = 1 |
141
+ | `Internal(left, right, h)` | Internal node. Hash = H(left) \* x^len(right) + H(right) |
142
+ | `RepeatNode(child, reps, h)` | Represents child^reps. Hash via geometric accumulator |
143
+
144
+ ### Rope operations
145
+
146
+ | Function | Time | Description |
147
+ |---|---|---|
148
+ | `rope_concat(left, right, h)` | O(k \|h_L - h_R\|) | Concatenate with recursive join and BB[2/7] rebalancing |
149
+ | `rope_split(node, pos, h)` | O(k log w) | Split at byte position |
150
+ | `rope_repeat(node, q, h)` | O(k log q) | Create RepeatNode |
151
+ | `rope_substr_hash(node, start, length, h)` | O(k log w) | Substring hash without allocation |
152
+ | `rope_len(node)` | O(1) | Total byte length |
153
+ | `rope_hash(node)` | O(1) | Hash of represented string |
154
+ | `rope_from_bytes(data, h)` | O(n) | Create Leaf from bytes |
155
+ | `rope_to_bytes(node)` | O(n) | Reconstruct bytes (for testing) |
156
+ | `validate_rope(node)` | O(w) | Assert all 9 invariants hold |
157
+
158
+ ### SlidingWindow
159
+
160
+ | Method | Description |
161
+ |---|---|
162
+ | `SlidingWindow(d_max, m_max, prime, base)` | Create window. W = d_max + m_max |
163
+ | `.append_bytes(data)` | Append literal bytes |
164
+ | `.append_copy(offset, length)` | Copy from window (handles overlapping) |
165
+ | `.current_hash()` | H(everything seen) in O(1) |
166
+ | `.final_hash()` | Same as current_hash (alias) |
167
+ | `.window_len` | Current window size in bytes |
168
+ | `.pos` | Total bytes ingested |
169
+
170
+ ## Mathematical foundation
171
+
172
+ The data structure and its correctness proofs come from the compressed-domain hashing framework (24 theorems, 14 lemmas, 5 corollaries). Key results used by hashrope:
173
+
174
+ | Result | Statement |
175
+ |---|---|
176
+ | Theorem 1 (Concatenation) | H(A \|\| B) = H(A) * x^{\|B\|} + H(B) |
177
+ | Theorem 2 (Repetition) | H(S^q) = H(S) * Phi(q, x^d) |
178
+ | Theorem 3 (Phi computation) | Phi(q, alpha) computable in O(log q) without modular inverse |
179
+ | Theorem 6 (Join) | Recursive join with BB[2/7] rebalancing |
180
+ | Theorem 7 (Split) | Split in O(k log w) with RepeatNode splittability |
181
+ | Theorem 8 (Repeat) | RepeatNode in O(k log q), O(1) space |
182
+ | Theorem 9 (SubstrHash) | Allocation-free substring hashing in O(k log w) |
183
+ | Theorem 11 (Sliding window) | Bounded-memory streaming with eviction |
184
+
185
+ ## Provenance
186
+
187
+ Extracted from [UHC](https://github.com/jemsbhai/uhc) (Unified Hash-Compression Engine). The rope and polynomial hash modules are verbatim copies with import paths updated. The sliding window is adapted from UHC's SlidingRopeState with the LZ77 dependency removed.
188
+
189
+ ## License
190
+
191
+ MIT
@@ -0,0 +1,8 @@
1
+ hashrope/__init__.py,sha256=Sx2C3jXNV5_mFIRc9GnuazzX4ZgBrpK6lwYLCH9E_AY,1383
2
+ hashrope/polynomial_hash.py,sha256=W45PrlkK4OJi_a7QHdTOMFZqYvTNJrLqxjXI3RIdjDc,7520
3
+ hashrope/rope.py,sha256=USsOegxBOqABiutYL6xSt6pDyrSCU5goWf6g8P-_Enk,19362
4
+ hashrope/sliding.py,sha256=VoKpmxvyl7UqzCMKgQaMGi8Plt0l6EubMqHu4TKO_58,6926
5
+ hashrope-0.1.0.dist-info/METADATA,sha256=gLMTmqb1LJcYplyOptSSQLfp1qXy2TpqaPCJd73EdFU,7400
6
+ hashrope-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ hashrope-0.1.0.dist-info/licenses/LICENSE,sha256=G8xR12Tb3ogZTszzaEMdOQYzc5ZpSvIHbiTm0bPIYpA,1091
8
+ hashrope-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muntaser Syed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.