tree.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import sys
  2. from copy import deepcopy
  3. from typing import List, Callable, Iterator, Union, Optional, Generic, TypeVar, TYPE_CHECKING
  4. from .lexer import Token
  5. if TYPE_CHECKING:
  6. from .lexer import TerminalDef
  7. try:
  8. import rich
  9. except ImportError:
  10. pass
  11. from typing import Literal
  12. ###{standalone
  13. class Meta:
  14. empty: bool
  15. line: int
  16. column: int
  17. start_pos: int
  18. end_line: int
  19. end_column: int
  20. end_pos: int
  21. orig_expansion: 'List[TerminalDef]'
  22. match_tree: bool
  23. def __init__(self):
  24. self.empty = True
  25. _Leaf_T = TypeVar("_Leaf_T")
  26. Branch = Union[_Leaf_T, 'Tree[_Leaf_T]']
  27. class Tree(Generic[_Leaf_T]):
  28. """The main tree class.
  29. Creates a new tree, and stores "data" and "children" in attributes of the same name.
  30. Trees can be hashed and compared.
  31. Parameters:
  32. data: The name of the rule or alias
  33. children: List of matched sub-rules and terminals
  34. meta: Line & Column numbers (if ``propagate_positions`` is enabled).
  35. meta attributes: (line, column, end_line, end_column, start_pos, end_pos,
  36. container_line, container_column, container_end_line, container_end_column)
  37. container_* attributes consider all symbols, including those that have been inlined in the tree.
  38. For example, in the rule 'a: _A B _C', the regular attributes will mark the start and end of B,
  39. but the container_* attributes will also include _A and _C in the range. However, rules that
  40. contain 'a' will consider it in full, including _A and _C for all attributes.
  41. """
  42. data: str
  43. children: 'List[Branch[_Leaf_T]]'
  44. def __init__(self, data: str, children: 'List[Branch[_Leaf_T]]', meta: Optional[Meta]=None) -> None:
  45. self.data = data
  46. self.children = children
  47. self._meta = meta
  48. @property
  49. def meta(self) -> Meta:
  50. if self._meta is None:
  51. self._meta = Meta()
  52. return self._meta
  53. def __repr__(self):
  54. return 'Tree(%r, %r)' % (self.data, self.children)
  55. __match_args__ = ("data", "children")
  56. def _pretty_label(self):
  57. return self.data
  58. def _pretty(self, level, indent_str):
  59. yield f'{indent_str*level}{self._pretty_label()}'
  60. if len(self.children) == 1 and not isinstance(self.children[0], Tree):
  61. yield f'\t{self.children[0]}\n'
  62. else:
  63. yield '\n'
  64. for n in self.children:
  65. if isinstance(n, Tree):
  66. yield from n._pretty(level+1, indent_str)
  67. else:
  68. yield f'{indent_str*(level+1)}{n}\n'
  69. def pretty(self, indent_str: str=' ') -> str:
  70. """Returns an indented string representation of the tree.
  71. Great for debugging.
  72. """
  73. return ''.join(self._pretty(0, indent_str))
  74. def __rich__(self, parent:Optional['rich.tree.Tree']=None) -> 'rich.tree.Tree':
  75. """Returns a tree widget for the 'rich' library.
  76. Example:
  77. ::
  78. from rich import print
  79. from lark import Tree
  80. tree = Tree('root', ['node1', 'node2'])
  81. print(tree)
  82. """
  83. return self._rich(parent)
  84. def _rich(self, parent):
  85. if parent:
  86. tree = parent.add(f'[bold]{self.data}[/bold]')
  87. else:
  88. import rich.tree
  89. tree = rich.tree.Tree(self.data)
  90. for c in self.children:
  91. if isinstance(c, Tree):
  92. c._rich(tree)
  93. else:
  94. tree.add(f'[green]{c}[/green]')
  95. return tree
  96. def __eq__(self, other):
  97. try:
  98. return self.data == other.data and self.children == other.children
  99. except AttributeError:
  100. return False
  101. def __ne__(self, other):
  102. return not (self == other)
  103. def __hash__(self) -> int:
  104. return hash((self.data, tuple(self.children)))
  105. def iter_subtrees(self) -> 'Iterator[Tree[_Leaf_T]]':
  106. """Depth-first iteration.
  107. Iterates over all the subtrees, never returning to the same node twice (Lark's parse-tree is actually a DAG).
  108. """
  109. queue = [self]
  110. subtrees = dict()
  111. for subtree in queue:
  112. subtrees[id(subtree)] = subtree
  113. queue += [c for c in reversed(subtree.children)
  114. if isinstance(c, Tree) and id(c) not in subtrees]
  115. del queue
  116. return reversed(list(subtrees.values()))
  117. def iter_subtrees_topdown(self):
  118. """Breadth-first iteration.
  119. Iterates over all the subtrees, return nodes in order like pretty() does.
  120. """
  121. stack = [self]
  122. stack_append = stack.append
  123. stack_pop = stack.pop
  124. while stack:
  125. node = stack_pop()
  126. if not isinstance(node, Tree):
  127. continue
  128. yield node
  129. for child in reversed(node.children):
  130. stack_append(child)
  131. def find_pred(self, pred: 'Callable[[Tree[_Leaf_T]], bool]') -> 'Iterator[Tree[_Leaf_T]]':
  132. """Returns all nodes of the tree that evaluate pred(node) as true."""
  133. return filter(pred, self.iter_subtrees())
  134. def find_data(self, data: str) -> 'Iterator[Tree[_Leaf_T]]':
  135. """Returns all nodes of the tree whose data equals the given data."""
  136. return self.find_pred(lambda t: t.data == data)
  137. ###}
  138. def find_token(self, token_type: str) -> Iterator[_Leaf_T]:
  139. """Returns all tokens whose type equals the given token_type.
  140. This is a recursive function that will find tokens in all the subtrees.
  141. Example:
  142. >>> term_tokens = tree.find_token('TERM')
  143. """
  144. return self.scan_values(lambda v: isinstance(v, Token) and v.type == token_type)
  145. def expand_kids_by_data(self, *data_values):
  146. """Expand (inline) children with any of the given data values. Returns True if anything changed"""
  147. changed = False
  148. for i in range(len(self.children)-1, -1, -1):
  149. child = self.children[i]
  150. if isinstance(child, Tree) and child.data in data_values:
  151. self.children[i:i+1] = child.children
  152. changed = True
  153. return changed
  154. def scan_values(self, pred: 'Callable[[Branch[_Leaf_T]], bool]') -> Iterator[_Leaf_T]:
  155. """Return all values in the tree that evaluate pred(value) as true.
  156. This can be used to find all the tokens in the tree.
  157. Example:
  158. >>> all_tokens = tree.scan_values(lambda v: isinstance(v, Token))
  159. """
  160. for c in self.children:
  161. if isinstance(c, Tree):
  162. for t in c.scan_values(pred):
  163. yield t
  164. else:
  165. if pred(c):
  166. yield c
  167. def __deepcopy__(self, memo):
  168. return type(self)(self.data, deepcopy(self.children, memo), meta=self._meta)
  169. def copy(self) -> 'Tree[_Leaf_T]':
  170. return type(self)(self.data, self.children)
  171. def set(self, data: str, children: 'List[Branch[_Leaf_T]]') -> None:
  172. self.data = data
  173. self.children = children
  174. ParseTree = Tree['Token']
  175. class SlottedTree(Tree):
  176. __slots__ = 'data', 'children', 'rule', '_meta'
  177. def pydot__tree_to_png(tree: Tree, filename: str, rankdir: 'Literal["TB", "LR", "BT", "RL"]'="LR", **kwargs) -> None:
  178. graph = pydot__tree_to_graph(tree, rankdir, **kwargs)
  179. graph.write_png(filename)
  180. def pydot__tree_to_dot(tree: Tree, filename, rankdir="LR", **kwargs):
  181. graph = pydot__tree_to_graph(tree, rankdir, **kwargs)
  182. graph.write(filename)
  183. def pydot__tree_to_graph(tree: Tree, rankdir="LR", **kwargs):
  184. """Creates a colorful image that represents the tree (data+children, without meta)
  185. Possible values for `rankdir` are "TB", "LR", "BT", "RL", corresponding to
  186. directed graphs drawn from top to bottom, from left to right, from bottom to
  187. top, and from right to left, respectively.
  188. `kwargs` can be any graph attribute (e. g. `dpi=200`). For a list of
  189. possible attributes, see https://www.graphviz.org/doc/info/attrs.html.
  190. """
  191. import pydot # type: ignore[import-not-found]
  192. graph = pydot.Dot(graph_type='digraph', rankdir=rankdir, **kwargs)
  193. i = [0]
  194. def new_leaf(leaf):
  195. node = pydot.Node(i[0], label=repr(leaf))
  196. i[0] += 1
  197. graph.add_node(node)
  198. return node
  199. def _to_pydot(subtree):
  200. color = hash(subtree.data) & 0xffffff
  201. color |= 0x808080
  202. subnodes = [_to_pydot(child) if isinstance(child, Tree) else new_leaf(child)
  203. for child in subtree.children]
  204. node = pydot.Node(i[0], style="filled", fillcolor="#%x" % color, label=subtree.data)
  205. i[0] += 1
  206. graph.add_node(node)
  207. for subnode in subnodes:
  208. graph.add_edge(pydot.Edge(node, subnode))
  209. return node
  210. _to_pydot(tree)
  211. return graph