packages feed

phino-0.0.114: test/FunctionsSpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT

module FunctionsSpec where

import AST
import Bytes (numToBts, strToBts)
import Control.Exception (SomeException)
import Control.Monad (forM_)
import Data.List (isInfixOf)
import Data.Map.Strict qualified as Map
import Deps (Term (TeAttribute, TeBindings, TeBytes, TeExpression))
import Functions (buildTerm)
import Logger (logDebug)
import Matcher (MetaValue (MvBindings), Subst (Subst), substEmpty)
import Misc (uniqueBindings')
import Printer (printExpression)
import Test.Hspec (Expectation, Spec, describe, it, shouldBe, shouldThrow)
import Text.Printf (printf)
import Yaml (ExtraArgument (ArgAttribute, ArgBinding, ArgBytes, ArgExpression))

throwsWith :: IO a -> String -> IO ()
throwsWith action needle =
  action `shouldThrow` (\exc -> needle `isInfixOf` show (exc :: SomeException))

-- 'Term' carries no 'Show'/'Eq' instance, so a term coming back from
-- 'buildTerm' is checked by pattern-matching out the constructor expected and
-- comparing the payload, which does have both.
expectExpression :: Term -> Expression -> Expectation
expectExpression (TeExpression got) want = got `shouldBe` want
expectExpression _ _ = fail "expected a TeExpression term"

expectBytes :: Term -> Bytes -> Expectation
expectBytes (TeBytes got) want = got `shouldBe` want
expectBytes _ _ = fail "expected a TeBytes term"

expectAttribute :: Term -> Attribute -> Expectation
expectAttribute (TeAttribute got) want = got `shouldBe` want
expectAttribute _ _ = fail "expected a TeAttribute term"

spec :: Spec
spec = describe "Functions" $ do
  describe "join" $ do
    it "contains only unique bindings after 'join'" $ do
      let first = ("B1", MvBindings [BiVoid AtRho, BiDelta BtEmpty, BiTau (AtLabel "x") ExRoot, BiVoid (AtLabel "a0")])
          second = ("B2", MvBindings [BiTau AtRho ExXi, BiLambda (Function "Func"), BiDelta (BtOne "00"), BiVoid (AtLabel "a1")])
          third = ("B3", MvBindings [BiLambda (Function "Some"), BiTau (AtLabel "y") ExXi, BiTau (AtLabel "x") ExXi, BiVoid (AtLabel "a0")])
          subst = Subst (Map.fromList [first, second, third])
      TeBindings bds <- buildTerm "join" [ArgBinding (BiMeta "B1"), ArgBinding (BiMeta "B2"), ArgBinding (BiMeta "B3")] subst
      bds' <- uniqueBindings' bds
      logDebug (printf "Joined bindings:\n%s" (printExpression (ExFormation bds')))
      length bds' `shouldBe` 9

    it "renames a duplicate tau binding (not rho/delta/lambda) instead of dropping it" $ do
      let first = ("B1", MvBindings [BiTau (AtLabel "x") ExRoot])
          second = ("B2", MvBindings [BiTau (AtLabel "x") ExXi])
          subst = Subst (Map.fromList [first, second])
      TeBindings bds <- buildTerm "join" [ArgBinding (BiMeta "B1"), ArgBinding (BiMeta "B2")] subst
      length bds `shouldBe` 2

    it "joins no bindings when given no arguments" $ do
      TeBindings bds <- buildTerm "join" [] substEmpty
      bds `shouldBe` []

  describe "contextualize" $
    it "replaces xi with the given context" $ do
      term <- buildTerm "contextualize" [ArgExpression ExXi, ArgExpression (ExFormation [BiVoid AtRho])] substEmpty
      expectExpression term (ExFormation [BiVoid AtRho])

  describe "random-tau" $
    it "returns a fresh label attribute" $ do
      term <- buildTerm "random-tau" [] substEmpty
      case term of
        TeAttribute (AtLabel _) -> pure ()
        _ -> fail "expected a label attribute"

  describe "dataize" $ do
    it "extracts bytes from a bytes argument" $ do
      term <- buildTerm "dataize" [ArgBytes (BtOne "00")] substEmpty
      expectBytes term (BtOne "00")
    it "extracts bytes from a data-object expression" $ do
      term <- buildTerm "dataize" [ArgExpression (DataNumber (numToBts 5))] substEmpty
      expectBytes term (numToBts 5)

  describe "size" $
    it "counts the bindings bound to a meta" $ do
      let subst = Subst (Map.singleton "B" (MvBindings [BiVoid AtRho, BiVoid (AtLabel "x")]))
      term <- buildTerm "size" [ArgBinding (BiMeta "B")] subst
      expectExpression term (DataNumber (numToBts 2))

  describe "successful calls" $
    forM_
      successCases
      (\(desc, name, args, check) -> it desc (buildTerm name args substEmpty >>= check))

  describe "calls that fail with a descriptive error" $
    forM_
      failureCases
      (\(desc, name, args, message) -> it desc (buildTerm name args substEmpty `throwsWith` message))
  where
    successCases :: [(String, String, [ExtraArgument], Term -> Expectation)]
    successCases =
      [
        ( "concat concatenates several string arguments"
        , "concat"
        , [ArgExpression (DataString (strToBts "foo")), ArgExpression (DataString (strToBts "bar"))]
        , \term -> expectExpression term (DataString (strToBts "foobar"))
        )
      ,
        ( "sed replaces every occurrence with the 'g' flag"
        , "sed"
        , [ArgExpression (DataString (strToBts "hello")), ArgExpression (DataString (strToBts "s/l/L/g"))]
        , \term -> expectExpression term (DataString (strToBts "heLLo"))
        )
      ,
        ( "sed replaces only the first occurrence without the 'g' flag"
        , "sed"
        , [ArgExpression (DataString (strToBts "hello")), ArgExpression (DataString (strToBts "s/l/L/"))]
        , \term -> expectExpression term (DataString (strToBts "heLlo"))
        )
      ,
        ( "sed applies several patterns in sequence"
        , "sed"
        ,
          [ ArgExpression (DataString (strToBts "hello"))
          , ArgExpression (DataString (strToBts "s/h/H/"))
          , ArgExpression (DataString (strToBts "s/o/O/"))
          ]
        , \term -> expectExpression term (DataString (strToBts "HellO"))
        )
      ,
        ( "random-string returns a literal pattern with no specials unchanged"
        , "random-string"
        , [ArgExpression (DataString (strToBts "static-name"))]
        , \term -> expectExpression term (DataString (strToBts "static-name"))
        )
      ,
        ( "tau parses an attribute out of a string expression"
        , "tau"
        , [ArgExpression (DataString (strToBts "x"))]
        , \term -> expectAttribute term (AtLabel "x")
        )
      ,
        ( "string converts a number expression to a string"
        , "string"
        , [ArgExpression (DataNumber (numToBts 5))]
        , \term -> expectExpression term (DataString (strToBts "5"))
        )
      ,
        ( "string keeps a string expression as is"
        , "string"
        , [ArgExpression (DataString (strToBts "already"))]
        , \term -> expectExpression term (DataString (strToBts "already"))
        )
      ,
        ( "string renders an attribute"
        , "string"
        , [ArgAttribute AtRho]
        , \term -> expectExpression term (DataString (strToBts "ρ"))
        )
      ,
        ( "number parses a number out of a string expression"
        , "number"
        , [ArgExpression (DataString (strToBts "42"))]
        , \term -> expectExpression term (DataNumber (numToBts 42))
        )
      ,
        ( "sum adds several numeric arguments"
        , "sum"
        , [ArgExpression (DataNumber (numToBts 2)), ArgExpression (DataNumber (numToBts 3))]
        , \term -> expectExpression term (DataNumber (numToBts 5))
        )
      ]

    failureCases :: [(String, String, [ExtraArgument], String)]
    failureCases =
      [ ("contextualize fails on the wrong number of arguments", "contextualize", [ArgExpression ExXi], "contextualize() requires exactly 2 arguments")
      , ("random-tau fails when given arguments", "random-tau", [ArgExpression ExRoot], "random-tau() requires exactly 0 arguments")
      , ("dataize fails on a non-data-object expression", "dataize", [ArgExpression ExRoot], "Only data objects and bytes are supported")
      , ("dataize fails on the wrong number of arguments", "dataize", [ArgExpression ExRoot, ArgExpression ExXi], "dataize() requires exactly 1 argument")
      , ("sed fails on fewer than two arguments", "sed", [ArgExpression (DataString (strToBts "hello"))], "sed() requires at least two arguments")
      ,
        ( "sed fails when the pattern does not start with 's/'"
        , "sed"
        , [ArgExpression (DataString (strToBts "hello")), ArgExpression (DataString (strToBts "l/L/"))]
        , "sed pattern must start with s/"
        )
      ,
        ( "sed fails when the pattern has an unknown trailing flag"
        , "sed"
        , [ArgExpression (DataString (strToBts "hello")), ArgExpression (DataString (strToBts "s/l/L/x"))]
        , "sed pattern must be in format s/pat/rep/[g]"
        )
      , ("random-string fails on the wrong number of arguments", "random-string", [], "random-string() requires exactly 1")
      , ("size fails on a non-meta binding argument", "size", [ArgBinding (BiVoid AtRho)], "size() requires exactly 1 meta binding")
      , ("tau fails on the wrong number of arguments", "tau", [], "tau() requires exactly 1 argument")
      ,
        ( "string fails on an expression that is neither a number nor a string"
        , "string"
        , [ArgExpression ExRoot]
        , "only 'Φ.number' or 'Φ.string' are allowed"
        )
      , ("string fails on the wrong number of arguments", "string", [], "string() requires exactly 1 argument")
      , ("number fails on an expression that is not a string", "number", [ArgExpression (DataNumber (numToBts 1))], "expects expression to be 'Φ.string'")
      , ("number fails on the wrong number of arguments", "number", [], "number() requires exactly 1 argument")
      ,
        ( "an unsupported function name fails with a descriptive message"
        , "no-such-function"
        , []
        , "Function no-such-function() is not supported or does not exist"
        )
      ]