packages feed

ychr-0.1.0.0: src/YCHR/Internal/Runtime/Var.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Logical variables, compound terms, unification, and equality.
--
-- This module provides the foundational layer of the CHR Haskell runtime:
-- mutable logical variables with binding chains, Prolog-style unification
-- (tell semantics) and equality checking (ask semantics), and compound
-- term construction and inspection.
--
-- Unification collects observer IDs from bound variables, enabling
-- selective constraint reactivation (per the paper, Section 5.3).
-- Path compression is applied during dereferencing to amortize
-- future lookups.
module YCHR.Internal.Runtime.Var
  ( -- * Types (re-exported from YCHR.Internal.Runtime.Types)
    VarId (..),
    Var (..),
    VarState (..),
    Value (..),

    -- * Operations
    newVar,
    deref,
    unify,
    unifiable,
    equal,
    makeTerm,
    matchTerm,
    getArg,
    addObserver,
    getVarId,
  )
where

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Reader (ask)
import Data.IORef
import Data.Text (Text)
import YCHR.Internal.Runtime.Monad (Chr, SessionEnv (..))
import YCHR.Internal.Runtime.Types
  ( SuspensionId,
    Value (..),
    Var (..),
    VarId (..),
    VarState (..),
  )

-- ---------------------------------------------------------------------------
-- Internal Unify primitives
-- ---------------------------------------------------------------------------

readVarState :: Var -> Chr VarState
readVarState (Var ref) = liftIO $ readIORef ref

writeVarState :: Var -> VarState -> Chr ()
writeVarState (Var ref) st = liftIO $ writeIORef ref st

newVarRef :: VarState -> Chr Var
newVarRef st = liftIO $ Var <$> newIORef st

freshVarId :: Chr VarId
freshVarId = do
  SessionEnv {varCounter} <- ask
  liftIO $ do
    vid@(VarId n) <- readIORef varCounter
    writeIORef varCounter (VarId (n + 1))
    pure vid

-- ---------------------------------------------------------------------------
-- Operations
-- ---------------------------------------------------------------------------

-- | Create a fresh unbound logical variable.
newVar :: Chr Value
newVar = do
  vid <- freshVarId
  v <- newVarRef (Unbound vid [])
  pure (VVar v)

-- | Follow binding chains to find the ultimate value, applying
-- path compression along the way. If the result is an unbound
-- variable, returns the 'VVar' wrapping it.
deref :: Value -> Chr Value
deref val@(VVar var@(Var ref)) = do
  st <- readVarState var
  case st of
    Unbound {} -> pure val
    Bound v -> do
      v' <- deref v
      case v' of
        VVar (Var ref')
          | ref == ref' -> pure ()
        _ -> writeVarState var (Bound v')
      pure v'
deref val = pure val

-- | Unify two values (tell semantics, Prolog @=@).
--
-- Returns @(success, observers)@: the boolean indicates whether
-- unification succeeded, and the list is the observer ids gathered
-- from every variable that was bound during the call. Callers
-- (typically the 'BUnify' interpretation in
-- "YCHR.Internal.Runtime.Interpreter") forward the observers to the
-- reactivation queue.
--
-- The observer list is meaningful even when @success@ is 'False':
-- 'unifyArgs' short-circuits on the first failing argument pair, but
-- variables bound by earlier pairs remain bound (we do not roll back),
-- and the observers from those bindings are still returned. Callers
-- must enqueue them so the half-committed bindings are followed up on.
unify :: Value -> Value -> Chr (Bool, [SuspensionId])
unify v1 v2 = do
  d1 <- deref v1
  d2 <- deref v2
  unify' d1 d2

unify' :: Value -> Value -> Chr (Bool, [SuspensionId])
unify' VWildcard _ = pure (True, [])
unify' _ VWildcard = pure (True, [])
unify' (VVar (Var ref1)) (VVar (Var ref2))
  | ref1 == ref2 = pure (True, [])
unify' (VVar var1) v2@(VVar var2) = do
  st1 <- readVarState var1
  case st1 of
    Bound {} -> error "unify': unexpected Bound after deref"
    Unbound _ obs1 -> do
      st2 <- readVarState var2
      case st2 of
        Unbound vid2 obs2 -> do
          writeVarState var1 (Bound v2)
          writeVarState var2 (Unbound vid2 (obs1 ++ obs2))
          pure (True, obs1)
        Bound {} -> error "unify': unexpected Bound after deref"
unify' (VVar var) v = do
  st <- readVarState var
  case st of
    Bound {} -> error "unify': unexpected Bound after deref"
    Unbound _ obs -> do
      writeVarState var (Bound v)
      pure (True, obs)
unify' v (VVar vr) = unify' (VVar vr) v
unify' (VInt a) (VInt b) = pure (a == b, [])
unify' (VFloat a) (VFloat b) = pure (a == b, [])
unify' (VAtom a) (VAtom b) = pure (a == b, [])
unify' (VText a) (VText b) = pure (a == b, [])
unify' (VBool a) (VBool b) = pure (a == b, [])
unify' (VTerm f1 args1) (VTerm f2 args2)
  | f1 == f2 && length args1 == length args2 = unifyArgs args1 args2
unify' _ _ = pure (False, [])

-- | Unify argument lists pairwise. Short-circuits on the first failure.
-- Observer lists from successful element unifications are concatenated.
unifyArgs :: [Value] -> [Value] -> Chr (Bool, [SuspensionId])
unifyArgs [] [] = pure (True, [])
unifyArgs (a : as) (b : bs) = do
  (ok, obs) <- unify a b
  if ok
    then do
      (ok', obs') <- unifyArgs as bs
      pure (ok', obs ++ obs')
    else pure (False, obs)
unifyArgs _ _ = pure (False, [])

-- | Check whether two values can be unified, without committing any
-- bindings. Returns 'True' iff 'unify' would succeed.
--
-- Mutations made to variable cells during the check are recorded on a
-- local trail and rolled back before returning, so the operation is
-- observably pure with respect to variable bindings. Path compression
-- performed by 'deref' is preserved (it is semantically invisible).
-- Observer lists are never modified.
unifiable :: Value -> Value -> Chr Bool
unifiable a b = do
  trailRef <- liftIO $ newIORef []
  result <- uni trailRef a b
  liftIO $ do
    entries <- readIORef trailRef
    -- Entries are prepended newest-first, so walking front-to-back
    -- restores each cell to its oldest captured state.
    mapM_ (\(Var ref, st) -> writeIORef ref st) entries
  pure result
  where
    trailWrite trailRef var@(Var ref) newSt = liftIO $ do
      cur <- readIORef ref
      modifyIORef' trailRef ((var, cur) :)
      writeIORef ref newSt

    uni trailRef v1 v2 = do
      d1 <- deref v1
      d2 <- deref v2
      uni' trailRef d1 d2

    uni' _ VWildcard _ = pure True
    uni' _ _ VWildcard = pure True
    uni' _ (VVar (Var ref1)) (VVar (Var ref2))
      | ref1 == ref2 = pure True
    uni' trailRef (VVar var1) v2@(VVar _) = do
      st1 <- readVarState var1
      case st1 of
        Bound {} -> error "unifiable: unexpected Bound after deref"
        Unbound _ _ -> do
          trailWrite trailRef var1 (Bound v2)
          pure True
    uni' trailRef (VVar var) v = do
      st <- readVarState var
      case st of
        Bound {} -> error "unifiable: unexpected Bound after deref"
        Unbound _ _ -> do
          trailWrite trailRef var (Bound v)
          pure True
    uni' trailRef v (VVar vr) = uni' trailRef (VVar vr) v
    uni' _ (VInt x) (VInt y) = pure (x == y)
    uni' _ (VFloat x) (VFloat y) = pure (x == y)
    uni' _ (VAtom x) (VAtom y) = pure (x == y)
    uni' _ (VText x) (VText y) = pure (x == y)
    uni' _ (VBool x) (VBool y) = pure (x == y)
    uni' trailRef (VTerm f1 args1) (VTerm f2 args2)
      | f1 == f2 && length args1 == length args2 = uniArgs trailRef args1 args2
    uni' _ _ _ = pure False

    uniArgs _ [] [] = pure True
    uniArgs trailRef (x : xs) (y : ys) = do
      ok <- uni trailRef x y
      if ok then uniArgs trailRef xs ys else pure False
    uniArgs _ _ _ = pure False

-- | Check equality of two values (ask semantics, Prolog @==@).
--
-- No mutation beyond path compression during dereferencing.
-- Two distinct unbound variables are /not/ equal.
equal :: Value -> Value -> Chr Bool
equal v1 v2 = do
  d1 <- deref v1
  d2 <- deref v2
  equal' d1 d2

equal' :: Value -> Value -> Chr Bool
equal' (VVar (Var ref1)) (VVar (Var ref2)) = pure (ref1 == ref2)
equal' (VVar _) _ = pure False
equal' _ (VVar _) = pure False
equal' (VInt a) (VInt b) = pure (a == b)
equal' (VFloat a) (VFloat b) = pure (a == b)
equal' (VAtom a) (VAtom b) = pure (a == b)
equal' (VText a) (VText b) = pure (a == b)
equal' (VBool a) (VBool b) = pure (a == b)
equal' (VTerm f1 args1) (VTerm f2 args2)
  | f1 == f2 && length args1 == length args2 = allEqual args1 args2
equal' _ _ = pure False

allEqual :: [Value] -> [Value] -> Chr Bool
allEqual [] [] = pure True
allEqual (a : as) (b : bs) = do
  ok <- equal a b
  if ok then allEqual as bs else pure False
allEqual _ _ = pure False

-- | Construct a compound term. Pure.
makeTerm :: Text -> [Value] -> Value
makeTerm = VTerm

-- | Check whether a value is a compound term with the given functor and
-- arity. Dereferences first. 0-arity compounds collapse to 'VAtom' at
-- the runtime layer, so a 'VAtom' matches when @arity == 0@ and its
-- name matches @functor@.
matchTerm :: Value -> Text -> Int -> Chr Bool
matchTerm v functor arity = do
  d <- deref v
  case d of
    VAtom a -> pure (arity == 0 && a == functor)
    VTerm f args -> pure (f == functor && length args == arity)
    _ -> pure False

-- | Extract an argument from a compound term by 0-based index.
-- Dereferences first. Raises an error if the value is not a term
-- or the index is out of bounds.
getArg :: Value -> Int -> Chr Value
getArg v idx = do
  d <- deref v
  case d of
    VTerm _ args
      | idx >= 0 && idx < length args -> pure (args !! idx)
      | otherwise -> error $ "getArg: index " ++ show idx ++ " out of bounds"
    _ -> error "getArg: not a compound term"

-- | Register an observer on every unbound variable reachable from a
-- value. A bare unbound variable is registered directly; a compound
-- term is traversed so that variables nested inside its arguments
-- (e.g. the @X@ in @pair(X, 1)@ or @[X, X]@) are observed too. Without
-- the recursion a constraint stored with such an argument would never
-- be reactivated when the nested variable is later bound, missing an
-- ωr /Reactivate/ step. Anything else (already bound, or a non-variable
-- leaf) is a no-op.
addObserver :: SuspensionId -> Value -> Chr ()
addObserver oid v = do
  d <- deref v
  case d of
    VVar var -> do
      st <- readVarState var
      case st of
        Unbound vid obs -> writeVarState var (Unbound vid (oid : obs))
        Bound {} -> pure ()
    VTerm _ args -> mapM_ (addObserver oid) args
    _ -> pure ()

-- | Extract the 'VarId' of an unbound variable after dereferencing.
-- Returns 'Nothing' if the value is not an unbound variable.
getVarId :: Value -> Chr (Maybe VarId)
getVarId v = do
  d <- deref v
  case d of
    VVar var -> do
      st <- readVarState var
      case st of
        Unbound vid _ -> pure (Just vid)
        Bound {} -> pure Nothing
    _ -> pure Nothing