From c9de7e6329e371ac92ff5428d664e00bcbdf3097 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Sat, 5 Sep 2026 11:15:52 +0800 Subject: [PATCH] Retain atom composition and incremental context validation facts --- native/src/atom_plan.rs | 782 ++++++++++++++++++++++++++++++++++ native/src/layout.rs | 922 ++++++++++++++++++++++++++++++++-------- native/src/lib.rs | 20 +- native/src/sequence.rs | 751 ++++++++++++++++++++++++++++++++ 4 files changed, 2304 insertions(+), 171 deletions(-) create mode 100644 native/src/atom_plan.rs create mode 100644 native/src/sequence.rs diff --git a/native/src/atom_plan.rs b/native/src/atom_plan.rs new file mode 100644 index 0000000..b298697 --- /dev/null +++ b/native/src/atom_plan.rs @@ -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 = 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, + 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, + decorations: Box<[Decoration]>, + }, +} + +#[must_use] +#[derive(Debug, Clone)] +pub(super) struct AtomPlan { + parts: Sequence, + 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) -> 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::>(); + 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 { + 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 { + 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>, output: &mut Vec) { + 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>) -> Option { + 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::>(); + 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::>() + ); + 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::>(); + 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); + } +} diff --git a/native/src/layout.rs b/native/src/layout.rs index 2f40df7..74aa0a1 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -6,6 +6,19 @@ use std::collections::BTreeMap; use std::collections::HashSet; use std::sync::Arc; +#[path = "atom_plan.rs"] +mod atom_plan; +use atom_plan::AtomPlan; +pub(crate) use atom_plan::AtomPlanWork; + +pub(crate) fn reset_atom_plan_work() { + atom_plan::reset_work(); +} + +pub(crate) fn atom_plan_work() -> AtomPlanWork { + atom_plan::work() +} + fn deserialize_arc_vec<'de, D, T>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, @@ -131,7 +144,18 @@ struct RetainedEntry { } #[derive(Debug, Clone, Default)] -struct LocalWorkSummary(BTreeMap<&'static str, usize>); +struct LocalWorkSummary(BTreeMap<&'static str, LocalFieldWork>); + +#[derive(Debug, Clone)] +struct LocalFieldWork { + units: usize, + viewport_heights: usize, +} + +struct LocalWorkDelta { + units: isize, + viewport_heights: i128, +} #[derive(Debug)] struct RadixNode { @@ -200,6 +224,7 @@ pub(crate) struct RetainedDocument { style_count: u32, property_template_count: u32, work_units: usize, + context_viewport_heights: usize, } #[derive(Debug)] @@ -319,6 +344,7 @@ impl RetainedDocument { entries: &mut Arc, seen: &mut HashSet, count: &mut u64, + context_viewport_heights: &mut usize, ) -> Result { fn localize( node: &mut LayoutNode, @@ -326,6 +352,7 @@ impl RetainedDocument { entries: &mut Arc, seen: &mut HashSet, count: &mut u64, + context_viewport_heights: &mut usize, ) -> Result<(), String> { fn localize_child( child: LayoutNode, @@ -333,22 +360,46 @@ impl RetainedDocument { entries: &mut Arc, seen: &mut HashSet, count: &mut u64, + context_viewport_heights: &mut usize, ) -> Result { if child.node_id().is_some() { - let installed = install_owner(child, entries, seen, count)?; + let installed = + install_owner(child, entries, seen, count, context_viewport_heights)?; return Ok(LayoutNode::NodeRef { node_id: installed }); } let mut child = child; - localize(&mut child, owner_id, entries, seen, count)?; + localize( + &mut child, + owner_id, + entries, + seen, + count, + context_viewport_heights, + )?; Ok(child) } + // Localization visits every input node once, including anonymous + // descendants that cannot be named by a later topology-preserving delta. + visit_context_height_fields(node, |_, size| { + *context_viewport_heights = context_viewport_heights + .checked_add(context_size_occurrences(size)?) + .ok_or_else(|| { + "Native retained context summary invariant failed".to_owned() + })?; + Ok(()) + })?; match node { LayoutNode::Box { child, .. } => { if let Some(old) = child.take() { let old = Arc::try_unwrap(old).unwrap_or_else(|value| (*value).clone()); *child = Some(Arc::new(localize_child( - old, owner_id, entries, seen, count, + old, + owner_id, + entries, + seen, + count, + context_viewport_heights, )?)); } } @@ -357,7 +408,14 @@ impl RetainedDocument { let old = Arc::try_unwrap(old).unwrap_or_else(|value| (*value).clone()); let mut localized = Vec::with_capacity(old.len()); for child in old { - localized.push(localize_child(child, owner_id, entries, seen, count)?); + localized.push(localize_child( + child, + owner_id, + entries, + seen, + count, + context_viewport_heights, + )?); } *children = Arc::new(localized); } @@ -366,7 +424,14 @@ impl RetainedDocument { let old = Arc::try_unwrap(old).unwrap_or_else(|value| (*value).clone()); let mut localized = Vec::with_capacity(old.len()); for mut item in old { - item.node = localize_child(item.node, owner_id, entries, seen, count)?; + item.node = localize_child( + item.node, + owner_id, + entries, + seen, + count, + context_viewport_heights, + )?; localized.push(item); } *items = Arc::new(localized); @@ -390,7 +455,14 @@ impl RetainedDocument { if node_id == 0 || !seen.insert(node_id) { return Err("Native retained document has invalid duplicate node id".to_owned()); } - localize(&mut node, node_id, entries, seen, count)?; + localize( + &mut node, + node_id, + entries, + seen, + count, + context_viewport_heights, + )?; let slot_one = match &node { LayoutNode::Box { child: Some(child), .. @@ -411,7 +483,14 @@ impl RetainedDocument { let mut entries = Arc::new(RadixNode::default()); let mut seen = HashSet::new(); let mut owner_count = 0; - let root_id = install_owner(document.root, &mut entries, &mut seen, &mut owner_count)?; + let mut context_viewport_heights = 0; + let root_id = install_owner( + document.root, + &mut entries, + &mut seen, + &mut owner_count, + &mut context_viewport_heights, + )?; debug_assert!(usize::try_from(owner_count).is_ok_and(|count| count <= node_count)); let mut styles = Arc::new(StyleRadixNode::default()); if document.styles.len() != document.style_count as usize { @@ -429,6 +508,7 @@ impl RetainedDocument { style_count: document.style_count, property_template_count: document.property_template_count, work_units, + context_viewport_heights, }), node_count as u64, )) @@ -484,6 +564,7 @@ impl RetainedDocument { let mut seen = HashSet::with_capacity(delta.entries.len()); let mut prepared = Vec::with_capacity(delta.entries.len()); let mut work_delta_total = 0_i128; + let mut context_delta_total = 0_i128; for update in delta.entries { if !seen.insert(update.node_id) { return Err("Native retained delta contains a duplicate node id".to_owned()); @@ -506,8 +587,11 @@ impl RetainedDocument { delta.property_template_target_count, )?; work_delta_total = work_delta_total - .checked_add(work_delta as i128) + .checked_add(work_delta.units as i128) .ok_or_else(|| "Native layout work estimate overflowed".to_owned())?; + context_delta_total = context_delta_total + .checked_add(work_delta.viewport_heights) + .ok_or_else(|| "Native retained context summary invariant failed".to_owned())?; prepared.push(( update.node_id, Arc::new(RetainedEntry { @@ -545,6 +629,10 @@ impl RetainedDocument { if work_units > MAX_LAYOUT_WORK_UNITS { return Err("Native layout exceeds the work-unit limit".to_owned()); } + let context_viewport_heights = (self.context_viewport_heights as i128) + .checked_add(context_delta_total) + .and_then(|count| usize::try_from(count).ok()) + .ok_or_else(|| "Native retained context summary invariant failed".to_owned())?; let parsed = prepared.len() as u64; let mut entries = Arc::clone(&self.entries); let mut copied = 0; @@ -568,6 +656,7 @@ impl RetainedDocument { style_count: target_style_count, property_template_count: delta.property_template_target_count, work_units, + context_viewport_heights, }), parsed, copied, @@ -581,10 +670,11 @@ fn patch_owner_slots( patches: &[LocalSlotPatch], style_count: u32, property_template_count: u32, -) -> Result<(LayoutNode, [Arc; 2], isize), String> { +) -> Result<(LayoutNode, [Arc; 2], LocalWorkDelta), String> { let mut result = base.clone(); let mut slot_work = base_work.clone(); let mut work_delta = 0_isize; + let mut context_delta = 0_i128; let mut seen = [false; 2]; for patch in patches { let index = patch.slot as usize; @@ -596,6 +686,8 @@ fn patch_owner_slots( result = patch_local_node(&result, &patch.local)?; validate_changed_fields(&result, &patch.local, style_count, property_template_count)?; let updated = slot_work[0].updated(&result, &patch.local)?; + context_delta += updated.changed_context_occurrences(&patch.local) + - slot_work[0].changed_context_occurrences(&patch.local); let new_work = updated.changed_work(&patch.local); work_delta += new_work as isize - old_work as isize; slot_work[0] = Arc::new(updated); @@ -618,13 +710,22 @@ fn patch_owner_slots( property_template_count, )?; let updated = slot_work[1].updated(&new_child, &patch.local)?; + context_delta += updated.changed_context_occurrences(&patch.local) + - slot_work[1].changed_context_occurrences(&patch.local); let new_work = updated.changed_work(&patch.local); work_delta += new_work as isize - old_work as isize; slot_work[1] = Arc::new(updated); *child = Some(Arc::new(new_child)); } } - Ok((result, slot_work, work_delta)) + Ok(( + result, + slot_work, + LocalWorkDelta { + units: work_delta, + viewport_heights: context_delta, + }, + )) } fn patch_local_node( @@ -804,6 +905,28 @@ fn size_work(size: &Size) -> Result { Ok(work) } +fn context_size_occurrences(size: &Size) -> Result { + let mut count = 0_usize; + visit_context_size_occurrences(size, &mut || { + count = count + .checked_add(1) + .ok_or_else(|| "Native retained context summary invariant failed".to_owned())?; + Ok(()) + })?; + Ok(count) +} + +fn context_field_occurrences(node: &LayoutNode, name: &str) -> Result { + let mut count = 0; + visit_context_height_fields(node, |field, size| { + if field == name { + count = context_size_occurrences(size)?; + } + Ok(()) + })?; + Ok(count) +} + fn field_work(node: &LayoutNode, name: &str) -> Result, String> { Ok(match (node, name) { (LayoutNode::Box { content, .. }, "content") => { @@ -854,7 +977,13 @@ fn local_work_summary(node: &LayoutNode) -> Result { let mut values = BTreeMap::new(); for name in FIELDS { if let Some(value) = field_work(node, name)? { - values.insert(name, value); + values.insert( + name, + LocalFieldWork { + units: value, + viewport_heights: context_field_occurrences(node, name)?, + }, + ); } } Ok(LocalWorkSummary(values)) @@ -865,7 +994,15 @@ impl LocalWorkSummary { fields .keys() .filter_map(|name| self.0.get(name.as_str())) - .copied() + .map(|work| work.units) + .sum() + } + + fn changed_context_occurrences(&self, fields: &JsonMap) -> i128 { + fields + .keys() + .filter_map(|name| self.0.get(name.as_str())) + .map(|work| work.viewport_heights as i128) .sum() } @@ -890,7 +1027,10 @@ impl LocalWorkSummary { "row-gap" => "row-gap", _ => continue, }, - value, + LocalFieldWork { + units: value, + viewport_heights: context_field_occurrences(node, name)?, + }, ); } } @@ -1504,23 +1644,27 @@ enum Atom { #[derive(Debug, Clone, Default)] struct Line { - atoms: Vec, + atoms: AtomPlan, width: i64, } impl Line { fn from_clusters(clusters: &[MeasuredCluster]) -> Self { - let mut line = Self::default(); + let mut atoms = Vec::with_capacity(clusters.len()); for cluster in clusters { let properties = AtomProperties { property_template_ids: cluster.source_template_id.into_iter().collect(), ..AtomProperties::default() }; if cluster.pixel_space { - line.push_space_with_properties(cluster.width, properties); + if cluster.width > 0 { + atoms.push(Atom::Space { + width: cluster.width, + properties, + }); + } } else { - line.width += cluster.width; - line.atoms.push(Atom::Text { + atoms.push(Atom::Text { text: cluster.text.clone(), width: cluster.width, cjk: cluster.cjk, @@ -1529,13 +1673,18 @@ impl Line { }); } } - line + let atoms = AtomPlan::from_owned_atoms(atoms); + Self { + width: atoms.width(), + atoms, + } } fn from_atoms(atoms: &[Atom]) -> Self { + let atoms = AtomPlan::from_atoms(atoms); Self { - atoms: atoms.to_vec(), - width: atoms.iter().map(Atom::width).sum(), + width: atoms.width(), + atoms, } } @@ -1548,7 +1697,7 @@ impl Line { return; } self.width += width; - self.atoms.push(Atom::Space { width, properties }); + self.atoms = self.atoms.push(Atom::Space { width, properties }); } fn prepend_space(&mut self, width: i64) { @@ -1560,12 +1709,12 @@ impl Line { return; } self.width += width; - self.atoms.insert(0, Atom::Space { width, properties }); + self.atoms = self.atoms.prepend(Atom::Space { width, properties }); } fn append(&mut self, other: &Self) { self.width += other.width; - self.atoms.extend(other.atoms.iter().cloned()); + self.atoms = self.atoms.append(&other.atoms); } fn padded(mut self, target: i64, align: HorizontalAlign) -> Self { @@ -1593,63 +1742,23 @@ impl Line { } fn whitespace_only(&self) -> bool { - self.atoms.iter().all(|atom| match atom { - Atom::Space { .. } => true, - Atom::Text { text, .. } => text.trim().is_empty(), - }) + self.atoms.whitespace_only() } fn has_noncontent_properties(&self) -> bool { - self.atoms.iter().any(|atom| { - let properties = atom.properties(); - !properties.style_ids.is_empty() - || !properties.roles.is_empty() - || !properties.property_template_ids.is_empty() - }) + self.atoms.has_noncontent_properties() } fn own_content(&mut self, region_id: i64, content_idx: i64) { - let has_owner = self - .atoms - .iter() - .any(|atom| atom.properties().owner.is_some() || !atom.properties().owners.is_empty()); - for atom in &mut self.atoms { - let properties = atom.properties_mut(); - if has_owner { - let mut owners = properties.owners.clone(); - if let Some(owner) = properties.owner { - if !owners.contains(&owner) { - owners.push(owner); - } - } - if !owners.contains(®ion_id) { - owners.push(region_id); - } - properties.owner = Some(region_id); - properties.owners = owners; - } else { - properties.owner = Some(region_id); - properties.owners = vec![region_id]; - } - } - let has_content = self - .atoms - .iter() - .any(|atom| atom.properties().content.is_some()); - if !has_content { - for atom in &mut self.atoms { - let properties = atom.properties_mut(); - properties.content = Some(region_id); - properties.content_idx = Some(content_idx); - } - } + self.atoms = self.atoms.own_content(region_id, content_idx); } fn collapse_whitespace_content(self, width: i64, region_id: i64) -> Self { if !self.whitespace_only() || self.has_noncontent_properties() { return self; } - let first = self.atoms.first().map(Atom::properties); + let first_atom = self.atoms.first(); + let first = first_atom.as_ref().map(Atom::properties); let mut properties = AtomProperties { content: first .and_then(|properties| properties.content) @@ -1670,28 +1779,18 @@ impl Line { fn apply_style(&mut self, style_id: Option) { if let Some(style_id) = style_id { - for atom in &mut self.atoms { - atom.properties_mut().style_ids.push(style_id); - } + self.atoms = self.atoms.apply_style(style_id); } } fn apply_property_template(&mut self, template_id: Option) { if let Some(template_id) = template_id { - for atom in &mut self.atoms { - atom.properties_mut() - .property_template_ids - .push(template_id); - } + self.atoms = self.atoms.apply_template(template_id); } } fn apply_role(&mut self, role: RegionRole, region_id: i64) { - for atom in &mut self.atoms { - atom.properties_mut() - .roles - .push(RegionRoleEntry { role, region_id }); - } + self.atoms = self.atoms.apply_role(role, region_id); } } @@ -1760,22 +1859,7 @@ impl Rendered { } self.lines .iter() - .map(|line| { - let mut maximum = 0; - let mut run = 0; - for atom in &line.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) - }) + .map(|line| line.atoms.min_content_width()) .max() .unwrap_or(0) } @@ -1786,9 +1870,7 @@ impl Rendered { fn apply_scroll_window(&mut self, region_id: i64) { for line in &mut self.lines { - for atom in &mut line.atoms { - atom.properties_mut().scroll_window = Some(region_id); - } + line.atoms = line.atoms.apply_scroll_window(region_id); } for properties in &mut self.breaks { properties.scroll_window = Some(region_id); @@ -1808,6 +1890,7 @@ impl Rendered { width: line.width, atoms: line .atoms + .to_vec() .into_iter() .map(|atom| match atom { Atom::Text { @@ -4027,51 +4110,19 @@ impl LayoutDocument { impl RetainedDocument { pub(crate) fn validate_context(&self, context: LayoutContext) -> Result<(), String> { - fn add_retained_context_work( - node: &LayoutNode, - document: &RetainedDocument, - context: LayoutContext, - work_units: &mut usize, - ) -> Result<(), String> { - let node = document.resolve(node)?; - match node { - LayoutNode::NodeRef { .. } => unreachable!("resolver returned a node reference"), - LayoutNode::Text { .. } => {} - LayoutNode::Box { - child, - height, - min_height, - max_height, - .. - } => { - for size in [height, min_height, max_height] { - add_context_size_work(size, context, work_units)?; - } - if let Some(child) = child { - add_retained_context_work(child, document, context, work_units)?; - } - } - LayoutNode::Row { children, .. } | LayoutNode::Column { children, .. } => { - for child in children.iter() { - add_retained_context_work(child, document, context, work_units)?; - } - } - LayoutNode::Flex { height, items, .. } => { - add_context_size_work(height, context, work_units)?; - for item in items.iter() { - add_retained_context_work(&item.node, document, context, work_units)?; - } - } - } + // Bootstrap establishes the root/reference invariant and deltas cannot + // change topology. The full traversal adds the same nonnegative amount + // for every occurrence, stopping at the work limit before a later add + // could overflow. Division preserves that error without multiplying. + if self.context_viewport_heights == 0 { + return Ok(()); + } + let height = usize::try_from(context.viewport_height).unwrap_or(usize::MAX); + if height > MAX_LAYOUT_WORK_UNITS / self.context_viewport_heights { + Err("Native layout exceeds the work-unit limit".to_owned()) + } else { Ok(()) } - - let root = self - .effective_node(self.root_id) - .ok_or_else(|| "Native retained document lost its root".to_owned())?; - RESOLVER_LOOKUP_COUNT.with(|count| count.set(count.get().saturating_add(1))); - let mut work_units = 0; - add_retained_context_work(root, self, context, &mut work_units) } pub(crate) fn layout_tape( @@ -4145,22 +4196,17 @@ fn add_vertical_size_work(size: &Size, work_units: &mut usize) -> Result<(), Str } } -fn add_context_size_work( +fn visit_context_size_occurrences( size: &Size, - context: LayoutContext, - work_units: &mut usize, + visit: &mut impl FnMut() -> Result<(), String>, ) -> Result<(), String> { + // Validation work adds every operand, including subtraction operands. match size { - Size::ViewportHeight => add_work_units( - work_units, - usize::try_from(context.viewport_height).unwrap_or(usize::MAX), - ), - Size::FitContent { limit: Some(limit) } => { - add_context_size_work(limit, context, work_units) - } + Size::ViewportHeight => visit(), + Size::FitContent { limit: Some(limit) } => visit_context_size_occurrences(limit, visit), Size::Add { values } | Size::Subtract { values } => { for value in &values.0 { - add_context_size_work(value, context, work_units)?; + visit_context_size_occurrences(value, visit)?; } Ok(()) } @@ -4168,26 +4214,58 @@ fn add_context_size_work( } } -fn add_context_work( - node: &LayoutNode, +fn add_context_size_work( + size: &Size, context: LayoutContext, work_units: &mut usize, +) -> Result<(), String> { + visit_context_size_occurrences(size, &mut || { + add_work_units( + work_units, + usize::try_from(context.viewport_height).unwrap_or(usize::MAX), + ) + }) +} + +fn visit_context_height_fields( + node: &LayoutNode, + mut visit: impl FnMut(&'static str, &Size) -> Result<(), String>, ) -> Result<(), String> { match node { - LayoutNode::NodeRef { .. } => { - return Err("Native full layout cannot contain a node reference".to_owned()); - } - LayoutNode::Text { .. } => {} LayoutNode::Box { - child, height, min_height, max_height, .. } => { - for size in [height, min_height, max_height] { - add_context_size_work(size, context, work_units)?; + for (name, size) in [ + ("height", height), + ("min-height", min_height), + ("max-height", max_height), + ] { + visit(name, size)?; } + } + LayoutNode::Flex { height, .. } => visit("height", height)?, + _ => {} + } + Ok(()) +} + +fn add_context_work( + node: &LayoutNode, + context: LayoutContext, + work_units: &mut usize, +) -> Result<(), String> { + visit_context_height_fields(node, |_, size| { + add_context_size_work(size, context, work_units) + })?; + match node { + LayoutNode::NodeRef { .. } => { + return Err("Native full layout cannot contain a node reference".to_owned()); + } + LayoutNode::Text { .. } => {} + LayoutNode::Box { child, .. } => { if let Some(child) = child { add_context_work(child, context, work_units)?; } @@ -4197,8 +4275,7 @@ fn add_context_work( add_context_work(child, context, work_units)?; } } - LayoutNode::Flex { height, items, .. } => { - add_context_size_work(height, context, work_units)?; + LayoutNode::Flex { items, .. } => { for item in items.iter() { add_context_work(&item.node, context, work_units)?; } @@ -4952,7 +5029,9 @@ fn wrap_rendered_line(line: &Line, max_width: i64, mode: WrapMode) -> Vec return vec![line.clone()]; } - let atoms = &line.atoms; + // Wrapping inspects the actual atom boundaries. Composition and decoration + // preserve the plan; only this geometry-dependent operation materializes it. + let atoms = line.atoms.to_vec(); let count = atoms.len(); let mut ranges = Vec::new(); let mut line_start = 0; @@ -7129,6 +7208,94 @@ fn collect_column_leaves<'a>(children: &'a [LayoutNode], output: &mut Vec<&'a La mod tests { use super::*; + #[test] + fn line_composition_preserves_mixed_ownership_and_decoration_order() { + let mut child = Line::from_clusters(&[cluster("x", 2, Some(7))]); + child.own_content(11, 3); + child.apply_style(Some(2)); + let original = Rendered::from_lines(vec![child.clone()]).into_tape(10); + let mut parent = Line::blank(4); + parent.append(&child); + parent.own_content(22, 9); + parent.apply_style(Some(5)); + parent.apply_property_template(Some(8)); + parent.apply_role(RegionRole::BorderTop, 22); + parent.push_space(6); + let tape = Rendered::from_lines(vec![parent]).into_tape(10); + assert_eq!(tape.lines[0].width, 12); + let TapeAtom::Space { + properties: left, .. + } = &tape.lines[0].atoms[0] + else { + panic!() + }; + assert_eq!(left.owner, Some(22)); + assert_eq!(left.owners, vec![22]); + assert_eq!( + left.content, None, + "a sibling's content prevents assigning padding content" + ); + assert_eq!(left.style_ids, vec![5]); + let TapeAtom::Text { + properties: text, .. + } = &tape.lines[0].atoms[1] + else { + panic!() + }; + assert_eq!(text.owner, Some(22)); + assert_eq!(text.owners, vec![11, 22]); + assert_eq!(text.content, Some(11)); + assert_eq!(text.content_idx, Some(3)); + assert_eq!(text.style_ids, vec![2, 5]); + assert_eq!(text.property_template_ids, vec![7, 8]); + assert_eq!( + text.roles, + vec![RegionRoleEntry { + role: RegionRole::BorderTop, + region_id: 22 + }] + ); + let TapeAtom::Space { + properties: right, .. + } = &tape.lines[0].atoms[2] + else { + panic!() + }; + assert_eq!( + right, + &AtomProperties::default(), + "later padding stays outside prior decorations" + ); + assert_eq!( + Rendered::from_lines(vec![child]).into_tape(10), + original, + "composing a parent must preserve its reusable child" + ); + } + + #[test] + fn line_whitespace_collapse_preserves_property_boundaries() { + let mut plain = Line::blank(5); + plain.own_content(10, 4); + let collapsed = + Rendered::from_lines(vec![plain.collapse_whitespace_content(7, 20)]).into_tape(0); + let TapeAtom::Space { width, properties } = &collapsed.lines[0].atoms[0] else { + panic!() + }; + assert_eq!(*width, 7); + assert_eq!(properties.content, Some(10)); + assert_eq!(properties.content_idx, Some(4)); + assert_eq!(properties.owner, Some(20)); + assert!(properties.owners.is_empty()); + let mut styled = Line::blank(5); + styled.apply_property_template(Some(3)); + let before = Rendered::from_lines(vec![styled.clone()]).into_tape(0); + assert_eq!( + Rendered::from_lines(vec![styled.collapse_whitespace_content(7, 20)]).into_tape(0), + before + ); + } + fn counted_layout_tape( document: &LayoutDocument, context: LayoutContext, @@ -7617,6 +7784,7 @@ mod tests { .iter() .map(|line| { line.atoms + .to_vec() .iter() .map(|atom| match atom { Atom::Text { text, .. } => text.as_str(), @@ -8057,6 +8225,7 @@ mod tests { .all(|properties| { properties.scroll_window == Some(9) })); assert!(stacked.lines.iter().all(|line| { line.atoms + .to_vec() .iter() .all(|atom| atom.properties().scroll_window == Some(9)) })); @@ -8825,6 +8994,423 @@ mod tests { assert!(fragment_style_delta(&old, &changed).is_none()); } + // Reconstruct the full document so its original context traversal remains an + // independent oracle for summaries, including after retained local patches. + fn retained_context_full_document(document: &RetainedDocument) -> LayoutDocument { + fn expand(node: &LayoutNode, document: &RetainedDocument) -> LayoutNode { + let mut node = document.resolve(node).unwrap().clone(); + match &mut node { + LayoutNode::Box { child, .. } => { + *child = child + .as_deref() + .map(|child| Arc::new(expand(child, document))); + } + LayoutNode::Row { children, .. } | LayoutNode::Column { children, .. } => { + *children = + Arc::new(children.iter().map(|node| expand(node, document)).collect()); + } + LayoutNode::Flex { items, .. } => { + *items = Arc::new( + items + .iter() + .map(|item| { + let mut item = item.clone(); + item.node = expand(&item.node, document); + item + }) + .collect(), + ); + } + LayoutNode::Text { .. } => {} + LayoutNode::NodeRef { .. } => unreachable!(), + } + node + } + retained_document(expand( + &LayoutNode::NodeRef { + node_id: document.root_id, + }, + document, + )) + } + + fn assert_retained_context_oracle(document: &RetainedDocument, occurrences: usize) { + assert_eq!(document.context_viewport_heights, occurrences); + let full = retained_context_full_document(document); + let boundary = MAX_LAYOUT_WORK_UNITS / occurrences.max(1); + for height in [ + i64::MIN, + -1, + 0, + 1, + boundary as i64, + boundary as i64 + 1, + MAX_LAYOUT_WORK_UNITS as i64, + MAX_LAYOUT_WORK_UNITS as i64 + 1, + MAX_LAYOUT_DIMENSION, + i64::MAX, + ] { + let context = LayoutContext { + viewport_height: height, + ..test_context() + }; + let expected = full.validate_context(context); + assert_eq!( + document.validate_context(context), + expected, + "height {height}" + ); + let accepted = occurrences == 0 || height >= 0 && height <= boundary as i64; + assert_eq!( + expected.is_ok(), + accepted, + "{occurrences} occurrences at height {height}" + ); + if !accepted { + assert_eq!( + expected.unwrap_err(), + "Native layout exceeds the work-unit limit" + ); + } + } + } + + fn context_test_delta(entries: Vec) -> DocumentDelta { + serde_json::from_value(serde_json::json!({ + "style-base-count": 0, + "styles-append": [], + "property-template-base-count": 0, + "property-template-target-count": 0, + "entries": entries + })) + .unwrap() + } + + fn context_test_flex(height: Size, nodes: Vec) -> LayoutNode { + LayoutNode::Flex { + node_id: None, + node_revision: None, + direction: FlexDirection::Column, + wrap: FlexWrap::Nowrap, + justify: FlexAlign::FlexStart, + align_items: FlexAlign::Stretch, + align_content: FlexAlign::Stretch, + width: Size::ViewportHeight, + height, + row_gap: 0, + column_gap: 0, + items: Arc::new( + nodes + .into_iter() + .map(|node| FlexItem { + node, + order: 0, + grow: 0.0, + shrink: 0.0, + basis: Size::ViewportHeight, + align_self: FlexAlign::Stretch, + }) + .collect(), + ), + } + } + + #[test] + fn retained_context_bootstrap_counts_deep_anonymous_and_nested_expressions() { + let mut leaf = text_box(2, measured_text(vec![vec![cluster("x", 1, None)]]), None); + let LayoutNode::Box { + height, + min_height, + max_height, + .. + } = &mut leaf + else { + unreachable!(); + }; + *height = serde_json::from_value(serde_json::json!({ + "kind": "subtract", "values": [ + {"kind": "viewport-height"}, + {"kind": "fit-content", "limit": { + "kind": "add", "values": [ + {"kind": "viewport-height"}, {"kind": "viewport-height"}, + {"kind": "viewport"}, {"kind": "lines", "value": 2} + ] + }} + ] + })) + .unwrap(); + *min_height = Size::ViewportHeight; + *max_height = Size::FitContent { + limit: Some(Box::new(Size::ViewportHeight)), + }; + let mut anonymous = context_test_flex(Size::ViewportHeight, vec![identified(leaf, 2, 1)]); + for index in 0..48 { + let mut wrapper = child_box(index + 3, anonymous, None); + let LayoutNode::Box { height, .. } = &mut wrapper else { + unreachable!() + }; + *height = Size::ViewportHeight; + anonymous = LayoutNode::Row { + node_id: None, + node_revision: None, + children: Arc::new(vec![wrapper]), + }; + } + let root = identified(child_box(1, anonymous, None), 1, 1); + let full = retained_document(root); + let (retained, parsed) = RetainedDocument::bootstrap(full.clone()).unwrap(); + assert_eq!(parsed, 99); + assert_retained_context_oracle(&retained, 54); + for height in [0, 1, 4629, 4630, -1, i64::MAX] { + let context = LayoutContext { + viewport_height: height, + ..test_context() + }; + assert_eq!( + retained.validate_context(context), + full.validate_context(context) + ); + } + } + + #[test] + fn retained_context_ignores_widths_and_preserves_zero_occurrence_errors() { + let mut leaf = text_box(2, measured_text(vec![vec![cluster("x", 1, None)]]), None); + let LayoutNode::Box { + width, + min_width, + max_width, + height, + .. + } = &mut leaf + else { + unreachable!(); + }; + *width = Size::ViewportHeight; + *min_width = Size::ViewportHeight; + *max_width = Size::ViewportHeight; + *height = Size::FitContent { limit: None }; + let root = identified( + child_box(1, context_test_flex(Size::Auto, vec![leaf]), None), + 1, + 1, + ); + let (retained, _) = RetainedDocument::bootstrap(retained_document(root)).unwrap(); + assert_retained_context_oracle(&retained, 0); + } + + #[test] + fn retained_context_slot_orders_revision_only_forks_and_invalid_cas_match_oracle() { + let leaf = identified( + text_box(2, measured_text(vec![vec![cluster("x", 1, None)]]), None), + 2, + 1, + ); + let root = identified( + child_box(1, context_test_flex(Size::ViewportHeight, vec![leaf]), None), + 1, + 7, + ); + let (base, _) = RetainedDocument::bootstrap(retained_document(root)).unwrap(); + let slot_zero = serde_json::json!({"slot": 0, "local": { + "height": {"kind": "viewport-height"}, + "min-height": {"kind": "viewport-height"}, + "max-height": {"kind": "fit-content", "limit": {"kind": "viewport-height"}}, + "padding-left": 2 + }}); + let slot_one = serde_json::json!({"slot": 1, "local": { + "height": {"kind": "subtract", "values": [ + {"kind": "viewport-height"}, {"kind": "viewport-height"} + ]}, + "width": {"kind": "viewport-height"} + }}); + let update = |patches: Vec| { + serde_json::json!({ + "node-id": 1, "expected-revision": 7, "target-revision": 8, + "slot-patches": patches + }) + }; + let forward = base + .apply_delta(context_test_delta(vec![update(vec![ + slot_zero.clone(), + slot_one.clone(), + ])])) + .unwrap() + .0; + let reverse = base + .apply_delta(context_test_delta(vec![update(vec![slot_one, slot_zero])])) + .unwrap() + .0; + assert_retained_context_oracle(&base, 1); + assert_retained_context_oracle(&forward, 5); + assert_retained_context_oracle(&reverse, 5); + + let revision = + serde_json::json!({"node-id": 1, "expected-revision": 8, "target-revision": 9}); + let no_op = forward + .apply_delta(context_test_delta(vec![revision])) + .unwrap() + .0; + assert_retained_context_oracle(&no_op, 5); + assert!(Arc::ptr_eq( + &radix_lookup(&forward.entries, 1).unwrap().node, + &radix_lookup(&no_op.entries, 1).unwrap().node + )); + + let remove = update(vec![ + serde_json::json!({"slot": 1, "local": {"height": {"kind": "auto"}}}), + ]); + let other_fork = base + .apply_delta(context_test_delta(vec![remove])) + .unwrap() + .0; + assert_retained_context_oracle(&other_fork, 0); + assert_retained_context_oracle(&base, 1); + assert_retained_context_oracle(&forward, 5); + + for (expected_revision, target_revision) in [(6, 8), (7, 7), (7, 6)] { + let invalid = serde_json::json!({ + "node-id": 1, "expected-revision": expected_revision, "target-revision": target_revision, + "slot-patches": [{"slot": 1, "local": {"height": {"kind": "auto"}}}] + }); + assert_eq!( + base.apply_delta(context_test_delta(vec![invalid])) + .unwrap_err(), + "Native retained delta node revision mismatch" + ); + assert_retained_context_oracle(&base, 1); + } + } + + #[test] + fn retained_context_large_occurrence_count_accepts_zero_height() { + let mut root = identified( + text_box(1, measured_text(vec![vec![cluster("x", 1, None)]]), None), + 1, + 1, + ); + let LayoutNode::Box { height, .. } = &mut root else { + unreachable!() + }; + *height = Size::Add { + values: Box::new(SizeValues( + vec![Size::ViewportHeight; MAX_LAYOUT_WORK_UNITS + 1].into_boxed_slice(), + )), + }; + let (retained, _) = RetainedDocument::bootstrap(retained_document(root)).unwrap(); + assert_retained_context_oracle(&retained, MAX_LAYOUT_WORK_UNITS + 1); + } + + #[test] + fn retained_context_flex_owner_and_anonymous_box_batch_updates_are_atomic() { + let mut anonymous = text_box(3, measured_text(vec![vec![cluster("x", 1, None)]]), None); + let LayoutNode::Box { + height, + min_height, + max_height, + .. + } = &mut anonymous + else { + unreachable!() + }; + *height = Size::ViewportHeight; + *min_height = Size::ViewportHeight; + *max_height = Size::ViewportHeight; + let owner = identified(child_box(2, anonymous, None), 2, 1); + let root = identified(context_test_flex(Size::ViewportHeight, vec![owner]), 1, 1); + let (base, _) = RetainedDocument::bootstrap(retained_document(root)).unwrap(); + let flex_update = serde_json::json!({ + "node-id": 1, "expected-revision": 1, "target-revision": 2, + "slot-patches": [{"slot": 0, "local": {"height": {"kind": "add", "values": [ + {"kind": "viewport-height"}, {"kind": "viewport-height"} + ]}}}] + }); + let box_update = serde_json::json!({ + "node-id": 2, "expected-revision": 1, "target-revision": 2, + "slot-patches": [{"slot": 1, "local": { + "height": {"kind": "auto"}, + "min-height": {"kind": "lines", "value": 2}, + "max-height": {"kind": "subtract", "values": [ + {"kind": "viewport-height"}, {"kind": "viewport-height"} + ]} + }}] + }); + let flex_only = base + .apply_delta(context_test_delta(vec![flex_update.clone()])) + .unwrap() + .0; + let box_only = base + .apply_delta(context_test_delta(vec![box_update.clone()])) + .unwrap() + .0; + let forward = base + .apply_delta(context_test_delta(vec![ + flex_update.clone(), + box_update.clone(), + ])) + .unwrap() + .0; + let reverse = base + .apply_delta(context_test_delta(vec![box_update, flex_update.clone()])) + .unwrap() + .0; + assert_retained_context_oracle(&base, 4); + assert_retained_context_oracle(&flex_only, 5); + assert_retained_context_oracle(&box_only, 3); + assert_retained_context_oracle(&forward, 4); + assert_retained_context_oracle(&reverse, 4); + let removed = forward + .apply_delta(context_test_delta(vec![ + serde_json::json!({"node-id": 1, "expected-revision": 2, "target-revision": 3, + "slot-patches": [{"slot": 0, "local": {"height": {"kind": "auto"}}}]}), + serde_json::json!({"node-id": 2, "expected-revision": 2, "target-revision": 3, + "slot-patches": [{"slot": 1, "local": {"max-height": {"kind": "none"}}}]}), + ])) + .unwrap() + .0; + assert_retained_context_oracle(&removed, 0); + assert_retained_context_oracle(&forward, 4); + + // A later CAS/field failure must discard the first entry's prepared summary. + for invalid in [ + serde_json::json!({"node-id": 2, "expected-revision": 0, "target-revision": 2}), + serde_json::json!({"node-id": 2, "expected-revision": 1, "target-revision": 2, + "slot-patches": [{"slot": 1, "local": {"height": {"kind": "lines", "value": -1}}}]}), + ] { + assert!(base + .apply_delta(context_test_delta(vec![flex_update.clone(), invalid])) + .is_err()); + assert_retained_context_oracle(&base, 4); + assert_eq!(radix_lookup(&base.entries, 1).unwrap().revision, 1); + } + } + + #[test] + fn retained_context_validation_performs_zero_resolver_lookups() { + let nodes = (2..514) + .map(|id| { + identified( + text_box(id, measured_text(vec![vec![cluster("x", 1, None)]]), None), + id as u64, + 1, + ) + }) + .collect(); + let root = identified( + child_box(1, context_test_flex(Size::ViewportHeight, nodes), None), + 1, + 1, + ); + let (retained, _) = RetainedDocument::bootstrap(retained_document(root)).unwrap(); + reset_resolver_lookups(); + retained.validate_context(test_context()).unwrap(); + assert_eq!( + resolver_lookups(), + 0, + "retained context validation traversed the document" + ); + } + #[test] fn retained_scalar_patch_path_copies_only_radix_and_shares_wide_edges() { let children = Arc::new( diff --git a/native/src/lib.rs b/native/src/lib.rs index f32f597..6ffb988 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -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>, } @@ -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) { 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()); diff --git a/native/src/sequence.rs b/native/src/sequence.rs new file mode 100644 index 0000000..2bf477a --- /dev/null +++ b/native/src/sequence.rs @@ -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 { + 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 { + 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 { + value: Arc, + measure: Measure, +} + +impl Clone for Entry { + fn clone(&self) -> Self { + Self { + value: Arc::clone(&self.value), + measure: self.measure, + } + } +} + +impl Entry { + pub fn new(value: Arc, measure: Measure) -> Result { + if measure.width_sum < 0 || measure.width_max < 0 { + return Err(Error::NegativeWidth); + } + Ok(Self { value, measure }) + } + + pub fn value(&self) -> &Arc { + &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 { + Leaf { + entries: Box<[Entry]>, + measure: Measure, + }, + Branch { + left: Arc>, + right: Arc>, + len: usize, + height: u32, + measure: Measure, + }, +} + +pub enum NodeView<'a, T> { + Leaf { + entries: &'a [Entry], + measure: Measure, + }, + Branch { + left: &'a Arc>, + right: &'a Arc>, + len: usize, + height: u32, + measure: Measure, + }, +} + +type OptionalRoot = Option>>; +type SplitRoots = (OptionalRoot, OptionalRoot); + +impl Node { + 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 { + root: Option>>, +} + +impl Clone for Sequence { + fn clone(&self) -> Self { + Self { + root: self.root.as_ref().map(Arc::clone), + } + } +} + +impl Default for Sequence { + fn default() -> Self { + Self::empty() + } +} + +impl Sequence { + pub fn empty() -> Self { + Self { root: None } + } + + pub fn from_entries(entries: I, work: &mut Work) -> Result + where + I: IntoIterator>, + { + let entries = entries.into_iter().collect::>(); + 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(leaves: &[Arc>], work: &mut Work) -> Result>, 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>> { + self.root.as_ref() + } + + pub fn get(&self, index: usize) -> Option<&Entry> { + fn get_at(node: &Node, index: usize) -> Option<&Entry> { + 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 { + fn prefix(node: &Node, end: usize) -> Result { + 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 { + Ok(Self { + root: join(self.root.clone(), other.root.clone(), work)?, + }) + } + + pub fn replace(&self, index: usize, entry: Entry, work: &mut Work) -> Result { + 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(entries: Vec>, work: &mut Work) -> Result>, 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( + left: Arc>, + right: Arc>, + work: &mut Work, +) -> Result>, 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( + left: Option>>, + right: Option>>, + work: &mut Work, +) -> Result>>, 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( + left: Arc>, + right: Arc>, + work: &mut Work, +) -> Result>, 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( + node: Option<&Arc>>, + index: usize, + work: &mut Work, +) -> Result, 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( + node: &Arc>, + index: usize, + entry: Entry, + work: &mut Work, +) -> Result>, 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 { + 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) -> Vec { + (0..sequence.len()) + .map(|index| sequence.get(index).unwrap().value().as_ref().clone()) + .collect() + } + + fn height(sequence: &Sequence) -> u32 { + sequence.root().map_or(0, |root| root.height()) + } + + fn shared_nodes(left: &Arc>, right: &Arc>) -> 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(node: &Arc>) -> (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::::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::>(); + 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::>() + ); + 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)); + } +}