8000 GitHub - al8n/iradix: A generic, persistent (copy-on-write) radix trie with structural sharing — bring-your-own-key, no_std + alloc, parameterized over Rc/Arc. · GitHub
[go: up one dir, main page]

Skip to content
al8nPublic

About

A generic, persistent (copy-on-write) radix trie with structural sharing — bring-your-own-key, no_std + alloc, parameterized over Rc/Arc.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

iradix

A generic, persistent (copy-on-write) radix trie with structural sharing — bring-your-own-key, no_std + alloc, parameterized over Rc/Arc.

github LoC Build codecov

docs.rs crates.io crates.io license

Discord

Introduction

iradix is a persistent (immutable, copy-on-write) radix trie keyed by an arbitrary sequence of components. Each mutation returns a logically new trie; unchanged subtrees are physically shared between versions, so a clone and a snapshot are O(1) and a write copies only the path it touches.

It is the data-structure half of a filesystem-watch stack, but it has no dependency on any of it: it is a plain, reusable container.

Highlights

  • Bring-your-own-key. RadixKey walks a key into components. A walk yields a borrowed Component<'a> and the trie stores an owned Owned (the trie's component type C); to_owned produces the owned form on insert, and compare orders a stored owned component against a walked borrowed one on descent. Each type keys on its natural element: sequences [C] / Vec<C> / Box<[C]> / [C; N] walk &C / store C; str / String → char; byte-addressed OsStr / OsString / CStr / CString and the integer types (u8…u128, i8…i128, big-endian and order-preserving within one integer type — a numeric trie must be homogeneous) → u8; and Path / PathBuf walk borrowed std::path::Components and store OsString path components (so /a/b is an ancestor of /a/b/c but not /a/bc). A walk — what components() does on every lookup, ancestor/descendant query, and removal — allocates nothing for every built-in key, Path / PathBuf included (a slice key borrows &C, a Path yields borrowed components); only insert allocates, turning each stored component into its owned form (for a Path, one OsString per stored component — never on a read). An operation's other costs are separate: insert allocates trie nodes, and the ordered/iterator reads (range, values, walk_*, …) allocate traversal state and Vec keys where they return keys. The stored-component bounds are just C: Ord (reads) and C: Ord + Clone (mutators); a method's key bound is RadixKey<Owned = C>. A str key and a Vec<char> key over the same characters address the same path.
  • Persistent + structurally shared. A shared internal node core mutates in place at refcount 1 and copies only when a node is shared, so old snapshots are never disturbed. The reference-counting pointer is an internal detail.
  • Sync / unsync split. unsync::Radix uses Rc (!Send, no atomics) and mutates in place via a direct &mut self copy-on-write handle. sync::Radix uses Arc (Send + Sync, auto-derived) and is an immutable persistent tree (go-immutable-radix's Tree): mutate it through a Txn (an owned working copy — open with txn(), edit, then commit() into the next tree; a one-shot edit is just txn() → edit → commit()). Both faces share one copy-on-write core, so a .clone() and a snapshot are O(1) and structurally shared.
  • Path-compressed, ordered. Edges live in the parent's Vec, sorted by their first component and located by binary search â 9EAD €” no hashing.
  • Prefix queries. Inclusive get_ancestor (longest covered prefix), strict_ancestor, ancestors, descendants, and bulk remove_descendants / drain_descendants (strict) or delete_prefix / drain_prefix (node-inclusive).
  • Ordered queries. minimum / maximum, forward and reverse value iteration (values / values_rev, descendants / descendants_rev, plus node-inclusive descendants_inclusive), and key-ordered (key, value) cursors in both directions: range (any RangeBounds) / seek_lower_bound ascending, and range_rev / seek_reverse_lower_bound descending. Key-carrying prefix and path walks — walk_prefix / walk_prefix_strict and walk_path (each with a _rev form) — mirror go-immutable-radix's WalkPrefix / WalkPath.
  • Bring-your-own concurrency. iradix ships no built-in concurrent holder. sync::Radix is an immutable, O(1)-clone tree, so you publish new versions yourself — the lock-free pattern wraps it in arc_swap::ArcSwap<sync::Radix<…>>: readers load() a wait-free snapshot, and a writer opens a txn(), commit()s the next tree, and publishes it with a compare-and-swap retry loop (so a write is never silently lost). A worked example ships in examples/sync.rs.
  • Watch (optional). With the watch feature, observe changes: watch(key) / watch_prefix(prefix) (or get_watch(key), which reads and arms against one immutable snapshot) return a Watch that resolves once a change to the key — or anything in its subtree — is published — block with block_wait() / block_wait_timeout(), or await changed() (runtime-agnostic, via event-listener; the future feature adds the bounded changed_timeout::<R>()). Notification is at publish, not at commit (the commit → publish → notify discipline): commit builds the next tree but fires nothing; the winner of publication then calls notify_changes_since(&base) (or folds both into publish_to(&base, swap)), so a tree that loses a CAS is discarded without waking anyone. That notify fires only the events of the nodes the new version replaced (the diff prunes pointer-equal subtrees, scanning the changed paths and their siblings — not the whole tree); a Watch is single-use, may over-notify (re-read to confirm), and is meant for a reload-and-re-arm loop. The feature is zero-cost when off. A worked example ships in examples/watch.rs.
  • no_std + alloc. std and alloc are independent features.

Installation

[dependencies]
iradix = "0.3"

For no_std (heap required):

[dependencies]
iradix = { version = "0.3", default-features = false, features = ["alloc"] }

Examples

Runnable, self-contained examples live in examples/:

example what it shows
unsync unsync::Radix — exact lookup; longest-covered-prefix queries (get_ancestor / strict_ancestor); ordered queries (minimum / maximum / range / seek_lower_bound); node-inclusive prefix removal; and O(1) snapshot isolation.
sync sync::Radix — lock-free concurrent reads (Send + Sync); a one-shot txn() → edit → commit() and a batching Txn → commit; and the lock-free shared-holder pattern with arc_swap::ArcSwap<sync::Radix<…>> (load to read; txn → commit → CAS to publish).
watch the watch feature — watch / watch_prefix / get_watch returning a Watch; blocking block_wait() and async changed().await; the commit → publish → notify loop over an ArcSwap (publish_to on a winning CAS, a lost CAS waking nobody, and reload-and-re-arm). Run with --features watch.

Run them with:

cargo run --example unsync
cargo run --example sync
cargo run --example watch --features watch

Cargo features

feature default effect
std yes links the real standard library.
alloc no no_std + heap (the alloc-as-std alias). Independent of std.
watch no observe key/prefix changes on the sync face: watch / watch_prefix / get_watch → blocking Watch::{block_wait, block_wait_timeout} (need std) or async Watch::changed() (a named Changed future; no_std + alloc), with publish-time notification via notify_changes_since / publish_to. Pulls event-listener.
future no runtime-agnostic async timeout: Watch::changed_timeout::<R>(d) (R = TokioRuntime / SmolRuntime / WasmRuntime / EmbassyRuntime / …) → Result<(), Elapsed>. Built on agnostic-lite; re-exports the RuntimeLite trait as iradix::RuntimeLite. Implies watch; no_std + alloc.
tokio / smol no convenience: enable agnostic-lite's tokio / smol backend and re-export its runtime as iradix::TokioRuntime / iradix::SmolRuntime (each implies future), so a runtime is nameable from iradix alone. Other backends (async-io / wasm / embassy) — add agnostic-lite directly.
triomphe no back the sync face with triomphe's Arc (no weak count, more compact) instead of std::sync::Arc, via archery's ArcTK. Same public API; opt-in; no_std + alloc.

std and alloc are independent (std does not imply alloc). The crate always needs the heap, so the bare no-feature configuration does not compile — build with at least alloc.

Panic safety

iradix upholds the strong exception guarantee for the recoverable panic a generic container can actually encounter: a panicking user Clone, Ord, or PartialEq. If insert, remove, remove_descendants, or drain_descendants unwinds because user code panicked, the trie is left exactly as it was — len stays accurate, every previously stored key still resolves, and no stored value is dropped or lost (a value-returning call either returns its value or leaves it in place).

Two failure modes are out of scope, both matching the standard library, which makes no guarantee across either:

  • Panicking Drop — a destructor that panics while unwinding aborts the process, so no container can stay sound across one. Removing an edge runs user destructors; if one panics, the operation may be left partially committed. Keep destructors panic-free.
  • Allocation failure — like Vec/Box, iradix reaches the global allocator through infallible APIs, so out-of-memory invokes the allocation-error handler (an abort by default), not an unwind. There is therefore no recoverable allocation panic to be safe across; OOM does not corrupt the trie because it does not return.

Benchmarks

benches/comparison.rs measures iradix against radix_trie, patricia_tree, qp-trie, im's OrdMap, and std's BTreeMap across insert / get / iter / remove / snapshot-then-insert, on random and path-like keysets — numbers and methodology in benches/README.md. The shape of the results: among ordered prefix maps iradix leads on insert/get/remove, and its copy-on-write snapshot stays O(1) — a clone-then-insert runs in nanoseconds where an ephemeral map deep-copies — which is the reason to reach for a persistent trie.

#![no_std]

The crate is no_std with alloc. Build it with --no-default-features --features alloc.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

A generic, persistent (copy-on-write) radix trie with structural sharing — bring-your-own-key, no_std + alloc, parameterized over Rc/Arc.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

Generated from al8n/template-rs
0