Skip to main content

ferrobridge_fhirpath/
check.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! A static check of the paths an expression navigates, against the type its
5//! context will have (N1 §11).
6//!
7//! The check follows the FHIR types a path can reach, a choice element to
8//! every type it admits, and stops at what N1 §11 says a compiler cannot type
9//! (`children()`, `descendants()`, `resolve()`, a resource-typed element) and
10//! at every system value. A member none of the reachable types defines, and a
11//! leading type name that is not the context's, are refused before any
12//! document is read. Anything the check cannot type passes, and the
13//! evaluation's own checks apply.
14//!
15//! A type test of the focus narrows what the focus reaches where the test's
16//! outcome is known: the right operand of `and` and `implies` sees the test
17//! true, the right operand of `or` sees it false, the second argument of
18//! `iif()` true and the third false, and `where()` answers the items its
19//! criterion keeps. Where no type is left the operand cannot be reached and
20//! nothing in it is refused.
21
22use crate::ast::{BinaryOp, Expr, Literal, TypeSpecifier};
23use crate::model::{FhirType, Model, field_type, is_a, named_type, specializes, variant_type};
24use fhir_types::schema::Kind;
25
26/// A path the model refuses.
27#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
28pub enum SemanticError {
29    /// A member none of the reachable types defines.
30    #[error("{type_name} has no element {element}")]
31    UnknownElement {
32        /// The reachable types.
33        type_name: String,
34        /// The member name.
35        element: String,
36    },
37    /// A leading type name that is not the context's type.
38    #[error("the expression names the type {named}, the context is a {context}")]
39    ContextType {
40        /// The type the expression names.
41        named: String,
42        /// The context's type.
43        context: String,
44    },
45    /// A type specifier that names no type.
46    #[error("{name} names no type of the model")]
47    UnknownType {
48        /// The specifier as written.
49        name: String,
50    },
51    /// A criterion that is a literal of a type other than Boolean, which
52    /// `iif()` does not take (N1 §5.5.1, §11).
53    #[error("{function}() takes a Boolean criterion, the expression gives a literal {found}")]
54    Criterion {
55        /// The function.
56        function: &'static str,
57        /// The literal's type.
58        found: &'static str,
59    },
60    /// A function given an input none of whose reachable types it takes
61    /// (N1 §11 "an input value ... not of the expected type").
62    #[error("{function}() takes no {type_name}")]
63    Input {
64        /// The function.
65        function: &'static str,
66        /// The reachable types.
67        type_name: String,
68    },
69}
70
71/// The FHIR types an expression can reach; `None` when unknown, and none
72/// at all where a type guard leaves the expression unreachable.
73type Reach = Option<Vec<FhirType>>;
74
75/// Checks `expr` with a context of type `context`.
76///
77/// # Errors
78///
79/// Returns the first [`SemanticError`] found.
80pub fn check(expr: &Expr, model: &dyn Model, context: FhirType) -> Result<(), SemanticError> {
81    check_any(expr, model, &[context])
82}
83
84/// Checks `expr` with a context that may hold any of the types `contexts`,
85/// as an occurrence of a choice element does (R4 §2.9.1.1): a member is
86/// accepted when one of them defines it.
87///
88/// # Errors
89///
90/// Returns the first [`SemanticError`] found.
91pub fn check_any(
92    expr: &Expr,
93    model: &dyn Model,
94    contexts: &[FhirType],
95) -> Result<(), SemanticError> {
96    // NOTE: N1 §11 lets an implementation choose its compile-time strictness, and lists a
97    // choice[x] member as one a compiler cannot type, so the union of its types is the bound.
98    let reach = Some(contexts.to_vec());
99    Checker { model }.infer(expr, &reach, &reach).map(|_| ())
100}
101
102/// The checker over one model.
103struct Checker<'m> {
104    model: &'m dyn Model,
105}
106
107/// The union of two reaches.
108fn union(a: Reach, b: Reach) -> Reach {
109    let (mut a, b) = (a?, b?);
110    for ty in b {
111        if !a.contains(&ty) {
112            a.push(ty);
113        }
114    }
115    Some(a)
116}
117
118impl Checker<'_> {
119    /// What `expr` reaches from `focus`, with `$this` reaching `this`.
120    fn infer(&self, expr: &Expr, focus: &Reach, this: &Reach) -> Result<Reach, SemanticError> {
121        match expr {
122            Expr::Literal(_) | Expr::Empty | Expr::External(_) | Expr::Index | Expr::Total => {
123                Ok(None)
124            }
125            Expr::This => Ok(this.clone()),
126            Expr::Member { input, name } => {
127                let from = match input {
128                    Some(input) => self.infer(input, focus, this)?,
129                    None => focus.clone(),
130                };
131                self.member(&from, name, input.is_none())
132            }
133            Expr::Call { input, name, args } => {
134                let from = match input {
135                    Some(input) => self.infer(input, focus, this)?,
136                    None => focus.clone(),
137                };
138                self.call(&from, name, args, focus, this)
139            }
140            Expr::Invocation { input, .. } => {
141                self.infer(input, focus, this)?;
142                Ok(None)
143            }
144            Expr::Indexer { input, index } => {
145                self.infer(index, focus, this)?;
146                self.infer(input, focus, this)
147            }
148            Expr::Negate(operand) | Expr::Plus(operand) => {
149                self.infer(operand, focus, this)?;
150                Ok(None)
151            }
152            Expr::Binary { op, left, right } => {
153                let a = self.infer(left, focus, this)?;
154                let b = match op {
155                    BinaryOp::And | BinaryOp::Implies => self.infer(
156                        right,
157                        &self.narrow(left, true, focus),
158                        &self.narrow(left, true, this),
159                    )?,
160                    BinaryOp::Or => self.infer(
161                        right,
162                        &self.narrow(left, false, focus),
163                        &self.narrow(left, false, this),
164                    )?,
165                    _ => self.infer(right, focus, this)?,
166                };
167                Ok(if *op == BinaryOp::Union {
168                    union(a, b)
169                } else {
170                    None
171                })
172            }
173            Expr::Is {
174                operand,
175                type_specifier,
176            } => {
177                self.infer(operand, focus, this)?;
178                self.resolve(type_specifier)?;
179                Ok(None)
180            }
181            Expr::As {
182                operand,
183                type_specifier,
184            } => {
185                self.infer(operand, focus, this)?;
186                self.resolve(type_specifier)
187            }
188            Expr::SortKey { key, .. } => self.infer(key, focus, this),
189        }
190    }
191
192    /// The FHIR type a specifier names, `None` for a system type.
193    fn resolve(&self, specifier: &TypeSpecifier) -> Result<Reach, SemanticError> {
194        let system = crate::value::SYSTEM_TYPES.contains(&specifier.name.as_str());
195        let fhir = || named_type(self.model, &specifier.name);
196        match specifier.namespace.as_deref() {
197            None | Some("FHIR") if fhir().is_some() => Ok(fhir().map(|ty| vec![ty])),
198            None | Some("System") if system => Ok(None),
199            _ => Err(SemanticError::UnknownType {
200                name: specifier.to_string(),
201            }),
202        }
203    }
204
205    /// The types `reach` keeps when the Boolean `guard` is `assume`.
206    ///
207    /// A guard is a type test of the focus (`$this is T`, `$this.is(T)`,
208    /// `is(T)`), its `not()`, or an `and` or `or` of guards; a test of any
209    /// other expression, or of a system type, tells nothing.
210    fn narrow(&self, guard: &Expr, assume: bool, reach: &Reach) -> Reach {
211        match guard {
212            Expr::Is {
213                operand,
214                type_specifier,
215            } if matches!(operand.as_ref(), Expr::This) => {
216                self.narrow_to(type_specifier, assume, reach)
217            }
218            Expr::Call { input, name, args }
219                if name == "is" && input.as_deref().is_none_or(|i| matches!(i, Expr::This)) =>
220            {
221                match args.as_slice() {
222                    [arg] => match arg.as_type_specifier() {
223                        Some(specifier) => self.narrow_to(&specifier, assume, reach),
224                        None => reach.clone(),
225                    },
226                    _ => reach.clone(),
227                }
228            }
229            Expr::Call {
230                input: Some(inner),
231                name,
232                args,
233            } if name == "not" && args.is_empty() => self.narrow(inner, !assume, reach),
234            Expr::Binary {
235                op: BinaryOp::And,
236                left,
237                right,
238            } => {
239                if assume {
240                    self.narrow(right, true, &self.narrow(left, true, reach))
241                } else {
242                    union(
243                        self.narrow(left, false, reach),
244                        self.narrow(right, false, reach),
245                    )
246                }
247            }
248            Expr::Binary {
249                op: BinaryOp::Or,
250                left,
251                right,
252            } => {
253                if assume {
254                    union(
255                        self.narrow(left, true, reach),
256                        self.narrow(right, true, reach),
257                    )
258                } else {
259                    self.narrow(right, false, &self.narrow(left, false, reach))
260                }
261            }
262            _ => reach.clone(),
263        }
264    }
265
266    /// The types `reach` keeps when the focus `is` the type `specifier`
267    /// names (`assume`), or is not (N1 §6.3.1: the type or a subclass).
268    fn narrow_to(&self, specifier: &TypeSpecifier, assume: bool, reach: &Reach) -> Reach {
269        let types = reach.as_ref()?;
270        let Ok(Some(named)) = self.resolve(specifier) else {
271            return reach.clone();
272        };
273        let Some(target) = named.first() else {
274            return reach.clone();
275        };
276        let mut kept: Vec<FhirType> = Vec::new();
277        for ty in types {
278            let found = if specializes(self.model, ty, target.name) {
279                assume.then_some(*ty)
280            } else if assume && specializes(self.model, target, ty.name) {
281                Some(*target)
282            } else if assume {
283                None
284            } else {
285                Some(*ty)
286            };
287            if let Some(found) = found
288                && !kept.contains(&found)
289            {
290                kept.push(found);
291            }
292        }
293        Some(kept)
294    }
295
296    /// What the member `name` reaches from `from`.
297    fn member(&self, from: &Reach, name: &str, at_root: bool) -> Result<Reach, SemanticError> {
298        let Some(types) = from else {
299            return Ok(None);
300        };
301        if types.is_empty() {
302            return Ok(Some(Vec::new()));
303        }
304        let mut reached: Vec<FhirType> = Vec::new();
305        let mut known = true;
306        let mut defined = false;
307        for ty in types {
308            if at_root && !ty.is_primitive() && is_a(self.model, ty, name) {
309                defined = true;
310                reached.push(*ty);
311                continue;
312            }
313            let schema = match ty.schema() {
314                Some(schema) => Some(schema),
315                None => self.model.schemas().type_named("Element"),
316            };
317            let Some(field) =
318                schema.and_then(|schema| schema.fields.iter().find(|field| field.name == name))
319            else {
320                continue;
321            };
322            defined = true;
323            match field.kind {
324                Kind::Choice(variants) => {
325                    for (suffix, kind) in variants {
326                        match variant_type(self.model, field, suffix, *kind) {
327                            Some(found) if !reached.contains(&found) => reached.push(found),
328                            Some(_) => {}
329                            None => known = false,
330                        }
331                    }
332                }
333                kind => match field_type(self.model, field, kind) {
334                    Some(found) if !reached.contains(&found) => reached.push(found),
335                    Some(_) => {}
336                    None => known = false,
337                },
338            }
339        }
340        if !defined {
341            let names = types
342                .iter()
343                .map(|ty| ty.name)
344                .collect::<Vec<_>>()
345                .join(" | ");
346            if at_root && named_type(self.model, name).is_some() {
347                return Err(SemanticError::ContextType {
348                    named: String::from(name),
349                    context: names,
350                });
351            }
352            return Err(SemanticError::UnknownElement {
353                type_name: names,
354                element: String::from(name),
355            });
356        }
357        Ok(known.then_some(reached))
358    }
359
360    /// What the function `name` reaches from `from`, its arguments checked.
361    fn call(
362        &self,
363        from: &Reach,
364        name: &str,
365        args: &[Expr],
366        focus: &Reach,
367        this: &Reach,
368    ) -> Result<Reach, SemanticError> {
369        let lambdas: &[usize] = match name {
370            "where" | "exists" | "all" | "select" | "repeat" | "aggregate" => &[0],
371            "trace" => &[1],
372            "iif" => &[0, 1, 2],
373            _ => &[],
374        };
375        if name == "iif"
376            && let Some(Expr::Literal(literal)) = args.first()
377        {
378            let found = match literal {
379                Literal::Boolean(_) => None,
380                Literal::String(_) => Some("String"),
381                Literal::Integer(_) => Some("Integer"),
382                Literal::Decimal(_) => Some("Decimal"),
383                Literal::Date(_) => Some("Date"),
384                Literal::DateTime(_) => Some("DateTime"),
385                Literal::Time(_) => Some("Time"),
386                Literal::Quantity(..) => Some("Quantity"),
387            };
388            if let Some(found) = found {
389                return Err(SemanticError::Criterion {
390                    function: "iif",
391                    found,
392                });
393            }
394        }
395        if name == "toString"
396            && let Some(types) = from
397            && !types.is_empty()
398            && types
399                .iter()
400                .all(|ty| !ty.is_primitive() && !is_a(self.model, ty, "Quantity"))
401        {
402            // NOTE: N1 §5.5.8 toString() takes a primitive or a Quantity (R4 §2.9.1.3); N1 §11
403            // lets a compiler refuse an input no reachable type of which it takes.
404            return Err(SemanticError::Input {
405                function: "toString",
406                type_name: types
407                    .iter()
408                    .map(|ty| ty.name)
409                    .collect::<Vec<_>>()
410                    .join(" | "),
411            });
412        }
413        let mut outputs = Vec::new();
414        for (index, arg) in args.iter().enumerate() {
415            let typed_argument = matches!(name, "ofType" | "as" | "is") && index == 0;
416            if typed_argument {
417                if let Some(specifier) = arg.as_type_specifier() {
418                    self.resolve(&specifier)?;
419                }
420                continue;
421            }
422            let reach = if name == "iif" && index > 0 {
423                let guarded = match args.first() {
424                    Some(criterion) => self.narrow(criterion, index == 1, from),
425                    None => from.clone(),
426                };
427                self.infer(arg, &guarded, &guarded)?
428            } else if lambdas.contains(&index) || name == "sort" {
429                self.infer(arg, from, from)?
430            } else {
431                self.infer(arg, focus, this)?
432            };
433            outputs.push(reach);
434        }
435        let first = outputs.first().cloned().flatten();
436        Ok(match name {
437            "where" => match args.first() {
438                Some(criterion) => self.narrow(criterion, true, from),
439                None => from.clone(),
440            },
441            "first" | "last" | "tail" | "skip" | "take" | "single" | "distinct" | "intersect"
442            | "exclude" | "trace" | "checkModifiers" | "slice" | "sort" => from.clone(),
443            "select" => first,
444            "union" | "combine" => union(from.clone(), first),
445            "extension" => named_type(self.model, "Extension").map(|ty| vec![ty]),
446            "ofType" | "as" => match args.first().and_then(Expr::as_type_specifier) {
447                Some(specifier) => self.resolve(&specifier)?,
448                None => None,
449            },
450            _ => None,
451        })
452    }
453}