diff --git a/spec/integration/compile_grammar_spec.cc b/spec/integration/compile_grammar_spec.cc index 0d55bd56..6dbf1e02 100644 --- a/spec/integration/compile_grammar_spec.cc +++ b/spec/integration/compile_grammar_spec.cc @@ -90,15 +90,13 @@ describe("compile_grammar", []() { Possible interpretations: - (math_operation expression '+' expression) • '+' … - - expression '+' (math_operation expression • '+' expression) + 1: (math_operation expression '+' expression) • '+' … + 2: expression '+' (math_operation expression • '+' expression) Possible resolutions: - Specify left or right associativity in the rules: math_operation - - Add a conflict for the rules: math_operation + 1: Specify a left or right associativity in `math_operation` + 2: Add a conflict for these rules: `math_operation` )MESSAGE"))); result = ts_compile_grammar(fill_template(grammar_template, { @@ -201,17 +199,15 @@ describe("compile_grammar", []() { Possible interpretations: - (expression identifier) • '{' … - - (function_call identifier • block) + 1: (expression identifier) • '{' … + 2: (function_call identifier • block) Possible resolutions: - Use different precedences in the rules: expression function_call - - Specify left or right associativity in the rules: expression - - Add a conflict for the rules: expression function_call + 1: Specify a higher precedence in `function_call` than in the other rules. + 2: Specify a higher precedence in `expression` than in the other rules. + 3: Specify a left or right associativity in `expression` + 4: Add a conflict for these rules: `expression` `function_call` )MESSAGE"))); // Giving function calls lower precedence than expressions causes `bar` @@ -246,6 +242,88 @@ describe("compile_grammar", []() { "(identifier) " "(block (expression (identifier)))))))"); }); + + it("does not allow conflicting precedences", [&]() { + string grammar_template = R"JSON({ + "name": "conflicting_precedence_example", + + "rules": { + "expression": { + "type": "CHOICE", + "members": [ + {"type": "SYMBOL", "name": "sum"}, + {"type": "SYMBOL", "name": "product"}, + {"type": "SYMBOL", "name": "other_thing"} + ] + }, + + "sum": { + "type": "PREC_LEFT", + "value": 0, + "content": { + "type": "SEQ", + "members": [ + {"type": "SYMBOL", "name": "expression"}, + {"type": "STRING", "value": "+"}, + {"type": "SYMBOL", "name": "expression"} + ] + } + }, + + "product": { + "type": "PREC_LEFT", + "value": 1, + "content": { + "type": "SEQ", + "members": [ + {"type": "SYMBOL", "name": "expression"}, + {"type": "STRING", "value": "*"}, + {"type": "SYMBOL", "name": "expression"} + ] + } + }, + + "other_thing": { + "type": "PREC_LEFT", + "value": -1, + "content": { + "type": "SEQ", + "members": [ + {"type": "SYMBOL", "name": "expression"}, + {"type": "STRING", "value": "*"}, + {"type": "STRING", "value": "*"} + ] + } + }, + + "identifier": { + "type": "PATTERN", + "value": "[a-zA-Z]+" + } + } + })JSON"; + + TSCompileResult result = ts_compile_grammar(fill_template(grammar_template, { + }).c_str()); + + AssertThat(result.error_message, Equals(dedent(R"MESSAGE( + Unresolved conflict for symbol sequence: + + expression '+' expression • '*' … + + Possible interpretations: + + 1: (sum expression '+' expression) • '*' … + 2: expression '+' (product expression • '*' expression) + 3: expression '+' (other_thing expression • '*' '*') + + Possible resolutions: + + 1: Specify a higher precedence in `product` and `other_thing` than in the other rules. + 2: Specify a higher precedence in `sum` than in the other rules. + 3: Add a conflict for these rules: `sum` `product` `other_thing` + )MESSAGE"))); + }); }); describe("when the grammar's start symbol is a token", [&]() { diff --git a/src/compiler/build_tables/build_parse_table.cc b/src/compiler/build_tables/build_parse_table.cc index 18511787..0102c67d 100644 --- a/src/compiler/build_tables/build_parse_table.cc +++ b/src/compiler/build_tables/build_parse_table.cc @@ -363,6 +363,7 @@ class ParseTableBuilder { ParseTableEntry &entry = parse_table.states[state_id].terminal_entries[lookahead]; int reduction_precedence = entry.actions.front().precedence(); set shift_items; + bool considered_associativity = false; for (const ParseAction &action : entry.actions) if (action.type == ParseActionTypeReduce) @@ -406,6 +407,7 @@ class ParseTableBuilder { // associative, prefer the shift. else if (shift_precedence.min == reduction_precedence && shift_precedence.max == reduction_precedence) { + considered_associativity = true; bool has_non_associative_reductions = false; bool has_left_associative_reductions = false; bool has_right_associative_reductions = false; @@ -435,8 +437,6 @@ class ParseTableBuilder { entry.actions.pop_back(); } } - } else { - return "Mismatched precedence"; } } @@ -473,8 +473,11 @@ class ParseTableBuilder { description += "Possible interpretations:\n\n"; + size_t interpretation_count = 1; for (const ParseAction &action : entry.actions) { if (action.type == ParseActionTypeReduce) { + description += " " + to_string(interpretation_count++) + ":"; + for (size_t i = 0; i < earliest_starting_item.step_index - action.consumed_symbol_count; i++) { description += " " + symbol_name(earliest_starting_item.production->at(i).symbol); } @@ -485,11 +488,13 @@ class ParseTableBuilder { } description += ")"; description += " \u2022 " + symbol_name(Symbol(lookahead, true)) + " \u2026"; - description += "\n\n"; + description += "\n"; } } for (const ParseItem &shift_item : shift_items) { + description += " " + to_string(interpretation_count++) + ":"; + for (size_t i = 0; i < earliest_starting_item.step_index - shift_item.step_index; i++) { description += " " + symbol_name(earliest_starting_item.production->at(i).symbol); } @@ -501,32 +506,53 @@ class ParseTableBuilder { description += " " + symbol_name(shift_item.production->at(i).symbol); } description += ")"; - description += "\n\n"; + description += "\n"; } - description += "Possible resolutions:\n\n"; + description += "\nPossible resolutions:\n\n"; + size_t resolution_count = 1; if (actual_conflict.size() > 1) { - description += " Use different precedences in the rules:"; - for (const Symbol &conflict_symbol : actual_conflict) { - description += " " + symbol_name(conflict_symbol); + if (!shift_items.empty()) { + description += " " + to_string(resolution_count++) + ": "; + description += "Specify a higher precedence in"; + bool is_first = true; + for (const ParseItem &shift_item : shift_items) { + if (!is_first) description += " and"; + description += " `" + symbol_name(shift_item.lhs()) + "`"; + is_first = false; + } + description += " than in the other rules.\n"; } - description += "\n\n"; - } - if (shift_items.size() > 0) { - description += " Specify left or right associativity in the rules:"; for (const ParseAction &action : entry.actions) { if (action.type == ParseActionTypeReduce) { - description += " " + symbol_name(action.symbol); + description += " " + to_string(resolution_count++) + ": "; + description += "Specify a higher precedence in `"; + description += symbol_name(action.symbol); + description += "` than in the other rules.\n"; } } - description += "\n\n"; } - description += " Add a conflict for the rules:"; + if (considered_associativity) { + description += " " + to_string(resolution_count++) + ": "; + description += "Specify a left or right associativity in"; + for (const ParseAction &action : entry.actions) { + bool is_first = true; + if (action.type == ParseActionTypeReduce) { + if (!is_first) description += " and"; + description += " `" + symbol_name(action.symbol) + "`"; + is_first = false; + } + } + description += "\n"; + } + + description += " " + to_string(resolution_count++) + ": "; + description += "Add a conflict for these rules:"; for (const Symbol &conflict_symbol : actual_conflict) { - description += " " + symbol_name(conflict_symbol); + description += " `" + symbol_name(conflict_symbol) + "`"; } description += "\n"; return description;