Skip to main content

jev/rules/
mod.rs

1//! Fuzzy rules over a reply: what `-r rules.toml` reads. A policy written over Jev's answers: each
2//! term reads a probability (a Noul's probability of yes, one Score level's or one Choice option's)
3//! and uses it as the degree to which the term holds. That is a modelling choice, not a fact about
4//! the numbers: a probability of 0.8 that it is hot is not "hot to degree 0.8". The rules combine
5//! the degrees with AND, OR, NOT and hedges, by fuzzy logic, into a support score per outcome, which
6//! counts as a yes at or over a threshold. A support score is not a probability, and isn't
7//! calibrated as one: the reply keeps the probabilities.
8//!
9//! ```toml
10//! [logic]                    # optional
11//! and = "min"                # min | product | lukasiewicz
12//! or  = "max"                # max | probsum | bounded
13//!
14//! [decide]                   # optional
15//! threshold = 0.5
16//!
17//! [terms]                    # the names rules use for the answers
18//! hot     = "temp.Hot"       # a Score's level, by its text
19//! stormy  = "sky.storm"      # a Choice's option
20//! raining = "raining"        # a Noul, by its id alone
21//!
22//! [[rule]]
23//! if     = "raining AND NOT SOMEWHAT hot"
24//! then   = "raincoat"
25//! weight = 1.0               # optional; the rule's score is multiplied by it
26//! ```
27//!
28//! Operators are uppercase (`AND`, `OR`, `NOT`, and the hedges `VERY`, `SOMEWHAT`, `EXTREMELY`,
29//! `INDEED`) and terms are lowercase, so neither can be taken for the other. Hedges and `NOT` bind
30//! tightest, then `AND`, then `OR`, and parentheses group. Rules with the same `then` are joined by
31//! the file's OR. `OUTPUT IS SET` always concludes in a declared `[output.OUTPUT]`.
32//!
33//! Before evaluating, every answer the terms read is checked: a probability outside 0 to 1, a
34//! distribution that doesn't add up to 1, or a level or option the question doesn't have is an
35//! error, never clamped into something that looks like an answer.
36//!
37//! A rules file is checked against the questions before anything is asked ([`Rules::parse`]), so a
38//! term naming a level the question doesn't have costs no call.
39
40use std::collections::BTreeMap;
41use std::fmt::Write as _;
42
43use serde::{Deserialize, Serialize};
44use serde_json::Value;
45
46use crate::{DecisionResponse, Options, Question};
47
48mod graph;
49mod output;
50mod svg;
51
52use output::Output;
53
54/// How AND joins two degrees.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum And {
58    /// The smaller: the weakest condition decides, and repeating one doesn't lower it.
59    #[default]
60    Min,
61    /// Their product: every weak condition lowers the result. Read as a probability, it assumes
62    /// the events are independent, which answers about one state seldom are.
63    Product,
64    /// `max(0, a + b − 1)`: strict, high only when both are.
65    Lukasiewicz,
66}
67
68impl And {
69    /// How it is worked out, for the graph.
70    fn describe(self) -> &'static str {
71        match self {
72            And::Min => "min",
73            And::Product => "a × b",
74            And::Lukasiewicz => "max(0, a + b − 1)",
75        }
76    }
77
78    fn apply(self, a: f64, b: f64) -> f64 {
79        match self {
80            And::Min => a.min(b),
81            And::Product => a * b,
82            And::Lukasiewicz => (a + b - 1.0).max(0.0),
83        }
84    }
85}
86
87/// How OR joins two degrees, in a rule and between rules with the same `then`.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum Or {
91    /// The larger: the strongest reason decides, and repeating one doesn't raise it.
92    #[default]
93    Max,
94    /// `a + b − ab`: reasons reinforce each other. A reason written twice counts twice (0.6 becomes
95    /// 0.84), so it suits distinct pieces of evidence, not restatements of one.
96    Probsum,
97    /// `min(1, a + b)`: reasons add up, to at most 1. For levels or options of one question, which
98    /// exclude each other, this is their probabilities' sum: the chance that one of them holds.
99    Bounded,
100}
101
102impl Or {
103    /// How it is worked out, for the graph.
104    fn describe(self) -> &'static str {
105        match self {
106            Or::Max => "max",
107            Or::Probsum => "a + b − ab",
108            Or::Bounded => "min(1, a + b)",
109        }
110    }
111
112    fn apply(self, a: f64, b: f64) -> f64 {
113        match self {
114            Or::Max => a.max(b),
115            Or::Probsum => a + b - a * b,
116            Or::Bounded => (a + b).min(1.0),
117        }
118    }
119}
120
121/// The file's `[logic]`: which AND and which OR.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct Logic {
125    #[serde(default)]
126    pub and: And,
127    #[serde(default)]
128    pub or: Or,
129}
130
131/// A word that reshapes a degree. It moves where the threshold falls (`VERY a` passes 0.5 only
132/// when `a` is at least 0.71), and nothing more: it doesn't make "angry" mean "very angry", or add
133/// evidence. A stronger meaning needs its own level or question.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135enum Hedge {
136    /// `a²`: only a strong answer stays strong.
137    Very,
138    /// `√a`: a weak answer still counts.
139    Somewhat,
140    /// `a³`.
141    Extremely,
142    /// Pushes a degree away from 0.5, toward 0 or 1. It makes no answer more certain.
143    Indeed,
144}
145
146impl Hedge {
147    fn named(word: &str) -> Option<Hedge> {
148        Some(match word {
149            "VERY" => Hedge::Very,
150            "SOMEWHAT" => Hedge::Somewhat,
151            "EXTREMELY" => Hedge::Extremely,
152            "INDEED" => Hedge::Indeed,
153            _ => return None,
154        })
155    }
156
157    /// Its name and how it is worked out, for the graph.
158    fn describe(self) -> (&'static str, &'static str) {
159        match self {
160            Hedge::Very => ("VERY", "x²"),
161            Hedge::Somewhat => ("SOMEWHAT", "√x"),
162            Hedge::Extremely => ("EXTREMELY", "x³"),
163            Hedge::Indeed => ("INDEED", "toward 0 or 1"),
164        }
165    }
166
167    fn apply(self, a: f64) -> f64 {
168        match self {
169            Hedge::Very => a * a,
170            Hedge::Somewhat => a.sqrt(),
171            Hedge::Extremely => a * a * a,
172            Hedge::Indeed if a <= 0.5 => 2.0 * a * a,
173            Hedge::Indeed => 1.0 - 2.0 * (1.0 - a) * (1.0 - a),
174        }
175    }
176}
177
178/// Every operator, for the messages that name them.
179const OPERATORS: &str = "AND, OR, NOT, VERY, SOMEWHAT, EXTREMELY, INDEED";
180
181/// A rule's `if`, parsed.
182#[derive(Debug, Clone, PartialEq)]
183enum Expr {
184    Term(String),
185    Not(Box<Expr>),
186    Hedge(Hedge, Box<Expr>),
187    And(Box<Expr>, Box<Expr>),
188    Or(Box<Expr>, Box<Expr>),
189}
190
191impl Expr {
192    /// The degree to which it holds, with each term's from `degree`.
193    fn eval(&self, logic: Logic, degree: &mut impl FnMut(&str) -> crate::Result<f64>) -> crate::Result<f64> {
194        Ok(match self {
195            Expr::Term(term) => degree(term)?,
196            Expr::Not(inner) => 1.0 - inner.eval(logic, degree)?,
197            Expr::Hedge(hedge, inner) => hedge.apply(inner.eval(logic, degree)?),
198            Expr::And(a, b) => logic.and.apply(a.eval(logic, degree)?, b.eval(logic, degree)?),
199            Expr::Or(a, b) => logic.or.apply(a.eval(logic, degree)?, b.eval(logic, degree)?),
200        })
201    }
202
203    /// The terms it uses, in the order written.
204    fn terms<'a>(&'a self, out: &mut Vec<&'a str>) {
205        match self {
206            Expr::Term(term) => out.push(term),
207            Expr::Not(inner) | Expr::Hedge(_, inner) => inner.terms(out),
208            Expr::And(a, b) | Expr::Or(a, b) => {
209                a.terms(out);
210                b.terms(out);
211            }
212        }
213    }
214}
215
216#[derive(Debug, Clone, PartialEq)]
217enum Token {
218    Word(String),
219    Open,
220    Close,
221}
222
223fn tokens(text: &str) -> Vec<Token> {
224    let mut tokens = Vec::new();
225    let mut word = String::new();
226    for character in text.chars() {
227        if character.is_whitespace() || character == '(' || character == ')' {
228            if !word.is_empty() {
229                tokens.push(Token::Word(std::mem::take(&mut word)));
230            }
231            match character {
232                '(' => tokens.push(Token::Open),
233                ')' => tokens.push(Token::Close),
234                _ => {}
235            }
236        } else {
237            word.push(character);
238        }
239    }
240    if !word.is_empty() {
241        tokens.push(Token::Word(word));
242    }
243    tokens
244}
245
246/// How many words and brackets one `if` may have.
247const MOST_TOKENS: usize = 256;
248
249/// A recursive descent over the tokens: OR of ANDs of hedged or negated terms and groups.
250struct Parser {
251    tokens: Vec<Token>,
252    at: usize,
253}
254
255impl Parser {
256    fn parse(text: &str) -> Result<Expr, String> {
257        let mut parser = Parser { tokens: tokens(text), at: 0 };
258        if parser.tokens.is_empty() {
259            return Err("the `if` is empty".to_owned());
260        }
261        // Parsing and evaluating recurse once per level of the tree, and a tree is never deeper
262        // than its words and brackets are many. Bounding them keeps a rule from a file or a model
263        // from running the stack out, which would end the process rather than return an error.
264        if parser.tokens.len() > MOST_TOKENS {
265            return Err(format!(
266                "it is {} words and brackets long, over the {MOST_TOKENS} a rule may have; split it into several rules with the same `then`",
267                parser.tokens.len()
268            ));
269        }
270        let expr = parser.or()?;
271        match parser.tokens.get(parser.at) {
272            None => Ok(expr),
273            Some(Token::Close) => Err("a `)` with no `(` before it".to_owned()),
274            Some(Token::Open) => Err("a `(` where AND or OR should be".to_owned()),
275            Some(Token::Word(word)) if word.chars().any(|character| character.is_uppercase()) => {
276                Err(format!("`{word}` isn't an operator: operators are {OPERATORS}"))
277            }
278            Some(Token::Word(word)) => Err(format!("`{word}` where AND or OR should be")),
279        }
280    }
281
282    fn eat(&mut self, keyword: &str) -> bool {
283        let found = matches!(self.tokens.get(self.at), Some(Token::Word(word)) if word == keyword);
284        if found {
285            self.at += 1;
286        }
287        found
288    }
289
290    fn or(&mut self) -> Result<Expr, String> {
291        let mut left = self.and()?;
292        while self.eat("OR") {
293            left = Expr::Or(Box::new(left), Box::new(self.and()?));
294        }
295        Ok(left)
296    }
297
298    fn and(&mut self) -> Result<Expr, String> {
299        let mut left = self.unary()?;
300        while self.eat("AND") {
301            left = Expr::And(Box::new(left), Box::new(self.unary()?));
302        }
303        Ok(left)
304    }
305
306    fn unary(&mut self) -> Result<Expr, String> {
307        if self.eat("NOT") {
308            return Ok(Expr::Not(Box::new(self.unary()?)));
309        }
310        if let Some(Token::Word(word)) = self.tokens.get(self.at) {
311            if let Some(hedge) = Hedge::named(word) {
312                self.at += 1;
313                return Ok(Expr::Hedge(hedge, Box::new(self.unary()?)));
314            }
315        }
316        self.atom()
317    }
318
319    fn atom(&mut self) -> Result<Expr, String> {
320        let token = self.tokens.get(self.at).cloned();
321        self.at += 1;
322        match token {
323            None => Err("it ends where a term should be".to_owned()),
324            Some(Token::Close) => Err("a `)` where a term should be".to_owned()),
325            Some(Token::Open) => {
326                let inner = self.or()?;
327                if self.tokens.get(self.at) != Some(&Token::Close) {
328                    return Err("a `(` that isn't closed".to_owned());
329                }
330                self.at += 1;
331                Ok(inner)
332            }
333            Some(Token::Word(word)) if word == "AND" || word == "OR" => Err(format!("`{word}` where a term should be")),
334            Some(Token::Word(word)) if is_term_name(&word) => Ok(Expr::Term(word)),
335            Some(Token::Word(word)) => Err(format!(
336                "`{word}` is neither an operator nor a term: operators are uppercase ({OPERATORS}), and terms are \
337                 lowercase names from [terms]"
338            )),
339        }
340    }
341}
342
343/// Whether `name` can be a term: a lowercase word of letters, digits and `_`, so that it can never
344/// be taken for an operator.
345fn is_term_name(name: &str) -> bool {
346    let mut characters = name.chars();
347    characters.next().is_some_and(|first| first.is_ascii_lowercase() || first == '_')
348        && characters.all(|character| character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_')
349}
350
351/// What a term reads from the reply: one level of a Score, one option of a Choice, or a Noul's yes.
352/// It keeps every label of its question as well, which the graph draws.
353#[derive(Debug, Clone, PartialEq)]
354struct Target {
355    id: String,
356    kind: Kind,
357    /// The Score's levels, the Choice's options, or `no` and `yes` for a Noul.
358    labels: Vec<String>,
359    /// Which of `labels` the term is.
360    selected: usize,
361}
362
363/// Which type of question a term reads.
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365enum Kind {
366    Noul,
367    Score,
368    Choice,
369}
370
371impl Target {
372    /// Which question `target` names in `questions`, and what of it. `target` is the question id
373    /// alone for a Noul, or the id, a `.`, and a level's text or an option's name.
374    fn resolve(target: &str, questions: &BTreeMap<&str, &Question>) -> Result<Target, String> {
375        let target = target.trim();
376        if let Some(question) = questions.get(target) {
377            return match question {
378                Question::Noul { .. } => {
379                    Ok(Target { id: target.to_owned(), kind: Kind::Noul, labels: vec!["no".to_owned(), "yes".to_owned()], selected: 1 })
380                }
381                Question::Score { criteria, .. } => {
382                    Err(format!("`{target}` is a score: name one of its levels, as {}", listed(target, &level_names(criteria))))
383                }
384                Question::Choice { criteria, .. } => {
385                    Err(format!("`{target}` is a choice: name one of its options, as {}", listed(target, &option_names(criteria))))
386                }
387            };
388        }
389        // An id may have a `.` of its own, so each `.` is tried until one ends a question's id: the
390        // last first, so that with both `weather` and `weather.temp` asked, `weather.temp.Hot` is
391        // the level of `weather.temp`.
392        for (at, _) in target.rmatch_indices('.') {
393            let (id, name) = (&target[..at], &target[at + 1..]);
394            let Some(question) = questions.get(id) else { continue };
395            return match question {
396                Question::Noul { .. } => Err(format!("`{id}` is a noul, which has no levels or options: write `{id}` alone")),
397                Question::Score { criteria, .. } => {
398                    // Levels are numbered in a reply as u8s, so a level past 255 can't be read back.
399                    match criteria.iter().position(|level| level.as_str() == Some(name)).filter(|at| u8::try_from(*at).is_ok()) {
400                        Some(selected) => Ok(Target { id: id.to_owned(), kind: Kind::Score, labels: level_labels(criteria), selected }),
401                        None => Err(format!("`{id}` has no level `{name}`; its levels are {}", listed(id, &level_names(criteria)))),
402                    }
403                }
404                Question::Choice { criteria, .. } => {
405                    let options = option_names(criteria);
406                    match options.iter().position(|option| option == name) {
407                        Some(selected) => Ok(Target { id: id.to_owned(), kind: Kind::Choice, labels: options, selected }),
408                        None => Err(format!("`{id}` has no option `{name}`; its options are {}", listed(id, &options))),
409                    }
410                }
411            };
412        }
413        let asked: Vec<&str> = questions.keys().copied().collect();
414        Err(format!("no question `{}`; the questions are {}", target.split('.').next().unwrap_or(target), asked.join(", ")))
415    }
416
417    /// The degree the reply gives it. An answer missing from the reply, or of another type, is an
418    /// error rather than a zero: a zero would read as a confident no.
419    fn degree(&self, reply: &DecisionResponse) -> crate::Result<f64> {
420        Ok(match self.kind {
421            Kind::Noul => reply.noul(&self.id)?,
422            Kind::Score => {
423                let level = u8::try_from(self.selected).unwrap_or(u8::MAX);
424                reply.score(&self.id)?.probabilities.get(&level).copied().ok_or_else(|| self.missing())?
425            }
426            Kind::Choice => {
427                reply.choice(&self.id)?.probabilities.get(&self.labels[self.selected]).copied().ok_or_else(|| self.missing())?
428            }
429        })
430    }
431}
432
433impl Target {
434    /// Whether the reply's answer to this term's question is one it can read: of the right type,
435    /// its numbers probabilities ([`crate::Answer::check`]), and no level or option the question
436    /// doesn't have.
437    fn check(&self, reply: &DecisionResponse) -> crate::Result<()> {
438        let bad = |why: String| crate::Error::BadAnswer { id: self.id.clone(), why };
439        match self.kind {
440            Kind::Noul => {
441                reply.noul(&self.id)?;
442            }
443            Kind::Score => {
444                if let Some(level) = reply.score(&self.id)?.probabilities.keys().find(|level| usize::from(**level) >= self.labels.len()) {
445                    return Err(bad(format!(
446                        "it gives a probability for level {level}, and the question's levels are 0 to {}",
447                        self.labels.len() - 1
448                    )));
449                }
450            }
451            Kind::Choice => {
452                if let Some(option) = reply.choice(&self.id)?.probabilities.keys().find(|option| !self.labels.contains(option)) {
453                    return Err(bad(format!("it gives a probability for `{option}`, which isn't one of the question's options")));
454                }
455            }
456        }
457        reply.answer(&self.id)?.check().map_err(bad)
458    }
459
460    /// The error for a reply whose answer gives no probability for this term's level or option.
461    fn missing(&self) -> crate::Error {
462        crate::Error::MissingProbability { id: self.id.clone(), label: self.labels[self.selected].clone() }
463    }
464}
465
466/// A Score's levels as the graph labels them: the text, or the number of a structured level.
467fn level_labels(levels: &[Value]) -> Vec<String> {
468    levels
469        .iter()
470        .enumerate()
471        .map(|(at, level)| match level {
472            Value::String(text) => text.clone(),
473            _ => format!("level {at}"),
474        })
475        .collect()
476}
477
478/// A Score's levels, as a term names them. A structured level has no text to be named by.
479fn level_names(levels: &[Value]) -> Vec<String> {
480    levels
481        .iter()
482        .enumerate()
483        .map(|(at, level)| match level {
484            Value::String(text) => text.clone(),
485            _ => format!("(level {at} is structured, so no term can name it)"),
486        })
487        .collect()
488}
489
490fn option_names(Options(options): &Options) -> Vec<String> {
491    options.iter().map(|(name, _)| name.clone()).collect()
492}
493
494/// `id.a`, `id.b`, … for a message.
495fn listed(id: &str, names: &[String]) -> String {
496    names.iter().map(|name| if name.starts_with('(') { name.clone() } else { format!("`{id}.{name}`") }).collect::<Vec<_>>().join(", ")
497}
498
499/// A rules file, as written.
500#[derive(Deserialize)]
501#[serde(deny_unknown_fields)]
502struct File {
503    #[serde(default)]
504    logic: Logic,
505    #[serde(default)]
506    decide: Decide,
507    #[serde(default)]
508    terms: BTreeMap<String, String>,
509    #[serde(default, rename = "output")]
510    outputs: BTreeMap<String, output::OutputFile>,
511    #[serde(default, rename = "rule")]
512    rules: Vec<RuleFile>,
513}
514
515#[derive(Deserialize)]
516#[serde(deny_unknown_fields)]
517struct Decide {
518    #[serde(default = "Decide::half")]
519    threshold: f64,
520}
521
522impl Decide {
523    fn half() -> f64 {
524        0.5
525    }
526}
527
528impl Default for Decide {
529    fn default() -> Decide {
530        Decide { threshold: Decide::half() }
531    }
532}
533
534#[derive(Deserialize)]
535#[serde(deny_unknown_fields)]
536struct RuleFile {
537    #[serde(rename = "if")]
538    when: String,
539    then: String,
540    #[serde(default = "RuleFile::full")]
541    weight: f64,
542}
543
544impl RuleFile {
545    fn full() -> f64 {
546        1.0
547    }
548}
549
550/// One rule, checked.
551#[derive(Debug, Clone, PartialEq)]
552struct Rule {
553    /// The `if` as written, which the outcome shows.
554    text: String,
555    when: Expr,
556    then: Then,
557    weight: f64,
558}
559
560/// What a rule concludes: an item, scored and then decided at the threshold, or one fuzzy set of an
561/// output, clipped at the rule's score and merged into a crisp value.
562#[derive(Debug, Clone, PartialEq)]
563enum Then {
564    Item(String),
565    /// `OUTPUT IS SET`, by position in [`Rules::outputs`] and in that output's sets.
566    Output {
567        output: usize,
568        set: usize,
569    },
570}
571
572/// A rules file, parsed and checked against the questions it will read.
573#[derive(Debug, Clone, PartialEq)]
574pub struct Rules {
575    logic: Logic,
576    threshold: f64,
577    terms: BTreeMap<String, Target>,
578    outputs: Vec<Output>,
579    rules: Vec<Rule>,
580}
581
582impl Rules {
583    /// The rules in `text` (TOML), for a reply to `questions`. Every term is checked against the
584    /// questions here, before anything is asked; the error says what is wrong and where.
585    pub fn parse<'a>(text: &str, questions: impl IntoIterator<Item = (&'a str, &'a Question)>) -> Result<Rules, String> {
586        let file: File = toml::from_str(text).map_err(|error| error.to_string().trim_end().to_owned())?;
587        if !(0.0..=1.0).contains(&file.decide.threshold) {
588            return Err(format!("[decide] threshold is {}; it goes from 0 to 1", file.decide.threshold));
589        }
590        let questions: BTreeMap<&str, &Question> = questions.into_iter().collect();
591        let mut terms = BTreeMap::new();
592        for (name, target) in &file.terms {
593            if !is_term_name(name) {
594                return Err(format!(
595                    "[terms] `{name}`: a term is a lowercase word of letters, digits and `_`, so it can't be taken for an \
596                     operator ({OPERATORS})"
597                ));
598            }
599            terms.insert(name.clone(), Target::resolve(target, &questions).map_err(|why| format!("[terms] {name}: {why}"))?);
600        }
601        let mut outputs = Vec::with_capacity(file.outputs.len());
602        for (name, output) in file.outputs {
603            outputs.push(Output::parse(name, output)?);
604        }
605        if file.rules.is_empty() {
606            return Err("no rules: add a [[rule]] with an `if` and a `then`".to_owned());
607        }
608        let mut rules = Vec::with_capacity(file.rules.len());
609        for (at, rule) in file.rules.into_iter().enumerate() {
610            let text = rule.when.trim().to_owned();
611            let wrong = |why: &str| format!("rule {} (`{text}`): {why}", at + 1);
612            // An operator written in lowercase reads as a term, and the parser would then trip over
613            // whatever follows it; saying what it is helps more than where it tripped.
614            for token in tokens(&text) {
615                let Token::Word(word) = token else { continue };
616                let upper = word.to_ascii_uppercase();
617                let what = match upper.as_str() {
618                    "AND" | "OR" | "NOT" => "operator",
619                    _ if Hedge::named(&upper).is_some() => "hedge",
620                    _ => continue,
621                };
622                if word != upper && !terms.contains_key(&word) {
623                    return Err(wrong(&format!("`{word}` isn't in [terms]; the {what} is written {upper}")));
624                }
625            }
626            let when = Parser::parse(&text).map_err(|why| wrong(&why))?;
627            let mut used = Vec::new();
628            when.terms(&mut used);
629            if let Some(missing) = used.iter().find(|term| !terms.contains_key(**term)) {
630                let named = match terms.is_empty() {
631                    true => "none".to_owned(),
632                    false => terms.keys().cloned().collect::<Vec<_>>().join(", "),
633                };
634                return Err(wrong(&format!("`{missing}` isn't in [terms], which names {named}")));
635            }
636            let then = Then::parse(&rule.then, &outputs).map_err(|why| wrong(&why))?;
637            if !(0.0..=1.0).contains(&rule.weight) {
638                return Err(wrong(&format!("the weight is {}; it goes from 0 to 1", rule.weight)));
639            }
640            rules.push(Rule { text, when, then, weight: rule.weight });
641        }
642        Ok(Rules { logic: file.logic, threshold: file.decide.threshold, terms, outputs, rules })
643    }
644
645    /// The outcome `reply` gives: every `then`, in the order the file first names it, with its
646    /// score and the rules behind it.
647    pub fn evaluate(&self, reply: &DecisionResponse) -> crate::Result<Outcome> {
648        self.check(reply)?;
649        let scores = self.scores(reply)?;
650        let mut items: Vec<Item> = Vec::new();
651        let mut outputs: Vec<OutputValue> = self
652            .outputs
653            .iter()
654            .map(|output| OutputValue {
655                output: output.name.clone(),
656                value: None,
657                sets: output.sets.iter().map(|set| SetScore { set: set.name.clone(), score: 0.0, rules: Vec::new() }).collect(),
658            })
659            .collect();
660        for (rule, &score) in self.rules.iter().zip(&scores) {
661            let fired = Fired { when: rule.text.clone(), weight: rule.weight, score };
662            match &rule.then {
663                Then::Item(name) => match items.iter_mut().find(|item| &item.item == name) {
664                    Some(item) => {
665                        item.score = self.logic.or.apply(item.score, score);
666                        item.rules.push(fired);
667                    }
668                    None => items.push(Item { item: name.clone(), score, yes: false, rules: vec![fired] }),
669                },
670                Then::Output { output, set } => {
671                    let set = &mut outputs[*output].sets[*set];
672                    set.score = self.logic.or.apply(set.score, score);
673                    set.rules.push(fired);
674                }
675            }
676        }
677        for item in &mut items {
678            item.yes = item.score >= self.threshold;
679        }
680        for (at, value) in outputs.iter_mut().enumerate() {
681            value.value = self.outputs[at].centroid(&self.set_scores(at, &scores), self.logic.or);
682        }
683        Ok(Outcome { threshold: self.threshold, items, outputs })
684    }
685
686    /// Every answer the terms read, checked once per question.
687    fn check(&self, reply: &DecisionResponse) -> crate::Result<()> {
688        let mut checked: Vec<&str> = Vec::new();
689        for target in self.terms.values() {
690            if !checked.contains(&target.id.as_str()) {
691                target.check(reply)?;
692                checked.push(&target.id);
693            }
694        }
695        Ok(())
696    }
697
698    /// Each rule's score for `reply`: what its `if` came to, times its weight.
699    fn scores(&self, reply: &DecisionResponse) -> crate::Result<Vec<f64>> {
700        let mut degree = |term: &str| self.terms[term].degree(reply);
701        self.rules.iter().map(|rule| Ok(rule.when.eval(self.logic, &mut degree)? * rule.weight)).collect()
702    }
703
704    /// Every set of output `at`, with the score it is clipped at: its rules' scores joined by the
705    /// file's OR, and 0 for a set no rule concludes. Each set is clipped once, at this score, so the
706    /// score an outcome reports, the shape drawn and the value taken from it are the same thing.
707    fn set_scores(&self, at: usize, scores: &[f64]) -> Vec<f64> {
708        let mut sets = vec![0.0; self.outputs[at].sets.len()];
709        for (rule, &score) in self.rules.iter().zip(scores) {
710            if let Then::Output { output, set } = rule.then {
711                if output == at {
712                    sets[set] = self.logic.or.apply(sets[set], score);
713                }
714            }
715        }
716        sets
717    }
718}
719
720/// What the rules made of a reply.
721#[derive(Debug, Clone, PartialEq, Serialize)]
722pub struct Outcome {
723    /// The score at or over which an item is a yes.
724    pub threshold: f64,
725    /// Every item a `then` names, in the order the file first names it.
726    pub items: Vec<Item>,
727    /// Every `[output]`, with the crisp value its sets' clipped shapes merge into.
728    #[serde(skip_serializing_if = "Vec::is_empty")]
729    pub outputs: Vec<OutputValue>,
730}
731
732/// An output's crisp value: the centroid of its sets, each clipped at the score its rules give it.
733/// The value says where the support lies, not how much there is: a set clipped at 0.01 alone gives
734/// the same value as one clipped at 1, so read the sets' scores before acting on it.
735#[derive(Debug, Clone, PartialEq, Serialize)]
736pub struct OutputValue {
737    pub output: String,
738    /// `None` when no rule concluding it scored above zero, so there is no shape to take a centre of.
739    pub value: Option<f64>,
740    /// Every set, in order along the output's range.
741    pub sets: Vec<SetScore>,
742}
743
744/// One set of an output: the score its rules clip it at (joined by the file's OR), and those rules.
745#[derive(Debug, Clone, PartialEq, Serialize)]
746pub struct SetScore {
747    pub set: String,
748    pub score: f64,
749    pub rules: Vec<Fired>,
750}
751
752/// One `then`: its score, whether that reaches the threshold, and the rules that gave it.
753#[derive(Debug, Clone, PartialEq, Serialize)]
754pub struct Item {
755    pub item: String,
756    pub score: f64,
757    pub yes: bool,
758    pub rules: Vec<Fired>,
759}
760
761/// One rule's part in an item's score.
762#[derive(Debug, Clone, PartialEq, Serialize)]
763pub struct Fired {
764    /// The rule's `if`, as written.
765    #[serde(rename = "if")]
766    pub when: String,
767    pub weight: f64,
768    /// What the `if` came to, times the weight.
769    pub score: f64,
770}
771
772impl Outcome {
773    /// One line per item, `name  score  yes`, then the threshold, then a line per output with its
774    /// value and each set's score. Ends with a newline.
775    pub fn text(&self) -> String {
776        let names = self.items.iter().map(|item| &item.item).chain(self.outputs.iter().map(|output| &output.output));
777        let width = names.map(|name| name.chars().count()).max().unwrap_or(0);
778        let mut out = String::new();
779        for item in &self.items {
780            let yes = if item.yes { "  yes" } else { "" };
781            let _ = writeln!(out, "{:width$}  {:.2}{yes}", item.item, item.score);
782        }
783        if !self.items.is_empty() {
784            let _ = writeln!(out, "threshold {:.2}", self.threshold);
785        }
786        for output in &self.outputs {
787            let _ = writeln!(out, "{:width$}  {}  ({})", output.output, output.value_text(), output.sets_text());
788        }
789        out
790    }
791}
792
793impl OutputValue {
794    /// The value to two places, or `-` when no rule fired.
795    pub fn value_text(&self) -> String {
796        self.value.map_or_else(|| "-".to_owned(), |value| format!("{value:.2}"))
797    }
798
799    /// `short 0.00, medium 0.80, long 0.10`.
800    pub fn sets_text(&self) -> String {
801        self.sets.iter().map(|set| format!("{} {:.2}", set.set, set.score)).collect::<Vec<_>>().join(", ")
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use serde_json::json;
808
809    use super::*;
810
811    fn questions() -> Vec<(String, Question)> {
812        vec![
813            ("temp".to_owned(), Question::score("How warm is it?", ["Cold", "Mild", "Hot"])),
814            ("humidity".to_owned(), Question::score("How humid is it?", ["Dry", "Normal", "Humid"])),
815            ("raining".to_owned(), Question::noul("Is it raining?")),
816            ("sky".to_owned(), Question::choice("What is the sky like?", [("clear", ""), ("cloudy", ""), ("storm", "")])),
817        ]
818    }
819
820    /// The weather from the examples: mostly mild, fairly humid, probably raining.
821    fn reply() -> DecisionResponse {
822        serde_json::from_value(json!({
823            "model": "typesafe/jev-1.13-20260917",
824            "answers": {
825                "temp": {"type": "score", "score": 1.1, "confidence": 0.7,
826                         "probabilities": {"0": 0.1, "1": 0.7, "2": 0.2}, "legend": {"0": "Cold", "1": "Mild", "2": "Hot"}},
827                "humidity": {"type": "score", "score": 1.5, "confidence": 0.6,
828                             "probabilities": {"0": 0.1, "1": 0.3, "2": 0.6}, "legend": {"0": "Dry", "1": "Normal", "2": "Humid"}},
829                "raining": {"type": "noul", "noul": 0.8},
830                "sky": {"type": "choice", "choice": "cloudy", "confidence": 0.5,
831                        "probabilities": {"clear": 0.1, "cloudy": 0.6, "storm": 0.3}},
832            },
833            "usage": {"input_tokens": 400, "output_tokens": 70},
834        }))
835        .unwrap()
836    }
837
838    const TERMS: &str = r#"
839        [terms]
840        cold = "temp.Cold"
841        mild = "temp.Mild"
842        hot = "temp.Hot"
843        humid = "humidity.Humid"
844        raining = "raining"
845        storm = "sky.storm"
846    "#;
847
848    fn rules(rules: &str) -> Result<Rules, String> {
849        let questions = questions();
850        Rules::parse(&format!("{TERMS}\n{rules}"), questions.iter().map(|(id, question)| (id.as_str(), question)))
851    }
852
853    /// The score `if` gives, under `logic`.
854    fn score(logic: &str, when: &str) -> f64 {
855        let rules = rules(&format!("{logic}\n[[rule]]\nif = \"{when}\"\nthen = \"x\"")).unwrap();
856        rules.evaluate(&reply()).unwrap().items[0].score
857    }
858
859    fn close(a: f64, b: f64) -> bool {
860        (a - b).abs() < 1e-9
861    }
862
863    #[test]
864    fn reads_each_kind_of_term() {
865        assert!(close(score("", "mild"), 0.7));
866        assert!(close(score("", "raining"), 0.8));
867        assert!(close(score("", "storm"), 0.3));
868    }
869
870    #[test]
871    fn combines_with_min_max_and_one_minus() {
872        assert!(close(score("", "raining AND humid"), 0.6));
873        assert!(close(score("", "raining OR humid"), 0.8));
874        assert!(close(score("", "NOT raining"), 0.2));
875        assert!(close(score("", "raining AND NOT hot"), 0.8));
876    }
877
878    #[test]
879    fn takes_the_files_and_and_or() {
880        assert!(close(score("[logic]\nand = \"product\"", "raining AND humid"), 0.48));
881        assert!(close(score("[logic]\nand = \"lukasiewicz\"", "raining AND humid"), 0.4));
882        assert!(close(score("[logic]\nor = \"probsum\"", "raining OR humid"), 0.92));
883        assert!(close(score("[logic]\nor = \"bounded\"", "raining OR humid"), 1.0));
884    }
885
886    #[test]
887    fn applies_hedges() {
888        assert!(close(score("", "VERY mild"), 0.49));
889        assert!(close(score("", "EXTREMELY mild"), 0.343));
890        assert!(close(score("", "SOMEWHAT mild"), 0.7f64.sqrt()));
891        assert!(close(score("", "INDEED mild"), 0.82));
892        assert!(close(score("", "INDEED cold"), 0.02));
893        // A hedge or NOT binds to the term after it, not to the rest of the rule.
894        assert!(close(score("", "NOT VERY mild"), 0.51));
895        assert!(close(score("", "VERY mild AND raining"), 0.49));
896    }
897
898    #[test]
899    fn and_binds_tighter_than_or_and_parentheses_group() {
900        // cold OR (raining AND humid) = max(0.1, 0.6)
901        assert!(close(score("", "cold OR raining AND humid"), 0.6));
902        // (cold OR raining) AND humid = min(0.8, 0.6), and (cold OR hot) AND raining = min(0.2, 0.8)
903        assert!(close(score("", "(cold OR raining) AND humid"), 0.6));
904        assert!(close(score("", "(cold OR hot) AND raining"), 0.2));
905        assert!(close(score("", "VERY (raining AND humid)"), 0.36));
906    }
907
908    #[test]
909    fn weights_rules_and_joins_the_same_then_by_or() {
910        let rules = rules(
911            r#"
912            [[rule]]
913            if = "cold"
914            then = "coat"
915
916            [[rule]]
917            if = "raining AND NOT hot"
918            then = "raincoat"
919
920            [[rule]]
921            if = "raining AND humid"
922            then = "coat"
923            weight = 0.5
924            "#,
925        )
926        .unwrap();
927        let outcome = rules.evaluate(&reply()).unwrap();
928        // In the order the file first names them.
929        let items: Vec<(&str, f64, bool)> = outcome.items.iter().map(|item| (item.item.as_str(), item.score, item.yes)).collect();
930        assert_eq!(items.len(), 2);
931        assert_eq!(items[0].0, "coat");
932        // max(0.1, 0.6 × 0.5)
933        assert!(close(items[0].1, 0.3));
934        assert!(!items[0].2);
935        assert_eq!(items[1].0, "raincoat");
936        assert!(close(items[1].1, 0.8) && items[1].2);
937        assert_eq!(outcome.items[0].rules.len(), 2);
938        assert_eq!(outcome.items[0].rules[1].when, "raining AND humid");
939        assert_eq!(outcome.text(), "coat      0.30\nraincoat  0.80  yes\nthreshold 0.50\n");
940    }
941
942    #[test]
943    fn decides_at_the_files_threshold() {
944        let outcome =
945            rules("[decide]\nthreshold = 0.25\n[[rule]]\nif = \"storm\"\nthen = \"stay in\"").unwrap().evaluate(&reply()).unwrap();
946        assert!(outcome.items[0].yes);
947        assert_eq!(outcome.threshold, 0.25);
948    }
949
950    #[test]
951    fn prints_the_outcome_as_json() {
952        let outcome = rules("[[rule]]\nif = \"raining\"\nthen = \"umbrella\"").unwrap().evaluate(&reply()).unwrap();
953        assert_eq!(
954            serde_json::to_value(&outcome).unwrap(),
955            json!({"threshold": 0.5, "items": [
956                {"item": "umbrella", "score": 0.8, "yes": true, "rules": [{"if": "raining", "weight": 1.0, "score": 0.8}]}
957            ]})
958        );
959    }
960
961    fn error(rules_text: &str) -> String {
962        rules(rules_text).unwrap_err()
963    }
964
965    #[test]
966    fn says_what_is_wrong_with_a_rule() {
967        let rule = |when: &str| error(&format!("[[rule]]\nif = \"{when}\"\nthen = \"x\""));
968        assert!(rule("").contains("empty"), "{}", rule(""));
969        assert!(rule("raining AND").contains("ends where a term should be"), "{}", rule("raining AND"));
970        assert!(rule("(raining AND hot").contains("isn't closed"));
971        assert!(rule("raining)").contains("`)` with no `(`"));
972        assert!(rule("raining hot").contains("`hot` where AND or OR should be"));
973        assert!(rule("raining XOR hot").contains("`XOR` isn't an operator: operators are AND, OR"));
974        assert!(rule("Hot").contains("`Hot` is neither"));
975        assert!(rule("AND hot").contains("`AND` where a term should be"));
976        // A term the file doesn't define, and the likeliest reason for one.
977        let wet = rule("wet");
978        assert!(wet.contains("rule 1 (`wet`): `wet` isn't in [terms]") && wet.contains("raining"), "{wet}");
979        assert!(rule("raining and hot").contains("the operator is written AND"));
980        assert!(rule("very hot").contains("the hedge is written VERY"));
981    }
982
983    #[test]
984    fn says_what_is_wrong_with_the_file() {
985        assert!(error("").contains("no rules"));
986        assert!(error("[[rule]]\nif = \"hot\"\nthen = \" \"").contains("`then` is empty"));
987        assert!(error("[[rule]]\nif = \"hot\"\nthen = \"x\"\nweight = 2.0").contains("weight is 2"));
988        assert!(error("[decide]\nthreshold = 1.5\n[[rule]]\nif = \"hot\"\nthen = \"x\"").contains("threshold is 1.5"));
989        assert!(error("[logic]\nand = \"average\"\n[[rule]]\nif = \"hot\"\nthen = \"x\"").contains("average"));
990        assert!(error("[[rule]]\nif = \"hot\"\nthen = \"x\"\nelse = \"y\"").contains("else"));
991    }
992
993    #[test]
994    fn checks_every_term_against_the_questions() {
995        let questions = questions();
996        let parse = |terms: &str| {
997            Rules::parse(&format!("[terms]\n{terms}\n[[rule]]\nif = \"t\"\nthen = \"x\""), questions.iter().map(|(id, q)| (id.as_str(), q)))
998        };
999        assert!(parse("t = \"temp.Hot\"").is_ok());
1000        // Exact: the level's own text.
1001        let hot = parse("t = \"temp.hot\"").unwrap_err();
1002        assert!(hot.contains("[terms] t: `temp` has no level `hot`; its levels are `temp.Cold`, `temp.Mild`, `temp.Hot`"), "{hot}");
1003        assert!(parse("t = \"sky.Storm\"").unwrap_err().contains("its options are `sky.clear`, `sky.cloudy`, `sky.storm`"));
1004        assert!(parse("t = \"temp\"").unwrap_err().contains("`temp` is a score: name one of its levels"));
1005        assert!(parse("t = \"sky\"").unwrap_err().contains("`sky` is a choice"));
1006        assert!(parse("t = \"raining.yes\"").unwrap_err().contains("write `raining` alone"));
1007        assert!(parse("t = \"wind.Strong\"").unwrap_err().contains("no question `wind`; the questions are"));
1008        // Checked even when no rule uses it: a typo there is still a typo.
1009        let unused = Rules::parse(
1010            "[terms]\nt = \"temp.Hot\"\nu = \"temp.Warm\"\n[[rule]]\nif = \"t\"\nthen = \"x\"",
1011            questions.iter().map(|(id, q)| (id.as_str(), q)),
1012        );
1013        assert!(unused.unwrap_err().contains("[terms] u"));
1014        assert!(parse("T = \"temp.Hot\"").unwrap_err().contains("[terms] `T`: a term is a lowercase word"));
1015    }
1016
1017    #[test]
1018    fn a_question_id_may_have_a_dot() {
1019        let questions = [("weather.temp".to_owned(), Question::score("How warm?", ["Cold", "Hot"]))];
1020        let parsed = Rules::parse(
1021            "[terms]\nhot = \"weather.temp.Hot\"\n[[rule]]\nif = \"hot\"\nthen = \"x\"",
1022            questions.iter().map(|(id, q)| (id.as_str(), q)),
1023        );
1024        let hot = &parsed.unwrap().terms["hot"];
1025        assert_eq!((hot.id.as_str(), hot.kind, hot.selected), ("weather.temp", Kind::Score, 1));
1026    }
1027
1028    #[test]
1029    fn a_longer_question_id_wins_over_its_prefix() {
1030        let questions = [
1031            ("weather".to_owned(), Question::score("How is it?", ["Fine", "Bad"])),
1032            ("weather.temp".to_owned(), Question::score("How warm?", ["Cold", "Hot"])),
1033        ];
1034        let parsed = Rules::parse(
1035            "[terms]\nhot = \"weather.temp.Hot\"\nbad = \"weather.Bad\"\n[[rule]]\nif = \"hot OR bad\"\nthen = \"x\"",
1036            questions.iter().map(|(id, q)| (id.as_str(), q)),
1037        )
1038        .unwrap();
1039        let (hot, bad) = (&parsed.terms["hot"], &parsed.terms["bad"]);
1040        assert_eq!((hot.id.as_str(), hot.selected), ("weather.temp", 1));
1041        assert_eq!((bad.id.as_str(), bad.selected), ("weather", 1));
1042    }
1043
1044    #[test]
1045    fn refuses_a_rule_too_deep_to_evaluate() {
1046        // Deep enough to overflow the stack if it were parsed: it must be refused before that.
1047        let deep = format!("{}raining{}", "NOT (".repeat(100_000), ")".repeat(100_000));
1048        let long = rule_error(&deep);
1049        assert!(long.contains("over the 256 a rule may have"), "{long}");
1050        let chain = vec!["raining"; 200].join(" AND ");
1051        assert!(rule_error(&chain).contains("over the 256"));
1052        // And a rule of a sensible length is fine.
1053        assert!(rules(&format!("[[rule]]\nif = \"{}\"\nthen = \"x\"", vec!["raining"; 100].join(" AND "))).is_ok());
1054    }
1055
1056    fn rule_error(when: &str) -> String {
1057        error(&format!("[[rule]]\nif = \"{when}\"\nthen = \"x\""))
1058    }
1059
1060    #[test]
1061    fn a_missing_probability_is_an_error_not_a_zero() {
1062        let rules = rules("[[rule]]\nif = \"hot OR storm\"\nthen = \"x\"").unwrap();
1063        let mut level_gone = reply();
1064        let crate::Answer::Score(temp) = level_gone.answers.get_mut("temp").unwrap() else { unreachable!() };
1065        // Its share moved to another level, so that the rest still adds up to 1.
1066        temp.probabilities.remove(&2);
1067        temp.probabilities.insert(1, 0.9);
1068        assert_eq!(rules.evaluate(&level_gone), Err(crate::Error::MissingProbability { id: "temp".to_owned(), label: "Hot".to_owned() }));
1069        let mut option_gone = reply();
1070        let crate::Answer::Choice(sky) = option_gone.answers.get_mut("sky").unwrap() else { unreachable!() };
1071        sky.probabilities.remove("storm");
1072        sky.probabilities.insert("cloudy".to_owned(), 0.9);
1073        assert_eq!(rules.evaluate(&option_gone).unwrap_err().to_string(), "question `sky` gives no probability for `storm`");
1074    }
1075
1076    #[test]
1077    fn an_answer_that_isnt_probabilities_is_an_error_not_clamped() {
1078        let rules = rules("[[rule]]\nif = \"NOT raining OR hot OR storm\"\nthen = \"x\"").unwrap();
1079        let why = |change: &dyn Fn(&mut DecisionResponse)| {
1080            let mut reply = reply();
1081            change(&mut reply);
1082            match rules.evaluate(&reply) {
1083                Err(error @ crate::Error::BadAnswer { .. }) => error.to_string(),
1084                other => panic!("{other:?}"),
1085            }
1086        };
1087        let noul = |value: f64| {
1088            move |reply: &mut DecisionResponse| {
1089                reply.answers.insert("raining".to_owned(), crate::Answer::Noul(crate::NoulAnswer { noul: value })).map(drop).unwrap_or(())
1090            }
1091        };
1092        // 1.2 would have read as a yes of 1.2, and NOT it as −0.2.
1093        assert_eq!(
1094            why(&noul(1.2)),
1095            "the answer to question `raining` can't be read: the probability of yes is 1.2, which isn't a probability from 0 to 1"
1096        );
1097        assert!(why(&noul(f64::NAN)).contains("NaN"));
1098        let temp = |levels: [(u8, f64); 3]| {
1099            move |reply: &mut DecisionResponse| {
1100                let crate::Answer::Score(temp) = reply.answers.get_mut("temp").unwrap() else { unreachable!() };
1101                temp.probabilities = levels.into();
1102            }
1103        };
1104        assert!(why(&temp([(0, 0.1), (1, 0.9), (2, 0.2)])).contains("its probabilities add up to 1.200, not 1"));
1105        assert!(why(&temp([(0, 0.9), (1, -0.1), (2, 0.2)])).contains("level 1's probability is -0.1"));
1106        assert!(why(&temp([(0, 0.1), (1, 0.9), (3, 0.0)])).contains("level 3, and the question's levels are 0 to 2"));
1107        assert!(why(&|reply: &mut DecisionResponse| {
1108            let crate::Answer::Choice(sky) = reply.answers.get_mut("sky").unwrap() else { unreachable!() };
1109            sky.probabilities.insert("hail".to_owned(), 0.0);
1110        })
1111        .contains("`hail`, which isn't one of the question's options"));
1112        // Two places of rounding is not a mistake: 0.33 three times is 0.99.
1113        let mut rounded = reply();
1114        let crate::Answer::Choice(sky) = rounded.answers.get_mut("sky").unwrap() else { unreachable!() };
1115        sky.probabilities = [("clear", 0.33), ("cloudy", 0.33), ("storm", 0.33)].map(|(option, p)| (option.to_owned(), p)).into();
1116        assert!(rules.evaluate(&rounded).is_ok());
1117        // The drawings check the same way.
1118        let mut bad = reply();
1119        noul(1.2)(&mut bad);
1120        assert!(rules.graph_text(Some(&bad)).is_err() && rules.graph_svg(Some(&bad)).is_err());
1121    }
1122
1123    #[test]
1124    fn a_missing_answer_is_an_error_not_a_zero() {
1125        let rules = rules("[[rule]]\nif = \"raining\"\nthen = \"umbrella\"").unwrap();
1126        let mut reply = reply();
1127        reply.answers.remove("raining");
1128        assert_eq!(rules.evaluate(&reply), Err(crate::Error::MissingAnswer("raining".to_owned())));
1129    }
1130}