Skip to main content

rustev_backend_rules/
check.rs

1//! The plan check a host runs at setup (spec 005, 3.11.4).
2
3use 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/// One way a plan's use of this backend does not match its program.
11#[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    /// The plan binds this backend id to a different descriptor.
20    Descriptor,
21    /// The program has no task with the step's task name.
22    NoTask(String),
23    /// The task exists but its operation, question or options differ.
24    TaskDiffers(&'static str),
25    /// A field reads a reference the step does not project.
26    NotProjected { field: String, reference: String },
27    /// A field's declared type does not match the definition's type there.
28    Incompatible { field: String, reference: String },
29}
30
31impl RulesBackend {
32    /// Check every semantic or rank step `plan` binds to this backend, as primary or
33    /// runtime fallback target. Returns every mismatch; an empty plan use is
34    /// not an error.
35    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                // A step is checked once however often it reaches this backend.
58                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
157/// The definition's type at a projection key: an input's type, or a bound
158/// item's record type through the step's `for_each` list.
159fn 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}