packages feed

tilia-0.0.1.0: tests/Tilia/Render/OperatorSpec.hs

{-# LANGUAGE LambdaCase #-}

-- | Regrouping operator chains by precedence.
module Tilia.Render.OperatorSpec (spec) where

import Data.List.NonEmpty (NonEmpty (..))
import Test.Hspec
import Tilia.Fixity (Direction (..), Fixity (..))
import Tilia.Render.Operator

spec :: Spec
spec = do
  describe "flatten" $ do
    it "leaves a leaf alone" $
      flatten split (Leaf 'a') `shouldBe` (Leaf 'a' :| [], [])
    it "reads a chain left to right" $
      flatten split (Apply (Apply (Leaf 'a') '+' (Leaf 'b')) '*' (Leaf 'c'))
        `shouldBe` (Leaf 'a' :| [Leaf 'b', Leaf 'c'], ['+', '*'])
    it "does not go inside a leaf" $
      flatten split (Apply (Leaf 'a') '+' (Opaque (Apply (Leaf 'b') '*' (Leaf 'c'))))
        `shouldBe` (Leaf 'a' :| [Opaque (Apply (Leaf 'b') '*' (Leaf 'c'))], ['+'])

  describe "associate" $ do
    it "makes one level of a chain that binds equally" $
      associate known (leaves "abc") ['+', '+']
        `shouldBe` Chain (Operand (Leaf 'a') :| [Operand (Leaf 'b'), Operand (Leaf 'c')]) ['+', '+']

    it "splits at the loosest operator" $
      associate known (leaves "abc") ['*', '+']
        `shouldBe` Chain
          ( Chain (Operand (Leaf 'a') :| [Operand (Leaf 'b')]) ['*']
              :| [Operand (Leaf 'c')]
          )
          ['+']

    it "puts every level in its place" $
      associate known (leaves "abcd") ['*', '+', '*']
        `shouldBe` Chain
          ( Chain (Operand (Leaf 'a') :| [Operand (Leaf 'b')]) ['*']
              :| [Chain (Operand (Leaf 'c') :| [Operand (Leaf 'd')]) ['*']]
          )
          ['+']

    it "keeps a single operand as one" $
      associate known (Leaf 'a' :| []) []
        `shouldBe` Operand (Leaf 'a')

    -- What the author wrote is the only information left when a fixity
    -- cannot be established, so it is what the layout follows.
    it "leaves the chain flat when one fixity is unknown" $
      associate known (leaves "abc") ['*', '?']
        `shouldBe` Chain (Operand (Leaf 'a') :| [Operand (Leaf 'b'), Operand (Leaf 'c')]) ['*', '?']

  describe "separators" $ do
    it "recognises an operator that introduces its operand" $
      isSeparator (Just (Fixity RightAssoc 0)) `shouldBe` True
    it "does not mistake a tight right-associative operator for one" $
      isSeparator (Just (Fixity RightAssoc 6)) `shouldBe` False
    it "says nothing about an operator it does not know" $
      isSeparator Nothing `shouldBe` False

----------------------------------------------------------------------------
-- A stand-in for the syntax tree

-- | Just enough of an expression to have operators in it.
data E
  = Leaf Char
  | Apply E Char E
  | -- | Something the chain builder must not look inside, standing in for a
    -- parenthesised subexpression.
    Opaque E
  deriving (Eq, Show)

split :: E -> Maybe (E, Char, E)
split = \case
  Apply l o r -> Just (l, o, r)
  _ -> Nothing

leaves :: [Char] -> NonEmpty E
leaves = \case
  [] -> error "leaves: none"
  (c : cs) -> Leaf c :| map Leaf cs

-- | @?@ is the operator nothing is known about.
known :: Char -> Maybe Fixity
known = \case
  '+' -> Just (Fixity LeftAssoc 6)
  '*' -> Just (Fixity LeftAssoc 7)
  _ -> Nothing