use polydat::dsl::compile::compile_polydat_to_assembler;
use polydat::{Engine, JitMode, Kernel, KernelError, Provenance};
fn engines() -> Vec<Engine> {
let mut all = vec![Engine::Interpreter(JitMode::Off)];
for m in [
Provenance::Raw,
Provenance::Push,
Provenance::Pull,
Provenance::PushPull,
] {
all.push(Engine::Closures(m));
#[cfg(feature = "jit")]
all.push(Engine::Native(m));
}
all
}
fn payload_text(p: Box<dyn std::any::Any + Send>) -> String {
p.downcast_ref::<String>()
.cloned()
.or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_else(|| "<non-string payload>".into())
}
fn without_location(msg: &str) -> String {
msg.lines()
.filter(|l| !l.trim_start().starts_with("↳ panicked at"))
.collect::<Vec<_>>()
.join("\n")
}
fn failure(src: &str, engine: Engine, drive: &dyn Fn(&mut dyn Kernel)) -> Option<String> {
failure_with(src, engine, JitMode::Off, drive)
}
fn failure_with(
src: &str,
engine: Engine,
mode: JitMode,
drive: &dyn Fn(&mut dyn Kernel),
) -> Option<String> {
let mut asm = compile_polydat_to_assembler(src).expect("the program compiles");
asm.set_jit_mode(mode);
let mut k = match asm.compile_with(engine) {
Ok(k) => k,
Err(KernelError::Refused { .. }) => return None,
Err(e) => panic!("{engine}: {e}"),
};
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
drive(&mut *k);
let outs: Vec<String> = k.output_names().iter().map(|s| s.to_string()).collect();
for o in &outs {
let _ = k.pull(o);
}
}));
match r {
Ok(()) => panic!("{engine}: the program did not fail"),
Err(p) => Some(payload_text(p)),
}
}
fn assert_same_failure(src: &str, drive: &dyn Fn(&mut dyn Kernel), expect: &[&str]) {
let want = failure(src, Engine::Interpreter(JitMode::Off), drive)
.expect("the interpreter runs everything");
for e in expect {
assert!(want.contains(e), "interpreter message lacks {e:?}:\n{want}");
}
assert!(
want.contains("↳ panicked at"),
"interpreter message lacks the location:\n{want}"
);
for engine in engines() {
let Some(got) = failure(src, engine, drive) else {
continue;
};
assert_eq!(
without_location(&got),
without_location(&want),
"{engine} reports the failure differently from the interpreter"
);
}
}
#[test]
fn a_predicate_violation_reads_the_same_on_every_engine() {
let src = "input cycle: u64\ndoubled := mul(cycle, 2)\nchecked := in_range(doubled, 0, 5)\n";
assert_same_failure(
src,
&|k| k.set_inputs(&[10]),
&[
"in_range",
"↳ in node `in_range` (output checked)",
"↳ inputs: [[0]=U64(20)]",
],
);
}
#[test]
fn a_string_coercion_failure_reads_the_same_on_every_engine() {
let src = "input cycle: u64\nextern s: str = \"12\"\nn := __str_to_u64(s)\n";
assert_same_failure(
src,
&|k| {
k.set_inputs(&[3]);
k.set_input("s", polydat::ast::Value::Str("twelve".into()))
.expect("s is an extern");
},
&[
"value \"twelve\" is not a whole number.",
"↳ in node `__str_to_u64` (output n)",
"↳ inputs: [[0]=Str(\"twelve\")]",
],
);
}
#[test]
fn the_attribution_names_the_context_and_the_original_message() {
let src = "input cycle: u64\nchecked := in_range(cycle, 0, 5)\n";
for engine in engines() {
let Some(got) = failure(src, engine, &|k| k.set_inputs(&[7])) else {
continue;
};
let first = got.lines().next().unwrap_or("");
assert!(
first.starts_with("in_range: value 7 outside [0, 5]"),
"{engine}: the original message is not first:\n{got}"
);
assert!(
got.contains("while evaluating"),
"{engine}: no context line:\n{got}"
);
}
}
#[cfg(feature = "jit")]
#[test]
fn a_cone_names_the_member_that_failed() {
let src = "input cycle: u64\ndoubled := mul(cycle, 2)\nchecked := in_range(doubled, 0, 5)\n";
let got = failure_with(
src,
Engine::Interpreter(JitMode::Force),
JitMode::Force,
&|k| k.set_inputs(&[10]),
)
.expect("the interpreter runs everything");
for e in [
"in_range: value 20 outside [0, 5]",
"↳ in node `in_range` (output checked) while evaluating (polydat)",
"↳ inputs: [[0]=U64(20)]",
] {
assert!(got.contains(e), "cone failure lacks {e:?}:\n{got}");
}
assert!(!got.contains("jit_cone"), "the cone is not a frame:\n{got}");
let native = failure_with(
src,
Engine::Native(Provenance::Auto),
JitMode::Force,
&|k| k.set_inputs(&[10]),
)
.expect("native code runs everything");
assert_eq!(
got, native,
"the fused interpreter and the native engine read alike"
);
}