diff --git a/sexp-grammar.cabal b/sexp-grammar.cabal
--- a/sexp-grammar.cabal
+++ b/sexp-grammar.cabal
@@ -1,5 +1,5 @@
 name:                sexp-grammar
-version:             2.0.2
+version:             2.1.0
 license:             BSD3
 license-file:        LICENSE
 author:              Eugene Smolanka, Sergey Vinokurov
@@ -39,7 +39,6 @@
   other-modules:
     Language.Sexp.Encode
     Language.Sexp.Lexer
-    Language.Sexp.LexerInterface
     Language.Sexp.Parser
     Language.Sexp.Pretty
     Language.Sexp.Token
diff --git a/src/Language/Sexp/Lexer.x b/src/Language/Sexp/Lexer.x
--- a/src/Language/Sexp/Lexer.x
+++ b/src/Language/Sexp/Lexer.x
@@ -13,16 +13,18 @@
   ) where
 
 import Data.Bifunctor
+import qualified Data.ByteString.Lazy as BLW
 import Data.ByteString.Lazy.Char8 (ByteString)
-import qualified Data.ByteString.Lazy.UTF8 as UTF8
 import qualified Data.ByteString.Lazy.Char8 as BL
+import qualified Data.ByteString.Lazy.UTF8 as UTF8
+import Data.Int
+import Data.Scientific (Scientific)
 import qualified Data.Text as T
 import qualified Data.Text.Lazy as TL
 import Data.Text.Lazy.Encoding (decodeUtf8)
 import Data.Text.Read
-import Data.Scientific (Scientific)
+import Data.Word
 
-import Language.Sexp.LexerInterface
 import Language.Sexp.Token
 import Language.Sexp.Types (Position (..), LocatedBy (..))
 
@@ -71,12 +73,20 @@
 @symbol            { TokSymbol  `via` decode     }
 \" @string* \"     { TokString  `via` readString }
 
-.                  { TokUnknown `via` BL.head    }
-
 {
 
-type AlexAction = LineCol -> ByteString -> LocatedBy LineCol Token
+----------------------------------------------------------------------
+-- Actions
 
+just :: Token -> AlexAction
+just tok _ = tok
+
+via :: (a -> Token) -> (ByteString -> a) -> AlexAction
+via ftok f = ftok . f
+
+----------------------------------------------------------------------
+-- Decoders
+
 readString :: ByteString -> T.Text
 readString = TL.toStrict . unescape . TL.tail . TL.init . decodeUtf8
 
@@ -86,34 +96,84 @@
 decode :: ByteString -> T.Text
 decode = TL.toStrict . decodeUtf8
 
-just :: Token -> AlexAction
-just tok pos _ =
-  pos :< tok
+----------------------------------------------------------------------
+-- Entry point
 
-via :: (a -> Token) -> (ByteString -> a) -> AlexAction
-via ftok f pos str =
-  (pos :<) . ftok . f $ str
+lexSexp :: Position -> ByteString -> [LocatedBy Position Token]
+lexSexp (Position fn line1 col1) =
+  map (bimap fixPos id) . alexScanTokens . mkAlexInput (LineCol line1 col1)
+  where
+    fixPos (LineCol l c) = Position fn l c
 
+----------------------------------------------------------------------
+-- Machinery
+
+type AlexAction = ByteString -> Token
+
 alexScanTokens :: AlexInput -> [LocatedBy LineCol Token]
 alexScanTokens input =
   case alexScan input defaultCode of
-    AlexEOF -> []
-    AlexError (AlexInput {aiInput, aiLineCol = LineCol line col}) ->
-      error $ "Lexical error at line " ++ show line ++ " column " ++ show col ++
-        ". Remaining input: " ++ show (UTF8.take 200 aiInput)
-    AlexSkip input _ -> alexScanTokens input
+    AlexEOF ->
+      [aiLineCol input :< TokEOF]
+    AlexError (AlexInput {aiInput, aiLineCol}) ->
+      let rest = T.takeWhile (/= '\n') $ decode $ UTF8.take 100 aiInput
+      in [aiLineCol :< (TokUnknown rest)]
+    AlexSkip input _ ->
+      alexScanTokens input
     AlexToken input' tokLen action ->
-      action inputPosn inputText : alexScanTokens input'
-      where
-        inputPosn = aiLineCol input
-        inputText = UTF8.take (fromIntegral tokLen) (aiInput input)
+      let inputText = UTF8.take (fromIntegral tokLen) (aiInput input)
+      in (aiLineCol input :< action inputText) : alexScanTokens input'
   where
     defaultCode :: Int
     defaultCode = 0
 
-lexSexp :: Position -> ByteString -> [LocatedBy Position Token]
-lexSexp (Position fn line1 col1) =
-  map (bimap fixPos id) . alexScanTokens . mkAlexInput (LineCol line1 col1)
+data LineCol = LineCol {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+
+columnsInTab :: Int
+columnsInTab = 8
+
+advanceLineCol :: Char -> LineCol -> LineCol
+advanceLineCol '\n' (LineCol line _)   = LineCol (line + 1) 1
+advanceLineCol '\t' (LineCol line col) = LineCol line (((col + columnsInTab - 1) `div` columnsInTab) * columnsInTab + 1)
+advanceLineCol _    (LineCol line col) = LineCol line (col + 1)
+
+data AlexInput = AlexInput
+  { aiInput     :: ByteString
+  , aiPrevChar  :: {-# UNPACK #-} !Char
+  , aiBytesLeft :: {-# UNPACK #-} !Int64
+  , aiLineCol   :: !LineCol
+  }
+
+mkAlexInput :: LineCol -> ByteString -> AlexInput
+mkAlexInput initPos source = AlexInput
+  { aiInput     = source
+  , aiPrevChar  = '\n'
+  , aiBytesLeft = 0
+  , aiLineCol   = initPos
+  }
+
+alexNextChar :: AlexInput -> Maybe AlexInput
+alexNextChar input
+  | aiBytesLeft input > 1 = Just $ input { aiBytesLeft = aiBytesLeft input - 1 }
+  | otherwise = case UTF8.decode (aiInput input) of
+      Just (c, n) -> Just $ input
+        { aiPrevChar  = c
+        , aiLineCol   = advanceLineCol c (aiLineCol input)
+        , aiBytesLeft = n
+        }
+      Nothing -> Nothing
+
+-- Alex interface - functions used by Alex
+alexInputPrevChar :: AlexInput -> Char
+alexInputPrevChar = aiPrevChar
+
+alexGetByte :: AlexInput -> Maybe (Word8, AlexInput)
+alexGetByte input =
+  alexNextChar input >>= getByte
   where
-    fixPos (LineCol l c) = Position fn l c
+    getByte :: AlexInput -> Maybe (Word8, AlexInput)
+    getByte input =
+      case BLW.uncons (aiInput input) of
+        Just (w, rest) -> Just (w, input { aiInput = rest })
+        Nothing -> Nothing
 }
diff --git a/src/Language/Sexp/LexerInterface.hs b/src/Language/Sexp/LexerInterface.hs
deleted file mode 100644
--- a/src/Language/Sexp/LexerInterface.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-{-# LANGUAGE BangPatterns      #-}
-{-# LANGUAGE RecordWildCards   #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Language.Sexp.LexerInterface
-  ( LineCol(..)
-  , AlexInput(..)
-  , mkAlexInput
-  -- Alex interfare
-  , alexInputPrevChar
-  , alexGetByte
-  ) where
-
-import Data.Int
-import Data.ByteString.Lazy (ByteString, uncons)
-import Data.ByteString.Lazy.UTF8 (decode)
-import Data.Word (Word8)
-
-data LineCol = LineCol {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-
-columnsInTab :: Int
-columnsInTab = 8
-
-advanceLineCol :: Char -> LineCol -> LineCol
-advanceLineCol '\n' (LineCol line _)   = LineCol (line + 1) 0
-advanceLineCol '\t' (LineCol line col) = LineCol line (((col + columnsInTab - 1) `div` columnsInTab) * columnsInTab + 1)
-advanceLineCol _    (LineCol line col) = LineCol line (col + 1)
-
-data AlexInput = AlexInput
-  { aiInput     :: ByteString
-  , aiPrevChar  :: {-# UNPACK #-} !Char
-  , aiCurChar   :: {-# UNPACK #-} !Char
-  , aiBytesLeft :: {-# UNPACK #-} !Int64
-  , aiLineCol   :: !LineCol
-  }
-
-mkAlexInput :: LineCol -> ByteString -> AlexInput
-mkAlexInput initPos source = alexNextChar $ AlexInput
-  { aiInput     = source
-  , aiPrevChar  = '\n'
-  , aiCurChar   = '\n'
-  , aiBytesLeft = 0
-  , aiLineCol   = initPos
-  }
-
-alexNextChar :: AlexInput -> AlexInput
-alexNextChar input =
-  case decode (aiInput input) of
-    Just (c, n) -> input
-      { aiPrevChar  = aiCurChar input
-      , aiCurChar   = c
-      , aiBytesLeft = n
-      }
-    Nothing     -> input
-      { aiPrevChar  = aiCurChar input
-      , aiCurChar   = '\n'
-      , aiBytesLeft = 0
-      }
-
-alexPropagatePos :: AlexInput -> AlexInput
-alexPropagatePos input =
-  input { aiLineCol = advanceLineCol (aiPrevChar input) (aiLineCol input) }
-
--- Alex interface - functions usedby Alex
-alexInputPrevChar :: AlexInput -> Char
-alexInputPrevChar = aiPrevChar
-
-alexGetByte :: AlexInput -> Maybe (Word8, AlexInput)
-alexGetByte input
-  | aiBytesLeft input == 0 = go . alexPropagatePos . alexNextChar $ input
-  | otherwise = go input
-  where
-    go :: AlexInput -> Maybe (Word8, AlexInput)
-    go input =
-      case uncons (aiInput input) of
-        Just (w, rest) -> Just (w, input
-          { aiBytesLeft = aiBytesLeft input - 1
-          , aiInput     = rest
-          })
-        Nothing -> Nothing
-
diff --git a/src/Language/Sexp/Parser.y b/src/Language/Sexp/Parser.y
--- a/src/Language/Sexp/Parser.y
+++ b/src/Language/Sexp/Parser.y
@@ -25,8 +25,8 @@
 import Language.Sexp.Types
 }
 
-%name parseSexp_ Sexp
-%name parseSexps_ Sexps
+%name parseSexp_ Sexp_
+%name parseSexps_ Sexps_
 %error { parseError }
 %tokentype { LocatedBy Position Token }
 %monad { Either String }
@@ -43,10 +43,18 @@
   NUMBER         { _ :< (TokNumber _) }
   STRING         { _ :< (TokString _) }
 
+  EOF            { _ :< TokEOF        }
+
 %%
 
+Sexps_ :: { [Sexp] }
+  : Sexps EOF                             { $1 }
+
 Sexps :: { [Sexp] }
   : list(Sexp)                            { $1 }
+
+Sexp_ :: { Sexp }
+  : Sexp EOF                              { $1 }
 
 Sexp :: { Sexp }
   : Atom                                  { AtomF                       @@ $1 }
diff --git a/src/Language/Sexp/Token.hs b/src/Language/Sexp/Token.hs
--- a/src/Language/Sexp/Token.hs
+++ b/src/Language/Sexp/Token.hs
@@ -30,7 +30,8 @@
   | TokNumber  { getNumber  :: !Scientific }  -- 42.0, -1.0, 3.14, -1e10
   | TokString  { getString  :: !Text }        -- "foo", "", "hello world"
   | TokSymbol  { getSymbol  :: !Text }        -- foo, bar
-  | TokUnknown { getUnknown :: !Char }        -- for unknown lexemes
+  | TokUnknown { getUnknown :: !Text }        -- for unknown lexemes
+  | TokEOF
     deriving (Show, Eq)
 
 instance Pretty Token where
@@ -44,7 +45,8 @@
   pretty (TokSymbol s)  = "symbol" <+> squotes (pretty s) <> squote
   pretty (TokNumber n)  = "number" <+> pretty (show n)
   pretty (TokString s)  = "string" <+> pretty (show s)
-  pretty (TokUnknown u) = "unrecognized" <+> pretty (show u)
+  pretty (TokUnknown u) = "unrecognized" <+> pretty u <> "..."
+  pretty TokEOF         = "end of file"
 
 
 newtype DText = DText (TL.Text -> TL.Text)
