// 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, } 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> = 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 { 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 { 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 { 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 { 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.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` 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>, } // `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>, /// 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>) -> Self { let len: u64 = children.iter().map(|c| c.len()).sum(); Node::Internal { children, len } } fn empty_leaf() -> Arc { Arc::new(Node::Leaf(Arc::from(&[][..]))) } fn leaf_from(bytes: &[u8]) -> Arc { 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), Two(Arc, Arc), } /// 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, 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], idx: usize, result: NodeOut) -> NodeOut { let new_children: Vec> = 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, start: u64, end: u64) -> Arc { 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> = 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) -> Arc { 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 { 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> = 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> = 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() {} fn collect(rope: &Rope) -> Vec { 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::(); assert_send_sync_static::(); } #[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 = (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 = (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 = (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 = (0..3000).map(|i| (i % 251) as u8).collect(); let r = Rope::from_bytes(&src); // Whole rope. let joined: Vec = r.chunks(0, r.len()).flatten().copied().collect(); assert_eq!(joined, src); // Mid-chunk window crossing leaf boundaries. let joined: Vec = 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 = (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 = (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 = (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) -> 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 = (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 = (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 = (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 = (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 = (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 = (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], ); } }