use std::collections::HashMap;
use crate::fx::FxHashMap;
use crate::messages::{raise, Refusal};
use crate::refers::{self, quote_sheet};
use crate::sheet::{cell_ref, shared_masters, strip_future_prefixes, Content, Workbook};
use super::eval::{Computed, Ctx, Env, Library, Reason};
use super::formula::{self, Expr, Read, RefExpr};
use super::graph::{self, areas_at, cells_in, dependencies, is_formula, CellId, Extent, Names};
use super::reach::Reach;
use super::vector::{self, Lanes, Repeats};
#[derive(Clone, Debug)]
pub struct Shape {
pub text: String,
pub expr: Result<Expr, String>,
pub anchor: CellId,
missing: Vec<String>,
pub(crate) risky: bool,
holds: (usize, usize),
repeats: Repeats,
}
impl Shape {
#[cfg(test)]
pub(crate) fn holds(&self) -> (usize, usize) {
self.holds
}
pub fn unread_at(&self, d_row: i64, d_col: i64) -> Option<String> {
if !self.risky || (d_row == 0 && d_col == 0) || self.expr.is_err() {
return None;
}
formula::parse(&refers::shift_a1_references(&self.text, d_row, d_col)).err()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Place {
pub shape: usize,
pub d_row: i64,
pub d_col: i64,
}
#[derive(Clone, Debug, Default)]
pub struct Shapes {
shapes: Vec<Shape>,
by_r1c1: HashMap<String, usize>,
by_shared: HashMap<(usize, u32), usize>,
places: FxHashMap<CellId, Place>,
unread: FxHashMap<CellId, String>,
order: Vec<CellId>,
cycles: Vec<Vec<CellId>>,
runs: Vec<Run>,
reach: Reach,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Run {
start: usize,
end: usize,
shape: usize,
}
pub(crate) const RUN: usize = 4;
impl Shapes {
pub fn new() -> Shapes {
Shapes::default()
}
pub fn of(
model: &Workbook,
sheets: usize,
library: &Library,
names: &Names,
extents: &[Extent],
) -> Shapes {
let mut s = Shapes::new();
for (si, sheet) in model.sheets.iter().enumerate().take(sheets) {
let masters = shared_masters(sheet);
for (&(row, col), cell) in &sheet.cells {
s.place_with(model, (si, row, col), &cell.content, &masters, library);
}
}
s.plan(model, names, extents);
s
}
pub fn replace(&mut self, model: &Workbook, id: CellId, library: &Library) {
self.places.remove(&id);
self.unread.remove(&id);
let Some(sheet) = model.sheets.get(id.0) else {
return;
};
let Some(cell) = sheet.cells.get(&(id.1, id.2)) else {
return;
};
if !is_formula(&cell.content) {
return;
}
let masters = shared_masters(sheet);
self.place_with(model, id, &cell.content, &masters, library);
}
fn place_with(
&mut self,
model: &Workbook,
id: CellId,
content: &Content,
masters: &HashMap<u32, (&str, u32, u32)>,
library: &Library,
) {
let (si, row, col) = id;
let (shape, anchor) = match content {
Content::SharedMaster { si: index, .. } | Content::SharedChild { si: index } => {
let Some(&(text, m_row, m_col)) = masters.get(index) else {
return;
};
let anchor = (si, m_row, m_col);
let key = (si, *index);
let shape = match self.by_shared.get(&key) {
Some(&s) => s,
None => {
let s = self.add(model, strip_future_prefixes(text), anchor, library);
self.by_shared.insert(key, s);
s
}
};
(shape, anchor)
}
Content::Formula(t) | Content::DynamicFormula(t) => {
let text = strip_future_prefixes(t);
let key = refers::r1c1(&text, row, col);
match self.by_r1c1.get(&key) {
Some(&s) => (s, self.shapes[s].anchor),
None => {
let s = self.add(model, text, id, library);
self.by_r1c1.insert(key, s);
(s, id)
}
}
}
_ => return,
};
let (d_row, d_col) = (
i64::from(row) - i64::from(anchor.1),
i64::from(col) - i64::from(anchor.2),
);
self.places.insert(
id,
Place {
shape,
d_row,
d_col,
},
);
match self.shapes[shape].unread_at(d_row, d_col) {
Some(construct) => self.unread.insert(id, construct),
None => self.unread.remove(&id),
};
}
fn add(&mut self, model: &Workbook, text: String, anchor: CellId, library: &Library) -> usize {
let mut expr = formula::parse(&text);
if let Ok(e) = expr.as_mut() {
find_sheets(e, model);
}
let mut missing: Vec<String> = formula::calls(&text)
.into_iter()
.filter(|name| !library.has(name))
.collect();
let risky = refers::scan(&text).iter().any(|r| {
r.kind == refers::Kind::ThreeD
|| (r.kind == refers::Kind::Broken && r.shape != refers::Shape::None)
});
missing.sort_unstable();
missing.dedup();
self.push(text, expr, anchor, missing, risky, library)
}
#[inline(never)]
fn push(
&mut self,
text: String,
expr: Result<Expr, String>,
anchor: CellId,
missing: Vec<String>,
risky: bool,
library: &Library,
) -> usize {
let (holds, repeats) = match &expr {
Ok(e) => {
let repeats = Repeats::of(library, e);
let (values, positions) = vector::holds(library, e);
((values + repeats.parts(), positions), repeats)
}
Err(_) => ((0, 0), Repeats::default()),
};
self.shapes.push(Shape {
text,
expr,
anchor,
missing,
risky,
holds,
repeats,
});
self.shapes.len() - 1
}
pub fn plan(&mut self, model: &Workbook, names: &Names, extents: &[Extent]) {
let mut ids: Vec<CellId> = self.places.keys().copied().collect();
ids.sort_unstable();
let index: HashMap<CellId, usize> =
ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
let mut holds = vec![false; model.sheets.len()];
for id in &ids {
holds[id.0] = true;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); ids.len()];
let mut shape_of: Vec<usize> = Vec::with_capacity(ids.len());
let mut deps: Vec<CellId> = Vec::new();
for (vi, id) in ids.iter().enumerate() {
deps.clear();
let p = self.places[id];
shape_of.push(p.shape);
let shape = &self.shapes[p.shape];
let read = !self.unread.contains_key(id);
match &shape.expr {
Ok(e) if read => e.each_read(&mut |r| match r {
Read::Ref(re) => {
let Ok(areas) = areas_at(model, id.0, re, p.d_row, p.d_col) else {
return;
};
for a in &areas {
if !holds.get(a.sheet).copied().unwrap_or(false) {
continue;
}
let sheet = &model.sheets[a.sheet];
let extent = extents.get(a.sheet).copied().flatten();
for ((row, col), cell) in cells_in(sheet, extent, a) {
if is_formula(&cell.content) {
deps.push((a.sheet, row, col));
}
}
}
}
Read::Name { name, sheet } => {
let scope = sheet.and_then(|s| model.find_sheet(s));
if let Some(t) = names.resolve(name, scope, id.0) {
dependencies(
model,
names,
extents,
scope.unwrap_or(id.0),
t,
&mut deps,
);
}
}
}),
_ => {
let moved = refers::shift_a1_references(&shape.text, p.d_row, p.d_col);
dependencies(model, names, extents, id.0, &moved, &mut deps);
}
}
let edges = &mut adj[vi];
for d in &deps {
if let Some(&di) = index.get(d) {
edges.push(di);
}
}
edges.sort_unstable();
edges.dedup();
}
let components = graph::tarjan(&adj);
let shape_at = self.order_and_runs(&ids, &adj, &shape_of, &components);
let places = &self.places;
let formula = |id: CellId| places.contains_key(&id);
self.reach = Reach::of(model, &self.shapes, &self.order, shape_at, &formula);
}
fn order_and_runs(
&mut self,
ids: &[CellId],
adj: &[Vec<usize>],
shape_of: &[usize],
components: &[Vec<usize>],
) -> Vec<u32> {
self.order.clear();
self.cycles.clear();
self.runs.clear();
let mut component = vec![0; ids.len()];
let mut level = vec![0; ids.len()];
let mut alone = vec![false; ids.len()];
for (c, comp) in components.iter().enumerate() {
for &v in comp {
component[v] = c;
}
let deepest = comp
.iter()
.flat_map(|&v| &adj[v])
.filter(|&&u| component[u] != c)
.map(|&u| level[u] + 1)
.max()
.unwrap_or(0);
for &v in comp {
level[v] = deepest;
}
if comp.len() == 1 && !adj[comp[0]].contains(&comp[0]) {
alone[comp[0]] = true;
} else {
let mut cells: Vec<CellId> = comp.iter().map(|&i| ids[i]).collect();
cells.sort_unstable();
self.cycles.push(cells);
}
}
self.cycles.sort();
let cells: Vec<usize> = (0..ids.len()).filter(|&v| alone[v]).collect();
let cells = by_key(&cells, self.shapes.len(), |v| shape_of[v]);
let levels = level.iter().max().map_or(0, |deepest| deepest + 1);
let order = by_key(&cells, levels, |v| level[v]);
let mut joined = vec![usize::MAX; ids.len()];
let mut run = Run {
start: 0,
end: 0,
shape: usize::MAX,
};
let mut shape_at = Vec::with_capacity(order.len());
for v in order {
let (id, shape) = (ids[v], shape_of[v]);
let pos = self.order.len();
let joins = shape == run.shape
&& id.0 == self.order[run.start].0
&& adj[v].iter().all(|&u| joined[u] != run.start);
if !joins {
self.keep(run);
run = Run {
start: pos,
end: pos,
shape,
};
}
joined[v] = run.start;
run.end = pos + 1;
self.order.push(id);
shape_at.push(shape as u32);
}
self.keep(run);
shape_at
}
fn keep(&mut self, run: Run) {
let Some(shape) = self.shapes.get(run.shape) else {
return;
};
if run.end - run.start >= RUN && !shape.risky && shape.expr.is_ok() {
self.runs.push(run);
}
}
#[cfg(test)]
pub(crate) fn cells_in_runs(&self) -> usize {
self.runs.iter().map(|r| r.end - r.start).sum()
}
pub(crate) fn run_at(&self, pos: usize) -> Option<(usize, usize)> {
let after = self.runs.partition_point(|r| r.start <= pos);
let run = self.runs.get(after.checked_sub(1)?)?;
(pos < run.end).then_some((run.shape, run.end))
}
pub(crate) fn evaluate_run(
&self,
env: &dyn Env,
library: &Library,
shape: usize,
cells: &[CellId],
lanes: &mut Lanes,
out: &mut Vec<Computed>,
) {
let s = &self.shapes[shape];
match &s.expr {
Ok(e) => {
let held = s.holds;
vector::evaluate(
env, library, e, held, &s.repeats, s.anchor, cells, lanes, out,
);
}
Err(_) => out.extend(cells.iter().map(|&id| {
env.enter(id);
self.evaluate(env, library, id)
})),
}
}
pub fn order(&self) -> &[CellId] {
&self.order
}
pub(crate) fn reach(&self) -> &Reach {
&self.reach
}
pub fn cycles(&self) -> &[Vec<CellId>] {
&self.cycles
}
pub fn formulas(&self) -> usize {
self.places.len()
}
pub fn shapes(&self) -> usize {
self.shapes.len()
}
pub fn holds(&self, id: CellId) -> bool {
self.places.contains_key(&id)
}
pub fn place(&self, id: CellId) -> Option<Place> {
self.places.get(&id).copied()
}
pub fn shape(&self, index: usize) -> Option<&Shape> {
self.shapes.get(index)
}
pub fn evaluate(&self, env: &dyn Env, library: &Library, id: CellId) -> Computed {
let Some(p) = self.places.get(&id) else {
return Computed::NotComputed(Reason::Construct(String::new()));
};
let shape = &self.shapes[p.shape];
if let Some(construct) = self.unread.get(&id) {
return Computed::NotComputed(Reason::Construct(construct.clone()));
}
match &shape.expr {
Err(construct) => Computed::NotComputed(Reason::Construct(construct.clone())),
Ok(e) => {
let ctx = Ctx::at(env, library, p.d_row, p.d_col);
match ctx.eval(e).and_then(|arg| ctx.one(&arg)) {
Ok(v) => Computed::Value(v.settled()),
Err(reason) => Computed::NotComputed(reason),
}
}
}
}
pub fn first_refusal(
&self,
model: &Workbook,
only: Option<&[CellId]>,
) -> Option<(CellId, Refusal)> {
if let Some(cycle) = self.cycles.first() {
let cells: Vec<String> = cycle.iter().map(|id| spell_cell(model, *id)).collect();
return Some((
cycle[0],
raise("calc-cycle", &[("cells", &cells.join(", "))]),
));
}
let mut ids: Vec<CellId> = match only {
Some(cells) => cells
.iter()
.copied()
.filter(|id| self.places.contains_key(id))
.collect(),
None => self.places.keys().copied().collect(),
};
ids.sort_unstable();
for id in &ids {
let shape = &self.shapes[self.places[id].shape];
if let Some(name) = shape.missing.first() {
return Some((
*id,
raise(
"calc-function-not-computed",
&[("cell", &spell_cell(model, *id)), ("function", name)],
),
));
}
}
for id in &ids {
let p = self.places[id];
let shape = &self.shapes[p.shape];
let label = match (&shape.expr, self.unread.get(id).cloned()) {
(_, Some(unread)) => Some(unread),
(Err(construct), None) if p.d_row == 0 && p.d_col == 0 => Some(construct.clone()),
(Err(construct), None) => {
let moved = refers::shift_a1_references(&shape.text, p.d_row, p.d_col);
Some(
formula::parse(&moved)
.err()
.unwrap_or_else(|| construct.clone()),
)
}
(Ok(_), None) => None,
};
if let Some(label) = label {
return Some((
*id,
formula::unread_refusal(&spell_cell(model, *id), &label),
));
}
}
None
}
}
fn by_key(cells: &[usize], keys: usize, key: impl Fn(usize) -> usize) -> Vec<usize> {
let mut start = vec![0; keys + 1];
for &v in cells {
start[key(v) + 1] += 1;
}
for k in 0..keys {
start[k + 1] += start[k];
}
let mut out = vec![0; cells.len()];
for &v in cells {
let k = key(v);
out[start[k]] = v;
start[k] += 1;
}
out
}
fn find_sheets(expr: &mut Expr, model: &Workbook) {
let mut stack = vec![expr];
while let Some(e) = stack.pop() {
match e {
Expr::Ref(r) => r.found = sheets_of(model, r),
Expr::Call(_, args) => stack.extend(args.iter_mut()),
Expr::Neg(x) | Expr::Pos(x) | Expr::Percent(x) => stack.push(x),
Expr::Binary(_, a, b) => {
stack.push(a);
stack.push(b);
}
Expr::Name { .. }
| Expr::Number(_)
| Expr::Text(_)
| Expr::Bool(_)
| Expr::Error(_)
| Expr::Empty => {}
}
}
}
fn sheets_of(model: &Workbook, r: &RefExpr) -> Option<(u8, u8)> {
let first = model.find_sheet(r.sheet.as_deref()?)?;
let last = match &r.last_sheet {
None => first,
Some(name) => model.find_sheet(name)?,
};
Some((u8::try_from(first).ok()?, u8::try_from(last).ok()?))
}
pub fn spell_cell(model: &Workbook, id: CellId) -> String {
let sheet = model
.sheets
.get(id.0)
.map(|s| s.name.as_str())
.unwrap_or("");
format!("{}!{}", quote_sheet(sheet), cell_ref(id.1, id.2))
}