File size: 3,833 Bytes
ab54eb4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | """
Language-dispatching source-file parser.
Maps a file path to the right per-language parser and returns its parsed
result. The C and Rust parsers return distinct ``Parsed*File`` types but
they are *structurally* compatible — same field names, same
``get_function_info`` / ``all_function_infos`` API — so the rest of
AProver's pipeline can consume either without branching on language.
Dispatch is by extension only. Unknown extensions raise
``UnsupportedSourceLanguage`` rather than silently falling back, so a
mistyped path surfaces immediately rather than producing an empty parse.
"""
from __future__ import annotations
from pathlib import Path
from typing import Optional, Union
from bmc_agent.parser import ParsedCFile, parse_c_file
from bmc_agent.rust_parser import ParsedRustFile, parse_rust_file
ParsedSourceFile = Union[ParsedCFile, ParsedRustFile]
class UnsupportedSourceLanguage(ValueError):
"""Raised when a path's extension does not map to a known parser."""
# Extension -> parser entry-point, in the form expected at the call site.
# Header files are routed to the C parser because header-only specs are
# the dominant pattern in AProver's existing C corpus. ``.i`` is the
# conventional output of ``cc -E`` / kernel ``make foo.i`` — a
# preprocessed C translation unit — so it routes to the C parser too.
_C_EXTS = frozenset({".c", ".h", ".i"})
_RUST_EXTS = frozenset({".rs"})
def detect_language(path: str | Path) -> str:
"""Return ``"c"`` or ``"rust"`` based on *path*'s extension.
Raises ``UnsupportedSourceLanguage`` for any other extension.
"""
ext = Path(path).suffix.lower()
if ext in _C_EXTS:
return "c"
if ext in _RUST_EXTS:
return "rust"
raise UnsupportedSourceLanguage(
f"No parser registered for extension {ext!r} (path={path!r}). "
"Supported: .c, .h, .i, .rs"
)
def parse_source_file(
path: str | Path,
source_text: Optional[str] = None,
) -> ParsedSourceFile:
"""Parse *path* with the right per-language backend.
Parameters
----------
path:
Path to the source file. The extension selects the parser.
source_text:
Optional in-memory source. Both parsers accept this so the
caller can pass preprocessed C (after ``cc -E``) or
macro-expanded Rust to the parser without re-reading from disk.
Returns
-------
Either a :class:`ParsedCFile` or :class:`ParsedRustFile`. Both
expose ``functions``, ``call_graph``, ``function_bodies``,
``preprocessed_source``, ``get_function_info`` and
``all_function_infos``, so downstream code does not need to branch.
"""
lang = detect_language(path)
if lang == "c":
parsed = parse_c_file(path, source_text=source_text)
# When the input is a preprocessed translation unit (cpp ``# N
# "filename"`` line directives present, ``primary_source`` is
# set), automatically drop header-inlined functions. A kernel
# driver ``.i`` pulls in ~4400 ``static inline`` helpers from
# ``include/linux/*.h``; the pipeline cares only about the
# ~25 functions actually defined in the driver. Filtering here
# (rather than at each CLI command's parse call) ensures every
# entry point — generate, check, verify, verify-dir, baselines
# — sees a consistent set and the harness generator's
# type-decl extractor doesn't choke on byte-range mismatches
# against thousands of complex function bodies.
if getattr(parsed, "primary_source", None) and hasattr(parsed, "restrict_to_primary_source"):
parsed.restrict_to_primary_source()
return parsed
# lang == "rust" — detect_language only returns "c" or "rust"
return parse_rust_file(path, source_text=source_text)
|