4"""audit_init_order.py -- per-app init-order linter.
6Walks every ``main.c`` under ``examples/`` at ANY depth, extracts the
7sequence of ``ra8_*_init(`` and ``ra8_board_*_init(`` calls in source
8order, and verifies the sequence respects the project-wide canonical
11 CGC -> MSTP -> IOPORT -> peripherals
13The check is structural rather than full-graph: for every adjacent pair
14of init calls we ensure the earlier one has a lower-or-equal canonical
15rank. Calls outside the ranked set (e.g. ``ra8_acmphs_init``) are
16treated as "peripheral" and must come last. The script emits a warning
17for each offending pair and -- in the default STRICT mode -- exits
18non-zero if any app fails. Pass ``--no-strict`` to downgrade to
19warn-only output (useful while landing a fix).
21Optionally writes a Markdown report to a path given via ``--report``.
23Copyright (c) 2026 Brighton Sikarskie
24SPDX-License-Identifier: MIT
27from __future__
import annotations
33from dataclasses
import dataclass
34from pathlib
import Path
36sys.path.insert(0, str(Path(__file__).resolve().parent))
38from selftest_assert
import expect, report
48SELFTEST_ORDERED_CALLS = 3
63 "ra8_cgc_init": RANK_CGC,
64 "ra8_cgc_get_clock_hz": RANK_CGC,
65 "ra8_mstp_init": RANK_MSTP,
66 "ra8_pfs_init": RANK_IOPORT,
67 "ra8_pfs_route": RANK_IOPORT,
68 "ra8_gpio_init": RANK_IOPORT,
69 "ra8_gpio_output_init": RANK_IOPORT,
70 "ra8_gpio_input_init": RANK_IOPORT,
71 "ra8_port_init": RANK_IOPORT,
72 "ra8_pin_validator": RANK_IOPORT,
73 "ra8_time_init": RANK_TIME,
74 "ra8_systick_init": RANK_TIME,
75 "ra8_icu_init": RANK_ISR_CORE,
76 "ra8_isr_init": RANK_ISR_CORE,
79INIT_CALL_RE = re.compile(
r"\b(ra8_[a-z0-9_]+|ra8_board_[a-z0-9_]+)\(", re.IGNORECASE)
84 """One ``ra8_*_init`` call site recorded from main.c."""
93 """Audit result for a single app."""
98 violations: list[tuple[InitCall, InitCall]]
101def rank_for(symbol: str) -> int:
102 """Return the canonical rank for ``symbol`` (substring match)."""
103 for key, rank
in RANK_TABLE.items():
104 if symbol.startswith(key):
106 return RANK_PERIPHERAL
109def is_init_call(symbol: str) -> bool:
110 """Return True for symbols we want to track."""
111 if symbol ==
"ra8_cgc_get_clock_hz":
113 return symbol.endswith((
"_init",
"_pins_init"))
or "_init_" in symbol
116_MAIN_SIG_RE = re.compile(
r"\b(?:int|int32_t|void)\s+main\s*\(")
119def extract_calls(main_path: Path) -> list[InitCall]:
120 """Walk ``main_path`` and pull every init-style call in source order.
122 Only calls whose source position falls inside the body of ``main()``
123 are considered: helper functions defined above ``main`` (like a
124 static ``demo_pins_init``) routinely call ``ra8_*_init`` symbols in
125 an order that matches the helper's local logic, not the boot-time
126 sequence the audit cares about. The textual brace-tracker below is
127 intentionally simple -- it works for the project's hand-written
128 ``main()`` style (no nested function defs, no preprocessor games).
130 calls: list[InitCall] = []
131 text = main_path.read_text(encoding=
"ascii", errors=
"replace")
134 for lineno, line
in enumerate(text.splitlines(), start=1):
136 stripped = line.lstrip()
137 if stripped.startswith((
"//",
"*")):
139 if not in_main
and _MAIN_SIG_RE.search(line)
is not None:
154 for match
in INIT_CALL_RE.finditer(line):
156 if not is_init_call(sym):
158 calls.append(InitCall(name=sym, line=lineno, rank=rank_for(sym)))
162def audit_app(app: str, main_path: Path) -> AppAudit:
163 """Run the rank-monotonicity check for one app."""
164 calls = extract_calls(main_path)
165 violations: list[tuple[InitCall, InitCall]] = []
166 last: InitCall |
None =
None
168 if last
is not None and call.rank < last.rank:
169 violations.append((last, call))
171 return AppAudit(app=app, main_path=main_path, calls=calls, violations=violations)
174def collect_apps(repo_root: Path) -> list[tuple[str, Path]]:
175 """Discover every app ``src/main.c`` under ``examples/``, at any depth.
177 The glob used to be ``examples/*/*/main.c`` -- exactly three levels -- while
178 the tree's real layout is ``examples/<tier>/.../<app>/``, up to five deep.
179 It therefore audited 11 of 217 apps and reported "0 with violations" on the
180 other 206, for as long as it had been wired into the pre-commit hook and the
181 ``pre-commit-checks`` gate. The same depth-capped glob had already been
182 found and fixed in the clang-tidy file collector; this is the second
183 instance, so the fix here is the recursive form plus the floor in ``main()``
184 that makes a collapsed discovery fail instead of pass.
187 (main.parent.parent.name, main)
for main
in sorted(repo_root.glob(
"examples/**/src/main.c"))
191def render_markdown(audits: list[AppAudit], repo_root: Path) -> str:
192 """Render a human-readable Markdown report of the audit run."""
193 lines: list[str] = []
194 lines.append(
"# Per-app Init-Order Audit")
197 "Generated by ``scripts/checks/audit_init_order.py``. Validates that"
198 " every app's main.c follows the canonical CGC -> MSTP -> IOPORT ->"
202 bad = [a
for a
in audits
if a.violations]
203 lines.append(f
"- Apps audited: {len(audits)}")
204 lines.append(f
"- Apps with violations: {len(bad)}")
207 lines.append(
"## Violations")
210 lines.append(f
"### {audit.app}")
211 for prev, cur
in audit.violations:
213 f
"- {prev.name} (rank {prev.rank}, line {prev.line}) precedes "
214 f
"{cur.name} (rank {cur.rank}, line {cur.line})"
217 lines.append(
"## Per-app init sequences")
220 rel = audit.main_path.relative_to(repo_root)
221 lines.append(f
"### {audit.app}")
223 lines.append(f
"Source: ``{rel}``")
226 lines.append(
"- (no init calls detected)")
228 lines.extend(f
"- L{c.line}: {c.name} (rank {c.rank})" for c
in audit.calls)
230 return "\n".join(lines) +
"\n"
233def selftest() -> int:
234 """Prove the ordering detector and the discovery floor, in both directions.
236 The two properties are asserted separately on purpose. A detector that has
237 stopped matching and a tree that is genuinely ordered produce identical
238 output, and so do a collapsed glob and an empty ``examples/``; only an
239 explicit assertion tells them apart.
242 0 when every assertion held, 1 otherwise.
244 failures: list[str] = []
245 root = Path(__file__).resolve().parents[2]
247 with tempfile.TemporaryDirectory()
as tmp:
248 app = Path(tmp) /
"examples" /
"tier" /
"group" /
"deep" /
"app"
250 src.mkdir(parents=
True)
255 def app_main(*calls: str) -> str:
256 body =
"\n".join(f
" {call}();" for call
in calls)
257 return f
"int main(void)\n{{\n{body}\n return 0;\n}}\n"
259 (src /
"main.c").write_text(app_main(
"ra8_cgc_init",
"ra8_mstp_init",
"ra8_sci_init"))
260 found = collect_apps(Path(tmp))
261 expect(len(found) == 1,
"recursive discovery reaches a nested app src/main.c", failures)
262 ordered = audit_app(
"app", src /
"main.c")
264 len(ordered.calls) == SELFTEST_ORDERED_CALLS,
265 "all three init calls are extracted from main()",
268 expect(
not ordered.violations,
"a correctly ordered app stays quiet", failures)
270 (src /
"main.c").write_text(app_main(
"ra8_sci_init",
"ra8_cgc_init"))
271 inverted = audit_app(
"app", src /
"main.c")
272 expect(bool(inverted.violations),
"a peripheral before CGC fires", failures)
274 live = collect_apps(root)
276 len(live) >= APP_FLOOR,
277 f
"live discovery sees {len(live)} app(s) (floor {APP_FLOOR})",
280 return report(failures)
283def build_parser() -> argparse.ArgumentParser:
284 """Build the command-line parser for this gate."""
285 parser = argparse.ArgumentParser(description=__doc__)
289 default=Path(__file__).resolve().parents[2],
290 help=
"Repository root (defaults to the script's grandparent).",
296 help=
"Optional Markdown report output path.",
301 help=
"prove the ordering detector fires on a bad sequence and spares a good one",
305 action=argparse.BooleanOptionalAction,
308 "Exit non-zero if any app has a violation (default: True). "
309 "Use --no-strict to suppress the non-zero exit (warn-only mode)."
316 """Gate the canonical CGC -> MSTP -> IOPORT -> peripheral init order.
318 Strict by default: an app whose init calls run out of canonical order
319 fails the run. ``--no-strict`` downgrades to warn-only output, which is
320 useful while landing a fix but is not what any caller in this tree passes.
322 Discovery is floored. A glob that stops matching does not report
323 violations it cannot see -- it reports a clean tree, which reads as an
324 improvement. So a discovery below ``APP_FLOOR`` is a hard error rather
325 than a quiet success.
328 0 when every discovered app is ordered correctly, 1 when any is not
329 (strict mode), and 2 when discovery collapsed.
331 args = build_parser().parse_args()
336 apps = collect_apps(args.repo_root)
337 if len(apps) < APP_FLOOR:
339 f
"audit_init_order: FATAL -- discovered only {len(apps)} app(s) under "
340 f
"{args.repo_root / 'examples'}, below the floor of {APP_FLOOR}.\n"
341 " A collapsed discovery reports success because it saw nothing. The\n"
342 " glob was depth-capped at three levels for the life of this gate and\n"
343 " audited 11 of 217 apps; the floor exists so that cannot recur.",
348 audits = [audit_app(app, main)
for app, main
in apps]
351 if not audit.violations:
354 rel = audit.main_path.relative_to(args.repo_root)
355 for prev, cur
in audit.violations:
357 f
"WARN {audit.app}: {rel}:{cur.line}: "
358 f
"{cur.name} (rank {cur.rank}) follows {prev.name} "
359 f
"(rank {prev.rank}) at line {prev.line}"
362 if args.report
is not None:
363 args.report.write_text(render_markdown(audits, args.repo_root))
364 print(f
"Report written to {args.report}")
366 print(f
"audit_init_order: {len(audits)} apps audited, {bad_count} with violations.")
367 return 1
if (args.strict
and bad_count)
else 0
370if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.