use std::sync::Arc;
use crate::scope_kernel::ScopeKernel;
use polydat::ast::Value;
use polydat::dsl::stub::{ExprStub, GraphMatter};
use crate::phase_outcome::Outcome;
pub struct ScopedPredicate {
kernel: ScopeKernel,
output: usize,
}
impl ScopedPredicate {
pub fn bind(parent: &ScopeKernel, output: &str, matter: GraphMatter) -> Result<Self, String> {
let kernel = ScopeKernel::build_under(
parent.kernel(),
crate::scope_kernel::SourceMatter::statements(
output,
matter.into_statements(),
Default::default(),
),
)
.map_err(|e| format!("scoped-expr subscope: {e}"))?;
let output = kernel
.program()
.output_index(output)
.ok_or_else(|| format!("scoped-expr: no output `{output}`"))?;
Ok(Self { kernel, output })
}
pub fn dataflow(&mut self) -> &mut dyn polydat::Kernel {
self.kernel.kernel_mut()
}
pub fn eval(&mut self) -> Value {
self.kernel.pull_at(self.output)
}
pub fn is_true(&mut self) -> bool {
match self.eval() {
Value::Bool(b) => b,
Value::F64(v) => v != 0.0,
v => v.as_u64() != 0,
}
}
}
pub mod wire {
pub const CYCLES_TOTAL: &str = "cycles_total";
pub const RESULT_FAILURE: &str = "result_failure";
pub const ATTEMPT_TOTAL: &str = "attempt_total";
pub const ATTEMPT_SUCCESS: &str = "attempt_success";
pub const ATTEMPT_FAILURE: &str = "attempt_failure";
pub const ELAPSED_MS: &str = "elapsed_ms";
pub const CHILDREN_TOTAL: &str = "children_total";
pub const CHILDREN_FAILED: &str = "children_failed";
pub const CHILDREN_DONE: &str = "children_done";
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct RuntimeState {
pub cycles_total: u64,
pub result_failure: u64,
pub elapsed_ms: u64,
pub attempt_total: u64,
pub attempt_success: u64,
pub attempt_failure: u64,
pub children_total: u64,
pub children_failed: u64,
pub children_done: u64,
}
impl RuntimeState {
fn result_failure_fraction(&self) -> f64 {
if self.cycles_total == 0 {
0.0
} else {
self.result_failure as f64 / self.cycles_total as f64
}
}
fn attempt_failure_fraction(&self) -> f64 {
if self.attempt_total == 0 {
0.0
} else {
self.attempt_failure as f64 / self.attempt_total as f64
}
}
pub fn describe(&self) -> String {
let mut parts = Vec::new();
if self.children_total > 0 {
parts.push(format!(
"children_done={}/{}",
self.children_done, self.children_total
));
if self.children_failed > 0 {
parts.push(format!("children_failed={}", self.children_failed));
}
} else {
parts.push(format!("cycles_total={}", self.cycles_total));
parts.push(format!("result_failure={}", self.result_failure));
parts.push(format!(
"result_failure/cycles_total={:.2}%",
self.result_failure_fraction() * 100.0
));
if self.attempt_total > self.cycles_total || self.attempt_failure > 0 {
parts.push(format!(
"attempt_failure={}/{}",
self.attempt_failure, self.attempt_total
));
parts.push(format!(
"attempt_failure_fraction={:.2}%",
self.attempt_failure_fraction() * 100.0
));
}
}
parts.push(format!("elapsed={:.1}s", self.elapsed_ms as f64 / 1000.0));
parts.join(", ")
}
fn wires(&self) -> [(&'static str, Value); 9] {
[
(wire::CYCLES_TOTAL, Value::U64(self.cycles_total)),
(wire::RESULT_FAILURE, Value::U64(self.result_failure)),
(wire::ELAPSED_MS, Value::U64(self.elapsed_ms)),
(wire::ATTEMPT_TOTAL, Value::U64(self.attempt_total)),
(wire::ATTEMPT_SUCCESS, Value::U64(self.attempt_success)),
(wire::ATTEMPT_FAILURE, Value::U64(self.attempt_failure)),
(wire::CHILDREN_TOTAL, Value::U64(self.children_total)),
(wire::CHILDREN_FAILED, Value::U64(self.children_failed)),
(wire::CHILDREN_DONE, Value::U64(self.children_done)),
]
}
pub fn inject_into(&self, ctx: &mut dyn polydat::Kernel) {
for (name, value) in self.wires() {
if let Some(idx) = ctx.input_index(name) {
let _ = ctx.set_input_at(idx, value);
}
}
}
pub fn trips(&self, condition: &mut ScopedPredicate) -> bool {
self.inject_into(condition.dataflow());
condition.is_true()
}
}
pub fn extern_matter() -> GraphMatter {
let mut m = GraphMatter::new();
m.extern_wire::<u64>(wire::CYCLES_TOTAL)
.extern_wire::<u64>(wire::RESULT_FAILURE)
.extern_wire::<u64>(wire::ELAPSED_MS)
.extern_wire::<u64>(wire::ATTEMPT_TOTAL)
.extern_wire::<u64>(wire::ATTEMPT_SUCCESS)
.extern_wire::<u64>(wire::ATTEMPT_FAILURE)
.extern_wire::<u64>(wire::CHILDREN_TOTAL)
.extern_wire::<u64>(wire::CHILDREN_FAILED)
.extern_wire::<u64>(wire::CHILDREN_DONE);
m
}
pub fn compile_stop_condition(
phase_kernel: &ScopeKernel,
idx: usize,
when: &str,
) -> Result<ScopedPredicate, String> {
let name = format!("__stop_cond_{idx}");
let mut matter = extern_matter();
const CANONICAL: [&str; 9] = [
wire::CYCLES_TOTAL,
wire::RESULT_FAILURE,
wire::ELAPSED_MS,
wire::ATTEMPT_TOTAL,
wire::ATTEMPT_SUCCESS,
wire::ATTEMPT_FAILURE,
wire::CHILDREN_TOTAL,
wire::CHILDREN_FAILED,
wire::CHILDREN_DONE,
];
let cells = phase_kernel.cells_in_scope();
for referenced in polydat::dsl::refs::referenced_names(when) {
if CANONICAL.contains(&referenced.as_str()) {
continue;
}
if let Some(cell) = cells.iter().find(|c| c.name == referenced) {
matter.extern_wire_typed(&cell.name, cell.port_type);
}
}
matter.bind(
ExprStub::parse(&name, when)
.map_err(|e| format!("stop condition {idx} predicate `{when}`: {e}"))?
.returning::<u64>()
.volatile(),
);
ScopedPredicate::bind(phase_kernel, &name, matter)
.map_err(|e| format!("stop condition {idx} predicate `{when}`: {e}"))
}
pub fn compile_continue_if(
when: &str,
coords: &[(String, Value)],
strict: bool,
) -> Result<Arc<ScopeKernel>, String> {
let mut source = String::new();
for (name, value) in coords {
source.push_str(&format!(
"extern {name}: {}\n",
value.port_type().to_keyword()
));
}
source.push_str(&format!("__continue_if := {when}"));
let options = polydat::dsl::compile::CompileOptions {
required_outputs: vec!["__continue_if".to_string()],
strict,
..Default::default()
};
crate::bindings::compile_scope_kernel(&source, &options)
.map(Arc::new)
.map_err(|e| format!("continue_if predicate `{when}`: {e}"))
}
pub fn eval_continue_if(
gate_canonical: &Arc<ScopeKernel>,
parent: &Arc<ScopeKernel>,
bindings: &[(String, Value)],
) -> Result<bool, String> {
let mut kernel = gate_canonical
.bind_under(parent.kernel(), bindings)
.map_err(|e| format!("continue_if: {e}"))?;
let pulled = kernel.pull("__continue_if");
Ok(match pulled {
Value::Bool(b) => b,
other => other.as_u64() != 0,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StopScope {
#[default]
Phase,
Scenario,
Workload,
}
#[derive(Debug, Clone)]
pub struct StopConditionDecl {
pub when: String,
pub effect: Outcome,
pub reason: Option<String>,
pub target: StopScope,
pub cancel_ops: bool,
}
impl StopConditionDecl {
pub fn error_rate_guard(max: f64) -> Self {
Self {
when: format!(
"cycles_total >= 50 && \
to_f64(result_failure) > (to_f64(cycles_total) * {max})"
),
effect: Outcome::failed(),
reason: Some("error_rate_exceeded".to_string()),
target: StopScope::Phase,
cancel_ops: false,
}
}
pub fn timeout_guard(timeout_ms: u64) -> Self {
Self {
when: format!("elapsed_ms > {timeout_ms}"),
effect: Outcome::failed(),
reason: Some("timeout".to_string()),
target: StopScope::Phase,
cancel_ops: false,
}
}
pub fn effect_from_str(effect: Option<&str>, default: Outcome) -> Outcome {
match effect {
Some("stop") => Outcome::interrupted(),
Some("fail") | Some("abort") => Outcome::failed(),
_ => default,
}
}
pub fn action_cancels_ops(action: Option<&str>) -> bool {
matches!(action, Some("abort"))
}
}
pub struct StopConditionSet {
conditions: Vec<StopCondition>,
}
struct StopCondition {
expr: ScopedPredicate,
effect: Outcome,
reason: String,
target: StopScope,
cancel_ops: bool,
}
impl StopConditionSet {
pub fn build_for_phase(
phase_kernel: &ScopeKernel,
declared: &[StopConditionDecl],
) -> Result<Self, String> {
let mut conditions = Vec::new();
for (idx, decl) in declared.iter().enumerate() {
let expr = compile_stop_condition(phase_kernel, idx, &decl.when)?;
conditions.push(StopCondition {
expr,
effect: decl.effect.clone(), reason: decl
.reason
.clone()
.unwrap_or_else(|| format!("stop_condition: {}", decl.when)),
target: decl.target,
cancel_ops: decl.cancel_ops,
});
}
Ok(Self { conditions })
}
pub fn empty() -> Self {
Self {
conditions: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.conditions.is_empty()
}
pub fn evaluate(&mut self, state: &RuntimeState) -> Option<(Outcome, String, StopScope, bool)> {
for cond in &mut self.conditions {
if state.trips(&mut cond.expr) {
return Some((
cond.effect.clone(),
cond.reason.clone(),
cond.target,
cond.cancel_ops,
));
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stop_condition_reads_root_shared_wire() {
let mut root = ScopeKernel::compile(
"shared recent_result_failures: f64 := 0.0
shared recent_result_total: f64 := 0.0
rx := 1",
)
.expect("root kernel");
let phase = ScopeKernel::build_under(
root.kernel(),
crate::scope_kernel::SourceMatter::source("phase_test", "x := 5", Default::default()),
)
.expect("phase kernel");
compile_stop_condition(&phase, 0, "result_failure >= 100")
.expect("canonical-only predicate must compile");
let mut cond = compile_stop_condition(
&phase, 0,
"((result_failure >= 100) & (to_f64(result_failure) > (to_f64(cycles_total) * 0.025))) \
| ((recent_result_failures >= 100.0) & (recent_result_failures > (recent_result_total + 1.0) * 0.025))")
.expect("shared-wire predicate must compile");
let state = RuntimeState {
cycles_total: 1000,
result_failure: 5,
..Default::default()
};
assert!(!state.trips(&mut cond), "cells at 0 must not trip");
let idx = root
.program()
.find_input("recent_result_failures")
.expect("root input slot for the shared wire");
root.set_input_at(idx, polydat::ast::Value::F64(150.0))
.expect("write the shared cell");
assert!(
state.trips(&mut cond),
"predicate must read the LIVE shared cell (150 >= 100, ratio floor 0)"
);
root.set_input_at(idx, polydat::ast::Value::F64(0.0))
.expect("write the shared cell");
assert!(!state.trips(&mut cond), "cell reset must un-trip");
assert!(
compile_stop_condition(&phase, 0, "recent_result_failurez >= 1").is_err(),
"unknown names must stay compile errors"
);
}
#[test]
fn failure_fraction_is_safe_at_zero_cycles() {
assert_eq!(RuntimeState::default().result_failure_fraction(), 0.0);
let s = RuntimeState {
cycles_total: 100,
result_failure: 10,
..Default::default()
};
assert_eq!(s.result_failure_fraction(), 0.1);
let s = RuntimeState {
cycles_total: 50,
result_failure: 50,
..Default::default()
};
assert_eq!(s.result_failure_fraction(), 1.0);
}
#[test]
fn error_rate_is_faithful_and_in_range_for_terminal_failures() {
let all_fail = RuntimeState {
cycles_total: 100,
result_failure: 100,
..Default::default()
};
assert_eq!(all_fail.result_failure_fraction(), 1.0);
let phase_kernel = ScopeKernel::compile("input cycle: u64\nx := 5").expect("phase kernel");
let mut cond = compile_stop_condition(
&phase_kernel,
0,
"cycles_total >= 50 && to_f64(result_failure) > (to_f64(cycles_total) * 1.0)",
)
.expect("compile scoped stop condition");
assert!(
!all_fail.trips(&mut cond),
"error_rate_max:1.0 must never trip"
);
let mut half = compile_stop_condition(
&phase_kernel,
0,
"cycles_total >= 50 && to_f64(result_failure) > (to_f64(cycles_total) * 0.5)",
)
.unwrap();
assert!(
RuntimeState {
cycles_total: 100,
result_failure: 60,
..Default::default()
}
.trips(&mut half)
);
}
#[test]
fn injects_referenced_wires_by_name_and_re_evaluates() {
let src = "\
extern cycles_total: u64 = 0\n\
extern result_failure: u64 = 0\n\
volatile sum := cycles_total + result_failure";
let mut k =
polydat::dsl::compile_polydat_interpreter(src).expect("compile predicate kernel");
RuntimeState {
cycles_total: 10,
result_failure: 5,
..Default::default()
}
.inject_into(&mut k);
assert_eq!(*k.pull_ref("sum"), Value::U64(15));
RuntimeState {
cycles_total: 40,
result_failure: 2,
..Default::default()
}
.inject_into(&mut k);
assert_eq!(*k.pull_ref("sum"), Value::U64(42));
}
#[test]
fn compiles_and_trips_a_scoped_stop_condition() {
let phase_kernel = ScopeKernel::compile("input cycle: u64\nx := 5").expect("phase kernel");
let mut cond = compile_stop_condition(
&phase_kernel,
0,
"cycles_total > 50 && to_f64(result_failure) > (to_f64(cycles_total) * 0.1)",
)
.expect("compile scoped stop condition");
assert!(
!RuntimeState {
cycles_total: 40,
result_failure: 20,
..Default::default()
}
.trips(&mut cond)
);
assert!(
RuntimeState {
cycles_total: 100,
result_failure: 20,
..Default::default()
}
.trips(&mut cond)
);
assert!(
!RuntimeState {
cycles_total: 100,
result_failure: 1,
..Default::default()
}
.trips(&mut cond)
);
}
#[test]
fn stop_condition_set_installs_default_error_rate_and_declared_predicates() {
let phase_kernel = ScopeKernel::compile("input cycle: u64\nx := 5").expect("phase kernel");
let mut set = StopConditionSet::build_for_phase(
&phase_kernel,
&[
StopConditionDecl::error_rate_guard(0.1),
StopConditionDecl {
when: "cycles_total > 1000".to_string(),
effect: Outcome::failed(),
reason: None,
target: StopScope::Phase,
cancel_ops: false,
},
],
)
.expect("build set");
drop(phase_kernel);
assert!(!set.is_empty());
assert!(
set.evaluate(&RuntimeState {
cycles_total: 10,
result_failure: 10,
..Default::default()
})
.is_none()
);
assert!(
set.evaluate(&RuntimeState {
cycles_total: 100,
result_failure: 5,
..Default::default()
})
.is_none()
);
assert_eq!(
set.evaluate(&RuntimeState {
cycles_total: 100,
result_failure: 20,
..Default::default()
}),
Some((
Outcome::failed(),
"error_rate_exceeded".to_string(),
StopScope::Phase,
false
))
);
assert_eq!(
set.evaluate(&RuntimeState {
cycles_total: 2000,
result_failure: 1,
..Default::default()
}),
Some((
Outcome::failed(),
"stop_condition: cycles_total > 1000".to_string(),
StopScope::Phase,
false
))
);
}
#[test]
fn abort_action_is_failed_and_cancels_ops() {
assert_eq!(
StopConditionDecl::effect_from_str(Some("abort"), Outcome::interrupted()),
Outcome::failed()
);
assert_eq!(
StopConditionDecl::effect_from_str(Some("fail"), Outcome::interrupted()),
Outcome::failed()
);
assert!(StopConditionDecl::action_cancels_ops(Some("abort")));
assert!(!StopConditionDecl::action_cancels_ops(Some("fail")));
assert!(!StopConditionDecl::action_cancels_ops(Some("stop")));
assert!(!StopConditionDecl::action_cancels_ops(None));
let root = ScopeKernel::compile("input cycle: u64").expect("root kernel");
let mut set = StopConditionSet::build_for_phase(
&root,
&[StopConditionDecl {
when: "result_failure > 0".to_string(),
effect: Outcome::failed(),
reason: Some("terminal_failure".to_string()),
target: StopScope::Workload,
cancel_ops: true,
}],
)
.expect("build set");
assert_eq!(
set.evaluate(&RuntimeState {
result_failure: 1,
..Default::default()
}),
Some((
Outcome::failed(),
"terminal_failure".to_string(),
StopScope::Workload,
true
))
);
}
}