use super::FormulaExecutor;
use logisheets_base::errors::BasicError;
use logisheets_base::{
BlockCellId, BlockId, BlockRange, CellId, EphemeralId, NormalRange, Range, RangeId, RefAbs,
SheetId,
};
use logisheets_parser::Parser;
use logisheets_parser::ast::{self, RangeDisplay};
use std::collections::HashSet;
use crate::formula_manager::{FormulaManager, Vertex, ctx::FormulaExecCtx};
pub fn input_formula<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
row: usize,
col: usize,
formula: String,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let cell_id = ctx.fetch_cell_id(&sheet, row, col)?;
input(executor, sheet, cell_id, formula, None, ctx)
}
pub fn input_block_formula<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
block_id: BlockId,
block_row: usize,
block_col: usize,
formula: String,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bcid = ctx.fetch_block_cell_id(&sheet, &block_id, block_row, block_col)?;
input(executor, sheet, CellId::BlockCell(bcid), formula, None, ctx)
}
pub fn input_ephemeral_formula<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
id: EphemeralId,
formula: String,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
if let Some((sheet_id, cell_id)) = ctx.get_cell_id_by_shadow_id(&id) {
let range = match cell_id {
CellId::NormalCell(normal_cell_id) => {
Range::Normal(NormalRange::Single(normal_cell_id))
}
CellId::BlockCell(block_cell_id) => Range::Block(BlockRange::Single(block_cell_id)),
CellId::EphemeralCell(e) => return Err(BasicError::EphemeralCellInReference(e)),
};
let range_id = ctx.fetch_range_id(&sheet_id, &range);
let placeholder_node = ast::Node {
pure: ast::PureNode::Reference(ast::CellReference::Mut(RangeDisplay {
range_id,
ref_abs: RefAbs::default(),
sheet_id,
})),
bracket: true,
};
if let CellId::BlockCell(block_cell_id) = cell_id {
if let Some(row_ctx) = ctx.block_cell_row_substitutes(sheet_id, &block_cell_id) {
let siblings: std::collections::HashMap<String, BlockCellId> =
row_ctx.siblings.into_iter().collect();
let key_node = ast::Node {
pure: ast::PureNode::Value(ast::Value::Text(row_ctx.key_value)),
bracket: false,
};
return input_with_resolver(
executor,
sheet,
CellId::EphemeralCell(id),
formula,
placeholder_node,
block_cell_id,
siblings,
key_node,
ctx,
);
}
}
input(
executor,
sheet,
CellId::EphemeralCell(id),
formula,
Some(placeholder_node),
ctx,
)
} else {
let cell_id = CellId::EphemeralCell(id);
input(executor, sheet, cell_id, formula, None, ctx)
}
}
fn resolve_field_placeholders<C: FormulaExecCtx>(
body: &str,
sheet: SheetId,
owner: &BlockCellId,
siblings: &std::collections::HashMap<String, BlockCellId>,
self_cell: Option<&BlockCellId>,
substituted: &mut HashSet<RangeId>,
ctx: &mut C,
) -> Result<std::collections::HashMap<logisheets_parser::Placeholder, ast::Node>, BasicError> {
use logisheets_parser::Placeholder;
let parser = Parser {};
let Some(needed) = parser.scan_placeholders(body) else {
return Ok(std::collections::HashMap::new());
};
let mut out = std::collections::HashMap::new();
for ph in needed {
let Placeholder::FieldRef(name, key) = &ph else {
continue;
};
let target = match key {
None => siblings.get(name).copied(),
Some(k) => ctx.block_cell_at_key(sheet, owner, k, name),
};
let Some(target) = target else { continue };
if self_cell == Some(&target) {
return Err(BasicError::InvalidFormula(format!(
"field rule {:?} resolves to the cell it is defining \
(field {:?}{}). A rule cannot compute from itself.",
body,
name,
match key {
Some(k) => format!(", key {:?}", k),
None => String::new(),
}
)));
}
let range = Range::Block(BlockRange::Single(target));
let range_id = ctx.fetch_range_id(&sheet, &range);
substituted.insert(range_id);
out.insert(
ph,
ast::Node {
pure: ast::PureNode::Reference(ast::CellReference::Mut(RangeDisplay {
range_id,
ref_abs: RefAbs::default(),
sheet_id: sheet,
})),
bracket: true,
},
);
}
Ok(out)
}
fn input_with_resolver<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet: SheetId,
cell_id: CellId,
formula: String,
placeholder: ast::Node,
owner: BlockCellId,
siblings: std::collections::HashMap<String, BlockCellId>,
key_node: ast::Node,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let mut substituted = HashSet::new();
let subs = resolve_field_placeholders(
&formula,
sheet,
&owner,
&siblings,
None,
&mut substituted,
ctx,
)?;
let parser = Parser {};
let ast = parser.parse_with_substitutes(&formula, sheet, ctx, &|kind| match kind {
logisheets_parser::PlaceholderKind::Placeholder => Some(placeholder.clone()),
logisheets_parser::PlaceholderKind::FieldRef(..) => subs.get(&kind.to_owned()).cloned(),
logisheets_parser::PlaceholderKind::Key => Some(key_node.clone()),
});
let Some(ast) = ast else { return Ok(executor) };
register_parsed_ast(executor, sheet, cell_id, ast, ctx)
}
pub fn convert_cells_to_block<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
block_id: BlockId,
master_row: usize,
master_col: usize,
row_cnt: usize,
col_cnt: usize,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let mut executor = executor;
for r in master_row..master_row + row_cnt {
for c in master_col..master_col + col_cnt {
let Ok(normal) = ctx.fetch_norm_cell_id(&sheet, r, c) else {
continue;
};
let old_id = CellId::NormalCell(normal);
let Some(ast) = executor.manager.formulas.remove(&(sheet, old_id)) else {
continue; };
let old_range = Range::Normal(NormalRange::Single(normal));
let old_range_id = ctx.fetch_range_id(&sheet, &old_range);
let old_vertex = Vertex::Range(sheet, old_range_id);
if let Some(deps) = executor.manager.graph.clone().get_deps(&old_vertex) {
for dep in deps.iter() {
executor.manager.graph.remove_dep(&old_vertex, dep);
}
}
let bcid =
ctx.fetch_block_cell_id(&sheet, &block_id, r - master_row, c - master_col)?;
executor = register_parsed_ast(executor, sheet, CellId::BlockCell(bcid), ast, ctx)?;
}
}
Ok(executor)
}
pub fn remove_formula<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
row: usize,
col: usize,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let cell_id = ctx.fetch_cell_id(&sheet, row, col)?;
remove(executor, sheet, cell_id, ctx)
}
pub fn remove_ephemeral_formula<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
id: EphemeralId,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
remove(executor, sheet, CellId::EphemeralCell(id), ctx)
}
fn remove<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet: SheetId,
cell_id: CellId,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let range = match cell_id {
CellId::NormalCell(normal) => Range::Normal(NormalRange::Single(normal)),
CellId::BlockCell(block) => Range::Block(BlockRange::Single(block)),
CellId::EphemeralCell(v) => Range::Ephemeral(v),
};
let range_id = ctx.fetch_range_id(&sheet, &range);
let this_vertex = Vertex::Range(sheet, range_id);
let FormulaManager {
mut graph,
mut formulas,
names,
} = executor.manager;
if let Some(old_formula_deps) = graph.clone().get_deps(&this_vertex) {
old_formula_deps.iter().for_each(|old_dep| {
graph.remove_dep(&this_vertex, old_dep);
})
};
if let CellId::BlockCell(bcid) = cell_id {
graph.remove_dep(&Vertex::BlockAll(sheet, bcid.block_id), &this_vertex);
}
formulas.remove(&(sheet, cell_id));
let mut ephemeral_shadows_cleared = executor.ephemeral_shadows_cleared;
if let CellId::EphemeralCell(_) = cell_id {
ephemeral_shadows_cleared.insert((sheet, cell_id));
}
Ok(FormulaExecutor {
manager: FormulaManager {
graph,
formulas,
names,
},
dirty_vertices: executor.dirty_vertices,
dirty_ranges: executor.dirty_ranges,
dirty_cubes: executor.dirty_cubes,
dirty_blocks: executor.dirty_blocks,
trigger: executor.trigger,
ephemeral_shadows_cleared,
})
}
fn input<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet: SheetId,
cell_id: CellId,
formula: String,
substitute: Option<ast::Node>,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let parser = Parser {};
let ast = if let Some(node) = substitute {
parser.parse_with_substitude(&formula, &node, sheet, ctx)
} else {
parser.parse(&formula, sheet, ctx)
};
let Some(ast) = ast else { return Ok(executor) };
register_parsed_ast(executor, sheet, cell_id, ast, ctx)
}
fn register_parsed_ast<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet: SheetId,
cell_id: CellId,
ast: ast::Node,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
let range = match cell_id {
CellId::NormalCell(normal) => Range::Normal(NormalRange::Single(normal)),
CellId::BlockCell(block) => Range::Block(BlockRange::Single(block)),
CellId::EphemeralCell(v) => Range::Ephemeral(v),
};
let range_id = ctx.fetch_range_id(&sheet, &range);
let this_vertex = Vertex::Range(sheet, range_id);
if let CellId::BlockCell(bcid) = cell_id {
if let Some(kind) = find_self_block_ref(&ast, sheet, bcid.block_id) {
return Err(BasicError::InvalidFormula(format!(
"{} targeting the cell's own block (sheet={}, block={}) \
is not allowed; use `#FIELD(\"...\")` for same-row \
sibling refs.",
kind, sheet, bcid.block_id
)));
}
}
let mut new_formula_deps = HashSet::<Vertex>::new();
get_all_vertices_from_ast(&ast, &mut new_formula_deps);
let FormulaManager {
mut graph,
mut formulas,
names,
} = executor.manager;
if let Some(old_formula_deps) = graph.clone().get_deps(&this_vertex) {
old_formula_deps.iter().for_each(|old_dep| {
graph.remove_dep(&this_vertex, old_dep);
})
};
new_formula_deps.into_iter().for_each(|new_dep| {
graph.add_dep(this_vertex.clone(), new_dep.clone());
let range_deps = ctx.get_range_deps(&new_dep);
range_deps.into_iter().for_each(|range_dep| {
graph.add_dep(new_dep.clone(), range_dep);
});
});
if let CellId::BlockCell(bcid) = cell_id {
graph.add_dep(Vertex::BlockAll(sheet, bcid.block_id), this_vertex.clone());
}
formulas.insert((sheet, cell_id), ast.clone());
let mut dirty_vertices = executor.dirty_vertices;
dirty_vertices.insert(this_vertex);
let mut ephemeral_shadows_cleared = executor.ephemeral_shadows_cleared;
if let CellId::EphemeralCell(_) = cell_id {
ephemeral_shadows_cleared.remove(&(sheet, cell_id));
}
Ok(FormulaExecutor {
manager: FormulaManager {
graph,
formulas,
names,
},
dirty_vertices,
dirty_ranges: executor.dirty_ranges,
dirty_cubes: executor.dirty_cubes,
dirty_blocks: executor.dirty_blocks,
trigger: executor.trigger,
ephemeral_shadows_cleared,
})
}
pub fn input_block_cell_template<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
block_id: BlockId,
block_row: usize,
block_col: usize,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
use std::collections::HashMap;
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bcid = ctx.fetch_block_cell_id(&sheet, &block_id, block_row, block_col)?;
let Some(tpl) = ctx.block_cell_template(sheet, &bcid) else {
if ctx.block_is_analysis(sheet, block_id) {
return remove(executor, sheet, CellId::BlockCell(bcid), ctx);
}
return Ok(executor);
};
let siblings: HashMap<String, BlockCellId> = tpl.siblings.into_iter().collect();
let key_node = ast::Node {
pure: ast::PureNode::Value(ast::Value::Text(tpl.key_value)),
bracket: false,
};
let body = tpl
.template
.strip_prefix('=')
.map(|s| s.to_string())
.unwrap_or(tpl.template);
let mut substituted: HashSet<RangeId> = HashSet::new();
let subs = resolve_field_placeholders(
&body,
sheet,
&bcid,
&siblings,
Some(&bcid),
&mut substituted,
ctx,
)?;
let parser = Parser {};
let ast = parser.parse_with_substitutes(&body, sheet, ctx, &|kind| match kind {
logisheets_parser::PlaceholderKind::FieldRef(..) => subs.get(&kind.to_owned()).cloned(),
logisheets_parser::PlaceholderKind::Key => Some(key_node.clone()),
logisheets_parser::PlaceholderKind::Placeholder => None,
});
let Some(ast) = ast else { return Ok(executor) };
let mut refs = Vec::new();
collect_ref_ranges(&ast, &mut refs);
for (ref_sheet, range_id) in refs {
if ref_sheet != sheet || substituted.contains(&range_id) {
continue;
}
if let Some(Range::Block(br)) = ctx.lookup_range(ref_sheet, range_id) {
if br.block_id() == block_id {
return Err(BasicError::InvalidFormula(format!(
"field rule {:?} references a cell inside its own block \
(sheet={}, block={}). A field rule is a template applied \
verbatim to every row, so a coordinate in it does not \
shift per row — on the block's first row it resolves to \
the very cell being defined. Use `#FIELD(\"name\")` for a \
same-row sibling. A cross-row reference (a running total \
reading the row above) has no template form; write that \
column as ordinary cells instead.",
body, sheet, block_id
)));
}
}
}
register_parsed_ast(executor, sheet, CellId::BlockCell(bcid), ast, ctx)
}
pub fn input_block_cell_shadow_template<C: FormulaExecCtx>(
executor: FormulaExecutor,
sheet_idx: usize,
block_id: BlockId,
block_row: usize,
block_col: usize,
kind: crate::sid_assigner::ShadowKind,
ctx: &mut C,
) -> Result<FormulaExecutor, BasicError> {
use std::collections::HashMap;
let sheet = ctx
.fetch_sheet_id_by_index(sheet_idx)
.map_err(|l| BasicError::SheetIdxExceed(l))?;
let bcid = ctx.fetch_block_cell_id(&sheet, &block_id, block_row, block_col)?;
let template = ctx.block_cell_shadow_template(sheet, &bcid, kind);
let row_ctx = ctx.block_cell_row_substitutes(sheet, &bcid);
let shadow_eid = match &template {
Some(_) => Some(ctx.allocate_block_cell_shadow_id(sheet, &bcid, kind)),
None => ctx.find_block_cell_shadow_id(sheet, &bcid, kind),
};
let Some(eid) = shadow_eid else {
return Ok(executor);
};
let shadow_cell = CellId::EphemeralCell(eid);
let Some(template_str) = template else {
return remove(executor, sheet, shadow_cell, ctx);
};
let Some(row_ctx) = row_ctx else {
return Ok(executor);
};
let siblings: HashMap<String, BlockCellId> = row_ctx.siblings.into_iter().collect();
let key_node = ast::Node {
pure: ast::PureNode::Value(ast::Value::Text(row_ctx.key_value)),
bracket: false,
};
let placeholder_range = Range::Block(BlockRange::Single(bcid));
let placeholder_range_id = ctx.fetch_range_id(&sheet, &placeholder_range);
let placeholder_node = ast::Node {
pure: ast::PureNode::Reference(ast::CellReference::Mut(RangeDisplay {
range_id: placeholder_range_id,
ref_abs: RefAbs::default(),
sheet_id: sheet,
})),
bracket: true,
};
let body = template_str
.strip_prefix('=')
.map(|s| s.to_string())
.unwrap_or(template_str);
input_with_resolver(
executor,
sheet,
shadow_cell,
body,
placeholder_node,
bcid,
siblings,
key_node,
ctx,
)
}
pub fn add_ast_node(
manager: &mut FormulaManager,
sheet_id: SheetId,
cell_id: CellId,
range_id: RangeId,
ast: ast::Node,
) {
let this_vertex = Vertex::Range(sheet_id, range_id);
let mut new_formula_deps = HashSet::<Vertex>::new();
get_all_vertices_from_ast(&ast, &mut new_formula_deps);
manager.formulas.insert((sheet_id, cell_id), ast);
new_formula_deps
.into_iter()
.for_each(|new_dep| manager.graph.add_dep(this_vertex.clone(), new_dep));
if let CellId::BlockCell(bcid) = cell_id {
manager
.graph
.add_dep(Vertex::BlockAll(sheet_id, bcid.block_id), this_vertex);
}
}
pub fn rebuild_range_deps<C: FormulaExecCtx>(manager: &mut FormulaManager, ctx: &C) {
let mut deps = HashSet::<Vertex>::new();
for ast in manager.formulas.values() {
get_all_vertices_from_ast(ast, &mut deps);
}
for ast in manager.names.values() {
get_all_vertices_from_ast(ast, &mut deps);
}
for dep in deps {
for range_dep in ctx.get_range_deps(&dep) {
manager.graph.add_dep(dep.clone(), range_dep);
}
}
}
fn find_self_block_ref(
ast: &ast::Node,
self_sheet: SheetId,
self_block: BlockId,
) -> Option<&'static str> {
match &ast.pure {
ast::PureNode::Func(func) => func
.args
.iter()
.find_map(|n| find_self_block_ref(n, self_sheet, self_block)),
ast::PureNode::Value(_) => None,
ast::PureNode::ArrayConstant(_) => None,
ast::PureNode::Reference(_) => None,
ast::PureNode::BlockRef(node) => match node {
ast::BlockRefNode::Single {
sheet_id,
block_id,
key,
..
} => {
if *sheet_id == self_sheet && *block_id == self_block {
Some("BLOCKREF")
} else {
find_self_block_ref(key, self_sheet, self_block)
}
}
ast::BlockRefNode::Multi {
sheet_id,
block_id,
key_condition,
field_condition,
..
} => {
if *sheet_id == self_sheet && *block_id == self_block {
Some("BLOCKREFS")
} else {
find_self_block_ref(key_condition, self_sheet, self_block)
.or_else(|| find_self_block_ref(field_condition, self_sheet, self_block))
}
}
},
}
}
fn collect_ref_ranges(ast: &ast::Node, out: &mut Vec<(SheetId, RangeId)>) {
match &ast.pure {
ast::PureNode::Func(func) => func.args.iter().for_each(|n| collect_ref_ranges(n, out)),
ast::PureNode::Value(_) | ast::PureNode::ArrayConstant(_) => {}
ast::PureNode::Reference(reference) => match reference {
ast::CellReference::Mut(r) => out.push((r.sheet_id, r.range_id)),
ast::CellReference::UnMut(_)
| ast::CellReference::Ext(_)
| ast::CellReference::Name(_)
| ast::CellReference::RefErr => {}
},
ast::PureNode::BlockRef(node) => match node {
ast::BlockRefNode::Single { key, .. } => collect_ref_ranges(key, out),
ast::BlockRefNode::Multi {
key_condition,
field_condition,
..
} => {
collect_ref_ranges(key_condition, out);
collect_ref_ranges(field_condition, out);
}
},
}
}
pub(crate) fn get_all_vertices_from_ast(ast: &ast::Node, vertices: &mut HashSet<Vertex>) {
match &ast.pure {
ast::PureNode::Func(func) => {
func.args
.iter()
.for_each(|n| get_all_vertices_from_ast(n, vertices));
}
ast::PureNode::Value(_) | ast::PureNode::ArrayConstant(_) => {}
ast::PureNode::Reference(reference) => match reference {
ast::CellReference::Mut(r) => {
let sheet_id = r.sheet_id;
let range_id = r.range_id;
let vertex = Vertex::Range(sheet_id, range_id);
vertices.insert(vertex);
}
ast::CellReference::UnMut(c) => {
let cube_id = c.cube_id;
let vertex = Vertex::Cube(cube_id);
vertices.insert(vertex);
}
ast::CellReference::Ext(ext_ref) => {
let ext_ref_id = ext_ref.ext_ref_id;
let vertex = Vertex::Ext(ext_ref_id);
vertices.insert(vertex);
}
ast::CellReference::Name(name) => {
let vertex = Vertex::Name(*name);
vertices.insert(vertex);
}
ast::CellReference::RefErr => {}
},
ast::PureNode::BlockRef(node) => match node {
ast::BlockRefNode::Single {
sheet_id,
block_id,
field_id,
key,
..
} => {
vertices.insert(Vertex::Block(*sheet_id, *block_id, *field_id));
vertices.insert(Vertex::BlockKey(*sheet_id, *block_id));
vertices.insert(Vertex::BlockAll(*sheet_id, *block_id));
get_all_vertices_from_ast(key, vertices);
}
ast::BlockRefNode::Multi {
sheet_id,
block_id,
key_condition,
field_condition,
..
} => {
vertices.insert(Vertex::BlockAll(*sheet_id, *block_id));
vertices.insert(Vertex::BlockKey(*sheet_id, *block_id));
get_all_vertices_from_ast(key_condition, vertices);
get_all_vertices_from_ast(field_condition, vertices);
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_all_vertices_from_ast() {
let sum = ast::Node {
pure: ast::PureNode::Func(ast::Func {
op: ast::Operator::Infix(ast::InfixOperator::Plus),
args: vec![
ast::Node {
pure: ast::PureNode::Value(ast::Value::Number(1.0)),
bracket: false,
},
ast::Node {
pure: ast::PureNode::Reference(ast::CellReference::Mut(
ast::RangeDisplay {
sheet_id: 0,
range_id: 0,
ref_abs: RefAbs::from_addr(false, false),
},
)),
bracket: false,
},
],
}),
bracket: false,
};
let ast = ast::Node {
pure: ast::PureNode::Func(ast::Func {
op: ast::Operator::Infix(ast::InfixOperator::Multiply),
args: vec![
sum,
ast::Node {
pure: ast::PureNode::Reference(ast::CellReference::Mut(
ast::RangeDisplay {
sheet_id: 0,
range_id: 1,
ref_abs: RefAbs::from_addr(false, false),
},
)),
bracket: false,
},
],
}),
bracket: false,
};
let mut vertices = HashSet::<Vertex>::new();
get_all_vertices_from_ast(&ast, &mut vertices);
assert_eq!(vertices.len(), 2);
}
}