phino-0.0.77: src/LaTeX.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT
module LaTeX
( explainRules
, explainMorphRules
, explainDataizeRules
, rewrittensToLatex
, programToLaTeX
, expressionToLaTeX
, defaultLatexContext
, defaultMeetLength
, defaultMeetPopularity
, LatexContext (..)
, meetInPrograms
, meetInProgram
) where
import AST
import CST
import Data.List (intercalate, nub)
import Data.Maybe (isJust)
import qualified Data.Text as T
import Encoding
import Lining
import Locator (locatedExpression)
import Margin (WithMargin, defaultMargin, withMargin)
import Matcher
import Misc
import Render (Render (render))
import Replacer (replaceProgram)
import Rewriter (Rewritten, Rewrittens')
import Sugar (SugarType (SWEET), ToSalty, withSugarType)
import Text.Printf (printf)
import Text.Read (readMaybe)
import qualified Yaml as Y
data LatexContext = LatexContext
{ _sugar :: SugarType
, _line :: LineFormat
, _margin :: Int
, _nonumber :: Bool
, _compress :: Bool
, _meetPopularity :: Int
, _meetLength :: Int
, _focus :: Expression
, _expression :: Maybe String
, _label :: Maybe String
, _meetPrefix :: Maybe String
}
defaultLatexContext :: LatexContext
defaultLatexContext = LatexContext SWEET SINGLELINE defaultMargin False False defaultMeetPopularity defaultMeetLength ExGlobal Nothing Nothing Nothing
defaultMeetPopularity :: Int
defaultMeetPopularity = 50
defaultMeetLength :: Int
defaultMeetLength = 8
meetInProgram :: Program -> Int -> Program -> [Expression]
meetInProgram (Program expr) len = meetInExpression expr
where
meetInExpression :: Expression -> Program -> [Expression]
meetInExpression (DataString _) _ = []
meetInExpression (DataNumber _) _ = []
meetInExpression (ExPhiMeet{}) _ = []
meetInExpression (ExPhiAgain{}) _ = []
meetInExpression ex prog =
let matched = if countNodes ex >= len then map (const ex) (matchProgram ex prog) else []
in matched ++ case ex of
ExDispatch ex' _ -> meetInExpression ex' prog
ExApplication ex' (BiTau _ arg) -> meetInExpression ex' prog ++ meetInExpression arg prog
ExFormation bds -> meetInBindings bds prog
_ -> []
meetInBindings :: [Binding] -> Program -> [Expression]
meetInBindings [] _ = []
meetInBindings (BiTau _ ex : bds) prog = meetInExpression ex prog ++ meetInBindings bds prog
meetInBindings (_ : bds) prog = meetInBindings bds prog
{- | Here we're trying to compress sequence of programs with \phiMeet{} and \phiAgain LaTeX functions.
We process the sequence of programs and trying to find all expressions in first program which are present
in following programs. Then we find ONE expression which is the most frequently encountered.
If it's encountered in more than specific percentage (_meetPopularity) of following programs - we replace
it with \phiAgain{} in following programs and with \phiMeet{} in first program.
-}
meetInPrograms :: [Program] -> LatexContext -> [Program]
meetInPrograms prog LatexContext{..} = meetInPrograms' prog 1
where
meetInPrograms' :: [Program] -> Int -> [Program]
meetInPrograms' [] _ = []
meetInPrograms' [program] _ = [program]
meetInPrograms' (first : rest) idx =
let met = map (meetInProgram first _meetLength) rest
unique = nub (concat met)
(frequent, _) =
foldl
( \(best, count) cur ->
let len = length (filter (elem cur) met)
in if len > count
then (Just cur, len)
else (best, count)
)
(Nothing, 0)
unique
next = first : meetInPrograms' rest idx
in case frequent of
Just expr ->
case matchProgram expr first of
(_ : substs) ->
let met' = map (filter (== expr)) met
program = replaceProgram (first, [expr], [ExPhiMeet _meetPrefix idx])
program' = replaceProgram (program, map (const expr) substs, map (const (ExPhiAgain _meetPrefix idx)) substs)
rest' = zipWith (\prgm exprs -> replaceProgram (prgm, exprs, map (const (ExPhiAgain _meetPrefix idx)) exprs)) rest met'
found = filter (not . null) met'
in if length met' > 1 && toDouble (length found) / toDouble (length met') >= popularity
then program' : meetInPrograms' rest' (idx + 1)
else next
[] -> next
_ -> next
popularity :: Double
popularity = toDouble _meetPopularity / 100.0
renderToLatex :: (ToSalty a, ToASCII a, ToSingleLine a, ToLaTeX a, WithMargin a, Render a) => a -> LatexContext -> String
renderToLatex renderable LatexContext{..} = T.unpack $ render (toLaTeX $ withLineFormat _line $ withMargin _margin $ withEncoding ASCII $ withSugarType _sugar renderable)
phiquation :: LatexContext -> String
phiquation LatexContext{_nonumber = True} = "phiquation*"
phiquation LatexContext{_nonumber = False} = "phiquation"
preamble :: LatexContext -> String
preamble ctx@LatexContext{..} =
concat
[ printf "\\begin{%s}\n" (phiquation ctx)
, maybe "" (printf "\\label{%s}\n") _label
, maybe "" (printf "\\phiExpression{%s} ") _expression
]
body :: [(a, Maybe String)] -> (a -> String) -> String
body printed toLatex =
intercalate
"\n \\leadsto "
( map
( \(item, maybeName) ->
let item' = toLatex item
in maybe item' (printf "%s \\leadsto_{\\nameref{r:%s}}" item') maybeName
)
printed
)
ending :: Bool -> LatexContext -> String
ending True ctx = printf " \\leadsto\n \\leadsto \\dots\n\\end{%s}" (phiquation ctx)
ending False ctx = printf "{.}\n\\end{%s}" (phiquation ctx)
compressedRewrittens :: [Rewritten] -> LatexContext -> [Rewritten]
compressedRewrittens rewrittens ctx@LatexContext{..} =
let (progs, rules) = unzip rewrittens
in if _compress then zip (meetInPrograms progs ctx) rules else rewrittens
rewrittensToLatex :: Rewrittens' -> LatexContext -> IO String
rewrittensToLatex (rewrittens, exceeded) ctx@LatexContext{_focus = ExGlobal} =
pure
( concat
[ preamble ctx
, body (compressedRewrittens rewrittens ctx) (\prog -> renderToLatex (programToCST prog) ctx)
, ending exceeded ctx
]
)
rewrittensToLatex (rewrittens, exceeded) ctx@LatexContext{..} = do
let (progs, rules) = unzip (compressedRewrittens rewrittens ctx)
exprs <- mapM (locatedExpression _focus) progs
pure
( concat
[ preamble ctx
, body (zip exprs rules) (\expr -> renderToLatex (expressionToCST expr) ctx)
, ending exceeded ctx
]
)
programToLaTeX :: Program -> LatexContext -> String
programToLaTeX prog ctx =
concat
[ preamble ctx
, renderToLatex (programToCST prog) ctx
, ending False ctx
]
expressionToLaTeX :: Expression -> LatexContext -> String
expressionToLaTeX ex ctx =
concat
[ preamble ctx
, renderToLatex (expressionToCST ex) ctx
, ending False ctx
]
piped :: T.Text -> T.Text
piped str = "|" <> toLaTeX str <> "|"
class ToLaTeX a where
toLaTeX :: a -> a
instance ToLaTeX PROGRAM where
toLaTeX PR_SWEET{..} = PR_SWEET BIG_LCB (toLaTeX expr) BIG_RCB SPACE
toLaTeX PR_SALTY{..} = PR_SALTY global arrow (toLaTeX expr)
instance ToLaTeX EXPRESSION where
toLaTeX EX_ATTR{..} = EX_ATTR (toLaTeX attr)
toLaTeX EX_FORMATION{..} = EX_FORMATION lsb eol tab (toLaTeX binding) eol' tab' rsb
toLaTeX EX_APPLICATION{..} = EX_APPLICATION (toLaTeX expr) SPACE eol tab (toLaTeX tau) eol' tab' indent
toLaTeX EX_APPLICATION_TAUS{..} = EX_APPLICATION_TAUS (toLaTeX expr) SPACE eol tab (toLaTeX taus) eol' tab' indent
toLaTeX EX_APPLICATION_EXPRS{..} = EX_APPLICATION_EXPRS (toLaTeX expr) SPACE eol tab (toLaTeX args) eol' tab' indent
toLaTeX EX_DISPATCH{..} = EX_DISPATCH (toLaTeX expr) SPACE (toLaTeX attr)
toLaTeX EX_PHI_MEET{..} = EX_PHI_MEET prefix idx (toLaTeX expr)
toLaTeX EX_PHI_AGAIN{..} = EX_PHI_AGAIN prefix idx (toLaTeX expr)
toLaTeX EX_META{..} = EX_META (toLaTeX meta)
toLaTeX EX_META_TAIL{..} = EX_META_TAIL (toLaTeX expr) (toLaTeX meta)
toLaTeX expr = expr
instance ToLaTeX ATTRIBUTE where
toLaTeX AT_LABEL{..} = AT_LABEL (piped label)
toLaTeX AT_META{..} = AT_META (toLaTeX meta)
toLaTeX AT_LAMBDA{} = AT_LAMBDA LAMBDA'
toLaTeX AT_DELTA{} = AT_DELTA DELTA'
toLaTeX AT_REST{} = AT_REST DOTS'
toLaTeX attr = attr
instance ToLaTeX APP_BINDING where
toLaTeX APP_BINDING{..} = APP_BINDING (toLaTeX pair)
instance ToLaTeX BINDING where
toLaTeX BI_PAIR{..} = BI_PAIR (toLaTeX pair) (toLaTeX bindings) tab
toLaTeX BI_META{..} = BI_META (toLaTeX meta) (toLaTeX bindings) tab
toLaTeX bd = bd
instance ToLaTeX BINDINGS where
toLaTeX BDS_PAIR{..} = BDS_PAIR eol tab (toLaTeX pair) (toLaTeX bindings)
toLaTeX BDS_META{..} = BDS_META eol tab (toLaTeX meta) (toLaTeX bindings)
toLaTeX bds = bds
instance ToLaTeX PAIR where
toLaTeX PA_DELTA{..} = toLaTeX (PA_DELTA' bytes)
toLaTeX PA_DELTA'{..} = PA_DELTA' (toLaTeX bytes)
toLaTeX PA_LAMBDA{..} = PA_LAMBDA' (piped func)
toLaTeX PA_LAMBDA'{..} = PA_LAMBDA' (piped func)
toLaTeX PA_VOID{..} = PA_VOID (toLaTeX attr) arrow void
toLaTeX PA_TAU{..} = PA_TAU (toLaTeX attr) arrow (toLaTeX expr)
toLaTeX PA_FORMATION{..} = PA_FORMATION (toLaTeX attr) (map toLaTeX voids) arrow (toLaTeX expr)
toLaTeX PA_META_DELTA{..} = toLaTeX (PA_META_DELTA' meta)
toLaTeX PA_META_DELTA'{..} = PA_META_DELTA' (toLaTeX meta)
toLaTeX PA_META_LAMBDA{..} = toLaTeX (PA_META_LAMBDA' meta)
toLaTeX PA_META_LAMBDA'{..} = PA_META_LAMBDA' (toLaTeX meta)
instance ToLaTeX META where
toLaTeX META{..} =
let idx = readMaybe (T.unpack rest) :: Maybe Int
rest' = if not (T.null rest) && T.length rest <= 2 && isJust idx then T.cons '_' rest else rest
in META NO_EXCL (toLaTeX hd) rest'
instance ToLaTeX META_HEAD where
toLaTeX E = E'
toLaTeX N = N'
toLaTeX A = TAU'
toLaTeX TAU = TAU'
toLaTeX B = B'
toLaTeX D = D'
toLaTeX mh = mh
instance ToLaTeX BYTES where
toLaTeX (BT_META meta) = BT_META (toLaTeX meta)
toLaTeX bts = bts
instance ToLaTeX APP_ARG where
toLaTeX APP_ARG{..} = APP_ARG (toLaTeX expr) (toLaTeX args)
instance ToLaTeX APP_ARGS where
toLaTeX AAS_EXPR{..} = AAS_EXPR eol tab (toLaTeX expr) (toLaTeX args)
toLaTeX args = args
instance ToLaTeX T.Text where
toLaTeX = T.concatMap escape
where
escape '$' = "\\char36{}"
escape '@' = "\\char64{}"
escape '^' = "\\char94{}"
escape '_' = "\\char95{}"
escape ch = T.singleton ch
instance ToLaTeX SET where
toLaTeX ST_BINDING{..} = ST_BINDING (toLaTeX binding)
toLaTeX ST_ATTRIBUTES{..} = ST_ATTRIBUTES (map toLaTeX attrs)
instance ToLaTeX NUMBER where
toLaTeX INDEX{..} = INDEX (toLaTeX attr)
toLaTeX LENGTH{..} = LENGTH (toLaTeX binding)
toLaTeX DOMAIN{..} = DOMAIN (toLaTeX binding)
toLaTeX literal@LITERAL{} = literal
instance ToLaTeX COMPARABLE where
toLaTeX CMP_EXPR{..} = CMP_EXPR (toLaTeX expr)
toLaTeX CMP_ATTR{..} = CMP_ATTR (toLaTeX attr)
toLaTeX CMP_NUM{..} = CMP_NUM (toLaTeX num)
instance ToLaTeX CONDITION where
toLaTeX CO_BELONGS{..} = CO_BELONGS (toLaTeX attr) belongs (toLaTeX set)
toLaTeX CO_LOGIC{..} = CO_LOGIC (map toLaTeX conditions) operator
toLaTeX CO_NF{..} = CO_NF (toLaTeX expr)
toLaTeX CO_XIFREE{..} = CO_XIFREE (toLaTeX expr)
toLaTeX CO_NOT{..} = CO_NOT (toLaTeX condition)
toLaTeX CO_COMPARE{..} = CO_COMPARE (toLaTeX left) equal (toLaTeX right)
toLaTeX CO_MATCHES{..} = CO_MATCHES regex (toLaTeX expr)
toLaTeX CO_PART_OF{..} = CO_PART_OF (toLaTeX expr) (toLaTeX binding)
toLaTeX CO_PRIMITIVE{..} = CO_PRIMITIVE (toLaTeX expr) belongs
toLaTeX CO_DISJOINT{..} = CO_DISJOINT (map toLaTeX attrs) (map toLaTeX groups)
toLaTeX CO_EMPTY = CO_EMPTY
instance ToLaTeX EXTRA_ARG where
toLaTeX ARG_ATTR{..} = ARG_ATTR (toLaTeX attr)
toLaTeX ARG_EXPR{..} = ARG_EXPR (toLaTeX expr)
toLaTeX ARG_BINDING{..} = ARG_BINDING (toLaTeX binding)
toLaTeX bts@ARG_BYTES{} = bts
instance ToLaTeX EXTRA where
toLaTeX EXTRA{..} = EXTRA (toLaTeX meta) func (map toLaTeX args)
explainRule :: Y.Rule -> String
explainRule rule =
trrule
rule.name
(renderToLatex (expressionToCST rule.pattern) defaultLatexContext)
(renderToLatex (expressionToCST rule.result) defaultLatexContext)
(joinedConditions rule.when rule.having)
rule.where_
where
-- Join two maybe conditions into single one using Y.And if at least one is just.
joinedConditions :: Maybe Y.Condition -> Maybe Y.Condition -> Maybe Y.Condition
joinedConditions Nothing Nothing = Nothing
joinedConditions first@(Just _) Nothing = first
joinedConditions Nothing second@(Just _) = second
joinedConditions (Just first) (Just second) = Just (Y.And [first, second])
explainMorphRule :: Y.MorphRule -> String
explainMorphRule rule =
trrule
rule.name
(morph (renderToLatex (expressionToCST rule.match) defaultLatexContext))
(morphOutcome rule.then_)
rule.when
rule.where_
where
morphOutcome :: Y.MorphOutcome -> String
morphOutcome (Y.MoMorph (Y.MaExpr expr)) = morph (renderToLatex (expressionToCST expr) defaultLatexContext)
morphOutcome (Y.MoMorph (Y.MaNormalize expr)) = morph (normalize (renderToLatex (expressionToCST expr) defaultLatexContext))
morphOutcome (Y.MoStop expr) = renderToLatex (expressionToCST expr) defaultLatexContext
explainDataizeRule :: Y.DataizeRule -> String
explainDataizeRule rule =
trrule
rule.name
(dataize (renderToLatex (expressionToCST rule.match) defaultLatexContext))
(dataizeOutcome rule.then_)
rule.when
rule.where_
where
dataizeOutcome :: Y.DataizeOutcome -> String
dataizeOutcome (Y.DoDataize (Y.DaExpr expr)) = dataize (renderToLatex (expressionToCST expr) defaultLatexContext)
dataizeOutcome (Y.DoDataize (Y.DaMorph expr)) = dataize (morph (renderToLatex (expressionToCST expr) defaultLatexContext))
dataizeOutcome (Y.DoData bytes) = T.unpack (render (toLaTeX (toCST' bytes :: BYTES)))
dataizeOutcome Y.DoNothing = "\\varnothing"
-- Render a single rule row through the shared \trrule macro: name, left-hand
-- side, right-hand side, the optional 'if' condition and 'where' extras.
trrule :: String -> String -> String -> Maybe Y.Condition -> Maybe [Y.Extra] -> String
trrule name lhs rhs cond extras =
intercalate
"\n "
[ "\\trrule{" ++ name ++ "}"
, braced lhs
, braced rhs
, conditionToLatex cond
, extraArgumentsToLatex extras
]
morph :: String -> String
morph inner = "\\mathbb{M}( " ++ inner ++ " )"
dataize :: String -> String
dataize inner = "\\mathbb{D}( " ++ inner ++ " )"
normalize :: String -> String
normalize inner = "\\mathcal{N}( " ++ inner ++ " )"
braced :: String -> String
braced = printf "{ %s }"
conditionToLatex :: Maybe Y.Condition -> String
conditionToLatex Nothing = "{ }"
conditionToLatex (Just cond) = case conditionToCST cond of
CO_EMPTY -> "{ }"
cond' -> braced ("if $ " <> renderToLatex cond' defaultLatexContext <> " $")
extraArgumentsToLatex :: Maybe [Y.Extra] -> String
extraArgumentsToLatex Nothing = "{ }"
extraArgumentsToLatex (Just extras) =
let extras' = map ((`renderToLatex` defaultLatexContext) . extraToCST) extras
in braced ("where " <> intercalate " and " extras')
explainRules :: [Y.Rule] -> String
explainRules = explainTabular . map explainRule
explainMorphRules :: [Y.MorphRule] -> String
explainMorphRules = explainTabular . map explainMorphRule
explainDataizeRules :: [Y.DataizeRule] -> String
explainDataizeRules = explainTabular . map explainDataizeRule
explainTabular :: [String] -> String
explainTabular rows =
intercalate
"\n"
[ "\\begin{tabular}{rl}"
, intercalate "\n" rows
, "\\end{tabular}"
]