1148 lines
39 KiB
Rust
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],
|
|
);
|
|
}
|
|
}
|