lambda-graphs 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. lambda_graphs/__init__.py +18 -0
  2. lambda_graphs/__main__.py +7 -0
  3. lambda_graphs/cli.py +183 -0
  4. lambda_graphs/codeviews/AST/AST.py +120 -0
  5. lambda_graphs/codeviews/AST/AST_driver.py +37 -0
  6. lambda_graphs/codeviews/AST/__init__.py +0 -0
  7. lambda_graphs/codeviews/CFG/CFG.py +42 -0
  8. lambda_graphs/codeviews/CFG/CFG_c.py +1182 -0
  9. lambda_graphs/codeviews/CFG/CFG_cpp.py +5604 -0
  10. lambda_graphs/codeviews/CFG/CFG_driver.py +45 -0
  11. lambda_graphs/codeviews/CFG/CFG_java.py +1722 -0
  12. lambda_graphs/codeviews/CFG/__init__.py +0 -0
  13. lambda_graphs/codeviews/CST/CST_driver.py +70 -0
  14. lambda_graphs/codeviews/CST/__init__.py +0 -0
  15. lambda_graphs/codeviews/DFG/DFG_driver.py +42 -0
  16. lambda_graphs/codeviews/DFG/__init__.py +0 -0
  17. lambda_graphs/codeviews/SDFG/SDFG.py +135 -0
  18. lambda_graphs/codeviews/SDFG/SDFG_c.py +2041 -0
  19. lambda_graphs/codeviews/SDFG/SDFG_cpp.py +4030 -0
  20. lambda_graphs/codeviews/SDFG/SDFG_java.py +1618 -0
  21. lambda_graphs/codeviews/SDFG/__init__.py +0 -0
  22. lambda_graphs/codeviews/__init__.py +0 -0
  23. lambda_graphs/codeviews/combined_graph/__init__.py +0 -0
  24. lambda_graphs/codeviews/combined_graph/combined_driver.py +163 -0
  25. lambda_graphs/tree_parser/__init__.py +0 -0
  26. lambda_graphs/tree_parser/c_parser.py +565 -0
  27. lambda_graphs/tree_parser/cpp_parser.py +424 -0
  28. lambda_graphs/tree_parser/custom_parser.py +66 -0
  29. lambda_graphs/tree_parser/java_parser.py +254 -0
  30. lambda_graphs/tree_parser/parser_driver.py +54 -0
  31. lambda_graphs/utils/DFG_utils.py +44 -0
  32. lambda_graphs/utils/__init__.py +0 -0
  33. lambda_graphs/utils/c_nodes.py +431 -0
  34. lambda_graphs/utils/cpp_nodes.py +1331 -0
  35. lambda_graphs/utils/java_nodes.py +756 -0
  36. lambda_graphs/utils/multi_file_merger.py +444 -0
  37. lambda_graphs/utils/postprocessor.py +83 -0
  38. lambda_graphs/utils/preprocessor.py +68 -0
  39. lambda_graphs/utils/src_parser.py +23 -0
  40. lambda_graphs-0.1.4.dist-info/METADATA +380 -0
  41. lambda_graphs-0.1.4.dist-info/RECORD +45 -0
  42. lambda_graphs-0.1.4.dist-info/WHEEL +5 -0
  43. lambda_graphs-0.1.4.dist-info/entry_points.txt +2 -0
  44. lambda_graphs-0.1.4.dist-info/licenses/LICENSE +21 -0
  45. lambda_graphs-0.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,444 @@
1
+ """
2
+ Multi-file merger module for combining multiple C/C++ source files into a single file.
3
+
4
+ This module uses tree-sitter to parse and extract code elements from multiple files,
5
+ then combines them in the correct order for graph generation.
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Dict, List, Tuple, Optional
12
+
13
+ from tree_sitter import Parser
14
+ from loguru import logger
15
+
16
+ from lambda_graphs import get_language_map
17
+
18
+
19
+ class MultiFileMerger:
20
+ """
21
+ Merges multiple C/C++ source files from a folder into a single file.
22
+
23
+ The merger extracts and orders code elements properly:
24
+ 1. System includes
25
+ 2. Macro definitions and preprocessor directives
26
+ 3. Type definitions (structs, unions, enums, typedefs)
27
+ 4. Global variable declarations
28
+ 5. Function declarations (prototypes)
29
+ 6. Function definitions
30
+ """
31
+
32
+ def __init__(self, folder_path: str, language: str):
33
+ """
34
+ Initialize the multi-file merger.
35
+
36
+ Args:
37
+ folder_path: Path to the folder containing source files
38
+ language: Target language ('c' or 'cpp')
39
+ """
40
+ self.folder_path = Path(folder_path)
41
+ self.language = language.lower()
42
+
43
+ if self.language not in ["c", "cpp", "java"]:
44
+ raise ValueError(
45
+ f"Unsupported language: {language}. Use 'c', 'cpp', or 'java'."
46
+ )
47
+
48
+ if not self.folder_path.exists():
49
+ raise FileNotFoundError(f"Folder not found: {folder_path}")
50
+
51
+ if not self.folder_path.is_dir():
52
+ raise ValueError(f"Path is not a directory: {folder_path}")
53
+
54
+ self.language_map = get_language_map()
55
+ self.parser = Parser(self.language_map[self.language])
56
+
57
+ self.system_includes: List[str] = []
58
+ self.local_includes: List[str] = []
59
+ self.macro_definitions: List[str] = []
60
+ self.type_definitions: List[str] = []
61
+ self.global_declarations: List[str] = []
62
+ self.function_declarations: List[str] = []
63
+ self.function_definitions: List[Tuple[str, str]] = []
64
+
65
+ self.seen_includes: set = set()
66
+ self.seen_macros: set = set()
67
+ self.seen_types: set = set()
68
+ self.seen_function_decls: set = set()
69
+ self.seen_function_defs: set = set()
70
+ self.seen_globals: set = set()
71
+
72
+ def _get_source_files(self) -> Tuple[List[Path], List[Path]]:
73
+ """
74
+ Get all source files from the folder.
75
+
76
+ Returns:
77
+ Tuple of (header_files, source_files)
78
+ """
79
+ header_files = []
80
+ source_files = []
81
+
82
+ if self.language == "c":
83
+ source_extensions = [".c"]
84
+ header_extensions = [".h"]
85
+ elif self.language == "java":
86
+ source_extensions = [".java"]
87
+ header_extensions = []
88
+ else:
89
+ source_extensions = [".cpp", ".cc", ".cxx", ".c++"]
90
+ header_extensions = [".h", ".hpp", ".hh", ".hxx", ".h++"]
91
+
92
+ for root, _, files in os.walk(self.folder_path):
93
+ for file in sorted(files):
94
+ file_path = Path(root) / file
95
+ ext = file_path.suffix.lower()
96
+
97
+ if ext in header_extensions:
98
+ header_files.append(file_path)
99
+ elif ext in source_extensions:
100
+ source_files.append(file_path)
101
+
102
+ header_files.sort()
103
+ source_files.sort()
104
+
105
+ logger.debug(
106
+ f"Found {len(header_files)} header files and {len(source_files)} source files"
107
+ )
108
+
109
+ return header_files, source_files
110
+
111
+ def _parse_file(self, file_path: Path) -> None:
112
+ """
113
+ Parse a single file and extract its elements.
114
+
115
+ Args:
116
+ file_path: Path to the source file
117
+ """
118
+ logger.debug(f"Parsing file: {file_path}")
119
+
120
+ try:
121
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
122
+ content = f.read()
123
+ except Exception as e:
124
+ logger.warning(f"Failed to read file {file_path}: {e}")
125
+ return
126
+
127
+ tree = self.parser.parse(bytes(content, "utf-8"))
128
+ root_node = tree.root_node
129
+
130
+ for child in root_node.children:
131
+ self._process_node(child, content)
132
+
133
+ def _process_node(self, node, content: str) -> None:
134
+ """
135
+ Process a top-level AST node and categorize it.
136
+
137
+ Args:
138
+ node: Tree-sitter node
139
+ content: Original source code
140
+ """
141
+ node_text = content[node.start_byte : node.end_byte]
142
+
143
+ if node.type == "comment":
144
+ return
145
+
146
+ if node.type == "preproc_include":
147
+ self._handle_include(node_text)
148
+ elif node.type == "preproc_def":
149
+ self._handle_macro(node_text)
150
+ elif node.type == "preproc_function_def":
151
+ self._handle_macro(node_text)
152
+ elif node.type == "preproc_ifdef":
153
+ self._handle_preprocessor_conditional(node, content)
154
+ elif node.type == "preproc_ifndef":
155
+ self._handle_preprocessor_conditional(node, content)
156
+ elif node.type == "preproc_if":
157
+ self._handle_preprocessor_conditional(node, content)
158
+
159
+ elif node.type == "struct_specifier":
160
+ self._handle_type_definition(node_text)
161
+ elif node.type == "union_specifier":
162
+ self._handle_type_definition(node_text)
163
+ elif node.type == "enum_specifier":
164
+ self._handle_type_definition(node_text)
165
+ elif node.type == "type_definition":
166
+ self._handle_type_definition(node_text)
167
+
168
+ elif node.type == "declaration":
169
+ self._handle_declaration(node, node_text)
170
+
171
+ elif node.type == "function_definition":
172
+ self._handle_function_definition(node, node_text)
173
+
174
+ elif node.type == "class_specifier":
175
+ self._handle_type_definition(node_text)
176
+ elif node.type == "namespace_definition":
177
+ self._handle_type_definition(node_text)
178
+ elif node.type == "template_declaration":
179
+ self._handle_template(node, content, node_text)
180
+ elif node.type == "using_declaration":
181
+ self._handle_type_definition(node_text)
182
+ elif node.type == "alias_declaration":
183
+ self._handle_type_definition(node_text)
184
+
185
+ elif node.type == "linkage_specification":
186
+ self._handle_linkage_spec(node, content)
187
+
188
+ elif node.type == "declaration_list":
189
+ for child in node.children:
190
+ self._process_node(child, content)
191
+
192
+ def _handle_include(self, text: str) -> None:
193
+ """Handle include directives."""
194
+ text = text.strip()
195
+ if text in self.seen_includes:
196
+ return
197
+ self.seen_includes.add(text)
198
+
199
+ if "<" in text:
200
+ self.system_includes.append(text)
201
+ else:
202
+ self.local_includes.append(text)
203
+
204
+ def _handle_macro(self, text: str) -> None:
205
+ """Handle macro definitions."""
206
+ text = text.strip()
207
+ lines = text.split("\n")
208
+ first_line = lines[0]
209
+ parts = first_line.split()
210
+ if len(parts) >= 2:
211
+ macro_name = parts[1].split("(")[0]
212
+ if macro_name in self.seen_macros:
213
+ return
214
+ self.seen_macros.add(macro_name)
215
+
216
+ self.macro_definitions.append(text)
217
+
218
+ def _handle_preprocessor_conditional(self, node, content: str) -> None:
219
+ """Handle preprocessor conditionals (ifdef, ifndef, if) by processing their children."""
220
+ for child in node.children:
221
+ if child.type not in [
222
+ "#ifndef",
223
+ "#ifdef",
224
+ "#if",
225
+ "#endif",
226
+ "#else",
227
+ "identifier",
228
+ ]:
229
+ self._process_node(child, content)
230
+
231
+ def _handle_linkage_spec(self, node, content: str) -> None:
232
+ """Handle linkage specification (extern "C" { ... })."""
233
+ for child in node.children:
234
+ if child.type == "declaration_list":
235
+ for subchild in child.children:
236
+ self._process_node(subchild, content)
237
+
238
+ def _handle_type_definition(self, text: str) -> None:
239
+ """Handle struct, union, enum, typedef, class definitions."""
240
+ text = text.strip()
241
+ if text in self.seen_types:
242
+ return
243
+ self.seen_types.add(text)
244
+ self.type_definitions.append(text)
245
+
246
+ def _handle_declaration(self, node, text: str) -> None:
247
+ """Handle variable and function declarations."""
248
+ text = text.strip()
249
+
250
+ is_function_decl = False
251
+ for child in node.children:
252
+ if child.type == "function_declarator":
253
+ is_function_decl = True
254
+ break
255
+ if child.type == "pointer_declarator":
256
+ for subchild in child.children:
257
+ if subchild.type == "function_declarator":
258
+ is_function_decl = True
259
+ break
260
+
261
+ if is_function_decl:
262
+ if text not in self.seen_function_decls:
263
+ self.seen_function_decls.add(text)
264
+ self.function_declarations.append(text)
265
+ else:
266
+ if text not in self.seen_globals:
267
+ self.seen_globals.add(text)
268
+ self.global_declarations.append(text)
269
+
270
+ def _handle_function_definition(self, node, text: str) -> None:
271
+ """Handle function definitions."""
272
+ text = text.strip()
273
+
274
+ func_name = self._extract_function_name(node)
275
+
276
+ if func_name in self.seen_function_defs:
277
+ logger.warning(f"Duplicate function definition: {func_name}")
278
+ return
279
+
280
+ self.seen_function_defs.add(func_name)
281
+ self.function_definitions.append((func_name, text))
282
+
283
+ def _handle_template(self, node, content: str, text: str) -> None:
284
+ """Handle C++ template declarations."""
285
+ text = text.strip()
286
+
287
+ has_function_def = False
288
+ for child in node.children:
289
+ if child.type == "function_definition":
290
+ has_function_def = True
291
+ break
292
+
293
+ if has_function_def:
294
+ func_name = f"template_{len(self.function_definitions)}"
295
+ if text not in self.seen_function_defs:
296
+ self.seen_function_defs.add(text)
297
+ self.function_definitions.append((func_name, text))
298
+ else:
299
+ if text not in self.seen_types:
300
+ self.seen_types.add(text)
301
+ self.type_definitions.append(text)
302
+
303
+ def _extract_function_name(self, node) -> str:
304
+ """Extract function name from a function definition node."""
305
+ for child in node.children:
306
+ if child.type == "function_declarator":
307
+ for subchild in child.children:
308
+ if subchild.type == "identifier":
309
+ return subchild.text.decode("utf-8")
310
+ elif subchild.type == "field_identifier":
311
+ return subchild.text.decode("utf-8")
312
+ elif subchild.type == "destructor_name":
313
+ return subchild.text.decode("utf-8")
314
+ elif subchild.type == "qualified_identifier":
315
+ return subchild.text.decode("utf-8")
316
+ elif child.type == "pointer_declarator":
317
+ return self._extract_function_name_from_declarator(child)
318
+
319
+ return f"unknown_func_{len(self.function_definitions)}"
320
+
321
+ def _extract_function_name_from_declarator(self, node) -> str:
322
+ """Recursively extract function name from declarator."""
323
+ for child in node.children:
324
+ if child.type == "function_declarator":
325
+ for subchild in child.children:
326
+ if subchild.type == "identifier":
327
+ return subchild.text.decode("utf-8")
328
+ elif child.type == "pointer_declarator":
329
+ return self._extract_function_name_from_declarator(child)
330
+ return "unknown_func"
331
+
332
+ def merge(self) -> str:
333
+ """
334
+ Merge all source files into a single string.
335
+
336
+ Returns:
337
+ Merged source code
338
+ """
339
+ header_files, source_files = self._get_source_files()
340
+
341
+ if not header_files and not source_files:
342
+ raise ValueError(f"No source files found in {self.folder_path}")
343
+
344
+ for header_file in header_files:
345
+ self._parse_file(header_file)
346
+
347
+ for source_file in source_files:
348
+ self._parse_file(source_file)
349
+
350
+ output_parts = []
351
+
352
+ output_parts.append(f"/* Merged source from {self.folder_path} */")
353
+ output_parts.append(f"/* Generated by lambda-graphs Multi-File Merger */")
354
+ output_parts.append("")
355
+
356
+ if self.system_includes:
357
+ output_parts.append("/* System Includes */")
358
+ for inc in self.system_includes:
359
+ output_parts.append(inc)
360
+ output_parts.append("")
361
+
362
+ if self.macro_definitions:
363
+ output_parts.append("/* Macro Definitions */")
364
+ for macro in self.macro_definitions:
365
+ output_parts.append(macro)
366
+ output_parts.append("")
367
+
368
+ if self.type_definitions:
369
+ output_parts.append("/* Type Definitions */")
370
+ for typedef in self.type_definitions:
371
+ output_parts.append(typedef)
372
+ output_parts.append("")
373
+
374
+ if self.global_declarations:
375
+ output_parts.append("/* Global Declarations */")
376
+ for decl in self.global_declarations:
377
+ output_parts.append(decl)
378
+ output_parts.append("")
379
+
380
+ if self.function_declarations:
381
+ output_parts.append("/* Function Declarations */")
382
+ for decl in self.function_declarations:
383
+ output_parts.append(decl)
384
+ output_parts.append("")
385
+
386
+ if self.function_definitions:
387
+ output_parts.append("/* Function Definitions */")
388
+ for func_name, func_code in self.function_definitions:
389
+ output_parts.append(f"/* Function: {func_name} */")
390
+ output_parts.append(func_code)
391
+ output_parts.append("")
392
+
393
+ return "\n".join(output_parts)
394
+
395
+ def merge_to_file(self, output_path: Optional[str] = None) -> str:
396
+ """
397
+ Merge all source files and write to a file.
398
+
399
+ Args:
400
+ output_path: Optional path for the output file. If not provided,
401
+ a temporary file will be created.
402
+
403
+ Returns:
404
+ Path to the merged file
405
+ """
406
+ merged_code = self.merge()
407
+
408
+ if output_path is None:
409
+ if self.language == "c":
410
+ suffix = ".c"
411
+ else:
412
+ suffix = ".cpp"
413
+
414
+ temp_dir = tempfile.mkdtemp(prefix="lambda_graphs_merged_")
415
+ output_path = os.path.join(temp_dir, f"project{suffix}")
416
+
417
+ with open(output_path, "w", encoding="utf-8") as f:
418
+ f.write(merged_code)
419
+
420
+ logger.info(f"Merged source written to: {output_path}")
421
+
422
+ return output_path
423
+
424
+
425
+ def merge_files(
426
+ folder_path: str, language: str, output_path: Optional[str] = None
427
+ ) -> str:
428
+ """
429
+ Convenience function to merge a folder of source files.
430
+
431
+ Args:
432
+ folder_path: Path to the folder containing source files
433
+ language: Target language ('c' or 'cpp')
434
+ output_path: Optional path for the output file
435
+
436
+ Returns:
437
+ Path to the merged file
438
+ """
439
+ merger = MultiFileMerger(folder_path, language)
440
+ return merger.merge_to_file(output_path)
441
+
442
+
443
+ FolderCombiner = MultiFileMerger
444
+ combine_folder = merge_files
@@ -0,0 +1,83 @@
1
+ import copy
2
+ import json
3
+ import os
4
+ import re
5
+ from subprocess import check_call
6
+
7
+ import networkx as nx
8
+ from networkx.readwrite import json_graph
9
+
10
+
11
+ def networkx_to_json(graph):
12
+ """Convert a networkx graph to a json object"""
13
+ graph_json = json_graph.node_link_data(graph)
14
+ return graph_json
15
+
16
+
17
+ def write_networkx_to_json(graph, filename):
18
+ """Convert a networkx graph to a json object"""
19
+ graph_json = json_graph.node_link_data(graph)
20
+ if not os.getenv("GITHUB_ACTIONS"):
21
+ with open(filename, "w") as f:
22
+ json.dump(graph_json, f)
23
+ return graph_json
24
+
25
+
26
+ def to_dot(graph):
27
+ return nx.nx_pydot.to_pydot(graph)
28
+
29
+
30
+ def write_to_dot(og_graph, filename, output_png=False, src_language=None):
31
+ graph = copy.deepcopy(og_graph)
32
+ if not os.getenv("GITHUB_ACTIONS"):
33
+ dot_reserved_keywords = {
34
+ "node",
35
+ "edge",
36
+ "graph",
37
+ "digraph",
38
+ "subgraph",
39
+ "strict",
40
+ "Node",
41
+ "Edge",
42
+ "Graph",
43
+ "Digraph",
44
+ "Subgraph",
45
+ "Strict",
46
+ }
47
+
48
+ for node in graph.nodes:
49
+ if "label" in graph.nodes[node]:
50
+ label = graph.nodes[node]["label"]
51
+
52
+ if src_language in ["c", "cpp", "java"]:
53
+ label = str(label)
54
+ label = label.replace("\\", "\\\\")
55
+ label = label.replace('"', '\\"')
56
+ label = label.replace("\n", " ")
57
+ label = label.replace("\r", " ")
58
+ else:
59
+ label = re.escape(label)
60
+
61
+ if (
62
+ src_language in ["c", "cpp", "java"]
63
+ or label in dot_reserved_keywords
64
+ ):
65
+ label = f'"{label}"'
66
+
67
+ graph.nodes[node]["label"] = label
68
+
69
+ for u, v, key, data in graph.edges(keys=True, data=True):
70
+ for attr_name in ["used_def", "used_var", "returned_value"]:
71
+ if attr_name in data:
72
+ attr_value = str(data[attr_name])
73
+ needs_quoting = attr_value in dot_reserved_keywords or (
74
+ src_language in ["c", "cpp", "java"] and "::" in attr_value
75
+ )
76
+ if needs_quoting:
77
+ graph.edges[u, v, key][attr_name] = f'"{attr_value}"'
78
+
79
+ nx.nx_pydot.write_dot(graph, filename)
80
+ if output_png:
81
+ check_call(
82
+ ["dot", "-Tpng", filename, "-o", filename.rsplit(".", 1)[0] + ".png"]
83
+ )
@@ -0,0 +1,68 @@
1
+ import re
2
+ import tokenize
3
+ from io import StringIO
4
+
5
+
6
+ def remove_empty_lines(source):
7
+ temp = []
8
+ for x in source.split("\n"):
9
+ temp.append(x)
10
+ return "\n".join(temp)
11
+
12
+
13
+ def remove_comments(lang, source):
14
+ if lang in ["python"]:
15
+ """
16
+ Returns 'source' minus comments and docstrings.
17
+ """
18
+ io_obj = StringIO(source)
19
+ out = ""
20
+ prev_toktype = tokenize.INDENT
21
+ last_lineno = -1
22
+ last_col = 0
23
+ for tok in tokenize.generate_tokens(io_obj.readline):
24
+ token_type = tok[0]
25
+ token_string = tok[1]
26
+ start_line, start_col = tok[2]
27
+ end_line, end_col = tok[3]
28
+ ltext = tok[4]
29
+ if start_line > last_lineno:
30
+ last_col = 0
31
+ if start_col > last_col:
32
+ out += " " * (start_col - last_col)
33
+ if token_type == tokenize.COMMENT:
34
+ pass
35
+ elif token_type == tokenize.STRING:
36
+ if prev_toktype != tokenize.INDENT:
37
+ if prev_toktype != tokenize.NEWLINE:
38
+ if start_col > 0:
39
+ out += token_string
40
+ else:
41
+ out += token_string
42
+ prev_toktype = token_type
43
+ last_col = end_col
44
+ last_lineno = end_line
45
+ temp = []
46
+ for x in out.split("\n"):
47
+ if x.strip() != "":
48
+ temp.append(x)
49
+ return "\n".join(temp)
50
+ elif lang in ["ruby"]:
51
+ return source
52
+ else:
53
+
54
+ def replacer(match):
55
+ s = match.group(0)
56
+ if s.startswith("/"):
57
+ return " "
58
+ else:
59
+ return s
60
+
61
+ pattern = re.compile(
62
+ r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
63
+ re.DOTALL | re.MULTILINE,
64
+ )
65
+ temp = []
66
+ for x in re.sub(pattern, replacer, source).split("\n"):
67
+ temp.append(x)
68
+ return "\n".join(temp)
@@ -0,0 +1,23 @@
1
+ def traverse_tree(tree, finest_granularity=None):
2
+ if finest_granularity is None:
3
+ finest_granularity = []
4
+ cursor = tree.walk()
5
+
6
+ reached_root = False
7
+ while not reached_root:
8
+ yield cursor.node
9
+
10
+ if cursor.goto_first_child() and cursor.node.type not in finest_granularity:
11
+ continue
12
+
13
+ if cursor.goto_next_sibling():
14
+ continue
15
+
16
+ retracing = True
17
+ while retracing:
18
+ if not cursor.goto_parent():
19
+ retracing = False
20
+ reached_root = True
21
+
22
+ if cursor.goto_next_sibling():
23
+ retracing = False