lalrpop 0.23.0

convenient LR(1) parser generator
Documentation
use crate::grammar::parse_tree::Grammar;
use crate::lexer::dfa::interpret;
use crate::normalize::NormResult;
use crate::normalize::resolve::resolve;
use crate::parser;
use crate::test_util;

fn validate_grammar(grammar: &str) -> NormResult<Grammar> {
    let parsed_grammar = parser::parse_grammar(grammar).expect("parse grammar");
    let parsed_grammar = resolve(parsed_grammar).expect("resolve");
    super::validate(parsed_grammar)
}

fn check_err(expected_err: &str, grammar: &str, span: &str) {
    let err = validate_grammar(grammar).unwrap_err();
    test_util::check_norm_err(expected_err, span, err);
}

fn check_intern_token(grammar: &str, expected_tokens: Vec<(&'static str, &'static str)>) {
    let parsed_grammar = validate_grammar(grammar).expect("validate");
    let intern_token = parsed_grammar.intern_token().expect("intern_token");
    println!("intern_token: {intern_token:?}");
    for (input, expected_user_name) in expected_tokens {
        let actual_user_name =
            interpret::interpret(&intern_token.dfa, input).map(|(index, text)| {
                let user_name = &intern_token.match_entries[index.index()].user_name;
                (user_name.clone(), text)
            });
        let actual_user_name = format!("{actual_user_name:?}");
        if expected_user_name != actual_user_name {
            panic!(
                "input `{input}` matched `{actual_user_name}` but we expected `{expected_user_name}`"
            );
        }
    }
}

#[test]
fn unknown_terminal() {
    check_err(
        r#"terminal `"\+"` does not have a pattern defined for it"#,
        r#"grammar; extern { enum Term { } } X = X "+";"#,
        r#"                                        ~~~ "#,
    );
}

#[test]
fn unknown_id_terminal() {
    check_err(
        r#"terminal `"foo"` does not have a pattern defined for it"#,
        r#"grammar; extern { enum Term { } } X = X "foo";"#,
        r#"                                        ~~~~~ "#,
    );
}

#[test]
fn tick_input_lifetime_already_declared() {
    check_err(
        r#".*the `'input` lifetime is implicit and cannot be declared"#,
        r#"grammar<'input>; X = X "foo";"#,
        r#"~~~~~~~                      "#,
    );
}

#[test]
fn input_parameter_already_declared() {
    check_err(
        r#".*the `input` parameter is implicit and cannot be declared"#,
        r#"grammar(input:u32); X = X "foo";"#,
        r#"~~~~~~~                         "#,
    );
}

#[test]
fn invalid_regular_expression_unterminated_group() {
    check_err(
        r#"unclosed group"#,
        r#"grammar; X = X r"(123";"#,
        r#"               ~~~~~~~ "#,
    );
}

#[test]
fn quoted_literals() {
    check_intern_token(
        r#"grammar; X = X "+" "-" "foo" "(" ")";"#,
        vec![
            ("+", r#"Some(("+", "+"))"#),
            ("-", r#"Some(("-", "-"))"#),
            ("(", r#"Some(("(", "("))"#),
            (")", r#"Some((")", ")"))"#),
            ("foo", r#"Some(("foo", "foo"))"#),
            ("<", r#"None"#),
        ],
    );
}

#[test]
fn regex_literals() {
    check_intern_token(
        r#"grammar; X = X r"[a-z]+" r"[0-9]+";"#,
        vec![
            ("a", r##"Some((r#"[a-z]+"#, "a"))"##),
            ("def", r##"Some((r#"[a-z]+"#, "def"))"##),
            ("1", r##"Some((r#"[0-9]+"#, "1"))"##),
            ("9123456", r##"Some((r#"[0-9]+"#, "9123456"))"##),
        ],
    );
}

/// Basic test for match mappings.
#[test]
// This test requires regex's unicode case support
#[cfg_attr(not(feature = "unicode"), ignore)]
fn match_mappings() {
    check_intern_token(
        r#"grammar; match { r"(?i)begin" => "BEGIN" } else { "abc" => ALPHA } X = "BEGIN" ALPHA;"#,
        vec![
            ("BEGIN", r#"Some(("BEGIN", "BEGIN"))"#),
            ("begin", r#"Some(("BEGIN", "begin"))"#),
            ("abc", r#"Some((ALPHA, "abc"))"#),
        ],
    );
}

/// Match mappings, exercising precedence. Here the ID regex *would*
/// be ambiguous with the begin regex.
#[test]
// This test requires regex's unicode case support
#[cfg_attr(not(feature = "unicode"), ignore)]
fn match_precedence() {
    check_intern_token(
        r#"grammar; match { r"(?i)begin" => "BEGIN" } else { r"\w+" => ID } X = ();"#,
        vec![
            ("BEGIN", r#"Some(("BEGIN", "BEGIN"))"#),
            ("begin", r#"Some(("BEGIN", "begin"))"#),
            ("abc", r#"Some((ID, "abc"))"#),
        ],
    );
}

/// Test that, without a `catch-all`, using unrecognized literals is an error.
#[test]
fn invalid_match_literal() {
    check_err(
        r#"terminal `"foo"` does not have a match mapping defined for it"#,
        r#"grammar; match { r"(?i)begin" => "BEGIN" } X = "foo";"#,
        r#"                                               ~~~~~ "#,
    );
}

/// Test that, without a `catch-all`, using unrecognized literals is an error.
#[test]
fn invalid_match_regex_literal() {
    check_err(
        r##"terminal `r#"foo"#` does not have a match mapping defined for it"##,
        r#"grammar; match { r"(?i)begin" => "BEGIN" } X = r"foo";"#,
        r#"                                               ~~~~~~ "#,
    );
}

/// Test that, with a catch-all, the previous two examples work.
#[test]
// This test requires regex's unicode case support
#[cfg_attr(not(feature = "unicode"), ignore)]
fn match_catch_all() {
    let grammar = r#"grammar; match { r"(?i)begin" => "BEGIN", _ } X = { "foo", r"foo" };"#;
    assert!(validate_grammar(grammar).is_ok())
}

/// Test that a `catch-all` can be use in the first `match` arm.
/// Before the pull request to close [issue 325](https://github.com/lalrpop/lalrpop/issues/325),
/// the usage of the `catch-all` symbol was not allowed in the first arm of a `match` block.
#[test]
fn match_catch_all_in_first_arm() {
    let grammar = r#"
        grammar;
        match {
            r"[a-z]",
            _
        } else {
            r"[[:word:]]+"
        }
        pub Term = {
            Num,
            "(" <Term> ")",
            r"[[:word:]]+" => format!("Id({})", <>),
        };
        Num: String = r"[0-9]+" => <>.to_string();
"#;
    assert!(validate_grammar(grammar).is_ok());
    check_intern_token(
        grammar,
        vec![
            ("x", r##"Some((r#"[a-z]"#, "x"))"##),
            ("xy", r##"Some((r#"[[:word:]]+"#, "xy"))"##),
        ],
    );
}

#[test]
// This test requires regex's unicode case support
#[cfg_attr(not(feature = "unicode"), ignore)]
fn complex_match() {
    let grammar = r#"
        grammar;
        match {
            "abc"        => "ABC",
            r"(?i)begin" => BEGIN
        }

        pub Query: String = {
            "ABC" BEGIN => String::from("Success")
        };
"#;
    assert!(validate_grammar(grammar).is_ok())
}

/// Test that overlapping regular expressions are still forbidden within one level
/// of a match declaration.
#[test]
// This test requires regex's unicode case support
#[cfg_attr(not(feature = "unicode"), ignore)]
fn ambiguity_within_match() {
    check_err(
        r##"ambiguity detected between the terminal `r#"b"#` and the terminal `r#"\(\?i\)b"#`"##,
        r#"grammar; match { r"(?i)b" => "B", r"b" => "b" }"#,
        r#"                                  ~~~~~~~~~~~~ "#,
    );
}

/// Test that using the **exact same regular expression** twice is
/// forbidden, even across multiple levels of the match expression.
/// No good reason to do that.
#[test]
fn same_literal_twice() {
    check_err(
        r##"multiple match entries for `r#"\[bB\]"#`"##,
        r#"grammar; match { r"[bB]" => "B" } else { r"[bB]" => "b" }"#,
        r#"                                         ~~~~~~~~~~~~~~~ "#,
    );
}