{-# 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 :: 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 :: Parser T.Body plane = T.plane <$> (string "plane" *> skipSpace *> lp *> skipSpace *> triple) <*> (skipSpace *> cancer *> skipSpace *> triple <* skipSpace <* rp) -- > ::= -- > 'sphere (' ';' ')' sphere :: Parser T.Body sphere = T.sphere <$> (string "sphere" *> skipSpace *> lp *> skipSpace *> triple) <*> (skipSpace *> cancer *> skipSpace *> double <* skipSpace <* rp) -- > ::= -- > 'cylinder (' ';' ';' ')' 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 :: 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 -- > ::= 'not' complement :: CSGParser T.Body complement = T.complement <$> (lift (string "not" *> skipSpace) *> body) -- > ::= 'or' union :: CSGParser T.Body union = binary "or" T.unite -- > ::= 'and' 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. -- -- > ::= -- > 'solid' '=' ';' 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 :: CSGParser T.Body body = union <|> intersection <|> complement <|> uncomposedBody -- Used to terminate left branch of binary compositions. -- -- > ::= | uncomposedBody :: CSGParser T.Body uncomposedBody = lift primitive <|> readName -- | Top-level object declaration. -- -- > ::= 'tlo' ';' 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 :: 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)