packages feed

ychr-0.1.0.0: test/YCHR/PrettyTest.hs

{-# LANGUAGE OverloadedStrings #-}

module YCHR.PrettyTest (tests) where

import Data.Map.Strict qualified as Map
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import YCHR.Internal.Pretty (prettyBindings, prettyQueryResult, prettyTerm, renderAtom)
import YCHR.Internal.Types (Name (..), Term (..))

tests :: TestTree
tests =
  testGroup
    "YCHR.Internal.Pretty"
    [ basicTests,
      renderAtomTests,
      listRenderingTests,
      closureUnwrapTests,
      bindingsTests
    ]

basicTests :: TestTree
basicTests =
  testGroup
    "prettyTerm basics"
    [ testCase "IntTerm" $
        prettyTerm (IntTerm 42) @?= "42",
      testCase "AtomTerm" $
        prettyTerm (CompoundTerm (Unqualified "foo") []) @?= "foo",
      testCase "VarTerm renders its name" $
        prettyTerm (VarTerm "X") @?= "X",
      testCase "Wildcard" $
        prettyTerm Wildcard @?= "_",
      testCase "CompoundTerm unqualified" $
        prettyTerm
          ( CompoundTerm
              (Unqualified "f")
              [IntTerm 1, CompoundTerm (Unqualified "a") []]
          )
          @?= "f(1, a)",
      testCase "CompoundTerm qualified" $
        prettyTerm (CompoundTerm (Qualified "m" "f") [IntTerm 1])
          @?= "m:f(1)",
      testCase "nested compound" $
        prettyTerm
          ( CompoundTerm
              (Unqualified "f")
              [CompoundTerm (Unqualified "g") [IntTerm 0]]
          )
          @?= "f(g(0))",
      -- Negative integers render with parentheses so e.g. binding output
      -- like @R = (-2)@ parses back as a single term rather than a
      -- subtraction expression. (Verified by an existing golden test,
      -- but pinned here at the unit level too.)
      testCase "negative integer is parenthesized" $
        prettyTerm (IntTerm (-2)) @?= "(-2)",
      testCase "negative float is parenthesized" $
        prettyTerm (FloatTerm (-1.5)) @?= "(-1.5)",
      testCase "TextTerm renders with surrounding quotes" $
        prettyTerm (TextTerm "hello") @?= "\"hello\"",
      testCase "TextTerm escapes embedded quotes/backslashes/newlines" $
        prettyTerm (TextTerm "a\"b\\c\n") @?= "\"a\\\"b\\\\c\\n\""
    ]

renderAtomTests :: TestTree
renderAtomTests =
  testGroup
    "renderAtom quoting"
    [ testCase "lowercase identifier stays bare" $
        renderAtom "foo" @?= "foo",
      testCase "lowercase with digits and underscore stays bare" $
        renderAtom "foo_bar2" @?= "foo_bar2",
      testCase "empty atom is quoted" $
        renderAtom "" @?= "''",
      testCase "uppercase-first is quoted" $
        renderAtom "Foo" @?= "'Foo'",
      testCase "leading underscore is quoted" $
        renderAtom "_foo" @?= "'_foo'",
      testCase "atom with embedded space is quoted" $
        renderAtom "hello world" @?= "'hello world'",
      testCase "atom with apostrophe is quoted, apostrophe doubled" $
        renderAtom "hello's" @?= "'hello''s'",
      testCase "word operator 'is' is quoted (would otherwise parse as op)" $
        renderAtom "is" @?= "'is'",
      -- The flattened-qualified marker '__' triggers quoting so that
      -- internal names like @prelude__.@ don't accidentally render as
      -- bare atoms that the parser would re-tokenize.
      testCase "atom containing '__' is quoted" $
        renderAtom "foo__bar" @?= "'foo__bar'"
    ]

listRenderingTests :: TestTree
listRenderingTests =
  testGroup
    "list canonicalization"
    [ testCase "canonical nil renders as []" $
        prettyTerm (CompoundTerm (Unqualified "prelude__[]") []) @?= "[]",
      testCase "single-element list" $
        prettyTerm (cons (IntTerm 1) nil) @?= "[1]",
      testCase "proper multi-element list" $
        prettyTerm (cons (IntTerm 1) (cons (IntTerm 2) (cons (IntTerm 3) nil)))
          @?= "[1, 2, 3]",
      testCase "nested lists" $
        prettyTerm
          ( cons
              (IntTerm 1)
              ( cons
                  (cons (IntTerm 2) (cons (IntTerm 3) nil))
                  nil
              )
          )
          @?= "[1, [2, 3]]",
      testCase "improper list with variable tail" $
        prettyTerm (cons (IntTerm 1) (VarTerm "T")) @?= "[1 | T]",
      testCase "improper list with atom tail" $
        prettyTerm (cons (IntTerm 1) (CompoundTerm (Unqualified "foo") [])) @?= "[1 | foo]"
    ]
  where
    nil = CompoundTerm (Unqualified "prelude__[]") []
    cons h t = CompoundTerm (Unqualified "prelude__.") [h, t]

closureUnwrapTests :: TestTree
closureUnwrapTests =
  testGroup
    "closure unwrapping"
    [ -- A closure value is a compound @__closure(name, sourceForm, ...captures)@.
      -- prettyTerm should show the user-visible source form, not the
      -- internal closure functor.
      testCase "closure renders its source form" $
        let source =
              CompoundTerm
                (Unqualified "->")
                [ CompoundTerm (Unqualified "fun") [CompoundTerm (Unqualified "X") []],
                  CompoundTerm (Unqualified "+") [CompoundTerm (Unqualified "X") [], IntTerm 1]
                ]
            closure =
              CompoundTerm
                (Unqualified "__closure")
                [CompoundTerm (Unqualified "__lambda_0") [], source]
         in prettyTerm closure @?= "fun(X) -> X + 1 end",
      -- 'unquoteToPExpr' turns atoms whose name looks like a variable
      -- (uppercase-first or leading underscore) back into variables —
      -- that's how a captured variable reference inside a closure body
      -- gets rendered as a variable rather than a quoted atom.
      testCase "closure body atoms that look like vars render as vars" $
        let source =
              CompoundTerm
                (Unqualified "->")
                [ CompoundTerm (Unqualified "fun") [CompoundTerm (Unqualified "X") []],
                  CompoundTerm (Unqualified "X") []
                ]
            closure =
              CompoundTerm
                (Unqualified "__closure")
                [CompoundTerm (Unqualified "__lambda_1") [], source]
         in prettyTerm closure @?= "fun(X) -> X end"
    ]

bindingsTests :: TestTree
bindingsTests =
  testGroup
    "binding-map formatting"
    [ testCase "prettyBindings sorted with trailing newline" $
        prettyBindings (Map.fromList [("R", IntTerm 55), ("X", Wildcard)])
          @?= "R = 55\nX = _\n",
      testCase "prettyBindings empty" $
        prettyBindings Map.empty @?= "",
      testCase "prettyQueryResult empty map is empty string" $
        prettyQueryResult Map.empty @?= "",
      -- Underscored names are internal/wildcard; filtering them out
      -- can leave the visible set empty even when the raw map is not.
      testCase "prettyQueryResult all-underscore map is empty string" $
        prettyQueryResult (Map.fromList [("_X", IntTerm 1), ("_Y", Wildcard)])
          @?= "",
      testCase "prettyQueryResult single binding ends with dot+newline" $
        prettyQueryResult (Map.fromList [("R", IntTerm 7)])
          @?= "R = 7.\n",
      testCase "prettyQueryResult multi-binding uses comma between, dot at end" $
        prettyQueryResult
          ( Map.fromList
              [ ("X", IntTerm 1),
                ("Y", CompoundTerm (Unqualified "ok") []),
                ("Z", Wildcard)
              ]
          )
          @?= "X = 1,\nY = ok,\nZ = _.\n",
      testCase "prettyQueryResult filters underscored names from a mixed map" $
        prettyQueryResult
          ( Map.fromList
              [ ("R", IntTerm 42),
                ("_internal", CompoundTerm (Unqualified "hidden") [])
              ]
          )
          @?= "R = 42.\n"
    ]