use crate::engine::{CycleConfig, CycleDetection, CyclePolicy, Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType, parse};
fn runtime_cfg() -> EvalConfig {
EvalConfig::default()
.with_cycle(CycleConfig {
detection: CycleDetection::Runtime,
policy: CyclePolicy::Error,
})
.with_virtual_dep_telemetry(true)
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
Rng((z ^ (z >> 31)) | 1)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: u32) -> u32 {
(self.next_u64() % n as u64) as u32
}
fn chance(&mut self, num: u32, den: u32) -> bool {
self.below(den) < num
}
}
#[derive(Clone, Debug)]
enum Cell {
Value(LiteralValue),
Formula(String),
}
struct Workbook {
seed: u64,
cells: Vec<Cell>, }
impl Workbook {
fn n(&self) -> usize {
self.cells.len()
}
fn a(i: usize) -> String {
format!("A{}", i + 1)
}
}
fn gen_workbook(seed: u64) -> Workbook {
let mut rng = Rng::new(seed);
let n = 10 + rng.below(31) as usize; let mut cells: Vec<Cell> = Vec::with_capacity(n);
let n_guards = 2 + rng.below(3) as usize; for _ in 0..n_guards {
if rng.chance(1, 2) {
cells.push(Cell::Value(LiteralValue::Boolean(rng.chance(1, 2))));
} else {
cells.push(Cell::Value(LiteralValue::Int(rng.below(3) as i64))); }
}
for _ in 0..2 {
cells.push(Cell::Value(LiteralValue::Int(1 + rng.below(9) as i64)));
}
while cells.len() < n {
let i = cells.len();
let f = gen_formula(&mut rng, i, n_guards, n);
cells.push(Cell::Formula(f));
}
Workbook { seed, cells }
}
fn guard_ref(rng: &mut Rng, n_guards: usize) -> String {
Workbook::a(rng.below(n_guards as u32) as usize)
}
fn earlier_ref(rng: &mut Rng, i: usize) -> String {
Workbook::a(rng.below(i as u32) as usize)
}
fn any_other_ref(rng: &mut Rng, i: usize, n: usize) -> String {
loop {
let j = rng.below(n as u32) as usize;
if j != i {
return Workbook::a(j);
}
}
}
fn gen_formula(rng: &mut Rng, i: usize, n_guards: usize, n: usize) -> String {
match rng.below(100) {
0..=34 => {
let g = guard_ref(rng, n_guards);
let lit = 1 + rng.below(50);
let other = any_other_ref(rng, i, n);
if rng.chance(1, 2) {
format!("=IF({g},{lit},{other})")
} else {
format!("=IF({g},{other},{lit})")
}
}
35..=54 => {
let g = guard_ref(rng, n_guards);
let lit = 1 + rng.below(50);
if i == 0 {
format!("={lit}")
} else {
let e = earlier_ref(rng, i);
format!("=IF({g},{e},{lit})")
}
}
55..=74 => {
if i == 0 {
format!("={}", 1 + rng.below(50))
} else {
let a = earlier_ref(rng, i);
let b = earlier_ref(rng, i);
let op = ["+", "-", "*"][rng.below(3) as usize];
format!("={a}{op}{b}")
}
}
75..=84 => {
let lit = 1 + rng.below(20);
let other = any_other_ref(rng, i, n);
if i == 0 {
format!("={lit}")
} else {
let e = earlier_ref(rng, i);
let cmp = [">", "<", "="][rng.below(3) as usize];
if rng.chance(1, 3) {
format!("=IF(NOT({e}{cmp}{lit}),{lit},{other})")
} else {
format!("=IF({e}{cmp}{lit},{lit},{other})")
}
}
}
85..=92 => {
if i < 3 {
format!("={}", 1 + rng.below(50))
} else {
let hi = rng.below(i as u32) as usize;
let lo = rng.below((hi + 1) as u32) as usize;
format!("=SUM({}:{})", Workbook::a(lo), Workbook::a(hi))
}
}
_ => {
let other = any_other_ref(rng, i, n);
let lit = 1 + rng.below(9);
let op = ["+", "*"][rng.below(2) as usize];
format!("={other}{op}{lit}")
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EKind {
Circ,
CircSettled,
Value,
}
impl EKind {
fn is_circ(self) -> bool {
matches!(self, EKind::Circ | EKind::CircSettled)
}
}
#[derive(Clone, Debug, PartialEq)]
enum OVal {
Num(f64),
Bool(bool),
Empty,
Err(EKind),
}
impl OVal {
fn is_err(&self) -> bool {
matches!(self, OVal::Err(_))
}
}
struct Oracle {
asts: Vec<Option<ASTNode>>, values: Vec<LiteralValue>, member: Vec<bool>,
memo: Vec<Option<OVal>>,
on_stack: Vec<bool>,
member_root: usize,
member_hit: bool,
n: usize,
}
impl Oracle {
fn new(wb: &Workbook) -> Self {
let n = wb.n();
let mut asts = Vec::with_capacity(n);
let mut values = Vec::with_capacity(n);
for c in &wb.cells {
match c {
Cell::Value(v) => {
asts.push(None);
values.push(v.clone());
}
Cell::Formula(f) => {
asts.push(Some(parse(f).expect("oracle parse")));
values.push(LiteralValue::Empty);
}
}
}
let mut o = Oracle {
asts,
values,
member: vec![false; n],
memo: vec![None; n],
on_stack: vec![false; n],
member_root: 0,
member_hit: false,
n,
};
o.compute_membership();
o
}
fn idx_of(&self, col: u32, row: u32) -> Option<usize> {
if col != 1 || row == 0 {
return None;
}
let i = (row - 1) as usize;
if i < self.n { Some(i) } else { None }
}
fn compute_membership(&mut self) {
for root in 0..self.n {
for s in &mut self.on_stack {
*s = false;
}
self.member_root = root;
self.member_hit = false;
self.member_walk(root);
if self.member_hit {
self.member[root] = true;
}
}
}
fn member_walk(&mut self, i: usize) {
if i == self.member_root && self.on_stack[i] {
self.member_hit = true;
return;
}
if self.on_stack[i] {
return; }
if self.member_hit {
return; }
let Some(ast) = self.asts[i].clone() else {
return; };
self.on_stack[i] = true;
self.walk_node(&ast);
self.on_stack[i] = false;
}
fn walk_node(&mut self, node: &ASTNode) {
if self.member_hit {
return;
}
match &node.node_type {
ASTNodeType::Literal(_) => {}
ASTNodeType::Reference { reference, .. } => self.walk_ref(reference),
ASTNodeType::UnaryOp { expr, .. } => self.walk_node(expr),
ASTNodeType::BinaryOp { left, right, .. } => {
self.walk_node(left);
self.walk_node(right);
}
ASTNodeType::Function { name, args } => match name.to_ascii_uppercase().as_str() {
"IF" => {
self.walk_node(&args[0]);
match self.live_branch(&args[0]) {
Some(true) => self.walk_node(&args[1]),
Some(false) => {
if args.len() > 2 {
self.walk_node(&args[2]);
}
}
None => {}
}
}
"NOT" => self.walk_node(&args[0]),
"SUM" => {
for arg in args {
self.walk_sum_arg(arg);
}
}
other => panic!("walk: unsupported function {other}"),
},
other => panic!("walk: unsupported node {other:?}"),
}
}
fn walk_sum_arg(&mut self, arg: &ASTNode) {
if let ASTNodeType::Reference {
reference: ReferenceType::Range {
start_row, end_row, ..
},
..
} = &arg.node_type
{
let (sr, er) = (start_row.unwrap(), end_row.unwrap());
for r in sr..=er {
if let Some(i) = self.idx_of(1, r) {
self.member_walk(i);
}
}
} else {
self.walk_node(arg);
}
}
fn walk_ref(&mut self, reference: &ReferenceType) {
if let ReferenceType::Cell { row, col, .. } = reference
&& let Some(i) = self.idx_of(*col, *row)
{
self.member_walk(i);
}
}
fn live_branch(&mut self, guard: &ASTNode) -> Option<bool> {
match self.eval_guard(guard) {
OVal::Bool(b) => Some(b),
OVal::Num(n) => Some(n != 0.0),
OVal::Empty => Some(false),
OVal::Err(_) => None,
}
}
fn eval_guard(&mut self, guard: &ASTNode) -> OVal {
let mut g = GuardEval {
asts: &self.asts,
values: &self.values,
on_stack: vec![false; self.n],
n: self.n,
};
g.eval_node(guard)
}
fn value_of(&mut self, i: usize) -> OVal {
for m in &mut self.memo {
*m = None;
}
self.eval_cell(i)
}
fn eval_cell(&mut self, i: usize) -> OVal {
if self.member[i] {
return OVal::Err(EKind::CircSettled);
}
if let Some(v) = &self.memo[i] {
return v.clone();
}
let result = match &self.asts[i] {
None => lit_to_oval(&self.values[i]),
Some(ast) => {
let ast = ast.clone();
self.eval_node(&ast)
}
};
self.memo[i] = Some(result.clone());
result
}
fn eval_node(&mut self, node: &ASTNode) -> OVal {
match &node.node_type {
ASTNodeType::Literal(v) => lit_to_oval(v),
ASTNodeType::Reference { reference, .. } => self.eval_ref(reference),
ASTNodeType::UnaryOp { op, expr } => {
let v = self.eval_node(expr);
if v.is_err() {
return v; }
match op.as_str() {
"-" => OVal::Num(-as_num(&v)),
"+" => OVal::Num(as_num(&v)),
other => panic!("oracle: unsupported unary op {other}"),
}
}
ASTNodeType::BinaryOp { op, left, right } => {
let l = self.eval_node(left);
if l.is_err() {
return l;
}
let r = self.eval_node(right);
if r.is_err() {
return r;
}
let (a, b) = (as_num(&l), as_num(&r));
match op.as_str() {
"+" => OVal::Num(a + b),
"-" => OVal::Num(a - b),
"*" => OVal::Num(a * b),
">" => OVal::Bool(a > b),
"<" => OVal::Bool(a < b),
"=" => OVal::Bool(a == b),
other => panic!("oracle: unsupported binary op {other}"),
}
}
ASTNodeType::Function { name, args } => {
let upper = name.to_ascii_uppercase();
match upper.as_str() {
"IF" => {
let cond = self.eval_node(&args[0]);
if let OVal::Err(e) = cond {
return condition_error(e);
}
if as_bool(&cond) {
self.eval_node(&args[1])
} else if args.len() > 2 {
self.eval_node(&args[2])
} else {
OVal::Bool(false) }
}
"NOT" => {
let v = self.eval_node(&args[0]);
if let OVal::Err(e) = v {
return condition_error(e);
}
OVal::Bool(!as_bool(&v))
}
"SUM" => {
let mut acc = 0.0;
for arg in args {
match self.sum_arg(arg) {
Ok(x) => acc += x,
Err(e) => return OVal::Err(e),
}
}
OVal::Num(acc)
}
other => panic!("oracle: unsupported function {other}"),
}
}
other => panic!("oracle: unsupported node {other:?}"),
}
}
fn sum_arg(&mut self, arg: &ASTNode) -> Result<f64, EKind> {
match &arg.node_type {
ASTNodeType::Reference { reference, .. } => match reference {
ReferenceType::Range {
start_row,
start_col,
end_row,
end_col,
..
} => {
let (sr, er) = (start_row.unwrap(), end_row.unwrap());
let col = start_col.unwrap_or(1);
debug_assert_eq!(col, end_col.unwrap_or(1));
let mut acc = 0.0;
for r in sr..=er {
if let Some(i) = self.idx_of(col, r) {
match self.eval_cell(i) {
OVal::Err(e) => return Err(e),
OVal::Num(x) => acc += x,
OVal::Bool(_) | OVal::Empty => {}
}
}
}
Ok(acc)
}
_ => match self.eval_ref(reference) {
OVal::Err(e) => Err(e),
v => Ok(as_num(&v)),
},
},
_ => match self.eval_node(arg) {
OVal::Err(e) => Err(e),
v => Ok(as_num(&v)),
},
}
}
fn eval_ref(&mut self, reference: &ReferenceType) -> OVal {
match reference {
ReferenceType::Cell { row, col, .. } => match self.idx_of(*col, *row) {
Some(i) => self.eval_cell(i),
None => OVal::Empty, },
other => panic!("oracle: unexpected scalar reference {other:?}"),
}
}
}
struct GuardEval<'a> {
asts: &'a [Option<ASTNode>],
values: &'a [LiteralValue],
on_stack: Vec<bool>,
n: usize,
}
impl GuardEval<'_> {
fn idx_of(&self, col: u32, row: u32) -> Option<usize> {
if col != 1 || row == 0 {
return None;
}
let i = (row - 1) as usize;
if i < self.n { Some(i) } else { None }
}
fn eval_cell(&mut self, i: usize) -> OVal {
if self.on_stack[i] {
return OVal::Err(EKind::Circ);
}
self.on_stack[i] = true;
let v = match &self.asts[i] {
None => lit_to_oval(&self.values[i]),
Some(ast) => {
let ast = ast.clone();
self.eval_node(&ast)
}
};
self.on_stack[i] = false;
v
}
fn eval_node(&mut self, node: &ASTNode) -> OVal {
match &node.node_type {
ASTNodeType::Literal(v) => lit_to_oval(v),
ASTNodeType::Reference { reference, .. } => match reference {
ReferenceType::Cell { row, col, .. } => match self.idx_of(*col, *row) {
Some(i) => self.eval_cell(i),
None => OVal::Empty,
},
_ => OVal::Empty, },
ASTNodeType::UnaryOp { op, expr } => {
let v = self.eval_node(expr);
if v.is_err() {
return v;
}
match op.as_str() {
"-" => OVal::Num(-as_num(&v)),
"+" => OVal::Num(as_num(&v)),
other => panic!("guard: unsupported unary op {other}"),
}
}
ASTNodeType::BinaryOp { op, left, right } => {
let l = self.eval_node(left);
if l.is_err() {
return l;
}
let r = self.eval_node(right);
if r.is_err() {
return r;
}
let (a, b) = (as_num(&l), as_num(&r));
match op.as_str() {
"+" => OVal::Num(a + b),
"-" => OVal::Num(a - b),
"*" => OVal::Num(a * b),
">" => OVal::Bool(a > b),
"<" => OVal::Bool(a < b),
"=" => OVal::Bool(a == b),
other => panic!("guard: unsupported binary op {other}"),
}
}
ASTNodeType::Function { name, args } => match name.to_ascii_uppercase().as_str() {
"IF" => {
let cond = self.eval_node(&args[0]);
if let OVal::Err(e) = cond {
return condition_error(e);
}
if as_bool(&cond) {
self.eval_node(&args[1])
} else if args.len() > 2 {
self.eval_node(&args[2])
} else {
OVal::Bool(false)
}
}
"NOT" => {
let v = self.eval_node(&args[0]);
if let OVal::Err(e) = v {
return condition_error(e);
}
OVal::Bool(!as_bool(&v))
}
"SUM" => {
let mut acc = 0.0;
for arg in args {
match self.sum_arg(arg) {
Ok(x) => acc += x,
Err(e) => return OVal::Err(e),
}
}
OVal::Num(acc)
}
other => panic!("guard: unsupported function {other}"),
},
other => panic!("guard: unsupported node {other:?}"),
}
}
fn sum_arg(&mut self, arg: &ASTNode) -> Result<f64, EKind> {
if let ASTNodeType::Reference {
reference: ReferenceType::Range {
start_row, end_row, ..
},
..
} = &arg.node_type
{
let (sr, er) = (start_row.unwrap(), end_row.unwrap());
let mut acc = 0.0;
for r in sr..=er {
if let Some(i) = self.idx_of(1, r) {
match self.eval_cell(i) {
OVal::Err(e) => return Err(e),
OVal::Num(x) => acc += x,
OVal::Bool(_) | OVal::Empty => {}
}
}
}
Ok(acc)
} else {
match self.eval_node(arg) {
OVal::Err(e) => Err(e),
v => Ok(as_num(&v)),
}
}
}
}
fn condition_error(e: EKind) -> OVal {
match e {
EKind::Circ => OVal::Err(EKind::Circ), EKind::CircSettled | EKind::Value => OVal::Err(EKind::Value),
}
}
fn lit_to_oval(v: &LiteralValue) -> OVal {
match v {
LiteralValue::Int(i) => OVal::Num(*i as f64),
LiteralValue::Number(n) => OVal::Num(*n),
LiteralValue::Boolean(b) => OVal::Bool(*b),
LiteralValue::Empty => OVal::Empty,
other => panic!("oracle: unexpected literal {other:?}"),
}
}
fn as_num(v: &OVal) -> f64 {
match v {
OVal::Num(n) => *n,
OVal::Bool(b) => {
if *b {
1.0
} else {
0.0
}
}
OVal::Empty => 0.0,
OVal::Err(_) => panic!("as_num on error value"),
}
}
fn as_bool(v: &OVal) -> bool {
match v {
OVal::Bool(b) => *b,
OVal::Num(n) => *n != 0.0,
OVal::Empty => false,
OVal::Err(_) => panic!("as_bool on error value"),
}
}
fn engine_to_oval(v: &Option<LiteralValue>) -> Result<OVal, String> {
match v {
None | Some(LiteralValue::Empty) => Ok(OVal::Empty),
Some(LiteralValue::Int(i)) => Ok(OVal::Num(*i as f64)),
Some(LiteralValue::Number(n)) => Ok(OVal::Num(*n)),
Some(LiteralValue::Boolean(b)) => Ok(OVal::Bool(*b)),
Some(LiteralValue::Error(e)) if e.kind == ExcelErrorKind::Circ => {
Ok(OVal::Err(EKind::Circ))
}
Some(LiteralValue::Error(e)) if e.kind == ExcelErrorKind::Value => {
Ok(OVal::Err(EKind::Value))
}
Some(other) => Err(format!("engine produced un-modelable value {other:?}")),
}
}
fn approx_eq(a: f64, b: f64) -> bool {
if a == b {
return true;
}
let diff = (a - b).abs();
let scale = a.abs().max(b.abs()).max(1.0);
diff <= 1e-9 * scale
}
fn ovals_match(oracle: &OVal, engine: &OVal) -> bool {
match (oracle, engine) {
(OVal::Err(a), OVal::Err(b)) if a.is_circ() && b.is_circ() => true,
(OVal::Err(a), OVal::Err(b)) => a == b,
(OVal::Num(a), OVal::Num(b)) => approx_eq(*a, *b),
(OVal::Bool(a), OVal::Bool(b)) => a == b,
(OVal::Bool(a), OVal::Num(b)) | (OVal::Num(b), OVal::Bool(a)) => {
approx_eq(if *a { 1.0 } else { 0.0 }, *b)
}
(OVal::Empty, OVal::Empty) => true,
(OVal::Empty, OVal::Num(n)) | (OVal::Num(n), OVal::Empty) => *n == 0.0,
_ => false,
}
}
fn build_engine(wb: &Workbook) -> Engine<TestWorkbook> {
let mut engine = Engine::new(TestWorkbook::new(), runtime_cfg());
for (i, cell) in wb.cells.iter().enumerate() {
let row = (i + 1) as u32;
match cell {
Cell::Value(v) => engine
.set_cell_value("Sheet1", row, 1, v.clone())
.expect("set value"),
Cell::Formula(f) => {
engine
.set_cell_formula("Sheet1", row, 1, parse(f).expect("parse"))
.unwrap_or_else(|e| {
panic!(
"seed {} A{row} formula {f:?} rejected at ingest: {e:?}\n{}",
wb.seed,
dump(wb)
)
});
}
}
}
engine
}
fn dump(wb: &Workbook) -> String {
let mut s = format!("── workbook dump (seed {}) ──\n", wb.seed);
for (i, c) in wb.cells.iter().enumerate() {
let body = match c {
Cell::Value(v) => format!("{v:?}"),
Cell::Formula(f) => f.clone(),
};
s.push_str(&format!(" A{} = {}\n", i + 1, body));
}
s
}
fn check_seed(seed: u64) -> Result<(), String> {
let wb = gen_workbook(seed);
let mut engine = build_engine(&wb);
engine
.evaluate_all()
.map_err(|e| format!("seed {seed}: evaluate_all errored: {e:?}\n{}", dump(&wb)))?;
let mut oracle = Oracle::new(&wb);
for i in 0..wb.n() {
let row = (i + 1) as u32;
let engine_raw = engine.get_cell_value("Sheet1", row, 1);
let engine_oval = engine_to_oval(&engine_raw).map_err(|why| {
format!(
"seed {seed}: A{row}: {why} (oracle subset only emits numbers/bools/#CIRC)\n{}",
dump(&wb)
)
})?;
let oracle_oval = oracle.value_of(i);
if !ovals_match(&oracle_oval, &engine_oval) {
return Err(format!(
"DISCREPANCY at seed {seed}, cell A{row}:\n \
oracle = {oracle_oval:?}\n engine = {engine_oval:?} (raw {engine_raw:?})\n{}",
dump(&wb)
));
}
}
Ok(())
}
#[test]
fn oracle_self_check_known_shapes() {
let wb = Workbook {
seed: 0,
cells: vec![
Cell::Value(LiteralValue::Boolean(true)), Cell::Formula("=IF(A1,555,A3)".into()), Cell::Formula("=IF(A1,A2,999)".into()), ],
};
let mut o = Oracle::new(&wb);
assert_eq!(o.value_of(1), OVal::Num(555.0));
assert_eq!(o.value_of(2), OVal::Num(555.0));
let wb = Workbook {
seed: 0,
cells: vec![
Cell::Value(LiteralValue::Boolean(false)),
Cell::Formula("=IF(A1,555,A3)".into()),
Cell::Formula("=IF(A1,A2,999)".into()),
],
};
let mut o = Oracle::new(&wb);
assert_eq!(o.value_of(1), OVal::Num(999.0));
assert_eq!(o.value_of(2), OVal::Num(999.0));
let wb = Workbook {
seed: 0,
cells: vec![Cell::Formula("=A2+1".into()), Cell::Formula("=A1+1".into())],
};
let mut o = Oracle::new(&wb);
assert!(matches!(o.value_of(0), OVal::Err(k) if k.is_circ()));
assert!(matches!(o.value_of(1), OVal::Err(k) if k.is_circ()));
assert!(o.member[0] && o.member[1], "both cells are cycle members");
let wb = Workbook {
seed: 0,
cells: vec![
Cell::Formula("=IF(A2>0,A2+1,5)".into()),
Cell::Formula("=A1".into()),
],
};
let mut o = Oracle::new(&wb);
assert!(
matches!(o.value_of(0), OVal::Err(k) if k.is_circ()),
"A1 #CIRC"
);
assert!(
matches!(o.value_of(1), OVal::Err(k) if k.is_circ()),
"A2 #CIRC"
);
let wb = Workbook {
seed: 0,
cells: vec![
Cell::Value(LiteralValue::Int(2)),
Cell::Value(LiteralValue::Int(3)),
Cell::Formula("=SUM(A1:A2)".into()),
],
};
let mut o = Oracle::new(&wb);
assert_eq!(o.value_of(2), OVal::Num(5.0));
let wb = Workbook {
seed: 0,
cells: vec![
Cell::Formula("=A2+1".into()),
Cell::Formula("=A1+1".into()),
Cell::Formula("=IF(A1>0,7,8)".into()),
],
};
let mut o = Oracle::new(&wb);
assert!(!o.member[2], "A3 is a downstream reader, not a member");
assert_eq!(o.value_of(2), OVal::Err(EKind::Value));
}
#[test]
fn harness_self_check_engine_known_shapes() {
let wb = Workbook {
seed: 0,
cells: vec![
Cell::Value(LiteralValue::Boolean(true)),
Cell::Formula("=IF(A1,555,A3)".into()),
Cell::Formula("=IF(A1,A2,999)".into()),
],
};
let mut engine = build_engine(&wb);
engine.evaluate_all().unwrap();
assert_eq!(
engine_to_oval(&engine.get_cell_value("Sheet1", 2, 1)).unwrap(),
OVal::Num(555.0)
);
let wb = Workbook {
seed: 0,
cells: vec![Cell::Formula("=A2+1".into()), Cell::Formula("=A1+1".into())],
};
let mut engine = build_engine(&wb);
engine.evaluate_all().unwrap();
assert_eq!(
engine_to_oval(&engine.get_cell_value("Sheet1", 1, 1)).unwrap(),
OVal::Err(EKind::Circ)
);
}
#[test]
#[ignore]
fn debug_classify_all() {
let mut classes: std::collections::BTreeMap<String, usize> = Default::default();
for seed in 1..=500u64 {
let wb = gen_workbook(seed);
let mut engine = build_engine(&wb);
engine.evaluate_all().unwrap();
let mut oracle = Oracle::new(&wb);
for i in 0..wb.n() {
let row = (i + 1) as u32;
let raw = engine.get_cell_value("Sheet1", row, 1);
let o = oracle.value_of(i);
let e = engine_to_oval(&raw);
let mismatch = match &e {
Ok(ev) => !ovals_match(&o, ev),
Err(_) => true,
};
if mismatch {
let key = match (&o, &raw) {
(OVal::Err(k), Some(LiteralValue::Error(er))) if k.is_circ() => {
format!("oracle=Circ engine=Error({:?})", er.kind)
}
(_, Some(LiteralValue::Error(er))) => {
format!("oracle=value engine=Error({:?})", er.kind)
}
_ => format!("oracle={o:?} engine={raw:?}"),
};
*classes.entry(key).or_default() += 1;
}
}
}
println!("── divergence classes ──");
for (k, c) in &classes {
println!(" {c:5} {k}");
}
}
#[test]
#[ignore]
fn debug_dump_seed() {
let seed: u64 = std::env::var("SEED")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(23);
let wb = gen_workbook(seed);
let mut engine = build_engine(&wb);
let res = engine.evaluate_all().unwrap();
let mut oracle = Oracle::new(&wb);
println!("{}", dump(&wb));
println!("cycle_errors={}", res.cycle_errors);
for i in 0..wb.n() {
let row = (i + 1) as u32;
let e = engine.get_cell_value("Sheet1", row, 1);
let o = oracle.value_of(i);
println!(
"A{row}: engine={e:?} oracle={o:?} member={}",
oracle.member[i]
);
}
}
#[test]
fn random_guarded_workbooks_match_reference_oracle() {
let mut first_failure: Option<String> = None;
let mut failures = 0usize;
let n_seeds: u64 = 500;
for seed in 1..=n_seeds {
if let Err(msg) = check_seed(seed) {
failures += 1;
if first_failure.is_none() {
first_failure = Some(msg);
}
}
}
if let Some(msg) = first_failure {
panic!("{failures}/{n_seeds} seeds diverged. First failure:\n\n{msg}");
}
}
#[test]
fn corpus_has_both_value_and_circ_workbooks() {
let mut with_circ = 0usize;
let mut all_values = 0usize;
let mut total_cells = 0usize;
let mut circ_cells = 0usize;
for seed in 1..=200u64 {
let wb = gen_workbook(seed);
let oracle = Oracle::new(&wb);
let mut any_circ = false;
for i in 0..wb.n() {
total_cells += 1;
if oracle.member[i] {
any_circ = true;
circ_cells += 1;
}
}
if any_circ {
with_circ += 1;
} else {
all_values += 1;
}
}
println!(
"corpus: 200 seeds, {total_cells} cells; {with_circ} workbooks contain #CIRC, \
{all_values} are all-values; {circ_cells} #CIRC cells total"
);
assert!(
with_circ > 0,
"no #CIRC workbooks generated — corpus too tame"
);
assert!(
all_values > 0,
"every workbook had a #CIRC — corpus too cyclic"
);
}