diff --git a/bench/Bytesmith.hs b/bench/Bytesmith.hs
--- a/bench/Bytesmith.hs
+++ b/bench/Bytesmith.hs
@@ -53,7 +53,7 @@
 src     = sexp >> P.endOfInput U
 runSexp = parseByteArray src
 
-longw     = P.cstring U (Ptr "thisisalongkeyword"#)
+longw     = P.cstring U (Ptr "thisisalongkeyword\NUL"#)
 longws    = some_ (longw >> ws) >> P.endOfInput U
 runLongws = parseByteArray longws
 
diff --git a/flatparse.cabal b/flatparse.cabal
--- a/flatparse.cabal
+++ b/flatparse.cabal
@@ -1,13 +1,13 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.33.0.
+-- This file has been generated from package.yaml by hpack version 0.34.4.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: dd4124fa98babb251701a98ec5a31a5016b54b6af73c2732c85c9e13fdb07478
+-- hash: 2ba3a084128297d0d1b6136ac0a6aff88c0a67a3998844684ae951a9e56aa339
 
 name:           flatparse
-version:        0.2.0.0
+version:        0.2.1.0
 synopsis:       High-performance parsing from strict bytestrings
 description:    @Flatparse@ is a high-performance parsing library, focusing on programming languages and
                 human-readable data formats. See the README for more information:
@@ -20,7 +20,8 @@
 copyright:      2021 András Kovács
 license:        MIT
 license-file:   LICENSE
-tested-with:    GHC == 8.8.4
+tested-with:
+    GHC == 8.8.4
 build-type:     Simple
 extra-source-files:
     README.md
@@ -39,7 +40,17 @@
       Paths_flatparse
   hs-source-dirs:
       src
-  default-extensions: BangPatterns BlockArguments ExplicitNamespaces LambdaCase MagicHash OverloadedStrings PatternSynonyms TemplateHaskell TupleSections UnboxedTuples
+  default-extensions:
+      BangPatterns
+      BlockArguments
+      ExplicitNamespaces
+      LambdaCase
+      MagicHash
+      OverloadedStrings
+      PatternSynonyms
+      TemplateHaskell
+      TupleSections
+      UnboxedTuples
   ghc-options: -Wall -Wno-name-shadowing -Wno-unused-binds -Wno-unused-matches -Wno-missing-signatures -O2
   build-depends:
       base >=4.7 && <5
@@ -61,7 +72,17 @@
       Paths_flatparse
   hs-source-dirs:
       bench
-  default-extensions: BangPatterns BlockArguments ExplicitNamespaces LambdaCase MagicHash OverloadedStrings PatternSynonyms TemplateHaskell TupleSections UnboxedTuples
+  default-extensions:
+      BangPatterns
+      BlockArguments
+      ExplicitNamespaces
+      LambdaCase
+      MagicHash
+      OverloadedStrings
+      PatternSynonyms
+      TemplateHaskell
+      TupleSections
+      UnboxedTuples
   ghc-options: -Wall -Wno-name-shadowing -Wno-unused-binds -Wno-unused-matches -Wno-missing-signatures -O2
   build-depends:
       attoparsec
diff --git a/src/FlatParse/Basic.hs b/src/FlatParse/Basic.hs
--- a/src/FlatParse/Basic.hs
+++ b/src/FlatParse/Basic.hs
@@ -82,6 +82,7 @@
   , validPos
   , posLineCols
   , unsafeSpanToByteString
+  , unsafeSlice
   , mkPos
   , FlatParse.Basic.lines
 
@@ -91,6 +92,10 @@
   , takeRest
   , traceRest
 
+  -- * `String` conversions
+  , packUTF8
+  , unpackUTF8
+
   -- * Internal functions
   , ensureBytes#
   , scan8#
@@ -100,7 +105,6 @@
   , scanAny8#
   , scanBytes#
   , setBack#
-  , packUTF8
 
   ) where
 
@@ -591,7 +595,7 @@
 
 -- | @isLatinLetter c = (\'A\' <= c && c <= \'Z\') || (\'a\' <= c && c <= \'z\')@
 isLatinLetter :: Char -> Bool
-isLatinLetter c = ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
+isLatinLetter c = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
 {-# inline isLatinLetter #-}
 
 -- | @isGreekLetter c = (\'Α\' <= c && c <= \'Ω\') || (\'α\' <= c && c <= \'ω\')@
@@ -634,9 +638,9 @@
 --   combine the results in a right-nested way using the @a -> b -> b@ function. Note: this is not
 --   the usual `chainr` function from the parsec libraries!
 chainr :: (a -> b -> b) -> Parser e a -> Parser e b -> Parser e b
-chainr f (Parser elem) (Parser end) = go where
-  go = Parser \fp eob s -> case elem fp eob s of
-    OK# a s -> case runParser# go fp eob s of
+chainr f (Parser elem) (Parser end) = Parser go where
+  go fp eob s = case elem fp eob s of
+    OK# a s -> case go fp eob s of
       OK# b s -> let !b' = f a b in OK# b' s
       x       -> x
     Fail# -> end fp eob s
@@ -647,9 +651,9 @@
 --   try to avoid this. Often it is possible to get rid of the intermediate list by using a
 --   combinator or a custom parser.
 many :: Parser e a -> Parser e [a]
-many (Parser f) = go where
-  go = Parser \fp eob s -> case f fp eob s of
-    OK# a s -> case runParser# go fp eob s of
+many (Parser f) = Parser go where
+  go fp eob s = case f fp eob s of
+    OK# a s -> case go fp eob s of
                  OK# as s -> OK# (a:as) s
                  x        -> x
     Fail#  -> OK# [] s
@@ -658,9 +662,9 @@
 
 -- | Skip a parser zero or more times.
 many_ :: Parser e a -> Parser e ()
-many_ (Parser f) = go where
-  go = Parser \fp eob s -> case f fp eob s of
-    OK# a s -> runParser# go fp eob s
+many_ (Parser f) = Parser go where
+  go fp eob s = case f fp eob s of
+    OK# a s -> go fp eob s
     Fail#   -> OK# () s
     Err# e  -> Err# e
 {-# inline many_ #-}
@@ -799,13 +803,22 @@
        OK res _ -> snd <$> sortBy (comparing fst) res
        _        -> error "invalid position"
 
--- | Create a `B.ByteString` from a `Span`. The result is invalid is the `Span` points
+-- | Create a `B.ByteString` from a `Span`. The result is invalid if the `Span` points
 --   outside the current buffer, or if the `Span` start is greater than the end position.
 unsafeSpanToByteString :: Span -> Parser e B.ByteString
 unsafeSpanToByteString (Span l r) =
   lookahead (setPos l >> byteStringOf (setPos r))
 {-# inline unsafeSpanToByteString #-}
 
+-- | Slice into a `B.ByteString` using a `Span`. The result is invalid if the `Span`
+--   is not a valid slice of the first argument.
+unsafeSlice :: B.ByteString -> Span -> B.ByteString
+unsafeSlice (B.PS (ForeignPtr addr fp) (I# start) (I# len))
+            (Span (Pos (I# o1)) (Pos (I# o2))) =
+  let end = addr `plusAddr#` start `plusAddr#` len
+  in B.PS (ForeignPtr (plusAddr# end (negateInt# o1)) fp) (I# 0#) (I# (o1 -# o2))
+{-# inline unsafeSlice #-}
+
 -- | Create a `Pos` from a line and column number. Throws an error on out-of-bounds
 --   line and column numbers.
 mkPos :: B.ByteString -> (Int, Int) -> Pos
@@ -869,6 +882,12 @@
 -- | Convert a `String` to an UTF-8-coded `B.ByteString`.
 packUTF8 :: String -> B.ByteString
 packUTF8 = B.pack . concatMap charToBytes
+
+-- | Convert an UTF-8-coded `B.ByteString` to a `String`.
+unpackUTF8 :: B.ByteString -> String
+unpackUTF8 str = case runParser takeRest str of
+  OK a _ -> a
+  _      -> error "unpackUTF8: invalid encoding"
 
 charToBytes :: Char -> [Word8]
 charToBytes c'
diff --git a/src/FlatParse/Examples/BasicLambda/Parser.hs b/src/FlatParse/Examples/BasicLambda/Parser.hs
--- a/src/FlatParse/Examples/BasicLambda/Parser.hs
+++ b/src/FlatParse/Examples/BasicLambda/Parser.hs
@@ -59,9 +59,13 @@
 digit = (\c -> ord c - ord '0') <$> satisfyASCII isDigit
 
 int :: Parser Int
-int = token $
-  snd <$> chainr (\n (!place, !acc) -> (place*10,acc+place*n)) digit ((10,) <$> digit)
 
+int = token do
+  (place, n) <- chainr (\n (!place, !acc) -> (place*10,acc+place*n)) digit (pure (1, 0))
+  case place of
+    1 -> empty
+    _ -> pure n
+
 -- | Parse a literal, identifier or parenthesized expression.
 atom :: Parser Tm
 atom =
@@ -135,7 +139,6 @@
 src' = ws *> tm' <* eof `cut` [Msg "end of input (lexical error)"]
 
 
-
 -- Examples
 --------------------------------------------------------------------------------
 
@@ -144,5 +147,6 @@
 p1 = unlines [
   "let f = lam x. lam y. x (x (x y)) in",
   "let g = if f true then false else true in",
+  "let h = f x y + 200 in",
   "f g g h"
   ]
diff --git a/src/FlatParse/Stateful.hs b/src/FlatParse/Stateful.hs
--- a/src/FlatParse/Stateful.hs
+++ b/src/FlatParse/Stateful.hs
@@ -88,6 +88,7 @@
   , validPos
   , posLineCols
   , unsafeSpanToByteString
+  , unsafeSlice
   , mkPos
   , FlatParse.Stateful.lines
 
@@ -97,6 +98,10 @@
   , takeRest
   , traceRest
 
+  -- * `String` conversions
+  , packUTF8
+  , unpackUTF8
+
   -- * Internal functions
   , ensureBytes#
   , scan8#
@@ -106,8 +111,8 @@
   , scanAny8#
   , scanBytes#
   , setBack#
-  , packUTF8
 
+
   ) where
 
 import Control.Monad
@@ -842,7 +847,16 @@
   lookahead (setPos l >> byteStringOf (setPos r))
 {-# inline unsafeSpanToByteString #-}
 
+-- | Slice into a `B.ByteString` using a `Span`. The result is invalid if the `Span`
+--   is not a valid slice of the first argument.
+unsafeSlice :: B.ByteString -> Span -> B.ByteString
+unsafeSlice (B.PS (ForeignPtr addr fp) (I# start) (I# len))
+            (Span (Pos (I# o1)) (Pos (I# o2))) =
+  let end = addr `plusAddr#` start `plusAddr#` len
+  in B.PS (ForeignPtr (plusAddr# end (negateInt# o1)) fp) (I# 0#) (I# (o1 -# o2))
+{-# inline unsafeSlice #-}
 
+
 -- | Create a `Pos` from a line and column number. Throws an error on out-of-bounds
 --   line and column numbers.
 mkPos :: B.ByteString -> (Int, Int) -> Pos
@@ -908,6 +922,12 @@
 -- | Convert a `String` to an UTF-8-coded `B.ByteString`.
 packUTF8 :: String -> B.ByteString
 packUTF8 = B.pack . concatMap charToBytes
+
+-- | Convert an UTF-8-coded `B.ByteString` to a `String`.
+unpackUTF8 :: B.ByteString -> String
+unpackUTF8 str = case runParser takeRest 0 0 str of
+  OK a _ _ -> a
+  _        -> error "unpackUTF8: invalid encoding"
 
 charToBytes :: Char -> [Word8]
 charToBytes c'
