diff --git a/Changelog.md b/Changelog.md
new file mode 100644
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,9 @@
+# 0.2.1.0
+* add naive stderr interpreters for `DataLog` and `Log`.
+
+# 0.2.0.0
+
+* `DataLog` got a second constructor, `Local`. It takes a higher-order `Sem` and a transformation function, the latter
+  of which is applied to all messages logged within the former.
+  This allows context manipulation for blocks of code.
+* `interceptDataLogConc` adds support for concurrent processing of log messages to any interpretation of `DataLog`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,34 @@
+Copyright (c) 2020 Torsten Schmits
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+  disclaimer.
+  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+  disclaimer in the documentation and/or other materials provided with the distribution.
+
+Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those
+receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except
+for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell,
+import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired
+or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
+
+  (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of
+  contributors, in source or binary form) alone; or
+  (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such
+  copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to
+  be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
+
+Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this
+license, whether expressly, by implication, estoppel or otherwise.
+
+DISCLAIMER
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/exon.cabal b/exon.cabal
new file mode 100644
--- /dev/null
+++ b/exon.cabal
@@ -0,0 +1,202 @@
+cabal-version: 2.2
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           exon
+version:        0.1.0.0
+synopsis:       Monoidal Quasiquote Interpolation
+description:    See <https://hackage.haskell.org/package/exon/docs/Exon.html>
+category:       String
+homepage:       https://github.com/tek/exon#readme
+bug-reports:    https://github.com/tek/exon/issues
+author:         Torsten Schmits
+maintainer:     haskell@tryp.io
+copyright:      2021 Torsten Schmits
+license:        BSD-2-Clause-Patent
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    readme.md
+    Changelog.md
+
+source-repository head
+  type: git
+  location: https://github.com/tek/exon
+
+library
+  exposed-modules:
+      Exon
+      Exon.Class.Exon
+      Exon.Combinators
+      Exon.Data.RawSegment
+      Exon.Data.Result
+      Exon.Data.Segment
+      Exon.Debug
+      Exon.Parse
+      Exon.Prelude
+      Exon.Quote
+  other-modules:
+      Prelude
+      Paths_exon
+  autogen-modules:
+      Paths_exon
+  hs-source-dirs:
+      lib
+  default-extensions:
+      AllowAmbiguousTypes
+      ApplicativeDo
+      BangPatterns
+      BinaryLiterals
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DefaultSignatures
+      DeriveAnyClass
+      DeriveDataTypeable
+      DeriveFoldable
+      DeriveFunctor
+      DeriveGeneric
+      DeriveTraversable
+      DerivingStrategies
+      DerivingVia
+      DisambiguateRecordFields
+      DoAndIfThenElse
+      DuplicateRecordFields
+      EmptyDataDecls
+      ExistentialQuantification
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTs
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      KindSignatures
+      LambdaCase
+      LiberalTypeSynonyms
+      MultiParamTypeClasses
+      MultiWayIf
+      NamedFieldPuns
+      OverloadedStrings
+      OverloadedLists
+      PackageImports
+      PartialTypeSignatures
+      PatternGuards
+      PatternSynonyms
+      PolyKinds
+      QuantifiedConstraints
+      QuasiQuotes
+      RankNTypes
+      RecordWildCards
+      RecursiveDo
+      ScopedTypeVariables
+      StandaloneDeriving
+      TemplateHaskell
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeFamilyDependencies
+      TypeOperators
+      TypeSynonymInstances
+      UndecidableInstances
+      UnicodeSyntax
+      ViewPatterns
+  ghc-options: -Wall -Wredundant-constraints -Wsimplifiable-class-constraints -Wunused-type-patterns
+  build-depends:
+      base ==4.*
+    , flatparse ==0.2.*
+    , haskell-src-exts <1.24
+    , haskell-src-meta <0.9
+    , relude >=0.7 && <1.2
+    , template-haskell
+    , text
+  mixins:
+      base hiding (Prelude)
+  default-language: Haskell2010
+
+test-suite exon-unit
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      Exon.Test.BasicTest
+      Exon.Test.Quote
+      Paths_exon
+  hs-source-dirs:
+      test
+  default-extensions:
+      AllowAmbiguousTypes
+      ApplicativeDo
+      BangPatterns
+      BinaryLiterals
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DefaultSignatures
+      DeriveAnyClass
+      DeriveDataTypeable
+      DeriveFoldable
+      DeriveFunctor
+      DeriveGeneric
+      DeriveTraversable
+      DerivingStrategies
+      DerivingVia
+      DisambiguateRecordFields
+      DoAndIfThenElse
+      DuplicateRecordFields
+      EmptyDataDecls
+      ExistentialQuantification
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTs
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      KindSignatures
+      LambdaCase
+      LiberalTypeSynonyms
+      MultiParamTypeClasses
+      MultiWayIf
+      NamedFieldPuns
+      OverloadedStrings
+      OverloadedLists
+      PackageImports
+      PartialTypeSignatures
+      PatternGuards
+      PatternSynonyms
+      PolyKinds
+      QuantifiedConstraints
+      QuasiQuotes
+      RankNTypes
+      RecordWildCards
+      RecursiveDo
+      ScopedTypeVariables
+      StandaloneDeriving
+      TemplateHaskell
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeFamilyDependencies
+      TypeOperators
+      TypeSynonymInstances
+      UndecidableInstances
+      UnicodeSyntax
+      ViewPatterns
+  ghc-options: -Wall -Wredundant-constraints -Wsimplifiable-class-constraints -Wunused-type-patterns -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base ==4.*
+    , exon
+    , flatparse ==0.2.*
+    , haskell-src-exts <1.24
+    , haskell-src-meta <0.9
+    , hedgehog
+    , relude >=0.7 && <1.2
+    , tasty
+    , tasty-hedgehog
+    , template-haskell
+    , text
+  mixins:
+      base hiding (Prelude)
+    , exon hiding (Prelude)
+    , exon (Exon.Prelude as Prelude)
+  default-language: Haskell2010
diff --git a/lib/Exon.hs b/lib/Exon.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon.hs
@@ -0,0 +1,48 @@
+module Exon (
+  -- $intro
+  module Exon.Quote,
+  -- * Customizing Concatenation
+  module Exon.Class.Exon,
+  -- * Combinators
+  module Exon.Combinators,
+  -- * Data Types
+  module Exon.Data.Segment,
+  module Exon.Data.Result,
+) where
+
+import Exon.Class.Exon (Exon (..), ExonDefault)
+import Exon.Data.Result (Result (..))
+import Exon.Data.Segment (Segment (..))
+import Exon.Quote (exon)
+import Exon.Combinators (intercalate)
+
+-- $intro
+-- This Haskell library provides quasiquote string interpolation with customizable concatenation for arbitrary types.
+--
+-- The default case uses `Monoid` and `IsString`:
+--
+-- @
+-- import Exon (exon)
+-- import Data.Text (toUpper)
+--
+-- newtype Name =
+--   Name Text
+--   deriving newtype (Show, Monoid, IsString)
+--
+-- instance Semigroup Name where
+--   Name l <> Name r = Name (l <> " | " <> r)
+--
+-- lastName :: Name
+-- lastName = "Fry"
+--
+-- up :: Name -> Name
+-- up (Name name) = Name (toUpper name)
+--
+-- >>> [exon|Philip J. #{up lastName}|]
+-- Name "Philip | J. | FRY"
+--
+-- @
+--
+-- Individual segments are tokenized at whitespace boundaries, expressions between `#{` and `}` are inserted verbatim.
+--
+-- The default implementation ignores whitespace when concatenating, while it is preserved for `String`, `Text` etc.
diff --git a/lib/Exon/Class/Exon.hs b/lib/Exon/Class/Exon.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Class/Exon.hs
@@ -0,0 +1,164 @@
+-- |Description: Internal
+module Exon.Class.Exon where
+
+import Exon.Data.Result (Result (Empty, Result))
+import qualified Exon.Data.Segment as Segment
+import Exon.Data.Segment (Segment)
+
+data ExonDefault
+
+data KeepWhitespace
+
+{- |
+This class is responsible for combining segments of an interpolated string, allowing users to define their own rules
+for how the result is constructed.
+The default implementation converts each literal part with 'IsString' and uses the result type's 'Monoid' to
+concatenate them.
+
+The raw parts are encoded as 'Segment', getting combined into a 'Result'.
+
+The default for 'convertSegment' skips whitespace by encoding it into the 'Result' constructor 'Empty', which is a
+unit object.
+To change this behavior, it can be easily overridden:
+
+@
+newtype Thing = Thing String deriving newtype (IsString, Semigroup, Monoid, Show)
+
+instance Exon ExonDefault Thing where
+  convertSegment = \case
+    Segment.String s -> Result (Thing s)
+    Segment.Expression thing -> Result thing
+    Segment.Whitespace _ -> Result (Thing " >>> ")
+
+  insertWhitespace s1 ws s2 =
+    appendSegment @ExonDefault (appendSegment @ExonDefault s1 (Segment.Whitespace ws)) s2
+@
+-}
+class Exon (tag :: Type) (a :: Type) where
+
+  -- |This check is used to allow empty expression segments to be skipped when they are empty.
+  -- The default is to never skip expressions.
+  isEmpty :: a -> Bool
+  isEmpty =
+    const False
+
+  -- |Convert a 'Segment' to a 'Result'.
+  -- The default implementation uses 'IsString' and ignores whitespace, returning 'Empty'.
+  convertSegment :: Segment a -> Result a
+
+  default convertSegment :: IsString a => Segment a -> Result a
+  convertSegment = \case
+    Segment.String a ->
+      Result (fromString a)
+    Segment.Expression a | isEmpty @tag a ->
+      Empty
+    Segment.Expression a ->
+      Result a
+    Segment.Whitespace _ ->
+      Empty
+
+  -- |Append a 'Segment' to a 'Result'.
+  -- The default implementation uses '(<>)'.
+  appendSegment :: Result a -> Segment a -> Result a
+
+  default appendSegment :: Semigroup a => Result a -> Segment a -> Result a
+  appendSegment z a =
+    z <> convertSegment @tag a
+
+  -- |Append whitespace and a 'Segment' to a 'Result', i.e. joining two parts of the interpolation by whitespace.
+  -- The default implementation ignores the whitespace, calling 'appendSegment' with the second argument.
+  insertWhitespace :: Result a -> String -> Segment a -> Result a
+
+  default insertWhitespace :: Result a -> String -> Segment a -> Result a
+  insertWhitespace s1 _ s2 =
+    appendSegment @tag s1 s2
+
+  -- |The entry point for concatenation, taking a list of segments parsed from the interpolation.
+  -- The default implementation skips leading whitespace and calls 'appendSegment' and 'insertWhitespace' to
+  -- concatenate.
+  concatSegments :: NonEmpty (Segment a) -> a
+
+  default concatSegments :: Monoid a => NonEmpty (Segment a) -> a
+  concatSegments (h :| t) =
+    fold (spin (convertSegment @tag h) t)
+    where
+      spin :: Result a -> [Segment a] -> Result a
+      spin Empty = \case
+        [] ->
+          Empty
+        Segment.Whitespace _ : ss ->
+          spin Empty ss
+        s1 : ss ->
+          spin (convertSegment @tag s1) ss
+      spin (Result s1) = \case
+        [] ->
+          Result s1
+        Segment.Whitespace _ : (Segment.Expression a) : ss | isEmpty @tag a ->
+          spin (Result s1) ss
+        Segment.Whitespace ws : s2 : ss ->
+          spin (insertWhitespace @tag (Result s1) ws s2) ss
+        [Segment.Whitespace _] ->
+          (Result s1)
+        s2 : ss ->
+          spin (appendSegment @tag (Result s1) s2) ss
+
+instance {-# overlappable #-} (
+    Monoid a,
+    IsString a
+  ) => Exon ExonDefault a where
+
+convertKeepWs ::
+  IsString a =>
+  Segment a ->
+  Result a
+convertKeepWs = \case
+  Segment.String a ->
+    Result (fromString a)
+  Segment.Expression a ->
+    Result a
+  Segment.Whitespace a ->
+    Result (fromString a)
+
+concatKeepWs ::
+  ∀ tag a .
+  Monoid a =>
+  Exon tag a =>
+  NonEmpty (Segment a) ->
+  a
+concatKeepWs =
+  fold . foldl' (appendSegment @tag) Empty
+
+instance Exon ExonDefault String where
+  convertSegment =
+    convertKeepWs
+
+  concatSegments =
+    concatKeepWs @ExonDefault
+
+instance Exon ExonDefault Text where
+  convertSegment =
+    convertKeepWs
+
+  concatSegments =
+    concatKeepWs @ExonDefault
+
+instance Exon ExonDefault LText where
+  convertSegment =
+    convertKeepWs
+
+  concatSegments =
+    concatKeepWs @ExonDefault
+
+instance Exon ExonDefault ByteString where
+  convertSegment =
+    convertKeepWs
+
+  concatSegments =
+    concatKeepWs @ExonDefault
+
+instance Exon ExonDefault LByteString where
+  convertSegment =
+    convertKeepWs
+
+  concatSegments =
+    concatKeepWs @ExonDefault
diff --git a/lib/Exon/Combinators.hs b/lib/Exon/Combinators.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Combinators.hs
@@ -0,0 +1,19 @@
+-- |Description: Internal
+module Exon.Combinators where
+
+import Prelude hiding (intercalate)
+
+-- |Monoidally combine all elements in the list, appending the separator between each pair of elements.
+intercalate ::
+  Monoid a =>
+  Foldable t =>
+  a ->
+  t a ->
+  a
+intercalate sep =
+  fold . foldl' f Nothing
+  where
+    f Nothing a =
+      Just a
+    f (Just z) a =
+      Just (z <> sep <> a)
diff --git a/lib/Exon/Data/RawSegment.hs b/lib/Exon/Data/RawSegment.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Data/RawSegment.hs
@@ -0,0 +1,9 @@
+module Exon.Data.RawSegment where
+
+data RawSegment =
+  WsSegment String
+  |
+  StringSegment String
+  |
+  ExpSegment String
+  deriving (Eq, Show)
diff --git a/lib/Exon/Data/Result.hs b/lib/Exon/Data/Result.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Data/Result.hs
@@ -0,0 +1,18 @@
+-- |Description: Internal
+module Exon.Data.Result where
+
+-- |The combined segments, either empty or a value.
+data Result a =
+  Empty
+  |
+  Result a 
+  deriving (Eq, Show, Foldable)
+
+instance Semigroup a => Semigroup (Result a) where
+  (<>) Empty a = a
+  (<>) a Empty = a
+  (<>) (Result l) (Result r) = Result (l <> r)
+
+instance Monoid a => Monoid (Result a) where
+  mempty =
+    Result mempty
diff --git a/lib/Exon/Data/Segment.hs b/lib/Exon/Data/Segment.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Data/Segment.hs
@@ -0,0 +1,12 @@
+-- |Description: Internal
+module Exon.Data.Segment where
+
+-- |The parts of an interpolation quasiquote.
+-- Text is split at each whitespace and interpolation splice marked by @#{@ and @}@.
+data Segment a =
+  String String
+  |
+  Whitespace String
+  |
+  Expression a
+  deriving (Eq, Show)
diff --git a/lib/Exon/Debug.hs b/lib/Exon/Debug.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Debug.hs
@@ -0,0 +1,100 @@
+{-# language NoImplicitPrelude #-}
+
+module Exon.Debug where
+
+import qualified Data.Text as Text
+import GHC.Stack (SrcLoc (..))
+import Relude
+import System.IO.Unsafe (unsafePerformIO)
+
+srcLoc :: CallStack -> SrcLoc
+srcLoc = \case
+  (getCallStack -> (_, loc) : _) -> loc
+  _ -> error "Debug.srcLoc: empty CallStack"
+
+debugPrint ::
+  SrcLoc ->
+  Text ->
+  IO ()
+debugPrint SrcLoc{srcLocModule = toText -> slm, srcLocStartLine} msg =
+  putStrLn (moduleName <> ":" <> show srcLocStartLine <> " " <> toString msg)
+  where
+    moduleName =
+      toString $ fromMaybe slm $ listToMaybe $ reverse $ Text.splitOn "." slm
+
+debugPrintWithLoc ::
+  Monad m =>
+  SrcLoc ->
+  Text ->
+  m ()
+debugPrintWithLoc loc msg = do
+  () <- return (unsafePerformIO (debugPrint loc msg))
+  pure ()
+
+dbg ::
+  HasCallStack =>
+  Monad m =>
+  Text ->
+  m ()
+dbg =
+  debugPrintWithLoc (srcLoc callStack)
+{-# inline dbg #-}
+
+dbgsWith ::
+  HasCallStack =>
+  Monad m =>
+  Show a =>
+  Text ->
+  a ->
+  m ()
+dbgsWith prefix a =
+  debugPrintWithLoc (srcLoc callStack) (prefix <> ": " <> show a)
+{-# inline dbgsWith #-}
+
+dbgs ::
+  HasCallStack =>
+  Monad m =>
+  Show a =>
+  a ->
+  m ()
+dbgs a =
+  debugPrintWithLoc (srcLoc callStack) (show a)
+{-# inline dbgs_ #-}
+
+dbgs_ ::
+  HasCallStack =>
+  Monad m =>
+  Show a =>
+  a ->
+  m a
+dbgs_ a =
+  a <$ debugPrintWithLoc (srcLoc callStack) (show a)
+{-# inline dbgs #-}
+
+tr ::
+  HasCallStack =>
+  Text ->
+  a ->
+  a
+tr msg a =
+  unsafePerformIO (a <$ debugPrint (srcLoc callStack) msg)
+{-# inline tr #-}
+
+trs ::
+  Show a =>
+  HasCallStack =>
+  a ->
+  a
+trs a =
+  unsafePerformIO (a <$ debugPrint (srcLoc callStack) (show a))
+{-# inline trs #-}
+
+trs' ::
+  Show b =>
+  HasCallStack =>
+  b ->
+  a ->
+  a
+trs' b a =
+  unsafePerformIO (a <$ debugPrint (srcLoc callStack) (show b))
+{-# inline trs' #-}
diff --git a/lib/Exon/Parse.hs b/lib/Exon/Parse.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Parse.hs
@@ -0,0 +1,98 @@
+-- |Description: Internal
+module Exon.Parse where
+
+import Data.Char (isSpace)
+import qualified FlatParse.Stateful as FlatParse
+import FlatParse.Stateful (
+  Result (Err, Fail, OK),
+  anyChar,
+  branch,
+  char,
+  empty,
+  eof,
+  get,
+  inSpan,
+  lookahead,
+  modify,
+  put,
+  runParserS,
+  satisfy,
+  some_,
+  spanned,
+  string,
+  takeRest,
+  (<|>),
+  )
+import Prelude hiding (empty, get, modify, put, span, (<|>))
+
+import Exon.Data.RawSegment (RawSegment (ExpSegment, StringSegment, WsSegment))
+
+type Parser =
+  FlatParse.Parser Text
+
+span :: Parser () -> Parser String
+span seek =
+  spanned seek \ _ sp -> inSpan sp takeRest
+
+ws :: Parser Char
+ws =
+  satisfy isSpace
+
+whitespace :: Parser RawSegment
+whitespace =
+  WsSegment <$> span (some_ ws)
+
+before ::
+  Parser a ->
+  Parser () ->
+  Parser () ->
+  Parser ()
+before =
+  branch . lookahead
+
+finishBefore ::
+  Parser a ->
+  Parser () ->
+  Parser ()
+finishBefore cond =
+  before (lookahead cond) pass
+
+expr :: Parser ()
+expr =
+  branch $(char '{') (modify (1 +) *> expr) $
+  before $(char '}') closing (anyChar *> expr)
+  where
+    closing =
+      get >>= \case
+        0 -> pass
+        cur -> put (cur - 1) *> $(char '}') *> expr
+
+interpolation :: Parser RawSegment
+interpolation =
+  $(string "#{") *> (ExpSegment <$> span expr) <* $(char '}')
+
+untilTokenEnd :: Parser ()
+untilTokenEnd =
+  branch $(char '\\') (anyChar *> untilTokenEnd) $
+  finishBefore ($(string "#{") <|> void ws) $
+  eof <|> (anyChar *> untilTokenEnd)
+
+text :: Parser RawSegment
+text =
+  StringSegment <$> span untilTokenEnd
+
+segment :: Parser RawSegment
+segment =
+  branch eof empty (whitespace <|> interpolation <|> text)
+
+parser :: Parser [RawSegment]
+parser =
+  FlatParse.many segment
+
+parse :: String -> Either Text [RawSegment]
+parse =
+  runParserS parser 0 0 >>> \case
+    OK a _ "" -> Right a
+    OK _ _ u -> Left ("unconsumed: " <> decodeUtf8 u)
+    Fail -> Left "fail"
+    Err e -> Left e
diff --git a/lib/Exon/Prelude.hs b/lib/Exon/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Prelude.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Exon.Prelude (
+  module Data.Kind,
+  module Exon.Debug,
+  module GHC.Err,
+  module Relude,
+) where
+
+import Data.Kind (Type)
+import GHC.Err (undefined)
+
+import Exon.Debug (dbg, dbgs, dbgs_, tr, trs, trs')
+import Relude hiding (Type, undefined)
diff --git a/lib/Exon/Quote.hs b/lib/Exon/Quote.hs
new file mode 100644
--- /dev/null
+++ b/lib/Exon/Quote.hs
@@ -0,0 +1,108 @@
+-- |Description: Internal
+module Exon.Quote where
+
+import Data.Traversable (for)
+import Language.Haskell.Exts (
+  Extension,
+  ParseMode (extensions),
+  ParseResult (ParseFailed, ParseOk),
+  defaultParseMode,
+  parseExpWithMode,
+  parseExtension,
+  )
+import Language.Haskell.Meta (toExp)
+import Language.Haskell.TH (Exp (AppE, InfixE, ListE), Q, extsEnabled, runQ)
+import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter))
+import Language.Haskell.TH.Syntax (Quasi)
+
+import Exon.Class.Exon (ExonDefault, concatSegments)
+import Exon.Data.RawSegment (RawSegment (ExpSegment, StringSegment, WsSegment))
+import qualified Exon.Data.Segment as Segment
+import Exon.Parse (parse)
+
+exonError ::
+  ToString e =>
+  MonadFail m =>
+  e ->
+  m a
+exonError err =
+  fail ("Exon: " <> toString err)
+
+segmentsQ ::
+  QOrIO m =>
+  String ->
+  m (NonEmpty RawSegment)
+segmentsQ =
+  parse >>> fmap nonEmpty >>> \case
+    Right (Just segs) -> pure segs
+    Right Nothing -> pure (pure (StringSegment ""))
+    Left err -> exonError err
+
+class Quasi m => QOrIO (m :: Type -> Type) where
+  fileExtensions :: m [Extension]
+
+instance QOrIO IO where
+  fileExtensions =
+    pure []
+
+instance QOrIO Q where
+  fileExtensions =
+    fmap (fmap (parseExtension . show)) extsEnabled
+
+reifyExp ::
+  QOrIO m =>
+  String ->
+  m Exp
+reifyExp s = do
+  exts <- fileExtensions
+  case parseExpWithMode defaultParseMode { extensions = exts } s of
+    ParseFailed _ err -> exonError err
+    ParseOk e -> pure (toExp e)
+
+reifySegments ::
+  QOrIO m =>
+  NonEmpty RawSegment ->
+  m (NonEmpty Exp)
+reifySegments segs = do
+  expCon <- runQ [e|Segment.Expression|]
+  for segs \case
+    StringSegment s ->
+      runQ [e|Segment.String s|]
+    ExpSegment s -> do
+      e <- reifyExp s
+      pure (AppE expCon e)
+    WsSegment s ->
+     runQ [e|Segment.Whitespace s|]
+
+quoteExp ::
+  QOrIO m =>
+  String ->
+  m Exp
+quoteExp code = do
+  raw <- segmentsQ code
+  hseg :| segs <- reifySegments raw
+  conc <- runQ [e|concatSegments @ExonDefault|]
+  consE <- runQ [e|(:|)|]
+  pure (AppE conc (InfixE (Just hseg) consE (Just (ListE segs))))
+
+-- |A quasiquoter that allows interpolation, concatenating the resulting segments monoidally.
+--
+-- >>> [exon|write #{show @Text (5 :: Int)} lines of code|] :: Text
+-- "write 5 lines of code"
+--
+-- The default implementation for any non-stringly type uses 'IsString' to construct the literal segments and 'mappend'
+-- to combine them, ignoring whitespace segments.
+--
+-- >>> newtype Part = Part Text deriving newtype (Show, Semigroup, Monoid, IsString)
+--
+-- >>> [exon|x #{Part "y"}z|] :: Part
+-- Part "xyz"
+--
+-- This behavior can be customized by writing an instance of 'Exon.Exon'.
+exon :: QuasiQuoter
+exon =
+  QuasiQuoter quoteExp (err "pattern") (err "type") (err "decl")
+  where
+    err :: String -> String -> Q a
+    err tpe _ =
+      exonError ("Cannot quote " <> tpe)
diff --git a/lib/Prelude.hs b/lib/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/lib/Prelude.hs
@@ -0,0 +1,5 @@
+module Prelude (
+  module Exon.Prelude,
+) where
+
+import Exon.Prelude
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,66 @@
+This Haskell library provides quasiquote string interpolation with customizable concatenation for arbitrary types.
+
+The default case uses `Monoid` and `IsString`:
+
+```haskell
+import Exon (exon)
+import Data.Text (toUpper)
+
+newtype Name =
+  Name Text
+  deriving newtype (Show, Monoid, IsString)
+
+instance Semigroup Name where
+  Name l <> Name r = Name (l <> " | " <> r)
+
+lastName :: Name
+lastName = "Fry"
+
+up :: Name -> Name
+up (Name name) = Name (toUpper name)
+
+>>> [exon|Philip J. #{up lastName}|]
+Name "Philip | J. | FRY"
+```
+
+Individual segments are tokenized at whitespace boundaries, expressions between `#{` and `}` are inserted verbatim.
+
+The default implementation ignores whitespace when concatenating, while it is preserved for `String`, `Text` etc.
+
+# Customization
+
+Concatenation is performed by the class `Exon.Exon`:
+
+```haskell
+class Exon (tag :: Type) (a :: Type) where
+  convertSegment :: Segment a -> Result a
+
+  appendSegment :: Result a -> Segment a -> Result a
+
+  insertWhitespace :: Result a -> String -> Segment a -> Result a
+
+  concatSegments :: NonEmpty (Segment a) -> a
+```
+
+All methods have default implementations.
+The `tag` parameter is an arbitrary type that allows the creation of different quoters, with `exon` using the tag
+`ExonDefault.`
+
+In order to get the default quoter to support custom rules for a type, one simply has to write an instance:
+
+```haskell
+import Exon (Exon, ExonDefault, Result)
+import qualified Exon as Segment (Segment(..))
+
+instance Exon ExonDefault Name where
+  convertSegment = \case
+    Segment.String s -> Result (Name s)
+    Segment.Expression name -> Result name
+    Segment.Whitespace ws -> Result (Name " >>> ")
+```
+
+# Acknowledgments
+
+Inspired by the magnificent [string-interpolate].
+
+[string-interpolate]: https://hackage.haskell.org/package/string-interpolate
diff --git a/test/Exon/Test/BasicTest.hs b/test/Exon/Test/BasicTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Exon/Test/BasicTest.hs
@@ -0,0 +1,63 @@
+module Exon.Test.BasicTest where
+
+import Data.Text (toUpper)
+import Hedgehog (TestT, (===))
+
+import Exon.Class.Exon (Exon(..), ExonDefault)
+import qualified Exon.Data.Segment as Segment
+import Exon.Quote (exon)
+import Exon.Data.Result (Result(Result))
+
+newtype Mon =
+  Mon String
+  deriving (Eq, Show)
+  deriving newtype (IsString)
+
+instance Semigroup Mon where
+  Mon l <> Mon r =
+    Mon (l <> r)
+
+instance Monoid Mon where
+  mempty =
+    Mon ""
+
+newtype Name =
+  Name Text
+  deriving newtype (Eq, Show, Monoid, IsString)
+
+instance Semigroup Name where
+  Name l <> Name r =
+    Name (l <> " | " <> r)
+
+lastName :: Name
+lastName = "Fry"
+
+up :: Name -> Name
+up (Name name) =
+  Name (toUpper name)
+
+test_basic :: TestT IO ()
+test_basic = do
+  Mon "fooandbar" === [exon|foo
+    #{var} bar|]
+  "foo and \\#{bar}" === ([exon|foo #{var} \#{bar}|] :: Text)
+  "Philip | J. | FRY" === [exon|Philip J. #{up lastName}|]
+  where
+    var :: IsString a => a
+    var =
+      "and"
+
+newtype Thing = Thing String deriving newtype (IsString, Semigroup, Monoid, Show, Eq)
+
+instance Exon ExonDefault Thing where
+  convertSegment = \case
+    Segment.String s -> Result (Thing s)
+    Segment.Expression thing -> Result thing
+    Segment.Whitespace _ -> Result (Thing " >>> ")
+
+  insertWhitespace s1 ws s2 =
+    appendSegment @ExonDefault (appendSegment @ExonDefault s1 (Segment.Whitespace ws)) s2
+
+test_keepWhitespace :: TestT IO ()
+test_keepWhitespace =
+  Thing "1 >>> 2 >>> 3" === [exon|1 #{Thing "2"}    3|]
diff --git a/test/Exon/Test/Quote.hs b/test/Exon/Test/Quote.hs
new file mode 100644
--- /dev/null
+++ b/test/Exon/Test/Quote.hs
@@ -0,0 +1,17 @@
+module Exon.Test.Quote where
+
+import Language.Haskell.TH.Quote (quoteExp, QuasiQuoter)
+import Language.Haskell.TH (Q, Exp, runQ)
+import Language.Haskell.TH.Ppr (pprint)
+
+thLines :: Q Exp -> IO [Text]
+thLines q =
+  lines . toText <$> runQ (pprint <$> q)
+
+qqLines :: QuasiQuoter -> String -> IO [Text]
+qqLines qq code =
+  thLines (quoteExp qq code)
+
+runQuote :: QuasiQuoter -> String -> IO Exp
+runQuote qq code =
+  runQ (quoteExp qq code)
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,24 @@
+module Main where
+
+import Exon.Test.BasicTest (test_basic, test_keepWhitespace)
+import Hedgehog (TestT, property, test, withTests)
+import Test.Tasty (TestName, TestTree, defaultMain, testGroup)
+import Test.Tasty.Hedgehog (testProperty)
+
+unitTest ::
+  TestName ->
+  TestT IO () ->
+  TestTree
+unitTest desc =
+  testProperty desc . withTests 1 . property . test
+
+tests :: TestTree
+tests =
+  testGroup "all" [
+    unitTest "basic" test_basic,
+    unitTest "keep whitespace" test_keepWhitespace
+  ]
+
+main :: IO ()
+main =
+  defaultMain tests
