3"""Turning a parsed translation unit into a symbol table and a call list.
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
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.
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.
24from __future__
import annotations
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
36_VECTOR_SECTION_RE = re.compile(
r'section\s*\(\s*"(\.[a-z0-9_]*vectors)"')
39VECTOR_ATTR_LOOKBACK_LINES = 3
42def collect_annotations(cursor: cindex.Cursor) -> list[str]:
43 """Return every ra8_* string attached to a cursor via AnnotateAttr."""
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):
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()
60def symbol_key(cursor: cindex.Cursor) -> str:
61 """Return the symbol-table key for a function cursor.
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.
72 where = str(cursor.location.file)
if cursor.location.file
else ""
73 return f
"{cursor.spelling}@{where}"
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:
80 return str(cursor.location.file)
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:
87 lines = source_lines(str(cursor.location.file))
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])))
96def is_vector_table(cursor: cindex.Cursor) -> bool:
97 """True when ``cursor`` declares a hardware exception vector table.
99 Recognised by what makes it a vector table, never by name. Either:
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
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.
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.
114 if cursor.kind != cindex.CursorKind.VAR_DECL:
116 parent = cursor.semantic_parent
117 if parent
is None or parent.kind != cindex.CursorKind.TRANSLATION_UNIT:
120 if array.kind
not in (cindex.TypeKind.CONSTANTARRAY, cindex.TypeKind.INCOMPLETEARRAY):
122 element = array.element_type
123 if not element.is_const_qualified():
125 canonical = element.get_canonical()
127 canonical.kind == cindex.TypeKind.POINTER
128 and canonical.get_pointee().get_canonical().kind == cindex.TypeKind.FUNCTIONPROTO
131 return _has_vector_section(cursor)
135 """One traversal, accumulating into the caller's collections.
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.
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
152 def _record_vector_entries(self, node: cindex.Cursor) ->
None:
153 """Collect the USR of every function named in a vector table."""
155 node.kind == cindex.CursorKind.DECL_REF_EXPR
156 and node.referenced
is not None
157 and node.referenced.kind == cindex.CursorKind.FUNCTION_DECL
159 self.vector_entries.add(symbol_key(node.referenced))
160 for child
in node.get_children():
161 self._record_vector_entries(child)
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:
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,
176 if not node.is_definition()
and not (
177 file_scope
and node.storage_class == cindex.StorageClass.STATIC
181 node.linkage == cindex.LinkageKind.INTERNAL
182 or node.storage_class == cindex.StorageClass.STATIC
184 key = usr_of(node)
or f
"{_file_of(node)}:{node.location.line}:{node.spelling}"
185 self.data_symbols.setdefault(
190 line=node.location.line,
191 is_file_scope=file_scope,
192 has_internal_linkage=internal,
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(
202 name=node.spelling, file=_file_of(node), line=node.location.line, usr=key
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)
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
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
229 sym.decl_files.add(_file_of(node))
231 tokens = [t.spelling
for t
in node.get_tokens()]
232 if "inline" in tokens
or "__inline__" in tokens:
233 sym.has_inline =
True
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
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)
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
253 self.stats.calls_resolved += 1
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),
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] !=
"&":
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 ""),
280 for child
in node.get_children()
282 child.kind == cindex.CursorKind.DECL_REF_EXPR
284 and child.referenced.kind == cindex.CursorKind.FUNCTION_DECL
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)
294 if node.kind == cindex.CursorKind.VAR_DECL:
295 self._record_data(node)
297 if is_vector_table(node):
298 self._record_vector_entries(node)
300 if node.kind == cindex.CursorKind.CALL_EXPR
and current_func
is not None:
301 self._record_call(node, current_func)
303 if node.kind == cindex.CursorKind.UNARY_OPERATOR:
304 self._record_address_of(node, current_func)
306 for child
in node.get_children():
307 self.visit(child, current_func)
311 tu: cindex.TranslationUnit,
314 """Fill function, data, call-site, and vector-table records in one pass."""
315 _Walker(state).visit(tu.cursor,
None)