packages feed

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

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Host call registry for the CHR runtime.
--
-- Provides a base registry of host language functions (arithmetic,
-- comparisons, string operations, type predicates) and generic helpers
-- for building custom host calls.
module YCHR.Internal.Runtime.Registry
  ( -- * Types (re-exported from "YCHR.Internal.Runtime.Monad")
    HostCallFn (..),
    HostCallRegistry,

    -- * Registry
    baseHostCallRegistry,

    -- * Utilities
    unit,

    -- * Value predicates
    isInteger,
    isFloat,
    isAtom,
    isBoolean,
    isString,
    isVar,
    isNonvar,

    -- * Generic helpers
    allM,
    collectVars,
    copyTerm,
    fromValueList,
    valueList,
  )
where

import Control.Monad.IO.Class (liftIO)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as T
import YCHR.Internal.Runtime.Error (runtimeErrorS)
import YCHR.Internal.Runtime.Monad (Chr, HostCallFn (..), HostCallRegistry)
import YCHR.Internal.Runtime.Types (Value (..), VarId)
import YCHR.Internal.Runtime.Var (deref, equal, getVarId, newVar, unifiable)
import YCHR.Internal.VM (Name (..))

-- ---------------------------------------------------------------------------
-- Registry
-- ---------------------------------------------------------------------------

-- | A base host call registry providing arithmetic, comparison, string,
-- and type predicate operations.
baseHostCallRegistry :: HostCallRegistry
baseHostCallRegistry =
  Map.fromList
    [ (Name "+", numArith2 (+) (+)),
      (Name "-", numArith2 (-) (-)),
      (Name "*", numArith2 (*) (*)),
      (Name "div", intDivOp2 "div" div),
      (Name "mod", intDivOp2 "mod" mod),
      (Name "rem", intDivOp2 "rem" rem),
      (Name "/", floatArith2 (/)),
      (Name "<", numCmp (<) (<)),
      (Name ">", numCmp (>) (>)),
      (Name "=<", numCmp (<=) (<=)),
      (Name ">=", numCmp (>=) (>=)),
      (Name "==", valEq),
      (Name "not", notBool),
      (Name "float", typePred isFloat),
      (Name "int_to_float", toFloatFn),
      (Name "float_to_int", toIntFn),
      (Name "unifiable", unifiableHost),
      (Name "string_concat", stringConcat),
      (Name "string_length", stringLength),
      (Name "string_upper", stringUpper),
      (Name "string_lower", stringLower),
      (Name "__chr_error", chrError),
      (Name "write", writeStr),
      (Name "writeln", writeStrLn),
      (Name "integer", typePred isInteger),
      (Name "atom", typePred isAtom),
      (Name "boolean", typePred isBoolean),
      (Name "string", typePred isString),
      (Name "var", typePred isVar),
      (Name "nonvar", typePred isNonvar),
      (Name "ground", groundPred),
      (Name "term_variables", termVariablesPred),
      (Name "compound_to_list", compoundToList),
      (Name "list_to_compound", listToCompound),
      (Name "copy_term", copyTermHost)
    ]
  where
    numArith2 intOp floatOp = HostCallFn $ \case
      [VInt a, VInt b] -> pure (VInt (intOp a b))
      [VFloat a, VFloat b] -> pure (VFloat (floatOp a b))
      args ->
        runtimeErrorS $
          "arithmetic host call: expected 2 numeric arguments of same type, got "
            ++ show (length args)
    intDivOp2 opName op = HostCallFn $ \case
      [VInt _, VInt 0] ->
        runtimeErrorS $ "integer " ++ opName ++ ": division by zero"
      [VInt a, VInt b] -> pure (VInt (op a b))
      args ->
        runtimeErrorS $
          "integer arithmetic host call: expected 2 Int arguments, got "
            ++ show (length args)
    floatArith2 op = HostCallFn $ \case
      [VFloat a, VFloat b] -> pure (VFloat (op a b))
      args ->
        runtimeErrorS $
          "float arithmetic host call: expected 2 Float arguments, got "
            ++ show (length args)
    numCmp intOp floatOp = HostCallFn $ \case
      [VInt a, VInt b] -> pure (VBool (intOp a b))
      [VFloat a, VFloat b] -> pure (VBool (floatOp a b))
      args ->
        runtimeErrorS $
          "comparison host call: expected 2 numeric arguments of same type, got "
            ++ show (length args)
    toFloatFn = HostCallFn $ \case
      [VInt n] -> pure (VFloat (fromIntegral n))
      [VFloat n] -> pure (VFloat n)
      _ -> runtimeErrorS "int_to_float: expected 1 numeric argument"
    toIntFn = HostCallFn $ \case
      [VFloat n] -> pure (VInt (truncate n))
      [VInt n] -> pure (VInt n)
      _ -> runtimeErrorS "float_to_int: expected 1 numeric argument"
    unifiableHost = HostCallFn $ \case
      [a, b] -> VBool <$> unifiable a b
      args ->
        runtimeErrorS $
          "unifiable host call: expected 2 arguments, got " ++ show (length args)
    valEq = HostCallFn $ \case
      [a, b] -> VBool <$> equal a b
      args ->
        runtimeErrorS $
          "== host call: expected 2 arguments, got " ++ show (length args)
    notBool = HostCallFn $ \case
      [v] -> do
        v' <- deref v
        case v' of
          VBool b -> pure (VBool (not b))
          _ -> runtimeErrorS "not: expected a boolean argument"
      args ->
        runtimeErrorS $
          "not host call: expected 1 argument, got " ++ show (length args)
    stringConcat = HostCallFn $ \case
      [VText a, VText b] -> pure (VText (a <> b))
      _ -> runtimeErrorS "string_concat: expected 2 Text arguments"
    stringLength = HostCallFn $ \case
      [VText s] -> pure (VInt (fromIntegral (T.length s)))
      _ -> runtimeErrorS "string_length: expected 1 Text argument"
    stringUpper = HostCallFn $ \case
      [VText s] -> pure (VText (T.toUpper s))
      _ -> runtimeErrorS "string_upper: expected 1 Text argument"
    stringLower = HostCallFn $ \case
      [VText s] -> pure (VText (T.toLower s))
      _ -> runtimeErrorS "string_lower: expected 1 Text argument"
    chrError = HostCallFn $ \_ -> runtimeErrorS "CHR runtime error: no matching equation"
    writeStr = HostCallFn $ \case
      [VText s] -> unit <$ liftIO (putStr (T.unpack s))
      _ -> runtimeErrorS "write: expected 1 Text argument"
    writeStrLn = HostCallFn $ \case
      [VText s] -> unit <$ liftIO (putStrLn (T.unpack s))
      _ -> runtimeErrorS "writeln: expected 1 Text argument"
    typePred p = HostCallFn $ \case
      [v] -> do
        v' <- deref v
        pure (VBool (p v'))
      _ -> runtimeErrorS "type predicate: expected 1 argument"
    groundPred = HostCallFn $ \case
      [v] -> VBool <$> isGround v
      _ -> runtimeErrorS "ground: expected 1 argument"
    isGround v = do
      v' <- deref v
      case v' of
        VVar _ -> pure False
        VWildcard -> pure False
        VTerm _ args -> allM isGround args
        _ -> pure True
    termVariablesPred = HostCallFn $ \case
      [v] -> do
        (vars, _) <- collectVars Set.empty v
        pure (valueList vars)
      _ -> runtimeErrorS "term_variables: expected 1 argument"
    compoundToList = HostCallFn $ \case
      [VTerm f args] -> pure (valueList (VAtom f : args))
      [v@(VAtom _)] -> pure (valueList [v])
      _ -> runtimeErrorS "compound_to_list: expected 1 compound or atom argument"
    listToCompound = HostCallFn $ \case
      [list] -> case fromValueList list of
        Just [VAtom f] -> pure (VAtom f)
        Just (VAtom f : args) -> pure (VTerm f args)
        _ -> runtimeErrorS "list_to_compound: expected a non-empty list with an atom head"
      _ -> runtimeErrorS "list_to_compound: expected 1 list argument"
    copyTermHost = HostCallFn $ \case
      [v] -> copyTerm v
      _ -> runtimeErrorS "copy_term: expected 1 argument"

-- ---------------------------------------------------------------------------
-- Utilities
-- ---------------------------------------------------------------------------

-- | The unit return value for host calls that are only used for side effects.
unit :: Value
unit = VAtom "()"

-- ---------------------------------------------------------------------------
-- Value predicates
-- ---------------------------------------------------------------------------

isInteger :: Value -> Bool
isInteger (VInt _) = True
isInteger _ = False

isFloat :: Value -> Bool
isFloat (VFloat _) = True
isFloat _ = False

isAtom :: Value -> Bool
isAtom (VAtom _) = True
isAtom _ = False

isBoolean :: Value -> Bool
isBoolean (VBool _) = True
isBoolean _ = False

isString :: Value -> Bool
isString (VText _) = True
isString _ = False

isVar :: Value -> Bool
isVar (VVar _) = True
isVar VWildcard = True
isVar _ = False

isNonvar :: Value -> Bool
isNonvar = not . isVar

-- ---------------------------------------------------------------------------
-- Generic helpers
-- ---------------------------------------------------------------------------

-- | Deep-copy a term, replacing all unbound variables with fresh ones.
-- Preserves sharing: the same original variable always maps to the same
-- fresh variable across the entire copied term.
copyTerm :: Value -> Chr Value
copyTerm val = fst <$> go Map.empty val
  where
    go cache v = do
      v' <- deref v
      case v' of
        VVar _ -> do
          mid <- getVarId v'
          case mid of
            Just vid -> case Map.lookup vid cache of
              Just fresh -> pure (fresh, cache)
              Nothing -> do
                fresh <- newVar
                pure (fresh, Map.insert vid fresh cache)
            Nothing -> pure (v', cache)
        VWildcard -> do
          fresh <- newVar
          pure (fresh, cache)
        VTerm f args -> do
          (args', cache') <- goMany cache args
          pure (VTerm f args', cache')
        other -> pure (other, cache)

    goMany cache [] = pure ([], cache)
    goMany cache (x : xs) = do
      (x', cache') <- go cache x
      (xs', cache'') <- goMany cache' xs
      pure (x' : xs', cache'')

-- | Collect all unique unbound variables in a term, traversing into
-- compound term arguments. Wildcards are replaced with fresh variables.
-- Returns the collected variables and the updated set of seen 'VarId's.
collectVars ::
  Set.Set VarId ->
  Value ->
  Chr ([Value], Set.Set VarId)
collectVars seen v = do
  v' <- deref v
  case v' of
    VVar _ -> do
      mid <- getVarId v'
      case mid of
        Just vid
          | Set.member vid seen -> pure ([], seen)
          | otherwise -> pure ([v'], Set.insert vid seen)
        Nothing -> pure ([], seen)
    VWildcard -> do
      fresh <- newVar
      pure ([fresh], seen)
    VTerm _ args -> collectVarsMany seen args
    _ -> pure ([], seen)
  where
    collectVarsMany s [] = pure ([], s)
    collectVarsMany s (x : xs) = do
      (vars, s') <- collectVars s x
      (rest, s'') <- collectVarsMany s' xs
      pure (vars ++ rest, s'')

-- | Monadic version of 'all'. Short-circuits on the first 'False'.
allM :: (Monad m) => (a -> m Bool) -> [a] -> m Bool
allM _ [] = pure True
allM p (x : xs) = do
  b <- p x
  if b then allM p xs else pure False

-- | Build a Prolog-style list (@[H|T]@) from a Haskell list of values.
--
-- The empty list is represented as the atom @[]@, and cons cells as
-- @.(H, T)@ compound terms.
valueList :: [Value] -> Value
valueList [] = VAtom "prelude__[]"
valueList (x : xs) = VTerm "prelude__." [x, valueList xs]

-- | Decompose a Prolog-style list back into a Haskell list. Recognizes
-- both the canonicalized cons form (@prelude__.@/@prelude__[]@,
-- emitted by the renamer-driven pipeline) and the legacy bare form
-- (@.@/@[]@, used by Haskell-side code that constructs values without
-- going through the renamer — e.g. test fixtures, the DSL).
-- Returns 'Nothing' if the value is not a well-formed list.
fromValueList :: Value -> Maybe [Value]
fromValueList (VAtom "prelude__[]") = Just []
fromValueList (VAtom "[]") = Just []
fromValueList (VTerm "prelude__." [x, rest]) = (x :) <$> fromValueList rest
fromValueList (VTerm "." [x, rest]) = (x :) <$> fromValueList rest
fromValueList _ = Nothing