Coverage for src/renaissance/integrations/clang/c_pattern_factory.py: 73%
139 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
1# Copyright (C) 2024-2026 TNO
2#
3# Licensed under the terms specified in the LICENSE file in the root directory
4# of this repository, also available at:
5# https://github.com/TNO/Renaissance.Py/blob/main/LICENSE
7"""AI: Factory for building C/C++ AST patterns from Clang-parsed source."""
9import re
10from collections.abc import Sequence
12from more_itertools import first
13from more_itertools.more import last
15from renaissance.integrations.clang.cpp_utils import CPPUtils
16from renaissance.integrations.clang.predicates import (
17 is_clang_compound_statement,
18 is_clang_kind,
19 is_clang_macro_definition,
20)
21from renaissance.syntax_tree.ast_factory import ASTFactory
22from renaissance.syntax_tree.ast_finder import find_nodes
23from renaissance.syntax_tree.ast_node import ASTNode
24from renaissance.syntax_tree.ast_shower import ASTShower
25from renaissance.syntax_tree.semantic_kind import SemanticKind
27SHOW_NODE = False
30def _matches_kind(node, kind) -> bool:
31 return kind is None or kind(node)
34def derive_header_text(language: str, ref_node: ASTNode | None):
35 """AI: Build the header text (includes, defines, declarations) needed to compile a standalone pattern snippet."""
36 # collect includes #defines and var decl from the refNode
37 header = "\n"
38 if ref_node:
39 language = ref_node.filename.split(".")[-1]
40 offset = min(
41 (
42 n.offset
43 for n in ref_node.children
44 if n.is_part_of_translation_unit() and n.parser_kind in {"InclusionDirective", "INCLUSION_DIRECTIVE"}
45 ),
46 default=0,
47 )
49 header = CPatternFactory.remove_indent(ref_node.content(0, offset))
50 header += "\n".join(
51 n.text + ";"
52 for n in ref_node.children
53 if n.is_part_of_translation_unit()
54 and (
55 n.semantic_kind in {SemanticKind.FUNCTION, SemanticKind.DECLARATION, SemanticKind.DEFINITION}
56 or is_clang_macro_definition(n)
57 )
58 and len(find_nodes(n, is_clang_compound_statement)) == 0
59 )
60 header += "\n"
62 return header, language
65class CPatternFactory:
66 """AI: Factory for building C-family AST patterns from text, with reserved placeholder names."""
68 reserved_function_name = "__rejuvenation__reserved__function__name__"
69 reserved_variable_name = "__rejuvenation__reserved__variable__name__"
71 def __init__(
72 self,
73 factory: ASTFactory,
74 ref_node: ASTNode | None = None,
75 language: str = "c",
76 ) -> None:
77 """AI: Prepare a pattern factory for creating C-family AST patterns from text."""
78 self.factory = factory
79 self.header, self.language = derive_header_text(language, ref_node)
81 @staticmethod
82 def remove_indent(text: str) -> str:
83 """AI: Strip the common leading indentation shared by every non-blank line of text."""
84 split = [len(line) - len(line.lstrip()) for line in text.splitlines() if line.strip()]
85 indent = split[0] if split else 0
86 return "\n".join([line[indent:] for line in text.splitlines()])
88 def create_expression(self, text: str, extra_declarations=None) -> ASTNode:
89 """AI: Create an AST node for the given C/C++ expression text."""
90 if extra_declarations is None:
91 extra_declarations = []
92 keywords = CPatternFactory._get_keywords_from_text(text)
93 keywords = [k for k in keywords if not any(k in ed for ed in extra_declarations)]
94 full_text = (
95 self.header
96 + "\n".join(extra_declarations)
97 + "\n"
98 + "\n".join(CPatternFactory._to_declaration(keywords))
99 + f"\nvoid {CPatternFactory.reserved_function_name}() {{ int {CPatternFactory.reserved_variable_name} = ({text}); }}"
100 )
101 root = self._create(full_text)
102 # return the first expression found in the tree as a ASTNode
103 return last(
104 n.children[0]
105 for n in find_nodes(root.children[-1], lambda node: is_clang_kind(node, "ParenExpr", "PAREN_EXPR"))
106 if n.is_part_of_translation_unit
107 )
109 def create_declarations(
110 self,
111 text: str,
112 types=None,
113 parameters=None,
114 extra_declarations=None,
115 declarations=None,
116 ):
117 """AI: Create the AST declaration nodes found in the given C/C++ text."""
118 if declarations is None:
119 declarations = []
120 if extra_declarations is None:
121 extra_declarations = []
122 if parameters is None:
123 parameters = []
124 if types is None:
125 types = []
126 keywords = CPatternFactory._get_keywords_from_text(text)
127 keywords = [
128 k
129 for k in keywords
130 if not any(k in ed for ed in extra_declarations)
131 and not any(k in ed for ed in parameters)
132 and not any(k in ed for ed in types)
133 and not any(k in ed for ed in declarations)
134 ]
135 return self._create_body(
136 text,
137 types,
138 [*parameters, *keywords],
139 extra_declarations,
140 lambda node: is_clang_kind(node, "DeclStmt", "DECL_STMT") or node.semantic_kind is SemanticKind.DECLARATION,
141 )
143 def create_declaration(
144 self,
145 text: str,
146 types=None,
147 parameters=None,
148 extra_declarations=None,
149 declarations=None,
150 ) -> ASTNode:
151 """AI: Create the single AST declaration node found in the given C/C++ text."""
152 if declarations is None:
153 declarations = []
154 if extra_declarations is None:
155 extra_declarations = []
156 if parameters is None:
157 parameters = []
158 if types is None:
159 types = []
160 result = self.create_declarations(text, types, parameters, extra_declarations, declarations)
161 assert len(result) > 0, "At least one declaration is expected"
162 return result[0]
164 def create_statements(
165 self,
166 text: str,
167 types=None,
168 extra_declarations=None,
169 kind=None,
170 ) -> Sequence[ASTNode]:
171 """AI: Create the AST statement nodes found in the given C/C++ text."""
172 # create a reference for all used variables excluding the specified types
173 if extra_declarations is None:
174 extra_declarations = []
175 if types is None:
176 types = []
177 parameters = [
178 par
179 for par in CPatternFactory._get_keywords_from_text(text)
180 if par not in types and not any(par in ed for ed in extra_declarations)
181 ]
182 return self._create_body(text, types, parameters, extra_declarations, kind)
184 def create(self, text: str, kind=None) -> ASTNode:
185 """Create an object using the factory from the provided text.
187 The object is created by the factory using the provided text and the header of the provided reference node.
188 It is up to the user to pick the right node for pattern matching.
190 Args:
191 text (str): The input text used to create the object.
192 kind (str, optional): The kind of the node to be returned. Defaults to None.
194 Returns:
195 object: The object created by the factory.
197 """
198 # print(self.header + text)
199 root = self.factory.create_from_text(self.header + text, "test." + self.language)
200 if kind:
201 return first(find_nodes(root.children[-1], lambda node: _matches_kind(node, kind)))
202 return root
204 def create_statement(
205 self,
206 text: str,
207 types=None,
208 extra_declarations=None,
209 kind=None,
210 ) -> ASTNode:
211 """AI: Create the single AST statement node found in the given C/C++ text."""
212 if extra_declarations is None:
213 extra_declarations = []
214 if types is None:
215 types = []
216 statements = list(self.create_statements(text, types, extra_declarations, kind))
217 assert len(statements) == 1, "Only one statement is expected"
218 return statements[0]
220 def _create_body(
221 self,
222 text: str,
223 types: Sequence[str],
224 parameters: Sequence[str],
225 extra_declarations: Sequence[str],
226 kind,
227 ) -> list[ASTNode]:
228 full_text = (
229 self.header
230 + "\n".join(CPatternFactory._to_typedef(types))
231 + "\n\n".join(CPatternFactory._to_declaration(parameters))
232 + "\n\n".join(extra_declarations)
233 + "\n"
234 "\nvoid " + CPatternFactory.reserved_function_name + "(){\n" + text + "\n}"
235 )
236 root = self._create(full_text)
238 # from the children of the compound statement that contains the text, get for each child the first
239 # node of the specified kind
241 body = first(find_nodes(root.children[-1], is_clang_compound_statement)).children
242 return list(n for n in body if n.is_part_of_translation_unit and first(find_nodes(n, lambda node: _matches_kind(node, kind))))
244 def _create(self, text: str) -> ASTNode:
245 atu = self.factory.create_from_text(text, "test." + self.language)
246 if SHOW_NODE:
247 ASTShower.show_node(atu)
248 return atu
250 @staticmethod
251 def _get_keywords_from_text(text: str) -> Sequence[str]:
252 # regex to get keywords that start with one of two dollars followed by a \\w+
253 pattern = re.compile(r"\${0,2}[a-zA-Z]\w*")
254 return list(k for k in set(re.findall(pattern, text)) if k not in CPPUtils.RESERVED_KEYWORDS)
256 @staticmethod
257 def _get_dollar_keywords_from_text(text: str) -> Sequence[str]:
258 # regex to get keywords that start with one of two dollars followed by a \\w+
259 pattern = re.compile(r"\${1,2}[a-zA-Z]\w*")
260 return list(set(re.findall(pattern, text)))
262 @staticmethod
263 def _get_non_dollar_keywords_from_text(text: str) -> Sequence[str]:
264 pattern = re.compile(r"[^$][a-zA-Z]\w*")
265 return list(set(re.findall(pattern, text)))
267 @staticmethod
268 def _to_declaration(keywords: Sequence[str], prefix: str = "int ", postfix: str = ";") -> Sequence[str]:
269 return [prefix + keyword + postfix for keyword in keywords]
271 @staticmethod
272 def _to_typedef(keywords: Sequence[str], prefix: str = "typedef int ", postfix: str = ";") -> Sequence[str]:
273 return [prefix + keyword + postfix for keyword in keywords]
276class CPPPatternFactory(CPatternFactory):
277 """AI: Factory for building C++-specific AST patterns, extending CPatternFactory with constructor-call support."""
279 def __init__(self, factory: ASTFactory, ref_node: ASTNode | None = None) -> None:
280 """AI: Prepare a pattern factory for creating C++-specific AST patterns from text."""
281 super().__init__(factory, ref_node, "cpp")
283 def create_constructor_call(self, pattern: str):
284 """AI: Create an AST node for a C++ constructor call matching the given pattern."""
285 class_and_args = re.match(R"([$\w]+)\(([^)]+)\)", pattern.replace(" ", ""))
286 if class_and_args:
287 class_name = class_and_args.group(1)
288 args = class_and_args.group(2).split(",")
289 return self._create_constructor_call(class_name, args)
290 return None
292 def _create_constructor_call(self, class_name: str, args=None):
293 if args is None:
294 args = []
295 arg_call_string = ",".join(args)
296 arg_decl_string = ",".join("int " + arg for arg in args)
297 code = f"""
298 class {class_name}{{
299 public:
300 {class_name}({arg_decl_string}) {{}}
301 }};
302 class derived : public {class_name}{{
303 public:
304 derived({arg_decl_string}) : {class_name}({arg_call_string}) {{ }}
305 }};
306 """
307 root: ASTNode = self.factory.create_from_text(code, "test." + self.language)
308 target_class = root.children[-1]
309 # this should yield something like:
310 # (TYPE_REF, $var, test.cpp[237:241]): |$var|
311 # (CALL_EXPR, , test.cpp[237:266]): |$var($container,$headerCount)|
312 # (DECL_REF_EXPR, $container, test.cpp[242:252]): |$container|
313 # (DECL_REF_EXPR, $headerCount, test.cpp[253:265]): |$headerCount|
314 if SHOW_NODE:
315 ASTShower.show_node(target_class)
316 # search the call expr and the preceding type ref
317 call_expr = last(find_nodes(target_class, lambda node: node.semantic_kind is SemanticKind.CALL))
318 # include the preceding type ref
319 assert isinstance(call_expr, ASTNode), "No call expression found"
320 type_ref = call_expr.preceding_sibling
321 assert isinstance(type_ref, ASTNode), "No type ref found"
322 # return the constrained pattern where the first node must be of type TypeRef
324 return call_expr