ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_no_driver_asm_guard.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Gate: a HAL driver shall not guard bare CPU asm on RA8_OFF_TARGET.
5
6Issue #293 (following #238) migrated every host-compatibility CPU primitive out
7of the HAL peripheral drivers and onto ONE shared seam:
8
9 - ``libs/ra8_hal/inc/ra8_hw_intrinsics.h`` (target inline asm / host decls)
10 - ``tests/mocks/src/ra8_host_asm_stub.c`` (the host-safe definitions)
11
12A driver that needs ``wfi`` / ``dsb`` / ``isb`` / ``nop`` / the ``cpsie i`` /
13``cpsid i`` gate / the post-reset spin now calls ``ra8_hw_wfi()`` and friends;
14it carries NO ``#ifdef RA8_OFF_TARGET`` of its own -- the seam owns the
15host/target divergence. That keeps coverage and MC/DC landing on the real
16shipping path instead of a compiled-out detour, exactly as #238 intended.
17
18This gate keeps a NEW guard class from creeping back in. It FAILS if any
19translation unit under ``libs/ra8_hal/src/`` contains an inline-asm statement
20(``__asm`` / ``__asm__``) inside a preprocessor conditional whose controlling
21expression references ``RA8_OFF_TARGET`` -- in EITHER branch. Comment text
22is stripped first, so prose that merely mentions ``__asm__`` never trips it.
23
24Scope note: this is deliberately limited to ``libs/ra8_hal/src/`` -- the HAL
25peripheral drivers. Boot code (``system_init.c``, ``vector_table.c``,
26``trustzone_init.c``), the core runtime (``ra8_core``), and the TrustZone /
27secure-boot flow legitimately host target-only asm (reset vectors, fault
28handlers, SAU bring-up, ``msr msp_ns``) that has no host code path to drive and
29is not a peripheral-driver short-circuit; those are out of scope by design.
30
31There is no allowlist: route the primitive through ``ra8_hw_intrinsics.h`` (add
32a new one there and to the host stub if it is genuinely missing), never behind a
33fresh in-driver ``#ifdef RA8_OFF_TARGET``.
34
35Run::
36
37 check_no_driver_asm_guard.py
38
39Exit status: 0 if every driver is clean, 1 otherwise.
40"""
41
42import re
43import sys
44import tempfile
45from pathlib import Path
46
47# Repo root: this file is scripts/checks/check_no_driver_asm_guard.py .
48REPO_ROOT = Path(__file__).resolve().parents[2]
49
50# The HAL peripheral drivers -- the only scope this gate governs.
51DRIVER_DIR = REPO_ROOT / "libs" / "ra8_hal" / "src"
52
53_RE_IF = re.compile(r"^\s*#\s*(if|ifdef|ifndef)\b(.*)$")
54_RE_ELIF = re.compile(r"^\s*#\s*elif\b(.*)$")
55_RE_ENDIF = re.compile(r"^\s*#\s*endif\b")
56_RE_ASM = re.compile(r"(?<![A-Za-z0-9_])__asm(__)?(?![A-Za-z0-9_])")
57_OFF_TARGET = "RA8_OFF_TARGET"
58
59
60def strip_comments(text: str) -> list[str]:
61 """Blank out C ``/* */`` and ``//`` comments, preserving line count.
62
63 Preprocessor directives never live inside comments, so a line-preserving
64 strip lets the conditional walk and the asm scan share one clean view
65 without a false positive from a doc block that mentions ``__asm__``.
66 """
67 out: list[str] = []
68 in_block = False
69 for line in text.splitlines():
70 buf: list[str] = []
71 i = 0
72 n = len(line)
73 while i < n:
74 two = line[i : i + 2]
75 if in_block:
76 if two == "*/":
77 in_block = False
78 i += 2
79 else:
80 i += 1
81 continue
82 if two == "/*":
83 in_block = True
84 i += 2
85 continue
86 if two == "//":
87 break
88 buf.append(line[i])
89 i += 1
90 out.append("".join(buf))
91 return out
92
93
94def check_file(path: Path, repo_root: Path = REPO_ROOT) -> list[str]:
95 """Return violation strings for one driver TU (empty when clean)."""
96 rel = path.relative_to(repo_root).as_posix()
97 lines = strip_comments(path.read_text(encoding="utf-8"))
98
99 # Stack of booleans: does this open conditional's region reference the
100 # fake flag (in its #if / any #elif)? A True anywhere on the stack
101 # means the current line compiles under a off-target-conditioned region.
102 stack: list[bool] = []
103 problems: list[str] = []
104
105 for idx, line in enumerate(lines, start=1):
106 m_if = _RE_IF.match(line)
107 if m_if is not None:
108 stack.append(_OFF_TARGET in m_if.group(2))
109 continue
110 m_elif = _RE_ELIF.match(line)
111 if m_elif is not None:
112 if stack:
113 stack[-1] = stack[-1] or (_OFF_TARGET in m_elif.group(1))
114 continue
115 if _RE_ENDIF.match(line):
116 if stack:
117 stack.pop()
118 continue
119 # #else keeps the frame's off-target-reference flag: both branches of a
120 # `#ifdef RA8_OFF_TARGET` are off-target-conditioned regions.
121 if _RE_ASM.search(line) and any(stack):
122 problems.append(
123 f"{rel}:{idx}: inline asm '{line.strip()}' sits inside a "
124 f"{_OFF_TARGET} conditional -- route it through "
125 f"libs/ra8_hal/inc/ra8_hw_intrinsics.h instead"
126 )
127 return problems
128
129
130def selftest() -> int:
131 """Prove guarded asm fires while seam calls and comment lookalikes stay quiet."""
132 with tempfile.TemporaryDirectory(prefix="driver-asm-selftest-") as raw:
133 root = Path(raw)
134 bad = root / "libs/ra8_hal/src/bad.c"
135 good = root / "libs/ra8_hal/src/good.c"
136 bad.parent.mkdir(parents=True)
137 bad.write_text(
138 '#ifdef RA8_OFF_TARGET\nvoid f(void) { __asm("nop"); }\n#else\n'
139 'void g(void) { __asm__("wfi"); }\n#endif\n',
140 encoding="ascii",
141 )
142 good.write_text(
143 '// __asm__("nop") under RA8_OFF_TARGET is prose\nvoid f(void) { ra8_hw_wfi(); }\n',
144 encoding="ascii",
145 )
146 bad_findings = check_file(bad, root)
147 good_findings = check_file(good, root)
148 expected_bad_findings = 2
149 cases = (
150 (len(bad_findings) == expected_bad_findings, "asm in both off-target branches fires"),
151 (not good_findings, "shared seam calls and comment lookalikes stay quiet"),
152 )
153 failed = [label for passed, label in cases if not passed]
154 for passed, label in cases:
155 print(f" [{'ok' if passed else 'FAIL'}] {label}")
156 if failed:
157 print(f"check_no_driver_asm_guard.py --selftest: {len(failed)} failure(s)")
158 return 1
159 print("check_no_driver_asm_guard.py --selftest: all cases pass (both directions).")
160 return 0
161
162
163def main() -> int:
164 """Fail any HAL driver that guards bare CPU asm on RA8_OFF_TARGET.
165
166 A missing driver directory exits 1 rather than 0. That is deliberate: this
167 gate has a single hardcoded scan root, so the directory vanishing means
168 the tree moved under it, and the one thing it must not do is report a
169 clean sweep of somewhere that does not exist.
170
171 Returns 0 when every driver routes its CPU primitives through
172 ra8_hw_intrinsics.h, 1 on a violation or a missing driver directory.
173 """
174 args = sys.argv[1:]
175 if args == ["--selftest"]:
176 return selftest()
177 if args:
178 print("usage: check_no_driver_asm_guard.py [--selftest]", file=sys.stderr)
179 return 2
180 if not DRIVER_DIR.is_dir():
181 print(f"check_no_driver_asm_guard.py: driver dir not found: {DRIVER_DIR}")
182 return 1
183
184 drivers = sorted(DRIVER_DIR.glob("*.c"))
185 all_problems: list[str] = []
186 for path in drivers:
187 all_problems.extend(check_file(path))
188
189 if all_problems:
190 print("check_no_driver_asm_guard.py: a HAL driver guards bare asm on RA8_OFF_TARGET:")
191 for p in all_problems:
192 print(f" {p}")
193 print("Fix at the root -- call the ra8_hw_* primitive from")
194 print(" libs/ra8_hal/inc/ra8_hw_intrinsics.h")
195 print("(add a new one there plus its host body in")
196 print(" tests/mocks/src/ra8_host_asm_stub.c if it does not exist yet).")
197 return 1
198
199 print(
200 f"check_no_driver_asm_guard.py: PASS -- {len(drivers)} HAL driver TU(s) "
201 f"carry no RA8_OFF_TARGET-guarded asm."
202 )
203 return 0
204
205
206if __name__ == "__main__":
207 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298