covenant-1.3.0: src/Covenant/Zipper.hs
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}
-- |
-- Module: Covenant.Zipper
-- Copyright: (C) MLabs 2025
-- License: Apache 2.0
-- Maintainer: koz@mlabs.city, sean@mlabs.city
--
-- A read-only zipper for the Covenant ASG, based on an action monad.
--
-- @since 1.3.0
module Covenant.Zipper
( -- * Types
ZipperAction,
Tape (..),
ZipperState (WorkingZipper, BrokenZipper),
ASGZipper,
-- * Functions
-- ** Actions
moveUp,
moveDown,
moveLeft,
moveRight,
resetZipper,
-- ** Elimination
runASGZipper,
)
where
import Control.Monad.Action
( Action (StateOf, act),
Actionable,
MonadUpdate,
UpdateT,
actionable,
runUpdateT,
)
import Covenant.ASG
( ASG,
ASGNode (ACompNode, AValNode, AnError),
Arg,
CompNodeInfo (Force, Lam),
Id,
Ref (AnArg, AnId),
ValNodeInfo (App, Cata, DataConstructor, Match, Thunk),
nodeAt,
topLevelId,
)
import Covenant.Util (pattern ConsV, pattern NilV)
import Data.Functor.Identity (Identity, runIdentity)
import Data.Kind (Type)
import Data.Monoid (Endo (Endo))
import Data.Vector qualified as Vector
import GHC.Exts (toList)
-- | A requested movement from the zipper. To build these, use dedicated smart
-- constructors in this module. You can \'chain together\' 'ZipperAction' using
-- the 'Semigroup' instance.
--
-- @since 1.3.0
newtype ZipperAction = ZipperAction (Actionable ZipperStep)
deriving
( -- | @since 1.3.0
Semigroup,
-- | @since 1.3.0
Monoid
)
via (Actionable ZipperStep)
-- | @since 1.3.0
instance Action ZipperAction where
type StateOf ZipperAction = ZipperState
act (ZipperAction acts) = foldMap go acts
where
go :: ZipperStep -> Endo ZipperState
go =
Endo . \case
ZipperDown -> downStep
ZipperUp -> upStep
ZipperLeft -> leftStep
ZipperRight -> rightStep
ZipperReset -> resetStep
-- | Move towards the source of the ASG, \'back up\' along the path taken to
-- reach the current position. Will put the zipper in a broken state if used at
-- the source node.
--
-- @since 1.3.0
moveUp :: ZipperAction
moveUp = ZipperAction . actionable $ ZipperUp
-- | Move to the leftmost child of the current position. Will put the zipper in
-- a broken state if used at a sink node.
--
-- @since 1.3.0
moveDown :: ZipperAction
moveDown = ZipperAction . actionable $ ZipperDown
-- | Move to the sibling immediately to the left of the current position. Will
-- put the zipper in a broken state if used at a leftmost sibling.
--
-- @since 1.3.0
moveLeft :: ZipperAction
moveLeft = ZipperAction . actionable $ ZipperLeft
-- | Move to the sibling immediately to the right of the current position. Will
-- put the zipper in a broken state if used at a rightmost sibling.
--
-- @since 1.3.0
moveRight :: ZipperAction
moveRight = ZipperAction . actionable $ ZipperRight
-- | If the zipper is currently in a broken state, reset it to the last position
-- it was at before breaking. Otherwise, this does nothing.
--
-- @since 1.3.0
resetZipper :: ZipperAction
resetZipper = ZipperAction . actionable $ ZipperReset
-- | A \'list with a focus\', which may be of a different type to the rest. The
-- first field is \'backwards\', in that its first element is actually the
-- /furthest/ from the focus. Thus, if we have @Tape [3, 2, 1] "foo" [4, 5]@,
-- the \'list\' actually looks like this:
--
-- @[1, 2, 3, "foo", 4, 5]@
--
-- but /not/ like this:
--
-- @[3, 2, 1, "foo", 4, 5]@
--
-- @since 1.3.0
data Tape a b = Tape [a] b [a]
deriving stock
( -- | @since 1.3.0
Functor
)
-- | The current state of the zipper, including whether it's in a broken state
-- or not, and if not in a broken state, the current position and the path taken
-- to get here.
--
-- @since 1.3.0
data ZipperState = ZipperState Bool ASG [Tape Ref Id] (Tape Ref Ref)
-- | Matches on a working zipper, giving access to a stack of 'Tape's
-- representing the path taken to get here (tracking sibling positions) and the
-- current position, with the focus at either an 'Arg' or an 'ASGNode'.
--
-- Parent positions use 'Id' for the focus, as 'Arg's cannot have descendants.
--
-- @since 1.3.0
pattern WorkingZipper :: [Tape Ref Id] -> Tape Ref (Either Arg (Id, ASGNode)) -> ZipperState
pattern WorkingZipper parents curr <- ZipperState False g parents (getNodeInfo g -> curr)
-- | Matches on a zipper in a broken state.
--
-- @since 1.3.0
pattern BrokenZipper :: ZipperState
pattern BrokenZipper <- ZipperState True _ _ _
{-# COMPLETE WorkingZipper, BrokenZipper #-}
-- | A \'zipper command monad\', designed to traverse an ASG. Based on an action
-- monad.
--
-- To perform zipper moves, use 'Control.Monad.Action.send' together with a
-- 'ZipperAction'. If you want to find out something about where we're standing,
-- use 'Control.Monad.Action.request', together with pattern matching on
-- 'ZipperState'.
--
-- @since 1.3.0
newtype ASGZipper (a :: Type)
= ASGZipper (UpdateT ZipperAction Identity a)
deriving
( -- | @since 1.3.0
Functor,
-- | @since 1.3.0
Applicative,
-- | @since 1.3.0
Monad,
-- | @since 1.3.0
MonadUpdate ZipperAction
)
via (UpdateT ZipperAction Identity)
-- | Perform the stated actions to traverse over the 'ASG' given by the
-- argument.
--
-- @since 1.3.0
runASGZipper ::
forall (a :: Type).
ASG ->
ASGZipper a ->
a
runASGZipper g (ASGZipper comp) =
let i = topLevelId g
in (\(_, _, x) -> x)
. runIdentity
. runUpdateT comp
. ZipperState False g []
. Tape [] (AnId i)
$ []
-- Helpers
data ZipperStep = ZipperDown | ZipperUp | ZipperLeft | ZipperRight | ZipperReset
deriving stock (Eq, Show)
downStep :: ZipperState -> ZipperState
downStep zs@(ZipperState walkedOff g parentLevels currentLevel) =
if walkedOff
then zs
else case currentLevel of
Tape lefts curr rights ->
let miss = ZipperState True g parentLevels currentLevel
in case curr of
AnArg _ -> miss
AnId i ->
let next = ZipperState walkedOff g (Tape lefts i rights : parentLevels)
in case nodeAt i g of
ACompNode _ info -> case info of
Lam r -> next . Tape [] r $ []
Force r -> next . Tape [] r $ []
_ -> miss
AValNode _ info -> case info of
App f args _ _ -> next . Tape [] (AnId f) . toList $ args
Thunk f -> next . Tape [] (AnId f) $ []
Cata _ arms x -> case arms of
NilV -> zs -- impossible
ConsV c cs -> next . Tape [] c . toList . Vector.snoc cs $ x -- next . Tape [] alg $ [x]
DataConstructor _ _ args -> case args of
NilV -> miss
ConsV arg args' -> next . Tape [] arg . toList $ args'
Match x handlers -> next . Tape [] x . toList $ handlers
_ -> miss
AnError -> miss
upStep :: ZipperState -> ZipperState
upStep zs@(ZipperState walkedOff g parentLevels currentLevel) =
if walkedOff
then zs
else case parentLevels of
[] -> ZipperState True g parentLevels currentLevel
(p : ps) -> case p of
Tape lefts curr rights -> ZipperState walkedOff g ps . Tape lefts (AnId curr) $ rights
leftStep :: ZipperState -> ZipperState
leftStep zs@(ZipperState walkedOff g parentLevels currentLevel) =
if walkedOff
then zs
else case currentLevel of
Tape lefts curr rights -> case lefts of
[] -> ZipperState True g parentLevels currentLevel
(l : ls) -> ZipperState walkedOff g parentLevels . Tape ls l $ curr : rights
rightStep :: ZipperState -> ZipperState
rightStep zs@(ZipperState walkedOff g parentLevels currentLevel) =
if walkedOff
then zs
else case currentLevel of
Tape lefts curr rights -> case rights of
[] -> ZipperState True g parentLevels currentLevel
(r : rs) -> ZipperState walkedOff g parentLevels . Tape (curr : lefts) r $ rs
resetStep :: ZipperState -> ZipperState
resetStep zs@(ZipperState walkedOff g parentLevels currentLevel) =
if walkedOff
then ZipperState False g parentLevels currentLevel
else zs
getNodeInfo :: ASG -> Tape Ref Ref -> Tape Ref (Either Arg (Id, ASGNode))
getNodeInfo g =
fmap
( \case
AnId i -> Right (i, nodeAt i g)
AnArg arg -> Left arg
)