| """ |
| C preprocessor integration for AMC. |
| |
| Expands #include directives via `cc -E` so that each source file |
| becomes a self-contained translation unit that the parser and CBMC |
| can handle without knowing the original include paths. |
| |
| Also strips GCC/ARM64 extensions that CBMC does not accept: |
| __attribute__((...)), __asm__/__asm blocks, _Noreturn, typeof, |
| and register-asm variable declarations. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import re |
| import subprocess |
| import tempfile |
| from pathlib import Path |
|
|
| from bmc_agent.logger import get_logger |
|
|
| logger = get_logger("preprocessor") |
|
|
|
|
| |
| |
| |
|
|
|
|
| def preprocess( |
| source_file: str | Path, |
| include_dirs: list[str] | None = None, |
| defines: list[str] | None = None, |
| cc: str = "cc", |
| ) -> str: |
| """ |
| Return a cleaned, self-contained C source string for *source_file*. |
| |
| Steps: |
| 1. Run ``cc -E -P`` to expand all #include references. |
| 2. Strip lines that originate from system headers (``/usr/``, ``/lib/``). |
| 3. Strip GCC/ARM64 extensions that confuse CBMC. |
| 4. Prepend standard CBMC-friendly headers. |
| |
| Parameters |
| ---------- |
| source_file: |
| Path to the ``.c`` file to preprocess. |
| include_dirs: |
| List of ``-I`` paths to pass to the compiler. |
| defines: |
| List of ``-D`` macros to pass to the compiler. |
| cc: |
| C compiler binary to use for preprocessing (default ``cc``). |
| """ |
| source_file = Path(source_file) |
| include_dirs = include_dirs or [] |
| defines = defines or [] |
|
|
| expanded = _run_preprocessor(source_file, include_dirs, defines, cc) |
| cleaned = _strip_system_content(expanded, source_file) |
| cleaned = _strip_gcc_extensions(cleaned) |
| cleaned = _prepend_cbmc_headers(cleaned) |
| return cleaned |
|
|
|
|
| def preprocess_to_file( |
| source_file: str | Path, |
| output_file: str | Path, |
| include_dirs: list[str] | None = None, |
| defines: list[str] | None = None, |
| cc: str = "cc", |
| ) -> Path: |
| """Preprocess *source_file* and write the result to *output_file*.""" |
| result = preprocess(source_file, include_dirs, defines, cc) |
| out = Path(output_file) |
| out.parent.mkdir(parents=True, exist_ok=True) |
| out.write_text(result, encoding="utf-8") |
| return out |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _run_preprocessor( |
| source_file: Path, |
| include_dirs: list[str], |
| defines: list[str], |
| cc: str, |
| ) -> str: |
| cmd = [cc, "-E", "-P"] |
| for d in include_dirs: |
| cmd += ["-I", d] |
| for define in defines: |
| cmd += ["-D", define] |
| |
| cmd += ["-w", "-x", "c", str(source_file)] |
|
|
| try: |
| result = subprocess.run( |
| cmd, |
| capture_output=True, |
| text=True, |
| timeout=60, |
| ) |
| if result.returncode != 0 and not result.stdout.strip(): |
| logger.warning("Preprocessor failed for %s: %s", source_file, result.stderr[:200]) |
| |
| return source_file.read_text(encoding="utf-8", errors="replace") |
| return result.stdout |
| except (FileNotFoundError, subprocess.TimeoutExpired) as exc: |
| logger.warning("Cannot run preprocessor (%s): %s — reading file as-is", cc, exc) |
| return source_file.read_text(encoding="utf-8", errors="replace") |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| _SYSTEM_PATHS = ("/usr/", "/lib/", "/opt/homebrew/", "/Applications/Xcode") |
| _LINEMARKER = re.compile(r'^# \d+ "([^"]+)"') |
|
|
|
|
| def _strip_system_content(source: str, original_file: Path) -> str: |
| """Remove content that expanded from system headers.""" |
| lines = source.splitlines(keepends=True) |
|
|
| |
| has_markers = any(_LINEMARKER.match(l) for l in lines[:50]) |
| if has_markers: |
| return _strip_by_linemarkers(lines, original_file) |
|
|
| |
| |
| |
| return _collapse_blanks(source) |
|
|
|
|
| def _strip_by_linemarkers(lines: list[str], original_file: Path) -> str: |
| in_user_file = True |
| out: list[str] = [] |
| for line in lines: |
| m = _LINEMARKER.match(line) |
| if m: |
| path = m.group(1) |
| in_user_file = not any(path.startswith(sp) for sp in _SYSTEM_PATHS) |
| continue |
| if in_user_file: |
| out.append(line) |
| return "".join(out) |
|
|
|
|
| def _collapse_blanks(source: str) -> str: |
| return re.sub(r'\n{3,}', '\n\n', source) |
|
|
|
|
| |
| |
| |
|
|
| def _strip_attributes_nested(source: str) -> str: |
| """Remove __attribute__((...)) handling nested parentheses correctly.""" |
| result = [] |
| i = 0 |
| n = len(source) |
| marker = "__attribute__" |
| mlen = len(marker) |
| while i < n: |
| if source[i:i+mlen] == marker: |
| j = i + mlen |
| |
| while j < n and source[j] in ' \t\n\r': |
| j += 1 |
| if j < n and source[j] == '(': |
| |
| depth = 0 |
| while j < n: |
| if source[j] == '(': |
| depth += 1 |
| elif source[j] == ')': |
| depth -= 1 |
| if depth == 0: |
| j += 1 |
| break |
| j += 1 |
| i = j |
| else: |
| result.append(source[i]) |
| i += 1 |
| else: |
| result.append(source[i]) |
| i += 1 |
| return ''.join(result) |
|
|
| _ATTR_RE = re.compile(r'__attribute__\s*\(.*?\)', re.DOTALL) |
| _DECLSPEC_RE = re.compile(r'__declspec\s*\([^)]*\)') |
| _TYPEOF_RE = re.compile(r'\btypeof\s*\(') |
| |
| _ASM_RE = re.compile( |
| r'__asm__\s*(?:volatile\s*)?\s*\([^;]*\)\s*;', |
| re.DOTALL, |
| ) |
| |
| _REGVAR_RE = re.compile( |
| r'\bregister\b([^;]+)\basm\s*\([^)]*\)\s*;', |
| ) |
| |
| _NORETURN_RE = re.compile(r'\b_Noreturn\b') |
| |
| _EXTENSION_RE = re.compile(r'\b__extension__\b') |
| |
| _RESTRICT_RE = re.compile(r'\b__restrict(?:__)?(\s)') |
| |
| _VOLATILE_RE = re.compile(r'\b__volatile__\b') |
| |
| _CONST_RE = re.compile(r'\b__const__\b') |
| |
| _SIGNED_RE = re.compile(r'\b__signed__\b') |
| |
| _INLINE_RE = re.compile(r'\b__inline(?:__)?\b') |
|
|
|
|
| def _strip_gcc_extensions(source: str) -> str: |
| source = _strip_attributes_nested(source) |
| source = _DECLSPEC_RE.sub('', source) |
| source = _ASM_RE.sub(';', source) |
| source = _REGVAR_RE.sub(r'/* register-asm variable removed */;', source) |
| source = _NORETURN_RE.sub('', source) |
| source = _EXTENSION_RE.sub('', source) |
| source = _RESTRICT_RE.sub(r'\1', source) |
| source = _VOLATILE_RE.sub('volatile', source) |
| source = _CONST_RE.sub('const', source) |
| source = _SIGNED_RE.sub('signed', source) |
| source = _INLINE_RE.sub('inline', source) |
| return source |
|
|
|
|
| |
| |
| |
|
|
| _CBMC_PREAMBLE = """\ |
| /* AMC preprocessor preamble — CBMC-friendly type definitions */ |
| #include <stdint.h> |
| #include <stddef.h> |
| #include <stdbool.h> |
| #include <assert.h> |
| #ifndef NULL |
| #define NULL ((void*)0) |
| #endif |
| #ifndef true |
| #define true 1 |
| #define false 0 |
| #endif |
| |
| """ |
|
|
|
|
| def _prepend_cbmc_headers(source: str) -> str: |
| |
| if "AMC preprocessor preamble" in source: |
| return source |
| return _CBMC_PREAMBLE + source |
|
|