#![cfg(feature = "program")]
use fugue::program::{
Addr, BinOp, Data, DistCall, Expr, Program, ProgramError, Registry, Stmt, Value,
FORMAT_VERSION, MAX_NESTING,
};
use fugue::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
const COIN: &str = r#"
let p <- sample(addr!("p"), Beta(2.0, 2.0));
for i in 0..data.len() {
observe(addr!("flip", i), Bernoulli(p), data[i]);
}
pure(p)
"#;
const COIN_JSON: &str = concat!(
r#"{"fugue_program":1,"body":["#,
r#"{"stmt":"sample","var":"p","addr":{"name":"p"},"dist":{"name":"Beta","args":[{"op":"f64","value":2.0},{"op":"f64","value":2.0}]}},"#,
r#"{"stmt":"for","var":"i","start":{"op":"int","value":0},"end":{"op":"len","array":{"op":"var","name":"data"}},"body":["#,
r#"{"stmt":"observe","addr":{"name":"flip","index":{"op":"var","name":"i"}},"dist":{"name":"Bernoulli","args":[{"op":"var","name":"p"}]},"value":{"op":"index","array":{"op":"var","name":"data"},"index":{"op":"var","name":"i"}}}"#,
r#"]}],"ret":{"op":"var","name":"p"}}"#
);
#[test]
fn json_form_is_pinned() {
assert_eq!(FORMAT_VERSION, 1);
let program = Program::parse(COIN).unwrap();
assert_eq!(program.to_json().unwrap(), COIN_JSON);
assert_eq!(Program::from_json(COIN_JSON).unwrap(), program);
let all = Program::parse(
r#"
let x <- sample(addr!("x"), Normal(0.0, 1.0));
let y = [1, -2.5, true][0];
y = -x + y - x * y / 2.0;
observe(addr!("o"), Normal(x, 1.0), y);
factor(0.0);
for i in 0..3 { if i == 1 { break; } }
if !(x < y) && x <= y || x > y && x >= y && x != y { } else { y = abs(y); }
pure(y)
"#,
)
.unwrap();
let json = all.to_json().unwrap();
for tag in [
r#""stmt":"sample""#,
r#""stmt":"let""#,
r#""stmt":"assign""#,
r#""stmt":"observe""#,
r#""stmt":"factor""#,
r#""stmt":"for""#,
r#""stmt":"if""#,
r#""stmt":"break""#,
r#""then":["#,
r#""else":["#,
r#""op":"f64""#,
r#""op":"int""#,
r#""op":"bool""#,
r#""op":"var""#,
r#""op":"array","items":["#,
r#""op":"index""#,
r#""op":"neg""#,
r#""op":"not""#,
r#""op":"add""#,
r#""op":"sub""#,
r#""op":"mul""#,
r#""op":"div""#,
r#""op":"eq""#,
r#""op":"ne""#,
r#""op":"lt""#,
r#""op":"le""#,
r#""op":"gt""#,
r#""op":"ge""#,
r#""op":"and""#,
r#""op":"or""#,
r#""op":"call","func":"abs""#,
] {
assert!(json.contains(tag), "{tag} missing from {json}");
}
assert_eq!(Program::from_json(&json).unwrap(), all);
}
#[test]
fn a_program_written_as_json_runs() {
let json = r#"{
"fugue_program": 1,
"body": [
{"stmt": "let", "var": "tries", "value": {"op": "int", "value": 0}},
{"stmt": "for", "var": "i",
"start": {"op": "int", "value": 0}, "end": {"op": "int", "value": 10},
"body": [
{"stmt": "sample", "var": "hit", "addr": {"name": "hit", "index": {"op": "var", "name": "i"}},
"dist": {"name": "Bernoulli", "args": [{"op": "var", "name": "p"}]}},
{"stmt": "assign", "var": "tries",
"value": {"op": "add", "lhs": {"op": "var", "name": "tries"}, "rhs": {"op": "int", "value": 1}}},
{"stmt": "if", "cond": {"op": "var", "name": "hit"}, "then": [{"stmt": "break"}]}
]}
],
"ret": {"op": "var", "name": "tries"}
}"#;
let from_json = Program::from_json(json).unwrap();
let from_text = Program::parse(
r#"
let tries = 0;
for i in 0..10 {
let hit <- sample(addr!("hit", i), Bernoulli(p));
tries = tries + 1;
if hit { break; }
}
pure(tries)
"#,
)
.unwrap();
assert_eq!(from_json, from_text);
let compiled = from_json
.compile(&Registry::new(), &Data::new().with("p", 0.4))
.unwrap();
let (tries, trace) = runtime::handler::run(
PriorHandler {
rng: &mut StdRng::seed_from_u64(3),
trace: Trace::default(),
},
compiled.build(),
);
assert_eq!(Some(trace.choices.len()), tries.as_usize());
}
#[test]
fn unknown_versions_are_rejected_with_a_clear_error() {
let body = r#""body": [], "ret": {"op": "int", "value": 0}"#;
for (version, found) in [("2", 2u64), ("0", 0), ("4294967296", 4_294_967_296)] {
let doc = format!(r#"{{"fugue_program": {version}, {body}}}"#);
let err = Program::from_json(&doc).unwrap_err();
assert_eq!(
err,
ProgramError::UnsupportedVersion {
found,
supported: FORMAT_VERSION
}
);
let msg = err.to_string();
assert!(
msg.contains(&format!("version {found}")) && msg.contains("reads version 1"),
"{msg}"
);
}
let newer = r#"{"fugue_program": 2, "body": [{"stmt": "while", "cond": {"op": "bool", "value": true}}],
"ret": {"op": "tuple", "items": []}, "meta": {}}"#;
assert!(matches!(
Program::from_json(newer),
Err(ProgramError::UnsupportedVersion { found: 2, .. })
));
for doc in [
format!("{{{body}}}"),
format!(r#"{{"fugue_program": "1", {body}}}"#),
format!(r#"{{"fugue_program": -1, {body}}}"#),
format!(r#"{{"fugue_program": 1.5, {body}}}"#),
] {
match Program::from_json(&doc) {
Err(ProgramError::Json(m)) => assert!(m.contains("fugue_program"), "{doc}: {m}"),
other => panic!("{doc}: {other:?}"),
}
}
let mut p = Program::new(Vec::new(), Expr::Int { value: 0 });
p.fugue_program = 7;
assert!(matches!(
p.to_json(),
Err(ProgramError::UnsupportedVersion { found: 7, .. })
));
assert!(p.compile(&Registry::new(), &Data::new()).is_err());
}
#[test]
fn json_decoding_is_strict() {
let ok = Program::parse(COIN).unwrap().to_json().unwrap();
for (from, to) in [
(r#""var":"p","addr""#, r#""var":"p","extra":1,"addr""#),
(r#"{"name":"flip","index""#, r#"{"name":"flip","indx""#),
(
r#""op":"var","name":"i"}"#,
r#""op":"var","name":"i","x":0}"#,
),
(r#""name":"Beta","args""#, r#""name":"Beta","argz""#),
(r#""ret":"#, r#""meta":{},"ret":"#),
(r#""op":"len""#, r#""op":"length""#),
(r#""stmt":"for""#, r#""stmt":"loop""#),
(r#""var":"i","#, ""),
(r#""value":2.0"#, r#""value":"2.0""#),
(r#""op":"int","value":0"#, r#""op":"int","value":0.5"#),
] {
assert!(ok.contains(from), "{from}");
let bad = ok.replacen(from, to, 1);
match Program::from_json(&bad) {
Err(ProgramError::Json(_)) => {}
other => panic!("{bad}: {other:?}"),
}
}
assert!(Program::from_json(&format!("{ok} x")).is_err());
assert!(Program::from_json("[]").is_err());
let brk = r#"{"fugue_program":1,"body":[{"stmt":"for","var":"i","start":{"op":"int","value":0},
"end":{"op":"int","value":1},"body":[{"stmt":"break","label":"outer"}]}],"ret":{"op":"int","value":0}}"#;
assert!(Program::from_json(brk).is_err());
}
#[test]
fn structural_validation_applies_to_every_entry_point() {
let nan = Program::new(Vec::new(), Expr::F64 { value: f64::NAN });
let inf = Program::new(
vec![Stmt::Factor {
logw: Expr::F64 {
value: f64::NEG_INFINITY,
},
}],
Expr::Int { value: 0 },
);
for p in [&nan, &inf] {
assert!(matches!(p.to_json(), Err(ProgramError::Check(m)) if m.contains("finite")));
assert!(p.compile(&Registry::new(), &Data::new()).is_err());
}
for (field, name) in [
("var", "1x"),
("var", "if"),
("var", "a b"),
("func", "a-b"),
] {
let doc = format!(
r#"{{"fugue_program": 1, "body": [{{"stmt": "let", "{field}": "{name}", "value": {{"op": "int", "value": 0}}}}], "ret": {{"op": "int", "value": 0}}}}"#
);
let doc = if field == "func" {
format!(
r#"{{"fugue_program": 1, "body": [], "ret": {{"op": "call", "func": "{name}", "args": []}}}}"#
)
} else {
doc
};
match Program::from_json(&doc) {
Err(ProgramError::Check(m)) => assert!(m.contains("identifier"), "{m}"),
other => panic!("{doc}: {other:?}"),
}
}
let odd = Program::new(
vec![Stmt::Observe {
addr: Addr {
name: "a#b\\c \"q\"\n\u{e9}".to_string(),
index: Some(Expr::Int { value: 1 }),
},
dist: DistCall {
name: "Normal".to_string(),
args: vec![Expr::F64 { value: 0.0 }, Expr::F64 { value: 1.0 }],
},
value: Expr::F64 { value: 0.5 },
}],
Expr::Int { value: 0 },
);
assert_eq!(Program::from_json(&odd.to_json().unwrap()).unwrap(), odd);
assert_eq!(Program::parse(&odd.to_string()).unwrap(), odd);
}
fn nots(k: usize) -> String {
format!("pure({}true)", "!".repeat(k))
}
#[test]
fn nesting_is_capped_the_same_way_by_every_entry_point() {
let at_limit = Program::parse(¬s(MAX_NESTING - 2)).unwrap();
let json = at_limit.to_json().unwrap();
assert_eq!(Program::from_json(&json).unwrap(), at_limit);
let compiled = at_limit.compile(&Registry::new(), &Data::new()).unwrap();
let (v, _) = runtime::handler::run(
PriorHandler {
rng: &mut StdRng::seed_from_u64(0),
trace: Trace::default(),
},
compiled.build(),
);
assert_eq!(v, Value::Bool(true));
let too_deep = nots(MAX_NESTING - 1);
assert!(matches!(
Program::parse(&too_deep),
Err(ProgramError::Syntax { line: 1, message })
if message.contains(&format!("at most {MAX_NESTING}"))
));
let mut e = Expr::Bool { value: true };
for _ in 0..MAX_NESTING - 1 {
e = Expr::Not { arg: Box::new(e) };
}
let built = Program::new(Vec::new(), e);
assert!(matches!(built.to_json(), Err(ProgramError::Check(_))));
assert!(built.compile(&Registry::new(), &Data::new()).is_err());
let deeper_json = json
.replacen(
r#"{"op":"not","arg":"#,
r#"{"op":"not","arg":{"op":"not","arg":"#,
1,
)
.replacen("}}", "}}}", 1);
assert!(matches!(
Program::from_json(&deeper_json),
Err(ProgramError::Check(_))
));
let way_deeper = format!(
r#"{{"fugue_program":1,"body":[],"ret":{}{}{}}}"#,
r#"{"op":"neg","arg":"#.repeat(5000),
r#"{"op":"int","value":1}"#,
"}".repeat(5000)
);
assert!(matches!(
Program::from_json(&way_deeper),
Err(ProgramError::Json(_))
));
}
#[test]
fn the_text_printer_reads_back() {
for src in [
COIN,
"pure(-(2.0))",
"pure(-2.0)",
"pure(-(-2))",
"pure(--x)",
"pure(-x.len())",
"pure((-2.0).len())",
"pure(-9223372036854775808)",
"pure(1 - (2 - 3))",
"pure((1 - 2) - 3)",
"pure(1 / (2 * 3))",
"pure((a < b) == (c < d))",
"pure((a || b) && !(c && d))",
"pure(!(a == b))",
"pure([1, [2.5][0], x[1 + i]].len())",
"pure(1e-7 + 1.5e300 + 0.1 + 5e-324)",
r#"observe(addr!("q\"uo\\te#", 1), Normal(0.0, 1.0), 0.0); pure(0)"#,
"for i in 0..3 { if i == 0 { } else if i == 1 { break; } else { let y = i; y = y + 1; } } pure(0)",
"if a { if b { } } else { if c { } else { } } pure(0)",
"let mut x = 1; x = x * -1; pure(x)",
] {
let p = Program::parse(src).unwrap_or_else(|e| panic!("{src}: {e}"));
let text = p.to_string();
assert_eq!(Program::parse(&text).unwrap(), p, "{src}\n=>\n{text}");
assert_eq!(Program::from_json(&p.to_json().unwrap()).unwrap(), p, "{src}");
}
let p = Program::parse(COIN).unwrap();
assert_eq!(
p.to_string(),
"let p <- sample(addr!(\"p\"), Beta(2.0, 2.0));\n\
for i in 0..data.len() {\n observe(addr!(\"flip\", i), Bernoulli(p), data[i]);\n}\n\
pure(p)"
);
assert_eq!(
"pure(1)".parse::<Program>().unwrap(),
Program::parse("pure(1)").unwrap()
);
let e = Expr::binary(BinOp::Le, Expr::var("a"), Expr::Int { value: 3 });
assert_eq!(e.to_string(), "a <= 3");
assert_eq!(e.as_binary().map(|(op, _, _)| op.symbol()), Some("<="));
}