packages feed

eo-phi-normalizer-2.2.2: src/Language/EO/Phi/Rules/Yaml.hs

{- FOURMOLU_DISABLE -}
-- The MIT License (MIT)

-- Copyright (c) 2016-2024 Objectionary.com

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:

-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
{- FOURMOLU_ENABLE -}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-forall-identifier #-}
{-# OPTIONS_GHC -Wno-orphans #-}
{-# OPTIONS_GHC -Wno-partial-fields #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}

module Language.EO.Phi.Rules.Yaml where

import Control.Monad (guard, unless)
import Control.Monad.State (State, evalState)
import Data.Aeson (FromJSON (..), Options (sumEncoding), SumEncoding (UntaggedValue), genericParseJSON)
import Data.Aeson.Types (defaultOptions)
import Data.Coerce (coerce)
import Data.List (intercalate)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.String (IsString (..))
import Data.Yaml qualified as Yaml
import GHC.Generics (Generic)

import Language.EO.Phi (printTree)
import Language.EO.Phi.Rules.Common (Context (..), NamedRule)
import Language.EO.Phi.Rules.Common qualified as Common
import Language.EO.Phi.Syntax.Abs
import PyF (fmt)

-- $setup
-- >>> :set -XOverloadedStrings
-- >>> :set -XOverloadedLists

instance FromJSON Object where parseJSON = fmap fromString . parseJSON
instance FromJSON Binding where parseJSON = fmap fromString . parseJSON

instance FromJSON ObjectMetaId where parseJSON = fmap ObjectMetaId . parseJSON
instance FromJSON LabelMetaId where parseJSON = fmap LabelMetaId . parseJSON
instance FromJSON TailMetaId where parseJSON = fmap TailMetaId . parseJSON
instance FromJSON BindingsMetaId where parseJSON = fmap BindingsMetaId . parseJSON
instance FromJSON BytesMetaId where parseJSON = fmap BytesMetaId . parseJSON

instance FromJSON MetaId where
  parseJSON = fmap fromString . parseJSON

instance FromJSON Attribute where parseJSON = fmap fromString . parseJSON
instance FromJSON RuleAttribute where parseJSON = fmap fromString . parseJSON

instance FromJSON LabelId
instance FromJSON AlphaIndex

data RuleSet = RuleSet
  { title :: String
  , rules :: [Rule]
  }
  deriving (Generic, FromJSON, Show)

data RuleContext = RuleContext
  { global_object :: Maybe Object
  , current_object :: Maybe Object
  , current_attribute :: Maybe Attribute
  }
  deriving (Generic, FromJSON, Show)

data Rule = Rule
  { name :: String
  , description :: String
  , context :: Maybe RuleContext
  , forall :: Maybe [MetaId]
  , pattern :: Object
  , result :: Object
  , fresh :: Maybe [FreshMetaId]
  , when :: Maybe [Condition]
  , tests :: Maybe [RuleTest]
  }
  deriving (Generic, FromJSON, Show)

data FreshMetaId = FreshMetaId
  { name :: LabelMetaId
  , prefix :: Maybe String
  }
  deriving (Generic, FromJSON, Show)

data RuleTest = RuleTest
  { name :: String
  , input :: Object
  , output :: [Object]
  , options :: Maybe [RuleTestOption]
  }
  deriving (Generic, FromJSON, Show)

newtype RuleTestOption = TakeOne {take_one :: Bool}
  -- deriving (Generic, Show, FromJSON)
  deriving (Eq, Generic, Show)
instance FromJSON RuleTestOption where
  parseJSON = genericParseJSON defaultOptions{sumEncoding = UntaggedValue}

data AttrsInBindings = AttrsInBindings
  { attrs :: [RuleAttribute]
  , bindings :: [Binding]
  }
  deriving (Generic, Show, FromJSON)
data Condition
  = IsNF {nf :: Object}
  | IsNFInsideFormation {nf_inside_formation :: Object}
  | PresentAttrs {present_attrs :: AttrsInBindings}
  | AbsentAttrs {absent_attrs :: AttrsInBindings}
  | AttrNotEqual {not_equal :: (Attribute, Attribute)}
  | ApplyInSubformations {apply_in_subformations :: Bool}
  | ApplyInAbstractSubformations {apply_in_abstract_subformations :: Bool}
  deriving (Generic, Show)
instance FromJSON Condition where
  parseJSON = genericParseJSON defaultOptions{sumEncoding = UntaggedValue}

parseRuleSetFromFile :: FilePath -> IO RuleSet
parseRuleSetFromFile = Yaml.decodeFileThrow

convertRule :: Rule -> Common.Rule
convertRule Rule{..} ctx obj = do
  -- first validate pattern and result in the rule
  -- TODO: we should perform this check once, not every time we run the rule
  let freshMetaIds =
        Set.mapMonotonic MetaIdLabel $
          foldMap (Set.fromList . map (\FreshMetaId{name = x} -> x)) fresh

      patternMetaIds = objectMetaIds pattern
      resultMetaIds = objectMetaIds result

      unusedFreshMetaIds = Set.difference freshMetaIds resultMetaIds

      ppMetaIds = intercalate ", " . map printTree . Set.toList

  unless (null unusedFreshMetaIds) $
    error ("invalid rule: result does not use some fresh variables quantified by the fresh: " <> ppMetaIds unusedFreshMetaIds)

  case forall of
    Nothing -> return ()
    Just forall' -> do
      let forallMetaIds = Set.fromList forall'
          resultAllowedMetaIds = forallMetaIds <> freshMetaIds
          unquantifiedMetaIds = Set.difference patternMetaIds forallMetaIds
          unusedMetaIds = Set.difference forallMetaIds patternMetaIds
          unquantifiedResultMetaIds = Set.difference resultMetaIds resultAllowedMetaIds
      unless (null unquantifiedMetaIds) $
        error ("invalid rule: pattern uses meta variables not quantified by the forall: " <> ppMetaIds unquantifiedMetaIds)
      unless (null unusedMetaIds) $
        error ("invalid rule: pattern does not use some variables quantified by the forall: " <> ppMetaIds unusedMetaIds)
      unless (null unquantifiedResultMetaIds) $
        error ("invalid rule: result uses meta variables not quantified by the forall or the fresh: " <> ppMetaIds unquantifiedResultMetaIds)

  contextSubsts <- matchContext ctx context
  let pattern' = applySubst contextSubsts pattern
      result' = applySubst contextSubsts result
  subst <- matchObject pattern' obj
  guard $ all (\cond -> checkCond ctx cond (contextSubsts <> subst)) (fromMaybe [] when)
  let substFresh = mkFreshSubst ctx result' fresh
      result'' = applySubst (contextSubsts <> subst <> substFresh) result'
      -- TODO #152:30m what context should we pass to evaluate meta funcs?
      obj' = evaluateMetaFuncs obj result''
  guard $ not (objectHasMetavars obj')
  pure obj'

convertRuleNamed :: Rule -> NamedRule
convertRuleNamed rule = (rule.name, convertRule rule)

mkFreshSubst :: Context -> Object -> Maybe [FreshMetaId] -> Subst
mkFreshSubst ctx obj metas =
  Subst
    { objectMetas = []
    , bindingsMetas = []
    , attributeMetas = mkFreshAttributes (usedLabelIds ctx <> objectLabelIds obj) (fromMaybe [] metas)
    , bytesMetas = []
    , contextMetas = []
    }

mkFreshAttributes :: Set LabelId -> [FreshMetaId] -> [(LabelMetaId, Attribute)]
mkFreshAttributes _ids [] = []
mkFreshAttributes ids (x : xs) =
  case mkFreshAttribute ids x of
    (ma, ids') -> ma : mkFreshAttributes ids' xs

mkFreshAttribute :: Set LabelId -> FreshMetaId -> ((LabelMetaId, Attribute), Set LabelId)
mkFreshAttribute ids FreshMetaId{..} = ((name, Label label), Set.insert label ids)
 where
  label =
    head
      [ l
      | i <- [1 ..]
      , let l = LabelId (fromMaybe "tmp" prefix <> "$" <> show i)
      , l `Set.notMember` ids
      ]

usedLabelIds :: Context -> Set LabelId
usedLabelIds Context{..} = objectLabelIds globalObject
 where
  globalObject = NonEmpty.last outerFormations

objectLabelIds :: Object -> Set LabelId
objectLabelIds = \case
  GlobalObject -> mempty
  ThisObject -> mempty
  Formation bindings -> foldMap bindingLabelIds bindings
  ObjectDispatch obj a -> objectLabelIds obj <> attrLabelIds a
  Application obj bindings -> objectLabelIds obj <> foldMap bindingLabelIds bindings
  Termination -> mempty
  MetaObject{} -> mempty
  MetaFunction _ obj -> objectLabelIds obj
  MetaTailContext obj _ -> objectLabelIds obj
  MetaSubstThis obj obj' -> objectLabelIds obj <> objectLabelIds obj'
  MetaContextualize obj obj' -> objectLabelIds obj <> objectLabelIds obj'

bindingLabelIds :: Binding -> Set LabelId
bindingLabelIds = \case
  AlphaBinding a obj -> objectLabelIds obj <> attrLabelIds a
  DeltaBinding _bytes -> mempty
  EmptyBinding a -> attrLabelIds a
  DeltaEmptyBinding -> mempty
  LambdaBinding _ -> mempty
  MetaBindings _ -> mempty
  MetaDeltaBinding _ -> mempty

attrLabelIds :: Attribute -> Set LabelId
attrLabelIds (Label l) = Set.singleton l
attrLabelIds _ = mempty

-- >>> matchContext (Context [] ["⟦ a ↦ ⟦ ⟧, x ↦ ξ.a ⟧"] (Label (LabelId "x"))) (Just (RuleContext Nothing (Just "⟦ !a ↦ !obj, !B ⟧") (Just "!a")))
-- [Subst {
--   objectMetas = [!obj -> 'ξ.a']
--   bindingsMetas = [!B -> 'a ↦ ⟦ ⟧']
--   attributeMetas = [!a -> 'x']
-- }]
matchContext :: Common.Context -> Maybe RuleContext -> [Subst]
matchContext Common.Context{} Nothing = [emptySubst]
matchContext Common.Context{..} (Just (RuleContext{..})) = do
  subst1 <- maybe [emptySubst] (`matchObject` globalObject) global_object
  subst2 <- maybe [emptySubst] ((`matchObject` thisObject) . applySubst subst1) current_object
  subst3 <- maybe [emptySubst] ((`matchAttr` currentAttr) . applySubstAttr (subst1 <> subst2)) current_attribute
  return (subst1 <> subst2 <> subst3)
 where
  globalObject = NonEmpty.last outerFormations
  thisObject = NonEmpty.head outerFormations

objectMetaIds :: Object -> Set MetaId
objectMetaIds (Formation bindings) = foldMap bindingMetaIds bindings
objectMetaIds (Application object bindings) = objectMetaIds object <> foldMap bindingMetaIds bindings
objectMetaIds (ObjectDispatch object attr) = objectMetaIds object <> attrMetaIds attr
objectMetaIds GlobalObject = mempty
objectMetaIds ThisObject = mempty
objectMetaIds Termination = mempty
objectMetaIds (MetaObject x) = Set.singleton (MetaIdObject x)
objectMetaIds (MetaFunction _ obj) = objectMetaIds obj
objectMetaIds (MetaTailContext obj x) = objectMetaIds obj <> Set.singleton (MetaIdTail x)
objectMetaIds (MetaSubstThis obj obj') = foldMap objectMetaIds [obj, obj']
objectMetaIds (MetaContextualize obj obj') = foldMap objectMetaIds [obj, obj']

bindingMetaIds :: Binding -> Set MetaId
bindingMetaIds (AlphaBinding attr obj) = attrMetaIds attr <> objectMetaIds obj
bindingMetaIds (EmptyBinding attr) = attrMetaIds attr
bindingMetaIds (DeltaBinding _) = mempty
bindingMetaIds DeltaEmptyBinding = mempty
bindingMetaIds (LambdaBinding _) = mempty
bindingMetaIds (MetaBindings x) = Set.singleton (MetaIdBindings x)
bindingMetaIds (MetaDeltaBinding x) = Set.singleton (MetaIdBytes x)

attrMetaIds :: Attribute -> Set MetaId
attrMetaIds Phi = mempty
attrMetaIds Rho = mempty
attrMetaIds (Label _) = mempty
attrMetaIds (Alpha _) = mempty
attrMetaIds (MetaAttr x) = Set.singleton (MetaIdLabel x)

objectHasMetavars :: Object -> Bool
objectHasMetavars (Formation bindings) = any bindingHasMetavars bindings
objectHasMetavars (Application object bindings) = objectHasMetavars object || any bindingHasMetavars bindings
objectHasMetavars (ObjectDispatch object attr) = objectHasMetavars object || attrHasMetavars attr
objectHasMetavars GlobalObject = False
objectHasMetavars ThisObject = False
objectHasMetavars Termination = False
objectHasMetavars (MetaObject _) = True
objectHasMetavars (MetaFunction _ _) = True
objectHasMetavars MetaTailContext{} = True
objectHasMetavars (MetaSubstThis _ _) = True -- technically not a metavar, but a substitution
objectHasMetavars (MetaContextualize _ _) = True

bindingHasMetavars :: Binding -> Bool
bindingHasMetavars (AlphaBinding attr obj) = attrHasMetavars attr || objectHasMetavars obj
bindingHasMetavars (EmptyBinding attr) = attrHasMetavars attr
bindingHasMetavars (DeltaBinding _) = False
bindingHasMetavars DeltaEmptyBinding = False
bindingHasMetavars (LambdaBinding _) = False
bindingHasMetavars (MetaBindings _) = True
bindingHasMetavars (MetaDeltaBinding _) = True

attrHasMetavars :: Attribute -> Bool
attrHasMetavars Phi = False
attrHasMetavars Rho = False
attrHasMetavars (Label _) = False
attrHasMetavars (Alpha _) = False
attrHasMetavars (MetaAttr _) = True

-- | Given a condition, and a substition from object matching
--   tells whether the condition matches the object
checkCond :: Common.Context -> Condition -> Subst -> Bool
checkCond ctx (IsNF obj) subst = Common.isNF ctx $ applySubst subst obj
checkCond ctx (IsNFInsideFormation obj) subst = Common.isNF ctx{insideFormation = True} $ applySubst subst obj
checkCond _ctx (PresentAttrs (AttrsInBindings attrs bindings)) subst = any (`hasAttr` substitutedBindings) substitutedAttrs
 where
  substitutedBindings = concatMap (applySubstBinding subst) bindings
  ruleToNormalAttr :: RuleAttribute -> Attribute
  ruleToNormalAttr (ObjectAttr a) = a
  -- Hack to be able to use applySubstAttr with RuleAttribute.
  -- Should not actually substitute anything anyway since they are not metavariables
  ruleToNormalAttr DeltaAttr = Label (LabelId "Δ")
  ruleToNormalAttr LambdaAttr = Label (LabelId "λ")
  normalToRuleAttr :: Attribute -> RuleAttribute
  normalToRuleAttr (Label (LabelId "Δ")) = DeltaAttr
  normalToRuleAttr (Label (LabelId "λ")) = LambdaAttr
  normalToRuleAttr a = ObjectAttr a
  substitutedAttrs = map (normalToRuleAttr . applySubstAttr subst . ruleToNormalAttr) attrs
checkCond ctx (AbsentAttrs s) subst = not $ checkCond ctx (PresentAttrs s) subst
checkCond _ctx (AttrNotEqual (a1, a2)) subst = applySubstAttr subst a1 /= applySubstAttr subst a2
checkCond ctx (ApplyInSubformations shouldApply) _subst
  | shouldApply = True
  | otherwise = not (insideFormation ctx)
checkCond ctx (ApplyInAbstractSubformations shouldApply) _subst
  | shouldApply = True
  | otherwise = not (insideAbstractFormation ctx)

hasAttr :: RuleAttribute -> [Binding] -> Bool
hasAttr attr = any (isAttr attr)
 where
  isAttr (ObjectAttr a) (AlphaBinding a' _) = a == a'
  isAttr (ObjectAttr a) (EmptyBinding a') = a == a'
  isAttr DeltaAttr (DeltaBinding _) = True
  isAttr DeltaAttr DeltaEmptyBinding = True
  isAttr LambdaAttr (LambdaBinding _) = True
  isAttr _ _ = False

-- input: ⟦ a ↦ ⟦ c ↦ ⟦ ⟧ ⟧, b ↦ ⟦ ⟧ ⟧.a

-- pattern:   ⟦ !a ↦ !n, !B ⟧.!a
-- result:    !n(ρ ↦ ⟦ !B ⟧)

-- match pattern input (get substitution):
--  !a = a
--  !n = ⟦ c ↦ ⟦ ⟧ ⟧
--  !B = b ↦ ⟦ ⟧

-- actual result (after applying substitution):
--  ⟦ c ↦ ⟦ ⟧ ⟧(ρ ↦ ⟦ b ↦ ⟦ ⟧ ⟧)

data OneHoleContext = OneHoleContext
  { holeMetaId :: !ObjectMetaId
  , contextObject :: !Object
  }
  deriving (Show)

data Subst = Subst
  { objectMetas :: [(ObjectMetaId, Object)]
  , bindingsMetas :: [(BindingsMetaId, [Binding])]
  , attributeMetas :: [(LabelMetaId, Attribute)]
  , bytesMetas :: [(BytesMetaId, Bytes)]
  , contextMetas :: [(TailMetaId, OneHoleContext)]
  }
instance Show Subst where
  show Subst{..} =
    intercalate
      "\n"
      [ "Subst {"
      , "  objectMetas = [" <> showMappings objectMetas <> "]"
      , "  bindingsMetas = [" <> showMappings bindingsMetas <> "]"
      , "  attributeMetas = [" <> showMappings attributeMetas <> "]"
      , "  bytesMetas = [" <> showMappings bytesMetas <> "]"
      , "  contextMetas = [" <> show contextMetas <> "]"
      , "}"
      ]
   where
    showMappings metas = intercalate "; " $ map (\(metaId, obj) -> [fmt|{printTree metaId} -> '{printTree obj}'|]) metas

instance Semigroup Subst where
  (<>) = mergeSubst

instance Monoid Subst where
  mempty = emptySubst

emptySubst :: Subst
emptySubst = Subst [] [] [] [] []

-- >>> putStrLn $ Language.EO.Phi.printTree (applySubst (Subst [("!n", "⟦ c ↦ ⟦ ⟧ ⟧")] [("!B", ["b ↦ ⟦ ⟧"])] [("!a", "a")]) "!n(ρ ↦ ⟦ !B ⟧)" :: Object)
-- ⟦ c ↦ ⟦ ⟧ ⟧ (ρ ↦ ⟦ b ↦ ⟦ ⟧ ⟧)
applySubst :: Subst -> Object -> Object
applySubst subst@Subst{..} = \case
  Formation bindings ->
    Formation (applySubstBindings subst bindings)
  Application obj bindings ->
    Application (applySubst subst obj) (applySubstBindings subst bindings)
  ObjectDispatch obj a ->
    ObjectDispatch (applySubst subst obj) (applySubstAttr subst a)
  GlobalObject -> GlobalObject
  ThisObject -> ThisObject
  obj@(MetaObject x) -> fromMaybe obj $ lookup x objectMetas
  Termination -> Termination
  MetaSubstThis obj thisObj -> MetaSubstThis (applySubst subst obj) (applySubst subst thisObj)
  MetaContextualize obj thisObj -> MetaContextualize (applySubst subst obj) (applySubst subst thisObj)
  obj@MetaFunction{} -> obj
  MetaTailContext obj c ->
    case lookup c contextMetas of
      Nothing -> MetaTailContext (applySubst subst obj) c
      Just OneHoleContext{..} ->
        let holeSubst = mempty{objectMetas = [(holeMetaId, applySubst subst obj)]}
         in applySubst holeSubst contextObject

applySubstAttr :: Subst -> Attribute -> Attribute
applySubstAttr Subst{..} = \case
  attr@(MetaAttr x) -> fromMaybe attr $ lookup x attributeMetas
  attr -> attr

applySubstBindings :: Subst -> [Binding] -> [Binding]
applySubstBindings subst = concatMap (applySubstBinding subst)

applySubstBinding :: Subst -> Binding -> [Binding]
applySubstBinding subst@Subst{..} = \case
  AlphaBinding a obj ->
    [AlphaBinding (applySubstAttr subst a) (applySubst subst obj)]
  EmptyBinding a ->
    [EmptyBinding (applySubstAttr subst a)]
  DeltaBinding bytes -> [DeltaBinding (coerce bytes)]
  DeltaEmptyBinding -> [DeltaEmptyBinding]
  LambdaBinding bytes -> [LambdaBinding (coerce bytes)]
  b@(MetaBindings m) -> fromMaybe [b] (lookup m bindingsMetas)
  b@(MetaDeltaBinding m) -> maybe [b] (pure . DeltaBinding) (lookup m bytesMetas)

mergeSubst :: Subst -> Subst -> Subst
mergeSubst (Subst xs ys zs ws us) (Subst xs' ys' zs' ws' us') =
  Subst (xs ++ xs') (ys ++ ys') (zs ++ zs') (ws ++ ws') (us ++ us')

-- 1. need to implement applySubst' :: Subst -> Object -> Object
-- 2. complete the code
matchObject :: Object -> Object -> [Subst]
matchObject (Formation ps) (Formation bs) = matchBindings ps bs
matchObject (Application obj bindings) (Application obj' bindings') = do
  subst1 <- matchObject obj obj'
  subst2 <- matchBindings (applySubstBindings subst1 bindings) bindings'
  pure (subst1 <> subst2)
matchObject (ObjectDispatch pat a) (ObjectDispatch obj a') = do
  subst1 <- matchObject pat obj
  subst2 <- matchAttr (applySubstAttr subst1 a) a'
  pure (subst1 <> subst2)
matchObject (MetaObject m) obj =
  pure emptySubst{objectMetas = [(m, obj)]}
matchObject (MetaTailContext pat x) obj = do
  (subst@Subst{..}, matchedCtx) <- matchOneHoleContext x pat obj
  return subst{contextMetas = contextMetas <> [(x, matchedCtx)]}
matchObject Termination Termination = [emptySubst]
matchObject ThisObject ThisObject = [emptySubst]
matchObject GlobalObject GlobalObject = [emptySubst]
matchObject _ _ = [] -- ? emptySubst ?

matchOneHoleContext :: TailMetaId -> Object -> Object -> [(Subst, OneHoleContext)]
matchOneHoleContext ctxId pat obj = matchWhole <> matchPart
 where
  TailMetaId name = ctxId
  holeId = ObjectMetaId (name ++ ":hole") -- FIXME: ensure fresh names
  matchWhole = do
    subst' <- matchObject pat obj
    pure (subst', OneHoleContext holeId (MetaObject holeId))
  matchPart = case obj of
    ObjectDispatch obj' a -> do
      (subst, OneHoleContext{..}) <- matchOneHoleContext ctxId pat obj'
      return (subst, OneHoleContext{contextObject = ObjectDispatch contextObject a, ..})
    -- FIXME: consider matching inside bindings of application as well
    Application obj' bindings -> do
      (subst, OneHoleContext{..}) <- matchOneHoleContext ctxId pat obj'
      return (subst, OneHoleContext{contextObject = Application contextObject bindings, ..})
    -- cases below cannot be matched
    Formation{} -> []
    GlobalObject -> []
    ThisObject -> []
    Termination -> []
    -- should cases below be errors?
    MetaSubstThis{} -> []
    MetaContextualize{} -> []
    MetaObject{} -> []
    MetaTailContext{} -> []
    MetaFunction{} -> []

-- | Evaluate meta functions
-- given top-level context as an object
-- and an object
evaluateMetaFuncs :: Object -> Object -> Object
evaluateMetaFuncs _obj' obj =
  evalState
    (evaluateMetaFuncs' obj)
    MetaState{}

data MetaState = MetaState
  {
  }
  deriving (Generic)

evaluateMetaFuncs' :: Object -> State MetaState Object
evaluateMetaFuncs' (Formation bindings) = Formation <$> mapM evaluateMetaFuncsBinding bindings
evaluateMetaFuncs' (Application obj bindings) = Application <$> evaluateMetaFuncs' obj <*> mapM evaluateMetaFuncsBinding bindings
evaluateMetaFuncs' (ObjectDispatch obj a) = ObjectDispatch <$> evaluateMetaFuncs' obj <*> pure a
evaluateMetaFuncs' (MetaSubstThis obj thisObj) = evaluateMetaFuncs' (substThis thisObj obj)
evaluateMetaFuncs' (MetaContextualize obj thisObj) = evaluateMetaFuncs' (contextualize thisObj obj)
evaluateMetaFuncs' obj = pure obj

evaluateMetaFuncsBinding :: Binding -> State MetaState Binding
evaluateMetaFuncsBinding (AlphaBinding attr obj) = AlphaBinding attr <$> evaluateMetaFuncs' obj
evaluateMetaFuncsBinding binding = pure binding

matchBindings :: [Binding] -> [Binding] -> [Subst]
matchBindings [] [] = [emptySubst]
matchBindings [MetaBindings b] bindings =
  pure
    emptySubst
      { bindingsMetas = [(b, bindings)]
      }
matchBindings (p : ps) bs = do
  (bs', subst1) <- matchFindBinding p bs
  subst2 <- matchBindings (applySubstBindings subst1 ps) bs'
  pure (subst1 <> subst2)
matchBindings [] _ = []

-- >>> select [1,2,3,4]
-- [(1,[2,3,4]),(2,[1,3,4]),(3,[1,2,4]),(4,[1,2,3])]
select :: [a] -> [(a, [a])]
select [] = []
select [x] = [(x, [])]
select (x : xs) =
  (x, xs)
    : [ (y, x : ys)
      | (y, ys) <- select xs
      ]

matchFindBinding :: Binding -> [Binding] -> [([Binding], Subst)]
matchFindBinding p bindings =
  [ (leftover, subst)
  | (binding, leftover) <- select bindings
  , subst <- matchBinding p binding
  ]

matchBinding :: Binding -> Binding -> [Subst]
matchBinding MetaBindings{} _ = []
matchBinding (AlphaBinding a obj) (AlphaBinding a' obj') = do
  subst1 <- matchAttr a a'
  subst2 <- matchObject obj obj'
  pure (subst1 <> subst2)
matchBinding (MetaDeltaBinding m) (DeltaBinding bytes) = [emptySubst{bytesMetas = [(m, bytes)]}]
matchBinding (DeltaBinding bytes) (DeltaBinding bytes')
  | bytes == bytes' = [emptySubst]
matchBinding DeltaEmptyBinding DeltaEmptyBinding = [emptySubst]
matchBinding (EmptyBinding a1) (EmptyBinding a2) = matchAttr a1 a2
matchBinding (LambdaBinding f1) (LambdaBinding f2)
  | f1 == f2 = [emptySubst]
matchBinding _ _ = []

matchAttr :: Attribute -> Attribute -> [Subst]
matchAttr l r | l == r = [emptySubst]
matchAttr (MetaAttr metaId) attr =
  [ emptySubst
      { attributeMetas = [(metaId, attr)]
      }
  ]
matchAttr _ _ = []

substThis :: Object -> Object -> Object
substThis thisObj = go
 where
  isAttachedRho (AlphaBinding Rho _) = True
  isAttachedRho _ = False

  isEmptyRho (EmptyBinding Rho) = True
  isEmptyRho _ = False

  go = \case
    ThisObject -> thisObj -- ξ is substituted
    -- IMPORTANT: we are injecting a ρ-attribute in formations!
    obj@(Formation bindings)
      | any isAttachedRho bindings -> obj
      | otherwise -> Formation (filter (not . isEmptyRho) bindings ++ [AlphaBinding Rho thisObj])
    -- everywhere else we simply recursively traverse the φ-term
    Application obj bindings -> Application (go obj) (map (substThisBinding thisObj) bindings)
    ObjectDispatch obj a -> ObjectDispatch (go obj) a
    GlobalObject -> GlobalObject
    Termination -> Termination
    obj@MetaTailContext{} -> error ("impossible: trying to substitute ξ in " <> printTree obj)
    obj@MetaContextualize{} -> error ("impossible: trying to substitute ξ in " <> printTree obj)
    obj@MetaSubstThis{} -> error ("impossible: trying to substitute ξ in " <> printTree obj)
    obj@MetaObject{} -> error ("impossible: trying to substitute ξ in " <> printTree obj)
    obj@MetaFunction{} -> error ("impossible: trying to substitute ξ in " <> printTree obj)

-- {⟦ x ↦ ⟦ b ↦ ⟦ Δ ⤍ 01- ⟧, φ ↦ ⟦ b ↦ ⟦ Δ ⤍ 02- ⟧, c ↦ ⟦ a ↦ ξ.ρ.ρ.b ⟧.a ⟧.c ⟧.φ, λ ⤍ Package ⟧}

-- {⟦ λ ⤍ Package, x ↦ ⟦ b ↦ ⟦⟧ ⟧ ⟧}

substThisBinding :: Object -> Binding -> Binding
substThisBinding obj = \case
  AlphaBinding a obj' -> AlphaBinding a (substThis obj obj')
  EmptyBinding a -> EmptyBinding a
  DeltaBinding bytes -> DeltaBinding bytes
  DeltaEmptyBinding -> DeltaEmptyBinding
  LambdaBinding bytes -> LambdaBinding bytes
  b@MetaBindings{} -> error ("impossible: trying to substitute ξ in " <> printTree b)
  b@MetaDeltaBinding{} -> error ("impossible: trying to substitute ξ in " <> printTree b)

contextualize :: Object -> Object -> Object
contextualize thisObj = go
 where
  go = \case
    ThisObject -> thisObj -- ξ is substituted
    obj@(Formation _bindings) -> obj
    ObjectDispatch obj a -> ObjectDispatch (go obj) a
    Application obj bindings -> Application (go obj) (map (contextualizeBinding thisObj) bindings)
    GlobalObject -> GlobalObject -- TODO: Change to what GlobalObject is attached to
    Termination -> Termination
    obj@MetaTailContext{} -> error ("impossible: trying to contextualize " <> printTree obj)
    obj@MetaContextualize{} -> error ("impossible: trying to contextualize " <> printTree obj)
    obj@MetaSubstThis{} -> error ("impossible: trying to contextualize " <> printTree obj)
    obj@MetaObject{} -> error ("impossible: trying to contextualize " <> printTree obj)
    obj@MetaFunction{} -> error ("impossible: trying to contextualize " <> printTree obj)

contextualizeBinding :: Object -> Binding -> Binding
contextualizeBinding obj = \case
  AlphaBinding a obj' -> AlphaBinding a (contextualize obj obj')
  EmptyBinding a -> EmptyBinding a
  DeltaBinding bytes -> DeltaBinding bytes
  DeltaEmptyBinding -> DeltaEmptyBinding
  LambdaBinding bytes -> LambdaBinding bytes
  b@MetaBindings{} -> error ("impossible: trying to contextualize " <> printTree b)
  b@MetaDeltaBinding{} -> error ("impossible: trying to contextualize " <> printTree b)