packages feed

ctpl-0.1.0.4: Text/CTPL0n.hs

{-# LANGUAGE TupleSections, FlexibleInstances #-}

-- | The new CTPL0 virtual machine.

module Text.CTPL0n where

import Control.Applicative
import Control.Arrow
import Control.Monad
import Control.Monad.ST
import Data.Array.MArray as M
import Data.Array.ST
import Data.Array.Unboxed
import Data.Char
import Data.List
import Data.STRef

-- | The VM`s state record.
data CTPL0State s = CTPL0State {
    bufferContent :: !(Buffer s),      -- ^ Tape buffer
    programContent :: UArray Int Char, -- ^ Program buffer
    bufferPointer :: !Int,             -- ^ Tape pointer
    programPointer :: !Int,            -- ^ Program pointer
    cp :: !Bool,                       -- ^ If set, the next instruction will use the top element of CK instead of AX. (Say: ``SX is CK top'' in contrast to ``SX is AX'')
    ax :: !Int,                        -- ^ Accumulator register (AX)
    mk :: [Buffer s],                  -- ^ Tape stack (MK)
    rk :: [Int],                       -- ^ Return address stack (RK)
    ck :: [Int]                        -- ^ Number stack (CK). The top element may be used like a register.
  }

-- | The VM`s configuration. Immutable during execution.
data VMConfig = VMConfig {
    maxTime :: !Int,           -- ^ Initial time limit.
    maxConditRecurs :: !Int,   -- ^ Maximum recursion depth for `x' condition (FUTURE)
    maxBufferMeasure :: !Int,  -- ^ Maximum buffer size (FUTURE)
    initBufferMeasure :: !Int, -- ^ Initial buffer size.
    vmFeatures :: [VMFeature]  -- ^ Enabled feature instructions.
  }

-- | Optional feature instructions.
data VMFeature = RandomF        -- ^ enable `Xr' instruction for random number retrieval (FUTURE)
               | FilterF        -- ^ enable `Xfs', `Xfd', `Xff' instructions for setting predefined filters on the tape (FUTURE)
               | CustomFilterF  -- ^ enable `Xfc' instruction for setting custom filters on the tape (FUTURE; won't work without FilterF)
               | InterruptsF    -- ^ enable `Xi' instruction for setting and calling interrupts (FUTURE)

-- | Predefined VM configuration, very restrictive. Allows 10000 cycles, no recursive conditions, no feature instructions, at maximum 3000 chars on tape.
safeVM :: VMConfig
safeVM = VMConfig{ maxTime=10000, maxConditRecurs=0,
                   maxBufferMeasure=3000, initBufferMeasure=0,
                   vmFeatures=[] }

-- | Predefined VM configuration, very permissive. Allows 10 million cycles, 10 levels of recursion, all available feature instructions, at maximum 30000 chars on tape.
fullVM :: VMConfig
fullVM = VMConfig{ maxTime=10000000, maxConditRecurs=10,
                   maxBufferMeasure=30000, initBufferMeasure=0,
                   vmFeatures=[RandomF, FilterF, CustomFilterF, InterruptsF] }

-- | Monad displaying success or failure. A close relative of the 'Either' monad.
data Exec a = Succ a     -- ^ Execution succeeded :)
            | Fail !Fail -- ^ Nope. See 'Fail' type.

instance Monad Exec where
  return = Succ
  fail = Fail . Dead
  (Succ a) >>= f = f a
  (Fail e) >>= f = Fail e

instance Applicative Exec where
  pure = return
  (<*>) = ap

instance Functor Exec where
  fmap = liftM

-- | Possible errors.
data Fail = Expired     -- ^ Time has expired. Program took too long to finish. You might want to increase time limit.
          | ConfViol    -- ^ Confidence violation. This may have several reasons, e.g. popping from an empty stack, jumping out of program bounds, ...
          | SynViol     -- ^ Syntax violation. I encountered an instruction (or condition) I do not understand.
          | Dead String -- ^ Program requested abort (FUTURE)

-- | The VM's execution monad. Behaves like a 'ReaderT' carrying a 'CTPL0State' reference and a 'VMConfig' wrapped around the 'ST' and the 'Exec' monad.  Also responsible for time consumption and passing.
newtype CTPL0 s a = CTPL0 { runCTPL0 :: Int ->
                                        STRef s (CTPL0State s) ->
                                        VMConfig ->
                                        ST s (Exec (a, Int)) }

-- | A tape buffer, implemented by wrapping modifiers around a mutable array.
data Buffer s = Insertion { insPlace :: !Int,  insLength :: !Int,  insContent :: String,  insNext :: Buffer s } -- ^ Character insertion
              | Deletion { delPlace :: !Int, delLength :: !Int, delNext :: Buffer s } -- ^ Character deletion
              | MBuffer { bufArray :: STUArray s Int Char } -- ^ The core: the mutable array

instance Show Fail where
  show Expired = "<Fatal: Time expired.>"
  show ConfViol = "<Fatal: Confidence violation.>"
  show SynViol = "<Fatal: Syntax violation.>"
  show (Dead s) = "<Error: "++s++">"

instance Show (Exec String) where
  show (Succ a) = a
  show (Fail f) = show f
  
-- | Read a char from a tape buffer.
readBuffer :: Buffer s -> Int -> ST s Char
readBuffer (MBuffer a) i = readArray a i
readBuffer (Insertion p l c n) i
  | i < p = readBuffer n i
  | i >= p+l = readBuffer n (i-l)
  | otherwise = return (c !! (i-p))
readBuffer (Deletion p l n) i
  | i < p = readBuffer n i
  | otherwise = readBuffer n (i+l)

-- | Write a char to the tape buffer.
writeBuffer :: Buffer s -> Int -> Char -> ST s (Buffer s)
writeBuffer (MBuffer a) i e = writeArray a i e >> return (MBuffer a)
writeBuffer (Insertion p l c n) i e
  | i < p = Insertion p l c <$> writeBuffer n i e
  | i >= p+l = Insertion p l c <$> writeBuffer n (i-l) e
  | otherwise = return $ Insertion p l (take (i-p) c ++ [e] ++ drop (i-p+1) c) n
writeBuffer (Deletion p l n) i e
  | i < p = Deletion p l <$> writeBuffer n i e
  | otherwise = Deletion p l <$> writeBuffer n (i+l) e

-- | Is the given position accessible in the tape buffer?
accBuffer :: Buffer s -> Int -> ST s Bool
accBuffer (MBuffer a) i = do
  b <- M.getBounds a
  return (snd b >= i)
accBuffer (Insertion p l c n) i
  | i < p = accBuffer n i
  | i >= p+l = accBuffer n (i-l)
  | otherwise = return True
accBuffer (Deletion p l n) i
  | i < p = accBuffer n i
  | otherwise = accBuffer n (i+l)

-- | Join the tape buffer to a string.
joinBuffer :: Buffer s -> ST s String
joinBuffer (MBuffer a) = M.getElems a
joinBuffer (Insertion p l c n) = do
  nb <- joinBuffer n
  let (a,b) = splitAt p nb
  return (a ++ c ++ b)
joinBuffer (Deletion p l n) = do
  nb <- joinBuffer n
  let (a,b) = splitAt p nb
  return (a ++ drop l b)

-- | Insert a string into the tape buffer.
insBuffer :: Buffer s -> Int -> String -> ST s (Buffer s)
insBuffer (MBuffer a) i e = return $ Insertion i (length e) e (MBuffer a)
insBuffer (Insertion p l c n) i e
  | i < p = Insertion (p+length e) l c <$> insBuffer n i e
  | i >= p+l = Insertion p l c <$> insBuffer n (i-l) e
  | i >= p && (i+length e) <= p+l =
    return $ Insertion p (l+length e) (a++e++b) n
  | otherwise = return $ Insertion i (length e) e $ Insertion p l c n
  where (a,b) = splitAt (i-p) c
insBuffer (Deletion p l n) i e
  | i < p = Deletion (p+length e) l <$> insBuffer n i e
  | i > p = Deletion p l <$> insBuffer n (i+l) e
  | otherwise = return $ Insertion i (length e) e $ Deletion p l n

-- | Delete a char from the tape buffer.
delBuffer :: Buffer s -> Int -> ST s (Buffer s)
delBuffer (MBuffer a) i = return $ Deletion i 1 $ MBuffer a
delBuffer (Insertion p l c n) i
  | i < p = Insertion (p-1) l c <$> delBuffer n i
  | i >= p+l = Insertion p l c <$> delBuffer n (i-l)
  | otherwise = return $ Insertion p (l-1) (a++b) n
  where (a,_:b) = splitAt (i-p) c
delBuffer (Deletion p l n) i
  | i < p = Deletion (p-1) l <$> delBuffer n i
  | i > p = Deletion p l <$> delBuffer n (i+l)
  | otherwise = return $ Deletion p (l+1) n

-- | Get a list of ETX positions in the tape buffer.
etxBuffer :: Buffer s -> ST s [Int]
etxBuffer (MBuffer a) = do
  es <- M.getElems a
  return $ elemIndices '\3' es
etxBuffer (Insertion p l c n) = do
  ne <- etxBuffer n
  let ie = elemIndices '\3' c
  return $ sort (map (+p) ie ++ map (\ne' -> if ne' < p then ne' else ne' + l) ne)
etxBuffer (Deletion p l n) = do
  ne <- etxBuffer n
  return $ map (\ne -> if ne < p then ne else ne-l) $ filter (/=p) ne

-- | Clone the tape buffer (fixing all modifiers into the newly created mutable array)
cloneBuffer :: Buffer s -> ST s (Buffer s)
cloneBuffer b = do
  s <- joinBuffer b
  a <- M.newListArray (0, length s - 1) s
  return $ MBuffer a

-- | Get the tape buffer`s length.
measBuffer :: Buffer s -> ST s Int
measBuffer (MBuffer a) = snd <$> M.getBounds a
measBuffer (Insertion p l c n) = (+l) <$> measBuffer n
measBuffer (Deletion p l n) = subtract l <$> measBuffer n

instance Monad (CTPL0 s) where
  return a = CTPL0 $ \i ref cfg -> return $ Succ (a,i)
  m >>= f = CTPL0 $ \i ref cfg -> do
    r <- runCTPL0 m i ref cfg
    case r of
      Succ (a, i') -> runCTPL0 (f a) i' ref cfg
      Fail f -> return $ Fail f

instance Functor (CTPL0 s) where
  fmap = liftM

instance Applicative (CTPL0 s) where
  pure = return
  (<*>) = ap

-- | Run an 'ST' action inside a compatible 'CTPL0' block
liftST :: ST s a -> CTPL0 s a
liftST m = CTPL0 $ \i ref cfg -> Succ <$> (,i) <$> m

-- | A handy wrapper around 'procInstrs'. Arguments: program, tape, config. Results: tape, leftover time, AX, CK top.
evalCTPL0' :: String -> String -> VMConfig -> Exec (String, Int, Int, Int)
evalCTPL0' program buffer cfg = runST $ do
  let limit = maxTime cfg
      bufsize = max (initBufferMeasure cfg) (length buffer+1)
  bc <- newListArray (0, bufsize-1) (buffer ++ '\3' : replicate (bufsize-length buffer-1) '\0')
  let pc = listArray (0, length program - 1) program
      st = CTPL0State (MBuffer bc) pc 0 0 False 0 [] [length program] [0]
  ref <- newSTRef st
  r <- runCTPL0 procInstrs limit ref cfg{initBufferMeasure=bufsize}
  case r of
    Succ (_, i') -> do
      st <- readSTRef ref
      buf <- joinBuffer (bufferContent st)
      return $ Succ (takeWhile (/='\3') buf, i', ax st, if null (ck st) then 0 else head $ ck st)
    Fail f -> return $ Fail f

-- | Another handy wrapper around 'procInstrs'. Less clumsy than 'evalCTPL0'', but provides less information. Arguments: program, tape, config. Results: tape only.
evalCTPL0 :: String -> String -> VMConfig  -> Exec String
evalCTPL0 program buffer cfg =
  case evalCTPL0' program buffer cfg of
    Succ (s,_,_,_) -> Succ s
    Fail f -> Fail f

-- | Gets the carried 'CTPL0State' and runs a function on it.
getState :: (CTPL0State s -> a) -> CTPL0 s a
getState f =
  CTPL0 $ \i ref cfg -> do
    st <- readSTRef ref
    return $ Succ (f st, i)

-- | Runs a function on the carried 'CTPL0State'.
modState :: (CTPL0State s -> CTPL0State s) -> CTPL0 s ()
modState f =
  CTPL0 $ \i ref cfg -> do
    modifySTRef ref f
    return $ Succ ((), i)

-- | Consume virtual time. Raise 'Expired' if limit is reached.
consumeTime :: CTPL0 s ()
consumeTime = CTPL0 $ \i ref cfg -> if i >= 1 then return (Succ ((), i-1)) else return (Fail Expired)

-- | Raise a 'ConfViol'.
confViol :: CTPL0 s a
confViol = CTPL0 $ \_ _ _ -> return $ Fail ConfViol

-- | Raise a 'SynViol'.
synViol :: CTPL0 s a
synViol = CTPL0 $ \_ _ _ -> return $ Fail SynViol

-- | Raise a 'Dead'.
die :: String -> CTPL0 s a
die s = CTPL0 $ \_ _ _ -> return . Fail $ Dead s

-- | Fetch the next instruction.
getInstr :: CTPL0 s Char
getInstr = do
  (c,p) <- getState (programContent &&& programPointer)
  modState (\st -> st{programPointer=programPointer st+1})
  return (c ! p)

-- | Have we reached the end of the program tape?
endOfInstr :: CTPL0 s Bool
endOfInstr = do
  (m,p) <- getState (snd . bounds . programContent &&& programPointer)
  return (m < p)

-- | Fetch numeric argument (as many digits as we can get)
instrNumArg :: CTPL0 s Int
instrNumArg = do
  (c,p) <- getState (programContent &&& programPointer)
  if snd (bounds c) <= p || not (isDigit (c!p)) then synViol else
    let coll p | snd (bounds c) <= p = ""
        coll p | not (isDigit (c!p)) = ""
        coll p = (c!p) : coll (p+1)
        str = coll p
    in do
      modState $ \st -> st{programPointer=programPointer st+length str}
      return $ read str

-- | Fetch string argument (delimited by '$')
instrDelimArg :: CTPL0 s String
instrDelimArg = do
  (c,p) <- getState (programContent &&& programPointer)
  if snd (bounds c) < p then synViol else
    let coll p | snd (bounds c) < p = ""
        coll p | (c!p) == '$' = "$"
        coll p = (c!p) : coll (p+1)
        str = coll p
    in do
      modState $ \st -> st{programPointer=programPointer st+length str}
      unless (last str == '$') synViol
      return $ init str

-- | Run an action (first arg) iff the test (second arg) succeeds. Raise 'ConfViol' otherwise.
provided :: CTPL0 s a -> CTPL0 s Bool -> CTPL0 s a
provided act test = do
  b <- test
  if b
    then act
    else confViol

-- | SX is AX by default, but CK top after 'C'.
sx :: CTPL0State s -> Int
sx r | cp r = head $ ck r
sx r        = ax r

-- | Set SX (AX or CK top) value.
setSX :: Int -> CTPL0State s -> CTPL0State s
setSX i r | cp r = if null (ck r) then r{ck=[i]} else r{ck=i:tail (ck r)}
setSX i r        = r{ax=i}

-- | Run the next instruction in program.
singleInstr :: CTPL0 s ()
singleInstr = do
  i <- getInstr
  consumeTime
  case i of
    -- Walk left
    '<' -> modState (\st -> st{bufferPointer=bufferPointer st-1})
           `provided` getState (\st -> bufferPointer st > 0)
    -- Walk right
    '>' -> modState (\st -> st{bufferPointer=bufferPointer st+1})
           `provided` do
             (p,c) <- getState (bufferPointer &&& bufferContent)
             liftST $ accBuffer c (p+1)
             
    -- Inc SX
    '+' -> do
      num <- instrNumArg
      modState $ \st -> setSX (sx st + num) st
    -- Dec SX
    '-' -> do
      num <- instrNumArg
      modState $ \st -> setSX (sx st - num) st
    -- Insert char, go after
    'i' -> do
      ch <- getInstr `provided` liftM not endOfInstr
      (p,c) <- getState (bufferPointer &&& bufferContent)
      c' <- liftST $ insBuffer c p [ch]
      modState $ \st -> st{bufferContent=c',bufferPointer=bufferPointer st + 1}
    -- Insert string, go after
    'I' -> do
      str <- instrDelimArg
      (p,c) <- getState (bufferPointer &&& bufferContent)
      c' <- liftST $ insBuffer c p str
      modState $ \st -> st{bufferContent=c',bufferPointer=bufferPointer st + length str}
    -- Replace char, stay
    'r' -> do
      ch <- getInstr `provided` liftM not endOfInstr
      (p,c) <- getState (bufferPointer &&& bufferContent)
      c' <- liftST $ writeBuffer c p ch
      modState $ \st -> st{bufferContent=c'}
    -- Delete char
    'x' -> do
      (p,c) <- getState (bufferPointer &&& bufferContent)
      c' <- liftST $ delBuffer c p
      modState $ \st -> st{bufferContent=c'}
    -- Append char, don't walk
    'a' -> do
      ch <- getInstr `provided` liftM not endOfInstr
      (p,c) <- getState (bufferPointer &&& bufferContent)
      etx <- liftST $ etxBuffer c
      case etx of
        [] -> confViol
        etx:_ -> do
          c' <- liftST $ writeBuffer c etx ch
          c'' <- liftST $ writeBuffer c' (etx+1) '\3'
          modState $ \st -> st{bufferContent=c''}
          when (etx<p) $ modState $ \st -> st{bufferPointer=bufferPointer st + 1}
    -- Append string, don't walk
    'A' -> do
      str <- instrDelimArg
      (p,c) <- getState (bufferPointer &&& bufferContent)
      etx <- liftST $ etxBuffer c
      case etx of
        [] -> confViol
        etx:_ -> do
          (c',etx') <- foldl (\l r -> do (c,etx) <- l; c' <- liftST $ writeBuffer c etx r; return (c,etx+1)) (return (c,etx)) str
          c'' <- liftST $ writeBuffer c' etx' '\3'
          modState $ \st -> st{bufferContent=c''}
          when (etx<p) $ modState $ \st -> st{bufferPointer=bufferPointer st + 1}
    -- Set SX=0
    '0' -> do
      modState $ setSX 0
    -- Set SX=CP
    'Q' -> do
      modState $ \st -> setSX (bufferPointer st) st
    -- Set CP=SX
    'm' -> do
      modState (\st -> st{bufferPointer=sx st}) `provided` do
        (c,s) <- getState (bufferContent &&& sx)
        liftST $ accBuffer c s
    -- Choose CK(0) for SX
    'C' -> modState (\st -> st{cp=True}) `provided` getState (not . null . ck)
    -- Load [CP] into SX
    'l' -> do
      (p,c) <- getState (bufferPointer &&& bufferContent)
      s <- liftST $ readBuffer c p
      modState $ \st -> setSX (ord s) st
    -- Save SX into [CP]
    's' -> do
      (p,(c,s)) <- getState (bufferPointer &&& bufferContent &&& sx)
      c' <- liftST $ writeBuffer c p (chr s)
      modState $ \st -> st{bufferContent=c'}
    -- Push SX onto CK
    'd' -> modState $ \st -> st{ck=sx st : ck st}
    -- Pop AX from CK
    'D' -> modState (\st -> st{ax=head $ ck st, ck=tail $ ck st})
           `provided` getState (not . null . ck)
    -- Pop CK, discard
    'k' -> modState (\st -> st{ck=tail $ ck st})
           `provided` getState (not . null . ck)
    -- Set SX=LEN
    'L' -> do
      c <- getState bufferContent
      etx <- liftST $ etxBuffer c
      case etx of
        [] -> confViol
        etx':_ -> modState $ setSX etx'
    -- Swap tape and MK(0)
    'H' ->
      modState (\st -> st{mk=bufferContent st : tail (mk st), bufferContent=head (mk st)})
      `provided` getState (not . null . mk)
    -- Push entire tape to MK
    't' -> do
      c <- getState bufferContent
      c' <- liftST $ cloneBuffer c
      modState $ \st -> st{mk=c' : mk st}
    -- Push [CP] to MK
    'y' -> do
      (c,p) <- getState (bufferContent &&& bufferPointer)
      (ch,len,a) <- liftST $ do
        ch <- readBuffer c p
        len <- measBuffer c
        a <- newListArray (0,len) (ch : '\3' : replicate (len-1) '\0')
        return (ch,len,a)
      modState $ \st -> st{mk=MBuffer a : mk st}
    -- Pop MK, discard content
    'p' -> modState (\st -> st{mk=tail $ mk st})
           `provided` getState (not . null . mk)
    -- Append [CP] to MK(0)
    'Y' -> (do
               (c,(p,a)) <- getState (bufferContent &&& bufferPointer &&& head . mk)
               ch <- liftST $ readBuffer c p
               etx <- liftST $ etxBuffer a
               case etx of
                 [] -> confViol
                 etx':_ -> do
                   a' <- liftST $ writeBuffer a etx' ch
                   a'' <- liftST $ writeBuffer a' (etx'+1) '\3'
                   modState (\st -> st{mk=a'':tail (mk st)}))
           `provided` getState (not . null . mk)
    -- Peek MK, insert, go after
    'P' -> (do
               (c,(p,a)) <- getState (bufferContent &&& bufferPointer &&& head . mk)
               js <- liftST $ joinBuffer a
               let s = takeWhile (/='\3') js
               c' <- liftST $ insBuffer c p s
               modState $ \st -> st{bufferContent=c',bufferPointer=bufferPointer st+length s})
           `provided` getState (not . null . mk)
    -- Absolute jump to SX
    'j' -> do
      a <- getState sx
      b <- singleCond
      when b $ modState $ \st -> st{programPointer=a}
    -- Relative jump by SX
    'J' -> do
      a <- getState sx
      b <- singleCond
      when b $ modState $ \st -> st{programPointer=a+programPointer st}
    -- Call procedure at SX, push IP to RK
    'c' -> do
      a <- getState sx
      b <- singleCond
      when b $ modState $ \st -> st{rk=(programPointer st):rk st, programPointer=a}
    -- Return to RK(0), pop RK
    'f' -> (modState $ \st -> st{programPointer=head (rk st), rk=tail (rk st)})
           `provided` getState (not . null . rk)
    -- Swap MK(0) and MK(1)
    'h' ->
      modState (\st -> st{mk=(mk st !! 1):(mk st !! 0):(drop 2 $ mk st)})
      `provided` getState ((>=2) . length . mk)
    -- Catch others
    o -> synViol
      {-do
      ip <- getState programPointer
      die ("SynViol in singleInstr: " ++ [o] ++ " (pos "++ show ip++")")-}
  unless (i=='C') $ modState $ \st -> st{cp=False}


-- | Evaluate a condition.
singleCond :: CTPL0 s Bool
singleCond = do
  i <- getInstr `provided` liftM not endOfInstr
  case i of
    -- Is uppercase?
    'U' -> do
      (c, p) <- getState (bufferContent &&& bufferPointer)
      isUpper <$> liftST (readBuffer c p)
    -- Is lowercase?
    'L' -> do
      (c, p) <- getState (bufferContent &&& bufferPointer)
      isLower <$> liftST (readBuffer c p)
    -- SX = 0?
    'z' -> getState ((==0) . sx)
    -- Always true
    't' -> return True
    -- Is digit?
    'N' -> do
      (c, p) <- getState (bufferContent &&& bufferPointer)
      isDigit <$> liftST (readBuffer c p)
    -- End of buffer?
    'e' -> do
      (c, p) <- getState (bufferContent &&& bufferPointer)
      realEnd <- not <$> liftST (accBuffer c (p+1))
      etxEnd <- if realEnd then return '\3' else liftST (readBuffer c p)
      return (etxEnd == '\3')      
    -- Negation
    '!' -> not <$> singleCond
    -- Disjunction
    '|' -> (||) <$> singleCond <*> singleCond
    -- Conjunction
    '&' -> (&&) <$> singleCond <*> singleCond
    -- match char?
    'q' -> do
      ch <- getInstr `provided` liftM not endOfInstr
      (c, p) <- getState (bufferContent &&& bufferPointer)
      a <- liftST $ readBuffer c p
      return (a == ch)
    -- CP < SX ?
    'l' -> uncurry (<) <$> getState (bufferPointer &&& sx)
    -- CP > SX ?
    'g' -> uncurry (>) <$> getState (bufferPointer &&& sx)
    -- CP == SX ?
    'E' -> uncurry (==) <$> getState (bufferPointer &&& sx)
    -- toggle SX
    'C' -> modState (\st -> st{cp=not $ cp st}) >> singleCond
           `provided` getState (not . null . ck)
    -- AX == CK(0) ?
    '=' -> uncurry (==) <$> getState (ax &&& head . ck)
           `provided` getState (not . null . ck)
    -- AX < CK(0) ?
    '<' -> uncurry (<) <$> getState (ax &&& head . ck)
           `provided` getState (not . null . ck)
    -- AX > CK(0) ?
    '>' -> uncurry (>) <$> getState (ax &&& head . ck)
           `provided` getState (not . null . ck)
    -- pop CK, discard, continue
    'k' -> (modState (\st -> st{ck=tail $ ck st}) >> singleCond)
           `provided` getState (not . null . ck)
    -- Catch others
    o -> synViol

-- | Run the entire program.
procInstrs :: CTPL0 s ()
procInstrs = singleInstr `asLongAs` (not <$> endOfInstr)
  where asLongAs act test = do
          b <- test
          when b $ act >> act `asLongAs` test