packages feed

phino-0.0.0.55: test/LaTeXSpec.hs

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

{- | Tests for the LaTeX module that provides conversion of phi-calculus
programs and rules to LaTeX format for academic documents.
Attention! Most of the tests are generated by LLM. Consider that when refactoring
-}
module LaTeXSpec where

import AST
import Control.Monad (forM_)
import LaTeX (LatexContext (..), defaultLatexContext, explainRules, meetInProgram, programToLaTeX, rewrittensToLatex)
import LaTeX qualified as L
import Lining (LineFormat (MULTILINE, SINGLELINE))
import Parser (parseExpressionThrows, parseProgramThrows)
import Sugar (SugarType (SALTY, SWEET))
import Test.Hspec (Spec, describe, it, shouldBe, shouldContain, shouldNotContain)
import Yaml qualified as Y

spec :: Spec
spec = do
  describe "meet program in program" $
    forM_
      [ ("Q.x.y", "{Q.x.y}", "{[[ x -> Q.x.y ]]}", ["Q.x.y"])
      , ("Q.x.y twice", "{Q.x.y}", "{[[ x -> Q.x.y, y -> Q.x.y.z ]]}", ["Q.x.y", "Q.x.y"])
      , ("Q.x.y.z.a and Q.x.y", "{Q.x.y.z.a}", "{[[ x -> Q.x.y, y -> Q.x.y.z ]]}", ["Q.x.y.z", "Q.x.y", "Q.x.y"])
      , ("Ignore data objects", "{[[ x -> \"foo\" ]]}", "{Q.x( y -> \"foo\" )}", [])
      , ("Not found [[ t -> 42 ]]", "{⟦ ex ↦ ⟦ x ↦ ⟦ t ↦ 42 ⟧.t ⟧.x ⟧}", "{⟦ ex ↦ ⟦ x ↦ 42 ⟧.x ⟧}", [])
      , ("Missed [[ t -> 42 ]]", "{⟦ ex ↦ ⟦ x ↦ ⟦ t ↦ 42 ⟧.t ⟧.x ⟧}", "{⟦ ex ↦ 42 ⟧}", [])
      ]
      ( \(desc, first, second, exprs) -> it desc $ do
          ptn <- parseProgramThrows first
          tgt <- parseProgramThrows second
          res <- traverse parseExpressionThrows exprs
          meetInProgram ptn tgt `shouldBe` res
      )

  describe "programToLaTeX with nonumber=True" $
    it "uses phiquation* environment" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\begin{phiquation*}"

  describe "programToLaTeX with nonumber=False" $
    it "uses phiquation environment" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = False}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\begin{phiquation}"

  describe "programToLaTeX output structure" $
    it "contains begin and end tags" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\end{phiquation*}"

  describe "programToLaTeX with SWEET sugar" $
    it "renders sweet syntax with braces" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\Big\\{"

  describe "programToLaTeX with SALTY sugar" $
    it "renders salty syntax with arrow" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{sugar = SALTY, line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "->"

  describe "programToLaTeX with MULTILINE format" $
    it "renders multiline format" $ do
      prog <- parseProgramThrows "{[[ x -> Q ]]}"
      let ctx = defaultLatexContext{line = MULTILINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "|x|"

  describe "rewrittensToLatex with empty list" $
    it "generates valid latex structure" $ do
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [] ctx
      result `shouldContain` "\\begin{phiquation*}"

  describe "rewrittensToLatex with single program" $
    it "renders single program without rule" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldContain` "\\Big\\{"

  describe "rewrittensToLatex with rule name" $
    it "includes nameref for rule" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog, Just "myrule")] ctx
      result `shouldContain` "\\nameref{r:myrule}"

  describe "rewrittensToLatex with label" $
    it "includes label command" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True, label = Just "myeq"}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldContain` "\\label{myeq}"

  describe "rewrittensToLatex without label" $
    it "excludes label command" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldNotContain` "\\label"

  describe "rewrittensToLatex with expression" $
    it "includes phiExpression command" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True, expression = Just "myexpr"}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldContain` "\\phiExpression{myexpr}"

  describe "rewrittensToLatex without expression" $
    it "excludes phiExpression command" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldNotContain` "\\phiExpression"

  describe "rewrittensToLatex with multiple programs" $
    it "joins with leadsto" $ do
      prog1 <- parseProgramThrows "{Q.x}"
      prog2 <- parseProgramThrows "{Q.y}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog1, Nothing), (prog2, Nothing)] ctx
      result `shouldContain` "\\leadsto"

  describe "rewrittensToLatex ends with period" $
    it "adds period before end tag" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldContain` ".\n\\end"

  describe "rewrittensToLatex with nonumber=False" $
    it "uses phiquation without asterisk" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = False}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldContain` "\\begin{phiquation}"

  describe "escapeUnprintedChars escapes dollar" $
    it "replaces $ with char36 in label via AST" $ do
      let prog = Program (ExFormation [BiTau (AtLabel "$my") ExGlobal])
          ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\char36{}"

  describe "escapeUnprintedChars escapes at sign" $
    it "replaces @ with char64 in label via AST" $ do
      let prog = Program (ExFormation [BiTau (AtLabel "@my") ExGlobal])
          ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\char64{}"

  describe "escapeUnprintedChars escapes caret" $
    it "replaces ^ with char94 in label via AST" $ do
      let prog = Program (ExFormation [BiTau (AtLabel "^my") ExGlobal])
          ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\char94{}"

  describe "escapeUnprintedChars escapes underscore" $
    it "replaces _ in label with char95" $ do
      prog <- parseProgramThrows "{Q.my_label}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "\\char95{}"

  describe "escapeUnprintedChars preserves regular chars" $
    it "keeps alphanumeric chars unchanged" $ do
      prog <- parseProgramThrows "{Q.abc123}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "abc123"

  describe "explainRules with empty list" $
    it "generates document structure" $ do
      let result = explainRules []
      result `shouldContain` "\\documentclass{article}"

  describe "explainRules document has amsmath" $
    it "includes amsmath package" $ do
      let result = explainRules []
      result `shouldContain` "\\usepackage{amsmath}"

  describe "explainRules document structure" $
    it "has begin and end document" $ do
      let result = explainRules []
      result `shouldContain` "\\begin{document}"

  describe "explainRules document ends correctly" $
    it "has end document tag" $ do
      let result = explainRules []
      result `shouldContain` "\\end{document}"

  describe "explainRules with named rule" $
    it "includes rule name" $ do
      let rule = Y.Rule (Just "DOT") Nothing ExGlobal ExGlobal Nothing Nothing Nothing
          result = explainRules [rule]
      result `shouldContain` "\\rule{DOT}"

  describe "explainRules with unnamed rule" $
    it "uses unnamed as fallback" $ do
      let rule = Y.Rule Nothing Nothing ExGlobal ExGlobal Nothing Nothing Nothing
          result = explainRules [rule]
      result `shouldContain` "\\rule{unnamed}"

  describe "explainRules with multiple rules" $
    it "includes all rule names" $ do
      let rule1 = Y.Rule (Just "RULE1") Nothing ExGlobal ExGlobal Nothing Nothing Nothing
          rule2 = Y.Rule (Just "RULE2") Nothing ExGlobal ExGlobal Nothing Nothing Nothing
          result = explainRules [rule1, rule2]
      result `shouldContain` "\\rule{RULE1}"

  describe "explainRules with multiple rules second" $
    it "includes second rule name" $ do
      let rule1 = Y.Rule (Just "FIRST") Nothing ExGlobal ExGlobal Nothing Nothing Nothing
          rule2 = Y.Rule (Just "SECOND") Nothing ExGlobal ExGlobal Nothing Nothing Nothing
          result = explainRules [rule1, rule2]
      result `shouldContain` "\\rule{SECOND}"

  describe "LatexContext sugar field" $
    it "stores sugar type correctly" $ do
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = False}
      sugar ctx `shouldBe` SWEET

  describe "LatexContext line field" $
    it "stores line format correctly" $ do
      let ctx = defaultLatexContext{line = MULTILINE, nonumber = True}
      line ctx `shouldBe` MULTILINE

  describe "LatexContext nonumber field" $
    it "stores nonumber flag correctly" $ do
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = False}
      nonumber ctx `shouldBe` False

  describe "LatexContext expression field" $
    it "stores expression correctly" $ do
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True, expression = Just "expr"}
      L.expression ctx `shouldBe` Just "expr"

  describe "LatexContext label field" $
    it "stores label correctly" $ do
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True, label = Just "lbl"}
      L.label ctx `shouldBe` Just "lbl"

  describe "programToLaTeX handles formation" $
    it "renders formation with bindings" $ do
      prog <- parseProgramThrows "{[[ x -> Q, y -> $ ]]}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "|x|"

  describe "programToLaTeX handles dispatch" $
    it "renders dispatch expression" $ do
      prog <- parseProgramThrows "{Q.x.y.z}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "|z|"

  describe "programToLaTeX handles application" $
    it "renders application expression" $ do
      prog <- parseProgramThrows "{Q.f(x -> Q)}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "|f|"

  describe "programToLaTeX handles void binding" $
    it "renders void in formation" $ do
      prog <- parseProgramThrows "{[[ x -> ? ]]}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "|x|"

  describe "programToLaTeX handles nested formations" $
    it "renders nested structures" $ do
      prog <- parseProgramThrows "{[[ x -> [[ y -> Q ]] ]]}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = programToLaTeX prog ctx
      result `shouldContain` "|y|"

  describe "rewrittensToLatex with label and expression" $
    it "includes both label and expression" $ do
      prog <- parseProgramThrows "{Q}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True, expression = Just "expr", label = Just "lbl"}
          result = rewrittensToLatex [(prog, Nothing)] ctx
      result `shouldContain` "\\label{lbl}"

  describe "rewrittensToLatex with complex rewrite chain" $
    it "shows rewrite sequence" $ do
      prog1 <- parseProgramThrows "{Q.x}"
      prog2 <- parseProgramThrows "{Q.y}"
      prog3 <- parseProgramThrows "{Q.z}"
      let ctx = defaultLatexContext{line = SINGLELINE, nonumber = True}
          result = rewrittensToLatex [(prog1, Just "r1"), (prog2, Just "r2"), (prog3, Nothing)] ctx
      result `shouldContain` "\\nameref{r:r1}"