//! Isolated context experiment. This is deliberately NOT Studio syntax or production code. use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use std::path::Path; type Bindings = BTreeMap; type Functions = BTreeMap; #[derive(Clone, Debug, PartialEq, Eq)] enum Expr { Int(i64), Missing, Ref(String), Sum(Vec), Le(Box, Box), Call(String, Vec), } #[derive(Clone, Debug, PartialEq, Eq)] struct Function { parameters: Vec, body: Expr, } #[derive(Clone, Debug, PartialEq, Eq)] struct Baseline { name: String, revision: u64, bindings: Bindings, } #[derive(Clone, Debug, PartialEq, Eq)] struct Case { name: String, base: Option, bindings: Bindings, } #[derive(Clone, Debug, PartialEq, Eq)] struct Study { mode: char, revision: u64, properties: Vec, functions: Functions, baseline: Option, cases: Vec, origin: Option, captured_origins: BTreeMap, } #[derive(Clone, Debug, PartialEq, Eq)] enum Value { Int(i64), Bool(bool), Missing(BTreeSet), } #[derive(Clone, Debug, PartialEq, Eq)] struct Observation { value: Value, origins: BTreeMap, document_revision: u64, } fn split_args(s: &str) -> Result, String> { if s.trim().is_empty() { return Ok(vec![]); } let mut depth = 0_i32; let mut start = 0; let mut out = vec![]; for (i, ch) in s.char_indices() { match ch { '(' => depth += 1, ')' => { depth -= 1; if depth < 0 { return Err("unbalanced expression".into()); } } ',' if depth == 0 => { out.push(s[start..i].trim()); start = i + 1; } _ => {} } } if depth != 0 { return Err("unbalanced expression".into()); } out.push(s[start..].trim()); Ok(out) } fn expr(s: &str) -> Result { let s = s.trim(); if s == "missing" { return Ok(Expr::Missing); } if let Ok(n) = s.parse() { return Ok(Expr::Int(n)); } if let Some(at) = s.find('(') { if !s.ends_with(')') { return Err("missing closing parenthesis".into()); } let args = split_args(&s[at + 1..s.len() - 1])? .into_iter() .map(expr) .collect::, _>>()?; return match &s[..at] { "sum" => Ok(Expr::Sum(args)), "le" if args.len() == 2 => Ok(Expr::Le( Box::new(args[0].clone()), Box::new(args[1].clone()), )), "le" => Err("le requires two arguments".into()), name => Ok(Expr::Call(name.into(), args)), }; } if s.is_empty() || !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { return Err(format!("invalid symbol {s}")); } Ok(Expr::Ref(s.into())) } fn spelling(e: &Expr) -> String { let list = |xs: &[Expr]| xs.iter().map(spelling).collect::>().join(", "); match e { Expr::Int(v) => v.to_string(), Expr::Missing => "missing".into(), Expr::Ref(s) => s.clone(), Expr::Sum(xs) => format!("sum({})", list(xs)), Expr::Le(a, b) => format!("le({}, {})", spelling(a), spelling(b)), Expr::Call(name, xs) => format!("{name}({})", list(xs)), } } fn parse(text: &str) -> Result { let mut lines = text.lines().filter(|l| !l.trim().is_empty()); let head = lines .next() .ok_or("empty study")? .split_whitespace() .collect::>(); if head.len() != 4 || head[0] != "study" || head[2] != "revision" || !matches!(head[1], "B" | "D") { return Err("invalid study header".into()); } let mut study = Study { mode: head[1].chars().next().unwrap(), revision: head[3].parse().map_err(|_| "bad revision")?, properties: vec![], functions: BTreeMap::new(), baseline: None, cases: vec![], origin: None, captured_origins: BTreeMap::new(), }; let mut current: Option<(bool, Case, u64)> = None; for line in lines { let line = line.trim(); if line == "end" { let (base, item, revision) = current.take().ok_or("unmatched end")?; if base { if study.baseline.is_some() { return Err("multiple baselines forbidden".into()); } study.baseline = Some(Baseline { name: item.name, revision, bindings: item.bindings, }); } else { if study.cases.iter().any(|c| c.name == item.name) { return Err("duplicate case".into()); } study.cases.push(item); } } else if let Some((_, case, _)) = current.as_mut() { let (name, value) = line.split_once(" = ").ok_or("expected binding")?; if case.bindings.insert(name.into(), expr(value)?).is_some() { return Err("duplicate binding".into()); } } else if let Some(properties) = line.strip_prefix("properties ") { if !study.properties.is_empty() { return Err("duplicate properties".into()); } study.properties = properties.split(", ").map(String::from).collect(); } else if let Some(origin) = line.strip_prefix("origin ") { study.origin = Some(origin.into()); } else if let Some(captured) = line.strip_prefix("captured ") { let (property, origin) = captured .split_once(" = ") .ok_or("invalid captured origin")?; if study .captured_origins .insert(property.into(), origin.into()) .is_some() { return Err("duplicate captured origin".into()); } } else if let Some(def) = line.strip_prefix("function ") { let (left, right) = def.split_once(" = ").ok_or("invalid function")?; let at = left.find('(').ok_or("function parameters required")?; if !left.ends_with(')') { return Err("bad parameters".into()); } let f = Function { parameters: split_args(&left[at + 1..left.len() - 1])? .into_iter() .map(String::from) .collect(), body: expr(right)?, }; if study.functions.insert(left[..at].into(), f).is_some() { return Err("duplicate function".into()); } } else { let words = line.split_whitespace().collect::>(); let (base, name, parent, revision) = match words.as_slice() { ["baseline", name, "revision", revision] => ( true, *name, None, revision.parse().map_err(|_| "bad baseline revision")?, ), ["case", name, "base", parent] => (false, *name, Some((*parent).to_owned()), 0), ["case", name, "independent"] => (false, *name, None, 0), _ => return Err(format!("invalid declaration: {line}")), }; current = Some(( base, Case { name: name.into(), base: parent, bindings: BTreeMap::new(), }, revision, )); } } if current.is_some() { return Err("unterminated block".into()); } validate(&study)?; Ok(study) } fn render(s: &Study) -> String { let mut text = format!("study {} revision {}\n", s.mode, s.revision); if let Some(origin) = &s.origin { writeln!(text, "origin {origin}").unwrap(); } for (property, origin) in &s.captured_origins { writeln!(text, "captured {property} = {origin}").unwrap(); } writeln!(text, "properties {}", s.properties.join(", ")).unwrap(); for (name, f) in &s.functions { writeln!( text, "function {name}({}) = {}", f.parameters.join(", "), spelling(&f.body) ) .unwrap(); } let write_bindings = |text: &mut String, bindings: &Bindings| { for (p, e) in bindings { writeln!(text, " {p} = {}", spelling(e)).unwrap(); } text.push_str("end\n"); }; if let Some(b) = &s.baseline { writeln!(text, "baseline {} revision {}", b.name, b.revision).unwrap(); write_bindings(&mut text, &b.bindings); } for c in &s.cases { writeln!( text, "case {} {}", c.name, c.base .as_ref() .map_or("independent".into(), |b| format!("base {b}")) ) .unwrap(); write_bindings(&mut text, &c.bindings); } text } fn refs(e: &Expr, names: &mut BTreeSet, calls: &mut BTreeSet) { match e { Expr::Ref(p) => { names.insert(p.clone()); } Expr::Sum(xs) | Expr::Call(_, xs) => { if let Expr::Call(f, _) = e { calls.insert(f.clone()); } for x in xs { refs(x, names, calls); } } Expr::Le(a, b) => { refs(a, names, calls); refs(b, names, calls); } _ => {} } } fn acyclic(graph: &BTreeMap>) -> Result<(), String> { fn visit( n: &str, graph: &BTreeMap>, path: &mut Vec, done: &mut BTreeSet, ) -> Result<(), String> { if path.iter().any(|p| p == n) { path.push(n.into()); return Err(format!("cycle: {}", path.join(" -> "))); } if done.contains(n) { return Ok(()); } path.push(n.into()); for next in graph .get(n) .ok_or_else(|| format!("unknown dependency {n}"))? { visit(next, graph, path, done)?; } path.pop(); done.insert(n.into()); Ok(()) } let mut done = BTreeSet::new(); for n in graph.keys() { visit(n, graph, &mut vec![], &mut done)?; } Ok(()) } fn resolved<'a>(s: &'a Study, c: &'a Case, property: &str) -> Result<(&'a Expr, String), String> { if let Some(e) = c.bindings.get(property) { return Ok((e, format!("case:{}", c.name))); } if let Some(base) = &c.base { let b = s .baseline .as_ref() .filter(|b| &b.name == base) .ok_or("unknown baseline")?; if let Some(e) = b.bindings.get(property) { return Ok(( e, format!("baseline:{}@{} via {}", b.name, b.revision, c.name), )); } } Err(format!("missing binding row: {}.{property}", c.name)) } fn validate(s: &Study) -> Result<(), String> { let properties = s.properties.iter().cloned().collect::>(); if properties.len() != s.properties.len() || properties.is_empty() { return Err("invalid property catalog".into()); } if (s.mode == 'B') != s.baseline.is_some() { return Err("B requires one explicit baseline; D forbids baselines".into()); } let mut function_graph = BTreeMap::new(); for (name, f) in &s.functions { let mut names = BTreeSet::new(); let mut calls = BTreeSet::new(); refs(&f.body, &mut names, &mut calls); let parameters = f.parameters.iter().cloned().collect::>(); if parameters.len() != f.parameters.len() || !names.is_subset(¶meters) { return Err(format!("function {name} captures unknown/project property")); } function_graph.insert(name.clone(), calls); } acyclic(&function_graph)?; let mut contexts = s.cases.clone(); if let Some(b) = &s.baseline { contexts.push(Case { name: format!("baseline_{}", b.name), base: None, bindings: b.bindings.clone(), }); } for c in &contexts { if !c.bindings.keys().all(|p| properties.contains(p)) { return Err("unknown binding property".into()); } if s.mode == 'D' && c.base.is_some() { return Err("D case cannot inherit".into()); } if let Some(name) = &c.base { if s.baseline.as_ref().is_none_or(|b| &b.name != name) { return Err("unknown baseline; case-to-case inheritance forbidden".into()); } } let mut graph = BTreeMap::new(); for p in &properties { let (e, _) = resolved(s, c, p)?; let mut names = BTreeSet::new(); let mut calls = BTreeSet::new(); refs(e, &mut names, &mut calls); if !names.is_subset(&properties) || !calls.iter().all(|f| s.functions.contains_key(f)) { return Err("unknown reference".into()); } graph.insert(p.clone(), names); } acyclic(&graph).map_err(|e| format!("{}: {e}", c.name))?; for p in &properties { evaluate(s, c, p)?; } } Ok(()) } fn evaluate(s: &Study, c: &Case, property: &str) -> Result { let mut origins = BTreeMap::new(); let mut cache = BTreeMap::new(); fn property_value( s: &Study, c: &Case, p: &str, origins: &mut BTreeMap, cache: &mut BTreeMap, ) -> Result { if let Some(v) = cache.get(p) { return Ok(v.clone()); } let (e, origin) = resolved(s, c, p)?; origins.insert( p.into(), s.captured_origins .get(p) .map_or(origin, |old| format!("frozen:{old}")), ); let value = run(s, c, e, &BTreeMap::new(), Some(p), origins, cache)?; if !matches!(value, Value::Missing(_)) && (p == "budget") != matches!(value, Value::Bool(_)) { return Err(format!("wrong type for {p}")); } cache.insert(p.into(), value.clone()); Ok(value) } fn run( s: &Study, c: &Case, e: &Expr, params: &BTreeMap, missing_property: Option<&str>, origins: &mut BTreeMap, cache: &mut BTreeMap, ) -> Result { match e { Expr::Int(n) => Ok(Value::Int(*n)), Expr::Missing => Ok(Value::Missing(BTreeSet::from([missing_property .unwrap_or("function-body") .into()]))), Expr::Ref(p) => params .get(p) .cloned() .map_or_else(|| property_value(s, c, p, origins, cache), Ok), Expr::Call(name, args) => { let f = s.functions.get(name).ok_or("unknown function")?; if f.parameters.len() != args.len() { return Err("wrong function arity".into()); } let mut bound = BTreeMap::new(); for (parameter, arg) in f.parameters.iter().zip(args) { bound.insert( parameter.clone(), run(s, c, arg, params, missing_property, origins, cache)?, ); } run(s, c, &f.body, &bound, missing_property, origins, cache) } Expr::Sum(xs) => { let mut total = 0_i64; let mut missing = BTreeSet::new(); for x in xs { match run(s, c, x, params, missing_property, origins, cache)? { Value::Int(n) => total = total.checked_add(n).ok_or("integer overflow")?, Value::Missing(m) => missing.extend(m), Value::Bool(_) => return Err("sum requires numeric inputs".into()), } } Ok(if missing.is_empty() { Value::Int(total) } else { Value::Missing(missing) }) } Expr::Le(a, b) => { let a = run(s, c, a, params, missing_property, origins, cache)?; let b = run(s, c, b, params, missing_property, origins, cache)?; match (a, b) { (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)), (Value::Missing(mut a), Value::Missing(b)) => { a.extend(b); Ok(Value::Missing(a)) } (Value::Missing(a), Value::Int(_)) | (Value::Int(_), Value::Missing(a)) => { Ok(Value::Missing(a)) } _ => Err("le requires numeric inputs".into()), } } } } let value = property_value(s, c, property, &mut origins, &mut cache)?; Ok(Observation { value, origins, document_revision: s.revision, }) } fn fixture(mode: char, factored: bool) -> Study { let inputs = (1..=20) .map(|i| format!("fixed{i:02}")) .chain(["varying01".into(), "varying02".into()]) .collect::>(); let mut all = inputs.clone(); all.extend(["total".into(), "budget".into()]); let mut bindings = (1..=20) .map(|i| (format!("fixed{i:02}"), Expr::Int(1000))) .collect::(); bindings.insert("varying01".into(), Expr::Int(750)); bindings.insert("varying02".into(), Expr::Int(1000)); let total = Expr::Sum(inputs.iter().cloned().map(Expr::Ref).collect()); let check = Expr::Le( Box::new(Expr::Ref("total".into())), Box::new(Expr::Int(22000)), ); let mut functions = BTreeMap::new(); if factored { functions.insert( "total_formula".into(), Function { parameters: inputs.clone(), body: total, }, ); functions.insert( "budget_formula".into(), Function { parameters: vec!["result".into()], body: Expr::Le( Box::new(Expr::Ref("result".into())), Box::new(Expr::Int(22000)), ), }, ); bindings.insert( "total".into(), Expr::Call( "total_formula".into(), inputs.into_iter().map(Expr::Ref).collect(), ), ); bindings.insert( "budget".into(), Expr::Call("budget_formula".into(), vec![Expr::Ref("total".into())]), ); } else { bindings.insert("total".into(), total); bindings.insert("budget".into(), check); } let cases = (0..20) .map(|i| { let mut rows = if mode == 'D' { bindings.clone() } else { BTreeMap::new() }; if mode == 'D' || i != 0 { rows.insert("varying01".into(), Expr::Int(750 + 50 * i)); rows.insert("varying02".into(), Expr::Int(1000 + 25 * i)); } Case { name: format!("case{:02}", i + 1), base: (mode == 'B').then(|| "engineering".into()), bindings: rows, } }) .collect(); Study { mode, revision: 1, properties: all, functions, baseline: (mode == 'B').then(|| Baseline { name: "engineering".into(), revision: 1, bindings, }), cases, origin: None, captured_origins: BTreeMap::new(), } } fn freeze(s: &Study, case: usize) -> Study { let c = &s.cases[case]; Study { mode: 'D', revision: 1, properties: s.properties.clone(), functions: s.functions.clone(), baseline: None, cases: vec![Case { name: c.name.clone(), base: None, bindings: s .properties .iter() .map(|p| (p.clone(), resolved(s, c, p).unwrap().0.clone())) .collect(), }], origin: Some(format!( "{}:{}:document{}{}", s.mode, c.name, s.revision, s.baseline .as_ref() .map_or(String::new(), |b| format!(":{}@{}", b.name, b.revision)) )), captured_origins: s .properties .iter() .map(|p| (p.clone(), resolved(s, c, p).unwrap().1)) .collect(), } } fn write_fixture(dir: &Path, name: &str, s: &Study) -> Result { let source = render(s); let back = parse(&source)?; assert_eq!(&back, s, "exact experiment AST round trip"); std::fs::write(dir.join(format!("{name}.study")), &source).map_err(|e| e.to_string())?; Ok(source) } fn diff(before: &str, after: &str) -> (usize, usize) { let a = before.lines().collect::>(); let b = after.lines().collect::>(); let mut row = vec![0; b.len() + 1]; for x in &a { let mut diagonal = 0; for (j, y) in b.iter().enumerate() { let old = row[j + 1]; row[j + 1] = if x == y { diagonal + 1 } else { row[j + 1].max(row[j]) }; diagonal = old; } } (a.len() - row[b.len()], b.len() - row[b.len()]) } fn assert_total(s: &Study, index: usize, expected: i64) { assert_eq!( evaluate(s, &s.cases[index], "total").unwrap().value, Value::Int(expected) ); } fn change_fixed(s: &mut Study, all: bool) { s.revision += 1; if let Some(b) = s.baseline.as_mut() { b.revision += 1; b.bindings.insert("fixed01".into(), Expr::Int(1200)); } else { for c in s.cases.iter_mut().take(if all { 20 } else { 1 }) { c.bindings.insert("fixed01".into(), Expr::Int(1200)); } } } fn change_threshold(s: &mut Study, value: i64) { s.revision += 1; if let Some(f) = s.functions.get_mut("budget_formula") { f.body = Expr::Le( Box::new(Expr::Ref("result".into())), Box::new(Expr::Int(value)), ); } else { let e = Expr::Le( Box::new(Expr::Ref("total".into())), Box::new(Expr::Int(value)), ); if let Some(b) = s.baseline.as_mut() { b.revision += 1; b.bindings.insert("budget".into(), e); } else { for c in &mut s.cases { c.bindings.insert("budget".into(), e.clone()); } } } } fn pass_count(s: &Study) -> usize { s.cases .iter() .filter(|c| evaluate(s, c, "budget").unwrap().value == Value::Bool(true)) .count() } fn main() -> Result<(), String> { let out = std::env::args() .nth(1) .ok_or("usage: analysis-context.exe OUTPUT_DIRECTORY")?; let dir = Path::new(&out); std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; let mut report = String::from( "# Executed context experiment\n\nAll input magnitudes are integer milliseconds. This tiny dialect is not Studio source.\n\n| Variant | Initial lines | Initial bytes | Binding rows | Fixed-all edit removed/added lines | Threshold edit removed/added lines | Initial passing cases | After fixed-all change |\n| --- | ---: | ---: | ---: | --- | --- | ---: | ---: |\n", ); let mut checks = 0; for mode in ['B', 'D'] { for factored in [false, true] { let name = format!("{mode}-{}", if factored { "factored" } else { "inline" }); let original = fixture(mode, factored); validate(&original)?; for i in 0..20 { assert_total(&original, i, 21750 + 75 * i as i64); checks += 1; } assert_eq!(pass_count(&original), 4); checks += 1; let initial = write_fixture(dir, &name, &original)?; let mut changed = original.clone(); change_fixed(&mut changed, true); validate(&changed)?; for i in 0..20 { assert_total(&changed, i, 21950 + 75 * i as i64); checks += 1; } assert_eq!(pass_count(&changed), 1); checks += 1; let fixed_source = write_fixture(dir, &format!("{name}-fixed-all"), &changed)?; let mut threshold = original.clone(); change_threshold(&mut threshold, 22500); validate(&threshold)?; assert_eq!(pass_count(&threshold), 11); checks += 1; let threshold_source = write_fixture(dir, &format!("{name}-threshold"), &threshold)?; let frozen = freeze(&original, 19); let frozen_before = evaluate(&frozen, &frozen.cases[0], "budget")?; assert_total(&frozen, 0, 23175); checks += 1; change_threshold(&mut changed, 25000); assert_eq!( evaluate(&frozen, &frozen.cases[0], "budget")?, frozen_before ); checks += 1; write_fixture(dir, &format!("{name}-frozen-case20"), &frozen)?; // A shallow case copy still follows its baseline and/or shared function environment. let shallow = original.cases[19].clone(); if mode == 'B' { assert_eq!( evaluate(&changed, &shallow, "total")?.value, Value::Int(23375) ); checks += 1; } if factored { assert_eq!( evaluate(&changed, &shallow, "budget")?.value, Value::Bool(true) ); checks += 1; } if mode == 'D' && !factored { assert_eq!( evaluate(&changed, &shallow, "total")?.value, Value::Int(23175) ); assert_eq!( evaluate(&changed, &shallow, "budget")?.value, Value::Bool(false) ); checks += 2; } assert_eq!(frozen.captured_origins.len(), 24); assert!( evaluate(&frozen, &frozen.cases[0], "total")? .origins .values() .all(|origin| origin.starts_with("frozen:")) ); checks += 2; let mut missing = original.clone(); missing.cases[19] .bindings .insert("varying01".into(), Expr::Missing); validate(&missing)?; assert_eq!( evaluate(&missing, &missing.cases[19], "budget")?.value, Value::Missing(BTreeSet::from(["varying01".into()])) ); checks += 1; write_fixture(dir, &format!("{name}-missing"), &missing)?; let frozen_missing = freeze(&missing, 19); assert_eq!( evaluate(&frozen_missing, &frozen_missing.cases[0], "budget")?.value, Value::Missing(BTreeSet::from(["varying01".into()])) ); checks += 1; missing.cases[19].bindings.remove("varying01"); if mode == 'B' { validate(&missing)?; assert_total(&missing, 19, 22225); checks += 1; write_fixture(dir, &format!("{name}-cleared-override"), &missing)?; } else { assert!( validate(&missing) .unwrap_err() .contains("missing binding row") ); checks += 1; std::fs::write( dir.join(format!("{name}-deleted-row.rejected-study")), render(&missing), ) .map_err(|e| e.to_string())?; } let mut cyclic = original.clone(); cyclic.cases[19] .bindings .insert("varying01".into(), Expr::Ref("total".into())); let cycle = validate(&cyclic).unwrap_err(); assert!(cycle.contains("cycle")); checks += 1; std::fs::write( dir.join(format!("{name}-cycle.rejected-study")), render(&cyclic), ) .map_err(|e| e.to_string())?; let observation = evaluate(&original, &original.cases[19], "budget")?; let mut detail = format!( "# {name}: case20 evaluation\n\nValue: {:?}\nDocument revision: {}\n\n", observation.value, observation.document_revision ); for (p, origin) in &observation.origins { writeln!(detail, "- {p}: {origin}").unwrap(); } writeln!(detail, "\nRejected cycle: {cycle}").unwrap(); std::fs::write(dir.join(format!("{name}-provenance.md")), detail) .map_err(|e| e.to_string())?; let rows = original.baseline.as_ref().map_or(0, |b| b.bindings.len()) + original .cases .iter() .map(|c| c.bindings.len()) .sum::(); let (a, b) = diff(&initial, &fixed_source); let (c, d) = diff(&initial, &threshold_source); writeln!( report, "| {name} | {} | {} | {rows} | {a}/{b} | {c}/{d} | 4 | 1 |", initial.lines().count(), initial.len() ) .unwrap(); } } // Negative controls establish that source locality and isolation differ. let mut d = fixture('D', false); change_fixed(&mut d, false); validate(&d)?; assert_total(&d, 0, 21950); assert_total(&d, 19, 23175); checks += 2; write_fixture(dir, "D-inline-fixed-one-only", &d)?; let b = fixture('B', false); let baseline_case = Case { name: "wrong_baseline_context".into(), base: None, bindings: b.baseline.as_ref().unwrap().bindings.clone(), }; assert_eq!( evaluate(&b, &baseline_case, "total")?.value, Value::Int(21750) ); assert_total(&b, 19, 23175); checks += 2; let mut explicit = fixture('B', false); explicit .baseline .as_mut() .unwrap() .bindings .insert("varying01".into(), Expr::Missing); explicit.cases[19].bindings.remove("varying01"); validate(&explicit)?; assert!(matches!( evaluate(&explicit, &explicit.cases[19], "total")?.value, Value::Missing(_) )); checks += 1; let mut chained = fixture('B', false); chained.cases[1].base = Some("case01".into()); assert!(validate(&chained).unwrap_err().contains("case-to-case")); checks += 1; let mut capture = fixture('D', true); capture.functions.get_mut("budget_formula").unwrap().body = Expr::Ref("fixed01".into()); assert!(validate(&capture).unwrap_err().contains("captures")); checks += 1; let mut recursive = fixture('D', true); recursive.functions.get_mut("budget_formula").unwrap().body = Expr::Call("budget_formula".into(), vec![Expr::Ref("result".into())]); assert!(validate(&recursive).unwrap_err().contains("cycle")); checks += 1; writeln!(report,"\nExecuted assertions: {checks}. All passed. Every accepted fixture was parsed, validated, rendered and parsed back with exact experiment AST equality. Rejected cases were checked separately.\n").unwrap(); report.push_str("Negative controls: D changing one repeated fixed input changes only that case; B evaluating inherited formulas against baseline inputs instead of effective case inputs gives 21,750 instead of case20's 23,175. A shallow B case copy is not frozen; factored D also needs its definition environment snapshotted. Clearing a B override restores its named baseline value, including an explicitly missing baseline. A deleted D binding row rejects. Baseline chains, recursive definitions and project captures inside pure functions reject.\n"); std::fs::write(dir.join("RESULTS.md"), &report).map_err(|e| e.to_string())?; println!("{report}"); Ok(()) }