packages feed

sound-change-0.1.0.1: src/Language/Change/Quote.hs

{-|
Module     : Language.Change.Quote
Copyright  : (c) Owen Bechtel, 2023
License    : MIT
Maintainer : ombspring@gmail.com
Stability  : experimental
-}

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TupleSections #-}
module Language.Change.Quote (ch, chs) where

import Data.Data (Data)
import Data.Void (Void)
import Data.List.NonEmpty (NonEmpty(..), toList, cons)
import Data.Char (isAlphaNum, isAsciiUpper, isSpace)
import Data.Bifunctor (first)
import Control.Monad (void)

import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Set (Set)

import qualified Text.Megaparsec as M
import Text.Megaparsec ((<|>))
import qualified Text.Megaparsec.Char as MC
import qualified Text.Megaparsec.Char.Lexer as Lexer
import qualified Control.Monad.Combinators.NonEmpty as NE

import Control.Monad.Reader (ReaderT(..), local)

import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote(QuasiQuoter(..), dataToExpQ)
import Language.Haskell.TH.Lib (appE, conE, varE)
import Data.Generics.Aliases (extQ)

import Language.Change (Change(..), Env(..), PSet(..), Pattern(..))

data CharS
  = Lit Char
  | AntiQ String -- antiquoted identifier
  | CList [Char] -- does not exist in syntax; used for compilation of antiquotes
  deriving (Data)

newtype SetS = SetS (NonEmpty CharS)
  deriving (Data)

data PSetS = PSetS SetS Bool
  deriving (Data)

data PatternS
  = OneS PSetS
  | OptionalS PSetS
  | ManyS PSetS
  deriving (Data)

data EnvS = EnvS [PatternS] [PatternS]
  deriving (Data)

type Pairing a b = NonEmpty (a, b)
type SoundList = NonEmpty CharS
type EnvList = NonEmpty EnvS
newtype Rep = Rep { convertRep :: String }
  deriving (Data)

data StatementS
  = Simple SoundList Rep EnvList
  | SoundSplit SoundList (Pairing Rep EnvList)
  | EnvSplit (Pairing SoundList Rep) EnvList 
  deriving (Data)

newtype ChangeS = ChangeS (NonEmpty StatementS)
  deriving (Data)

type Parser = ReaderT Bool (M.Parsec Void String) -- True = allow newlines

allowNewlines :: Parser a -> Parser a
allowNewlines = local (const True)

isSound, isIdentifier :: Char -> Bool
isSound c = not (isSpace c || isAsciiUpper c || c `elem` ",>/;{}[]%_!?*")
isIdentifier c = isAlphaNum c || c == '_' || c == '\''

spaces :: Parser ()
spaces = ReaderT $ \allowNs ->
  Lexer.space (if allowNs then MC.space1 else MC.hspace1) lineComment M.empty
  where lineComment = Lexer.skipLineComment "//"

symbol :: Char -> Parser ()
symbol c = M.single c >> spaces

charS :: Parser CharS
charS = M.choice [ lit, oneChar, multipleChar ]
  where
    lit = Lit <$> M.satisfy isSound <* spaces

    oneChar = AntiQ . pure <$> M.satisfy isAsciiUpper <* spaces

    multipleChar = AntiQ
      <$ symbol '['
      <*> M.takeWhile1P (Just "Haskell identifier") isIdentifier
      <* spaces
      <* symbol ']'

setS :: Parser SetS
setS = M.choice
  [ SetS . pure <$> charS
  , SetS <$ symbol '{' <*> NE.some charS <* symbol '}'
  ]

pSetS :: Parser PSetS
pSetS = PSetS 
  <$> setS 
  <*> M.choice [ False <$ symbol '!', return True ]

patternS :: Parser PatternS
patternS = do
  pset <- pSetS
  M.choice
    [ OptionalS pset <$ symbol '?'
    , ManyS pset <$ symbol '*'
    , return (OneS pset)
    ]

envS :: Parser EnvS
envS = EnvS
  <$> (reverse <$> M.many patternS)
  <* symbol '_'
  <*> M.many patternS

pairing :: Parser a -> Parser () -> Parser b -> Parser (Pairing a b)
pairing pa psep pb = do
  allowNewlines (symbol '{')
  ps <- allowNewlines (NE.sepBy1 pair (symbol ';'))
  symbol '}'
  return ps
  where
    pair = (,) <$> pa <* psep <*> pb

soundList :: Parser SoundList
soundList = NE.sepBy1 charS (symbol ',')

envList :: Parser EnvList
envList = NE.sepBy1 envS (symbol ',')

rep :: Parser Rep
rep = M.choice
  [ Rep <$> M.takeWhile1P (Just "phoneme") isSound <* spaces
  , Rep "" <$ symbol '%'
  ]

statementS :: Parser StatementS
statementS = envSplit <|> other
  where
    envSplit = EnvSplit 
      <$> pairing soundList (symbol '>') rep 
      <* symbol '/' 
      <*> envList
    
    other = do
      sounds <- soundList
      symbol '>'
      M.choice
        [ SoundSplit sounds <$> pairing rep (symbol '/') envList
        , Simple sounds <$> rep <* symbol '/' <*> envList
        ]

someNewlines :: Parser ()
someNewlines = void (M.some (symbol '\n' <|> symbol '\r'))

changeS :: Parser ChangeS
changeS = ChangeS <$> NE.sepEndBy1 statementS someNewlines

bulletedChanges :: Parser (NonEmpty ChangeS)
bulletedChanges = do
  symbol '*'
  stmt <- statementS
  newline <- M.option False (True <$ someNewlines)
  if newline 
    then M.choice 
      [ (ChangeS (stmt :| []) `cons`) <$> bulletedChanges
      , (\(stmts, cs) -> ChangeS (stmt `cons` stmts) :| cs) <$> nonBulleted
      , return (ChangeS (stmt :| []) :| [])
      ]
    else return (ChangeS (stmt :| []) :| [])

  where
    nonBulleted :: Parser (NonEmpty StatementS, [ChangeS])
    nonBulleted = do
      stmt <- statementS
      newline <- M.option False (True <$ someNewlines)
      if newline
        then M.choice
          [ (\cs -> (stmt :| [], toList cs)) <$> bulletedChanges
          , first (stmt `cons`) <$> nonBulleted
          , return (stmt :| [], [])
          ]
        else return (stmt :| [], [])

total :: Parser a -> Parser a
total p = allowNewlines spaces >> (p <* M.eof)

{-|
Compile a sound change specification into a value of type t'Change' 'Char'.

Example:

@
setV = "aeiou"

change1 = [ch|
  m, n, ŋ > { m \/ _{mpb}; n \/ _{ntd}; ŋ \/ _{ŋkg} }
  { p > b; t > d; k > g } \/ V_V
  |]
@

The sound change in this example consists of two statements separated by a newline. The first statement says 
\"m, n, and ŋ become \/m\/ when followed by m, p, or b, \/n\/ when followed by n, t, or d, and \/ŋ\/ when followed by ŋ, k, or g.\"
The second statement says that voiceless stops (p, t, and k) become voiced when between two vowels. The uppercase @V@ here is
shorthand for \"any element of @setV@.\" 

All uppercase letters are interpreted in this way, which means they are not allowed as phonemes. 
Whitespace characters, as well as the following symbols ( @,>\/;{}[]%_!?*@ ) are also not allowed as phonemes. 
All other unicode characters are allowed. (For example, you can use @#@ as a \"phoneme\" representing the start or end of a word.)
 
Here's another example:

@
change2 = [ch|
  V > % \/ s_{ptk}             \/\/ vowel loss after \/s\/ before stops
  { o > ø; u > y } \/ _V!*{ji} \/\/ umlaut before \/j\/ and \/i\/ (with optional consonants in between) 
  y > i \/ _                   \/\/ unconditional shift
 |]
@

This example illustrates several features:

* You can create line comments with @\/\/@. 

* The left side of @>@ need not be a single phoneme; it can also be a set of phonemes (in this case @V@).

* The symbol @%@ represents the empty string.

* Certain suffixes can be added to phonemes and phoneme-sets to create more complex patterns:

    * @X!@ matches anything /not/ in the set @X@.

    * @X*@ matches zero or more elements of @X@.

    * @X?@ matches zero or one elements of @X@.

    * There are also the combinations @X!*@ and @X!?@.

* To create an unconditional sound change, use the empty environment @_@.

* The statements in a sound change are applied in a single traversal.
This means that the same phoneme cannot be affected by more than one statement.
For example, the change above would convert \"yni\" into \"ini,\" and \"uni\" into \"yni.\"
(If you want to sequence multiple changes, use 'chs'.)

* If multiple statements are applicable to the same phoneme, the first one is applied.
For example, the change above would convert \"soki\" into \"ski\", not \"søki.\"

Other noteworthy things:

* As mentioned above, capital letters refer to sets of characters: @V@ refers to @setV@, @N@ refers to @setN@, and so on.
To refer to sets with longer names, you can use square brackets; 
for example, if you define @setNasal :: [Char]@, you can refer to it with @[Nasal]@.

* It is possible to have multiple conditions in a single statement. For example, @e > i / _a, a_@ means \"\/e\/ becomes \/i\/ when before \/a\/ /or/ after \/a\/.\" 

* The parser is whitespace-sensitive. Horizontal space characters can go almost anywhere, 
but newlines are only allowed between statements (where they are required)
and inside of @{}@ blocks.
-}

ch :: QuasiQuoter
ch = QuasiQuoter 
  { quoteExp = makeQuoter (total changeS) (varE 'convertChangeS)
  , quotePat = undefined 
  , quoteType = undefined
  , quoteDec = undefined 
  }

{-|
Compile a sequence of sound changes into a value of type @[t'Change' 'Char']@.
Each change is preceded by a bullet @*@. For example:

@
changes = [chs|
  * p > f \/ _{tk}
    t > θ \/ _{pk}
    k > x \/ _{pt} \/\/ these three statements are applied in a single traversal
  * x > ç \/ i_    \/\/ this statement is applied after the ones before it
  * i > j \/ _V
  |]
@

For example, @'Language.Change.applyChanges' changes "ikt"@ would return @"içt"@.
-}

chs :: QuasiQuoter
chs = QuasiQuoter 
  { quoteExp = makeQuoter (toList <$> total bulletedChanges) (appE (varE 'map) (varE 'convertChangeS))
  , quotePat = undefined 
  , quoteType = undefined
  , quoteDec = undefined 
  }
  
initialState :: s -> M.SourcePos -> M.State s Void
initialState input position =
  M.State
    { M.stateInput = input
    , M.stateOffset = 0
    , M.statePosState =
        M.PosState
          { M.pstateInput = input
          , M.pstateOffset = 0
          , M.pstateSourcePos = position
          , M.pstateTabWidth = M.defaultTabWidth
          , M.pstateLinePrefix = ""
          }
    , M.stateParseErrors = []
    }

makeQuoter :: (Data a) => Parser a -> TH.ExpQ -> String -> TH.ExpQ
makeQuoter parse convert input = do
  loc <- TH.location
  let file = TH.loc_filename loc
      (line, col) = TH.loc_start loc
      state = initialState input (M.SourcePos file (M.mkPos line) (M.mkPos col))
  
  case snd (M.runParser' (runReaderT parse False) state) of
    Left errors -> fail (M.errorBundlePretty errors)
    Right change -> 
      let change' = dataToExpQ (const Nothing `extQ` antiquote) change
      in appE convert change'

antiquote :: CharS -> Maybe TH.ExpQ
antiquote = \case
  Lit _ -> Nothing
  AntiQ s -> Just (appE (conE 'CList) (varE (TH.mkName ("set" ++ s))))
  CList _ -> Nothing  

badAntiQ :: a
badAntiQ = error "Language.Change.Quote: Unexpected AntiQ at runtime.\nThis error should never happen."

convertCharList :: CharS -> [Char]
convertCharList = \case
  Lit c -> [c]
  AntiQ _ -> badAntiQ
  CList cs -> cs

convertCharSet :: CharS -> Set Char
convertCharSet = \case
  Lit c -> Set.singleton c
  AntiQ _ -> badAntiQ 
  CList cs -> Set.fromList cs

convertSetS :: SetS -> Set Char
convertSetS (SetS cs) = foldMap convertCharSet cs

convertPSetS :: PSetS -> PSet Char
convertPSetS (PSetS set b) = PSet (convertSetS set) b 

convertPatternS :: PatternS -> Pattern Char
convertPatternS = \case
  OneS set -> One (convertPSetS set)
  OptionalS set -> Optional (convertPSetS set)
  ManyS set -> Many (convertPSetS set)

convertEnvS :: EnvS -> Env Char
convertEnvS (EnvS e1 e2) = Env (map convertPatternS e1) (map convertPatternS e2)

convertEnvPair :: (Rep, EnvList) -> [(String, Env Char)]
convertEnvPair (rp, envs) =
  let rp' = convertRep rp
  in map (\env -> (rp', convertEnvS env)) (toList envs)

convertStatementS :: StatementS -> [(Char, [(String, Env Char)])]
convertStatementS = \case
  Simple sounds rp envs ->
    let pairs = convertEnvPair (rp, envs) 
    in map (, pairs) (concatMap convertCharList sounds)

  SoundSplit sounds envPairs -> 
    let pairs = concatMap convertEnvPair envPairs 
    in map (, pairs) (concatMap convertCharList sounds)
    
  EnvSplit soundPairs envs ->
    let envs' = map convertEnvS (toList envs)
    in concatMap (\(sounds, rp) -> 
          let rp' = convertRep rp
              pairs = map (rp', ) envs'
          in map (, pairs) (concatMap convertCharList sounds))
        soundPairs

convertChangeS :: ChangeS -> Change Char
convertChangeS (ChangeS stmts) = Change (Map.fromListWith (flip (++)) (concatMap convertStatementS stmts))