pub mod load;
pub mod plane;
use std::cell::Cell as Slot;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use crate::calc::graph::{is_formula, Extent, Names};
use crate::calc::reach::Reached;
use crate::calc::shape::{spell_cell, Shapes, RUN};
use crate::calc::vector::{self, Lanes};
use crate::calc::{CellId, Computed, Ctx, Dense, Env, ErrorKind, Library, Reason, Value, Values};
use crate::form::Form;
use crate::fx::FxHashMap;
use crate::messages::{raise, Refusal};
use crate::reader::read_forms;
use crate::refers::shift_a1_references;
use crate::rows::Visibility;
use crate::sheet::{
formula_text, parse_a1_range, shared_masters, A1Range, Cell, Content, Sheet, Workbook,
};
use crate::view::{view_text_valued, window_of};
pub use load::{read_rows, sheet_name_ok, thousands, twin_base, Draft};
pub const CELLS: usize = 1_048_576;
pub const HANDLES: usize = 16;
pub const WRITE_ROWS: &str = "cell, formula and derived";
const SLOTS_A_CELL: u64 = 4;
const WRITTEN: usize = 64;
#[derive(Clone, Debug, PartialEq)]
pub struct LineRefusal {
pub line: u32,
pub refusal: Refusal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Loaded {
pub sheets: usize,
pub cells: usize,
pub formulas: usize,
}
impl Loaded {
pub fn row(&self, handle: u32) -> String {
format!(
"(loaded {handle} {} {} {})",
self.sheets, self.cells, self.formulas
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Step {
pub frame: u64,
pub evaluated: usize,
pub of: usize,
pub done: bool,
}
impl Step {
pub fn row(&self) -> String {
format!(
"(step {} {} {} {})",
self.frame,
self.evaluated,
self.of,
if self.done { "done" } else { "yielded" }
)
}
}
pub fn written_row(n: usize) -> String {
format!("(written {n})")
}
pub fn unloaded_row(handle: u32) -> String {
format!("(unloaded {handle})")
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Viewed {
Text(String),
Bytes(Vec<u8>),
}
impl Viewed {
pub fn bytes(&self) -> &[u8] {
match self {
Viewed::Text(t) => t.as_bytes(),
Viewed::Bytes(b) => b,
}
}
}
#[derive(Debug)]
pub struct Handles<T> {
next: u32,
live: BTreeMap<u32, T>,
}
impl<T> Default for Handles<T> {
fn default() -> Self {
Handles::new()
}
}
impl<T> Handles<T> {
pub const fn new() -> Handles<T> {
Handles {
next: 1,
live: BTreeMap::new(),
}
}
pub fn room(&self) -> Result<(), Refusal> {
if self.live.len() >= HANDLES || self.next == u32::MAX {
return Err(raise(
"grid-handle-limit",
&[("limit", &HANDLES.to_string())],
));
}
Ok(())
}
pub fn insert(&mut self, item: T) -> Result<u32, Refusal> {
self.room()?;
let handle = self.next;
self.next += 1;
self.live.insert(handle, item);
Ok(handle)
}
pub fn get(&self, handle: u32) -> Result<&T, Refusal> {
self.live.get(&handle).ok_or_else(|| unknown_handle(handle))
}
pub fn get_mut(&mut self, handle: u32) -> Result<&mut T, Refusal> {
self.live
.get_mut(&handle)
.ok_or_else(|| unknown_handle(handle))
}
pub fn remove(&mut self, handle: u32) -> Result<T, Refusal> {
self.live
.remove(&handle)
.ok_or_else(|| unknown_handle(handle))
}
pub fn live(&self) -> usize {
self.live.len()
}
}
fn unknown_handle(handle: u32) -> Refusal {
raise("grid-handle-unknown", &[("handle", &handle.to_string())])
}
pub struct Machine {
book: Workbook,
sheets: usize,
library: Library,
names: Names,
extents: Vec<Extent>,
shapes: Shapes,
values: Vec<Option<Computed>>,
before: FxHashMap<u32, Option<Computed>>,
twin_reasons: FxHashMap<CellId, Reason>,
reasons_before: FxHashMap<CellId, Reason>,
frame: u64,
progress: Option<usize>,
cells: usize,
here: Slot<CellId>,
wanted: Vec<Slot<bool>>,
planes: Vec<Option<plane::Raster>>,
dense: Vec<Option<Dense>>,
behind: Vec<Vec<u64>>,
lagging: Vec<bool>,
lanes: Lanes,
answers: Vec<Computed>,
gathered: Vec<CellId>,
gathered_at: Vec<u32>,
every_next: bool,
every: bool,
marked: Vec<u64>,
whole: Vec<bool>,
recheck: Vec<bool>,
parts: Vec<Option<Result<Value, Reason>>>,
changed: Vec<u32>,
changed_over: bool,
written: Vec<CellId>,
twin_changed: Vec<CellId>,
by_runs: bool,
incremental: bool,
lazy: bool,
#[cfg(test)]
computed: usize,
}
impl std::fmt::Debug for Machine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Machine")
.field("sheets", &self.sheets)
.field("cells", &self.cells)
.field("formulas", &self.shapes.formulas())
.field("shapes", &self.shapes.shapes())
.field("frame", &self.frame)
.field("progress", &self.progress)
.finish()
}
}
#[derive(Default)]
struct Log {
undo: Vec<(CellId, Option<Cell>)>,
changed: BTreeMap<CellId, u32>,
written: usize,
added: usize,
sheets: BTreeSet<usize>,
}
impl Machine {
pub fn load(text: &str, library: Library) -> Result<Machine, LineRefusal> {
let forms = read_forms(text).map_err(|refusal| LineRefusal { line: 0, refusal })?;
let draft = read_rows(&forms)?;
Machine::new(draft, library)
}
pub fn new(draft: Draft, library: Library) -> Result<Machine, LineRefusal> {
let Draft {
mut workbook,
values,
lines,
cells,
} = draft;
let sheets = workbook.sheets.len();
let names = Names::of(&workbook);
let mut extents: Vec<Extent> = workbook.sheets.iter().map(Sheet::extent).collect();
for i in 0..sheets {
let mut twin = Sheet::new(&format!("{}.last", workbook.sheets[i].name));
twin.visibility = Visibility::Hidden;
workbook.sheets.push(twin);
extents.push(None);
}
let shapes = Shapes::of(&workbook, sheets, &library, &names, &extents);
if let Some((cell, refusal)) = shapes.first_refusal(&workbook, None) {
let line = if refusal.id == "calc-cycle" {
shapes
.cycles()
.first()
.and_then(|c| c.iter().filter_map(|id| lines.get(id)).min().copied())
.unwrap_or(0)
} else {
lines.get(&cell).copied().unwrap_or(0)
};
return Err(LineRefusal { line, refusal });
}
Ok(Machine::made(
(workbook, sheets, cells),
(library, names, extents),
shapes,
values,
))
}
#[inline(never)]
fn made(
(book, sheets, cells): (Workbook, usize, usize),
(library, names, extents): (Library, Names, Vec<Extent>),
shapes: Shapes,
loaded: HashMap<CellId, Computed>,
) -> Machine {
let mut values: Vec<Option<Computed>> = vec![None; shapes.order().len()];
for (id, value) in loaded {
if let Some(p) = shapes.reach().position(id) {
values[p] = Some(value);
}
}
let mut m = Machine {
book,
sheets,
library,
names,
extents,
shapes,
values,
before: FxHashMap::default(),
twin_reasons: FxHashMap::default(),
reasons_before: FxHashMap::default(),
frame: 0,
progress: None,
cells,
here: Slot::new((0, 1, 1)),
wanted: (0..sheets).map(|_| Slot::new(false)).collect(),
planes: vec![None; sheets],
dense: vec![None; sheets],
behind: vec![Vec::new(); sheets],
lagging: vec![false; sheets],
lanes: Lanes::default(),
answers: Vec::new(),
gathered: Vec::with_capacity(vector::CHUNK),
gathered_at: Vec::with_capacity(vector::CHUNK),
every_next: true,
every: true,
marked: Vec::new(),
whole: Vec::new(),
recheck: Vec::new(),
parts: Vec::new(),
changed: Vec::new(),
changed_over: false,
written: Vec::with_capacity(WRITTEN),
twin_changed: Vec::new(),
by_runs: true,
incremental: true,
lazy: true,
#[cfg(test)]
computed: 0,
};
m.fit();
m.walk();
m
}
fn fit(&mut self) {
let of = self.shapes.order().len();
self.marked.clear();
self.marked.resize(of.div_ceil(64), 0);
let shapes = self.shapes.shapes();
self.whole.clear();
self.whole.resize(shapes, false);
self.recheck.clear();
self.recheck.resize(shapes, false);
self.parts.clear();
self.parts.resize(self.shapes.reach().part_count(), None);
fn room<T>(v: &mut Vec<T>, n: usize) {
v.reserve(n.saturating_sub(v.len()));
}
room(&mut self.changed, of / 2 + 64);
room(&mut self.twin_changed, self.cells / 2 + 64);
}
pub fn loaded(&self) -> Loaded {
Loaded {
sheets: self.sheets,
cells: self.cells,
formulas: self.shapes.formulas(),
}
}
pub fn workbook(&mut self) -> &Workbook {
self.make_twins();
&self.book
}
pub fn find_sheet(&self, name: &str) -> Option<usize> {
self.book.find_sheet(name)
}
pub fn sheet_name(&self, index: usize) -> Option<&str> {
self.book.sheets.get(index).map(|s| s.name.as_str())
}
pub fn sheets(&self) -> usize {
self.sheets
}
pub fn frame(&self) -> u64 {
self.frame
}
pub fn in_progress(&self) -> bool {
self.progress.is_some()
}
pub fn resume_at(&mut self, frame: u64) -> Result<(), Refusal> {
if let Some(done) = self.progress {
return Err(self.during_step(done));
}
self.frame = frame;
Ok(())
}
pub fn formulas(&self) -> (usize, usize) {
(self.shapes.formulas(), self.shapes.shapes())
}
fn during_step(&self, done: usize) -> Refusal {
raise(
"grid-write-during-step",
&[
("frame", &thousands(self.frame + 1)),
("evaluated", &thousands(done as u64)),
("of", &thousands(self.shapes.order().len() as u64)),
],
)
}
pub fn step(&mut self, budget: usize) -> Step {
let of = self.shapes.order().len();
if self.progress.is_none() {
self.begin();
}
let start = self.progress.unwrap_or(0);
let end = if budget == 0 {
of
} else {
start.saturating_add(budget).min(of)
};
let keep = end < of;
let mut i = start;
while i < end {
match self.shapes.run_at(i).filter(|_| self.by_runs) {
Some((shape, after)) => {
let to = after.min(end);
self.evaluate_run(shape, i, to, keep);
i = to;
}
None => {
if self.wants(i) {
let id = self.shapes.order()[i];
let computed = self.evaluate(id);
self.keep(i, computed, keep);
}
i += 1;
}
}
}
if end < of {
self.progress = Some(end);
return Step {
frame: self.frame + 1,
evaluated: end,
of,
done: false,
};
}
self.progress = None;
self.frame += 1;
self.before.clear();
self.snapshot();
Step {
frame: self.frame,
evaluated: of,
of,
done: true,
}
}
fn begin(&mut self) {
self.every = self.every_next || !self.incremental;
self.every_next = false;
#[cfg(test)]
{
self.computed = 0;
}
if self.every {
for s in 0..self.shapes.shapes() {
self.parts_moved(s);
}
self.twin_changed.clear();
return;
}
let Machine {
shapes,
marked,
whole,
recheck,
twin_changed,
written,
..
} = self;
let reach = shapes.reach();
for &s in reach.unfollowed() {
whole[s] = true;
}
for id in twin_changed.iter().chain(written.iter()) {
reach.each(*id, &mut |r| match r {
Reached::Cell(p) => marked[p / 64] |= 1 << (p % 64),
Reached::Shape(s) => whole[s] = true,
Reached::Parts(s) => recheck[s] = true,
});
}
twin_changed.clear();
for s in 0..self.shapes.shapes() {
if self.recheck[s] && self.parts_moved(s) {
self.whole[s] = true;
}
}
}
fn parts_moved(&mut self, s: usize) -> bool {
let mut moved = false;
for k in self.shapes.reach().parts(s) {
let anchor = self.shapes.shape(s).map_or((0, 1, 1), |shape| shape.anchor);
self.here.set(anchor);
let got = Ctx::at(self, &self.library, 0, 0).scalar(self.shapes.reach().part(k));
moved |= !self.parts[k]
.as_ref()
.is_some_and(|was| same_got(was, &got));
self.parts[k] = Some(got);
}
moved
}
fn wants(&self, pos: usize) -> bool {
self.every
|| self.whole[self.shapes.reach().shape_at(pos)]
|| self.marked[pos / 64] & (1 << (pos % 64)) != 0
}
fn evaluate(&self, id: CellId) -> Computed {
self.here.set(id);
self.shapes.evaluate(self, &self.library, id)
}
fn evaluate_run(&mut self, shape: usize, from: usize, to: usize, keep: bool) {
let mut lanes = std::mem::take(&mut self.lanes);
let mut answers = std::mem::take(&mut self.answers);
let mut gathered = std::mem::take(&mut self.gathered);
let mut gathered_at = std::mem::take(&mut self.gathered_at);
let every = self.every || self.whole[shape];
let mut i = from;
while i < to {
if every {
let j = (i + vector::CHUNK).min(to);
let cells = &self.shapes.order()[i..j];
self.shapes.evaluate_run(
self,
&self.library,
shape,
cells,
&mut lanes,
&mut answers,
);
for (k, computed) in answers.drain(..).enumerate() {
self.keep(i + k, computed, keep);
}
i = j;
continue;
}
gathered.clear();
gathered_at.clear();
while gathered.len() < vector::CHUNK {
match next_marked(&self.marked, i, to) {
Some(p) => {
gathered.push(self.shapes.order()[p]);
gathered_at.push(p as u32);
i = p + 1;
}
None => {
i = to;
break;
}
}
}
if gathered.len() < RUN {
for (&id, &p) in gathered.iter().zip(&gathered_at) {
let computed = self.evaluate(id);
self.keep(p as usize, computed, keep);
}
continue;
}
self.shapes.evaluate_run(
self,
&self.library,
shape,
&gathered,
&mut lanes,
&mut answers,
);
for (&p, computed) in gathered_at.iter().zip(answers.drain(..)) {
self.keep(p as usize, computed, keep);
}
}
self.lanes = lanes;
self.answers = answers;
self.gathered = gathered;
self.gathered_at = gathered_at;
}
fn keep(&mut self, p: usize, computed: Computed, keep: bool) {
#[cfg(test)]
{
self.computed += 1;
}
let held = &mut self.values[p];
if let Some(h) = held {
if same_computed(h, &computed) {
return;
}
}
let was = held.replace(computed);
if keep {
self.before.entry(p as u32).or_insert(was);
}
if self.changed.len() < self.changed.capacity() {
self.changed.push(p as u32);
} else {
self.changed_over = true;
}
if !self.every {
let Machine {
shapes,
marked,
whole,
..
} = self;
let id = shapes.order()[p];
shapes.reach().each(id, &mut |r| match r {
Reached::Cell(p) => marked[p / 64] |= 1 << (p % 64),
Reached::Shape(s) | Reached::Parts(s) => whole[s] = true,
});
}
}
fn snapshot(&mut self) {
self.marked.fill(0);
self.whole.fill(false);
self.recheck.fill(false);
self.twin_changed.clear();
let few = (self.changed.len() + self.written.len()) * 2 <= self.cells;
if !(self.incremental && !self.changed_over && few && self.planes_kept() && self.copy()) {
self.walk();
}
self.changed.clear();
self.changed_over = false;
self.written.clear();
}
fn planes_kept(&self) -> bool {
(0..self.sheets).all(|s| {
!self.wanted[s].get()
|| self.planes[s].is_some()
|| !plane::fits(self.extents[s], CELLS as u64)
})
}
fn copy(&mut self) -> bool {
let Machine {
book,
sheets,
shapes,
values,
twin_reasons,
wanted,
planes,
dense,
behind,
lagging,
lazy,
changed,
written,
twin_changed,
every_next,
..
} = self;
let (rows, twins) = book.sheets.split_at_mut(*sheets);
let mut to = Twins {
sheets: *sheets,
twins,
twin_reasons,
wanted,
planes,
dense,
behind,
lagging,
twin_changed,
every_next,
};
for &p in changed.iter() {
let (id, value) = (shapes.order()[p as usize], values[p as usize].as_ref());
let (content, byte) = (formula_twin(value), plane::formula_byte(value));
let put = match *lazy {
true => to.put_behind(id, content, value, byte),
false => to.put(id, content, value, byte),
};
if !put {
return false;
}
}
for &id in written.iter() {
let Some(cell) = rows[id.0].cells.get(&(id.1, id.2)) else {
return false;
};
let value = is_formula(&cell.content).then(|| value_at(shapes, values, id));
let byte = plane::kept_byte(&cell.content, value.flatten());
if !to.put(id, twin_content(cell, value), value.flatten(), byte) {
return false;
}
}
true
}
fn walk(&mut self) {
self.make_twins();
std::mem::swap(&mut self.twin_reasons, &mut self.reasons_before);
self.twin_reasons.clear();
self.twin_changed.clear();
let sheets = self.sheets;
let (rows, twins) = self.book.sheets.split_at_mut(sheets);
for (s, (sheet, twin)) in rows.iter().zip(twins.iter_mut()).enumerate() {
let t = sheets + s;
let mut raster = if self.wanted[s].get() {
plane::Raster::over(self.extents[s], CELLS as u64, self.planes[s].take())
} else {
None
};
let slots = (sheet.cells.len() as u64).saturating_mul(SLOTS_A_CELL);
let mut kept = Dense::over(
self.extents[s],
slots.min(CELLS as u64),
self.dense[s].take(),
);
let mut fits = true;
let mut whole = true;
let mut stand = twin.cells.iter_mut();
let mut in_place = true;
for (&(row, col), cell) in &sheet.cells {
let value = is_formula(&cell.content)
.then(|| value_at(&self.shapes, &self.values, (s, row, col)));
if let Some(r) = raster.as_mut() {
fits &= r.set(row, col, plane::kept_byte(&cell.content, value.flatten()));
}
let reason = match value {
Some(Some(Computed::NotComputed(r))) => Some(r),
_ => None,
};
if let Some(r) = reason {
self.twin_reasons.insert((t, row, col), r.clone());
}
if self.reasons_before.get(&(t, row, col)) != reason {
note(&mut self.twin_changed, &mut self.every_next, (t, row, col));
}
let content = twin_content(cell, value);
if let Some(k) = kept.as_mut() {
match k.slot(row, col) {
Some(slot) => refresh(slot, content.as_ref()),
None => whole = false,
}
}
if let (Some(content), true) = (content, in_place) {
match stand.next() {
Some((&at, held)) if at == (row, col) => {
if !same_content(&held.content, &content) {
held.content = content;
note(&mut self.twin_changed, &mut self.every_next, (t, row, col));
}
}
_ => in_place = false,
}
}
}
let in_place = in_place && stand.next().is_none();
self.planes[s] = raster.filter(|_| fits);
self.dense[s] = kept.filter(|_| whole);
let words = self.dense[s].as_ref().map_or(0, |k| k.slots().div_ceil(64));
self.behind[s].clear();
self.behind[s].resize(words, 0);
if !in_place {
self.every_next = true;
twin.cells = sheet
.cells
.iter()
.filter_map(|(&(row, col), cell)| {
let value = is_formula(&cell.content)
.then(|| value_at(&self.shapes, &self.values, (s, row, col)));
let content = twin_content(cell, value)?;
Some(((row, col), Cell { content, style: 0 }))
})
.collect();
self.extents[t] = twin.extent();
}
}
}
fn make_twins(&mut self) {
let Machine {
book,
sheets,
dense,
behind,
lagging,
..
} = self;
for s in 0..*sheets {
if !std::mem::take(&mut lagging[s]) {
continue;
}
let Some(k) = dense[s].as_ref() else {
behind[s].fill(0);
continue;
};
let twin = &mut book.sheets[*sheets + s];
for (w, word) in behind[s].iter_mut().enumerate() {
let mut bits = std::mem::take(word);
while bits != 0 {
let (slot, at) = k.at(w * 64 + bits.trailing_zeros() as usize);
bits &= bits - 1;
match twin.cells.get_mut(&at) {
Some(cell) => cell.content = value_content(slot),
None => debug_assert!(false, "a twin cell behind its slot is a cell"),
}
}
}
}
}
pub fn view(
&self,
projection: &str,
sheet: &str,
window: Option<&str>,
) -> Result<Viewed, Refusal> {
match projection {
"grid" => {
let w = window_of(&self.book, sheet, window)?;
Ok(Viewed::Text(view_text_valued(&self.book, &w, self)))
}
"plane" => {
let w = window_of(&self.book, sheet, window)?;
let cells = w.range.cells();
if cells > CELLS as u64 {
return Err(raise(
"view-window-too-large",
&[
("window", &w.range.text()),
("cells", &thousands(cells)),
("limit", &thousands(CELLS as u64)),
],
));
}
let rows_sheet = self.book.find_sheet(&w.sheet).filter(|&s| s < self.sheets);
if let Some(s) = rows_sheet {
self.wanted[s].set(true);
if let Some(kept) = &self.planes[s] {
return Ok(Viewed::Bytes(kept.window(&w.range)));
}
}
Ok(Viewed::Bytes(plane::bytes(&self.book, self, &w)))
}
other => Err(raise("view-projection-unknown", &[("name", other)])),
}
}
pub fn write(&mut self, text: &str) -> Result<usize, LineRefusal> {
if let Some(done) = self.progress {
return Err(LineRefusal {
line: 0,
refusal: self.during_step(done),
});
}
let forms = read_forms(text).map_err(|refusal| LineRefusal { line: 0, refusal })?;
let mut log = Log::default();
for form in &forms {
if let Err(e) = self.write_row(form, &mut log) {
self.undo(&log);
return Err(e);
}
}
self.settle(log)
}
fn write_row(&mut self, form: &Form, log: &mut Log) -> Result<(), LineRefusal> {
let Some((head, items, line)) = load::row_parts(form) else {
let line = form.as_list().map(|l| l.line).unwrap_or(0);
return Err(load::malformed(line, form, ""));
};
if !matches!(head, "cell" | "formula" | "derived") {
return Err(load::unknown(line, head, WRITE_ROWS));
}
let [Form::Str(sheet), Form::Str(addr), third] = items else {
return Err(load::malformed(line, form, head));
};
let Some(range) = parse_a1_range(addr) else {
return Err(load::malformed(line, form, head));
};
let what = match head {
"formula" => match third {
Form::Str(t) if t.len() > 1 && t.starts_with('=') => None,
_ => return Err(load::malformed(line, form, head)),
},
_ => Some(load::datum(third).ok_or_else(|| load::malformed(line, form, head))?),
};
let Some(si) = self.book.find_sheet(sheet) else {
let sheets: Vec<&str> = self.book.sheets[..self.sheets]
.iter()
.map(|s| s.name.as_str())
.collect();
return Err(LineRefusal {
line,
refusal: raise(
"grid-sheet-unknown",
&[
("target", &format!("{sheet}!{addr}")),
("name", sheet),
("sheets", &sheets.join(", ")),
],
),
});
};
if si >= self.sheets {
return Err(load::into_twin(line, &self.book.sheets[si].name, addr));
}
let count = range.cells();
if count > CELLS as u64 {
return Err(load::too_many(line, sheet, addr, self.cells as u64 + count));
}
let present = self.book.sheets[si]
.cells
.range((range.top, 0)..=(range.bottom, u32::MAX))
.filter(|((_, col), _)| *col >= range.left && *col <= range.right)
.count() as u64;
let grows = count - present;
let now = (self.cells + log.added) as u64;
if now + grows > CELLS as u64 {
return Err(load::too_many(line, sheet, addr, now + grows));
}
if head == "derived" {
let formula_cell = self.book.sheets[si]
.cells
.range((range.top, 0)..=(range.bottom, u32::MAX))
.find(|((_, col), c)| {
*col >= range.left && *col <= range.right && is_formula(&c.content)
})
.map(|(&(row, col), _)| (si, row, col));
if let Some(id) = formula_cell {
return Err(LineRefusal {
line,
refusal: raise(
"grid-write-derived",
&[
("target", &format!("{sheet}!{addr}")),
("cell", &spell_cell(&self.book, id)),
],
),
});
}
}
match (what, third) {
(Some(value), _) => self.put_values(si, range, &value.content(), line, log),
(None, Form::Str(text)) => self.put_formula(si, range, &text[1..], line, log),
(None, _) => Err(load::malformed(line, form, head)),
}
}
fn set_cell(&mut self, id: CellId, cell: Option<Cell>, log: &mut Log) {
let sheet = &mut self.book.sheets[id.0];
let old = match cell {
Some(c) => sheet.cells.insert((id.1, id.2), c),
None => sheet.cells.remove(&(id.1, id.2)),
};
log.undo.push((id, old));
log.sheets.insert(id.0);
}
fn release_masters(&mut self, si: usize, range: A1Range, log: &mut Log) {
let masters: Vec<(u32, u32, u32, String)> = self.book.sheets[si]
.cells
.range((range.top, 0)..=(range.bottom, u32::MAX))
.filter(|((_, col), _)| *col >= range.left && *col <= range.right)
.filter_map(|(&(row, col), c)| match &c.content {
Content::SharedMaster {
text, si: index, ..
} => Some((row, col, *index, text.clone())),
_ => None,
})
.collect();
for (m_row, m_col, index, text) in masters {
let children: Vec<((u32, u32), u32)> = self.book.sheets[si]
.cells
.iter()
.filter(|((row, col), c)| {
matches!(c.content, Content::SharedChild { si } if si == index)
&& !(*row >= range.top
&& *row <= range.bottom
&& *col >= range.left
&& *col <= range.right)
})
.map(|(&k, c)| (k, c.style))
.collect();
for ((row, col), style) in children {
let moved = shift_a1_references(
&text,
i64::from(row) - i64::from(m_row),
i64::from(col) - i64::from(m_col),
);
self.set_cell(
(si, row, col),
Some(Cell {
content: Content::Formula(moved),
style,
}),
log,
);
}
}
}
fn put_values(
&mut self,
si: usize,
range: A1Range,
content: &Content,
line: u32,
log: &mut Log,
) -> Result<(), LineRefusal> {
self.release_masters(si, range, log);
for row in range.top..=range.bottom {
for col in range.left..=range.right {
let old = self.book.sheets[si].cells.get(&(row, col)).cloned();
if let Some(o) = &old {
if !is_formula(&o.content) && o.content == *content {
continue;
}
}
let style = old.as_ref().map(|c| c.style).unwrap_or(0);
if old.is_none() {
log.added += 1;
}
self.set_cell(
(si, row, col),
Some(Cell {
content: content.clone(),
style,
}),
log,
);
log.changed.entry((si, row, col)).or_insert(line);
log.written += 1;
}
}
Ok(())
}
fn put_formula(
&mut self,
si: usize,
range: A1Range,
text: &str,
line: u32,
log: &mut Log,
) -> Result<(), LineRefusal> {
let masters_text: HashMap<u32, (String, u32, u32)> = shared_masters(&self.book.sheets[si])
.into_iter()
.map(|(k, (t, r, c))| (k, (t.to_string(), r, c)))
.collect();
let masters: HashMap<u32, (&str, u32, u32)> = masters_text
.iter()
.map(|(k, (t, r, c))| (*k, (t.as_str(), *r, *c)))
.collect();
let mut differs: Vec<(u32, u32)> = Vec::new();
for row in range.top..=range.bottom {
for col in range.left..=range.right {
let moved = shift_a1_references(
text,
i64::from(row) - i64::from(range.top),
i64::from(col) - i64::from(range.left),
);
let old_text = self.book.sheets[si]
.cells
.get(&(row, col))
.and_then(|c| formula_text(&c.content, row, col, &masters));
if old_text.as_deref() != Some(crate::sheet::strip_future_prefixes(&moved).as_str())
{
differs.push((row, col));
}
}
}
if differs.is_empty() {
return Ok(());
}
self.release_masters(si, range, log);
let mut olds: Vec<((u32, u32), Option<Cell>)> = Vec::with_capacity(range.cells() as usize);
for row in range.top..=range.bottom {
for col in range.left..=range.right {
olds.push((
(row, col),
self.book.sheets[si].cells.get(&(row, col)).cloned(),
));
}
}
let style_of: HashMap<(u32, u32), u32> = olds
.iter()
.filter_map(|(k, c)| c.as_ref().map(|c| (*k, c.style)))
.collect();
self.book.sheets[si].set_formula(range, text, 0);
for (k, old) in olds {
if old.is_none() {
log.added += 1;
}
if let Some(style) = style_of.get(&k) {
if let Some(c) = self.book.sheets[si].cells.get_mut(&k) {
c.style = *style;
}
}
log.undo.push(((si, k.0, k.1), old));
log.changed.entry((si, k.0, k.1)).or_insert(line);
}
log.sheets.insert(si);
log.written += differs.len();
Ok(())
}
fn undo(&mut self, log: &Log) {
for (id, old) in log.undo.iter().rev() {
let sheet = &mut self.book.sheets[id.0];
match old {
Some(c) => {
sheet.cells.insert((id.1, id.2), c.clone());
}
None => {
sheet.cells.remove(&(id.1, id.2));
}
}
}
for &s in &log.sheets {
self.extents[s] = self.book.sheets[s].extent();
}
}
fn settle(&mut self, log: Log) -> Result<usize, LineRefusal> {
if log.changed.is_empty() {
return Ok(log.written);
}
for &s in &log.sheets {
self.extents[s] = self.book.sheets[s].extent();
}
let formula_changed = log.changed.keys().any(|id| {
self.shapes.holds(*id)
|| self.book.sheets[id.0]
.cells
.get(&(id.1, id.2))
.is_some_and(|c| is_formula(&c.content))
});
if formula_changed {
let saved = self.shapes.clone();
for id in log.changed.keys() {
self.shapes.replace(&self.book, *id, &self.library);
}
self.shapes.plan(&self.book, &self.names, &self.extents);
let placed: Vec<CellId> = log
.changed
.keys()
.copied()
.filter(|id| self.shapes.holds(*id))
.collect();
if let Some((cell, refusal)) = self.shapes.first_refusal(&self.book, Some(&placed)) {
let line = if refusal.id == "calc-cycle" {
self.shapes
.cycles()
.first()
.and_then(|c| c.iter().filter_map(|id| log.changed.get(id)).min().copied())
.unwrap_or(0)
} else {
log.changed.get(&cell).copied().unwrap_or(0)
};
self.shapes = saved;
self.undo(&log);
return Err(LineRefusal { line, refusal });
}
let mut moved: Vec<Option<Computed>> = vec![None; self.shapes.order().len()];
for (&id, value) in saved.order().iter().zip(std::mem::take(&mut self.values)) {
if let (Some(q), Some(v)) = (self.shapes.reach().position(id), value) {
moved[q] = Some(v);
}
}
self.values = moved;
}
self.cells += log.added;
if formula_changed || log.added > 0 {
self.every_next = true;
self.fit();
}
for id in log.changed.keys() {
if let Some(p) = self.shapes.reach().position(*id) {
self.values[p] = None;
}
if !self.shapes.holds(*id) {
self.written.push(*id);
}
}
for &s in &log.sheets {
if let Some(kept) = self.planes.get_mut(s) {
*kept = None;
}
}
Ok(log.written)
}
}
impl Values for Machine {
fn computed(&self, id: CellId) -> Option<&Computed> {
let p = self.shapes.reach().position(id)?;
if !self.before.is_empty() {
if let Some(was) = self.before.get(&(p as u32)) {
return was.as_ref();
}
}
self.values[p].as_ref()
}
fn lags(&self, sheet: usize) -> bool {
sheet
.checked_sub(self.sheets)
.and_then(|s| self.lagging.get(s))
.is_some_and(|&l| l)
}
fn content(&self, id: CellId) -> Option<Content> {
let s = id.0.checked_sub(self.sheets)?;
let k = self.dense.get(s)?.as_ref()?;
let i = k.index(id.1, id.2)?;
let bit = self.behind.get(s)?.get(i / 64)? >> (i % 64) & 1;
(bit == 1).then(|| value_content(k.at(i).0))
}
}
impl Env for Machine {
fn model(&self) -> &Workbook {
&self.book
}
fn here(&self) -> CellId {
self.here.get()
}
fn read(&self, id: CellId) -> Result<Value, Reason> {
if id.0 < self.sheets {
if let Some(p) = self.shapes.reach().position(id) {
return match &self.values[p] {
Some(Computed::Value(v)) => Ok(v.clone()),
Some(Computed::NotComputed(r)) => Err(r.clone()),
None => Err(Reason::Cycle),
};
}
} else if let Some(kept) = self.dense(id.0) {
if let Some(v) = kept.get(id.1, id.2).filter(|v| !matches!(v, Value::Empty)) {
return Ok(v.clone());
}
return match self.twin_reasons.get(&id) {
Some(r) => Err(r.clone()),
None => Ok(Value::Empty),
};
}
let Some(cell) = self
.book
.sheets
.get(id.0)
.and_then(|s| s.cells.get(&(id.1, id.2)))
else {
return match self.twin_reasons.get(&id) {
Some(r) => Err(r.clone()),
None => Ok(Value::Empty),
};
};
Ok(content_value(&cell.content))
}
fn extent(&self, sheet: usize) -> Extent {
self.extents.get(sheet).copied().flatten()
}
fn name(&self, name: &str, scope: Option<usize>, home: usize) -> Option<&str> {
self.names.resolve(name, scope, home)
}
fn dense(&self, sheet: usize) -> Option<&Dense> {
let s = sheet.checked_sub(self.sheets)?;
self.dense.get(s)?.as_ref()
}
fn enter(&self, id: CellId) {
self.here.set(id);
}
}
fn value_at<'v>(
shapes: &Shapes,
values: &'v [Option<Computed>],
id: CellId,
) -> Option<&'v Computed> {
values.get(shapes.reach().position(id)?)?.as_ref()
}
fn content_value(content: &Content) -> Value {
match content {
Content::Text(t) => Value::Text(t.clone()),
Content::Number(n) => Value::Number(*n),
Content::Bool(b) => Value::Bool(*b),
Content::Error(e) => Value::Error(ErrorKind::parse(e).unwrap_or(ErrorKind::Value)),
_ => Value::Empty,
}
}
fn twin_content(cell: &Cell, value: Option<Option<&Computed>>) -> Option<Content> {
match value {
None => Some(cell.content.clone()),
Some(value) => formula_twin(value),
}
}
fn formula_twin(value: Option<&Computed>) -> Option<Content> {
match value {
Some(Computed::Value(v)) => Some(value_content(v)),
Some(Computed::NotComputed(_)) | None => None,
}
}
fn refresh(slot: &mut Value, content: Option<&Content>) {
match (slot, content) {
(Value::Text(held), Some(Content::Text(t))) => {
if *held != *t {
held.clone_from(t);
}
}
(slot, content) => *slot = content.map_or(Value::Empty, content_value),
}
}
struct Twins<'a> {
sheets: usize,
twins: &'a mut [Sheet],
twin_reasons: &'a mut FxHashMap<CellId, Reason>,
wanted: &'a [Slot<bool>],
planes: &'a mut [Option<plane::Raster>],
dense: &'a mut [Option<Dense>],
behind: &'a mut [Vec<u64>],
lagging: &'a mut [bool],
twin_changed: &'a mut Vec<CellId>,
every_next: &'a mut bool,
}
impl Twins<'_> {
fn put(
&mut self,
id: CellId,
content: Option<Content>,
value: Option<&Computed>,
byte: u8,
) -> bool {
let (s, row, col) = id;
if let Some(Some(k)) = self.dense.get_mut(s) {
if let Some(i) = k.index(row, col) {
refresh(k.slot_at(i), content.as_ref());
self.behind[s][i / 64] &= !(1 << (i % 64));
}
}
let mut moved = match (self.twins[s].cells.get_mut(&(row, col)), content) {
(Some(held), Some(content)) => {
let differs = !same_content(&held.content, &content);
if differs {
held.content = content;
}
differs
}
(None, None) => false,
_ => return false,
};
moved |= self.byte_and_reason(id, value, byte);
if moved {
note(
self.twin_changed,
self.every_next,
(self.sheets + s, row, col),
);
}
true
}
fn put_behind(
&mut self,
id: CellId,
content: Option<Content>,
value: Option<&Computed>,
byte: u8,
) -> bool {
let (s, row, col) = id;
let Some(Some(k)) = self.dense.get_mut(s) else {
return self.put(id, content, value, byte);
};
let Some(i) = k.index(row, col) else {
return self.put(id, content, value, byte);
};
let slot = k.slot_at(i);
if matches!(slot, Value::Empty) == content.is_some() {
return false;
}
let mut moved = !same_slot(slot, content.as_ref());
if moved {
refresh(slot, content.as_ref());
self.behind[s][i / 64] |= 1 << (i % 64);
self.lagging[s] = true;
}
moved |= self.byte_and_reason(id, value, byte);
if moved {
note(
self.twin_changed,
self.every_next,
(self.sheets + s, row, col),
);
}
true
}
fn byte_and_reason(&mut self, id: CellId, value: Option<&Computed>, byte: u8) -> bool {
let (s, row, col) = id;
if self.wanted[s].get() {
if let Some(r) = self.planes[s].as_mut() {
r.set(row, col, byte);
}
}
let t = (self.sheets + s, row, col);
let reason = match value {
Some(Computed::NotComputed(r)) => Some(r),
_ => None,
};
if self.twin_reasons.get(&t) == reason {
return false;
}
match reason {
Some(r) => self.twin_reasons.insert(t, r.clone()),
None => self.twin_reasons.remove(&t),
};
true
}
}
fn same_slot(slot: &Value, content: Option<&Content>) -> bool {
match (slot, content) {
(Value::Empty, None) => true,
(Value::Number(x), Some(Content::Number(y))) => x.to_bits() == y.to_bits(),
(Value::Text(x), Some(Content::Text(y))) => x == y,
(Value::Bool(x), Some(Content::Bool(y))) => x == y,
(Value::Error(k), Some(Content::Error(e))) => {
*k == ErrorKind::parse(e).unwrap_or(ErrorKind::Value)
}
_ => false,
}
}
fn note(changed: &mut Vec<CellId>, every_next: &mut bool, id: CellId) {
if changed.len() < changed.capacity() {
changed.push(id);
} else {
*every_next = true;
}
}
fn next_marked(marked: &[u64], from: usize, to: usize) -> Option<usize> {
let mut w = from / 64;
let mut word = *marked.get(w)? & (!0u64 << (from % 64));
loop {
if word != 0 {
let p = w * 64 + word.trailing_zeros() as usize;
return (p < to).then_some(p);
}
w += 1;
if w * 64 >= to {
return None;
}
word = *marked.get(w)?;
}
}
fn same_value(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(x), Value::Number(y)) => x.to_bits() == y.to_bits(),
_ => a == b,
}
}
fn same_got(a: &Result<Value, Reason>, b: &Result<Value, Reason>) -> bool {
match (a, b) {
(Ok(x), Ok(y)) => same_value(x, y),
(Err(x), Err(y)) => x == y,
_ => false,
}
}
fn same_computed(a: &Computed, b: &Computed) -> bool {
match (a, b) {
(Computed::Value(x), Computed::Value(y)) => same_value(x, y),
(Computed::NotComputed(x), Computed::NotComputed(y)) => x == y,
_ => false,
}
}
fn same_content(a: &Content, b: &Content) -> bool {
match (a, b) {
(Content::Number(x), Content::Number(y)) => x.to_bits() == y.to_bits(),
_ => a == b,
}
}
fn value_content(v: &Value) -> Content {
match v {
Value::Number(n) => Content::Number(*n),
Value::Text(t) => Content::Text(t.clone()),
Value::Bool(b) => Content::Bool(*b),
Value::Error(k) => Content::Error(k.text().to_string()),
Value::Empty => Content::Number(0.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calc::library;
use crate::calc::shape::Shapes;
use crate::projection::Window;
use crate::reader::read_forms;
use crate::view::{view_text_valued, whole};
fn load(text: &str) -> Machine {
Machine::load(text, library::language())
.unwrap_or_else(|e| panic!("line {}: {}", e.line, e.refusal.text))
}
fn refused(text: &str) -> LineRefusal {
match Machine::load(text, library::language()) {
Ok(m) => panic!("loaded: {m:?}"),
Err(e) => e,
}
}
fn grid(m: &Machine, sheet: &str, window: Option<&str>) -> String {
match m.view("grid", sheet, window) {
Ok(Viewed::Text(t)) => t,
other => panic!("{other:?}"),
}
}
fn plane(m: &Machine, sheet: &str, window: &str) -> Vec<u8> {
match m.view("plane", sheet, Some(window)) {
Ok(Viewed::Bytes(b)) => b,
other => panic!("{other:?}"),
}
}
fn at(m: &Machine, sheet: &str, addr: &str) -> Option<Computed> {
let i = m.find_sheet(sheet).unwrap();
let r = parse_a1_range(addr).unwrap();
m.computed((i, r.top, r.left)).cloned()
}
fn number(m: &Machine, sheet: &str, addr: &str) -> f64 {
match at(m, sheet, addr) {
Some(Computed::Value(Value::Number(n))) => n,
other => panic!("{sheet}!{addr}: {other:?}"),
}
}
fn content(m: &Machine, sheet: &str, addr: &str) -> Option<Content> {
let i = m.find_sheet(sheet).unwrap();
let r = parse_a1_range(addr).unwrap();
let held = m.book.sheets[i].cells.get(&(r.top, r.left))?;
Some(Values::content(m, (i, r.top, r.left)).unwrap_or_else(|| held.content.clone()))
}
fn formula_rows(m: &Machine) -> Vec<String> {
let mut out = Vec::new();
for s in &m.book.sheets {
for line in grid(m, &s.name, None).lines() {
if line.starts_with("(formula ") {
out.push(line.to_string());
}
}
}
out
}
fn save(m: &Machine) -> String {
let mut out = String::new();
for s in &m.book.sheets[..m.sheets()] {
out.push_str(&grid(m, &s.name, None));
}
out
}
#[derive(Debug)]
struct Held(HashMap<CellId, Computed>);
impl Values for Held {
fn computed(&self, id: CellId) -> Option<&Computed> {
self.0.get(&id)
}
}
const COUNTER: &str = "\
(sheet \"Board\" visible)
(formula \"Board\" \"A1\" \"=Board.last!A1+1\")
(formula \"Board\" \"B1\" \"=A1*10\")
(formula \"Board\" \"C1\" \"=SUM(A1:B1)\")
(cell \"Board\" \"D1\" 5)
(formula \"Board\" \"E1\" \"=D1+C1\")
";
#[test]
fn handles_start_at_one_are_never_reused_and_hold_sixteen() {
let mut h: Handles<u8> = Handles::new();
for n in 1..=16u32 {
assert_eq!(h.insert(n as u8), Ok(n));
}
let full = h.insert(0).unwrap_err();
assert_eq!(full.id, "grid-handle-limit");
assert!(full.text.contains("16 grids"), "{}", full.text);
assert_eq!(h.remove(3), Ok(3));
let gone = h.get(3).unwrap_err();
assert_eq!(gone.id, "grid-handle-unknown");
assert!(gone.text.contains("handle 3"), "{}", gone.text);
assert_eq!(h.get(0).unwrap_err().id, "grid-handle-unknown");
assert_eq!(h.insert(9), Ok(17), "a handle is never given twice");
assert_eq!(h.live(), 16);
assert_eq!(h.remove(3).unwrap_err().id, "grid-handle-unknown");
}
#[test]
fn a_load_answers_its_counts_and_computes_nothing() {
let m = load(COUNTER);
assert_eq!(
m.loaded(),
Loaded {
sheets: 1,
cells: 5,
formulas: 4
}
);
assert_eq!(m.loaded().row(7), "(loaded 7 1 5 4)");
assert_eq!(m.frame(), 0);
assert!(at(&m, "Board", "A1").is_none(), "nothing computed at load");
let record = grid(&m, "Board", None);
assert!(!record.contains("(value "), "{record}");
assert!(record.starts_with(
"(sheet \"Board\" visible)\n(sheet \"Board.last\" hidden)\n(window \"Board\" \"A1:E1\")\n"
));
}
#[test]
fn a_step_reads_the_last_frame_from_the_twin_and_this_frame_from_the_grid() {
let mut m = load(COUNTER);
let s = m.step(0);
assert_eq!(s.row(), "(step 1 4 4 done)");
assert_eq!(number(&m, "Board", "A1"), 1.0, "an empty twin reads as 0");
assert_eq!(number(&m, "Board", "B1"), 10.0, "this frame's A1");
assert_eq!(number(&m, "Board", "C1"), 11.0);
assert_eq!(number(&m, "Board", "E1"), 16.0);
assert_eq!(content(&m, "Board.last", "A1"), Some(Content::Number(1.0)));
assert_eq!(content(&m, "Board.last", "D1"), Some(Content::Number(5.0)));
assert_eq!(m.step(0).row(), "(step 2 4 4 done)");
assert_eq!(number(&m, "Board", "A1"), 2.0);
assert_eq!(number(&m, "Board", "B1"), 20.0);
let twin = grid(&m, "Board.last", None);
assert!(twin.contains("(cell \"Board.last\" \"B1\" 20)"), "{twin}");
assert!(!twin.contains("(formula "), "a twin holds values only");
let record = grid(&m, "Board", None);
assert!(record.contains(
"(formula \"Board\" \"A1\" \"=Board.last!A1+1\")\n(value \"Board\" \"A1\" 2)\n"
));
}
#[test]
fn a_budget_yields_resumes_and_never_changes_a_frame() {
let mut whole = load(COUNTER);
let mut chunked = load(COUNTER);
for frame in 1..=3u64 {
assert!(whole.step(0).done);
let first = chunked.step(3);
assert_eq!(first.row(), format!("(step {frame} 3 4 yielded)"));
let r = chunked.write("(cell \"Board\" \"D1\" 9)").unwrap_err();
assert_eq!(
(r.line, r.refusal.id.as_str()),
(0, "grid-write-during-step")
);
assert!(
r.refusal
.text
.starts_with(&format!("Frame {frame} is being computed, 3 of 4 cells")),
"{}",
r.refusal.text
);
let last = (frame > 1).then(|| Computed::Value(Value::Number((frame - 1) as f64)));
assert_eq!(at(&chunked, "Board", "A1"), last);
assert_eq!(chunked.step(3).row(), format!("(step {frame} 4 4 done)"));
assert_eq!(save(&whole), save(&chunked), "frame {frame}");
}
assert_eq!(content(&chunked, "Board", "D1"), Some(Content::Number(5.0)));
}
#[test]
fn a_step_writes_no_formula() {
let mut m = load(COUNTER);
let before = formula_rows(&m);
for _ in 0..3 {
m.step(0);
}
assert_eq!(
formula_rows(&m),
before,
"AD-7: the formula cells are unchanged"
);
assert_eq!(before.len(), 4);
}
#[test]
fn one_write_per_cell_names_both_lines() {
let e = refused("(cell \"S\" \"A1\" 1)\n(cell \"S\" \"B1\" 2)\n(cell \"S\" \"A1\" 1)\n");
assert_eq!(
(e.line, e.refusal.id.as_str()),
(3, "grid-cell-written-twice")
);
assert!(
e.refusal
.text
.starts_with("S!A1 was already written by line 1;"),
"{}",
e.refusal.text
);
let e = refused("(formula \"S\" \"A1:C3\" \"=1\")\n(cell \"S\" \"B2\" 1)\n");
assert_eq!(
(e.line, e.refusal.id.as_str()),
(2, "grid-cell-written-twice")
);
assert!(e.refusal.text.contains("S!B2"), "{}", e.refusal.text);
let m = load(
"(sheet \"S\" visible)\n(sheet \"S\" visible)\n(format 0 none general nowrap)\n(format 0 none general nowrap)\n(cell \"S\" \"A1\" 1)\n",
);
assert_eq!(m.loaded().cells, 1);
let e = refused("(format 1 none general nowrap)\n(format 1 none text nowrap)\n");
assert_eq!(
(e.line, e.refusal.id.as_str()),
(2, "grid-cell-written-twice")
);
assert!(e.refusal.text.contains("(format 1)"), "{}", e.refusal.text);
let e = refused("(cell \"S\" \"A1\" 1)\n(formula \"S\" \"B1\" \"=A1\")\n(value \"S\" \"B1\" 1)\n(value \"S\" \"B1\" 1)\n");
assert_eq!(
(e.line, e.refusal.id.as_str()),
(4, "grid-cell-written-twice")
);
}
#[test]
fn a_load_refuses_what_it_cannot_hold_by_name() {
let cases: &[(&str, u32, &str)] = &[
("(chart \"S\" \"A1\")\n", 1, "grid-row-unknown"),
("(cell \"S\" \"A0\" 1)\n", 1, "grid-row-malformed"),
("(cell \"S\" \"A1\" inf)\n", 1, "grid-row-malformed"),
(
"(cell \"S\" \"A1\" (date \"2026-10-09\"))\n",
1,
"grid-row-malformed",
),
("(formula \"S\" \"A1\" \"1+1\")\n", 1, "grid-row-malformed"),
("(value \"S\" \"A1\" 1)\n", 1, "grid-row-malformed"),
(
"(cell \"S\" \"A1\" 1)\n(value \"S\" \"A1\" 1)\n",
2,
"grid-row-malformed",
),
("\n\n(cell \"Board.last\" \"A1\" 1)\n", 3, "grid-write-last"),
(
"(sheet \"Board\" visible)\n(gridlines \"Board.last\" off)\n",
2,
"grid-sheet-name-invalid",
),
(
"(column \"Board.last\" \"A\" 10 shown none)\n",
1,
"grid-sheet-name-invalid",
),
(
"(cell \"Twenty-seven characters, ab\" \"A1\" 1)\n",
1,
"grid-sheet-name-invalid",
),
("(cell \"a:b\" \"A1\" 1)\n", 1, "grid-sheet-name-invalid"),
("(cell \".last\" \"A1\" 1)\n", 1, "grid-sheet-name-invalid"),
(
"(sheet \"Board.last\" hidden)\n",
1,
"grid-sheet-name-invalid",
),
(
"(cell \"S\" \"A1:XFD1048576\" 0)\n",
1,
"grid-too-many-cells",
),
(
"(formula \"S\" \"A1\" \"=B1\")\n(formula \"S\" \"B1\" \"=A1\")\n",
1,
"calc-cycle",
),
(
"(cell \"S\" \"A1\" 1)\n(formula \"S\" \"B2\" \"=NOW()\")\n",
2,
"calc-function-not-computed",
),
(
"(formula \"S\" \"C3\" \"={1,2}\")\n",
1,
"calc-construct-not-read",
),
(
"(name \"Rate\" \"0.2\")\n(name \"rate\" \"0.3\")\n",
2,
"grid-cell-written-twice",
),
("(cell \"S\" \"A1\" 1)\n(oops\n", 0, "vla-unbalanced-parens"),
];
for (text, line, id) in cases {
let e = refused(text);
assert_eq!((e.line, e.refusal.id.as_str()), (*line, *id), "{text}");
assert!(!e.refusal.text.starts_with("Line "), "{}", e.refusal.text);
}
let e = refused("(cell \"S\" \"A1\" 1)\n(formula \"S\" \"B2\" \"=NOW()\")\n");
assert!(
e.refusal.text.contains("S!B2") && e.refusal.text.contains("NOW"),
"{}",
e.refusal.text
);
let e = refused("(cell \"S\" \"A1\" (date \"2026-10-09\"))\n");
assert!(
e.refusal.text.contains("(error \"#N/A\")"),
"{}",
e.refusal.text
);
let m = load(
"(sheet \"Board\" visible)\n(sheet \"Board.last\" hidden)\n(cell \"Board\" \"A1\" 1)\n",
);
assert_eq!(m.sheets(), 1);
assert_eq!(m.book.sheets.len(), 2);
}
#[test]
fn a_write_changes_cells_counts_them_and_a_refused_write_changes_nothing() {
let mut m = load(COUNTER);
m.step(0);
assert_eq!(
m.write("(cell \"Board\" \"D1\" 5)"),
Ok(0),
"the value it holds"
);
assert_eq!(
m.write("(cell \"Board\" \"D1\" 6)\n(cell \"Board\" \"F1:G2\" \"x\")"),
Ok(5)
);
assert_eq!(m.loaded().cells, 9);
let before = save(&m);
let e = m
.write("(cell \"Board\" \"D1\" 7)\n(cell \"Nowhere\" \"A1\" 1)")
.unwrap_err();
assert_eq!((e.line, e.refusal.id.as_str()), (2, "grid-sheet-unknown"));
assert!(
e.refusal.text.contains("Nowhere!A1") && e.refusal.text.contains("it holds Board."),
"{}",
e.refusal.text
);
assert_eq!(save(&m), before, "a refused write changes nothing");
let e = m.write("(cell \"Board.last\" \"A1\" 1)").unwrap_err();
assert_eq!((e.line, e.refusal.id.as_str()), (1, "grid-write-last"));
assert!(
e.refusal
.text
.starts_with("Board.last!A1 is a cell of Board.last,")
&& e.refusal.text.contains("write Board!A1,"),
"{}",
e.refusal.text
);
let e = m.write("(sheet \"Board\" visible)").unwrap_err();
assert_eq!(e.refusal.id, "grid-row-unknown");
assert!(
e.refusal.text.contains("cell, formula and derived"),
"{}",
e.refusal.text
);
let e = m.write("(cell \"Board\" \"A1:XFD1048576\" 0)").unwrap_err();
assert_eq!(e.refusal.id, "grid-too-many-cells");
assert_eq!(save(&m), before);
assert_eq!(m.write("(cell \"Board\" \"B1\" 100)"), Ok(1));
m.step(0);
assert_eq!(number(&m, "Board", "C1"), 102.0, "A1 is 2, B1 holds 100");
assert_eq!(m.formulas().0, 3);
}
#[test]
fn a_derived_write_lands_in_a_value_cell_or_is_refused_and_the_frame_goes_on() {
let mut m = load(COUNTER);
m.step(0);
assert_eq!(m.write("(derived \"Board\" \"D1\" 7)"), Ok(1));
let e = m.write("(derived \"Board\" \"C1:D1\" 1)").unwrap_err();
assert_eq!((e.line, e.refusal.id.as_str()), (1, "grid-write-derived"));
assert!(
e.refusal.text.contains("Board!C1:D1")
&& e.refusal.text.contains("Board!C1 holds a formula"),
"{}",
e.refusal.text
);
assert_eq!(content(&m, "Board", "D1"), Some(Content::Number(7.0)));
assert_eq!(
m.write("(derived \"Board.last\" \"D1\" 1)")
.unwrap_err()
.refusal
.id,
"grid-write-last"
);
assert_eq!(
m.write("(derived \"Nowhere\" \"A1\" 1)")
.unwrap_err()
.refusal
.id,
"grid-sheet-unknown"
);
assert_eq!(m.step(0).row(), "(step 2 4 4 done)");
assert_eq!(number(&m, "Board", "E1"), 29.0, "D1 7, C1 2 + 20");
}
#[test]
fn a_formula_write_is_planned_again_and_refused_when_it_cannot_be_stepped() {
let mut m = load(COUNTER);
m.step(0);
let before = save(&m);
let e = m.write("(formula \"Board\" \"D1\" \"=E1\")").unwrap_err();
assert_eq!((e.line, e.refusal.id.as_str()), (1, "calc-cycle"));
assert!(
e.refusal.text.contains("Board!D1, Board!E1"),
"{}",
e.refusal.text
);
assert_eq!(save(&m), before);
let e = m
.write("\n(formula \"Board\" \"F1\" \"=RAND()\")")
.unwrap_err();
assert_eq!(
(e.line, e.refusal.id.as_str()),
(2, "calc-function-not-computed")
);
assert_eq!(save(&m), before);
assert_eq!(m.write("(formula \"Board\" \"F1:F3\" \"=D1*2\")"), Ok(3));
assert!(
at(&m, "Board", "F1").is_none(),
"no value before the next step"
);
assert_eq!(m.formulas(), (7, 5));
m.step(0);
assert_eq!(number(&m, "Board", "F1"), 10.0);
assert_eq!(number(&m, "Board", "F2"), 0.0, "D2 is empty");
assert_eq!(m.write("(formula \"Board\" \"F1:F3\" \"=D1*2\")"), Ok(0));
}
fn no_twin_cell_is_placed_or_valued(m: &Machine, when: &str) {
assert_eq!(m.shapes.order().len(), m.shapes.formulas(), "{when}");
for id in m.shapes.order() {
assert!(id.0 < m.sheets, "{when}: {id:?} placed on a twin");
}
assert_eq!(m.values.len(), m.shapes.order().len(), "{when}: the values");
for &p in m.before.keys() {
assert!((p as usize) < m.values.len(), "{when}: {p} kept beside");
}
for (s, sheet) in m.book.sheets.iter().enumerate().skip(m.sheets) {
for &(row, col) in sheet.cells.keys() {
assert!(!m.shapes.holds((s, row, col)), "{when}: {s} {row} {col}");
}
}
for id in m.twin_reasons.keys() {
assert!(id.0 >= m.sheets, "{when}: a reason beside {id:?}");
}
}
#[test]
fn a_twin_cell_is_never_placed_or_valued() {
let mut m = load(COUNTER);
no_twin_cell_is_placed_or_valued(&m, "the load");
m.step(0);
no_twin_cell_is_placed_or_valued(&m, "a step");
for (text, written) in [
("(cell \"Board\" \"D1\" 6)", true),
("(cell \"Board\" \"F1:G2\" \"x\")", true),
("(formula \"Board\" \"H1:H3\" \"=Board.last!A1*2\")", true),
("(derived \"Board\" \"D1\" 7)", true),
("(cell \"Board\" \"B1\" 100)", true),
("(cell \"Board.last\" \"A1\" 1)", false),
("(formula \"Board.last\" \"A1\" \"=1\")", false),
("(formula \"Board\" \"D1\" \"=E1\")", false),
] {
assert_eq!(m.write(text).is_ok(), written, "{text}");
no_twin_cell_is_placed_or_valued(&m, text);
m.step(0);
no_twin_cell_is_placed_or_valued(&m, text);
}
assert!(!m.step(2).done);
no_twin_cell_is_placed_or_valued(&m, "a yield");
m.step(0);
assert_eq!(m.write("(formula \"Board\" \"J1\" \"=F1:G1*2\")"), Ok(1));
m.step(0);
assert!(!m.twin_reasons.is_empty());
no_twin_cell_is_placed_or_valued(&m, "a reason kept");
let twin = m.book.find_sheet("Board.last").unwrap();
assert_eq!(
m.read((twin, 1, 10)),
Err(Reason::Construct("F1:G1".to_string())),
"a formula reading the twin's cell inherits the reason"
);
assert_eq!(m.read((twin, 1, 4)), Ok(Value::Number(7.0)));
assert_eq!(m.read((twin, 9, 9)), Ok(Value::Empty));
}
fn twin_cell(m: &Machine, t: usize, row: u32, col: u32) -> Result<Value, Reason> {
let held = m.book.sheets[t].cells.get(&(row, col));
let newer = held.and_then(|_| Values::content(m, (t, row, col)));
let Some(cell) = newer.or_else(|| held.map(|c| c.content.clone())) else {
return match m.twin_reasons.get(&(t, row, col)) {
Some(r) => Err(r.clone()),
None => Ok(Value::Empty),
};
};
Ok(match &cell {
Content::Number(n) => Value::Number(*n),
Content::Text(s) => Value::Text(s.clone()),
Content::Bool(b) => Value::Bool(*b),
Content::Error(e) => Value::Error(ErrorKind::parse(e).unwrap_or(ErrorKind::Value)),
other => panic!("a twin holds {other:?}"),
})
}
fn dense_kept(m: &Machine, sheet: &str) -> bool {
let twin = m.find_sheet(&format!("{sheet}.last")).unwrap();
m.dense(twin).is_some()
}
fn dense_is_the_twin(m: &Machine, kept_now: &[(&str, bool)], when: &str) {
use crate::calc::graph::cells_in;
use crate::calc::{Area, Ctx};
use crate::sheet::{MAX_COLUMN, MAX_ROW};
let ctx = Ctx::new(m, &m.library);
for s in 0..m.sheets {
let t = m.sheets + s;
let name = m.book.sheets[t].name.clone();
let Some(((top, left), (bottom, right))) = m.extents[s] else {
continue;
};
let (top, left) = (top.saturating_sub(1).max(1), left.saturating_sub(1).max(1));
let (bottom, right) = ((bottom + 1).min(MAX_ROW), (right + 1).min(MAX_COLUMN));
let mut cells: Vec<(u32, u32)> = Vec::new();
if u64::from(bottom - top + 1) * u64::from(right - left + 1) <= 20_000 {
for row in top..=bottom {
cells.extend((left..=right).map(|col| (row, col)));
}
} else {
cells.extend(m.book.sheets[t].cells.keys().copied());
cells.extend([(top, left), (top, right), (bottom, left), (bottom, right)]);
}
for (row, col) in cells {
assert_eq!(
m.read((t, row, col)),
twin_cell(m, t, row, col),
"{when}: {name} ({row}, {col})"
);
}
let areas = [
(top, left, bottom, right),
(1, 1, MAX_ROW, MAX_COLUMN),
(top + 1, left + 1, top + 1, right),
(top, right - 1, bottom, right - 1),
(top + 2, left + 2, bottom + 5, right + 5),
(bottom, left, bottom.saturating_add(9), left + 9),
(top + 1, left + 1, top + 1, left + 1),
(1, left + 1, MAX_ROW, left + 1),
(top + 1, 1, top + 1, MAX_COLUMN),
];
for (a_top, a_left, a_bottom, a_right) in areas {
let area = Area {
sheet: t,
top: a_top.min(MAX_ROW),
left: a_left.min(MAX_COLUMN),
bottom: a_bottom.min(MAX_ROW),
right: a_right.min(MAX_COLUMN),
};
let mut walked = Vec::new();
ctx.each(&[area], |v| {
walked.push(v);
Ok(())
})
.unwrap();
let want: Vec<Value> = cells_in(&m.book.sheets[t], m.extents[t], &area)
.map(|((row, col), _)| twin_cell(m, t, row, col).unwrap())
.filter(|v| *v != Value::Empty)
.collect();
assert_eq!(walked, want, "{when}: {name} {area:?}");
}
}
for (sheet, kept) in kept_now {
assert_eq!(dense_kept(m, sheet), *kept, "{when}: {sheet} kept");
}
}
#[test]
fn a_twins_array_is_its_cells_after_every_step_and_every_write() {
let both = [("Board", true), ("Other", true)];
let mut m = load(PLANES);
dense_is_the_twin(&m, &both, "the load");
for frame in 1..=3 {
m.step(0);
dense_is_the_twin(&m, &both, &format!("frame {frame}"));
}
for row in [
"(cell \"Board\" \"B2\" 9)",
"(formula \"Board\" \"C2\" \"=B2+1\")",
"(derived \"Board\" \"G2\" 3)",
"(formula \"Board\" \"E2\" \"=B2:C2\")",
"(cell \"Board\" \"D2\" \"a longer text than x\")",
"(cell \"Board\" \"C5:D6\" 2)",
"(cell \"Board\" \"I1\" \"far\")",
] {
assert!(m.write(row).is_ok(), "{row}");
dense_is_the_twin(&m, &both, row);
m.step(0);
dense_is_the_twin(&m, &both, row);
}
assert!(m
.write("(cell \"Board\" \"B2\" 1)\n(cell \"Nowhere\" \"A1\" 1)")
.is_err());
dense_is_the_twin(&m, &both, "a refused write");
assert!(!m.step(2).done);
dense_is_the_twin(&m, &both, "a yield");
while !m.step(2).done {}
dense_is_the_twin(&m, &both, "the yielded frame done");
m.resume_at(50).unwrap();
m.step(0);
dense_is_the_twin(&m, &both, "a frame count set");
let again = load(&save(&m));
dense_is_the_twin(&again, &both, "a save loaded");
let mut life = load(LIFE);
let kept = [("Screen", true), ("Clock", true)];
dense_is_the_twin(&life, &kept, "Life's load");
for frame in 1..=4u64 {
life_step(&mut life);
dense_is_the_twin(&life, &kept, &format!("Life's frame {frame}"));
}
assert_eq!(plane(&life, "Screen", "A1:BL41"), life_here(4));
}
#[test]
fn a_sheet_too_sparse_for_an_array_is_read_through_its_cells() {
let mut m = load(concat!(
"(cell \"Big\" \"A1\" 1)\n",
"(cell \"Big\" \"XFD1048576\" 2)\n",
"(formula \"Big\" \"B1\" \"=SUM(Big.last!A:B)+Big.last!XFD1048576\")\n",
"(cell \"Edge\" \"A1:B1\" 1)\n",
"(cell \"Edge\" \"D2\" 1)\n",
));
let kept = [("Big", false), ("Edge", true)];
dense_is_the_twin(&m, &kept, "the load");
m.step(0);
assert_eq!(number(&m, "Big", "B1"), 3.0);
m.step(0);
assert_eq!(
number(&m, "Big", "B1"),
6.0,
"the twin's B1 read in the range"
);
dense_is_the_twin(&m, &kept, "two steps");
assert_eq!(m.write("(cell \"Edge\" \"J2\" 1)"), Ok(1));
m.step(0);
dense_is_the_twin(&m, &[("Big", false), ("Edge", false)], "Edge grown");
}
#[test]
fn a_range_over_a_twin_skips_a_cell_not_computed_as_it_did() {
let mut m = load(concat!(
"(cell \"S\" \"A1:A3\" 1)\n",
"(cell \"S\" \"B1\" 5)\n",
"(formula \"S\" \"B2\" \"=A1:A2\")\n",
"(cell \"S\" \"B3\" 7)\n",
"(formula \"S\" \"C1\" \"=SUM(S.last!B1:B3)\")\n",
"(formula \"S\" \"C2\" \"=S.last!B2\")\n",
"(formula \"S\" \"C3\" \"=SUM(B1:B3)\")\n",
));
m.step(0);
m.step(0);
assert!(dense_kept(&m, "S"));
let reason = Some(Computed::NotComputed(Reason::Construct(
"A1:A2".to_string(),
)));
assert_eq!(at(&m, "S", "B2"), reason);
assert_eq!(number(&m, "S", "C1"), 12.0, "the twin's range skips B2");
assert_eq!(at(&m, "S", "C2"), reason, "a read of B2's twin answers");
assert_eq!(at(&m, "S", "C3"), reason, "a range over the sheet answers");
}
#[test]
fn a_placed_reference_reads_the_sheets_found_once_as_the_names_find_them() {
use crate::calc::Calc;
let mut m = load(concat!(
"(cell \"Data\" \"A1:C3\" 2)\n",
"(cell \"Jan\" \"A1\" 1)\n",
"(cell \"Feb\" \"A1\" 10)\n",
"(cell \"Mar\" \"A1\" 100)\n",
"(formula \"Sum\" \"A1\" \"=SUM(Jan:Mar!A1)\")\n",
"(formula \"Sum\" \"A2\" \"=SUM(Data!A1:C3)*Data!B2\")\n",
"(formula \"Sum\" \"A3\" \"=Nowhere!A1\")\n",
"(formula \"Sum\" \"B1:B3\" \"=A1+Sum.last!B1\")\n",
"(formula \"Jan\" \"B1\" \"=Data!A1*2+A1\")\n",
"(formula \"Feb\" \"B1\" \"=Data!A1*2+A1\")\n",
));
let (cells, shapes) = m.formulas();
assert_eq!((cells, shapes), (8, 5), "Jan's and Feb's B1 one shape");
let (mut found, mut unfound) = (0, 0);
for i in 0..shapes {
let Ok(e) = &m.shapes.shape(i).unwrap().expr else {
panic!("shape {i} parses")
};
e.each_read(&mut |read| {
let crate::calc::formula::Read::Ref(r) = read else {
return;
};
let first = r.sheet.as_deref().and_then(|s| m.book.find_sheet(s));
let last = match &r.last_sheet {
Some(s) => m.book.find_sheet(s),
None => first,
};
let want = first
.zip(last)
.map(|(a, b)| (u8::try_from(a).unwrap(), u8::try_from(b).unwrap()));
assert_eq!(r.found, want, "{}", r.written);
if want.is_some() {
found += 1;
} else {
unfound += 1;
}
});
}
assert_eq!(
(found, unfound),
(5, 3),
"Jan:Mar, Data twice, Sum.last, Data"
);
let before = Calc::run(&m.book, &m.library);
let mut cells: Vec<CellId> = m.shapes.order().to_vec();
cells.sort_unstable();
let want: Vec<Option<Computed>> = cells
.iter()
.map(|id| before.computed(*id).cloned())
.collect();
m.step(0);
let got: Vec<Option<Computed>> = cells.iter().map(|id| m.computed(*id).cloned()).collect();
assert_eq!(got, want, "the machine's frame and Calc's, cell for cell");
assert_eq!(number(&m, "Sum", "A1"), 111.0, "the span");
assert_eq!(number(&m, "Sum", "A2"), 36.0);
assert_eq!(
at(&m, "Sum", "A3"),
Some(Computed::Value(Value::Error(ErrorKind::Ref))),
"a sheet the grid does not hold"
);
assert_eq!(
number(&m, "Feb", "B1"),
14.0,
"the shared shape on its own sheet"
);
}
#[test]
fn a_sheet_past_the_256th_is_found_by_name_as_before() {
let mut text = String::new();
for i in 1..=130 {
text.push_str(&format!("(cell \"S{i}\" \"A1\" {i})\n"));
}
text.push_str("(formula \"S1\" \"B1\" \"=S130!A1+S126.last!A1+S127.last!A1\")\n");
let mut m = load(&text);
let Ok(e) = &m.shapes.shape(0).unwrap().expr else {
panic!("parses")
};
let mut found = Vec::new();
e.each_read(&mut |read| {
if let crate::calc::formula::Read::Ref(r) = read {
found.push(r.found);
}
});
assert_eq!(
found,
vec![Some((129, 129)), Some((255, 255)), None],
"S130, S126.last the 256th, S127.last past it"
);
m.step(0);
m.step(0);
assert_eq!(number(&m, "S1", "B1"), 130.0 + 126.0 + 127.0);
}
#[test]
fn a_strict_call_s_arguments_are_evaluated_in_order_at_every_count() {
let mut m = load(concat!(
"(cell \"S\" \"A1:A2\" 1)\n",
"(formula \"S\" \"B1\" \"=SUM()\")\n",
"(formula \"S\" \"B2\" \"=SUM(1)\")\n",
"(formula \"S\" \"B3\" \"=SUM(1,2)\")\n",
"(formula \"S\" \"B4\" \"=SUM(1,2,3)\")\n",
"(formula \"S\" \"B5\" \"=SUM(1,2,3,4)\")\n",
"(formula \"S\" \"B6\" \"=SUM(1,2,3,4,5)\")\n",
"(formula \"S\" \"C1\" \"=MOD(7,3)\")\n",
"(formula \"S\" \"C2\" \"=SUM(A1:A2*1,Z1:Z2*1)\")\n",
"(formula \"S\" \"C3\" \"=SUM(1,A1:A2*1,Z1:Z2*1)\")\n",
"(formula \"S\" \"C4\" \"=SUM(1,2,A1:A2*1,Z1:Z2*1)\")\n",
"(formula \"S\" \"C5\" \"=SUM(1,2,3,4,A1:A2*1,Z1:Z2*1)\")\n",
));
m.step(0);
for (addr, want) in [
("B1", 0.0),
("B2", 1.0),
("B3", 3.0),
("B4", 6.0),
("B5", 10.0),
("B6", 15.0),
("C1", 1.0),
] {
assert_eq!(number(&m, "S", addr), want, "{addr}");
}
let first = Some(Computed::NotComputed(Reason::Construct(
"A1:A2".to_string(),
)));
for addr in ["C2", "C3", "C4", "C5"] {
assert_eq!(at(&m, "S", addr), first, "{addr}: the first not computed");
}
}
fn twin_from(m: &Machine, s: usize) -> BTreeMap<(u32, u32), Content> {
m.book.sheets[s]
.cells
.iter()
.filter_map(|(&k, c)| {
if !is_formula(&c.content) {
return Some((k, c.content.clone()));
}
let content = match m.computed((s, k.0, k.1)) {
Some(Computed::Value(Value::Number(n))) => Content::Number(*n),
Some(Computed::Value(Value::Text(t))) => Content::Text(t.clone()),
Some(Computed::Value(Value::Bool(b))) => Content::Bool(*b),
Some(Computed::Value(Value::Error(e))) => Content::Error(e.text().to_string()),
Some(Computed::Value(Value::Empty)) => Content::Number(0.0),
Some(Computed::NotComputed(_)) | None => return None,
};
Some((k, content))
})
.collect()
}
fn twin_cells(m: &Machine, t: usize) -> BTreeMap<(u32, u32), Content> {
m.book.sheets[t]
.cells
.iter()
.map(|(&k, c)| {
let content = Values::content(m, (t, k.0, k.1));
(k, content.unwrap_or_else(|| c.content.clone()))
})
.collect()
}
fn twins_are_their_sheets(m: &Machine, when: &str) {
for s in 0..m.sheets {
let t = m.sheets + s;
let twin = &m.book.sheets[t];
assert_eq!(twin_cells(m, t), twin_from(m, s), "{when}: {}", twin.name);
assert_eq!(
m.extents[t],
twin.extent(),
"{when}: {}'s extent",
twin.name
);
assert!(twin.cells.values().all(|c| c.style == 0), "{when}");
}
}
#[test]
fn a_twin_overwritten_in_place_is_its_sheet_after_every_step() {
let mut m = load(PLANES);
twins_are_their_sheets(&m, "the load");
for frame in 1..=3 {
m.step(0);
twins_are_their_sheets(&m, &format!("frame {frame}"));
}
for row in [
"(cell \"Board\" \"B2\" 9)",
"(formula \"Board\" \"E2\" \"=B2:C2\")",
"(cell \"Board\" \"D2\" \"a longer text than x\")",
"(cell \"Board\" \"C5:D6\" 2)",
"(formula \"Board\" \"D6\" \"=B2:C2\")",
"(formula \"Board\" \"F2\" \"=B2:C2\")\n(cell \"Board\" \"H3\" 5)",
"(cell \"Board\" \"I1\" \"far\")",
"(cell \"Other\" \"A1\" 4)",
] {
assert!(m.write(row).is_ok(), "{row}");
m.step(0);
twins_are_their_sheets(&m, row);
}
assert!(m
.write("(cell \"Board\" \"B2\" 1)\n(cell \"Nowhere\" \"A1\" 1)")
.is_err());
assert!(!m.step(2).done);
twins_are_their_sheets(&m, "a yield");
while !m.step(2).done {}
twins_are_their_sheets(&m, "the yielded frame done");
let mut life = load(LIFE);
for frame in 1..=3u64 {
life_step(&mut life);
twins_are_their_sheets(&life, &format!("Life's frame {frame}"));
}
}
struct Dice(u64);
impl Dice {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn pick<'a>(&mut self, xs: &[&'a str]) -> &'a str {
xs[self.below(xs.len())]
}
}
struct Band<'a> {
before: Option<&'a str>,
top: u32,
rows: u32,
left: &'a str,
}
const LETTERS: [&str; 11] = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"];
fn marked(d: &mut Dice, col: &str, row: u32) -> String {
match d.below(4) {
0 => format!("${col}${row}"),
1 => format!("${col}{row}"),
2 => format!("{col}${row}"),
_ => format!("{col}{row}"),
}
}
fn reference(d: &mut Dice, at: &Band) -> String {
let (col, row) = (d.pick(&LETTERS), 1 + d.below(at.rows as usize) as u32);
match d.below(9) {
0 => {
let held = if d.below(2) == 0 { "$" } else { "" };
format!("${}{held}{}", d.pick(&["A", "B"]), 1 + d.below(20))
}
1 | 2 => {
let twin = d.pick(&["A", "B"]);
format!("{twin}.last!{}", marked(d, col, row))
}
3 if at.top > 1 => {
let above = 1 + d.below(at.top as usize - 1) as u32;
let col = d.pick(&LETTERS[2..]);
marked(d, col, above)
}
4 => format!("{}{}", at.left, at.top),
5 => d.pick(&["XFD1", "A1048576", "A.last!XFD2"]).to_string(),
6 if d.below(8) == 0 => "A:B!$A3".to_string(),
7 => held(d, at),
_ => match at.before {
Some(s) => format!("{s}!{}", marked(d, col, row)),
None => format!("$A{}", 1 + d.below(20)),
},
}
}
fn held(d: &mut Dice, at: &Band) -> String {
let col = d.pick(&LETTERS[2..]);
if at.top > 1 && d.below(2) == 0 {
format!("${col}${}", 1 + d.below(at.top as usize - 1))
} else {
let twin = d.pick(&["A", "B"]);
format!("{twin}.last!${col}${}", 1 + d.below(at.rows as usize))
}
}
fn range(d: &mut Dice, at: &Band) -> String {
let (r1, c1) = (1 + d.below(at.rows as usize) as u32, d.below(9));
let (h, w) = (d.below(3) as u32, d.below(3));
match d.below(3) {
0 => format!("$A{r1}:$B{}", r1 + h),
1 if at.top > 2 => {
let r = 1 + d.below(at.top as usize - 2) as u32;
format!("C{r}:{}{}", LETTERS[2 + w], (r + h).min(at.top - 1))
}
_ => {
let twin = d.pick(&["A", "B"]);
let first = marked(d, LETTERS[c1], r1);
format!("{twin}.last!{first}:{}{}", LETTERS[c1 + w], r1 + h)
}
}
}
fn leaf(d: &mut Dice, at: &Band) -> String {
match d.below(10) {
0 => d
.pick(&["0", "1", "2", "3", "0.5", "2.5", "10", "1E+300"])
.to_string(),
1 => d
.pick(&["\"x\"", "\"5\"", "\"TRUE\"", "\"\"", "\" 2 \""])
.to_string(),
2 => d.pick(&["TRUE", "FALSE", "#DIV/0!", "#N/A"]).to_string(),
3 => d.pick(&["Rate", "Box", "Prev"]).to_string(),
_ => reference(d, at),
}
}
fn expr(d: &mut Dice, at: &Band, depth: u32) -> String {
if depth >= 3 || d.below(4) == 0 {
return leaf(d, at);
}
let e = |d: &mut Dice| expr(d, at, depth + 1);
let operands = |d: &mut Dice, n: usize| {
let parts: Vec<String> = (0..n)
.map(|_| {
if d.below(3) == 0 {
range(d, at)
} else {
expr(d, at, depth + 1)
}
})
.collect();
parts.join(",")
};
match d.below(20) {
0 => format!("-({})", e(d)),
1 => format!("({})%", e(d)),
2 => format!("+{}", reference(d, at)),
3 => {
let ops = [
"+", "-", "*", "/", "^", "&", "=", "<>", "<", ">", "<=", ">=",
];
let op = d.pick(&ops);
format!("({}){op}({})", e(d), e(d))
}
4 => format!("({})+({})-({})*({})", e(d), e(d), e(d), e(d)),
5 => format!("IF({},{},{})", e(d), e(d), e(d)),
6 => match d.below(3) {
0 => format!("IF({},{})", e(d), e(d)),
1 => format!("IF({},,{})", e(d), e(d)),
_ => format!("IF({},{},)", e(d), e(d)),
},
7 => {
let f = d.pick(&["AND", "OR"]);
let n = 1 + d.below(3);
format!("{f}({})", operands(d, n))
}
8 => format!("NOT({})", e(d)),
9 => {
let f = d.pick(&["SUM", "MIN", "MAX"]);
let n = d.below(5);
format!("{f}({})", operands(d, n))
}
10 => format!("{}({})", d.pick(&["ABS", "INT", "SIN"]), e(d)),
11 => format!("MOD({},{})", e(d), e(d)),
12 => match d.below(3) {
0 => d.pick(&["ROW()", "COLUMN()"]).to_string(),
1 => format!("ROW({})", reference(d, at)),
_ => format!("COLUMN({})", range(d, at)),
},
13 => format!("CHOOSE({},{},{},{})", e(d), e(d), e(d), e(d)),
14 => format!("SUM(IF({},{},{}))", e(d), range(d, at), range(d, at)),
15 => format!("({})+1", range(d, at)),
16 => {
let op = d.pick(&["+", "&", "<"]);
format!("{}{op}({})", held(d, at), e(d))
}
18 => {
let p = e(d);
match d.below(5) {
0 => format!("({p})+({p})*({p})"),
1 => format!("{}(({p}),({p}))", d.pick(&["SUM", "MAX", "AND", "MOD"])),
2 => format!("IF({p},{p},({p})-1)"),
3 => format!("IF({},({p})*2,({p})&\"x\")", e(d)),
_ => format!("NOT({p})+({p})"),
}
}
19 => {
let q = e(d);
let p = format!("({q})-({q})*2");
format!("({p})+({p})")
}
_ => format!("{}*{}", reference(d, at), reference(d, at)),
}
}
fn datum(d: &mut Dice) -> Option<&'static str> {
let all = [
"0",
"1",
"-2.5",
"3",
"\"5\"",
"\"x\"",
"\"TRUE\"",
"true",
"(error \"#N/A\")",
];
(d.below(4) != 0).then(|| d.pick(&all))
}
fn generated_grid(d: &mut Dice) -> String {
let rows = 4 + d.below(60) as u32;
let singles = d.below(3) == 0;
let mut out = String::new();
for (sheet, before) in [("A", None), ("B", Some("A"))] {
out.push_str(&format!("(sheet \"{sheet}\" visible)\n"));
for row in 1..=20 {
for col in ["A", "B"] {
if let Some(v) = datum(d) {
out.push_str(&format!("(cell \"{sheet}\" \"{col}{row}\" {v})\n"));
}
}
}
let mut top = 1;
while top <= rows {
let bottom = (top + d.below(rows as usize) as u32).min(rows);
let at = Band {
before,
top,
rows,
left: "B",
};
let f = crate::rows::quoted(&format!("={}", expr(d, &at, 0)));
out.push_str(&format!("(formula \"{sheet}\" \"C{top}:H{bottom}\" {f})\n"));
top = bottom + 1;
}
for row in (1..=rows).filter(|_| singles) {
let at = Band {
before,
top: row,
rows,
left: "J",
};
let f = crate::rows::quoted(&format!("={}", expr(d, &at, 0)));
out.push_str(&format!("(formula \"{sheet}\" \"K{row}\" {f})\n"));
}
}
out.push_str("(name \"Rate\" \"A!$A$2\")\n");
out.push_str("(name \"Box\" \"A!$A$1:$B$3\")\n");
out.push_str("(name \"Prev\" \"A.last!$C$1\")\n");
out
}
fn values(m: &Machine) -> BTreeMap<CellId, String> {
m.shapes
.order()
.iter()
.zip(&m.values)
.filter_map(|(id, c)| Some((*id, format!("{:?}", c.as_ref()?))))
.collect()
}
#[test]
fn a_run_evaluated_at_once_is_its_cells_evaluated_one_by_one() {
let (mut in_runs, mut by_cell) = (0, 0);
for seed in 1..=80u64 {
let mut d = Dice(seed);
let rows = generated_grid(&mut d);
let mut runs = Machine::load(&rows, library::language()).unwrap_or_else(|e| {
panic!("seed {seed}: line {}: {}\n{rows}", e.line, e.refusal.text)
});
let mut alone = load(&rows);
alone.by_runs = false;
let calc: BTreeMap<CellId, String> = {
let calc = crate::calc::Calc::run(&runs.book, &runs.library);
let spell = |id: &CellId| format!("{:?}", calc.computed(*id).expect("computed"));
runs.shapes
.order()
.iter()
.map(|id| (*id, spell(id)))
.collect()
};
for frame in 1..=4 {
in_runs += runs.shapes.cells_in_runs();
by_cell += runs.shapes.order().len();
while !runs.step(1 + d.below(300)).done {}
assert!(alone.step(0).done);
assert_eq!(
values(&runs),
values(&alone),
"seed {seed}, frame {frame}\n{rows}"
);
if frame == 1 {
assert_eq!(values(&runs), calc, "seed {seed}: Calc\n{rows}");
}
let write = match d.below(4) {
0 => format!("(cell \"A\" \"A{}\" 7)", 1 + d.below(20)),
1 => format!("(cell \"B\" \"B{}\" \"y\")", 1 + d.below(20)),
2 => format!(
"(cell \"A\" \"{}{}\" 2)",
d.pick(&LETTERS[2..8]),
1 + d.below(4)
),
_ => continue,
};
let (a, b) = (runs.write(&write), alone.write(&write));
assert!(a.is_ok() && b.is_ok(), "seed {seed}: {write}: {a:?} {b:?}");
}
}
assert!(
in_runs * 2 > by_cell,
"{in_runs} of {by_cell} cells were in runs: too few for the law to hold over"
);
}
#[test]
fn a_cell_in_a_run_answers_the_first_reason_its_evaluation_meets() {
let grid = concat!(
"(cell \"S\" \"A1:B8\" 1)\n",
"(formula \"S\" \"X1\" \"=A1:A2\")\n",
"(formula \"S\" \"C1:C8\" \"=(A1:A2)+(B1:B2)\")\n",
"(formula \"S\" \"D1:D8\" \"=$X$1+(B1:B2)\")\n",
"(formula \"S\" \"E1:E8\" \"=(B1:B2)+$X$1\")\n",
"(formula \"S\" \"F1:F8\" \"=MOD((A1:A2)+1,(B1:B2)+1)\")\n",
"(formula \"S\" \"G1:G8\" \"=SUM((A1:A2)+1,(B1:B2)+1,(A3:A4)+1)\")\n",
"(formula \"S\" \"H1:H8\" \"=ABS((B1:B2)*2)\")\n",
"(formula \"S\" \"I1:I8\" \"=1+(A1:A2)-(B1:B2)\")\n",
"(formula \"S\" \"J1:J8\" \"=IF((A1:A2)=1,(B1:B2),0)\")\n",
"(formula \"S\" \"K1:K8\" \"=$X$1&$X$1\")\n",
);
let mut runs = load(grid);
let mut alone = load(grid);
alone.by_runs = false;
assert_eq!(runs.shapes.cells_in_runs(), 72, "nine runs of eight");
runs.step(0);
alone.step(0);
assert_eq!(values(&runs), values(&alone));
let first = |col: &str, row: u32| match at(&runs, "S", &format!("{col}{row}")) {
Some(Computed::NotComputed(Reason::Construct(c))) => c,
other => panic!("{col}{row}: {other:?}"),
};
for (col, wants) in [
("C", "A3:A4"),
("D", "A1:A2"),
("E", "B3:B4"),
("F", "A3:A4"),
("G", "A3:A4"),
("H", "B3:B4"),
("I", "A3:A4"),
("J", "A3:A4"),
("K", "A1:A2"),
] {
assert_eq!(first(col, 3), wants, "{col}3");
}
}
#[test]
fn a_run_makes_every_buffer_it_holds_in_its_first_frame() {
let mut m = load(concat!(
"(cell \"S\" \"A1\" 1)\n",
"(formula \"S\" \"B1:B300\" \"=IF(S.last!$A$1=1,0,IF(OR(SUM(S.last!B1:B3)=3,",
"AND(S.last!B2=1,MOD(SUM(S.last!A1:C3),4)=2)),1,0))\")\n",
"(formula \"S\" \"C1:C300\" \"=IF(S.last!$A$1=1,0,IF(OR(SUM(S.last!B1:D3)-S.last!C2=3,",
"AND(S.last!C2=1,SUM(S.last!B1:D3)-S.last!C2=2)),1,0))\")\n",
));
assert_eq!(m.shapes.cells_in_runs(), 600);
m.step(0);
let made = m.lanes.made();
assert_eq!(m.write("(cell \"S\" \"A1\" 2)"), Ok(1));
for frame in 2..=4 {
m.step(0);
assert_eq!(m.lanes.made(), made, "frame {frame}");
}
assert_eq!(number(&m, "S", "B2"), 0.0);
assert!(made.0 > 3 && made.4 == vector::CHUNK, "{made:?}");
assert!(
made.1 == made.0 && made.2 == made.0,
"each kind of lane: {made:?}"
);
}
#[test]
fn a_run_holds_its_parts_kept_lanes_within_its_shape_s_bound() {
let mut m = load(concat!(
"(cell \"S\" \"C1:D300\" 2)\n",
"(formula \"S\" \"B1:B300\" \"=S.last!C1+(S.last!D1*2)+S.last!C1\")\n",
));
assert_eq!(m.shapes.cells_in_runs(), 300);
m.step(0);
let (held, _) = m.shapes.shape(0).expect("the shape").holds();
assert_eq!(held, 3, "two lanes, and one kept for the part");
let made = m.lanes.made();
assert_eq!((made.0, made.1, made.2), (held, held, held), "{made:?}");
assert_eq!(number(&m, "S", "B7"), 8.0);
}
thread_local! {
static TICKS: Slot<usize> = const { Slot::new(0) };
}
fn tick(_: &crate::calc::Ctx<'_>, _: &[crate::calc::Arg<'_>]) -> Result<Value, Reason> {
TICKS.with(|t| t.set(t.get() + 1));
Ok(Value::Number(1.0))
}
#[test]
fn a_run_evaluates_no_cell_past_a_step_s_budget() {
let mut lib = library::language();
lib.register("TICK", tick);
let mut m = Machine::load("(formula \"S\" \"A1:F100\" \"=TICK()+S.last!A1\")\n", lib)
.unwrap_or_else(|e| panic!("{}", e.refusal.text));
assert_eq!(m.shapes.cells_in_runs(), 600);
for budget in [1, 7, 255, 256, 257, 300, 0] {
let mut done = 0;
loop {
let before = TICKS.with(Slot::get);
let step = m.step(budget);
let ticks = TICKS.with(Slot::get) - before;
assert_eq!(ticks, step.evaluated - done, "budget {budget} after {done}");
if step.done {
break;
}
done = step.evaluated;
}
}
assert_eq!(number(&m, "S", "F100"), 7.0, "seven frames");
}
fn columns(values: &str, rows: u32, formulas: &[String]) -> String {
let mut out = values.to_string();
for (k, f) in formulas.iter().enumerate() {
let col = 4 + k as u32;
let range = format!(
"{}:{}",
crate::sheet::cell_ref(1, col),
crate::sheet::cell_ref(rows, col)
);
let text = crate::rows::quoted(&format!("={f}"));
out.push_str(&format!("(formula \"S\" \"{range}\" {text})\n"));
}
out
}
fn by_runs_and_alone(rows: &str, writes: &[&str]) -> Machine {
let mut runs = load(rows);
let mut alone = load(rows);
alone.by_runs = false;
assert_eq!(
runs.shapes.cells_in_runs(),
runs.shapes.order().len(),
"every cell in a run"
);
let calc: BTreeMap<CellId, String> = {
let calc = crate::calc::Calc::run(&runs.book, &runs.library);
let spell = |id: &CellId| format!("{:?}", calc.computed(*id).expect("computed"));
runs.shapes
.order()
.iter()
.map(|id| (*id, spell(id)))
.collect()
};
for frame in 1..=writes.len() + 2 {
assert!(runs.step(0).done && alone.step(0).done);
assert_eq!(values(&runs), values(&alone), "frame {frame}");
if frame == 1 {
assert_eq!(values(&runs), calc, "Calc");
}
if let Some(w) = writes.get(frame - 1) {
assert!(runs.write(w).is_ok() && alone.write(w).is_ok(), "{w}");
}
}
runs
}
#[test]
fn the_aggregates_a_run_computes_are_the_library_s_at_every_cell() {
let values_rows = concat!(
"(cell \"S\" \"A1\" 1)\n",
"(cell \"S\" \"A2\" -2.5)\n",
"(cell \"S\" \"A3\" \"5\")\n",
"(cell \"S\" \"A4\" \"x\")\n",
"(cell \"S\" \"A5\" true)\n",
"(cell \"S\" \"A6\" (error \"#N/A\"))\n",
"(cell \"S\" \"B1\" 0)\n",
"(cell \"S\" \"B2\" false)\n",
"(cell \"S\" \"B3\" 3)\n",
"(cell \"S\" \"B5\" \"TRUE\")\n",
"(cell \"Far\" \"A1\" 2)\n",
"(cell \"Far\" \"Z900\" 3)\n",
);
let operands = [
"S.last!A1:B3",
"S.last!A4:B6",
"$A$1:$B$3",
"Far.last!A1:B3",
"S.last!A1,\"5\",TRUE,S.last!B2",
"Nope!A1:B2",
"S.last!A1048575:A1048576",
"",
"1,2,0,4,5,6,7,8",
"1,2,0,4,5,6,7,8,S.last!A1",
"S.last!A1>0,S.last!B1<1",
"S.last!A1+1,S.last!B3*2",
"-S.last!A2,S.last!A1:A3,S.last!B1=0",
];
let mut formulas = Vec::new();
for f in ["SUM", "MIN", "MAX", "AND", "OR"] {
for o in operands {
formulas.push(format!("{f}({o})"));
}
}
for o in [
"S.last!A1>0",
"S.last!A1",
"S.last!A1+0",
"\"TRUE\"",
"S.last!A3&\"\"",
"S.last!A1:A2",
"",
"1,2",
] {
formulas.push(format!("NOT({o})"));
}
let grid = columns(values_rows, 8, &formulas);
let m = by_runs_and_alone(
&grid,
&[
"(cell \"S\" \"A1\" 4)",
"(cell \"S\" \"B3\" \"y\")",
"(cell \"Far\" \"A1\" (error \"#DIV/0!\"))",
],
);
assert_eq!(number(&m, "S", "D1"), 1.5);
}
fn forty_two(_: &crate::calc::Ctx<'_>, _: &[crate::calc::Arg<'_>]) -> Result<Value, Reason> {
Ok(Value::Number(42.0))
}
#[test]
fn a_meaning_registered_over_the_language_s_own_is_the_one_a_run_computes() {
for name in ["SUM", "MIN", "MAX", "AND", "OR", "NOT"] {
let mut lib = library::language();
lib.register(name, forty_two);
let rows = format!(
"(cell \"S\" \"A1:A8\" 1)\n(formula \"S\" \"B1:B8\" \"={name}(S.last!A1>0)\")\n"
);
let mut m = Machine::load(&rows, lib).unwrap_or_else(|e| panic!("{}", e.refusal.text));
assert_eq!(m.shapes.cells_in_runs(), 8, "{name}");
m.step(0);
for row in 1..=8 {
assert_eq!(number(&m, "S", &format!("B{row}")), 42.0, "{name}");
}
}
}
#[test]
fn a_lane_of_numbers_is_values_from_the_first_answer_of_another_kind() {
let values_rows = concat!(
"(cell \"S\" \"A1\" 1)\n",
"(cell \"S\" \"A2\" 2)\n",
"(cell \"S\" \"A3\" 0)\n",
"(cell \"S\" \"A4\" \"t\")\n",
"(cell \"S\" \"A6\" (error \"#N/A\"))\n",
"(cell \"S\" \"A7\" 4)\n",
"(cell \"S\" \"A8\" 1E+300)\n",
"(cell \"S\" \"B1:B2\" 1)\n",
"(cell \"S\" \"B3\" 0)\n",
"(cell \"S\" \"B4:B8\" 1)\n",
);
let formulas: Vec<String> = [
"S.last!A1",
"S.last!A1*2",
"1/S.last!B1",
"S.last!A1*1E+300",
"(S.last!A1>1)+0",
"IF(S.last!A1>1,\"big\",S.last!A1)",
"IF(S.last!B1,S.last!A1*2,-1)",
"IF(S.last!B1>0,S.last!B1,\"no\")",
"NOT(S.last!B1+0)",
"NOT(S.last!B1>0)",
"MOD(S.last!A1,S.last!B1)",
"-S.last!B1",
"S.last!B1%",
"S.last!B1&\"x\"",
"S.last!A1=S.last!B1",
"S.last!B1=S.last!B2",
"S.last!B1<S.last!B1*2",
"S.last!B1*3>=S.last!B1+1",
"S.last!B1+1>S.last!B1",
"2^S.last!B1",
"S.last!B1-S.last!B2*3",
"SUM(S.last!A1:A2)/S.last!B1",
"ABS(S.last!B1-2)",
]
.iter()
.map(|f| f.to_string())
.collect();
let grid = columns(values_rows, 8, &formulas);
let m = by_runs_and_alone(
&grid,
&[
"(cell \"S\" \"B5\" \"x\")",
"(cell \"S\" \"A4\" 3)",
"(cell \"S\" \"B3\" 2)",
],
);
assert_eq!(number(&m, "S", "F3"), 0.5);
}
#[test]
fn a_value_follows_its_cell_when_a_formula_s_write_plans_the_order_again() {
let rows = concat!(
"(cell \"S\" \"A1\" 2)\n",
"(formula \"S\" \"B1:B8\" \"=S.last!$A$1*ROW()\")\n",
"(formula \"S\" \"D1\" \"=B8+1\")\n",
);
let mut m = load(rows);
m.step(0);
let kept = values(&m);
let at_b5 = m.shapes.reach().position((0, 5, 2));
assert_eq!(m.write("(formula \"S\" \"B4\" \"=D1*2\")"), Ok(1));
assert_ne!(m.shapes.reach().position((0, 5, 2)), at_b5, "B5 moved");
for (id, v) in &kept {
let now = m.computed(*id).map(|c| format!("{c:?}"));
if *id == (0, 4, 2) {
assert_eq!(now, None, "B4, written");
} else {
assert_eq!(now.as_ref(), Some(v), "{id:?}");
}
}
m.step(0);
let mut fresh = load(concat!(
"(cell \"S\" \"A1\" 2)\n",
"(formula \"S\" \"B1:B3\" \"=S.last!$A$1*ROW()\")\n",
"(formula \"S\" \"B4\" \"=D1*2\")\n",
"(formula \"S\" \"B5:B8\" \"=S.last!$A$1*ROW()\")\n",
"(formula \"S\" \"D1\" \"=B8+1\")\n",
));
fresh.step(0);
assert_eq!(values(&m), values(&fresh), "the frame after");
assert_eq!(number(&m, "S", "B4"), 34.0, "(16 + 1) * 2");
}
#[test]
fn a_part_written_more_than_once_is_computed_once_a_run() {
let rows = concat!(
"(cell \"S\" \"A1:A12\" 3)\n",
"(formula \"S\" \"B1:B12\" \"=(TICK()+A1)*2+(TICK()+A1)-MAX(TICK()+A1,0)\")\n",
"(formula \"S\" \"C1:C12\" \"=IF(TICK()>0,TICK()+1,TICK()+2)\")\n",
"(formula \"S\" \"D1:D12\" ",
"\"=((TICK()+A1)*(TICK()+A1))-(TICK()+A1)*(TICK()+A1)+B1\")\n",
);
let frame = |by_runs: bool| {
let mut lib = library::language();
lib.register("TICK", tick);
let mut m = Machine::load(rows, lib).unwrap_or_else(|e| panic!("{}", e.refusal.text));
assert_eq!(m.shapes.cells_in_runs(), 36, "every cell in a run");
m.by_runs = by_runs;
let before = TICKS.with(Slot::get);
assert!(m.step(0).done);
(TICKS.with(Slot::get) - before, values(&m))
};
let ((runs, by_run), (alone, by_cell)) = (frame(true), frame(false));
assert_eq!(by_run, by_cell, "the values");
assert_eq!(number_in(&by_run, (0, 2, 4)), "Value(Number(8.0))");
assert_eq!(
(runs, alone),
(12 + 24 + 12, 36 + 24 + 48),
"TICK's calls in a frame, by runs and alone"
);
}
fn number_in(values: &BTreeMap<CellId, String>, id: CellId) -> &str {
values.get(&id).map_or("none", String::as_str)
}
#[test]
fn the_parts_a_run_computes_once_are_those_written_in_one_branch() {
let lib = library::language();
let life = "IF(Clock!$B$1=1,IF(MOD(MOD(MOD(Clock!$B$3*1103515+ROW()*1597334+\
COLUMN()*1830421+12345,67108859)*MOD(Clock!$B$3*2012345+ROW()*1209487+\
COLUMN()*1949213+54321,67108859),67108859)*MOD(MOD(Clock!$B$3*1103515+\
ROW()*1597334+COLUMN()*1830421+12345,67108859)*MOD(Clock!$B$3*2012345+\
ROW()*1209487+COLUMN()*1949213+54321,67108859),67108859),67108859)\
<25165822,1,0),IF(OR(SUM(Screen.last!A1:C3)-Screen.last!B2=3,\
AND(Screen.last!B2=1,SUM(Screen.last!A1:C3)-Screen.last!B2=2)),1,0))";
for (text, wants) in [
("A1+A1", (1, 2)),
("A1*2+A1-A1", (1, 3)),
("1+1", (0, 0)),
("\"x\"&\"x\"", (0, 0)),
("SUM(A1:B2)+SUM(A1:B2)", (1, 2)),
("SUM(A1,A1)", (0, 0)),
("ABS(A1)+ABS(A1)", (1, 2)),
("Rate+Rate", (1, 2)),
("IF(A1>0,A1,A1)", (0, 0)),
("IF(B1,A1+A1,0)", (1, 2)),
("IF(B1,A1,0)+IF(B1,A1,0)", (1, 2)),
("CHOOSE(1,A1+A1,A1)", (0, 0)),
("CHOOSE(1,A1,B1)*CHOOSE(1,A1,B1)", (1, 2)),
("(A1+B1)*(A1+B1)+A1", (2, 4)),
("(A1+B1)*(A1-B1)", (2, 4)),
(
"IF(OR(SUM(S.last!A1:C3)-S.last!B2=3,AND(S.last!B2=1,\
SUM(S.last!A1:C3)-S.last!B2=2)),1,0)",
(2, 4),
),
(life, (6, 12)),
] {
let e = crate::calc::formula::parse(text).unwrap_or_else(|c| panic!("{text}: {c}"));
let r = vector::Repeats::of(&lib, &e);
assert_eq!((r.parts(), r.places()), wants, "{text}");
}
}
fn twin_views(m: &Machine) -> Vec<(String, Vec<u8>)> {
(m.sheets..m.book.sheets.len())
.map(|t| {
let name = m.sheet_name(t).expect("a twin");
(
grid(m, name, None),
plane(m, name, &whole(&m.book.sheets[t]).text()),
)
})
.collect()
}
fn write_between(d: &mut Dice, frame: u32) -> Option<String> {
Some(match d.below(9) {
0..=2 => format!(
"(cell \"A\" \"{}{}\" {})",
d.pick(&["A", "B"]),
1 + d.below(20),
d.pick(&["0", "1", "2", "7", "\"x\"", "true"])
),
6 | 7 => format!(
"(cell \"A\" \"A{}\" {})\n(cell \"A\" \"B{}\" {})",
1 + d.below(4),
d.pick(&["0", "7"]),
1 + d.below(4),
d.pick(&["0", "7"])
),
3 => format!(
"(cell \"B\" \"A{}\" {})",
1 + d.below(20),
d.pick(&["1", "3", "\"y\""])
),
4 if frame % 3 == 0 => format!(
"(cell \"A\" \"{}{}\" 2)",
d.pick(&LETTERS[2..8]),
1 + d.below(4)
),
5 if frame % 4 == 0 => format!("(cell \"A\" \"J{}\" 4)", 1 + d.below(20)),
_ => return None,
})
}
#[test]
fn a_twin_s_cells_made_when_read_are_the_cells_a_frame_s_end_copies() {
let mut seen = (0, 0, 0);
for seed in 1..=40u64 {
let mut d = Dice(seed ^ 0x7A1C);
let rows = generated_grid(&mut d);
lazy_against_eager(&rows, &mut d, 8, &mut seen, write_between);
}
for seed in 1..=12u64 {
let mut d = Dice(seed ^ 0x11FE);
let soup = format!("ROW()*{}+COLUMN()*{}", 3 + d.below(20), 5 + d.below(20));
let rows = LIFE.replace("ROW()*7+COLUMN()*13", &soup);
lazy_against_eager(&rows, &mut d, 16, &mut seen, |_, frame| {
Some(format!("(cell \"Clock\" \"B1\" {frame})"))
});
}
let (behind, lent, walked) = seen;
assert!(
behind > 100 && lent > 20 && walked > 20,
"{behind} frames left a twin cell behind, {lent} twins lent whole made and \
{walked} walked: too few for the law to hold over"
);
}
fn lazy_against_eager(
rows: &str,
d: &mut Dice,
frames: u32,
seen: &mut (usize, usize, usize),
mut write: impl FnMut(&mut Dice, u32) -> Option<String>,
) {
let mut lazy = load(rows);
let mut eager = load(rows);
eager.lazy = false;
let first = lazy.sheet_name(0).expect("a sheet").to_string();
for m in [&lazy, &eager] {
m.view("plane", &first, None)
.expect("a plane of the first sheet");
}
for frame in 1..=frames {
let when = format!("frame {frame}\n{rows}");
if let Some(w) = write(d, frame) {
let (a, b) = (lazy.write(&w), eager.write(&w));
assert_eq!(a.is_ok(), b.is_ok(), "{when}: {w}: {a:?} {b:?}");
}
let budget = 1 + d.below(200);
if !lazy.step(budget).done {
let views = twin_views(&lazy);
assert_eq!(views, twin_views(&eager), "{when}: a view during a yield");
while !lazy.step(budget).done {}
}
assert!(eager.step(0).done);
let lagging = lazy.lagging.iter().any(|&l| l);
seen.0 += usize::from(lagging);
assert_eq!(
twin_views(&lazy),
twin_views(&eager),
"{when}: the twins' views"
);
same_state(&lazy, &eager, &when);
let walk = d.below(4) == 0;
for m in [&mut lazy, &mut eager] {
m.incremental = !walk;
}
seen.2 += usize::from(walk && lagging);
if d.below(4) == 0 {
seen.1 += usize::from(lagging);
let rows_sheets = lazy.sheets;
let made: Vec<String> = lazy.workbook().sheets[rows_sheets..]
.iter()
.map(|s| format!("{:?}", s.cells))
.collect();
assert!(lazy.lagging.iter().all(|&l| !l), "{when}: every twin made");
assert!(lazy.behind.iter().flatten().all(|&w| w == 0), "{when}");
let copied: Vec<String> = eager.book.sheets[eager.sheets..]
.iter()
.map(|s| format!("{:?}", s.cells))
.collect();
assert_eq!(made, copied, "{when}: the twins made");
}
}
}
#[test]
fn a_value_cell_s_twin_is_copied_as_written_beside_formulas_made_from_their_slots() {
let mut m = load(concat!(
"(cell \"S\" \"A1:A8\" 1)\n",
"(cell \"S\" \"C1:C20\" 0)\n",
"(formula \"S\" \"B1:B8\" \"=S.last!B1+A1\")\n",
));
for _ in 0..3 {
m.step(0);
}
assert_eq!(m.write("(cell \"S\" \"A3\" (error \"#FOO!\"))"), Ok(1));
m.step(0);
assert!(m.lagging[0], "B's twin cells behind their slots");
assert_eq!((m.find_sheet("s.LAST"), m.find_sheet("T")), (Some(1), None));
assert_eq!((m.sheet_name(1), m.sheet_name(2)), (Some("S.last"), None));
let wants = [
("A3", Content::Error("#FOO!".to_string())),
("B2", Content::Number(4.0)),
("B3", Content::Error("#VALUE!".to_string())),
];
let record = grid(&m, "S.last", None);
assert!(
record.contains("(cell \"S.last\" \"A3\" (error \"#FOO!\"))"),
"{record}"
);
for (at, wanted) in &wants {
assert_eq!(content(&m, "S.last", at).as_ref(), Some(wanted), "{at}");
}
let book = m.workbook();
for (at, wanted) in wants {
let r = parse_a1_range(at).unwrap();
assert_eq!(
book.sheets[1].cells[&(r.top, r.left)].content,
wanted,
"{at}, made"
);
}
assert!(!m.lagging[0]);
assert_eq!(grid(&m, "S.last", None), record, "the record, made");
m.step(0);
assert!(m.lagging[0], "B's twin cells behind again");
assert_eq!(m.write("(cell \"S\" \"B5\" (error \"#BAR!\"))"), Ok(1));
m.step(0);
assert!(m.lagging[0], "B's other twin cells behind");
let bar = Content::Error("#BAR!".to_string());
assert_eq!(content(&m, "S.last", "B5").as_ref(), Some(&bar), "B5");
assert_eq!(
m.workbook().sheets[1].cells[&(5, 2)].content,
bar,
"B5, made"
);
}
fn same_state(a: &Machine, b: &Machine, when: &str) {
assert_eq!(values(a), values(b), "{when}: the values");
for t in a.sheets..a.book.sheets.len() {
let cells = |m: &Machine| format!("{:?}", twin_cells(m, t));
let name = &a.book.sheets[t].name;
assert_eq!(cells(a), cells(b), "{when}: {name}");
assert_eq!(a.extents[t], b.extents[t], "{when}: {name}'s extent");
}
let reasons = |m: &Machine| -> BTreeMap<CellId, Reason> {
m.twin_reasons
.iter()
.map(|(k, r)| (*k, r.clone()))
.collect()
};
assert_eq!(reasons(a), reasons(b), "{when}: the reasons");
assert_eq!(
format!("{:?}", a.dense),
format!("{:?}", b.dense),
"{when}: the arrays"
);
assert_eq!(a.planes, b.planes, "{when}: the planes");
}
#[test]
fn a_frame_that_computes_what_changed_is_the_frame_computed_whole() {
let (mut followed, mut frames, mut part, mut computed, mut passed) = (0, 0, 0, 0, 0);
for seed in 1..=60u64 {
let mut d = Dice(seed ^ 0x5EED);
let rows = generated_grid(&mut d);
let mut inc = load(&rows);
let mut whole = load(&rows);
whole.incremental = false;
for m in [&inc, &whole] {
m.view("plane", "A", None).expect("a plane of A");
}
for frame in 1..=8 {
let when = format!("seed {seed}, frame {frame}");
let last = grid(&whole, "A", None) + &grid(&whole, "B", None);
let budget = 1 + d.below(200);
if !inc.step(budget).done {
let now = grid(&inc, "A", None) + &grid(&inc, "B", None);
assert_eq!(now, last, "{when}: a view during a yield\n{rows}");
while !inc.step(budget).done {}
}
assert!(whole.step(0).done);
same_state(&inc, &whole, &format!("{when}\n{rows}"));
frames += 1;
if !inc.every {
let of = inc.shapes.order().len();
followed += 1;
part += usize::from(inc.computed > 0 && inc.computed < of);
computed += inc.computed;
passed += of;
}
let Some(write) = write_between(&mut d, frame) else {
continue;
};
let (a, b) = (inc.write(&write), whole.write(&write));
assert_eq!(a.is_ok(), b.is_ok(), "{when}: {write}: {a:?} {b:?}");
}
}
assert!(
followed * 3 > frames && part * 3 > followed && computed * 2 < passed,
"{followed} of {frames} frames followed what changed, {part} of them computing \
some cells and not others, {computed} cells of {passed}: too few for the law \
to hold over"
);
}
fn ticked(m: &mut Machine) -> usize {
let before = TICKS.with(Slot::get);
assert!(m.step(0).done);
TICKS.with(Slot::get) - before
}
#[test]
fn a_frame_computes_the_cells_a_change_reaches_and_no_other() {
let mut lib = library::language();
lib.register("TICK", tick);
let mut m = Machine::load(
concat!(
"(cell \"S\" \"A1:A10\" 1)\n",
"(name \"Rate\" \"S!$A$1\")\n",
"(formula \"S\" \"B1:B10\" \"=TICK()+A1\")\n",
"(formula \"S\" \"C1:C10\" \"=TICK()+IF($A$1>100,1,0)\")\n",
"(formula \"S\" \"D1:D10\" \"=TICK()+S.last!B1\")\n",
"(formula \"S\" \"E1\" \"=TICK()+Rate\")\n",
"(formula \"S\" \"F1:F10\" \"=TICK()+SUM($A$1:$A$3)\")\n",
),
lib,
)
.unwrap_or_else(|e| panic!("{}", e.refusal.text));
assert_eq!(ticked(&mut m), 41, "the first frame computes every cell");
assert_eq!(
ticked(&mut m),
41,
"and the second, its twins having gained cells"
);
assert_eq!(ticked(&mut m), 1, "then the name's alone");
assert_eq!(m.write("(cell \"S\" \"A5\" 2)"), Ok(1));
assert_eq!(ticked(&mut m), 2, "B5, and the name's");
assert_eq!(ticked(&mut m), 2, "D5 a frame late, and the name's");
assert_eq!(ticked(&mut m), 1);
assert_eq!(m.write("(cell \"S\" \"A1\" 7)"), Ok(1));
assert_eq!(
ticked(&mut m),
12,
"B1, every F, the name's: C's part is FALSE still"
);
assert_eq!(ticked(&mut m), 2, "D1 a frame late, and the name's");
assert_eq!(m.write("(cell \"S\" \"A1\" 500)"), Ok(1));
assert_eq!(ticked(&mut m), 22, "B1, every C, every F, the name's");
assert_eq!(number(&m, "S", "C4"), 2.0);
assert_eq!(number(&m, "S", "F4"), 503.0);
assert_eq!(number(&m, "S", "E1"), 501.0);
assert_eq!(m.write("(cell \"S\" \"A1\" 500)"), Ok(0));
assert_eq!(
ticked(&mut m),
2,
"D1 a frame late, and the name's: an equal write writes nothing"
);
assert_eq!(ticked(&mut m), 1);
}
#[test]
fn a_part_is_computed_whenever_a_cell_it_reads_changes() {
let mut m = load(concat!(
"(cell \"S\" \"A1\" 1)\n",
"(cell \"S\" \"A2\" 0)\n",
"(formula \"S\" \"C1:C4\" \"=IF(ROW()=$A$1,IF($A$2=1,12,15),0)\")\n",
));
m.step(0);
m.step(0);
assert_eq!(number(&m, "S", "C1"), 15.0);
assert_eq!(
m.write("(cell \"S\" \"A1\" 3)\n(cell \"S\" \"A2\" 1)"),
Ok(2)
);
m.step(0);
assert_eq!(number(&m, "S", "C3"), 12.0);
m.step(0);
assert_eq!(m.write("(cell \"S\" \"A2\" 0)"), Ok(1));
m.step(0);
assert_eq!(number(&m, "S", "C3"), 15.0, "the press let go is seen");
assert_eq!(number(&m, "S", "C1"), 0.0);
}
#[test]
fn a_part_reading_a_formula_cell_reaches_its_whole_shape() {
let mut m = load(concat!(
"(cell \"S\" \"A1\" 1)\n",
"(cell \"S\" \"B1\" 1)\n",
"(formula \"S\" \"X1\" \"=B1\")\n",
"(formula \"S\" \"C1:C4\" \"=($X$1+$A$1)*10+ROW()\")\n",
"(formula \"S\" \"D1:D4\" \"=(S!$X$1+S!$A$1)*10+ROW()\")\n",
));
m.step(0);
m.step(0);
for col in ["C", "D"] {
assert_eq!(number(&m, "S", &format!("{col}2")), 22.0, "{col}");
}
assert_eq!(m.write("(cell \"S\" \"B1\" 2)"), Ok(1));
m.step(0);
for col in ["C", "D"] {
let at = format!("{col}2");
assert_eq!(number(&m, "S", &at), 32.0, "{col}: X1 changed in the frame");
}
assert_eq!(m.write("(cell \"S\" \"A1\" 0)"), Ok(1));
m.step(0);
for col in ["C", "D"] {
let at = format!("{col}2");
assert_eq!(number(&m, "S", &at), 22.0, "{col}: the part back on 2 + 0");
}
}
#[test]
fn a_twin_s_reason_changed_reaches_its_readers_however_the_twins_are_filled() {
for value_cells in [0, 20] {
let grid = format!(
"(cell \"S\" \"A1\" 1)\n{}(formula \"S\" \"C1\" \"=IF(A1=1,D1:D2,E1:E2)\")\n\
(formula \"S\" \"F1\" \"=S.last!C1\")\n",
(1..=value_cells)
.map(|r| format!("(cell \"S\" \"H{r}\" {r})\n"))
.collect::<String>()
);
let mut m = load(&grid);
for _ in 0..3 {
m.step(0);
}
let reason = |m: &Machine, addr: &str| match at(m, "S", addr) {
Some(Computed::NotComputed(Reason::Construct(c))) => c,
other => panic!("{addr}: {other:?}"),
};
assert_eq!(reason(&m, "F1"), "D1:D2", "{value_cells} value cells");
assert_eq!(m.write("(cell \"S\" \"A1\" 2)"), Ok(1));
m.step(0);
assert_eq!(reason(&m, "C1"), "E1:E2", "{value_cells} value cells");
m.step(0);
assert_eq!(reason(&m, "F1"), "E1:E2", "{value_cells} value cells");
}
}
#[test]
fn lifes_rule_under_a_frame_counter_computes_near_what_changed() {
let mut lib = library::language();
lib.register("TICK", tick);
let mut m = Machine::load(
concat!(
"(cell \"S\" \"A1\" 0)\n",
"(cell \"Seed\" \"E4:E6\" 1)\n",
"(formula \"Board\" \"B2:O15\" \"=IF(S!$A$1=1,Seed!B2,TICK()*0+",
"IF(OR(SUM(Board.last!A1:C3)-Board.last!B2=3,AND(Board.last!B2=1,",
"SUM(Board.last!A1:C3)-Board.last!B2=2)),1,0))\")\n",
),
lib,
)
.unwrap_or_else(|e| panic!("{}", e.refusal.text));
let mut computed = Vec::new();
for frame in 1..=8u32 {
assert_eq!(m.write(&format!("(cell \"S\" \"A1\" {frame})")), Ok(1));
computed.push(ticked(&mut m));
let upright = frame % 2 == 1;
for (addr, alive) in [
("E4", upright),
("E6", upright),
("D5", !upright),
("F5", !upright),
] {
assert_eq!(
number(&m, "Board", addr),
f64::from(u8::from(alive)),
"frame {frame}: {addr}"
);
}
assert_eq!(number(&m, "Board", "E5"), 1.0, "frame {frame}");
}
assert_eq!(computed, [0, 196, 21, 21, 21, 21, 21, 21]);
}
#[test]
fn a_range_where_one_value_is_wanted_is_labelled_as_written() {
let mut m = load(concat!(
"(cell \"S\" \"A1:B3\" 1)\n",
"(name \"Wide\" \"S!$A$1:$B$1\")\n",
"(formula \"S\" \"D1:D2\" \"=A1:B1*2\")\n",
"(formula \"S\" \"E1:E2\" \"=Wide*2\")\n",
));
assert_eq!(m.formulas(), (4, 2));
m.step(0);
let label = |s: &str| Some(Computed::NotComputed(Reason::Construct(s.to_string())));
assert_eq!(at(&m, "S", "D1"), label("A1:B1"));
assert_eq!(at(&m, "S", "D2"), label("A2:B2"), "moved with its cell");
assert_eq!(at(&m, "S", "E1"), label("S!$A$1:$B$1"));
assert_eq!(
at(&m, "S", "E2"),
label("S!$A$1:$B$1"),
"a name does not move"
);
}
#[test]
fn a_write_over_a_shared_formula_s_first_cell_keeps_the_rest() {
let mut m = load("(cell \"S\" \"A1:A3\" 2)\n(formula \"S\" \"B1:B3\" \"=A1*10\")\n");
assert_eq!(m.formulas(), (3, 1), "one shape over the range");
assert_eq!(m.write("(cell \"S\" \"B1\" 5)"), Ok(1));
let record = grid(&m, "S", None);
assert!(
record.contains("(formula \"S\" \"B2\" \"=A2*10\")"),
"{record}"
);
assert!(
record.contains("(formula \"S\" \"B3\" \"=A3*10\")"),
"{record}"
);
m.step(0);
assert_eq!(number(&m, "S", "B3"), 20.0);
assert_eq!(content(&m, "S", "B1"), Some(Content::Number(5.0)));
}
#[test]
fn the_plane_is_a_byte_a_cell_and_agrees_with_the_record() {
let mut m = load(
"(cell \"S\" \"A1\" 0)\n(cell \"S\" \"B1\" 1)\n(cell \"S\" \"C1\" 254)\n(cell \"S\" \"D1\" 255)\n(cell \"S\" \"E1\" 1.5)\n(cell \"S\" \"F1\" -1)\n(cell \"S\" \"G1\" \"x\")\n(cell \"S\" \"H1\" true)\n(cell \"S\" \"I1\" (error \"#N/A\"))\n(formula \"S\" \"A2\" \"=B1+2\")\n(formula \"S\" \"B2\" \"=1/0\")\n",
);
let row1 = [0u8, 1, 254, 255, 255, 255, 255, 255, 255, 0];
assert_eq!(plane(&m, "S", "A1:J1"), row1);
assert_eq!(
plane(&m, "S", "A2:C2"),
[0, 0, 0],
"no value before the first step"
);
m.step(0);
assert_eq!(plane(&m, "S", "A2:C2"), [3, 255, 0]);
assert_eq!(plane(&m, "S", "J9:K10"), [0, 0, 0, 0]);
let record = grid(&m, "S", Some("A2:C2"));
assert!(record.contains("(value \"S\" \"A2\" 3)"), "{record}");
assert!(
record.contains("(value \"S\" \"B2\" (error \"#DIV/0!\"))"),
"{record}"
);
}
const PLANES: &str = "\
(cell \"Board\" \"B2\" 7)
(cell \"Board\" \"C2\" 254)
(cell \"Board\" \"D2\" \"x\")
(cell \"Board\" \"E2\" true)
(cell \"Board\" \"F2\" (error \"#N/A\"))
(cell \"Board\" \"G2\" 1.5)
(formula \"Board\" \"B3\" \"=Board.last!B3+1\")
(formula \"Board\" \"C3\" \"=B3*100\")
(formula \"Board\" \"D3\" \"=B2:C2\")
(formula \"Board\" \"E3\" \"=IF(B3>1,D2,B3)\")
(formula \"Board\" \"F3\" \"=1/0\")
(formula \"Board\" \"B4:D4\" \"=B3+B2\")
(formula \"Board\" \"F5\" \"=Z99\")
(formula \"Board\" \"G5\" \"=Board.last!D3\")
(cell \"Other\" \"A1\" 3)
(formula \"Other\" \"B1\" \"=A1*2\")
";
const BOARD_WINDOWS: &[&str] = &[
"B2:G5", "C3:E4", "A1:D3", "F4:J7", "L9:M10", "D3", "A1:Z20", "G1:G9",
];
fn kept(m: &Machine, sheet: &str) -> bool {
let s = m.find_sheet(sheet).unwrap();
m.planes.get(s).is_some_and(Option::is_some)
}
fn walked(m: &Machine, sheet: &str, windows: &[&str], kept_now: bool) {
let was_kept = kept(m, sheet);
for w in windows {
let window = window_of(&m.book, sheet, Some(w)).unwrap();
let walk = plane::bytes(&m.book, m, &window);
assert_eq!(plane(m, sheet, w), walk, "{sheet}!{w}, frame {}", m.frame());
}
assert_eq!(was_kept, kept_now, "{sheet} kept, frame {}", m.frame());
}
#[test]
fn a_kept_plane_is_the_walk_after_every_step_and_every_write() {
let mut m = load(PLANES);
walked(&m, "Board", BOARD_WINDOWS, false);
m.step(0);
walked(&m, "Board", BOARD_WINDOWS, true);
assert!(!kept(&m, "Other"), "a sheet never viewed keeps no plane");
m.step(0);
assert!(
matches!(at(&m, "Board", "D3"), Some(Computed::NotComputed(_))),
"{:?}",
at(&m, "Board", "D3")
);
assert_eq!(plane(&m, "Board", "B3:D3"), [2, 200, 255]);
m.step(0);
assert_eq!(plane(&m, "Board", "B3:D3"), [3, 255, 255]);
walked(&m, "Board", BOARD_WINDOWS, true);
for row in [
"(cell \"Board\" \"B2\" 9)",
"(formula \"Board\" \"C2\" \"=B2+1\")",
"(derived \"Board\" \"G2\" 3)",
"(cell \"Board\" \"C5:D6\" 2)",
"(cell \"Board\" \"I1\" 1)",
] {
assert!(m.write(row).is_ok(), "{row}");
walked(&m, "Board", BOARD_WINDOWS, false);
m.step(0);
walked(&m, "Board", BOARD_WINDOWS, true);
}
assert!(m
.write("(cell \"Board\" \"B2\" 1)\n(cell \"Nowhere\" \"A1\" 1)")
.is_err());
walked(&m, "Board", BOARD_WINDOWS, true);
assert_eq!(m.write("(cell \"Other\" \"A1\" 4)"), Ok(1));
walked(&m, "Board", BOARD_WINDOWS, true);
let last = plane(&m, "Board", "A1:Z20");
assert!(!m.step(2).done);
walked(&m, "Board", BOARD_WINDOWS, true);
assert_eq!(plane(&m, "Board", "A1:Z20"), last);
while !m.step(2).done {}
walked(&m, "Board", BOARD_WINDOWS, true);
assert_ne!(plane(&m, "Board", "A1:Z20"), last, "the counter moved");
m.resume_at(100).unwrap();
walked(&m, "Board", BOARD_WINDOWS, true);
walked(&m, "Board.last", BOARD_WINDOWS, false);
m.step(0);
walked(&m, "Board.last", BOARD_WINDOWS, false);
}
#[test]
fn a_kept_plane_from_a_saves_values_is_the_walk() {
let mut m = load(PLANES);
for _ in 0..3 {
m.step(0);
}
let mut again = load(&save(&m));
walked(&again, "Board", BOARD_WINDOWS, false);
again.walk();
walked(&again, "Board", BOARD_WINDOWS, true);
}
#[test]
fn lifes_plane_is_kept_through_the_clocks_writes() {
let windows = [
"B2:BK40",
"A1:BL41",
"A1:C3",
"BJ39:BM42",
"CA50:CB51",
"C3:D5",
];
let mut m = load(LIFE);
walked(&m, "Screen", &windows, false);
for frame in 1..=6u64 {
assert_eq!(m.write(&format!("(cell \"Clock\" \"B1\" {frame})")), Ok(1));
walked(&m, "Screen", &windows, frame > 1);
assert!(m.step(0).done);
walked(&m, "Screen", &windows, true);
}
}
#[test]
fn a_sheet_past_a_planes_limit_is_walked() {
let mut m = load("(cell \"Big\" \"A1\" 1)\n(cell \"Big\" \"XFD1048576\" 2)\n");
assert_eq!(plane(&m, "Big", "A1:B2"), [1, 0, 0, 0]);
m.step(0);
assert!(!kept(&m, "Big"), "its extent passes the limit");
assert_eq!(plane(&m, "Big", "XFC1048575:XFD1048576"), [0, 0, 0, 2]);
}
#[test]
fn a_view_refuses_what_it_does_not_have_by_name() {
let m = load(COUNTER);
let e = m.view("raster", "Board", None).unwrap_err();
assert_eq!(e.id, "view-projection-unknown");
assert!(
e.text.starts_with("raster is not a projection"),
"{}",
e.text
);
let e = m.view("grid", "Nowhere", None).unwrap_err();
assert_eq!(e.id, "view-sheet-unknown");
assert_eq!(
e.text,
"The grid holds no sheet named Nowhere; it holds Board, Board.last."
);
assert_eq!(
m.view("plane", "Board", Some("A:A")).unwrap_err().id,
"view-window-not-a-range"
);
let e = m.view("plane", "Board", Some("A1:XFD1048576")).unwrap_err();
assert_eq!(e.id, "view-window-too-large");
let twin = grid(&m, "board.LAST", None);
assert!(twin.contains("(window \"Board.last\" \"D1\")"), "{twin}");
assert!(twin.contains("(cell \"Board.last\" \"D1\" 5)"), "{twin}");
}
#[test]
fn load_is_the_inverse_of_view() {
let text = "\
(sheet \"Model\" visible)
(sheet \"Notes\" hidden)
(name \"Rate\" \"0.25\")
(gridlines \"Model\" off)
(column \"Model\" \"A\" none hidden none)
(column \"Model\" \"B\" 72 shown 1)
(row \"Model\" 2 30 shown)
(row \"Model\" 4 none hidden)
(format 0 none general nowrap)
(format 1 \"F7F4FC\" text nowrap)
(format 2 \"FF0000\" general wrap)
(look \"Model\" 1 2)
(look \"Model\" \"hot\" 2)
(cell \"Model\" \"A1\" \"say \\\"hi\\\"\")
(style \"Model\" \"A1\" 1)
(sentence \"Model\" \"A1\" 3)
(cell \"Model\" \"B1\" -2.5)
(cell \"Model\" \"C1\" true)
(cell \"Model\" \"D1\" (error \"#N/A\"))
(cell \"Model\" \"A2:B3\" 1)
(formula \"Model\" \"C2:C4\" \"=A2*Rate+Model.last!C2\")
(formula \"Model\" \"D2\" \"=IF(C2>0,\\\"up\\\",\\\"down\\\")\")
(cell \"Notes\" \"A1\" \"the notes\")
";
let mut m = load(text);
for _ in 0..2 {
let saved = save(&m);
let again = load(&saved);
assert_eq!(save(&again), saved, "row for row");
m.step(0);
}
let saved = save(&m);
assert!(saved.contains("(look \"Model\" \"hot\" 2)"), "{saved}");
assert!(saved.contains("(row \"Model\" 4 none hidden)"), "{saved}");
assert!(saved.contains("(sheet \"Notes\" hidden)"), "{saved}");
assert!(saved.contains("(value \"Model\" \"C3\" 0.5)"), "{saved}");
}
#[test]
fn load_is_the_inverse_of_view_on_the_view_goldens() {
let goldens: &[(&str, &str, &str)] = &[
(
include_str!("../../testdata/view/fixture_frazaro.vla"),
"Frazaro",
"B1:C11",
),
(
include_str!("../../testdata/view/fixture_output.vla"),
"Output",
"A1:F20",
),
(
include_str!("../../testdata/view/fixture_data.vla"),
"data",
"A1",
),
(
include_str!("../../testdata/view/into_output.vla"),
"Output",
"A1:B1",
),
(
include_str!("../../testdata/view/into_checks.vla"),
"checks",
"A1",
),
];
for (golden, sheet, window) in goldens {
let golden = golden.replace("\r\n", "\n");
let forms = read_forms(&golden).unwrap();
let draft = read_rows(&forms).unwrap_or_else(|e| panic!("{}", e.refusal.text));
let w = Window {
sheet: sheet.to_string(),
range: parse_a1_range(window).unwrap(),
};
let again = view_text_valued(&draft.workbook, &w, &Held(draft.values.clone()));
assert_eq!(again, golden, "{sheet} {window}");
}
}
const LIFE: &str = "\
(sheet \"Screen\" visible)
(cell \"Clock\" \"A1\" \"frame\")
(cell \"Clock\" \"B1\" 0)
(formula \"Screen\" \"B2:BK40\" \"=IF(Clock!$B$1=1,IF(MOD(ROW()*7+COLUMN()*13+ROW()*COLUMN(),5)<2,1,0),IF(OR(SUM(Screen.last!A1:C3)-Screen.last!B2=3,AND(Screen.last!B2=1,SUM(Screen.last!A1:C3)-Screen.last!B2=2)),1,0))\")
";
const W: usize = 64;
const H: usize = 41;
fn life_here(frames: u64) -> Vec<u8> {
let mut g = vec![0u8; W * H];
for frame in 1..=frames {
let mut next = vec![0u8; W * H];
for r in 2..H {
for c in 2..W {
let (row, col) = (r as u64, c as u64);
next[(r - 1) * W + (c - 1)] = if frame == 1 {
u8::from((row * 7 + col * 13 + row * col) % 5 < 2)
} else {
let mut n = 0;
for dr in 0..3 {
for dc in 0..3 {
if dr != 1 || dc != 1 {
n += g[(r - 2 + dr) * W + (c - 2 + dc)];
}
}
}
let alive = g[(r - 1) * W + (c - 1)] == 1;
u8::from(n == 3 || (alive && n == 2))
};
}
}
g = next;
}
g
}
fn life_step(m: &mut Machine) {
let frame = m.frame() + 1;
assert_eq!(m.write(&format!("(cell \"Clock\" \"B1\" {frame})")), Ok(1));
assert!(m.step(0).done);
}
#[test]
fn life_on_a_small_board_is_the_life_written_here() {
let mut m = load(LIFE);
assert_eq!(m.formulas(), (39 * 62, 1), "one shape for the whole rule");
let before = formula_rows(&m);
for frame in 1..=12u64 {
life_step(&mut m);
assert_eq!(
plane(&m, "Screen", "A1:BL41"),
life_here(frame),
"frame {frame}"
);
}
assert_eq!(formula_rows(&m), before, "AD-7 over twelve frames");
assert!(life_here(12).contains(&1), "the board is not dead");
}
#[test]
fn a_save_resumes() {
let mut m = load(LIFE);
for _ in 0..5 {
life_step(&mut m);
}
let saved = save(&m);
let mut again = load(&saved);
assert_eq!(
again.formulas(),
(39 * 62, 1),
"a save's 2,418 rows are one shape"
);
again.resume_at(m.frame()).unwrap();
life_step(&mut m);
life_step(&mut again);
assert_eq!(again.frame(), 6);
assert_eq!(save(&again), save(&m), "cell for cell");
assert_eq!(plane(&again, "Screen", "A1:BL41"), life_here(6));
}
#[test]
fn a_shape_at_an_offset_is_the_parse_of_its_moved_text() {
use crate::calc::formula::{parse, Expr};
fn same(a: &Expr, b: &Expr, d: (i64, i64)) -> bool {
match (a, b) {
(Expr::Ref(x), Expr::Ref(y)) => {
x.sheet == y.sheet
&& x.last_sheet == y.last_sheet
&& x.moved(d.0, d.1) == Some((y.top, y.left, y.bottom, y.right))
}
(Expr::Ref(x), Expr::Error(crate::calc::ErrorKind::Ref)) => {
x.moved(d.0, d.1).is_none()
}
(Expr::Call(n, xs), Expr::Call(m, ys)) => {
n == m && xs.len() == ys.len() && xs.iter().zip(ys).all(|(x, y)| same(x, y, d))
}
(Expr::Binary(o, x1, x2), Expr::Binary(p, y1, y2)) => {
o == p && same(x1, y1, d) && same(x2, y2, d)
}
(Expr::Neg(x), Expr::Neg(y))
| (Expr::Pos(x), Expr::Pos(y))
| (Expr::Percent(x), Expr::Percent(y)) => same(x, y, d),
_ => a == b,
}
}
let fixture = include_str!("../../testdata/refers.txt").replace("\r\n", "\n");
let mut formulas: Vec<String> = Vec::new();
let lines: Vec<&str> = fixture.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.starts_with("=== ") && i + 2 < lines.len() {
formulas.push(lines[i + 2].to_string());
}
}
formulas.push(
LIFE.lines()
.nth(3)
.unwrap()
.split('"')
.nth(5)
.unwrap()
.replace("\\\"", "\""),
);
let offsets = [
(1, 0),
(0, 1),
(2, 3),
(-1, 0),
(0, -1),
(-3, -2),
(1_048_575, 0),
(0, 16_383),
(-1_048_575, 0),
(0, -16_383),
];
let mut compared = 0;
let mut unread = 0;
for text in &formulas {
let Ok(e) = parse(text) else { continue };
let mut shapes = Shapes::new();
let mut book = Workbook::new();
let s = book.ensure_sheet("Model");
book.sheets[s].set(
1,
1,
Cell {
content: Content::Formula(text.trim_start_matches('=').to_string()),
style: 0,
},
);
shapes.replace(&book, (0, 1, 1), &library::language());
let shape = shapes.shape(0).unwrap();
for d in offsets {
let moved = shift_a1_references(text, d.0, d.1);
match parse(&moved) {
Ok(pm) => {
assert_eq!(shape.unread_at(d.0, d.1), None, "{moved}");
assert!(same(&e, &pm, d), "{text} moved by {d:?} is {moved}");
compared += 1;
}
Err(construct) => {
assert_eq!(
shape.unread_at(d.0, d.1),
Some(construct),
"{text} moved by {d:?} is {moved}"
);
unread += 1;
}
}
}
}
assert!(
compared >= 500,
"{compared} formulas and distances compared"
);
assert!(unread > 0, "a 3D span moved off the sheet was reached");
}
#[test]
fn plain_formulas_of_one_r1c1_text_are_one_shape() {
let m = load("(formula \"S\" \"A2\" \"=A1+1\")\n(formula \"S\" \"A3\" \"=A2+1\")\n(formula \"S\" \"B3\" \"=B2+1\")\n(formula \"S\" \"C3\" \"=$A$1+1\")\n");
assert_eq!(m.formulas(), (4, 2));
let book = &m.book;
let names = Names::of(book);
let extents: Vec<Extent> = book.sheets.iter().map(Sheet::extent).collect();
let s = Shapes::of(book, 1, &library::language(), &names, &extents);
assert_eq!(
s.order().first(),
Some(&(0, 2, 1)),
"A2 before what reads it"
);
assert!(s.cycles().is_empty());
}
}