Skip to main content

vla_lang/
view.rs

1//! The view record (KERNEL.4, 2026-10-07): one window of the sheet model as
2//! lines, the first implementation of the kernel's projections seam
3//! ([`crate::projection::Projection`]; the design is `web/CALLOSUM.md` §7,
4//! decisions 1 and 2, and §8, slice 1).
5//!
6//! A view is a pure function of the model and a window: no handle kept, no
7//! state, nothing read back from the `.xlsx` bytes. The model is the one the
8//! writer builds (`build::build_workbook`), so a door that translates and
9//! builds a program draws it without writing a file, and a viewport
10//! (KERNEL.5) draws from this record alone. One form a line, in the proof
11//! corpus's notation, as `reflect` prints a file:
12//!
13//! ```text
14//! (sheet "Frazaro" visible)          every sheet of the model, in tab order, with
15//!                                    its own state: visible, hidden, very-hidden
16//! (window "Output" "A1:F20")         the window, its sheet as the model spells it
17//! (extent "Output" "B1:D4")          the sheet's used rectangle, or none
18//! (gridlines "Output" on)
19//! (column "Frazaro" "B" 72 shown 1)  a column of the window with settings:
20//!                                    width or none, shown or hidden, format or none
21//! (row "Board" 3 30 shown)           a row of the window with settings (KERNEL.22):
22//!                                    height or none, shown or hidden
23//! (format 1 "F7F4FC" text nowrap)    format 0 and every format the window uses:
24//!                                    fill or none, general, text or a code of
25//!                                    Excel's in quotes (KERNEL.47), wrap or nowrap
26//! (look "Board" 1 2)                 the sheet's look map (KERNEL.22): a cell
27//!                                    holding the value draws with the format
28//! (cell "Output" "B2" 5)             a value, spelled as reflect spells it
29//! (formula "Output" "B3" "=B2*2")    a formula's text as the bar shows it
30//! (value "Output" "B3" 10)           its computed value, once values exist
31//! (text "Output" "B3" "10.00")       a number's text under its format's code
32//!                                    (KERNEL.47), after its cell or value row
33//! (style "Output" "B2" 1)            the cell's format, when it is not 0
34//! (sentence "Output" "B2" 4)         the row of the sentence that wrote the cell
35//! ```
36//!
37//! The cells come in row-major order, each `cell` or `formula` row followed
38//! by its `value` row (a formula's, once values exist), its `text` row, its
39//! `style` row and its `sentence` row. A `text` row is the text Excel draws
40//! for the cell's number, or for its text under a code's fourth section,
41//! under the code of the format the cell draws with, its look's where its
42//! value has one (KERNEL.47; `crate::numfmt`): so a viewport draws the text
43//! and never respells a value. A record whose formats hold no code prints
44//! none. The `cell` and `formula` rows are the reader's own,
45//! spelled by `reflect::print`, which is the free oracle: the view of a
46//! built model's sheet, whole, is `reflect`'s reading of the file the
47//! writer writes from that model, row for row. A `cell` row is a value the
48//! sentences put there, a `formula` row is text the host would compute, and
49//! the record never shows one as the other. Since recalculation landed
50//! (KERNEL.7, 2026-10-08; `Alonzo/SPEC.md` section 4.5 and decision 14) a
51//! `(value "<sheet>" "<addr>" <v>)` row follows each formula row when the
52//! caller computed the model ([`view_text_valued`], a [`Grid`] holding a
53//! [`Calc`]): the value spelled as a `cell` row's is, or
54//! `(not-computed "<name>")` for a formula this version does not compute,
55//! naming the function or the construct, `(not-computed cycle)` for one in
56//! a cycle. The bare [`view_text`] prints no value row, which is what a
57//! grid shows before its first step.
58//!
59//! The second projection, the values ([`ValueGrid`], `values`; KERNEL.37,
60//! 2026-10-12), is the same record with its `formula` rows left out: every
61//! other row the grid's, in the grid's order, so that for any window it is
62//! the grid record with its `(formula ` lines removed, byte for byte. It is
63//! what an engine's host draws every frame ([`view_text_values`]), a
64//! formula's text being the bar's, one cell at a time, where the grid moves
65//! every shared child's text from its master at every view. A formula with
66//! no value yet prints no row of its own, its `style` and `sentence` rows
67//! kept; a shared formula's cell whose master the sheet has lost prints
68//! nothing in either record. Both read a sheet's `extent` row from the
69//! extent their values keep ([`Values::kept_extent`]), where they keep one.
70//!
71//! The changes of a window (KERNEL.42, 2026-10-15) are an engine's machine's
72//! to answer, since they need two frames, and are printed here: a `changes`
73//! row naming the sheet, the window, the frame they are since and the frame
74//! they are at, and how many cells changed, then each changed cell's rows as
75//! the values print them, by the same code, in the same order; a cell that
76//! now prints no row is `(empty "<sheet>" "<addr>")`. Where the machine
77//! cannot say which cells changed, the row ends `whole` and none follows,
78//! and the host views the window. A host holding the window's values at the
79//! first frame, and replacing each named cell's rows with the rows given it,
80//! holds the window's values at the second, byte for byte.
81
82use std::collections::{BTreeSet, HashMap};
83
84use crate::calc::{self, Extent, Values};
85use crate::machine::load::Datum;
86use crate::messages::{raise, Refusal};
87use crate::projection::{Projection, Window};
88use crate::rows::{line as relation, quoted};
89use crate::rows::{Row, Value};
90use crate::sheet::{
91    cell_ref, column_letters, formula_text, number_text, parse_a1_range, shared_masters, A1Range,
92    Cell, Column, Content, NumFmt, Sheet, Style, Styles, Workbook,
93};
94
95/// The grid: the view record of one window, the first projection. With the
96/// values of a recalculation over the same model (a [`crate::calc::Calc`],
97/// or an engine's machine since KERNEL.22), each formula row is followed by
98/// its `value` row; without them, the record shows formulas as text alone.
99#[derive(Clone, Copy, Debug, Default)]
100pub struct Grid<'v> {
101    pub values: Option<&'v dyn Values>,
102}
103
104impl<'v> Grid<'v> {
105    /// The grid over a computed model.
106    pub fn valued(values: &'v dyn Values) -> Grid<'v> {
107        Grid {
108            values: Some(values),
109        }
110    }
111}
112
113impl Projection for Grid<'_> {
114    fn name(&self) -> &str {
115        "grid"
116    }
117
118    fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
119        self.rows(model, window, true, out);
120    }
121}
122
123/// The values: the view record of one window with its `formula` rows left
124/// out, the second projection (KERNEL.37). With the values of a
125/// recalculation or an engine's machine, each formula's `value` row stands
126/// where the grid prints it, after the row the grid prints before it; the
127/// record of a window holding no formula is the grid's.
128#[derive(Clone, Copy, Debug, Default)]
129pub struct ValueGrid<'v> {
130    pub values: Option<&'v dyn Values>,
131}
132
133impl<'v> ValueGrid<'v> {
134    /// The values of a computed model.
135    pub fn valued(values: &'v dyn Values) -> ValueGrid<'v> {
136        ValueGrid {
137            values: Some(values),
138        }
139    }
140}
141
142impl Projection for ValueGrid<'_> {
143    fn name(&self) -> &str {
144        "values"
145    }
146
147    fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
148        Grid {
149            values: self.values,
150        }
151        .rows(model, window, false, out);
152    }
153}
154
155impl Grid<'_> {
156    /// The record's rows of one window: every row the grid prints, or with
157    /// `formulas` false every row but the `formula` rows, which is the values
158    /// (KERNEL.37).
159    fn rows(&self, model: &Workbook, window: &Window, formulas: bool, out: &mut dyn FnMut(&str)) {
160        for s in &model.sheets {
161            out(&relation(&Row::Sheet {
162                name: &s.name,
163                visibility: s.visibility,
164            }));
165        }
166        let Some(i) = model.find_sheet(&window.sheet) else {
167            // A window onto no sheet of the model, which `window_of` never
168            // makes: the sheet rows and the window asked for, nothing more.
169            out(&format!(
170                "(window {} {})",
171                quoted(&window.sheet),
172                quoted(&window.range.text())
173            ));
174            return;
175        };
176        let sheet = &model.sheets[i];
177        let name = sheet.name.as_str();
178        let range = window.range;
179        out(&format!(
180            "(window {} {})",
181            quoted(name),
182            quoted(&range.text())
183        ));
184        match extent_in(sheet, i, self.values) {
185            Some(e) => out(&format!("(extent {} {})", quoted(name), quoted(&e.text()))),
186            None => out(&format!("(extent {} none)", quoted(name))),
187        }
188        out(&format!(
189            "(gridlines {} {})",
190            quoted(name),
191            if sheet.gridlines { "on" } else { "off" }
192        ));
193        let mut columns: Vec<&Column> = sheet
194            .columns
195            .iter()
196            .filter(|c| c.index >= range.left && c.index <= range.right)
197            .collect();
198        columns.sort_by_key(|c| c.index);
199        let mut used: BTreeSet<u32> = BTreeSet::new();
200        used.insert(0);
201        for c in &columns {
202            let width = c
203                .width
204                .and_then(number_text)
205                .unwrap_or_else(|| "none".to_string());
206            let style = match c.style {
207                Some(s) => {
208                    used.insert(s);
209                    s.to_string()
210                }
211                None => "none".to_string(),
212            };
213            out(&format!(
214                "(column {} {} {} {} {})",
215                quoted(name),
216                quoted(&column_letters(c.index)),
217                width,
218                if c.hidden { "hidden" } else { "shown" },
219                style
220            ));
221        }
222        // The rows of the window with settings, the column rows' twins
223        // (KERNEL.22; Alonzo/SPEC.md decision 19).
224        for (n, r) in sheet.rows.range(range.top..=range.bottom) {
225            let height = r
226                .height
227                .and_then(number_text)
228                .unwrap_or_else(|| "none".to_string());
229            out(&format!(
230                "(row {} {} {} {})",
231                quoted(name),
232                n,
233                height,
234                if r.hidden { "hidden" } else { "shown" }
235            ));
236        }
237        let cells: Vec<(&(u32, u32), &Cell)> = sheet
238            .cells
239            .range((range.top, 0)..=(range.bottom, u32::MAX))
240            .filter(|((_, col), _)| *col >= range.left && *col <= range.right)
241            .collect();
242        for (_, cell) in &cells {
243            used.insert(cell.style);
244        }
245        for (_, index) in &sheet.looks {
246            used.insert(*index);
247        }
248        for index in used {
249            out(&format_row(index, model.styles.xfs().get(index as usize)));
250        }
251        // The sheet's look map, a value to the format a cell holding it draws
252        // with (KERNEL.22; Alonzo/SPEC.md decision 19).
253        for (value, index) in &sheet.looks {
254            out(&format!("(look {} {} {})", quoted(name), value, index));
255        }
256        // The sheet's shared formulas' masters, found by one walk of the
257        // sheet at the first cell that needs them (KERNEL.37): the grid moves
258        // a child's text from its master; the values ask only whether a
259        // child with no value has one.
260        let mut masters = None;
261        // A sheet whose cells are newer where its values say (KERNEL.27): an
262        // engine's twin, read through its values' own content.
263        let lags = self.values.filter(|v| v.lags(i));
264        let coded = Coded::of(model, sheet);
265        #[cfg(test)]
266        crate::sheet::work::viewed(cells.len());
267        for (&(row, col), cell) in cells {
268            self.cell_rows(
269                sheet,
270                i,
271                (row, col),
272                cell,
273                lags,
274                formulas,
275                &mut masters,
276                coded.as_ref(),
277                out,
278            );
279        }
280    }
281
282    /// One cell's rows, as the record prints them (KERNEL.42: the changes
283    /// print a changed cell's by this code too): its `cell` row, or its
284    /// `formula` row where the record has them and its `value` row once it
285    /// has one, then its `text` row where its value draws under a code
286    /// (KERNEL.47), then its `style` and `sentence` rows; whether it printed
287    /// any. `lags` are the values of a sheet whose cells are newer where they
288    /// say, `masters` the sheet's shared formulas', found once, and `coded`
289    /// what a `text` row needs, where the style table holds a code.
290    #[allow(clippy::too_many_arguments)]
291    fn cell_rows<'s>(
292        &self,
293        sheet: &'s Sheet,
294        i: usize,
295        (row, col): (u32, u32),
296        cell: &Cell,
297        lags: Option<&dyn Values>,
298        formulas: bool,
299        masters: &mut Option<HashMap<u32, (&'s str, u32, u32)>>,
300        coded: Option<&Coded<'_>>,
301        out: &mut dyn FnMut(&str),
302    ) -> bool {
303        let name = sheet.name.as_str();
304        let addr = cell_ref(row, col);
305        let newer = lags.and_then(|v| v.content((i, row, col)));
306        // Whether the cell printed a row, and the value a code draws.
307        let (printed, shown) = match newer.as_ref().unwrap_or(&cell.content) {
308            Content::Text(t) => {
309                let value = Value::Text(t.clone());
310                out(&relation(&Row::Cell {
311                    sheet: name,
312                    addr: &addr,
313                    value: &value,
314                }));
315                (true, Some(calc::Value::Text(t.clone())))
316            }
317            Content::Number(n) => {
318                // What the writer puts in the file: the number's text,
319                // or Excel's #NUM! for one no cell can hold.
320                let value = match number_text(*n) {
321                    Some(text) => Value::Number(text),
322                    None => Value::Error("#NUM!".to_string()),
323                };
324                out(&relation(&Row::Cell {
325                    sheet: name,
326                    addr: &addr,
327                    value: &value,
328                }));
329                (true, Some(calc::Value::Number(*n)))
330            }
331            Content::Bool(b) => {
332                let value = Value::Bool(*b);
333                out(&relation(&Row::Cell {
334                    sheet: name,
335                    addr: &addr,
336                    value: &value,
337                }));
338                (true, None)
339            }
340            Content::Error(e) => {
341                let value = Value::Error(e.clone());
342                out(&relation(&Row::Cell {
343                    sheet: name,
344                    addr: &addr,
345                    value: &value,
346                }));
347                (true, None)
348            }
349            other => {
350                let computed = self.values.and_then(|calc| calc.computed((i, row, col)));
351                // A formula with a text: every one but a shared formula's
352                // cell whose master the sheet has lost, which shows
353                // nothing in either record.
354                let held = if formulas {
355                    let walked = masters.get_or_insert_with(|| shared_masters(sheet));
356                    match formula_text(other, row, col, walked) {
357                        Some(text) => {
358                            let text = format!("={text}");
359                            out(&relation(&Row::Formula {
360                                sheet: name,
361                                addr: &addr,
362                                text: &text,
363                            }));
364                            true
365                        }
366                        None => false,
367                    }
368                } else {
369                    match other {
370                        // A value is only ever computed for a formula with
371                        // a text, so a child with one has its master; a
372                        // child without one asks the sheet.
373                        Content::SharedChild { si } if computed.is_none() => masters
374                            .get_or_insert_with(|| shared_masters(sheet))
375                            .contains_key(si),
376                        _ => true,
377                    }
378                };
379                let mut shown = None;
380                if let Some(computed) = computed.filter(|_| held) {
381                    out(&format!(
382                        "(value {} {} {})",
383                        quoted(name),
384                        quoted(&addr),
385                        computed.spell()
386                    ));
387                    // The value as the row spells it: an empty result is 0.
388                    if let calc::Computed::Value(v) = computed {
389                        shown = Some(v.clone().settled());
390                    }
391                }
392                (held, shown)
393            }
394        };
395        if !printed {
396            return false;
397        }
398        if let (Some(coded), Some(v)) = (coded, shown) {
399            if let Some(text) = coded.text(cell.style, &v) {
400                out(&format!(
401                    "(text {} {} {})",
402                    quoted(name),
403                    quoted(&addr),
404                    quoted(&text)
405                ));
406            }
407        }
408        if cell.style != 0 {
409            out(&format!(
410                "(style {} {} {})",
411                quoted(name),
412                quoted(&addr),
413                cell.style
414            ));
415        }
416        if let Some(r) = sheet.sentences.get(&(row, col)) {
417            out(&format!(
418                "(sentence {} {} {})",
419                quoted(name),
420                quoted(&addr),
421                r
422            ));
423        }
424        true
425    }
426}
427
428/// A `format` row: the index, the fill as six hex digits or `none`, the
429/// number format, `general`, `text` or a code quoted (KERNEL.47), and
430/// whether text wraps. An index the style table does not hold reads as the
431/// default format.
432fn format_row(index: u32, style: Option<&Style>) -> String {
433    let default = Style::default();
434    let s = style.unwrap_or(&default);
435    let fill = match s.fill {
436        Some(rgb) => quoted(&format!("{:02X}{:02X}{:02X}", rgb.0, rgb.1, rgb.2)),
437        None => "none".to_string(),
438    };
439    let num = match &s.num_fmt {
440        NumFmt::General => "general".to_string(),
441        NumFmt::Text => "text".to_string(),
442        NumFmt::Code(code) => quoted(code.as_str()),
443    };
444    format!(
445        "(format {index} {fill} {num} {})",
446        if s.wrap { "wrap" } else { "nowrap" }
447    )
448}
449
450/// What a record needs for a cell's `text` row (KERNEL.47): the style table
451/// and the sheet's looks read back from their spellings, made once a view
452/// and only where the table holds a code, so that a record whose formats
453/// hold none prints as it did and finds no look.
454struct Coded<'m> {
455    styles: &'m Styles,
456    looks: Vec<(Look, u32)>,
457}
458
459/// A look's value, as a value a code draws meets it: a number by its value,
460/// so that 7 and 7.0 are one and so are 0 and -0, and a text by its
461/// characters, the viewport's rule (Alonzo's `SPEC.md` §3.1). A look of a
462/// truth value or an error matches no value a code draws.
463enum Look {
464    Number(f64),
465    Text(String),
466}
467
468impl<'m> Coded<'m> {
469    fn of(model: &'m Workbook, sheet: &Sheet) -> Option<Coded<'m>> {
470        if !model.styles.has_code() {
471            return None;
472        }
473        let looks = sheet
474            .looks
475            .iter()
476            .filter_map(|(spelled, index)| {
477                let forms = crate::reader::read_forms(spelled).ok()?;
478                let look = match crate::machine::load::datum(forms.first()?)? {
479                    Datum::Number(n) => Look::Number(n),
480                    Datum::Text(t) => Look::Text(t),
481                    _ => return None,
482                };
483                Some((look, *index))
484            })
485            .collect();
486        Some(Coded {
487            styles: &model.styles,
488            looks,
489        })
490    }
491
492    /// The text a value draws as in a cell of format `style`: under its
493    /// look's format in place of the cell's own where its value has one, as
494    /// the viewport draws a look (Alonzo's `SPEC.md` decision 19), the first
495    /// look of the record's that it meets; `None` where that format holds no
496    /// code or the code draws the value as the record spells it.
497    fn text(&self, style: u32, value: &calc::Value) -> Option<String> {
498        let met = self.looks.iter().find(|(look, _)| match (look, value) {
499            (Look::Number(a), calc::Value::Number(b)) => a == b,
500            (Look::Text(a), calc::Value::Text(b)) => a == b,
501            _ => false,
502        });
503        let index = met.map_or(style, |(_, index)| *index);
504        match self.styles.get(index).map(|s| &s.num_fmt) {
505            Some(NumFmt::Code(code)) => code.text(value),
506            _ => None,
507        }
508    }
509}
510
511/// The sheet's used rectangle, or `None` for a sheet with no cell.
512fn extent_of(sheet: &Sheet) -> Option<A1Range> {
513    rectangle(sheet.extent())
514}
515
516/// The used rectangle of sheet `i`, read from the extent `values` keep
517/// where they keep one, else walked (KERNEL.37).
518fn extent_in(sheet: &Sheet, i: usize, values: Option<&dyn Values>) -> Option<A1Range> {
519    match values.and_then(|v| v.kept_extent(i)) {
520        Some(kept) => rectangle(kept),
521        None => extent_of(sheet),
522    }
523}
524
525/// An extent as a rectangle of cells.
526fn rectangle(extent: Extent) -> Option<A1Range> {
527    extent.map(|((top, left), (bottom, right))| A1Range {
528        top,
529        left,
530        bottom,
531        right,
532    })
533}
534
535/// The window over a whole sheet: its extent, or `A1` alone for a sheet
536/// with no cell.
537pub fn whole(sheet: &Sheet) -> A1Range {
538    or_a1(extent_of(sheet))
539}
540
541/// A rectangle, or `A1` alone for none.
542fn or_a1(range: Option<A1Range>) -> A1Range {
543    range.unwrap_or(A1Range {
544        top: 1,
545        left: 1,
546        bottom: 1,
547        right: 1,
548    })
549}
550
551/// The window a door asked for, against the model: the sheet by name as
552/// Excel compares names, without case, spelled in the window as the model
553/// spells it; the range as written (`A1:F20`, `b2`, corners in any order),
554/// or the sheet's whole extent when none is given. A sheet the model does
555/// not hold is refused naming the ones it does (`view-sheet-unknown`); a
556/// text that is not a rectangle of cells, a whole column or row included,
557/// is refused as written (`view-window-not-a-range`).
558pub fn window_of(model: &Workbook, sheet: &str, range: Option<&str>) -> Result<Window, Refusal> {
559    window_over(model, sheet, range, None)
560}
561
562/// [`window_of`], a whole sheet's extent read from the extent `values` keep
563/// where they keep one (KERNEL.37): the window an engine's machine views.
564pub(crate) fn window_over(
565    model: &Workbook,
566    sheet: &str,
567    range: Option<&str>,
568    values: Option<&dyn Values>,
569) -> Result<Window, Refusal> {
570    let Some(i) = model.find_sheet(sheet) else {
571        let sheets = model
572            .sheets
573            .iter()
574            .map(|s| s.name.as_str())
575            .collect::<Vec<_>>()
576            .join(", ");
577        return Err(raise(
578            "view-sheet-unknown",
579            &[("name", sheet), ("sheets", &sheets)],
580        ));
581    };
582    let s = &model.sheets[i];
583    let range = match range {
584        Some(text) => parse_a1_range(text)
585            .ok_or_else(|| raise("view-window-not-a-range", &[("text", text)]))?,
586        None => or_a1(extent_in(s, i, values)),
587    };
588    Ok(Window {
589        sheet: s.name.clone(),
590        range,
591    })
592}
593
594/// The view record of one window as text, each line ending in a line feed:
595/// the API's surface, the tests' and a door's that holds the text.
596pub fn view_text(model: &Workbook, window: &Window) -> String {
597    let mut out = String::new();
598    Grid::default().project(model, window, &mut |line| {
599        out.push_str(line);
600        out.push('\n');
601    });
602    out
603}
604
605/// The view record with each formula's computed value after it: the record
606/// a door prints once it has recalculated the model (`frazaro view` through
607/// the core's Excel library; an engine after a step). `values` must be over
608/// `model`.
609pub fn view_text_valued(model: &Workbook, window: &Window, values: &dyn Values) -> String {
610    let mut out = String::new();
611    Grid::valued(values).project(model, window, &mut |line| {
612        out.push_str(line);
613        out.push('\n');
614    });
615    out
616}
617
618/// The values record of one window (KERNEL.37): [`view_text_valued`]'s
619/// record with its `formula` lines left out, what an engine's host draws
620/// every frame. `values` must be over `model`.
621pub fn view_text_values(model: &Workbook, window: &Window, values: &dyn Values) -> String {
622    let mut out = String::new();
623    ValueGrid::valued(values).project(model, window, &mut |line| {
624        out.push_str(line);
625        out.push('\n');
626    });
627    out
628}
629
630/// The changes of one window (KERNEL.42) as text, each line ending in a
631/// line feed: `(changes "<sheet>" "<range>" <since> <at> <n>)`, the window
632/// as its values record names it, then the rows of the `n` cells `cells`
633/// names, row by row, each as the values print it, a cell that prints none
634/// `(empty "<sheet>" "<addr>")`; or, `cells` being `None`, the row ending
635/// `whole` and no other. An engine's machine says which cells changed, and
636/// only it can (`crate::machine::Machine::view`); `values` must be over
637/// `model`, and `cells` inside the window, in row-major order.
638pub(crate) fn changes_text(
639    model: &Workbook,
640    window: &Window,
641    values: &dyn Values,
642    (since, at): (u64, u64),
643    cells: Option<&[(u32, u32)]>,
644) -> String {
645    let Some(i) = model.find_sheet(&window.sheet) else {
646        return String::new();
647    };
648    let sheet = &model.sheets[i];
649    let head = |n: &str| {
650        format!(
651            "(changes {} {} {since} {at} {n})\n",
652            quoted(&sheet.name),
653            quoted(&window.range.text())
654        )
655    };
656    let Some(cells) = cells else {
657        return head("whole");
658    };
659    let mut out = head(&cells.len().to_string());
660    #[cfg(test)]
661    crate::sheet::work::viewed(cells.len());
662    let grid = Grid::valued(values);
663    let lags = Some(values).filter(|v| v.lags(i));
664    let coded = Coded::of(model, sheet);
665    let mut masters = None;
666    let mut push = |line: &str| {
667        out.push_str(line);
668        out.push('\n');
669    };
670    for &(row, col) in cells {
671        let printed = sheet.cells.get(&(row, col)).is_some_and(|cell| {
672            grid.cell_rows(
673                sheet,
674                i,
675                (row, col),
676                cell,
677                lags,
678                false,
679                &mut masters,
680                coded.as_ref(),
681                &mut push,
682            )
683        });
684        if !printed {
685            push(&format!(
686                "(empty {} {})",
687                quoted(&sheet.name),
688                quoted(&cell_ref(row, col))
689            ));
690        }
691    }
692    out
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698    use crate::sheet::{Rgb, Styles};
699
700    fn range(text: &str) -> A1Range {
701        parse_a1_range(text).unwrap()
702    }
703
704    fn lines(text: &str) -> Vec<&str> {
705        text.lines().collect()
706    }
707
708    /// A small model made by hand: two sheets, the first with columns and
709    /// formats, a shared formula, a dynamic one, a truth value, a number no
710    /// cell can hold, and the rows of the sentences that wrote them.
711    fn a_model() -> Workbook {
712        let mut wb = Workbook::new();
713        let mut styles = Styles::new();
714        let lavender = styles.id(Style {
715            fill: Some(Rgb(247, 244, 252)),
716            num_fmt: NumFmt::Text,
717            wrap: false,
718        });
719        let gray = styles.id(Style {
720            fill: Some(Rgb(242, 242, 242)),
721            num_fmt: NumFmt::General,
722            wrap: true,
723        });
724        wb.styles = styles;
725        let mut model = Sheet::new("Model");
726        model.gridlines = false;
727        model.columns = vec![
728            Column {
729                index: 3,
730                width: Some(60.0),
731                hidden: false,
732                style: Some(gray),
733            },
734            Column {
735                index: 1,
736                width: None,
737                hidden: true,
738                style: None,
739            },
740            Column {
741                index: 2,
742                width: Some(72.0),
743                hidden: false,
744                style: Some(lavender),
745            },
746        ];
747        model.set(
748            1,
749            2,
750            Cell {
751                content: Content::Text("Revenue".to_string()),
752                style: lavender,
753            },
754        );
755        model.set(
756            2,
757            2,
758            Cell {
759                content: Content::Number(1200.5),
760                style: 0,
761            },
762        );
763        model.set(
764            3,
765            2,
766            Cell {
767                content: Content::Bool(true),
768                style: 0,
769            },
770        );
771        model.set(
772            4,
773            2,
774            Cell {
775                content: Content::Number(f64::INFINITY),
776                style: 0,
777            },
778        );
779        model.set_formula(range("C2:C4"), "B2*2", 0);
780        model.set_formula_dynamic(range("D2"), "_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")", 0);
781        model.wrote(range("B1"), 1);
782        model.wrote(range("B2:B4"), 2);
783        model.wrote(range("C2:C4"), 3);
784        model.wrote(range("D2"), 4);
785        wb.sheets.push(model);
786        wb.sheets.push(Sheet::new("Empty"));
787        wb
788    }
789
790    #[test]
791    fn the_record_s_rows_have_the_fixed_spelling_in_the_fixed_order() {
792        let wb = a_model();
793        let text = view_text(
794            &wb,
795            &Window {
796                sheet: "Model".to_string(),
797                range: range("A1:F20"),
798            },
799        );
800        assert_eq!(
801            lines(&text),
802            vec![
803                "(sheet \"Model\" visible)",
804                "(sheet \"Empty\" visible)",
805                "(window \"Model\" \"A1:F20\")",
806                "(extent \"Model\" \"B1:D4\")",
807                "(gridlines \"Model\" off)",
808                "(column \"Model\" \"A\" none hidden none)",
809                "(column \"Model\" \"B\" 72 shown 1)",
810                "(column \"Model\" \"C\" 60 shown 2)",
811                "(format 0 none general nowrap)",
812                "(format 1 \"F7F4FC\" text nowrap)",
813                "(format 2 \"F2F2F2\" general wrap)",
814                "(cell \"Model\" \"B1\" \"Revenue\")",
815                "(style \"Model\" \"B1\" 1)",
816                "(sentence \"Model\" \"B1\" 1)",
817                "(cell \"Model\" \"B2\" 1200.5)",
818                "(sentence \"Model\" \"B2\" 2)",
819                "(formula \"Model\" \"C2\" \"=B2*2\")",
820                "(sentence \"Model\" \"C2\" 3)",
821                "(formula \"Model\" \"D2\" \"=IFS(B2>3,\\\"big\\\",TRUE,\\\"small\\\")\")",
822                "(sentence \"Model\" \"D2\" 4)",
823                "(cell \"Model\" \"B3\" true)",
824                "(sentence \"Model\" \"B3\" 2)",
825                "(formula \"Model\" \"C3\" \"=B3*2\")",
826                "(sentence \"Model\" \"C3\" 3)",
827                "(cell \"Model\" \"B4\" (error \"#NUM!\"))",
828                "(sentence \"Model\" \"B4\" 2)",
829                "(formula \"Model\" \"C4\" \"=B4*2\")",
830                "(sentence \"Model\" \"C4\" 3)",
831            ]
832        );
833    }
834
835    #[test]
836    fn the_changes_print_each_cell_s_rows_as_the_values_print_them() {
837        // KERNEL.42: the changes' text of the cells a machine named, each
838        // cell's rows the values' own, printed by the same code: a value
839        // cell's with its style and sentence rows, a formula's value row and
840        // its sentence row; a cell that prints no row, absent or a shared
841        // formula's cell whose master the sheet has lost, spelled empty; and
842        // the row alone, ending whole, where the machine cannot say.
843        let mut wb = a_model();
844        let library = crate::calc::library::language();
845        let calc = crate::calc::Calc::run(&wb, &library);
846        let w = Window {
847            sheet: "Model".to_string(),
848            range: range("A1:F20"),
849        };
850        let values = view_text_values(&wb, &w, &calc);
851        let rows = |addr: &str| -> String {
852            let at = format!(" \"{addr}\" ");
853            values
854                .lines()
855                .filter(|l| l.contains(&at))
856                .map(|l| format!("{l}\n"))
857                .collect()
858        };
859        let text = changes_text(&wb, &w, &calc, (6, 7), Some(&[(1, 2), (2, 3), (9, 9)]));
860        assert_eq!(
861            text,
862            format!(
863                "(changes \"Model\" \"A1:F20\" 6 7 3)\n{}{}(empty \"Model\" \"I9\")\n",
864                rows("B1"),
865                rows("C2")
866            )
867        );
868        assert_eq!(rows("B1").lines().count(), 3, "{values}");
869        assert_eq!(
870            changes_text(&wb, &w, &calc, (6, 7), None),
871            "(changes \"Model\" \"A1:F20\" 6 7 whole)\n"
872        );
873        wb.sheets[0].cells.remove(&(2, 3));
874        let lost = crate::calc::Calc::run(&wb, &library);
875        assert_eq!(
876            changes_text(&wb, &w, &lost, (0, 1), Some(&[(3, 3)])),
877            "(changes \"Model\" \"A1:F20\" 0 1 1)\n(empty \"Model\" \"C3\")\n",
878            "a fill's cell whose master is lost"
879        );
880    }
881
882    #[test]
883    fn a_window_clips_to_its_rectangle_and_an_empty_one_prints_its_header_alone() {
884        let wb = a_model();
885        // The clip: the shared formula's children keep their moved text, and
886        // only column C's settings are in the window.
887        let text = view_text(
888            &wb,
889            &Window {
890                sheet: "Model".to_string(),
891                range: range("C3:D4"),
892            },
893        );
894        assert_eq!(
895            lines(&text),
896            vec![
897                "(sheet \"Model\" visible)",
898                "(sheet \"Empty\" visible)",
899                "(window \"Model\" \"C3:D4\")",
900                "(extent \"Model\" \"B1:D4\")",
901                "(gridlines \"Model\" off)",
902                "(column \"Model\" \"C\" 60 shown 2)",
903                "(format 0 none general nowrap)",
904                "(format 2 \"F2F2F2\" general wrap)",
905                "(formula \"Model\" \"C3\" \"=B3*2\")",
906                "(sentence \"Model\" \"C3\" 3)",
907                "(formula \"Model\" \"C4\" \"=B4*2\")",
908                "(sentence \"Model\" \"C4\" 3)",
909            ]
910        );
911        // Past the extent: the header says where the cells are, and no cell row follows.
912        let text = view_text(
913            &wb,
914            &Window {
915                sheet: "Model".to_string(),
916                range: range("H10:J12"),
917            },
918        );
919        assert_eq!(
920            lines(&text)[2..],
921            [
922                "(window \"Model\" \"H10:J12\")",
923                "(extent \"Model\" \"B1:D4\")",
924                "(gridlines \"Model\" off)",
925                "(format 0 none general nowrap)",
926            ]
927        );
928        // An empty sheet: no extent, gridlines on, the default format.
929        let text = view_text(
930            &wb,
931            &Window {
932                sheet: "Empty".to_string(),
933                range: whole(&wb.sheets[1]),
934            },
935        );
936        assert_eq!(
937            lines(&text)[2..],
938            [
939                "(window \"Empty\" \"A1\")",
940                "(extent \"Empty\" none)",
941                "(gridlines \"Empty\" on)",
942                "(format 0 none general nowrap)",
943            ]
944        );
945        // A window onto no sheet, which window_of never makes: the sheets,
946        // the window, and nothing else.
947        let text = view_text(
948            &wb,
949            &Window {
950                sheet: "Nowhere".to_string(),
951                range: range("A1"),
952            },
953        );
954        assert_eq!(lines(&text).len(), 3);
955        assert_eq!(lines(&text)[2], "(window \"Nowhere\" \"A1\")");
956    }
957
958    #[test]
959    fn row_settings_looks_and_hidden_sheets_print_in_their_places() {
960        // KERNEL.22: the `row` rows after the `column` rows, the `look` rows
961        // after the `format` rows with their formats among them, and each
962        // sheet's own visibility.
963        let mut wb = a_model();
964        wb.sheets[1].visibility = crate::rows::Visibility::Hidden;
965        let model = &mut wb.sheets[0];
966        model.rows.insert(
967            3,
968            crate::sheet::RowSettings {
969                height: Some(30.0),
970                hidden: false,
971            },
972        );
973        model.rows.insert(
974            9,
975            crate::sheet::RowSettings {
976                height: None,
977                hidden: true,
978            },
979        );
980        model.looks.push(("1".to_string(), 2));
981        let text = view_text(
982            &wb,
983            &Window {
984                sheet: "Model".to_string(),
985                range: range("B2:B4"),
986            },
987        );
988        assert_eq!(
989            lines(&text)[..10],
990            [
991                "(sheet \"Model\" visible)",
992                "(sheet \"Empty\" hidden)",
993                "(window \"Model\" \"B2:B4\")",
994                "(extent \"Model\" \"B1:D4\")",
995                "(gridlines \"Model\" off)",
996                "(column \"Model\" \"B\" 72 shown 1)",
997                "(row \"Model\" 3 30 shown)",
998                "(format 0 none general nowrap)",
999                "(format 1 \"F7F4FC\" text nowrap)",
1000                "(format 2 \"F2F2F2\" general wrap)",
1001            ]
1002        );
1003        assert_eq!(lines(&text)[10], "(look \"Model\" 1 2)");
1004        assert!(
1005            !text.contains("(row \"Model\" 9"),
1006            "row 9 is outside the window"
1007        );
1008    }
1009
1010    #[test]
1011    fn window_of_finds_the_sheet_without_case_and_refuses_the_rest_by_name() {
1012        let wb = a_model();
1013        let w = window_of(&wb, "MODEL", None).unwrap();
1014        assert_eq!(w.sheet, "Model");
1015        assert_eq!(w.range, range("B1:D4"));
1016        let w = window_of(&wb, "empty", None).unwrap();
1017        assert_eq!((w.sheet.as_str(), w.range.text().as_str()), ("Empty", "A1"));
1018        let w = window_of(&wb, "Model", Some("f20:a1")).unwrap();
1019        assert_eq!(w.range.text(), "A1:F20");
1020        let r = window_of(&wb, "Summary", None).unwrap_err();
1021        assert_eq!(
1022            (r.id.as_str(), r.source.as_str()),
1023            ("view-sheet-unknown", "VLA-View")
1024        );
1025        assert!(r.text.contains("Summary"), "{}", r.text);
1026        assert!(r.text.contains("Model, Empty"), "{}", r.text);
1027        for text in ["A:A", "3:3", "nonsense", "", "A1:B"] {
1028            let r = window_of(&wb, "Model", Some(text)).unwrap_err();
1029            assert_eq!(r.id, "view-window-not-a-range", "{text}");
1030            assert!(r.text.contains(text), "{}", r.text);
1031        }
1032        assert_eq!(Grid::default().name(), "grid");
1033    }
1034
1035    /// A record less its `formula` lines: what the values must be (KERNEL.37).
1036    fn less_formulas(record: &str) -> String {
1037        record
1038            .lines()
1039            .filter(|l| !l.starts_with("(formula "))
1040            .map(|l| format!("{l}\n"))
1041            .collect()
1042    }
1043
1044    /// The values record of a window, over the values given or none.
1045    fn values_text(wb: &Workbook, window: &Window, values: Option<&dyn Values>) -> String {
1046        let mut out = String::new();
1047        ValueGrid { values }.project(wb, window, &mut |line| {
1048            out.push_str(line);
1049            out.push('\n');
1050        });
1051        out
1052    }
1053
1054    /// What `f` walked, counted (`sheet::work`).
1055    fn walks(f: impl FnOnce()) -> crate::sheet::work::Work {
1056        crate::sheet::work::begin();
1057        f();
1058        crate::sheet::work::end()
1059    }
1060
1061    #[test]
1062    fn the_values_are_the_grid_less_its_formula_rows() {
1063        // KERNEL.37: every window of the model, computed and not, its values
1064        // its grid record with the formula lines left out, a formula's style
1065        // and sentence rows kept whether it has a value or not.
1066        let mut wb = a_model();
1067        wb.sheets[0].cells.get_mut(&(3, 3)).unwrap().style = 2;
1068        let library = crate::calc::library::language();
1069        let calc = crate::calc::Calc::run(&wb, &library);
1070        for (sheet, window) in [
1071            ("Model", "A1:F20"),
1072            ("Model", "C3:D4"),
1073            ("Model", "B1:B4"),
1074            ("Model", "H10:J12"),
1075            ("Empty", "A1"),
1076        ] {
1077            let w = Window {
1078                sheet: sheet.to_string(),
1079                range: range(window),
1080            };
1081            let bare = values_text(&wb, &w, None);
1082            assert_eq!(bare, less_formulas(&view_text(&wb, &w)), "{window}");
1083            let valued = view_text_values(&wb, &w, &calc);
1084            assert_eq!(
1085                valued,
1086                less_formulas(&view_text_valued(&wb, &w, &calc)),
1087                "{window}"
1088            );
1089            assert!(!valued.contains("(formula "), "{valued}");
1090        }
1091        let w = Window {
1092            sheet: "Model".to_string(),
1093            range: range("A1:F20"),
1094        };
1095        let bare = values_text(&wb, &w, None);
1096        assert!(bare.contains("(style \"Model\" \"C3\" 2)\n(sentence \"Model\" \"C3\" 3)\n"));
1097        assert!(!bare.contains("(value "), "{bare}");
1098        let valued = view_text_values(&wb, &w, &calc);
1099        assert!(
1100            valued.contains("(value \"Model\" \"C2\" 2401)\n(sentence \"Model\" \"C2\" 3)\n"),
1101            "{valued}"
1102        );
1103        // A window of values alone is the grid's own record.
1104        let w = Window {
1105            sheet: "Model".to_string(),
1106            range: range("B1:B4"),
1107        };
1108        assert_eq!(values_text(&wb, &w, None), view_text(&wb, &w));
1109        assert_eq!(ValueGrid::default().name(), "values");
1110    }
1111
1112    #[test]
1113    fn a_fill_s_cell_whose_master_is_lost_shows_in_neither_record() {
1114        // KERNEL.37: a value over a fill's first cell, as the core's build
1115        // writes one, leaves the fill's other cells children of a master the
1116        // sheet no longer holds. Neither record shows them, not even their
1117        // style and sentence rows, and with no value to vouch for them the
1118        // values find that out by one walk of the sheet.
1119        let mut wb = Workbook::new();
1120        let mut sheet = Sheet::new("S");
1121        sheet.set(
1122            2,
1123            2,
1124            Cell {
1125                content: Content::Number(5.0),
1126                style: 0,
1127            },
1128        );
1129        sheet.set_formula(range("C2:C4"), "B2+1", 1);
1130        sheet.wrote(range("C2:C4"), 2);
1131        sheet.set(
1132            2,
1133            3,
1134            Cell {
1135                content: Content::Number(9.0),
1136                style: 0,
1137            },
1138        );
1139        sheet.wrote(range("C2"), 3);
1140        wb.sheets.push(sheet);
1141        let w = Window {
1142            sheet: "S".to_string(),
1143            range: range("A1:D5"),
1144        };
1145        let library = crate::calc::library::language();
1146        let calc = crate::calc::Calc::run(&wb, &library);
1147        let grid = view_text(&wb, &w);
1148        let valued = view_text_valued(&wb, &w, &calc);
1149        let mut bare = String::new();
1150        let work = walks(|| bare = values_text(&wb, &w, None));
1151        assert_eq!(bare, less_formulas(&grid));
1152        assert_eq!(work.walked, 4, "the sheet's four cells walked once");
1153        let mut values = String::new();
1154        let work = walks(|| values = view_text_values(&wb, &w, &calc));
1155        assert_eq!(values, less_formulas(&valued));
1156        assert_eq!(work.walked, 4, "no value vouches for C3, so one walk");
1157        assert_eq!(work.extents, 0, "the extent the calculation keeps");
1158        for record in [&grid, &valued, &bare, &values] {
1159            assert!(
1160                !record.contains("\"C3\"") && !record.contains("\"C4\""),
1161                "{record}"
1162            );
1163            assert!(record.contains("(cell \"S\" \"C2\" 9)\n(sentence \"S\" \"C2\" 3)\n"));
1164        }
1165    }
1166
1167    #[test]
1168    fn the_values_of_a_computed_fill_walk_nothing() {
1169        // KERNEL.37: where every cell of a fill has a value, the values walk
1170        // the sheet neither for its masters nor for its extent; the grid
1171        // walks it once for its masters, to move their texts; with no
1172        // values, both walk it for the extent.
1173        let mut wb = Workbook::new();
1174        let mut sheet = Sheet::new("S");
1175        for row in 1..=4 {
1176            sheet.set(
1177                row,
1178                1,
1179                Cell {
1180                    content: Content::Number(f64::from(row)),
1181                    style: 0,
1182                },
1183            );
1184        }
1185        sheet.set_formula(range("B1:C4"), "A1*2", 0);
1186        wb.sheets.push(sheet);
1187        let w = Window {
1188            sheet: "S".to_string(),
1189            range: range("B2:C3"),
1190        };
1191        let library = crate::calc::library::language();
1192        let calc = crate::calc::Calc::run(&wb, &library);
1193        let work = walks(|| {
1194            view_text_values(&wb, &w, &calc);
1195        });
1196        assert_eq!((work.walked, work.extents), (0, 0));
1197        let work = walks(|| {
1198            view_text_valued(&wb, &w, &calc);
1199        });
1200        assert_eq!((work.walked, work.extents), (12, 0));
1201        let work = walks(|| {
1202            values_text(&wb, &w, None);
1203        });
1204        assert_eq!((work.walked, work.extents), (12, 12));
1205        let work = walks(|| {
1206            view_text(&wb, &w);
1207        });
1208        assert_eq!((work.walked, work.extents), (12, 12));
1209    }
1210}