diff --git a/crates/generate/src/build_tables/build_parse_table.rs b/crates/generate/src/build_tables/build_parse_table.rs index 49f034038..c7722ffae 100644 --- a/crates/generate/src/build_tables/build_parse_table.rs +++ b/crates/generate/src/build_tables/build_parse_table.rs @@ -770,6 +770,7 @@ impl<'a> ParseTableBuilder<'a> { // If the SHIFT action has higher precedence, remove all the REDUCE actions. let mut shift_is_less = false; + let mut shift_is_equal = false; let mut shift_is_more = false; for p in shift_precedence { match Self::compare_precedence( @@ -781,7 +782,7 @@ impl<'a> ParseTableBuilder<'a> { ) { Ordering::Greater => shift_is_more = true, Ordering::Less => shift_is_less = true, - Ordering::Equal => {} + Ordering::Equal => shift_is_equal = true, } } @@ -790,8 +791,28 @@ impl<'a> ParseTableBuilder<'a> { } // If the REDUCE actions have higher precedence, remove the SHIFT action. else if shift_is_less && !shift_is_more { - entry.actions.pop(); - conflicting_items.retain(|item| item.is_done()); + // Exception: if one SHIFT interpretation ties the REDUCE actions in + // precedence while another has lower precedence, and the REDUCE + // actions are purely right associative, honor that right + // associativity by shifting rather than reducing. The + // lower-precedence interpretation coexists with the tying one, so on + // its own it must not force a REDUCE that would flip the tie to left + // associative. + if shift_is_equal + && matches!( + ( + reduction_info.has_left_assoc, + reduction_info.has_non_assoc, + reduction_info.has_right_assoc, + ), + (false, false, true) + ) + { + entry.actions.drain(0..entry.actions.len() - 1); + } else { + entry.actions.pop(); + conflicting_items.retain(|item| item.is_done()); + } } // If the SHIFT and REDUCE actions have the same precedence, consider // the REDUCE actions' associativity. diff --git a/test/fixtures/test_grammars/associativity_left_with_lower_precedence_shift/corpus.txt b/test/fixtures/test_grammars/associativity_left_with_lower_precedence_shift/corpus.txt new file mode 100644 index 000000000..7a837d01c --- /dev/null +++ b/test/fixtures/test_grammars/associativity_left_with_lower_precedence_shift/corpus.txt @@ -0,0 +1,37 @@ +================================================================================ +Addition is left associative +================================================================================ + +1 + 1 + 1 + 1 + +-------------------------------------------------------------------------------- + +(expression + (addition + (expression + (addition + (expression + (addition + (expression + (number)) + (expression + (number)))) + (expression + (number)))) + (expression + (number)))) + +================================================================================ +Superaddition still parses +================================================================================ + +1 + + 1 + +-------------------------------------------------------------------------------- + +(expression + (superaddition + (expression + (number)) + (expression + (number)))) diff --git a/test/fixtures/test_grammars/associativity_left_with_lower_precedence_shift/grammar.js b/test/fixtures/test_grammars/associativity_left_with_lower_precedence_shift/grammar.js new file mode 100644 index 000000000..a15e2b101 --- /dev/null +++ b/test/fixtures/test_grammars/associativity_left_with_lower_precedence_shift/grammar.js @@ -0,0 +1,16 @@ +export default grammar({ + name: 'associativity_left_with_lower_precedence_shift', + + rules: { + expression: $ => choice( + $.addition, + $.superaddition, + $.number, + ), + + addition: $ => prec.left(1, seq($.expression, '+', $.expression)), + superaddition: $ => prec.right(seq($.expression, '+', '+', $.expression)), + + number: _ => '1', + } +}); diff --git a/test/fixtures/test_grammars/associativity_right_with_lower_precedence_shift/corpus.txt b/test/fixtures/test_grammars/associativity_right_with_lower_precedence_shift/corpus.txt new file mode 100644 index 000000000..d845a77ee --- /dev/null +++ b/test/fixtures/test_grammars/associativity_right_with_lower_precedence_shift/corpus.txt @@ -0,0 +1,22 @@ +================================================================================ +Addition is right associative +================================================================================ + +1 + 1 + 1 + 1 + +-------------------------------------------------------------------------------- + +(expression + (addition + (expression + (number)) + (expression + (addition + (expression + (number)) + (expression + (addition + (expression + (number)) + (expression + (number)))))))) diff --git a/test/fixtures/test_grammars/associativity_right_with_lower_precedence_shift/grammar.js b/test/fixtures/test_grammars/associativity_right_with_lower_precedence_shift/grammar.js new file mode 100644 index 000000000..4c5f9afe9 --- /dev/null +++ b/test/fixtures/test_grammars/associativity_right_with_lower_precedence_shift/grammar.js @@ -0,0 +1,15 @@ +module.exports = grammar({ + name: 'associativity_right_with_lower_precedence_shift', + + rules: { + expression: $ => choice( + $.addition, + $.superaddition, + $.number + ), + addition: $ => prec.right(1, seq($.expression, "+", $.expression)), + superaddition: $ => prec.right(seq($.expression, "+", "+", $.expression)), + number: _ => "1" + } + }); +