| """ |
| Tree-sitter C parser for call graph extraction. |
| |
| Tries to use tree-sitter + tree-sitter-c; falls back to regex-based |
| extraction if the grammar is not available. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import re |
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Optional |
|
|
| |
| |
| |
|
|
|
|
| @dataclass |
| class FunctionSignature: |
| """Parsed signature of a C function.""" |
|
|
| name: str |
| return_type: str |
| parameters: list[tuple[str, str]] |
| is_static: bool = False |
|
|
|
|
| @dataclass |
| class FunctionInfo: |
| """All information about a single C function.""" |
|
|
| name: str |
| signature: FunctionSignature |
| body: str |
| callees: set[str] |
| source_file: str |
|
|
|
|
| @dataclass |
| class ParsedCFile: |
| """Result of parsing a C source file.""" |
|
|
| path: str |
| functions: dict[str, FunctionSignature] |
| call_graph: dict[str, set[str]] |
| function_bodies: dict[str, str] |
| |
| |
| |
| |
| |
| function_definitions: dict[str, str] = field(default_factory=dict) |
| |
| |
| |
| |
| |
| |
| |
| |
| struct_definitions: dict[str, list[tuple[str, str]]] = field(default_factory=dict) |
| |
| |
| |
| preprocessed_source: Optional[str] = None |
| |
| |
| |
| |
| |
| |
| |
| function_source_files: dict[str, str] = field(default_factory=dict) |
| |
| |
| |
| |
| |
| |
| |
| primary_source: Optional[str] = None |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| address_taken_in: dict[str, set[str]] = field(default_factory=dict) |
|
|
| def restrict_to_primary_source(self) -> int: |
| """Drop functions whose body did NOT originate in |
| ``self.primary_source``. No-op if ``primary_source`` is None or |
| ``function_source_files`` is empty (i.e. the input wasn't |
| preprocessed and we have no provenance info). |
| |
| Returns the number of functions dropped, so the caller can log |
| the filtering action. |
| """ |
| if not self.primary_source or not self.function_source_files: |
| return 0 |
| primary_base = self.primary_source.rsplit("/", 1)[-1] |
| keep: set[str] = set() |
| for name, origin in self.function_source_files.items(): |
| if not origin: |
| continue |
| if origin == self.primary_source: |
| keep.add(name) |
| continue |
| |
| |
| |
| if origin.rsplit("/", 1)[-1] == primary_base: |
| keep.add(name) |
| dropped = [n for n in list(self.functions) if n not in keep] |
| for n in dropped: |
| self.functions.pop(n, None) |
| self.function_bodies.pop(n, None) |
| self.function_definitions.pop(n, None) |
| self.call_graph.pop(n, None) |
| self.function_source_files.pop(n, None) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| return len(dropped) |
|
|
| def get_function_info(self, name: str) -> Optional["FunctionInfo"]: |
| """Return a FunctionInfo for the named function, or None if not found.""" |
| if name not in self.functions: |
| return None |
| return FunctionInfo( |
| name=name, |
| signature=self.functions[name], |
| body=self.function_bodies.get(name, ""), |
| callees=self.call_graph.get(name, set()), |
| source_file=self.path, |
| ) |
|
|
| def all_function_infos(self) -> list["FunctionInfo"]: |
| """Return FunctionInfo for every parsed function.""" |
| return [self.get_function_info(n) for n in self.functions] |
|
|
|
|
| |
| |
| |
|
|
| _TS_AVAILABLE = False |
| _TS_LANGUAGE = None |
|
|
|
|
| def _try_load_tree_sitter() -> None: |
| """Attempt to load the tree-sitter C grammar; set _TS_AVAILABLE on success.""" |
| global _TS_AVAILABLE, _TS_LANGUAGE |
| if _TS_AVAILABLE: |
| return |
|
|
| try: |
| import tree_sitter_c as tsc |
| from tree_sitter import Language |
|
|
| |
| if hasattr(tsc, "language"): |
| _TS_LANGUAGE = Language(tsc.language()) |
| else: |
| |
| _TS_LANGUAGE = Language(tsc.__file__, "c") |
| _TS_AVAILABLE = True |
| except Exception: |
| _TS_AVAILABLE = False |
|
|
|
|
| |
| |
| |
|
|
|
|
| def parse_c_file( |
| path: str | Path, |
| source_text: Optional[str] = None, |
| ) -> ParsedCFile: |
| """ |
| Parse a C source file and return function signatures + call graph. |
| |
| Parameters |
| ---------- |
| path: |
| Path to the original ``.c`` file (used for artifact naming). |
| source_text: |
| If provided, parse this string instead of reading *path* from disk. |
| Use this to pass preprocessed / expanded source. |
| |
| Uses tree-sitter if available; otherwise falls back to regex. |
| """ |
| path = Path(path) |
| provided_source = source_text is not None |
| if source_text is None: |
| source_bytes = path.read_bytes() |
| source_text = source_bytes.decode("utf-8", errors="replace") |
| else: |
| source_bytes = source_text.encode("utf-8", errors="replace") |
|
|
| _try_load_tree_sitter() |
|
|
| if _TS_AVAILABLE: |
| try: |
| result = _parse_with_tree_sitter(source_bytes, source_text, str(path)) |
| if provided_source or result.primary_source: |
| |
| |
| |
| |
| |
| |
| result.preprocessed_source = source_text |
| return result |
| except Exception: |
| pass |
|
|
| result = _parse_with_regex(source_text, str(path)) |
| if provided_source or result.primary_source: |
| result.preprocessed_source = source_text |
| return result |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _build_line_to_source_map(source: str) -> tuple[list[str], Optional[str]]: |
| """Walk cpp ``# N "filename" [flags]`` line directives and return |
| ``(line_to_source, primary_source)``: |
| |
| * ``line_to_source[i]`` is the originating source filename for the |
| 0-indexed line ``i`` (empty string for lines that fall outside any |
| directive). |
| * ``primary_source`` is the first non-cpp-synthetic filename seen |
| (i.e. the original ``.c`` the TU was built from), or ``None`` if |
| the input has no cpp line directives. |
| |
| The map covers each non-directive line. Directive lines themselves |
| get the file they introduce — they're tagged with the same source as |
| the lines below them. |
| """ |
| lines = source.split("\n") |
| line_to_source: list[str] = [""] * len(lines) |
| primary: Optional[str] = None |
| current: str = "" |
| |
| pat = re.compile(r'^#\s+\d+\s+"([^"]+)"') |
| for i, line in enumerate(lines): |
| m = pat.match(line) |
| if m: |
| current = m.group(1) |
| |
| |
| if primary is None and not (current.startswith("<") and current.endswith(">")): |
| primary = current |
| line_to_source[i] = current |
| return line_to_source, primary |
|
|
|
|
| def _parse_with_tree_sitter(src_bytes: bytes, source: str, path: str) -> ParsedCFile: |
| """Parse using tree-sitter. Uses byte offsets for all node slicing.""" |
| from tree_sitter import Parser |
|
|
| parser = Parser(_TS_LANGUAGE) |
| tree = parser.parse(src_bytes) |
| root = tree.root_node |
|
|
| functions: dict[str, FunctionSignature] = {} |
| call_graph: dict[str, set[str]] = {} |
| function_bodies: dict[str, str] = {} |
| function_definitions: dict[str, str] = {} |
| function_source_files: dict[str, str] = {} |
| line_to_source, primary_source = _build_line_to_source_map(source) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _PREPROC_CONTAINER_TYPES = { |
| "preproc_if", "preproc_ifdef", "preproc_ifndef", |
| "preproc_else", "preproc_elif", "preproc_elifdef", "preproc_elifndef", |
| "linkage_specification", |
| "compound_statement", |
| "ERROR", |
| } |
|
|
| def _collect_function_defs(node): |
| """Yield every function_definition node, recursing through |
| preprocessor / linkage wrappers and parse-error-recovery |
| compound_statement wrappers. |
| |
| At a ``function_definition`` we yield the node *and* recurse |
| into its compound_statement body. The recursion is needed |
| because tree-sitter's error recovery on macro-heavy kernel TUs |
| often nests trailing functions inside an earlier function's |
| body (parent chain: ``function_definition → compound_statement |
| → function_definition``). Without this, ``hid-pidff.c``'s |
| ``pidff_rescale`` and ~10 siblings vanish. Real GCC nested |
| function defs are extremely rare in kernel/driver code, so the |
| spurious recurse cost is negligible. |
| """ |
| if node.type == "function_definition": |
| yield node |
| for child in node.children: |
| if child.type == "compound_statement": |
| for sub in child.children: |
| yield from _collect_function_defs(sub) |
| return |
| if node.type in _PREPROC_CONTAINER_TYPES or node.type == "translation_unit": |
| for child in node.children: |
| yield from _collect_function_defs(child) |
|
|
| for node in _collect_function_defs(root): |
| sig = _extract_sig_ts(node, src_bytes) |
| if sig: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| true_end = _brace_balanced_end_byte( |
| src_bytes, node.start_byte, node.end_byte |
| ) |
|
|
| new_def_text = src_bytes[ |
| node.start_byte:true_end |
| ].decode("utf-8", errors="replace") |
| |
| body_node = node.child_by_field_name("body") |
| new_body_text = "" |
| new_callees: set[str] = set() |
| if body_node: |
| body_end = _brace_balanced_end_byte( |
| src_bytes, body_node.start_byte, body_node.end_byte |
| ) |
| new_body_text = src_bytes[ |
| body_node.start_byte:body_end |
| ].decode("utf-8", errors="replace") |
| _collect_calls_ts(body_node, new_callees, src_bytes) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| prev_body = function_bodies.get(sig.name) |
| if prev_body is not None and len(new_body_text) <= len(prev_body): |
| continue |
|
|
| functions[sig.name] = sig |
| call_graph[sig.name] = new_callees |
| function_definitions[sig.name] = new_def_text |
| row = node.start_point[0] |
| if 0 <= row < len(line_to_source): |
| function_source_files[sig.name] = line_to_source[row] |
| if new_body_text: |
| function_bodies[sig.name] = new_body_text |
|
|
| struct_definitions = _collect_struct_defs(root, src_bytes) |
|
|
| address_taken_in = _compute_address_takers(functions, function_bodies) |
|
|
| return ParsedCFile( |
| path=path, |
| functions=functions, |
| call_graph=call_graph, |
| function_bodies=function_bodies, |
| function_definitions=function_definitions, |
| struct_definitions=struct_definitions, |
| function_source_files=function_source_files, |
| primary_source=primary_source, |
| address_taken_in=address_taken_in, |
| ) |
|
|
|
|
| def _brace_balanced_end_byte(src_bytes: bytes, start: int, ts_end: int) -> int: |
| """Return the byte offset of the function's true closing ``}``. |
| |
| Tree-sitter occasionally truncates a function_definition's |
| ``end_byte`` on macro-heavy kernel bodies (FIELD_PREP + |
| _Static_assert inside ``struct{}`` inside GCC statement-expressions |
| ``({ ... })``). The grammar mistakes a ``}`` of an inner expression |
| for the body close. |
| |
| Count ``{`` / ``}`` over the captured slice, skipping over string |
| literals, character literals, and ``/* */`` / ``//`` comments. If |
| the count is positive (more ``{`` than ``}``), walk forward from |
| ``ts_end`` byte-by-byte (using the same skip rules) until the count |
| reaches zero. Return that offset (inclusive of the final ``}``). |
| |
| Conservative fallback: if walking forward never balances within a |
| safety cap, return the original ``ts_end`` unchanged. Better to |
| leave a faulty bound than chew the rest of the TU. |
| """ |
| |
| |
| |
| depth = 0 |
| i = start |
| end = ts_end |
| n = len(src_bytes) |
| |
| |
| |
| |
| cap = min(n, ts_end + 200_000) |
|
|
| def _skip_string_or_char(j: int, quote: int) -> int: |
| j += 1 |
| while j < n and src_bytes[j] != quote: |
| if src_bytes[j] == 0x5C: |
| j += 2 |
| else: |
| j += 1 |
| return j + 1 |
|
|
| def _skip_block_comment(j: int) -> int: |
| k = src_bytes.find(b"*/", j + 2) |
| return k + 2 if k != -1 else n |
|
|
| def _skip_line_comment(j: int) -> int: |
| k = src_bytes.find(b"\n", j + 2) |
| return k + 1 if k != -1 else n |
|
|
| |
| while i < end: |
| b = src_bytes[i] |
| if b == 0x22: |
| i = _skip_string_or_char(i, 0x22) |
| continue |
| if b == 0x27: |
| i = _skip_string_or_char(i, 0x27) |
| continue |
| if b == 0x2F and i + 1 < n: |
| nxt = src_bytes[i + 1] |
| if nxt == 0x2A: |
| i = _skip_block_comment(i) |
| continue |
| if nxt == 0x2F: |
| i = _skip_line_comment(i) |
| continue |
| if b == 0x7B: |
| depth += 1 |
| elif b == 0x7D: |
| depth -= 1 |
| if depth == 0: |
| |
| return end |
| i += 1 |
|
|
| |
| |
| |
| if depth <= 0: |
| return end |
|
|
| |
| i = end |
| while i < cap: |
| b = src_bytes[i] |
| if b == 0x22: |
| i = _skip_string_or_char(i, 0x22) |
| continue |
| if b == 0x27: |
| i = _skip_string_or_char(i, 0x27) |
| continue |
| if b == 0x2F and i + 1 < n: |
| nxt = src_bytes[i + 1] |
| if nxt == 0x2A: |
| i = _skip_block_comment(i) |
| continue |
| if nxt == 0x2F: |
| i = _skip_line_comment(i) |
| continue |
| if b == 0x7B: |
| depth += 1 |
| elif b == 0x7D: |
| depth -= 1 |
| if depth == 0: |
| return i + 1 |
| i += 1 |
|
|
| |
| return ts_end |
|
|
|
|
| def _collect_struct_defs(root, src_bytes: bytes) -> dict[str, list[tuple[str, str]]]: |
| """Walk the translation unit and collect struct definitions, keyed by |
| tag name (or typedef'd alias for anonymous structs). |
| |
| Each value is a list of ``(field_type, field_name)`` pairs in |
| declaration order. Forward declarations (``struct opaque;``) are |
| skipped because they have no field_declaration_list. |
| """ |
| _PREPROC_CONTAINER_TYPES = { |
| "preproc_if", "preproc_ifdef", "preproc_ifndef", |
| "preproc_else", "preproc_elif", "preproc_elifdef", "preproc_elifndef", |
| "linkage_specification", |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| "function_definition", "compound_statement", |
| "ERROR", |
| } |
| structs: dict[str, list[tuple[str, str]]] = {} |
| |
| |
| |
| pending_aliases: list[tuple[str, str]] = [] |
|
|
| def walk(node): |
| if node.type == "struct_specifier": |
| _record_struct(node, src_bytes, structs, alias=None) |
| return |
| if node.type == "type_definition": |
| |
| |
| |
| |
| |
| |
| |
| inner_struct = None |
| inner_struct_has_body = False |
| inner_struct_tag = None |
| alias = None |
| for c in node.children: |
| if c.type == "struct_specifier": |
| inner_struct = c |
| for cc in c.children: |
| if cc.type == "type_identifier": |
| inner_struct_tag = src_bytes[cc.start_byte:cc.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| elif cc.type == "field_declaration_list": |
| inner_struct_has_body = True |
| elif c.type == "type_identifier": |
| alias = src_bytes[c.start_byte:c.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| if inner_struct is not None and inner_struct_has_body: |
| _record_struct(inner_struct, src_bytes, structs, alias=alias) |
| elif alias and inner_struct_tag: |
| |
| |
| pending_aliases.append((alias, inner_struct_tag)) |
| return |
| if node.type in _PREPROC_CONTAINER_TYPES or node.type == "translation_unit": |
| for c in node.children: |
| walk(c) |
|
|
| walk(root) |
| |
| |
| |
| |
| |
| |
| for alias, tag in pending_aliases: |
| if tag in structs and alias not in structs: |
| structs[alias] = structs[tag] |
| |
| |
| |
| |
| for tag in list(structs.keys()): |
| if tag.startswith("_"): |
| alias = tag[1:] |
| if alias and alias not in structs: |
| structs[alias] = structs[tag] |
| return structs |
|
|
|
|
| def _record_struct( |
| struct_node, |
| src_bytes: bytes, |
| structs: dict[str, list[tuple[str, str]]], |
| alias: Optional[str], |
| ) -> None: |
| """Pull (type, name) field pairs out of a struct_specifier and store |
| them under the tag name and/or typedef alias.""" |
| tag_name: Optional[str] = None |
| fdecl_list = None |
| for c in struct_node.children: |
| if c.type == "type_identifier": |
| tag_name = src_bytes[c.start_byte:c.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| elif c.type == "field_declaration_list": |
| fdecl_list = c |
| if fdecl_list is None: |
| |
| return |
|
|
| fields: list[tuple[str, str]] = [] |
| for fdecl in fdecl_list.children: |
| if fdecl.type != "field_declaration": |
| continue |
| |
| type_parts: list[str] = [] |
| declarator_node = None |
| for c in fdecl.children: |
| if c.type in { |
| "type_qualifier", "primitive_type", "type_identifier", |
| "sized_type_specifier", "struct_specifier", |
| "union_specifier", "enum_specifier", |
| }: |
| |
| |
| if c.type == "struct_specifier": |
| tag = None |
| for cc in c.children: |
| if cc.type == "type_identifier": |
| tag = src_bytes[cc.start_byte:cc.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| type_parts.append(f"struct {tag}" if tag else "struct") |
| else: |
| type_parts.append( |
| src_bytes[c.start_byte:c.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| ) |
| elif c.type in { |
| "field_identifier", "pointer_declarator", "array_declarator", |
| }: |
| declarator_node = c |
|
|
| if declarator_node is None: |
| continue |
|
|
| |
| |
| name, type_suffix = _flatten_declarator(declarator_node, src_bytes) |
| if not name: |
| continue |
| ftype = " ".join(type_parts) + type_suffix |
| fields.append((ftype.strip(), name)) |
|
|
| if not fields: |
| return |
| if tag_name: |
| structs[tag_name] = fields |
| if alias and alias not in structs: |
| structs[alias] = fields |
|
|
|
|
| def _flatten_declarator(decl_node, src_bytes: bytes) -> tuple[str, str]: |
| """Return (field_name, type_suffix) for a field declarator. |
| |
| ``type_suffix`` accumulates ``*`` stars (pointer_declarator) and |
| ``[N]`` array dimensions (array_declarator) so callers can append |
| them to the type prefix. |
| """ |
| suffix = "" |
| node = decl_node |
| while True: |
| if node.type == "pointer_declarator": |
| suffix = "*" + suffix |
| inner = node.child_by_field_name("declarator") |
| if inner is None: |
| return ("", suffix) |
| node = inner |
| elif node.type == "array_declarator": |
| |
| text = src_bytes[node.start_byte:node.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| |
| |
| |
| inner = node.child_by_field_name("declarator") |
| if inner is None: |
| return ("", suffix) |
| |
| bracket_idx = text.find("[") |
| if bracket_idx >= 0: |
| suffix = suffix + text[bracket_idx:] |
| node = inner |
| elif node.type == "field_identifier": |
| name = src_bytes[node.start_byte:node.end_byte].decode( |
| "utf-8", errors="replace" |
| ) |
| return (name, suffix) |
| else: |
| return ("", suffix) |
|
|
|
|
| def _slice_bytes(src_bytes: bytes, node) -> str: |
| """Decode a node's byte range from src_bytes.""" |
| return src_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace") |
|
|
|
|
| def _extract_sig_ts(node, src_bytes: bytes) -> Optional[FunctionSignature]: |
| """Extract FunctionSignature from a tree-sitter function_definition node.""" |
| declarator = node.child_by_field_name("declarator") |
| if declarator is None: |
| return None |
|
|
| |
| pointer_stars = "" |
| while declarator.type == "pointer_declarator": |
| pointer_stars += "*" |
| declarator = declarator.child_by_field_name("declarator") or declarator |
|
|
| fn_name = "" |
| params: list[tuple[str, str]] = [] |
|
|
| if declarator.type == "function_declarator": |
| name_node = declarator.child_by_field_name("declarator") |
| if name_node: |
| fn_name = _slice_bytes(src_bytes, name_node).strip() |
|
|
| param_list = declarator.child_by_field_name("parameters") |
| if param_list: |
| siblings = list(param_list.named_children) |
| for idx, child in enumerate(siblings): |
| if child.type == "parameter_declaration": |
| p_type, p_name = _extract_param_ts(child, src_bytes) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if ( |
| p_name |
| and _looks_like_macro(p_name) |
| and idx + 1 < len(siblings) |
| and siblings[idx + 1].type == "ERROR" |
| ): |
| err_node = siblings[idx + 1] |
| err_text = _slice_bytes(src_bytes, err_node).strip() |
| if err_text and err_text.isidentifier(): |
| p_type = f"{p_type} {p_name}".strip() |
| p_name = err_text |
| params.append((p_type, p_name)) |
| elif child.type == "variadic_parameter": |
| params.append(("...", "")) |
|
|
| if not fn_name: |
| return None |
|
|
| |
| type_node = node.child_by_field_name("type") |
| ret_type = _slice_bytes(src_bytes, type_node).strip() if type_node else "unknown" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if type_node is not None and _looks_like_macro(ret_type): |
| for c in node.children: |
| if c.type == "ERROR": |
| err_text = _slice_bytes(src_bytes, c).strip() |
| if err_text and err_text.isidentifier(): |
| ret_type = f"{ret_type} {err_text}" |
| break |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if type_node is not None: |
| prepend = _recover_struct_keyword(src_bytes, type_node.start_byte) |
| if prepend and not ret_type.split()[:1] == [prepend]: |
| ret_type = f"{prepend} {ret_type}" |
|
|
| if pointer_stars: |
| ret_type = ret_type + " " + pointer_stars |
|
|
| is_static = "static" in ret_type.split() |
| return FunctionSignature(name=fn_name, return_type=ret_type, parameters=params, is_static=is_static) |
|
|
|
|
| _STRUCT_TAG_KEYWORDS = (b"struct", b"union", b"enum") |
|
|
|
|
| def _looks_like_macro(name: str) -> bool: |
| """Heuristic: identifier is a parameter-qualifier or storage-class |
| macro (not a real parameter name or type). |
| |
| True when the identifier is: |
| * all-uppercase with at least one underscore (``GGML_RESTRICT``, |
| ``OSSL_API``), OR |
| * a single all-uppercase word of >= 4 chars (``UNITTEST``, |
| ``EXPORT``, ``INLINE``), OR |
| * begins with a double underscore (``__restrict__``, |
| ``__nonnull__``, ``__attribute__``). |
| |
| Used to detect tree-sitter's misparse of ``MACRO RealType name`` |
| (return type) or ``T * MACRO name`` (param) so the qualifier is |
| folded into the type and the real identifier is recovered from a |
| sibling ERROR node. Conservative — single-letter caps (``T`` |
| template-style) and 2-3 letter words (``OK``, ``NO``) do not match. |
| """ |
| if not name: |
| return False |
| if name.startswith("__"): |
| return True |
| if "_" in name and name.isupper(): |
| return True |
| if len(name) >= 4 and name.isupper() and name.isalpha(): |
| return True |
| return False |
|
|
|
|
| def _recover_struct_keyword(src_bytes: bytes, type_start: int) -> str: |
| """Return ``struct`` / ``union`` / ``enum`` if it precedes the type |
| in the source text (separated only by whitespace and statement- |
| local tokens), else ``""``. |
| |
| Used to repair the tree-sitter misparse where a macro prefix |
| (GGML_API, EXPORT) causes the parser to consume the struct keyword |
| as part of a stray declaration. We scan backwards from ``type_start`` |
| over whitespace, then check whether the next preceding token is |
| one of struct/union/enum. We stop at ``;``, ``{``, or ``}`` so |
| keywords from an unrelated earlier declaration are not picked up. |
| """ |
| i = type_start - 1 |
| |
| while i >= 0 and src_bytes[i:i + 1] in (b" ", b"\t", b"\n", b"\r"): |
| i -= 1 |
| if i < 0: |
| return "" |
| |
| |
| if src_bytes[i:i + 1] in (b";", b"{", b"}", b")", b"("): |
| return "" |
| |
| end = i + 1 |
| while i >= 0 and ( |
| src_bytes[i:i + 1].isalpha() or src_bytes[i:i + 1].isdigit() or src_bytes[i:i + 1] == b"_" |
| ): |
| i -= 1 |
| start = i + 1 |
| token = src_bytes[start:end] |
| if token in _STRUCT_TAG_KEYWORDS: |
| return token.decode("ascii") |
| return "" |
|
|
|
|
| def _extract_param_ts(param_node, src_bytes: bytes) -> tuple[str, str]: |
| """Return (type_str, name_str) from a parameter_declaration node. |
| |
| Handles three declarator shapes that put non-identifier punctuation |
| onto the "name" half of a whitespace split: |
| |
| * pointer prefix: ``T *p`` → type=``T*`` name=``p`` |
| * double pointer: ``T **pp`` → type=``T**`` name=``pp`` |
| * array decay: ``T buf[N]`` → type=``T*`` name=``buf`` |
| (the array size is lost — that's correct for C, where array |
| parameters decay to pointers at the call site; a downstream |
| harness that emits ``buf[N]`` from this string would be passing |
| an element, not the array. ch341/pl2303 sweep regression.) |
| """ |
| full_text = _slice_bytes(src_bytes, param_node).strip() |
| parts = full_text.rsplit(None, 1) |
| |
| |
| |
| if len(parts) == 1: |
| m = re.match( |
| r"^(.+?)\s*(\*+)([A-Za-z_]\w*)\s*$", |
| full_text, |
| ) |
| if m: |
| parts = [m.group(1).strip() + m.group(2), m.group(3)] |
| if len(parts) == 2: |
| last = parts[1] |
| |
| |
| name = last.lstrip("*") |
| stars = "*" * (len(last) - len(name)) |
| type_str = parts[0].strip() + stars |
| |
| |
| |
| |
| |
| if "[" in name: |
| bracket = name.index("[") |
| tail = name[bracket:] |
| name = name[:bracket] |
| |
| |
| |
| |
| |
| first_close = tail.find("]") |
| remainder = tail[first_close + 1:] if first_close >= 0 else "" |
| type_str = type_str + "*" + remainder |
| return type_str, name |
| return full_text, "" |
|
|
|
|
| def _collect_calls_ts(node, callees: set[str], src_bytes: bytes) -> None: |
| """Recursively collect function call names from a tree-sitter subtree.""" |
| if node.type == "call_expression": |
| fn_node = node.child_by_field_name("function") |
| if fn_node: |
| name = _slice_bytes(src_bytes, fn_node).strip() |
| callees.add(name) |
| for child in node.children: |
| _collect_calls_ts(child, callees, src_bytes) |
|
|
|
|
| |
| |
| |
|
|
| |
| _FUNC_DEF_RE = re.compile( |
| r""" |
| (?:^|\n) # start of line |
| (?P<ret>[\w\s\*]+?) # return type (non-greedy) |
| \s+ |
| (?P<name>[A-Za-z_]\w*) # function name |
| \s*\( # opening paren |
| (?P<params>[^)]*) # parameter list |
| \)\s*\{ # closing paren + opening brace |
| """, |
| re.VERBOSE | re.MULTILINE, |
| ) |
|
|
| _CALL_RE = re.compile(r"\b([A-Za-z_]\w*)\s*\(") |
|
|
| _KEYWORDS = frozenset( |
| [ |
| "if", "else", "while", "for", "do", "switch", "case", "return", |
| "sizeof", "typeof", "alignof", "alignas", "static", "extern", |
| "inline", "const", "volatile", "struct", "union", "enum", |
| "typedef", "void", "int", "long", "short", "char", "float", |
| "double", "unsigned", "signed", |
| ] |
| ) |
|
|
|
|
| def _parse_with_regex(source: str, path: str) -> ParsedCFile: |
| """Fallback regex-based C parser.""" |
| functions: dict[str, FunctionSignature] = {} |
| call_graph: dict[str, set[str]] = {} |
| function_bodies: dict[str, str] = {} |
| function_definitions: dict[str, str] = {} |
|
|
| matches = list(_FUNC_DEF_RE.finditer(source)) |
|
|
| for i, m in enumerate(matches): |
| fn_name = m.group("name") |
| if fn_name in _KEYWORDS: |
| continue |
|
|
| ret_type = m.group("ret").strip() |
| raw_params = m.group("params").strip() |
| params = _parse_params_regex(raw_params) |
| is_static = "static" in ret_type.split() |
|
|
| |
| body_start = m.end() - 1 |
| body_text = _extract_body(source, body_start) |
| body_end = body_start + len(body_text) |
|
|
| functions[fn_name] = FunctionSignature( |
| name=fn_name, |
| return_type=ret_type, |
| parameters=params, |
| is_static=is_static, |
| ) |
| function_bodies[fn_name] = body_text |
| function_definitions[fn_name] = source[m.start():body_end] |
|
|
| |
| callees: set[str] = set() |
| for cm in _CALL_RE.finditer(body_text): |
| callee = cm.group(1) |
| if callee not in _KEYWORDS: |
| callees.add(callee) |
| call_graph[fn_name] = callees |
|
|
| address_taken_in = _compute_address_takers(functions, function_bodies) |
|
|
| return ParsedCFile( |
| path=path, |
| functions=functions, |
| call_graph=call_graph, |
| function_bodies=function_bodies, |
| function_definitions=function_definitions, |
| address_taken_in=address_taken_in, |
| ) |
|
|
|
|
| def _compute_address_takers( |
| functions: "dict[str, FunctionSignature]", |
| function_bodies: "dict[str, str]", |
| ) -> dict[str, set[str]]: |
| """For each function in *functions*, find which OTHER functions' bodies |
| contain its identifier in a non-call position. |
| |
| Returns a dict ``address_taken_in[name] = {caller1, caller2, ...}`` |
| where each entry means "caller's body mentions name without an |
| immediately-following ``(``". |
| |
| Used by Phase 3's caller-feasibility check: a function whose |
| address is taken (passed to a registration function, stored in a |
| vtable struct field, used as a callback) is reachable through |
| that indirection even though direct-call search returns nothing. |
| |
| Conservative — picks up some non-callback references too (e.g. a |
| function name appearing in a comment, in a #define expansion, or |
| cast to a pointer for diagnostic purposes). Those false-positives |
| are harmless for Phase 3's "is the function reachable at all" |
| question. |
| |
| Cheap to compute: one body-scan per function, with the name set as |
| a precomputed regex alternation. |
| """ |
| if not functions or not function_bodies: |
| return {} |
| fn_names = list(functions.keys()) |
| if not fn_names: |
| return {} |
| |
| |
| |
| name_alt = "|".join(re.escape(n) for n in fn_names) |
| |
| |
| pat = re.compile(rf"\b({name_alt})\b(?!\s*\()") |
| result: dict[str, set[str]] = {} |
| for caller_name, body in function_bodies.items(): |
| if not body: |
| continue |
| for m in pat.finditer(body): |
| target = m.group(1) |
| if target == caller_name: |
| |
| |
| continue |
| result.setdefault(target, set()).add(caller_name) |
| return result |
|
|
|
|
| def _parse_params_regex(raw: str) -> list[tuple[str, str]]: |
| """Parse a raw parameter string into [(type, name), ...] pairs.""" |
| if not raw or raw.strip() in ("", "void"): |
| return [] |
| params: list[tuple[str, str]] = [] |
| for part in raw.split(","): |
| part = part.strip() |
| tokens = part.split() |
| if len(tokens) >= 2: |
| last = tokens[-1] |
| name = last.lstrip("*") |
| stars = "*" * (len(last) - len(name)) |
| typ = " ".join(tokens[:-1]) + stars |
| params.append((typ, name)) |
| elif tokens: |
| params.append((tokens[0], "")) |
| return params |
|
|
|
|
| def _extract_body(source: str, open_brace: int) -> str: |
| """Extract the text from the opening brace to its matching closing brace.""" |
| depth = 0 |
| i = open_brace |
| while i < len(source): |
| ch = source[i] |
| if ch == "{": |
| depth += 1 |
| elif ch == "}": |
| depth -= 1 |
| if depth == 0: |
| return source[open_brace: i + 1] |
| i += 1 |
| return source[open_brace:] |
|
|