diff --git a/.gitignore b/.gitignore index bf746bb4..d18dbaeb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ cabal.sandbox.config .*.swo /.vscode/ tests/*.hs +*~ +TAGS diff --git a/ChangeLog.md b/ChangeLog.md index 785910da..d77347be 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,13 @@ # Revision history for Happy +## 2.2 + +* Error out when `<$>` appears in semantic action code + ([#335](https://github.com/haskell/happy/issues/335)). + +* Tested with GHC 8.0 - 9.14.1. + The Haskell code generated by Happy is for GHC 8.0 and up. + ## 2.1.7 * Add support for `{-# OPTIONS_HAPPY ... #-}` pragmas diff --git a/doc/syntax.rst b/doc/syntax.rst index d32b517a..458c4e06 100644 --- a/doc/syntax.rst +++ b/doc/syntax.rst @@ -445,6 +445,9 @@ It is an error to refer to ``$i`` when ``i`` is larger than the number of symbol The symbol ``$`` may be inserted literally in the Haskell expression using the sequence ``\$`` (this isn't necessary inside a string or character literal). Additionally, the sequence ``$>`` can be used to represent the value of the rightmost symbol. +Since version 2.2, Happy throws an error when ``<$>`` appears in the Haskell expression; +Happy interprets this as ``< $>`` but users often mean the operator version of ``fmap``. +(Observe that the latter is available as ``<\$>``.) A semantic value of the form ``{% ... }`` is a *monadic action*, and is only valid when the grammar file contains a ``%monad`` directive (:ref:`Monad Directive `). Monadic actions are discussed in :ref:`Monadic Parsers `. diff --git a/happy.cabal b/happy.cabal index c94b18e5..e2b8c42c 100644 --- a/happy.cabal +++ b/happy.cabal @@ -1,6 +1,6 @@ cabal-version: 1.18 name: happy -version: 2.1.7 +version: 2.2 license: BSD2 license-file: LICENSE copyright: (c) Andy Gill, Simon Marlow @@ -114,6 +114,9 @@ extra-source-files: tests/error001.stderr tests/error001.stdout tests/error001.y + tests/issue335.stderr + tests/issue335.stdout + tests/issue335.y tests/monad001.y tests/monad002.ly tests/monaderror.y @@ -141,7 +144,7 @@ executable happy main-is: Main.lhs build-depends: base >= 4.9 && < 5, - happy-lib == 2.1.7 + happy-lib == 2.2 default-language: Haskell98 default-extensions: CPP, MagicHash, FlexibleContexts, NamedFieldPuns diff --git a/lib/frontend/src/Happy/Frontend/Mangler.lhs b/lib/frontend/src/Happy/Frontend/Mangler.lhs index b2062eb3..25bdf7a7 100644 --- a/lib/frontend/src/Happy/Frontend/Mangler.lhs +++ b/lib/frontend/src/Happy/Frontend/Mangler.lhs @@ -87,7 +87,7 @@ go do special processing. If not, pass on to the regular processing routine > ns -> ns > error_resumptive | ResumptiveErrorHandler{} <- getError dirs = True > | otherwise = False -> + > start_strs = [ startName++'_':p | (TokenName p _ _) <- starts' ] Build up a mapping from name values to strings. @@ -132,7 +132,7 @@ Start symbols... Deal with priorities... > priodir = zip [1..] (getPrios dirs) -> + > mkPrio :: Int -> Directive a -> Priority > mkPrio i (TokenNonassoc _) = Prio None i > mkPrio i (TokenRight _) = Prio RightAssoc i @@ -155,10 +155,10 @@ Translate the rules from string to name-based. > convNT (Rule1 nt prods ty) > = do nt' <- mapToName nt > return (nt', prods, ty) -> + > transRule (nt, prods, _ty) > = mapM (finishRule nt) prods -> + > finishRule :: Name -> Prod1 e -> Writer [ErrMsg] (Production e) > finishRule nt (Prod1 lhs code line prec) > = mapWriter (\(a,e) -> (a, map (addLine line) e)) $ do @@ -168,7 +168,7 @@ Translate the rules from string to name-based. > Left s -> do addErr ("Undeclared precedence token: " ++ s) > return (Production nt lhs' code' No) > Right p -> return (Production nt lhs' code' p) -> + > mkPrec :: [Name] -> Prec -> Either String Priority > mkPrec lhs PrecNone = > case filter (flip elem terminal_names) lhs of @@ -180,9 +180,9 @@ Translate the rules from string to name-based. > case lookup s prioByString of > Nothing -> Left s > Just p -> Right p -> + > mkPrec _ PrecShift = Right PrioLowest -> + > -- in > rules1 <- mapM convNT rules @@ -191,7 +191,7 @@ Translate the rules from string to name-based. > let > type_env = [(nt, t) | Rule1 nt _ (Just (t,[])) <- rules] ++ > [(nt, getTokenType dirs) | nt <- terminal_strs] -- XXX: Doesn't handle $$ type! -> + > fixType (ty,s) = go "" ty > where go acc [] = return (reverse acc) > go acc (c:r) | isLower c = -- look for a run of alphanumerics starting with a lower case letter @@ -205,14 +205,14 @@ Translate the rules from string to name-based. > go1 (c:cs) > Just t -> go1 $ "(" ++ t ++ ")" > | otherwise = go (c:acc) r -> + > convType (nm, t) > = do t' <- fixType t > return (nm, t') -> + > -- in > tys <- mapM convType [ (nm, t) | (nm, _, Just t) <- rules1 ] -> + > let > type_array :: Array Name (Maybe String) @@ -238,7 +238,7 @@ Get the token specs in terms of Names. > lookup_prods :: Name -> [Int] > lookup_prods x | x >= firstStartTok && x < first_t = arr ! x > lookup_prods _ = error "lookup_prods" -> + > productions' = start_prods ++ concat rules2 > prod_array = listArray (0,length productions' - 1) productions' @@ -278,7 +278,7 @@ Gofer-like stuff: > combine [] = [] > combine ((a,b):(c,d):r) | a == c = combine ((a,b++d) : r) > combine (a:r) = a : combine r -> + For combining actions with possible error messages. @@ -307,12 +307,15 @@ So is this. -- At the same time, we collect a list of the variables actually used in this -- code, which is used by the backend. -> doCheckCode :: Int -> String -> M (String, [Int]) +> doCheckCode :: +> Int -- ^ Arity of the rule, i.e., maximal number of a variable. +> -> String -- ^ Haskell code for the semantic action of the rule. +> -> M (String, [Int]) -- ^ The translated code and the variable occurrences (in reverse order). > doCheckCode arity code0 = go code0 "" [] > where go code acc used = > case code of > [] -> return (reverse acc, used) -> + > '"' :r -> case reads code :: [(String,String)] of > [] -> go r ('"':acc) used > (s,r'):_ -> go r' (reverse (show s) ++ acc) used @@ -321,13 +324,24 @@ So is this. > [] -> go r ('\'':acc) used > (c,r'):_ -> go r' (reverse (show c) ++ acc) used > '\\':'$':r -> go r ('$':acc) used -> + + +Issue #335 (Andreas Abel, 2026-01-14): +Users often write '<$>' to mean '`fmap`', but Happy interprets it as '< $>' where '$>' +denotes the maximal variable (rightmost item). +Thus, reject '<$>' in semantic action code and suggest to either escape the '$' +or put a space after '<'. + +> c@'<':r@('$':'>':_) -> do +> addErr "Found '<$>' in semantic action code; either write '<\\$>' (for what usually is fmap) or '< $>' (for \"less than rightmost item\")" +> go r (c:acc) used + > '$':'>':r -- the "rightmost token" > | arity == 0 -> do addErr "$> in empty rule" > go r acc used > | otherwise -> go r (reverse (mkHappyVar arity) ++ acc) > (arity : used) -> + > '$':r@(i:_) | isDigit i -> > case reads r :: [(Int,String)] of > (j,r'):_ -> diff --git a/lib/happy-lib.cabal b/lib/happy-lib.cabal index 42e34f84..41492094 100644 --- a/lib/happy-lib.cabal +++ b/lib/happy-lib.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: happy-lib -version: 2.1.7 +version: 2.2 license: BSD-2-Clause copyright: (c) Andy Gill, Simon Marlow author: Andy Gill and Simon Marlow diff --git a/tests/.gitignore b/tests/.gitignore index 13ffdd0a..641386b9 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -5,3 +5,5 @@ *.n.hs *.stderr *.stdout +!issue335.stderr +!issue335.stdout diff --git a/tests/Makefile b/tests/Makefile index fe69f39b..056e129b 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 +ERROR_TESTS = error001.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/issue335.stderr b/tests/issue335.stderr new file mode 100644 index 00000000..c8e3f5d3 --- /dev/null +++ b/tests/issue335.stderr @@ -0,0 +1,2 @@ +issue335.y: 7: Found '<$>' in semantic action code; either write '<\$>' (for what usually is fmap) or '< $>' (for "less than rightmost item") + diff --git a/tests/issue335.stdout b/tests/issue335.stdout new file mode 100644 index 00000000..e69de29b diff --git a/tests/issue335.y b/tests/issue335.y new file mode 100644 index 00000000..6ef71e10 --- /dev/null +++ b/tests/issue335.y @@ -0,0 +1,6 @@ +%name foo +%tokentype { String } + +%% + +foo : foo { void <$> putStr "The '<$>' shall mean 'fmap' but happy will read it as '< $>' which is not what we want." }