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                    // A variable hides an element of the focus with the same name: almost always
766                    // a slip (`src.code as code where (code.exists())` tests the variable).
767                    // A primitive's own `value` is the primitive: no difference there.
768                    if !(focus.is_primitive() && name == "value")
769                        && matches!(self.types().lookup(focus, &name), Lookup::Found(_))
770                    {
771                        let message = format!(
772                            "`{name}` here is the variable `{name}`, not the element `{name}` of the focus; rename the variable, or write `$this.{name}` for the element"
773                        );
774                        self.report(Severity::Warning, trimmed_range(node), message);
775                    }
776                    return variable.ty.clone();
777                }
778                // `Patient.name` on a Patient: the type name selects the focus itself.
779                if focus.fhir_type().as_deref() == Some(name.as_str()) {
780                    return focus.clone();
781                }
782                self.element_of(focus, &name, node)
783            }
784            SyntaxKind::MemberExpr => {
785                let mut parts = node.children();
786                let (Some(left), Some(right)) = (parts.next(), parts.next()) else {
787                    return Type::Unknown;
788                };
789                let input = self.expression_type(&left, focus, scope);
790                match right.kind() {
791                    SyntaxKind::NameRef => match right.first_token() {
792                        Some(token) if token.kind() != SyntaxKind::DollarIdent => {
793                            self.element_of(&input, &ast::name_text(&token), &right)
794                        }
795                        _ => Type::Unknown,
796                    },
797                    SyntaxKind::CallExpr => self.call_type(&right, &input, scope),
798                    _ => Type::Unknown,
799                }
800            }
801            SyntaxKind::CallExpr => self.call_type(node, focus, scope),
802            SyntaxKind::ParenExpr => match node.children().next() {
803                Some(inner) => self.expression_type(&inner, focus, scope),
804                None => Type::Unknown,
805            },
806            SyntaxKind::IndexExpr => {
807                let mut parts = node.children();
808                let input = match parts.next() {
809                    Some(left) => self.expression_type(&left, focus, scope),
810                    None => Type::Unknown,
811                };
812                if let Some(index) = parts.next() {
813                    self.expression_type(&index, focus, scope);
814                }
815                input
816            }
817            SyntaxKind::BinaryExpr | SyntaxKind::PrefixExpr | SyntaxKind::TypeExpr => {
818                for child in node.children().filter(|c| Expr::can_cast(c.kind())) {
819                    self.expression_type(&child, focus, scope);
820                }
821                Type::Unknown
822            }
823            _ => Type::Unknown,
824        }
825    }
826
827    fn element_of(&mut self, ty: &Type, name: &str, at: &SyntaxNode) -> Type {
828        match self.types().lookup(ty, name) {
829            Lookup::Found(found) => self.types().element_type(&found, None),
830            Lookup::Missing => {
831                let message = format!("`{}` has no element `{name}`", ty.display());
832                self.report(Severity::Warning, trimmed_range(at), message);
833                Type::Unknown
834            }
835            Lookup::Unknown => Type::Unknown,
836        }
837    }
838
839    /// A function call on `input`. The iterating functions see each input item as the focus of
840    /// their argument; the arguments of the others are left unchecked.
841    fn call_type(&mut self, call: &SyntaxNode, input: &Type, scope: &Scope) -> Type {
842        let name = call
843            .first_token()
844            .map(|t| t.text().to_owned())
845            .unwrap_or_default();
846        let arguments: Vec<SyntaxNode> = call
847            .children()
848            .find(|n| n.kind() == SyntaxKind::ArgumentList)
849            .map(|list| {
850                list.children()
851                    .filter(|c| Expr::can_cast(c.kind()))
852                    .collect()
853            })
854            .unwrap_or_default();
855        let iterating = matches!(
856            name.as_str(),
857            "where" | "select" | "exists" | "all" | "repeat" | "sort" | "aggregate"
858        );
859        let argument_focus = if iterating {
860            input.clone()
861        } else {
862            Type::Unknown
863        };
864        let mut last = Type::Unknown;
865        for argument in &arguments {
866            last = self.expression_type(argument, &argument_focus, scope);
867        }
868        match name.as_str() {
869            "where" | "first" | "last" | "tail" | "skip" | "take" | "single" | "distinct"
870            | "trace" | "repeat" | "sort" => input.clone(),
871            "select" => last,
872            _ => Type::Unknown,
873        }
874    }
875
876    /// Follows `context.a.b` from the context variable's type, reporting missing elements, and
877    /// returns the type at the end. `annotation` is the source's `: Type`, which chooses a type of
878    /// a choice element.
879    fn walk(
880        &mut self,
881        path: &QualifiedName,
882        mut ty: Type,
883        annotation: Option<&str>,
884        annotation_node: Option<&ast::TypeAnnotation>,
885    ) -> Type {
886        let segments = path.segments();
887        for (index, segment) in segments.iter().enumerate().skip(1) {
888            let last = index == segments.len() - 1;
889            let segment = segment.trim_end_matches("[x]");
890            ty = match self.types().lookup(&ty, segment) {
891                Lookup::Unknown => return Type::Unknown,
892                Lookup::Missing => {
893                    let message = format!("`{}` has no element `{segment}`", ty.display());
894                    self.error_at(segment_range(path, index), message);
895                    return Type::Unknown;
896                }
897                Lookup::Found(found) => {
898                    // A `uses` alias names its structure (a profile, say), as in the engine.
899                    let resolved = annotation.map(|name| {
900                        self.type_names
901                            .get(name)
902                            .cloned()
903                            .unwrap_or_else(|| name.to_owned())
904                    });
905                    let chosen = if last { resolved.as_deref() } else { None };
906                    if let (Some(chosen), Some(node)) = (chosen, annotation_node)
907                        && !found.element.types.is_empty()
908                        && !self
909                            .types()
910                            .is_one_of(chosen, &found.element.types, &found.owner.url)
911                    {
912                        self.error(
913                            node.syntax(),
914                            format!(
915                                "`{}` is not a type of `{}.{segment}`; it is {}",
916                                annotation.unwrap_or(chosen),
917                                ty.display(),
918                                found.element.types.join(", ")
919                            ),
920                        );
921                    }
922                    self.types().element_type(&found, chosen)
923                }
924            };
925        }
926        ty
927    }
928}
929
930/// Transforms with their argument counts (minimum, maximum).
931const TRANSFORMS: &[(&str, usize, Option<usize>)] = &[
932    ("create", 0, Some(1)),
933    ("copy", 1, Some(1)),
934    ("truncate", 2, Some(2)),
935    ("escape", 2, Some(3)),
936    ("cast", 1, Some(2)),
937    ("append", 1, None),
938    ("translate", 3, Some(3)),
939    ("reference", 1, Some(1)),
940    ("dateOp", 2, Some(3)),
941    ("uuid", 0, Some(0)),
942    ("pointer", 1, Some(1)),
943    ("evaluate", 1, Some(2)),
944    ("cc", 1, Some(3)),
945    ("c", 2, Some(3)),
946    ("qty", 1, Some(4)),
947    ("id", 2, Some(3)),
948    ("cp", 1, Some(2)),
949    ("toDateTime", 1, Some(2)),
950    ("unixToDateTime", 1, Some(2)),
951    ("toDate", 1, Some(2)),
952    ("unixToDate", 1, Some(2)),
953    ("toTime", 1, Some(2)),
954    ("unixToTime", 1, Some(2)),
955];
956
957/// Variables by name, in nested frames: group parameters, then one frame per rule.
958#[derive(Debug, Default)]
959struct Scope {
960    frames: Vec<HashMap<String, Variable>>,
961}
962
963impl Scope {
964    fn push(&mut self) {
965        self.frames.push(HashMap::new());
966    }
967
968    fn pop(&mut self) {
969        self.frames.pop();
970    }
971
972    fn define(&mut self, name: String, variable: Variable) {
973        if self.frames.is_empty() {
974            self.frames.push(HashMap::new());
975        }
976        self.frames.last_mut().unwrap().insert(name, variable);
977    }
978
979    fn get(&self, name: &str) -> Option<&Variable> {
980        self.frames.iter().rev().find_map(|frame| frame.get(name))
981    }
982}
983
984/// A string literal argument's value.
985fn string_literal(expression: &Expr) -> Option<String> {
986    let literal = ast::Literal::cast(expression.syntax().clone())?;
987    matches!(
988        literal.kind()?,
989        SyntaxKind::SingleQuotedString | SyntaxKind::DoubleQuotedString
990    )
991    .then(|| literal.value())
992    .flatten()
993}
994
995/// The range of the `index`-th name in `a.b.c`.
996fn segment_range(path: &QualifiedName, index: usize) -> TextRange {
997    path.syntax()
998        .children_with_tokens()
999        .filter_map(|e| e.into_token())
1000        .filter(|t| matches!(t.kind(), SyntaxKind::Ident | SyntaxKind::DelimitedIdent))
1001        .nth(index)
1002        .map(|t| t.text_range())
1003        .unwrap_or_else(|| trimmed_range(path.syntax()))
1004}
1005
1006fn trimmed_range(node: &SyntaxNode) -> TextRange {
1007    let mut tokens = node
1008        .descendants_with_tokens()
1009        .filter_map(|e| e.into_token())
1010        .filter(|t| !t.kind().is_trivia());
1011    match tokens.next() {
1012        Some(first) => {
1013            let end = tokens
1014                .last()
1015                .map_or(first.text_range().end(), |t| t.text_range().end());
1016            TextRange::new(first.text_range().start(), end)
1017        }
1018        None => node.text_range(),
1019    }
1020}