Skip to main content

frazaro_core/
api.rs

1//! The translate API (PORT.6, slice 6g): VLA_Browser.bas's two functions,
2//! the surface a door calls - the C-ABI export (6h), the web page, any
3//! embedding. Text in, text out, and a refusal back as the teaching text the
4//! add-in shows, as a value instead of a dialog.
5//!
6//! `EnglishTranslateTextToVla` is `EnglishResetGrammar`, then
7//! `EnglishLoadVocabularyText` for each phrasebook text under the name
8//! `vocab-N` (N from 1, in the order given), then `EnglishToVla`;
9//! `EnglishTranslateTextToVba` is the same and then `VlaTranspile` with the
10//! prelude set by `VlaSetPreludeOverride`. One difference the port cannot
11//! avoid: the reference's VLA stage reads its prelude from the workbook (for
12//! `ValidateRawVla`'s probe and a generator's expansion), and a host-free core
13//! has no workbook, so both functions take the prelude's text.
14//!
15//! The text path is ungated, as the reference's is (`TestRawConsentTextPathUngated`):
16//! SEC.2's consent for a `(raw ...)` form and F.10's capability check are a
17//! door's questions, asked before this is called, with
18//! `english::vocab::vocab_text_has_raw_form` and
19//! `english::vocab::vocab_requires_check_capability`, as the CLI asks them.
20
21use crate::english::program::RefusalAtLine;
22use crate::english::vocab::{vocab_requires_check_capability, vocab_text_has_raw_form};
23use crate::english::Grammar;
24use crate::messages::{raise, Refusal};
25
26/// VBA `EnglishTranslateTextToVla`: the program's VLA text, or the refusal
27/// with its id and the program line it stands on (0 when a phrasebook was
28/// refused at load, whose text carries its own `vocab-N line L` location).
29pub fn english_translate_text_to_vla(
30    program_text: &str,
31    prelude_text: &str,
32    vocab_texts: &[&str],
33) -> Result<String, RefusalAtLine> {
34    let g = load_grammar(prelude_text, vocab_texts)?;
35    g.translate_program_at(program_text).map(|t| t.vla)
36}
37
38/// VBA `EnglishTranslateTextToVba`: the program's VBA text, or the English
39/// stage's refusal as above; a refusal of the compile stage carries line 0
40/// and names its VLA line in its own text.
41pub fn english_translate_text_to_vba(
42    program_text: &str,
43    prelude_text: &str,
44    vocab_texts: &[&str],
45) -> Result<String, RefusalAtLine> {
46    let g = load_grammar(prelude_text, vocab_texts)?;
47    let t = g.translate_program_at(program_text)?;
48    crate::emit::compile(&t.vla, prelude_text).map_err(|refusal| RefusalAtLine { refusal, line: 0 })
49}
50
51/// The writer's surface (PORT.7): the program translated as
52/// [`english_translate_text_to_vla`] translates it, then built into a
53/// workbook's bytes (`build::build_xlsx`); the English stage's refusal with
54/// its line, or the build's with line 0.
55pub fn english_build_xlsx(
56    program_text: &str,
57    prelude_text: &str,
58    vocab_texts: &[&str],
59) -> Result<Vec<u8>, RefusalAtLine> {
60    let g = load_grammar(prelude_text, vocab_texts)?;
61    let t = g.translate_program_at(program_text)?;
62    crate::build::build_xlsx(program_text, &t.vla, prelude_text, vocab_texts)
63        .map_err(|refusal| RefusalAtLine { refusal, line: 0 })
64}
65
66/// `frazaro rebuild` (PORT.7, slice 7c): a built workbook's sentences read
67/// back out of its `Frazaro` sheet, its stamp checked against them and
68/// against the prelude and phrasebooks given, the workbook built again from
69/// them and compared whole. The refusal is `rebuild-not-a-build` for a file
70/// that is not a build this core can verify; a build that does not match is
71/// not a refusal but an answer, `Rebuilt::matches` false with its reason.
72pub fn english_rebuild_xlsx(
73    file: &[u8],
74    prelude_text: &str,
75    vocab_texts: &[&str],
76) -> Result<crate::build::Rebuilt, RefusalAtLine> {
77    use crate::build::{build_workbook_with, own_part_text, Rebuilt};
78    use crate::sheet::merge::HostInfo;
79    use crate::sheet::ooxml::{sheet_part, sheet_xml, Render};
80
81    let read =
82        crate::build::read_build(file).map_err(|refusal| RefusalAtLine { refusal, line: 0 })?;
83    let partial = read.stamp.into.is_some();
84    let answer = |matches: bool, why: String| Rebuilt {
85        sentences: read.sentences,
86        built_by: read.stamp.version.clone(),
87        matches,
88        why,
89        partial,
90    };
91    if let Some(why) = read
92        .stamp
93        .disagreement(&read.program_text, prelude_text, vocab_texts)
94    {
95        return Ok(answer(false, why));
96    }
97    let version_note = || {
98        if read.stamp.version != crate::VERSION {
99            format!(
100                "it was built by Frazaro {} and this is Frazaro {}, which writes the file differently",
101                read.stamp.version,
102                crate::VERSION
103            )
104        } else {
105            "its parts are not what this core builds from these sentences: a host has saved it since, or it was built differently".to_string()
106        }
107    };
108    match &read.stamp.into {
109        None => {
110            let again = english_build_xlsx(&read.program_text, prelude_text, vocab_texts)?;
111            let matches = again == file;
112            Ok(answer(
113                matches,
114                if matches {
115                    String::new()
116                } else {
117                    version_note()
118                },
119            ))
120        }
121        Some(into) => {
122            // The model is not at hand, so the build's own sheets are rendered
123            // again with what the stamp recorded and compared part by part.
124            let host = HostInfo {
125                label: "the workbook".to_string(),
126                sheet_names: read.host_sheets.clone(),
127                model_hex: into.model_hex.clone(),
128                style_base: into.style_base,
129                cm: Some(into.cm),
130            };
131            let g = load_grammar(prelude_text, vocab_texts)?;
132            let t = g.translate_program_at(&read.program_text)?;
133            let wb = build_workbook_with(
134                &read.program_text,
135                &t.vla,
136                prelude_text,
137                vocab_texts,
138                Some(&host),
139            )
140            .map_err(|refusal| RefusalAtLine { refusal, line: 0 })?;
141            let render = Render {
142                style_base: into.style_base,
143                cm: into.cm,
144            };
145            let mut matches = true;
146            for (i, sheet) in wb.sheets.iter().enumerate() {
147                let want = sheet_xml(sheet, false, &render);
148                if own_part_text(file, &sheet_part(i + 1)).as_deref() != Some(want.as_str()) {
149                    matches = false;
150                }
151            }
152            if own_part_text(file, &sheet_part(wb.sheets.len() + 1)).is_some() {
153                matches = false; // a sheet the build did not make
154            }
155            Ok(answer(
156                matches,
157                if matches {
158                    String::new()
159                } else {
160                    version_note()
161                },
162            ))
163        }
164    }
165}
166
167/// The writer's surface for adding to a workbook (`--into`): the program
168/// translated and built into `model_bytes`, the model's own parts as they
169/// were. `label` is what a refusal calls the model.
170pub fn english_build_xlsx_into(
171    program_text: &str,
172    prelude_text: &str,
173    vocab_texts: &[&str],
174    model_bytes: Vec<u8>,
175    label: &str,
176) -> Result<Vec<u8>, RefusalAtLine> {
177    let g = load_grammar(prelude_text, vocab_texts)?;
178    let t = g.translate_program_at(program_text)?;
179    crate::build::build_xlsx_into(
180        program_text,
181        &t.vla,
182        prelude_text,
183        vocab_texts,
184        model_bytes,
185        label,
186    )
187    .map_err(|refusal| RefusalAtLine { refusal, line: 0 })
188}
189
190/// The view record (KERNEL.4): the program translated and built into the
191/// sheet model as [`english_build_xlsx`] builds it, nothing written, and one
192/// window of one sheet projected as the lines a viewport draws from
193/// (`view::Grid`, the first projection through the kernel's projections
194/// seam). `sheet` is the sheet's name, compared as Excel compares names;
195/// `window` an A1 rectangle, or `None` for the sheet's whole extent. The
196/// English stage's refusal with its line, or the build's or the view's
197/// (`view-sheet-unknown`, `view-window-not-a-range`) with line 0. Since
198/// KERNEL.7 the model is recalculated under Excel's library
199/// (`crate::excel::library`) and each formula row is followed by its
200/// `value` row, the formula's computed value or `(not-computed "<name>")`.
201pub fn english_view(
202    program_text: &str,
203    prelude_text: &str,
204    vocab_texts: &[&str],
205    sheet: &str,
206    window: Option<&str>,
207) -> Result<String, RefusalAtLine> {
208    let g = load_grammar(prelude_text, vocab_texts)?;
209    let t = g.translate_program_at(program_text)?;
210    let at_line_0 = |refusal| RefusalAtLine { refusal, line: 0 };
211    let model = crate::build::build_workbook(program_text, &t.vla, prelude_text, vocab_texts)
212        .map_err(at_line_0)?;
213    let window = crate::view::window_of(&model, sheet, window).map_err(at_line_0)?;
214    let library = crate::excel::library();
215    let calc = vla_lang::calc::Calc::run(&model, &library);
216    Ok(crate::view::view_text_valued(&model, &window, &calc))
217}
218
219/// Recalculation over a workbook's file (KERNEL.7): the file read through
220/// the reader into the grid, every formula computed by the language's
221/// evaluator under Excel's library, and one `calc` row per formula cell
222/// with the host's cached value and the verdict beside it
223/// (`reflect::calc::calc_text`); `label` is what a refusal calls the file.
224/// Empty for a workbook with no formula.
225pub fn calc_rows(bytes: &[u8], label: &str) -> Result<String, Refusal> {
226    crate::reflect::calc::calc_text(bytes, label, &crate::excel::library())
227}
228
229/// The functions a workbook's formulas call, counted by the formula cells
230/// calling each (`reflect::histogram`), one `function` row a line, most
231/// called first: KERNEL.6's instrument, counts alone.
232pub fn function_histogram(bytes: &[u8], label: &str) -> Result<String, Refusal> {
233    crate::reflect::histogram::histogram_text(bytes, label)
234}
235
236/// The reader's surface (PORT.8, slice 8a): a workbook's bytes, an OOXML
237/// package or an OpenDocument spreadsheet (8e), as the relations of
238/// `REFLECT`, one row a line in the fixed order (`reflect::reflect_text`);
239/// `label` is what a refusal calls the file. Seven of the eight relations,
240/// as the module says; `refers` joins in slice 8b.
241pub fn reflect_relations(bytes: &[u8], label: &str) -> Result<String, Refusal> {
242    crate::reflect::reflect_text(bytes, label)
243}
244
245/// The difference between two workbooks (PORT.8, slice 8c): both files'
246/// bytes read through the reader and compared, the `changed` relation and
247/// the sheets in one file alone, one row a line in the fixed order
248/// (`reflect::diff::diff_text`); each label is what a refusal calls its
249/// file. Empty when the two hold the same sheets, names, Tables and cells.
250pub fn diff_relations(
251    old_bytes: &[u8],
252    old_label: &str,
253    new_bytes: &[u8],
254    new_label: &str,
255) -> Result<String, Refusal> {
256    crate::reflect::diff::diff_text(old_bytes, old_label, new_bytes, new_label)
257}
258
259/// The audit list (PORT.8, slice 8d): a workbook's bytes read through the
260/// reader into the audit's index and the six walks run, one finding a line
261/// in the fixed order (`reflect::audit::audit_text`); `label` is what a
262/// refusal calls the file. Empty when there is nothing to report.
263pub fn audit_findings(bytes: &[u8], label: &str) -> Result<String, Refusal> {
264    crate::reflect::audit::audit_text(bytes, label)
265}
266
267/// `EnglishResetGrammar` and the loads: a fresh grammar over the prelude, each
268/// phrasebook text loaded in order as `vocab-N`, its proofs run as the add-in
269/// runs them, the first refusal ending the call.
270fn load_grammar(prelude_text: &str, vocab_texts: &[&str]) -> Result<Grammar, RefusalAtLine> {
271    let mut g = Grammar::new(prelude_text);
272    for (i, text) in vocab_texts.iter().enumerate() {
273        g.load_vocabulary_text(text, &format!("vocab-{}", i + 1))
274            .map_err(|refusal| RefusalAtLine { refusal, line: 0 })?;
275    }
276    Ok(g)
277}
278
279/// The door's two gates before a phrasebook text it did not ship loads, as
280/// the reference's file loader asks them and its text loader does not
281/// (`EnglishLoadVocabulary`): F.10's capability check, then SEC.2's consent
282/// for a `(raw ...)` form, which a person gives (`--allow-raw`, a checkbox).
283/// `source_name` is the name the refusal shows.
284pub fn vocab_gate(text: &str, source_name: &str, allow_raw: bool) -> Result<(), Refusal> {
285    vocab_requires_check_capability(text, source_name)?;
286    if !allow_raw && vocab_text_has_raw_form(text) {
287        return Err(raise(
288            "english-vocab-raw-consent-declined",
289            &[("source", source_name)],
290        ));
291    }
292    Ok(())
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    const PRELUDE: &str = include_str!("../../scripts/prelude.vla");
300    const ENGLISH: &str = include_str!("../../scripts/polyglotta/english.vla");
301    const ESPANOL: &str = include_str!("../../scripts/polyglotta/espanol.vla");
302    const DANSK: &str = include_str!("../../scripts/polyglotta/dansk.vla");
303    const DEUTSCHE: &str = include_str!("../../scripts/polyglotta/deutsche.vla");
304    const ESPERANTO: &str = include_str!("../../scripts/polyglotta/esperanto.vla");
305    const FRANCAIS: &str = include_str!("../../scripts/polyglotta/francais.vla");
306    const LATIN: &str = include_str!("../../scripts/polyglotta/latin.vla");
307    const PIRATE: &str = include_str!("../../scripts/polyglotta/pirate.vla");
308    const PROGRAM: &str = include_str!("../../scripts/instructions.txt");
309    const GOLDEN_VLA: &str = include_str!("../../scripts/instructions_golden.vla");
310    const GOLDEN_VBA: &str = include_str!("../../scripts/instructions_golden.vba");
311
312    fn normalized(s: &str) -> String {
313        s.replace("\r\n", "\n").trim_end_matches('\n').to_string()
314    }
315
316    /// Oracle 1 through the API: `instructions.txt` to the `.vla` golden less
317    /// its stamp line.
318    #[test]
319    fn to_vla_reproduces_the_translate_golden() {
320        let got = english_translate_text_to_vla(PROGRAM, PRELUDE, &[ENGLISH])
321            .unwrap_or_else(|e| panic!("{}", e.refusal.text));
322        let want = GOLDEN_VLA
323            .split_once('\n')
324            .map(|(_, rest)| rest)
325            .unwrap_or("");
326        assert_eq!(normalized(&got), normalized(want));
327    }
328
329    /// Oracle 1a through the API: `instructions.txt` to the `.vba` golden.
330    #[test]
331    fn to_vba_reproduces_the_compile_golden() {
332        let got = english_translate_text_to_vba(PROGRAM, PRELUDE, &[ENGLISH])
333            .unwrap_or_else(|e| panic!("{}", e.refusal.text));
334        assert_eq!(normalized(&got), normalized(GOLDEN_VBA));
335    }
336
337    #[test]
338    fn a_phrasebook_refusal_names_the_browser_s_source() {
339        let e = english_translate_text_to_vla("Log 1.", PRELUDE, &[ENGLISH, "hello"]).unwrap_err();
340        assert_eq!(e.refusal.id, "english-vocab-expected-directive");
341        // The reader gives a bare atom at the top level no line, so the
342        // location says line 0: the reference's own reading, which the
343        // refusal golden pins (scripts/refusals_golden.txt).
344        assert!(
345            e.refusal.text.starts_with("vocab-2 line 0: "),
346            "{}",
347            e.refusal.text
348        );
349        assert_eq!(e.line, 0);
350    }
351
352    #[test]
353    fn a_program_refusal_carries_its_line_and_to_vba_passes_it_through() {
354        let program = "Log 1.\nSet total to $5.";
355        let e = english_translate_text_to_vla(program, PRELUDE, &[ENGLISH]).unwrap_err();
356        assert_eq!(
357            (e.refusal.id.as_str(), e.line),
358            ("english-unknown-character", 2)
359        );
360        let e2 = english_translate_text_to_vba(program, PRELUDE, &[ENGLISH]).unwrap_err();
361        assert_eq!(e2, e);
362        let vba = english_translate_text_to_vba("Log 1.", PRELUDE, &[ENGLISH])
363            .unwrap_or_else(|e| panic!("{}", e.refusal.text));
364        assert!(vba.contains("Debug.Print 1"), "{vba}");
365    }
366
367    /// The web page's language picker loads a dialect after english.vla, as
368    /// the add-in loads an edition; each dialect's first proof sentence is
369    /// the picker's example, and must read over the base.
370    #[test]
371    fn every_dialect_loads_over_english_and_reads_its_example() {
372        let cases = [
373            ("dansk", DANSK, "Saet formlen \"=B2*2\" i cellen B3."),
374            (
375                "deutsche",
376                DEUTSCHE,
377                "Setze die Formel \"=B2*2\" in Zelle B3.",
378            ),
379            (
380                "espanol",
381                ESPANOL,
382                "Pon la formula \"=B2*2\" en la celda B3.",
383            ),
384            (
385                "esperanto",
386                ESPERANTO,
387                "Metu la formulon \"=B2*2\" en la chelon B3.",
388            ),
389            (
390                "francais",
391                FRANCAIS,
392                "Mets la formule \"=B2*2\" dans la cellule B3.",
393            ),
394            ("latin", LATIN, "Pone formulam \"=B2*2\" in cellula B3."),
395            (
396                "pirate",
397                PIRATE,
398                "Chart the course \"=B2*2\" onto the cell B3, arr.",
399            ),
400        ];
401        for (name, book, sentence) in cases {
402            let vla = english_translate_text_to_vla(sentence, PRELUDE, &[ENGLISH, book])
403                .unwrap_or_else(|e| panic!("{name}: {}", e.refusal.text));
404            assert!(
405                vla.contains("(range \"b3\")") && vla.contains("\"=B2*2\""),
406                "{name}: {vla}"
407            );
408        }
409    }
410
411    #[test]
412    fn the_gate_asks_capability_first_then_consent() {
413        let raw = "(english-vla \"char cell {r:cell}\" (raw \"Debug.Print 2\"))";
414        let e = vocab_gate(raw, "pasted.vla", false).unwrap_err();
415        assert_eq!(e.id, "english-vocab-raw-consent-declined");
416        assert!(
417            e.text.starts_with("'pasted.vla' was not loaded"),
418            "{}",
419            e.text
420        );
421        assert!(vocab_gate(raw, "pasted.vla", true).is_ok());
422        let both = "(requires-capability \"network\")\n(english-vla \"x\" (raw \"y\"))";
423        let e = vocab_gate(both, "pasted.vla", true).unwrap_err();
424        assert_eq!(e.id, "english-vocab-requires-capability-ungranted");
425        assert!(vocab_gate(
426            "(english-vla \"wobble {x:expr}\" (debug-print {x}))",
427            "p",
428            false
429        )
430        .is_ok());
431    }
432
433    #[test]
434    fn phrasebooks_load_in_the_order_given_and_the_text_path_is_ungated() {
435        // A dialect over the base, as the add-in loads them.
436        let vla =
437            english_translate_text_to_vla("Pon 5 en la celda B2.", PRELUDE, &[ENGLISH, ESPANOL])
438                .unwrap_or_else(|e| panic!("{}", e.refusal.text));
439        assert!(vla.contains("(set! (range \"b2\") 5)"), "{vla}");
440        // SEC.2: the text path asks no consent; a door asks before calling.
441        let book = "(english-vla \"char cell {r:cell}\" (raw \"Debug.Print 2\"))";
442        let vla = english_translate_text_to_vla("Char cell A1.", PRELUDE, &[book])
443            .unwrap_or_else(|e| panic!("{}", e.refusal.text));
444        assert!(vla.contains("(raw \"Debug.Print 2\")"), "{vla}");
445    }
446
447    const FIXTURE: &str = include_str!("../../scripts/build/fixture.txt");
448    const INTO: &str = include_str!("../../scripts/build/into.txt");
449
450    /// A golden as the door prints it: the files are CRLF on disk.
451    fn golden(text: &str) -> String {
452        text.replace("\r\n", "\n")
453    }
454
455    /// Oracle 11 through the API (KERNEL.4). The test is here, and not with
456    /// the view in `vla-lang`, since it builds through English (PORT.12).
457    #[test]
458    fn the_view_goldens_are_reproduced() {
459        for (program, sheet, window, golden_text, path) in [
460            (
461                FIXTURE,
462                "Frazaro",
463                None,
464                include_str!("../../scripts/view/fixture_frazaro.vla"),
465                "scripts/view/fixture_frazaro.vla",
466            ),
467            (
468                FIXTURE,
469                "Output",
470                Some("A1:F20"),
471                include_str!("../../scripts/view/fixture_output.vla"),
472                "scripts/view/fixture_output.vla",
473            ),
474            (
475                FIXTURE,
476                "Output",
477                Some("B2:C3"),
478                include_str!("../../scripts/view/fixture_output_b2_c3.vla"),
479                "scripts/view/fixture_output_b2_c3.vla",
480            ),
481            (
482                FIXTURE,
483                "Data",
484                None,
485                include_str!("../../scripts/view/fixture_data.vla"),
486                "scripts/view/fixture_data.vla",
487            ),
488            (
489                INTO,
490                "Output",
491                None,
492                include_str!("../../scripts/view/into_output.vla"),
493                "scripts/view/into_output.vla",
494            ),
495            (
496                INTO,
497                "Checks",
498                None,
499                include_str!("../../scripts/view/into_checks.vla"),
500                "scripts/view/into_checks.vla",
501            ),
502        ] {
503            let got = english_view(program, PRELUDE, &[ENGLISH], sheet, window)
504                .unwrap_or_else(|r| panic!("{path}: {}", r.refusal));
505            let want = golden(golden_text);
506            if got != want {
507                let at = got
508                    .lines()
509                    .zip(want.lines())
510                    .position(|(a, b)| a != b)
511                    .map(|i| i + 1)
512                    .unwrap_or(got.lines().count().min(want.lines().count()) + 1);
513                panic!(
514                    "{path} differs at line {at}: got {:?}, want {:?}",
515                    got.lines().nth(at - 1),
516                    want.lines().nth(at - 1)
517                );
518            }
519        }
520    }
521
522    /// The free oracle (web/CALLOSUM.md section 8, slice 1): the window is
523    /// drawn from the model, the file is written from the model, and the
524    /// reader reads the file; the cell and formula rows agree row for row,
525    /// and the sheet rows too.
526    #[test]
527    fn the_view_of_a_built_model_is_the_reflect_of_its_file() {
528        use crate::kernel::Window;
529        use crate::reflect::print::quoted;
530        use crate::view::{view_text, whole};
531        for (program, label) in [(FIXTURE, "fixture.txt"), (INTO, "into.txt")] {
532            let vla = english_translate_text_to_vla(program, PRELUDE, &[ENGLISH])
533                .unwrap_or_else(|r| panic!("{label}: {}", r.refusal));
534            let model = crate::build::build_workbook(program, &vla, PRELUDE, &[ENGLISH])
535                .unwrap_or_else(|r| panic!("{label}: {r}"));
536            let bytes = crate::sheet::ooxml::workbook_bytes(&model).unwrap();
537            let reflected = crate::reflect::reflect_text(&bytes, label).unwrap();
538            let file_sheets: Vec<&str> = reflected
539                .lines()
540                .filter(|l| l.starts_with("(sheet "))
541                .collect();
542            let mut seen = 0;
543            for sheet in &model.sheets {
544                let view = view_text(
545                    &model,
546                    &Window {
547                        sheet: sheet.name.clone(),
548                        range: whole(sheet),
549                    },
550                );
551                let view_sheets: Vec<&str> =
552                    view.lines().filter(|l| l.starts_with("(sheet ")).collect();
553                assert_eq!(view_sheets, file_sheets, "{label}: the sheet rows");
554                let body: Vec<&str> = view
555                    .lines()
556                    .filter(|l| l.starts_with("(cell ") || l.starts_with("(formula "))
557                    .collect();
558                let prefix_cell = format!("(cell {} ", quoted(&sheet.name));
559                let prefix_formula = format!("(formula {} ", quoted(&sheet.name));
560                let read: Vec<&str> = reflected
561                    .lines()
562                    .filter(|l| l.starts_with(&prefix_cell) || l.starts_with(&prefix_formula))
563                    .collect();
564                assert_eq!(body, read, "{label}: sheet {}", sheet.name);
565                seen += body.len();
566            }
567            assert!(seen > 10, "{label}: the comparison covered {seen} rows");
568        }
569    }
570}