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)