packages feed

ychr-0.1.0.0: test/YCHR/MetaTest.hs

{-# LANGUAGE OverloadedStrings #-}

module YCHR.MetaTest (tests) where

import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase)
import YCHR.Internal.Compile.Names (vmName)
import YCHR.Internal.Compile.Pipeline (CompiledProgram (..))
import YCHR.Internal.Meta (metaHostCallRegistry, valueToTerm)
import YCHR.Internal.Runtime.Interpreter
  ( HostCallFn (..),
    HostCallRegistry,
    baseHostCallRegistry,
  )
import YCHR.Internal.Runtime.Monad (Chr, initSessionEnv, runChr)
import YCHR.Internal.Runtime.Types (Value (..))
import YCHR.Internal.Runtime.Var (deref, equal)
import YCHR.Internal.Types (Term (..))
import YCHR.Internal.Types qualified as Types
import YCHR.Internal.VM (Name (..))
import YCHR.Run (compileModules, runProgramWithQuery)

tests :: TestTree
tests =
  testGroup
    "YCHR.Internal.Meta"
    [ readTermTests,
      vmNameRoundTripTests
    ]

hostCalls :: HostCallRegistry
hostCalls = baseHostCallRegistry <> metaHostCallRegistry

runChrBase :: Chr a -> IO a
runChrBase action = do
  env <- initSessionEnv [] [] Map.empty baseHostCallRegistry Map.empty Map.empty Set.empty
  runChr action env

-- | Invoke the read_term_from_string host call directly and return the Value.
readTerm :: Text -> IO Value
readTerm s = case Map.lookup (Name "read_term_from_string") metaHostCallRegistry of
  Nothing -> assertFailure "read_term_from_string not found in registry"
  Just (HostCallFn f) -> runChrBase (f [VText s])

compileOrFail :: [(FilePath, Text)] -> IO CompiledProgram
compileOrFail inputs = case compileModules False inputs of
  Left err -> assertFailure $ show err
  Right (cp, _) -> pure cp

readTermTests :: TestTree
readTermTests =
  testGroup
    "read_term_from_string"
    [ testCase "integer" $ do
        v <- readTerm "42"
        case v of
          VInt 42 -> pure ()
          _ -> assertFailure "expected VInt 42",
      testCase "negative integer" $ do
        v <- readTerm "-7"
        case v of
          VInt (-7) -> pure ()
          _ -> assertFailure "expected VInt (-7)",
      testCase "atom" $ do
        v <- readTerm "hello"
        case v of
          VAtom "hello" -> pure ()
          _ -> assertFailure "expected VAtom hello",
      testCase "quoted atom" $ do
        v <- readTerm "'hello world'"
        case v of
          VAtom "hello world" -> pure ()
          _ -> assertFailure "expected VAtom 'hello world'",
      testCase "string" $ do
        v <- readTerm "\"hello\""
        case v of
          VText "hello" -> pure ()
          _ -> assertFailure "expected VText hello",
      testCase "wildcard" $ do
        v <- readTerm "_"
        case v of
          VWildcard -> pure ()
          _ -> assertFailure "expected VWildcard",
      testCase "compound term" $ do
        v <- readTerm "f(1, hello)"
        case v of
          VTerm "f" [VInt 1, VAtom "hello"] -> pure ()
          _ -> assertFailure "unexpected result for f(1, hello)",
      testCase "nested compound term" $ do
        v <- readTerm "f(g(1), h(2, 3))"
        case v of
          VTerm "f" [VTerm "g" [VInt 1], VTerm "h" [VInt 2, VInt 3]] -> pure ()
          _ -> assertFailure "unexpected result for f(g(1), h(2, 3))",
      testCase "variable produces a fresh unbound var" $ do
        v <- readTerm "X"
        v' <- runChrBase (deref v)
        case v' of
          VVar _ -> pure ()
          _ -> assertFailure "expected unbound variable",
      testCase "same variable name maps to same var" $ do
        v <- readTerm "f(X, X)"
        eq <- runChrBase $ case v of
          VTerm "f" [a, b] -> equal a b
          _ -> pure False
        assertBool "both X args should be the same variable" eq,
      testCase "different variable names map to different vars" $ do
        v <- readTerm "f(X, Y)"
        eq <- runChrBase $ case v of
          VTerm "f" [a, b] -> equal a b
          _ -> pure True
        assertBool "X and Y should be different variables" (not eq),
      testCase "list syntax" $ do
        v <- readTerm "[1, 2, 3]"
        case v of
          VTerm "." [VInt 1, VTerm "." [VInt 2, VTerm "." [VInt 3, VAtom "[]"]]] -> pure ()
          _ -> assertFailure "unexpected result for [1, 2, 3]",
      testCase "infix operator <=> parses as compound term" $ do
        v <- readTerm "a <=> b"
        case v of
          VTerm "<=>" [VAtom "a", VAtom "b"] -> pure ()
          _ -> assertFailure "unexpected result for a <=> b",
      testCase "infix operator = parses as compound term" $ do
        v <- readTerm "a = b"
        case v of
          VTerm "=" [VAtom "a", VAtom "b"] -> pure ()
          _ -> assertFailure "unexpected result for a = b",
      endToEndReadTermTest
    ]

endToEndReadTermTest :: TestTree
endToEndReadTermTest =
  testCase "end-to-end: read_term_from_string in CHR query" $ do
    let src =
          ":- module(m, [check/2]).\n\
          \:- chr_constraint check/2.\n\
          \\n\
          \check(X, X) <=> true.\n"
    prog <- compileOrFail [("m.chr", src)]
    bindings <-
      runProgramWithQuery
        prog
        hostCalls
        "T is host:read_term_from_string(\"f(1, hello)\"), check(T, f(1, hello))."
    case Map.lookup "T" bindings of
      Just
        ( CompoundTerm
            (Types.Unqualified "f")
            [IntTerm 1, CompoundTerm (Types.Unqualified "hello") []]
          ) -> pure ()
      other -> assertFailure $ "Expected T = f(1, hello), got: " ++ show other

-- | Property: 'YCHR.Internal.Meta.valueToTerm' (run on a 'VAtom' whose payload
-- comes from 'YCHR.Internal.Compile.Names.vmName') recovers the original
-- 'Types.Name' as a qualified or unqualified 'CompoundTerm'. This
-- pins the injectivity of the mangling pair @encodeText@\/@%%u@
-- escape ↔ @decodeMangled@\/@decodeEscapes@.
vmNameRoundTripTests :: TestTree
vmNameRoundTripTests =
  testGroup
    "vmName round-trip"
    [ roundTrip "ASCII qualified" (Types.Qualified "mymodule" "foo"),
      roundTrip "non-ASCII base" (Types.Qualified "m" "naïve"),
      roundTrip "non-ASCII module" (Types.Qualified "naïve" "foo"),
      roundTrip "non-ASCII both" (Types.Qualified "café" "naïve"),
      -- Previously broken: base whose encoded form follows a non-ASCII
      -- escape with literal "u<hex>" chars, which the old "__u<HEX>__"
      -- decoder mis-split.
      roundTrip "uffï base" (Types.Qualified "mymodule" "uffï"),
      -- Previously broken: module ending in non-ASCII + literal
      -- "u<hex>". With the old encoding this collided with another
      -- (m, n) pair; the new "%%u<6 hex>" encoding is injective.
      roundTrip "fooáue module" (Types.Qualified "fooáue" "b"),
      -- Base that LOOKS like a stale "__u<HEX>__" escape but is just
      -- ASCII content past the separator.
      roundTrip "uaafoo base" (Types.Qualified "mymodule" "uaafoo"),
      -- 0-arity 'Unqualified' atoms go through 'VAtom' too.
      roundTripUnqualified "ASCII unqualified" "foo",
      roundTripUnqualified "unicode unqualified" "naïve"
    ]
  where
    roundTrip label name = testCase label $ do
      let mangled = (vmName name).unName
      t <- runChrBase (valueToTerm Map.empty (VAtom mangled))
      case t of
        CompoundTerm n [] | n == name -> pure ()
        other ->
          assertFailure $
            "Round-trip failed for "
              ++ show name
              ++ "\n  mangled = "
              ++ show mangled
              ++ "\n  got     = "
              ++ show other
    roundTripUnqualified label n = testCase label $ do
      let mangled = (vmName (Types.Unqualified n)).unName
      t <- runChrBase (valueToTerm Map.empty (VAtom mangled))
      case t of
        CompoundTerm (Types.Unqualified n') [] | n' == n -> pure ()
        other ->
          assertFailure $
            "Round-trip failed for unqualified "
              ++ show n
              ++ "\n  mangled = "
              ++ show mangled
              ++ "\n  got     = "
              ++ show other