symantic-parser-0.0.0.20210101: src/Symantic/Parser/Machine/Instructions.hs
{-# LANGUAGE ConstraintKinds #-} -- For Executable
{-# LANGUAGE DerivingStrategies #-} -- For Show (LetName a)
{-# LANGUAGE PatternSynonyms #-} -- For Fmap, App, …
{-# LANGUAGE UndecidableInstances #-} -- For Cursorable (Cursor inp)
module Symantic.Parser.Machine.Instructions where
import Data.Bool (Bool(..))
import Data.Either (Either)
import Data.Eq (Eq)
import Data.Ord (Ord)
import Data.Function (($), (.))
import Data.Kind (Type)
import System.IO.Unsafe (unsafePerformIO)
import Text.Show (Show(..), showString)
import qualified Data.Functor as Functor
import qualified Language.Haskell.TH as TH
import qualified Language.Haskell.TH.Syntax as TH
import qualified Symantic.Parser.Haskell as H
import Symantic.Parser.Grammar
import Symantic.Parser.Machine.Input
import Symantic.Univariant.Trans
-- * Type 'Instr'
-- | 'Instr'uctions for the 'Machine'.
data Instr input valueStack (failStack::Peano) returnValue where
-- | @('Push' x k)@ pushes @(x)@ on the 'valueStack'
-- and continues with the next 'Instr'uction @(k)@.
Push ::
InstrPure v ->
Instr inp (v ': vs) es ret ->
Instr inp vs es ret
-- | @('Pop' k)@ pushes @(x)@ on the 'valueStack'.
Pop ::
Instr inp vs es ret ->
Instr inp (v ': vs) es ret
-- | @('LiftI2' f k)@ pops two values from the 'valueStack',
-- and pushes the result of @(f)@ applied to them.
LiftI2 ::
InstrPure (x -> y -> z) ->
Instr inp (z : vs) es ret ->
Instr inp (y : x : vs) es ret
-- | @('Fail')@ raises an error from the 'failStack'.
Fail ::
[ErrorItem (InputToken inp)] ->
Instr inp vs ('Succ es) ret
-- | @('PopFail' k)@ removes a 'FailHandler' from the 'failStack'
-- and continues with the next 'Instr'uction @(k)@.
PopFail ::
Instr inp vs es ret ->
Instr inp vs ('Succ es) ret
-- | @('CatchFail' l r)@ tries the @(l)@ 'Instr'uction
-- in a new failure scope such that if @(l)@ raises a failure, it is caught,
-- then the input is pushed as it was before trying @(l)@ on the 'valueStack',
-- and the control flow goes on with the @(r)@ 'Instr'uction.
CatchFail ::
Instr inp vs ('Succ es) ret ->
Instr inp (Cursor inp ': vs) es ret ->
Instr inp vs es ret
-- | @('LoadInput' k)@ removes the input from the 'valueStack'
-- and continues with the next 'Instr'uction @(k)@ using that input.
LoadInput ::
Instr inp vs es r ->
Instr inp (Cursor inp : vs) es r
-- | @('PushInput' k)@ pushes the input @(inp)@ on the 'valueStack'
-- and continues with the next 'Instr'uction @(k)@.
PushInput ::
Instr inp (Cursor inp ': vs) es ret ->
Instr inp vs es ret
-- | @('Case' l r)@.
Case ::
Instr inp (x ': vs) es r ->
Instr inp (y ': vs) es r ->
Instr inp (Either x y ': vs) es r
-- | @('Swap' k)@ pops two values on the 'valueStack',
-- pushes the first popped-out, then the second,
-- and continues with the next 'Instr'uction @(k)@.
Swap ::
Instr inp (x ': y ': vs) es r ->
Instr inp (y ': x ': vs) es r
-- | @('Choices' ps bs d)@.
Choices ::
[InstrPure (v -> Bool)] ->
[Instr inp vs es ret] ->
Instr inp vs es ret ->
Instr inp (v ': vs) es ret
-- | @('Subroutine' n v k)@ binds the 'LetName' @(n)@ to the 'Instr'uction's @(v)@,
-- 'Call's @(n)@ and
-- continues with the next 'Instr'uction @(k)@.
Subroutine ::
LetName v -> Instr inp '[] ('Succ 'Zero) v ->
Instr inp vs ('Succ es) ret ->
Instr inp vs ('Succ es) ret
-- | @('Jump' n k)@ pass the control-flow to the 'Subroutine' named @(n)@.
Jump ::
LetName ret ->
Instr inp '[] ('Succ es) ret
-- | @('Call' n k)@ pass the control-flow to the 'Subroutine' named @(n)@,
-- and when it 'Ret'urns, continues with the next 'Instr'uction @(k)@.
Call ::
LetName v ->
Instr inp (v ': vs) ('Succ es) ret ->
Instr inp vs ('Succ es) ret
-- | @('Ret')@ returns the value stored in a singleton 'valueStack'.
Ret ::
Instr inp '[ret] es ret
-- | @('Read' expected p k)@ reads a 'Char' @(c)@ from the 'inp'ut,
-- if @(p c)@ is 'True' then continues with the next 'Instr'uction @(k)@ on,
-- otherwise 'Fail'.
Read ::
[ErrorItem (InputToken inp)] ->
InstrPure (InputToken inp -> Bool) ->
Instr inp (InputToken inp ': vs) ('Succ es) ret ->
Instr inp vs ('Succ es) ret
DefJoin ::
LetName v -> Instr inp (v ': vs) es ret ->
Instr inp vs es ret ->
Instr inp vs es ret
RefJoin ::
LetName v ->
Instr inp (v ': vs) es ret
-- ** Type 'InstrPure'
data InstrPure a where
InstrPureHaskell :: H.Haskell a -> InstrPure a
InstrPureSameOffset :: Cursorable cur => InstrPure (cur -> cur -> Bool)
instance Show (InstrPure a) where
showsPrec p = \case
InstrPureHaskell x -> showsPrec p x
InstrPureSameOffset -> showString "InstrPureSameOffset"
instance Trans InstrPure TH.CodeQ where
trans = \case
InstrPureHaskell x -> trans x
InstrPureSameOffset -> sameOffset
-- ** Type 'LetName'
newtype LetName a = LetName { unLetName :: TH.Name }
deriving (Eq)
deriving newtype Show
-- * Class 'Executable'
type Executable repr =
( Stackable repr
, Branchable repr
, Failable repr
, Inputable repr
, Routinable repr
, Joinable repr
)
-- ** Class 'Stackable'
class Stackable (repr :: Type -> [Type] -> Peano -> Type -> Type) where
push ::
InstrPure v ->
repr inp (v ': vs) n ret ->
repr inp vs n ret
pop ::
repr inp vs n ret ->
repr inp (v ': vs) n ret
liftI2 ::
InstrPure (x -> y -> z) ->
repr inp (z ': vs) es ret ->
repr inp (y ': x ': vs) es ret
swap ::
repr inp (x ': y ': vs) n r ->
repr inp (y ': x ': vs) n r
-- ** Class 'Branchable'
class Branchable (repr :: Type -> [Type] -> Peano -> Type -> Type) where
case_ ::
repr inp (x ': vs) n r ->
repr inp (y ': vs) n r ->
repr inp (Either x y ': vs) n r
choices ::
[InstrPure (v -> Bool)] ->
[repr inp vs es ret] ->
repr inp vs es ret ->
repr inp (v ': vs) es ret
-- ** Class 'Failable'
class Failable (repr :: Type -> [Type] -> Peano -> Type -> Type) where
fail :: [ErrorItem (InputToken inp)] -> repr inp vs ('Succ es) ret
popFail ::
repr inp vs es ret ->
repr inp vs ('Succ es) ret
catchFail ::
repr inp vs ('Succ es) ret ->
repr inp (Cursor inp ': vs) es ret ->
repr inp vs es ret
-- ** Class 'Inputable'
class Inputable (repr :: Type -> [Type] -> Peano -> Type -> Type) where
loadInput ::
repr inp vs es r ->
repr inp (Cursor inp ': vs) es r
pushInput ::
repr inp (Cursor inp ': vs) es ret ->
repr inp vs es ret
-- ** Class 'Routinable'
class Routinable (repr :: Type -> [Type] -> Peano -> Type -> Type) where
subroutine ::
LetName v -> repr inp '[] ('Succ 'Zero) v ->
repr inp vs ('Succ es) ret ->
repr inp vs ('Succ es) ret
call ::
LetName v -> repr inp (v ': vs) ('Succ es) ret ->
repr inp vs ('Succ es) ret
ret ::
repr inp '[ret] es ret
jump ::
LetName ret ->
repr inp '[] ('Succ es) ret
-- ** Class 'Joinable'
class Joinable (repr :: Type -> [Type] -> Peano -> Type -> Type) where
defJoin ::
LetName v ->
repr inp (v ': vs) es ret ->
repr inp vs es ret ->
repr inp vs es ret
refJoin ::
LetName v ->
repr inp (v ': vs) es ret
-- ** Class 'Readable'
class Readable (repr :: Type -> [Type] -> Peano -> Type -> Type) (tok::Type) where
read ::
tok ~ InputToken inp =>
[ErrorItem tok] ->
InstrPure (tok -> Bool) ->
repr inp (tok ': vs) ('Succ es) ret ->
repr inp vs ('Succ es) ret
instance
( Executable repr
, Readable repr (InputToken inp)
) => Trans (Instr inp vs es) (repr inp vs es) where
trans = \case
Push x k -> push x (trans k)
Pop k -> pop (trans k)
LiftI2 f k -> liftI2 f (trans k)
Fail err -> fail err
PopFail k -> popFail (trans k)
CatchFail l r -> catchFail (trans l) (trans r)
LoadInput k -> loadInput (trans k)
PushInput k -> pushInput (trans k)
Case l r -> case_ (trans l) (trans r)
Swap k -> swap (trans k)
Choices ps bs d -> choices ps (trans Functor.<$> bs) (trans d)
Subroutine n sub k -> subroutine n (trans sub) (trans k)
Jump n -> jump n
Call n k -> call n (trans k)
Ret -> ret
Read es p k -> read es p (trans k)
DefJoin n sub k -> defJoin n (trans sub) (trans k)
RefJoin n -> refJoin n
-- ** Type 'Peano'
-- | Type-level natural numbers, using the Peano recursive encoding.
data Peano = Zero | Succ Peano
-- | @('Fmap' f k)@.
pattern Fmap ::
InstrPure (x -> y) ->
Instr inp (y ': xs) es ret ->
Instr inp (x ': xs) es ret
pattern Fmap f k = Push f (LiftI2 (InstrPureHaskell (H.Flip H.:@ (H.:$))) k)
-- | @('App' k)@ pops @(x)@ and @(x2y)@ from the 'valueStack',
-- pushes @(x2y x)@ and continues with the next 'Instr'uction @(k)@.
pattern App ::
Instr inp (y : vs) es ret ->
Instr inp (x : (x -> y) : vs) es ret
pattern App k = LiftI2 (InstrPureHaskell (H.:$)) k
-- | @('If' ok ko)@ pops a 'Bool' from the 'valueStack'
-- and continues either with the 'Instr'uction @(ok)@ if it is 'True'
-- or @(ko)@ otherwise.
pattern If ::
Instr inp vs es ret ->
Instr inp vs es ret ->
Instr inp (Bool ': vs) es ret
pattern If ok ko = Choices [InstrPureHaskell H.Id] [ok] ko
-- * Type 'Machine'
-- | Making the control-flow explicit.
data Machine inp v = Machine { unMachine ::
forall vs es ret.
{-k-}Instr inp (v ': vs) ('Succ es) ret ->
Instr inp vs ('Succ es) ret
}
runMachine ::
forall inp v es repr.
Executable repr =>
Readable repr (InputToken inp) =>
Machine inp v -> repr inp '[] ('Succ es) v
runMachine (Machine auto) =
trans @(Instr inp '[] ('Succ es)) $
auto Ret
instance Applicable (Machine inp) where
pure x = Machine $ Push (InstrPureHaskell x)
Machine f <*> Machine x = Machine $ f . x . App
liftA2 f (Machine x) (Machine y) = Machine $
x . y . LiftI2 (InstrPureHaskell f)
Machine x *> Machine y = Machine $ x . Pop . y
Machine x <* Machine y = Machine $ x . y . Pop
instance
Cursorable (Cursor inp) =>
Alternable (Machine inp) where
empty = Machine $ \_k -> Fail []
Machine l <|> Machine r = Machine $ \k ->
makeJoin k $ \j ->
CatchFail
(l (PopFail j))
(failIfConsumed (r j))
try (Machine x) = Machine $ \k ->
CatchFail (x (PopFail k))
-- On exception, reset the input,
-- and propagate the failure.
(LoadInput (Fail []))
-- | If no input has been consumed by the failing alternative
-- then continue with the given continuation.
-- Otherwise, propagate the 'Fail'ure.
failIfConsumed ::
Cursorable (Cursor inp) =>
Instr inp vs ('Succ es) ret ->
Instr inp (Cursor inp : vs) ('Succ es) ret
failIfConsumed k = PushInput (LiftI2 InstrPureSameOffset (If k (Fail [])))
-- | @('makeJoin' k f)@ factorizes @(k)@ in @(f)@,
-- by introducing a 'DefJoin' if necessary,
-- and passing the corresponding 'RefJoin' to @(f)@,
-- or @(k)@ as is when factorizing is useless.
makeJoin ::
Instr inp (v : vs) es ret ->
(Instr inp (v : vs) es ret -> Instr inp vs es ret) ->
Instr inp vs es ret
-- Double RefJoin Optimization:
-- If a join-node points directly to another join-node,
-- then reuse it
makeJoin k@RefJoin{} = ($ k)
-- Terminal RefJoin Optimization:
-- If a join-node points directly to a terminal operation,
-- then it's useless to introduce a join-point.
makeJoin k@Ret = ($ k)
makeJoin k =
let joinName = LetName $ unsafePerformIO $ TH.qNewName "join" in
\f -> DefJoin joinName k (f (RefJoin joinName))
instance tok ~ InputToken inp => Satisfiable (Machine inp) tok where
satisfy es p = Machine $ Read es (InstrPureHaskell p)
instance Selectable (Machine inp) where
branch (Machine lr) (Machine l) (Machine r) = Machine $ \k ->
makeJoin k $ \j ->
lr (Case (l (Swap (App j)))
(r (Swap (App j))))
instance Matchable (Machine inp) where
conditional ps bs (Machine m) (Machine default_) = Machine $ \k ->
makeJoin k $ \j ->
m (Choices (InstrPureHaskell Functor.<$> ps)
((\b -> unMachine b j) Functor.<$> bs)
(default_ j))
instance
( Ord (InputToken inp)
, Cursorable (Cursor inp)
) => Lookable (Machine inp) where
look (Machine x) = Machine $ \k ->
PushInput (x (Swap (LoadInput k)))
eof = negLook (satisfy [{-discarded by negLook-}] (H.const H..@ H.bool True))
-- Set a better failure message
<|> (Machine $ \_k -> Fail [ErrorItemEnd])
negLook (Machine x) = Machine $ \k ->
CatchFail
-- On x success, discard the result,
-- and replace this 'CatchFail''s failure handler
-- by a 'Fail'ure whose 'farthestExpecting' is negated,
-- then a failure is raised from the input
-- when entering 'negLook', to avoid odd cases:
-- - where the failure that made (negLook x)
-- succeed can get the blame for the overall
-- failure of the grammar.
-- - where the overall failure of
-- the grammar might be blamed on something in x
-- that, if corrected, still makes x succeed and
-- (negLook x) fail.
(PushInput (x (Pop (PopFail (LoadInput (Fail []))))))
-- On x failure, reset the input,
-- and go on with the next 'Instr'uctions.
(LoadInput (Push (InstrPureHaskell H.unit) k))
instance Letable TH.Name (Machine inp) where
def n v = Machine $ \k ->
Subroutine (LetName n) (unMachine v Ret) (Call (LetName n) k)
ref _isRec n = Machine $ \case
Ret -> Jump (LetName n)
k -> Call (LetName n) k
instance Cursorable (Cursor inp) => Foldable (Machine inp) where
{-
chainPre op p = go <*> p
where go = (H..) <$> op <*> go <|> pure H.id
chainPost p op = p <**> go
where go = (H..) <$> op <*> go <|> pure H.id
-}