ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
annot_walk.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Turning a parsed translation unit into a symbol table and a call list.
4
5One pass over the AST fills three things the rules then reason over: the
6USR-keyed symbol table, every call site, and the set of functions a hardware
7vector table names.
8
9Identity is the whole game here
10-------------------------------
11Symbols are keyed by USR (:func:`symbol_key`), never by name. C lets every
12translation unit have its own ``static`` helper with the same name and this
13repo does exactly that -- three unrelated ``internal_zero_bytes`` live in
14net_pal, usb_hmsc and usb_pmsc. A name-keyed table merges them into one entry
15whose annotations are the union of all three, so an annotation on one namesake
16gets enforced against another's callers. That is not hypothetical: it took
17``dev`` red, and the selftest still reproduces the exact walk order that did it.
18
19Call sites record the USR on *both* ends for the same reason. A callee-keyed
20lookup that used names would attribute one module's RA8_PRIV tag to another
21module's file-local helper of the same name.
22"""
23
24from __future__ import annotations
25
26import contextlib
27import re
28
29from annot_clang import SECTION_ATTR_KIND, cindex
30from annot_model import AnnotatedSymbol, CallSite, DataSymbol, WalkState
31from annot_rulekeys import ANNOTATION_PREFIXES
32from annot_source import source_lines
33
34#: A section name the linker script maps to a hardware vector table.
35#: ``.vectors`` for the M85 images, ``.cpu1_vectors`` for the M33 ones.
36_VECTOR_SECTION_RE = re.compile(r'section\s*\‍(\s*"(\.[a-z0-9_]*vectors)"')
37
38#: How many lines above a declaration to search for its section attribute.
39VECTOR_ATTR_LOOKBACK_LINES = 3
40
41
42def collect_annotations(cursor: cindex.Cursor) -> list[str]:
43 """Return every ra8_* string attached to a cursor via AnnotateAttr."""
44 out: list[str] = []
45 for child in cursor.get_children():
46 if child.kind == cindex.CursorKind.ANNOTATE_ATTR:
47 text = child.spelling or child.displayname or ""
48 if text.startswith(ANNOTATION_PREFIXES):
49 out.append(text)
50 return out
51
52
53def usr_of(cursor: cindex.Cursor) -> str:
54 """Return the canonical USR of ``cursor``, or '' when unavailable."""
55 with contextlib.suppress(Exception):
56 return cursor.canonical.get_usr()
57 return ""
58
59
60def symbol_key(cursor: cindex.Cursor) -> str:
61 """Return the symbol-table key for a function cursor.
62
63 The USR when libclang can produce one -- it encodes the defining file
64 for internal-linkage symbols, which is the only thing that separates
65 same-named ``static`` helpers in different modules. A libclang build
66 that cannot produce USRs degrades to name plus file, which keeps the
67 namesakes apart for the common case rather than merging them.
68 """
69 usr = usr_of(cursor)
70 if usr:
71 return usr
72 where = str(cursor.location.file) if cursor.location.file else ""
73 return f"{cursor.spelling}@{where}"
74
75
76def _file_of(cursor: cindex.Cursor) -> str:
77 """Return the path holding ``cursor``, or '' when it has no location."""
78 if cursor.location.file is None:
79 return ""
80 return str(cursor.location.file)
81
82
83def _has_vector_section(cursor: cindex.Cursor) -> bool:
84 """True when ``cursor``'s declaration carries a ``*vectors`` section."""
85 if cursor.location.file is None:
86 return False
87 lines = source_lines(str(cursor.location.file))
88 if not lines:
89 return False
90 # The attribute sits on the declaration line or just above it; C23
91 # attributes may be split across a couple of lines by the formatter.
92 first = max(0, cursor.extent.start.line - VECTOR_ATTR_LOOKBACK_LINES)
93 return bool(_VECTOR_SECTION_RE.search("\n".join(lines[first : cursor.extent.start.line])))
94
95
96def is_vector_table(cursor: cindex.Cursor) -> bool:
97 """True when ``cursor`` declares a hardware exception vector table.
98
99 Recognised by what makes it a vector table, never by name. Either:
100
101 * a file-scope array of ``const`` pointers to functions -- the shape
102 the Cortex-M core reads through VTOR, which nothing else in this
103 tree has; or
104 * a file-scope ``const`` array the linker places in a ``*vectors``
105 section. The two-entry copy-to-run payload table and the M33
106 ``.cpu1_vectors`` table are ``const uintptr_t[]``, so the type alone
107 does not identify them, but the section placement does -- that
108 placement is the whole reason the array exists.
109
110 The section is read out of the declaration's source text because
111 ``CursorKind.SECTION_ATTR`` is absent from the libclang 18.1.x wheels
112 the runners install, so an AST lookup would silently find nothing.
113 """
114 if cursor.kind != cindex.CursorKind.VAR_DECL:
115 return False
116 parent = cursor.semantic_parent
117 if parent is None or parent.kind != cindex.CursorKind.TRANSLATION_UNIT:
118 return False
119 array = cursor.type
120 if array.kind not in (cindex.TypeKind.CONSTANTARRAY, cindex.TypeKind.INCOMPLETEARRAY):
121 return False
122 element = array.element_type
123 if not element.is_const_qualified():
124 return False
125 canonical = element.get_canonical()
126 if (
127 canonical.kind == cindex.TypeKind.POINTER
128 and canonical.get_pointee().get_canonical().kind == cindex.TypeKind.FUNCTIONPROTO
129 ):
130 return True
131 return _has_vector_section(cursor)
132
133
134class _Walker:
135 """One traversal, accumulating into the caller's collections.
136
137 A class rather than a nest of closures so each construct the walk knows
138 about -- function declaration, call, address-of, vector table -- is a
139 separately readable method instead of another branch in one long visitor.
140 """
141
142 def __init__(
143 self,
144 state: WalkState,
145 ) -> None:
146 self.symbols = state.symbols
147 self.data_symbols = state.data_symbols
148 self.calls = state.calls
149 self.stats = state.stats
150 self.vector_entries = state.vector_entries
151
152 def _record_vector_entries(self, node: cindex.Cursor) -> None:
153 """Collect the USR of every function named in a vector table."""
154 if (
155 node.kind == cindex.CursorKind.DECL_REF_EXPR
156 and node.referenced is not None
157 and node.referenced.kind == cindex.CursorKind.FUNCTION_DECL
158 ):
159 self.vector_entries.add(symbol_key(node.referenced))
160 for child in node.get_children():
161 self._record_vector_entries(child)
162
163 def _record_data(self, node: cindex.Cursor) -> None:
164 """Record one data definition without confusing locals with statics."""
165 if node.location.file is None:
166 return
167 parent = node.semantic_parent
168 file_scope = parent is not None and parent.kind in (
169 cindex.CursorKind.TRANSLATION_UNIT,
170 cindex.CursorKind.NAMESPACE,
171 )
172 # C's uninitialised file-scope ``static`` is a tentative definition;
173 # libclang reports ``is_definition() == false`` even though the TU
174 # owns storage for it. It is still exactly the data the naming rule
175 # must classify.
176 if not node.is_definition() and not (
177 file_scope and node.storage_class == cindex.StorageClass.STATIC
178 ):
179 return
180 internal = (
181 node.linkage == cindex.LinkageKind.INTERNAL
182 or node.storage_class == cindex.StorageClass.STATIC
183 )
184 key = usr_of(node) or f"{_file_of(node)}:{node.location.line}:{node.spelling}"
185 self.data_symbols.setdefault(
186 key,
187 DataSymbol(
188 name=node.spelling,
189 file=_file_of(node),
190 line=node.location.line,
191 is_file_scope=file_scope,
192 has_internal_linkage=internal,
193 ),
194 )
195
196 def _symbol_for(self, node: cindex.Cursor) -> AnnotatedSymbol:
197 """Return (creating if needed) the table entry for a function cursor."""
198 key = symbol_key(node)
199 return self.symbols.setdefault(
200 key,
201 AnnotatedSymbol(
202 name=node.spelling, file=_file_of(node), line=node.location.line, usr=key
203 ),
204 )
205
206 @staticmethod
207 def _update_declaration(sym: AnnotatedSymbol, node: cindex.Cursor) -> None:
208 """Fold one declaration or definition of a function into its entry."""
209 for a in collect_annotations(node):
210 if a not in sym.annotations:
211 sym.annotations.append(a)
212 # RA8_INTERNAL promises the declaration actually spells ``static``.
213 # C++ anonymous namespaces also have internal linkage, but that is a
214 # different mechanism and must not satisfy a contract about storage
215 # class by accident.
216 sym.is_static = node.storage_class == cindex.StorageClass.STATIC or sym.is_static
217 sym.has_internal_linkage = (
218 node.linkage == cindex.LinkageKind.INTERNAL or sym.has_internal_linkage
219 )
220 sym.return_type = node.result_type.spelling
221 if any(arg.type.kind == cindex.TypeKind.POINTER for arg in node.get_arguments()):
222 sym.has_pointer_param = True
223 if node.is_definition():
224 sym.is_defined = True
225 sym.file = _file_of(node)
226 sym.line = node.location.line
227 sym.end_line = node.extent.end.line
228 else:
229 sym.decl_files.add(_file_of(node))
230 # libclang exposes inline-ness via the cursor's tokens
231 tokens = [t.spelling for t in node.get_tokens()]
232 if "inline" in tokens or "__inline__" in tokens:
233 sym.has_inline = True
234 # Section attribute (only when this libclang exposes the kind).
235 if SECTION_ATTR_KIND is not None:
236 for child in node.get_children():
237 if child.kind == SECTION_ATTR_KIND:
238 sym.section = child.spelling
239
240 def _visit_function(self, node: cindex.Cursor, current_func: cindex.Cursor | None) -> None:
241 """Record a function declaration, then descend into its body."""
242 self._update_declaration(self._symbol_for(node), node)
243 inner = node if node.is_definition() else current_func
244 for child in node.get_children():
245 self.visit(child, inner)
246
247 def _record_call(self, node: cindex.Cursor, current_func: cindex.Cursor) -> None:
248 """Record one direct call, and whether its callee resolved."""
249 self.stats.calls_seen += 1
250 callee = node.referenced
251 if callee is None:
252 return
253 self.stats.calls_resolved += 1
254 self.calls.append(
255 CallSite(
256 callee_name=callee.spelling,
257 caller_name=current_func.spelling,
258 caller_file=_file_of(node),
259 caller_line=node.location.line,
260 callee_usr=symbol_key(callee),
261 caller_usr=symbol_key(current_func),
262 )
263 )
264
265 def _record_address_of(self, node: cindex.Cursor, current_func: cindex.Cursor | None) -> None:
266 """Record ``&foo`` references, which are uses but not direct calls."""
267 tokens = [t.spelling for t in node.get_tokens()]
268 if not tokens or tokens[0] != "&":
269 return
270 self.calls.extend(
271 CallSite(
272 callee_name=child.referenced.spelling,
273 caller_name=(current_func.spelling if current_func else ""),
274 caller_file=_file_of(node),
275 caller_line=node.location.line,
276 callee_usr=symbol_key(child.referenced),
277 caller_usr=(symbol_key(current_func) if current_func else ""),
278 in_address_of=True,
279 )
280 for child in node.get_children()
281 if (
282 child.kind == cindex.CursorKind.DECL_REF_EXPR
283 and child.referenced
284 and child.referenced.kind == cindex.CursorKind.FUNCTION_DECL
285 )
286 )
287
288 def visit(self, node: cindex.Cursor, current_func: cindex.Cursor | None) -> None:
289 """Dispatch one cursor, then recurse into its children."""
290 if node.kind == cindex.CursorKind.FUNCTION_DECL:
291 self._visit_function(node, current_func)
292 return
293
294 if node.kind == cindex.CursorKind.VAR_DECL:
295 self._record_data(node)
296
297 if is_vector_table(node):
298 self._record_vector_entries(node)
299
300 if node.kind == cindex.CursorKind.CALL_EXPR and current_func is not None:
301 self._record_call(node, current_func)
302
303 if node.kind == cindex.CursorKind.UNARY_OPERATOR:
304 self._record_address_of(node, current_func)
305
306 for child in node.get_children():
307 self.visit(child, current_func)
308
309
310def walk_tu(
311 tu: cindex.TranslationUnit,
312 state: WalkState,
313) -> None:
314 """Fill function, data, call-site, and vector-table records in one pass."""
315 _Walker(state).visit(tu.cursor, None)