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