ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
annot_rulekeys.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""The annotation vocabulary, and the proof that it matches what the macros emit.
4
5This module is small on purpose. It holds the set of rule keys the checker
6dispatches on, and one cross-check -- :func:`check_rule_keys` -- that compares
7that set against the strings ``ra8_attributes.h`` actually writes.
8
9That cross-check is the reason the vocabulary is not just spelled inline where
10it is used. A rule keyed on a spelling no macro produces matches zero symbols
11and reports zero violations forever, which is indistinguishable from a clean
12tree. Four rules in this checker were in exactly that state at once. Keeping
13the keys and their proof in one file makes the two impossible to edit apart.
14"""
15
16from __future__ import annotations
17
18import re
19
20from annot_model import Violation
21from annot_scope import repo_root
22
23#: Path to the single header that defines every annotation macro. The
24#: rule-key self-check reads the strings straight out of it. Bound to the
25#: real checkout: this file is a fixed part of the repository, and unlike
26#: the scan roots it is never re-pointed at a synthetic tree.
27ATTRIBUTES_HEADER = repo_root() / "libs" / "ra8_core" / "inc" / "ra8_attributes.h"
28
29#: Rules that record information rather than assert a property. They are
30#: reported but never fail the gate -- there is nothing for a developer to
31#: fix, the entry exists so the value shows up in the build log.
32INFORMATIONAL_RULES = {"ra8_latency_budget_ns", "ra8_reviewed_by", "ra8_register_bank"}
33
34ANNOTATION_PREFIXES = (
35 "ra8_test_helper",
36 "ra8_internal",
37 "ra8_priv",
38 "ra8_di_slot",
39 "ra8_nsc_veneer",
40 # Every entry here must match exactly what the corresponding macro in
41 # libs/ra8_core/inc/ra8_attributes.h emits; check_rule_keys() proves
42 # that on every run. Four of these were once spelled as something no
43 # macro produced ("ra8_hw_mmio", "ra8_p10_rule3_exception",
44 # "ra8_stack_max", "ra8_latency_max_ns") and the rules keyed on them
45 # matched nothing at all while reporting success.
46 "ra8_hw_register_access",
47 "ra8_nasa_rule_3_ok",
48 "ra8_mcdc_deactivated",
49 "ra8_max_stack",
50 "ra8_isr_safe",
51 "ra8_expects_lock",
52 "ra8_host_friendly",
53 "ra8_latency_budget_ns",
54 "ra8_no_recursion",
55 "ra8_bounded_loop",
56 "ra8_validates",
57 "ra8_owns_resource",
58 "ra8_releases_resource",
59 "ra8_reviewed_by",
60 "ra8_register_bank",
61)
62
63#: The three linkage annotations a non-static function may carry.
64LINKAGE_ANNOTATIONS = frozenset({"ra8_priv", "ra8_internal", "ra8_test_helper"})
65
66
67def parse_annotation(ann: str) -> tuple[str, str]:
68 """Split ``ra8_max_stack:512`` -> (``ra8_max_stack``, ``512``)."""
69 if ":" in ann:
70 rule, _, arg = ann.partition(":")
71 return rule.strip(), arg.strip()
72 return ann.strip(), ""
73
74
75def emitted_annotation_keys() -> set[str]:
76 """Return every annotation string ``ra8_attributes.h`` can emit.
77
78 Read straight out of the header rather than restated here, because a
79 restatement is what goes stale. Each macro expands through
80 ``RA8_INTERNAL_ANNOTATE("ra8_<rule>...")``, or through the shared
81 ``RA8_INTERNAL_ANNOTATE_ARG("ra8_<rule>:", arg)`` helper the macros
82 that carry a value use; the rule key is the text up to the first colon.
83 """
84 try:
85 text = ATTRIBUTES_HEADER.read_text(errors="ignore")
86 except OSError:
87 return set()
88 pattern = r'RA8_INTERNAL_ANNOTATE(?:_ARG)?\‍(\s*"(ra8_[a-z0-9_]+)'
89 return {m.group(1) for m in re.finditer(pattern, text)}
90
91
92def check_rule_keys() -> list[Violation]:
93 """Fail when a rule keys on a string no annotation macro emits.
94
95 This is the failure mode that looks exactly like success. A rule
96 keyed on a spelling nothing produces matches zero symbols and reports
97 zero violations for as long as nobody checks, and four rules in this
98 file were in that state at once: RA8_HW_REGISTER_ACCESS emits
99 "ra8_hw_register_access" but rule 6 looked for "ra8_hw_mmio", and the
100 NASA-rule-3, stack-budget and latency-budget rules each looked for a
101 key their macro never wrote. Cross-checking both directions against
102 the header makes the whole class impossible to reintroduce silently.
103 """
104 emitted = emitted_annotation_keys()
105 if not emitted:
106 return [
107 Violation(
108 "ra8_rule_keys",
109 str(ATTRIBUTES_HEADER),
110 0,
111 "no RA8_INTERNAL_ANNOTATE() strings found -- the annotation "
112 "header moved or changed shape, so every rule key is unverified",
113 )
114 ]
115 known = set(ANNOTATION_PREFIXES)
116 out: list[Violation] = []
117 out.extend(
118 Violation(
119 "ra8_rule_keys",
120 str(ATTRIBUTES_HEADER),
121 0,
122 f"rule key '{key}' is not emitted by any macro in ra8_attributes.h; "
123 f"the rule keyed on it can never match",
124 )
125 for key in sorted(known - emitted)
126 )
127 out.extend(
128 Violation(
129 "ra8_rule_keys",
130 str(ATTRIBUTES_HEADER),
131 0,
132 f"annotation '{key}' is emitted by a macro but no rule recognises it; "
133 f"every use of that macro is silently ignored",
134 )
135 for key in sorted(emitted - known)
136 )
137 return out