Skip to main content

hashrope

A BB[2/7] weight-balanced binary tree augmented with polynomial hash metadata at every node.

PyPI version Python 3.10+ License: MIT

Zero dependencies. Pure Python. Python 3.10+.

What it does

hashrope is a rope data structure where every node carries a polynomial hash. This enables:

  • O(k log w) concat and split with strict BB[2/7] rebalancing via Adams-style balanced join
  • O(k log q) repetition encoding via RepeatNode -- represents "repeat this subtree q times" without materializing the data
  • O(k log w) substring hashing without allocating new nodes
  • O(1) whole-sequence hash at any point during streaming ingestion
  • Bounded-memory sliding window that evicts old data while maintaining a running hash of everything seen

All arithmetic uses Mersenne prime modular reduction (bit-shift, no division).

Fingerprints are noncryptographic candidate-match keys. Matching fingerprints do not prove equal content. Use rope_equal or rope_lcp for exact decisions; these compare bytes while taking advantage of shared structure and repetition.

Install

Version 0.3.4 synchronizes the published packages. Python's comparison and fingerprint implementations are unchanged from 0.3.3, which introduced exact comparison and hash-profile checks. Existing fingerprints and defaults remain compatible. Version parity does not promise identical APIs or speed across languages; see the release scope and performance caveats.

pip install "hashrope>=0.3.4"

Quick start

from hashrope import PolynomialHash, Leaf, rope_concat, rope_split, rope_substr_hash, rope_hash

h = PolynomialHash()

# Build a rope
a = Leaf(b"hello ", h)
b = Leaf(b"world", h)
ab = rope_concat(a, b, h)

# Whole-string hash matches direct computation
assert rope_hash(ab) == h.hash(b"hello world")

# Split at any byte position
left, right = rope_split(ab, 6, h)

# Substring hash without allocating rope nodes
sub_h = rope_substr_hash(ab, 0, 5, h)
assert sub_h == h.hash(b"hello")

Exact comparison and hash profiles

from hashrope import Leaf, PolynomialHash, rope_equal, rope_hash, rope_lcp

h = PolynomialHash()  # Existing defaults remain p=2**61-1, base=131.
a = Leaf(bytes([0, 131, 42]), h)
b = Leaf(bytes([1, 0, 42]), h)
assert rope_hash(a) == rope_hash(b)  # A deliberate legacy collision.
assert not rope_equal(a, b)
assert rope_lcp(a, b) == 0

# Exact comparisons also work across profiles and tree shapes.
c = Leaf(a.data, PolynomialHash(base=257))
assert rope_equal(a, c)
assert rope_lcp(a, c) == len(a.data)

rope_lcp returns a byte count; for fixed-width four-byte token encodings, rope_lcp(a, b) // 4 gives the exact token LCP. Neither exact API depends on fingerprints. Empty ropes (None) are supported. Node == retains its existing structural meaning; use rope_equal for content equality across tree shapes.

For slices, rope_range_equal(left, left_start, right, right_start, length) and rope_range_lcp(left, left_start, right, right_start, length) compare byte ranges directly, without splitting or recomputing hashes. The latter returns a count relative to the two starts. Both reject negative or out-of-bounds ranges with ValueError; an empty range may start at the rope's end. They also work across hash profiles.

The comparison walks virtual ranges without splitting or flattening ropes. Identical immutable subtrees are skipped, and a per-call cache reuses previously verified non-leaf ranges in shared DAGs. Two periodic spans can be certified by comparing at most p + q - gcd(p, q) bytes, where p and q are their periods (Fine–Wilf periodicity theorem). This also handles different period lengths and shifted phases. Unshared inputs can still require reading the entire common prefix, plus traversal, cache and integer-arithmetic costs: exact LCP has no universal logarithmic bound. Temporary storage includes the traversal stack, verified-range cache, and bounded leaf-comparison slices (at most 192 KiB of temporary byte payload). Unequal chunks use native exact byte comparisons to locate late differences; a short scalar prefix check keeps early differences cheap. The sum of byte work over those shrinking probes is linear in the chunk size, and the overall exact-comparison bound is unchanged.

Each hasher exposes an immutable HashProfile(prime, base) through .profile; nodes retain that profile. Separate hashers with equal parameters are compatible. Internal, RepeatNode, rope_concat, rope_split, rope_repeat, and rope_substr_hash raise ValueError on incompatible profiles, including calls that otherwise return an unchanged node or cached hash. Replacing a sliding window's hasher with incompatible parameters is rejected before hashing or mutating the window. Create a new hasher and rebuild content to change profiles. Hasher subclasses must preserve the fingerprint algorithm when overriding methods for instrumentation.

All existing fingerprint values, defaults, and valid same-profile operation results are unchanged. No stable node-pickle format is promised: rebuild nodes serialized by older versions, which lack profile provenance. validate_rope checks structure and profile consistency; it does not recompute fingerprints. Initial safety measurements are in benchmarks/comparison_safety. The exact-path optimization rerun profiles leaf mismatch localization and compares archived cores on the same consumer workloads. The optimization improves late mismatches; it does not speed up every offset or remove the traversal cost of independent equal trees. The report includes the narrow regression at the 64-byte probe boundary.

RepeatNode

Encode repetitions in O(log q) time and O(1) space:

from hashrope import PolynomialHash, Leaf, rope_repeat, rope_hash

h = PolynomialHash()
pattern = Leaf(b"ab", h)
repeated = rope_repeat(pattern, 1000, h)  # "ab" x 1000

# Hash computed in O(log 1000), not O(2000)
assert rope_hash(repeated) == h.hash(b"ab" * 1000)

SlidingWindow

Bounded-memory streaming hash with copy-from-window support:

from hashrope import PolynomialHash, SlidingWindow

h = PolynomialHash()
sw = SlidingWindow(d_max=1024, m_max=256)

# Stream data in
sw.append_bytes(b"hello ")
sw.append_bytes(b"world")

# Hash of everything seen so far
assert sw.current_hash() == h.hash(b"hello world")

# Copy-from-window (like LZ77 back-references)
sw.append_copy(offset=5, length=5)  # copies "world" again
assert sw.final_hash() == h.hash(b"hello worldworld")

API reference

Byte-index contract

Every index and length in this API (rope_split pos, rope_substr_hash start/length, SlidingWindow offsets) is a byte offset into the stored byte string. The library stores, splits, and hashes bytes; it has no notion of characters or encodings. When the bytes are UTF-8-encoded text, an arbitrary byte offset may fall inside a multibyte character's encoding. That split is still exact at the byte level: concatenating the two halves (or rope_to_bytes of each) reproduces the original bytes, and every hash is computed over bytes, so nothing is lost or corrupted. Only the two halves, viewed on their own, may not each be valid UTF-8. Callers who need splits on character boundaries must supply byte offsets that land on encoded character boundaries (for example len(text[:k].encode("utf-8")) for a split after the first k characters).

Polynomial hash

Function / Class Description
PolynomialHash(prime, base) Hash function over Z/pZ. Default: p = 2^61 - 1, base = 131
HashProfile(prime, base) Immutable fingerprint parameters; available as hasher.profile and node.profile
PolynomialHash.hash(data) H(data) with +1 byte offset to prevent zero collisions
PolynomialHash.hash_concat(h_a, len_b, h_b) H(A || B) from H(A), |B|, H(B) in O(1)
PolynomialHash.hash_repeat(h_s, d, q) H(S^q) from H(S) and |S| in O(log q)
PolynomialHash.hash_overlap(h_p, d, l, h_prefix) H(P^q || P[0..r-1]) for overlapping references
phi(q, alpha, p) Geometric accumulator: sum of alpha^i for i in [0, q) in O(log q)
mersenne_mod(a, p) Bit-shift reduction mod Mersenne prime
mersenne_mul(a, b, p) Modular multiply via mersenne_mod
MERSENNE_61 2^61 - 1 (default prime)
MERSENNE_127 2^127 - 1 (wider noncryptographic fingerprint)

Rope nodes

Class Description
Leaf(data, h) Immutable leaf storing a byte sequence. Weight = 1
Internal(left, right, h) Internal node. Hash = H(left) * x^len(right) + H(right)
RepeatNode(child, reps, h) Represents child^reps. Hash via geometric accumulator

Rope operations

Function Time Description
rope_concat(left, right, h) O(k |h_L - h_R|) Concatenate with Adams-style balanced join and BB[2/7] rebalancing
rope_split(node, pos, h) O(k log w) Split at byte position
rope_repeat(node, q, h) O(k log q) Create RepeatNode
rope_substr_hash(node, start, length, h) O(k log w) Substring hash without allocating rope nodes
rope_len(node) O(1) Total byte length
rope_hash(node) O(1) Hash of represented string
rope_equal(left, right) Content-dependent; may scan unshared content Exact byte equality, including across profiles
rope_lcp(left, right) Content-dependent; may scan unshared content Exact common-prefix length in bytes
rope_range_equal(left, start_a, right, start_b, length) Content-dependent Checked exact range equality without splitting
rope_range_lcp(left, start_a, right, start_b, length) Content-dependent Checked exact range LCP, relative to the starts
rope_from_bytes(data, h) O(n) Create Leaf from bytes
rope_to_bytes(node) O(n) Reconstruct bytes (for testing)
validate_rope(node, h=None) O(w) Check structure and profile consistency, without rehashing

SlidingWindow

Method Description
SlidingWindow(d_max, m_max, prime, base) Create window. W = d_max + m_max
.append_bytes(data) Append literal bytes
.append_copy(offset, length) Copy from window (handles overlapping)
.current_hash() H(everything seen) in O(1)
.final_hash() Same as current_hash (alias)
.window_len Current window size in bytes
.pos Total bytes ingested

Mathematical foundation

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:

Result Statement
Theorem 1 (Concatenation) H(A || B) = H(A) * x^{|B|} + H(B)
Theorem 2 (Repetition) H(S^q) = H(S) * Phi(q, x^d)
Theorem 3 (Phi computation) Phi(q, alpha) computable in O(log q) without modular inverse
Theorem 6 (Join) Adams-style balanced join with BB[2/7] rebalancing
Theorem 7 (Split) Split in O(k log w) with RepeatNode splittability
Theorem 8 (Repeat) RepeatNode in O(k log q), O(1) space
Theorem 9 (SubstrHash) Allocation-free substring hashing in O(k log w)
Theorem 11 (Sliding window) Bounded-memory streaming with eviction

Changelog

See CHANGELOG.md for version history and details on the v0.2.1 balance bugfix.

Provenance

Extracted from 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.

License

MIT

Metadata

Release files for hashrope 0.3.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hashrope 0.3.4
File Size Uploaded Provenance
hashrope-0.3.4.tar.gz 39.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hashrope 0.3.4
File Interpreter ABI Platform
hashrope-0.3.4-py3-none-any.whl Python 3 none any Details

Total release size: 60.8 kB

Release files / hashrope-0.3.4.tar.gz

Download URL hashrope-0.3.4.tar.gz
Size 39.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b44f853abc99c41bf1ca143c3030d2132b08e31ae6dce3121730922ce472b417
BLAKE2b-256 checksum
How to use checksums
74b8647cbb3e36157ce4af392c6ef299a03681efdbd20319415e91db52773497
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.2

Release files / hashrope-0.3.4-py3-none-any.whl

Download URL hashrope-0.3.4-py3-none-any.whl
Size 21.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bcce68fef301f309587a0380c9907dc6a2a66c8ae5411c1600e1c63fb321d9eb
BLAKE2b-256 checksum
How to use checksums
2faee466f610bdd74fba45a211b0fdc7dbc6f475faadb4e486c00724ae3fdf05
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.2

Security for hashrope 0.3.4

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page