dao-0.1.0.1: src/Dao/Token.hs
-- "src/Dao/Token.hs" Defines the 'Token' and 'Location' types
-- used by "src/Dao/Interpreter.hs" and "src/Dao/Parser.hs".
--
-- Copyright (C) 2008-2014 Ramin Honary.
-- This file is part of the Dao System.
--
-- The Dao System is free software: you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- The Dao System is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program (see the file called "LICENSE"). If not, see
-- <http://www.gnu.org/licenses/agpl.html>.
{-# LANGUAGE DeriveDataTypeable #-}
module Dao.Token where
import Dao.String
import Dao.PPrint
import Data.Monoid
import Data.Word
import Data.List (intercalate)
import Data.Typeable
import Data.Data
import Control.Monad
import Control.DeepSeq
----------------------------------------------------------------------------------------------------
type LineNum = Word
type ColumnNum = Word
type TabWidth = Word
-- | If an object contains a location, it can instantiate this class to allow locations to be
-- updated or deleted (deleted by converting it to 'LocationUnknown'. Only three types in this
-- module instantiate this class, but any data type that makes up an Abstract Syntax Tree, for
-- example 'Dao.Interpreter.ObjectExpr' or 'Dao.Interpreter.AST.ObjectExrpr' also instantiate this class.
class HasLocation a where
getLocation :: a -> Location
setLocation :: a -> Location -> a
delLocation :: a -> a
instance HasLocation () where
{ getLocation _ = LocationUnknown; setLocation a _ = a; delLocation a = a; }
instance HasLocation UStr where
{ getLocation _ = LocationUnknown; setLocation a _ = a; delLocation a = a; }
instance HasLocation Name where
{ getLocation _ = LocationUnknown; setLocation a _ = a; delLocation a = a; }
instance HasLocation a => HasLocation (Maybe a) where
getLocation = maybe LocationUnknown getLocation
setLocation o loc = fmap (flip setLocation loc) o
delLocation o = fmap delLocation o
-- | Contains two points, a starting and and ending point, where each point consists of a row (line
-- number) and column (character count from the beginning of a line) for locating entities in a
-- parsable text. This type does not contain information regarding the source of the text, or
-- whether or not the input text is a file or stream.
data Location
= LocationUnknown
| Location
{ startingLine :: LineNum
-- ^ the 'Location' but without the starting/ending character count
, startingColumn :: ColumnNum
, endingLine :: LineNum
, endingColumn :: ColumnNum
}
deriving (Eq, Typeable, Data)
instance HasLocation Location where
getLocation = id
setLocation = flip const
delLocation = const LocationUnknown
instance Show Location where
show t = case t of
LocationUnknown -> ""
Location a b _ _ -> show a ++ ':' : show b
instance Monoid Location where
mempty = LocationUnknown
-- Location
-- { startingLine = 0
-- , startingColumn = 0
-- , endingLine = 0
-- , endingColumn = 0
-- }
mappend loc a = case loc of
LocationUnknown -> a
_ -> case a of
LocationUnknown -> loc
_ ->
loc
{ startingLine = min (startingLine loc) (startingLine a)
, startingColumn = min (startingColumn loc) (startingColumn a)
, endingLine = max (endingLine loc) (endingLine a)
, endingColumn = max (endingColumn loc) (endingColumn a)
}
instance Ord Location where
compare a b = case (a,b) of
(LocationUnknown, LocationUnknown) -> EQ
(_ , LocationUnknown) -> LT
(LocationUnknown, _ ) -> GT
_ -> compare (abs(ela-sla), abs(eca-sca), sla, sca) (abs(elb-slb), abs(ecb-scb), slb, scb)
where
sla = startingLine a
ela = endingLine a
slb = startingLine b
elb = endingLine b
sca = startingColumn a
eca = endingColumn a
scb = startingColumn b
ecb = endingColumn b
-- ^ Greater-than is determined by a heuristic value of how large and uncertain the position of
-- the error is. If the exact location is known, it has the lowest uncertainty and is therefore
-- less than a location that might occur across two lines. The 'LocationUnknown' value is the most
-- uncertain and is greater than everything except itself. Using this comparison function, you can
-- sort lists of locations from least to greatest and hopefully get the most helpful, most
-- specific location at the top of the list.
instance HasNullValue Location where
nullValue = LocationUnknown
testNull LocationUnknown = True
testNull _ = False
instance PPrintable Location where
pPrint o = case o of
LocationUnknown -> pString "srcLoc()"
Location a b c d -> pList (pString "srcLoc") "(" ", " ")" [pShow a, pShow b, pShow c, pShow d]
instance NFData Location where
rnf LocationUnknown = ()
rnf (Location a b c d) = deepseq a $! deepseq b $! deepseq c $! deepseq d ()
-- | Create a location where the starting and ending point is the same row and column.
atPoint :: LineNum -> ColumnNum -> Location
atPoint a b =
Location
{ startingLine = a
, endingLine = a
, startingColumn = b
, endingColumn = b
}
-- | The the coordinates from a 'Location':
-- @(('startingLine', 'startingColumn'), ('endingLine', 'endingColumn'))@
locationCoords :: Location -> Maybe ((LineNum, ColumnNum), (LineNum, ColumnNum))
locationCoords loc = case loc of
LocationUnknown -> Nothing
_ -> Just ((startingLine loc, startingColumn loc), (endingLine loc, endingColumn loc))
----------------------------------------------------------------------------------------------------
-- $All_about_tokens
-- This module was designed to create parsers which operate in two phases: a lexical analysis phase
-- (see 'lexicalAnalysis') where input text is split up into tokens, and a syntactic analysis phase
-- where a stream of tokens is converted into data. 'Token' is the data type that makes this
-- possible.
class HasLineNumber a where { lineNumber :: a -> LineNum }
class HasColumnNumber a where { columnNumber :: a -> ColumnNum }
class HasToken a where { getToken :: a tok -> Token tok }
-- | Every token emitted by a lexical analyzer must have at least a type. 'Token' is polymorphic
-- over the type of token.
data Token tok
= EmptyToken { tokType :: tok }
-- ^ Often times, tokens may not need to contain any text. This is often true of opreator
-- symbols and keywords. This constructor constructs a token with just a type and no text. The
-- more descriptive your token types are, the less you need you will have for storing the text
-- along with the token type, and the more memory you will save.
| CharToken { tokType :: tok, tokChar :: !Char }
-- ^ Constructs tokens along with the text. If the text is only a single character, this
-- constructor is used, which can save a little memory as compared to storing a
-- 'Dao.String.UStr'.
| Token { tokType :: tok, tokUStr :: UStr }
-- ^ Constructs tokens that contain a copy of the text extracted by the lexical analyzer to
-- create the token.
deriving (Eq, Typeable)
instance Show tok => Show (Token tok) where
show tok =
let cont = tokToStr tok
in show (tokType tok) ++ (if null cont then "" else ' ':show cont)
instance Functor Token where
fmap f t = case t of
EmptyToken t -> EmptyToken (f t)
CharToken t c -> CharToken (f t) c
Token t u -> Token (f t) u
-- | If the lexical analyzer emitted a token with a copy of the text used to create it, this
-- function can retrieve that text. Returns 'Dao.String.nil' if there is no text.
tokToUStr :: Token tok -> UStr
tokToUStr tok = case tok of
EmptyToken _ -> nil
CharToken _ c -> ustr [c]
Token _ u -> u
-- | Like 'tokToUStr' but returns a 'Prelude.String' or @""@ instead.
tokToStr :: Token tok -> String
tokToStr tok = case tok of
EmptyToken _ -> ""
CharToken _ c -> [c]
Token _ u -> uchars u
-- | This data type stores the starting point (the line number and column number) in the
-- source file of where the token was emitted along with the 'Token' itself.
data TokenAt tok
= TokenAt
{ tokenAtLineNumber :: LineNum
, tokenAtColumnNumber :: ColumnNum
, getTokenValue :: Token tok
}
instance Show tok =>
Show (TokenAt tok) where
show tok = let (a,b,c) = asTriple tok in show a++':':show b++' ':show c
instance HasLineNumber (TokenAt tok) where { lineNumber = tokenAtLineNumber }
instance HasColumnNumber (TokenAt tok) where { columnNumber = tokenAtColumnNumber }
instance HasToken TokenAt where { getToken = getTokenValue }
instance Functor TokenAt where
fmap f t =
TokenAt
{ tokenAtLineNumber = tokenAtLineNumber t
, tokenAtColumnNumber = tokenAtColumnNumber t
, getTokenValue = fmap f (getToken t)
}
-- | Extract the type of the token.
asTokType :: TokenAt tok -> tok
asTokType = tokType . getToken
-- | Extract the string value stored in this token. /WARNING:/ keyword and operator tokens contain
-- no strings to save memory, so evaluating this function on any token type defind by
-- 'Dao.Parser.operator', 'Dao.Parser.operatorTable', or 'Dao.Parser.keyword' will result in a null
-- sring.
asString :: TokenAt tok -> String
asString = tokToStr . getToken
-- | Extract the string value stored in this token. /WARNING:/ keyword and operator tokens contain
-- no strings to save memory, so evaluating this function on any token type defind by
-- 'Dao.Parser.operator', 'Dao.Parser.operatorTable', or 'Dao.Parser.keyword' will result in a null
-- sring.
asUStr :: TokenAt tok -> UStr
asUStr = tokToUStr . getToken
-- | Extract the string value stored in this token. /WARNING:/ keyword and operator tokens contain
-- no strings to save memory, so evaluating this function on any token type defind by
-- 'Dao.Parser.operator', 'Dao.Parser.operatorTable', or 'Dao.Parser.keyword' will result in a null
-- sring.
asName :: TokenAt tok -> Name
asName = fromUStr . asUStr
-- | That is as-zero, because "0" looks kind of like "()".
-- This function is useful when it is necessary to pass a function argument to functions like
-- 'Dao.Parser.token' and 'Dao.Parser.tokenBy' but you want to ignore the token returned.
as0 :: TokenAt tok -> ()
as0 = const ()
-- | Retrieve the token part of a 'TokenAt' object.
asToken :: TokenAt tok -> Token tok
asToken = getToken
-- | Synonym for 'Prelude.id', used when it is necessary to pass a function argument to functions like
-- 'Dao.Parser.token' and 'Dao.Parser.tokenBy' but you just want the whole 'TokenAt' object.
asTokenAt :: TokenAt tok -> TokenAt tok
asTokenAt = id
-- | Convert the contents of a 'TokenAt' object to a tripple containg it's component parts.
asTriple :: TokenAt tok -> (LineNum, ColumnNum, Token tok)
asTriple tok = (lineNumber tok, columnNumber tok, getToken tok)
-- | Convert the contents of a 'TokenAt' object to a pair containg it's component parts, but not the
-- 'Token' itself.
asLineColumn :: TokenAt tok -> (LineNum, ColumnNum)
asLineColumn tok = (lineNumber tok, columnNumber tok)
-- | Convert the contents of the 'TokenAt' object's 'lineNumber' and 'columnNumber' to a 'Location'
-- object.
asLocation :: TokenAt tok -> Location
asLocation = uncurry atPoint . asLineColumn
-- | The lexical analysis phase emits a stream of 'TokenAt' objects, but it is not memory
-- efficient to store the line and column number with every single token. To save space, the token
-- stream is "compressed" into 'Lines', where 'TokenAt' that has the same 'lineNumber' is
-- placed into the same 'Line' object. The 'Line' stores the 'lineNumber', and the
-- 'lineNumber's are deleted from every 'TokenAt', leaving only the 'columnNumber' and 'Token'
-- in each line.
data Line tok
= Line
{ lineLineNumber :: LineNum
, lineTokens :: [(ColumnNum, Token tok)]
-- ^ a list of tokens, each with an associated column number.
}
instance HasLineNumber (Line tok) where { lineNumber = lineLineNumber }
instance Show tok => Show (Line tok) where
show line = concat $
[ "Line ", show (lineLineNumber line), ":\n"
, intercalate ", " $
map (\ (col, tok) -> show col++" "++show tok) (lineTokens line)
]
instance Functor Line where
fmap f t =
Line
{ lineLineNumber = lineLineNumber t
, lineTokens = fmap (fmap (fmap f)) (lineTokens t)
}
lineToTokensAt :: Line tok -> [TokenAt tok]
lineToTokensAt line = map f (lineTokens line) where
lineNum = lineNumber line
f (colNum, tok) =
TokenAt
{ tokenAtLineNumber = lineNum
, tokenAtColumnNumber = colNum
, getTokenValue = tok
}
----------------------------------------------------------------------------------------------------
-- $Error_handling
-- The lexical analyzer and syntactic analysis monads all instantiate
-- 'Control.Monad.ParseError.Class.MonadError' in the Monad Transformer Library ("mtl" package). This is
-- the type used for 'Control.Monad.ParseError.Class.throwError' and
-- 'Control.Monad.ParseError.Class.catchError'.
-- | This data structure is used by both the lexical analysis and the syntactic analysis phase.
data ParseError st tok
= ParseError
{ parseErrLoc :: Location
, parseErrMsg :: Maybe UStr
, parseErrTok :: Maybe (Token tok)
, parseStateAtErr :: Maybe st
}
deriving (Eq, Typeable)
instance (PPrintable st, Show tok) => Show (ParseError st tok) where { show err = prettyShow err }
instance Functor (ParseError st) where
fmap f e =
ParseError
{ parseErrLoc = parseErrLoc e
, parseErrMsg = parseErrMsg e
, parseErrTok = fmap (fmap f) (parseErrTok e)
, parseStateAtErr = parseStateAtErr e
}
instance (PPrintable st, Show tok) => PPrintable (ParseError st tok) where
pPrint err = do
pString (show (parseErrLoc err)++": ")
sp <-
maybe
(return False)
(\tok -> pString ("(on token: "++show tok++")") >> return True)
(parseErrTok err)
maybe
(return False)
(\msg -> when sp (pString " ") >> pUStr msg >> return True)
(parseErrMsg err)
maybe (return ()) (\st -> pEndLine >> pIndent (pPrint st)) (parseStateAtErr err)
fmapParseErrorState :: (stA -> stB) -> ParseError stA tok -> ParseError stB tok
fmapParseErrorState f err@(ParseError{ parseStateAtErr=st }) = err{parseStateAtErr=fmap f st}
-- | An initial blank parser error you can use to construct more detailed error messages.
parserErr :: Location -> ParseError st tok
parserErr loc =
ParseError
{ parseErrLoc = loc
, parseErrMsg = Nothing
, parseErrTok = Nothing
, parseStateAtErr = Nothing
}
newParseError :: ParseError st tok
newParseError =
ParseError
{ parseErrLoc = LocationUnknown
, parseErrMsg = Nothing
, parseErrTok = Nothing
, parseStateAtErr = Nothing
}