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}