Skip to main content

fresh/model/
piece_tree_diff.rs

1use std::ops::Range;
2use std::sync::Arc;
3
4use crate::model::piece_tree::{LeafData, PieceTreeNode};
5
6/// Summary of differences between two piece tree roots.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct PieceTreeDiff {
9    /// Whether the two trees represent identical piece sequences.
10    pub equal: bool,
11    /// Changed byte ranges in the "after" tree (exclusive end). Empty when `equal` is true.
12    pub byte_ranges: Vec<Range<usize>>,
13    /// Number of tree nodes visited during the diff walk.
14    /// When `Arc::ptr_eq` short-circuits effectively, this should be
15    /// proportional to the edited region, not the entire tree.
16    pub nodes_visited: usize,
17}
18
19/// Compute a diff between two piece tree roots.
20///
21/// Uses structural sharing (Arc::ptr_eq) to skip identical subtrees in O(1),
22/// falling back to leaf-level comparison only for subtrees that actually differ.
23/// After path-copying edits, this is O(changed_path) instead of O(all_leaves).
24pub fn diff_piece_trees(before: &Arc<PieceTreeNode>, after: &Arc<PieceTreeNode>) -> PieceTreeDiff {
25    // Fast path: identical subtree (same Arc pointer)
26    if Arc::ptr_eq(before, after) {
27        return PieceTreeDiff {
28            equal: true,
29            byte_ranges: Vec::new(),
30            nodes_visited: 1,
31        };
32    }
33
34    // Collect leaves only from differing subtrees using structural walk.
35    // Spans include document-absolute byte offsets.
36    let mut before_spans = Vec::new();
37    let mut after_spans = Vec::new();
38    let mut nodes_visited: usize = 0;
39    let mut before_doc_offset: usize = 0;
40    let mut after_doc_offset: usize = 0;
41    diff_collect_leaves(
42        before,
43        after,
44        &mut before_spans,
45        &mut after_spans,
46        &mut nodes_visited,
47        &mut before_doc_offset,
48        &mut after_doc_offset,
49    );
50
51    let before_spans = normalize_spans(before_spans);
52    let after_spans = normalize_spans(after_spans);
53
54    // Fast-path: identical leaf sequences (same content, different tree structure).
55    if span_slices_equal(&before_spans, &after_spans) {
56        return PieceTreeDiff {
57            equal: true,
58            byte_ranges: Vec::new(),
59            nodes_visited,
60        };
61    }
62
63    // Longest common prefix at byte granularity.
64    let prefix = common_prefix_bytes(&before_spans, &after_spans);
65    // Longest common suffix without overlapping prefix.
66    let suffix = common_suffix_bytes(&before_spans, &after_spans, prefix);
67
68    let ranges = collect_diff_ranges(&before_spans, &after_spans, prefix, suffix);
69
70    PieceTreeDiff {
71        equal: false,
72        byte_ranges: ranges,
73        nodes_visited,
74    }
75}
76
77/// Total bytes stored under a node (without calling private methods).
78fn node_bytes(node: &PieceTreeNode) -> usize {
79    match node {
80        PieceTreeNode::Internal {
81            left_bytes, right, ..
82        } => left_bytes + node_bytes(right),
83        PieceTreeNode::Leaf { bytes, .. } => *bytes,
84    }
85}
86
87/// Parallel tree walk that uses Arc::ptr_eq to skip identical subtrees.
88/// Only collects leaves from subtrees that differ between before and after.
89/// Tracks running document byte offsets so collected spans have absolute positions.
90fn diff_collect_leaves(
91    before: &Arc<PieceTreeNode>,
92    after: &Arc<PieceTreeNode>,
93    before_out: &mut Vec<Span>,
94    after_out: &mut Vec<Span>,
95    nodes_visited: &mut usize,
96    before_doc_offset: &mut usize,
97    after_doc_offset: &mut usize,
98) {
99    *nodes_visited += 2; // counting both before and after nodes
100
101    // Identical subtree - skip entirely, advancing document offsets
102    if Arc::ptr_eq(before, after) {
103        let bytes = node_bytes(before);
104        *before_doc_offset += bytes;
105        *after_doc_offset += bytes;
106        return;
107    }
108
109    match (before.as_ref(), after.as_ref()) {
110        // Both internal: recurse into children
111        (
112            PieceTreeNode::Internal {
113                left: b_left,
114                right: b_right,
115                ..
116            },
117            PieceTreeNode::Internal {
118                left: a_left,
119                right: a_right,
120                ..
121            },
122        ) => {
123            diff_collect_leaves(
124                b_left,
125                a_left,
126                before_out,
127                after_out,
128                nodes_visited,
129                before_doc_offset,
130                after_doc_offset,
131            );
132            diff_collect_leaves(
133                b_right,
134                a_right,
135                before_out,
136                after_out,
137                nodes_visited,
138                before_doc_offset,
139                after_doc_offset,
140            );
141        }
142        // Structure mismatch - fall back to full leaf collection for both subtrees
143        _ => {
144            collect_leaves_with_offsets(before, before_out, nodes_visited, before_doc_offset);
145            collect_leaves_with_offsets(after, after_out, nodes_visited, after_doc_offset);
146        }
147    }
148}
149
150fn collect_leaves_with_offsets(
151    node: &Arc<PieceTreeNode>,
152    out: &mut Vec<Span>,
153    nodes_visited: &mut usize,
154    doc_offset: &mut usize,
155) {
156    *nodes_visited += 1;
157    match node.as_ref() {
158        PieceTreeNode::Internal { left, right, .. } => {
159            collect_leaves_with_offsets(left, out, nodes_visited, doc_offset);
160            collect_leaves_with_offsets(right, out, nodes_visited, doc_offset);
161        }
162        PieceTreeNode::Leaf {
163            location,
164            offset,
165            bytes,
166            line_feed_cnt,
167        } => {
168            let leaf = LeafData::new(*location, *offset, *bytes, *line_feed_cnt);
169            out.push(Span {
170                leaf,
171                doc_offset: *doc_offset,
172            });
173            *doc_offset += bytes;
174        }
175    }
176}
177
178#[derive(Clone)]
179struct Span {
180    leaf: LeafData,
181    doc_offset: usize,
182}
183
184fn spans_equal(a: &Span, b: &Span) -> bool {
185    a.leaf.location == b.leaf.location
186        && a.leaf.offset == b.leaf.offset
187        && a.leaf.bytes == b.leaf.bytes
188}
189
190fn span_slices_equal(a: &[Span], b: &[Span]) -> bool {
191    a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| spans_equal(x, y))
192}
193
194fn normalize_spans(spans: Vec<Span>) -> Vec<Span> {
195    if spans.is_empty() {
196        return spans;
197    }
198
199    let mut it = spans.into_iter();
200    let mut current = it.next().unwrap();
201    let mut normalized = Vec::new();
202
203    for span in it {
204        let contiguous = current.leaf.location == span.leaf.location
205            && current.leaf.offset + current.leaf.bytes == span.leaf.offset
206            && current.doc_offset + current.leaf.bytes == span.doc_offset;
207        if contiguous {
208            current.leaf.bytes += span.leaf.bytes;
209            current.leaf.line_feed_cnt = match (current.leaf.line_feed_cnt, span.leaf.line_feed_cnt)
210            {
211                (Some(a), Some(b)) => Some(a + b),
212                _ => None,
213            };
214        } else {
215            normalized.push(current);
216            current = span;
217        }
218    }
219
220    normalized.push(current);
221    normalized
222}
223
224fn common_prefix_bytes(before: &[Span], after: &[Span]) -> usize {
225    let mut b_idx = 0;
226    let mut a_idx = 0;
227    let mut b_off = 0;
228    let mut a_off = 0;
229    let mut consumed = 0;
230
231    while b_idx < before.len() && a_idx < after.len() {
232        let b_span = &before[b_idx];
233        let a_span = &after[a_idx];
234        let b = &b_span.leaf;
235        let a = &a_span.leaf;
236
237        let b_pos = b.offset + b_off;
238        let a_pos = a.offset + a_off;
239
240        // Must also ensure they are at the same document relative position
241        // if they were separated by gaps.
242        if b.location == a.location
243            && b_pos == a_pos
244            && (b_span.doc_offset + b_off) == (a_span.doc_offset + a_off)
245        {
246            let b_rem = b.bytes - b_off;
247            let a_rem = a.bytes - a_off;
248            let take = b_rem.min(a_rem);
249
250            consumed += take;
251            b_off += take;
252            a_off += take;
253
254            if b_off == b.bytes {
255                b_idx += 1;
256                b_off = 0;
257            }
258            if a_off == a.bytes {
259                a_idx += 1;
260                a_off = 0;
261            }
262        } else {
263            break;
264        }
265    }
266
267    consumed
268}
269
270fn common_suffix_bytes(before: &[Span], after: &[Span], prefix_bytes: usize) -> usize {
271    let total_before = before
272        .last()
273        .map(|s| s.doc_offset + s.leaf.bytes)
274        .unwrap_or(0);
275    let total_after = after
276        .last()
277        .map(|s| s.doc_offset + s.leaf.bytes)
278        .unwrap_or(0);
279
280    let mut b_idx: isize = before.len() as isize - 1;
281    let mut a_idx: isize = after.len() as isize - 1;
282    let mut b_off = 0;
283    let mut a_off = 0;
284    let mut consumed = 0;
285
286    while b_idx >= 0
287        && a_idx >= 0
288        && (total_before - consumed) > prefix_bytes
289        && (total_after - consumed) > prefix_bytes
290    {
291        let b_span = &before[b_idx as usize];
292        let a_span = &after[a_idx as usize];
293        let b_leaf = &b_span.leaf;
294        let a_leaf = &a_span.leaf;
295
296        let b_pos = b_leaf.offset + b_leaf.bytes - b_off;
297        let a_pos = a_leaf.offset + a_leaf.bytes - a_off;
298
299        // Compare by buffer identity only (location + offset). Suffix bytes are
300        // at different doc_offsets in before vs after when insertions/deletions
301        // change the total size, but they still contain the same data.
302        if b_leaf.location == a_leaf.location && b_pos == a_pos {
303            let b_rem = b_leaf.bytes - b_off;
304            let a_rem = a_leaf.bytes - a_off;
305            let take = b_rem.min(a_rem);
306
307            consumed += take;
308            b_off += take;
309            a_off += take;
310
311            if b_off == b_leaf.bytes {
312                b_idx -= 1;
313                b_off = 0;
314            }
315            if a_off == a_leaf.bytes {
316                a_idx -= 1;
317                a_off = 0;
318            }
319        } else {
320            break;
321        }
322    }
323
324    consumed.min(total_after.saturating_sub(prefix_bytes))
325}
326
327fn collect_diff_ranges(
328    before: &[Span],
329    after: &[Span],
330    prefix: usize,
331    suffix: usize,
332) -> Vec<Range<usize>> {
333    let mut ranges = Vec::new();
334    let mut b_idx = 0;
335    let mut a_idx = 0;
336    let mut b_off = 0;
337    let mut a_off = 0;
338    let mut matched_prefix = 0;
339
340    // Skip matching prefix
341    while matched_prefix < prefix && b_idx < before.len() && a_idx < after.len() {
342        let b = &before[b_idx].leaf;
343        let a = &after[a_idx].leaf;
344        let b_rem = b.bytes - b_off;
345        let a_rem = a.bytes - a_off;
346        let take = b_rem.min(a_rem).min(prefix - matched_prefix);
347        matched_prefix += take;
348        b_off += take;
349        a_off += take;
350        if b_off == b.bytes {
351            b_idx += 1;
352            b_off = 0;
353        }
354        if a_off == a.bytes {
355            a_idx += 1;
356            a_off = 0;
357        }
358    }
359
360    // compare_limit in document-absolute space: end of collected spans minus suffix
361    let doc_end = after
362        .last()
363        .map(|s| s.doc_offset + s.leaf.bytes)
364        .unwrap_or(0);
365    let compare_limit = doc_end.saturating_sub(suffix);
366
367    // prefix_start in document-absolute space
368    let doc_start = after.first().map(|s| s.doc_offset).unwrap_or(0);
369
370    let mut current_start: Option<usize> = None;
371    let mut current_end: usize = 0;
372
373    while a_idx < after.len() {
374        let a = &after[a_idx];
375        let pos = a.doc_offset + a_off;
376        if pos >= compare_limit {
377            break;
378        }
379
380        // If we have a current range but there's a gap in document offset,
381        // it means there was an identical subtree that was skipped.
382        // We must break the range here.
383        if let Some(start) = current_start {
384            if pos > current_end {
385                ranges.push(start..current_end);
386                current_start = None;
387            }
388        }
389
390        let matches = if b_idx < before.len() {
391            let b = &before[b_idx].leaf;
392            let b_pos = b.offset + b_off;
393            let a_pos = a.leaf.offset + a_off;
394            // Compare by buffer identity only. Insertions/deletions shift
395            // doc_offsets, but matching buffer location + offset means same data.
396            b.location == a.leaf.location && b_pos == a_pos
397        } else {
398            false
399        };
400
401        if matches {
402            if let Some(start) = current_start.take() {
403                ranges.push(start..current_end);
404            }
405
406            let b = &before[b_idx].leaf;
407            let b_rem = b.bytes - b_off;
408            let a_rem = a.leaf.bytes - a_off;
409            let take = b_rem.min(a_rem).min(compare_limit.saturating_sub(pos));
410
411            b_off += take;
412            a_off += take;
413
414            if b_off == b.bytes {
415                b_idx += 1;
416                b_off = 0;
417            }
418            if a_off == a.leaf.bytes {
419                a_idx += 1;
420                a_off = 0;
421            }
422        } else {
423            if current_start.is_none() {
424                current_start = Some(pos);
425            }
426            let take = (a.leaf.bytes - a_off).min(compare_limit.saturating_sub(pos));
427            current_end = pos + take;
428            a_off += take;
429            if a_off == a.leaf.bytes {
430                a_idx += 1;
431                a_off = 0;
432            }
433        }
434    }
435
436    if let Some(start) = current_start {
437        ranges.push(start..current_end);
438    }
439
440    // Any trailing unmatched "after" spans up to suffix boundary
441    while a_idx < after.len() {
442        let start = after[a_idx].doc_offset + a_off;
443        if start >= compare_limit {
444            break;
445        }
446        let end = (after[a_idx].doc_offset + after[a_idx].leaf.bytes).min(compare_limit);
447        ranges.push(start..end);
448        a_idx += 1;
449        a_off = 0;
450    }
451
452    if ranges.is_empty() {
453        // Anchor range: either there's content between prefix and suffix, or
454        // the trees differ only in size (deletion) — report the anchor point.
455        ranges.push((doc_start + prefix)..compare_limit);
456    }
457
458    ranges
459}
460
461#[cfg(test)]
462#[allow(clippy::single_range_in_vec_init)]
463mod tests {
464    use super::*;
465    use crate::model::piece_tree::BufferLocation;
466
467    fn sum_bytes(leaves: &[LeafData]) -> usize {
468        leaves.iter().map(|leaf| leaf.bytes).sum()
469    }
470
471    fn leaf(loc: BufferLocation, offset: usize, bytes: usize, lfs: Option<usize>) -> LeafData {
472        LeafData::new(loc, offset, bytes, lfs)
473    }
474
475    // Minimal balanced builder for tests.
476    fn build(leaves: &[LeafData]) -> Arc<PieceTreeNode> {
477        if leaves.is_empty() {
478            return Arc::new(PieceTreeNode::Leaf {
479                location: BufferLocation::Stored(0),
480                offset: 0,
481                bytes: 0,
482                line_feed_cnt: Some(0),
483            });
484        }
485        if leaves.len() == 1 {
486            let l = leaves[0];
487            return Arc::new(PieceTreeNode::Leaf {
488                location: l.location,
489                offset: l.offset,
490                bytes: l.bytes,
491                line_feed_cnt: l.line_feed_cnt,
492            });
493        }
494
495        let mid = leaves.len() / 2;
496        let left = build(&leaves[..mid]);
497        let right = build(&leaves[mid..]);
498
499        Arc::new(PieceTreeNode::Internal {
500            left_bytes: sum_bytes(&leaves[..mid]),
501            lf_left: leaves[..mid]
502                .iter()
503                .map(|l| l.line_feed_cnt)
504                .try_fold(0usize, |acc, v| v.map(|b| acc + b)),
505            left,
506            right,
507        })
508    }
509
510    #[test]
511    fn detects_identical_trees() {
512        let leaves = vec![leaf(BufferLocation::Stored(0), 0, 10, Some(0))];
513        let before = build(&leaves);
514        let after = build(&leaves);
515
516        let diff = diff_piece_trees(&before, &after);
517        assert!(diff.equal);
518        assert!(diff.byte_ranges.is_empty());
519    }
520
521    #[test]
522    fn detects_single_line_change() {
523        let before = build(&[leaf(BufferLocation::Stored(0), 0, 5, Some(0))]);
524        let after = build(&[leaf(BufferLocation::Added(1), 0, 5, Some(0))]);
525
526        let diff = diff_piece_trees(&before, &after);
527        assert!(!diff.equal);
528        assert_eq!(diff.byte_ranges, vec![0..5]);
529    }
530
531    #[test]
532    fn tracks_newlines_in_changed_span() {
533        let before = build(&[leaf(BufferLocation::Stored(0), 0, 6, Some(0))]);
534        let after = build(&[leaf(BufferLocation::Added(1), 0, 6, Some(1))]);
535
536        let diff = diff_piece_trees(&before, &after);
537        assert!(!diff.equal);
538        assert_eq!(diff.byte_ranges, vec![0..6]);
539    }
540
541    #[test]
542    fn handles_deletion_by_marking_anchor() {
543        let before = build(&[
544            leaf(BufferLocation::Stored(0), 0, 6, Some(1)),
545            leaf(BufferLocation::Stored(0), 6, 4, Some(0)),
546        ]);
547        let after = build(&[leaf(BufferLocation::Stored(0), 0, 6, Some(1))]);
548
549        let diff = diff_piece_trees(&before, &after);
550        assert!(!diff.equal);
551        assert_eq!(diff.byte_ranges, vec![6..6]);
552    }
553
554    /// Uses real PieceTree::insert (path-copy) near EOF.
555    /// The diff must produce document-absolute offsets.
556    #[test]
557    fn diff_after_path_copy_insert_at_eof() {
558        use crate::model::piece_tree::{PieceTree, StringBuffer};
559
560        let chunk_size = 1000;
561        let total = 10_000;
562        let content: Vec<u8> = (0..total)
563            .map(|i| if i % 100 == 99 { b'\n' } else { b'A' })
564            .collect();
565        let buf = StringBuffer::new_loaded(0, content, false);
566
567        let mut saved_tree = PieceTree::new(BufferLocation::Stored(0), 0, total, None);
568        saved_tree.split_leaves_to_chunk_size(chunk_size);
569        let lf_updates: Vec<(usize, usize)> = (0..10).map(|i| (i, 10)).collect();
570        saved_tree.update_leaf_line_feeds(&lf_updates);
571        let saved_root = saved_tree.root();
572
573        let mut after_tree = saved_tree;
574        let insert_offset = total - 100;
575        let insert_buf = StringBuffer::new_loaded(1, b"HELLO".to_vec(), false);
576        after_tree.insert(
577            insert_offset,
578            BufferLocation::Added(1),
579            0,
580            5,
581            Some(0),
582            &[buf, insert_buf],
583        );
584        let after_root = after_tree.root();
585
586        let diff = diff_piece_trees(&saved_root, &after_root);
587        assert!(!diff.equal);
588
589        assert!(
590            diff.byte_ranges[0].start >= total - 200,
591            "byte_ranges should be document-absolute (near EOF): got {:?}, expected near {}",
592            diff.byte_ranges,
593            insert_offset,
594        );
595    }
596
597    /// After rebalance, Arc sharing is destroyed. The diff must still produce
598    /// the same byte_ranges as the path-copy version.
599    #[test]
600    fn diff_after_rebalance_matches_path_copy_diff() {
601        use crate::model::piece_tree::{PieceTree, StringBuffer};
602
603        let chunk_size = 1000;
604        let total = 10_000;
605        let content: Vec<u8> = (0..total)
606            .map(|i| if i % 100 == 99 { b'\n' } else { b'A' })
607            .collect();
608        let buf = StringBuffer::new_loaded(0, content, false);
609
610        let mut saved_tree = PieceTree::new(BufferLocation::Stored(0), 0, total, None);
611        saved_tree.split_leaves_to_chunk_size(chunk_size);
612        let lf_updates: Vec<(usize, usize)> = (0..10).map(|i| (i, 10)).collect();
613        saved_tree.update_leaf_line_feeds(&lf_updates);
614        let saved_root = saved_tree.root();
615
616        let mut after_tree = saved_tree;
617        let insert_buf = StringBuffer::new_loaded(1, b"HELLO".to_vec(), false);
618        after_tree.insert(
619            total - 100,
620            BufferLocation::Added(1),
621            0,
622            5,
623            Some(0),
624            &[buf.clone(), insert_buf.clone()],
625        );
626
627        let diff_shared = diff_piece_trees(&saved_root, &after_tree.root());
628
629        after_tree.rebalance();
630        let diff_rebalanced = diff_piece_trees(&saved_root, &after_tree.root());
631
632        assert!(!diff_shared.equal);
633        assert!(!diff_rebalanced.equal);
634        assert_eq!(
635            diff_shared.byte_ranges, diff_rebalanced.byte_ranges,
636            "byte_ranges should be identical whether or not Arc sharing exists"
637        );
638    }
639
640    #[test]
641    fn tolerates_split_leaves_with_same_content_prefix() {
642        let before = build(&[leaf(BufferLocation::Stored(0), 0, 100, Some(1))]);
643        let after = build(&[
644            leaf(BufferLocation::Stored(0), 0, 50, Some(0)),
645            leaf(BufferLocation::Added(1), 0, 10, Some(0)),
646            leaf(BufferLocation::Stored(0), 50, 50, Some(1)),
647        ]);
648
649        let diff = diff_piece_trees(&before, &after);
650        assert!(!diff.equal);
651        assert_eq!(diff.byte_ranges, vec![50..60]);
652    }
653
654    #[test]
655    fn diff_with_disjoint_changes() {
656        let leaf1_before = leaf(BufferLocation::Stored(0), 0, 10, Some(0));
657        let leaf2 = leaf(BufferLocation::Stored(0), 10, 10, Some(1));
658        let leaf3_before = leaf(BufferLocation::Stored(0), 20, 10, Some(0));
659
660        let leaf1_after = leaf(BufferLocation::Added(1), 0, 10, Some(0));
661        let leaf3_after = leaf(BufferLocation::Added(1), 10, 10, Some(0));
662
663        let leaf2_arc = Arc::new(PieceTreeNode::Leaf {
664            location: leaf2.location,
665            offset: leaf2.offset,
666            bytes: leaf2.bytes,
667            line_feed_cnt: leaf2.line_feed_cnt,
668        });
669
670        let before = Arc::new(PieceTreeNode::Internal {
671            left_bytes: 10,
672            lf_left: Some(0),
673            left: Arc::new(PieceTreeNode::Leaf {
674                location: leaf1_before.location,
675                offset: leaf1_before.offset,
676                bytes: leaf1_before.bytes,
677                line_feed_cnt: leaf1_before.line_feed_cnt,
678            }),
679            right: Arc::new(PieceTreeNode::Internal {
680                left_bytes: 10,
681                lf_left: Some(1),
682                left: Arc::clone(&leaf2_arc),
683                right: Arc::new(PieceTreeNode::Leaf {
684                    location: leaf3_before.location,
685                    offset: leaf3_before.offset,
686                    bytes: leaf3_before.bytes,
687                    line_feed_cnt: leaf3_before.line_feed_cnt,
688                }),
689            }),
690        });
691
692        let after = Arc::new(PieceTreeNode::Internal {
693            left_bytes: 10,
694            lf_left: Some(0),
695            left: Arc::new(PieceTreeNode::Leaf {
696                location: leaf1_after.location,
697                offset: leaf1_after.offset,
698                bytes: leaf1_after.bytes,
699                line_feed_cnt: leaf1_after.line_feed_cnt,
700            }),
701            right: Arc::new(PieceTreeNode::Internal {
702                left_bytes: 10,
703                lf_left: Some(1),
704                left: Arc::clone(&leaf2_arc), // Shared!
705                right: Arc::new(PieceTreeNode::Leaf {
706                    location: leaf3_after.location,
707                    offset: leaf3_after.offset,
708                    bytes: leaf3_after.bytes,
709                    line_feed_cnt: leaf3_after.line_feed_cnt,
710                }),
711            }),
712        });
713
714        let diff = diff_piece_trees(&before, &after);
715
716        assert_eq!(diff.byte_ranges, vec![0..10, 20..30]);
717    }
718}