diff --git a/BNFC.cabal b/BNFC.cabal
--- a/BNFC.cabal
+++ b/BNFC.cabal
@@ -1,5 +1,5 @@
 Name: BNFC
-Version: 2.9.5.1
+Version: 2.9.6
 cabal-version: 2.0
   -- >=2.0  for build-tool-depends: hspec-discover
   -- Andreas, 2022-12-16, issue #429:
@@ -30,9 +30,10 @@
   an XML representation,
   a LaTeX file containing a readable specification of the language.
 
+-- Support range when build with cabal
 tested-with:
   GHC == 9.12.2
-  GHC == 9.10.3
+  GHC == 9.10.2
   GHC == 9.8.4
   GHC == 9.6.7
   GHC == 9.4.8
@@ -44,7 +45,6 @@
   GHC == 8.4.4
   GHC == 8.2.2
   GHC == 8.0.2
-  -- GHC == 7.10.3
 
 extra-doc-files:
   README.md
@@ -67,7 +67,6 @@
   stack-8.8.yaml
   stack-8.6.yaml
   stack-8.4.yaml
-  stack-8.2.yaml
 
 source-repository head
   type:     git
@@ -85,6 +84,7 @@
     -- Generated by cabal
     Paths_BNFC
   autogen-modules:
+    -- Generated by cabal
     Paths_BNFC
   default-extensions:
     -- Keep in alphabetical order.
@@ -100,7 +100,7 @@
   default-language:
     Haskell2010
   build-depends:
-      base        >= 4.8   && < 5
+      base        >= 4.9   && < 5
     , array
     , containers
     , deepseq
@@ -113,9 +113,6 @@
     , time
     , transformers
         -- for Control.Monad.IO.Class, which is in base >= 4.9 but not below
-  if impl(ghc < 8)
-    build-depends:
-      semigroups
   build-tool-depends:
     alex:alex,
     happy:happy
@@ -123,13 +120,11 @@
     src
   ghc-options:
     -Wall
-    -fno-warn-dodgy-imports
-    -fno-warn-name-shadowing
+    -Wcompat
+    -Wno-dodgy-imports
+    -Wno-name-shadowing
     -- for cabal repl -w doctest :
-    -- -fno-warn-type-defaults
-  if impl(ghc >= 8.0)
-    ghc-options:
-      -Wcompat
+    -- -Wno-type-defaults
   if impl(ghc >= 9.2)
     ghc-options:
       -Wno-incomplete-uni-patterns
@@ -159,10 +154,16 @@
   --   BNFC.Lex
   --   -- Generated by happy
   --   BNFC.Par
-  other-modules:
+  -- 2023-11-03 We cannot add BNFC.{Lex,Par} as then the Lex.x and Par.y files
+  -- are not bundled by cabal dist.
+  -- Just make sure that there is no src/BNFC/{Lex,Par}.hs before running cabal sdist,
+  -- otherwise we will end up with both Lex.hs and Lex.x (resp. Par.{hs,y})
+  -- which will cause alex/happy to not be run, leading to build failures.
+  autogen-modules:
     -- Generated by cabal
     Paths_BNFC
-  autogen-modules:
+  other-modules:
+    -- Generated by cabal
     Paths_BNFC
   exposed-modules:
     -- Generated from LICENSE
@@ -271,6 +272,11 @@
     -- Agda backend
     BNFC.Backend.Agda
 
+    -- Tree-sitter backend
+    BNFC.Backend.TreeSitter
+    BNFC.Backend.TreeSitter.CFtoTreeSitter
+    BNFC.Backend.TreeSitter.RegToJSReg
+
 ----- Testing --------------------------------------------------------------
 
 test-suite unit-tests
@@ -296,9 +302,6 @@
     deepseq,
     string-qq,
     time
-  if impl(ghc < 8)
-    build-depends:
-      semigroups
   build-tool-depends:
     alex:alex,
     happy:happy,
@@ -309,11 +312,9 @@
     test
   ghc-options:
     -W
+    -Wcompat
     -- TODO: -Wall
-    -fno-warn-name-shadowing
-  if impl(ghc >= 8.0)
-    ghc-options:
-      -Wcompat
+    -Wno-name-shadowing
   default-extensions:
     -- Keep in alphabetical order.
     LambdaCase
@@ -335,6 +336,7 @@
     BNFC.Backend.JavaSpec
     BNFC.Backend.LatexSpec
     BNFC.Backend.OCamlSpec
+    BNFC.Backend.TreeSitterSpec
     -- Generated by cabal
     Paths_BNFC
   autogen-modules:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,18 @@
-# 2.9.5.1
+# 2.9.6
 
-Andreas Abel <andreas.abel@gu.se>  Jan 2026
+Andreas Abel <andreas.abel@gu.se>,  August 2025
 
-* Cabal 3.12 compatibility: remove generated `Lex.hs` and `Par.hs` from source tarball
-* Build tested with GHC 8.0 - 9.12.2
+* C: preserve case in constructors (union): e.g. label `EInt` now is union member `eInt_` rather than `eint_`
+  [[#479](https://github.com/BNFC/bnfc/issues/479)]
+* Haskell: lex ASCII 222 as upper rather than lower case letter
+  [[#497](https://github.com/BNFC/bnfc/issues/497)]
+* Haskell: no longer derive `Typeable` (obsolete since GHC 7.10)
+  [[#503](https://github.com/BNFC/bnfc/issues/503)]
+* Haskell: drop support for GHC 7, generated code needs at least GHC 8.0
+
+Builds with GHC versions:
+* with `cabal`, GHC 8.0 - 9.12
+* with `stack`, GHC 8.4 - 9.12
 
 # 2.9.5
 
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@
 CABAL_BUILD_OPTS     = --enable-tests
 # --builddir=dist-ghc-$(GHC_VERSION)$(CABAL_BUILDDIR_SUFFIX)
 CABAL_CONFIGURE_OPTS = --enable-tests
-CABAL_INSTALL_OPTS   = $(CABAL_CONFIGURE_OPTS) $(CABAL_BUILD_OPTS) --overwrite-policy=always
+CABAL_INSTALL_OPTS   = --overwrite-policy=always
 CABAL_TEST_OPTS      = $(CABAL_BUILD_OPTS)
 
 CABAL                = cabal $(CABAL_OPTS)
@@ -38,7 +38,7 @@
 doctest: build doctest-install doctest-quick
 
 doctest-install:
-	cabal install doctest --program-suffix=-${GHC_VERSION}
+	cabal install doctest --ignore-project --program-suffix=-${GHC_VERSION}
 
 doctest-quick:
 	cabal repl -w doctest-${GHC_VERSION} --repl-options=-Wno-type-defaults
@@ -62,6 +62,15 @@
 # Binary package (tgz, for linux)
 bdist: dist/${BDIST_TAG}.tar.gz
 
+# Source package
+# Andreas, 2023-11-03, PR #466: need to remove BNFC/{Lex,Par}.hs,
+# otherwise they will be shipped in the sdist package
+# and cause compilation to fail.
+sdist:
+	make -C src clean
+	cabal sdist
+
+# OLD goal
 dist/%.tar.gz:
 	cabal v1-clean
 	cabal v1-install ${CABAL_OPTS} --only-dependencies
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -43,6 +43,19 @@
   bnfc --help
 ```
 
+BNFC versions in major Stackage LTS snapshots:
+
+|  GHC   |  LTS  |  BNFC   |
+|--------|-------|---------|
+| 8.8.4  | 16.31 | 2.8.4   |
+| 8.10.7 | 18.28 | 2.9.4   |
+| 9.0.2  | 19.33 | 2.9.4   |
+| 9.2.8  | 20.26 | 2.9.4.1 |
+| 9.4.8  | 21.25 | 2.9.5   |
+| 9.6.7  | 22.44 | 2.9.5   |
+| 9.8.4  | 23.27 | 2.9.5   |
+| 9.10.2 | 24.3  | 2.9.5   |
+
 ### Installation via cabal
 
 You need a running installation of a recent version of
@@ -71,9 +84,9 @@
 ```
 or
 ```
-  stack install --stack-yaml stack-8.10.7.yaml
+  stack install --stack-yaml stack-9.12.yaml
 ```
-(replace `8.10.7` with your GHC version, and if you want to build with
+(replace `9.12` with your GHC major version, and if you want to build with
 your installed GHC then add flag `--system-ghc`).
 
 Mini tutorial
diff --git a/main/Main.hs b/main/Main.hs
--- a/main/Main.hs
+++ b/main/Main.hs
@@ -25,6 +25,7 @@
 import BNFC.Backend.Latex
 import BNFC.Backend.OCaml
 import BNFC.Backend.Pygments
+import BNFC.Backend.TreeSitter
 import BNFC.CF (CF)
 import BNFC.GetCF
 import BNFC.Options hiding (make, Backend)
@@ -81,3 +82,4 @@
     TargetOCaml        -> makeOCaml
     TargetPygments     -> makePygments
     TargetCheck        -> error "impossible"
+    TargetTreeSitter   -> makeTreeSitter
diff --git a/src/BNFC/Backend/Agda.hs b/src/BNFC/Backend/Agda.hs
--- a/src/BNFC/Backend/Agda.hs
+++ b/src/BNFC/Backend/Agda.hs
@@ -5,9 +5,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
 
--- #if __GLASGOW_HASKELL__ >= 800
--- {-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
--- #endif
+-- {-# OPTIONS_GHC -Wno-unused-top-binds #-}
 
 -- | Agda backend.
 --
diff --git a/src/BNFC/Backend/C/CFtoCAbs.hs b/src/BNFC/Backend/C/CFtoCAbs.hs
--- a/src/BNFC/Backend/C/CFtoCAbs.hs
+++ b/src/BNFC/Backend/C/CFtoCAbs.hs
@@ -35,7 +35,7 @@
 import BNFC.Options  ( RecordPositions(..) )
 import BNFC.Utils    ( (+++), unless )
 import BNFC.Backend.Common.NamedVariables
-import BNFC.Backend.C.Common ( posixC )
+import BNFC.Backend.C.Common ( posixC, memName )
 
 
 -- | The result is two files (.H file, .C file)
@@ -304,12 +304,12 @@
 --   switch(p->kind)
 --   {
 --   case is_EInt:
---     return make_EInt (p->u.eint_.integer_);
+--     return make_EInt (p->u.eInt_.integer_);
 -- <BLANKLINE>
 --   case is_EAdd:
 --     return make_EAdd
---       ( clone_Exp(p->u.eadd_.exp_1)
---       , clone_Exp(p->u.eadd_.exp_2)
+--       ( clone_Exp(p->u.eAdd_.exp_1)
+--       , clone_Exp(p->u.eAdd_.exp_2)
 --       );
 -- <BLANKLINE>
 --   default:
@@ -375,7 +375,7 @@
   prCloneCat :: String -> (Cat, Doc) -> String
   prCloneCat fnm (cat, nt) = cloner cat member
     where
-    member = concat [ "p->u.", map toLower fnm, "_.", render nt ]
+    member = concat [ "p->u.", memName fnm, ".", render nt ]
 
 -- | Clone or not depending on the category.
 --   Only pointers need to be cloned.
@@ -401,8 +401,8 @@
 --     break;
 -- <BLANKLINE>
 --   case is_EAdd:
---     free_Exp(p->u.eadd_.exp_1);
---     free_Exp(p->u.eadd_.exp_2);
+--     free_Exp(p->u.eAdd_.exp_1);
+--     free_Exp(p->u.eAdd_.exp_2);
 --     break;
 -- <BLANKLINE>
 --   default:
@@ -482,8 +482,8 @@
   prFreeCat fnm (cat, nt) = Just $ concat
       [ maybe ("free_" ++ identCat (normCat cat)) (const "free") $ maybeTokenCat cat
       , "(p->u."
-      , map toLower fnm
-      , "_.", render nt, ");"
+      , memName fnm
+      , ".", render nt, ");"
       ]
 
 
@@ -614,21 +614,15 @@
     prParam n c = (text (identCat c), text ("p" ++ show n))
 
 -- | Prints the assignments of parameters to instance variables.
--- >>> prAssigns "A" [("A",1),("B",2)] [text "abc", text "def"]
+-- >>> prAssigns "A" [("A",1),("BA",2)] [text "abc", text "def"]
 -- tmp->u.a_.a_ = abc;
--- tmp->u.a_.b_2 = def;
+-- tmp->u.a_.ba_2 = def;
 prAssigns :: String -> [IVar] -> [Doc] -> Doc
 prAssigns c vars params = vcat $ zipWith prAssign vars params
   where
     prAssign (t,n) p =
-        text ("tmp->u." ++ c' ++ "_." ++ vname t n) <+> char '=' <+> p <> semi
+        text ("tmp->u." ++ memName c ++ "." ++ vname t n) <+> char '=' <+> p <> semi
     vname t n
       | n == 1, [_] <- filter ((t ==) . fst) vars
                   = varName t
       | otherwise = varName t ++ showNum n
-    c' = map toLower c
-
-{- **** Helper Functions **** -}
-
-memName :: String -> String
-memName s = map toLower s ++ "_"
diff --git a/src/BNFC/Backend/C/CFtoCPrinter.hs b/src/BNFC/Backend/C/CFtoCPrinter.hs
--- a/src/BNFC/Backend/C/CFtoCPrinter.hs
+++ b/src/BNFC/Backend/C/CFtoCPrinter.hs
@@ -33,7 +33,8 @@
 
 import BNFC.Backend.Common
 import BNFC.Backend.Common.NamedVariables
-import BNFC.Backend.Common.StrUtils (renderCharOrString)
+import BNFC.Backend.Common.StrUtils       ( renderCharOrString )
+import BNFC.Backend.C.Common              ( memName )
 
 -- | Produces (.h file, .c file).
 
@@ -446,7 +447,7 @@
   where
     p   = precRule r
     fnm = funName fun
-    pre = concat [ "p->u.", map toLower fnm, "_." ]
+    pre = concat [ "p->u.", memName fnm, "." ]
 
 -- | Only render the rhs (items) of a rule.
 
@@ -567,8 +568,8 @@
   [ "    sh"
   , maybe (identCat $ normCat cat) basicFunName $ maybeTokenCat cat
   , "(p->u."
-  , map toLower fnm
-  , "_."
+  , memName fnm
+  , "."
   , render nt
   , ");\n"
   ]
diff --git a/src/BNFC/Backend/C/CFtoCSkel.hs b/src/BNFC/Backend/C/CFtoCSkel.hs
--- a/src/BNFC/Backend/C/CFtoCSkel.hs
+++ b/src/BNFC/Backend/C/CFtoCSkel.hs
@@ -19,6 +19,7 @@
 import BNFC.CF
 import BNFC.Utils                         ( (+++), capitalize )
 import BNFC.Backend.Common.NamedVariables
+import BNFC.Backend.C.Common              ( memName )
 import Data.Char                          ( toLower )
 import Data.Either                        ( lefts )
 
@@ -166,11 +167,11 @@
 
 -- | Visits all the instance variables of a category.
 -- >>> let ab = Cat "Ab"
--- >>> prPrintRule (Rule "Abc" undefined [Left ab, Left ab] Parsable)
---   case is_Abc:
---     /* Code for Abc Goes Here */
---     visitAb(p->u.abc_.ab_1);
---     visitAb(p->u.abc_.ab_2);
+-- >>> prPrintRule (Rule "ABC" undefined [Left ab, Left ab] Parsable)
+--   case is_ABC:
+--     /* Code for ABC Goes Here */
+--     visitAb(p->u.aBC_.ab_1);
+--     visitAb(p->u.aBC_.ab_2);
 --     break;
 -- <BLANKLINE>
 -- >>> let ab = TokenCat "Ab"
@@ -209,8 +210,7 @@
       let visitf = "visit" <> if isTokenCat cat
                        then basicFunName cat
                        else text (identCat (normCat cat))
-      in visitf <> parens ("p->u." <> text v <> "_." <> vname ) <> ";"
-    where v = map toLower fnm
+      in visitf <> parens ("p->u." <> text (memName fnm) <> "." <> vname ) <> ";"
 
 -- | The visit-function name of a basic type
 
diff --git a/src/BNFC/Backend/C/Common.hs b/src/BNFC/Backend/C/Common.hs
--- a/src/BNFC/Backend/C/Common.hs
+++ b/src/BNFC/Backend/C/Common.hs
@@ -1,10 +1,12 @@
 -- | Common definitions for the modules of the C backend.
 
 module BNFC.Backend.C.Common
-  ( posixC
+  ( memName
+  , posixC
   ) where
 
 import Prelude
+import BNFC.Backend.Common.NamedVariables
 
 -- | Switch C to language variant that has @strdup@.
 
@@ -16,3 +18,8 @@
   , " */"
   , "#define _POSIX_C_SOURCE 200809L"
   ]
+
+-- | Variant names in unions.
+
+memName :: String -> String
+memName s = firstLowerCase s ++ "_"
diff --git a/src/BNFC/Backend/Haskell/CFtoAbstract.hs b/src/BNFC/Backend/Haskell/CFtoAbstract.hs
--- a/src/BNFC/Backend/Haskell/CFtoAbstract.hs
+++ b/src/BNFC/Backend/Haskell/CFtoAbstract.hs
@@ -39,15 +39,14 @@
   -> CF        -- ^ Grammar.
   -> Doc
 -- tokenText :: TokenText -- ^ Use @ByteString@ or @Text@ instead of @String@?
--- generic   :: Bool      -- ^ Derive @Data@, Generic@, @Typeable@?
+-- generic   :: Bool      -- ^ Derive @Data@ and Generic@?
 -- functor   :: Bool      -- ^ Make the tree a functor?
 cf2Abstract Options{ lang, tokenText, generic, functor } name cf = vsep . concat $
     [ []
 
     -- Modules header
     , [ vcat . concat $
-        [ [ "{-# LANGUAGE DeriveDataTypeable #-}"         | gen ]
-        , [ "{-# LANGUAGE DeriveGeneric #-}"              | gen ]
+        [ [ "{-# LANGUAGE DeriveGeneric #-}"              | gen ]
         , [ "{-# LANGUAGE DeriveTraversable #-}"          | fun ]
         , [ "{-# LANGUAGE FlexibleInstances #-}"          | fun ]
         , [ "{-# LANGUAGE GeneralizedNewtypeDeriving #-}" | hasIdentLikeNoPos ] -- for IsString
@@ -71,8 +70,8 @@
       ]
     , [ vcat . concat $
         [ when hasTextualToks $ map text $ tokenTextImport tokenText
-        , [ "import qualified Data.Data    as C (Data, Typeable)" | gen ]
-        , [ "import qualified GHC.Generics as C (Generic)"        | gen ]
+        , [ "import qualified Data.Data    as C (Data)"    | gen ]
+        , [ "import qualified GHC.Generics as C (Generic)" | gen ]
         ]
       ]
 
@@ -140,7 +139,7 @@
 
     stdClasses = [ "Eq", "Ord", "Show", "Read" ]
     funClasses = [ "Functor", "Foldable", "Traversable" ]
-    genClasses = [ "Data", "Typeable", "Generic" ]
+    genClasses = [ "Data", "Generic" ]
     derivingClasses functor = map ("C." ++) $ concat
       [ stdClasses
       , when functor funClasses
diff --git a/src/BNFC/Backend/Haskell/CFtoAlex3.hs b/src/BNFC/Backend/Haskell/CFtoAlex3.hs
--- a/src/BNFC/Backend/Haskell/CFtoAlex3.hs
+++ b/src/BNFC/Backend/Haskell/CFtoAlex3.hs
@@ -37,7 +37,7 @@
 prelude name tokenText = concat
   [ [ "-- Lexer definition for use with Alex 3"
     , "{"
-    , "{-# OPTIONS -fno-warn-incomplete-patterns #-}"
+    , "{-# OPTIONS -Wno-incomplete-patterns #-}"
     , "{-# OPTIONS_GHC -w #-}"
     , ""
     , "{-# LANGUAGE PatternSynonyms #-}"
@@ -62,8 +62,8 @@
 cMacros =
   [ "-- Predefined character classes"
   , ""
-  , "$c = [A-Z\\192-\\221] # [\\215]  -- capital isolatin1 letter (215 = \\times) FIXME"
-  , "$s = [a-z\\222-\\255] # [\\247]  -- small   isolatin1 letter (247 = \\div  ) FIXME"
+  , "$c = [A-Z\\192-\\222] # [\\215]  -- capital isolatin1 letter (215 = \\times)"
+  , "$s = [a-z\\223-\\255] # [\\247]  -- small   isolatin1 letter (247 = \\div  )"
   , "$l = [$c $s]         -- letter"
   , "$d = [0-9]           -- digit"
   , "$i = [$l $d _ ']     -- identifier character"
diff --git a/src/BNFC/Backend/Haskell/CFtoHappy.hs b/src/BNFC/Backend/Haskell/CFtoHappy.hs
--- a/src/BNFC/Backend/Haskell/CFtoHappy.hs
+++ b/src/BNFC/Backend/Haskell/CFtoHappy.hs
@@ -62,7 +62,7 @@
 header modName absName lexName tokenText eps = unlines $ concat
   [ [ "-- Parser definition for use with Happy"
     , "{"
-    , "{-# OPTIONS_GHC -fno-warn-incomplete-patterns -fno-warn-overlapping-patterns #-}"
+    , "{-# OPTIONS_GHC -Wno-incomplete-patterns -Wno-overlapping-patterns #-}"
     , "{-# LANGUAGE PatternSynonyms #-}"
     , ""
     , "module " ++ modName
diff --git a/src/BNFC/Backend/Haskell/CFtoLayout.hs b/src/BNFC/Backend/Haskell/CFtoLayout.hs
--- a/src/BNFC/Backend/Haskell/CFtoLayout.hs
+++ b/src/BNFC/Backend/Haskell/CFtoLayout.hs
@@ -23,7 +23,7 @@
 
 cf2Layout :: String -> String -> CF -> String
 cf2Layout layName lexName cf = unlines $ concat
-  [ [ "{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}"
+  [ [ "{-# OPTIONS_GHC -Wno-incomplete-patterns #-}"
     , ""
     , "{-# LANGUAGE LambdaCase #-}"
     , "{-# LANGUAGE PatternGuards #-}"
diff --git a/src/BNFC/Backend/Haskell/CFtoPrinter.hs b/src/BNFC/Backend/Haskell/CFtoPrinter.hs
--- a/src/BNFC/Backend/Haskell/CFtoPrinter.hs
+++ b/src/BNFC/Backend/Haskell/CFtoPrinter.hs
@@ -63,13 +63,9 @@
     , "{-# LANGUAGE LambdaCase #-}"
     ]
   , [ "{-# LANGUAGE GADTs #-}"                | useGadt ]
-  , [ "#if __GLASGOW_HASKELL__ <= 708"
-    , "{-# LANGUAGE OverlappingInstances #-}"
-    , "#endif"
-    ]
   , [ ""
     -- -- WAS: Needed for precedence category lists, e.g. @[Exp2]@:
-    -- , "{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}"
+    -- , "{-# OPTIONS_GHC -Wno-incomplete-patterns #-}"
     -- , ""
     , "-- | Pretty-printer for " ++ takeWhile ('.' /=) name ++ "."
     , ""
diff --git a/src/BNFC/Backend/Haskell/MkErrM.hs b/src/BNFC/Backend/Haskell/MkErrM.hs
--- a/src/BNFC/Backend/Haskell/MkErrM.hs
+++ b/src/BNFC/Backend/Haskell/MkErrM.hs
@@ -16,10 +16,6 @@
 mkErrM errMod = vcat
     [ "{-# LANGUAGE CPP #-}"
     , ""
-    , "#if __GLASGOW_HASKELL__ >= 708"
-    , "---------------------------------------------------------------------------"
-    , "-- Pattern synonyms exist since ghc 7.8."
-    , ""
     , "-- | BNF Converter: Error Monad."
     , "--"
     , "-- Module for backwards compatibility."
@@ -60,47 +56,4 @@
     , "instance MonadPlus Err where"
     , "  mzero = empty"
     , "  mplus = (<|>)"
-    , ""
-    , "#else"
-    , "---------------------------------------------------------------------------"
-    , "-- ghc 7.6 and before: use old definition as data type."
-    , ""
-    , "-- | BNF Converter: Error Monad"
-    , ""
-    , "-- Copyright (C) 2004  Author:  Aarne Ranta"
-    , "-- This file comes with NO WARRANTY and may be used FOR ANY PURPOSE."
-    , ""
-    , "module" <+> text errMod <+> "where"
-    , ""
-    , "-- the Error monad: like Maybe type with error msgs"
-    , ""
-    , "import Control.Applicative (Applicative(..), Alternative(..))"
-    , "import Control.Monad       (MonadPlus(..), liftM)"
-    , ""
-    , "data Err a = Ok a | Bad String"
-    , "  deriving (Read, Show, Eq, Ord)"
-    , ""
-    , "instance Monad Err where"
-    , "  return      = Ok"
-    , "  Ok a  >>= f = f a"
-    , "  Bad s >>= _ = Bad s"
-    , ""
-    , "instance Applicative Err where"
-    , "  pure = Ok"
-    , "  (Bad s) <*> _ = Bad s"
-    , "  (Ok f) <*> o  = liftM f o"
-    , ""
-    , "instance Functor Err where"
-    , "  fmap = liftM"
-    , ""
-    , "instance MonadPlus Err where"
-    , "  mzero = Bad \"Err.mzero\""
-    , "  mplus (Bad _) y = y"
-    , "  mplus x       _ = x"
-    , ""
-    , "instance Alternative Err where"
-    , "  empty = mzero"
-    , "  (<|>) = mplus"
-    , ""
-    , "#endif"
     ]
diff --git a/src/BNFC/Backend/Haskell/Utils.hs b/src/BNFC/Backend/Haskell/Utils.hs
--- a/src/BNFC/Backend/Haskell/Utils.hs
+++ b/src/BNFC/Backend/Haskell/Utils.hs
@@ -38,10 +38,7 @@
 -- * GHC pragmas
 
 noWarnUnusedMatches :: IsString a => a
-noWarnUnusedMatches =
-  "{-# OPTIONS_GHC -fno-warn-unused-matches #-}"
-  -- ALT: only from GHC 8
-  -- "{-# OPTIONS_GHC -Wno-unused-matches #-}"
+noWarnUnusedMatches = "{-# OPTIONS_GHC -Wno-unused-matches #-}"
 
 -- * Names for position data type.
 
diff --git a/src/BNFC/Backend/HaskellGADT/CFtoAbstractGADT.hs b/src/BNFC/Backend/HaskellGADT/CFtoAbstractGADT.hs
--- a/src/BNFC/Backend/HaskellGADT/CFtoAbstractGADT.hs
+++ b/src/BNFC/Backend/HaskellGADT/CFtoAbstractGADT.hs
@@ -27,14 +27,14 @@
   , [ "{-# LANGUAGE EmptyCase #-}" | emptyTree ]
   , [ "{-# LANGUAGE LambdaCase #-}"
     , ""
-    , "{-# OPTIONS_GHC -fno-warn-unused-binds #-}"
+    , "{-# OPTIONS_GHC -Wno-unused-binds #-}"
       -- unused-local-binds would be sufficient, but parses only from GHC 8.0
-    , "{-# OPTIONS_GHC -fno-warn-unused-imports #-}"
-    , "{-# OPTIONS_GHC -fno-warn-unused-matches #-}"
-    , "{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}"
+    , "{-# OPTIONS_GHC -Wno-unused-imports #-}"
+    , "{-# OPTIONS_GHC -Wno-unused-matches #-}"
+    , "{-# OPTIONS_GHC -Wno-incomplete-patterns #-}"
       -- defects of coverage checker, e.g. in 8.2.2, may lead to warning
       -- about exceeded iterations for pattern match checker
-    , "{-# OPTIONS_GHC -fno-warn-overlapping-patterns #-}"
+    , "{-# OPTIONS_GHC -Wno-overlapping-patterns #-}"
     , ""
     , "module" +++ name +++ "(" ++ List.intercalate ", " exports ++ ")" +++ "where"
     , ""
diff --git a/src/BNFC/Backend/OCaml/CFtoOCamlLex.hs b/src/BNFC/Backend/OCaml/CFtoOCamlLex.hs
--- a/src/BNFC/Backend/OCaml/CFtoOCamlLex.hs
+++ b/src/BNFC/Backend/OCaml/CFtoOCamlLex.hs
@@ -98,9 +98,9 @@
 cMacros :: [String]
 cMacros =
   [ "(* BNFC character classes *)"
-  , "let _letter = ['a'-'z' 'A'-'Z' '\\192' - '\\255'] # ['\\215' '\\247']    (*  isolatin1 letter FIXME *)"
-  , "let _upper  = ['A'-'Z' '\\192'-'\\221'] # '\\215'      (*  capital isolatin1 letter FIXME *)"
-  , "let _lower  = ['a'-'z' '\\222'-'\\255'] # '\\247'      (*  small isolatin1 letter FIXME *)"
+  , "let _letter = ['A'-'Z' 'a'-'z' '\\181' '\\192' - '\\255'] # ['\\215' '\\247']    (*  isolatin1 letter *)"
+  , "let _upper  = ['A'-'Z' '\\192'-'\\222'] # '\\215'      (*  capital isolatin1 letter *)"
+  , "let _lower  = ['a'-'z' '\\181' '\\223'-'\\255'] # '\\247'      (*  small isolatin1 letter *)"
   , "let _digit  = ['0'-'9']                             (*  _digit *)"
   , "let _idchar = _letter | _digit | ['_' '\\'']         (*  identifier character *)"
   , "let _universal = _                                  (* universal: any character *)"
diff --git a/src/BNFC/Backend/TreeSitter.hs b/src/BNFC/Backend/TreeSitter.hs
new file mode 100644
--- /dev/null
+++ b/src/BNFC/Backend/TreeSitter.hs
@@ -0,0 +1,32 @@
+{-
+    BNF Converter: TreeSitter Grammar Generator
+    Copyright (C) 2004  Author:  Markus Forsberg, Michael Pellauer,
+                                 Bjorn Bringert
+
+    Description   : This module generates the grammar.js input file for
+                    tree-sitter.
+
+    Author        : Kangjing Huang (huangkangjing@gmail.com)
+    Created       : 23 Nov, 2023
+
+-}
+
+module BNFC.Backend.TreeSitter where
+
+import BNFC.Backend.Base
+import BNFC.Backend.TreeSitter.CFtoTreeSitter (cfToTreeSitter)
+import BNFC.CF
+import BNFC.Options hiding (Backend)
+import BNFC.PrettyPrint
+
+-- | Entry point: create grammar.js file
+makeTreeSitter :: SharedOptions -> CF -> Backend
+makeTreeSitter opts cf = do
+  mkfile "grammar.js" comment (render $ cfToTreeSitter name cf)
+  where
+    name = lang opts
+
+comment :: String -> String
+comment = ("// " ++)
+
+-- | TODO: Add Makefile generation for tree-sitter
diff --git a/src/BNFC/Backend/TreeSitter/CFtoTreeSitter.hs b/src/BNFC/Backend/TreeSitter/CFtoTreeSitter.hs
new file mode 100644
--- /dev/null
+++ b/src/BNFC/Backend/TreeSitter/CFtoTreeSitter.hs
@@ -0,0 +1,244 @@
+{-
+    BNF Converter: TreeSitter Grammar Generator
+    Copyright (C) 2004  Author:  Markus Forsberg, Michael Pellauer,
+                                 Bjorn Bringert
+
+    Description   : This module converts BNFC grammar to the contents of a
+                    tree-sitter grammar.js file
+
+    Author        : Kangjing Huang (huangkangjing@gmail.com)
+    Created       : 08 Nov, 2023
+
+-}
+
+module BNFC.Backend.TreeSitter.CFtoTreeSitter where
+
+import BNFC.Abs (Reg (RSeq, RSeqs, RStar, RAny))
+import BNFC.Backend.TreeSitter.RegToJSReg
+import BNFC.CF
+import BNFC.Lexing (mkRegMultilineComment)
+import BNFC.PrettyPrint
+import Prelude hiding ((<>))
+
+-- | Indent one level of 2 spaces
+indent :: Doc -> Doc
+indent = nest 2
+
+-- | Create content of grammar.js file
+cfToTreeSitter :: String -> CF -> Doc
+cfToTreeSitter name cf =
+  -- Overall structure of grammar.js
+  text "module.exports = grammar({"
+    $+$ indent
+      ( text "name: '" <> text name <> text "',"
+          $+$ extrasSection
+          $+$ wordSection
+          $+$ rulesSection
+      )
+    $+$ text "});"
+  where
+    extrasSection = prExtras cf
+    wordSection = prWord cf
+    rulesSection =
+      text "rules: {"
+        $+$ indent
+          ( prRules cf
+              $+$ prUsrTokenRules cf
+              $+$ prBuiltinTokenRules cf
+          )
+        $+$ text "},"
+
+-- | Print rules for comments
+prExtras :: CF -> Doc
+prExtras cf =
+  if extraNeeded
+    then
+      defineSymbol "extras" <> "["
+        $+$ indent
+          ( -- default rule for white spaces
+            text "/\\s/,"
+              $+$ mRules
+              $+$ sRules
+          )
+        $+$ text "],"
+    else empty
+  where
+    extraNeeded = length commentMRules + length commentSRules > 0
+    (commentMRules, commentSRules) = comments cf
+    mRules = vcat' $ map mkOneMRule commentMRules
+    sRules = vcat' $ map mkOneSRule commentSRules
+    mkOneSRule s = text (printRegJSReg $ RSeq (RSeqs s) (RStar RAny)) <> text ","
+    mkOneMRule (s, e) = text (printRegJSReg $ mkRegMultilineComment s e) <> text ","
+
+-- | Print word section, this section is needed for tree-sitter
+--   to do keyword extraction before any parsing/lexing, see
+--   https://tree-sitter.github.io/tree-sitter/creating-parsers#keyword-extraction
+--   TODO: currently, we just add every user defined token as well
+--   as the predefined Ident token to this list to be safe. Ideally,
+--   we should enumerate all defined tokens against all occurrences of
+--   keywords. Any tokens patterns that could accept a keyword will go
+--   into this list. This will require integration of a regex engine.
+prWord :: CF -> Doc
+prWord cf =
+  if wordNeeded
+    then
+      defineSymbol "word"
+        $+$ indent
+          ( wrapChoice
+              ( usrTokensFormatted
+                  ++ [text "$.token_Ident" | identUsed]
+              )
+          )
+          <> ","
+    else empty
+  where
+    wordNeeded = identUsed || usrTokens /= []
+    identUsed = isUsedCat cf (TokenCat catIdent)
+    usrTokens = tokenPragmas cf
+    usrTokensFormatted =
+      map (text . refName . formatCatName False . TokenCat . fst) $ usrTokens
+
+-- | Print builtin token rules according to their usage
+prBuiltinTokenRules :: CF -> Doc
+prBuiltinTokenRules cf =
+  ifC catInteger integerRule
+    $+$ ifC catDouble doubleRule
+    $+$ ifC catChar charRule
+    $+$ ifC catString stringRule
+    $+$ ifC catIdent identRule
+  where
+    ifC cat d = if isUsedCat cf (TokenCat cat) then d else empty
+
+-- | Predefined builtin token rules
+integerRule, doubleRule, charRule, stringRule, identRule :: Doc
+integerRule = defineSymbol "token_Integer" <+> text "/\\d+/" <> ","
+doubleRule = defineSymbol "token_Double" <+> text "/\\d+\\.\\d+(e-?\\d+)?/" <> ","
+charRule =
+  defineSymbol "token_Char" <+> text "/'([^'\\\\]|(\\\\[\"'\\\\tnrf]))'/" <> ","
+stringRule =
+  defineSymbol "token_String" <+> text "/\"([^'\\\\]|(\\\\[\"'\\\\tnrf]))*\"/" <> ","
+identRule =
+  defineSymbol "token_Ident" <+> text "/[a-zA-Z][a-zA-Z\\d_']*/" <> ","
+
+-- | First print the entrypoint rule, tree-sitter always use the
+--   first rule as entrypoint and does not support multi-entrypoint.
+--   Then print rest of the rules
+prRules :: CF -> Doc
+prRules cf =
+  if onlyOneEntry
+    then
+      prOneCat entryRules entryCat
+        $+$ prOtherRules entryCat cf
+    else error "Tree-sitter only supports one entrypoint"
+  where
+    --If entrypoint is defined, there must be only one entrypoint
+    --If it is not defined, defaults to use the first rule as entrypoint
+    onlyOneEntry = not (hasEntryPoint cf) || onlyOneEntryDefined
+    onlyOneEntryDefined = length (allEntryPoints cf) == 1
+    entryCat = firstEntry cf
+    entryRules = rulesForCat' cf entryCat
+
+-- | Print all other rules except the entrypoint
+prOtherRules :: Cat -> CF -> Doc
+prOtherRules entryCat cf = vcat' $ map mkOne rules
+  where
+    rules = [(c, r) | (c, r) <- ruleGroupsInternals cf, c /= entryCat]
+    mkOne (cat, rules) = prOneCat rules cat
+
+prUsrTokenRules :: CF -> Doc
+prUsrTokenRules cf = vcat' $ map prOneToken tokens
+  where
+    tokens = tokenPragmas cf
+
+-- | Check if a set of rules contains internal rules
+hasInternal :: [Rule] -> Bool
+hasInternal = not . all isParsable
+
+-- | Generates one or two tree-sitter rule(s) for one non-terminal from CF.
+-- Uses choice function from tree-sitter to combine rules for the non-terminal
+-- If the non-terminal has internal rules, an internal version of the non-terminal
+-- will be created (prefixed with "_" in tree-sitter), and all internal rules will
+-- be sectioned as such.
+prOneCat :: [Rule] -> NonTerminal -> Doc
+prOneCat rules nt =
+  defineSymbol (formatCatName False nt)
+    $+$ indent (appendComma parRhs)
+    $+$ internalRules
+  where
+    int = hasInternal rules
+    internalRules =
+      if int
+        then defineSymbol (formatCatName True nt) $+$ indent (appendComma intRhs)
+        else empty
+    parRhs = wrapChoice $ transChoice ++ genChoice (filter isParsable rules)
+    transChoice = [text $ refName $ formatCatName True nt | int]
+    intRhs = wrapChoice $ genChoice (filter (not . isParsable) rules)
+    genChoice = map (wrapSeq . formatRhs . rhsRule)
+
+-- | Generate one tree-sitter rule for one defined token
+prOneToken :: (TokenCat, Reg) -> Doc
+prOneToken (cat, exp) =
+  defineSymbol (formatCatName False $ TokenCat cat)
+    $+$ indent (text $ printRegJSReg exp) <> ","
+
+-- | Start a defined symbol block in tree-sitter grammar
+defineSymbol :: String -> Doc
+defineSymbol name = hsep [text name <> ":", text "$", text "=>"]
+
+appendComma :: Doc -> Doc
+appendComma = (<> text ",")
+
+commaJoin :: Bool -> [Doc] -> Doc
+commaJoin newline =
+  foldl comma empty
+  where
+    comma a b
+      | isEmpty a = b
+      | isEmpty b = a
+      | otherwise = (if newline then ($+$) else (<>)) (a <> ",") b
+
+wrapSeq :: [Doc] -> Doc
+wrapSeq = wrapOptListFun "seq" False
+
+wrapChoice :: [Doc] -> Doc
+wrapChoice = wrapOptListFun "choice" True
+
+-- | Wrap list using tree-sitter fun if the list contains multiple items
+-- Returns the only item without wrapping otherwise
+wrapOptListFun :: String -> Bool -> [Doc] -> Doc
+wrapOptListFun fun newline list =
+  if length list == 1
+    then head list
+    else wrapFun fun newline (commaJoin newline list)
+
+wrapFun :: String -> Bool -> Doc -> Doc
+wrapFun fun newline arg = joinOp [text fun <> text "(", indent arg, text ")"]
+  where
+    joinOp = if newline then vcat' else hcat
+
+-- | Helper for referring to non-terminal names in tree-sitter
+refName :: String -> String
+refName = ("$." ++)
+
+-- | Format right hand side into list of strings
+formatRhs :: SentForm -> [Doc]
+formatRhs =
+  map (\case
+    Left c -> text $ refName $ formatCatName False c
+    Right term -> quoted term)
+
+quoted :: String -> Doc
+quoted s = text "\"" <> text s <> text "\""
+
+-- | Format string for cat name, prefix "_" if the name is for internal rules
+formatCatName :: Bool -> Cat -> String
+formatCatName internal c =
+  if internal
+    then "_" ++ formatted
+    else formatted
+  where
+    formatted = formatName c
+    formatName (Cat name) = name
+    formatName (TokenCat name) = "token_" ++ name
+    formatName (ListCat c) = "list_" ++ formatName c
+    formatName (CoercCat name i) = name ++ show i
diff --git a/src/BNFC/Backend/TreeSitter/RegToJSReg.hs b/src/BNFC/Backend/TreeSitter/RegToJSReg.hs
new file mode 100644
--- /dev/null
+++ b/src/BNFC/Backend/TreeSitter/RegToJSReg.hs
@@ -0,0 +1,94 @@
+{-
+    BNF Converter: TreeSitter Grammar Generator
+    Copyright (C) 2004  Author:  Markus Forsberg, Michael Pellauer,
+                                 Bjorn Bringert
+
+    Description   : This module converts BNFC Reg to Javascript regular
+                    expressions that is used in
+
+    Author        : Kangjing Huang (huangkangjing@gmail.com)
+    Created       : 23 Nov, 2023
+
+-}
+{-# LANGUAGE LambdaCase #-}
+
+module BNFC.Backend.TreeSitter.RegToJSReg (printRegJSReg) where
+
+import BNFC.Abs
+
+printRegJSReg :: Reg -> String
+printRegJSReg r = "/" ++ render (prt 0 r) ++ "/"
+
+-- There is no space-based formatting for javascript regex
+-- We just concat everything together
+render :: [String] -> String
+render = concat
+
+parenth :: [String] -> [String]
+parenth s = ["("] ++ s ++ [")"]
+
+-- Printer class
+class Print a where
+  prt :: Int -> a -> [String]
+
+prPrec :: Int -> Int -> [String] -> [String]
+prPrec i j = if i > j then parenth else id
+
+-- | reserved characters for Javascript regex format, sourced from
+--   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
+reserved :: String
+reserved = ".*+?^${}()|[]\\/"
+
+-- | reserved characters for alt expressions in Javascript regex format
+reservedAlt :: String
+reservedAlt = "^[]-\\/"
+
+-- | Pattern for matching empty string in Javascript regex format
+emptyPat :: String
+emptyPat = " [^\\u0000-\\uFFFF]?"
+
+-- | escape character according to Javascript regex format
+escapeCharFrom :: String -> Char -> String
+escapeCharFrom reservedChars x
+  | x `elem` reservedChars = '\\' : [x]
+  | otherwise = [x]
+
+escapeChar :: Char -> String
+escapeChar = escapeCharFrom reserved
+
+escapeStr :: String -> String
+escapeStr = concatMap escapeChar
+
+escapeCharAlt :: Char -> String
+escapeCharAlt = escapeCharFrom reservedAlt
+
+escapeStrAlt :: String -> String
+escapeStrAlt = concatMap escapeChar
+
+instance Print Identifier where
+  prt _ (Identifier (_, i)) = [i]
+
+instance Print Reg where
+  prt i = \case
+    RSeq reg0 reg -> prPrec i 2 $ prt 2 reg0 ++ prt 3 reg
+    RAlt reg0 reg -> prPrec i 1 $ prt 1 reg0 ++  ["|"] ++ prt 2 reg
+    -- Javascript regex does not support general set difference
+    RMinus reg0 REps -> prt i reg0 -- REps is identity for difference
+    RMinus RAny (RChar c) -> ["[^" ++ escapeCharAlt c ++ "]"]
+    RMinus RAny (RAlts s) -> ["[^" ++ escapeStrAlt s ++ "]"]
+    -- TODO: It is possible to use external scanners in tree-sitter:
+    -- https://tree-sitter.github.io/tree-sitter/creating-parsers#external-scanners
+    -- to support general set differences in the future
+    RMinus _ _ -> error "Javascript regex does not support general set differences"
+    RStar reg -> prt 3 reg ++ ["*"]
+    RPlus reg -> prt 3 reg ++ ["+"]
+    ROpt reg -> prt 3 reg ++ ["?"]
+    REps -> [emptyPat]
+    RChar c -> [escapeChar c]
+    RAlts str -> ["[" ++ escapeStrAlt str ++ "]"]
+    RSeqs str -> [escapeStr str]
+    RDigit -> ["\\d"]
+    RLetter -> ["[a-zA-Z]"]
+    RUpper -> ["[A-Z]"]
+    RLower -> ["[a-z]"]
+    RAny -> ["."]
diff --git a/src/BNFC/Backend/XML.hs b/src/BNFC/Backend/XML.hs
--- a/src/BNFC/Backend/XML.hs
+++ b/src/BNFC/Backend/XML.hs
@@ -7,7 +7,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE PatternSynonyms #-}
 
-{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
 
 module BNFC.Backend.XML ---- (cf2DTD, cf2XML)
   where
diff --git a/src/BNFC/CF.hs b/src/BNFC/CF.hs
--- a/src/BNFC/CF.hs
+++ b/src/BNFC/CF.hs
@@ -783,6 +783,9 @@
 isPositionCat :: CFG f -> TokenCat -> Bool
 isPositionCat cf cat = or [ b | TokenReg name b _ <- cfgPragmas cf, wpThing name == cat]
 
+-- | Is there a explicitly defined @entrypoint@ pragma?
+hasEntryPoint :: CFG f -> Bool
+hasEntryPoint cf = or [ True | EntryPoints{} <- cfgPragmas cf]
 
 -- | Categories that are entry points to the parser.
 --
diff --git a/src/BNFC/GetCF.hs b/src/BNFC/GetCF.hs
--- a/src/BNFC/GetCF.hs
+++ b/src/BNFC/GetCF.hs
@@ -18,12 +18,12 @@
 -- | Check LBNF input file and turn it into the 'CF' internal representation.
 
 module BNFC.GetCF
-  ( parseCF
+  ( parseCF, parseRawCF
   , checkRule, transItem
   ) where
 
 import Control.Arrow (left)
-import Control.Monad.Reader (ReaderT, runReaderT, MonadReader(..), asks)
+import Control.Monad.Reader (ReaderT, runReaderT, MonadReader(..), ask)
 import Control.Monad.State (State, evalState, get, modify)
 import Control.Monad.Except (MonadError(..))
 
@@ -63,19 +63,24 @@
 -- $setup
 -- >>> import BNFC.Print
 
--- | Entrypoint.
+-- | Parse raw CF from LBNF file without checking backend requirements
 
-parseCF :: SharedOptions -> Target -> String -> IO CF
-parseCF opts target content = do
-  cf <- runErr $ pGrammar (myLexer content)
+parseRawCF :: FilePath -> String -> Err CF
+parseRawCF fileName content = pGrammar (myLexer content)
                     -- <&> expandRules -- <&> from ghc 8.4
                     >>= return . expandRules
-                    >>= getCF opts
+                    >>= getCF fileName
                     >>= return . markTokenCategories
+                    -- Construct the typing information in 'define' expressions.
+                    >>= runTypeChecker . checkDefinitions
 
-  -- Construct the typing information in 'define' expressions.
-  cf <- either die return $ runTypeChecker $ checkDefinitions cf
+-- | Entrypoint. Parses full CF from LBNF file and checks against
+--   all backend requirements
 
+parseCF :: SharedOptions -> Target -> String -> IO CF
+parseCF opts target content = do
+  cf <- runErr $ parseRawCF (lbnfFile opts) content
+
   -- Some backends do not allow the grammar name to coincide with
   -- one of the category or constructor names.
   let names    = allNames cf
@@ -162,9 +167,13 @@
             ]
 
   -- Warn or fail if the grammar uses names not unique modulo upper/lowercase.
-  when False $
-   case nub $ filter (`notElem` nonUniqueNames) $ filter (not . isDefinedRule) $
-       concatMap List1.toList $ duplicatesOn (map toLower . wpThing) names of
+  case nub
+    . filter (`notElem` nonUniqueNames)
+    . concatMap List1.toList
+    . duplicatesOn (map toLower . wpThing)
+    . filter (not . isDefinedRule)
+    $ names
+    of
     [] -> return ()
     ns | target `elem` [ TargetJava ]
        -> dieUnlessForce $ unlines $ concat
@@ -259,9 +268,9 @@
 -- | Translate the parsed grammar file into a context-free grammar 'CF'.
 --   Desugars and type-checks.
 
-getCF :: SharedOptions -> Abs.Grammar -> Err CF
-getCF opts (Abs.Grammar defs) = do
-    (pragma, rules) <- partitionEithers . concat <$> mapM transDef defs `runTrans` opts
+getCF :: FilePath -> Abs.Grammar -> Err CF
+getCF inputFile (Abs.Grammar defs) = do
+    (pragma, rules) <- partitionEithers . concat <$> mapM transDef defs `runTrans` inputFile
     let reservedWords      = nub [ t | r <- rules, isParsable r, Right t <- rhsRule r, not $ all isSpace t ]
           -- Issue #204: exclude keywords from internal rules
           -- Issue #70: whitespace separators should be treated like "", at least in the parser
@@ -345,10 +354,10 @@
     }
 
 -- | Translation monad.
-newtype Trans a = Trans { unTrans :: ReaderT SharedOptions Err a }
-  deriving (Functor, Applicative, Monad, MonadReader SharedOptions, MonadError String)
+newtype Trans a = Trans { unTrans :: ReaderT FilePath Err a }
+  deriving (Functor, Applicative, Monad, MonadReader FilePath, MonadError String)
 
-runTrans :: Trans a -> SharedOptions -> Err a
+runTrans :: Trans a -> FilePath -> Err a
 runTrans m opts = unTrans m `runReaderT` opts
 
 transDef :: Abs.Def -> Trans [Either Pragma Rule]
@@ -447,7 +456,7 @@
 
 ebnfRules :: Abs.Identifier -> [Abs.RHS] -> Trans [Rule]
 ebnfRules (Abs.Identifier ((line, col), c)) rhss = do
-  file <- asks lbnfFile
+  file <- ask
   let wp = WithPosition $ Position file line col
   let rule x rhs = Rule (wp x) (wp $ strToCat c) rhs Parsable
   return
@@ -487,7 +496,7 @@
 transCat = \case
     Abs.ListCat cat                             -> fmap ListCat <$> transCat cat
     Abs.IdCat (Abs.Identifier ((line, col), c)) -> do
-      file <- asks lbnfFile
+      file <- ask
       return $ WithPosition (Position file line col) $ strToCat c
 
 transLabel :: Abs.Label -> Trans RFun
@@ -500,7 +509,7 @@
 
 transIdent :: Abs.Identifier -> Trans RString
 transIdent (Abs.Identifier ((line, col), str)) = do
-  file <- asks lbnfFile
+  file <- ask
   return $ WithPosition (Position file line col) str
 
 transArg :: Abs.Arg -> (String, Base)
diff --git a/src/BNFC/Options.hs b/src/BNFC/Options.hs
--- a/src/BNFC/Options.hs
+++ b/src/BNFC/Options.hs
@@ -4,7 +4,7 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE TupleSections #-}
 
-{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
 
 module BNFC.Options
   ( Mode(..), Target(..), Backend
@@ -63,6 +63,7 @@
 data Target = TargetC | TargetCpp | TargetCppNoStl
             | TargetHaskell | TargetHaskellGadt | TargetLatex
             | TargetJava | TargetOCaml | TargetPygments
+            | TargetTreeSitter
             | TargetCheck
   deriving (Eq, Bounded, Enum, Ord)
 
@@ -80,6 +81,7 @@
   show TargetJava         = "Java"
   show TargetOCaml        = "OCaml"
   show TargetPygments     = "Pygments"
+  show TargetTreeSitter   = "Tree-sitter"
   show TargetCheck        = "Check LBNF file"
 
 -- | Which version of Alex is targeted?
@@ -132,7 +134,7 @@
   --- Haskell specific:
   , inDir         :: Bool        -- ^ Option @-d@.
   , functor       :: Bool        -- ^ Option @--functor@.  Make AST functorial?
-  , generic       :: Bool        -- ^ Option @--generic@.  Derive Data, Generic, Typeable?
+  , generic       :: Bool        -- ^ Option @--generic@.  Derive Data and Generic?
   , alexMode      :: AlexVersion -- ^ Options @--alex@.
   , tokenText     :: TokenText   -- ^ Options @--bytestrings@, @--string-token@, and @--text-token@.
   , glr           :: HappyMode   -- ^ Happy option @--glr@.
@@ -258,6 +260,7 @@
   TargetJava        -> "java"
   TargetOCaml       -> "ocaml"
   TargetPygments    -> "pygments"
+  TargetTreeSitter  -> "tree-sitter"
   TargetCheck       -> "check"
 
 printAlexOption :: AlexVersion -> String
@@ -309,6 +312,8 @@
     "Output OCaml code for use with ocamllex and menhir (short for --ocaml --menhir)"
   , Option "" ["pygments"]      (NoArg (\o -> o {target = TargetPygments}))
     "Output a Python lexer for Pygments"
+  , Option "" ["tree-sitter"]   (NoArg (\o -> o {target = TargetTreeSitter}))
+    "Output grammar.js file for use with tree-sitter"
   , Option "" ["check"]         (NoArg (\ o -> o{target = TargetCheck }))
     "No output. Just check input LBNF file"
   ]
@@ -373,7 +378,7 @@
           "Make the AST a functor and use it to store the position of the nodes"
     , haskellTargets )
   , ( Option []    ["generic"] (NoArg (\o -> o {generic = True}))
-          "Derive Data, Generic, and Typeable instances for AST types"
+          "Derive Data and Generic instances for AST types"
     , haskellTargets )
   , ( Option []    ["xml"] (NoArg (\o -> o {xml = 1}))
           "Also generate a DTD and an XML printer"
@@ -524,6 +529,7 @@
     TargetJava        -> True
     TargetOCaml       -> True
     TargetPygments    -> True
+    TargetTreeSitter  -> True
     TargetCheck       -> True
 
 instance Maintained AlexVersion where
@@ -654,5 +660,4 @@
     , ("-generic"             , "--generic")
     , ("--ghc"                , "--generic")
     , ("--deriveGeneric"      , "--generic")
-    , ("--deriveDataTypeable" , "--generic")
     ]
diff --git a/src/BNFC/Print.hs b/src/BNFC/Print.hs
--- a/src/BNFC/Print.hs
+++ b/src/BNFC/Print.hs
@@ -1,13 +1,10 @@
--- File generated by the BNF Converter (bnfc 2.9.3).
+-- File generated by the BNF Converter (bnfc 2.9.6).
 
 {-# LANGUAGE CPP #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE LambdaCase #-}
-#if __GLASGOW_HASKELL__ <= 708
-{-# LANGUAGE OverlappingInstances #-}
-#endif
 
-{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
+{-# OPTIONS_GHC -Wno-incomplete-patterns #-}
 
 -- | Pretty-printer for BNFC.
 
diff --git a/src/BNFC/Utils.hs b/src/BNFC/Utils.hs
--- a/src/BNFC/Utils.hs
+++ b/src/BNFC/Utils.hs
@@ -3,8 +3,6 @@
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE PatternSynonyms #-}
 
-{-# OPTIONS_GHC -fno-warn-orphans #-}  -- ghc 7.10
-
 module BNFC.Utils
     ( ModuleName
     , when, unless, unlessNull, unlessNull'
@@ -56,18 +54,6 @@
 type ModuleName = String
 
 -- * Control flow.
-
--- ghc 7.10 misses the instance Monoid a => Monoid (IO a)
-
-#if __GLASGOW_HASKELL__ <= 710
-instance {-# OVERLAPPING #-} Semigroup (IO ()) where
-  (<>) = (>>)
-
-instance {-# OVERLAPPING #-} Monoid (IO ()) where
-  mempty  = return ()
-  mappend = (<>)
-  mconcat = sequence_
-#endif
 
 -- | Generalization of 'Control.Monad.when'.
 when :: Monoid m => Bool -> m -> m
diff --git a/src/Makefile b/src/Makefile
--- a/src/Makefile
+++ b/src/Makefile
@@ -62,4 +62,8 @@
 rm-fake-cabal :
 	rm inferior-haskell-find-project-root.cabal
 
+.PHONY: clean
+clean :
+	rm BNFC/Lex.hs BNFC/Par.hs BNFC/*.bak
+
 #EOF
diff --git a/stack-8.2.yaml b/stack-8.2.yaml
deleted file mode 100644
--- a/stack-8.2.yaml
+++ /dev/null
@@ -1,1 +0,0 @@
-resolver: lts-11.22
diff --git a/stack-9.10.yaml b/stack-9.10.yaml
--- a/stack-9.10.yaml
+++ b/stack-9.10.yaml
@@ -1,1 +1,1 @@
-resolver: lts-24.15
+resolver: lts-24.3
diff --git a/stack-9.12.yaml b/stack-9.12.yaml
--- a/stack-9.12.yaml
+++ b/stack-9.12.yaml
@@ -1,3 +1,3 @@
-resolver: nightly-2025-10-14
+resolver: nightly-2025-08-08
 compiler: ghc-9.12.2
 compiler-check: newer-minor
diff --git a/stack-9.8.yaml b/stack-9.8.yaml
--- a/stack-9.8.yaml
+++ b/stack-9.8.yaml
@@ -1,4 +1,4 @@
-resolver: lts-23.28
+resolver: lts-23.27
 compiler: ghc-9.8.4
 compiler-check: newer-minor
 
diff --git a/test/BNFC/Backend/TreeSitterSpec.hs b/test/BNFC/Backend/TreeSitterSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/BNFC/Backend/TreeSitterSpec.hs
@@ -0,0 +1,25 @@
+module BNFC.Backend.TreeSitterSpec where
+
+import BNFC.Options
+import BNFC.GetCF
+
+import Test.Hspec
+import BNFC.Hspec
+
+import BNFC.Backend.TreeSitter -- SUT
+
+calcOptions = defaultOptions { lang = "Calc" }
+getCalc = parseCF  calcOptions TargetTreeSitter $
+  unlines [ "EAdd. Exp ::= Exp \"+\" Exp1  ;"
+          , "ESub. Exp ::= Exp \"-\" Exp1  ;"
+          , "EMul. Exp1  ::= Exp1  \"*\" Exp2  ;"
+          , "EDiv. Exp1  ::= Exp1  \"/\" Exp2  ;"
+          , "EInt. Exp2  ::= Integer ;"
+          , "coercions Exp 2 ;" ]
+
+spec = do
+
+  describe "Tree-Sitter backend" $ do
+    it "creates the grammar.js file" $ do
+      calc <- getCalc
+      makeTreeSitter calcOptions calc `shouldGenerate` "grammar.js"
