4"""Gate: a HAL driver shall not guard bare CPU asm on RA8_OFF_TARGET.
6Issue #293 (following #238) migrated every host-compatibility CPU primitive out
7of the HAL peripheral drivers and onto ONE shared seam:
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)
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.
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.
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.
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``.
37 check_no_driver_asm_guard.py
39Exit status: 0 if every driver is clean, 1 otherwise.
45from pathlib
import Path
48REPO_ROOT = Path(__file__).resolve().parents[2]
51DRIVER_DIR = REPO_ROOT /
"libs" /
"ra8_hal" /
"src"
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"
60def strip_comments(text: str) -> list[str]:
61 """Blank out C ``/* */`` and ``//`` comments, preserving line count.
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__``.
69 for line
in text.splitlines():
90 out.append(
"".join(buf))
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"))
102 stack: list[bool] = []
103 problems: list[str] = []
105 for idx, line
in enumerate(lines, start=1):
106 m_if = _RE_IF.match(line)
108 stack.append(_OFF_TARGET
in m_if.group(2))
110 m_elif = _RE_ELIF.match(line)
111 if m_elif
is not None:
113 stack[-1] = stack[-1]
or (_OFF_TARGET
in m_elif.group(1))
115 if _RE_ENDIF.match(line):
121 if _RE_ASM.search(line)
and any(stack):
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"
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:
134 bad = root /
"libs/ra8_hal/src/bad.c"
135 good = root /
"libs/ra8_hal/src/good.c"
136 bad.parent.mkdir(parents=
True)
138 '#ifdef RA8_OFF_TARGET\nvoid f(void) { __asm("nop"); }\n#else\n'
139 'void g(void) { __asm__("wfi"); }\n#endif\n',
143 '// __asm__("nop") under RA8_OFF_TARGET is prose\nvoid f(void) { ra8_hw_wfi(); }\n',
146 bad_findings = check_file(bad, root)
147 good_findings = check_file(good, root)
148 expected_bad_findings = 2
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"),
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}")
157 print(f
"check_no_driver_asm_guard.py --selftest: {len(failed)} failure(s)")
159 print(
"check_no_driver_asm_guard.py --selftest: all cases pass (both directions).")
164 """Fail any HAL driver that guards bare CPU asm on RA8_OFF_TARGET.
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.
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.
175 if args == [
"--selftest"]:
178 print(
"usage: check_no_driver_asm_guard.py [--selftest]", file=sys.stderr)
180 if not DRIVER_DIR.is_dir():
181 print(f
"check_no_driver_asm_guard.py: driver dir not found: {DRIVER_DIR}")
184 drivers = sorted(DRIVER_DIR.glob(
"*.c"))
185 all_problems: list[str] = []
187 all_problems.extend(check_file(path))
190 print(
"check_no_driver_asm_guard.py: a HAL driver guards bare asm on RA8_OFF_TARGET:")
191 for p
in all_problems:
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).")
200 f
"check_no_driver_asm_guard.py: PASS -- {len(drivers)} HAL driver TU(s) "
201 f
"carry no RA8_OFF_TARGET-guarded asm."
206if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.