pmacs/src/rope.rs

1148 lines
39 KiB
Rust

// rope.rs --- Persistent byte-addressed sequence backing every buffer.
//! Persistent byte-addressed sequence backing every buffer.
//!
//! Implements the rope contract from spec §3.1. The interface is byte-addressed
//! (not codepoint-, not grapheme-): grapheme awareness lives in the text view.
//! Edits do not mutate `self`; they return a new [`Rope`] sharing structure
//! with the old via [`std::sync::Arc`], plus an [`Edit`] description so views
//! can update incrementally without diffing.
//!
//! # Implementation
//!
//! A B-tree of immutable byte chunks. Internal nodes hold up to
//! [`MAX_CHILDREN`] children and cache subtree byte length. Leaves hold up to
//! [`MAX_LEAF_BYTES`] bytes in an `Arc<[u8]>`. All leaves are at uniform
//! depth (B-tree invariant). Edits build new nodes along the touched path
//! and share unchanged subtrees with the source.
//!
//! # Threading
//!
//! [`Rope`] is `Send + Sync`. Workers receive their own handle via
//! [`Rope::snapshot`]; the underlying tree is shared via `Arc`, reads are
//! lock-free, and edits never mutate existing nodes. There are no internal
//! locks.
use std::marker::PhantomData;
use std::sync::Arc;
// ---------------------------------------------------------------------------
// Tuning constants
// ---------------------------------------------------------------------------
/// Soft cap on bytes per leaf chunk. Inserts that overflow split the leaf in
/// half so each half stays at or below this size.
const MAX_LEAF_BYTES: usize = 1024;
/// Soft cap on children per internal node. Inserts that overflow split the
/// internal in half. Together with [`MAX_LEAF_BYTES`] this caps tree depth
/// at roughly `log_MAX_CHILDREN(rope.len() / MAX_LEAF_BYTES)`.
const MAX_CHILDREN: usize = 8;
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
// `Position` is re-exported from `pmacs-protocol` (session 1 of the
// `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). The type alias
// is a `u64` byte offset into a [`Rope`]: not a codepoint index, not
// a grapheme index. Grapheme awareness is a view-layer concern.
pub use pmacs_protocol::Position;
/// A persistent rope of bytes.
///
/// Cheap to clone (an `Arc` bump). Edits return a new rope sharing structure
/// with the old; the old rope remains valid as long as any handle holds it.
///
/// # Threading
///
/// `Send + Sync`. Any thread that holds a handle may read it; edits return
/// fresh handles, so the original is never mutated.
#[derive(Clone, Debug)]
pub struct Rope {
/// Shared tree root. The tree is immutable; new edits build new trees
/// that share unchanged subtrees with the source.
root: Arc<Node>,
}
impl Rope {
/// Construct an empty rope.
///
/// Threading: any thread.
#[must_use]
pub fn new() -> Self {
Self {
root: Node::empty_leaf(),
}
}
/// Construct a rope holding exactly the given bytes.
///
/// Builds a balanced tree directly: O(n / `MAX_LEAF_BYTES`) leaves,
/// O(n / `MAX_LEAF_BYTES`) internal nodes. This is the bulk-load path
/// for opening files. Threading: any thread.
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Self {
if bytes.is_empty() {
return Self::new();
}
let leaves: Vec<Arc<Node>> = bytes
.chunks(MAX_LEAF_BYTES)
.map(|c| Arc::new(Node::Leaf(Arc::from(c))))
.collect();
Self {
root: build_balanced_from_nodes(leaves),
}
}
/// Take a snapshot of this rope.
///
/// Equivalent to [`Clone::clone`]. Named explicitly because workers always
/// go through this entry point so the intent is documented at every call
/// site (R23). The returned `Rope` is independent of `self`: subsequent
/// edits to the buffer that produced `self` do not affect the snapshot.
///
/// Threading: any thread. The result is `Send + Sync` and may be passed
/// to a worker.
#[must_use]
pub fn snapshot(&self) -> Self {
self.clone()
}
/// Return the length of the rope in bytes.
///
/// O(1). Threading: any thread.
#[must_use]
pub fn len(&self) -> Position {
self.root.len()
}
/// Return true iff the rope holds zero bytes.
///
/// Threading: any thread.
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Read the byte at `pos`, or `None` if `pos >= len()`.
///
/// O(log n). Threading: any thread.
#[must_use]
pub fn byte_at(&self, pos: Position) -> Option<u8> {
if pos >= self.len() {
return None;
}
Some(byte_at_in(&self.root, pos))
}
/// Copy the bytes in `[start, end)` into `out`.
///
/// `out.len()` must equal `end - start`, and the range must be within
/// the rope; both are enforced by debug assertion. Threading: any thread.
pub fn slice(&self, start: Position, end: Position, out: &mut [u8]) {
debug_assert!(start <= end);
debug_assert!(end <= self.len());
debug_assert_eq!(out.len() as u64, end - start);
if out.is_empty() {
return;
}
slice_in(&self.root, start, end, 0, out);
}
/// Iterate over the bytes in `[start, end)` as contiguous chunk slices.
///
/// The returned iterator borrows from this handle: the rope must outlive
/// the iterator. Bytes are not copied; the slices alias chunk memory.
/// Yields zero items if `start == end`. Threading: any thread, but the
/// iterator itself is single-threaded.
#[must_use]
pub fn chunks(&self, start: Position, end: Position) -> Chunks<'_> {
let mut out: Vec<&[u8]> = Vec::new();
if start < end {
collect_leaves(&self.root, start, end, 0, &mut out);
}
Chunks {
inner: out.into_iter(),
_phantom: PhantomData,
}
}
/// Insert `bytes` at `pos`.
///
/// Returns a new rope and an [`Edit`] describing the change relative to
/// `self`. Returns [`RopeError::OutOfBounds`] if `pos > len()`. Threading:
/// any thread; `self` is not mutated.
pub fn insert(&self, pos: Position, bytes: &[u8]) -> Result<Edit, RopeError> {
if pos > self.len() {
return Err(RopeError::OutOfBounds {
pos,
len: self.len(),
});
}
if bytes.is_empty() {
return Ok(Edit {
new_rope: self.clone(),
range: Range::new(pos, pos),
inserted_len: 0,
crdt_op: None,
});
}
let mut root = Arc::clone(&self.root);
let mut at = pos;
for chunk in bytes.chunks(MAX_LEAF_BYTES) {
root = match insert_small(&root, at, chunk) {
NodeOut::One(n) => n,
NodeOut::Two(a, b) => Arc::new(Node::make_internal(vec![a, b])),
};
at += chunk.len() as u64;
}
Ok(Edit {
new_rope: Self { root },
range: Range::new(pos, pos),
inserted_len: bytes.len() as u64,
crdt_op: None,
})
}
/// Delete the byte range `[start, end)`.
///
/// Returns a new rope and an [`Edit`] describing the change. Returns
/// [`RopeError::OutOfBounds`] if `start > end` or `end > len()`. Threading:
/// any thread; `self` is not mutated.
pub fn delete(&self, start: Position, end: Position) -> Result<Edit, RopeError> {
if start > end {
return Err(RopeError::OutOfBounds {
pos: start,
len: self.len(),
});
}
if end > self.len() {
return Err(RopeError::OutOfBounds {
pos: end,
len: self.len(),
});
}
if start == end {
return Ok(Edit {
new_rope: self.clone(),
range: Range::new(start, end),
inserted_len: 0,
crdt_op: None,
});
}
let new_root = if start == 0 && end == self.len() {
Node::empty_leaf()
} else {
collapse_root(delete_in(&self.root, start, end))
};
Ok(Edit {
new_rope: Self { root: new_root },
range: Range::new(start, end),
inserted_len: 0,
crdt_op: None,
})
}
/// Replace the byte range `[start, end)` with `bytes`.
///
/// Equivalent to a delete followed by an insert at `start`, fused into a
/// single [`Edit`] description. Returns [`RopeError::OutOfBounds`] if the
/// range is invalid. Threading: any thread; `self` is not mutated.
pub fn replace(&self, start: Position, end: Position, bytes: &[u8]) -> Result<Edit, RopeError> {
let after_delete = self.delete(start, end)?;
let after_insert = after_delete.new_rope.insert(start, bytes)?;
Ok(Edit {
new_rope: after_insert.new_rope,
range: Range::new(start, end),
inserted_len: bytes.len() as u64,
crdt_op: None,
})
}
}
impl Default for Rope {
fn default() -> Self {
Self::new()
}
}
/// Iterator over the byte chunks of a rope range.
///
/// Each `next` yields a contiguous `&[u8]` borrowed from the rope's storage;
/// the union of the slices is exactly the requested range. Empty ranges
/// produce zero items.
pub struct Chunks<'a> {
inner: std::vec::IntoIter<&'a [u8]>,
_phantom: PhantomData<&'a Rope>,
}
impl<'a> Iterator for Chunks<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
/// The result of a successful edit.
///
/// Carries the new rope and a precise description of what changed, so views
/// can update incrementally without diffing. The semantics:
///
/// * `range` is the byte range *in the OLD rope* that was affected.
/// * `inserted_len` is the number of bytes inserted at `range.start`
/// *in the NEW rope*.
///
/// A pure insert has `range.start == range.end` and `inserted_len > 0`.
/// A pure delete has `range.start < range.end` and `inserted_len == 0`.
/// A replace has both nonzero.
#[derive(Clone, Debug)]
pub struct Edit {
/// The rope after the edit. `Send + Sync`; safe to hand to a worker.
pub new_rope: Rope,
/// The byte range in the *old* rope that was affected.
pub range: Range,
/// Number of bytes inserted at `range.start` in the *new* rope.
pub inserted_len: u64,
/// T M10.2 Day 3: optional CRDT-op metadata.
///
/// `Some` when this Edit was produced by a CRDT-backed Buffer's
/// edit path (`apply_edit` / `undo` / `redo`); `None` otherwise — both
/// in v0.1 mode (no CRDT) and for no-op edits in CRDT mode (an
/// empty insert at an empty range produces no CRDT op).
///
/// `Box` indirection: keeps Edit's None-case cost to 8 bytes
/// (Box has a niche-optimized None) rather than the ~32 bytes
/// inline `Option<CrdtOp>` would take. Edit is constructed in
/// hot paths (every rope edit), so the size matters; CRDT mode
/// pays one allocation per edit, v0.1 mode pays nothing extra.
///
/// Always present (not `#[cfg]`-gated) to avoid feature-flag
/// proliferation through every Edit consumer (views, hooks,
/// intercepts, undo stack — dozens of touch points). Consumers
/// that don't care ignore the field; M10.5 (wire protocol) and
/// M10.4 (per-frontend undo) consume it.
pub crdt_op: Option<Box<CrdtOp>>,
}
// `CrdtOp` moved to `pmacs-protocol::crdt` (session 1 of the
// `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). Re-exported here
// so existing `crate::rope::CrdtOp` import paths continue to resolve.
pub use pmacs_protocol::CrdtOp;
/// A half-open byte range `[start, end)` into a rope.
///
/// A range is *valid* for a rope iff `start <= end <= rope.len()`.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Range {
/// Inclusive start offset, in bytes.
pub start: Position,
/// Exclusive end offset, in bytes.
pub end: Position,
}
impl Range {
/// Construct a range. Does not validate; validation happens at use sites.
#[must_use]
pub const fn new(start: Position, end: Position) -> Self {
Self { start, end }
}
/// Length of the range in bytes (`end - start`).
#[must_use]
pub const fn len(self) -> u64 {
self.end.saturating_sub(self.start)
}
/// True iff `start == end`.
#[must_use]
pub const fn is_empty(self) -> bool {
self.start == self.end
}
}
/// Errors produced by the rope.
#[derive(Debug, thiserror::Error)]
pub enum RopeError {
/// A position or range fell outside the rope.
///
/// Carries the offending position and the rope's length at the time of
/// the error so the message is useful at the point of display (R12).
#[error("rope position {pos} out of bounds (len = {len})")]
OutOfBounds {
/// The offending position. For ranges, `start` if `start > end`,
/// otherwise `end`.
pos: u64,
/// Length of the rope when the error was raised.
len: u64,
},
}
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------
/// B-tree node. Private; the `Rope` struct is the only exposed handle.
#[derive(Debug)]
enum Node {
/// Up to [`MAX_LEAF_BYTES`] bytes of immutable storage.
Leaf(Arc<[u8]>),
/// 1..=[`MAX_CHILDREN`] children of identical depth, plus the cumulative
/// byte length of those children.
Internal {
/// Children of this node. Empty only inside transient build steps.
children: Vec<Arc<Node>>,
/// Cumulative byte length, cached so reads don't re-sum.
len: u64,
},
}
impl Node {
fn len(&self) -> u64 {
match self {
Node::Leaf(b) => b.len() as u64,
Node::Internal { len, .. } => *len,
}
}
fn make_internal(children: Vec<Arc<Node>>) -> Self {
let len: u64 = children.iter().map(|c| c.len()).sum();
Node::Internal { children, len }
}
fn empty_leaf() -> Arc<Self> {
Arc::new(Node::Leaf(Arc::from(&[][..])))
}
fn leaf_from(bytes: &[u8]) -> Arc<Self> {
Arc::new(Node::Leaf(Arc::from(bytes)))
}
#[cfg(test)]
fn depth(&self) -> u32 {
match self {
Node::Leaf(_) => 0,
Node::Internal { children, .. } => 1 + children[0].depth(),
}
}
}
/// Result of an insert step at a given level: either replace the source slot
/// with one node (no growth) or with two (split). Both same depth as input.
enum NodeOut {
One(Arc<Node>),
Two(Arc<Node>, Arc<Node>),
}
/// Insert at most [`MAX_LEAF_BYTES`] bytes into `node` at byte position `pos`.
/// Returns 1 or 2 replacement nodes of identical depth.
fn insert_small(node: &Arc<Node>, pos: u64, bytes: &[u8]) -> NodeOut {
debug_assert!(bytes.len() <= MAX_LEAF_BYTES);
debug_assert!(pos <= node.len());
match &**node {
Node::Leaf(chunk) => {
let p = pos as usize;
let total = chunk.len() + bytes.len();
let mut buf = Vec::with_capacity(total);
buf.extend_from_slice(&chunk[..p]);
buf.extend_from_slice(bytes);
buf.extend_from_slice(&chunk[p..]);
if total <= MAX_LEAF_BYTES {
NodeOut::One(Node::leaf_from(&buf))
} else {
// Total ≤ 2 * MAX_LEAF_BYTES (chunk and bytes each ≤ MAX_LEAF).
// Splitting at total / 2 keeps both halves ≤ MAX_LEAF_BYTES.
let mid = total / 2;
NodeOut::Two(Node::leaf_from(&buf[..mid]), Node::leaf_from(&buf[mid..]))
}
}
Node::Internal { children, .. } => {
// Find which child contains `pos`. Inclusive on the trailing edge
// so an insert at `len` lands in the last child.
let mut offset = 0u64;
for (i, child) in children.iter().enumerate() {
let child_len = child.len();
if pos <= offset + child_len {
let local = pos - offset;
let result = insert_small(child, local, bytes);
return splice_internal(children, i, result);
}
offset += child_len;
}
unreachable!("insert_small: pos > node.len() (caller invariant violated)");
}
}
}
/// Replace `children[idx]` with the 1-or-2 nodes from `result`, then either
/// produce a single internal (≤ `MAX_CHILDREN`) or split into two.
fn splice_internal(children: &[Arc<Node>], idx: usize, result: NodeOut) -> NodeOut {
let new_children: Vec<Arc<Node>> = match result {
NodeOut::One(n) => {
let mut v = Vec::with_capacity(children.len());
for (j, c) in children.iter().enumerate() {
v.push(if j == idx {
Arc::clone(&n)
} else {
Arc::clone(c)
});
}
v
}
NodeOut::Two(a, b) => {
let mut v = Vec::with_capacity(children.len() + 1);
for (j, c) in children.iter().enumerate() {
if j == idx {
v.push(a.clone());
v.push(b.clone());
} else {
v.push(Arc::clone(c));
}
}
v
}
};
if new_children.len() <= MAX_CHILDREN {
NodeOut::One(Arc::new(Node::make_internal(new_children)))
} else {
let mid = new_children.len() / 2;
let left = new_children[..mid].to_vec();
let right = new_children[mid..].to_vec();
NodeOut::Two(
Arc::new(Node::make_internal(left)),
Arc::new(Node::make_internal(right)),
)
}
}
/// Delete `[start, end)` from `node`. Caller guarantees the range is strictly
/// less than the entire node. The result has the same depth as `node`.
fn delete_in(node: &Arc<Node>, start: u64, end: u64) -> Arc<Node> {
debug_assert!(start < end);
debug_assert!(end <= node.len());
debug_assert!(!(start == 0 && end == node.len()));
match &**node {
Node::Leaf(chunk) => {
let s = start as usize;
let e = end as usize;
let mut buf = Vec::with_capacity(chunk.len() - (e - s));
buf.extend_from_slice(&chunk[..s]);
buf.extend_from_slice(&chunk[e..]);
Node::leaf_from(&buf)
}
Node::Internal { children, .. } => {
let mut new_children: Vec<Arc<Node>> = Vec::with_capacity(children.len());
let mut offset = 0u64;
for child in children {
let child_len = child.len();
let child_end = offset + child_len;
if end <= offset || start >= child_end {
// No overlap: keep child unchanged.
new_children.push(Arc::clone(child));
} else if start <= offset && end >= child_end {
// Fully consumed: drop child.
} else {
// Partial overlap: recurse.
let local_start = start.saturating_sub(offset);
let local_end = (end - offset).min(child_len);
let new_child = delete_in(child, local_start, local_end);
new_children.push(new_child);
}
offset = child_end;
}
// We cannot have zero children: the caller guaranteed the
// delete is strictly less than the whole node, so at least one
// child either survives unchanged or in modified form.
debug_assert!(!new_children.is_empty());
Arc::new(Node::make_internal(new_children))
}
}
}
/// After a delete, the root may be a single-child internal whose only child
/// has the same content. Collapse such chains so depth tracks size.
fn collapse_root(node: Arc<Node>) -> Arc<Node> {
let mut cur = node;
loop {
match &*cur {
Node::Internal { children, .. } if children.len() == 1 => {
let only = Arc::clone(&children[0]);
cur = only;
}
_ => break,
}
}
cur
}
fn byte_at_in(node: &Node, pos: u64) -> u8 {
match node {
Node::Leaf(chunk) => chunk[pos as usize],
Node::Internal { children, .. } => {
let mut offset = 0u64;
for child in children {
let cl = child.len();
if pos < offset + cl {
return byte_at_in(child, pos - offset);
}
offset += cl;
}
unreachable!("byte_at_in: pos >= node.len()");
}
}
}
fn slice_in(node: &Node, start: u64, end: u64, node_offset: u64, out: &mut [u8]) {
match node {
Node::Leaf(chunk) => {
let local_start = (start - node_offset) as usize;
let local_end = (end - node_offset) as usize;
out.copy_from_slice(&chunk[local_start..local_end]);
}
Node::Internal { children, .. } => {
let mut offset = node_offset;
let mut written = 0usize;
for child in children {
let cl = child.len();
let child_end = offset + cl;
if end <= offset {
break;
}
if start < child_end && start.max(offset) < end.min(child_end) {
let take_start = start.max(offset);
let take_end = end.min(child_end);
let take = (take_end - take_start) as usize;
slice_in(
child,
take_start,
take_end,
offset,
&mut out[written..written + take],
);
written += take;
}
offset = child_end;
}
}
}
}
fn collect_leaves<'a>(
node: &'a Node,
start: u64,
end: u64,
node_offset: u64,
out: &mut Vec<&'a [u8]>,
) {
match node {
Node::Leaf(chunk) => {
let node_end = node_offset + chunk.len() as u64;
if end <= node_offset || start >= node_end {
return;
}
let local_start = start.saturating_sub(node_offset) as usize;
let local_end = ((end - node_offset).min(chunk.len() as u64)) as usize;
if local_start < local_end {
out.push(&chunk[local_start..local_end]);
}
}
Node::Internal { children, .. } => {
let mut offset = node_offset;
for child in children {
let cl = child.len();
let child_end = offset + cl;
if end <= offset {
break;
}
if start < child_end {
collect_leaves(child, start, end, offset, out);
}
offset = child_end;
}
}
}
}
/// Build a balanced internal-tree from a flat list of same-depth nodes.
/// Bottom-up: pack groups of up to [`MAX_CHILDREN`], recurse until one node
/// remains. A trailing group of size 1 is folded into the previous group to
/// avoid pathological single-child internals (cheap rebalance for free).
fn build_balanced_from_nodes(nodes: Vec<Arc<Node>>) -> Arc<Node> {
if nodes.is_empty() {
return Node::empty_leaf();
}
if nodes.len() == 1 {
return nodes.into_iter().next().expect("len 1");
}
let mut current = nodes;
while current.len() > 1 {
let mut next: Vec<Arc<Node>> = Vec::with_capacity(current.len().div_ceil(MAX_CHILDREN));
let mut i = 0;
while i < current.len() {
let remaining = current.len() - i;
// If we'd leave a single straggler at the end, take fewer now so
// the next group has at least 2 elements.
let take = if remaining > MAX_CHILDREN && remaining - MAX_CHILDREN == 1 {
MAX_CHILDREN - 1
} else {
remaining.min(MAX_CHILDREN)
};
let group: Vec<Arc<Node>> = current[i..i + take].to_vec();
next.push(Arc::new(Node::make_internal(group)));
i += take;
}
current = next;
}
current.into_iter().next().expect("non-empty after loop")
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
fn assert_send_sync_static<T: Send + Sync + 'static>() {}
fn collect(rope: &Rope) -> Vec<u8> {
let mut out = vec![0u8; rope.len() as usize];
rope.slice(0, rope.len(), &mut out);
out
}
fn check_invariants(node: &Node) {
match node {
Node::Leaf(b) => {
assert!(
b.len() <= MAX_LEAF_BYTES,
"leaf {} > MAX_LEAF_BYTES",
b.len()
);
}
Node::Internal { children, len } => {
assert!(!children.is_empty(), "internal node with zero children");
assert!(
children.len() <= MAX_CHILDREN,
"internal {} > MAX_CHILDREN",
children.len()
);
let depth = children[0].depth();
let sum: u64 = children.iter().map(|c| c.len()).sum();
assert_eq!(sum, *len, "internal len out of sync");
for c in children {
assert_eq!(c.depth(), depth, "depth mismatch among siblings");
check_invariants(c);
}
}
}
}
// ----- type-level / smoke -----
#[test]
fn rope_is_send_sync() {
assert_send_sync_static::<Rope>();
assert_send_sync_static::<Edit>();
}
#[test]
fn empty_rope_basics() {
let r = Rope::new();
assert_eq!(r.len(), 0);
assert!(r.is_empty());
assert_eq!(r.byte_at(0), None);
assert_eq!(r.chunks(0, 0).count(), 0);
check_invariants(&r.root);
}
#[test]
fn range_basics() {
let r = Range::new(3, 8);
assert_eq!(r.len(), 5);
assert!(!r.is_empty());
let empty = Range::new(5, 5);
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
}
// ----- from_bytes / read paths -----
#[test]
fn from_bytes_small_roundtrip() {
let src = b"hello world";
let r = Rope::from_bytes(src);
assert_eq!(r.len(), src.len() as u64);
assert_eq!(collect(&r), src);
check_invariants(&r.root);
}
#[test]
fn from_bytes_multi_chunk_roundtrip() {
// Force multiple leaves: 4096 bytes / 1024 leaf cap -> 4 leaves.
let src: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
let r = Rope::from_bytes(&src);
assert_eq!(r.len(), src.len() as u64);
assert_eq!(collect(&r), src);
check_invariants(&r.root);
}
#[test]
fn from_bytes_large_roundtrip() {
// 100 KB exercises a multi-level internal tree.
let src: Vec<u8> = (0..100_000).map(|i| (i % 251) as u8).collect();
let r = Rope::from_bytes(&src);
assert_eq!(r.len(), src.len() as u64);
assert_eq!(collect(&r), src);
check_invariants(&r.root);
}
#[test]
fn byte_at_walks_tree() {
let src: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
let r = Rope::from_bytes(&src);
for i in [0u64, 1, 100, 1023, 1024, 1025, 4095] {
assert_eq!(r.byte_at(i), Some(src[i as usize]));
}
assert_eq!(r.byte_at(4096), None);
assert_eq!(r.byte_at(u64::MAX), None);
}
#[test]
fn chunks_match_slice() {
let src: Vec<u8> = (0..3000).map(|i| (i % 251) as u8).collect();
let r = Rope::from_bytes(&src);
// Whole rope.
let joined: Vec<u8> = r.chunks(0, r.len()).flatten().copied().collect();
assert_eq!(joined, src);
// Mid-chunk window crossing leaf boundaries.
let joined: Vec<u8> = r.chunks(500, 2500).flatten().copied().collect();
assert_eq!(joined, src[500..2500]);
// Empty range yields zero items.
assert_eq!(r.chunks(100, 100).count(), 0);
}
// ----- insert -----
#[test]
fn insert_into_empty() {
let r = Rope::new();
let edit = r.insert(0, b"abc").unwrap();
assert_eq!(edit.range, Range::new(0, 0));
assert_eq!(edit.inserted_len, 3);
assert_eq!(collect(&edit.new_rope), b"abc");
check_invariants(&edit.new_rope.root);
}
#[test]
fn insert_at_start_middle_end() {
let r = Rope::from_bytes(b"hello");
let r = r.insert(5, b"!").unwrap().new_rope;
assert_eq!(collect(&r), b"hello!");
let r = r.insert(0, b"[").unwrap().new_rope;
assert_eq!(collect(&r), b"[hello!");
let r = r.insert(3, b"-").unwrap().new_rope;
assert_eq!(collect(&r), b"[he-llo!");
check_invariants(&r.root);
}
#[test]
fn insert_past_end_is_error() {
let r = Rope::from_bytes(b"abc");
let err = r.insert(4, b"x").unwrap_err();
match err {
RopeError::OutOfBounds { pos, len } => {
assert_eq!(pos, 4);
assert_eq!(len, 3);
}
}
}
#[test]
fn insert_triggers_leaf_split() {
// Start with a near-full leaf, then insert into the middle.
let mut chunk = vec![b'a'; MAX_LEAF_BYTES - 10];
let r = Rope::from_bytes(&chunk);
// Insert 100 bytes in the middle: total 1014 + 100 = 1114 > 1024 → split.
let inserted = vec![b'b'; 100];
let edited = r
.insert((MAX_LEAF_BYTES / 2) as u64, &inserted)
.unwrap()
.new_rope;
let mut expected = chunk.clone();
expected.splice(
(MAX_LEAF_BYTES / 2)..(MAX_LEAF_BYTES / 2),
inserted.iter().copied(),
);
assert_eq!(collect(&edited), expected);
check_invariants(&edited.root);
// Confirm we actually split: the resulting root is now an Internal.
chunk.clear();
match &*edited.root {
Node::Leaf(_) => panic!("expected internal after split"),
Node::Internal { .. } => {}
}
}
#[test]
fn insert_large_block() {
let r = Rope::from_bytes(b"prefix-suffix");
let middle: Vec<u8> = (0..50_000).map(|i| (i % 251) as u8).collect();
let edited = r.insert(7, &middle).unwrap().new_rope;
let mut expected = Vec::with_capacity(13 + middle.len());
expected.extend_from_slice(b"prefix-");
expected.extend_from_slice(&middle);
expected.extend_from_slice(b"suffix");
assert_eq!(edited.len(), expected.len() as u64);
assert_eq!(collect(&edited), expected);
check_invariants(&edited.root);
}
// ----- delete -----
#[test]
fn delete_in_single_leaf() {
let r = Rope::from_bytes(b"hello world");
let r = r.delete(5, 11).unwrap().new_rope;
assert_eq!(collect(&r), b"hello");
check_invariants(&r.root);
}
#[test]
fn delete_full_rope() {
let r = Rope::from_bytes(b"hello");
let edit = r.delete(0, 5).unwrap();
assert_eq!(edit.new_rope.len(), 0);
assert_eq!(edit.inserted_len, 0);
assert_eq!(edit.range, Range::new(0, 5));
check_invariants(&edit.new_rope.root);
}
#[test]
fn delete_across_chunks() {
let src: Vec<u8> = (0..10_000).map(|i| (i % 251) as u8).collect();
let r = Rope::from_bytes(&src);
let edited = r.delete(500, 9500).unwrap().new_rope;
let mut expected = src[..500].to_vec();
expected.extend_from_slice(&src[9500..]);
assert_eq!(collect(&edited), expected);
check_invariants(&edited.root);
}
#[test]
fn delete_invalid_ranges() {
let r = Rope::from_bytes(b"abc");
assert!(r.delete(2, 1).is_err());
assert!(r.delete(0, 4).is_err());
}
// ----- replace -----
#[test]
fn replace_simple() {
let r = Rope::from_bytes(b"hello world");
let edit = r.replace(6, 11, b"there").unwrap();
assert_eq!(collect(&edit.new_rope), b"hello there");
assert_eq!(edit.range, Range::new(6, 11));
assert_eq!(edit.inserted_len, 5);
check_invariants(&edit.new_rope.root);
}
#[test]
fn replace_with_empty_is_delete() {
let r = Rope::from_bytes(b"abcdef");
let edit = r.replace(2, 5, b"").unwrap();
assert_eq!(collect(&edit.new_rope), b"abf");
assert_eq!(edit.inserted_len, 0);
}
#[test]
fn replace_empty_range_is_insert() {
let r = Rope::from_bytes(b"abcdef");
let edit = r.replace(3, 3, b"XYZ").unwrap();
assert_eq!(collect(&edit.new_rope), b"abcXYZdef");
assert_eq!(edit.range, Range::new(3, 3));
assert_eq!(edit.inserted_len, 3);
}
// ----- snapshot independence -----
#[test]
fn snapshot_is_independent() {
let r = Rope::from_bytes(b"original");
let snap = r.snapshot();
let edited = r.insert(0, b"X").unwrap().new_rope;
// The snapshot is unaffected by the new edit.
assert_eq!(collect(&snap), b"original");
assert_eq!(collect(&edited), b"Xoriginal");
}
// ----- structural sharing -----
#[test]
fn unaffected_leaves_are_shared_by_arc() {
// A 32 KB rope, edit at the end, observe that the front portion's
// first leaf is still pointer-equal to the original's first leaf.
let src: Vec<u8> = (0..32 * 1024).map(|i| (i % 251) as u8).collect();
let original = Rope::from_bytes(&src);
let original_first_leaf = first_leaf_arc(&original.root);
let edited = original.insert(original.len(), b"Z").unwrap().new_rope;
let edited_first_leaf = first_leaf_arc(&edited.root);
// The first leaf is untouched by an end-of-rope insert -> structural
// sharing means the Arc points to the same allocation.
assert!(Arc::ptr_eq(&original_first_leaf, &edited_first_leaf));
}
fn first_leaf_arc(node: &Arc<Node>) -> Arc<[u8]> {
match &**node {
Node::Leaf(b) => Arc::clone(b),
Node::Internal { children, .. } => first_leaf_arc(&children[0]),
}
}
// ----- random fuzz -----
#[test]
fn random_edit_sequence_matches_reference() {
// Pseudorandom sequence with a fixed seed: deterministic and
// dense enough to exercise both leaf and internal splits / collapses.
let mut rng_state: u64 = 0xDEAD_BEEF;
let mut rng = || {
rng_state = rng_state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(rng_state >> 33) as u32
};
let mut reference: Vec<u8> = (0..2048).map(|i| (i % 251) as u8).collect();
let mut rope = Rope::from_bytes(&reference);
for _ in 0..1000 {
let len = reference.len();
let op = rng() % 3;
match op {
0 => {
// Insert
let pos = (rng() as usize) % (len + 1);
let n = (rng() % 64 + 1) as usize;
let bytes: Vec<u8> = (0..n)
.map(|i| (rng() as u8).wrapping_add(i as u8))
.collect();
rope = rope.insert(pos as u64, &bytes).unwrap().new_rope;
reference.splice(pos..pos, bytes);
}
1 => {
// Delete
if len == 0 {
continue;
}
let s = (rng() as usize) % len;
let e = s + (rng() as usize) % (len - s + 1).max(1);
let e = e.min(len);
if s == e {
continue;
}
rope = rope.delete(s as u64, e as u64).unwrap().new_rope;
reference.drain(s..e);
}
_ => {
// Replace
if len == 0 {
continue;
}
let s = (rng() as usize) % len;
let e = s + (rng() as usize) % (len - s + 1).max(1);
let e = e.min(len);
let n = (rng() % 32) as usize;
let bytes: Vec<u8> = (0..n)
.map(|i| (rng() as u8).wrapping_add(i as u8))
.collect();
rope = rope.replace(s as u64, e as u64, &bytes).unwrap().new_rope;
reference.splice(s..e, bytes);
}
}
assert_eq!(rope.len(), reference.len() as u64);
check_invariants(&rope.root);
}
assert_eq!(collect(&rope), reference);
}
// ----- perf smoke -----
//
// Coarse timing checks. The acceptance suite (T M1.10) holds the
// proper benchmarks; these exist so a regression here shows up
// immediately rather than waiting for the formal harness.
#[test]
#[ignore = "perf smoke; run with --release --ignored"]
fn perf_smoke_load_100mb() {
let src: Vec<u8> = (0..100 * 1024 * 1024).map(|i| (i % 251) as u8).collect();
let started = std::time::Instant::now();
let rope = Rope::from_bytes(&src);
let elapsed = started.elapsed();
eprintln!("from_bytes(100MB): {elapsed:?}");
assert_eq!(rope.len(), src.len() as u64);
}
#[test]
#[ignore = "perf smoke; run with --release --ignored"]
fn perf_smoke_snapshot() {
let src: Vec<u8> = (0..10 * 1024 * 1024).map(|i| (i % 251) as u8).collect();
let rope = Rope::from_bytes(&src);
let started = std::time::Instant::now();
let mut snaps = Vec::with_capacity(10_000);
for _ in 0..10_000 {
snaps.push(rope.snapshot());
}
let elapsed = started.elapsed();
let per_op = elapsed / 10_000;
eprintln!("snapshot p~avg over 10k: {per_op:?}");
// Sanity: keep snaps alive past the timing window.
std::hint::black_box(snaps);
}
#[test]
#[ignore = "perf smoke; run with --release --ignored"]
fn perf_smoke_edit_latency() {
let src: Vec<u8> = (0..10 * 1024 * 1024).map(|i| (i % 251) as u8).collect();
let mut rope = Rope::from_bytes(&src);
let mut times = Vec::with_capacity(1000);
for i in 0..1000 {
let pos = (i * 1024) % rope.len();
let started = std::time::Instant::now();
rope = rope.insert(pos, b"x").unwrap().new_rope;
times.push(started.elapsed());
}
times.sort_unstable();
eprintln!(
"edit p50 = {:?}, p99 = {:?}, max = {:?}",
times[500], times[990], times[999],
);
}
}