Source code for ppbcc.code_complexity.config

"""Loading and representation of the analysis configuration.

The configuration consists of two TOML resources shipped with the package in
``ppbcc/code_complexity/share``:

* ``cpp_keywords.toml`` -- the baseline C++ keyword sets used to distinguish
  operators from operands.
* ``dialects.toml`` -- the registry of GPU/parallel programming *dialects*
  (Kokkos, CUDA, OpenMP, ...) whose constructs are counted as dialect
  operators.

Both files can be overridden with user-supplied paths, e.g. via the CLI.
"""

from __future__ import annotations

import re
import tomllib
from dataclasses import dataclass, field
from importlib import resources
from pathlib import Path

from loguru import logger

#: Number of ``detect_patterns`` hits in a source file required to activate a
#: dialect during automatic detection (headers, file extensions, pragmas and
#: namespace usage activate a dialect with a single hit instead).
DETECTION_MIN_HITS: int = 3

#: File extensions collected when a directory is passed as a source.
SOURCE_EXTENSIONS: frozenset[str] = frozenset({
    ".c", ".cc", ".cpp", ".cxx", ".c++",
    ".h", ".hh", ".hpp", ".hxx", ".h++", ".inl", ".inc", ".ipp",
    ".cu", ".cuh", ".hip",
    ".cl",
    ".comp", ".glsl", ".vert", ".frag", ".geom", ".tesc", ".tese",
    ".slang", ".hlsl", ".wgsl", ".metal",
})

#: Names of ``language_dialect`` that mean "plain C++, no dialect".
BASELINE_DIALECT_NAMES: frozenset[str] = frozenset({"cpp", "c++", "none", "baseline"})

#: Name of the pseudo dialect that triggers automatic per-file detection.
AUTO_DIALECT_NAME: str = "auto"


[docs] @dataclass(frozen=True) class CppKeywords: """Baseline C++ keyword sets. Attributes: keywords: Keywords counted as operators (e.g. ``for``, ``const``). operand_keywords: Keywords that denote values and therefore count as operands (e.g. ``true``, ``nullptr``, ``this``). """ keywords: frozenset[str] operand_keywords: frozenset[str]
[docs] @dataclass(frozen=True) class DialectSpec: """Description of a single GPU/parallel-programming dialect. All regular expressions are pre-compiled; identifier patterns are matched with ``fullmatch`` against single identifiers, header and detection patterns with ``search``. Attributes: name: Canonical dialect name (the table name in ``dialects.toml``). aliases: Lower-case names accepted for ``language_dialect``. namespace_prefixes: Namespace prefixes (as tuples of segments) that turn a whole qualified name into a single dialect operator. identifier_pattern: Combined regex for dialect identifiers, or ``None`` if the dialect defines no identifier patterns. keywords: Exact identifiers belonging to the dialect. pragma_prefixes: First pragma tokens claiming a ``#pragma`` line. pragma_clauses: Identifiers counted as operators inside a claimed pragma line (all other pragma identifiers count as operands). punctuation: Extra punctuation operators (e.g. CUDA ``<<<``). header_patterns: Regexes matched against included header names for automatic detection. detect_patterns: Regexes searched in the raw source for automatic detection (requires :data:`DETECTION_MIN_HITS` total hits). extensions: File extensions that imply the dialect. """ name: str aliases: frozenset[str] namespace_prefixes: tuple[tuple[str, ...], ...] = () identifier_pattern: re.Pattern[str] | None = None keywords: frozenset[str] = frozenset() pragma_prefixes: frozenset[str] = frozenset() pragma_clauses: frozenset[str] = frozenset() punctuation: frozenset[str] = frozenset() header_patterns: tuple[re.Pattern[str], ...] = () detect_patterns: tuple[re.Pattern[str], ...] = () extensions: frozenset[str] = frozenset()
[docs] def matches_identifier(self, identifier: str) -> bool: """Checks whether a single identifier belongs to this dialect. Args: identifier: The identifier text, e.g. ``"cudaMalloc"``. Returns: True if the identifier is a dialect keyword or fully matches one of the dialect's identifier patterns. """ if identifier in self.keywords: return True return bool(self.identifier_pattern and self.identifier_pattern.fullmatch(identifier))
[docs] def matches_qualified(self, segments: tuple[str, ...]) -> bool: """Checks whether a qualified name belongs to this dialect. Args: segments: The ``::``-separated name split into segments, e.g. ``("Kokkos", "parallel_for")``. Returns: True if the name starts with one of the dialect's namespace prefixes. """ return any( segments[: len(prefix)] == prefix for prefix in self.namespace_prefixes if len(segments) >= len(prefix) )
[docs] @dataclass(frozen=True) class DialectRegistry: """All known dialects plus lookup helpers. Attributes: dialects: Mapping from canonical dialect name to its specification. alias_map: Mapping from every accepted (lower-case) alias to the canonical dialect name. """ dialects: dict[str, DialectSpec] alias_map: dict[str, str] = field(default_factory=dict)
[docs] def resolve(self, name: str) -> DialectSpec: """Resolves a dialect name or alias to its specification. Args: name: Dialect name or alias, case-insensitive (e.g. ``"Kokkos"``). Returns: The matching :class:`DialectSpec`. Raises: KeyError: If the name is not a known dialect or alias. """ canonical = self.alias_map.get(name.strip().lower()) if canonical is None: known = ", ".join(sorted(self.alias_map)) raise KeyError(f"Unknown dialect {name!r}. Known dialects/aliases: {known}") return self.dialects[canonical]
[docs] def resolve_all(self, names: str | list[str]) -> list[DialectSpec]: """Resolves a dialect selection string or list to specifications. Args: names: Either a single (possibly comma-separated) string such as ``"kokkos,openmp"`` or a list of dialect names. Baseline names (``"cpp"``, ``"none"``, ...) resolve to an empty selection. Returns: List of resolved dialect specifications (without duplicates). Raises: KeyError: If any name is neither a dialect alias nor a baseline name. ``"auto"`` must be handled by the caller and also raises here. """ if isinstance(names, str): names = [part for part in names.split(",") if part.strip()] specs: dict[str, DialectSpec] = {} for name in names: if name.strip().lower() in BASELINE_DIALECT_NAMES: continue spec = self.resolve(name) specs[spec.name] = spec return list(specs.values())
def _read_toml(default_resource: str, override: Path | None) -> dict: """Reads a TOML configuration, either packaged or user-supplied. Args: default_resource: File name of the packaged resource in ``ppbcc/code_complexity/share``. override: Optional path to a user-supplied TOML file replacing the packaged one. Returns: The parsed TOML document as a dictionary. """ if override is not None: logger.debug("Loading configuration from override file {}", override) return tomllib.loads(Path(override).read_text(encoding="utf-8")) resource = resources.files("ppbcc.code_complexity") / "share" / default_resource logger.trace("Loading packaged configuration resource {}", default_resource) return tomllib.loads(resource.read_text(encoding="utf-8"))
[docs] def load_cpp_keywords(path: Path | None = None) -> CppKeywords: """Loads the baseline C++ keyword sets. Args: path: Optional path to a TOML file overriding the packaged ``cpp_keywords.toml``. Returns: The loaded :class:`CppKeywords`. """ data = _read_toml("cpp_keywords.toml", path) return CppKeywords( keywords=frozenset(data.get("keywords", [])), operand_keywords=frozenset(data.get("operand_keywords", [])), )
def _compile_dialect(name: str, data: dict) -> DialectSpec: """Compiles one dialect table from ``dialects.toml`` into a spec. Args: name: Canonical dialect name (TOML table name). data: The dialect's TOML table contents. Returns: The compiled :class:`DialectSpec`. """ identifier_patterns = data.get("identifier_patterns", []) combined = None if identifier_patterns: combined = re.compile("|".join(f"(?:{p})" for p in identifier_patterns)) return DialectSpec( name=name, aliases=frozenset(alias.lower() for alias in data.get("aliases", [name])), namespace_prefixes=tuple( tuple(prefix.split("::")) for prefix in data.get("namespace_prefixes", []) ), identifier_pattern=combined, keywords=frozenset(data.get("keywords", [])), pragma_prefixes=frozenset(data.get("pragma_prefixes", [])), pragma_clauses=frozenset(data.get("pragma_clauses", [])), punctuation=frozenset(data.get("punctuation", [])), header_patterns=tuple(re.compile(p) for p in data.get("headers", [])), detect_patterns=tuple(re.compile(p) for p in data.get("detect_patterns", [])), extensions=frozenset(ext.lower() for ext in data.get("extensions", [])), )
[docs] def load_dialects(path: Path | None = None) -> DialectRegistry: """Loads the dialect registry. Args: path: Optional path to a TOML file overriding the packaged ``dialects.toml``. Returns: The loaded :class:`DialectRegistry` with all regexes compiled. """ data = _read_toml("dialects.toml", path) dialects = { name: _compile_dialect(name, table) for name, table in data.get("dialects", {}).items() } alias_map: dict[str, str] = {} for name, spec in dialects.items(): alias_map[name.lower()] = name for alias in spec.aliases: alias_map[alias] = name logger.debug("Loaded {} dialects: {}", len(dialects), ", ".join(dialects)) return DialectRegistry(dialects=dialects, alias_map=alias_map)