packages feed

peg-matching-0.1.0.0: src/Syntax/ParsedTree.hs

{-|
Module      : Syntax.ParsedTree
Description : Representation of parsed trees.
Copyright   : (c) Guilherme Drummond, Rodrigo Ribeiro, 2025
License     : BSD-3-Clause
Maintainer  : rodrigo.ribeiro@ufop.edu.br
Stability   : experimental
Portability : POSIX

This module defines the structure of a parsed tree ('ParsedTree') and
associated functions, such as the 'flatten' function to extract the terminals from a tree.
It also provides an instance of the 'Pretty' class for formatted printing.
-}
module Syntax.ParsedTree
    ( ParsedTree(..)
    , ParsedTreeZipper
    , ParsedTreePath
    , ParsedTreeCrumbs
    , flatten
    , goUp
    , goDown
    , goLeft
    , goRight
    , pullFromRight
    , ofExpression
    ) where

import Syntax.Base (Terminal(..), NonTerminal, Pretty(..))
import Text.PrettyPrint.HughesPJ (text, Doc, (<+>), (<>), empty, lbrack, rbrack, hcat)
import Prelude hiding ((<>))
import Data.Generics (Data, Typeable, mkQ, everything)
import Syntax.Peg (Expression(..), Grammar, expression)

{-|
Represents an abstract syntax tree (AST).

A 'ParsedTree' can be:
- 'ParsedEpsilon': Represents the empty tree (ε).
- 'ParsedT': A terminal symbol.
- 'ParsedNT': A non-terminal symbol associated with a subtree.
- 'ParsedSeq': A sequence of two trees.
- 'ParsedChoiceLeft': Represents the left choice in a choice operation.
- 'ParsedChoiceRight': Represents the right choice in a choice operation.
- 'ParsedStar': Represents a repetition of zero or more times of a tree.
- 'ParsedNot': Represents the negation of a tree.
- 'ParsedIndent': Represents that a list of trees must be indented with respect to another tree.

@since 1.0.0
-}
data ParsedTree
    = ParsedEpsilon
    | ParsedT Terminal
    | ParsedNT NonTerminal ParsedTree
    | ParsedSeq ParsedTree ParsedTree
    | ParsedChoiceLeft ParsedTree
    | ParsedChoiceRight ParsedTree
    | ParsedStar [ParsedTree]
    | ParsedNot
    | ParsedIndent ParsedTree [ParsedTree]
    deriving (Show, Typeable, Data)

{-|
Breadcrumbs used to reconstruct the parent context while navigating a
'ParsedTree' with a zipper.

Each constructor records the information required to rebuild the tree when
moving back up from the current focus.
-}
data ParsedTreeCrumbs
    = ParsedNTCrumb NonTerminal
    | ParsedSeqFirst ParsedTree
    | ParsedSeqSecond ParsedTree
    | ParsedChoiceLeftCrumb
    | ParsedChoiceRightCrumb
    | ParsedStarCrumb [ParsedTree] [ParsedTree] -- primeiro é o que falta, segundo é o que já foi
    | ParsedIndentFirst [ParsedTree]
    | ParsedIndentSecond ParsedTree

{-|
A zipper path is the list of breadcrumbs representing the current position
inside a 'ParsedTree'. The most recent breadcrumb is at the head of the list.
-}
type ParsedTreePath = [ParsedTreeCrumbs]

{-|
A zipper for a parsed tree. The first component is the current focus, and the
second component is the path back to the root.
-}
type ParsedTreeZipper = (ParsedTree, ParsedTreePath)

{-|
Move the focus of a 'ParsedTreeZipper' up to its parent node, if possible.

This reconstructs the parent node from the current focus and the breadcrumb
stored in the zipper path.
-}
goUp :: ParsedTreeZipper -> Maybe ParsedTreeZipper
goUp (t, ParsedNTCrumb nt:z)                 = Just (ParsedNT nt t, z)
goUp (t, ParsedSeqFirst t':z)                = Just (ParsedSeq t t', z)
goUp (t, ParsedSeqSecond t':z)               = Just (ParsedSeq t' t, z)
goUp (t, ParsedChoiceLeftCrumb:z)            = Just (ParsedChoiceLeft t, z)
goUp (t, ParsedChoiceRightCrumb:z)           = Just (ParsedChoiceRight t, z)
-- goUp (t, (ParsedStarCrumb ts []):z)          = Just (ParsedStar (t:ts), z)
-- goUp z@(_, (ParsedStarCrumb _ _):_)          = goUp =<< goLeft z
goUp (t, ParsedStarCrumb ts1 ts2:z)          = Just (ParsedStar (t : reverse ts2 ++ ts1), z)
goUp (t, ParsedIndentFirst ts:z)             = Just (ParsedIndent t ts, z)
goUp (ParsedStar ts, ParsedIndentSecond t:z) = Just (ParsedIndent t ts, z)
goUp (_, ParsedIndentSecond _:_)             = Nothing
goUp (_, [])                                 = Nothing

-- TODO:
-- goLeft, goRight e goDown não dão muito bem quando tentam acessar esquerda e direita
-- de uma árvore que está dentro de uma lista, pois a preferência é por andar na lista.

{-|
Move the focus down into a child subtree, when the current focus is a node
that contains a single child or a non-empty star list.
-}
goDown :: ParsedTreeZipper -> Maybe ParsedTreeZipper
goDown (ParsedNT nt t, z)       = Just (t, ParsedNTCrumb nt:z)
goDown (ParsedChoiceLeft t, z)  = Just (t, ParsedChoiceLeftCrumb:z)
goDown (ParsedChoiceRight t, z) = Just (t, ParsedChoiceRightCrumb:z)
goDown (ParsedStar [], _)       = Nothing
goDown (ParsedStar (t:ts), z)   = Just (t, ParsedStarCrumb ts []:z)
goDown _                        = Nothing

{-|
Move the focus left within the current zipper context.

This is valid for star lists, sequence nodes, and indent nodes where a
left sibling exists.
-}
goLeft :: ParsedTreeZipper -> Maybe ParsedTreeZipper
goLeft (_, (ParsedStarCrumb _ []):_)         = Nothing
goLeft (t, (ParsedStarCrumb ts1 (t':ts2)):z) = Just (t', ParsedStarCrumb (t:ts1) ts2:z)
goLeft (ParsedSeq t1 t2, z)                  = Just (t1, ParsedSeqFirst t2:z)
goLeft (ParsedIndent t ts, z)                = Just (t, ParsedIndentFirst ts:z)
goLeft _                                     = Nothing

{-|
Move the focus right within the current zipper context.

This is valid for star lists, sequence nodes, and indent nodes where a
right sibling exists.
-}
goRight :: ParsedTreeZipper -> Maybe ParsedTreeZipper
goRight (_, (ParsedStarCrumb [] _):_)         = Nothing
goRight (t, (ParsedStarCrumb (t':ts1) ts2):z) = Just (t', ParsedStarCrumb ts1 (t:ts2):z)
goRight (ParsedSeq t1 t2, z)                  = Just (t2, ParsedSeqSecond t1:z)
goRight (ParsedIndent t ts, z)                = Just (ParsedStar ts, ParsedIndentSecond t:z)
goRight _                                     = Nothing

{-|
Pull the first element from the right side of a sequence and append it to
its left side.

If the provided tree is not a sequence, this returns 'Nothing'.
-}
pullFromRight :: ParsedTree -> Maybe ParsedTree
pullFromRight (ParsedSeq t1 t2) = maybe (Just t1') (Just . ParsedSeq t1') tT
    where
        (tH, tT) = getHead t2
        t1' = addAtEnd t1 tH
pullFromRight _ = Nothing

addAtEnd :: ParsedTree -> ParsedTree -> ParsedTree
addAtEnd (ParsedSeq t1 t2) e3 = ParsedSeq t1 $ addAtEnd t2 e3
addAtEnd e e3 = ParsedSeq e e3

getHead :: ParsedTree -> (ParsedTree, Maybe ParsedTree)
getHead (ParsedSeq t1 t2) = (t1, Just t2)
getHead e                = (e, Nothing)

{-|
Instance of the 'Pretty' class for 'ParsedTree'.

Prints the syntax tree in a readable format, with indentation
and visual symbols to represent the tree hierarchy.

@since 1.0.0
-}
instance Pretty ParsedTree where
    pPrint :: ParsedTree -> Doc
    pPrint pt = pPrint' Text.PrettyPrint.HughesPJ.empty pt <> text "\n"

-- Auxiliary functions for tree formatting
nest :: Doc -> Doc
nest i = i <> text "├╴"

nest1 :: Doc -> Doc
nest1 i = i <> text "╰╴"

continue :: Doc -> Doc
continue i = i <> text "| "

continue1 :: Doc -> Doc
continue1 i = i <> text "  "

{-|
Auxiliary function for formatted printing of a 'ParsedTree'.

@since 1.0.0
-}
pPrint' :: Doc -> ParsedTree -> Doc
pPrint' _ ParsedEpsilon = text "ε"
pPrint' _ (ParsedT t) = pPrint t
pPrint' indent (ParsedNT nt tree) =
    text "NT" <+> pPrint nt <> text "\n"
    <> nest1 indent <> pPrint' (continue1 indent) tree
pPrint' indent (ParsedSeq t1 t2) =
    text "Seq" <> text "\n"
    <> nest indent <> pPrint' (continue indent) t1 <> text "\n"
    <> nest1 indent <> pPrint' (continue1 indent) t2
pPrint' indent (ParsedChoiceLeft tree) =
    text "Left" <> text "\n"
    <> nest1 indent <> pPrint' (continue1 indent) tree
pPrint' indent (ParsedChoiceRight tree) =
    text "Right" <> text "\n"
    <> nest1 indent <> pPrint' (continue1 indent) tree
pPrint' indent (ParsedStar ts) =
    text "Star" <+> lbrack <> list' <> rbrack
    where
        listnest = if null ts then Text.PrettyPrint.HughesPJ.empty else text "\n"
        listEnd   = if null ts then Text.PrettyPrint.HughesPJ.empty else nest1 indent
        list      = hcat (map (\ x -> nest indent <> pPrint' (continue indent) x <> text "\n") ts)
        list'     = listnest <> list <> listEnd
pPrint' _ ParsedNot = Text.PrettyPrint.HughesPJ.empty
pPrint' indent (ParsedIndent e b) =
    text "Indent" <> text "\n"
    <> nest indent <> pPrint' (continue indent) e <> text "\n"
    <> nest1 indent <> pPrint' (continue1 indent) (ParsedStar b)

{-|
Extracts all terminal symbols from a 'ParsedTree' as a single string.

=== Usage examples:

>>> flatten (ParsedSeq (ParsedT (T "a")) (ParsedT (T "b")))
"ab"

>>> flatten ParsedEpsilon
""

@since 1.0.0
-}
flatten :: ParsedTree -> String
flatten = everything (++) ("" `mkQ` term)
    where
        term (ParsedT (T t)) = t
        term _ = ""

{-|
Check whether a 'ParsedTree' corresponds to a given grammar expression.

The function follows the structure of the expression and compares it with the
parsed tree, resolving non-terminals using the provided grammar.
-}
ofExpression :: Grammar -> Expression -> ParsedTree -> Bool
ofExpression _ Empty ParsedEpsilon = True
ofExpression _ (ExprT t) (ParsedT t') = t == t'
ofExpression g (ExprNT nt) (ParsedNT nt' t) = nt == nt' && case expression g nt of
                                                Just x -> ofExpression g x t
                                                Nothing -> False
ofExpression g (Sequence e1 e2) (ParsedSeq t1 t2) = ofExpression g e1 t1 && ofExpression g e2 t2
ofExpression g (Choice e1 _) (ParsedChoiceLeft t) = ofExpression g e1 t
ofExpression g (Choice _ e2) (ParsedChoiceRight t)  = ofExpression g e2 t
ofExpression g (Star e) (ParsedStar ts) = all (ofExpression g e) ts
ofExpression _ (Not _) ParsedNot = True
ofExpression _ (Flatten _) (ParsedT _) = True 
ofExpression g (Indent e1 e2) (ParsedIndent t ts) = ofExpression g e1 t && all (ofExpression g e2) ts
ofExpression _ _ _ = False