1use expectation_model::text;
8use expectation_model::{
9 Constraint, Diagnostic, EventId, Fact, Interval, PhraseId, Said, Shift, Span, VarId, VarKind, Variable, Vocabulary,
10};
11
12pub enum Meaning {
13 Fact(Fact),
14 Event(EventId),
15 Shift(VarId, Shift),
17 Phrase(PhraseId),
19}
20
21impl Meaning {
22 pub fn said(&self) -> Said {
24 match self {
25 Meaning::Fact(f) => Said::Fact(f.var, f.constraint),
26 Meaning::Event(e) => Said::Event(*e),
27 Meaning::Shift(v, s) => Said::Shift(*v, *s),
28 Meaning::Phrase(p) => Said::Phrase(*p),
29 }
30 }
31}
32
33const COPULAS: &[(&str, bool)] = &[
35 (" is not ", true),
36 (" are not ", true),
37 (" isn't ", true),
38 (" aren't ", true),
39 (" doesn't have ", true),
40 (" don't have ", true),
41 (" is ", false),
42 (" are ", false),
43 (" has ", false),
44 (" have ", false),
45];
46
47pub fn resolve(raw: &str, span: &Span, vocab: &Vocabulary) -> Result<Meaning, Diagnostic> {
48 let k = text::key(raw);
49 if let Some(e) = vocab.events.iter().find(|e| e.key == k) {
50 return Ok(Meaning::Event(e.id));
51 }
52 if let Some(p) = vocab.phrases.iter().find(|p| p.key == k) {
53 return Ok(Meaning::Phrase(p.id));
54 }
55 if let Some(shift) = shift(raw, &k, span, vocab) {
56 return shift;
57 }
58 let mut best: Option<(&Variable, bool, &str)> = None;
59 for v in &vocab.variables {
60 let Some(rest) = k.strip_prefix(&v.key) else { continue };
61 let Some((neg, pred)) = COPULAS.iter().find_map(|(c, neg)| Some((*neg, rest.strip_prefix(c)?))) else {
62 continue;
63 };
64 if best.is_none_or(|(b, ..)| v.key.len() > b.key.len()) {
65 best = Some((v, neg, pred));
66 }
67 }
68 let Some((var, neg, pred)) = best else { return Err(unknown_step(raw, &k, span, vocab)) };
69 let constraint = match &var.kind {
70 VarKind::Enum(values) => enum_constraint(var, values, pred, neg),
71 VarKind::Number { min, max, unit } => number_constraint(pred, neg, *min, *max, unit),
72 }
73 .map_err(|(msg, help)| {
74 let d = Diagnostic::error("bad-value", msg).at(span, "");
75 if let Some(h) = help { d.help(h) } else { d }
76 })?;
77 Ok(Meaning::Fact(Fact { var: var.id, constraint, span: span.clone() }))
78}
79
80fn shift(raw: &str, k: &str, span: &Span, vocab: &Vocabulary) -> Option<Result<Meaning, Diagnostic>> {
83 let (var, rest) = vocab
84 .variables
85 .iter()
86 .filter_map(|v| Some((v, k.strip_prefix(&v.key)?)))
87 .filter(|(_, rest)| rest.starts_with(" goes ") || *rest == " stays the same")
88 .max_by_key(|(v, _)| v.key.len())?;
89 if rest == " stays the same" {
90 return Some(Ok(Meaning::Shift(var.id, Shift::Same)));
91 }
92 let bad = |msg: String| Some(Err(Diagnostic::error("bad-value", msg).at(span, "")));
93 let (sign, n) = if let Some(n) = rest.strip_prefix(" goes up by ") {
94 (1.0, n)
95 } else if let Some(n) = rest.strip_prefix(" goes down by ") {
96 (-1.0, n)
97 } else {
98 return bad(format!("`{raw}`: expected `goes up by <n>` or `goes down by <n>`"));
99 };
100 let VarKind::Number { unit, .. } = &var.kind else {
101 return bad(format!("{} isn't a number, so it can't go up or down", var.subject));
102 };
103 match parse_number(n, unit) {
104 Some(v) => Some(Ok(Meaning::Shift(var.id, Shift::By(sign * v)))),
105 None => bad(format!("expected a number after `goes {} by`", if sign > 0.0 { "up" } else { "down" })),
106 }
107}
108
109type Problem = (String, Option<String>);
110
111fn enum_constraint(var: &Variable, values: &[String], pred: &str, neg: bool) -> Result<Constraint, Problem> {
112 let mut mask = 0u64;
113 for part in pred.split(" or ") {
114 let Some(i) = var.value_index(part) else {
115 let help = match text::closest(&text::normalize(part), values.iter().map(|v| v.as_str())) {
116 Some(c) => format!("did you mean `{c}`?"),
117 None => format!("{} is one of: {}", var.subject, values.join(", ")),
118 };
119 return Err((format!("`{part}` isn't a value of {}", var.subject), Some(help)));
120 };
121 mask |= 1 << i;
122 }
123 let Constraint::OneOf(all) = var.full() else { unreachable!() };
124 Ok(Constraint::OneOf(if neg { all & !mask } else { mask }))
125}
126
127fn number_constraint(pred: &str, neg: bool, min: f64, max: f64, unit: &Option<String>) -> Result<Constraint, Problem> {
128 if neg {
129 return Err((
130 "a number can't be negated".into(),
131 Some("say `above`, `below`, `at least` or `at most` instead".into()),
132 ));
133 }
134 let num = |s: &str| -> Result<f64, Problem> {
135 parse_number(s, unit).ok_or_else(|| {
136 let expected = match unit {
137 Some(u) => format!("a number, optionally followed by `{u}`"),
138 None => "a number".into(),
139 };
140 (format!("expected {expected}, found `{s}`"), None)
141 })
142 };
143 let range = if let Some(rest) = pred.strip_prefix("between ") {
144 let (a, b) = rest.split_once(" and ").ok_or_else(|| ("expected `between <a> and <b>`".to_string(), None))?;
145 Interval::closed(num(a)?, num(b)?)
146 } else if let Some(v) = strip_any(pred, &["above ", "over ", "more than ", "greater than ", "higher than "]) {
147 Interval { lo: num(v)?, lo_open: true, hi: max, hi_open: false }
148 } else if let Some(v) = strip_any(pred, &["below ", "under ", "less than ", "fewer than ", "lower than "]) {
149 Interval { lo: min, lo_open: false, hi: num(v)?, hi_open: true }
150 } else if let Some(v) = pred.strip_prefix("at least ") {
151 Interval { lo: num(v)?, lo_open: false, hi: max, hi_open: false }
152 } else if let Some(v) = pred.strip_prefix("at most ") {
153 Interval { lo: min, lo_open: false, hi: num(v)?, hi_open: false }
154 } else {
155 Interval::point(num(pred)?)
156 };
157 let clipped = range.intersect(&Interval::closed(min, max));
158 if clipped.is_empty() {
159 let u = unit.as_deref().map(|u| format!(" {u}")).unwrap_or_default();
160 return Err((format!("no value in {}..{}{u} satisfies this", text::number(min), text::number(max)), None));
161 }
162 Ok(Constraint::Range(clipped))
163}
164
165fn strip_any<'a>(s: &'a str, prefixes: &[&str]) -> Option<&'a str> {
166 prefixes.iter().find_map(|p| s.strip_prefix(p))
167}
168
169fn parse_number(s: &str, unit: &Option<String>) -> Option<f64> {
171 let s = s.trim();
172 let end = s.find(|c: char| !(c.is_ascii_digit() || matches!(c, '.' | '-' | '+'))).unwrap_or(s.len());
173 let v: f64 = s[..end].parse().ok()?;
174 let rest = s[end..].trim();
175 let singular = |s: &str| text::normalize(s).trim_end_matches('s').to_string();
177 let unit_ok = rest.is_empty() || unit.as_deref().is_some_and(|u| singular(u) == singular(rest));
178 unit_ok.then_some(v)
179}
180
181fn unknown_step(raw: &str, k: &str, span: &Span, vocab: &Vocabulary) -> Diagnostic {
182 let d = Diagnostic::error("unknown-step", format!("`{raw}` isn't in the vocabulary")).at(span, "");
183 let subject = COPULAS.iter().find_map(|(c, _)| k.split_once(c).map(|(s, _)| s));
184 let suggestion = match subject {
185 Some(s) => text::closest(s, vocab.variables.iter().map(|v| v.key.as_str()))
186 .and_then(|key| vocab.variables.iter().find(|v| v.key == key))
187 .map(|v| format!("did you mean `{}`? It {}", v.subject, describe_kind(v))),
188 None => text::closest(k, vocab.events.iter().map(|e| e.key.as_str()))
189 .and_then(|key| vocab.events.iter().find(|e| e.key == key))
190 .map(|e| format!("did you mean the event `{}`?", e.text)),
191 };
192 d.help(suggestion.unwrap_or_else(|| "declare it in a `Vocabulary:` block, with `State:` or `Event:`".into()))
193}
194
195fn describe_kind(v: &Variable) -> String {
196 match &v.kind {
197 VarKind::Enum(values) => format!("is {}", values.join(" | ")),
198 VarKind::Number { min, max, unit } => {
199 format!(
200 "is a number in {}..{}{}",
201 text::number(*min),
202 text::number(*max),
203 unit.as_deref().map(|u| format!(" {u}")).unwrap_or_default()
204 )
205 }
206 }
207}
208
209pub fn declare_state(decl: &str, span: &Span, id: VarId) -> Result<Variable, Diagnostic> {
211 let bad = |msg: &str| {
212 Diagnostic::error("bad-state", msg.to_string())
213 .at(span, "")
214 .help("write `State: the kettle is off | heating` or `State: the dial is 0..100 %`")
215 };
216 let (at, len) = [" is ", " are ", " has ", " have "]
217 .iter()
218 .filter_map(|c| decl.find(c).map(|i| (i, c.len())))
219 .min()
220 .ok_or_else(|| bad("expected `<subject> is <values>`"))?;
221 let subject = decl[..at].trim().to_string();
222 let values = decl[at + len..].trim();
223 let kind = if let Some((lo, hi)) = values.split_once("..") {
224 let lo: f64 = lo.trim().parse().map_err(|_| bad("expected a number before `..`"))?;
225 let hi = hi.trim();
226 let end = hi.find(|c: char| !(c.is_ascii_digit() || matches!(c, '.' | '-' | '+'))).unwrap_or(hi.len());
227 let max: f64 = hi[..end].parse().map_err(|_| bad("expected a number after `..`"))?;
228 let unit = Some(hi[end..].trim().to_string()).filter(|u| !u.is_empty());
229 if lo >= max {
230 return Err(bad("the range is empty"));
231 }
232 VarKind::Number { min: lo, max, unit }
233 } else {
234 let names: Vec<String> = values.split('|').map(|v| v.trim().to_string()).filter(|v| !v.is_empty()).collect();
235 if names.len() < 2 {
236 return Err(bad("a state needs at least two values, separated by `|`"));
237 }
238 if names.len() > expectation_model::vocabulary::MAX_VALUES {
239 return Err(bad("a state can have at most 64 values"));
240 }
241 for (i, n) in names.iter().enumerate() {
242 if names[..i].iter().any(|m| text::normalize(m) == text::normalize(n)) {
243 return Err(bad(&format!("`{n}` is listed twice")));
244 }
245 if n.contains(" or ") {
246 return Err(bad(&format!("`{n}` contains ` or `, which steps use to list values")));
247 }
248 }
249 VarKind::Enum(names)
250 };
251 let verb = decl[at + 1..at + len - 1].to_string();
252 Ok(Variable { id, key: text::key(&subject), subject, kind, verb, span: span.clone(), set: None, shows: vec![] })
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use expectation_model::Event;
259
260 fn vocab() -> Vocabulary {
261 let span = Span::new("t", 1);
262 let kettle = declare_state("the kettle is off | heating | dry", &span, VarId(0)).ok().unwrap();
263 let dial = declare_state("the dial is 0..100 %", &span, VarId(1)).ok().unwrap();
264 let reset = Event {
265 id: EventId(0),
266 text: "the user presses reset".into(),
267 key: text::key("the user presses reset"),
268 span: span.clone(),
269 say: None,
270 };
271 Vocabulary { variables: vec![kettle, dial], events: vec![reset], phrases: vec![] }
272 }
273
274 fn fact(s: &str) -> Constraint {
275 match resolve(s, &Span::new("t", 1), &vocab()) {
276 Ok(Meaning::Fact(f)) => f.constraint,
277 Ok(Meaning::Event(_) | Meaning::Shift(..) | Meaning::Phrase(_)) => panic!("{s}: not a fact"),
278 Err(d) => panic!("{s}: {}", d.message),
279 }
280 }
281
282 #[test]
283 fn enum_facts() {
284 assert_eq!(fact("the kettle is heating"), Constraint::OneOf(0b010));
285 assert_eq!(fact("The Kettle is Off"), Constraint::OneOf(0b001));
286 assert_eq!(fact("the kettle is not dry"), Constraint::OneOf(0b011));
287 assert_eq!(fact("kettle is heating or dry"), Constraint::OneOf(0b110));
288 }
289
290 #[test]
291 fn number_facts() {
292 let r = |lo, lo_open, hi, hi_open| Constraint::Range(Interval { lo, lo_open, hi, hi_open });
293 assert_eq!(fact("the dial is above 55 %"), r(55.0, true, 100.0, false));
294 assert_eq!(fact("the dial is 0%"), r(0.0, false, 0.0, false));
295 assert_eq!(fact("the dial is at most 10"), r(0.0, false, 10.0, false));
296 assert_eq!(fact("the dial is between 20 and 40 %"), r(20.0, false, 40.0, false));
297 }
298
299 #[test]
300 fn has_reads_like_has() {
301 let span = Span::new("t", 1);
302 let plan = declare_state("the drawing has 0..100 shapes", &span, VarId(0)).ok().unwrap();
303 let v = Vocabulary { variables: vec![plan], events: vec![], phrases: vec![] };
304 let c = |s: &str| match resolve(s, &span, &v) {
305 Ok(Meaning::Fact(f)) => f.constraint,
306 _ => panic!("{s}"),
307 };
308 let p = &v.variables[0];
309 assert_eq!(p.describe(&c("the drawing has 1 shape")), "the drawing has 1 shape");
310 assert_eq!(p.describe(&c("the drawing has more than 3 shapes")), "the drawing has more than 3 shapes");
311 assert_eq!(p.describe(&c("the drawing has at least 1 shape")), "the drawing has at least 1 shape");
312 }
313
314 #[test]
315 fn relative_thens() {
316 let v = vocab();
317 let span = Span::new("t", 1);
318 let shift = |s: &str| match resolve(s, &span, &v) {
319 Ok(Meaning::Shift(_, shift)) => Ok(shift),
320 Ok(_) => panic!("{s}: not a shift"),
321 Err(d) => Err(d.code),
322 };
323 assert_eq!(shift("the dial goes up by 10 %"), Ok(Shift::By(10.0)));
324 assert_eq!(shift("the dial goes down by 5"), Ok(Shift::By(-5.0)));
325 assert_eq!(shift("the kettle stays the same"), Ok(Shift::Same));
326 assert_eq!(shift("the kettle goes up by 1"), Err("bad-value"));
327 }
328
329 #[test]
330 fn events_and_errors() {
331 let v = vocab();
332 let span = Span::new("t", 1);
333 assert!(matches!(resolve("The user presses reset.", &span, &v), Ok(Meaning::Event(EventId(0)))));
334 let err = |s| resolve(s, &span, &v).err().unwrap();
335 assert_eq!(err("the kettle is boiling").code, "bad-value");
336 assert!(err("the kettel is heating").help.unwrap().contains("the kettle"));
337 assert!(err("the user presses rest").help.unwrap().contains("reset"));
338 assert_eq!(err("the dial is above 120 %").code, "bad-value");
339 assert_eq!(err("the dial is not 5").code, "bad-value");
340 }
341}