From eb800aacf4c2f33106a7560c195838ec0514398a Mon Sep 17 00:00:00 2001 From: Sebastian Graf Date: Sun, 12 Apr 2026 16:36:05 +0200 Subject: [PATCH] Fix #354 by relaxing %expect to check for upper bound --- ChangeLog.md | 7 +++++++ app/Main.lhs | 12 ++++++++---- doc/syntax.rst | 9 ++++++--- tests/Makefile | 2 +- tests/expect_upper.y | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 tests/expect_upper.y diff --git a/ChangeLog.md b/ChangeLog.md index a00c34d5..7c6e8ea3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,12 @@ # Revision history for Happy +## 2.2.2 + +* #353 broke some programs using `expect% ` declarations. + This release changes `%expect` semantics to interpret `n` as + an upper bound instead to allow for backward compatibility. + A non-fatal warning is emitted when `n` is not matched exactly. + ## 2.2.1 * Omit some useless productions due to an off-by-one error and diff --git a/app/Main.lhs b/app/Main.lhs index 762e2b99..04092c61 100644 --- a/app/Main.lhs +++ b/app/Main.lhs @@ -165,15 +165,19 @@ Pretty print the AbsSyn. Report any conflicts in the grammar. > case expect common_options of -> Just n | n == sr && rr == 0 -> return () > Just _ | rr > 0 -> > die ("The grammar has reduce/reduce conflicts.\n" ++ > "This is not allowed when an expect directive is given\n") -> Just _ -> +> Just n | sr > n -> > die ("The grammar has " ++ show sr ++ > " shift/reduce conflicts.\n" ++ -> "This is different from the number given in the " ++ -> "expect directive\n") +> "This is more than the " ++ show n ++ +> " given in the %expect directive\n") +> Just n | sr < n -> +> do hPutStrLn stderr ("shift/reduce conflicts: " ++ show sr) +> hPutStrLn stderr ("(the %expect directive permits up to " ++ +> show n ++ ")") +> Just _ -> return () > _ -> do > (if sr /= 0 diff --git a/doc/syntax.rst b/doc/syntax.rst index 458c4e06..ff6cfac9 100644 --- a/doc/syntax.rst +++ b/doc/syntax.rst @@ -255,11 +255,14 @@ These conflicts generate warnings. But when you have checked the warnings and made sure that Happy handles them correctly these warnings are just annoying. The ``%expect`` directive gives a way of avoiding them. Declaring ``%expect n`` is a way of telling Happy -“There are exactly shift/reduce conflicts and zero reduce/reduce conflicts in this grammar. +“There are at most shift/reduce conflicts and zero reduce/reduce conflicts in this grammar. I promise I have checked them and they are resolved correctly”. -When processing the grammar, Happy will check the actual number of conflicts against the ``%expect`` declaration if any, and if there is a discrepancy then an error will be reported. +When processing the grammar, Happy will check the actual number of conflicts against the ``%expect`` declaration if any, and if the number exceeds ``n`` then an error will be reported. +If the number if below ``n``, a non-fatal warnings will be emitted instead. -Happy's ``%expect`` directive works exactly like that of yacc. +Happy's ``%expect`` directive works like that of yacc, but relaxes the check to +“at most conflicts” instead of “exactly conflicts” to allow for +backward compatible improvements of the state machine. .. _sec-error-directive: diff --git a/tests/Makefile b/tests/Makefile index 056e129b..0c3208cf 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -41,7 +41,7 @@ TESTS = Test.ly TestMulti.ly TestPrecedence.ly bug001.ly \ AttrGrammar001.y AttrGrammar002.y \ Pragma.y -ERROR_TESTS = error001.y issue335.y +ERROR_TESTS = error001.y expect_upper.y issue335.y # NOTE: `cabal` will set the `happy_datadir` env-var accordingly before invoking the test-suite #TEST_HAPPY_OPTS = --strict --template=.. diff --git a/tests/expect_upper.y b/tests/expect_upper.y new file mode 100644 index 00000000..7b5839a4 --- /dev/null +++ b/tests/expect_upper.y @@ -0,0 +1,35 @@ +-- Test that %expect checks for an upper bound, not an exact match. +-- This grammar has exactly 1 shift/reduce conflict (the dangling else). +-- %expect 2 should succeed because 1 <= 2. + +{ +module Main where +} + +%expect 2 +%name parse +%tokentype { Token } + +%token + 'if' { If } + 'then' { Then } + 'else' { Else } + 'x' { X } + +%% + +stmt : 'if' 'x' 'then' stmt 'else' stmt { $4 ++ $6 } + | 'if' 'x' 'then' stmt { $4 } + | 'x' { "x" } + +{ +main :: IO () +main = + if parse [If, X, Then, X] == "x" + then return () + else error "bad parse" + +data Token = If | Then | Else | X + +happyError _ = error "parse error" +}