packages feed

crucible-syntax-0.4.1: src/Lang/Crucible/Syntax/SExpr.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}
module Lang.Crucible.Syntax.SExpr
  ( pattern A
  , pattern L
  , pattern (:::)
  , Syntax(..)
  , Datum(..)
  , Syntactic(..)
  ,  Parser
  , syntaxPos
  , withPosFrom
  , sexp
  , identifier
  , toText
  , datumToText
  , skipWhitespace
  , PrintRules(..)
  , PrintStyle(..)
  , Layer(..)
  , IsAtom(..)
  ) where

import Data.Char (isDigit, isLetter)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Void
import What4.ProgramLoc as C

import Text.Megaparsec as MP
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import qualified Prettyprinter as PP
import qualified Prettyprinter.Render.Text as PP (renderStrict)


-- | Syntax objects, in which each layer is annotated with a source position.
newtype Syntax a = Syntax { unSyntax :: Posd (Layer Syntax a) }
  deriving (Show, Functor, Eq)

-- | Syntax objects divorced of their source-code context, without source positions.
newtype Datum a = Datum { unDatum :: Layer Datum a}
  deriving (Show, Functor, Eq)

-- | Extract the source position from a 'Syntax' object.
syntaxPos :: Syntax a -> Position
syntaxPos (Syntax (Posd p _)) = p

-- | Use the position from a syntax object around something else.
withPosFrom :: Syntax a -> b -> Posd b
withPosFrom stx x = Posd (syntaxPos stx) x

-- | Instances of 'Syntactic' support observations using the 'L' and 'A' patterns.
class Syntactic a b | a -> b where
  syntaxE :: a -> Layer Syntax b

instance Syntactic (Layer Syntax a) a where
  syntaxE = id


instance Syntactic (Syntax a) a where
  syntaxE (Syntax (Posd _ e)) = e

-- | Match an atom from a syntactic structure
pattern A :: Syntactic a b => b -> a
pattern A x <- (syntaxE -> Atom x)

-- | Match a list from a syntactic structure
pattern L :: Syntactic a b => [Syntax b] -> a
pattern L xs <- (syntaxE -> List xs)

-- | Match the head and tail of a list-like structure
pattern (:::) :: Syntactic a b => Syntax b -> [Syntax b] -> a
pattern x ::: xs <- (syntaxE -> List (x : xs))

-- | The pattern functor for syntax, used both for 'Syntax' and
-- 'Datum'. In 'Syntax', it is composed with another structure that
-- adds source positions.
data Layer f a = List [f a] | Atom a
  deriving (Show, Functor, Eq)

-- | Convert any syntactic structure to its simplest description.
syntaxToDatum :: Syntactic expr atom => expr -> Datum atom
syntaxToDatum (A x) = Datum (Atom x)
syntaxToDatum (L xs) = Datum (List (map syntaxToDatum xs))
syntaxToDatum _ = error "impossible - bad Syntactic instance"

-- | A parser for s-expressions.
type Parser = Parsec Void Text

-- | Skip whitespace.
skipWhitespace :: Parser ()
skipWhitespace = L.space space1 lineComment blockComment
  where lineComment = L.skipLineComment ";"
        blockComment = L.skipBlockComment "#|" "|#"

-- | Skip the whitespace after a token.
lexeme :: Parser a -> Parser a
lexeme = L.lexeme skipWhitespace

-- | Parse something with its location.
withPos :: Parser a -> Parser (Posd a)
withPos p =
  do MP.SourcePos file line col <- getSourcePos
     let loc = C.SourcePos (T.pack file) (unPos line) (unPos col)
     res <- p
     return $ Posd loc res

-- | Parse a particular string.
symbol :: Text -> Parser Text
symbol = L.symbol skipWhitespace

-- | Parse a parenthesized list.
list :: Parser (Syntax a) -> Parser (Syntax a)
list p =
  do Posd loc _ <- withPos (symbol "(")
     xs <- many p
     _ <- lexeme $ symbol ")"
     return $ Syntax (Posd loc (List xs))

-- | Given a parser for atoms, parse an s-expression that contains them.
sexp :: Parser a -> Parser (Syntax a)
sexp atom =
  (Syntax . fmap Atom <$> lexeme (withPos atom)) <|>
  list (sexp atom)

-- | Parse an identifier.
identifier :: Parser Text
identifier = T.pack <$> identString
  where letterLike x = isLetter x || elem x ("<>=+-*/!_\\?" :: [Char])
        nameChar x = letterLike x || isDigit x || elem x ("$" :: [Char])
        identString = (:) <$> satisfy letterLike <*> many (satisfy nameChar)

-- | Styles of printing
data PrintStyle =
  -- | Special forms should treat the first n subforms as special, and
  -- the remaining as a body. For instance, for a Lisp-like
  -- let-expression, use 'Special 1' for indentation.
  Special Int

-- | Printing rules describe how to specially format expressions that
-- begin with particular atoms.
newtype PrintRules a = PrintRules (a -> Maybe PrintStyle)

instance Semigroup (PrintRules a) where
  PrintRules f <> PrintRules g = PrintRules $ \z -> f z <|> g z

instance Monoid (PrintRules a) where
  mempty = PrintRules $ const Nothing


class IsAtom a where
  showAtom :: a -> Text

pprint :: (Syntactic expr a, IsAtom a) => PrintRules a -> expr -> PP.Doc ann
pprint rules expr = pprintDatum rules (syntaxToDatum expr)

pprintDatum :: IsAtom a => PrintRules a -> Datum a -> PP.Doc ann
pprintDatum rules@(PrintRules getLayout) stx =
  case unDatum stx of
    Atom at -> ppAtom at
    List lst ->
      PP.parens . PP.group $
      case lst of
        [] -> mempty
        [x] -> pprintDatum rules x
        ((unDatum -> Atom car) : xs) ->
          case getLayout car of
            Nothing -> ppAtom car <> PP.space <> PP.align (PP.vsep $ pprintDatum rules <$> xs)
            Just (Special i) ->
              let (special, rest) = splitAt i xs
              in PP.hang 2 $ PP.vsep $
                 PP.group (PP.hang 2 $ PP.vsep $ ppAtom car : (map (pprintDatum rules) special)) :
                 map (pprintDatum rules) rest
        xs -> PP.vsep $ pprintDatum rules <$> xs

  where ppAtom = PP.pretty . showAtom

-- | Render a syntactic structure to text, according to rules.
toText :: (Syntactic expr a, IsAtom a) => PrintRules a -> expr -> Text
toText rules stx = PP.renderStrict (PP.layoutSmart opts $ pprint rules stx)
  where opts = PP.LayoutOptions (PP.AvailablePerLine 80 0.8)

-- | Render a datum to text according to rules.
datumToText :: IsAtom a => PrintRules a -> Datum a -> Text
datumToText rules dat = PP.renderStrict (PP.layoutSmart opts $ pprintDatum rules dat)
  where opts = PP.LayoutOptions (PP.AvailablePerLine 80 0.8)