diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change log
+
+Imp follows the [Package Versioning Policy](https://pvp.haskell.org).
+You can find release notes [on GitHub](https://github.com/tfausak/imp/releases).
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Taylor Fausak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Imp
+
+Imp is a GHC plugin that automatically imports modules when they are used, like
+GHCi does. This allows writing code using qualified identifiers without
+manually importing the modules.
diff --git a/imp.cabal b/imp.cabal
new file mode 100644
--- /dev/null
+++ b/imp.cabal
@@ -0,0 +1,100 @@
+cabal-version: 2.2
+name: imp
+version: 0.2024.3.18
+synopsis: Automatically import modules.
+description:
+  Imp is a GHC plugin that automatically imports modules when they are used,
+  like GHCi does. This allows writing code using qualified identifiers without
+  manually importing the modules.
+
+category: Plugin
+maintainer: Taylor Fausak
+license: MIT
+license-file: LICENSE.txt
+extra-doc-files:
+  CHANGELOG.md
+  README.md
+
+source-repository head
+  location: https://github.com/tfausak/imp
+  type: git
+
+flag pedantic
+  default: False
+  manual: True
+
+common library
+  build-depends: base ^>=4.17.0.0 || ^>=4.18.0.0 || ^>=4.19.0.0
+  default-language: Haskell2010
+  ghc-options:
+    -Weverything
+    -Wno-all-missed-specialisations
+    -Wno-implicit-prelude
+    -Wno-missed-specialisations
+    -Wno-missing-deriving-strategies
+    -Wno-missing-export-lists
+    -Wno-missing-kind-signatures
+    -Wno-missing-safe-haskell-mode
+    -Wno-prepositive-qualified-module
+    -Wno-safe
+    -Wno-unsafe
+
+common executable
+  import: library
+  build-depends: imp
+  ghc-options:
+    -rtsopts
+    -threaded
+
+library
+  import: library
+  autogen-modules: Paths_imp
+  build-depends:
+    containers ^>=0.6.7,
+    exceptions ^>=0.10.5,
+    ghc ^>=9.4.1 || ^>=9.6.1 || ^>=9.8.1,
+
+  exposed-modules: Imp.Ghc
+  -- cabal-gild: discover source/library
+  exposed-modules:
+    Imp
+    Imp.Exception.InvalidAlias
+    Imp.Exception.InvalidModuleName
+    Imp.Exception.InvalidOption
+    Imp.Exception.ShowHelp
+    Imp.Exception.ShowVersion
+    Imp.Exception.UnexpectedArgument
+    Imp.Exception.UnknownOption
+    Imp.Extra.Exception
+    Imp.Extra.HsModule
+    Imp.Extra.HsParsedModule
+    Imp.Extra.ImportDecl
+    Imp.Extra.ModuleName
+    Imp.Extra.ParsedResult
+    Imp.Extra.ReadP
+    Imp.Type.Alias
+    Imp.Type.Config
+    Imp.Type.Context
+    Imp.Type.Flag
+
+  hs-source-dirs: source/library
+  other-modules: Paths_imp
+
+  if impl(ghc >= 9.6)
+    hs-source-dirs: source/ghc-9.6
+  else
+    hs-source-dirs: source/ghc-9.4
+
+  if flag(pedantic)
+    ghc-options: -Werror
+
+test-suite imp-test-suite
+  import: executable
+  build-depends:
+    exceptions,
+    ghc,
+    hspec ^>=2.11.7,
+
+  hs-source-dirs: source/test-suite
+  main-is: Main.hs
+  type: exitcode-stdio-1.0
diff --git a/source/ghc-9.4/Imp/Ghc.hs b/source/ghc-9.4/Imp/Ghc.hs
new file mode 100644
--- /dev/null
+++ b/source/ghc-9.4/Imp/Ghc.hs
@@ -0,0 +1,24 @@
+module Imp.Ghc where
+
+import qualified GHC.Hs as Hs
+import qualified GHC.Plugins as Plugin
+import qualified GHC.Types.SourceText as SourceText
+
+type HsModulePs = Hs.HsModule
+
+newImportDecl ::
+  Plugin.ModuleName ->
+  Hs.ImportDecl Hs.GhcPs
+newImportDecl moduleName =
+  Hs.ImportDecl
+    { Hs.ideclExt = Hs.noAnn,
+      Hs.ideclSourceSrc = SourceText.NoSourceText,
+      Hs.ideclImplicit = False,
+      Hs.ideclName = Hs.noLocA moduleName,
+      Hs.ideclPkgQual = Plugin.NoRawPkgQual,
+      Hs.ideclSource = Plugin.NotBoot,
+      Hs.ideclSafe = False,
+      Hs.ideclQualified = Hs.QualifiedPre,
+      Hs.ideclAs = Nothing,
+      Hs.ideclHiding = Nothing
+    }
diff --git a/source/ghc-9.6/Imp/Ghc.hs b/source/ghc-9.6/Imp/Ghc.hs
new file mode 100644
--- /dev/null
+++ b/source/ghc-9.6/Imp/Ghc.hs
@@ -0,0 +1,27 @@
+module Imp.Ghc where
+
+import qualified GHC.Hs as Hs
+import qualified GHC.Plugins as Plugin
+import qualified GHC.Types.SourceText as SourceText
+
+type HsModulePs = Hs.HsModule Hs.GhcPs
+
+newImportDecl ::
+  Plugin.ModuleName ->
+  Hs.ImportDecl Hs.GhcPs
+newImportDecl moduleName =
+  Hs.ImportDecl
+    { Hs.ideclExt =
+        Hs.XImportDeclPass
+          { Hs.ideclAnn = Hs.noAnn,
+            Hs.ideclSourceText = SourceText.NoSourceText,
+            Hs.ideclImplicit = False
+          },
+      Hs.ideclName = Hs.noLocA moduleName,
+      Hs.ideclPkgQual = Plugin.NoRawPkgQual,
+      Hs.ideclSource = Hs.NotBoot,
+      Hs.ideclSafe = False,
+      Hs.ideclQualified = Hs.QualifiedPre,
+      Hs.ideclAs = Nothing,
+      Hs.ideclImportList = Nothing
+    }
diff --git a/source/library/Imp.hs b/source/library/Imp.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE TypeApplications #-}
+
+module Imp where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Data as Data
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified GHC.Hs as Hs
+import qualified GHC.Plugins as Plugin
+import qualified Imp.Exception.ShowHelp as ShowHelp
+import qualified Imp.Exception.ShowVersion as ShowVersion
+import qualified Imp.Extra.Exception as Exception
+import qualified Imp.Extra.HsModule as HsModule
+import qualified Imp.Extra.HsParsedModule as HsParsedModule
+import qualified Imp.Extra.ImportDecl as ImportDecl
+import qualified Imp.Extra.ParsedResult as ParsedResult
+import qualified Imp.Ghc as Ghc
+import qualified Imp.Type.Config as Config
+import qualified Imp.Type.Context as Context
+import qualified Imp.Type.Flag as Flag
+import qualified System.Exit as Exit
+import qualified System.IO as IO
+
+plugin :: Plugin.Plugin
+plugin =
+  Plugin.defaultPlugin
+    { Plugin.parsedResultAction = parsedResultAction,
+      Plugin.pluginRecompile = Plugin.purePlugin
+    }
+
+parsedResultAction ::
+  [Plugin.CommandLineOption] ->
+  modSummary ->
+  Plugin.ParsedResult ->
+  Plugin.Hsc Plugin.ParsedResult
+parsedResultAction commandLineOptions _ =
+  Plugin.liftIO
+    . Exception.handle handleException
+    . ParsedResult.overModule (HsParsedModule.overModule $ imp commandLineOptions)
+
+handleException :: Exception.SomeException -> IO a
+handleException e = do
+  IO.hPutStrLn IO.stderr $ Exception.displayException e
+  Exit.exitWith $ exceptionToExitCode e
+
+exceptionToExitCode :: Exception.SomeException -> Exit.ExitCode
+exceptionToExitCode e
+  | Exception.isType @ShowHelp.ShowHelp e = Exit.ExitSuccess
+  | Exception.isType @ShowVersion.ShowVersion e = Exit.ExitSuccess
+  | otherwise = Exit.ExitFailure 1
+
+imp ::
+  (Exception.MonadThrow m) =>
+  [String] ->
+  Plugin.Located Ghc.HsModulePs ->
+  m (Plugin.Located Ghc.HsModulePs)
+imp arguments lHsModule = do
+  flags <- Flag.fromArguments arguments
+  config <- Config.fromFlags flags
+  context <- Context.fromConfig config
+  let aliases = Context.aliases context
+      moduleNames =
+        Set.fromList @Plugin.ModuleName
+          . biplate
+          . Hs.hsmodDecls
+          $ Plugin.unLoc lHsModule
+  pure $ fmap (HsModule.overImports $ updateImports aliases moduleNames) lHsModule
+
+biplate :: (Data.Data a, Data.Data b) => a -> [b]
+biplate = concat . Data.gmapQ (\d -> maybe (biplate d) pure $ Data.cast d)
+
+updateImports ::
+  Map.Map Plugin.ModuleName Plugin.ModuleName ->
+  Set.Set Plugin.ModuleName ->
+  [Hs.LImportDecl Hs.GhcPs] ->
+  [Hs.LImportDecl Hs.GhcPs]
+updateImports aliases want imports =
+  let have = Set.fromList $ fmap (ImportDecl.toModuleName . Plugin.unLoc) imports
+      need = Set.toList $ Set.difference want have
+   in imports <> fmap (Hs.noLocA . createImport aliases) need
+
+createImport ::
+  Map.Map Plugin.ModuleName Plugin.ModuleName ->
+  Plugin.ModuleName ->
+  Hs.ImportDecl Hs.GhcPs
+createImport aliases target =
+  let source = Map.findWithDefault target target aliases
+   in (Ghc.newImportDecl source)
+        { Hs.ideclAs =
+            if source == target
+              then Nothing
+              else Just $ Hs.noLocA target
+        }
diff --git a/source/library/Imp/Exception/InvalidAlias.hs b/source/library/Imp/Exception/InvalidAlias.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/InvalidAlias.hs
@@ -0,0 +1,13 @@
+module Imp.Exception.InvalidAlias where
+
+import qualified Control.Monad.Catch as Exception
+
+newtype InvalidAlias
+  = InvalidAlias String
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidAlias where
+  displayException (InvalidAlias x) = "invalid alias: " <> show x
+
+new :: String -> InvalidAlias
+new = InvalidAlias
diff --git a/source/library/Imp/Exception/InvalidModuleName.hs b/source/library/Imp/Exception/InvalidModuleName.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/InvalidModuleName.hs
@@ -0,0 +1,13 @@
+module Imp.Exception.InvalidModuleName where
+
+import qualified Control.Monad.Catch as Exception
+
+newtype InvalidModuleName
+  = InvalidModuleName String
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidModuleName where
+  displayException (InvalidModuleName x) = "invalid module name: " <> show x
+
+new :: String -> InvalidModuleName
+new = InvalidModuleName
diff --git a/source/library/Imp/Exception/InvalidOption.hs b/source/library/Imp/Exception/InvalidOption.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/InvalidOption.hs
@@ -0,0 +1,15 @@
+module Imp.Exception.InvalidOption where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Char as Char
+import qualified Data.List as List
+
+newtype InvalidOption
+  = InvalidOption String
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidOption where
+  displayException (InvalidOption x) = "invalid option: " <> x
+
+new :: String -> InvalidOption
+new = InvalidOption . List.dropWhile Char.isSpace
diff --git a/source/library/Imp/Exception/ShowHelp.hs b/source/library/Imp/Exception/ShowHelp.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/ShowHelp.hs
@@ -0,0 +1,15 @@
+module Imp.Exception.ShowHelp where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Imp.Type.Flag as Flag
+import qualified System.Console.GetOpt as GetOpt
+
+data ShowHelp
+  = ShowHelp
+  deriving (Eq, Show)
+
+instance Exception.Exception ShowHelp where
+  displayException = const $ GetOpt.usageInfo "imp" Flag.options
+
+new :: ShowHelp
+new = ShowHelp
diff --git a/source/library/Imp/Exception/ShowVersion.hs b/source/library/Imp/Exception/ShowVersion.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/ShowVersion.hs
@@ -0,0 +1,15 @@
+module Imp.Exception.ShowVersion where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Version as Version
+import qualified Paths_imp as This
+
+data ShowVersion
+  = ShowVersion
+  deriving (Eq, Show)
+
+instance Exception.Exception ShowVersion where
+  displayException = const $ Version.showVersion This.version
+
+new :: ShowVersion
+new = ShowVersion
diff --git a/source/library/Imp/Exception/UnexpectedArgument.hs b/source/library/Imp/Exception/UnexpectedArgument.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/UnexpectedArgument.hs
@@ -0,0 +1,13 @@
+module Imp.Exception.UnexpectedArgument where
+
+import qualified Control.Monad.Catch as Exception
+
+newtype UnexpectedArgument
+  = UnexpectedArgument String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnexpectedArgument where
+  displayException (UnexpectedArgument x) = "unexpected argument: " <> show x
+
+new :: String -> UnexpectedArgument
+new = UnexpectedArgument
diff --git a/source/library/Imp/Exception/UnknownOption.hs b/source/library/Imp/Exception/UnknownOption.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Exception/UnknownOption.hs
@@ -0,0 +1,13 @@
+module Imp.Exception.UnknownOption where
+
+import qualified Control.Monad.Catch as Exception
+
+newtype UnknownOption
+  = UnknownOption String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownOption where
+  displayException (UnknownOption x) = "unknown option: " <> show x
+
+new :: String -> UnknownOption
+new = UnknownOption
diff --git a/source/library/Imp/Extra/Exception.hs b/source/library/Imp/Extra/Exception.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/Exception.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Imp.Extra.Exception where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Maybe as Maybe
+
+isType :: forall e. (Exception.Exception e) => Exception.SomeException -> Bool
+isType = Maybe.isJust . Exception.fromException @e
diff --git a/source/library/Imp/Extra/HsModule.hs b/source/library/Imp/Extra/HsModule.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/HsModule.hs
@@ -0,0 +1,10 @@
+module Imp.Extra.HsModule where
+
+import qualified GHC.Hs as Hs
+import qualified Imp.Ghc as Ghc
+
+overImports ::
+  ([Hs.LImportDecl Hs.GhcPs] -> [Hs.LImportDecl Hs.GhcPs]) ->
+  Ghc.HsModulePs ->
+  Ghc.HsModulePs
+overImports f x = x {Hs.hsmodImports = f $ Hs.hsmodImports x}
diff --git a/source/library/Imp/Extra/HsParsedModule.hs b/source/library/Imp/Extra/HsParsedModule.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/HsParsedModule.hs
@@ -0,0 +1,12 @@
+module Imp.Extra.HsParsedModule where
+
+import qualified GHC.Hs as Hs
+import qualified GHC.Plugins as Plugin
+import qualified Imp.Ghc as Ghc
+
+overModule ::
+  (Functor f) =>
+  (Plugin.Located Ghc.HsModulePs -> f (Plugin.Located Ghc.HsModulePs)) ->
+  Plugin.HsParsedModule ->
+  f Plugin.HsParsedModule
+overModule f x = (\y -> x {Hs.hpm_module = y}) <$> f (Hs.hpm_module x)
diff --git a/source/library/Imp/Extra/ImportDecl.hs b/source/library/Imp/Extra/ImportDecl.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/ImportDecl.hs
@@ -0,0 +1,8 @@
+module Imp.Extra.ImportDecl where
+
+import qualified Data.Maybe as Maybe
+import qualified GHC.Hs as Hs
+import qualified GHC.Plugins as Plugin
+
+toModuleName :: Hs.ImportDecl Hs.GhcPs -> Plugin.ModuleName
+toModuleName x = Plugin.unLoc . Maybe.fromMaybe (Hs.ideclName x) $ Hs.ideclAs x
diff --git a/source/library/Imp/Extra/ModuleName.hs b/source/library/Imp/Extra/ModuleName.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/ModuleName.hs
@@ -0,0 +1,11 @@
+module Imp.Extra.ModuleName where
+
+import qualified Control.Monad.Catch as Exception
+import qualified GHC.Plugins as Plugin
+import qualified Imp.Exception.InvalidModuleName as InvalidModuleName
+import qualified Imp.Extra.ReadP as ReadP
+
+fromString :: (Exception.MonadThrow m) => String -> m Plugin.ModuleName
+fromString x =
+  maybe (Exception.throwM $ InvalidModuleName.new x) pure $
+    ReadP.run Plugin.parseModuleName x
diff --git a/source/library/Imp/Extra/ParsedResult.hs b/source/library/Imp/Extra/ParsedResult.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/ParsedResult.hs
@@ -0,0 +1,10 @@
+module Imp.Extra.ParsedResult where
+
+import qualified GHC.Plugins as Plugin
+
+overModule ::
+  (Functor f) =>
+  (Plugin.HsParsedModule -> f Plugin.HsParsedModule) ->
+  Plugin.ParsedResult ->
+  f Plugin.ParsedResult
+overModule f x = (\y -> x {Plugin.parsedResultModule = y}) <$> f (Plugin.parsedResultModule x)
diff --git a/source/library/Imp/Extra/ReadP.hs b/source/library/Imp/Extra/ReadP.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Extra/ReadP.hs
@@ -0,0 +1,7 @@
+module Imp.Extra.ReadP where
+
+import qualified Data.List as List
+import qualified Text.ParserCombinators.ReadP as ReadP
+
+run :: ReadP.ReadP a -> String -> Maybe a
+run x = fmap fst . List.find (null . snd) . ReadP.readP_to_S x
diff --git a/source/library/Imp/Type/Alias.hs b/source/library/Imp/Type/Alias.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Type/Alias.hs
@@ -0,0 +1,25 @@
+module Imp.Type.Alias where
+
+import qualified Control.Monad as Monad
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Map as Map
+import qualified GHC.Plugins as Plugin
+import qualified Imp.Exception.InvalidAlias as InvalidAlias
+import qualified Imp.Extra.ModuleName as ModuleName
+
+data Alias = Alias
+  { source :: Plugin.ModuleName,
+    target :: Plugin.ModuleName
+  }
+  deriving (Eq, Show)
+
+fromString :: (Exception.MonadThrow m) => String -> m Alias
+fromString string = do
+  let (before, after) = break (== ':') string
+  Monad.when (null after) . Exception.throwM $ InvalidAlias.new string
+  src <- ModuleName.fromString before
+  tgt <- ModuleName.fromString . drop 1 $ after
+  pure Alias {source = src, target = tgt}
+
+toMap :: [Alias] -> Map.Map Plugin.ModuleName Plugin.ModuleName
+toMap = Map.fromListWith (const id) . fmap (\x -> (target x, source x))
diff --git a/source/library/Imp/Type/Config.hs b/source/library/Imp/Type/Config.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Type/Config.hs
@@ -0,0 +1,32 @@
+module Imp.Type.Config where
+
+import qualified Control.Monad as Monad
+import qualified Control.Monad.Catch as Exception
+import qualified Imp.Type.Alias as Alias
+import qualified Imp.Type.Flag as Flag
+
+data Config = Config
+  { aliases :: [Alias.Alias],
+    help :: Bool,
+    version :: Bool
+  }
+  deriving (Eq, Show)
+
+initial :: Config
+initial =
+  Config
+    { aliases = [],
+      help = False,
+      version = False
+    }
+
+fromFlags :: (Exception.MonadThrow m) => [Flag.Flag] -> m Config
+fromFlags = Monad.foldM applyFlag initial
+
+applyFlag :: (Exception.MonadThrow m) => Config -> Flag.Flag -> m Config
+applyFlag config flag = case flag of
+  Flag.Alias string -> do
+    alias <- Alias.fromString string
+    pure config {aliases = alias : aliases config}
+  Flag.Help bool -> pure config {help = bool}
+  Flag.Version bool -> pure config {version = bool}
diff --git a/source/library/Imp/Type/Context.hs b/source/library/Imp/Type/Context.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Type/Context.hs
@@ -0,0 +1,24 @@
+module Imp.Type.Context where
+
+import qualified Control.Monad as Monad
+import qualified Control.Monad.Catch as Exception
+import qualified Data.Map as Map
+import qualified GHC.Plugins as Plugin
+import qualified Imp.Exception.ShowHelp as ShowHelp
+import qualified Imp.Exception.ShowVersion as ShowVersion
+import qualified Imp.Type.Alias as Alias
+import qualified Imp.Type.Config as Config
+
+newtype Context = Context
+  { aliases :: Map.Map Plugin.ModuleName Plugin.ModuleName
+  }
+  deriving (Eq, Show)
+
+fromConfig :: (Exception.MonadThrow m) => Config.Config -> m Context
+fromConfig config = do
+  Monad.when (Config.help config) $ Exception.throwM ShowHelp.new
+  Monad.when (Config.version config) $ Exception.throwM ShowVersion.new
+  pure
+    Context
+      { aliases = Alias.toMap $ Config.aliases config
+      }
diff --git a/source/library/Imp/Type/Flag.hs b/source/library/Imp/Type/Flag.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Imp/Type/Flag.hs
@@ -0,0 +1,52 @@
+module Imp.Type.Flag where
+
+import qualified Control.Monad.Catch as Exception
+import qualified Imp.Exception.InvalidOption as InvalidOption
+import qualified Imp.Exception.UnexpectedArgument as UnexpectedArgument
+import qualified Imp.Exception.UnknownOption as UnknownOption
+import qualified System.Console.GetOpt as GetOpt
+
+data Flag
+  = Alias String
+  | Help Bool
+  | Version Bool
+  deriving (Eq, Show)
+
+options :: [GetOpt.OptDescr Flag]
+options =
+  [ GetOpt.Option
+      ['h', '?']
+      ["help"]
+      (GetOpt.NoArg $ Help True)
+      "Prints this help message then exits.",
+    GetOpt.Option
+      []
+      ["no-help"]
+      (GetOpt.NoArg $ Help False)
+      "",
+    GetOpt.Option
+      ['v']
+      ["version"]
+      (GetOpt.NoArg $ Version True)
+      "Prints the version number then exits.",
+    GetOpt.Option
+      []
+      ["no-version"]
+      (GetOpt.NoArg $ Version False)
+      "",
+    GetOpt.Option
+      []
+      ["alias"]
+      (GetOpt.ReqArg Alias "SOURCE:TARGET")
+      "Adds a new alias, allowing TARGET to be used in place of SOURCE. \
+      \For example `--alias=Data.String:String` allows `String.words` to mean `Data.String.words`. \
+      \Later aliases will overwrite earlier ones."
+  ]
+
+fromArguments :: (Exception.MonadThrow m) => [String] -> m [Flag]
+fromArguments arguments = do
+  let (flgs, args, opts, errs) = GetOpt.getOpt' GetOpt.Permute options arguments
+  mapM_ (Exception.throwM . UnexpectedArgument.new) args
+  mapM_ (Exception.throwM . UnknownOption.new) opts
+  mapM_ (Exception.throwM . InvalidOption.new) errs
+  pure flgs
diff --git a/source/test-suite/Main.hs b/source/test-suite/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/test-suite/Main.hs
@@ -0,0 +1,114 @@
+{-# LANGUAGE CPP #-}
+
+import qualified Control.Monad.Catch as Exception
+import qualified GHC.Data.EnumSet as EnumSet
+import qualified GHC.Data.FastString as FastString
+import qualified GHC.Data.StringBuffer as StringBuffer
+import qualified GHC.Parser as Parser
+import qualified GHC.Parser.Lexer as Lexer
+import qualified GHC.Stack as Stack
+import qualified GHC.Types.SrcLoc as SrcLoc
+import qualified GHC.Utils.Error as Error
+import qualified GHC.Utils.Outputable as Outputable
+import qualified Imp
+import qualified Imp.Ghc as Ghc
+import qualified Test.Hspec as Hspec
+
+main :: IO ()
+main = Hspec.hspec . Hspec.parallel . Hspec.describe "Imp" $ do
+  Hspec.it "does nothing with an empty module" $ do
+    expectImp
+      []
+      ""
+      ""
+
+  Hspec.it "does nothing when nothing needs to be imported" $ do
+    expectImp
+      []
+      "true = True"
+      "true = True"
+
+  Hspec.it "inserts an import for a qualified value" $ do
+    expectImp
+      []
+      "true = Data.Bool.True"
+      "import qualified Data.Bool\ntrue = Data.Bool.True"
+
+  Hspec.it "inserts an aliased import" $ do
+    expectImp
+      ["--alias=Data.Bool:Bool"]
+      "true = Bool.True"
+      "import qualified Data.Bool as Bool\ntrue = Bool.True"
+
+  Hspec.it "prefers later aliases over earlier ones" $ do
+    expectImp
+      ["--alias=Relude.Bool:Bool", "--alias=Data.Bool:Bool"]
+      "true = Bool.True"
+      "import qualified Data.Bool as Bool\ntrue = Bool.True"
+
+  Hspec.it "inserts an import for a qualified type" $ do
+    expectImp
+      []
+      "true = True :: Data.Bool.Bool"
+      "import qualified Data.Bool\ntrue = True :: Data.Bool.Bool"
+
+  Hspec.it "inserts multiple imports sorted" $ do
+    expectImp
+      []
+      "true :: Relude.Bool.Bool\ntrue = Data.Bool.True"
+      "import qualified Data.Bool\nimport qualified Relude.Bool\ntrue :: Relude.Bool.Bool\ntrue = Data.Bool.True"
+
+  Hspec.it "does not re-import an open import" $ do
+    expectImp
+      []
+      "import Data.Bool\ntrue = Data.Bool.True"
+      "import Data.Bool\ntrue = Data.Bool.True"
+
+  Hspec.it "does not re-import a qualified import" $ do
+    expectImp
+      []
+      "import qualified Data.Bool\ntrue = Data.Bool.True"
+      "import qualified Data.Bool\ntrue = Data.Bool.True"
+
+  Hspec.it "does not re-import an aliased import" $ do
+    expectImp
+      []
+      "import qualified Data.Bool as Bool\ntrue = Bool.True"
+      "import qualified Data.Bool as Bool\ntrue = Bool.True"
+
+  Hspec.it "inserts imports after existing ones" $ do
+    expectImp
+      []
+      "import qualified Relude.Bool\ntrue :: Relude.Bool.Bool\ntrue = Data.Bool.True"
+      "import qualified Relude.Bool\nimport qualified Data.Bool\ntrue :: Relude.Bool.Bool\ntrue = Data.Bool.True"
+
+expectImp :: (Stack.HasCallStack) => [String] -> String -> String -> Hspec.Expectation
+expectImp arguments input expected = do
+  before <- parseModule input
+  after <- Imp.imp arguments before
+  let actual = Outputable.showPprUnsafe after
+  actual `Hspec.shouldBe` expected
+
+parseModule :: (Exception.MonadThrow m) => String -> m (SrcLoc.Located Ghc.HsModulePs)
+parseModule input = do
+  let parserOpts = Lexer.mkParserOpts EnumSet.empty emptyDiagOpts [] False False False False
+      stringBuffer = StringBuffer.stringToStringBuffer input
+      realSrcLoc = SrcLoc.mkRealSrcLoc (FastString.mkFastString "<interactive>") 1 1
+      pState = Lexer.initParserState parserOpts stringBuffer realSrcLoc
+      parseResult = Lexer.unP Parser.parseModule pState
+  case parseResult of
+    Lexer.PFailed _ -> Exception.throwM $ InvalidInput input
+    Lexer.POk _ lHsModule -> pure lHsModule
+
+emptyDiagOpts :: Error.DiagOpts
+#if MIN_VERSION_ghc(9, 8, 1)
+emptyDiagOpts = Error.emptyDiagOpts
+#else
+emptyDiagOpts = Error.DiagOpts EnumSet.empty EnumSet.empty False False Nothing Outputable.defaultSDocContext
+#endif
+
+newtype InvalidInput
+  = InvalidInput String
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidInput
