1use 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#[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
94pub(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
124fn 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
134fn 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
170fn 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
239pub 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
310pub 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
397fn 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
419pub enum GrammarLoweringFailureKind {
420 LossyEmission,
422 NotExecutable,
424 FeatureDropped,
426 SampleRejected,
428 SampleAccepted,
430}
431
432impl GrammarLoweringFailureKind {
433 #[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#[derive(Clone, Debug, PartialEq, Eq)]
448pub struct GrammarLoweringFailure {
449 pub kind: GrammarLoweringFailureKind,
451 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#[derive(Clone, Debug, PartialEq, Eq)]
466pub struct GrammarLoweringReport {
467 pub status: GrammarLoweringStatus,
469 pub failures: Vec<GrammarLoweringFailure>,
471 pub lowering: GrammarLowering,
473 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
488pub 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 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
607pub 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}