Skip to main content

vla_lang/calc/
mod.rs

1//! Recalculation's mechanism (KERNEL.7, slice 1, 2026-10-08): the formula
2//! parser over the reference scanner, the dependency graph from `refers`,
3//! evaluation in topological order, a cycle refused by name, Excel's error
4//! values as a value kind, and a registry seam for functions, with the
5//! language's own functions registered.
6//!
7//! It lands here, in the language, and not in `frazaro-core`, by the
8//! three-crate order `Alonzo/CHARTER.md` section 4 and the roadmap's dated
9//! note on `KERNEL.7` decide: an evaluator written into the bridges with
10//! Excel's functions tangled in would make every game depend on the
11//! bridges. So the mechanism and the day-one functions are the language's
12//! ([`library::language`]); Excel's wider library registers through
13//! [`Library`] from `frazaro-core`, by measurement and with a fixture per
14//! function, and `frazaro calc` compares what it computes against the
15//! values a host saved.
16//!
17//! An engine's grid is stepped by `crate::machine` (KERNEL.22,
18//! 2026-10-09), over the same evaluator and the plan of shapes in
19//! [`shape`]: every formula parsed once per shape and evaluated at each
20//! cell's offset, a step with a budget in cells that yields and resumes
21//! (`Alonzo/SPEC.md` section 4.4), the previous frame's `.last` twins being
22//! ordinary sheets to it, each kept beside as an array over its extent that
23//! a read indexes and a range walks ([`dense`], KERNEL.20), a run of a
24//! shape's cells that read none of one another evaluated at once, node by
25//! node (`vector`, KERNEL.20), and a changed cell followed to the cells that
26//! read it, so that a frame computes those and keeps every other cell's
27//! value (`reach`, KERNEL.20). [`Calc`] keeps its plan of texts and its
28//! parse at evaluation, since it evaluates each formula once and an engine
29//! every formula every frame. The plan of shapes prints itself on request,
30//! its census (`census`, KERNEL.30): each shape's cells, levels and runs,
31//! and the path each of its reads takes, so that a grid's cost is read
32//! before a frame runs.
33//!
34//! What is not here, by name, each a slice of its own: dates, text
35//! functions, lookups, dynamic arrays and spills, implicit intersection
36//! and `@`, `LET` and `LAMBDA` (`KERNEL.8`); the rest of speed,
37//! content-addressed evaluation among them (`KERNEL.20`). A formula that
38//! reaches any
39//! of them is not computed, and says so naming the function or the
40//! construct ([`Reason`]), which is the honest static label of `HORIZON.md`
41//! section 12.6; a cell that reads such a cell inherits the reason.
42
43pub(crate) mod census;
44pub mod dense;
45pub mod eval;
46pub mod formula;
47pub mod graph;
48pub mod library;
49pub(crate) mod reach;
50pub mod shape;
51pub mod value;
52pub(crate) mod vector;
53
54use std::cell::Cell as Slot;
55use std::collections::HashMap;
56
57use crate::messages::{raise, Refusal};
58use crate::refers::quote_sheet;
59use crate::sheet::{cell_ref, formula_text, shared_masters, Content, Workbook};
60
61pub use dense::Dense;
62pub use eval::{Arg, Computed, Ctx, Env, Form, Library, Reason, Strict};
63pub use formula::{calls, parse, Expr};
64pub use graph::{Area, Areas, CellId, Extent, Names, Plan};
65pub use shape::{Place, Shape, Shapes};
66pub use value::{agrees, ErrorKind, Value};
67
68/// How far a step got: the cells evaluated so far in this frame, the
69/// frame's total, and whether the frame is complete.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Progress {
72    pub evaluated: usize,
73    pub of: usize,
74    pub done: bool,
75}
76
77/// One recalculation of one model under one library: the plan made once,
78/// the cells evaluated in order, by steps or all at once.
79pub struct Calc<'m> {
80    model: &'m Workbook,
81    library: &'m Library,
82    names: Names,
83    extents: Vec<Extent>,
84    texts: HashMap<CellId, String>,
85    plan: Plan,
86    values: HashMap<CellId, Computed>,
87    next: usize,
88    here: Slot<CellId>,
89}
90
91impl<'m> Calc<'m> {
92    /// The plan over the model: every formula cell's text read off the
93    /// grid (a shared formula's child as its master's text moved), the
94    /// graph built from `refers`, the order found and every cycle named.
95    /// Nothing is evaluated yet.
96    pub fn new(model: &'m Workbook, library: &'m Library) -> Calc<'m> {
97        let names = Names::of(model);
98        let extents: Vec<Extent> = model.sheets.iter().map(|s| s.extent()).collect();
99        let mut texts = HashMap::new();
100        for (si, sheet) in model.sheets.iter().enumerate() {
101            let masters = shared_masters(sheet);
102            for (&(row, col), cell) in &sheet.cells {
103                if let Some(text) = formula_text(&cell.content, row, col, &masters) {
104                    texts.insert((si, row, col), text);
105                }
106            }
107        }
108        let plan = graph::plan(model, &names, &extents, &texts);
109        let mut values = HashMap::with_capacity(texts.len());
110        for cycle in &plan.cycles {
111            for id in cycle {
112                values.insert(*id, Computed::NotComputed(Reason::Cycle));
113            }
114        }
115        Calc {
116            model,
117            library,
118            names,
119            extents,
120            texts,
121            plan,
122            values,
123            next: 0,
124            here: Slot::new((0, 1, 1)),
125        }
126    }
127
128    /// The plan made and every cell evaluated.
129    pub fn run(model: &'m Workbook, library: &'m Library) -> Calc<'m> {
130        let mut calc = Calc::new(model, library);
131        calc.step(0);
132        calc
133    }
134
135    /// The model.
136    pub fn model(&self) -> &'m Workbook {
137        self.model
138    }
139
140    /// The library.
141    pub fn library(&self) -> &'m Library {
142        self.library
143    }
144
145    /// The plan: the order and the cycles.
146    pub fn plan(&self) -> &Plan {
147        &self.plan
148    }
149
150    /// Every cycle, each its cells sorted, the cycles by their first cell.
151    pub fn cycles(&self) -> &[Vec<CellId>] {
152        &self.plan.cycles
153    }
154
155    /// How many formula cells the model holds.
156    pub fn formulas(&self) -> usize {
157        self.texts.len()
158    }
159
160    /// A formula cell's text without its `=`, or `None` for a value cell.
161    pub fn formula(&self, id: CellId) -> Option<&str> {
162        self.texts.get(&id).map(String::as_str)
163    }
164
165    /// Every formula cell, in sheet then row-major order.
166    pub fn formula_cells(&self) -> Vec<CellId> {
167        let mut ids: Vec<CellId> = self.texts.keys().copied().collect();
168        ids.sort_unstable();
169        ids
170    }
171
172    /// What a formula cell holds, once evaluated; `None` for a value cell
173    /// or a cell the steps have not reached.
174    pub fn computed(&self, id: CellId) -> Option<&Computed> {
175        self.values.get(&id)
176    }
177
178    /// Evaluate the next cells of the order, at most `budget` of them, 0
179    /// meaning every one: `SD-34`'s step, a budget in cells and never in
180    /// time, so that the frame's values are the same whatever the host's
181    /// chunking. The cells in a cycle count as done from the start.
182    pub fn step(&mut self, budget: usize) -> Progress {
183        let order_len = self.plan.order.len();
184        let end = if budget == 0 {
185            order_len
186        } else {
187            self.next.saturating_add(budget).min(order_len)
188        };
189        for i in self.next..end {
190            let id = self.plan.order[i];
191            let computed = self.evaluate(id);
192            self.values.insert(id, computed);
193        }
194        self.next = end;
195        self.progress()
196    }
197
198    /// Where the evaluation stands.
199    pub fn progress(&self) -> Progress {
200        let in_cycles = self.plan.cycles.iter().map(Vec::len).sum::<usize>();
201        Progress {
202            evaluated: self.next + in_cycles,
203            of: self.plan.formulas(),
204            done: self.next == self.plan.order.len(),
205        }
206    }
207
208    /// Start the order again, every value forgotten but the cycles'.
209    pub fn restart(&mut self) {
210        self.values
211            .retain(|_, v| matches!(v, Computed::NotComputed(Reason::Cycle)));
212        self.next = 0;
213    }
214
215    fn evaluate(&self, id: CellId) -> Computed {
216        let Some(text) = self.texts.get(&id) else {
217            return Computed::NotComputed(Reason::Construct(String::new()));
218        };
219        self.here.set(id);
220        let parsed = match formula::parse(text) {
221            Ok(e) => e,
222            Err(construct) => return Computed::NotComputed(Reason::Construct(construct)),
223        };
224        let ctx = Ctx::new(self, self.library);
225        match ctx.eval(&parsed).and_then(|arg| ctx.one(&arg)) {
226            Ok(v) => Computed::Value(v.settled()),
227            Err(reason) => Computed::NotComputed(reason),
228        }
229    }
230
231    /// The functions the formulas call that the library does not hold,
232    /// with the first cell calling each, in sheet then row-major order:
233    /// what the engine's `load` refuses by name before the first step.
234    pub fn missing_functions(&self) -> Vec<(CellId, String)> {
235        let mut seen: HashMap<String, CellId> = HashMap::new();
236        for id in self.formula_cells() {
237            for name in formula::calls(&self.texts[&id]) {
238                if !self.library.has(&name) {
239                    seen.entry(name).or_insert(id);
240                }
241            }
242        }
243        let mut out: Vec<(CellId, String)> = seen.into_iter().map(|(n, id)| (id, n)).collect();
244        out.sort();
245        out
246    }
247
248    /// `Sheet!A1` for a cell, the sheet quoted as a reference quotes it.
249    pub fn spell_cell(&self, id: CellId) -> String {
250        let sheet = self
251            .model
252            .sheets
253            .get(id.0)
254            .map(|s| s.name.as_str())
255            .unwrap_or("");
256        format!("{}!{}", quote_sheet(sheet), cell_ref(id.1, id.2))
257    }
258
259    /// The first thing that stops the whole grid from being stepped, as a
260    /// refusal from the catalogue: a cycle, naming the cells that close it
261    /// (`calc-cycle`), else the first function the library does not hold,
262    /// naming the cell that calls it (`calc-function-not-computed`), else
263    /// the first formula whose text this version does not read, naming the
264    /// construct (`calc-construct-not-read`, KERNEL.22: the "unreadable
265    /// reference" KERNEL.7's fifth decision named for an engine's load);
266    /// `None` when every formula can be computed. An engine's `load` asks
267    /// this once; `frazaro calc` never does, since a workbook is read as it
268    /// is and each cell says what it can.
269    pub fn first_refusal(&self) -> Option<Refusal> {
270        if let Some(cycle) = self.plan.cycles.first() {
271            let cells: Vec<String> = cycle.iter().map(|id| self.spell_cell(*id)).collect();
272            return Some(raise("calc-cycle", &[("cells", &cells.join(", "))]));
273        }
274        if let Some((id, name)) = self.missing_functions().into_iter().next() {
275            return Some(raise(
276                "calc-function-not-computed",
277                &[("cell", &self.spell_cell(id)), ("function", &name)],
278            ));
279        }
280        let (id, construct) = self.formula_cells().into_iter().find_map(|id| {
281            formula::parse(&self.texts[&id])
282                .err()
283                .map(|construct| (id, construct))
284        })?;
285        Some(formula::unread_refusal(&self.spell_cell(id), &construct))
286    }
287}
288
289/// What a projection asks of a recalculation (KERNEL.22): a formula cell's
290/// computed value, or `None` for a value cell or a formula with no value
291/// yet. [`Calc`] answers it, and so does an engine's machine, whose values
292/// outlive any one recalculation; the view record prints its `value` rows
293/// from either.
294pub trait Values: std::fmt::Debug {
295    fn computed(&self, id: CellId) -> Option<&Computed>;
296
297    /// Whether some of a sheet's cells hold content newer than the model's
298    /// (KERNEL.27): an engine's machine makes a twin's cells from its array
299    /// only when a reader of the twin as a sheet asks, and the record and the
300    /// plane read such a sheet's cells through [`Values::content`]. No sheet,
301    /// for any other.
302    fn lags(&self, _sheet: usize) -> bool {
303        false
304    }
305
306    /// A cell's content where it is newer than the model's, on a sheet that
307    /// [`Values::lags`]; `None` for the model's own.
308    fn content(&self, _id: CellId) -> Option<Content> {
309        None
310    }
311}
312
313impl Values for Calc<'_> {
314    fn computed(&self, id: CellId) -> Option<&Computed> {
315        Calc::computed(self, id)
316    }
317}
318
319impl std::fmt::Debug for Calc<'_> {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        f.debug_struct("Calc")
322            .field("formulas", &self.texts.len())
323            .field("cycles", &self.plan.cycles.len())
324            .field("progress", &self.progress())
325            .finish()
326    }
327}
328
329impl Env for Calc<'_> {
330    fn model(&self) -> &Workbook {
331        self.model
332    }
333
334    fn here(&self) -> CellId {
335        self.here.get()
336    }
337
338    fn read(&self, id: CellId) -> Result<Value, Reason> {
339        if self.texts.contains_key(&id) {
340            return match self.values.get(&id) {
341                Some(Computed::Value(v)) => Ok(v.clone()),
342                Some(Computed::NotComputed(r)) => Err(r.clone()),
343                // Every dependency precedes its reader in the order; a
344                // formula with no value yet can only be read from a cycle.
345                None => Err(Reason::Cycle),
346            };
347        }
348        let Some(cell) = self
349            .model
350            .sheets
351            .get(id.0)
352            .and_then(|s| s.cells.get(&(id.1, id.2)))
353        else {
354            return Ok(Value::Empty);
355        };
356        Ok(match &cell.content {
357            Content::Text(t) => Value::Text(t.clone()),
358            Content::Number(n) => Value::Number(*n),
359            Content::Bool(b) => Value::Bool(*b),
360            Content::Error(e) => Value::Error(ErrorKind::parse(e).unwrap_or(ErrorKind::Value)),
361            _ => Value::Empty,
362        })
363    }
364
365    fn extent(&self, sheet: usize) -> Extent {
366        self.extents.get(sheet).copied().flatten()
367    }
368
369    fn name(&self, name: &str, scope: Option<usize>, home: usize) -> Option<&str> {
370        self.names.resolve(name, scope, home)
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::sheet::{parse_a1_range, Cell, Sheet};
378
379    /// A model from rows of `(sheet, address, content)`, a formula's text
380    /// beginning with `=`.
381    fn model(rows: &[(&str, &str, &str)]) -> Workbook {
382        let mut wb = Workbook::new();
383        for (sheet, addr, text) in rows {
384            let i = wb.ensure_sheet(sheet);
385            let range = parse_a1_range(addr).unwrap();
386            let content = if let Some(f) = text.strip_prefix('=') {
387                Content::Formula(f.to_string())
388            } else if let Ok(n) = text.parse::<f64>() {
389                Content::Number(n)
390            } else if *text == "TRUE" || *text == "FALSE" {
391                Content::Bool(*text == "TRUE")
392            } else if let Some(e) = ErrorKind::parse(text) {
393                Content::Error(e.text().to_string())
394            } else {
395                Content::Text(text.to_string())
396            };
397            wb.sheets[i].set(range.top, range.left, Cell { content, style: 0 });
398        }
399        wb
400    }
401
402    fn at(calc: &Calc<'_>, sheet: &str, addr: &str) -> Computed {
403        let i = calc.model().find_sheet(sheet).unwrap();
404        let r = parse_a1_range(addr).unwrap();
405        calc.computed((i, r.top, r.left)).cloned().unwrap()
406    }
407
408    fn number(calc: &Calc<'_>, sheet: &str, addr: &str) -> f64 {
409        match at(calc, sheet, addr) {
410            Computed::Value(Value::Number(n)) => n,
411            other => panic!("{sheet}!{addr}: {other:?}"),
412        }
413    }
414
415    #[test]
416    fn arithmetic_runs_in_dependency_order_whatever_the_cell_order() {
417        let wb = model(&[
418            ("S", "A3", "=A2*2"),
419            ("S", "A2", "=A1+1"),
420            ("S", "A1", "5"),
421            ("S", "B1", "=A3-A2"),
422        ]);
423        let lib = library::language();
424        let calc = Calc::run(&wb, &lib);
425        assert_eq!(number(&calc, "S", "A2"), 6.0);
426        assert_eq!(number(&calc, "S", "A3"), 12.0);
427        assert_eq!(number(&calc, "S", "B1"), 6.0);
428        assert_eq!(calc.formulas(), 3);
429        assert!(calc.cycles().is_empty());
430        assert_eq!(
431            calc.progress(),
432            Progress {
433                evaluated: 3,
434                of: 3,
435                done: true
436            }
437        );
438    }
439
440    #[test]
441    fn a_budget_never_changes_a_value() {
442        let wb = model(&[
443            ("S", "A1", "1"),
444            ("S", "A2", "=A1+1"),
445            ("S", "A3", "=A2+1"),
446            ("S", "A4", "=A3+1"),
447            ("S", "A5", "=SUM(A1:A4)"),
448        ]);
449        let lib = library::language();
450        let whole = Calc::run(&wb, &lib);
451        let mut chunked = Calc::new(&wb, &lib);
452        let p1 = chunked.step(2);
453        assert_eq!(
454            p1,
455            Progress {
456                evaluated: 2,
457                of: 4,
458                done: false
459            }
460        );
461        assert!(
462            chunked.computed((0, 4, 1)).is_none(),
463            "A4 waits for the next step"
464        );
465        let p2 = chunked.step(1);
466        assert!(!p2.done);
467        let p3 = chunked.step(5);
468        assert!(p3.done && p3.evaluated == 4);
469        for id in whole.formula_cells() {
470            assert_eq!(whole.computed(id), chunked.computed(id), "{id:?}");
471        }
472        assert_eq!(number(&whole, "S", "A5"), 10.0);
473        chunked.restart();
474        assert!(chunked.computed((0, 5, 1)).is_none());
475        chunked.step(0);
476        assert_eq!(number(&chunked, "S", "A5"), 10.0);
477    }
478
479    #[test]
480    fn a_cycle_is_named_and_what_reads_it_is_not_computed() {
481        let wb = model(&[
482            ("S", "A1", "=B1+1"),
483            ("S", "B1", "=A1+1"),
484            ("S", "C1", "=A1*2"),
485            ("S", "D1", "=D1"),
486            ("S", "E1", "=1+1"),
487        ]);
488        let lib = library::language();
489        let calc = Calc::run(&wb, &lib);
490        assert_eq!(
491            calc.cycles(),
492            &[vec![(0, 1, 1), (0, 1, 2)], vec![(0, 1, 4)]]
493        );
494        assert_eq!(at(&calc, "S", "A1"), Computed::NotComputed(Reason::Cycle));
495        assert_eq!(at(&calc, "S", "C1"), Computed::NotComputed(Reason::Cycle));
496        assert_eq!(at(&calc, "S", "D1"), Computed::NotComputed(Reason::Cycle));
497        assert_eq!(number(&calc, "S", "E1"), 2.0);
498        let refusal = calc.first_refusal().unwrap();
499        assert_eq!(refusal.id, "calc-cycle");
500        assert!(refusal.text.contains("S!A1, S!B1"), "{}", refusal.text);
501        assert_eq!(
502            calc.progress(),
503            Progress {
504                evaluated: 5,
505                of: 5,
506                done: true
507            }
508        );
509    }
510
511    #[test]
512    fn a_cell_that_reads_itself_through_another_sheet_s_twin_is_no_cycle() {
513        // The engine's previous frame: Screen.last holds values only.
514        let wb = model(&[
515            ("Screen", "B2", "=Screen.last!B2+1"),
516            ("Screen.last", "B2", "41"),
517        ]);
518        let lib = library::language();
519        let calc = Calc::run(&wb, &lib);
520        assert!(calc.cycles().is_empty());
521        assert_eq!(number(&calc, "Screen", "B2"), 42.0);
522    }
523
524    #[test]
525    fn ranges_names_and_spans_are_read_as_excel_reads_them() {
526        let mut wb = model(&[
527            ("Model", "B1", "1200"),
528            ("Model", "B2", "800"),
529            ("Model", "B3", "=B1-B2"),
530            ("Model", "C1", "x"),
531            ("Model", "C2", "TRUE"),
532            ("Model", "D1", "=SUM(B:B)"),
533            ("Model", "D2", "=SUM(B1:C2)"),
534            ("Model", "D3", "=SUM(B1,\"5\",TRUE,C2)"),
535            ("Model", "D4", "=B3*Rate"),
536            ("Model", "D5", "=Local+1"),
537            ("Model", "D6", "=SUM(Model:Scratch!B1)"),
538            ("Model", "D7", "=Nothing"),
539            ("Model", "D8", "=Missing!A1"),
540            ("Model", "D9", "=MAX(B1:B3,C1)"),
541            ("Model", "D10", "=MIN(C1:C2)"),
542            ("Scratch", "B1", "7"),
543            ("Scratch", "B2", "=Model!D1*2"),
544        ]);
545        wb.defined_names
546            .push(("Rate".to_string(), "0.2".to_string()));
547        wb.defined_names
548            .push(("Model!Local".to_string(), "Model!$B$2".to_string()));
549        let lib = library::language();
550        let calc = Calc::run(&wb, &lib);
551        assert_eq!(number(&calc, "Model", "B3"), 400.0);
552        assert_eq!(
553            number(&calc, "Model", "D1"),
554            2400.0,
555            "the formula in B3 counts"
556        );
557        assert_eq!(
558            number(&calc, "Model", "D2"),
559            2000.0,
560            "a text and a truth value in a range are skipped"
561        );
562        assert_eq!(
563            number(&calc, "Model", "D3"),
564            1206.0,
565            "given directly they are coerced; C2 is read through a reference and skipped"
566        );
567        assert_eq!(number(&calc, "Model", "D4"), 80.0);
568        assert_eq!(number(&calc, "Model", "D5"), 801.0);
569        assert_eq!(
570            number(&calc, "Model", "D6"),
571            1207.0,
572            "a span sums both sheets"
573        );
574        assert_eq!(
575            at(&calc, "Model", "D7"),
576            Computed::Value(Value::Error(ErrorKind::Name))
577        );
578        assert_eq!(
579            at(&calc, "Model", "D8"),
580            Computed::Value(Value::Error(ErrorKind::Ref))
581        );
582        assert_eq!(number(&calc, "Model", "D9"), 1200.0);
583        assert_eq!(
584            number(&calc, "Model", "D10"),
585            0.0,
586            "no number in the range is 0"
587        );
588        assert_eq!(
589            number(&calc, "Scratch", "B2"),
590            4800.0,
591            "a sheet reads another's formula after it is computed"
592        );
593    }
594
595    #[test]
596    fn an_aggregate_folds_in_the_walk_s_order_and_keeps_the_first_answer() {
597        // KERNEL.25: SUM, MIN, MAX, AND and OR fold as they walk instead of
598        // collecting. The order is a range's walk, row by row: these four
599        // cells sum to 0 by rows and to 1 by columns, as doubles.
600        let wb = model(&[
601            ("S", "A1", "1E16"),
602            ("S", "B1", "1"),
603            ("S", "A2", "-1E16"),
604            ("S", "B2", "0"),
605            ("S", "C1", "#N/A"),
606            ("S", "C2", "=VLOOKUP(1,A1:A2,2)"),
607            ("S", "D1", "=SUM(A1:B2)"),
608            ("S", "D2", "=SUM(C1:C2)"),
609            ("S", "D3", "=SUM(C1+0,C2:C2)"),
610            ("S", "D4", "=AND(FALSE,C1)"),
611            ("S", "D5", "=OR(TRUE,C1)"),
612            ("S", "D6", "=MIN(Z1:Z3)"),
613            ("S", "D7", "=MAX(Z1:Z3)"),
614            ("S", "D8", "=MIN(A1:B2)"),
615            ("S", "D9", "=MAX(A1:B2,-5)"),
616            ("S", "D10", "=OR(A2:B2)"),
617            ("S", "D11", "=AND(A1:B1,TRUE)"),
618        ]);
619        let lib = library::language();
620        let calc = Calc::run(&wb, &lib);
621        let err = |k| Computed::Value(Value::Error(k));
622        let by_rows = [1e16, 1.0, -1e16, 0.0].iter().fold(-0.0, |a, b| a + b);
623        let by_columns = [1e16, -1e16, 1.0, 0.0].iter().fold(-0.0, |a, b| a + b);
624        assert_ne!(
625            by_rows, by_columns,
626            "the fixture tells the two orders apart"
627        );
628        assert_eq!(number(&calc, "S", "D1"), by_rows);
629        assert_eq!(number(&calc, "S", "D1"), 0.0);
630        assert_eq!(
631            at(&calc, "S", "D2"),
632            Computed::NotComputed(Reason::Function("VLOOKUP".to_string())),
633            "a range is walked to its end: the cell not computed answers after the error"
634        );
635        assert_eq!(
636            at(&calc, "S", "D3"),
637            err(ErrorKind::NA),
638            "an error given directly answers before a later range is walked"
639        );
640        assert_eq!(at(&calc, "S", "D4"), err(ErrorKind::NA), "AND is strict");
641        assert_eq!(at(&calc, "S", "D5"), err(ErrorKind::NA), "OR is strict");
642        assert_eq!(number(&calc, "S", "D6"), 0.0, "MIN of no number");
643        assert_eq!(number(&calc, "S", "D7"), 0.0, "MAX of no number");
644        assert_eq!(number(&calc, "S", "D8"), -1e16);
645        assert_eq!(number(&calc, "S", "D9"), 1e16);
646        assert_eq!(
647            at(&calc, "S", "D10"),
648            Computed::Value(Value::Bool(true)),
649            "-1E16 is true, 0 false"
650        );
651        assert_eq!(at(&calc, "S", "D11"), Computed::Value(Value::Bool(true)));
652    }
653
654    #[test]
655    fn errors_are_values_and_propagate_as_excel_propagates_them() {
656        let wb = model(&[
657            ("S", "A1", "=1/0"),
658            ("S", "A2", "=A1+1"),
659            ("S", "A3", "=IF(A1>0,1,2)"),
660            ("S", "A4", "=IF(TRUE,1,A1)"),
661            ("S", "A5", "=SUM(A1:A2)"),
662            ("S", "A6", "#N/A"),
663            ("S", "A7", "=A6"),
664            ("S", "A8", "=\"x\"+1"),
665            ("S", "A9", "=-\"5\""),
666            ("S", "A10", "=50%"),
667            ("S", "A11", "=1&2"),
668            ("S", "A12", "=2^10"),
669            ("S", "A13", "=B99"),
670            ("S", "A14", "=#REF!+1"),
671        ]);
672        let lib = library::language();
673        let calc = Calc::run(&wb, &lib);
674        let err = |k| Computed::Value(Value::Error(k));
675        assert_eq!(at(&calc, "S", "A1"), err(ErrorKind::Div0));
676        assert_eq!(at(&calc, "S", "A2"), err(ErrorKind::Div0));
677        assert_eq!(
678            at(&calc, "S", "A3"),
679            err(ErrorKind::Div0),
680            "the condition's error"
681        );
682        assert_eq!(
683            number(&calc, "S", "A4"),
684            1.0,
685            "the branch not taken is not computed"
686        );
687        assert_eq!(at(&calc, "S", "A5"), err(ErrorKind::Div0));
688        assert_eq!(
689            at(&calc, "S", "A7"),
690            err(ErrorKind::NA),
691            "an error typed into a cell"
692        );
693        assert_eq!(at(&calc, "S", "A8"), err(ErrorKind::Value));
694        assert_eq!(number(&calc, "S", "A9"), -5.0);
695        assert_eq!(number(&calc, "S", "A10"), 0.5);
696        assert_eq!(
697            at(&calc, "S", "A11"),
698            Computed::Value(Value::Text("12".to_string()))
699        );
700        assert_eq!(number(&calc, "S", "A12"), 1024.0);
701        assert_eq!(
702            number(&calc, "S", "A13"),
703            0.0,
704            "an empty cell read is 0 in the cell"
705        );
706        assert_eq!(at(&calc, "S", "A14"), err(ErrorKind::Ref));
707    }
708
709    #[test]
710    fn the_day_one_functions_compute_exactly() {
711        let wb = model(&[
712            ("S", "A1", "=IF(1>0,\"yes\",\"no\")"),
713            ("S", "A2", "=IF(0)"),
714            ("S", "A3", "=IF(FALSE,1)"),
715            ("S", "A4", "=IF(TRUE,,1)"),
716            ("S", "A5", "=AND(TRUE,1,\"TRUE\")"),
717            ("S", "A6", "=AND(TRUE,0)"),
718            ("S", "A7", "=OR(FALSE,0,2)"),
719            ("S", "A8", "=NOT(0)"),
720            ("S", "A9", "=AND(\"yes\")"),
721            ("S", "A10", "=ABS(-3.5)"),
722            ("S", "A11", "=INT(-1.5)"),
723            ("S", "A12", "=MOD(-7,3)"),
724            ("S", "A13", "=MOD(7,-3)"),
725            ("S", "A14", "=MOD(7,0)"),
726            ("S", "A15", "=ROW()"),
727            ("S", "A16", "=COLUMN()"),
728            ("S", "A17", "=ROW(C9)"),
729            ("S", "A18", "=COLUMN(C9:D10)"),
730            ("S", "A19", "=CHOOSE(2,\"a\",\"b\",\"c\")"),
731            ("S", "A20", "=CHOOSE(4,\"a\",\"b\",\"c\")"),
732            ("S", "A21", "=SIN(0)"),
733            ("S", "A22", "=MOD(5.5,2)"),
734            ("S", "A23", "=CHOOSE(1.9,\"a\",\"b\")"),
735            ("S", "A24", "=AND()"),
736            ("S", "B1", "=VLOOKUP(1,A1:A2,2)"),
737            ("S", "B2", "=B1+1"),
738            ("S", "B3", "=SUM(A1:A2 B1)"),
739            ("S", "B4", "={1,2}"),
740            ("S", "B5", "=A1:A2*2"),
741        ]);
742        let lib = library::language();
743        let calc = Calc::run(&wb, &lib);
744        let text = |s: &str| Computed::Value(Value::Text(s.to_string()));
745        let truth = |b: bool| Computed::Value(Value::Bool(b));
746        let err = |k| Computed::Value(Value::Error(k));
747        assert_eq!(at(&calc, "S", "A1"), text("yes"));
748        assert_eq!(
749            at(&calc, "S", "A2"),
750            err(ErrorKind::Value),
751            "IF wants a branch"
752        );
753        assert_eq!(at(&calc, "S", "A3"), truth(false), "no else is FALSE");
754        assert_eq!(number(&calc, "S", "A4"), 0.0, "a branch left out is 0");
755        assert_eq!(at(&calc, "S", "A5"), truth(true));
756        assert_eq!(at(&calc, "S", "A6"), truth(false));
757        assert_eq!(at(&calc, "S", "A7"), truth(true));
758        assert_eq!(at(&calc, "S", "A8"), truth(true));
759        assert_eq!(at(&calc, "S", "A9"), err(ErrorKind::Value));
760        assert_eq!(number(&calc, "S", "A10"), 3.5);
761        assert_eq!(number(&calc, "S", "A11"), -2.0);
762        assert_eq!(number(&calc, "S", "A12"), 2.0, "the divisor's sign");
763        assert_eq!(number(&calc, "S", "A13"), -2.0);
764        assert_eq!(at(&calc, "S", "A14"), err(ErrorKind::Div0));
765        assert_eq!(number(&calc, "S", "A15"), 15.0);
766        assert_eq!(number(&calc, "S", "A16"), 1.0);
767        assert_eq!(number(&calc, "S", "A17"), 9.0);
768        assert_eq!(number(&calc, "S", "A18"), 3.0);
769        assert_eq!(at(&calc, "S", "A19"), text("b"));
770        assert_eq!(at(&calc, "S", "A20"), err(ErrorKind::Value));
771        assert_eq!(number(&calc, "S", "A21"), 0.0);
772        assert_eq!(number(&calc, "S", "A22"), 1.5);
773        assert_eq!(
774            at(&calc, "S", "A23"),
775            text("a"),
776            "the index is read down to a whole number"
777        );
778        assert_eq!(
779            at(&calc, "S", "A24"),
780            err(ErrorKind::Value),
781            "nothing to AND"
782        );
783        assert_eq!(
784            at(&calc, "S", "B1"),
785            Computed::NotComputed(Reason::Function("VLOOKUP".to_string()))
786        );
787        assert_eq!(
788            at(&calc, "S", "B2"),
789            Computed::NotComputed(Reason::Function("VLOOKUP".to_string())),
790            "a reader inherits the reason"
791        );
792        assert_eq!(
793            at(&calc, "S", "B3"),
794            Computed::NotComputed(Reason::Construct("intersection".to_string()))
795        );
796        assert_eq!(
797            at(&calc, "S", "B4"),
798            Computed::NotComputed(Reason::Construct("{".to_string()))
799        );
800        assert_eq!(
801            at(&calc, "S", "B5"),
802            Computed::NotComputed(Reason::Construct("A1:A2".to_string())),
803            "a range where one value is wanted waits for KERNEL.8"
804        );
805        assert_eq!(
806            calc.missing_functions(),
807            vec![((0, 1, 2), "VLOOKUP".to_string())]
808        );
809        let refusal = calc.first_refusal().unwrap();
810        assert_eq!(refusal.id, "calc-function-not-computed");
811        assert!(
812            refusal.text.contains("VLOOKUP") && refusal.text.contains("S!B1"),
813            "{}",
814            refusal.text
815        );
816        assert_eq!(at(&calc, "S", "A1").spell(), "\"yes\"");
817        assert_eq!(at(&calc, "S", "B1").spell(), "(not-computed \"VLOOKUP\")");
818    }
819
820    #[test]
821    fn a_construct_not_read_is_the_third_thing_a_load_refuses() {
822        // KERNEL.22: after the cycle and the function, a formula whose text
823        // this version does not read, naming the construct.
824        let wb = model(&[
825            ("S", "A1", "=1+1"),
826            ("S", "B2", "={1,2}"),
827            ("S", "C3", "=@A1"),
828        ]);
829        let lib = library::language();
830        let calc = Calc::new(&wb, &lib);
831        let r = calc.first_refusal().unwrap();
832        assert_eq!(r.id, "calc-construct-not-read");
833        assert!(
834            r.text.contains("S!B2") && r.text.contains("reaches {"),
835            "{}",
836            r.text
837        );
838        // A function the library does not hold comes first.
839        let wb = model(&[("S", "B2", "={1,2}"), ("S", "C3", "=NOW()")]);
840        let calc = Calc::new(&wb, &lib);
841        assert_eq!(
842            calc.first_refusal().unwrap().id,
843            "calc-function-not-computed"
844        );
845        let wb = model(&[("S", "A1", "=1+1")]);
846        assert!(Calc::new(&wb, &lib).first_refusal().is_none());
847    }
848
849    #[test]
850    fn a_chain_of_operators_evaluates_left_to_right_however_long() {
851        // VLA.1: a chain goes round a loop instead of recursing once a
852        // link, read as Excel reads it, left to right: the values the
853        // recursion gave, at every length Excel holds.
854        let long = format!("={}1", "1+".repeat(4095));
855        let wb = model(&[
856            ("S", "A1", "=10-1-1-1"),
857            ("S", "A2", "=2^3^2"),
858            ("S", "A3", "=1+2*3-4/2+5"),
859            ("S", "A4", "=\"a\"&\"b\"&\"c\""),
860            ("S", "A5", long.as_str()),
861            ("S", "A6", "=1/0+1+1"),
862            ("S", "A7", "=1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1"),
863        ]);
864        let lib = library::language();
865        let calc = Calc::run(&wb, &lib);
866        assert_eq!(number(&calc, "S", "A1"), 7.0);
867        assert_eq!(number(&calc, "S", "A2"), 64.0);
868        assert_eq!(number(&calc, "S", "A3"), 10.0);
869        assert_eq!(
870            at(&calc, "S", "A4"),
871            Computed::Value(Value::Text("abc".to_string()))
872        );
873        assert_eq!(number(&calc, "S", "A5"), 4096.0);
874        assert_eq!(
875            at(&calc, "S", "A6"),
876            Computed::Value(Value::Error(ErrorKind::Div0))
877        );
878        // Past the sixteen links held in place, the rest on the heap.
879        assert_eq!(number(&calc, "S", "A7"), 20.0);
880    }
881
882    #[test]
883    fn excel_s_two_limits_are_refused_by_their_own_ids() {
884        // VLA.1: past Excel's nesting or length limit an engine's load is
885        // refused by the limit's own id, and frazaro calc, which never
886        // refuses, shows the label in the cell.
887        let deep = format!("={}1{}", "(".repeat(65), ")".repeat(65));
888        let long = format!("={}1", "1+".repeat(4096));
889        let lib = library::language();
890        let wb = model(&[("S", "A1", deep.as_str())]);
891        let r = Calc::new(&wb, &lib).first_refusal().unwrap();
892        assert_eq!(r.id, "calc-formula-too-deep");
893        assert_eq!(
894            r.text,
895            "The formula in S!A1 nests more than 64 levels deep, more than Excel itself allows, so the formula cannot be computed."
896        );
897        let wb = model(&[("S", "A1", long.as_str())]);
898        let r = Calc::new(&wb, &lib).first_refusal().unwrap();
899        assert_eq!(r.id, "calc-formula-too-long");
900        assert_eq!(
901            r.text,
902            "The formula in S!A1 is longer than 8,192 characters, more than Excel itself allows, so the formula cannot be computed."
903        );
904        let calc = Calc::run(&wb, &lib);
905        assert_eq!(
906            at(&calc, "S", "A1"),
907            Computed::NotComputed(Reason::Construct(formula::TOO_LONG.to_string()))
908        );
909    }
910
911    #[test]
912    fn a_logical_test_coerces_a_text_read_through_a_reference() {
913        // Measured: Excel's save of the subset fixture
914        // (scripts/recalc/subset_saved.xlsx, logic!C3, C6 and C13): IF
915        // coerces the text TRUE found in a cell as it coerces a typed one and
916        // refuses another text; AND reads the reference as a range and skips
917        // the text. NOT follows IF's rule, which scripts/recalc/edges.txt
918        // holds for an Excel save to witness.
919        let mut wb = model(&[
920            ("S", "C1", "=IF(A1,1,2)"),
921            ("S", "C2", "=IF(A2,1,2)"),
922            ("S", "C3", "=AND(A1)"),
923            ("S", "C4", "=NOT(A1)"),
924        ]);
925        for (row, text) in [(1, "TRUE"), (2, "x")] {
926            wb.sheets[0].set(
927                row,
928                1,
929                Cell {
930                    content: Content::Text(text.to_string()),
931                    style: 0,
932                },
933            );
934        }
935        let lib = library::language();
936        let calc = Calc::run(&wb, &lib);
937        let err = |k| Computed::Value(Value::Error(k));
938        assert_eq!(number(&calc, "S", "C1"), 1.0);
939        assert_eq!(at(&calc, "S", "C2"), err(ErrorKind::Value));
940        assert_eq!(at(&calc, "S", "C3"), err(ErrorKind::Value));
941        assert_eq!(at(&calc, "S", "C4"), Computed::Value(Value::Bool(false)));
942    }
943
944    #[test]
945    fn shared_and_dynamic_formulas_are_read_through_their_masters() {
946        let mut wb = Workbook::new();
947        let i = wb.ensure_sheet("Output");
948        let sheet: &mut Sheet = &mut wb.sheets[i];
949        sheet.set(
950            2,
951            2,
952            Cell {
953                content: Content::Number(5.0),
954                style: 0,
955            },
956        );
957        sheet.set(
958            3,
959            2,
960            Cell {
961                content: Content::Formula("B2*2".to_string()),
962                style: 0,
963            },
964        );
965        sheet.set_formula(parse_a1_range("C2:C4").unwrap(), "B2+B3", 0);
966        sheet.set_formula_dynamic(parse_a1_range("D2:D3").unwrap(), "B2*10", 0);
967        let lib = library::language();
968        let calc = Calc::run(&wb, &lib);
969        assert_eq!(
970            calc.formula((0, 3, 3)),
971            Some("B3+B4"),
972            "the child's text is the master's moved"
973        );
974        assert_eq!(number(&calc, "Output", "C2"), 15.0);
975        assert_eq!(number(&calc, "Output", "C3"), 10.0);
976        assert_eq!(number(&calc, "Output", "C4"), 0.0);
977        assert_eq!(number(&calc, "Output", "D2"), 50.0);
978        assert_eq!(number(&calc, "Output", "D3"), 100.0);
979        assert_eq!(calc.spell_cell((0, 3, 3)), "Output!C3");
980    }
981}