Skip to main content

runtime/
elision.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! What a cut says, and where a cut may land.
7//!
8//! Four places in this crate remove bytes from a text: the frozen
9//! prefix, the result pipeline, the compactor and the sieve. They share
10//! two facts, and both live here.
11//!
12//! **One sentence for "bytes were taken away".** A reader — a person or
13//! a model — meets the same wording wherever it happens, and
14//! `replay::rebuild_prefix` reproduces a recorded prefix byte for byte
15//! because the sentence has one author. The marker is English and
16//! ASCII: it faces the model's English window.
17//!
18//! **One rule for where a cut lands, and it answers with a value.** A
19//! byte budget and a cut in the middle of a character produce bytes
20//! nothing downstream can read, so every cut moves to a character
21//! boundary, and the answer is a `Boundary`: the bytes on each side
22//! of the cut, each holding whole characters. An offset and the text it
23//! indexes are two facts that can disagree, and `str::get` answers the
24//! disagreement with `None`, which a caller then reads as "nothing was
25//! kept here"; the two sides cannot disagree with the text they came
26//! from.
27//!
28//! The count a marker carries is **source bytes removed**, and the
29//! marker itself is never one of them: add `dropped` to the length of
30//! the text without its marker and the input length comes back, at any
31//! offset a caller computed.
32
33use kernel::ByteLen;
34
35/// Where the bytes that are gone used to be.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Elided {
38    /// Nothing was removed, so the text carries no marker.
39    Nothing,
40    /// The front is gone and the marker opens the text.
41    Head,
42    /// The middle is gone and the marker sits on a line of its own.
43    Middle,
44    /// The tail is gone and the marker closes the text.
45    Tail,
46}
47
48/// One shortened text, with the account of what it no longer carries.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Cut {
51    pub text: String,
52    /// Source bytes removed; the marker's own bytes are not counted.
53    pub dropped: ByteLen,
54    pub place: Elided,
55}
56
57impl Cut {
58    /// The text as it arrived: nothing removed, nothing marked.
59    #[must_use]
60    pub fn whole(text: &str) -> Cut {
61        Cut {
62            text: text.to_owned(),
63            dropped: ByteLen::new(0),
64            place: Elided::Nothing,
65        }
66    }
67}
68
69/// The sentence that stands where the removed bytes were.
70#[must_use]
71pub fn marker(dropped: ByteLen) -> String {
72    let dropped = dropped.get();
73    format!("[truncated: {dropped} bytes]")
74}
75
76/// The same sentence on a line of its own, for bytes removed from
77/// inside a text rather than from one of its ends.
78#[must_use]
79pub fn gap_marker(dropped: ByteLen) -> String {
80    let marker = marker(dropped);
81    format!("\n{marker}\n")
82}
83
84/// How many bytes to reserve for a marker before the count is known.
85///
86/// A cut cannot remove more bytes than the text holds, and the marker
87/// grows only with the digits of its count, so the marker for a text's
88/// whole length is the widest one that text can produce.
89#[must_use]
90pub fn marker_room(text_len: usize) -> usize {
91    marker(saturating_len(text_len)).len()
92}
93
94/// `marker_room` for a marker on a line of its own.
95#[must_use]
96pub fn gap_marker_room(text_len: usize) -> usize {
97    gap_marker(saturating_len(text_len)).len()
98}
99
100/// Where a cut may land in a text: the bytes before the cut and the
101/// bytes from it, each holding whole characters.
102///
103/// A position is this value and never a bare offset, because the two
104/// sides cannot disagree with the text they were taken from while an
105/// offset and its text can. The constructors ask the text for the split
106/// itself — the same question a caller would answer with
107/// `str::is_char_boundary` — so the text's answer is the only one, and
108/// a cut that would land inside a character cannot be constructed.
109#[derive(Debug, Clone, Copy)]
110pub(crate) struct Boundary<'a> {
111    head: &'a str,
112    tail: &'a str,
113}
114
115impl<'a> Boundary<'a> {
116    /// The last position at or before `at`.
117    #[must_use]
118    pub(crate) fn before(text: &'a str, at: usize) -> Boundary<'a> {
119        let mut at = at.min(text.len());
120        loop {
121            // Zero is a boundary of every text, and `at` walks down to
122            // it, so this search ends.
123            if let Some((head, tail)) = text.split_at_checked(at) {
124                return Boundary { head, tail };
125            }
126            at = at.saturating_sub(1);
127        }
128    }
129
130    /// The first position at or after `at`.
131    #[must_use]
132    pub(crate) fn after(text: &'a str, at: usize) -> Boundary<'a> {
133        let mut at = at.min(text.len());
134        loop {
135            // The end of every text is a boundary, and `at` climbs to
136            // it, so this search ends.
137            if let Some((head, tail)) = text.split_at_checked(at) {
138                return Boundary { head, tail };
139            }
140            at = at.saturating_add(1);
141        }
142    }
143
144    /// The bytes before the cut.
145    #[must_use]
146    pub(crate) fn head(self) -> &'a str {
147        self.head
148    }
149
150    /// The bytes from the cut on.
151    #[must_use]
152    pub(crate) fn tail(self) -> &'a str {
153        self.tail
154    }
155
156    /// How far the cut is from the start of the text, in bytes.
157    #[must_use]
158    pub(crate) fn offset(self) -> usize {
159        self.head.len()
160    }
161}
162
163/// The largest character boundary at or before `at`.
164///
165/// `Boundary::before` is this offset's one author; a caller that wants
166/// the bytes on either side rather than the offset asks there.
167#[must_use]
168pub fn boundary_before(text: &str, at: usize) -> usize {
169    Boundary::before(text, at).offset()
170}
171
172/// The smallest character boundary at or after `at`.
173#[must_use]
174pub fn boundary_after(text: &str, at: usize) -> usize {
175    Boundary::after(text, at).offset()
176}
177
178/// A length this machine can address, as a byte count this city can
179/// hold. A text longer than `u64` cannot exist on a machine whose
180/// pointers are 64 bits, so the saturation is unreachable rather than a
181/// decision anybody has to review.
182fn saturating_len(len: usize) -> ByteLen {
183    ByteLen::new(u64::try_from(len).unwrap_or(u64::MAX))
184}
185
186/// The cut a caller assembles: the bytes before `front`, the marker,
187/// and the bytes from `back` on.
188///
189/// Both ends move to a character boundary — back for `front`, forward
190/// for `back` — and the far end is never left behind the near one. The
191/// removed count is then the distance between two positions the text
192/// itself reported, which is what makes the marker's number true by
193/// construction, and why the count and the survivors always add back up
194/// into the input.
195#[must_use]
196pub fn splice(text: &str, front: usize, back: usize, place: Elided) -> Cut {
197    let back = back.max(front);
198    let from = Boundary::before(text, front);
199    let to = Boundary::after(text, back);
200    let dropped = saturating_len(to.offset().saturating_sub(from.offset()));
201    let mut out = String::new();
202    out.push_str(from.head());
203    match place {
204        Elided::Nothing => {}
205        Elided::Head | Elided::Tail => out.push_str(&marker(dropped)),
206        Elided::Middle => out.push_str(&gap_marker(dropped)),
207    }
208    out.push_str(to.tail());
209    Cut {
210        text: out,
211        dropped,
212        place,
213    }
214}
215
216#[cfg(test)]
217#[allow(
218    clippy::unwrap_used,
219    clippy::expect_used,
220    clippy::panic,
221    clippy::indexing_slicing,
222    clippy::arithmetic_side_effects,
223    reason = "test code"
224)]
225mod tests {
226    use super::*;
227    use proptest::prelude::*;
228
229    #[test]
230    fn the_count_in_the_marker_is_the_bytes_the_reader_lost() {
231        let text = "0123456789";
232        let cut = splice(text, 3, 8, Elided::Middle);
233        assert_eq!(cut.dropped, ByteLen::new(5));
234        assert_eq!(cut.text, format!("012{}89", gap_marker(ByteLen::new(5))));
235        let carried = cut.text.len() - gap_marker(ByteLen::new(5)).len();
236        assert_eq!(
237            carried + 5,
238            text.len(),
239            "the marker is not a source byte, so the two add back up"
240        );
241    }
242
243    #[test]
244    fn reserved_room_is_never_less_than_the_marker_that_arrives() {
245        for len in [0usize, 1, 9, 10, 4_096, 1_000_000] {
246            let room = marker_room(len);
247            for dropped in [0u64, 1, 9, 10] {
248                let actual = marker(ByteLen::new(dropped.min(saturating_len(len).get())));
249                assert!(actual.len() <= room, "a marker wider than its reservation");
250            }
251            assert!(gap_marker_room(len) > room, "the gap marker owns two lines");
252        }
253    }
254
255    #[test]
256    fn a_cut_never_lands_inside_a_character() {
257        let text = "字字字";
258        for at in 0..=text.len() {
259            assert!(text.is_char_boundary(boundary_before(text, at)));
260            assert!(text.is_char_boundary(boundary_after(text, at)));
261        }
262        assert_eq!(boundary_before(text, 100), text.len());
263        assert_eq!(boundary_after(text, 100), text.len());
264    }
265
266    /// A caller that computed an offset without asking for a boundary
267    /// must not lose bytes the marker does not count: the two numbers a
268    /// cut reports are the input split in two.
269    #[test]
270    fn a_cut_keeps_the_bytes_it_was_given_at_any_offset() {
271        let text = "字字字"; // three characters, nine bytes
272        for front in 0..=text.len() {
273            for back in front..=text.len() {
274                let cut = splice(text, front, back, Elided::Tail);
275                let carried = cut.text.len() - marker(cut.dropped).len();
276                assert_eq!(
277                    carried + usize::try_from(cut.dropped.get()).unwrap_or(usize::MAX),
278                    text.len(),
279                    "front {front} back {back} lost {} bytes and reported {}",
280                    text.len() - carried,
281                    cut.dropped.get()
282                );
283            }
284        }
285    }
286
287    proptest! {
288        #![proptest_config(ProptestConfig::with_cases(64))]
289
290        /// The same statement over every text and every pair of offsets,
291        /// including a pair a caller inverted.
292        #[test]
293        fn the_count_and_the_survivors_add_back_up_to_the_input(
294            text in "[ -~\u{1b}字]{0,80}",
295            front in 0usize..300,
296            back in 0usize..300,
297            place in prop::sample::select(vec![
298                Elided::Nothing, Elided::Head, Elided::Middle, Elided::Tail,
299            ]),
300        ) {
301            let cut = splice(&text, front, back, place);
302            let own = match place {
303                Elided::Middle => gap_marker(cut.dropped),
304                Elided::Nothing => String::new(),
305                Elided::Head | Elided::Tail => marker(cut.dropped),
306            };
307            let carried = cut.text.len().saturating_sub(own.len());
308            prop_assert_eq!(
309                carried + usize::try_from(cut.dropped.get()).unwrap_or(usize::MAX),
310                text.len()
311            );
312        }
313    }
314}