rustev_backend_rules/
check.rs1use rustev_contract::Identified;
4use rustev_contract::definition::{ItemSource, SemanticDecl, StepBody, TypeDecl};
5use rustev_contract::plan::{Plan, PlanExecution, PlanStepDetail};
6
7use crate::RulesBackend;
8use crate::program::{CTask, FieldType};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PlanMismatch {
13 pub step: String,
14 pub kind: PlanMismatchKind,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum PlanMismatchKind {
19 Descriptor,
21 NoTask(String),
23 TaskDiffers(&'static str),
25 NotProjected { field: String, reference: String },
27 Incompatible { field: String, reference: String },
29}
30
31impl RulesBackend {
32 pub fn check_plan(&self, plan: &Plan) -> Result<(), Vec<PlanMismatch>> {
36 let me = &self.descriptor.backend_id;
37 let my_descriptor = self.descriptor.id().ok();
38 let mut bound: Vec<(&str, bool)> = vec![];
39 for s in &plan.steps {
40 let binding = match &s.detail {
41 PlanStepDetail::Semantic(b) => Some((&b.backend_id, &b.descriptor)),
42 PlanStepDetail::Rank(b) => Some((&b.backend_id, &b.descriptor)),
43 _ => None,
44 };
45 if let Some((backend, descriptor)) = binding
46 && backend == me
47 {
48 bound.push((&s.id, Some(descriptor) == my_descriptor.as_ref()));
49 }
50 }
51 if let PlanExecution::Declared(d) = &plan.execution {
52 for f in d.fallbacks.iter().filter(|f| &f.backend_id == me) {
53 let entry = (
54 f.step.as_str(),
55 Some(&f.descriptor) == my_descriptor.as_ref(),
56 );
57 if !bound.contains(&entry) {
59 bound.push(entry);
60 }
61 }
62 }
63 let mut out = vec![];
64 for (step, same_descriptor) in bound {
65 let mismatch = |kind| PlanMismatch {
66 step: step.to_string(),
67 kind,
68 };
69 if !same_descriptor {
70 out.push(mismatch(PlanMismatchKind::Descriptor));
71 continue;
72 }
73 let decl = plan.definition.steps.iter().find_map(|s| match &s.body {
74 StepBody::Semantic(d) if s.id == step => Some(d.clone()),
75 StepBody::Rank(r) if s.id == step => {
76 use rustev_contract::definition::*;
77 if r.mode != rustev_contract::ranking::RankMode::Pointwise {
78 out.push(mismatch(PlanMismatchKind::TaskDiffers("mode")));
79 }
80 let mut project = r.project.clone();
81 project.push(format!("bind:{}", r.bind));
82 Some(SemanticDecl {
83 operation: Operation::Rank,
84 task: r.task.clone(),
85 question: r.question.clone(),
86 options: vec!["score".into()],
87 candidates: Candidates::From(r.candidates.clone()),
88 requires: RequiredKind::Scores,
89 project,
90 for_each: vec![ForEach {
91 over: r.candidates.clone(),
92 bind: r.bind.clone(),
93 items: r.items.clone(),
94 }],
95 backend: r.backend.clone(),
96 calibration: CalibrationRef::None,
97 fallback: Fallback::None,
98 })
99 }
100 _ => None,
101 });
102 let Some(decl) = decl else { continue };
103 let Some(task) = self.tasks.iter().find(|t| t.name == decl.task) else {
104 out.push(mismatch(PlanMismatchKind::NoTask(decl.task.clone())));
105 continue;
106 };
107 if task.operation != decl.operation {
108 out.push(mismatch(PlanMismatchKind::TaskDiffers("operation")));
109 }
110 if task.question != decl.question {
111 out.push(mismatch(PlanMismatchKind::TaskDiffers("question")));
112 }
113 if task.options != decl.options {
114 out.push(mismatch(PlanMismatchKind::TaskDiffers("options")));
115 }
116 for kind in fields(task, &decl, plan) {
117 out.push(mismatch(kind));
118 }
119 }
120 if out.is_empty() { Ok(()) } else { Err(out) }
121 }
122}
123
124fn fields(task: &CTask, decl: &SemanticDecl, plan: &Plan) -> Vec<PlanMismatchKind> {
125 let mut out = vec![];
126 for f in &task.fields {
127 let reference = match &f.member {
128 Some(m) => format!("{}/{m}", f.key),
129 None => f.key.clone(),
130 };
131 if !decl.project.contains(&f.key) {
132 out.push(PlanMismatchKind::NotProjected {
133 field: f.name.clone(),
134 reference,
135 });
136 continue;
137 }
138 let ty = declared_type(&f.key, decl, plan).and_then(|t| match &f.member {
139 None => Some(t),
140 Some(m) => match t {
141 TypeDecl::Record { fields, .. } => {
142 fields.iter().find(|x| &x.name == m).map(|x| &x.ty)
143 }
144 _ => None,
145 },
146 });
147 if !ty.is_some_and(|t| compatible(t, f.ty)) {
148 out.push(PlanMismatchKind::Incompatible {
149 field: f.name.clone(),
150 reference,
151 });
152 }
153 }
154 out
155}
156
157fn declared_type<'p>(key: &str, decl: &SemanticDecl, plan: &'p Plan) -> Option<&'p TypeDecl> {
160 let input = |name: &str| {
161 plan.definition
162 .inputs
163 .iter()
164 .find(|i| i.name == name)
165 .map(|i| &i.ty)
166 };
167 if let Some(name) = key.strip_prefix("input:") {
168 return input(name);
169 }
170 let bind = key.strip_prefix("bind:")?;
171 let f = decl.for_each.iter().find(|f| f.bind == bind)?;
172 let ItemSource::List { list, .. } = &f.items else {
173 return None;
174 };
175 match input(list.strip_prefix("input:")?)? {
176 TypeDecl::List { item, .. } => Some(item),
177 _ => None,
178 }
179}
180
181fn compatible(t: &TypeDecl, f: FieldType) -> bool {
182 matches!(
183 (t, f),
184 (
185 TypeDecl::Text { .. } | TypeDecl::Enum { .. },
186 FieldType::Text
187 ) | (TypeDecl::Bool, FieldType::Bool)
188 | (TypeDecl::Integer | TypeDecl::Timestamp, FieldType::Integer)
189 | (TypeDecl::Decimal, FieldType::Decimal)
190 )
191}