1use std::collections::BTreeMap;
4
5use rustev_contract::bounded::{ParseLimits, parse_bounded};
6use rustev_contract::decimal::{Decimal, Rounding};
7use rustev_contract::output::RawOutput;
8use serde_json::{Map, Value as Json};
9
10use crate::program::{CCond, CContrib, CTask, CompareOp, FieldType, NoMatch, OnMissing};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum Evaluated {
15 Output(RawOutput),
17 Failed(String),
20 Cancelled(String),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(crate) enum Point {
28 BeforeParse,
29 AfterFields,
30 BeforeRule(usize),
31}
32
33enum FieldValue {
35 Text(String, String),
37 Bool(bool),
38 Integer(i64),
39 Decimal(Decimal),
40}
41
42impl FieldValue {
43 fn canonical(&self) -> Option<String> {
45 match self {
46 FieldValue::Text(s, _) => Some(s.clone()),
47 FieldValue::Bool(b) => Some(b.to_string()),
48 FieldValue::Integer(i) => Some(i.to_string()),
49 FieldValue::Decimal(_) => None,
50 }
51 }
52
53 fn number(&self) -> Option<Decimal> {
54 match self {
55 FieldValue::Integer(i) => Some(Decimal::from_i64(*i)),
56 FieldValue::Decimal(d) => Some(*d),
57 _ => None,
58 }
59 }
60}
61
62fn projection_limits(max_bytes: u64) -> ParseLimits {
64 let max_bytes = usize::try_from(max_bytes).unwrap_or(usize::MAX);
65 ParseLimits {
66 max_bytes,
67 max_depth: 16,
68 max_string_bytes: max_bytes,
69 max_collection_len: 4_096,
70 max_total_values: 50_000,
71 max_number_bytes: max_bytes,
73 allow_fractional_numbers: false,
74 }
75}
76
77pub(crate) fn evaluate(
80 tasks: &[CTask],
81 max_projection_bytes: u64,
82 projection: &[u8],
83 cancelled: &mut dyn FnMut(Point) -> bool,
84) -> Evaluated {
85 if cancelled(Point::BeforeParse) {
86 return Evaluated::Cancelled("before parsing the projection".into());
87 }
88 if projection.len() as u64 > max_projection_bytes {
89 return Evaluated::Failed(format!(
90 "projection of {} bytes exceeds the program's limit of {max_projection_bytes}",
91 projection.len()
92 ));
93 }
94 let doc: Json = match parse_bounded(projection, &projection_limits(max_projection_bytes)) {
95 Ok(v) => v,
96 Err(e) => return Evaluated::Failed(format!("projection refused: {e}")),
97 };
98 let task = match choose(tasks, &doc) {
99 Ok(t) => t,
100 Err(e) => return Evaluated::Failed(e),
101 };
102 let values = match read_fields(task, &doc) {
103 Ok(v) => v,
104 Err(e) => return Evaluated::Failed(format!("task {:?}: {e}", task.name)),
105 };
106 if cancelled(Point::AfterFields) {
107 return Evaluated::Cancelled("after field validation".into());
108 }
109 let mut logits = task.base.clone();
110 let mut applied = false;
111 for (i, rule) in task.rules.iter().enumerate() {
112 if cancelled(Point::BeforeRule(i)) {
113 return Evaluated::Cancelled(format!("before rule {:?}", rule.name));
114 }
115 if !holds(&rule.when, &values) {
116 continue;
117 }
118 applied = true;
119 for c in &rule.then {
120 if let Err(e) = apply(c, &values, &mut logits, task) {
121 return Evaluated::Failed(format!(
122 "task {:?}, rule {:?}: {e}",
123 task.name, rule.name
124 ));
125 }
126 }
127 if rule.stop {
128 break;
129 }
130 }
131 if !applied && task.on_no_match == NoMatch::Fail {
132 return Evaluated::Failed(format!("task {:?}: no rule applied", task.name));
133 }
134 Evaluated::Output(RawOutput::Logits(
135 task.options
136 .iter()
137 .zip(&logits)
138 .map(|(o, d)| (o.clone(), d.to_f64_nearest()))
139 .collect::<BTreeMap<_, _>>(),
140 ))
141}
142
143fn choose<'t>(tasks: &'t [CTask], doc: &Json) -> Result<&'t CTask, String> {
146 let name = doc
147 .get("task")
148 .and_then(Json::as_str)
149 .ok_or("projection has no task")?;
150 let task = tasks
151 .iter()
152 .find(|t| t.name == name)
153 .ok_or_else(|| format!("no rules for task {name:?}"))?;
154 let operation = serde_json::to_value(task.operation).unwrap_or(Json::Null);
155 if doc.get("operation") != Some(&operation) {
156 return Err(format!(
157 "task {name:?}: operation differs from the program's"
158 ));
159 }
160 if doc.get("question").and_then(Json::as_str) != Some(task.question.as_str()) {
161 return Err(format!(
162 "task {name:?}: question differs from the program's"
163 ));
164 }
165 let options: Option<Vec<&str>> = doc
166 .get("options")
167 .and_then(Json::as_array)
168 .map(|a| a.iter().filter_map(Json::as_str).collect());
169 let expected: Vec<&str> = task.options.iter().map(String::as_str).collect();
170 if options.as_ref() != Some(&expected)
171 || doc.get("options").and_then(Json::as_array).map(Vec::len) != Some(expected.len())
172 {
173 return Err(format!("task {name:?}: options differ from the program's"));
174 }
175 Ok(task)
176}
177
178fn read_fields(task: &CTask, doc: &Json) -> Result<Vec<FieldValue>, String> {
180 let empty = Map::new();
181 let values = doc
182 .get("values")
183 .and_then(Json::as_object)
184 .unwrap_or(&empty);
185 task.fields
186 .iter()
187 .map(|f| {
188 let mut v = values.get(&f.key);
189 if let Some(m) = &f.member {
190 v = match v {
191 Some(Json::Object(o)) => o.get(m),
192 Some(Json::Null) | None => None,
193 Some(_) => {
194 return Err(format!(
195 "incompatible field {:?}: {} is not a record",
196 f.name, f.key
197 ));
198 }
199 };
200 }
201 let v = match v {
202 None | Some(Json::Null) => {
203 return Err(format!("missing field {:?}", f.name));
204 }
205 Some(v) => v,
206 };
207 let bad = || format!("incompatible field {:?}: not {:?}", f.name, f.ty);
208 Ok(match f.ty {
209 FieldType::Text => {
210 let s = v.as_str().ok_or_else(bad)?;
211 FieldValue::Text(s.to_string(), s.to_ascii_lowercase())
212 }
213 FieldType::Bool => FieldValue::Bool(v.as_bool().ok_or_else(bad)?),
214 FieldType::Integer => FieldValue::Integer(v.as_i64().ok_or_else(bad)?),
215 FieldType::Decimal => FieldValue::Decimal(
216 v.as_str()
217 .and_then(|s| Decimal::parse(s).ok())
218 .ok_or_else(bad)?,
219 ),
220 })
221 })
222 .collect()
223}
224
225fn holds(c: &CCond, values: &[FieldValue]) -> bool {
226 match c {
227 CCond::Always => true,
228 CCond::ContainsAny { field, terms } => match &values[*field] {
229 FieldValue::Text(_, lower) => terms.iter().any(|t| lower.contains(t.as_str())),
230 _ => false,
231 },
232 CCond::Equals { field, value } => {
233 values[*field].canonical().as_deref() == Some(value.as_str())
234 }
235 CCond::Compare { field, op, value } => match values[*field].number() {
236 Some(n) => match op {
237 CompareOp::Lt => n < *value,
238 CompareOp::Le => n <= *value,
239 CompareOp::Ge => n >= *value,
240 CompareOp::Gt => n > *value,
241 },
242 None => false,
243 },
244 CCond::All(v) => v.iter().all(|c| holds(c, values)),
245 CCond::Any(v) => v.iter().any(|c| holds(c, values)),
246 CCond::Not(c) => !holds(c, values),
247 }
248}
249
250fn add_all(adds: &[(usize, Decimal)], logits: &mut [Decimal], task: &CTask) -> Result<(), String> {
251 for (k, d) in adds {
252 logits[*k] = logits[*k]
253 .checked_add(*d)
254 .map_err(|_| format!("overflow in option {:?}", task.options[*k]))?;
255 }
256 Ok(())
257}
258
259fn apply(
260 c: &CContrib,
261 values: &[FieldValue],
262 logits: &mut [Decimal],
263 task: &CTask,
264) -> Result<(), String> {
265 match c {
266 CContrib::Add(a) => add_all(a, logits, task),
267 CContrib::Linear {
268 field,
269 coefficients,
270 } => {
271 let x = values[*field]
272 .number()
273 .ok_or("linear field is not numeric")?;
274 for (k, coefficient) in coefficients {
275 let term = coefficient
276 .checked_mul(x, Rounding::HalfEven)
277 .map_err(|_| format!("overflow in option {:?}", task.options[*k]))?;
278 add_all(&[(*k, term)], logits, task)?;
279 }
280 Ok(())
281 }
282 CContrib::Lookup {
283 field,
284 table,
285 on_missing,
286 } => {
287 let key = values[*field]
288 .canonical()
289 .ok_or("lookup field has no canonical text")?;
290 match (table.get(&key), on_missing) {
291 (Some(a), _) => add_all(a, logits, task),
292 (None, OnMissing::Skip) => Ok(()),
293 (None, OnMissing::Fail) => Err(format!("no lookup entry for {key:?}")),
294 }
295 }
296 }
297}