Skip to main content

fml_check/
check.rs

1//! The checker: walks a map with the types of its variables and reports what cannot work.
2
3use std::collections::hash_map::Entry;
4use std::collections::{HashMap, HashSet};
5
6use fml_syntax::ast::{
7    self, AstNode, Expr, Invocation, ParameterMode, QualifiedName, TransformValue,
8};
9use fml_syntax::{Diagnostic, Severity, SyntaxKind, SyntaxNode, TextRange};
10
11use crate::definitions::{Definitions, Kind, type_url};
12use crate::types::{Lookup, Type, Types};
13
14/// Checks a parsed map. Syntax errors are not repeated; see [`fml_syntax::Parse::diagnostics`].
15pub fn check(file: &ast::SourceFile, definitions: &dyn Definitions) -> Vec<Diagnostic> {
16    let mut checker = Checker::new(file, definitions);
17    checker.run();
18    let mut diagnostics = checker.diagnostics;
19    diagnostics.sort_by_key(|d| (d.range.start(), d.range.end()));
20    diagnostics
21}
22
23/// The reserved keywords of the mapping language (fml.md, "Reserved keywords").
24const RESERVED: &[&str] = &[
25    "map",
26    "uses",
27    "as",
28    "alias",
29    "imports",
30    "group",
31    "extends",
32    "default",
33    "where",
34    "check",
35    "log",
36    "then",
37    "true",
38    "false",
39    "types",
40    "type",
41    "first",
42    "not_first",
43    "last",
44    "not_last",
45    "only_one",
46    "share",
47    "single",
48    "source",
49    "target",
50    "queried",
51    "produced",
52    "conceptmap",
53    "prefix",
54    "let",
55];
56
57/// How deep untyped groups are followed into the groups they call.
58const MAX_CALL_DEPTH: usize = 32;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61enum Mode {
62    Source,
63    Target,
64}
65
66#[derive(Debug, Clone)]
67struct Variable {
68    ty: Type,
69    mode: Mode,
70}
71
72struct Checker<'a> {
73    file: &'a ast::SourceFile,
74    definitions: &'a dyn Definitions,
75    /// `uses` aliases and the names of unaliased structures, to their URLs.
76    type_names: HashMap<String, String>,
77    groups: HashMap<String, ast::Group>,
78    has_imports: bool,
79    /// Groups already checked with given argument types.
80    checked: HashSet<(String, Vec<String>)>,
81    reported: HashSet<(TextRange, String)>,
82    diagnostics: Vec<Diagnostic>,
83}
84
85impl<'a> Checker<'a> {
86    fn types(&self) -> Types<'a> {
87        Types::new(self.definitions)
88    }
89
90    fn new(file: &'a ast::SourceFile, definitions: &'a dyn Definitions) -> Self {
91        Self {
92            file,
93            definitions,
94            type_names: HashMap::new(),
95            groups: HashMap::new(),
96            has_imports: file.imports().next().is_some(),
97            checked: HashSet::new(),
98            reported: HashSet::new(),
99            diagnostics: Vec::new(),
100        }
101    }
102
103    fn report(&mut self, severity: Severity, range: TextRange, message: String) {
104        if self.reported.insert((range, message.clone())) {
105            self.diagnostics.push(Diagnostic {
106                severity,
107                range,
108                message,
109            });
110        }
111    }
112
113    fn error(&mut self, node: &SyntaxNode, message: impl Into<String>) {
114        self.report(Severity::Error, trimmed_range(node), message.into());
115    }
116
117    fn warning(&mut self, node: &SyntaxNode, message: impl Into<String>) {
118        self.report(Severity::Warning, trimmed_range(node), message.into());
119    }
120
121    fn error_at(&mut self, range: TextRange, message: impl Into<String>) {
122        self.report(Severity::Error, range, message.into());
123    }
124
125    fn run(&mut self) {
126        self.declarations();
127        self.identity();
128        self.names();
129        self.group_signatures();
130        self.rule_names();
131        let groups: Vec<ast::Group> = self.file.groups().collect();
132        for group in &groups {
133            let types = group
134                .parameters()
135                .map(|parameter| self.parameter_type(&parameter))
136                .collect();
137            self.check_group(group, types, 0);
138        }
139    }
140
141    // ---- Declarations -----------------------------------------------------------------------
142
143    fn declarations(&mut self) {
144        for uses in self.file.uses() {
145            let Some(url) = uses.url().and_then(|u| u.value()) else {
146                continue;
147            };
148            match self.definitions.structure(&url) {
149                Some(structure) => {
150                    let name = uses.alias().unwrap_or_else(|| structure.name.clone());
151                    self.type_names.insert(name, url);
152                }
153                None => {
154                    if self.definitions.covers(&url) {
155                        let at = uses.url().unwrap();
156                        self.error(at.syntax(), format!("unknown structure `{url}`"));
157                    }
158                    if let Some(alias) = uses.alias() {
159                        self.type_names.insert(alias, url);
160                    }
161                }
162            }
163            if uses.mode().is_none() {
164                self.error(
165                    uses.syntax(),
166                    "`uses` needs a mode: `as source` or `as target`",
167                );
168            }
169        }
170        for group in self.file.groups() {
171            let Some(name) = group.name() else { continue };
172            match self.groups.entry(name) {
173                Entry::Occupied(entry) => {
174                    let message = format!("group `{}` is declared twice", entry.key());
175                    self.error(group.syntax(), message);
176                }
177                Entry::Vacant(entry) => {
178                    entry.insert(group);
179                }
180            }
181        }
182        for group in self.file.groups() {
183            if let Some(extends) = group.extends()
184                && !self.groups.contains_key(&extends)
185                && !self.has_imports
186            {
187                self.error(
188                    group.syntax(),
189                    format!("group `{extends}` is not declared and nothing is imported"),
190                );
191            }
192        }
193    }
194
195    /// Constant names must be unique in the file, and no name the map defines may be a reserved
196    /// keyword (fml.md, "Constants" and "Reserved keywords").
197    fn names(&mut self) {
198        let mut constants: HashSet<String> = HashSet::new();
199        for constant in self.file.constants() {
200            let Some(name) = constant.name() else {
201                continue;
202            };
203            if !constants.insert(name.clone()) {
204                self.error(
205                    constant.syntax(),
206                    format!("constant `{name}` is declared twice"),
207                );
208            }
209            if RESERVED.contains(&name.as_str()) {
210                self.warning(constant.syntax(), format!("`{name}` is a reserved keyword"));
211            }
212        }
213        for group in self.file.groups() {
214            if let Some(name) = group.name()
215                && RESERVED.contains(&name.as_str())
216            {
217                self.warning(group.syntax(), format!("`{name}` is a reserved keyword"));
218            }
219            for parameter in group.parameters() {
220                if let Some(name) = parameter.name()
221                    && RESERVED.contains(&name.as_str())
222                {
223                    self.warning(
224                        parameter.syntax(),
225                        format!("`{name}` is a reserved keyword"),
226                    );
227                }
228            }
229        }
230    }
231
232    /// `url` and `name` SHALL be given (fml.md, "Metadata"), in metadata or a `map` line.
233    fn identity(&mut self) {
234        let declaration = self.file.map_declaration();
235        let metadata: Vec<String> = self
236            .file
237            .metadata()
238            .filter_map(|m| m.key().map(|k| k.text()))
239            .collect();
240        let has = |key: &str| metadata.iter().any(|k| k == key);
241        let url = has("url") || declaration.as_ref().is_some_and(|d| d.url().is_some());
242        let name = has("name") || declaration.as_ref().is_some_and(|d| d.name().is_some());
243        let at = TextRange::empty(0.into());
244        if !url {
245            self.error_at(at, "the map has no `url`: add `/// url = '…'`");
246        }
247        if !name {
248            self.error_at(at, "the map has no `name`: add `/// name = '…'`");
249        }
250    }
251
252    /// The parameters a group declares, as (mode, name, type).
253    fn signature(group: &ast::Group) -> Vec<(Mode, String, Option<String>)> {
254        group
255            .parameters()
256            .map(|parameter| {
257                let mode = match parameter.mode() {
258                    Some(ParameterMode::Target) => Mode::Target,
259                    _ => Mode::Source,
260                };
261                let type_name = parameter.type_annotation().and_then(|t| t.type_name());
262                (mode, parameter.name().unwrap_or_default(), type_name)
263            })
264            .collect()
265    }
266
267    /// A group that extends another SHALL have the other's parameters, by mode, name and type
268    /// (fml.md, "Groups"); a default group SHALL have a typed source and a typed target, in that
269    /// order.
270    fn group_signatures(&mut self) {
271        let groups: Vec<ast::Group> = self.file.groups().collect();
272        for group in &groups {
273            let own = Self::signature(group);
274            if let Some(base) = group
275                .extends()
276                .and_then(|name| self.groups.get(&name).cloned())
277            {
278                let base_name = base.name().unwrap_or_default();
279                for (mode, name, type_name) in Self::signature(&base) {
280                    let matching = own.iter().find(|(_, own_name, _)| *own_name == name);
281                    let problem = match matching {
282                        None => Some(format!("has no parameter `{name}`")),
283                        Some((own_mode, _, _)) if *own_mode != mode => {
284                            Some(format!("has `{name}` with another mode"))
285                        }
286                        Some((_, _, own_type))
287                            if type_name.is_some()
288                                && own_type.is_some()
289                                && *own_type != type_name =>
290                        {
291                            Some(format!(
292                                "has `{name}` as `{}`, not `{}`",
293                                own_type.as_deref().unwrap_or_default(),
294                                type_name.as_deref().unwrap_or_default()
295                            ))
296                        }
297                        _ => None,
298                    };
299                    if let Some(problem) = problem {
300                        self.error(
301                            group.syntax(),
302                            format!(
303                                "group `{}` extends `{base_name}` but {problem}; it needs the parameters of `{base_name}`",
304                                group.name().unwrap_or_default()
305                            ),
306                        );
307                    }
308                }
309            }
310            if group.type_mode().is_some() {
311                let valid = matches!(
312                    own.as_slice(),
313                    [(Mode::Source, _, Some(_)), (Mode::Target, _, Some(_))]
314                );
315                if !valid {
316                    self.warning(
317                        group.syntax(),
318                        format!(
319                            "default group `{}` needs two typed parameters, a source and then a target; it is not used as a default",
320                            group.name().unwrap_or_default()
321                        ),
322                    );
323                }
324            }
325        }
326    }
327
328    /// Rule names given in the map must be unique within it (fml.md, "Transform Rules").
329    fn rule_names(&mut self) {
330        let mut rules: Vec<ast::Rule> = Vec::new();
331        let mut pending: Vec<ast::Rule> = self
332            .file
333            .groups()
334            .flat_map(|g| g.rules().collect::<Vec<_>>())
335            .collect();
336        while let Some(rule) = pending.pop() {
337            if let Some(block) = rule.dependent().and_then(|d| d.rules()) {
338                pending.extend(block.rules());
339            }
340            rules.push(rule);
341        }
342        rules.sort_by_key(|rule| rule.syntax().text_range().start());
343        let mut seen: HashSet<String> = HashSet::new();
344        for rule in rules {
345            let Some(name) = rule.name() else {
346                continue;
347            };
348            if !seen.insert(name.clone()) {
349                self.warning(
350                    rule.syntax(),
351                    format!("rule name `{name}` is used twice in the map"),
352                );
353            }
354        }
355    }
356
357    /// The declared type of a group parameter: a `uses` alias, the name of a used structure, or a
358    /// core type name.
359    fn parameter_type(&mut self, parameter: &ast::Parameter) -> Option<Type> {
360        let annotation = parameter.type_annotation()?;
361        let name = annotation.type_name()?;
362        if let Some(url) = self.type_names.get(&name).cloned() {
363            return Some(self.types().structure(&url));
364        }
365        let url = type_url(&name, "http://hl7.org/fhir/StructureDefinition/");
366        if let Some(structure) = self.definitions.structure(&url) {
367            return Some(Type::Node {
368                structure,
369                path: Vec::new(),
370            });
371        }
372        if self.definitions.covers(&url) {
373            self.error(annotation.syntax(), format!("unknown type `{name}`"));
374        }
375        Some(Type::Unknown)
376    }
377
378    // ---- Groups -----------------------------------------------------------------------------
379
380    /// Checks a group with argument types: declared types, or for untyped parameters the types
381    /// inferred at a call site (`None` when not known).
382    fn check_group(&mut self, group: &ast::Group, arguments: Vec<Option<Type>>, depth: usize) {
383        let name = group.name().unwrap_or_default();
384        let parameters: Vec<ast::Parameter> = group.parameters().collect();
385        let mut scope = Scope::default();
386        for constant in self.file.constants() {
387            if let Some(constant_name) = constant.name() {
388                scope.define(
389                    constant_name,
390                    Variable {
391                        ty: Type::Unknown,
392                        mode: Mode::Source,
393                    },
394                );
395            }
396        }
397        scope.push();
398        let mut key = Vec::new();
399        for (index, parameter) in parameters.iter().enumerate() {
400            let declared = self.parameter_type(parameter);
401            let ty = declared
402                .or_else(|| arguments.get(index).cloned().flatten())
403                .unwrap_or(Type::Unknown);
404            key.push(ty.key());
405            let mode = match parameter.mode() {
406                Some(ParameterMode::Target) => Mode::Target,
407                _ => Mode::Source,
408            };
409            if let Some(parameter_name) = parameter.name() {
410                scope.define(parameter_name, Variable { ty, mode });
411            }
412        }
413        if !self.checked.insert((name, key)) || depth > MAX_CALL_DEPTH {
414            return;
415        }
416        if let Some(body) = group.body() {
417            for rule in body.rules() {
418                self.check_rule(&rule, &mut scope, depth);
419            }
420        }
421    }
422
423    // ---- Rules ------------------------------------------------------------------------------
424
425    fn check_rule(&mut self, rule: &ast::Rule, outer: &mut Scope, depth: usize) {
426        outer.push();
427        for source in rule.sources() {
428            self.check_source(&source, outer);
429        }
430        let fields = rule.identity_fields();
431        let targets: Vec<ast::Target> = rule.targets().collect();
432        if !fields.is_empty() {
433            self.check_identity(rule, &fields, outer);
434        }
435        for target in &targets {
436            self.check_target(target, outer);
437        }
438        if let Some(dependent) = rule.dependent() {
439            for invocation in dependent.invocations() {
440                self.check_call(&invocation, outer, depth);
441            }
442            if let Some(block) = dependent.rules() {
443                for nested in block.rules() {
444                    self.check_rule(&nested, outer, depth);
445                }
446            }
447        }
448        outer.pop();
449    }
450
451    fn check_source(&mut self, source: &ast::Source, scope: &mut Scope) {
452        let Some(path) = source.path() else { return };
453        let segments = path.segments();
454        let Some(context) = segments.first() else {
455            return;
456        };
457        let Some(variable) = scope.get(context).cloned() else {
458            self.error_at(
459                segment_range(&path, 0),
460                format!("unknown variable `{context}`"),
461            );
462            return;
463        };
464        // A rule reads from source variables; a variable a target made is not one (the engine,
465        // like the reference engine, looks the context up among the source variables).
466        if variable.mode == Mode::Target {
467            self.error_at(
468                segment_range(&path, 0),
469                format!("`{context}` is a target variable; a source reads from a source variable"),
470            );
471        }
472        let annotation = source.type_annotation();
473        let annotated = annotation.as_ref().and_then(|a| a.type_name());
474        let ty = self.walk(
475            &path,
476            variable.ty,
477            annotated.as_deref(),
478            annotation.as_ref(),
479        );
480        if let Some(default) = source.default_value().and_then(|d| d.expression()) {
481            self.check_expression(&default, &Type::Unknown, scope);
482        }
483        if let Some(alias) = source.alias() {
484            scope.define(
485                alias,
486                Variable {
487                    ty: ty.clone(),
488                    mode: Mode::Source,
489                },
490            );
491        }
492        // `where`, `check` and `log` see each item as the focus, and its variable.
493        for clause in [
494            source.where_clause(),
495            source.check_clause(),
496            source.log_clause(),
497        ]
498        .into_iter()
499        .flatten()
500        {
501            self.check_expression(&clause, &ty, scope);
502        }
503    }
504
505    /// `src -> tgt : a, b;`: each field exists on both sides.
506    fn check_identity(&mut self, rule: &ast::Rule, fields: &[String], scope: &Scope) {
507        let source = rule.sources().next().and_then(|s| s.path());
508        let target = rule.targets().next().and_then(|t| t.path());
509        for path in [source, target].into_iter().flatten() {
510            let Some(variable) = path.segments().first().and_then(|c| scope.get(c)).cloned() else {
511                continue;
512            };
513            for field in fields {
514                if let Lookup::Missing = self.types().lookup(&variable.ty, field) {
515                    let message = format!("`{}` has no element `{field}`", variable.ty.display());
516                    self.error(rule.syntax(), message);
517                }
518            }
519        }
520    }
521
522    fn check_target(&mut self, target: &ast::Target, scope: &mut Scope) {
523        let mut ty = Type::Unknown;
524        if let Some(path) = target.path() {
525            let segments = path.segments();
526            let context = &segments[0];
527            match scope.get(context).cloned() {
528                None => {
529                    self.error_at(
530                        segment_range(&path, 0),
531                        format!("unknown variable `{context}`"),
532                    );
533                }
534                Some(variable) => {
535                    if variable.mode == Mode::Source {
536                        self.error_at(
537                            segment_range(&path, 0),
538                            format!("`{context}` is a source variable; a target writes into a target variable"),
539                        );
540                    }
541                    ty = self.walk(&path, variable.ty, None, None);
542                }
543            }
544        }
545        if let Some(transform) = target.transform() {
546            ty = self.check_transform(&transform, ty, scope);
547        }
548        if let Some(alias) = target.alias() {
549            scope.define(
550                alias,
551                Variable {
552                    ty,
553                    mode: Mode::Target,
554                },
555            );
556        }
557    }
558
559    /// Checks a transform; returns the type of the value it produces where that is known.
560    fn check_transform(
561        &mut self,
562        transform: &TransformValue,
563        element: Type,
564        scope: &Scope,
565    ) -> Type {
566        match transform {
567            TransformValue::Name(name) => {
568                let segments = name.segments();
569                match scope.get(&segments[0]).cloned() {
570                    Some(variable) if segments.len() == 1 => {
571                        if matches!(element, Type::Unknown) {
572                            variable.ty
573                        } else {
574                            element
575                        }
576                    }
577                    Some(variable) => {
578                        self.walk(name, variable.ty, None, None);
579                        element
580                    }
581                    None => {
582                        self.error_at(
583                            segment_range(name, 0),
584                            format!("unknown variable `{}`", segments[0]),
585                        );
586                        element
587                    }
588                }
589            }
590            TransformValue::Invocation(invocation) => {
591                self.check_invocation(invocation, element, scope)
592            }
593            TransformValue::Expression(expression) => {
594                self.check_expression(expression, &Type::Unknown, scope);
595                element
596            }
597            TransformValue::Literal(_) => element,
598        }
599    }
600
601    fn check_invocation(&mut self, invocation: &Invocation, element: Type, scope: &Scope) -> Type {
602        let name = invocation.name().unwrap_or_default();
603        let arguments: Vec<Expr> = invocation.arguments().collect();
604        let Some(&(_, min, max)) = TRANSFORMS.iter().find(|(n, _, _)| *n == name) else {
605            self.error(invocation.syntax(), format!("unknown transform `{name}`"));
606            return element;
607        };
608        if arguments.len() < min || max.is_some_and(|max| arguments.len() > max) {
609            let expected = match max {
610                Some(max) if max == min => format!("{min}"),
611                Some(max) => format!("{min} to {max}"),
612                None => format!("at least {min}"),
613            };
614            self.error(
615                invocation.syntax(),
616                format!(
617                    "`{name}` takes {expected} argument(s), found {}",
618                    arguments.len()
619                ),
620            );
621        }
622        if name == "evaluate"
623            && let [focus, expression] = arguments.as_slice()
624        {
625            let focus = scope
626                .get(&focus.text())
627                .map(|v| v.ty.clone())
628                .unwrap_or(Type::Unknown);
629            self.check_expression(expression, &focus, scope);
630        }
631        for (index, argument) in arguments.iter().enumerate() {
632            let is_expression = name == "evaluate" && index == 1;
633            if !is_expression && argument.syntax().kind() == SyntaxKind::NameRef {
634                let variable = argument.text();
635                if scope.get(&variable).is_none() {
636                    self.error(argument.syntax(), format!("unknown variable `{variable}`"));
637                }
638            }
639        }
640        if name == "create"
641            && let Some(type_name) = arguments.first().and_then(string_literal)
642        {
643            return self.created_type(&type_name, &element, &arguments[0]);
644        }
645        element
646    }
647
648    /// The type `create('Type')` makes, checked against the element it is assigned to.
649    fn created_type(&mut self, type_name: &str, element: &Type, at: &Expr) -> Type {
650        // In a logical model, a type next to the element's own (`create('CE')` into a `CE`
651        // element, or a subtype of it), named as the engine names it.
652        if let (None, Type::Node { structure, .. }) = (self.type_names.get(type_name), element)
653            && structure.kind == Kind::Logical
654            && let Some((prefix, _)) = structure.url.rsplit_once('/')
655        {
656            for id in [type_name.to_owned(), type_name.replace('_', "-")] {
657                if let Some(found) = self.definitions.structure(&format!("{prefix}/{id}"))
658                    && found.name == type_name
659                {
660                    return Type::Node {
661                        structure: found,
662                        path: Vec::new(),
663                    };
664                }
665            }
666        }
667        let url = match (self.type_names.get(type_name), element) {
668            (Some(url), _) => url.clone(),
669            (None, Type::Node { structure, .. }) => type_url(type_name, &structure.url),
670            // The element's type, and so the FHIR release the created type belongs to, is unknown.
671            (None, Type::Unknown) => return Type::Unknown,
672        };
673        match self.definitions.structure(&url) {
674            Some(structure) => {
675                if structure.is_abstract {
676                    self.error(
677                        at.syntax(),
678                        format!("`{type_name}` is abstract and cannot be created"),
679                    );
680                }
681                Type::Node {
682                    structure,
683                    path: Vec::new(),
684                }
685            }
686            None => {
687                if self.definitions.covers(&url) {
688                    self.error(at.syntax(), format!("unknown type `{type_name}`"));
689                }
690                Type::Unknown
691            }
692        }
693    }
694
695    /// `then Group(a, b)`: the group exists, takes that many arguments, and is checked with the
696    /// argument types when its parameters are untyped.
697    fn check_call(&mut self, invocation: &Invocation, scope: &Scope, depth: usize) {
698        let name = invocation.name().unwrap_or_default();
699        let arguments: Vec<Expr> = invocation.arguments().collect();
700        let mut types = Vec::new();
701        for argument in &arguments {
702            if argument.syntax().kind() == SyntaxKind::NameRef {
703                let variable = argument.text();
704                match scope.get(&variable) {
705                    Some(v) => types.push(Some(v.ty.clone())),
706                    None => {
707                        self.error(argument.syntax(), format!("unknown variable `{variable}`"));
708                        types.push(None);
709                    }
710                }
711            } else {
712                types.push(None);
713            }
714        }
715        let Some(group) = self.groups.get(&name).cloned() else {
716            if !self.has_imports {
717                self.error(
718                    invocation.syntax(),
719                    format!("group `{name}` is not declared and nothing is imported"),
720                );
721            }
722            return;
723        };
724        let parameters = group.parameters().count();
725        if parameters != arguments.len() {
726            self.error(
727                invocation.syntax(),
728                format!(
729                    "group `{name}` takes {parameters} argument(s), found {}",
730                    arguments.len()
731                ),
732            );
733            return;
734        }
735        self.check_group(&group, types, depth + 1);
736    }
737
738    // ---- Element paths ----------------------------------------------------------------------
739
740    // ---- FHIRPath ---------------------------------------------------------------------------
741
742    /// Checks the element paths of a FHIRPath expression: a name that is not a variable is an
743    /// element of `focus`. Problems are warnings, since a wrong path gives an empty result rather
744    /// than an error, but it is nearly always a mistake.
745    fn check_expression(&mut self, expression: &Expr, focus: &Type, scope: &Scope) {
746        self.expression_type(expression.syntax(), focus, scope);
747    }
748
749    /// The type an expression gives, where it is known, reporting missing elements on the way.
750    fn expression_type(&mut self, node: &SyntaxNode, focus: &Type, scope: &Scope) -> Type {
751        match node.kind() {
752            SyntaxKind::NameRef => {
753                let Some(token) = node.first_token() else {
754                    return Type::Unknown;
755                };
756                if token.kind() == SyntaxKind::DollarIdent {
757                    return if token.text() == "$this" {
758                        focus.clone()
759                    } else {
760                        Type::Unknown
761                    };
762                }
763                let name = ast::name_text(&token);
764                if let Some(variable) = scope.get(&name) {
765                    return variable.ty.clone();
766                }
767                // `Patient.name` on a Patient: the type name selects the focus itself.
768                if focus.fhir_type().as_deref() == Some(name.as_str()) {
769                    return focus.clone();
770                }
771                self.element_of(focus, &name, node)
772            }
773            SyntaxKind::MemberExpr => {
774                let mut parts = node.children();
775                let (Some(left), Some(right)) = (parts.next(), parts.next()) else {
776                    return Type::Unknown;
777                };
778                let input = self.expression_type(&left, focus, scope);
779                match right.kind() {
780                    SyntaxKind::NameRef => match right.first_token() {
781                        Some(token) if token.kind() != SyntaxKind::DollarIdent => {
782                            self.element_of(&input, &ast::name_text(&token), &right)
783                        }
784                        _ => Type::Unknown,
785                    },
786                    SyntaxKind::CallExpr => self.call_type(&right, &input, scope),
787                    _ => Type::Unknown,
788                }
789            }
790            SyntaxKind::CallExpr => self.call_type(node, focus, scope),
791            SyntaxKind::ParenExpr => match node.children().next() {
792                Some(inner) => self.expression_type(&inner, focus, scope),
793                None => Type::Unknown,
794            },
795            SyntaxKind::IndexExpr => {
796                let mut parts = node.children();
797                let input = match parts.next() {
798                    Some(left) => self.expression_type(&left, focus, scope),
799                    None => Type::Unknown,
800                };
801                if let Some(index) = parts.next() {
802                    self.expression_type(&index, focus, scope);
803                }
804                input
805            }
806            SyntaxKind::BinaryExpr | SyntaxKind::PrefixExpr | SyntaxKind::TypeExpr => {
807                for child in node.children().filter(|c| Expr::can_cast(c.kind())) {
808                    self.expression_type(&child, focus, scope);
809                }
810                Type::Unknown
811            }
812            _ => Type::Unknown,
813        }
814    }
815
816    fn element_of(&mut self, ty: &Type, name: &str, at: &SyntaxNode) -> Type {
817        match self.types().lookup(ty, name) {
818            Lookup::Found(found) => self.types().element_type(&found, None),
819            Lookup::Missing => {
820                let message = format!("`{}` has no element `{name}`", ty.display());
821                self.report(Severity::Warning, trimmed_range(at), message);
822                Type::Unknown
823            }
824            Lookup::Unknown => Type::Unknown,
825        }
826    }
827
828    /// A function call on `input`. The iterating functions see each input item as the focus of
829    /// their argument; the arguments of the others are left unchecked.
830    fn call_type(&mut self, call: &SyntaxNode, input: &Type, scope: &Scope) -> Type {
831        let name = call
832            .first_token()
833            .map(|t| t.text().to_owned())
834            .unwrap_or_default();
835        let arguments: Vec<SyntaxNode> = call
836            .children()
837            .find(|n| n.kind() == SyntaxKind::ArgumentList)
838            .map(|list| {
839                list.children()
840                    .filter(|c| Expr::can_cast(c.kind()))
841                    .collect()
842            })
843            .unwrap_or_default();
844        let iterating = matches!(
845            name.as_str(),
846            "where" | "select" | "exists" | "all" | "repeat" | "sort" | "aggregate"
847        );
848        let argument_focus = if iterating {
849            input.clone()
850        } else {
851            Type::Unknown
852        };
853        let mut last = Type::Unknown;
854        for argument in &arguments {
855            last = self.expression_type(argument, &argument_focus, scope);
856        }
857        match name.as_str() {
858            "where" | "first" | "last" | "tail" | "skip" | "take" | "single" | "distinct"
859            | "trace" | "repeat" | "sort" => input.clone(),
860            "select" => last,
861            _ => Type::Unknown,
862        }
863    }
864
865    /// Follows `context.a.b` from the context variable's type, reporting missing elements, and
866    /// returns the type at the end. `annotation` is the source's `: Type`, which chooses a type of
867    /// a choice element.
868    fn walk(
869        &mut self,
870        path: &QualifiedName,
871        mut ty: Type,
872        annotation: Option<&str>,
873        annotation_node: Option<&ast::TypeAnnotation>,
874    ) -> Type {
875        let segments = path.segments();
876        for (index, segment) in segments.iter().enumerate().skip(1) {
877            let last = index == segments.len() - 1;
878            let segment = segment.trim_end_matches("[x]");
879            ty = match self.types().lookup(&ty, segment) {
880                Lookup::Unknown => return Type::Unknown,
881                Lookup::Missing => {
882                    let message = format!("`{}` has no element `{segment}`", ty.display());
883                    self.error_at(segment_range(path, index), message);
884                    return Type::Unknown;
885                }
886                Lookup::Found(found) => {
887                    // A `uses` alias names its structure (a profile, say), as in the engine.
888                    let resolved = annotation.map(|name| {
889                        self.type_names
890                            .get(name)
891                            .cloned()
892                            .unwrap_or_else(|| name.to_owned())
893                    });
894                    let chosen = if last { resolved.as_deref() } else { None };
895                    if let (Some(chosen), Some(node)) = (chosen, annotation_node)
896                        && !found.element.types.is_empty()
897                        && !self
898                            .types()
899                            .is_one_of(chosen, &found.element.types, &found.owner.url)
900                    {
901                        self.error(
902                            node.syntax(),
903                            format!(
904                                "`{}` is not a type of `{}.{segment}`; it is {}",
905                                annotation.unwrap_or(chosen),
906                                ty.display(),
907                                found.element.types.join(", ")
908                            ),
909                        );
910                    }
911                    self.types().element_type(&found, chosen)
912                }
913            };
914        }
915        ty
916    }
917}
918
919/// Transforms with their argument counts (minimum, maximum).
920const TRANSFORMS: &[(&str, usize, Option<usize>)] = &[
921    ("create", 0, Some(1)),
922    ("copy", 1, Some(1)),
923    ("truncate", 2, Some(2)),
924    ("escape", 2, Some(3)),
925    ("cast", 1, Some(2)),
926    ("append", 1, None),
927    ("translate", 3, Some(3)),
928    ("reference", 1, Some(1)),
929    ("dateOp", 2, Some(3)),
930    ("uuid", 0, Some(0)),
931    ("pointer", 1, Some(1)),
932    ("evaluate", 1, Some(2)),
933    ("cc", 1, Some(3)),
934    ("c", 2, Some(3)),
935    ("qty", 1, Some(4)),
936    ("id", 2, Some(3)),
937    ("cp", 1, Some(2)),
938    ("toDateTime", 1, Some(2)),
939    ("unixToDateTime", 1, Some(2)),
940    ("toDate", 1, Some(2)),
941    ("unixToDate", 1, Some(2)),
942    ("toTime", 1, Some(2)),
943    ("unixToTime", 1, Some(2)),
944];
945
946/// Variables by name, in nested frames: group parameters, then one frame per rule.
947#[derive(Debug, Default)]
948struct Scope {
949    frames: Vec<HashMap<String, Variable>>,
950}
951
952impl Scope {
953    fn push(&mut self) {
954        self.frames.push(HashMap::new());
955    }
956
957    fn pop(&mut self) {
958        self.frames.pop();
959    }
960
961    fn define(&mut self, name: String, variable: Variable) {
962        if self.frames.is_empty() {
963            self.frames.push(HashMap::new());
964        }
965        self.frames.last_mut().unwrap().insert(name, variable);
966    }
967
968    fn get(&self, name: &str) -> Option<&Variable> {
969        self.frames.iter().rev().find_map(|frame| frame.get(name))
970    }
971}
972
973/// A string literal argument's value.
974fn string_literal(expression: &Expr) -> Option<String> {
975    let literal = ast::Literal::cast(expression.syntax().clone())?;
976    matches!(
977        literal.kind()?,
978        SyntaxKind::SingleQuotedString | SyntaxKind::DoubleQuotedString
979    )
980    .then(|| literal.value())
981    .flatten()
982}
983
984/// The range of the `index`-th name in `a.b.c`.
985fn segment_range(path: &QualifiedName, index: usize) -> TextRange {
986    path.syntax()
987        .children_with_tokens()
988        .filter_map(|e| e.into_token())
989        .filter(|t| matches!(t.kind(), SyntaxKind::Ident | SyntaxKind::DelimitedIdent))
990        .nth(index)
991        .map(|t| t.text_range())
992        .unwrap_or_else(|| trimmed_range(path.syntax()))
993}
994
995fn trimmed_range(node: &SyntaxNode) -> TextRange {
996    let mut tokens = node
997        .descendants_with_tokens()
998        .filter_map(|e| e.into_token())
999        .filter(|t| !t.kind().is_trivia());
1000    match tokens.next() {
1001        Some(first) => {
1002            let end = tokens
1003                .last()
1004                .map_or(first.text_range().end(), |t| t.text_range().end());
1005            TextRange::new(first.text_range().start(), end)
1006        }
1007        None => node.text_range(),
1008    }
1009}