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