Retain atom composition and incremental context validation facts
This commit is contained in:
parent
e48af058b5
commit
c9de7e6329
782
native/src/atom_plan.rs
Normal file
782
native/src/atom_plan.rs
Normal file
@ -0,0 +1,782 @@
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::sequence::{Entry, Measure, Node, NodeView, Sequence, Work};
|
||||
|
||||
use super::{Atom, AtomProperties, RegionRole, RegionRoleEntry};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) struct AtomPlanWork {
|
||||
pub(crate) sequence_nodes_created: u64,
|
||||
pub(crate) sequence_entries_written: u64,
|
||||
pub(crate) materialized_atoms: u64,
|
||||
pub(crate) property_applications: u64,
|
||||
pub(crate) atom_clones: u64,
|
||||
pub(crate) operation_block_writes: u64,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static WORK: Cell<AtomPlanWork> = Cell::new(AtomPlanWork::default());
|
||||
}
|
||||
|
||||
pub(crate) fn reset_work() {
|
||||
WORK.set(AtomPlanWork::default());
|
||||
}
|
||||
|
||||
pub(crate) fn work() -> AtomPlanWork {
|
||||
WORK.get()
|
||||
}
|
||||
|
||||
fn record_sequence(work: Work) {
|
||||
WORK.set(WORK.get().checked_add(AtomPlanWork {
|
||||
sequence_nodes_created: work.nodes_created,
|
||||
sequence_entries_written: work.entries_written,
|
||||
materialized_atoms: 0,
|
||||
property_applications: 0,
|
||||
atom_clones: 0,
|
||||
operation_block_writes: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
fn record_materialized(count: usize) {
|
||||
let count = u64::try_from(count).expect("validated native atom count fits u64");
|
||||
WORK.set(WORK.get().checked_add(AtomPlanWork {
|
||||
materialized_atoms: count,
|
||||
atom_clones: count,
|
||||
..AtomPlanWork::default()
|
||||
}));
|
||||
}
|
||||
|
||||
fn record_atom_clones(count: usize) {
|
||||
let count = u64::try_from(count).expect("validated native atom count fits u64");
|
||||
WORK.set(WORK.get().checked_add(AtomPlanWork {
|
||||
atom_clones: count,
|
||||
..AtomPlanWork::default()
|
||||
}));
|
||||
}
|
||||
|
||||
fn record_operation_block_writes(count: usize) {
|
||||
let count = u64::try_from(count).expect("native atom decoration block fits u64");
|
||||
WORK.set(WORK.get().checked_add(AtomPlanWork {
|
||||
operation_block_writes: count,
|
||||
..AtomPlanWork::default()
|
||||
}));
|
||||
}
|
||||
|
||||
fn record_property_application() {
|
||||
WORK.set(WORK.get().checked_add(AtomPlanWork {
|
||||
property_applications: 1,
|
||||
..AtomPlanWork::default()
|
||||
}));
|
||||
}
|
||||
|
||||
impl AtomPlanWork {
|
||||
pub(crate) fn accumulate(&mut self, other: Self) {
|
||||
let combined = Self {
|
||||
sequence_nodes_created: self
|
||||
.sequence_nodes_created
|
||||
.checked_add(other.sequence_nodes_created)
|
||||
.expect("native atom plan node work counter overflow"),
|
||||
sequence_entries_written: self
|
||||
.sequence_entries_written
|
||||
.checked_add(other.sequence_entries_written)
|
||||
.expect("native atom plan entry work counter overflow"),
|
||||
materialized_atoms: self
|
||||
.materialized_atoms
|
||||
.checked_add(other.materialized_atoms)
|
||||
.expect("native atom plan materialization counter overflow"),
|
||||
property_applications: self
|
||||
.property_applications
|
||||
.checked_add(other.property_applications)
|
||||
.expect("native atom plan property work counter overflow"),
|
||||
atom_clones: self
|
||||
.atom_clones
|
||||
.checked_add(other.atom_clones)
|
||||
.expect("native atom plan clone work counter overflow"),
|
||||
operation_block_writes: self
|
||||
.operation_block_writes
|
||||
.checked_add(other.operation_block_writes)
|
||||
.expect("native atom plan operation work counter overflow"),
|
||||
};
|
||||
*self = combined;
|
||||
}
|
||||
|
||||
fn checked_add(mut self, other: Self) -> Self {
|
||||
self.accumulate(other);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct RunSummary {
|
||||
prefix: i64,
|
||||
suffix: i64,
|
||||
maximum: i64,
|
||||
all_run: bool,
|
||||
}
|
||||
|
||||
impl RunSummary {
|
||||
fn atom(atom: &Atom) -> Self {
|
||||
if atom.wrap_space() {
|
||||
Self::default()
|
||||
} else if atom.wrap_cjk() {
|
||||
Self {
|
||||
maximum: atom.width(),
|
||||
..Self::default()
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
prefix: atom.width(),
|
||||
suffix: atom.width(),
|
||||
maximum: atom.width(),
|
||||
all_run: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn combine(self, other: Self) -> Self {
|
||||
let cross = self
|
||||
.suffix
|
||||
.checked_add(other.prefix)
|
||||
.expect("validated native atom widths fit i64");
|
||||
Self {
|
||||
prefix: if self.all_run {
|
||||
self.prefix
|
||||
.checked_add(other.prefix)
|
||||
.expect("validated native atom widths fit i64")
|
||||
} else {
|
||||
self.prefix
|
||||
},
|
||||
suffix: if other.all_run {
|
||||
self.suffix
|
||||
.checked_add(other.suffix)
|
||||
.expect("validated native atom widths fit i64")
|
||||
} else {
|
||||
other.suffix
|
||||
},
|
||||
maximum: self.maximum.max(other.maximum).max(cross),
|
||||
all_run: self.all_run && other.all_run,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Summary {
|
||||
atom_count: usize,
|
||||
any_owner: bool,
|
||||
any_content: bool,
|
||||
noncontent: bool,
|
||||
whitespace: bool,
|
||||
first_properties: Option<AtomProperties>,
|
||||
runs: RunSummary,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
fn atom(atom: &Atom) -> Self {
|
||||
let properties = atom.properties();
|
||||
Self {
|
||||
atom_count: 1,
|
||||
any_owner: properties.owner.is_some() || !properties.owners.is_empty(),
|
||||
any_content: properties.content.is_some(),
|
||||
noncontent: !properties.style_ids.is_empty()
|
||||
|| !properties.roles.is_empty()
|
||||
|| !properties.property_template_ids.is_empty(),
|
||||
whitespace: match atom {
|
||||
Atom::Text { text, .. } => text.trim().is_empty(),
|
||||
Atom::Space { .. } => true,
|
||||
},
|
||||
first_properties: Some(properties.clone()),
|
||||
runs: RunSummary::atom(atom),
|
||||
}
|
||||
}
|
||||
|
||||
fn combine(self, other: Self) -> Self {
|
||||
Self {
|
||||
atom_count: self
|
||||
.atom_count
|
||||
.checked_add(other.atom_count)
|
||||
.expect("validated native atom count fits usize"),
|
||||
any_owner: self.any_owner || other.any_owner,
|
||||
any_content: self.any_content || other.any_content,
|
||||
noncontent: self.noncontent || other.noncontent,
|
||||
whitespace: self.whitespace && other.whitespace,
|
||||
first_properties: self.first_properties.or(other.first_properties),
|
||||
runs: self.runs.combine(other.runs),
|
||||
}
|
||||
}
|
||||
|
||||
fn atoms(atoms: &[Atom]) -> Self {
|
||||
let first = atoms.first().expect("nonempty native atom block");
|
||||
let mut summary = Self {
|
||||
atom_count: 0,
|
||||
any_owner: false,
|
||||
any_content: false,
|
||||
noncontent: false,
|
||||
whitespace: true,
|
||||
first_properties: Some(first.properties().clone()),
|
||||
runs: RunSummary {
|
||||
all_run: true,
|
||||
..RunSummary::default()
|
||||
},
|
||||
};
|
||||
for atom in atoms {
|
||||
let properties = atom.properties();
|
||||
summary.atom_count = summary
|
||||
.atom_count
|
||||
.checked_add(1)
|
||||
.expect("validated native atom count fits usize");
|
||||
summary.any_owner |= properties.owner.is_some() || !properties.owners.is_empty();
|
||||
summary.any_content |= properties.content.is_some();
|
||||
summary.noncontent |= !properties.style_ids.is_empty()
|
||||
|| !properties.roles.is_empty()
|
||||
|| !properties.property_template_ids.is_empty();
|
||||
summary.whitespace &= match atom {
|
||||
Atom::Text { text, .. } => text.trim().is_empty(),
|
||||
Atom::Space { .. } => true,
|
||||
};
|
||||
summary.runs = summary.runs.combine(RunSummary::atom(atom));
|
||||
}
|
||||
summary
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Decoration {
|
||||
OwnContent {
|
||||
region: i64,
|
||||
index: i64,
|
||||
had_owner: bool,
|
||||
had_content: bool,
|
||||
},
|
||||
Style(u32),
|
||||
Template(u32),
|
||||
Role(RegionRole, i64),
|
||||
ScrollWindow(i64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Part {
|
||||
Atoms(Box<[Atom]>),
|
||||
Decorated {
|
||||
input: Arc<AtomPlan>,
|
||||
decorations: Box<[Decoration]>,
|
||||
},
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct AtomPlan {
|
||||
parts: Sequence<Part>,
|
||||
summary: Summary,
|
||||
}
|
||||
|
||||
impl Default for AtomPlan {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
parts: Sequence::empty(),
|
||||
summary: Summary {
|
||||
whitespace: true,
|
||||
..Summary::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AtomPlan {
|
||||
pub(super) fn from_atoms(atoms: &[Atom]) -> Self {
|
||||
record_atom_clones(atoms.len());
|
||||
Self::from_owned_atoms(atoms.to_vec())
|
||||
}
|
||||
|
||||
pub(super) fn from_owned_atoms(atoms: Vec<Atom>) -> Self {
|
||||
if atoms.is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
let summary = Summary::atoms(&atoms);
|
||||
let mut sequence_work = Work::default();
|
||||
let mut entries = Vec::with_capacity(atoms.len().div_ceil(16));
|
||||
let mut atoms = atoms.into_iter();
|
||||
loop {
|
||||
let chunk = atoms.by_ref().take(16).collect::<Vec<_>>();
|
||||
if chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
let measure = atoms_measure(&chunk);
|
||||
entries.push(
|
||||
Entry::new(Arc::new(Part::Atoms(chunk.into_boxed_slice())), measure)
|
||||
.expect("validated native atom measure is nonnegative"),
|
||||
);
|
||||
}
|
||||
let parts = Sequence::from_entries(entries, &mut sequence_work)
|
||||
.expect("validated native atom sequence aggregates fit limits");
|
||||
record_sequence(sequence_work);
|
||||
Self { parts, summary }
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.parts.is_empty()
|
||||
}
|
||||
|
||||
pub(super) fn append(&self, other: &Self) -> Self {
|
||||
if self.is_empty() {
|
||||
return other.clone();
|
||||
}
|
||||
if other.is_empty() {
|
||||
return self.clone();
|
||||
}
|
||||
let mut sequence_work = Work::default();
|
||||
let parts = self
|
||||
.parts
|
||||
.concat(&other.parts, &mut sequence_work)
|
||||
.expect("validated native atom sequence aggregates fit limits");
|
||||
record_sequence(sequence_work);
|
||||
Self {
|
||||
parts,
|
||||
summary: self.summary.clone().combine(other.summary.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push(&self, atom: Atom) -> Self {
|
||||
self.append(&Self::from_single(atom))
|
||||
}
|
||||
|
||||
pub(super) fn prepend(&self, atom: Atom) -> Self {
|
||||
Self::from_single(atom).append(self)
|
||||
}
|
||||
|
||||
pub(super) fn to_vec(&self) -> Vec<Atom> {
|
||||
let mut output = Vec::with_capacity(self.summary.atom_count);
|
||||
if let Some(root) = self.parts.root() {
|
||||
materialize_node(root, &mut output);
|
||||
}
|
||||
record_materialized(output.len());
|
||||
output
|
||||
}
|
||||
|
||||
pub(super) fn first(&self) -> Option<Atom> {
|
||||
first_node(self.parts.root()?)
|
||||
}
|
||||
|
||||
pub(super) fn whitespace_only(&self) -> bool {
|
||||
self.summary.whitespace
|
||||
}
|
||||
|
||||
pub(super) fn has_noncontent_properties(&self) -> bool {
|
||||
self.summary.noncontent
|
||||
}
|
||||
|
||||
pub(super) fn own_content(&self, region: i64, index: i64) -> Self {
|
||||
if self.is_empty() {
|
||||
return self.clone();
|
||||
}
|
||||
self.decorate(Decoration::OwnContent {
|
||||
region,
|
||||
index,
|
||||
had_owner: self.summary.any_owner,
|
||||
had_content: self.summary.any_content,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn apply_style(&self, style: u32) -> Self {
|
||||
self.decorate(Decoration::Style(style))
|
||||
}
|
||||
|
||||
pub(super) fn apply_template(&self, template: u32) -> Self {
|
||||
self.decorate(Decoration::Template(template))
|
||||
}
|
||||
|
||||
pub(super) fn apply_role(&self, role: RegionRole, region: i64) -> Self {
|
||||
self.decorate(Decoration::Role(role, region))
|
||||
}
|
||||
|
||||
pub(super) fn apply_scroll_window(&self, region: i64) -> Self {
|
||||
self.decorate(Decoration::ScrollWindow(region))
|
||||
}
|
||||
|
||||
pub(super) fn min_content_width(&self) -> i64 {
|
||||
self.summary.runs.maximum
|
||||
}
|
||||
|
||||
pub(super) fn width(&self) -> i64 {
|
||||
self.parts.measure().width_sum
|
||||
}
|
||||
|
||||
fn from_single(atom: Atom) -> Self {
|
||||
let summary = Summary::atom(&atom);
|
||||
let measure = atom_measure(&atom);
|
||||
let mut sequence_work = Work::default();
|
||||
let parts = Sequence::from_entries(
|
||||
[Entry::new(
|
||||
Arc::new(Part::Atoms(vec![atom].into_boxed_slice())),
|
||||
measure,
|
||||
)
|
||||
.expect("validated native atom measure is nonnegative")],
|
||||
&mut sequence_work,
|
||||
)
|
||||
.expect("single native atom sequence fits limits");
|
||||
record_sequence(sequence_work);
|
||||
Self { parts, summary }
|
||||
}
|
||||
|
||||
fn decorate(&self, decoration: Decoration) -> Self {
|
||||
if self.is_empty() {
|
||||
return self.clone();
|
||||
}
|
||||
let mut summary = self.summary.clone();
|
||||
if let Some(properties) = &mut summary.first_properties {
|
||||
apply_decoration(properties, &decoration);
|
||||
}
|
||||
match &decoration {
|
||||
Decoration::OwnContent { .. } => {
|
||||
summary.any_owner = true;
|
||||
summary.any_content = true;
|
||||
}
|
||||
Decoration::Style(_) | Decoration::Template(_) | Decoration::Role(_, _) => {
|
||||
summary.noncontent = true;
|
||||
}
|
||||
Decoration::ScrollWindow(_) => {}
|
||||
}
|
||||
let (input, decorations) = self
|
||||
.parts
|
||||
.get(0)
|
||||
.filter(|_| self.parts.len() == 1)
|
||||
.and_then(|entry| match entry.value().as_ref() {
|
||||
Part::Decorated { input, decorations } if decorations.len() < 16 => {
|
||||
let mut combined = decorations.to_vec();
|
||||
combined.push(decoration.clone());
|
||||
Some((input.clone(), combined.into_boxed_slice()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| (Arc::new(self.clone()), vec![decoration].into_boxed_slice()));
|
||||
record_operation_block_writes(decorations.len());
|
||||
let measure = self.parts.measure();
|
||||
let mut sequence_work = Work::default();
|
||||
let parts = Sequence::from_entries(
|
||||
[
|
||||
Entry::new(Arc::new(Part::Decorated { input, decorations }), measure)
|
||||
.expect("validated native atom plan measure is nonnegative"),
|
||||
],
|
||||
&mut sequence_work,
|
||||
)
|
||||
.expect("decorated native atom sequence fits limits");
|
||||
record_sequence(sequence_work);
|
||||
Self { parts, summary }
|
||||
}
|
||||
}
|
||||
|
||||
fn atom_measure(atom: &Atom) -> Measure {
|
||||
let chars = match atom {
|
||||
Atom::Text { text, .. } => text.chars().count() as u64,
|
||||
Atom::Space { .. } => 1,
|
||||
};
|
||||
Measure::new(chars, 0, atom.width(), atom.width())
|
||||
.expect("validated native atom width is nonnegative")
|
||||
}
|
||||
|
||||
fn atoms_measure(atoms: &[Atom]) -> Measure {
|
||||
atoms.iter().fold(Measure::default(), |measure, atom| {
|
||||
measure
|
||||
.checked_combine(atom_measure(atom))
|
||||
.expect("validated native atom measures fit limits")
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_decoration(properties: &mut AtomProperties, decoration: &Decoration) {
|
||||
record_property_application();
|
||||
match decoration {
|
||||
Decoration::OwnContent {
|
||||
region,
|
||||
index,
|
||||
had_owner,
|
||||
had_content,
|
||||
} => {
|
||||
if *had_owner {
|
||||
let mut owners = properties.owners.clone();
|
||||
if let Some(owner) = properties.owner {
|
||||
if !owners.contains(&owner) {
|
||||
owners.push(owner);
|
||||
}
|
||||
}
|
||||
if !owners.contains(region) {
|
||||
owners.push(*region);
|
||||
}
|
||||
properties.owner = Some(*region);
|
||||
properties.owners = owners;
|
||||
} else {
|
||||
properties.owner = Some(*region);
|
||||
properties.owners = vec![*region];
|
||||
}
|
||||
if !had_content {
|
||||
properties.content = Some(*region);
|
||||
properties.content_idx = Some(*index);
|
||||
}
|
||||
}
|
||||
Decoration::Style(style) => properties.style_ids.push(*style),
|
||||
Decoration::Template(template) => properties.property_template_ids.push(*template),
|
||||
Decoration::Role(role, region) => properties.roles.push(RegionRoleEntry {
|
||||
role: *role,
|
||||
region_id: *region,
|
||||
}),
|
||||
Decoration::ScrollWindow(region) => properties.scroll_window = Some(*region),
|
||||
}
|
||||
}
|
||||
|
||||
fn decorate_atom(mut atom: Atom, decoration: &Decoration) -> Atom {
|
||||
apply_decoration(atom.properties_mut(), decoration);
|
||||
atom
|
||||
}
|
||||
|
||||
fn materialize_node(node: &Arc<Node<Part>>, output: &mut Vec<Atom>) {
|
||||
match node.view() {
|
||||
NodeView::Leaf { entries, .. } => {
|
||||
for entry in entries {
|
||||
match entry.value().as_ref() {
|
||||
Part::Atoms(atoms) => output.extend(atoms.iter().cloned()),
|
||||
Part::Decorated { input, decorations } => {
|
||||
let start = output.len();
|
||||
if let Some(root) = input.parts.root() {
|
||||
materialize_node(root, output);
|
||||
}
|
||||
for atom in &mut output[start..] {
|
||||
for decoration in decorations {
|
||||
apply_decoration(atom.properties_mut(), decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeView::Branch { left, right, .. } => {
|
||||
materialize_node(left, output);
|
||||
materialize_node(right, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first_node(node: &Arc<Node<Part>>) -> Option<Atom> {
|
||||
match node.view() {
|
||||
NodeView::Leaf { entries, .. } => match entries.first()?.value().as_ref() {
|
||||
Part::Atoms(atoms) => {
|
||||
record_materialized(1);
|
||||
Some(atoms.first()?.clone())
|
||||
}
|
||||
Part::Decorated { input, decorations } => input
|
||||
.first()
|
||||
.map(|atom| decorations.iter().fold(atom, decorate_atom)),
|
||||
},
|
||||
NodeView::Branch { left, .. } => first_node(left),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn text(value: &str, width: i64, cjk: bool, space: bool) -> Atom {
|
||||
Atom::Text {
|
||||
text: value.to_owned(),
|
||||
width,
|
||||
cjk,
|
||||
space,
|
||||
properties: AtomProperties::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoration_is_lazy_ordered_and_new_padding_is_not_retroactive() {
|
||||
reset_work();
|
||||
let content = AtomPlan::from_atoms(&[text("x", 1, false, false)])
|
||||
.apply_style(3)
|
||||
.own_content(7, 2);
|
||||
let plan = content.prepend(Atom::Space {
|
||||
width: 4,
|
||||
properties: AtomProperties::default(),
|
||||
});
|
||||
assert_eq!(work().materialized_atoms, 0);
|
||||
let atoms = plan.to_vec();
|
||||
assert!(atoms[0].properties().style_ids.is_empty());
|
||||
assert_eq!(atoms[1].properties().style_ids, vec![3]);
|
||||
assert_eq!(atoms[1].properties().content, Some(7));
|
||||
assert_eq!(atoms[1].properties().content_idx, Some(2));
|
||||
assert_eq!(work().materialized_atoms, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_content_uses_whole_line_owner_and_content_branches() {
|
||||
let mut owned = text("a", 1, false, false);
|
||||
owned.properties_mut().owner = Some(5);
|
||||
owned.properties_mut().owners = vec![5];
|
||||
owned.properties_mut().content = Some(4);
|
||||
let plan = AtomPlan::from_atoms(&[owned, text("b", 1, false, false)]).own_content(9, 3);
|
||||
let atoms = plan.to_vec();
|
||||
assert_eq!(atoms[0].properties().content, Some(4));
|
||||
assert_eq!(atoms[1].properties().content, None);
|
||||
assert_eq!(atoms[0].properties().owner, Some(9));
|
||||
assert_eq!(atoms[0].properties().owners, vec![5, 9]);
|
||||
assert_eq!(atoms[1].properties().owner, Some(9));
|
||||
assert_eq!(atoms[1].properties().owners, vec![9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_content_summary_matches_space_and_cjk_breaks() {
|
||||
let plan = AtomPlan::from_atoms(&[
|
||||
text("ab", 2, false, false),
|
||||
text(" ", 1, false, true),
|
||||
text("界", 4, true, false),
|
||||
text("cd", 3, false, false),
|
||||
]);
|
||||
assert_eq!(plan.min_content_width(), 4);
|
||||
assert!(!plan.whitespace_only());
|
||||
assert!(!plan.has_noncontent_properties());
|
||||
assert!(!plan.is_empty());
|
||||
assert!(AtomPlan::default().first().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_shares_parts_and_materializes_once() {
|
||||
reset_work();
|
||||
let left = AtomPlan::from_atoms(&[text("a", 1, false, false)]);
|
||||
let right = AtomPlan::from_owned_atoms(vec![text("b", 2, false, false)]);
|
||||
let plan = left.append(&right).apply_template(2).apply_scroll_window(8);
|
||||
assert_eq!(plan.width(), 3);
|
||||
assert_eq!(work().materialized_atoms, 0);
|
||||
let atoms = plan.to_vec();
|
||||
assert_eq!(atoms.len(), 2);
|
||||
assert_eq!(atoms[0].properties().property_template_ids, vec![2]);
|
||||
assert_eq!(atoms[1].properties().scroll_window, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_append_and_decorations_do_no_atom_work_until_materialized() {
|
||||
for size in [32, 128, 512, 8192] {
|
||||
let atoms = (0..size)
|
||||
.map(|_| text("x", 1, false, false))
|
||||
.collect::<Vec<_>>();
|
||||
let plan = AtomPlan::from_atoms(&atoms);
|
||||
reset_work();
|
||||
let combined = plan
|
||||
.append(&plan)
|
||||
.own_content(7, 0)
|
||||
.apply_style(1)
|
||||
.apply_template(2)
|
||||
.apply_role(RegionRole::PaddingLeft, 7)
|
||||
.apply_scroll_window(7);
|
||||
let lazy = work();
|
||||
assert_eq!(lazy.materialized_atoms, 0);
|
||||
assert_eq!(lazy.sequence_nodes_created, 6);
|
||||
assert!(lazy.sequence_entries_written <= 21, "{lazy:?}");
|
||||
assert_eq!(lazy.property_applications, 5);
|
||||
assert_eq!(lazy.atom_clones, 0);
|
||||
assert_eq!(lazy.operation_block_writes, 15);
|
||||
|
||||
let materialized = combined.to_vec();
|
||||
assert_eq!(materialized.len(), size * 2);
|
||||
let eager = work();
|
||||
assert_eq!(eager.materialized_atoms, (size * 2) as u64);
|
||||
assert_eq!(eager.property_applications, 5 + (size * 2 * 5) as u64);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_content_randomized_summary_matches_vec_oracle() {
|
||||
fn oracle(atoms: &[Atom]) -> i64 {
|
||||
let mut maximum = 0;
|
||||
let mut run = 0_i64;
|
||||
for atom in atoms {
|
||||
if atom.wrap_space() {
|
||||
maximum = maximum.max(run);
|
||||
run = 0;
|
||||
} else if atom.wrap_cjk() {
|
||||
maximum = maximum.max(run).max(atom.width());
|
||||
run = 0;
|
||||
} else {
|
||||
run += atom.width();
|
||||
}
|
||||
}
|
||||
maximum.max(run)
|
||||
}
|
||||
|
||||
let mut seed = 0x5e91_7ac3_u64;
|
||||
for size in 0..257 {
|
||||
let mut atoms = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let width = ((seed >> 32) % 8 + 1) as i64;
|
||||
atoms.push(match seed % 5 {
|
||||
0 => text(" ", width, false, true),
|
||||
1 => text("界", width, true, false),
|
||||
_ => text("x", width, false, false),
|
||||
});
|
||||
}
|
||||
let split = size / 3;
|
||||
let plan = AtomPlan::default()
|
||||
.append(&AtomPlan::from_atoms(&atoms[..split]))
|
||||
.append(&AtomPlan::default())
|
||||
.append(&AtomPlan::from_atoms(&atoms[split..]));
|
||||
assert_eq!(plan.min_content_width(), oracle(&atoms));
|
||||
assert_eq!(plan.is_empty(), atoms.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_counts_one_atom_and_each_lazy_property_application() {
|
||||
let plan = AtomPlan::from_atoms(&[text("a", 1, false, false), text("b", 1, false, false)])
|
||||
.apply_style(3)
|
||||
.apply_template(4);
|
||||
reset_work();
|
||||
let first = plan.first().unwrap();
|
||||
assert_eq!(first.properties().style_ids, vec![3]);
|
||||
assert_eq!(first.properties().property_template_ids, vec![4]);
|
||||
assert_eq!(work().materialized_atoms, 1);
|
||||
assert_eq!(work().property_applications, 2);
|
||||
assert_eq!(work().atom_clones, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoration_blocks_are_bounded_ordered_and_keep_old_fork() {
|
||||
let base = AtomPlan::from_owned_atoms(vec![text("x", 1, false, false)]);
|
||||
let fork = base.clone();
|
||||
let old_root = Arc::clone(base.parts.root().unwrap());
|
||||
reset_work();
|
||||
let decorated = (0..33).fold(base, |plan, style| plan.apply_style(style));
|
||||
assert!(Arc::ptr_eq(fork.parts.root().unwrap(), &old_root));
|
||||
assert_eq!(work().materialized_atoms, 0);
|
||||
assert_eq!(work().operation_block_writes, 273);
|
||||
|
||||
let mut block_lengths = Vec::new();
|
||||
let mut current = &decorated;
|
||||
loop {
|
||||
let part = current.parts.get(0).unwrap().value().as_ref();
|
||||
match part {
|
||||
Part::Decorated { input, decorations } => {
|
||||
block_lengths.push(decorations.len());
|
||||
current = input;
|
||||
}
|
||||
Part::Atoms(_) => break,
|
||||
}
|
||||
}
|
||||
assert_eq!(block_lengths, vec![1, 16, 16]);
|
||||
assert_eq!(
|
||||
decorated.to_vec()[0].properties().style_ids,
|
||||
(0..33).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(fork.to_vec()[0].properties().style_ids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_owned_bootstrap_writes_one_sequence_entry_per_atom_block() {
|
||||
let atoms = (0..8192)
|
||||
.map(|_| text("x", 1, false, false))
|
||||
.collect::<Vec<_>>();
|
||||
reset_work();
|
||||
let plan = AtomPlan::from_owned_atoms(atoms);
|
||||
let bootstrap = work();
|
||||
assert_eq!(bootstrap.atom_clones, 0);
|
||||
assert_eq!(bootstrap.sequence_entries_written, 512);
|
||||
assert_eq!(bootstrap.sequence_nodes_created, 63);
|
||||
assert_eq!(plan.width(), 8192);
|
||||
assert_eq!(plan.summary.atom_count, 8192);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
mod layout;
|
||||
pub mod sequence;
|
||||
|
||||
use layout::{
|
||||
encode_error_tape, DocumentDelta, LayoutContext, LayoutDocument, LayoutTape, RetainedDocument,
|
||||
@ -468,6 +469,7 @@ struct RenderedJob {
|
||||
base_renders: u64,
|
||||
target_renders: u64,
|
||||
resolver_lookups: u64,
|
||||
atom_plan_work: layout::AtomPlanWork,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -520,6 +522,7 @@ struct RuntimeState {
|
||||
document_delta_entries_validated: u64,
|
||||
document_trie_path_nodes_copied: u64,
|
||||
document_resolver_lookups: u64,
|
||||
atom_plan_work: layout::AtomPlanWork,
|
||||
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
||||
}
|
||||
|
||||
@ -580,6 +583,7 @@ struct SessionStats {
|
||||
document_delta_entries_validated: u64,
|
||||
document_trie_path_nodes_copied: u64,
|
||||
document_resolver_lookups: u64,
|
||||
atom_plan_work: layout::AtomPlanWork,
|
||||
pending_baselines: usize,
|
||||
confirmed_baseline: bool,
|
||||
confirmed_baseline_bytes: usize,
|
||||
@ -652,6 +656,7 @@ impl Session {
|
||||
document_delta_entries_validated: 0,
|
||||
document_trie_path_nodes_copied: 0,
|
||||
document_resolver_lookups: 0,
|
||||
atom_plan_work: layout::AtomPlanWork::default(),
|
||||
confirmed_baseline,
|
||||
}),
|
||||
readiness_channel: Mutex::new(None),
|
||||
@ -1077,6 +1082,7 @@ impl Session {
|
||||
state.base_renders += output.base_renders;
|
||||
state.target_renders += output.target_renders;
|
||||
state.document_resolver_lookups += output.resolver_lookups;
|
||||
state.atom_plan_work.accumulate(output.atom_plan_work);
|
||||
state.document_parses += input_stats.parses;
|
||||
state.document_validations += input_stats.validations;
|
||||
state.document_reuses += input_stats.reuses;
|
||||
@ -1225,6 +1231,7 @@ impl Session {
|
||||
document_delta_entries_validated: state.document_delta_entries_validated,
|
||||
document_trie_path_nodes_copied: state.document_trie_path_nodes_copied,
|
||||
document_resolver_lookups: state.document_resolver_lookups,
|
||||
atom_plan_work: state.atom_plan_work,
|
||||
pending_baselines,
|
||||
confirmed_baseline: state.confirmed_baseline.is_some(),
|
||||
confirmed_baseline_bytes,
|
||||
@ -1437,6 +1444,7 @@ fn render_layout_payload(
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
resolver_lookups: 0,
|
||||
atom_plan_work: layout::AtomPlanWork::default(),
|
||||
},
|
||||
JobPayload::Layout {
|
||||
document,
|
||||
@ -1494,6 +1502,7 @@ fn render_layout_payload(
|
||||
String,
|
||||
>;
|
||||
layout::reset_resolver_lookups();
|
||||
layout::reset_atom_plan_work();
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
|
||||
let target_styles = document.styles()?;
|
||||
if let Some(base_context) = base_context {
|
||||
@ -1568,6 +1577,7 @@ fn render_layout_payload(
|
||||
}));
|
||||
let resolver_lookups =
|
||||
validation_resolver_lookups.saturating_add(layout::resolver_lookups());
|
||||
let atom_plan_work = layout::atom_plan_work();
|
||||
match result {
|
||||
Ok(Ok((bytes, tape, styles, baseline_hit, base_renders, target_renders))) => {
|
||||
RenderedJob {
|
||||
@ -1583,6 +1593,7 @@ fn render_layout_payload(
|
||||
base_renders,
|
||||
target_renders,
|
||||
resolver_lookups,
|
||||
atom_plan_work,
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => RenderedJob {
|
||||
@ -1592,6 +1603,7 @@ fn render_layout_payload(
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
resolver_lookups,
|
||||
atom_plan_work,
|
||||
},
|
||||
Err(_) => RenderedJob {
|
||||
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
|
||||
@ -1600,6 +1612,7 @@ fn render_layout_payload(
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
resolver_lookups,
|
||||
atom_plan_work,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1749,6 +1762,7 @@ fn worker_loop(shared: Arc<Shared>) {
|
||||
state.base_renders += output.base_renders;
|
||||
state.target_renders += output.target_renders;
|
||||
state.document_resolver_lookups += output.resolver_lookups;
|
||||
state.atom_plan_work.accumulate(output.atom_plan_work);
|
||||
state.results.insert(
|
||||
(job.generation, job.key),
|
||||
ResultEntry {
|
||||
@ -2573,9 +2587,9 @@ mod tests {
|
||||
assert_eq!(stats.document_delta_entries_validated, 1);
|
||||
assert_eq!(stats.document_trie_path_nodes_copied, 17);
|
||||
assert_eq!(stats.document_delta_input_bytes, delta.len() as u64);
|
||||
assert!(
|
||||
stats.document_resolver_lookups >= 3,
|
||||
"target/base validation and target render lookups must all remain visible"
|
||||
assert_eq!(
|
||||
stats.document_resolver_lookups, stats.target_renders,
|
||||
"each single-owner render resolves its root; retained context validation resolves no nodes"
|
||||
);
|
||||
assert!(session.confirm(2, 2, 2).unwrap());
|
||||
|
||||
|
||||
751
native/src/sequence.rs
Normal file
751
native/src/sequence.rs
Normal file
@ -0,0 +1,751 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
const LEAF_CAPACITY: usize = 16;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Measure {
|
||||
pub chars: u64,
|
||||
pub lines: u64,
|
||||
pub width_sum: i64,
|
||||
pub width_max: i64,
|
||||
}
|
||||
|
||||
impl Measure {
|
||||
pub fn new(chars: u64, lines: u64, width_sum: i64, width_max: i64) -> Result<Self, Error> {
|
||||
if width_sum < 0 || width_max < 0 {
|
||||
return Err(Error::NegativeWidth);
|
||||
}
|
||||
Ok(Self {
|
||||
chars,
|
||||
lines,
|
||||
width_sum,
|
||||
width_max,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn checked_combine(self, other: Self) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
chars: self.chars.checked_add(other.chars).ok_or(Error::Overflow)?,
|
||||
lines: self.lines.checked_add(other.lines).ok_or(Error::Overflow)?,
|
||||
width_sum: self
|
||||
.width_sum
|
||||
.checked_add(other.width_sum)
|
||||
.ok_or(Error::Overflow)?,
|
||||
width_max: self.width_max.max(other.width_max),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Entry<T> {
|
||||
value: Arc<T>,
|
||||
measure: Measure,
|
||||
}
|
||||
|
||||
impl<T> Clone for Entry<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
value: Arc::clone(&self.value),
|
||||
measure: self.measure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Entry<T> {
|
||||
pub fn new(value: Arc<T>, measure: Measure) -> Result<Self, Error> {
|
||||
if measure.width_sum < 0 || measure.width_max < 0 {
|
||||
return Err(Error::NegativeWidth);
|
||||
}
|
||||
Ok(Self { value, measure })
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &Arc<T> {
|
||||
&self.value
|
||||
}
|
||||
|
||||
pub fn measure(&self) -> Measure {
|
||||
self.measure
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Work {
|
||||
pub nodes_created: u64,
|
||||
pub entries_written: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
IndexOutOfBounds,
|
||||
NegativeWidth,
|
||||
Overflow,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Node<T> {
|
||||
Leaf {
|
||||
entries: Box<[Entry<T>]>,
|
||||
measure: Measure,
|
||||
},
|
||||
Branch {
|
||||
left: Arc<Node<T>>,
|
||||
right: Arc<Node<T>>,
|
||||
len: usize,
|
||||
height: u32,
|
||||
measure: Measure,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum NodeView<'a, T> {
|
||||
Leaf {
|
||||
entries: &'a [Entry<T>],
|
||||
measure: Measure,
|
||||
},
|
||||
Branch {
|
||||
left: &'a Arc<Node<T>>,
|
||||
right: &'a Arc<Node<T>>,
|
||||
len: usize,
|
||||
height: u32,
|
||||
measure: Measure,
|
||||
},
|
||||
}
|
||||
|
||||
type OptionalRoot<T> = Option<Arc<Node<T>>>;
|
||||
type SplitRoots<T> = (OptionalRoot<T>, OptionalRoot<T>);
|
||||
|
||||
impl<T> Node<T> {
|
||||
pub fn view(&self) -> NodeView<'_, T> {
|
||||
match self {
|
||||
Self::Leaf { entries, measure } => NodeView::Leaf {
|
||||
entries,
|
||||
measure: *measure,
|
||||
},
|
||||
Self::Branch {
|
||||
left,
|
||||
right,
|
||||
len,
|
||||
height,
|
||||
measure,
|
||||
} => NodeView::Branch {
|
||||
left,
|
||||
right,
|
||||
len: *len,
|
||||
height: *height,
|
||||
measure: *measure,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::Leaf { entries, .. } => entries.len(),
|
||||
Self::Branch { len, .. } => *len,
|
||||
}
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
match self {
|
||||
Self::Leaf { .. } => 1,
|
||||
Self::Branch { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
|
||||
fn measure(&self) -> Measure {
|
||||
match self {
|
||||
Self::Leaf { measure, .. } | Self::Branch { measure, .. } => *measure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sequence<T> {
|
||||
root: Option<Arc<Node<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Sequence<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
root: self.root.as_ref().map(Arc::clone),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Sequence<T> {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sequence<T> {
|
||||
pub fn empty() -> Self {
|
||||
Self { root: None }
|
||||
}
|
||||
|
||||
pub fn from_entries<I>(entries: I, work: &mut Work) -> Result<Self, Error>
|
||||
where
|
||||
I: IntoIterator<Item = Entry<T>>,
|
||||
{
|
||||
let entries = entries.into_iter().collect::<Vec<_>>();
|
||||
let mut leaves = Vec::with_capacity(entries.len().div_ceil(LEAF_CAPACITY));
|
||||
for chunk in entries.chunks(LEAF_CAPACITY) {
|
||||
leaves.push(make_leaf(chunk.to_vec(), work)?);
|
||||
}
|
||||
fn build<T>(leaves: &[Arc<Node<T>>], work: &mut Work) -> Result<Arc<Node<T>>, Error> {
|
||||
if leaves.len() == 1 {
|
||||
return Ok(Arc::clone(&leaves[0]));
|
||||
}
|
||||
let middle = leaves.len() / 2;
|
||||
make_branch(
|
||||
build(&leaves[..middle], work)?,
|
||||
build(&leaves[middle..], work)?,
|
||||
work,
|
||||
)
|
||||
}
|
||||
Ok(Self {
|
||||
root: if leaves.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(build(&leaves, work)?)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.root.as_ref().map_or(0, |root| root.len())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.root.is_none()
|
||||
}
|
||||
|
||||
pub fn measure(&self) -> Measure {
|
||||
self.root
|
||||
.as_ref()
|
||||
.map_or(Measure::default(), |root| root.measure())
|
||||
}
|
||||
|
||||
pub fn root(&self) -> Option<&Arc<Node<T>>> {
|
||||
self.root.as_ref()
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<&Entry<T>> {
|
||||
fn get_at<T>(node: &Node<T>, index: usize) -> Option<&Entry<T>> {
|
||||
match node {
|
||||
Node::Leaf { entries, .. } => entries.get(index),
|
||||
Node::Branch { left, right, .. } => {
|
||||
if index < left.len() {
|
||||
get_at(left, index)
|
||||
} else {
|
||||
get_at(right, index - left.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(index < self.len()).then(|| get_at(self.root.as_deref()?, index))?
|
||||
}
|
||||
|
||||
pub fn prefix_measure(&self, end: usize) -> Result<Measure, Error> {
|
||||
fn prefix<T>(node: &Node<T>, end: usize) -> Result<Measure, Error> {
|
||||
match node {
|
||||
Node::Leaf { entries, .. } => entries[..end]
|
||||
.iter()
|
||||
.try_fold(Measure::default(), |sum, entry| {
|
||||
sum.checked_combine(entry.measure)
|
||||
}),
|
||||
Node::Branch { left, right, .. } => {
|
||||
if end <= left.len() {
|
||||
prefix(left, end)
|
||||
} else {
|
||||
left.measure()
|
||||
.checked_combine(prefix(right, end - left.len())?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if end > self.len() {
|
||||
return Err(Error::IndexOutOfBounds);
|
||||
}
|
||||
self.root
|
||||
.as_deref()
|
||||
.map_or(Ok(Measure::default()), |root| prefix(root, end))
|
||||
}
|
||||
|
||||
pub fn split_at(&self, index: usize, work: &mut Work) -> Result<(Self, Self), Error> {
|
||||
if index > self.len() {
|
||||
return Err(Error::IndexOutOfBounds);
|
||||
}
|
||||
let (left, right) = split_node(self.root.as_ref(), index, work)?;
|
||||
Ok((Self { root: left }, Self { root: right }))
|
||||
}
|
||||
|
||||
pub fn concat(&self, other: &Self, work: &mut Work) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
root: join(self.root.clone(), other.root.clone(), work)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace(&self, index: usize, entry: Entry<T>, work: &mut Work) -> Result<Self, Error> {
|
||||
if index >= self.len() {
|
||||
return Err(Error::IndexOutOfBounds);
|
||||
}
|
||||
Ok(Self {
|
||||
root: Some(replace_node(
|
||||
self.root.as_ref().expect("nonempty checked"),
|
||||
index,
|
||||
entry,
|
||||
work,
|
||||
)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn count_node(work: &mut Work) -> Result<(), Error> {
|
||||
work.nodes_created = work.nodes_created.checked_add(1).ok_or(Error::Overflow)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_leaf<T>(entries: Vec<Entry<T>>, work: &mut Work) -> Result<Arc<Node<T>>, Error> {
|
||||
debug_assert!(!entries.is_empty() && entries.len() <= LEAF_CAPACITY);
|
||||
let measure = entries.iter().try_fold(Measure::default(), |sum, entry| {
|
||||
sum.checked_combine(entry.measure)
|
||||
})?;
|
||||
let written = u64::try_from(entries.len()).map_err(|_| Error::Overflow)?;
|
||||
let next_written = work
|
||||
.entries_written
|
||||
.checked_add(written)
|
||||
.ok_or(Error::Overflow)?;
|
||||
count_node(work)?;
|
||||
work.entries_written = next_written;
|
||||
Ok(Arc::new(Node::Leaf {
|
||||
entries: entries.into_boxed_slice(),
|
||||
measure,
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_branch<T>(
|
||||
left: Arc<Node<T>>,
|
||||
right: Arc<Node<T>>,
|
||||
work: &mut Work,
|
||||
) -> Result<Arc<Node<T>>, Error> {
|
||||
let len = left.len().checked_add(right.len()).ok_or(Error::Overflow)?;
|
||||
let height = left
|
||||
.height()
|
||||
.max(right.height())
|
||||
.checked_add(1)
|
||||
.ok_or(Error::Overflow)?;
|
||||
let measure = left.measure().checked_combine(right.measure())?;
|
||||
count_node(work)?;
|
||||
Ok(Arc::new(Node::Branch {
|
||||
left,
|
||||
right,
|
||||
len,
|
||||
height,
|
||||
measure,
|
||||
}))
|
||||
}
|
||||
|
||||
fn join<T>(
|
||||
left: Option<Arc<Node<T>>>,
|
||||
right: Option<Arc<Node<T>>>,
|
||||
work: &mut Work,
|
||||
) -> Result<Option<Arc<Node<T>>>, Error> {
|
||||
let (left, right) = match (left, right) {
|
||||
(None, right) => return Ok(right),
|
||||
(left, None) => return Ok(left),
|
||||
(Some(left), Some(right)) => (left, right),
|
||||
};
|
||||
if let (Node::Leaf { entries: a, .. }, Node::Leaf { entries: b, .. }) =
|
||||
(left.as_ref(), right.as_ref())
|
||||
{
|
||||
if a.len() + b.len() <= LEAF_CAPACITY {
|
||||
let mut entries = Vec::with_capacity(a.len() + b.len());
|
||||
entries.extend_from_slice(a);
|
||||
entries.extend_from_slice(b);
|
||||
return make_leaf(entries, work).map(Some);
|
||||
}
|
||||
}
|
||||
if left.height() > right.height().saturating_add(1) {
|
||||
let Node::Branch {
|
||||
left: outer,
|
||||
right: inner,
|
||||
..
|
||||
} = left.as_ref()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let joined = join(Some(Arc::clone(inner)), Some(right), work)?.expect("nonempty join");
|
||||
return rebalance(Arc::clone(outer), joined, work).map(Some);
|
||||
}
|
||||
if right.height() > left.height().saturating_add(1) {
|
||||
let Node::Branch {
|
||||
left: inner,
|
||||
right: outer,
|
||||
..
|
||||
} = right.as_ref()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let joined = join(Some(left), Some(Arc::clone(inner)), work)?.expect("nonempty join");
|
||||
return rebalance(joined, Arc::clone(outer), work).map(Some);
|
||||
}
|
||||
make_branch(left, right, work).map(Some)
|
||||
}
|
||||
|
||||
fn rebalance<T>(
|
||||
left: Arc<Node<T>>,
|
||||
right: Arc<Node<T>>,
|
||||
work: &mut Work,
|
||||
) -> Result<Arc<Node<T>>, Error> {
|
||||
if left.height() > right.height().saturating_add(1) {
|
||||
let Node::Branch {
|
||||
left: ll,
|
||||
right: lr,
|
||||
..
|
||||
} = left.as_ref()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
if ll.height() >= lr.height() {
|
||||
return make_branch(
|
||||
Arc::clone(ll),
|
||||
make_branch(Arc::clone(lr), right, work)?,
|
||||
work,
|
||||
);
|
||||
}
|
||||
let Node::Branch {
|
||||
left: lrl,
|
||||
right: lrr,
|
||||
..
|
||||
} = lr.as_ref()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
return make_branch(
|
||||
make_branch(Arc::clone(ll), Arc::clone(lrl), work)?,
|
||||
make_branch(Arc::clone(lrr), right, work)?,
|
||||
work,
|
||||
);
|
||||
}
|
||||
if right.height() > left.height().saturating_add(1) {
|
||||
let Node::Branch {
|
||||
left: rl,
|
||||
right: rr,
|
||||
..
|
||||
} = right.as_ref()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
if rr.height() >= rl.height() {
|
||||
return make_branch(
|
||||
make_branch(left, Arc::clone(rl), work)?,
|
||||
Arc::clone(rr),
|
||||
work,
|
||||
);
|
||||
}
|
||||
let Node::Branch {
|
||||
left: rll,
|
||||
right: rlr,
|
||||
..
|
||||
} = rl.as_ref()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
return make_branch(
|
||||
make_branch(left, Arc::clone(rll), work)?,
|
||||
make_branch(Arc::clone(rlr), Arc::clone(rr), work)?,
|
||||
work,
|
||||
);
|
||||
}
|
||||
make_branch(left, right, work)
|
||||
}
|
||||
|
||||
fn split_node<T>(
|
||||
node: Option<&Arc<Node<T>>>,
|
||||
index: usize,
|
||||
work: &mut Work,
|
||||
) -> Result<SplitRoots<T>, Error> {
|
||||
let Some(node) = node else {
|
||||
return Ok((None, None));
|
||||
};
|
||||
if index == 0 {
|
||||
return Ok((None, Some(Arc::clone(node))));
|
||||
}
|
||||
if index == node.len() {
|
||||
return Ok((Some(Arc::clone(node)), None));
|
||||
}
|
||||
match node.as_ref() {
|
||||
Node::Leaf { entries, .. } => Ok((
|
||||
Some(make_leaf(entries[..index].to_vec(), work)?),
|
||||
Some(make_leaf(entries[index..].to_vec(), work)?),
|
||||
)),
|
||||
Node::Branch { left, right, .. } => {
|
||||
if index < left.len() {
|
||||
let (before, after) = split_node(Some(left), index, work)?;
|
||||
Ok((before, join(after, Some(Arc::clone(right)), work)?))
|
||||
} else if index == left.len() {
|
||||
Ok((Some(Arc::clone(left)), Some(Arc::clone(right))))
|
||||
} else {
|
||||
let (before, after) = split_node(Some(right), index - left.len(), work)?;
|
||||
Ok((join(Some(Arc::clone(left)), before, work)?, after))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_node<T>(
|
||||
node: &Arc<Node<T>>,
|
||||
index: usize,
|
||||
entry: Entry<T>,
|
||||
work: &mut Work,
|
||||
) -> Result<Arc<Node<T>>, Error> {
|
||||
match node.as_ref() {
|
||||
Node::Leaf { entries, .. } => {
|
||||
let mut entries = entries.to_vec();
|
||||
entries[index] = entry;
|
||||
make_leaf(entries, work)
|
||||
}
|
||||
Node::Branch { left, right, .. } => {
|
||||
if index < left.len() {
|
||||
make_branch(
|
||||
replace_node(left, index, entry, work)?,
|
||||
Arc::clone(right),
|
||||
work,
|
||||
)
|
||||
} else {
|
||||
make_branch(
|
||||
Arc::clone(left),
|
||||
replace_node(right, index - left.len(), entry, work)?,
|
||||
work,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(value: usize) -> Entry<String> {
|
||||
let text = if value % 5 == 0 {
|
||||
format!("界{value}")
|
||||
} else {
|
||||
value.to_string()
|
||||
};
|
||||
Entry::new(
|
||||
Arc::new(text.clone()),
|
||||
Measure::new(text.chars().count() as u64, 1, value as i64, value as i64).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn values(sequence: &Sequence<String>) -> Vec<String> {
|
||||
(0..sequence.len())
|
||||
.map(|index| sequence.get(index).unwrap().value().as_ref().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn height<T>(sequence: &Sequence<T>) -> u32 {
|
||||
sequence.root().map_or(0, |root| root.height())
|
||||
}
|
||||
|
||||
fn shared_nodes<T>(left: &Arc<Node<T>>, right: &Arc<Node<T>>) -> usize {
|
||||
if Arc::ptr_eq(left, right) {
|
||||
return 1;
|
||||
}
|
||||
match (left.as_ref(), right.as_ref()) {
|
||||
(
|
||||
Node::Branch {
|
||||
left: ll,
|
||||
right: lr,
|
||||
..
|
||||
},
|
||||
Node::Branch {
|
||||
left: rl,
|
||||
right: rr,
|
||||
..
|
||||
},
|
||||
) => shared_nodes(ll, rl) + shared_nodes(lr, rr),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_valid<T>(node: &Arc<Node<T>>) -> (usize, u32, Measure) {
|
||||
match node.as_ref() {
|
||||
Node::Leaf { entries, measure } => {
|
||||
assert!(!entries.is_empty() && entries.len() <= LEAF_CAPACITY);
|
||||
let calculated = entries
|
||||
.iter()
|
||||
.try_fold(Measure::default(), |sum, entry| {
|
||||
sum.checked_combine(entry.measure())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(*measure, calculated);
|
||||
(entries.len(), 1, calculated)
|
||||
}
|
||||
Node::Branch {
|
||||
left,
|
||||
right,
|
||||
len,
|
||||
height,
|
||||
measure,
|
||||
} => {
|
||||
let (left_len, left_height, left_measure) = assert_valid(left);
|
||||
let (right_len, right_height, right_measure) = assert_valid(right);
|
||||
assert!(left_height.abs_diff(right_height) <= 1);
|
||||
assert_eq!(*len, left_len + right_len);
|
||||
assert_eq!(*height, left_height.max(right_height) + 1);
|
||||
assert_eq!(
|
||||
*measure,
|
||||
left_measure.checked_combine(right_measure).unwrap()
|
||||
);
|
||||
(*len, *height, *measure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_unicode_measures_are_exact() {
|
||||
let empty = Sequence::<String>::empty();
|
||||
assert!(empty.is_empty());
|
||||
assert_eq!(empty.measure(), Measure::default());
|
||||
let mut work = Work::default();
|
||||
let sequence = Sequence::from_entries([entry(0), entry(12)], &mut work).unwrap();
|
||||
assert_eq!(sequence.len(), 2);
|
||||
assert_eq!(sequence.measure().chars, 4);
|
||||
assert_eq!(sequence.measure().lines, 2);
|
||||
assert_eq!(sequence.measure().width_sum, 12);
|
||||
assert_eq!(sequence.measure().width_max, 12);
|
||||
assert_eq!(sequence.prefix_measure(1).unwrap().chars, 2);
|
||||
for size in 1..=257 {
|
||||
let mut work = Work::default();
|
||||
let varied = Sequence::from_entries((0..size).map(entry), &mut work).unwrap();
|
||||
assert_eq!(varied.len(), size);
|
||||
assert_valid(varied.root().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_split_point_rejoins_exactly() {
|
||||
let mut setup = Work::default();
|
||||
let original = Sequence::from_entries((0..97).map(entry), &mut setup).unwrap();
|
||||
let oracle = values(&original);
|
||||
for index in 0..=original.len() {
|
||||
let mut work = Work::default();
|
||||
let (left, right) = original.split_at(index, &mut work).unwrap();
|
||||
assert_eq!(values(&left), oracle[..index]);
|
||||
assert_eq!(values(&right), oracle[index..]);
|
||||
let joined = left.concat(&right, &mut work).unwrap();
|
||||
assert_eq!(values(&joined), oracle);
|
||||
assert!(height(&joined) <= 8);
|
||||
assert_valid(joined.root().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_left_and_right_concat_remain_balanced() {
|
||||
let mut left = Sequence::empty();
|
||||
let mut right = Sequence::empty();
|
||||
let mut work = Work::default();
|
||||
for index in 0..2048 {
|
||||
let one = Sequence::from_entries([entry(index)], &mut work).unwrap();
|
||||
left = left.concat(&one, &mut work).unwrap();
|
||||
right = one.concat(&right, &mut work).unwrap();
|
||||
}
|
||||
assert_eq!(left.len(), 2048);
|
||||
assert_eq!(right.len(), 2048);
|
||||
assert!(height(&left) <= 16, "left height={}", height(&left));
|
||||
assert!(height(&right) <= 16, "right height={}", height(&right));
|
||||
assert_valid(left.root().unwrap());
|
||||
assert_valid(right.root().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_random_operations_match_vec_oracle() {
|
||||
let mut seed = 0x91e1_0da5_u64;
|
||||
let mut oracle = (0..64).collect::<Vec<_>>();
|
||||
let mut setup = Work::default();
|
||||
let mut sequence =
|
||||
Sequence::from_entries(oracle.iter().copied().map(entry), &mut setup).unwrap();
|
||||
for _ in 0..1000 {
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let index = (seed as usize) % oracle.len();
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let value = (seed >> 16) as usize % 10_000;
|
||||
oracle[index] = value;
|
||||
let mut work = Work::default();
|
||||
sequence = sequence.replace(index, entry(value), &mut work).unwrap();
|
||||
assert!(work.entries_written <= LEAF_CAPACITY as u64);
|
||||
}
|
||||
assert_eq!(
|
||||
values(&sequence),
|
||||
oracle
|
||||
.iter()
|
||||
.map(|value| entry(*value).value().as_ref().clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_valid(sequence.root().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_replace_is_logarithmic_and_preserves_old_fork() {
|
||||
for size in [32, 128, 512, 8192] {
|
||||
let mut setup = Work::default();
|
||||
let original = Sequence::from_entries((0..size).map(entry), &mut setup).unwrap();
|
||||
let fork = original.clone();
|
||||
let old_value = original.get(1).unwrap().value().clone();
|
||||
let mut work = Work::default();
|
||||
let updated = original.replace(1, entry(99_999), &mut work).unwrap();
|
||||
assert!(work.nodes_created <= u64::from(height(&original)) + 1);
|
||||
assert!(work.entries_written <= LEAF_CAPACITY as u64);
|
||||
assert!(shared_nodes(original.root().unwrap(), updated.root().unwrap()) > 0);
|
||||
assert!(Arc::ptr_eq(fork.get(1).unwrap().value(), &old_value));
|
||||
assert_eq!(fork.get(1).unwrap().value().as_str(), "1");
|
||||
assert_eq!(updated.get(1).unwrap().value().as_str(), "99999");
|
||||
assert_valid(updated.root().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_split_and_rejoin_touch_only_logarithmic_paths() {
|
||||
let mut setup = Work::default();
|
||||
let original = Sequence::from_entries((0..8192).map(entry), &mut setup).unwrap();
|
||||
let mut split_work = Work::default();
|
||||
let (left, right) = original.split_at(4097, &mut split_work).unwrap();
|
||||
assert!(split_work.nodes_created <= 32, "{split_work:?}");
|
||||
assert!(split_work.entries_written <= 32, "{split_work:?}");
|
||||
let mut concat_work = Work::default();
|
||||
let rejoined = left.concat(&right, &mut concat_work).unwrap();
|
||||
assert!(concat_work.nodes_created <= 32, "{concat_work:?}");
|
||||
assert_eq!(values(&rejoined), values(&original));
|
||||
assert_valid(rejoined.root().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_and_invalid_updates_leave_the_old_root_unchanged() {
|
||||
let huge = Entry::new(
|
||||
Arc::new("x".to_owned()),
|
||||
Measure::new(u64::MAX, 0, i64::MAX, i64::MAX).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let one = Entry::new(Arc::new("y".to_owned()), Measure::new(1, 0, 1, 1).unwrap()).unwrap();
|
||||
let mut work = Work::default();
|
||||
let base = Sequence::from_entries([huge], &mut work).unwrap();
|
||||
let old_root = Arc::clone(base.root().unwrap());
|
||||
let mut failed_work = Work::default();
|
||||
assert!(matches!(
|
||||
base.concat(
|
||||
&Sequence::from_entries([one], &mut Work::default()).unwrap(),
|
||||
&mut failed_work
|
||||
),
|
||||
Err(Error::Overflow)
|
||||
));
|
||||
assert!(Arc::ptr_eq(base.root().unwrap(), &old_root));
|
||||
assert!(matches!(
|
||||
base.replace(2, entry(2), &mut failed_work),
|
||||
Err(Error::IndexOutOfBounds)
|
||||
));
|
||||
assert!(Measure::new(0, 0, -1, 0).is_err());
|
||||
assert!(Arc::ptr_eq(base.root().unwrap(), &old_root));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user