Files
ARCADIA-Studio/harness/experiments/analysis-context-20260911/main.rs
T

914 lines
32 KiB
Rust

//! 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<String, Expr>;
type Functions = BTreeMap<String, Function>;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Expr {
Int(i64),
Missing,
Ref(String),
Sum(Vec<Expr>),
Le(Box<Expr>, Box<Expr>),
Call(String, Vec<Expr>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Function {
parameters: Vec<String>,
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<String>,
bindings: Bindings,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Study {
mode: char,
revision: u64,
properties: Vec<String>,
functions: Functions,
baseline: Option<Baseline>,
cases: Vec<Case>,
origin: Option<String>,
captured_origins: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Value {
Int(i64),
Bool(bool),
Missing(BTreeSet<String>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Observation {
value: Value,
origins: BTreeMap<String, String>,
document_revision: u64,
}
fn split_args(s: &str) -> Result<Vec<&str>, 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<Expr, String> {
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::<Result<Vec<_>, _>>()?;
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::<Vec<_>>().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<Study, String> {
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
let head = lines
.next()
.ok_or("empty study")?
.split_whitespace()
.collect::<Vec<_>>();
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::<Vec<_>>();
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<String>, calls: &mut BTreeSet<String>) {
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<String, BTreeSet<String>>) -> Result<(), String> {
fn visit(
n: &str,
graph: &BTreeMap<String, BTreeSet<String>>,
path: &mut Vec<String>,
done: &mut BTreeSet<String>,
) -> 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::<BTreeSet<_>>();
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::<BTreeSet<_>>();
if parameters.len() != f.parameters.len() || !names.is_subset(&parameters) {
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<Observation, String> {
let mut origins = BTreeMap::new();
let mut cache = BTreeMap::new();
fn property_value(
s: &Study,
c: &Case,
p: &str,
origins: &mut BTreeMap<String, String>,
cache: &mut BTreeMap<String, Value>,
) -> Result<Value, String> {
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<String, Value>,
missing_property: Option<&str>,
origins: &mut BTreeMap<String, String>,
cache: &mut BTreeMap<String, Value>,
) -> Result<Value, String> {
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::<Vec<_>>();
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>();
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<String, String> {
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::<Vec<_>>();
let b = after.lines().collect::<Vec<_>>();
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::<usize>();
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(())
}