parol 4.5.0

LL(k) and LALR(1) parser generator for Rust
Documentation
use std::fs;
use std::ops::Range;

use crate::{GrammarAnalysisError, ParolParserError};
use parol_runtime::{
    Report,
    codespan_reporting::{
        diagnostic::{Diagnostic, Label},
        files::SimpleFiles,
        term::{self, Config, termcolor::StandardStream},
    },
};

/// Error reporter for user errors generated by the parol parser itself.
pub struct ParolErrorReporter {}

impl Report for ParolErrorReporter {
    fn report_user_error(err: &anyhow::Error) -> anyhow::Result<()> {
        let files: SimpleFiles<String, String> = SimpleFiles::new();
        let mut writer = StandardStream::stderr(term::termcolor::ColorChoice::Auto);
        let config = Config::default();

        if let Some(err) = err.downcast_ref::<ParolParserError>() {
            match err {
                ParolParserError::UnknownScanner {
                    context,
                    name,
                    input,
                    token,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                            &mut writer,
                            &config,
                            &files,
                            &Diagnostic::error()
                                .with_message(format!("{context} - Unknown scanner {name}"))
                                .with_code("parol::parser::unknown_scanner")
                                .with_labels(vec![Label::primary(file_id, Into::<Range<usize>>::into(token))])
                                .with_notes(vec!["Undeclared scanner found. Please declare a scanner via %scanner name {{...}}".to_string()])
                        )?)
                }
                ParolParserError::EmptyGroup {
                    context,
                    input,
                    start,
                    end,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!("{context} - Empty Group not allowed"))
                            .with_code("parol::parser::empty_group")
                            .with_labels(vec![
                                Label::primary(file_id, Into::<Range<usize>>::into(start))
                                    .with_message("Start"),
                                Label::primary(file_id, Into::<Range<usize>>::into(end))
                                    .with_message("End"),
                            ])
                            .with_notes(vec!["Empty groups can be safely removed.".to_string()]),
                    )?)
                }
                ParolParserError::EmptyOptional {
                    context,
                    input,
                    start,
                    end,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!("{context} - Empty Optionals not allowed"))
                            .with_code("parol::parser::empty_optional")
                            .with_labels(vec![
                                Label::primary(file_id, Into::<Range<usize>>::into(start))
                                    .with_message("Start"),
                                Label::primary(file_id, Into::<Range<usize>>::into(end))
                                    .with_message("End"),
                            ])
                            .with_notes(vec!["Empty optionals can be safely removed.".to_string()]),
                    )?)
                }
                ParolParserError::EmptyRepetition {
                    context,
                    input,
                    start,
                    end,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!("{context} - Empty Repetitions not allowed"))
                            .with_code("parol::parser::empty_repetition")
                            .with_labels(vec![
                                Label::primary(file_id, Into::<Range<usize>>::into(start))
                                    .with_message("Start"),
                                Label::primary(file_id, Into::<Range<usize>>::into(end))
                                    .with_message("End"),
                            ])
                            .with_notes(vec![
                                "Empty repetitions can be safely removed.".to_string(),
                            ]),
                    )?)
                }
                ParolParserError::ConflictingTokenAliases {
                    first_alias,
                    second_alias,
                    expanded,
                    input,
                    first,
                    second,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!(
                                r"Multiple token aliases that expand to the same text:
    '{first_alias}' and '{second_alias}' expand both to '{expanded}'."
                            ))
                            .with_code("parol::parser::conflicting_token_aliases")
                            .with_labels(vec![
                                Label::primary(file_id, Into::<Range<usize>>::into(first))
                                    .with_message("First alias"),
                                Label::primary(file_id, Into::<Range<usize>>::into(second))
                                    .with_message("Second alias"),
                            ])
                            .with_notes(vec![
                                "Consider using only one single terminal instead of two."
                                    .to_string(),
                            ]),
                    )?)
                }
                ParolParserError::EmptyScanners { empty_scanners } => {
                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!(
                                "Empty scanner states ({empty_scanners:?}) found"
                            ))
                            .with_code("parol::parser::empty_scanner_states")
                            .with_notes(vec![
                                "Assign at least one terminal or remove them.".to_string(),
                            ]),
                    )?)
                }
                ParolParserError::UnsupportedGrammarType {
                    grammar_type,
                    input,
                    token,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!("{grammar_type} - Unsupported grammar type"))
                            .with_code("parol::parser::unsupported_grammar_type")
                            .with_labels(vec![Label::primary(
                                file_id,
                                Into::<Range<usize>>::into(token),
                            )])
                            .with_notes(vec![
                                "Only 'LL(k)' and 'LALR(1)' are supported. Use RawString literals here.".to_string()
                            ]),
                    )?)
                }
                ParolParserError::UnsupportedFeature {
                    feature,
                    hint,
                    input,
                    token,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!("{feature} - Unsupported feature"))
                            .with_code("parol::parser::unsupported_feature")
                            .with_labels(vec![Label::primary(
                                file_id,
                                Into::<Range<usize>>::into(token),
                            )])
                            .with_notes(vec![
                                "This feature is not supported.".to_string(),
                                hint.to_string(),
                            ]),
                    )?)
                }
                ParolParserError::InvalidTokenInTransition {
                    context,
                    token,
                    input,
                    location,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!(
                                "{context} - Invalid token '{token}' in transition. Use a primary non-terminal for the token."
                            ))
                            .with_code("parol::parser::invalid_token_in_transition")
                            .with_labels(vec![Label::primary(
                                file_id,
                                Into::<Range<usize>>::into(location),
                            )])
                            .with_notes(vec![
                                "Please use a primary non-terminal for the token.".to_string()
                            ]),
                    )?)
                }
                ParolParserError::TokenIsNotInScanner {
                    context,
                    scanner,
                    token,
                    input,
                    location,
                } => {
                    let mut files = SimpleFiles::new();
                    let content = fs::read_to_string(input).unwrap_or_default();
                    let file_id = files.add(input.display().to_string(), content);

                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message(format!(
                                "{context} - Token '{token}' is not defined in scanner '{scanner}'"
                            ))
                            .with_code("parol::parser::token_is_not_in_scanner")
                            .with_labels(vec![Label::primary(
                                file_id,
                                Into::<Range<usize>>::into(location),
                            )])
                            .with_notes(vec![
                                "Only tokens used in a scanner can initiate a transition from it to another scanner."
                                    .to_string(),
                            ]),
                    )?)
                }
                _ => {
                    unreachable!(
                        "Scanner switching directives have been removed from the grammar syntax."
                    );
                }
            }
        } else if let Some(err) = err.downcast_ref::<GrammarAnalysisError>() {
            match err {
                GrammarAnalysisError::LeftRecursion { recursions } => {
                    let non_terminals = recursions
                        .iter()
                        .map(|r| r.name.to_string())
                        .collect::<Vec<String>>()
                        .join(", ");
                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message("Grammar contains left-recursions")
                            .with_code("parol::analysis::left_recursion")
                            .with_notes(vec![
                                "Left-recursions detected.".to_string(),
                                non_terminals,
                                "Please rework your grammar to remove these recursions."
                                    .to_string(),
                            ]),
                    )?)
                }
                GrammarAnalysisError::UnreachableNonTerminals { non_terminals } => {
                    let non_terminals = non_terminals
                        .iter()
                        .map(|r| r.hint.clone())
                        .collect::<Vec<String>>()
                        .join(", ");
                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message("Grammar contains unreachable non-terminals")
                            .with_code("parol::analysis::unreachable_non_terminals")
                            .with_notes(vec![
                                "Non-terminals:".to_string(),
                                non_terminals,
                                "Unreachable non-terminals are not allowed. If not used they can be safely removed.".to_string(),
                            ]),
                    )?)
                }
                GrammarAnalysisError::NonProductiveNonTerminals { non_terminals } => {
                    let non_terminals = non_terminals
                        .iter()
                        .map(|r| r.hint.clone())
                        .collect::<Vec<String>>()
                        .join(", ");
                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message("Grammar contains nonproductive non-terminals")
                            .with_code("parol::analysis::nonproductive_non_terminals")
                            .with_notes(vec![
                                "Non-terminals:".to_string(),
                                non_terminals,
                                "Nonproductive non-terminals are not allowed. If not used they can be safely removed.".to_string(),
                            ]),
                    )?)
                }
                GrammarAnalysisError::MaxKExceeded { max_k } => Ok(term::emit_to_write_style(
                    &mut writer,
                    &config,
                    &files,
                    &Diagnostic::error()
                        .with_message(format!("Maximum lookahead of {max_k} exceeded"))
                        .with_code("parol::analysis::max_k_exceeded")
                        .with_notes(vec!["Please examine your grammar.".to_string()]),
                )?),
                GrammarAnalysisError::LALR1ParseTableConstructionFailed { conflict } => {
                    Ok(term::emit_to_write_style(
                        &mut writer,
                        &config,
                        &files,
                        &Diagnostic::error()
                            .with_message("LALR(1) parse table construction failed with conflicts")
                            .with_code("parol::analysis::lalr1_parse_table_construction_failed")
                            .with_notes(vec![
                                "Please examine your grammar.".to_string(),
                                format!("{}", conflict),
                            ]),
                    )?)
                }
            }
        } else {
            let result = term::emit_to_write_style(
                &mut writer,
                &config,
                &files,
                &Diagnostic::error()
                    .with_message("Parol error")
                    .with_notes(vec![
                        err.to_string(),
                        err.source()
                            .map_or("No details".to_string(), |s| s.to_string()),
                    ]),
            );
            result.map_err(|e| anyhow::anyhow!(e))
        }
    }
}