Skip to main content

datalogic_rs/
check.rs

1//! [`Engine::check`](crate::Engine::check): the problems the engine can see
2//! in a rule before it runs, collected in one pass.
3//!
4//! The walk mirrors the compiler's (`compile::walker`): the same routing for
5//! template keys, escaped keys, built-ins, custom operators and unknown
6//! keys, and the same exceptions (an argument form the compiler does not
7//! compile, the tensor wire form). What it reports is read from the
8//! operator table, so an argument count is an error exactly when the
9//! operator would reject it.
10
11use std::fmt;
12
13use datavalue::OwnedDataValue;
14use serde::Serialize;
15
16use crate::node::MAX_COMPILE_DEPTH;
17use crate::operators::meta::{ArgsForm, Effect, Extra, Miss};
18use crate::{Engine, OpCode};
19
20/// Which mode a rule is checked in. See [`Engine::check`](crate::Engine::check).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum CheckMode {
24    /// The engine's own mode, as [`Engine::compile`](crate::Engine::compile)
25    /// would compile the rule.
26    Engine,
27    /// Outside templating mode, as
28    /// [`Engine::compile_strict`](crate::Engine::compile_strict) would.
29    Strict,
30    /// In templating mode, as `Engine::compile_template` would (that method
31    /// needs the `templating` feature; the check does not).
32    Template,
33}
34
35/// How serious a [`Diagnostic`] is.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
37#[serde(rename_all = "lowercase")]
38#[non_exhaustive]
39pub enum Severity {
40    /// The rule fails to compile, or fails whenever this part of it runs.
41    Error,
42    /// The rule runs, but probably not as its author meant.
43    Warning,
44}
45
46/// What a [`Diagnostic`] is about. Serialised as the variant name.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
48#[non_exhaustive]
49pub enum DiagnosticCode {
50    /// The rule is not valid JSON.
51    Unparsable,
52    /// A single-key object whose key is no operator this engine knows.
53    UnknownOperator,
54    /// An object with several keys outside templating mode, where only an
55    /// operator call (one key) is valid.
56    NotAnOperator,
57    /// `and`, `or` or `if` given a single argument instead of an array.
58    ArgumentForm,
59    /// An argument count the operator rejects (an error), or arguments it
60    /// never evaluates (a warning).
61    ArgumentCount,
62    /// A literal timezone name that is not in the timezone database.
63    InvalidTimezone,
64    /// In a template, a key that is an output field but is one edit away
65    /// from an operator name.
66    SimilarToOperator,
67    /// The compiler rejected the rule for a reason the checks above do not
68    /// cover.
69    Compile,
70    /// A custom operator's own [`check`](crate::CustomOperator::check)
71    /// rejected the call.
72    OperatorCheck,
73}
74
75/// One problem in a rule, located by an RFC 6901 JSON Pointer into it.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77#[non_exhaustive]
78pub struct Diagnostic {
79    /// What the problem is.
80    pub code: DiagnosticCode,
81    /// Whether the rule can run as written.
82    pub severity: Severity,
83    /// A sentence describing the problem, naming the operator involved.
84    pub message: String,
85    /// Where the problem is: an [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901)
86    /// JSON Pointer into the rule, to the object (or argument) at fault.
87    /// `""` is the whole rule. `{"if": [x, {"vr": 1}]}` reports the
88    /// unknown `vr` at `/if/1`.
89    pub pointer: String,
90    /// The operator (or key) the problem is about, when there is one.
91    pub operator: Option<String>,
92}
93
94impl Diagnostic {
95    /// An error from a custom operator's
96    /// [`check`](crate::CustomOperator::check): the call will fail, so
97    /// [`Engine::compile_checked`](crate::Engine::compile_checked) refuses
98    /// the rule. It points at the whole call until
99    /// [`Self::at_argument`] narrows it.
100    pub fn error(message: impl Into<String>) -> Self {
101        Self::from_operator(Severity::Error, message.into())
102    }
103
104    /// A warning from a custom operator's
105    /// [`check`](crate::CustomOperator::check): the call runs, but probably
106    /// not as meant.
107    pub fn warning(message: impl Into<String>) -> Self {
108        Self::from_operator(Severity::Warning, message.into())
109    }
110
111    /// Point at argument `index` of the call instead of the whole call. In
112    /// a diagnostic an operator returns, `pointer` is relative to the call
113    /// (`"/1"` for argument 1); the checker places it in the rule.
114    #[must_use]
115    pub fn at_argument(mut self, index: usize) -> Self {
116        self.pointer = format!("/{index}");
117        self
118    }
119
120    fn from_operator(severity: Severity, message: String) -> Self {
121        Diagnostic {
122            code: DiagnosticCode::OperatorCheck,
123            severity,
124            message,
125            pointer: String::new(),
126            operator: None,
127        }
128    }
129}
130
131impl fmt::Display for Diagnostic {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        let severity = match self.severity {
134            Severity::Error => "error",
135            Severity::Warning => "warning",
136        };
137        let at = if self.pointer.is_empty() {
138            "/"
139        } else {
140            &self.pointer
141        };
142        write!(f, "{severity} at {at}: {}", self.message)
143    }
144}
145
146/// A rule [`Engine::compile_checked`](crate::Engine::compile_checked)
147/// refused: every diagnostic the check found, errors and warnings, in rule
148/// order.
149#[derive(Debug, Clone, PartialEq, Eq)]
150#[non_exhaustive]
151pub struct CompileError {
152    /// At least one of them is an error.
153    pub diagnostics: Vec<Diagnostic>,
154}
155
156impl fmt::Display for CompileError {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        let errors = self
159            .diagnostics
160            .iter()
161            .filter(|d| d.severity == Severity::Error)
162            .count();
163        write!(f, "rule has {errors} error(s)")?;
164        for d in &self.diagnostics {
165            write!(f, "\n  {d}")?;
166        }
167        Ok(())
168    }
169}
170
171impl std::error::Error for CompileError {}
172
173impl CompileError {
174    pub(crate) fn from_error(err: crate::Error) -> Self {
175        let code = match err.kind {
176            crate::ErrorKind::ParseError(_) => DiagnosticCode::Unparsable,
177            _ => DiagnosticCode::Compile,
178        };
179        CompileError {
180            diagnostics: vec![Diagnostic {
181                code,
182                severity: Severity::Error,
183                message: crate::Error::new(err.kind).to_string(),
184                pointer: String::new(),
185                operator: None,
186            }],
187        }
188    }
189}
190
191/// Check `rule` on `engine`, in templating mode when `templating` holds.
192pub(crate) fn check(engine: &Engine, rule: &OwnedDataValue, templating: bool) -> Vec<Diagnostic> {
193    let mut checker = Checker {
194        engine,
195        templating,
196        escape: if templating {
197            engine.template_key_escape()
198        } else {
199            None
200        },
201        out: Vec::new(),
202        depth: 0,
203        too_deep: false,
204        protected: 0,
205        unread: 0,
206    };
207    checker.node(rule, &mut String::new());
208    checker.out
209}
210
211struct Checker<'e> {
212    engine: &'e Engine,
213    templating: bool,
214    escape: Option<char>,
215    out: Vec<Diagnostic>,
216    /// Nesting depth, counted as the compiler counts it, so a rule too
217    /// deep to compile is reported instead of overflowing the stack here.
218    depth: usize,
219    /// The depth error has been reported (once per rule).
220    too_deep: bool,
221    /// Inside this many arms whose errors `try` catches: an error there
222    /// cannot fail the rule, so it is reported as a warning.
223    protected: usize,
224    /// Inside this many arguments the call never evaluates: only what
225    /// stops the rule compiling (reported directly, not through
226    /// [`Self::report`]) is reported there.
227    unread: usize,
228}
229
230/// Run `f` with `token` appended to `pointer`, then restore it.
231fn under(pointer: &mut String, token: &str, f: impl FnOnce(&mut String)) {
232    let len = pointer.len();
233    crate::node::push_pointer_token(pointer, token);
234    f(pointer);
235    pointer.truncate(len);
236}
237
238impl Checker<'_> {
239    fn report(
240        &mut self,
241        code: DiagnosticCode,
242        severity: Severity,
243        pointer: &str,
244        operator: &str,
245        message: String,
246    ) {
247        if self.unread > 0 {
248            return;
249        }
250        let severity = if self.protected > 0 {
251            Severity::Warning
252        } else {
253            severity
254        };
255        self.out.push(Diagnostic {
256            code,
257            severity,
258            message,
259            pointer: pointer.to_string(),
260            operator: Some(operator.to_string()),
261        });
262    }
263
264    fn node(&mut self, node: &OwnedDataValue, pointer: &mut String) {
265        if self.depth >= MAX_COMPILE_DEPTH {
266            if !self.too_deep {
267                self.too_deep = true;
268                self.out.push(Diagnostic {
269                    code: DiagnosticCode::Compile,
270                    severity: Severity::Error,
271                    message: format!(
272                        "rule nesting exceeds the maximum compile depth of {}",
273                        MAX_COMPILE_DEPTH
274                    ),
275                    pointer: pointer.clone(),
276                    operator: None,
277                });
278            }
279            return;
280        }
281        self.depth += 1;
282        self.node_inner(node, pointer);
283        self.depth -= 1;
284    }
285
286    fn node_inner(&mut self, node: &OwnedDataValue, pointer: &mut String) {
287        match node {
288            OwnedDataValue::Object(pairs) if pairs.len() > 1 => {
289                if self.templating {
290                    for (key, value) in pairs {
291                        under(pointer, key, |p| self.node(value, p));
292                    }
293                } else {
294                    self.out.push(Diagnostic {
295                        code: DiagnosticCode::NotAnOperator,
296                        severity: Severity::Error,
297                        message: format!(
298                            "an object with {} keys is not an operator call; it is only valid as an output template",
299                            pairs.len()
300                        ),
301                        pointer: pointer.clone(),
302                        operator: None,
303                    });
304                }
305            }
306            OwnedDataValue::Object(pairs) if pairs.len() == 1 => {
307                let (op, argv) = &pairs[0];
308                self.call(op, argv, pointer);
309            }
310            OwnedDataValue::Array(items) => {
311                let mut buf = itoa::Buffer::new();
312                for (i, item) in items.iter().enumerate() {
313                    under(pointer, buf.format(i), |p| self.node(item, p));
314                }
315            }
316            _ => {}
317        }
318    }
319
320    /// Check each argument, where the compiler would compile it.
321    fn args(&mut self, op: &str, argv: &OwnedDataValue, pointer: &mut String) {
322        self.args_where(op, argv, pointer, |_| ArgUse::Runs);
323    }
324
325    /// Check the arguments as `uses` says each one runs: an argument never
326    /// evaluated cannot fail the rule, so only what stops it compiling is
327    /// reported there, and one whose errors are caught has its errors
328    /// reported as warnings.
329    fn args_where(
330        &mut self,
331        op: &str,
332        argv: &OwnedDataValue,
333        pointer: &mut String,
334        uses: impl Fn(usize) -> ArgUse,
335    ) {
336        let one = |this: &mut Self, i: usize, item: &OwnedDataValue, p: &mut String| match uses(i) {
337            // Still walked: the compiler compiles it, so a rule that
338            // cannot compile is still reported.
339            ArgUse::Never => {
340                this.unread += 1;
341                this.node(item, p);
342                this.unread -= 1;
343            }
344            ArgUse::Runs => this.node(item, p),
345            ArgUse::Caught => {
346                this.protected += 1;
347                this.node(item, p);
348                this.protected -= 1;
349            }
350        };
351        under(pointer, op, |p| match argv {
352            OwnedDataValue::Array(items) => {
353                let mut buf = itoa::Buffer::new();
354                for (i, item) in items.iter().enumerate() {
355                    under(p, buf.format(i), |p| one(self, i, item, p));
356                }
357            }
358            other => one(self, 0, other, p),
359        });
360    }
361
362    fn call(&mut self, op: &str, argv: &OwnedDataValue, pointer: &mut String) {
363        // An escaped key is an output field, checked before any operator.
364        // `escape` is `None` outside templating mode.
365        if self.escape.is_some_and(|c| op.starts_with(c)) {
366            under(pointer, op, |p| self.node(argv, p));
367            return;
368        }
369        let count = match argv {
370            OwnedDataValue::Array(items) => items.len(),
371            _ => 1,
372        };
373        if let Some(opcode) = self.engine.builtin(op) {
374            self.builtin(op, opcode, argv, count, pointer);
375            return;
376        }
377        if let Some(custom) = self.engine.custom_operator(op) {
378            let info = custom.info();
379            if !info.accepts(count) {
380                let message = format!(
381                    "`{op}` takes {}, not {count}",
382                    describe_count(info.min_args, info.max_args)
383                );
384                self.report(
385                    DiagnosticCode::ArgumentCount,
386                    Severity::Error,
387                    pointer,
388                    op,
389                    message,
390                );
391            } else {
392                let args = match argv {
393                    OwnedDataValue::Array(items) => items.as_slice(),
394                    other => std::slice::from_ref(other),
395                };
396                if let Err(d) = custom.check(args) {
397                    self.operator_diagnostic(d, op, argv, pointer);
398                }
399            }
400            self.args(op, argv, pointer);
401            return;
402        }
403        if self.templating {
404            if let Some(near) = self.similar_operator(op) {
405                let message =
406                    format!("`{op}` is an output field here; did you mean the operator `{near}`?");
407                self.report(
408                    DiagnosticCode::SimilarToOperator,
409                    Severity::Warning,
410                    pointer,
411                    op,
412                    message,
413                );
414            }
415            under(pointer, op, |p| self.node(argv, p));
416            return;
417        }
418        let message = match self.similar_operator(op) {
419            Some(near) => format!("unknown operator `{op}`; did you mean `{near}`?"),
420            None => format!("unknown operator `{op}`"),
421        };
422        self.report(
423            DiagnosticCode::UnknownOperator,
424            Severity::Error,
425            pointer,
426            op,
427            message,
428        );
429        self.args(op, argv, pointer);
430    }
431
432    /// Place a diagnostic a custom operator returned: its pointer is
433    /// relative to the call (empty, or `"/i"` for argument `i`).
434    fn operator_diagnostic(
435        &mut self,
436        mut d: Diagnostic,
437        op: &str,
438        argv: &OwnedDataValue,
439        pointer: &str,
440    ) {
441        let relative = std::mem::take(&mut d.pointer);
442        let mut placed = pointer.to_string();
443        if !relative.is_empty() {
444            crate::node::push_pointer_token(&mut placed, op);
445            // A lone argument that is not an array is the argument list's
446            // only member, written in place: argument 0 is the call's value.
447            match (argv, relative.strip_prefix("/0")) {
448                (OwnedDataValue::Array(_), _) => placed.push_str(&relative),
449                (_, Some(rest)) if rest.is_empty() || rest.starts_with('/') => {
450                    placed.push_str(rest)
451                }
452                _ => placed.push_str(&relative),
453            }
454        }
455        d.pointer = placed;
456        d.operator.get_or_insert_with(|| op.to_string());
457        if self.unread > 0 {
458            return;
459        }
460        if self.protected > 0 {
461            d.severity = Severity::Warning;
462        }
463        self.out.push(d);
464    }
465
466    fn builtin(
467        &mut self,
468        op: &str,
469        opcode: OpCode,
470        argv: &OwnedDataValue,
471        count: usize,
472        pointer: &mut String,
473    ) {
474        let meta = opcode.meta();
475        if meta.args_form == ArgsForm::ArrayOnly && !matches!(argv, OwnedDataValue::Array(_)) {
476            let message = format!("`{op}` takes its arguments as an array");
477            self.report(
478                DiagnosticCode::ArgumentForm,
479                Severity::Error,
480                pointer,
481                op,
482                message,
483            );
484            return;
485        }
486        #[cfg(feature = "tensor")]
487        if opcode == OpCode::TensorMake
488            && let OwnedDataValue::Object(fields) = argv
489            && crate::compile::hooks::is_tensor_wire_body(fields)
490        {
491            return;
492        }
493
494        let arity = opcode.arity();
495        let (min, max) = (arity.min as usize, arity.max.map(usize::from));
496        // Which arguments the call can evaluate: all of them, unless the
497        // count makes it return a fixed value or skip the extras.
498        let mut reads = ..count;
499        if count < min {
500            if let Miss::Return(_) = meta.on_missing {
501                reads = ..0;
502            }
503            if matches!(meta.on_missing, Miss::InvalidArgs | Miss::Err(_)) {
504                let message = format!("`{op}` takes {}, not {count}", describe_count(min, max));
505                self.report(
506                    DiagnosticCode::ArgumentCount,
507                    Severity::Error,
508                    pointer,
509                    op,
510                    message,
511                );
512            }
513        } else if let Some(max) = max
514            && count > max
515        {
516            let (severity, message) = match meta.on_extra {
517                Extra::InvalidArgs | Extra::Err(_) => (
518                    Severity::Error,
519                    format!(
520                        "`{op}` takes {}, not {count}",
521                        describe_count(min, Some(max))
522                    ),
523                ),
524                Extra::Ignore => {
525                    reads = ..max;
526                    (
527                        Severity::Warning,
528                        format!(
529                            "`{op}` reads {}; the other {} never evaluated",
530                            describe_count(min, Some(max)),
531                            plural(count - max, "argument is", "arguments are")
532                        ),
533                    )
534                }
535                Extra::Return(_) => {
536                    reads = ..0;
537                    (
538                        Severity::Warning,
539                        format!(
540                            "`{op}` takes {}; with {count} it returns a fixed value without evaluating them",
541                            describe_count(min, Some(max))
542                        ),
543                    )
544                }
545            };
546            self.report(
547                DiagnosticCode::ArgumentCount,
548                severity,
549                pointer,
550                op,
551                message,
552            );
553        }
554
555        #[cfg(feature = "datetime")]
556        if matches!(opcode, OpCode::ParseDate | OpCode::FormatDate)
557            && let OwnedDataValue::Array(items) = argv
558            && let Some(OwnedDataValue::String(zone)) = items.get(2)
559            && zone.parse::<chrono_tz::Tz>().is_err()
560        {
561            let mut at = pointer.clone();
562            crate::node::push_pointer_token(&mut at, op);
563            crate::node::push_pointer_token(&mut at, "2");
564            let message = format!("unknown timezone `{zone}`");
565            self.report(
566                DiagnosticCode::InvalidTimezone,
567                Severity::Error,
568                &at,
569                op,
570                message,
571            );
572        }
573
574        // A catching operator (`try`) catches the errors of every arm but
575        // the last.
576        let caught = if matches!(meta.effect, Effect::Catches) {
577            count.saturating_sub(1)
578        } else {
579            0
580        };
581        self.args_where(op, argv, pointer, |i| {
582            if !reads.contains(&i) {
583                ArgUse::Never
584            } else if i < caught {
585                ArgUse::Caught
586            } else {
587                ArgUse::Runs
588            }
589        });
590    }
591
592    /// An operator name one edit from `key`, if any: built-in names first,
593    /// then the engine's custom operators. Short names are left out, since
594    /// almost any short key is one edit from `+` or `in`.
595    fn similar_operator(&self, key: &str) -> Option<String> {
596        let key_chars = key.chars().count();
597        let close = |name: &str| {
598            let name_chars = name.chars().count();
599            name_chars >= 3
600                && key_chars.abs_diff(name_chars) <= 1
601                && one_edit_apart_with_len(key, key_chars, name, name_chars)
602        };
603        if let Some(name) = self.engine.builtin_operator_names().find(|n| close(n)) {
604            return Some(name.to_string());
605        }
606        let mut custom: Vec<&str> = self
607            .engine
608            .custom_operator_names()
609            .filter(|n| close(n))
610            .collect();
611        custom.sort_unstable();
612        custom.first().map(|n| n.to_string())
613    }
614}
615
616/// Whether a call can evaluate one of its arguments.
617#[derive(Clone, Copy)]
618enum ArgUse {
619    /// Never evaluated at this argument count.
620    Never,
621    /// Evaluated, and an error there fails the call.
622    Runs,
623    /// Evaluated, and an error there is caught (a `try` arm).
624    Caught,
625}
626
627fn plural(n: usize, one: &str, many: &str) -> String {
628    if n == 1 {
629        format!("1 {one}")
630    } else {
631        format!("{n} {many}")
632    }
633}
634
635/// `exactly 2 arguments`, `at least 1 argument`, `1 to 3 arguments`.
636fn describe_count(min: usize, max: Option<usize>) -> String {
637    let noun = |n: usize| if n == 1 { "argument" } else { "arguments" };
638    match max {
639        Some(max) if max == min => format!("exactly {min} {}", noun(min)),
640        Some(max) => format!("{min} to {max} {}", noun(max)),
641        None => format!("at least {min} {}", noun(min)),
642    }
643}
644
645/// Whether `a` and `b` are exactly one edit apart (an insertion, a
646/// deletion or a substitution of one char): Levenshtein distance 1,
647/// without the table.
648#[cfg(test)]
649fn one_edit_apart(a: &str, b: &str) -> bool {
650    one_edit_apart_with_len(a, a.chars().count(), b, b.chars().count())
651}
652
653fn one_edit_apart_with_len(a: &str, la: usize, b: &str, lb: usize) -> bool {
654    match la.abs_diff(lb) {
655        0 => a.chars().zip(b.chars()).filter(|(x, y)| x != y).count() == 1,
656        1 => {
657            let (long, short) = if la > lb { (a, b) } else { (b, a) };
658            let mut long = long.chars();
659            let mut skipped = false;
660            for c in short.chars() {
661                loop {
662                    match long.next() {
663                        Some(x) if x == c => break,
664                        Some(_) if !skipped => skipped = true,
665                        _ => return false,
666                    }
667                }
668            }
669            true
670        }
671        _ => false,
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::{describe_count, one_edit_apart};
678
679    #[test]
680    fn distances() {
681        assert!(one_edit_apart("vr", "var"));
682        assert!(one_edit_apart("var", "vr"));
683        assert!(one_edit_apart("mapp", "map"));
684        assert!(one_edit_apart("mop", "map"));
685        assert!(one_edit_apart("vars", "var"));
686        assert!(!one_edit_apart("map", "map"));
687        assert!(!one_edit_apart("name", "none"));
688        assert!(!one_edit_apart("ab", "ba"));
689        assert!(!one_edit_apart("", "abc"));
690        assert!(!one_edit_apart("mapxy", "map"));
691    }
692
693    #[test]
694    fn counts() {
695        assert_eq!(describe_count(2, Some(2)), "exactly 2 arguments");
696        assert_eq!(describe_count(1, Some(1)), "exactly 1 argument");
697        assert_eq!(describe_count(1, None), "at least 1 argument");
698        assert_eq!(describe_count(1, Some(3)), "1 to 3 arguments");
699    }
700}