diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,26 @@
+## 0.8.0 (2023-07-16)
+
+- Support for base >= 4.15 / GHC 9.
+- Dropped support for GHC versions that have no corresponding HLS version.
+
+## 0.7.0 (2019-09-12)
+
+- New `%Q` and `%q` format specifiers accept strict and lazy Text as input
+  respectively. Otherwise they function identically to the `%s` specifier.
+- th-printf can now produce lazy Text as well as String, and the improved
+  internal representation of format strings should slightly increase performance.
+  - Directly producing Text should now be significantly faster than using the
+    string formatter and `pack`ing the result, especially with Text format arguments.
+- Dropped support for GHC < 8.
+
+## 0.6.0 (2018-08-18)
+
+Backported new backpack-based code to pre GHC-8.4 versions.
+
+- Rename of public modules
+- Parser rewrite
+- th-printf now prints a warning when given an erroneous format string
+- Several printf behaviors have been updated to comply with spec:
+  - `x`, `u`, etc. specifiers now only apply to positive integers
+  - Length specifiers are allowed
+- Generated testsuite covers more cases
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,20 @@
-The MIT License (MIT)
-
-Copyright (c) 2013 Joel Taylor
+Copyright (c) 2018 Jude Taylor
 
-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:
+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 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.
+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/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/parser/Parser.hs b/parser/Parser.hs
new file mode 100644
--- /dev/null
+++ b/parser/Parser.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+
+module Parser where
+
+import Control.Applicative hiding ((<|>))
+import Control.Monad (when)
+import Control.Monad.Fix
+import Control.Monad.RWS
+import Data.Char
+import Data.CharSet hiding (map)
+import Data.Maybe
+import qualified Data.Set as S
+import Lens.Micro.Platform
+import Parser.Types
+import Text.Parsec hiding (many)
+import Text.ParserCombinators.ReadP (readP_to_S)
+import Text.Read.Lex (lexChar)
+
+type Warning = String
+
+parseStr :: String -> Either ParseError ([Atom], [[Warning]])
+parseStr = fmap (unzip . map normalizeAndWarn) . parse printfStr "" . lexChars
+ where
+  lexChars x = (`fix` x) $ \f s ->
+    if Prelude.null s
+      then []
+      else case readP_to_S lexChar s of
+        ((c, rest) : _) -> c : f rest
+        [] -> error "malformed input"
+
+normalizeAndWarn :: Atom -> (Atom, [Warning])
+normalizeAndWarn s@Str{} = (s, [])
+normalizeAndWarn (Arg f) = (Arg a, b)
+ where
+  (_, a, b) = runRWS (warnLength f >> go (spec f)) () f
+  go c | c `elem` "aAeEfFgGxXo" = return ()
+  go c | c `elem` "csqQ?" = warnSign >> warnPrefix >> warnZero >> warnSpace
+  go c | c `elem` "diu" = warnPrefix
+  go 'p' = warnSign >> warnPrefix >> warnZero
+  go _ = undefined
+  warnFlag ::
+    (Eq a, MonadWriter [String] m, MonadState FormatArg m) =>
+    Lens' FlagSet a ->
+    a ->
+    a ->
+    Char ->
+    m ()
+  warnFlag lens' bad good flagName = do
+    oldVal <- use (flags_ . lens')
+    when (oldVal == bad) $ do
+      c <- use spec_
+      flags_ . lens' .= good
+      tell
+        [ "`"
+            ++ [flagName]
+            ++ "` flag has no effect on `"
+            ++ [c]
+            ++ "` specifier"
+        ]
+  warnSign = warnFlag signed_ True False '+'
+  warnPrefix = warnFlag prefixed_ True False '#'
+  warnSpace = warnFlag spaced_ True False ' '
+  warnZero = warnFlag adjustment_ (Just ZeroPadded) Nothing '0'
+  phonyLengthSpec =
+    S.fromList $
+      [(x, y) | x <- "diuoxX", y <- ["L"]]
+        ++ [ (x, y)
+           | x <- "fFeEgGaA"
+           , y <- ["hh", "h", "l", "ll", "j", "z", "t"]
+           ]
+        ++ [(x, y) | x <- "csqQ", y <- ["hh", "h", "ll", "j", "z", "t", "L"]]
+        ++ map ('p',) ["hh", "h", "l", "ll", "j", "z", "t", "L"]
+  warnLength FormatArg{spec, lengthSpec = Just l}
+    | (spec, show l) `S.member` phonyLengthSpec =
+        tell
+          [ "`"
+              ++ show l
+              ++ "` length modifier has no effect when combined with `"
+              ++ [spec]
+              ++ "` specifier"
+          ]
+  warnLength _ = return ()
+
+flagSet :: CharSet
+flagSet = fromList "-+ #0"
+
+specSet :: CharSet
+specSet = fromList "diuoxXfFeEaAgGpcsQq?"
+
+lengthSpecifiers :: [(String, LengthSpecifier)]
+lengthSpecifiers =
+  [ ("hh", HH)
+  , ("h", H)
+  , ("ll", LL)
+  , ("l", L)
+  , ("j", J)
+  , ("z", Z)
+  , ("t", T)
+  , ("L", BigL)
+  ]
+
+oneOfSet :: Stream s m Char => CharSet -> ParsecT s u m Char
+oneOfSet s = satisfy (`member` s)
+
+printfStr :: Stream s m Char => ParsecT s u m [Atom]
+printfStr =
+  many $
+    Str "%" <$ try (string "%%")
+      <|> Arg <$> fmtArg
+      <|> Str <$> some (satisfy (/= '%'))
+
+fmtArg :: Stream s m Char => ParsecT s u m FormatArg
+fmtArg = do
+  _ <- char '%'
+  flags <- do
+    fs <- many $ do
+      c <- oneOfSet flagSet <?> "flag"
+      pure $ case c of
+        '-' -> FlagLJust
+        '+' -> FlagSigned
+        ' ' -> FlagSpaced
+        '#' -> FlagPrefixed
+        '0' -> FlagZeroPadded
+        _ -> error "unreachable"
+    let flagSet' = S.fromList fs
+    if S.size flagSet' < length fs
+      then fail "Duplicate flags specified"
+      else pure $ toFlagSet flagSet'
+  width <- numArg <?> "width"
+  precision <- optionMaybe (char '.' *> numArg) <?> "precision"
+  lengthSpec <-
+    optionMaybe $ choice $ Prelude.map (\(a, b) -> b <$ string a) lengthSpecifiers
+  spec <- oneOfSet specSet <?> "valid specifier"
+  pure $ FormatArg flags width (fromMaybe (Given 0) <$> precision) spec lengthSpec
+ where
+  nat = do
+    c <- many1 $ satisfy isDigit
+    return (read c :: Integer)
+  numArg = optionMaybe (Given <$> nat <|> Need <$ char '*')
diff --git a/parser/Parser/Types.hs b/parser/Parser/Types.hs
new file mode 100644
--- /dev/null
+++ b/parser/Parser/Types.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Parser.Types (
+  Atom (..),
+  FormatArg (..),
+  Flag (..),
+  adjustmentFlags,
+  Adjustment (..),
+  FormatStr,
+  MaySpecify (..),
+  emptyFlagSet,
+  toFlagSet,
+  FlagSet (..),
+  LengthSpecifier (..),
+  flags_,
+  spec_,
+  signed_,
+  prefixed_,
+  spaced_,
+  adjustment_,
+) where
+
+import Data.Foldable (
+  elem,
+  notElem,
+ )
+import Data.Set (Set)
+import qualified Data.Set as S
+import Language.Haskell.TH.Lift
+import Lens.Micro.Platform
+import Prelude hiding (
+  elem,
+  notElem,
+ )
+
+data Atom
+  = Arg FormatArg
+  | Str String
+  deriving (Show)
+
+data LengthSpecifier
+  = HH
+  | H
+  | BigL
+  | LL
+  | L
+  | J
+  | Z
+  | T
+  deriving (Eq)
+
+instance Show LengthSpecifier where
+  show HH = "hh"
+  show H = "h"
+  show BigL = "L"
+  show LL = "ll"
+  show L = "l"
+  show J = "j"
+  show Z = "z"
+  show T = "t"
+
+data Flag
+  = FlagLJust
+  | FlagSigned
+  | FlagSpaced
+  | FlagPrefixed
+  | FlagZeroPadded
+  deriving (Show, Eq, Ord)
+
+adjustmentFlags :: Set Flag
+adjustmentFlags = S.fromList [FlagLJust, FlagZeroPadded]
+
+data Adjustment
+  = LeftJustified
+  | ZeroPadded
+  deriving (Show, Eq)
+
+data MaySpecify
+  = Given Integer
+  | Need
+  deriving (Show)
+
+data FormatArg = FormatArg
+  { flags :: FlagSet
+  , width :: Maybe MaySpecify
+  , precision :: Maybe MaySpecify
+  , spec :: Char
+  , lengthSpec :: Maybe LengthSpecifier
+  }
+  deriving (Show)
+
+type FormatStr = [Atom]
+
+data FlagSet = FlagSet
+  { adjustment :: Maybe Adjustment
+  , signed :: Bool
+  , spaced :: Bool
+  , prefixed :: Bool
+  }
+  deriving (Show)
+
+emptyFlagSet :: FlagSet
+emptyFlagSet = FlagSet Nothing False False False
+
+toFlagSet :: Set Flag -> FlagSet
+toFlagSet fs = set'
+ where
+  adjustment
+    | FlagLJust `S.member` fs = Just LeftJustified
+    | FlagZeroPadded `S.member` fs = Just ZeroPadded
+    | otherwise = Nothing
+  set' =
+    FlagSet
+      { signed = FlagSigned `elem` fs
+      , prefixed = FlagPrefixed `elem` fs
+      , adjustment
+      , spaced = FlagSpaced `elem` fs && FlagSigned `notElem` fs
+      }
+
+makeLensesFor
+  [ ("adjustment", "adjustment_")
+  , ("signed", "signed_")
+  , ("spaced", "spaced_")
+  , ("prefixed", "prefixed_")
+  ]
+  ''FlagSet
+
+makeLensesFor
+  [ ("flags", "flags_")
+  , ("spec", "spec_")
+  ]
+  ''FormatArg
+
+deriveLiftMany [''Adjustment, ''FlagSet, ''LengthSpecifier]
diff --git a/src/Buf.hs b/src/Buf.hs
new file mode 100644
--- /dev/null
+++ b/src/Buf.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Buf (Buf (..), SizedStr, SizedBuilder) where
+
+import Data.Char (intToDigit)
+import qualified Data.DList as D
+import Data.Kind (Type)
+import Data.String
+import qualified Data.Text as S
+import Data.Text.Lazy (Text)
+import qualified Data.Text.Lazy as L
+import qualified Data.Text.Lazy.Builder as T
+import qualified Data.Text.Lazy.Builder.Int as T
+
+newtype Sized a = Sized {unSized :: (a, Int)} deriving (Show, Ord, Eq)
+
+type SizedStr = Sized (D.DList Char)
+type SizedBuilder = Sized T.Builder
+
+instance (IsString a) => IsString (Sized a) where
+  fromString s = Sized (fromString s, length s)
+
+instance (Semigroup a) => Semigroup (Sized a) where
+  Sized (a, b) <> Sized (c, d) = Sized (a <> c, b + d)
+  {-# INLINE (<>) #-}
+
+instance (Monoid a) => Monoid (Sized a) where
+  mempty = Sized (mempty, 0)
+  mappend = (<>)
+  {-# INLINE mappend #-}
+
+class (Monoid a) => Buf a where
+  type Output a :: Type
+
+  str :: String -> a
+
+  sText :: S.Text -> a
+  sText = str . S.unpack
+  lText :: L.Text -> a
+  lText = str . L.unpack
+
+  singleton :: Char -> a
+  digit :: Int -> a
+  digit = singleton . intToDigit
+  {-# INLINE digit #-}
+
+  cons :: Char -> a -> a
+  cons c s = singleton c <> s
+  {-# INLINE cons #-}
+
+  repeatN :: Int -> Char -> a
+  repeatN n = str . replicate n
+
+  size :: a -> Int
+
+  finalize :: a -> Output a
+
+instance Buf SizedStr where
+  type Output SizedStr = String
+  str a = Sized (D.fromList a, length a)
+  singleton c = Sized (D.singleton c, 1)
+  finalize = D.toList . fst . unSized
+  cons c (Sized (r, m)) = Sized (D.cons c r, m + 1)
+  repeatN n c = Sized (D.replicate n c, n)
+  size = snd . unSized
+
+instance Buf SizedBuilder where
+  type Output SizedBuilder = Text
+  str a = Sized (fromString a, length a)
+  sText a = Sized (T.fromText a, S.length a)
+  lText a = Sized (T.fromLazyText a, fromIntegral (L.length a))
+  singleton c = Sized (T.singleton c, 1)
+  digit c = Sized (T.hexadecimal c, 1)
+  finalize = T.toLazyText . fst . unSized
+  size = snd . unSized
diff --git a/src/Language/Haskell/Printf.hs b/src/Language/Haskell/Printf.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Printf.hs
@@ -0,0 +1,110 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+{- | "Text.Printf" is a useful module, but due to the typeclass hacks it uses, it can
+be hard to tell if the format string you wrote is well-formed or not.
+This package provides a mechanism to create formatting functions at compile time.
+
+Note that, to maintain consistency with other printf implementations, negative ints
+that are printed as unsigned will \"underflow\". (Text.Printf does this too.)
+
+>>> [s|%u|] (-1 :: Int32)
+WAS "4294967295"
+NOW Not in scope: type constructor or class `Int32'
+
+Thus, any time you want to print a number using the unsigned, octal, or hex specifiers,
+your input must be an instance of "Bounded".
+-}
+module Language.Haskell.Printf (
+  s,
+  t,
+  p,
+  hp,
+) where
+
+import Control.Monad.IO.Class
+import Language.Haskell.Printf.Lib
+import Language.Haskell.TH.Lib
+import Language.Haskell.TH.Quote
+import Language.Haskell.TH.Syntax
+import System.IO (hPutStr)
+
+{- | @
+['s'|Hello, %s! (%d people greeted)|] :: ... -> 'String'
+@
+
+This formatter follows the guidelines listed
+<http://www.cplusplus.com/reference/cstdio/printf/ here>, except for
+@%n@ (store number of printed characters) for obvious
+reasons.
+
+@
+%c     :: 'Char'
+%s     :: 'String'
+%q     :: 'Data.Text.Lazy.Text' -- lazy text
+%Q     :: 'Data.Text.Text' -- strict text
+
+-- datatypes with Show instances
+%?     :: 'Show' a => a
+
+-- signed integer types
+%d, %i :: 'Integral' i => i
+
+-- unsigned integer types
+%u     :: ('Bounded' i, 'Integral' i) => i
+%o     :: ('Bounded' i, 'Integral' i) => i
+%x, %X :: ('Bounded' i, 'Integral' i) => i
+
+-- floats
+%a, %A :: 'RealFloat' f => f
+%e, %E :: 'RealFloat' f => f
+%f, %F :: 'RealFloat' f => f
+%g, %G :: 'RealFloat' f => f
+
+%p     :: 'Foreign.Ptr.Ptr' a
+@
+-}
+s :: QuasiQuoter
+s = quoter $ \s' -> do
+  (lhss, rhs) <- toSplices s' OutputString
+  return $ LamE lhss rhs
+
+-- | Behaves identically to 's', but produces lazy 'Data.Text.Lazy.Text'.
+t :: QuasiQuoter
+t = quoter $ \s' -> do
+  (lhss, rhs) <- toSplices s' OutputText
+  return $ LamE lhss rhs
+
+{- | Like 's', but prints the resulting string to @stdout@.
+
+@
+[p|Hello, %s! (%d people greeted)|] :: 'MonadIO' m => ... -> m ()
+@
+-}
+p :: QuasiQuoter
+p = quoter $ \s' -> do
+  (lhss, rhs) <- toSplices s' OutputString
+  lamE (map pure lhss) [|liftIO (putStr $(pure rhs))|]
+
+{- | Like 'p', but takes as its first argument the 'System.IO.Handle' to print to.
+
+@
+[hp|Hello, %s! (%d people greeted)|] :: 'MonadIO' m => 'System.IO.Handle' -> ... -> m ()
+@
+-}
+hp :: QuasiQuoter
+hp = quoter $ \s' -> do
+  (lhss, rhs) <- toSplices s' OutputString
+  h <- newName "h"
+  lamE (varP h : map pure lhss) [|liftIO (hPutStr $(varE h) $(pure rhs))|]
+
+quoter :: (String -> ExpQ) -> QuasiQuoter
+quoter e =
+  QuasiQuoter
+    { quoteExp = e
+    , quotePat = error "this quoter cannot be used in a pattern context"
+    , quoteType = error "this quoter cannot be used in a type context"
+    , quoteDec = error "this quoter cannot be used in a declaration context"
+    }
diff --git a/src/Language/Haskell/Printf/Geometry.hs b/src/Language/Haskell/Printf/Geometry.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Printf/Geometry.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Language.Haskell.Printf.Geometry (
+  sign',
+  padDecimal,
+  prefix,
+  fromPrintfArg,
+  formatOne,
+  Value (..),
+) where
+
+import Control.Monad
+import Data.Maybe
+import Language.Haskell.PrintfArg
+import Parser.Types (Adjustment (..))
+
+import Buf
+import StrUtils
+
+data Value buf = Value
+  { valArg :: PrintfArg buf
+  , valPrefix :: Maybe buf
+  , valSign :: Maybe buf
+  }
+  deriving (Show)
+
+sign' :: (Num n, Ord n, Buf buf) => PrintfArg n -> Maybe buf
+sign' pf
+  | value pf < 0 = Just (singleton '-')
+  | spaced pf = Just (singleton ' ')
+  | signed pf = Just (singleton '+')
+  | otherwise = Nothing
+
+padDecimal :: (Buf buf, Eq v, Num v) => PrintfArg v -> buf -> buf
+padDecimal spec
+  | prec spec == Just 0 && value spec == 0 = const mempty
+  | otherwise = maybe id (`justifyRight` '0') (prec spec)
+
+prefix :: (Num n, Eq n, Buf buf) => buf -> PrintfArg n -> Maybe buf
+prefix s pf = guard (prefixed pf && value pf /= 0) >> Just s
+
+fromPrintfArg ::
+  (Buf buf) =>
+  (n -> buf) ->
+  (PrintfArg n -> Maybe buf) ->
+  (PrintfArg n -> Maybe buf) ->
+  PrintfArg n ->
+  Value buf
+fromPrintfArg f b c a = Value (f <$> a) (b a) (c a)
+
+formatOne :: (Buf buf) => Value buf -> buf
+formatOne Value{..}
+  | Nothing <- width valArg = prefix' <> text
+  | Just w <- width valArg = case adjustment valArg of
+    Just ZeroPadded
+      | isn'tDecimal || isNothing (prec valArg) ->
+        prefix' <> justifyRight (w - size prefix') '0' text
+    Just LeftJustified -> justifyLeft w ' ' (prefix' <> text)
+    _ -> justify' w (prefix' <> text)
+ where
+  isn'tDecimal = fieldSpec valArg `notElem` ("diouxX" :: String)
+  justify' n
+    | n < 0 = justifyLeft (abs n) ' '
+    | otherwise = justifyRight n ' '
+  prefix' = fromMaybe mempty valSign <> fromMaybe mempty valPrefix
+  text = value valArg
diff --git a/src/Language/Haskell/Printf/Lib.hs b/src/Language/Haskell/Printf/Lib.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Printf/Lib.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Language.Haskell.Printf.Lib (
+  toSplices,
+  OutputType (..),
+) where
+
+import Data.Maybe
+import Data.String (fromString)
+import GHC.Generics (Generic)
+import Language.Haskell.Printf.Geometry (
+  formatOne,
+ )
+import qualified Language.Haskell.Printf.Printers as Printers
+import Language.Haskell.PrintfArg
+import Language.Haskell.TH
+import Language.Haskell.TH.Syntax
+
+import Buf (
+  SizedBuilder,
+  SizedStr,
+  finalize,
+ )
+import Control.Monad (mapAndUnzipM)
+import Parser (parseStr)
+import Parser.Types hiding (
+  lengthSpec,
+  width,
+ )
+
+data OutputType = OutputString | OutputText
+  deriving (Show, Eq, Ord, Generic, Enum, Bounded)
+
+{- | Takes a format string as input and produces a tuple @(args, outputExpr)@.
+
+This function processes character escapes as they would appear in Haskell source code.
+It will emit warnings (or throw an error, as appropriate) when given an invalid format
+string.
+
+Use if you wish to leverage @th-printf@ in conjunction with, for example, an existing
+logging library.
+-}
+toSplices :: String -> OutputType -> Q ([Pat], Exp)
+toSplices s' ot = case parseStr s' of
+  Left x -> fail $ show x
+  Right (y, warns) -> do
+    mapM_ (qReport False) (concat warns)
+    (lhss, rhss) <- mapAndUnzipM extractExpr y
+    rhss' <-
+      appE
+        [|finalize|]
+        (sigE (foldr1 (\x y' -> infixApp x [|(<>)|] y') rhss) otype)
+    return (map VarP $ concat lhss, rhss')
+ where
+  otype = case ot of
+    OutputString -> [t|SizedStr|]
+    OutputText -> [t|SizedBuilder|]
+
+extractExpr :: Atom -> Q ([Name], ExpQ)
+extractExpr (Str s') = return ([], [|fromString $(stringE s')|])
+extractExpr (Arg (FormatArg flags' width' precision' spec' lengthSpec')) = do
+  (warg, wexp) <- extractArgs width'
+  (parg, pexp) <- extractArgs precision'
+  varg <- newName "arg"
+  return
+    ( catMaybes [warg, parg, Just varg]
+    , appE
+        [|formatOne|]
+        ( appE
+            formatter
+            [|
+              PrintfArg
+                { flagSet = $(lift flags')
+                , width = $(wexp)
+                , prec = $(pexp)
+                , value = $(varE varg)
+                , lengthSpec = $(lift lengthSpec')
+                , fieldSpec = $(lift spec')
+                }
+              |]
+        )
+    )
+ where
+  extractArgs n = case n of
+    Just Need -> do
+      a <- newName "arg"
+      pure (Just a, [|Just (fromInteger (fromIntegral $(varE a)))|])
+    Just (Given n') -> pure (Nothing, [|Just $(litE $ integerL n')|])
+    Nothing -> pure (Nothing, [|Nothing|])
+  formatter = case spec' of
+    's' -> [|Printers.printfString|]
+    'q' -> [|Printers.printfLazyText|]
+    'Q' -> [|Printers.printfStrictText|]
+    '?' -> [|Printers.printfShow|]
+    'd' -> [|Printers.printfDecimal|]
+    'i' -> [|Printers.printfDecimal|]
+    'p' -> [|Printers.printfPtr|]
+    'c' -> [|Printers.printfChar|]
+    'u' -> [|Printers.printfUnsigned|]
+    'x' -> [|Printers.printfHex False|]
+    'X' -> [|Printers.printfHex True|]
+    'o' -> [|Printers.printfOctal|]
+    'f' -> [|Printers.printfFloating False|]
+    'F' -> [|Printers.printfFloating True|]
+    'e' -> [|Printers.printfScientific False|]
+    'E' -> [|Printers.printfScientific True|]
+    'g' -> [|Printers.printfGeneric False|]
+    'G' -> [|Printers.printfGeneric True|]
+    'a' -> [|Printers.printfFloatHex False|]
+    'A' -> [|Printers.printfFloatHex True|]
+    _ -> undefined
diff --git a/src/Language/Haskell/Printf/Printers.hs b/src/Language/Haskell/Printf/Printers.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/Printf/Printers.hs
@@ -0,0 +1,200 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+
+module Language.Haskell.Printf.Printers where
+
+import Data.Char
+import Data.Maybe (fromMaybe)
+import Data.String (IsString, fromString)
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+import Foreign.Ptr
+import GHC.Float (FFFormat (..))
+import Language.Haskell.Printf.Geometry
+import Language.Haskell.PrintfArg
+import Math.NumberTheory.Logarithms
+
+import Buf
+import NumUtils
+import qualified Parser.Types as P
+
+type Printer n buf = PrintfArg n -> Value buf
+
+printfString :: (Buf buf) => Printer String buf
+printfString spec =
+  Value
+    { valArg = case prec spec of
+        Nothing -> str <$> spec
+        Just c -> str . take c <$> spec
+    , valPrefix = Nothing
+    , valSign = Nothing
+    }
+
+printfStrictText :: (Buf buf) => Printer S.Text buf
+printfStrictText spec =
+  Value
+    { valArg = case prec spec of
+        Nothing -> sText <$> spec
+        Just c -> sText . S.take c <$> spec
+    , valPrefix = Nothing
+    , valSign = Nothing
+    }
+
+printfLazyText :: (Buf buf) => Printer L.Text buf
+printfLazyText spec =
+  Value
+    { valArg = case prec spec of
+        Nothing -> lText <$> spec
+        Just c -> lText . L.take (fromIntegral c) <$> spec
+    , valPrefix = Nothing
+    , valSign = Nothing
+    }
+
+printfShow :: (Buf buf, Show a) => Printer a buf
+printfShow spec = printfString (fromString . show <$> spec)
+
+printfChar :: (Buf buf) => Printer Char buf
+printfChar spec =
+  Value
+    { valArg = singleton <$> spec
+    , valPrefix = Nothing
+    , valSign = Nothing
+    }
+
+{-# ANN printfPtr ("HLint: ignore Use showHex" :: String) #-}
+printfPtr :: (Buf buf) => Printer (Ptr a) buf
+printfPtr spec =
+  Value
+    { valArg =
+        PrintfArg
+          { width = width spec
+          , prec = Nothing
+          , flagSet = P.emptyFlagSet{P.prefixed = True}
+          , lengthSpec = Nothing
+          , fieldSpec = 'p'
+          , value = showIntAtBase 16 intToDigit (ptrToWordPtr $ value spec)
+          }
+    , valPrefix = Just (str "0x")
+    , valSign = Nothing
+    }
+
+printfDecimal :: (Buf buf, Show n, Integral n) => PrintfArg n -> Value buf
+printfDecimal spec =
+  Value
+    { valArg = padDecimal spec . showIntAtBase 10 intToDigit . abs <$> spec
+    , valPrefix = Nothing
+    , valSign = sign' spec
+    }
+
+fmtUnsigned ::
+  (Bounded a, Integral a, Buf buf) =>
+  (Integer -> buf) ->
+  (PrintfArg a -> Maybe buf) ->
+  Printer a buf
+fmtUnsigned shower p spec =
+  Value
+    { valArg = padDecimal spec . shower . clampUnsigned <$> spec
+    , valPrefix = p spec
+    , valSign = Nothing
+    }
+
+printfHex :: (Bounded a, Integral a, Buf buf, IsString buf) => Bool -> Printer a buf
+printfHex b = fmtUnsigned showHex (prefix (if b then "0X" else "0x"))
+ where
+  showHex = showIntAtBase 16 ((if b then toUpper else id) . intToDigit)
+
+printfUnsigned :: (Bounded a, Integral a, Buf buf) => Printer a buf
+printfUnsigned = fmtUnsigned (showIntAtBase 10 intToDigit) (const Nothing)
+
+-- printing octal is really annoying.  consider
+--
+-- printf "%#-8.5x" 1234
+--
+-- "0x004d2 "
+--  ^~~~~~~^ width (8)
+--    ^~~~^  precision (5)
+--  ^^       prefix (2)
+--    ^^     padding (2)
+--
+-- printf "%#-8.5o" 1234
+--
+-- "02322   "
+--  ^~~~~~~^ width (8)
+--  ^~~~^    precision (5)
+--  ^        prefix (1)
+--  ^        padding (1, same character)
+--
+-- in octal, when combining prefix and padding, the prefix
+-- must eat the first padding char
+{-# ANN printfOctal ("HLint: ignore Use showOct" :: String) #-}
+printfOctal :: (Buf buf, IsString buf, Bounded n, Integral n) => PrintfArg n -> Value buf
+printfOctal spec =
+  fmtUnsigned
+    (showIntAtBase 8 intToDigit)
+    (\y -> if shouldUnpad then Nothing else prefix "0" y)
+    spec
+ where
+  expectedWidth = integerLogBase 8 (max 1 $ clampUnsigned $ value spec) + 1
+  shouldUnpad = prefixed spec && fromMaybe 0 (prec spec) > expectedWidth
+
+printfFloating :: (Buf buf, RealFloat n) => Bool -> PrintfArg n -> Value buf
+printfFloating upperFlag spec =
+  Value
+    { valArg = showFloat . abs <$> spec
+    , valPrefix = Nothing
+    , valSign = sign' spec
+    }
+ where
+  precision = case prec spec of
+    Just n -> Just (fromIntegral n)
+    Nothing | Just P.ZeroPadded <- adjustment spec -> Just 6
+    _ -> Nothing
+  showFloat = formatRealFloatAlt FFFixed precision (prefixed spec) upperFlag
+
+printfScientific :: (Buf buf, RealFloat n) => Bool -> PrintfArg n -> Value buf
+printfScientific upperFlag spec =
+  Value
+    { valArg = showSci . abs <$> spec
+    , valPrefix = Nothing
+    , valSign = sign' spec
+    }
+ where
+  showSci =
+    formatRealFloatAlt
+      FFExponent
+      (fromIntegral <$> prec spec)
+      (prefixed spec)
+      upperFlag
+
+printfGeneric :: (Buf buf, RealFloat n) => Bool -> PrintfArg n -> Value buf
+printfGeneric upperFlag spec =
+  Value
+    { valArg = showSci . abs <$> spec
+    , valPrefix = Nothing
+    , valSign = sign' spec
+    }
+ where
+  showSci =
+    formatRealFloatAlt
+      FFGeneric
+      (fromIntegral <$> prec spec)
+      (prefixed spec)
+      upperFlag
+
+printfFloatHex :: (Buf buf, RealFloat n, IsString buf) => Bool -> PrintfArg n -> Value buf
+printfFloatHex upperFlag spec =
+  Value
+    { valArg = showHexFloat . abs <$> spec
+    , valPrefix = Just (if upperFlag then "0X" else "0x")
+    , valSign = sign' spec
+    }
+ where
+  showHexFloat =
+    formatHexFloat (fromIntegral <$> prec spec) (prefixed spec) upperFlag
+
+clampUnsigned :: (Bounded a, Integral a) => a -> Integer
+clampUnsigned x
+  | x < 0 = toInteger x + (-2 * toInteger (minBound `asTypeOf` x))
+  | otherwise = toInteger x
diff --git a/src/Language/Haskell/PrintfArg.hs b/src/Language/Haskell/PrintfArg.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/PrintfArg.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE DeriveFunctor #-}
+
+module Language.Haskell.PrintfArg (
+  PrintfArg (..),
+  adjustment,
+  signed,
+  spaced,
+  prefixed,
+) where
+
+import qualified Parser.Types as P
+
+data PrintfArg v = PrintfArg
+  { flagSet :: P.FlagSet
+  , width :: Maybe Int
+  , prec :: Maybe Int
+  , lengthSpec :: Maybe P.LengthSpecifier
+  , fieldSpec :: Char
+  , value :: v
+  }
+  deriving (Show, Functor)
+
+adjustment :: PrintfArg v -> Maybe P.Adjustment
+adjustment = P.adjustment . flagSet
+
+signed, spaced, prefixed :: PrintfArg v -> Bool
+signed = P.signed . flagSet
+spaced = P.spaced . flagSet
+prefixed = P.prefixed . flagSet
diff --git a/src/NumUtils.hs b/src/NumUtils.hs
new file mode 100644
--- /dev/null
+++ b/src/NumUtils.hs
@@ -0,0 +1,144 @@
+{-# LANGUAGE NoMonomorphismRestriction #-}
+
+module NumUtils (showIntAtBase, formatRealFloatAlt, formatHexFloat) where
+
+import Data.Bits
+import Data.Char
+import Data.Foldable
+import Data.Ord
+import Data.Semigroup ((<>))
+import Data.Tuple
+import GHC.Float (
+  FFFormat (..),
+  roundTo,
+ )
+import Numeric (floatToDigits)
+import Prelude hiding (
+  exp,
+  foldr,
+  (<>),
+ )
+
+import Buf
+import StrUtils
+
+showIntAtBase ::
+  (Buf buf, Show a, Integral a) => a -> (Int -> Char) -> a -> buf
+showIntAtBase base toChr n0
+  | base <= 1 = error "unsupported base"
+  | n0 < 0 = error $ "negative number " ++ show n0
+  | otherwise = showIt (quotRem n0 base) mempty
+ where
+  showIt (n, d) r = case n of
+    0 -> r'
+    _ -> showIt (quotRem n base) r'
+   where
+    r' = cons (toChr (fromIntegral d)) r
+
+formatRealFloatAlt ::
+  (Buf buf, RealFloat a) =>
+  FFFormat ->
+  Maybe Int ->
+  Bool ->
+  Bool ->
+  a ->
+  buf
+formatRealFloatAlt fmt decs forceDot upper x
+  | isNaN x = str "NaN"
+  | isInfinite x = str $ if x < 0 then "-Infinity" else "Infinity"
+  | x < 0 || isNegativeZero x =
+    cons
+      '-'
+      (doFmt fmt (floatToDigits 10 (- x)) False)
+  | otherwise = doFmt fmt (floatToDigits 10 x) False
+ where
+  eChar
+    | upper = 'E'
+    | otherwise = 'e'
+  doFmt FFFixed (digs, exp) fullRounding
+    | exp < 0 =
+      doFmt FFFixed (replicate (negate exp) 0 ++ digs, 0) fullRounding
+    | null part =
+      fromDigits False whole <> (if forceDot then singleton '.' else mempty)
+    | null whole =
+      str "0." <> fromDigits False part
+    | otherwise =
+      fromDigits False whole <> singleton '.' <> fromDigits False part
+   where
+    (whole, part) =
+      uncurry (flip splitAt) (toRoundedDigits decs (digs, exp) fullRounding)
+  doFmt FFExponent ([0], _) _
+    | forceDot = str "0.e+00"
+    | otherwise = str "0e+00"
+  doFmt FFExponent (digs, exp) fullRounding =
+    shownDigs <> cons eChar shownExponent
+   where
+    shownDigs = case digs' of
+      [] -> undefined
+      [x'] ->
+        cons (intToDigit x') (if forceDot then singleton '.' else mempty)
+      (x' : xs) -> cons (intToDigit x') (cons '.' (fromDigits False xs))
+    digs' = case decs of
+      Just n ->
+        case roundTo
+          10
+          (if fullRounding then min (length digs) n else n + 1)
+          digs of
+          (1, xs) -> 1 : xs
+          (_, ys) -> ys
+      Nothing -> digs
+    exp' = exp - 1
+    shownExponent =
+      cons (if exp' < 0 then '-' else '+') $
+        justifyRight 2 '0' $
+          showIntAtBase 10 intToDigit $
+            abs exp'
+  doFmt FFGeneric d _ =
+    minimumBy (comparing size) [doFmt FFFixed d True, doFmt FFExponent d True]
+
+toRoundedDigits :: Maybe Int -> ([Int], Int) -> Bool -> ([Int], Int)
+toRoundedDigits Nothing (digs, exp) _ = (digs, exp)
+toRoundedDigits (Just prec) (digs, exp) fullRounding = (digs', exp + overflow)
+ where
+  (overflow, digs') =
+    roundTo
+      10
+      (if fullRounding && prec > exp then min (length digs) prec else prec + exp)
+      digs
+
+fromDigits :: (Buf buf) => Bool -> [Int] -> buf
+fromDigits upper =
+  foldr (cons . (if upper then toUpper else id) . intToDigit) mempty
+
+formatHexFloat ::
+  (Buf buf, RealFloat a) => Maybe Int -> Bool -> Bool -> a -> buf
+formatHexFloat decs alt upper x = doFmt (floatToDigits 2 x)
+ where
+  pChar
+    | upper = 'P'
+    | otherwise = 'p'
+  doFmt ([], _) = undefined
+  doFmt ([0], 0) = cons '0' (cons pChar (str "+0"))
+  -- possible ghcjs bug - some floats are encoded as ([0,...], exp + 1)
+  -- but the first digit should never be 0 unless the input is 0.0
+  doFmt (0 : bits, exp) = doFmt (bits, exp - 1)
+  doFmt (_ : bits, exp) =
+    cons '1' $
+      (if not (null hexDigits) || alt then singleton '.' else mempty)
+        <> fromDigits upper hexDigits
+        <> singleton pChar
+        <> (if exp > 0 then singleton '+' else mempty)
+        <> str (show (exp - 1 + overflow))
+   where
+    hexDigits' = go bits
+    (overflow, hexDigits) = case decs of
+      Just n -> case roundTo 16 n hexDigits' of
+        (1, _ : digs) -> (1, digs)
+        x' -> x'
+      Nothing -> (0, hexDigits')
+    go (a : b : c : d : xs) =
+      ((a `shiftL` 3) .|. (b `shiftL` 2) .|. (c `shiftL` 1) .|. d) : go xs
+    go [a, b, c] = go [a, b, c, 0]
+    go [a, b] = go [a, b, 0, 0]
+    go [a] = go [a, 0, 0, 0]
+    go [] = []
diff --git a/src/StrUtils.hs b/src/StrUtils.hs
new file mode 100644
--- /dev/null
+++ b/src/StrUtils.hs
@@ -0,0 +1,17 @@
+module StrUtils (justifyLeft, justifyRight) where
+
+import Buf
+
+justifyLeft :: (Buf a) => Int -> Char -> a -> a
+justifyLeft n c s
+  | diff <= 0 = s
+  | otherwise = s <> repeatN diff c
+ where
+  diff = n - size s
+
+justifyRight :: (Buf a) => Int -> Char -> a -> a
+justifyRight n c s
+  | diff <= 0 = s
+  | otherwise = repeatN diff c <> s
+ where
+  diff = n - size s
diff --git a/src/Text/Printf/TH.hs b/src/Text/Printf/TH.hs
deleted file mode 100644
--- a/src/Text/Printf/TH.hs
+++ /dev/null
@@ -1,303 +0,0 @@
-{-# LANGUAGE DeriveDataTypeable #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE TupleSections #-}
-{-# LANGUAGE QuasiQuotes #-}
-
-module Text.Printf.TH (s, st, lt, sb, lb, sP, stP, ltP, sbP, lbP) where
-
-import Control.Applicative
-import Control.Monad.IO.Class
-import Data.Attoparsec.Text hiding (space)
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Lazy as LB
-import qualified Data.ByteString.Char8 as B8
-import qualified Data.ByteString.Lazy.Char8 as LB8
-import Data.Char hiding (Space)
-import Data.Data
-import Data.Maybe
-import Data.Monoid
-import Data.String
-import Data.Text (pack, unpack)
-import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import qualified Data.Text.Lazy as LT
-import qualified Data.Text.Lazy.IO as LT
-import Data.Word
-import Language.Haskell.TH
-import Language.Haskell.TH.Quote
-import Numeric
-import Prelude hiding (lex)
-import Text.ParserCombinators.ReadP (readP_to_S)
-import Text.Read.Lex
-
-data Specifier = SignedDec | Octal | UnsignedHex | UnsignedHexUpper
-               | FloatS | FloatUpper | Sci | SciUpper | ShorterFloat | ShorterFloatUpper
-               | CharS | Str | Percent | Showable deriving (Eq, Show, Data, Typeable)
-
-data Flag = Minus | Plus | Space | Hash | Zero deriving (Eq, Show, Data, Typeable)
-
-data Width = Width Integer | WidthStar deriving (Data, Show, Typeable, Eq)
-
-data Precision = Precision Integer | PrecisionStar deriving (Data, Show, Typeable, Eq)
-
-data Chunk = Chunk
-           { flags :: [Flag]
-           , width :: Maybe Width
-           , precision :: Maybe Precision
-           , spec :: Specifier
-           } | Plain String
-           deriving (Data, Show, Typeable)
-
-quoterOfType :: Name -> Bool -> QuasiQuoter
-quoterOfType m b = QuasiQuoter
-                 { quoteExp = \s' -> let lexed = readP_to_S lex $ '"' : concatMap escape s' ++ "\""
-                                         escape '"' = "\\\""
-                                         escape m' = [m']
-                                     in case lexed of
-                        [(String str,"")] -> case parseOnly formatP (pack str) of
-                            Right r -> chunksToFormatter r m b
-                            Left m' -> error $ "Error when parsing format string: " ++ show m'
-                        _ -> error "Error when parsing format string"
-                 , quotePat = error "printf cannot be used in pattern context"
-                 , quoteType = error "printf cannot be used in type context"
-                 , quoteDec = error "printf cannot be used in declaration context"
-                 }
-
-s, st, lt, sb, lb, sP, stP, ltP, sbP, lbP :: QuasiQuoter
-
-s = quoterOfType ''String False
-st = quoterOfType ''T.Text False
-lt = quoterOfType ''LT.Text False
-sb = quoterOfType ''B.ByteString False
-lb = quoterOfType ''LB.ByteString False
-sP = quoterOfType ''String True
-stP = quoterOfType ''T.Text True
-ltP = quoterOfType ''LT.Text True
-sbP = quoterOfType ''B.ByteString True
-lbP = quoterOfType ''LB.ByteString True
-
-formatP :: Parser [Chunk]
-formatP = many1 ( char '%' *> chunkP
-              <|> fmap (Plain . unpack) (takeWhile1 (/= '%')) )
-              <* endOfInput
-
-chunkP :: Parser Chunk
-chunkP = do
-    f <- many flagP
-    w <- option Nothing (Just <$> widthP)
-    p <- option Nothing (Just <$> precisionP)
-    m <- specP
-    return $ Chunk f w p m
-
-flagP :: Parser Flag
-flagP = Minus <$ char '-'
-    <|> Plus <$ char '+'
-    <|> Space <$ char ' '
-    <|> Hash <$ char '#'
-    <|> Zero <$ char '0'
-
-widthP :: Parser Width
-widthP = ( Width <$> decimal
-       <|> WidthStar <$ char '*' )
-
-precisionP :: Parser Precision
-precisionP = char '.' *> ( Precision <$> decimal
-                       <|> PrecisionStar <$ char '*' )
-
-specP :: Parser Specifier
-specP = SignedDec <$ (char 'd' <|> char 'i')
-    <|> Octal <$ char 'o'
-    <|> UnsignedHex <$ char 'x'
-    <|> UnsignedHexUpper <$ char 'X'
-    <|> FloatS <$ char 'f'
-    <|> FloatUpper <$ char 'F'
-    <|> Sci <$ char 'e'
-    <|> SciUpper <$ char 'E'
-    <|> ShorterFloat <$ char 'g'
-    <|> ShorterFloatUpper <$ char 'G'
-    <|> CharS <$ char 'c'
-    <|> Str <$ char 's'
-    <|> Percent <$ char '%'
-    <|> Showable <$ char '?'
-
-data PrintfArg = PrintfArg
-               { paSpec :: Chunk
-               , widthArg :: Maybe Name
-               , precArg :: Maybe Name
-               , valArg :: Maybe Name
-               } deriving Show
-
-collectArgs :: PrintfArg -> [PatQ]
-collectArgs (PrintfArg _ n1 n2 n3) = map varP $ catMaybes [n1, n2, n3]
-
-chunksToFormatter :: [Chunk] -> Name -> Bool -> ExpQ
-chunksToFormatter cs ty pr = do
-    ns <- mapM argify cs
-    let processor = if pr then [e|output|] else [e|id|]
-    lamE (concatMap collectArgs ns) [e|$(processor) (mconcat $(listE $ map arg ns) :: $(conT ty))|]
-    where
-        argify p@Plain{} = return $ PrintfArg p Nothing Nothing Nothing
-        argify c@Chunk{spec = Percent} = return $ PrintfArg c Nothing Nothing Nothing
-        argify c@Chunk{width = w, precision = p} = do
-            wa <- if w == Just WidthStar
-                      then Just <$> newName "a"
-                      else return Nothing
-            pa <- if p == Just PrecisionStar
-                      then Just <$> newName "a"
-                      else return Nothing
-            q' <- newName "a"
-            return $ PrintfArg c wa pa (Just q')
-
-q :: Data a => a -> Q Exp
-q = dataToExpQ (const Nothing)
-
-arg :: PrintfArg -> ExpQ
-arg PrintfArg{paSpec = Plain str} = stringE str
-arg PrintfArg{paSpec = Chunk{spec = Percent}} = stringE "%"
-arg c@PrintfArg{valArg = Just v} = (\n -> dispatch n c v) $
-    case spec $ paSpec c of
-        SignedDec -> 'showIntegral
-        Octal -> 'showOctal
-        UnsignedHex -> 'showHexP
-        UnsignedHexUpper -> 'showUpperHex
-        FloatS -> 'showFloatP
-        FloatUpper -> 'showUpperFloat
-        Sci -> 'showSci
-        SciUpper -> 'showUpperSci
-        ShorterFloat -> 'showShorter
-        ShorterFloatUpper -> 'showUpperShorter
-        CharS -> 'showCharP
-        Str -> 'showStringP
-        Showable -> 'showShowP
-        m -> error $ "Unhandled specifier: " ++ show m
-arg m = error $ "Unhandled argument: " ++ show m
-
-dispatch :: Name -> PrintfArg -> Name -> ExpQ
-dispatch s' n v = appE (varE 'fromString)
-    $ foldl1 appE [ varE s'
-                  , q $ paSpec n
-                  , normalize True (widthArg n)
-                  , normalize False (precArg n)
-                  , varE v ]
-    where
-        normalize b v' = case v' of
-            Nothing -> litE . integerL $ if b then calcWidth $ paSpec n else calcPrec $ paSpec n
-            Just q' -> varE q'
-
-showIntegralBasic :: Chunk -- ^ options
-                  -> Integer -- ^ width
-                  -> Bool -- ^ less than 0?
-                  -> String -- ^ prefix
-                  -> String -- ^ value
-                  -> String
-showIntegralBasic c w b pre n = space c . plus b c . prefix pre c . pad w c $ n
-
-showIntegral :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String
-showIntegral pa w _ n = showIntegralBasic pa w (n >= 0) "" $ show n
-
-showOctal :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String
-showOctal pa w _ n = showIntegralBasic pa w (n >= 0) "0" $ showOct n ""
-
-showHexP :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String
-showHexP pa w _ n = showIntegralBasic pa w (n >= 0) "0x" $ showHex n ""
-
-showUpperHex :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String
-showUpperHex pa w _ n = showIntegralBasic pa w (n >= 0) "0X" . map toUpper $ showHex n ""
-
-showFloatP :: RealFloat a => Chunk -> Integer -> Integer -> a -> String
-showFloatP pa w pr n = plus (n >= 0) pa
-                     . padDelim '.' w pa
-                     $ showFFloat (if pr < 0 then Nothing else Just $ fromIntegral pr) n ""
-
-showUpperFloat :: RealFloat a => Chunk -> Integer -> Integer -> a -> String
-showUpperFloat pa w pr n = map toUpper $ showFloatP pa w pr n
-
-showSci :: RealFloat a => Chunk -> Integer -> Integer -> a -> String
-showSci pa w pr n = plus (n >= 0) pa
-                  . padDelim '.' w pa
-                  $ showEFloat (if pr < 0 then Nothing else Just $ fromIntegral pr) n ""
-
-showUpperSci :: RealFloat a => Chunk -> Integer -> Integer -> a -> String
-showUpperSci pa w pr n = map toUpper $ showSci pa w pr n
-
-showShorter :: RealFloat a => Chunk -> Integer -> Integer -> a -> String
-showShorter pa w pr n = if length f > length e then e else f
-    where f = showFloatP pa w pr n
-          e = showSci pa w pr n
-
-showUpperShorter :: RealFloat a => Chunk -> Integer -> Integer -> a -> String
-showUpperShorter pa w pr n = if length f > length e then e else f
-    where f = showUpperFloat pa w pr n
-          e = showUpperSci pa w pr n
-
-showCharP :: ToChar a => Chunk -> Integer -> Integer -> a -> String
-showCharP _ _ _ c = [asChar c]
-
-showStringP :: ToString a => Chunk -> Integer -> Integer -> a -> String
-showStringP pa w _ n = space pa . pad w pa $ toString n
-
-showShowP :: Show a => Chunk -> Integer -> Integer -> a -> String
-showShowP pa w _ n = space pa . pad w pa $ show n
-
-space :: Chunk -> String -> String
-space c = if Space `elem` flags c && Plus `notElem` flags c then (' ':) else id
-
-plus :: Bool -> Chunk -> String -> String
-plus b c = if Plus `elem` flags c
-               then if b then ('+':) else ('-':)
-               else id
-
-prefix :: String -> Chunk -> String -> String
-prefix s' p = if Hash `elem` flags p then (s' ++) else id
-
-padDelim :: Integral a => Char -> a -> Chunk -> String -> String
-padDelim c w pa s' = a (replicate (fromIntegral w - len) c') s'
-    where len = length $ Prelude.takeWhile (/=c) s'
-          a = if Minus `elem` flags pa
-                  then flip (++)
-                  else (++)
-          c' = if Zero `elem` flags pa
-                  then '0'
-                  else ' '
-
-pad :: Integral a => a -> Chunk -> String -> String
-pad w pa s' = a (replicate (fromIntegral w - length s') c) s'
-    where a = if Minus `elem` flags pa
-                  then flip (++)
-                  else (++)
-          c = if Zero `elem` flags pa
-                  then '0'
-                  else ' '
-
-calcWidth :: Chunk -> Integer
-calcWidth (Chunk _ (Just (Width n)) _ _) = n
-calcWidth _ = -1
-
-calcPrec :: Chunk -> Integer
-calcPrec (Chunk _ _ (Just (Precision n)) _) = n
-calcPrec _ = -1
-
-class ToString a where toString :: a -> String
-
-instance ToChar a => ToString [a] where toString = map asChar
-instance ToString T.Text where toString = T.unpack
-instance ToString LT.Text where toString = LT.unpack
-instance ToString B.ByteString where toString = map asChar . B.unpack
-instance ToString LB.ByteString where toString = map asChar . LB.unpack
-
-class ToChar m where asChar :: m -> Char
-
-instance ToChar Char where asChar = id
-instance ToChar Int where asChar = chr
-instance ToChar Word8 where asChar = chr . fromIntegral
-
-class Printable a where output :: MonadIO m => a -> m ()
-
-instance Printable String where output = liftIO . putStrLn
-instance Printable T.Text where output = liftIO . T.putStrLn
-instance Printable LT.Text where output = liftIO . LT.putStrLn
-instance Printable B.ByteString where output = liftIO . B8.putStrLn
-instance Printable LB.ByteString where output = liftIO . LB8.putStrLn
diff --git a/tests/format.hs b/tests/format.hs
deleted file mode 100644
--- a/tests/format.hs
+++ /dev/null
@@ -1,67 +0,0 @@
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE QuasiQuotes #-}
-
-module Main where
-
-import Control.Exception
-import Control.Monad
-import qualified Data.Text as T
-import qualified Data.Text.Lazy as LT
-import qualified Data.ByteString.Char8 as B
-import qualified Data.ByteString.Lazy.Char8 as LB
-import Language.Haskell.TH.Quote
-import Test.Hspec
-import Test.HUnit
-import Test.QuickCheck
-import Text.Printf.TH
-
-#if __GLASGOW_HASKELL__ <= 706
-instance Eq ErrorCall where
-    ErrorCall m == ErrorCall n = m == n
-#endif
-
-main :: IO ()
-main = hspec $ do
-    describe "quoter" $ do
-        it "handles escapes properly" $
-            [s|\n\STX\\|] @?= "\n\STX\\"
-
-        it "accepts escaped format specifiers" $
-            [s|\37\115|] "foo" @?= "foo"
-
-        it "rejects unknown escape sequences" $
-            assertException (ErrorCall "Error when parsing format string") . evaluate $ quoteExp s "\\UNKNOWN"
-
-    describe "string substitution" $ do
-        it "inserts strings of different types" $
-            [s|%s %s %s %s %s %s|] "foo"
-                                   (T.pack "foo")
-                                   (LT.pack "foo")
-                                   (B.pack "foo")
-                                   (LB.pack "foo")
-                                   [102 :: Int, 111, 111]
-              @?= ("foo foo foo foo foo foo" :: String)
-
-        it "pads" $ do
-            [s|%10s|] "foo"  @?= "       foo"
-            [s|%010s|] "foo" @?= "0000000foo"
-            [s|%*s|] 5 "foo" @?= "  foo"
-
-        it "ignores magic hash" $
-            forAll str $ \n -> [s|%#s|] n == [s|%s|] n
-
-        it "ignores precision" $
-            forAll str $ \n -> [s|%.10s|] n == [s|%s|] n
-
-str :: Gen String
-str = do
-    n <- elements [7..20]
-    vectorOf n (elements ['A'..'z'])
-
-assertException :: (Exception e, Eq e) => e -> IO a -> IO ()
-assertException ex action =
-    handleJust isWanted (const $ return ()) $ do
-        a <- action
-        assertFailure $ a `seq` "Expected exception: " ++ show ex
-    where isWanted = guard . (==ex)
diff --git a/tests/printf/GeneratedSpec.hs b/tests/printf/GeneratedSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/printf/GeneratedSpec.hs
@@ -0,0 +1,450 @@
+-- This file contains autogenerated erroneous printf strings, which th-printf
+-- will warn about, but they shouldn't break a -Werror build
+{-# OPTIONS_GHC -Wwarn #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module GeneratedSpec where
+
+import Data.Int
+import Data.Word
+import Language.Haskell.Printf
+import Test.HUnit
+import Test.Hspec
+
+spec :: Spec
+spec = do
+    it "test-case #1" $ [s|%.*f|] (2 :: Int32) (0.33333333 :: Double) @?= "0.33"
+    it "test-case #2" $ [s|%.3s|] "foobar" @?= "foo"
+    it "test-case #3" $ [s|%10.5d|] (4 :: Int32) @?= "     00004"
+    it "test-case #4" $ [s|% d|] (42 :: Int32) @?= " 42"
+    it "test-case #5" $ [s|% d|] (-42 :: Int32) @?= "-42"
+    it "test-case #6" $ [s|% 5d|] (42 :: Int32) @?= "   42"
+    it "test-case #7" $ [s|% 5d|] (-42 :: Int32) @?= "  -42"
+    it "test-case #8" $ [s|% 15d|] (42 :: Int32) @?= "             42"
+    it "test-case #9" $ [s|% 15d|] (-42 :: Int32) @?= "            -42"
+    it "test-case #10" $ [s|%+d|] (42 :: Int32) @?= "+42"
+    it "test-case #11" $ [s|%+d|] (-42 :: Int32) @?= "-42"
+    it "test-case #12" $ [s|%+5d|] (42 :: Int32) @?= "  +42"
+    it "test-case #13" $ [s|%+5d|] (-42 :: Int32) @?= "  -42"
+    it "test-case #14" $ [s|%+15d|] (42 :: Int32) @?= "            +42"
+    it "test-case #15" $ [s|%+15d|] (-42 :: Int32) @?= "            -42"
+    it "test-case #16" $ [s|%0d|] (42 :: Int32) @?= "42"
+    it "test-case #17" $ [s|%0d|] (-42 :: Int32) @?= "-42"
+    it "test-case #18" $ [s|%05d|] (42 :: Int32) @?= "00042"
+    it "test-case #19" $ [s|%05d|] (-42 :: Int32) @?= "-0042"
+    it "test-case #20" $ [s|%015d|] (42 :: Int32) @?= "000000000000042"
+    it "test-case #21" $ [s|%015d|] (-42 :: Int32) @?= "-00000000000042"
+    it "test-case #22" $ [s|%-d|] (42 :: Int32) @?= "42"
+    it "test-case #23" $ [s|%-d|] (-42 :: Int32) @?= "-42"
+    it "test-case #24" $ [s|%-5d|] (42 :: Int32) @?= "42   "
+    it "test-case #25" $ [s|%-5d|] (-42 :: Int32) @?= "-42  "
+    it "test-case #26" $ [s|%-15d|] (42 :: Int32) @?= "42             "
+    it "test-case #27" $ [s|%-15d|] (-42 :: Int32) @?= "-42            "
+    it "test-case #28" $ [s|%-0d|] (42 :: Int32) @?= "42"
+    it "test-case #29" $ [s|%-0d|] (-42 :: Int32) @?= "-42"
+    it "test-case #30" $ [s|%-05d|] (42 :: Int32) @?= "42   "
+    it "test-case #31" $ [s|%-05d|] (-42 :: Int32) @?= "-42  "
+    it "test-case #32" $ [s|%-015d|] (42 :: Int32) @?= "42             "
+    it "test-case #33" $ [s|%-015d|] (-42 :: Int32) @?= "-42            "
+    it "test-case #34" $ [s|%0-d|] (42 :: Int32) @?= "42"
+    it "test-case #35" $ [s|%0-d|] (-42 :: Int32) @?= "-42"
+    it "test-case #36" $ [s|%0-5d|] (42 :: Int32) @?= "42   "
+    it "test-case #37" $ [s|%0-5d|] (-42 :: Int32) @?= "-42  "
+    it "test-case #38" $ [s|%0-15d|] (42 :: Int32) @?= "42             "
+    it "test-case #39" $ [s|%0-15d|] (-42 :: Int32) @?= "-42            "
+    it "test-case #43" $ [s|%.2f|] (42.8952 :: Double) @?= "42.90"
+    it "test-case #44" $ [s|%.2F|] (42.8952 :: Double) @?= "42.90"
+    it "test-case #45" $ [s|%.10f|] (42.8952 :: Double) @?= "42.8952000000"
+    it "test-case #46" $ [s|%1.2f|] (42.8952 :: Double) @?= "42.90"
+    it "test-case #47" $ [s|%6.2f|] (42.8952 :: Double) @?= " 42.90"
+    it "test-case #49" $ [s|%+6.2f|] (42.8952 :: Double) @?= "+42.90"
+    it "test-case #50" $ [s|%5.10f|] (42.8952 :: Double) @?= "42.8952000000"
+  -- 51: anti-test
+  -- 52: anti-test
+  -- 53: excluded for Haskell
+  -- 55: excluded for Haskell
+  -- 56: excluded for Haskell
+  -- 58: excluded for Haskell
+  -- 59: excluded for Haskell
+    it "test-case #60" $ [s|%*s|] (4 :: Int32) "foo" @?= " foo"
+    it "test-case #61" $
+        [s|%*.*f|] (10 :: Int32) (2 :: Int32) (3.14159265 :: Double) @?= "      3.14"
+    it "test-case #63" $
+        [s|%-*.*f|] (10 :: Int32) (2 :: Int32) (3.14159265 :: Double) @?= "3.14      "
+  -- 64: anti-test
+  -- 65: anti-test
+    it "test-case #66" $ [s|+%s+|] "hello" @?= "+hello+"
+    it "test-case #67" $ [s|+%d+|] (10 :: Int32) @?= "+10+"
+    it "test-case #68" $ [s|%c|] 'a' @?= "a"
+  -- unsound test: it "test-case #69" $ [s|%c|] (32 :: Int32) @?= " "
+  -- unsound test: it "test-case #70" $ [s|%c|] (36 :: Int32) @?= "$"
+    it "test-case #71" $ [s|%d|] (10 :: Int32) @?= "10"
+  -- 72: anti-test
+  -- 73: anti-test
+  -- 74: excluded for Haskell
+  -- 75: excluded for Haskell
+    it "test-case #76" $
+        [s|%+#22.15e|] (7.89456123e8 :: Double) @?= "+7.894561230000000e+08"
+    it "test-case #77" $
+        [s|%-#22.15e|] (7.89456123e8 :: Double) @?= "7.894561230000000e+08 "
+    it "test-case #78" $
+        [s|%#22.15e|] (7.89456123e8 :: Double) @?= " 7.894561230000000e+08"
+    it "test-case #79" $ [s|%#1.1g|] (7.89456123e8 :: Double) @?= "8.e+08"
+    it "test-case #81" $ [s|%+8lld|] (100 :: Int64) @?= "    +100"
+    it "test-case #82" $ [s|%+.8lld|] (100 :: Int64) @?= "+00000100"
+    it "test-case #83" $ [s|%+10.8lld|] (100 :: Int64) @?= " +00000100"
+  -- 84: excluded for Haskell
+    it "test-case #85" $ [s|%-1.5lld|] (-100 :: Int64) @?= "-00100"
+    it "test-case #86" $ [s|%5lld|] (100 :: Int64) @?= "  100"
+    it "test-case #87" $ [s|%5lld|] (-100 :: Int64) @?= " -100"
+    it "test-case #88" $ [s|%-5lld|] (100 :: Int64) @?= "100  "
+    it "test-case #89" $ [s|%-5lld|] (-100 :: Int64) @?= "-100 "
+    it "test-case #90" $ [s|%-.5lld|] (100 :: Int64) @?= "00100"
+    it "test-case #91" $ [s|%-.5lld|] (-100 :: Int64) @?= "-00100"
+    it "test-case #92" $ [s|%-8.5lld|] (100 :: Int64) @?= "00100   "
+    it "test-case #93" $ [s|%-8.5lld|] (-100 :: Int64) @?= "-00100  "
+    it "test-case #94" $ [s|%05lld|] (100 :: Int64) @?= "00100"
+    it "test-case #95" $ [s|%05lld|] (-100 :: Int64) @?= "-0100"
+    it "test-case #96" $ [s|% lld|] (100 :: Int64) @?= " 100"
+    it "test-case #97" $ [s|% lld|] (-100 :: Int64) @?= "-100"
+    it "test-case #98" $ [s|% 5lld|] (100 :: Int64) @?= "  100"
+    it "test-case #99" $ [s|% 5lld|] (-100 :: Int64) @?= " -100"
+    it "test-case #100" $ [s|% .5lld|] (100 :: Int64) @?= " 00100"
+    it "test-case #101" $ [s|% .5lld|] (-100 :: Int64) @?= "-00100"
+    it "test-case #102" $ [s|% 8.5lld|] (100 :: Int64) @?= "   00100"
+    it "test-case #103" $ [s|% 8.5lld|] (-100 :: Int64) @?= "  -00100"
+    it "test-case #104" $ [s|%.0lld|] (0 :: Int64) @?= ""
+    it "test-case #105" $ [s|%#+21.18llx|] (-100 :: Int64) @?= " 0x00ffffffffffffff9c"
+    it "test-case #106" $ [s|%#.25llo|] (-100 :: Int64) @?= "0001777777777777777777634"
+    it "test-case #107" $ [s|%#+24.20llo|] (-100 :: Int64) @?= " 01777777777777777777634"
+    it "test-case #108" $ [s|%#+18.21llX|] (-100 :: Int64) @?= "0X00000FFFFFFFFFFFFFF9C"
+    it "test-case #109" $ [s|%#+20.24llo|] (-100 :: Int64) @?= "001777777777777777777634"
+    it "test-case #110" $ [s|%#+25.22llu|] (-1 :: Int64) @?= "   0018446744073709551615"
+    it "test-case #111" $ [s|%#+25.22llu|] (-1 :: Int64) @?= "   0018446744073709551615"
+    it "test-case #112" $
+        [s|%#+30.25llu|] (-1 :: Int64) @?= "     0000018446744073709551615"
+    it "test-case #113" $ [s|%+#25.22lld|] (-1 :: Int64) @?= "  -0000000000000000000001"
+    it "test-case #114" $ [s|%#-8.5llo|] (100 :: Int64) @?= "00144   "
+    it "test-case #115" $ [s|%#-+ 08.5lld|] (100 :: Int64) @?= "+00100  "
+    it "test-case #116" $ [s|%#-+ 08.5lld|] (100 :: Int64) @?= "+00100  "
+    it "test-case #117" $
+        [s|%.40lld|] (1 :: Int64) @?= "0000000000000000000000000000000000000001"
+    it "test-case #118" $
+        [s|% .40lld|] (1 :: Int64) @?= " 0000000000000000000000000000000000000001"
+    it "test-case #119" $
+        [s|% .40d|] (1 :: Int32) @?= " 0000000000000000000000000000000000000001"
+  -- 121: excluded for Haskell
+  -- 124: excluded for Haskell
+    it "test-case #125" $ [s|% d|] (1 :: Int32) @?= " 1"
+    it "test-case #126" $ [s|%+ d|] (1 :: Int32) @?= "+1"
+    it "test-case #129" $ [s|%#012x|] (1 :: Int32) @?= "0x0000000001"
+    it "test-case #130" $ [s|%#04.8x|] (1 :: Int32) @?= "0x00000001"
+    it "test-case #131" $ [s|%#-08.2x|] (1 :: Int32) @?= "0x01    "
+    it "test-case #132" $ [s|%#08o|] (1 :: Int32) @?= "00000001"
+  -- 133: excluded for Haskell
+  -- 137: excluded for Haskell
+    it "test-case #142" $ [s|%.1s|] "foo" @?= "f"
+    it "test-case #143" $ [s|%.*s|] (1 :: Int32) "foo" @?= "f"
+    it "test-case #144" $ [s|%*s|] (-5 :: Int32) "foo" @?= "foo  "
+    it "test-case #145" $ [s|hello|] @?= "hello"
+  -- 147: excluded for Haskell
+    it "test-case #148" $ [s|%3c|] 'a' @?= "  a"
+    it "test-case #149" $ [s|%3d|] (1234 :: Int32) @?= "1234"
+  -- 150: excluded for Haskell
+    it "test-case #152" $ [s|%-1d|] (2 :: Int32) @?= "2"
+    it "test-case #153" $ [s|%2.4f|] (8.6 :: Double) @?= "8.6000"
+    it "test-case #154" $ [s|%0f|] (0.6 :: Double) @?= "0.600000"
+    it "test-case #155" $ [s|%.0f|] (0.6 :: Double) @?= "1"
+    it "test-case #156" $ [s|%2.4e|] (8.6 :: Double) @?= "8.6000e+00"
+    it "test-case #157" $ [s|% 2.4e|] (8.6 :: Double) @?= " 8.6000e+00"
+    it "test-case #159" $ [s|% 2.4e|] (-8.6 :: Double) @?= "-8.6000e+00"
+    it "test-case #160" $ [s|%+2.4e|] (8.6 :: Double) @?= "+8.6000e+00"
+    it "test-case #161" $ [s|%2.4g|] (8.6 :: Double) @?= "8.6"
+    it "test-case #162" $ [s|%-i|] (-1 :: Int32) @?= "-1"
+    it "test-case #163" $ [s|%-i|] (1 :: Int32) @?= "1"
+    it "test-case #164" $ [s|%+i|] (1 :: Int32) @?= "+1"
+    it "test-case #165" $ [s|%o|] (10 :: Int32) @?= "12"
+  -- 166: excluded for Haskell
+  -- 167: excluded for Haskell
+    it "test-case #169" $ [s|%s|] "%%%%" @?= "%%%%"
+    it "test-case #170" $ [s|%u|] (-1 :: Int32) @?= "4294967295"
+  -- 171: excluded for Haskell
+  -- 172: excluded for Haskell
+  -- 173: excluded for Haskell
+  -- 174: excluded for Haskell
+  -- 176: excluded for Haskell
+    it "test-case #177" $ [s|%%0|] @?= "%0"
+  -- 178: excluded for Haskell
+  -- unsound test: it "test-case #179" $ [s|%hhx|] 'a' @?= "61"
+    it "test-case #181" $ [s|Hallo heimur|] @?= "Hallo heimur"
+    it "test-case #182" $ [s|%s|] "Hallo heimur" @?= "Hallo heimur"
+    it "test-case #183" $ [s|%d|] (1024 :: Int32) @?= "1024"
+    it "test-case #184" $ [s|%d|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #185" $ [s|%i|] (1024 :: Int32) @?= "1024"
+    it "test-case #186" $ [s|%i|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #187" $ [s|%u|] (1024 :: Int32) @?= "1024"
+    it "test-case #188" $ [s|%u|] (4294966272 :: Word32) @?= "4294966272"
+    it "test-case #189" $ [s|%o|] (511 :: Int32) @?= "777"
+    it "test-case #190" $ [s|%o|] (4294966785 :: Word32) @?= "37777777001"
+    it "test-case #191" $ [s|%x|] (305441741 :: Int32) @?= "1234abcd"
+    it "test-case #192" $ [s|%x|] (3989525555 :: Word32) @?= "edcb5433"
+    it "test-case #193" $ [s|%X|] (305441741 :: Int32) @?= "1234ABCD"
+    it "test-case #194" $ [s|%X|] (3989525555 :: Word32) @?= "EDCB5433"
+    it "test-case #195" $ [s|%c|] 'x' @?= "x"
+    it "test-case #196" $ [s|%%|] @?= "%"
+    it "test-case #197" $ [s|%+s|] "Hallo heimur" @?= "Hallo heimur"
+    it "test-case #198" $ [s|%+d|] (1024 :: Int32) @?= "+1024"
+    it "test-case #199" $ [s|%+d|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #200" $ [s|%+i|] (1024 :: Int32) @?= "+1024"
+    it "test-case #201" $ [s|%+i|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #202" $ [s|%+u|] (1024 :: Int32) @?= "1024"
+    it "test-case #203" $ [s|%+u|] (4294966272 :: Word32) @?= "4294966272"
+    it "test-case #204" $ [s|%+o|] (511 :: Int32) @?= "777"
+    it "test-case #205" $ [s|%+o|] (4294966785 :: Word32) @?= "37777777001"
+    it "test-case #206" $ [s|%+x|] (305441741 :: Int32) @?= "1234abcd"
+    it "test-case #207" $ [s|%+x|] (3989525555 :: Word32) @?= "edcb5433"
+    it "test-case #208" $ [s|%+X|] (305441741 :: Int32) @?= "1234ABCD"
+    it "test-case #209" $ [s|%+X|] (3989525555 :: Word32) @?= "EDCB5433"
+    it "test-case #210" $ [s|%+c|] 'x' @?= "x"
+    it "test-case #211" $ [s|% s|] "Hallo heimur" @?= "Hallo heimur"
+    it "test-case #212" $ [s|% d|] (1024 :: Int32) @?= " 1024"
+    it "test-case #213" $ [s|% d|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #214" $ [s|% i|] (1024 :: Int32) @?= " 1024"
+    it "test-case #215" $ [s|% i|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #216" $ [s|% u|] (1024 :: Int32) @?= "1024"
+    it "test-case #217" $ [s|% u|] (4294966272 :: Word32) @?= "4294966272"
+    it "test-case #218" $ [s|% o|] (511 :: Int32) @?= "777"
+    it "test-case #219" $ [s|% o|] (4294966785 :: Word32) @?= "37777777001"
+    it "test-case #220" $ [s|% x|] (305441741 :: Int32) @?= "1234abcd"
+    it "test-case #221" $ [s|% x|] (3989525555 :: Word32) @?= "edcb5433"
+    it "test-case #222" $ [s|% X|] (305441741 :: Int32) @?= "1234ABCD"
+    it "test-case #223" $ [s|% X|] (3989525555 :: Word32) @?= "EDCB5433"
+    it "test-case #224" $ [s|% c|] 'x' @?= "x"
+    it "test-case #225" $ [s|%+ s|] "Hallo heimur" @?= "Hallo heimur"
+    it "test-case #226" $ [s|%+ d|] (1024 :: Int32) @?= "+1024"
+    it "test-case #227" $ [s|%+ d|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #228" $ [s|%+ i|] (1024 :: Int32) @?= "+1024"
+    it "test-case #229" $ [s|%+ i|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #230" $ [s|%+ u|] (1024 :: Int32) @?= "1024"
+    it "test-case #231" $ [s|%+ u|] (4294966272 :: Word32) @?= "4294966272"
+    it "test-case #232" $ [s|%+ o|] (511 :: Int32) @?= "777"
+    it "test-case #233" $ [s|%+ o|] (4294966785 :: Word32) @?= "37777777001"
+    it "test-case #234" $ [s|%+ x|] (305441741 :: Int32) @?= "1234abcd"
+    it "test-case #235" $ [s|%+ x|] (3989525555 :: Word32) @?= "edcb5433"
+    it "test-case #236" $ [s|%+ X|] (305441741 :: Int32) @?= "1234ABCD"
+    it "test-case #237" $ [s|%+ X|] (3989525555 :: Word32) @?= "EDCB5433"
+    it "test-case #238" $ [s|%+ c|] 'x' @?= "x"
+    it "test-case #239" $ [s|%#o|] (511 :: Int32) @?= "0777"
+    it "test-case #240" $ [s|%#o|] (4294966785 :: Word32) @?= "037777777001"
+    it "test-case #241" $ [s|%#x|] (305441741 :: Int32) @?= "0x1234abcd"
+    it "test-case #242" $ [s|%#x|] (3989525555 :: Word32) @?= "0xedcb5433"
+    it "test-case #243" $ [s|%#X|] (305441741 :: Int32) @?= "0X1234ABCD"
+    it "test-case #244" $ [s|%#X|] (3989525555 :: Word32) @?= "0XEDCB5433"
+    it "test-case #245" $ [s|%#o|] (0 :: Word32) @?= "0"
+    it "test-case #246" $ [s|%#x|] (0 :: Word32) @?= "0"
+    it "test-case #247" $ [s|%#X|] (0 :: Word32) @?= "0"
+    it "test-case #248" $ [s|%1s|] "Hallo heimur" @?= "Hallo heimur"
+    it "test-case #249" $ [s|%1d|] (1024 :: Int32) @?= "1024"
+    it "test-case #250" $ [s|%1d|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #251" $ [s|%1i|] (1024 :: Int32) @?= "1024"
+    it "test-case #252" $ [s|%1i|] (-1024 :: Int32) @?= "-1024"
+    it "test-case #253" $ [s|%1u|] (1024 :: Int32) @?= "1024"
+    it "test-case #254" $ [s|%1u|] (4294966272 :: Word32) @?= "4294966272"
+    it "test-case #255" $ [s|%1o|] (511 :: Int32) @?= "777"
+    it "test-case #256" $ [s|%1o|] (4294966785 :: Word32) @?= "37777777001"
+    it "test-case #257" $ [s|%1x|] (305441741 :: Int32) @?= "1234abcd"
+    it "test-case #258" $ [s|%1x|] (3989525555 :: Word32) @?= "edcb5433"
+    it "test-case #259" $ [s|%1X|] (305441741 :: Int32) @?= "1234ABCD"
+    it "test-case #260" $ [s|%1X|] (3989525555 :: Word32) @?= "EDCB5433"
+    it "test-case #261" $ [s|%1c|] 'x' @?= "x"
+    it "test-case #262" $ [s|%20s|] "Hallo" @?= "               Hallo"
+    it "test-case #263" $ [s|%20d|] (1024 :: Int32) @?= "                1024"
+    it "test-case #264" $ [s|%20d|] (-1024 :: Int32) @?= "               -1024"
+    it "test-case #265" $ [s|%20i|] (1024 :: Int32) @?= "                1024"
+    it "test-case #266" $ [s|%20i|] (-1024 :: Int32) @?= "               -1024"
+    it "test-case #267" $ [s|%20u|] (1024 :: Int32) @?= "                1024"
+    it "test-case #268" $ [s|%20u|] (4294966272 :: Word32) @?= "          4294966272"
+    it "test-case #269" $ [s|%20o|] (511 :: Int32) @?= "                 777"
+    it "test-case #270" $ [s|%20o|] (4294966785 :: Word32) @?= "         37777777001"
+    it "test-case #271" $ [s|%20x|] (305441741 :: Int32) @?= "            1234abcd"
+    it "test-case #272" $ [s|%20x|] (3989525555 :: Word32) @?= "            edcb5433"
+    it "test-case #273" $ [s|%20X|] (305441741 :: Int32) @?= "            1234ABCD"
+    it "test-case #274" $ [s|%20X|] (3989525555 :: Word32) @?= "            EDCB5433"
+    it "test-case #275" $ [s|%20c|] 'x' @?= "                   x"
+    it "test-case #276" $ [s|%-20s|] "Hallo" @?= "Hallo               "
+    it "test-case #277" $ [s|%-20d|] (1024 :: Int32) @?= "1024                "
+    it "test-case #278" $ [s|%-20d|] (-1024 :: Int32) @?= "-1024               "
+    it "test-case #279" $ [s|%-20i|] (1024 :: Int32) @?= "1024                "
+    it "test-case #280" $ [s|%-20i|] (-1024 :: Int32) @?= "-1024               "
+    it "test-case #281" $ [s|%-20u|] (1024 :: Int32) @?= "1024                "
+    it "test-case #282" $ [s|%-20u|] (4294966272 :: Word32) @?= "4294966272          "
+    it "test-case #283" $ [s|%-20o|] (511 :: Int32) @?= "777                 "
+    it "test-case #284" $ [s|%-20o|] (4294966785 :: Word32) @?= "37777777001         "
+    it "test-case #285" $ [s|%-20x|] (305441741 :: Int32) @?= "1234abcd            "
+    it "test-case #286" $ [s|%-20x|] (3989525555 :: Word32) @?= "edcb5433            "
+    it "test-case #287" $ [s|%-20X|] (305441741 :: Int32) @?= "1234ABCD            "
+    it "test-case #288" $ [s|%-20X|] (3989525555 :: Word32) @?= "EDCB5433            "
+    it "test-case #289" $ [s|%-20c|] 'x' @?= "x                   "
+    it "test-case #290" $ [s|%020d|] (1024 :: Int32) @?= "00000000000000001024"
+    it "test-case #291" $ [s|%020d|] (-1024 :: Int32) @?= "-0000000000000001024"
+    it "test-case #292" $ [s|%020i|] (1024 :: Int32) @?= "00000000000000001024"
+    it "test-case #293" $ [s|%020i|] (-1024 :: Int32) @?= "-0000000000000001024"
+    it "test-case #294" $ [s|%020u|] (1024 :: Int32) @?= "00000000000000001024"
+    it "test-case #295" $ [s|%020u|] (4294966272 :: Word32) @?= "00000000004294966272"
+    it "test-case #296" $ [s|%020o|] (511 :: Int32) @?= "00000000000000000777"
+    it "test-case #297" $ [s|%020o|] (4294966785 :: Word32) @?= "00000000037777777001"
+    it "test-case #298" $ [s|%020x|] (305441741 :: Int32) @?= "0000000000001234abcd"
+    it "test-case #299" $ [s|%020x|] (3989525555 :: Word32) @?= "000000000000edcb5433"
+    it "test-case #300" $ [s|%020X|] (305441741 :: Int32) @?= "0000000000001234ABCD"
+    it "test-case #301" $ [s|%020X|] (3989525555 :: Word32) @?= "000000000000EDCB5433"
+    it "test-case #302" $ [s|%#20o|] (511 :: Int32) @?= "                0777"
+    it "test-case #303" $ [s|%#20o|] (4294966785 :: Word32) @?= "        037777777001"
+    it "test-case #304" $ [s|%#20x|] (305441741 :: Int32) @?= "          0x1234abcd"
+    it "test-case #305" $ [s|%#20x|] (3989525555 :: Word32) @?= "          0xedcb5433"
+    it "test-case #306" $ [s|%#20X|] (305441741 :: Int32) @?= "          0X1234ABCD"
+    it "test-case #307" $ [s|%#20X|] (3989525555 :: Word32) @?= "          0XEDCB5433"
+    it "test-case #308" $ [s|%#020o|] (511 :: Int32) @?= "00000000000000000777"
+    it "test-case #309" $ [s|%#020o|] (4294966785 :: Word32) @?= "00000000037777777001"
+    it "test-case #310" $ [s|%#020x|] (305441741 :: Int32) @?= "0x00000000001234abcd"
+    it "test-case #311" $ [s|%#020x|] (3989525555 :: Word32) @?= "0x0000000000edcb5433"
+    it "test-case #312" $ [s|%#020X|] (305441741 :: Int32) @?= "0X00000000001234ABCD"
+    it "test-case #313" $ [s|%#020X|] (3989525555 :: Word32) @?= "0X0000000000EDCB5433"
+    it "test-case #314" $ [s|%0-20s|] "Hallo" @?= "Hallo               "
+    it "test-case #315" $ [s|%0-20d|] (1024 :: Int32) @?= "1024                "
+    it "test-case #316" $ [s|%0-20d|] (-1024 :: Int32) @?= "-1024               "
+    it "test-case #317" $ [s|%0-20i|] (1024 :: Int32) @?= "1024                "
+    it "test-case #318" $ [s|%0-20i|] (-1024 :: Int32) @?= "-1024               "
+    it "test-case #319" $ [s|%0-20u|] (1024 :: Int32) @?= "1024                "
+    it "test-case #320" $ [s|%0-20u|] (4294966272 :: Word32) @?= "4294966272          "
+    it "test-case #321" $ [s|%-020o|] (511 :: Int32) @?= "777                 "
+    it "test-case #322" $ [s|%-020o|] (4294966785 :: Word32) @?= "37777777001         "
+    it "test-case #323" $ [s|%-020x|] (305441741 :: Int32) @?= "1234abcd            "
+    it "test-case #324" $ [s|%-020x|] (3989525555 :: Word32) @?= "edcb5433            "
+    it "test-case #325" $ [s|%-020X|] (305441741 :: Int32) @?= "1234ABCD            "
+    it "test-case #326" $ [s|%-020X|] (3989525555 :: Word32) @?= "EDCB5433            "
+    it "test-case #327" $ [s|%-020c|] 'x' @?= "x                   "
+    it "test-case #328" $ [s|%*s|] (20 :: Int32) "Hallo" @?= "               Hallo"
+    it "test-case #329" $
+        [s|%*d|] (20 :: Int32) (1024 :: Int32) @?= "                1024"
+    it "test-case #330" $
+        [s|%*d|] (20 :: Int32) (-1024 :: Int32) @?= "               -1024"
+    it "test-case #331" $
+        [s|%*i|] (20 :: Int32) (1024 :: Int32) @?= "                1024"
+    it "test-case #332" $
+        [s|%*i|] (20 :: Int32) (-1024 :: Int32) @?= "               -1024"
+    it "test-case #333" $
+        [s|%*u|] (20 :: Int32) (1024 :: Int32) @?= "                1024"
+    it "test-case #334" $
+        [s|%*u|] (20 :: Int32) (4294966272 :: Word32) @?= "          4294966272"
+    it "test-case #335" $ [s|%*o|] (20 :: Int32) (511 :: Int32) @?= "                 777"
+    it "test-case #336" $
+        [s|%*o|] (20 :: Int32) (4294966785 :: Word32) @?= "         37777777001"
+    it "test-case #337" $
+        [s|%*x|] (20 :: Int32) (305441741 :: Int32) @?= "            1234abcd"
+    it "test-case #338" $
+        [s|%*x|] (20 :: Int32) (3989525555 :: Word32) @?= "            edcb5433"
+    it "test-case #339" $
+        [s|%*X|] (20 :: Int32) (305441741 :: Int32) @?= "            1234ABCD"
+    it "test-case #340" $
+        [s|%*X|] (20 :: Int32) (3989525555 :: Word32) @?= "            EDCB5433"
+    it "test-case #341" $ [s|%*c|] (20 :: Int32) 'x' @?= "                   x"
+    it "test-case #342" $ [s|%.20s|] "Hallo heimur" @?= "Hallo heimur"
+    it "test-case #343" $ [s|%.20d|] (1024 :: Int32) @?= "00000000000000001024"
+    it "test-case #344" $ [s|%.20d|] (-1024 :: Int32) @?= "-00000000000000001024"
+    it "test-case #345" $ [s|%.20i|] (1024 :: Int32) @?= "00000000000000001024"
+    it "test-case #346" $ [s|%.20i|] (-1024 :: Int32) @?= "-00000000000000001024"
+    it "test-case #347" $ [s|%.20u|] (1024 :: Int32) @?= "00000000000000001024"
+    it "test-case #348" $ [s|%.20u|] (4294966272 :: Word32) @?= "00000000004294966272"
+    it "test-case #349" $ [s|%.20o|] (511 :: Int32) @?= "00000000000000000777"
+    it "test-case #350" $ [s|%.20o|] (4294966785 :: Word32) @?= "00000000037777777001"
+    it "test-case #351" $ [s|%.20x|] (305441741 :: Int32) @?= "0000000000001234abcd"
+    it "test-case #352" $ [s|%.20x|] (3989525555 :: Word32) @?= "000000000000edcb5433"
+    it "test-case #353" $ [s|%.20X|] (305441741 :: Int32) @?= "0000000000001234ABCD"
+    it "test-case #354" $ [s|%.20X|] (3989525555 :: Word32) @?= "000000000000EDCB5433"
+    it "test-case #355" $ [s|%20.5s|] "Hallo heimur" @?= "               Hallo"
+    it "test-case #356" $ [s|%20.5d|] (1024 :: Int32) @?= "               01024"
+    it "test-case #357" $ [s|%20.5d|] (-1024 :: Int32) @?= "              -01024"
+    it "test-case #358" $ [s|%20.5i|] (1024 :: Int32) @?= "               01024"
+    it "test-case #359" $ [s|%20.5i|] (-1024 :: Int32) @?= "              -01024"
+    it "test-case #360" $ [s|%20.5u|] (1024 :: Int32) @?= "               01024"
+    it "test-case #361" $ [s|%20.5u|] (4294966272 :: Word32) @?= "          4294966272"
+    it "test-case #362" $ [s|%20.5o|] (511 :: Int32) @?= "               00777"
+    it "test-case #363" $ [s|%20.5o|] (4294966785 :: Word32) @?= "         37777777001"
+    it "test-case #364" $ [s|%20.5x|] (305441741 :: Int32) @?= "            1234abcd"
+    it "test-case #365" $ [s|%20.10x|] (3989525555 :: Word32) @?= "          00edcb5433"
+    it "test-case #366" $ [s|%20.5X|] (305441741 :: Int32) @?= "            1234ABCD"
+    it "test-case #367" $ [s|%20.10X|] (3989525555 :: Word32) @?= "          00EDCB5433"
+    it "test-case #369" $ [s|%020.5d|] (1024 :: Int32) @?= "               01024"
+    it "test-case #370" $ [s|%020.5d|] (-1024 :: Int32) @?= "              -01024"
+    it "test-case #371" $ [s|%020.5i|] (1024 :: Int32) @?= "               01024"
+    it "test-case #372" $ [s|%020.5i|] (-1024 :: Int32) @?= "              -01024"
+    it "test-case #373" $ [s|%020.5u|] (1024 :: Int32) @?= "               01024"
+    it "test-case #374" $ [s|%020.5u|] (4294966272 :: Word32) @?= "          4294966272"
+    it "test-case #375" $ [s|%020.5o|] (511 :: Int32) @?= "               00777"
+    it "test-case #376" $ [s|%020.5o|] (4294966785 :: Word32) @?= "         37777777001"
+    it "test-case #377" $ [s|%020.5x|] (305441741 :: Int32) @?= "            1234abcd"
+    it "test-case #378" $ [s|%020.10x|] (3989525555 :: Word32) @?= "          00edcb5433"
+    it "test-case #379" $ [s|%020.5X|] (305441741 :: Int32) @?= "            1234ABCD"
+    it "test-case #380" $ [s|%020.10X|] (3989525555 :: Word32) @?= "          00EDCB5433"
+    it "test-case #381" $ [s|%.0s|] "Hallo heimur" @?= ""
+    it "test-case #382" $ [s|%20.0s|] "Hallo heimur" @?= "                    "
+    it "test-case #383" $ [s|%.s|] "Hallo heimur" @?= ""
+    it "test-case #384" $ [s|%20.s|] "Hallo heimur" @?= "                    "
+    it "test-case #385" $ [s|%20.0d|] (1024 :: Int32) @?= "                1024"
+    it "test-case #386" $ [s|%20.d|] (-1024 :: Int32) @?= "               -1024"
+    it "test-case #387" $ [s|%20.d|] (0 :: Int32) @?= "                    "
+    it "test-case #388" $ [s|%20.0i|] (1024 :: Int32) @?= "                1024"
+    it "test-case #389" $ [s|%20.i|] (-1024 :: Int32) @?= "               -1024"
+    it "test-case #390" $ [s|%20.i|] (0 :: Int32) @?= "                    "
+    it "test-case #391" $ [s|%20.u|] (1024 :: Int32) @?= "                1024"
+    it "test-case #392" $ [s|%20.0u|] (4294966272 :: Word32) @?= "          4294966272"
+    it "test-case #393" $ [s|%20.u|] (0 :: Word32) @?= "                    "
+    it "test-case #394" $ [s|%20.o|] (511 :: Int32) @?= "                 777"
+    it "test-case #395" $ [s|%20.0o|] (4294966785 :: Word32) @?= "         37777777001"
+    it "test-case #396" $ [s|%20.o|] (0 :: Word32) @?= "                    "
+    it "test-case #397" $ [s|%20.x|] (305441741 :: Int32) @?= "            1234abcd"
+    it "test-case #398" $ [s|%20.0x|] (3989525555 :: Word32) @?= "            edcb5433"
+    it "test-case #399" $ [s|%20.x|] (0 :: Word32) @?= "                    "
+    it "test-case #400" $ [s|%20.X|] (305441741 :: Int32) @?= "            1234ABCD"
+    it "test-case #401" $ [s|%20.0X|] (3989525555 :: Word32) @?= "            EDCB5433"
+    it "test-case #402" $ [s|%20.X|] (0 :: Word32) @?= "                    "
+    it "test-case #403" $
+        [s|% -0+*.*s|] (20 :: Int32) (5 :: Int32) "Hallo heimur" @?=
+        "Hallo               "
+    it "test-case #404" $
+        [s|% -0+*.*d|] (20 :: Int32) (5 :: Int32) (1024 :: Int32) @?=
+        "+01024              "
+    it "test-case #405" $
+        [s|% -0+*.*d|] (20 :: Int32) (5 :: Int32) (-1024 :: Int32) @?=
+        "-01024              "
+    it "test-case #406" $
+        [s|% -0+*.*i|] (20 :: Int32) (5 :: Int32) (1024 :: Int32) @?=
+        "+01024              "
+    it "test-case #407" $
+        [s|% 0-+*.*i|] (20 :: Int32) (5 :: Int32) (-1024 :: Int32) @?=
+        "-01024              "
+    it "test-case #408" $
+        [s|% 0-+*.*u|] (20 :: Int32) (5 :: Int32) (1024 :: Int32) @?=
+        "01024               "
+    it "test-case #409" $
+        [s|% 0-+*.*u|] (20 :: Int32) (5 :: Int32) (4294966272 :: Word32) @?=
+        "4294966272          "
+    it "test-case #410" $
+        [s|%+ -0*.*o|] (20 :: Int32) (5 :: Int32) (511 :: Int32) @?=
+        "00777               "
+    it "test-case #411" $
+        [s|%+ -0*.*o|] (20 :: Int32) (5 :: Int32) (4294966785 :: Word32) @?=
+        "37777777001         "
+    it "test-case #412" $
+        [s|%+ -0*.*x|] (20 :: Int32) (5 :: Int32) (305441741 :: Int32) @?=
+        "1234abcd            "
+    it "test-case #413" $
+        [s|%+ -0*.*x|] (20 :: Int32) (10 :: Int32) (3989525555 :: Word32) @?=
+        "00edcb5433          "
+    it "test-case #414" $
+        [s|% -+0*.*X|] (20 :: Int32) (5 :: Int32) (305441741 :: Int32) @?=
+        "1234ABCD            "
+    it "test-case #415" $
+        [s|% -+0*.*X|] (20 :: Int32) (10 :: Int32) (3989525555 :: Word32) @?=
+        "00EDCB5433          "
+    it "test-case #416" $ [s|%*sx|] (-3 :: Int32) "hi" @?= "hi x"
diff --git a/tests/printf/format.hs b/tests/printf/format.hs
new file mode 100644
--- /dev/null
+++ b/tests/printf/format.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -Wwarn #-}
+
+module Main where
+
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+import Foreign.Ptr
+import GeneratedSpec
+import Language.Haskell.Printf
+import Test.HUnit
+import Test.Hspec
+
+main :: IO ()
+main = hspec $ describe "th-printf" $ do
+  GeneratedSpec.spec
+  it "text" $ do
+    -- sanity checking that text can be rendered
+    -- all the actual string formatting is in GeneratedSpec
+    [s|Hello, %Q!|] (S.pack "world") @?= "Hello, world!"
+    [s|Hello, %q!|] (L.pack "world") @?= "Hello, world!"
+  it "hexadecimal float" $ do
+    [s|%a|] 0.857421875 @?= "0x1.b7p-1"
+    [s|%A|] 3.1415926 @?= "0X1.921FB4D12D84AP+1"
+    [s|%.3a|] 1.999999999 @?= "0x1.000p+1"
+    [s|%.0a|] 1.999999999 @?= "0x1p+1"
+    [s|%#.0a|] 1.999999999 @?= "0x1.p+1"
+    [s|%.3a|] 0.7576 @?= "0x1.83ep-1"
+    [s|%015.3a|] 0.7576 @?= "0x000001.83ep-1"
+    [s|% 15.3a|] 0.7576 @?= "     0x1.83ep-1"
+  it "Show instances" $ do
+    [s|%?|] () @?= "()"
+    [s|%10?|] () @?= "        ()"
+  it "pointer" $ do
+    [s|%p|] nullPtr @?= "0x0"
+    [s|%15p|] fakePtr @?= "     0xdeadbeef"
+    -- sign flag does nothing
+    [s|%+p|] fakePtr @?= "0xdeadbeef"
+    -- prefix flag does nothing
+    [s|%#p|] fakePtr @?= "0xdeadbeef"
+    -- zero flag does nothing
+    [s|%015p|] fakePtr @?= "     0xdeadbeef"
+    -- left-pad flag does nothing
+    [s|%-15p|] fakePtr @?= "     0xdeadbeef"
+
+fakePtr :: Ptr ()
+fakePtr = nullPtr `plusPtr` 0xdeadbeef
diff --git a/th-printf.cabal b/th-printf.cabal
--- a/th-printf.cabal
+++ b/th-printf.cabal
@@ -1,34 +1,90 @@
-name:                th-printf
-version:             0.3.1
-synopsis:            Compile-time printf
-description:         Quasiquoters for printf: string, bytestring, text.
-homepage:            https://github.com/joelteon/th-printf
-license:             MIT
-license-file:        LICENSE
-author:              Joel Taylor
-maintainer:          me@joelt.io
-category:            Text
-build-type:          Simple
-cabal-version:       >=1.10
+cabal-version: 1.12
 
-library
-  hs-source-dirs:      src
-  exposed-modules:     Text.Printf.TH
-  build-depends:       base <4.10
-                     , attoparsec
-                     , bytestring
-                     , template-haskell
-                     , text
-                     , transformers
-  default-language:    Haskell2010
+-- This file has been generated from package.yaml by hpack version 0.35.3.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 7e86d199e67bceb4c613b25421b603adc41dcb4508283966e741b036d0075e3d
 
+name:           th-printf
+version:        0.8
+synopsis:       Quasiquoters for printf
+description:    Quasiquoters for string and text printf
+category:       Text
+homepage:       https://github.com/pikajude/th-printf#readme
+bug-reports:    https://github.com/pikajude/th-printf/issues
+author:         Jude Taylor
+maintainer:     me@jude.xyz
+license:        MIT
+license-file:   LICENSE
+tested-with:
+    GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.7, GHC == 9.0.2, GHC == 9.2.7, GHC == 9.4.5, GHC == 9.6.2
+build-type:     Simple
+extra-source-files:
+    CHANGELOG.md
+
 source-repository head
   type: git
-  location: git://github.com/joelteon/th-printf.git
+  location: https://github.com/pikajude/th-printf
 
+flag werror
+  description: Build with -Werror
+  manual: True
+  default: False
+
+library
+  exposed-modules:
+      Language.Haskell.Printf
+      Language.Haskell.Printf.Lib
+  other-modules:
+      Buf
+      Language.Haskell.Printf.Geometry
+      Language.Haskell.Printf.Printers
+      Language.Haskell.PrintfArg
+      NumUtils
+      StrUtils
+      Parser
+      Parser.Types
+      Paths_th_printf
+  hs-source-dirs:
+      src
+      parser
+  ghc-options: -Wall
+  build-depends:
+      base >=4.12 && <5
+    , charset
+    , containers
+    , dlist
+    , integer-logarithms
+    , microlens-platform
+    , mtl
+    , parsec
+    , semigroups
+    , template-haskell
+    , text
+    , th-lift
+    , transformers
+  default-language: Haskell2010
+  if flag(werror)
+    ghc-options: -Werror
+
 test-suite format
   type: exitcode-stdio-1.0
   main-is: format.hs
-  hs-source-dirs: tests
-  build-depends: base, bytestring, hspec, HUnit, QuickCheck, template-haskell, text, th-printf
-  default-language:    Haskell2010
+  other-modules:
+      GeneratedSpec
+      Paths_th_printf
+  hs-source-dirs:
+      tests/printf
+  ghc-options: -Wall -fno-warn-type-defaults
+  build-depends:
+      HUnit
+    , QuickCheck
+    , base >=4.12 && <5
+    , hspec
+    , template-haskell
+    , text
+    , th-printf
+  default-language: Haskell2010
+  if flag(werror)
+    ghc-options: -Werror
