{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Language.OpenSCAD
(
parse,
Ident (..),
ident,
TopLevel (..),
topLevel,
Object (..),
object,
Expr (..),
expression,
Argument (..),
Range (..),
)
where
import Control.Applicative
import Control.Monad (void)
import qualified Data.ByteString.Char8 as BS
import Data.Char (digitToInt, ord)
import qualified Data.CharSet as CS
import qualified Data.CharSet.Unicode as CS
import Data.Foldable (foldr')
import Data.Function (fix)
import Data.Functor (($>), (<&>))
import Data.Functor.Identity (Identity (..))
import Data.List (find, foldl')
import Data.Maybe
import Data.Monoid ((<>))
import qualified Data.Scientific as Sci
import Data.Text.Prettyprint.Doc ((<+>))
import qualified Data.Text.Prettyprint.Doc as PP
import GHC.Generics (Generic (..), Generic1 (..))
import qualified Test.QuickCheck as QC
import Text.Parser.Expression
import Text.Parser.Token.Style (emptyOps)
import Text.Trifecta hiding (ident)
newtype Ident = Ident String
deriving (Show, Eq, Ord)
instance PP.Pretty Ident where
pretty (Ident i) = PP.pretty i
instance QC.Arbitrary Ident where
arbitrary = do
c <- QC.elements $ CS.toList ident1Chars
l <- QC.getSize
rest <- QC.vectorOf l . QC.elements $ CS.toList identChars
pure $ Ident (c : rest)
shrink (Ident s) = case s of
c1 : c2 : cs ->
fmap Ident $ [c1 : cs] <> [c2 : cs | c2 `CS.member` ident1Chars]
_ -> mempty
identChars :: CS.CharSet
identChars = CS.letter <> CS.decimalNumber <> CS.fromList "_"
ident1Chars :: CS.CharSet
ident1Chars = CS.fromList "$_" <> CS.letter
ident :: Parser Ident
ident = token $ do
c <- oneOfSet ident1Chars
rest <- many $ oneOfSet identChars
return $ Ident (c : rest)
data Argument a
=
Argument a
|
NamedArgument Ident a
deriving (Show, Eq, Generic)
instance QC.Arbitrary a => QC.Arbitrary (Argument a) where
arbitrary =
QC.oneof
[ Argument <$> QC.arbitrary,
NamedArgument <$> QC.arbitrary <*> QC.arbitrary
]
shrink = QC.genericShrink
instance PP.Pretty a => PP.Pretty (Argument a) where
pretty v = case v of
Argument a -> PP.pretty a
NamedArgument i a -> PP.pretty i <+> "=" <+> PP.pretty a
data Object
= Module Ident [Argument Expr] (Maybe Object)
| ForLoop Ident Expr Object
|
Objects [Object]
| If Expr Object (Maybe Object)
| BackgroundMod Object
| DebugMod Object
| RootMod Object
| DisableMod Object
| ModuleDef
{ moduleName :: Ident,
moduleArgs :: [(Ident, Maybe Expr)],
moduleBody :: [Object]
}
| VarDef
{ varName :: Ident,
varValue :: Expr
}
| FuncDef
{ funcName :: Ident,
funcArgs :: [Ident],
funcBody :: Expr
}
deriving (Show, Generic, Eq)
instance QC.Arbitrary Object where
arbitrary = QC.sized $
fix $ \rec n ->
QC.oneof $
[ do
l <- QC.choose (0, n)
let n' = n `div` (2 * max l 1)
Module
<$> QC.arbitrary
<*> QC.vectorOf l (QC.resize n' QC.arbitrary)
<*> QC.resize n' QC.arbitrary
]
<> if n > 0
then
[ let n' = n `div` 2
in ForLoop <$> QC.arbitrary
<*> QC.resize n' QC.arbitrary
<*> rec n',
do
l <- QC.choose (0, n)
let n' = n `div` max l 1
Objects <$> QC.vectorOf l (rec n'),
let n' = n `div` 3
in ( If
<$> QC.resize n' QC.arbitrary
<*> rec n'
<*> QC.resize n' QC.arbitrary
)
`QC.suchThat` (not . isAmbiguousIfElse),
BackgroundMod <$> rec (n -1),
DebugMod <$> rec (n -1),
RootMod <$> rec (n -1),
DisableMod <$> rec (n -1),
do
l <- QC.choose (0, n)
l' <- QC.choose (0, n)
let n' = n `div` (max l 1 * max l' 1)
ModuleDef
<$> QC.arbitrary
<*> QC.vectorOf
l
((,) <$> QC.arbitrary <*> QC.resize n' QC.arbitrary)
<*> QC.vectorOf l' (rec n'),
VarDef <$> QC.arbitrary <*> QC.resize (n -1) QC.arbitrary,
FuncDef
<$> QC.arbitrary
<*> QC.listOf QC.arbitrary
<*> QC.resize (n -1) QC.arbitrary
]
else []
shrink =
filter (not . isAmbiguousIfElse) . QC.genericShrink
instance PP.Pretty Object where
pretty v = PP.group $ case v of
Module i args mBody ->
PP.pretty i
<> ( if null args
then PP.lparen <> PP.rparen
else PP.align (PP.tupled (PP.pretty <$> args))
)
<> case mBody of
Nothing -> PP.semi
Just os@(Objects _) -> PP.space <> PP.pretty os
Just o -> PP.nest 2 $ PP.line <> PP.pretty o
ForLoop i e o ->
"for"
<> PP.parens (PP.pretty i <+> "=" <+> PP.pretty e)
<> case o of
Objects _ -> PP.space <> PP.pretty o
_ -> PP.nest 2 $ PP.line <> PP.pretty o
Objects os ->
PP.enclose PP.lbrace (PP.line <> PP.rbrace)
. PP.nest 2
$ PP.line <> PP.vsep (PP.pretty <$> os)
If c t me ->
"if"
<+> PP.parens (PP.pretty c)
<> ( case t of
Objects _ -> PP.space <> PP.pretty t
_ -> PP.nest 2 (PP.line <> PP.pretty t)
)
<> maybe
mempty
(\e -> PP.line <> "else" <> PP.line <> PP.pretty e)
me
BackgroundMod o ->
"%" <> PP.pretty o
DebugMod o ->
"#" <> PP.pretty o
RootMod o ->
"!" <> PP.pretty o
DisableMod o ->
"*" <> PP.pretty o
ModuleDef {moduleName, moduleArgs, moduleBody} ->
"module"
<+> PP.pretty moduleName
<> ( if null moduleArgs
then PP.lparen <> PP.rparen
else
PP.align . PP.tupled $
moduleArgs <&> \(i, mV) ->
PP.pretty i
<> maybe
mempty
(\v -> PP.space <> PP.equals <+> PP.pretty v)
mV
)
<> ( case moduleBody of
[] -> PP.space <> PP.lbrace <> PP.rbrace
_ ->
PP.space
<> PP.enclose
PP.lbrace
(PP.line <> PP.rbrace)
(PP.nest 2 $ PP.line <> PP.vsep (PP.pretty <$> moduleBody))
)
VarDef {varName, varValue} ->
PP.pretty varName
<> PP.nest
2
( PP.line
<> PP.equals
<+> PP.group (PP.pretty varValue <> PP.semi)
)
FuncDef {funcName, funcArgs, funcBody} ->
"function"
<+> PP.pretty funcName
<> ( if null funcArgs
then PP.lparen <> PP.rparen
else PP.align . PP.tupled $ PP.pretty <$> funcArgs
)
<> PP.nest
2
( PP.line
<> PP.equals
<+> PP.pretty funcBody
<> PP.semi
)
isAmbiguousIfElse :: Object -> Bool
isAmbiguousIfElse v = case v of
If _ t (Just _) ->
let f e =
case e of
If _ _ Nothing -> True
If _ _ (Just e') -> f e'
ForLoop _ _ o -> f o
BackgroundMod o -> f o
DebugMod o -> f o
RootMod o -> f o
DisableMod o -> f o
Module _ _ (Just o) -> f o
_ -> False
in f t
_ -> False
data Expr
= EVar Ident
| EIndex Expr Expr
| ENum Double
| EVec [Expr]
| ERange (Range Expr)
| EString String
| EBool Bool
| EFunc Ident [Argument Expr]
| ENegate Expr
| EPlus Expr Expr
| EMinus Expr Expr
| EMult Expr Expr
| EDiv Expr Expr
| EMod Expr Expr
| EEquals Expr Expr
| ENotEquals Expr Expr
| EGT Expr Expr
| EGE Expr Expr
| ELT Expr Expr
| ELE Expr Expr
| ENot Expr
| EOr Expr Expr
| EAnd Expr Expr
| ETernary Expr Expr Expr
| EParen Expr
deriving (Show, Eq, Generic)
instance QC.Arbitrary Expr where
arbitrary =
QC.sized $
fix
( \rec p mAssoc n ->
let
simpleTerms =
[ EBool <$> QC.arbitrary,
EString <$> QC.arbitrary,
EVar <$> QC.arbitrary,
ENum <$> QC.arbitrary
]
recursiveTerms =
[ EParen <$> QC.resize (n -1) QC.arbitrary,
do
l <- QC.choose (0, n)
let n' = n `div` max 1 l
EVec <$> QC.vectorOf l (QC.resize n' QC.arbitrary),
ERange <$> QC.resize (n - 1) QC.arbitrary,
do
l <- QC.choose (0, n)
let n' = n `div` max 1 l
EFunc <$> QC.arbitrary
<*> QC.vectorOf l (QC.resize n' QC.arbitrary),
let n' = n `div` 2
in EIndex
<$> QC.resize
n'
(QC.oneof $ simpleTerms <> recursiveTerms)
<*> QC.resize n' QC.arbitrary
]
ops =
[ let n' = n `div` 3
in ETernary <$> QC.resize n' (QC.oneof simpleTerms)
<*> QC.resize n' QC.arbitrary
<*> QC.resize n' QC.arbitrary
| p == 0
]
++ catMaybes
[ genOp p' op
| (p', ops) <-
zip (reverse [1 .. length opTable]) opTable,
op <- ops
]
where
isNegatedNum e = case e of
ENegate e' ->
let f e'' = case e'' of
ENum _ -> True
EIndex e''' _ -> f e'''
_ -> False
in f e'
_ -> False
isNegativeNum e = case e of
ENum d -> d < 0
EIndex e' _ -> isNegativeNum e'
_ -> False
genOp p' op = case op of
Prefix (OperatorParser c _)
| p' > p ->
Just $
( c <$> rec (p' + 1) Nothing (n - 1)
`QC.suchThat` (not . isNegativeNum)
)
`QC.suchThat` (not . isNegatedNum)
Postfix (OperatorParser c _)
| p' > p ->
Just $ c <$> rec (p' + 1) Nothing (n - 1)
Infix (OperatorParser c _) assoc'
| p' > p || p' == p && maybe True (== assoc') mAssoc ->
let n' = n `div` 2
in Just $
c <$> rec (p' + 1) (Just assoc') n'
<*> rec (p' + 1) (Just assoc') n'
_ -> Nothing
in if n <= 0
then QC.oneof simpleTerms
else QC.oneof $ recursiveTerms <> ops
)
0
Nothing
shrink = QC.genericShrink
instance PP.Pretty Expr where
pretty e = case e of
EVar i -> PP.pretty i
EIndex e1 idx -> PP.pretty e1 <> PP.brackets (PP.pretty idx)
ENum d -> PP.pretty d
EVec es ->
if null es
then PP.lbracket <> PP.rbracket
else PP.align . PP.list $ PP.pretty <$> es
ERange r -> PP.pretty r
EString s ->
let escape c acc =
case lookup c escapedChars of
Just c' -> '\\' : c' : acc
Nothing -> c : acc
in PP.dquotes $ PP.pretty (foldr' escape "" s)
EBool b -> case b of
True -> "true"
False -> "false"
EFunc name args ->
PP.pretty name
<> ( if null args
then PP.lparen <> PP.rparen
else PP.align . PP.tupled $ (PP.pretty <$> args)
)
ENegate e1 -> prefix "-" e1
EPlus e1 e2 -> binary "+" e1 e2
EMinus e1 e2 -> binary "-" e1 e2
EMult e1 e2 -> binary "*" e1 e2
EDiv e1 e2 -> binary "/" e1 e2
EMod e1 e2 -> binary "%" e1 e2
EEquals e1 e2 -> binary "==" e1 e2
ENotEquals e1 e2 -> binary "!=" e1 e2
EGT e1 e2 -> binary ">" e1 e2
EGE e1 e2 -> binary ">=" e1 e2
ELT e1 e2 -> binary "<" e1 e2
ELE e1 e2 -> binary "<=" e1 e2
ENot e1 -> prefix "!" e1
EOr e1 e2 -> binary "||" e1 e2
EAnd e1 e2 -> binary "&&" e1 e2
ETernary e1 e2 e3 -> ternary "?" ":" e1 e2 e3
EParen e1 ->
PP.group
. PP.align
. PP.enclose (PP.flatAlt "( " "(") (PP.flatAlt " )" ")")
$ PP.pretty e1
where
prefix op e1 = op <> PP.pretty e1
binary op e1 e2 =
PP.align $
PP.pretty e1 <> PP.line <> op <+> PP.pretty e2
ternary op1 op2 e1 e2 e3 =
let f s =
PP.pretty e1
<> s
<> op1 <+> PP.pretty e2
<> s
<> op2 <+> PP.pretty e3
in PP.group $ PP.align (f PP.line) `PP.flatAlt` f PP.space
data Range a = Range
{ rangeStart :: a,
rangeEnd :: a,
rangeStep :: Maybe a
}
deriving (Show, Eq, Generic1)
instance QC.Arbitrary a => QC.Arbitrary (Range a) where
arbitrary = Range <$> QC.arbitrary <*> QC.arbitrary <*> QC.arbitrary
instance PP.Pretty a => PP.Pretty (Range a) where
pretty (Range start end mStep) =
PP.align
. PP.group
. PP.encloseSep
(PP.flatAlt "[ " "[")
(PP.flatAlt " ]" "]")
(PP.flatAlt ": " ":")
. fmap PP.pretty
$ [start] <> maybe [] pure mStep <> [end]
sepByTill :: Parser delim -> Parser end -> Parser a -> Parser [a]
sepByTill delim end parser = (end *> return []) <|> go []
where
go xs = do
x <- parser
let xs' = x : xs
(end *> return (reverse xs')) <|> (delim >> go xs')
betweenSepBy ::
Parser delim ->
Parser start ->
Parser end ->
Parser a ->
Parser [a]
betweenSepBy delim start end parser = start >> sepByTill delim end parser
equals :: Parser Char
equals = symbolic '='
refArguments :: Parser [Argument Expr]
refArguments = list <?> "argument list"
where
list = parens $ commaSep $ try namedArg <|> arg
namedArg = do
name <- try $ ident <* equals
value <- expression
return $ NamedArgument name value
arg = spaces >> Argument <$> expression
range :: Parser (Range Expr)
range = brackets $ do
e1 <- expression
colon
e2 <- expression
me3 <- option Nothing $ do
colon
Just <$> expression
return $ case me3 of
Nothing -> Range {rangeStart = e1, rangeEnd = e2, rangeStep = Nothing}
Just e3 -> Range {rangeStart = e1, rangeEnd = e3, rangeStep = Just e2}
double' :: Parser Double
double' =
notIdent $
choice
[ try $ do
s <- sign
n <- decimal
f <-
try fractExponent
<|> (fromInteger <$ char '.')
<|> pure fromInteger
return $ realToFrac $ s $ f n,
do
s <- sign
realToFrac . s . ($ 0) <$> fractExponent
]
fractExponent :: forall m. TokenParsing m => m (Integer -> Sci.Scientific)
fractExponent =
( \fract expo n ->
(fromInteger n + fract) * expo
)
<$> fraction
<*> option 1 exponent'
<|> (\expo n -> fromInteger n * expo)
<$> exponent'
where
fraction :: m Sci.Scientific
fraction = foldl' op 0 <$> (char '.' *> (some digit <?> "fraction"))
op f d =
f
+ Sci.scientific (fromIntegral (digitToInt d)) (Sci.base10Exponent f - 1)
exponent' :: m Sci.Scientific
exponent' =
((\f e -> power (f e)) <$ oneOf "eE" <*> sign <*> (decimal <?> "exponent"))
<?> "exponent"
power = Sci.scientific 1 . fromInteger
sign :: (Num a, TokenParsing m) => m (a -> a)
sign =
negate <$ char '-'
<|> id <$ char '+'
<|> pure id
escapedChars :: [(Char, Char)]
escapedChars =
[ ('\\', '\\'),
('"', '"'),
('\t', 't'),
('\n', 'n'),
('\r', 'r')
]
term :: Parser Expr
term = do
e <-
choice
[ try funcRef,
ERange <$> try range,
EVec <$> brackets (sepEndBy expression (some comma)),
EString <$> stringLit,
EBool
<$> choice
[ keyword "true" >> return True,
keyword "false" >> return False
],
EVar <$> ident,
ENum <$> double',
EParen <$> parens expression
]
idxs <- many $ brackets expression <?> "index expression"
spaces
return $ foldl' EIndex e idxs
where
funcRef = do
name <- ident
args <- refArguments
return $ EFunc name args
stringLit =
between (char '"') (char '"') $
many $ escapedChar <|> notChar '"'
escapedChar =
char '\\'
>> choice ((\(c, escapeChar) -> char escapeChar $> c) <$> escapedChars)
notIdent :: Parser a -> Parser a
notIdent parser = do
x <- parser
notFollowedBy $ oneOfSet identChars
return x
keyword :: String -> Parser ()
keyword word = void $ notIdent (symbol word)
expression :: Parser Expr
expression =
choice
[ try ternary,
buildExpressionParser opTable' term
]
<?> "expression"
where
ternary = do
e1 <- term
symbolic '?'
e2 <- expression
colon
e3 <- expression
return $ ETernary e1 e2 e3
opTable' :: [[Operator Parser Expr]]
opTable' =
let mkParser op = case op of
Infix (OperatorParser fun p) assoc -> Infix (fun <$ p) assoc
Prefix (OperatorParser fun p) -> Prefix (fun <$ p)
Postfix (OperatorParser fun p) -> Postfix (fun <$ p)
in fmap mkParser <$> opTable
data OperatorParser a = OperatorParser
{ opFun :: a,
opParser :: Parser ()
}
opTable :: [[Operator OperatorParser Expr]]
opTable =
[ [ Prefix
(OperatorParser ENegate (try $ reservedOp "-" >> notFollowedBy double')),
Prefix
(OperatorParser id (try $ reservedOp "+" >> notFollowedBy double')),
prefix "!" ENot
],
[ binary "*" EMult AssocLeft,
binary "/" EDiv AssocLeft,
binary "%" EMod AssocLeft
],
[binary "+" EPlus AssocLeft, binary "-" EMinus AssocLeft],
[ binary "==" EEquals AssocLeft,
binary "!=" ENotEquals AssocLeft,
binary ">" EGT AssocLeft,
binary ">=" EGE AssocLeft,
binary "<" ELT AssocLeft,
binary "<=" ELE AssocLeft
],
[binary "||" EOr AssocLeft, binary "&&" EAnd AssocLeft]
]
where
binary name fun assoc = Infix (OperatorParser fun (reservedOp name)) assoc
prefix name fun = Prefix (OperatorParser fun (reservedOp name))
reservedOp name = reserve emptyOps name
comment :: Parser String
comment = do
spaces
(singleLine <|> multiLine) <?> "comment"
where
singleLine = string "//" *> manyTill anyChar (char '\n')
multiLine = string "/*" *> manyTill anyChar (string "*/")
block :: Parser a -> Parser [a]
block parser = do
xs <- between (char '{' >> spaces) (char '}') (many parser)
spaces
optional someSemis
return xs
object :: Parser Object
object =
spaces
>> choice
[ forLoop <?> "for loop",
conditional <?> "if statement",
moduleDef <?> "module definition",
funcDef <?> "function definition",
try varDef <?> "variable definition",
moduleRef <?> "module reference",
Objects <$> block object,
mod '%' BackgroundMod,
mod '#' DebugMod,
mod '!' RootMod,
mod '*' DisableMod
]
where
moduleRef = do
name <- ident
args <- refArguments
spaces
block <- (someSemis >> return Nothing) <|> fmap Just object
return $ Module name args block
forLoop = do
symbol "for"
(var, range) <-
parens $
((,) <$> ident <* equals <*> expression)
body <- object
return $ ForLoop var range body
conditional = do
symbol "if"
e <- parens expression
_then <- object
_else <- optional $ do
symbol "else"
object
return $ If e _then _else
mod :: Char -> (Object -> Object) -> Parser Object
mod c f = do
symbolic c
f <$> object
moduleDef = do
symbol "module"
name <- ident
args <- arguments
body <-
choice
[ braces $ many object,
singleton <$> object
]
return $ ModuleDef name args body
where
arguments = parens $
commaSep $ do
name <- ident
value <- optional $ equals >> expression
return (name, value)
varDef = do
name <- ident
equals
value <- expression
someSemis
return $ VarDef name value
funcDef = do
symbol "function"
name <- ident <* spaces
args <- parens $ commaSep ident
equals
body <- expression
someSemis
return $ FuncDef name args body
data TopLevel
= TopLevelScope Object
| UseDirective String
| IncludeDirective String
deriving (Show, Eq, Generic)
instance QC.Arbitrary TopLevel where
arbitrary =
QC.oneof
[ TopLevelScope <$> QC.arbitrary,
UseDirective <$> (QC.arbitrary `QC.suchThat` notElem '>'),
IncludeDirective <$> (QC.arbitrary `QC.suchThat` notElem '>')
]
shrink = QC.genericShrink
instance PP.Pretty TopLevel where
pretty v = case v of
TopLevelScope o -> PP.pretty o
IncludeDirective s -> "include" <> PP.angles (PP.pretty s)
UseDirective s -> "use" <> PP.angles (PP.pretty s)
topLevel :: Parser TopLevel
topLevel = do
spaces
optional someSemis
tl <-
choice
[ UseDirective <$> fileDirective "use",
IncludeDirective <$> fileDirective "include",
TopLevelScope <$> object
]
optional someSemis
return tl
where
fileDirective keyword = try $ do
symbol keyword
path <- runUnspaced $ angles $ many (notChar '>')
optional someSemis
return path
parseFile :: FilePath -> IO (Either String [TopLevel])
parseFile = fmap resultToEither . parseFromFileEx (some topLevel)
parse :: BS.ByteString -> Either String [TopLevel]
parse =
resultToEither . parseByteString (some topLevel <* eof) mempty . stripComments
resultToEither :: Result a -> Either String a
resultToEither (Failure err) = Left $ show err
resultToEither (Success r) = Right r
stripComments :: BS.ByteString -> BS.ByteString
stripComments = go BS.empty
where
go accum b | BS.null b = accum
go accum b =
let (before, after) = BS.span (/= '/') b
(before', after') = case after of
c
| BS.null c -> (before, BS.empty)
| "/*" `BS.isPrefixOf` c ->
let (_, d) = BS.breakSubstring "*/" c
in (before, BS.drop 2 d)
| "//" `BS.isPrefixOf` c -> (before, BS.dropWhile (/= '\n') c)
| otherwise -> (before <> "/", BS.drop 1 after)
in go (accum <> before') after'
singleton :: a -> [a]
singleton x = [x]
someSemis :: Parser ()
someSemis = sepEndBy1 semi spaces >> return ()