Verify native behavior creation recovery and source authoring contracts
This commit is contained in:
@@ -0,0 +1,913 @@
|
||||
//! 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(¶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<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(())
|
||||
}
|
||||
Reference in New Issue
Block a user