1use std::collections::{BTreeMap, BTreeSet};
12
13use super::Grammar;
14use super::emit::{
15 EmitReport, GrammarEmitError, emit_abnf, emit_antlr, emit_bnf, emit_ebnf, emit_gbnf, emit_lark,
16 emit_pest, emit_tree_sitter_json,
17};
18use super::import::{
19 GrammarImportError, import_abnf, import_antlr, import_bnf, import_ebnf, import_gbnf,
20 import_lark, import_pest, import_tree_sitter_json,
21};
22use super::merge::{GrammarMergeOptions, GrammarMergeSource, merge_grammars, rename_grammar_rule};
23use super::round_trip::{GrammarRoundTrip, GrammarRoundTripError, check_grammar_round_trip};
24use super::validate::{Severity, validate};
25
26mod feature_forms;
27mod feature_links;
28mod json;
29mod links;
30mod lossless;
31mod lowering;
32mod native;
33
34pub use feature_links::{render_declaration_links, render_rule_fields};
35pub use json::{deserialize_grammar, serialize_grammar};
36pub use links::{
37 parse_grammar_links, parse_links_expression, percent_decode_links_text,
38 percent_encode_links_text, render_grammar_links, render_links_expression, render_rule_link,
39};
40pub use lossless::{
41 GRAMMAR_LOSSLESS_FORMATS, GrammarLayout, GrammarLayoutDefinition, GrammarLayoutImplicit,
42 GrammarLosslessError, GrammarSourceDefinition, GrammarSourceSplit, capture_grammar_layout,
43 emit_grammar_lossless, import_grammar_lossless, parse_grammar_layout_links,
44 render_grammar_layout_links, split_grammar_source,
45};
46pub use lowering::{
47 GRAMMAR_LOWERING_FORMATS, GrammarLowering, GrammarLoweringEncoding, GrammarLoweringError,
48 GrammarLoweringFailure, GrammarLoweringFailureKind, GrammarLoweringMetadata,
49 GrammarLoweringOptions, GrammarLoweringReport, GrammarLoweringStatus, GrammarLoweringStep,
50 MAX_LOWERED_CHARACTERS, check_grammar_lowering, dropped_grammar_features, lower_grammar,
51 parse_lowering_metadata, reconstruct_grammar, render_lowering_metadata,
52};
53pub use native::{
54 parse_native_expression, parse_native_grammar, render_native_expression, render_native_feature,
55 render_native_grammar,
56};
57
58pub type GrammarImporter = fn(&str) -> Result<Grammar, GrammarImportError>;
60
61pub type GrammarEmitter = fn(&Grammar) -> Result<(String, EmitReport), GrammarEmitError>;
63
64pub const GRAMMAR_IMPORT_FORMATS: &[&str] = &[
66 "abnf",
67 "antlr",
68 "bnf",
69 "ebnf",
70 "gbnf",
71 "lark",
72 "native",
73 "pest",
74 "tree-sitter-json",
75];
76
77pub const GRAMMAR_EXPORT_FORMATS: &[&str] = &[
79 "abnf",
80 "antlr",
81 "bnf",
82 "ebnf",
83 "gbnf",
84 "lark",
85 "native",
86 "pest",
87 "tree-sitter-json",
88];
89
90#[must_use]
92pub fn grammar_importer(format: &str) -> Option<GrammarImporter> {
93 let importer: GrammarImporter = match format {
94 "abnf" => import_abnf,
95 "antlr" => import_antlr,
96 "bnf" => import_bnf,
97 "ebnf" => import_ebnf,
98 "gbnf" => import_gbnf,
99 "lark" => import_lark,
100 "native" => parse_native_grammar,
101 "pest" => import_pest,
102 "tree-sitter-json" => import_tree_sitter_json,
103 _ => return None,
104 };
105 Some(importer)
106}
107
108#[must_use]
110pub fn grammar_emitter(format: &str) -> Option<GrammarEmitter> {
111 let emitter: GrammarEmitter = match format {
112 "abnf" => emit_abnf,
113 "antlr" => emit_antlr,
114 "bnf" => emit_bnf,
115 "ebnf" => emit_ebnf,
116 "gbnf" => emit_gbnf,
117 "lark" => emit_lark,
118 "native" => |grammar| Ok((render_native_grammar(grammar), EmitReport::default())),
119 "pest" => emit_pest,
120 "tree-sitter-json" => emit_tree_sitter_json,
121 _ => return None,
122 };
123 Some(emitter)
124}
125
126pub const GRAMMAR_COMMAND_USAGE: &str = "usage: meta-language grammar <command> [options]
128
129commands:
130 formats
131 list the import and export formats
132 import --from FORMAT FILE
133 print the grammar as a native listing
134 validate --from FORMAT FILE
135 report grammar diagnostics; exit 1 when one is an error
136 convert --from FORMAT --to FORMAT FILE
137 translate a grammar to another format; lossy steps go to standard error
138 export --to FORMAT FILE
139 export a native listing to another format
140 merge --source FORMAT:FILE... [--language NAME] [--require A=B]... [--to FORMAT]
141 merge grammars of one language; earlier sources take precedence
142 rename --from FORMAT --rule OLD --name NEW [--namespace NS] [--to FORMAT] FILE
143 rename a rule and every reference to it
144 round-trip --from FORMAT [--accept TEXT]... [--reject TEXT]... FILE
145 export and re-import a mutated grammar; exit 1 when it is not preserved
146 help
147 print this help
148
149exit status: 0 success, 1 a problem was found, 2 a usage or input error
150";
151
152#[derive(Clone, Debug, PartialEq, Eq)]
154pub struct GrammarCommandOutput {
155 pub exit_code: i32,
157 pub stdout: String,
159 pub stderr: String,
161}
162
163pub type GrammarFileReader<'a> = &'a dyn Fn(&str) -> std::io::Result<String>;
165
166#[derive(Clone, Copy, PartialEq, Eq)]
167enum Arity {
168 One,
169 Many,
170}
171
172struct CommandSpec {
173 options: &'static [(&'static str, Arity)],
174 required: &'static [&'static str],
175 files: usize,
176 run: fn(&mut Context<'_>) -> Result<i32, CommandError>,
177}
178
179struct CommandError {
180 message: String,
181 detail: Option<String>,
182}
183
184impl CommandError {
185 fn new(message: impl Into<String>) -> Self {
186 Self {
187 message: message.into(),
188 detail: None,
189 }
190 }
191
192 fn with_detail(message: impl Into<String>, detail: impl Into<String>) -> Self {
193 Self {
194 message: message.into(),
195 detail: Some(detail.into()),
196 }
197 }
198}
199
200struct Context<'a> {
201 options: BTreeMap<&'static str, Vec<String>>,
202 files: Vec<String>,
203 read_file: GrammarFileReader<'a>,
204 stdout: Vec<String>,
205 stderr: Vec<String>,
206}
207
208impl Context<'_> {
209 fn one(&self, option: &str) -> Option<&str> {
210 self.options
211 .get(option)
212 .and_then(|values| values.first())
213 .map(String::as_str)
214 }
215
216 fn many(&self, option: &str) -> &[String] {
217 self.options.get(option).map_or(&[], Vec::as_slice)
218 }
219
220 fn required(&self, option: &str) -> &str {
221 self.one(option).unwrap_or_default()
222 }
223}
224
225fn command_spec(name: &str) -> Option<CommandSpec> {
226 use Arity::{Many, One};
227 Some(match name {
228 "formats" => CommandSpec {
229 options: &[],
230 required: &[],
231 files: 0,
232 run: formats_command,
233 },
234 "import" => CommandSpec {
235 options: &[("from", One)],
236 required: &["from"],
237 files: 1,
238 run: import_command,
239 },
240 "validate" => CommandSpec {
241 options: &[("from", One)],
242 required: &["from"],
243 files: 1,
244 run: validate_command,
245 },
246 "convert" => CommandSpec {
247 options: &[("from", One), ("to", One)],
248 required: &["from", "to"],
249 files: 1,
250 run: convert_command,
251 },
252 "export" => CommandSpec {
253 options: &[("to", One)],
254 required: &["to"],
255 files: 1,
256 run: export_command,
257 },
258 "merge" => CommandSpec {
259 options: &[
260 ("source", Many),
261 ("language", One),
262 ("require", Many),
263 ("to", One),
264 ],
265 required: &["source"],
266 files: 0,
267 run: merge_command,
268 },
269 "rename" => CommandSpec {
270 options: &[
271 ("from", One),
272 ("rule", One),
273 ("name", One),
274 ("namespace", One),
275 ("to", One),
276 ],
277 required: &["from", "rule", "name"],
278 files: 1,
279 run: rename_command,
280 },
281 "round-trip" => CommandSpec {
282 options: &[("from", One), ("accept", Many), ("reject", Many)],
283 required: &["from"],
284 files: 1,
285 run: round_trip_command,
286 },
287 _ => return None,
288 })
289}
290
291#[must_use]
296pub fn run_grammar_command(
297 args: &[String],
298 read_file: GrammarFileReader<'_>,
299) -> GrammarCommandOutput {
300 let Some((name, rest)) = args.split_first() else {
301 return GrammarCommandOutput {
302 exit_code: 2,
303 stdout: String::new(),
304 stderr: GRAMMAR_COMMAND_USAGE.to_owned(),
305 };
306 };
307 if matches!(name.as_str(), "help" | "--help" | "-h") {
308 return GrammarCommandOutput {
309 exit_code: 0,
310 stdout: GRAMMAR_COMMAND_USAGE.to_owned(),
311 stderr: String::new(),
312 };
313 }
314 let mut context = Context {
315 options: BTreeMap::new(),
316 files: Vec::new(),
317 read_file,
318 stdout: Vec::new(),
319 stderr: Vec::new(),
320 };
321 let result = command_spec(name)
322 .ok_or_else(|| {
323 CommandError::new(format!(
324 "unknown command {name}; run meta-language grammar help"
325 ))
326 })
327 .and_then(|spec| {
328 parse_arguments(name, &spec, rest, &mut context)?;
329 (spec.run)(&mut context)
330 });
331 let exit_code = result.unwrap_or_else(|error| {
332 context.stderr.push(format!("error: {}", error.message));
333 context.stderr.extend(error.detail);
334 2
335 });
336 GrammarCommandOutput {
337 exit_code,
338 stdout: join_lines(&context.stdout),
339 stderr: join_lines(&context.stderr),
340 }
341}
342
343fn join_lines(lines: &[String]) -> String {
344 lines
345 .iter()
346 .map(|line| {
347 if line.ends_with('\n') {
348 line.clone()
349 } else {
350 format!("{line}\n")
351 }
352 })
353 .collect()
354}
355
356fn parse_arguments(
357 name: &str,
358 spec: &CommandSpec,
359 rest: &[String],
360 context: &mut Context<'_>,
361) -> Result<(), CommandError> {
362 let mut arguments = rest.iter();
363 while let Some(argument) = arguments.next() {
364 let Some(option) = argument.strip_prefix("--") else {
365 context.files.push(argument.clone());
366 continue;
367 };
368 let Some(&(key, arity)) = spec.options.iter().find(|(key, _)| *key == option) else {
369 return Err(CommandError::new(format!(
370 "unknown option {argument} for grammar {name}"
371 )));
372 };
373 let Some(value) = arguments.next() else {
374 return Err(CommandError::new(format!(
375 "option {argument} needs a value"
376 )));
377 };
378 let values = context.options.entry(key).or_default();
379 if arity == Arity::One && !values.is_empty() {
380 return Err(CommandError::new(format!(
381 "option {argument} is given twice"
382 )));
383 }
384 values.push(value.clone());
385 }
386 for option in spec.required {
387 if !context.options.contains_key(option) {
388 return Err(CommandError::new(format!(
389 "grammar {name} needs --{option}"
390 )));
391 }
392 }
393 if context.files.len() > spec.files {
394 return Err(CommandError::new(format!(
395 "unexpected argument {}",
396 context.files[spec.files]
397 )));
398 }
399 if context.files.len() < spec.files {
400 return Err(CommandError::new(format!(
401 "grammar {name} needs a grammar file"
402 )));
403 }
404 Ok(())
405}
406
407fn import_format(format: &str) -> Result<&str, CommandError> {
408 if GRAMMAR_IMPORT_FORMATS.contains(&format) {
409 Ok(format)
410 } else {
411 Err(CommandError::new(format!(
412 "unknown import format {format} (expected one of: {})",
413 GRAMMAR_IMPORT_FORMATS.join(", ")
414 )))
415 }
416}
417
418fn export_format(format: &str) -> Result<&str, CommandError> {
419 if GRAMMAR_EXPORT_FORMATS.contains(&format) {
420 Ok(format)
421 } else {
422 Err(CommandError::new(format!(
423 "unknown export format {format} (expected one of: {})",
424 GRAMMAR_EXPORT_FORMATS.join(", ")
425 )))
426 }
427}
428
429fn read(context: &Context<'_>, file: &str) -> Result<String, CommandError> {
430 (context.read_file)(file).map_err(|_| CommandError::new(format!("cannot read {file}")))
431}
432
433fn import_failure(file: &str, format: &str, error: &GrammarImportError) -> CommandError {
434 let category = match error {
435 GrammarImportError::Unsupported { .. } => "unsupported construct",
436 GrammarImportError::Parse { .. } => "parse error",
437 };
438 CommandError::with_detail(
439 format!("cannot import {file} as {format} ({category})"),
440 error.to_string(),
441 )
442}
443
444fn emit_failure(format: &str, error: &GrammarEmitError) -> CommandError {
445 CommandError::with_detail(format!("cannot export as {format}"), error.to_string())
446}
447
448fn load(context: &Context<'_>, file: &str, format: &str) -> Result<Grammar, CommandError> {
449 let source = read(context, file)?;
450 let import = grammar_importer(format).expect("checked import format");
451 import(&source).map_err(|error| import_failure(file, format, &error))
452}
453
454fn emit(context: &mut Context<'_>, format: &str, grammar: &Grammar) -> Result<(), CommandError> {
455 let emit = grammar_emitter(format).expect("checked export format");
456 let (source, report) = emit(grammar).map_err(|error| emit_failure(format, &error))?;
457 context.stdout.push(source);
458 context
459 .stderr
460 .extend(report.lossy.iter().map(|note| format!("lossy: {note}")));
461 Ok(())
462}
463
464#[allow(
465 clippy::unnecessary_wraps,
466 reason = "every command shares the runner signature"
467)]
468fn formats_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
469 context
470 .stdout
471 .push(format!("import: {}", GRAMMAR_IMPORT_FORMATS.join(" ")));
472 context
473 .stdout
474 .push(format!("export: {}", GRAMMAR_EXPORT_FORMATS.join(" ")));
475 Ok(0)
476}
477
478fn import_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
479 let format = import_format(context.required("from"))?;
480 let grammar = load(context, &context.files[0], format)?;
481 context.stdout.push(render_native_grammar(&grammar));
482 Ok(0)
483}
484
485fn validate_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
486 let format = import_format(context.required("from"))?;
487 let diagnostics = validate(&load(context, &context.files[0], format)?);
488 for diagnostic in &diagnostics {
489 context.stdout.push(format!(
490 "{} {} {}: {}",
491 diagnostic.severity.as_str(),
492 diagnostic.kind.as_str(),
493 diagnostic.location.rule,
494 diagnostic.message
495 ));
496 }
497 let errors = diagnostics
498 .iter()
499 .filter(|diagnostic| diagnostic.severity == Severity::Error)
500 .count();
501 context.stdout.push(format!(
502 "{errors} error(s), {} warning(s)",
503 diagnostics.len() - errors
504 ));
505 Ok(i32::from(errors > 0))
506}
507
508fn convert_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
509 let to = export_format(context.required("to"))?.to_owned();
510 let format = import_format(context.required("from"))?;
511 let grammar = load(context, &context.files[0], format)?;
512 emit(context, &to, &grammar)?;
513 Ok(0)
514}
515
516fn export_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
517 let to = export_format(context.required("to"))?.to_owned();
518 let grammar = load(context, &context.files[0], "native")?;
519 emit(context, &to, &grammar)?;
520 Ok(0)
521}
522
523fn merge_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
524 let to = export_format(context.one("to").unwrap_or("native"))?.to_owned();
525 let language = context.one("language").unwrap_or("grammar").to_owned();
526 let mut ids = BTreeSet::new();
527 let mut sources = Vec::new();
528 for (precedence, argument) in context.many("source").iter().enumerate() {
529 let Some((format, file)) = argument.split_once(':') else {
530 return Err(CommandError::new(format!(
531 "a merge source is FORMAT:FILE, not {argument}"
532 )));
533 };
534 let format = import_format(format)?;
535 let id = base_name(file);
536 if !ids.insert(id.to_owned()) {
537 return Err(CommandError::new(format!(
538 "merge sources need distinct file names, {id} is given twice"
539 )));
540 }
541 let precedence = u32::try_from(precedence).unwrap_or(u32::MAX);
542 sources.push(
543 GrammarMergeSource::new(id, language.as_str(), load(context, file, format)?)
544 .with_precedence(precedence),
545 );
546 }
547 let mut required_equivalences = Vec::new();
548 for pair in context.many("require") {
549 let Some((first, second)) = pair.split_once('=') else {
550 return Err(CommandError::new(format!(
551 "a required equivalence is SOURCE:RULE=SOURCE:RULE, not {pair}"
552 )));
553 };
554 required_equivalences.push((first.to_owned(), second.to_owned()));
555 }
556 let options = GrammarMergeOptions {
557 required_equivalences,
558 ..GrammarMergeOptions::default()
559 };
560 let result =
561 merge_grammars(&sources, &options).map_err(|error| CommandError::new(error.to_string()))?;
562 let group = &result.groups[0];
563 emit(context, &to, &group.grammar)?;
564 for decision in &group.decisions {
565 context.stderr.push(format!(
566 "{} {}: {} ({})",
567 decision.kind.as_str(),
568 decision.name,
569 decision.members.join(" "),
570 decision.basis
571 ));
572 }
573 for alternative in group.alternatives.iter().chain(&result.alternatives) {
574 context.stderr.push(format!(
575 "alternative {} {}: {}",
576 alternative.reason.as_str(),
577 alternative.name,
578 alternative.options.join(" ")
579 ));
580 }
581 for failure in &result.failures {
582 context.stderr.push(format!(
583 "unresolved {}: {}",
584 failure.members.join(" = "),
585 failure.reason.as_str()
586 ));
587 }
588 Ok(i32::from(result.status() != "complete"))
589}
590
591fn base_name(file: &str) -> &str {
592 file.rsplit(['/', '\\']).next().unwrap_or(file)
593}
594
595fn rename_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
596 let to = export_format(context.one("to").unwrap_or("native"))?.to_owned();
597 let format = import_format(context.required("from"))?;
598 let grammar = load(context, &context.files[0], format)?;
599 let renamed = rename_grammar_rule(
600 &grammar,
601 context.required("rule"),
602 context.required("name"),
603 context.one("namespace"),
604 &[],
605 )
606 .map_err(|error| CommandError::new(error.to_string()))?;
607 emit(context, &to, &renamed.grammar)?;
608 for alias in &renamed.aliases {
609 context
610 .stderr
611 .push(format!("alias {} = {}", alias.canonical, alias.original));
612 }
613 Ok(0)
614}
615
616fn round_trip_command(context: &mut Context<'_>) -> Result<i32, CommandError> {
617 let format = import_format(context.required("from"))?.to_owned();
618 if !GRAMMAR_EXPORT_FORMATS.contains(&format.as_str()) {
619 return Err(CommandError::new(format!(
620 "round-trip needs a format that is both imported and exported (one of: {})",
621 GRAMMAR_EXPORT_FORMATS.join(", ")
622 )));
623 }
624 let file = context.files[0].clone();
625 let source = read(context, &file)?;
626 let import = grammar_importer(&format).expect("checked import format");
627 let emit = grammar_emitter(&format).expect("checked export format");
628 let (accepts, rejects) = (context.many("accept"), context.many("reject"));
629 let report = check_grammar_round_trip(
630 &source,
631 &GrammarRoundTrip::new(&import, &emit).with_samples(accepts, rejects),
632 )
633 .map_err(|error| match error {
634 GrammarRoundTripError::Import(error) => import_failure(&file, &format, &error),
635 GrammarRoundTripError::Emit(error) => emit_failure(&format, &error),
636 error @ (GrammarRoundTripError::Normalize(_) | GrammarRoundTripError::MissingStartRule) => {
637 CommandError::with_detail(format!("cannot round-trip {file}"), error.to_string())
638 }
639 })?;
640 context.stdout.push(report.status.as_str().to_owned());
641 for failure in &report.failures {
642 context.stdout.push(format!(
643 "{} {}: {}",
644 failure.kind.as_str(),
645 failure.stage.as_str(),
646 failure.detail
647 ));
648 }
649 Ok(i32::from(report.status.as_str() != "preserved"))
650}