dsmc-0.1.0.1: src/DSMC/Traceables/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Parser for body definitions.
--
-- Body definition contains a number of solid definitions and ends
-- with the top level object definition. RHS of solid equations may
-- reference other solids to compose into complex bodies.
--
-- Multiple-body compositions are right-associative.
--
-- > # comment
-- >
-- > # define few primitives
-- > solid b1 = sphere (0, 0, 0; 5);
-- > solid p1 = plane (0, 0, 0; 1, 0, 0);
-- >
-- > # define a composition
-- > solid body = b1 and p1;
-- >
-- > # assign it to be the top level object
-- > tlo body;
--
-- Statements must end with a semicolon (newlines are optional).
-- Excessive spaces are ignored.
--
-- Top-level object line must reference a previously defined solid.
--
-- Syntax for primitives follows the signatures of 'Traceables'
-- constructors for 'T.plane' and 'T.sphere', but differs for cylinder
-- and cone, as this module provides access only to frustums
-- ('T.cylinderFrustum' and 'T.coneFrustum').
--
-- [Half-space] @plane (px, py, pz; nx, ny, nz)@, where @(px, py, pz)@
-- is a point on a plane which defines the half-space and @(nx, ny,
-- nz)@ is a normal to the plane (outward to the half-space), not
-- necessarily a unit vector.
--
-- [Sphere] @sphere (cx, cy, cz; r)@, where @(cx, cy, cz)@ is a
-- central point of a sphere and @r@ is radius.
--
-- [Right circular cylinder] @cylinder (p1x, p1y, p1z; p2x, p2y, p2z;
-- r)@ where @(p1x, p1y, p1z)@ and @(p2x, p2y, p2z)@ are bottom and
-- top points on axis and @r@ is radius.
--
-- [Right circular conical frustum] @cone (p1x, p1y, p1z; r1; p2x,
-- p2y, p2z; r2)@ where @(p1x, p1y, p1z)@ and @(p2x, p2y, p2z)@ are
-- bottom and top points on cone axis and @r1@, @r2@ are the
-- corresponding radii.
module DSMC.Traceables.Parser
( parseBody
, parseBodyFile
)
where
import Prelude as P
import Control.Applicative
import qualified Control.Exception as E
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.State.Strict
import Data.Attoparsec.Char8
import Data.ByteString.Char8 as B
import qualified Data.Map as M
import qualified DSMC.Traceables as T
import DSMC.Util.Vector
-- | Transformer which adds lookup table to underlying monad.
type TableT a k v = StateT (M.Map k v) a
-- | Add entry to the lookup table.
addEntry :: (Ord k, Monad a) => k -> v -> TableT a k v ()
addEntry key value = liftM (M.insert key value) get >>= put
-- | Lookup entry in the table.
getEntry :: (Ord k, Monad a) => k -> TableT a k v (Maybe v)
getEntry key = liftM (M.lookup key) get
-- | Parser with lookup table.
type CSGParser = TableT Parser String T.Body
lp :: Parser Char
lp = char '('
rp :: Parser Char
rp = char ')'
eq :: Parser Char
eq = char '='
cancer :: Parser Char
cancer = char ';'
comma :: Parser Char
comma = char ','
-- | Read comma-separated three doubles into point.
--
-- > <triple> ::= <double> ',' <double> ',' <double>
triple :: Parser Point
triple = (,,) <$> double
<*>
(skipSpace *> comma *> skipSpace *>
double
<* skipSpace <* comma <* skipSpace)
<*>
double
keywords :: [String]
keywords = [ "solid"
, "tlo"
, "plane"
, "sphere"
, "cylinder"
, "cone"
]
-- | Read variable name or fail if it's a keyword.
varName :: CSGParser String
varName = do
k <- lift $ many1 (letter_ascii <|> digit)
case (P.elem k keywords) of
False -> return k
True -> fail $ "Unexpected keyword: " ++ k
-- | Lookup body in table by its name or fail if it is undefined.
readName :: CSGParser T.Body
readName = do
k <- varName
v <- getEntry k
case v of
Just b -> return b
_ -> fail $ "Undefined solid: " ++ k
-- > <plane> ::=
-- > 'plane (' <triple> ';' <triple> ')'
plane :: Parser T.Body
plane = T.plane <$>
(string "plane" *> skipSpace *> lp *> skipSpace *> triple) <*>
(skipSpace *> cancer *> skipSpace *> triple <* skipSpace <* rp)
-- > <sphere> ::=
-- > 'sphere (' <triple> ';' <double> ')'
sphere :: Parser T.Body
sphere = T.sphere <$>
(string "sphere" *> skipSpace *> lp *> skipSpace *> triple) <*>
(skipSpace *> cancer *> skipSpace *> double <* skipSpace <* rp)
-- > <cylinder> ::=
-- > 'cylinder (' <triple> ';' <triple> ';' <double> ')'
cylinder :: Parser T.Body
cylinder = T.cylinderFrustum <$>
(string "cylinder" *> skipSpace *> lp *> skipSpace *> triple) <*>
(skipSpace *> cancer *> skipSpace *> triple) <*>
(skipSpace *> cancer *> skipSpace *> double <* skipSpace <* rp)
-- > <cone> ::=
-- > 'cone (' <triple> ';' <double> ';' <triple> ';' <double> ')'
cone :: Parser T.Body
cone = T.coneFrustum <$>
((,) <$>
(string "cone" *> skipSpace *> lp *> skipSpace *> triple) <*>
(skipSpace *> cancer *> skipSpace *> double)) <*>
((,) <$>
(skipSpace *> cancer *> skipSpace *> triple) <*>
(skipSpace *> cancer *> skipSpace *> double <* skipSpace <* rp))
primitive :: Parser T.Body
primitive = plane <|> sphere <|> cylinder <|> cone
-- > <complement> ::= 'not' <body>
complement :: CSGParser T.Body
complement = T.complement <$> (lift (string "not" *> skipSpace) *> body)
-- > <union> ::= <uncomposed-body> 'or' <body>
union :: CSGParser T.Body
union = binary "or" T.unite
-- > <intersection> ::= <uncomposed-body> 'and' <body>
intersection :: CSGParser T.Body
intersection = binary "and" T.intersect
-- | Parse binary operation on two bodies with given composition
-- operators.
binary :: ByteString -> (T.Body -> T.Body -> T.Body) -> CSGParser T.Body
binary op compose = do
b1 <- uncomposedBody
lift (skipSpace *> string op *> skipSpace)
b2 <- body
return $ compose b1 b2
-- | Read stamement which adds new solid entry to lookup table.
--
-- > <statement> ::=
-- > 'solid' <varname> '=' <body> ';'
statement :: CSGParser ()
statement = do
lift $ string "solid" *> skipSpace
k <- varName
lift $ skipSpace <* eq <* skipSpace
v <- body <* lift (cancer *> skipSpace)
addEntry k v
-- | Expression is either a primitive, a reference to previously
-- defined solid or an operation on expressions.
--
-- > <body> ::= <union> | <intersection> | <complement> | <primitive> | <reference>
body :: CSGParser T.Body
body = union <|> intersection <|> complement <|> uncomposedBody
-- Used to terminate left branch of binary compositions.
--
-- > <uncomposed-body> ::= <primitive> | <reference>
uncomposedBody :: CSGParser T.Body
uncomposedBody = lift primitive <|> readName
-- | Top-level object declaration.
--
-- > <tlo> ::= 'tlo' <body> ';'
topLevel :: CSGParser T.Body
topLevel = lift (string "tlo" *> skipSpace) *>
readName
<* lift (cancer <* skipSpace)
-- | Read one-line comment starting with hash sign.
comment :: Parser ()
comment = char '#' >> (manyTill anyChar endOfLine) >> return ()
-- | Read sequence of statements which define solids, and finally read
-- top level object definition.
--
-- > <geoFile> ::= <statement> <geoFile> | <comment> <geoFile> | <tlo>
geoFile :: CSGParser T.Body
geoFile = (many1 $ lift comment <|> statement) *> topLevel
-- | Try to read body definition from bytestring. Return body or error
-- message if parsing fails.
parseBody :: ByteString -> Either String T.Body
parseBody input =
case (parseOnly (runStateT geoFile M.empty) input) of
Right (b, _) -> Right b
Left msg -> Left msg
-- | Read body definition from file. If parsing fails or IOError when
-- reading file occurs, return error message.
parseBodyFile :: FilePath -> IO (Either String T.Body)
parseBodyFile file = do
res <- E.try $ B.readFile file
return $ case res of
Right d -> parseBody d
Left e -> Left $ show (e :: E.IOException)