diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.1.0.0
+
+Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2021, Zachary Wood
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Guillaume Bouchard nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/LICENSE-PyF b/LICENSE-PyF
new file mode 100644
--- /dev/null
+++ b/LICENSE-PyF
@@ -0,0 +1,30 @@
+Copyright (c) 2017, Guillaume Bouchard
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Guillaume Bouchard nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+# ghc-hs-meta
+
+Generate Template Haskell expressions from Haskell source code using the GHC parser.
+This package runs on GHC versions 8.10.7, 9.0.2, and 9.2.1.
+
+## Usage
+
+Pass a String containing Haskell source code to `parseExp`.
+Example from the tests:
+
+```haskell
+case parseExp "a @b" of
+    Right exp -> exp `shouldBe` TH.AppTypeE (TH.VarE (TH.mkName "a")) (TH.VarT (TH.mkName "b"))
+    Left (_, _, errMsg) -> error errMsg
+```
+
+See Hackage documentation for more documentation.
+
+## Thank you, PyF
+
+This code originated from the excellent parser included in the [`PyF`](https://github.com/guibou/PyF) package.
+I extracted the relevant code and refactored/renamed things to be usable in a more general context.
+Without PyF, this could wouldn't have been possible. Thank you!
+
+The original license for PyF can be found in the `LICENSE-PyF` file included in this repository.
diff --git a/ghc-hs-meta.cabal b/ghc-hs-meta.cabal
new file mode 100644
--- /dev/null
+++ b/ghc-hs-meta.cabal
@@ -0,0 +1,55 @@
+cabal-version:  2.4
+
+name:           ghc-hs-meta
+version:        0.1.0.0
+build-type:     Simple
+author:         Zachary Wood
+maintainer:     zac.wood@hey.com
+license:        BSD-3-Clause
+license-files:  LICENSE LICENSE-PyF
+synopsis:       Translate Haskell source to Template Haskell expression
+description:    Translate from Haskell source code to Template Haskell expressions using the GHC parser
+category:       ghc,template-haskell
+tested-with:    GHC == 8.10.7, GHC == 9.0.2, GHC == 9.2.1
+
+extra-source-files: CHANGELOG.md README.md
+
+library
+  exposed-modules:
+      Language.Haskell.Meta.Parse
+      Language.Haskell.Meta.Settings
+      Language.Haskell.Meta.Translate
+  hs-source-dirs:
+      src
+  default-extensions:
+      TemplateHaskell
+      LambdaCase
+  ghc-options: -Wall
+  build-depends:
+      base >= 4.14 && <4.17
+    , ghc >= 8.10.7 && < 9.3
+    , ghc-boot >= 8.10.7 && < 9.3
+    , template-haskell >= 2.16.0 && < 2.19
+    , bytestring >= 0.10 && < 0.12
+  default-language: Haskell2010
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  default-language: Haskell2010
+  default-extensions:
+      TemplateHaskell
+  hs-source-dirs:
+    test
+  build-depends:
+      base
+    , template-haskell
+    , ghc
+    , ghc-boot
+    , hspec
+    , bytestring
+    , ghc-hs-meta
+
+source-repository head
+  type:     git
+  location: https://github.com/zacwood9/ghc-hs-meta
diff --git a/src/Language/Haskell/Meta/Parse.hs b/src/Language/Haskell/Meta/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Meta/Parse.hs
@@ -0,0 +1,146 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE ViewPatterns #-}
+
+-- | This module is here to parse Haskell expression using the GHC Api
+module Language.Haskell.Meta.Parse (parseExp, parseExpWithExts, parseExpWithFlags, parseHsExpr) where
+
+#if MIN_VERSION_ghc(9,2,0)
+import qualified GHC.Parser.Errors.Ppr as ParserErrorPpr
+import GHC.Driver.Config (initParserOpts)
+import GHC.Parser.Annotation (LocatedA)
+#endif
+
+#if MIN_VERSION_ghc(9,0,0)
+import GHC.Parser.PostProcess
+import qualified GHC.Types.SrcLoc as SrcLoc
+import GHC.Driver.Session
+import GHC.Data.StringBuffer
+import GHC.Parser.Lexer
+import qualified GHC.Parser.Lexer as Lexer
+import qualified GHC.Parser as Parser
+import GHC.Data.FastString
+import GHC.Types.SrcLoc
+#else
+import qualified SrcLoc
+import DynFlags (DynFlags)
+import Lexer (ParseResult (..), PState (..))
+import StringBuffer
+import Lexer
+import qualified Parser
+import FastString
+import SrcLoc
+import RdrName
+import RdrHsSyn (runECP_P)
+#endif
+
+import GHC.Hs.Extension (GhcPs)
+
+-- @HsExpr@ is available from GHC.Hs.Expr in all versions we support.
+-- However, the goal of GHC is to split HsExpr into its own package, under
+-- the namespace Language.Haskell.Syntax. The module split happened in 9.0,
+-- but still in the ghc package.
+#if MIN_VERSION_ghc(9,2,0)
+import Language.Haskell.Syntax (HsExpr(..))
+#else
+import GHC.Hs.Expr (HsExpr(..))
+#endif
+
+import Language.Haskell.TH (Extension(..))
+import qualified Language.Haskell.TH.Syntax as TH
+
+import qualified Language.Haskell.Meta.Settings as Settings
+import Language.Haskell.Meta.Translate (toExp)
+
+-- | Parse a Haskell expression from source code into a Template Haskell expression.
+-- See @parseExpWithExts@ or @parseExpWithFlags@ for customizing with additional extensions and settings.
+parseExp :: String -> Either (Int, Int, String) TH.Exp
+#if MIN_VERSION_ghc(9,2,0)
+parseExp = parseExpWithExts
+    [ TypeApplications
+    , OverloadedRecordDot
+    , OverloadedLabels
+    , OverloadedRecordUpdate
+    ]
+#else
+parseExp = parseExpWithExts
+    [ TypeApplications
+    ]
+#endif
+
+-- | Parse a Haskell expression from source code into a Template Haskell expression
+-- using a given set of GHC extensions.
+parseExpWithExts :: [Extension] -> String -> Either (Int, Int, String) TH.Exp
+parseExpWithExts exts = parseExpWithFlags (Settings.baseDynFlags exts)
+
+-- | Parse a Haskell expression from source code into a Template Haskell expression
+-- using a given set of GHC DynFlags.
+parseExpWithFlags :: DynFlags -> String -> Either (Int, Int, String) TH.Exp
+parseExpWithFlags flags expStr = do
+  hsExpr <- parseHsExpr flags expStr
+  pure (toExp flags hsExpr)
+
+-- | Run the GHC parser to parse a Haskell expression into a @HsExpr@.
+parseHsExpr :: DynFlags -> String -> Either (Int, Int, String) (HsExpr GhcPs)
+parseHsExpr dynFlags s =
+  case runParser dynFlags s of
+    POk _ locatedExpr ->
+      let expr = SrcLoc.unLoc locatedExpr
+       in Right
+            expr
+
+{- ORMOLU_DISABLE #-}
+#if MIN_VERSION_ghc(9,2,0)
+    -- TODO messages?
+    PFailed PState{loc=SrcLoc.psRealLoc -> srcLoc, errors=errorMessages} ->
+            let
+                psErrToString e = show $ ParserErrorPpr.pprError e
+                err = concatMap psErrToString errorMessages
+                -- err = concatMap show errorMessages
+                line = SrcLoc.srcLocLine srcLoc
+                col = SrcLoc.srcLocCol srcLoc
+            in Left (line, col, err)
+#else
+#if MIN_VERSION_ghc(9,0,0)
+    PFailed PState{loc=SrcLoc.psRealLoc -> srcLoc, messages=msgs} ->
+#elif MIN_VERSION_ghc(8,10,0)
+    PFailed PState{loc=srcLoc, messages=msgs} ->
+#endif
+            let -- TODO: do not ignore "warnMessages"
+                -- I have no idea what they can be
+                (_warnMessages, errorMessages) = msgs dynFlags
+                err = concatMap show errorMessages
+                line = SrcLoc.srcLocLine srcLoc
+                col = SrcLoc.srcLocCol srcLoc
+            in Left (line, col, err)
+#endif
+
+-- From Language.Haskell.GhclibParserEx.GHC.Parser
+
+parse :: P a -> String -> DynFlags -> ParseResult a
+parse p str flags =
+  Lexer.unP p parseState
+  where
+    location = mkRealSrcLoc (mkFastString "<string>") 1 1
+    strBuffer = stringToStringBuffer str
+    parseState =
+#if MIN_VERSION_ghc(9, 2, 0)
+      initParserState (initParserOpts flags) strBuffer location
+#else
+      mkPState flags strBuffer location
+#endif
+
+#if MIN_VERSION_ghc(9, 2, 0)
+runParser :: DynFlags -> String -> ParseResult (LocatedA (HsExpr GhcPs))
+runParser flags str =
+  case parse Parser.parseExpression str flags of
+    POk s e -> unP (runPV (unECP e)) s
+    PFailed ps -> PFailed ps
+#elif MIN_VERSION_ghc(8, 10, 0)
+runParser :: DynFlags -> String -> ParseResult (Located (HsExpr GhcPs))
+runParser flags str =
+  case parse Parser.parseExpression str flags of
+    POk s e -> unP (runECP_P e) s
+    PFailed ps -> PFailed ps
+#else
+parseExpression = parse Parser.parseExpression
+#endif
diff --git a/src/Language/Haskell/Meta/Settings.hs b/src/Language/Haskell/Meta/Settings.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Meta/Settings.hs
@@ -0,0 +1,179 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# OPTIONS_GHC -Wno-missing-fields -Wno-name-shadowing -Wno-unused-imports #-}
+
+-- | Settings needed for running the GHC Parser.
+module Language.Haskell.Meta.Settings (baseDynFlags) where
+
+{- ORMOLU_DISABLE -}
+
+#if MIN_VERSION_ghc(9,0,0)
+import GHC.Settings.Config
+import GHC.Driver.Session
+import GHC.Utils.Fingerprint
+import GHC.Platform
+import GHC.Settings
+#elif MIN_VERSION_ghc(8, 10, 0)
+import Config
+import DynFlags
+import Fingerprint
+import GHC.Platform
+import ToolSettings
+#else
+import Config
+import DynFlags
+import Fingerprint
+import Platform
+#endif
+
+#if MIN_VERSION_ghc(8,10,0)
+import GHC.Hs
+#else
+import HsSyn
+#endif
+
+#if MIN_VERSION_ghc(9, 2, 0)
+import GHC.Driver.Config
+#endif
+
+#if MIN_VERSION_ghc(9,0,0)
+import GHC.Parser.PostProcess
+import GHC.Driver.Session
+import GHC.Data.StringBuffer
+import GHC.Parser.Lexer
+import qualified GHC.Parser.Lexer as Lexer
+import qualified GHC.Parser as Parser
+import GHC.Data.FastString
+import GHC.Types.SrcLoc
+import GHC.Driver.Backpack.Syntax
+import GHC.Unit.Info
+import GHC.Types.Name.Reader
+#else
+import StringBuffer
+import Lexer
+import qualified Parser
+import FastString
+import SrcLoc
+import RdrName
+#endif
+
+#if MIN_VERSION_ghc(9, 0, 0)
+#else
+import RdrHsSyn
+#endif
+
+import Data.Data hiding (Fixity)
+
+#if MIN_VERSION_ghc(9,0,0)
+import GHC.Hs
+
+#if MIN_VERSION_ghc(9, 2, 0)
+import GHC.Types.Fixity
+import GHC.Types.SourceText
+#else
+import GHC.Types.Basic
+#endif
+
+import GHC.Types.Name.Reader
+import GHC.Types.Name
+import GHC.Types.SrcLoc
+#elif MIN_VERSION_ghc(8, 10, 0)
+import BasicTypes
+import OccName
+#else
+import BasicTypes
+import OccName
+#endif
+
+import qualified Language.Haskell.TH.Syntax as GhcTH
+import Data.Maybe
+
+fakeSettings :: Settings
+fakeSettings = Settings
+#if MIN_VERSION_ghc(9, 2, 0)
+  { sGhcNameVersion=ghcNameVersion
+  , sFileSettings=fileSettings
+  , sTargetPlatform=platform
+  , sPlatformMisc=platformMisc
+  , sToolSettings=toolSettings
+  }
+#elif MIN_VERSION_ghc(8, 10, 0)
+  { sGhcNameVersion=ghcNameVersion
+  , sFileSettings=fileSettings
+  , sTargetPlatform=platform
+  , sPlatformMisc=platformMisc
+  , sPlatformConstants=platformConstants
+  , sToolSettings=toolSettings
+  }
+#else
+  { sTargetPlatform=platform
+  , sPlatformConstants=platformConstants
+  , sProjectVersion=cProjectVersion
+  , sProgramName="ghc"
+  , sOpt_P_fingerprint=fingerprint0
+  }
+#endif
+  where
+#if MIN_VERSION_ghc(8, 10, 0)
+    toolSettings = ToolSettings {
+      toolSettings_opt_P_fingerprint=fingerprint0
+      }
+    fileSettings = FileSettings {}
+    platformMisc = PlatformMisc {}
+    ghcNameVersion =
+      GhcNameVersion{ghcNameVersion_programName="ghc"
+                    ,ghcNameVersion_projectVersion=cProjectVersion
+                    }
+#endif
+    platform =
+      Platform{
+#if MIN_VERSION_ghc(9, 0, 0)
+    -- It doesn't matter what values we write here as these fields are
+    -- not referenced for our purposes. However the fields are strict
+    -- so we must say something.
+        platformByteOrder=LittleEndian
+      , platformHasGnuNonexecStack=True
+      , platformHasIdentDirective=False
+      , platformHasSubsectionsViaSymbols=False
+      , platformIsCrossCompiling=False
+      , platformLeadingUnderscore=False
+      , platformTablesNextToCode=False
+#if MIN_VERSION_ghc(9, 2, 0)
+      , platform_constants=platformConstants
+#endif
+      ,
+#endif
+
+#if MIN_VERSION_ghc(9, 2, 0)
+        platformWordSize=PW8
+      , platformArchOS=ArchOS {archOS_arch=ArchUnknown, archOS_OS=OSUnknown}
+#elif MIN_VERSION_ghc(8, 10, 0)
+        platformWordSize=PW8
+      , platformMini=PlatformMini {platformMini_arch=ArchUnknown, platformMini_os=OSUnknown}
+#else
+        platformWordSize=8
+      , platformOS=OSUnknown
+#endif
+      , platformUnregisterised=True
+      }
+#if MIN_VERSION_ghc(9, 2, 0)
+    platformConstants = Nothing
+#else
+    platformConstants =
+      PlatformConstants{pc_DYNAMIC_BY_DEFAULT=False,pc_WORD_SIZE=8}
+#endif
+
+#if MIN_VERSION_ghc(8, 10, 0)
+fakeLlvmConfig :: LlvmConfig
+fakeLlvmConfig = LlvmConfig [] []
+#else
+fakeLlvmConfig :: (LlvmTargets, LlvmPasses)
+fakeLlvmConfig = ([], [])
+#endif
+
+baseDynFlags :: [GhcTH.Extension] -> DynFlags
+baseDynFlags exts =
+  let enable = GhcTH.TemplateHaskellQuotes : exts
+   in foldl xopt_set (defaultDynFlags fakeSettings fakeLlvmConfig) enable
diff --git a/src/Language/Haskell/Meta/Translate.hs b/src/Language/Haskell/Meta/Translate.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Meta/Translate.hs
@@ -0,0 +1,263 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
+{-# LANGUAGE ViewPatterns #-}
+
+module Language.Haskell.Meta.Translate (toExp) where
+
+#if MIN_VERSION_ghc(9,2,0)
+import GHC.Hs.Type (HsWildCardBndrs (..), HsType (..), HsSigType(HsSig), sig_body)
+#elif MIN_VERSION_ghc(9,0,0)
+import GHC.Hs.Type (HsWildCardBndrs (..), HsType (..), HsImplicitBndrs(HsIB), hsib_body)
+#elif MIN_VERSION_ghc(8,10,0)
+import GHC.Hs.Types (HsWildCardBndrs (..), HsType (..), HsImplicitBndrs (HsIB, hsib_body))
+#else
+import HsTypes (HsWildCardBndrs (..), HsType (..), HsImplicitBndrs (HsIB), hsib_body)
+#endif
+
+#if MIN_VERSION_ghc(8,10,0)
+import GHC.Hs.Expr as Expr
+import GHC.Hs.Extension as Ext
+import GHC.Hs.Pat as Pat
+import GHC.Hs.Lit
+#else
+import HsExpr as Expr
+import HsExtension as Ext
+import HsPat as Pat
+import HsLit
+#endif
+
+import qualified Data.ByteString as B
+import qualified Language.Haskell.TH.Syntax as GhcTH
+import qualified Language.Haskell.TH.Syntax as TH
+
+#if MIN_VERSION_ghc(9,0,0)
+import GHC.Types.SrcLoc
+import GHC.Types.Name
+import GHC.Types.Name.Reader
+import GHC.Data.FastString
+#if MIN_VERSION_ghc(9,2,0)
+import GHC.Utils.Outputable (ppr)
+import GHC.Types.Basic (Boxity(..))
+import GHC.Types.SourceText (il_value, rationalFromFractionalLit)
+import GHC.Driver.Ppr (showSDocDebug)
+#else
+import GHC.Utils.Outputable (ppr, showSDocDebug)
+import GHC.Types.Basic (il_value, fl_value, Boxity(..))
+#endif
+import GHC.Driver.Session (DynFlags, xopt_set, defaultDynFlags)
+import qualified GHC.Unit.Module as Module
+#else
+import SrcLoc
+import Name
+import RdrName
+import FastString
+import Outputable (ppr, showSDocDebug)
+import BasicTypes (il_value, fl_value, Boxity(..))
+import DynFlags (DynFlags, xopt_set, defaultDynFlags)
+import qualified Module
+#endif
+
+import GHC.Stack
+import qualified Language.Haskell.Meta.Settings as Settings
+
+import qualified Data.List.NonEmpty as NonEmpty
+
+#if MIN_VERSION_ghc(9,2,0)
+-- TODO: why this disapears in GHC >= 9.2?
+fl_value = rationalFromFractionalLit
+#endif
+
+
+
+-----------------------------
+
+toLit :: HsLit GhcPs -> TH.Lit
+toLit (HsChar _ c) = TH.CharL c
+toLit (HsCharPrim _ c) = TH.CharPrimL c
+toLit (HsString _ s) = TH.StringL (unpackFS s)
+toLit (HsStringPrim _ s) = TH.StringPrimL (B.unpack s)
+toLit (HsInt _ i) = TH.IntegerL (il_value i)
+toLit (HsIntPrim _ i) = TH.IntPrimL i
+toLit (HsWordPrim _ i) = TH.WordPrimL i
+toLit (HsInt64Prim _ i) = TH.IntegerL i
+toLit (HsWord64Prim _ i) = TH.WordPrimL i
+toLit (HsInteger _ i _) = TH.IntegerL i
+toLit (HsRat _ f _) = TH.FloatPrimL (fl_value f)
+toLit (HsFloatPrim _ f) = TH.FloatPrimL (fl_value f)
+toLit (HsDoublePrim _ f) = TH.DoublePrimL (fl_value f)
+
+#if !MIN_VERSION_ghc(9,0,0)
+toLit (XLit _) = noTH "toLit" "XLit"
+#endif
+
+toLit' :: OverLitVal -> TH.Lit
+toLit' (HsIntegral i) = TH.IntegerL (il_value i)
+toLit' (HsFractional f) = TH.RationalL (fl_value f)
+toLit' (HsIsString _ fs) = TH.StringL (unpackFS fs)
+
+toType :: HsType GhcPs -> TH.Type
+toType (HsWildCardTy _) = TH.WildCardT
+toType (HsTyVar _ _ n) =
+  let n' = unLoc n
+   in if isRdrTyVar n'
+        then TH.VarT (toName n')
+        else TH.ConT (toName n')
+toType t = todo "toType" (showSDocDebug (Settings.baseDynFlags []) . ppr $ t)
+
+toName :: RdrName -> TH.Name
+toName n = case n of
+  (Unqual o) -> TH.mkName (occNameString o)
+  (Qual m o) -> TH.mkName (Module.moduleNameString m <> "." <> occNameString o)
+  (Orig _ _) -> error "orig"
+  (Exact _) -> error "exact"
+
+toFieldExp :: a
+toFieldExp = undefined
+
+toPat :: DynFlags -> Pat.Pat GhcPs -> TH.Pat
+toPat _dynFlags (Pat.VarPat _ (unLoc -> name)) = TH.VarP (toName name)
+toPat dynFlags p = todo "toPat" (showSDocDebug dynFlags . ppr $ p)
+
+toExp :: DynFlags -> Expr.HsExpr GhcPs -> TH.Exp
+toExp _ (Expr.HsVar _ n) =
+  let n' = unLoc n
+   in if isRdrDataCon n'
+        then TH.ConE (toName n')
+        else TH.VarE (toName n')
+
+#if MIN_VERSION_ghc(9,0,0)
+toExp _ (Expr.HsUnboundVar _ n)              = TH.UnboundVarE (TH.mkName . occNameString $ n)
+#else
+toExp _ (Expr.HsUnboundVar _ n)              = TH.UnboundVarE (TH.mkName . occNameString . Expr.unboundVarOcc $ n)
+#endif
+
+toExp _ Expr.HsIPVar {}
+  = noTH "toExp" "HsIPVar"
+
+toExp _ (Expr.HsLit _ l)
+  = TH.LitE (toLit l)
+
+toExp _ (Expr.HsOverLit _ OverLit {ol_val})
+  = TH.LitE (toLit' ol_val)
+
+toExp d (Expr.HsApp _ e1 e2)
+  = TH.AppE (toExp d . unLoc $ e1) (toExp d . unLoc $ e2)
+
+#if MIN_VERSION_ghc(9,2,0)
+toExp d (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp d . unLoc $ e) (toType . unLoc $ hswc_body)
+toExp d (Expr.ExprWithTySig _ e HsWC{hswc_body=unLoc -> HsSig{sig_body}}) = TH.SigE (toExp d . unLoc $ e) (toType . unLoc $ sig_body)
+#elif MIN_VERSION_ghc(8,8,0)
+toExp d (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp d . unLoc $ e) (toType . unLoc $ hswc_body)
+toExp d (Expr.ExprWithTySig _ e HsWC{hswc_body=HsIB{hsib_body}}) = TH.SigE (toExp d . unLoc $ e) (toType . unLoc $ hsib_body)
+#else
+toExp d (Expr.HsAppType HsWC {hswc_body} e) = TH.AppTypeE (toExp d . unLoc $ e) (toType . unLoc $ hswc_body)
+#endif
+
+toExp d (Expr.OpApp _ e1 o e2)
+  = TH.UInfixE (toExp d . unLoc $ e1) (toExp d . unLoc $ o) (toExp d . unLoc $ e2)
+
+toExp d (Expr.NegApp _ e _)
+  = TH.AppE (TH.VarE 'negate) (toExp d . unLoc $ e)
+
+-- NOTE: for lambda, there is only one match
+toExp d (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])) _))
+  = TH.LamE (fmap (toPat d) ps) (toExp d e)
+
+-- toExp (Expr.Let _ bs e)                       = TH.LetE (toDecs bs) (toExp e)
+--
+#if MIN_VERSION_ghc(9, 0, 0)
+toExp d (Expr.HsIf _ a b c)                   = TH.CondE (toExp d (unLoc a)) (toExp d (unLoc b)) (toExp d (unLoc c))
+#else
+toExp d (Expr.HsIf _ _ a b c)                   = TH.CondE (toExp d (unLoc a)) (toExp d (unLoc b)) (toExp d (unLoc c))
+#endif
+
+-- toExp (Expr.MultiIf _ ifs)                    = TH.MultiIfE (map toGuard ifs)
+-- toExp (Expr.Case _ e alts)                    = TH.CaseE (toExp e) (map toMatch alts)
+-- toExp (Expr.Do _ ss)                          = TH.DoE (map toStmt ss)
+-- toExp e@Expr.MDo{}                            = noTH "toExp" e
+--
+#if MIN_VERSION_ghc(9, 2, 0)
+toExp d (Expr.ExplicitTuple _ args boxity) = ctor tupArgs
+#else
+toExp d (Expr.ExplicitTuple _ (map unLoc -> args) boxity) = ctor tupArgs
+#endif
+  where
+    toTupArg (Expr.Present _ e) = Just $ unLoc e
+    toTupArg (Expr.Missing _) = Nothing
+    toTupArg _ = error "impossible case"
+
+    ctor = case boxity of
+      Boxed -> TH.TupE
+      Unboxed -> TH.UnboxedTupE
+
+#if MIN_VERSION_ghc(8,10,0)
+    tupArgs = fmap ((fmap (toExp d)) . toTupArg) args
+#else
+    tupArgs = case traverse toTupArg args of
+      Nothing -> error "Tuple section are not supported by template haskell < 8.10"
+      Just args -> fmap (toExp d) args
+#endif
+
+-- toExp (Expr.List _ xs)                        = TH.ListE (fmap toExp xs)
+toExp d (Expr.HsPar _ e)
+  = TH.ParensE (toExp d . unLoc $ e)
+
+toExp d (Expr.SectionL _ (unLoc -> a) (unLoc -> b))
+  = TH.InfixE (Just . toExp d $ a) (toExp d b) Nothing
+
+toExp d (Expr.SectionR _ (unLoc -> a) (unLoc -> b))
+  = TH.InfixE Nothing (toExp d a) (Just . toExp d $ b)
+
+toExp _ (Expr.RecordCon _ name HsRecFields {rec_flds})
+  = TH.RecConE (toName . unLoc $ name) (fmap toFieldExp rec_flds)
+
+-- toExp (Expr.RecUpdate _ e xs)                 = TH.RecUpdE (toExp e) (fmap toFieldExp xs)
+-- toExp (Expr.ListComp _ e ss)                  = TH.CompE $ map convert ss ++ [TH.NoBindS (toExp e)]
+--  where
+--   convert (Expr.QualStmt _ st)                = toStmt st
+--   convert s                                   = noTH "toExp ListComp" s
+-- toExp (Expr.ExpTypeSig _ e t)                 = TH.SigE (toExp e) (toType t)
+--
+#if MIN_VERSION_ghc(9, 2, 0)
+toExp d (Expr.ExplicitList _ (map unLoc -> args)) = TH.ListE (map (toExp d) args)
+#else
+toExp d (Expr.ExplicitList _ _ (map unLoc -> args)) = TH.ListE (map (toExp d) args)
+#endif
+
+toExp d (Expr.ArithSeq _ _ e)
+  = TH.ArithSeqE $ case e of
+    (From a) -> TH.FromR (toExp d $ unLoc a)
+    (FromThen a b) -> TH.FromThenR (toExp d $ unLoc a) (toExp d $ unLoc b)
+    (FromTo a b) -> TH.FromToR (toExp d $ unLoc a) (toExp d $ unLoc b)
+    (FromThenTo a b c) -> TH.FromThenToR (toExp d $ unLoc a) (toExp d $ unLoc b) (toExp d $ unLoc c)
+
+#if MIN_VERSION_ghc(9, 2, 0)
+toExp _ (Expr.HsProjection _ locatedFields) =
+  let
+    extractFieldLabel (HsFieldLabel _ locatedStr) = locatedStr
+    extractFieldLabel _ = error "Don't know how to handle XHsFieldLabel constructor..."
+  in
+    TH.ProjectionE (NonEmpty.map (unpackFS . unLoc . extractFieldLabel . unLoc) locatedFields)
+
+toExp d (Expr.HsGetField _ expr locatedField) =
+  let
+    extractFieldLabel (HsFieldLabel _ locatedStr) = locatedStr
+    extractFieldLabel _ = error "Don't know how to handle XHsFieldLabel constructor..."
+  in
+    TH.GetFieldE (toExp d (unLoc expr)) (unpackFS . unLoc . extractFieldLabel . unLoc $ locatedField)
+
+toExp _ (Expr.HsOverLabel _ fastString) = TH.LabelE (unpackFS fastString)
+#endif
+
+toExp dynFlags e = todo "toExp" (showSDocDebug dynFlags . ppr $ e)
+
+todo :: (HasCallStack, Show e) => String -> e -> a
+todo fun thing = error . concat $ [moduleName, ".", fun, ": not implemented: ", show thing]
+
+noTH :: (HasCallStack, Show e) => String -> e -> a
+noTH fun thing = error . concat $ [moduleName, ".", fun, ": no TemplateHaskell for: ", show thing]
+
+moduleName :: String
+moduleName = "Language.Haskell.Meta.Translate"
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE CPP #-}
+
+module Main where
+
+import Data.List.NonEmpty (nonEmpty)
+import Language.Haskell.Meta.Parse
+import qualified Language.Haskell.TH as TH
+import qualified Language.Haskell.TH.Syntax as TH
+import Test.Hspec ( hspec, describe, it, shouldBe, Spec )
+
+spec :: Spec
+spec = do
+#if MIN_VERSION_ghc(9,2,0)
+  describe "Record dot syntax" $ do
+    it "parses access" $ do
+      let Right exp = parseExp "a.b"
+      exp `shouldBe` TH.GetFieldE (TH.VarE (TH.mkName "a")) "b"
+    it "parses single projection" $ do
+      let Right exp = parseExp "(.b)"
+      let Just list = nonEmpty ["b"]
+      exp `shouldBe` TH.ProjectionE list
+    it "parses multi projection" $ do
+      let Right exp = parseExp "(.b.c.d)"
+      let Just list = nonEmpty ["b", "c", "d"]
+      exp `shouldBe` TH.ProjectionE list
+
+  describe "Overloaded labels" $ do
+    it "parses labels" $ do
+      let Right exp = parseExp "#name"
+      exp `shouldBe` TH.LabelE "name"
+#endif
+
+  describe "Type application" $ do
+    it "parses application" $ do
+      let Right exp = parseExp "a @b"
+      exp `shouldBe` TH.AppTypeE (TH.VarE (TH.mkName "a")) (TH.VarT (TH.mkName "b"))
+
+  describe "infix operators" $ do
+    it "parses infix operator" $ do
+      let Right exp = parseExp "a |> b"
+      exp `shouldBe` TH.UInfixE (TH.VarE (TH.mkName "a")) (TH.VarE (TH.mkName "|>")) (TH.VarE (TH.mkName "b"))
+main = hspec spec
