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 if task.operation == rustev_contract::definition::Operation::Rank {
135 let id = doc["presented"][0]["id"].as_str().unwrap();
137 return Evaluated::Output(RawOutput::Scores(BTreeMap::from([(
138 id.to_owned(),
139 logits[0].to_f64_nearest(),
140 )])));
141 }
142 Evaluated::Output(RawOutput::Logits(
143 task.options
144 .iter()
145 .zip(&logits)
146 .map(|(o, d)| (o.clone(), d.to_f64_nearest()))
147 .collect::<BTreeMap<_, _>>(),
148 ))
149}
150
151fn choose<'t>(tasks: &'t [CTask], doc: &Json) -> Result<&'t CTask, String> {
154 let name = doc
155 .get("task")
156 .and_then(Json::as_str)
157 .ok_or("projection has no task")?;
158 let task = tasks
159 .iter()
160 .find(|t| t.name == name)
161 .ok_or_else(|| format!("no rules for task {name:?}"))?;
162 let operation = serde_json::to_value(task.operation).unwrap_or(Json::Null);
163 if doc.get("operation") != Some(&operation) {
164 return Err(format!(
165 "task {name:?}: operation differs from the program's"
166 ));
167 }
168 if doc.get("question").and_then(Json::as_str) != Some(task.question.as_str()) {
169 return Err(format!(
170 "task {name:?}: question differs from the program's"
171 ));
172 }
173 if task.operation == rustev_contract::definition::Operation::Rank {
174 let presented = doc.get("presented").and_then(Json::as_array);
175 let candidate = presented
176 .filter(|p| p.len() == 1)
177 .and_then(|p| p[0].get("id"))
178 .and_then(Json::as_str);
179 if doc.get("mode").and_then(Json::as_str) != Some("pointwise")
180 || candidate.is_none_or(str::is_empty)
181 || doc.get("candidates") != Some(&serde_json::json!([candidate]))
182 || doc.get("options") != Some(&serde_json::json!([]))
183 || presented
184 .is_none_or(|p| !matches!(p[0].get("item"), Some(Json::Null | Json::Object(_))))
185 {
186 return Err("capability: rank requires one pointwise candidate".into());
187 }
188 return Ok(task);
189 }
190 let options: Option<Vec<&str>> = doc
191 .get("options")
192 .and_then(Json::as_array)
193 .map(|a| a.iter().filter_map(Json::as_str).collect());
194 let expected: Vec<&str> = task.options.iter().map(String::as_str).collect();
195 if options.as_ref() != Some(&expected)
196 || doc.get("options").and_then(Json::as_array).map(Vec::len) != Some(expected.len())
197 {
198 return Err(format!("task {name:?}: options differ from the program's"));
199 }
200 Ok(task)
201}
202
203fn read_fields(task: &CTask, doc: &Json) -> Result<Vec<FieldValue>, String> {
205 let empty = Map::new();
206 let values = doc
207 .get("values")
208 .and_then(Json::as_object)
209 .unwrap_or(&empty);
210 task.fields
211 .iter()
212 .map(|f| {
213 let mut v = if task.operation == rustev_contract::definition::Operation::Rank
214 && f.key.starts_with("bind:")
215 {
216 doc.pointer("/presented/0/item")
217 } else {
218 values.get(&f.key)
219 };
220 if let Some(m) = &f.member {
221 v = match v {
222 Some(Json::Object(o)) => o.get(m),
223 Some(Json::Null) | None => None,
224 Some(_) => {
225 return Err(format!(
226 "incompatible field {:?}: {} is not a record",
227 f.name, f.key
228 ));
229 }
230 };
231 }
232 let v = match v {
233 None | Some(Json::Null) => {
234 return Err(format!("missing field {:?}", f.name));
235 }
236 Some(v) => v,
237 };
238 let bad = || format!("incompatible field {:?}: not {:?}", f.name, f.ty);
239 Ok(match f.ty {
240 FieldType::Text => {
241 let s = v.as_str().ok_or_else(bad)?;
242 FieldValue::Text(s.to_string(), s.to_ascii_lowercase())
243 }
244 FieldType::Bool => FieldValue::Bool(v.as_bool().ok_or_else(bad)?),
245 FieldType::Integer => FieldValue::Integer(v.as_i64().ok_or_else(bad)?),
246 FieldType::Decimal => FieldValue::Decimal(
247 v.as_str()
248 .and_then(|s| Decimal::parse(s).ok())
249 .ok_or_else(bad)?,
250 ),
251 })
252 })
253 .collect()
254}
255
256fn holds(c: &CCond, values: &[FieldValue]) -> bool {
257 match c {
258 CCond::Always => true,
259 CCond::ContainsAny { field, terms } => match &values[*field] {
260 FieldValue::Text(_, lower) => terms.iter().any(|t| lower.contains(t.as_str())),
261 _ => false,
262 },
263 CCond::Equals { field, value } => {
264 values[*field].canonical().as_deref() == Some(value.as_str())
265 }
266 CCond::Compare { field, op, value } => match values[*field].number() {
267 Some(n) => match op {
268 CompareOp::Lt => n < *value,
269 CompareOp::Le => n <= *value,
270 CompareOp::Ge => n >= *value,
271 CompareOp::Gt => n > *value,
272 },
273 None => false,
274 },
275 CCond::All(v) => v.iter().all(|c| holds(c, values)),
276 CCond::Any(v) => v.iter().any(|c| holds(c, values)),
277 CCond::Not(c) => !holds(c, values),
278 }
279}
280
281fn add_all(adds: &[(usize, Decimal)], logits: &mut [Decimal], task: &CTask) -> Result<(), String> {
282 for (k, d) in adds {
283 logits[*k] = logits[*k]
284 .checked_add(*d)
285 .map_err(|_| format!("overflow in option {:?}", task.options[*k]))?;
286 }
287 Ok(())
288}
289
290fn apply(
291 c: &CContrib,
292 values: &[FieldValue],
293 logits: &mut [Decimal],
294 task: &CTask,
295) -> Result<(), String> {
296 match c {
297 CContrib::Add(a) => add_all(a, logits, task),
298 CContrib::Linear {
299 field,
300 coefficients,
301 } => {
302 let x = values[*field]
303 .number()
304 .ok_or("linear field is not numeric")?;
305 for (k, coefficient) in coefficients {
306 let term = coefficient
307 .checked_mul(x, Rounding::HalfEven)
308 .map_err(|_| format!("overflow in option {:?}", task.options[*k]))?;
309 add_all(&[(*k, term)], logits, task)?;
310 }
311 Ok(())
312 }
313 CContrib::Lookup {
314 field,
315 table,
316 on_missing,
317 } => {
318 let key = values[*field]
319 .canonical()
320 .ok_or("lookup field has no canonical text")?;
321 match (table.get(&key), on_missing) {
322 (Some(a), _) => add_all(a, logits, task),
323 (None, OnMissing::Skip) => Ok(()),
324 (None, OnMissing::Fail) => Err(format!("no lookup entry for {key:?}")),
325 }
326 }
327 }
328}