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