Skip to main content

fml_model/
compile.rs

1//! FML → StructureMap.
2//!
3//! Follows the HL7 reference compiler (`StructureMapUtilities.parse` in org.hl7.fhir.core), so the
4//! StructureMap is the one the IG Publisher and the validator would build from the same text:
5//! auto-generated rule names, the `vvv` variables of the simple copy form, documentation taken
6//! from comments, and FHIRPath expressions stored in canonical form.
7
8use fml_syntax::ast::{
9    self, AstNode, ConceptMap as ConceptMapNode, Expr, GroupTypeMode, ListMode, ModelMode,
10    ParameterMode, TransformValue,
11};
12use std::collections::HashMap;
13
14use fml_syntax::{
15    Diagnostic, Direction, LineIndex, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode,
16    TextRange, parse, quote_fhirpath_string,
17};
18use serde_json::{Map, Value, json};
19
20use crate::metadata;
21use crate::model::{
22    Constant, Dependent, Extension, Group, Input, Parameter, ParameterValue, Rule, Source,
23    Structure, StructureMap, Target,
24};
25
26/// Extension holding source path segments after the first element (`src.a.b` → `b`).
27pub const SOURCE_ELEMENT_EXTENSION: &str =
28    "http://hl7.org/fhir/6.0/StructureDefinition/extension-StructureMap.group.rule.source.element";
29/// Extension holding target path segments after the first element.
30pub const TARGET_ELEMENT_EXTENSION: &str =
31    "http://hl7.org/fhir/6.0/StructureDefinition/extension-StructureMap.group.rule.target.element";
32/// Extension on a parameter value that refers to a contained resource.
33pub const REFERENCES_CONTAINED_EXTENSION: &str =
34    "http://hl7.org/fhir/StructureDefinition/referencesContained";
35/// The variable the simple copy form `src.a -> tgt.a;` binds on both sides.
36pub const AUTO_VARIABLE: &str = "vvv";
37/// Name prefix of the rules produced by an unnamed batch identity rule `src -> tgt : a, b;`.
38pub const BATCH_UNNAMED_PREFIX: &str = "unnamed";
39
40/// The outcome of compiling FML: the map, as complete as the source allows, and the problems.
41#[derive(Debug, Clone)]
42pub struct Compiled {
43    pub map: StructureMap,
44    /// Syntax errors first, then problems found while building the map.
45    pub diagnostics: Vec<Diagnostic>,
46    /// Where each group, rule, source and target of the map is in the text.
47    pub locations: Locations,
48}
49
50/// The text range of each part of a compiled map, by its location in the StructureMap
51/// (`StructureMap.group[0].rule[2].source[0]`, as a debug trace names it). The rules a batch
52/// identity rule produces all point at that rule.
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct Locations {
55    ranges: HashMap<String, TextRange>,
56}
57
58/// A range in the text as the Mapping Language debug trace gives it: zero-based rows and
59/// zero-based columns in UTF-16 code units, the end exclusive.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Span {
62    pub start_row: u32,
63    pub start_column: u32,
64    pub end_row: u32,
65    pub end_column: u32,
66}
67
68impl Locations {
69    /// The byte range of the part at `position`.
70    pub fn range(&self, position: &str) -> Option<TextRange> {
71        self.ranges.get(position).copied()
72    }
73
74    /// The rows and columns of the part at `position` in `text`, the source the map was
75    /// compiled from.
76    pub fn span(&self, text: &str, index: &LineIndex, position: &str) -> Option<Span> {
77        let range = self.range(position)?;
78        let start = index.line_col(text, range.start());
79        let end = index.line_col(text, range.end());
80        Some(Span {
81            start_row: start.line,
82            start_column: start.col_utf16,
83            end_row: end.line,
84            end_column: end.col_utf16,
85        })
86    }
87
88    pub fn len(&self) -> usize {
89        self.ranges.len()
90    }
91
92    pub fn is_empty(&self) -> bool {
93        self.ranges.is_empty()
94    }
95}
96
97impl Compiled {
98    pub fn has_errors(&self) -> bool {
99        self.diagnostics
100            .iter()
101            .any(|d| d.severity == fml_syntax::Severity::Error)
102    }
103}
104
105/// Compiles FML source text to a StructureMap.
106pub fn compile(text: &str) -> Compiled {
107    let parse = parse(text);
108    let mut compiled = compile_tree(&parse.tree());
109    let mut diagnostics = parse.diagnostics().to_vec();
110    diagnostics.append(&mut compiled.diagnostics);
111    compiled.diagnostics = diagnostics;
112    compiled
113}
114
115/// Compiles a parsed file. Syntax errors in the tree are not repeated in the result.
116pub fn compile_tree(file: &ast::SourceFile) -> Compiled {
117    let mut compiler = Compiler {
118        diagnostics: Vec::new(),
119        locations: Locations::default(),
120    };
121    let map = compiler.file(file);
122    Compiled {
123        map,
124        diagnostics: compiler.diagnostics,
125        locations: compiler.locations,
126    }
127}
128
129struct Compiler {
130    diagnostics: Vec<Diagnostic>,
131    locations: Locations,
132}
133
134impl Compiler {
135    fn error(&mut self, node: &SyntaxNode, message: impl Into<String>) {
136        self.diagnostics
137            .push(Diagnostic::error(trimmed_range(node), message));
138    }
139
140    fn locate(&mut self, position: String, node: &SyntaxNode) {
141        self.locations.ranges.insert(position, trimmed_range(node));
142    }
143
144    fn warning(&mut self, node: &SyntaxNode, message: impl Into<String>) {
145        self.diagnostics
146            .push(Diagnostic::warning(trimmed_range(node), message));
147    }
148
149    fn file(&mut self, file: &ast::SourceFile) -> StructureMap {
150        let mut map = StructureMap::new();
151        if let Some(declaration) = file.map_declaration() {
152            map.url = declaration.url().and_then(|u| u.value());
153            map.name = declaration.name().and_then(|n| n.value());
154            map.description = trailing_comments(declaration.syntax());
155            map.status = Some("draft".to_owned());
156        }
157        for metadata in file.metadata() {
158            self.metadata(&mut map, &metadata);
159        }
160        if map.id.is_none()
161            && let Some(name) = &map.name
162        {
163            let id = make_id(name);
164            if !id.is_empty() {
165                map.id = Some(id);
166            }
167        }
168        if map.status.is_none() {
169            map.status = Some("draft".to_owned());
170        }
171        if map.description.is_none() {
172            map.description = map.title.clone();
173        }
174        for concept_map in file.concept_maps() {
175            let contained = self.concept_map(&concept_map, map.status.as_deref());
176            map.contained.push(contained);
177        }
178        for uses in file.uses() {
179            let mode = uses.mode().unwrap_or(ModelMode::Source);
180            map.structure.push(Structure {
181                url: uses.url().and_then(|u| u.value()).unwrap_or_default(),
182                mode: mode.as_str().to_owned(),
183                alias: uses.alias(),
184                documentation: leading_comments(uses.syntax()),
185                other: Map::new(),
186            });
187        }
188        for import in file.imports() {
189            if let Some(url) = import.url().and_then(|u| u.value()) {
190                map.import.push(url);
191            }
192        }
193        for constant in file.constants() {
194            map.constants.push(Constant {
195                name: constant.name().unwrap_or_default(),
196                value: constant
197                    .expression()
198                    .map(|e| canonical(&e))
199                    .unwrap_or_default(),
200                other: Map::new(),
201            });
202        }
203        for group in file.groups() {
204            let position = format!("StructureMap.group[{}]", map.group.len());
205            let group = self.group(&group, &position);
206            map.group.push(group);
207        }
208        map
209    }
210
211    fn metadata(&mut self, map: &mut StructureMap, metadata: &ast::Metadata) {
212        let Some(path) = metadata.key().map(|k| k.segments()) else {
213            return;
214        };
215        if path.first().is_some_and(|name| metadata::is_generic(name)) {
216            let literal = metadata.value().and_then(|literal| {
217                Some(metadata::Literal {
218                    kind: literal.kind()?,
219                    text: literal.value()?,
220                })
221            });
222            if let Err(message) = metadata::apply(&mut map.other, &path, literal) {
223                self.error(metadata.syntax(), message);
224            }
225            return;
226        }
227        let key = path.join(".");
228        let value = metadata.value().and_then(|v| v.value());
229        let field = match key.as_str() {
230            "url" => &mut map.url,
231            "name" => &mut map.name,
232            "title" => &mut map.title,
233            "version" => &mut map.version,
234            "description" => &mut map.description,
235            "status" => &mut map.status,
236            "date" => &mut map.date,
237            "publisher" => &mut map.publisher,
238            "purpose" => &mut map.purpose,
239            "copyright" => &mut map.copyright,
240            "experimental" => {
241                map.experimental = value.map(|v| v == "true");
242                return;
243            }
244            _ => {
245                self.warning(
246                    metadata.syntax(),
247                    format!("metadata `{key}` is not supported and is ignored"),
248                );
249                return;
250            }
251        };
252        *field = value;
253    }
254
255    /// `conceptmap "id" { … }` → a contained ConceptMap (R5 shape).
256    fn concept_map(&mut self, node: &ConceptMapNode, status: Option<&str>) -> Value {
257        let id = node.url().and_then(|u| u.value()).unwrap_or_default();
258        let prefixes: Vec<(String, String)> = node
259            .prefixes()
260            .filter_map(|p| Some((p.name()?, p.url()?.value()?)))
261            .collect();
262        let resolve = |compiler: &mut Compiler, prefix: Option<String>, at: &SyntaxNode| {
263            let prefix = prefix.unwrap_or_default();
264            match prefixes.iter().find(|(name, _)| *name == prefix) {
265                Some((_, url)) => Some(url.clone()),
266                None => {
267                    compiler.error(at, format!("unknown concept map prefix `{prefix}`"));
268                    None
269                }
270            }
271        };
272        let mut groups: Vec<ConceptMapGroup> = Vec::new();
273        for unmapped in node.unmapped() {
274            let Some(source) = resolve(self, unmapped.prefix(), unmapped.syntax()) else {
275                continue;
276            };
277            let index = concept_map_group(&mut groups, &source, None);
278            match unmapped.mode().as_deref() {
279                Some("provided") => groups[index].unmapped = Some("use-source-code"),
280                other => self.error(
281                    unmapped.syntax(),
282                    format!(
283                        "unsupported unmapped mode `{}`; only `provided` is defined",
284                        other.unwrap_or_default()
285                    ),
286                ),
287            }
288        }
289        for entry in node.entries() {
290            let (Some(source_code), Some(target_code)) = (entry.source(), entry.target()) else {
291                continue;
292            };
293            let source = resolve(self, source_code.prefix(), source_code.syntax());
294            let target = resolve(self, target_code.prefix(), target_code.syntax());
295            let (Some(source), Some(target)) = (source, target) else {
296                continue;
297            };
298            let relation = entry.relation().unwrap_or_default();
299            let relationship = match relation.as_str() {
300                "-" => "related-to",
301                "==" => "equivalent",
302                "!=" => "not-related-to",
303                "<=" => "source-is-narrower-than-target",
304                ">=" => "source-is-broader-than-target",
305                other => {
306                    self.error(
307                        entry.syntax(),
308                        format!("unknown concept map relationship `{other}`"),
309                    );
310                    continue;
311                }
312            };
313            let index = concept_map_group(&mut groups, &source, Some(&target));
314            groups[index].elements.push(json!({
315                "code": source_code.code().unwrap_or_default(),
316                "target": [{
317                    "code": target_code.code().unwrap_or_default(),
318                    "relationship": relationship,
319                }],
320            }));
321        }
322        let mut map = Map::new();
323        map.insert("resourceType".into(), "ConceptMap".into());
324        map.insert("id".into(), id.into());
325        if let Some(status) = status {
326            map.insert("status".into(), status.into());
327        }
328        let groups: Vec<Value> = groups
329            .into_iter()
330            .map(|g| {
331                let mut group = Map::new();
332                group.insert("source".into(), g.source.into());
333                if let Some(target) = g.target {
334                    group.insert("target".into(), target.into());
335                }
336                if !g.elements.is_empty() {
337                    group.insert("element".into(), g.elements.into());
338                }
339                if let Some(mode) = g.unmapped {
340                    group.insert("unmapped".into(), json!({ "mode": mode }));
341                }
342                Value::Object(group)
343            })
344            .collect();
345        if !groups.is_empty() {
346            map.insert("group".into(), groups.into());
347        }
348        Value::Object(map)
349    }
350
351    fn group(&mut self, node: &ast::Group, position: &str) -> Group {
352        self.locate(position.to_owned(), node.syntax());
353        let mut group = Group {
354            name: node.name().unwrap_or_default(),
355            extends: node.extends(),
356            type_mode: node.type_mode().map(|mode| {
357                match mode {
358                    GroupTypeMode::Types => "types",
359                    GroupTypeMode::TypeAndTypes => "type-and-types",
360                }
361                .to_owned()
362            }),
363            documentation: leading_comments(node.syntax()),
364            ..Group::default()
365        };
366        for parameter in node.parameters() {
367            group.input.push(Input {
368                name: parameter.name().unwrap_or_default(),
369                type_name: parameter.type_annotation().and_then(|t| t.type_name()),
370                mode: match parameter.mode() {
371                    Some(ParameterMode::Target) => "target",
372                    _ => "source",
373                }
374                .to_owned(),
375                documentation: None,
376                other: Map::new(),
377            });
378        }
379        if let Some(body) = node.body() {
380            group.rule = self.rules(&body, position);
381        }
382        group
383    }
384
385    fn rules(&mut self, block: &ast::RuleBlock, parent: &str) -> Vec<Rule> {
386        let mut rules = Vec::new();
387        for node in block.rules() {
388            let first = rules.len();
389            self.rule(&node, &mut rules, parent);
390            self.locate_rules(&node, parent, first..rules.len());
391        }
392        rules
393    }
394
395    /// Records where the rules `indices` of `parent`, compiled from `node`, are.
396    fn locate_rules(&mut self, node: &ast::Rule, parent: &str, indices: std::ops::Range<usize>) {
397        let batch = indices.len() > 1;
398        let sources: Vec<ast::Source> = node.sources().collect();
399        let targets: Vec<ast::Target> = node.targets().collect();
400        for index in indices {
401            let position = format!("{parent}.rule[{index}]");
402            self.locate(position.clone(), node.syntax());
403            for (k, source) in sources.iter().enumerate() {
404                let at = if batch {
405                    node.syntax()
406                } else {
407                    source.syntax()
408                };
409                self.locate(format!("{position}.source[{k}]"), at);
410            }
411            for (k, target) in targets.iter().enumerate() {
412                let at = if batch {
413                    node.syntax()
414                } else {
415                    target.syntax()
416                };
417                self.locate(format!("{position}.target[{k}]"), at);
418            }
419        }
420    }
421
422    /// Compiles one rule into `out`; a batch identity rule produces one rule per field.
423    fn rule(&mut self, node: &ast::Rule, out: &mut Vec<Rule>, parent: &str) {
424        let mut rule = Rule {
425            documentation: leading_comments(node.syntax()),
426            ..Rule::default()
427        };
428        rule.source = node.sources().map(|s| self.source(&s)).collect();
429        rule.target = node.targets().map(|t| self.target(&t)).collect();
430
431        let fields = node.identity_fields();
432        if !fields.is_empty() {
433            return self.batch_identity(node, rule, &fields, out);
434        }
435
436        if let Some(dependent) = node.dependent() {
437            if let Some(block) = dependent.rules() {
438                let position = format!("{parent}.rule[{}]", out.len());
439                rule.rule = self.rules(&block, &position);
440            }
441            for invocation in dependent.invocations() {
442                rule.dependent.push(Dependent {
443                    name: invocation.name().unwrap_or_default(),
444                    parameter: invocation
445                        .arguments()
446                        .map(|argument| self.parameter(&argument, false).into())
447                        .collect(),
448                    other: Map::new(),
449                });
450            }
451        }
452        if is_simple_copy(&rule) {
453            rule.source[0].variable = Some(AUTO_VARIABLE.to_owned());
454            rule.target[0].variable = Some(AUTO_VARIABLE.to_owned());
455            rule.target[0].transform = Some("create".to_owned());
456        }
457        rule.name = match node.name() {
458            Some(name) => Some(name.replace('-', "")),
459            None => self.generated_name(node, &rule),
460        };
461        out.push(rule);
462    }
463
464    /// The name the reference compiler gives an unnamed rule: the source path, plus the
465    /// capitalised type when the source is typed.
466    fn generated_name(&mut self, node: &ast::Rule, rule: &Rule) -> Option<String> {
467        if rule.source.len() != 1 {
468            self.error(node.syntax(), "a rule with several sources needs a name");
469            return None;
470        }
471        let source = &rule.source[0];
472        let path = source_path(source);
473        if path.is_empty() {
474            self.warning(
475                node.syntax(),
476                "a rule whose source has no element needs a name",
477            );
478            return None;
479        }
480        Some(match &source.type_name {
481            Some(type_name) => path + &capitalize(type_name),
482            None => path,
483        })
484    }
485
486    /// `src -> tgt : a, b "name";` → one copy rule per field, named `name` + field.
487    fn batch_identity(
488        &mut self,
489        node: &ast::Rule,
490        mut first: Rule,
491        fields: &[String],
492        out: &mut Vec<Rule>,
493    ) {
494        let prefix = node
495            .name()
496            .map(|n| n.replace('-', ""))
497            .unwrap_or_else(|| BATCH_UNNAMED_PREFIX.to_owned());
498        let source_context = first
499            .source
500            .first()
501            .map(|s| s.context.clone())
502            .unwrap_or_default();
503        let target_context = first.target.first().and_then(|t| t.context.clone());
504        if first.target.is_empty() {
505            first.target.push(Target::default());
506        }
507        let copy = |rule: &mut Rule, field: &str| {
508            let source = &mut rule.source[0];
509            source.element = Some(field.to_owned());
510            source.variable = Some(AUTO_VARIABLE.to_owned());
511            let target = &mut rule.target[0];
512            target.element = Some(field.to_owned());
513            target.variable = Some(AUTO_VARIABLE.to_owned());
514            target.transform = Some("create".to_owned());
515            rule.name = Some(make_id(&format!("{prefix}{field}")));
516        };
517        copy(&mut first, &fields[0]);
518        out.push(first);
519        for field in &fields[1..] {
520            let mut rule = Rule {
521                source: vec![Source {
522                    context: source_context.clone(),
523                    ..Source::default()
524                }],
525                target: vec![Target {
526                    context: target_context.clone(),
527                    ..Target::default()
528                }],
529                ..Rule::default()
530            };
531            copy(&mut rule, field);
532            out.push(rule);
533        }
534    }
535
536    fn source(&mut self, node: &ast::Source) -> Source {
537        let mut segments = node.path().map(|p| p.segments()).unwrap_or_default();
538        let mut source = Source::default();
539        if !segments.is_empty() {
540            source.context = segments.remove(0);
541        }
542        let mut segments = segments.into_iter();
543        source.element = segments.next();
544        source.extension = segments
545            .map(|s| Extension::string(SOURCE_ELEMENT_EXTENSION, s))
546            .collect();
547        source.type_name = node.type_annotation().and_then(|t| t.type_name());
548        if let Some((min, max)) = node.cardinality() {
549            source.min = min.parse().ok();
550            source.max = Some(max);
551        }
552        if let Some(default) = node.default_value()
553            && let Some(expression) = default.expression()
554        {
555            // The legacy `default "text"` form holds a string, stored as a FHIRPath literal.
556            source.default_value = Some(match expression.syntax().kind() {
557                SyntaxKind::Literal
558                    if expression
559                        .syntax()
560                        .first_token()
561                        .is_some_and(|t| t.kind() == SyntaxKind::DoubleQuotedString) =>
562                {
563                    quote_fhirpath_string(&ast::unescape(
564                        expression.syntax().first_token().unwrap().text(),
565                    ))
566                }
567                _ => canonical(&expression),
568            });
569        }
570        source.list_mode = node
571            .list_mode()
572            .map(|mode| list_mode_code(&mode).to_owned());
573        source.variable = node.alias();
574        source.condition = node.where_clause().map(|e| canonical(&e));
575        source.check = node.check_clause().map(|e| canonical(&e));
576        source.log_message = node.log_clause().map(|e| canonical(&e));
577        source
578    }
579
580    fn target(&mut self, node: &ast::Target) -> Target {
581        let mut target = Target::default();
582        if let Some(path) = node.path() {
583            let mut segments = path.segments().into_iter();
584            target.context = segments.next();
585            target.element = segments.next();
586            target.extension = segments
587                .map(|s| Extension::string(TARGET_ELEMENT_EXTENSION, s))
588                .collect();
589        }
590        match node.transform() {
591            Some(TransformValue::Literal(literal)) => {
592                target.transform = Some("copy".to_owned());
593                target.parameter.push(literal_parameter(&literal).into());
594            }
595            Some(TransformValue::Name(name)) => {
596                target.transform = Some("copy".to_owned());
597                target
598                    .parameter
599                    .push(ParameterValue::Id(name.text()).into());
600            }
601            Some(TransformValue::Expression(expression)) => {
602                target.transform = Some("evaluate".to_owned());
603                target
604                    .parameter
605                    .push(ParameterValue::String(canonical(&expression)).into());
606            }
607            Some(TransformValue::Invocation(invocation)) => {
608                let name = invocation.name().unwrap_or_default();
609                if !TRANSFORMS.contains(&name.as_str()) {
610                    self.error(invocation.syntax(), format!("unknown transform `{name}`"));
611                }
612                let evaluate = name == "evaluate";
613                target.parameter = invocation
614                    .arguments()
615                    .enumerate()
616                    .map(|(index, argument)| {
617                        Parameter::from(self.parameter(&argument, evaluate && index == 1))
618                    })
619                    .collect();
620                // `translate(v, '#id', …)` names a contained ConceptMap; the reference compiler
621                // records that with an extension on the value.
622                if name == "translate"
623                    && let Some(parameter) = target.parameter.get_mut(1)
624                    && let ParameterValue::String(map) = &parameter.value
625                    && map.starts_with('#')
626                {
627                    parameter.value_element = Some(json!({
628                        "extension": [{
629                            "url": REFERENCES_CONTAINED_EXTENSION,
630                            "valueReference": { "reference": map },
631                        }],
632                    }));
633                }
634                target.transform = Some(name);
635            }
636            None => {}
637        }
638        target.variable = node.alias();
639        if let Some(mode) = node.list_mode() {
640            if let ListMode::Share(id) = &mode {
641                target.list_rule_id = Some(id.clone());
642            }
643            target.list_mode.push(list_mode_code(&mode).to_owned());
644        }
645        target
646    }
647
648    /// A transform or dependent-group argument: a variable, a constant, or (for the expression
649    /// argument of `evaluate`) FHIRPath text.
650    fn parameter(&mut self, argument: &Expr, expression: bool) -> ParameterValue {
651        if expression {
652            return ParameterValue::String(canonical(argument));
653        }
654        let node = argument.syntax();
655        match node.kind() {
656            SyntaxKind::NameRef => {
657                let token = node.first_token().expect("a name has a token");
658                ParameterValue::Id(ast::name_text(&token))
659            }
660            SyntaxKind::Literal => match ast::Literal::cast(node.clone()) {
661                Some(literal) => literal_parameter(&literal),
662                None => ParameterValue::String(canonical(argument)),
663            },
664            SyntaxKind::PrefixExpr
665                if node
666                    .first_token()
667                    .is_some_and(|t| t.kind() == SyntaxKind::Minus) =>
668            {
669                let text: String = canonical(argument).split_whitespace().collect();
670                if let Ok(value) = text.parse::<i64>() {
671                    ParameterValue::Integer(value)
672                } else if let Ok(value) = text.parse::<serde_json::Number>() {
673                    ParameterValue::Decimal(value)
674                } else {
675                    self.complex_parameter(argument)
676                }
677            }
678            _ => self.complex_parameter(argument),
679        }
680    }
681
682    fn complex_parameter(&mut self, argument: &Expr) -> ParameterValue {
683        self.error(
684            argument.syntax(),
685            "a transform argument must be a variable or a constant; use `evaluate` for expressions",
686        );
687        ParameterValue::String(canonical(argument))
688    }
689}
690
691/// The transforms of the Mapping Language IG, R5 and earlier versions.
692const TRANSFORMS: &[&str] = &[
693    "create",
694    "copy",
695    "truncate",
696    "escape",
697    "cast",
698    "append",
699    "translate",
700    "reference",
701    "dateOp",
702    "uuid",
703    "pointer",
704    "evaluate",
705    "cc",
706    "c",
707    "qty",
708    "id",
709    "cp",
710    "toDateTime",
711    "unixToDateTime",
712    "toDate",
713    "unixToDate",
714    "toTime",
715    "unixToTime",
716];
717
718struct ConceptMapGroup {
719    source: String,
720    target: Option<String>,
721    elements: Vec<Value>,
722    unmapped: Option<&'static str>,
723}
724
725/// The group for a source and target system, created when missing; same rules as the reference
726/// compiler, where a group without a target yet adopts the first target used with it.
727fn concept_map_group(
728    groups: &mut Vec<ConceptMapGroup>,
729    source: &str,
730    target: Option<&str>,
731) -> usize {
732    for (index, group) in groups.iter_mut().enumerate() {
733        if group.source == source
734            && (group.target.is_none() || target.is_none() || group.target.as_deref() == target)
735        {
736            if group.target.is_none()
737                && let Some(target) = target
738            {
739                group.target = Some(target.to_owned());
740            }
741            return index;
742        }
743    }
744    groups.push(ConceptMapGroup {
745        source: source.to_owned(),
746        target: target.map(str::to_owned),
747        elements: Vec::new(),
748        unmapped: None,
749    });
750    groups.len() - 1
751}
752
753/// The simple copy form `src.a -> tgt.a;`: one source and one target, both with an element and no
754/// variable, no parameters, no dependent rules.
755fn is_simple_copy(rule: &Rule) -> bool {
756    let [source] = rule.source.as_slice() else {
757        return false;
758    };
759    let [target] = rule.target.as_slice() else {
760        return false;
761    };
762    !source.context.is_empty()
763        && source.element.is_some()
764        && source.variable.is_none()
765        && target.context.is_some()
766        && target.element.is_some()
767        && target.variable.is_none()
768        && target.parameter.is_empty()
769        && rule.dependent.is_empty()
770        && rule.rule.is_empty()
771}
772
773/// The source element path: the element and the path segments in the element extension.
774pub fn source_path(source: &Source) -> String {
775    source
776        .element
777        .iter()
778        .cloned()
779        .chain(
780            source
781                .extension
782                .iter()
783                .filter(|e| e.url == SOURCE_ELEMENT_EXTENSION)
784                .filter_map(|e| e.value_string.clone()),
785        )
786        .collect::<Vec<_>>()
787        .join(".")
788}
789
790/// The FHIRPath text StructureMap stores; see [`crate::canonical`].
791fn canonical(expression: &Expr) -> String {
792    crate::canonical::canonical(expression)
793}
794
795fn literal_parameter(literal: &ast::Literal) -> ParameterValue {
796    let value = literal.value().unwrap_or_default();
797    match literal.kind() {
798        Some(SyntaxKind::Integer | SyntaxKind::LongNumber) => value
799            .trim_end_matches('L')
800            .parse()
801            .map(ParameterValue::Integer)
802            .unwrap_or(ParameterValue::String(value)),
803        Some(SyntaxKind::Decimal) => value
804            .parse()
805            .map(ParameterValue::Decimal)
806            .unwrap_or(ParameterValue::String(value)),
807        Some(SyntaxKind::Date) => ParameterValue::Date(value.trim_start_matches('@').to_owned()),
808        Some(SyntaxKind::DateTime) => {
809            ParameterValue::DateTime(value.trim_start_matches('@').to_owned())
810        }
811        Some(SyntaxKind::Time) => ParameterValue::Time(value.trim_start_matches("@T").to_owned()),
812        Some(SyntaxKind::Ident) if value == "true" || value == "false" => {
813            ParameterValue::Boolean(value == "true")
814        }
815        _ => ParameterValue::String(value),
816    }
817}
818
819fn list_mode_code(mode: &ListMode) -> &'static str {
820    match mode {
821        ListMode::First => "first",
822        ListMode::NotFirst => "not_first",
823        ListMode::Last => "last",
824        ListMode::NotLast => "not_last",
825        ListMode::OnlyOne => "only_one",
826        ListMode::Single => "single",
827        ListMode::Collate => "collate",
828        ListMode::Share(_) => "share",
829    }
830}
831
832/// `Utilities.makeId` of the reference implementation: keeps ASCII letters, digits, `-` and `.`.
833pub fn make_id(name: &str) -> String {
834    name.chars()
835        .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '.')
836        .collect()
837}
838
839fn capitalize(text: &str) -> String {
840    let mut chars = text.chars();
841    match chars.next() {
842        Some(first) => first.to_uppercase().chain(chars).collect(),
843        None => String::new(),
844    }
845}
846
847/// The documentation the reference compiler takes from comments: the comments between the
848/// previous token and this statement, one per line, without their `//` or `/* */`. A comment on
849/// the same line as the end of the previous rule or `uses` belongs to that statement instead.
850fn leading_comments(node: &SyntaxNode) -> Option<String> {
851    let mut comments = Vec::new();
852    let mut previous: Option<SyntaxElement> = None;
853    for element in node.siblings_with_tokens(Direction::Prev).skip(1) {
854        match element {
855            NodeOrToken::Token(token) if token.kind() == SyntaxKind::Whitespace => {}
856            NodeOrToken::Token(token) if token.kind().is_comment() => comments.push(token),
857            element => {
858                previous = Some(element);
859                break;
860            }
861        }
862    }
863    comments.reverse();
864    // Comments right after `map "url" = "name"` are the map's description.
865    if previous
866        .as_ref()
867        .is_some_and(|p| p.kind() == SyntaxKind::MapDeclaration)
868    {
869        return None;
870    }
871    let owns_trailing = previous
872        .as_ref()
873        .is_some_and(|p| matches!(p.kind(), SyntaxKind::Rule | SyntaxKind::UsesDeclaration));
874    if owns_trailing
875        && comments
876            .first()
877            .is_some_and(|first| !after_line_break(first))
878    {
879        comments.remove(0);
880    }
881    if comments.is_empty() {
882        return None;
883    }
884    Some(
885        comments
886            .iter()
887            .map(|token| comment_text(token.text()))
888            .collect::<Vec<_>>()
889            .join("\r\n"),
890    )
891}
892
893/// Whether a line break comes right before `token`.
894fn after_line_break(token: &fml_syntax::SyntaxToken) -> bool {
895    token.prev_sibling_or_token().is_some_and(|e| match e {
896        NodeOrToken::Token(t) => {
897            t.kind() == SyntaxKind::Whitespace && t.text().contains(['\n', '\r'])
898        }
899        NodeOrToken::Node(_) => false,
900    })
901}
902
903/// Comments right after `node` on the following lines, up to the next token.
904fn trailing_comments(node: &SyntaxNode) -> Option<String> {
905    let comments: Vec<String> = node
906        .siblings_with_tokens(Direction::Next)
907        .skip(1)
908        .take_while(|e| e.kind().is_trivia())
909        .filter_map(|e| e.into_token())
910        .filter(|t| t.kind().is_comment())
911        .map(|t| comment_text(t.text()))
912        .collect();
913    (!comments.is_empty()).then(|| comments.join("\r\n"))
914}
915
916fn comment_text(text: &str) -> String {
917    let inner = if let Some(line) = text.strip_prefix("//") {
918        line
919    } else {
920        text.strip_prefix("/*")
921            .map(|t| t.strip_suffix("*/").unwrap_or(t))
922            .unwrap_or(text)
923    };
924    inner.trim().to_owned()
925}
926
927/// The node's range without leading and trailing trivia, for diagnostics.
928fn trimmed_range(node: &SyntaxNode) -> TextRange {
929    let mut tokens = node
930        .descendants_with_tokens()
931        .filter_map(NodeOrToken::into_token)
932        .filter(|t| !t.kind().is_trivia());
933    match tokens.next() {
934        Some(first) => {
935            let end = tokens
936                .last()
937                .map_or(first.text_range().end(), |t| t.text_range().end());
938            TextRange::new(first.text_range().start(), end)
939        }
940        None => node.text_range(),
941    }
942}