Skip to main content

meta_language/grammar/interchange/lowering/
metadata.rs

1//! The reconstruction metadata of a lowering as links, the reconstruction of
2//! the original grammar from the executable and the metadata, and the check
3//! that reports every feature a lowering does not carry.
4
5use std::collections::HashMap;
6
7use links_notation::{LiNo, ParserConfig, parse_lino_to_links_with_config};
8
9use super::super::super::round_trip::canonical_rule_definition;
10use super::super::super::runtime::GrammarParser;
11use super::super::super::{
12    Grammar, GrammarDeclarations, GrammarExpr, GrammarFormat, GrammarImportError, GrammarRule,
13    RuleAttributes, RuleKind,
14};
15use super::super::links::{
16    parse_grammar_links, parse_node, percent_decode_links_text, percent_encode_links_text,
17    render_links_expression,
18};
19use super::super::{grammar_importer, render_declaration_links, render_rule_fields};
20use super::{
21    GrammarImportFn, GrammarLowering, GrammarLoweringEncoding, GrammarLoweringError,
22    GrammarLoweringMetadata, GrammarLoweringOptions, GrammarLoweringStatus, GrammarLoweringStep,
23    assemble, check_format, lower_grammar, map_children, rule,
24};
25
26type Node = LiNo<String>;
27
28/// Renders the reconstruction metadata as links, one link per line.
29#[must_use]
30pub fn render_lowering_metadata(metadata: &GrammarLoweringMetadata) -> String {
31    let text = |value: &str| percent_encode_links_text(value);
32    let mut lines = vec![
33        format!(
34            "(lowering {} {})",
35            metadata.format,
36            metadata.status.as_str()
37        ),
38        metadata.source.map_or_else(
39            || "(source)".to_owned(),
40            |source| format!("(source {})", text(source.as_str())),
41        ),
42        format!("(start {})", text(&metadata.start)),
43        format!(
44            "(rules {})",
45            metadata
46                .order
47                .iter()
48                .map(|name| text(name))
49                .collect::<Vec<_>>()
50                .join(" ")
51        ),
52    ];
53    for step in &metadata.steps {
54        lines.push(match step {
55            GrammarLoweringStep::Helper {
56                helper,
57                owner,
58                construct,
59                encoding,
60                note,
61                original,
62            } => format!(
63                "(helper {} {} {construct} {} {} {})",
64                text(helper),
65                text(owner),
66                encoding.as_str(),
67                text(note),
68                render_links_expression(original)
69            ),
70            GrammarLoweringStep::Rename { rule, value } => {
71                format!("(rename {} {})", text(rule), text(value))
72            }
73            GrammarLoweringStep::Kind { rule, kind } => {
74                format!("(kind {} {})", text(rule), text(kind.as_str()))
75            }
76            GrammarLoweringStep::Doc { rule, doc } => {
77                format!("(doc {} {})", text(rule), text(doc))
78            }
79            GrammarLoweringStep::Declarations { declarations } => {
80                format!("(declarations {})", text(declarations))
81            }
82            GrammarLoweringStep::Attributes { rule, attributes } => {
83                format!("(attributes {} {})", text(rule), text(attributes))
84            }
85        });
86    }
87    lines.iter().fold(String::new(), |mut text, line| {
88        text.push_str(line);
89        text.push('\n');
90        text
91    })
92}
93
94/// The declarations as native links (a grammar link with the matching and
95/// the settling, then one link per declaration), or `None` for a grammar without declarations.
96pub(super) fn render_declarations_text(declarations: &GrammarDeclarations) -> Option<String> {
97    let lines = render_declaration_links(declarations);
98    if declarations.matching.is_none() && declarations.settling.is_none() && lines.is_empty() {
99        return None;
100    }
101    let fields = declarations
102        .matching
103        .iter()
104        .map(|matching| format!(" (matching {matching})"))
105        .chain(
106            declarations
107                .settling
108                .iter()
109                .map(|settling| format!(" (settling {})", settling.join(" "))),
110        )
111        .collect::<String>();
112    let header = format!("(grammar{fields})");
113    Some(
114        std::iter::once(header)
115            .chain(lines)
116            .fold(String::new(), |mut text, line| {
117                text.push_str(&line);
118                text.push('\n');
119                text
120            }),
121    )
122}
123
124/// Reads a declarations step back through the native links reader, which
125/// needs one rule after the declarations.
126fn parse_declarations_text(text: &str) -> Result<GrammarDeclarations, GrammarLoweringError> {
127    Ok(
128        parse_grammar_links(&format!("{text}(rule _ normal empty)\n"))?
129            .declarations()
130            .clone(),
131    )
132}
133
134/// Reads an attributes step back as the rule fields of a placeholder rule.
135fn parse_rule_fields(fields: &str) -> Result<RuleAttributes, GrammarLoweringError> {
136    let grammar = parse_grammar_links(&format!("(grammar)\n(rule _ normal empty {fields})\n"))?;
137    Ok(grammar
138        .rule("_")
139        .map(|placeholder| placeholder.attributes.clone())
140        .unwrap_or_default())
141}
142
143fn metadata_error(detail: impl AsRef<str>) -> GrammarLoweringError {
144    GrammarLoweringError::Import(GrammarImportError::Parse {
145        format: GrammarFormat::MetaLanguage,
146        message: format!("lowering metadata: {}", detail.as_ref()),
147    })
148}
149
150fn as_word(node: &Node) -> Option<&str> {
151    match node {
152        LiNo::Ref(word) => Some(word),
153        LiNo::Link {
154            id: Some(word),
155            values,
156        } if values.is_empty() => Some(word),
157        LiNo::Link { .. } => None,
158    }
159}
160
161fn word(node: Option<&Node>) -> Result<&str, GrammarLoweringError> {
162    node.and_then(as_word)
163        .ok_or_else(|| metadata_error("expected a word"))
164}
165
166fn decoded(node: Option<&Node>) -> Result<String, GrammarLoweringError> {
167    Ok(percent_decode_links_text(word(node)?)?)
168}
169
170/// A metadata statement as its head word and arguments; a link of one word
171/// reads as a word.
172fn head(statement: &Node) -> Result<(&str, &[Node]), GrammarLoweringError> {
173    if let Some(word) = as_word(statement) {
174        return Ok((word, &[]));
175    }
176    let LiNo::Link { id: None, values } = statement else {
177        return Err(metadata_error("every metadata entry is a link"));
178    };
179    let Some((first, rest)) = values.split_first() else {
180        return Err(metadata_error("every metadata entry is a link"));
181    };
182    as_word(first)
183        .map(|first| (first, rest))
184        .ok_or_else(|| metadata_error("a metadata link starts with a word"))
185}
186
187fn step(kind: &str, args: &[Node]) -> Result<GrammarLoweringStep, GrammarLoweringError> {
188    match (kind, args.len()) {
189        ("helper", 6) => {
190            let encoding = match word(args.get(3))? {
191                "exact" => GrammarLoweringEncoding::Exact,
192                "approximate" => GrammarLoweringEncoding::Approximate,
193                other => return Err(metadata_error(format!("unknown encoding {other}"))),
194            };
195            Ok(GrammarLoweringStep::Helper {
196                helper: decoded(args.first())?,
197                owner: decoded(args.get(1))?,
198                construct: word(args.get(2))?.to_owned(),
199                encoding,
200                note: decoded(args.get(4))?,
201                original: parse_node(&args[5])?,
202            })
203        }
204        ("declarations", 1) => {
205            let declarations = decoded(args.first())?;
206            parse_declarations_text(&declarations)?;
207            Ok(GrammarLoweringStep::Declarations { declarations })
208        }
209        ("attributes", 2) => {
210            let attributes = decoded(args.get(1))?;
211            parse_rule_fields(&attributes)?;
212            Ok(GrammarLoweringStep::Attributes {
213                rule: decoded(args.first())?,
214                attributes,
215            })
216        }
217        ("rename" | "kind" | "doc", 2) => {
218            let value = decoded(args.get(1))?;
219            if kind == "kind" {
220                let Some(kind) = RuleKind::from_tag(&value) else {
221                    return Err(metadata_error(format!("unknown rule kind {value}")));
222                };
223                return Ok(GrammarLoweringStep::Kind {
224                    rule: decoded(args.first())?,
225                    kind,
226                });
227            }
228            let rule = decoded(args.first())?;
229            Ok(if kind == "rename" {
230                GrammarLoweringStep::Rename { rule, value }
231            } else {
232                GrammarLoweringStep::Doc { rule, doc: value }
233            })
234        }
235        _ => Err(metadata_error(format!("unexpected link {kind}"))),
236    }
237}
238
239/// Reads the metadata written by [`render_lowering_metadata`].
240///
241/// # Errors
242///
243/// Returns a [`GrammarLoweringError`] for malformed links, an unsupported
244/// format or an unknown status, encoding, rule kind or source format.
245pub fn parse_lowering_metadata(
246    source: &str,
247) -> Result<GrammarLoweringMetadata, GrammarLoweringError> {
248    let statements = parse_lino_to_links_with_config(source, &ParserConfig::without_comments())
249        .map_err(|error| metadata_error(error.to_string()))?;
250    let links = statements.iter().map(head).collect::<Result<Vec<_>, _>>()?;
251    let [
252        (header, header_args),
253        (source_head, source_args),
254        (start_head, start_args),
255        (rules_head, rules_args),
256        rest @ ..,
257    ] = links.as_slice()
258    else {
259        return Err(metadata_error(
260            "the metadata starts with the lowering, source, start and rules links",
261        ));
262    };
263    if *header != "lowering" || header_args.len() != 2 {
264        return Err(metadata_error(
265            "the first link must be (lowering FORMAT STATUS)",
266        ));
267    }
268    let format = word(header_args.first())?;
269    check_format(format)?;
270    let status = match word(header_args.get(1))? {
271        "exact" => GrammarLoweringStatus::Exact,
272        "approximate" => GrammarLoweringStatus::Approximate,
273        other => return Err(metadata_error(format!("unknown status {other}"))),
274    };
275    if *source_head != "source" || source_args.len() > 1 {
276        return Err(metadata_error("the second link must be (source [FORMAT])"));
277    }
278    if *start_head != "start" || start_args.len() != 1 {
279        return Err(metadata_error("the third link must be (start NAME)"));
280    }
281    if *rules_head != "rules" {
282        return Err(metadata_error("the fourth link must be (rules NAME...)"));
283    }
284    let steps = rest
285        .iter()
286        .map(|(kind, args)| step(kind, args))
287        .collect::<Result<Vec<_>, _>>()?;
288    let source_format = if source_args.is_empty() {
289        None
290    } else {
291        let tag = decoded(source_args.first())?;
292        let Some(format) = GrammarFormat::from_tag(&tag) else {
293            return Err(metadata_error(format!("unknown source format {tag}")));
294        };
295        Some(format)
296    };
297    Ok(GrammarLoweringMetadata {
298        format: format.to_owned(),
299        status,
300        source: source_format,
301        start: decoded(start_args.first())?,
302        order: rules_args
303            .iter()
304            .map(|value| decoded(Some(value)))
305            .collect::<Result<_, _>>()?,
306        steps,
307    })
308}
309
310/// Reconstructs the original grammar from an executable text and its
311/// lowering metadata.
312///
313/// The executable is imported, every renamed rule gets its
314/// original name back, every helper reference is replaced with the original
315/// expression the metadata records, the helpers are removed and the rule
316/// kinds and documentation the target did not keep, the rule fields and the
317/// grammar declarations are restored. `import`
318/// replaces the format's own importer.
319///
320/// # Errors
321///
322/// Returns a [`GrammarLoweringError`] for malformed metadata, an importer
323/// error or an executable without a rule the metadata names.
324pub fn reconstruct_grammar(
325    executable: &str,
326    metadata: &str,
327    import: Option<GrammarImportFn<'_>>,
328) -> Result<Grammar, GrammarLoweringError> {
329    let metadata = parse_lowering_metadata(metadata)?;
330    let own_import = grammar_importer(&metadata.format)
331        .ok_or_else(|| GrammarLoweringError::UnsupportedFormat(metadata.format.clone()))?;
332    let imported = import.unwrap_or(&own_import)(executable)?;
333    let mut helpers: HashMap<&str, &GrammarExpr> = HashMap::new();
334    let mut renames: HashMap<&str, &str> = HashMap::new();
335    let mut originals: HashMap<&str, &str> = HashMap::new();
336    let mut kinds: HashMap<&str, RuleKind> = HashMap::new();
337    let mut docs: HashMap<&str, &str> = HashMap::new();
338    let mut attributes: HashMap<&str, RuleAttributes> = HashMap::new();
339    let mut declarations = None;
340    for step in &metadata.steps {
341        match step {
342            GrammarLoweringStep::Helper {
343                helper, original, ..
344            } => {
345                helpers.insert(helper, original);
346            }
347            GrammarLoweringStep::Rename { rule, value } => {
348                renames.insert(rule, value);
349                originals.insert(value, rule);
350            }
351            GrammarLoweringStep::Kind { rule, kind } => {
352                kinds.insert(rule, *kind);
353            }
354            GrammarLoweringStep::Doc { rule, doc } => {
355                docs.insert(rule, doc);
356            }
357            GrammarLoweringStep::Declarations { declarations: text } => declarations = Some(text),
358            GrammarLoweringStep::Attributes {
359                rule,
360                attributes: fields,
361            } => {
362                attributes.insert(rule, parse_rule_fields(fields)?);
363            }
364        }
365    }
366    let restore = |expr: &GrammarExpr| restore(expr, &helpers, &originals);
367    let mut rules = Vec::new();
368    for name in &metadata.order {
369        let executable_name = renames.get(name.as_str()).copied().unwrap_or(name);
370        let Some(back) = imported.rule(executable_name) else {
371            return Err(GrammarLoweringError::MissingRule(
372                executable_name.to_owned(),
373            ));
374        };
375        let doc = docs
376            .get(name.as_str())
377            .map(|doc| (*doc).to_owned())
378            .or_else(|| back.doc.clone());
379        let mut restored = rule(
380            name,
381            kinds.get(name.as_str()).copied().unwrap_or(back.kind),
382            restore(&back.expr),
383            doc,
384        );
385        if let Some(fields) = attributes.remove(name.as_str()) {
386            restored.attributes = fields;
387        }
388        rules.push(restored);
389    }
390    let mut grammar = assemble(&metadata.start, rules, metadata.source);
391    if let Some(text) = declarations {
392        grammar.set_declarations(parse_declarations_text(text)?);
393    }
394    Ok(grammar)
395}
396
397/// `expr` with every renamed reference given its original name back and every
398/// helper reference replaced with the expression the helper stands for.
399fn restore(
400    expr: &GrammarExpr,
401    helpers: &HashMap<&str, &GrammarExpr>,
402    originals: &HashMap<&str, &str>,
403) -> GrammarExpr {
404    if let GrammarExpr::NonTerminal(name) = expr {
405        let name = originals.get(name.as_str()).copied().unwrap_or(name);
406        return helpers.get(name).map_or_else(
407            || GrammarExpr::NonTerminal(name.to_owned()),
408            |original| (*original).clone(),
409        );
410    }
411    map_children::<std::convert::Infallible>(expr, &mut |item| {
412        Ok(restore(item, helpers, originals))
413    })
414    .unwrap_or_else(|never| match never {})
415}
416
417/// The way a lowering check failed.
418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
419pub enum GrammarLoweringFailureKind {
420    /// The emitter noted a construct it could not write.
421    LossyEmission,
422    /// The executable does not read back as the lowered grammar.
423    NotExecutable,
424    /// The reconstruction lost a feature of the original grammar.
425    FeatureDropped,
426    /// An exact lowering rejected an accept sample.
427    SampleRejected,
428    /// An exact lowering accepted a reject sample.
429    SampleAccepted,
430}
431
432impl GrammarLoweringFailureKind {
433    /// Stable tag shared with the JavaScript runtime.
434    #[must_use]
435    pub const fn as_str(self) -> &'static str {
436        match self {
437            Self::LossyEmission => "lossy-emission",
438            Self::NotExecutable => "not-executable",
439            Self::FeatureDropped => "feature-dropped",
440            Self::SampleRejected => "sample-rejected",
441            Self::SampleAccepted => "sample-accepted",
442        }
443    }
444}
445
446/// One failure of a lowering check.
447#[derive(Clone, Debug, PartialEq, Eq)]
448pub struct GrammarLoweringFailure {
449    /// The way the check failed.
450    pub kind: GrammarLoweringFailureKind,
451    /// A readable detail.
452    pub detail: String,
453}
454
455impl GrammarLoweringFailure {
456    fn new(kind: GrammarLoweringFailureKind, detail: impl Into<String>) -> Self {
457        Self {
458            kind,
459            detail: detail.into(),
460        }
461    }
462}
463
464/// The outcome of [`check_grammar_lowering`].
465#[derive(Clone, Debug, PartialEq, Eq)]
466pub struct GrammarLoweringReport {
467    /// The lowering status, or broken when any check failed.
468    pub status: GrammarLoweringStatus,
469    /// Every failed check.
470    pub failures: Vec<GrammarLoweringFailure>,
471    /// The lowering under check.
472    pub lowering: GrammarLowering,
473    /// The grammar reconstructed from the package, when it could be.
474    pub reconstructed: Option<Grammar>,
475}
476
477fn body(expr: &GrammarExpr) -> Result<String, GrammarLoweringError> {
478    Ok(canonical_rule_definition(&GrammarRule::new(
479        "",
480        expr.clone(),
481    ))?)
482}
483
484fn accepts(grammar: &Grammar, text: &str) -> bool {
485    GrammarParser::new(grammar.clone()).accepts(text)
486}
487
488/// Lowers `grammar` into `format`, reconstructs it from the package and
489/// reports every way the lowering failed.
490///
491/// The failures are an emission note, an executable
492/// that does not read back as the lowered grammar, a feature the
493/// reconstruction lost, and for an exact lowering an accept or reject sample
494/// on which the executable disagrees.
495///
496/// # Errors
497///
498/// Returns a [`GrammarLoweringError`] when the lowering itself cannot be
499/// carried out or a rule definition cannot be normalized.
500pub fn check_grammar_lowering(
501    grammar: &Grammar,
502    format: &str,
503    options: &GrammarLoweringOptions<'_>,
504) -> Result<GrammarLoweringReport, GrammarLoweringError> {
505    use GrammarLoweringFailureKind as Kind;
506    let lowering = lower_grammar(grammar, format, options)?;
507    let mut failures: Vec<GrammarLoweringFailure> = lowering
508        .report
509        .lossy
510        .iter()
511        .map(|note| GrammarLoweringFailure::new(Kind::LossyEmission, note.clone()))
512        .collect();
513    // The executable must read back as the lowered grammar, which already
514    // carries the target's rule names: the same rules, start and definitions.
515    let expected = lowering.grammar.rule_names();
516    let actual = lowering.imported.rule_names();
517    if expected != actual {
518        failures.push(GrammarLoweringFailure::new(
519            Kind::NotExecutable,
520            format!(
521                "rules [{}] read back as [{}]",
522                expected.join(", "),
523                actual.join(", ")
524            ),
525        ));
526    }
527    let start = lowering
528        .grammar
529        .start_rule()
530        .map_or("", |start| start.name.as_str());
531    if lowering
532        .imported
533        .start_rule()
534        .map(|back| back.name.as_str())
535        != Some(start)
536    {
537        failures.push(GrammarLoweringFailure::new(
538            Kind::NotExecutable,
539            format!("the executable does not start at rule {start}"),
540        ));
541    }
542    for lowered in lowering.grammar.rules() {
543        if let Some(back) = lowering.imported.rule(&lowered.name)
544            && body(&back.expr)? != body(&lowered.expr)?
545        {
546            failures.push(GrammarLoweringFailure::new(
547                Kind::NotExecutable,
548                format!(
549                    "rule {} reads back with a different definition",
550                    lowered.name
551                ),
552            ));
553        }
554    }
555
556    let metadata = options.edit_metadata.map_or_else(
557        || lowering.metadata.clone(),
558        |edit| edit(&lowering.metadata),
559    );
560    let reconstructed = match reconstruct_grammar(&lowering.executable, &metadata, options.import) {
561        Ok(reconstructed) => Some(reconstructed),
562        Err(error) => {
563            failures.push(GrammarLoweringFailure::new(
564                Kind::FeatureDropped,
565                error.to_string(),
566            ));
567            None
568        }
569    };
570    if let Some(reconstructed) = &reconstructed {
571        failures.extend(
572            dropped_grammar_features(grammar, reconstructed)?
573                .into_iter()
574                .map(|detail| GrammarLoweringFailure::new(Kind::FeatureDropped, detail)),
575        );
576    }
577    if lowering.status == GrammarLoweringStatus::Exact {
578        for text in options.accepts {
579            if !accepts(&lowering.imported, text) {
580                failures.push(GrammarLoweringFailure::new(
581                    Kind::SampleRejected,
582                    text.clone(),
583                ));
584            }
585        }
586        for text in options.rejects {
587            if accepts(&lowering.imported, text) {
588                failures.push(GrammarLoweringFailure::new(
589                    Kind::SampleAccepted,
590                    text.clone(),
591                ));
592            }
593        }
594    }
595    Ok(GrammarLoweringReport {
596        status: if failures.is_empty() {
597            lowering.status
598        } else {
599            GrammarLoweringStatus::Broken
600        },
601        failures,
602        lowering,
603        reconstructed,
604    })
605}
606
607/// The features of `expected` that `actual` does not carry, as readable
608/// details: the rule names and their order, the start rule, and every rule's
609/// kind, definition and documentation.
610///
611/// # Errors
612///
613/// Returns a [`GrammarLoweringError`] when a rule definition cannot be
614/// normalized for comparison.
615pub fn dropped_grammar_features(
616    expected: &Grammar,
617    actual: &Grammar,
618) -> Result<Vec<String>, GrammarLoweringError> {
619    let mut details = Vec::new();
620    if expected.rule_names() != actual.rule_names() {
621        details.push(format!(
622            "rules [{}] became [{}]",
623            expected.rule_names().join(", "),
624            actual.rule_names().join(", ")
625        ));
626    }
627    let start = |grammar: &Grammar| grammar.start_rule().map(|start| start.name.clone());
628    if start(expected) != start(actual) {
629        details.push("the start rule changed".to_owned());
630    }
631    if render_declarations_text(expected.declarations())
632        != render_declarations_text(actual.declarations())
633    {
634        details.push("the grammar declarations changed".to_owned());
635    }
636    for original in expected.rules() {
637        let Some(other) = actual.rule(&original.name) else {
638            continue;
639        };
640        if other.kind != original.kind {
641            details.push(format!(
642                "rule {} lost its kind {}",
643                original.name,
644                original.kind.as_str()
645            ));
646        }
647        if render_rule_fields(&other.attributes) != render_rule_fields(&original.attributes) {
648            details.push(format!(
649                "rule {} changed its parameters, channel, modes or action",
650                original.name
651            ));
652        }
653        if canonical_rule_definition(other)? != canonical_rule_definition(original)? {
654            details.push(format!("rule {} changed its definition", original.name));
655        }
656        if other.doc != original.doc {
657            details.push(format!("rule {} changed its documentation", original.name));
658        }
659    }
660    Ok(details)
661}