ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_hum_register_map.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: register symbols in first-party code must exist in the HUM.
5
6``cite_check.py`` asks "is this citation well-formed, and does it name a real
7chapter and a page inside that chapter?". It cannot ask "does the cited text
8describe the register being touched?", and it cannot ask "does this register
9exist at all?". Three landed defects lived in that gap, every one of them
10carrying a citation naming a REAL chapter and a REAL page:
11
12* the ``ra8_rsip`` crypto family addressed registers HUM Ch 52 does not
13 publish -- that chapter is six pages long and describes no registers at all;
14* ``ra8_ptp_regs.h`` declared a thirteen-register window at ``0x403E_0100``,
15 a reserved hole in the GPTP aperture. The demo printed ``gptp: clock PASS``
16 because a reserved aperture echoed its own writes back (#498);
17* ``ra8_etha_regs.h`` declared ``EASCR`` at ``0x0580``, a symbol absent from
18 the whole of Ch 32, and ~26 ETHA citations pointed at real pages describing
19 other registers (#539).
20
21This gate closes that gap by connecting a register SYMBOL in our source to the
22symbol table the manual publishes. Four rules, all reported per file:
23
24``unknown-cite-symbol``
25 A citation names symbol ``S`` in chapter ``C`` and ``S`` appears nowhere
26 in chapter ``C``. This is the signal that actually worked by hand: a
27 register name that is absent from the cited chapter is almost always
28 invented. When ``S`` does exist in some other chapter the diagnostic says
29 so, because a mis-attributed real register is a much smaller mistake.
30
31``wrong-cite-page``
32 A citation names a real symbol but points outside the pages that symbol's
33 description occupies. A description routinely spills onto the following
34 page, so the comparison is against a page RANGE (heading page up to the
35 next section heading) rather than a single page -- a naive equality check
36 would shout about hundreds of correct citations.
37
38``unknown-window-symbol``
39 A register-window struct member, or an offset enumerator, in any
40 first-party header whose symbol appears in none of the chapters that
41 header names. This is the rule that would have caught #498, whose
42 citations named no symbol at all.
43
44``wrong-window-offset``
45 An offset enumerator whose value disagrees with the offset the manual
46 prints for that register.
47
48WHAT THIS DOES NOT CHECK
49------------------------
50Being precise about the edge is part of not crying wolf:
51
52* **Bit fields.** Only register-level symbols are cross-checked. A driver
53 writing the right register with the wrong field -- which is exactly what
54 #539's third defect was, ``EATASGL0`` receiving a gate state instead of an
55 entry address -- is invisible here and always will be. Nothing mechanical
56 substitutes for reading the field table.
57* **Citations that name no register.** ``/* HUM Ch 32.4 "Error Interrupt
58 Sources" p 1685 */`` carries no symbol, so there is nothing to resolve;
59 1665 of the tree's citations name a register and are checked, the rest are
60 skipped rather than guessed at.
61* **Headers that name no HUM chapter.** ``ra8_touch_gt911_regs.h`` describes
62 an external I2C touch controller that is not in this manual at all, so it
63 is unattributable by construction and is left alone.
64* **Absolute-address enumerators, entirely.** The window scan recognises
65 struct members and OFFSET enumerators (``k_ra8_etha_off_eatasgl0``). An
66 enumerator holding a whole address instead -- ``k_ra8_wdt_ofs0_addr =
67 0x03001E04UL`` -- matches neither shape and is not examined at all. Nor
68 would the symbol rule help if it were: ``OFS0`` and ``OFS3`` are REAL
69 registers (HUM Ch 7.2.1 p 280 and Ch 7.2.6 p 287), documented by absolute
70 address with no offset, so this gate has nothing to compare. The defect in
71 that case is that ``0x03001E04`` / ``0x03001E20`` fall inside a window both
72 manuals mark ``Reserved area`` -- a wrong ADDRESS, not an invented NAME.
73 That needs an address-versus-reserved-window guard, which is a different
74 rule and is being added separately under #545. Do not read this gate as
75 covering it.
76
77WHAT MAKES THIS GATE ABLE TO FAIL
78---------------------------------
79Both acceptance properties the #190 audit distilled are designed in, because
80they are precisely what a gate like this gets wrong:
81
82* **No check compares a constant to itself.** The authority is always the
83 committed manual PDF, re-parsed on every run. The committed
84 ``HUM_REGISTERS.csv`` is a reviewable convenience and is byte-compared
85 against that fresh parse, so a CSV edited to make a violation disappear
86 fails the gate instead of hiding the defect.
87* **Every scan has a vacuity floor.** A PDF extraction that silently yielded
88 zero symbols would report every register in the tree clean, forever -- the
89 single most likely failure mode here. ``hum_regmap.assert_not_vacuous``
90 makes that a hard error, and this gate additionally floors the number of
91 source claims it found: a scan that stops matching our own headers is just
92 as dishonest as one that stops matching the manual.
93
94RATCHET, NOT WARN-ONLY
95----------------------
96A full-tree sweep starts with a backlog these four rules did not create: they
97made it visible. Switching rules off, or running warn-only, is the "gate that
98silently does nothing" pattern this tree keeps finding, so instead the debt is
99COUNTED per (file, rule) in ``.github/hum-register-baseline.txt`` -- the same
100shape ``tidy_ratchet.py`` and ``misra_ratchet.py`` use. A new or increased
101count FAILS. Shrinkage passes with a notice to re-baseline, which locks the
102progress in. Closing the debt means the baseline reaching zero rows and being
103deleted, so ``--update`` refuses to grow a bucket.
104
105USAGE
106 check_hum_register_map.py --selftest # prove it fires AND stays quiet
107 check_hum_register_map.py # the gate
108 check_hum_register_map.py --update # re-baseline (shrink only)
109"""
110
111from __future__ import annotations
112
113import argparse
114import pathlib
115import re
116import sys
117from collections import Counter
118from dataclasses import dataclass
119
120sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
121
122import hum_regmap
123import hum_regmap_scan
124from lint_targets import first_party_paths
125from selftest_assert import expect, report
126
127REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
128BASELINE_FILE = REPO_ROOT / ".github" / "hum-register-baseline.txt"
129
130SOURCE_SUFFIXES = (".c", ".h", ".cpp", ".hpp")
131
132RULES = (
133 "unknown-cite-symbol",
134 "wrong-cite-page",
135 "unknown-window-symbol",
136 "wrong-window-offset",
137)
138
139# Vacuity floors on OUR side of the comparison. The manual's side is floored
140# in hum_regmap. A scan that stopped recognising our citations or our register
141# windows would report a clean tree because it looked at nothing.
142MIN_FILES_SCANNED = 500
143MIN_CITE_CLAIMS = 400
144MIN_WINDOW_CLAIMS = 400
145
146MAX_DETAIL_LINES = 25
147"""Cap on offending findings echoed before the report truncates."""
148
149BASELINE_COLUMNS = 3
150"""Column count of one baseline row: file, rule, count."""
151
152ARRAY_STRIDE_RE = re.compile(r"\+\s*(?P<stride>0[xX][0-9A-Fa-f]+)\s*\*")
153"""The stride of an indexed register's offset, e.g. the 0x4 of "0x0040 + 0x4 * q"."""
154
155
156@dataclass(frozen=True)
157class Finding:
158 """One rule violation, ready to print and to bucket for the ratchet."""
159
160 path: str
161 line: int
162 rule: str
163 detail: str
164
165
166def _check_cite(claim: hum_regmap_scan.SymbolClaim, rmap: hum_regmap.RegisterMap) -> Finding | None:
167 """Apply the two citation rules to one citation claim."""
168 if claim.chapter is None or claim.page is None or claim.page_end is None:
169 return None
170 rows = rmap.lookup(claim.chapter, claim.symbol)
171 if not rows:
172 elsewhere = sorted({row.chapter for row in rmap.find_anywhere(claim.symbol)})
173 where = f"but Ch {elsewhere} describes it" if elsewhere else "and no chapter describes it"
174 return Finding(
175 claim.path,
176 claim.line,
177 "unknown-cite-symbol",
178 f"{claim.symbol} does not appear in HUM Ch {claim.chapter} -- {where}",
179 )
180 if any(row.covers(claim.page, claim.page_end) for row in rows):
181 return None
182 spans = ", ".join(f"p {row.page}-{row.page_end}" for row in rows)
183 return Finding(
184 claim.path,
185 claim.line,
186 "wrong-cite-page",
187 f"{claim.symbol} cited at p {claim.page}; HUM describes it at {spans}",
188 )
189
190
191def _array_base_rows(
192 symbol: str, chapters: set[int], rmap: hum_regmap.RegisterMap
193) -> list[hum_regmap.HumRegister]:
194 """Resolve ``EATMFSC0``-style names for the manual's ``EATMFSCq`` array.
195
196 A header names the base element of an indexed register with an explicit
197 ``0`` (``k_ra8_etha_off_eatmfsc0`` for what the manual calls
198 ``EATMFSCq``). That is a real alias, not an invented register, so it is
199 resolved rather than reported -- but only when the manual's own entry is
200 genuinely INDEXED, i.e. its offset expression carries a stride. Without
201 that condition the rule would also swallow ``EAEIS9``, and the point of
202 trailing digits staying significant is that ``EAEIS0`` and ``EAEIS1`` are
203 different registers.
204 """
205 base = symbol.rstrip("0123456789")
206 if base == symbol or not base:
207 return []
208 rows = [row for chapter in sorted(chapters) for row in rmap.lookup(chapter, base)]
209 return [row for row in rows if "*" in row.offset_expr]
210
211
212def _expected_offsets(row: hum_regmap.HumRegister, symbol: str) -> list[int]:
213 """Byte offsets the manual allows for a header symbol naming `row`.
214
215 For a plain register that is just the printed offset. For an INDEXED
216 register the header may name any element explicitly -- ``IPCSEM15`` for
217 the manual's ``IPCSEMn`` at ``0x000 + 0x004 * n`` -- so the element index
218 is read off the header's own symbol and the stride applied. Comparing
219 element 15 against the array's base offset was reporting sixteen correct
220 declarations as wrong.
221 """
222 if not row.offset:
223 return []
224 base = int(row.offset, 16)
225 stride_match = ARRAY_STRIDE_RE.search(row.offset_expr)
226 if stride_match is None:
227 return [base]
228 digits = symbol[len(symbol.rstrip("0123456789")) :]
229 if not digits:
230 return [base]
231 return [base, base + int(digits) * int(stride_match.group("stride"), 16)]
232
233
234def _check_window(
235 claim: hum_regmap_scan.SymbolClaim, chapters: set[int], rmap: hum_regmap.RegisterMap
236) -> Finding | None:
237 """Apply the two register-window rules to one struct or offset claim."""
238 rows = [row for chapter in sorted(chapters) for row in rmap.lookup(chapter, claim.symbol)]
239 if not rows:
240 rows = _array_base_rows(claim.symbol, chapters, rmap)
241 if not rows:
242 elsewhere = sorted({row.chapter for row in rmap.find_anywhere(claim.symbol)})
243 where = f"but Ch {elsewhere} describes it" if elsewhere else "and no chapter describes it"
244 return Finding(
245 claim.path,
246 claim.line,
247 "unknown-window-symbol",
248 f"{claim.symbol} does not appear in the chapters this file cites "
249 f"({sorted(chapters)}) -- {where}",
250 )
251 if claim.offset is None:
252 return None
253 expected = sorted({addr for row in rows for addr in _expected_offsets(row, claim.symbol)})
254 if not expected or claim.offset in expected:
255 return None
256 printed = ", ".join(f"0x{addr:04X}" for addr in expected)
257 return Finding(
258 claim.path,
259 claim.line,
260 "wrong-window-offset",
261 f"{claim.symbol} declared at 0x{claim.offset:04X}; HUM puts it at {printed}",
262 )
263
264
265@dataclass
266class ScanResult:
267 """Everything one full-tree scan produced, findings and vacuity counters."""
268
269 findings: list[Finding]
270 files: int
271 cite_claims: int
272 window_claims: int
273
274
275def scan_tree(rmap: hum_regmap.RegisterMap, paths: list[str] | None = None) -> ScanResult:
276 """Apply every rule to every first-party C file."""
277 targets = (
278 paths
279 if paths is not None
280 else first_party_paths(SOURCE_SUFFIXES, respect_language_excludes=True)
281 )
282 result = ScanResult([], 0, 0, 0)
283 for rel in targets:
284 try:
285 text = (REPO_ROOT / rel).read_text(encoding="utf-8", errors="replace")
286 except OSError:
287 continue
288 result.files += 1
289 cites = hum_regmap_scan.cite_claims(rel, text)
290 result.cite_claims += len(cites)
291 for claim in cites:
292 finding = _check_cite(claim, rmap)
293 if finding is not None:
294 result.findings.append(finding)
295 # Register windows are declared in headers. Scoping this to
296 # "*_regs.h" looked tidy and missed ra8_rsip_regs_offsets.h, which
297 # declares the whole RSIP offset map under a name the pattern does
298 # not match; any header may declare a window, so any header is read.
299 if not rel.endswith((".h", ".hpp")):
300 continue
301 windows = hum_regmap_scan.window_claims(rel, text)
302 result.window_claims += len(windows)
303 # Every chapter the file cites, NOT only the ones that publish
304 # registers. Intersecting with rmap.chapters would skip a header whose
305 # only cited chapter documents no registers at all -- which is exactly
306 # ra8_rsip_regs.h, citing a six-page Ch 52 that publishes none. A file
307 # with no citation whatsoever is genuinely unattributable (the GT911
308 # touch controller is an external I2C part, not in this manual) and is
309 # the one case that is skipped.
310 chapters = hum_regmap_scan.cited_chapters(text)
311 if not chapters:
312 continue
313 for claim in windows:
314 finding = _check_window(claim, chapters, rmap)
315 if finding is not None:
316 result.findings.append(finding)
317 return result
318
319
320def assert_scan_not_vacuous(result: ScanResult) -> None:
321 """Fail when the source-side scan found too little to have worked.
322
323 Raises:
324 hum_regmap.HumExtractionError: a floor was not met.
325 """
326 problems = []
327 if result.files < MIN_FILES_SCANNED:
328 problems.append(f"{result.files} files scanned < floor {MIN_FILES_SCANNED}")
329 if result.cite_claims < MIN_CITE_CLAIMS:
330 problems.append(f"{result.cite_claims} register citations < floor {MIN_CITE_CLAIMS}")
331 if result.window_claims < MIN_WINDOW_CLAIMS:
332 problems.append(f"{result.window_claims} window symbols < floor {MIN_WINDOW_CLAIMS}")
333 if problems:
334 raise hum_regmap.HumExtractionError(
335 "source scan is vacuous -- every rule would pass because nothing was "
336 "examined: " + "; ".join(problems)
337 )
338
339
340def bucket(findings: list[Finding]) -> Counter[tuple[str, str]]:
341 """Reduce findings to the ``(file, rule) -> count`` the ratchet compares.
342
343 Line numbers churn on every unrelated edit above them, so they are not
344 part of the key: a count per file per rule is invariant under that and
345 still trips the moment a file gains another violation.
346 """
347 return Counter((finding.path, finding.rule) for finding in findings)
348
349
350def load_baseline() -> Counter[tuple[str, str]]:
351 """Read the committed per-(file, rule) debt, or an empty tally."""
352 counts: Counter[tuple[str, str]] = Counter()
353 if not BASELINE_FILE.is_file():
354 return counts
355 for raw in BASELINE_FILE.read_text(encoding="utf-8").splitlines():
356 line = raw.strip()
357 if not line or line.startswith("#"):
358 continue
359 parts = line.split("\t")
360 if len(parts) != BASELINE_COLUMNS:
361 continue
362 counts[(parts[0], parts[1])] = int(parts[2])
363 return counts
364
365
366def write_baseline(counts: Counter[tuple[str, str]]) -> None:
367 """Write the ratchet file in a stable, diffable order."""
368 lines = [
369 "# HUM register-map debt, per (file, rule). Generated by",
370 "# scripts/checks/check_hum_register_map.py --update.",
371 "#",
372 "# A NEW or INCREASED count fails the gate. Shrinkage passes with a notice",
373 "# to re-baseline, which locks the progress in. Closing the debt means this",
374 "# file reaching zero rows and being deleted -- --update refuses to grow a",
375 "# bucket, so a genuine increase has to be a reviewable hand edit.",
376 ]
377 for (path, rule), count in sorted(counts.items()):
378 if count:
379 lines.append(f"{path}\t{rule}\t{count}")
380 BASELINE_FILE.parent.mkdir(parents=True, exist_ok=True)
381 BASELINE_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
382
383
384def compare(actual: Counter[tuple[str, str]], baseline: Counter[tuple[str, str]]) -> list[str]:
385 """Return one message per bucket that grew above its baseline."""
386 regressions = []
387 for key, count in sorted(actual.items()):
388 allowed = baseline.get(key, 0)
389 if count > allowed:
390 path, rule = key
391 regressions.append(f"{path}: {rule} {count} > baseline {allowed}")
392 return regressions
393
394
395def freshness_error(rows: list[hum_regmap.HumRegister]) -> str | None:
396 """Describe how the committed CSV differs from a fresh parse of the PDF."""
397 fresh = hum_regmap.to_csv(rows)
398 if not hum_regmap.HUM_CSV.is_file():
399 return f"{hum_regmap.HUM_CSV} is missing"
400 committed = hum_regmap.HUM_CSV.read_text(encoding="utf-8")
401 if committed == fresh:
402 return None
403 return (
404 f"{hum_regmap.HUM_CSV.relative_to(REPO_ROOT)} does not match a fresh parse of "
405 f"{hum_regmap.HUM_PDF.name} ({len(committed)} vs {len(fresh)} bytes); "
406 "run scripts/gen/gen_hum_register_map.py"
407 )
408
409
410def _print_findings(findings: list[Finding]) -> None:
411 """Echo up to ``MAX_DETAIL_LINES`` findings, worst rule first."""
412 order = {rule: index for index, rule in enumerate(RULES)}
413 ranked = sorted(findings, key=lambda f: (order.get(f.rule, 99), f.path, f.line))
414 for finding in ranked[:MAX_DETAIL_LINES]:
415 print(
416 f" {finding.path}:{finding.line}: [{finding.rule}] {finding.detail}", file=sys.stderr
417 )
418 if len(ranked) > MAX_DETAIL_LINES:
419 print(f" ... and {len(ranked) - MAX_DETAIL_LINES} more", file=sys.stderr)
420
421
422def _verdict(actual: Counter[tuple[str, str]], findings: list[Finding]) -> int:
423 """Compare against the committed baseline and print the gate's verdict."""
424 baseline = load_baseline()
425 regressions = compare(actual, baseline)
426 if regressions:
427 print(
428 f"\nFAIL: {len(regressions)} (file, rule) bucket(s) above the committed baseline.",
429 file=sys.stderr,
430 )
431 for message in regressions:
432 print(f" {message}", file=sys.stderr)
433 grown = {key for key, count in actual.items() if count > baseline.get(key, 0)}
434 print("\nOffending findings:", file=sys.stderr)
435 _print_findings([f for f in findings if (f.path, f.rule) in grown])
436 return 1
437
438 shrunk = sum(1 for key, count in baseline.items() if actual.get(key, 0) < count)
439 if shrunk:
440 print(
441 f"\nNOTICE: {shrunk} bucket(s) shrank. Re-baseline with "
442 "`python3 scripts/checks/check_hum_register_map.py --update` to lock it in."
443 )
444 print("\nPASS: no register symbol regressed against the manual.")
445 return 0
446
447
448def run_gate(update: bool) -> int:
449 """Regenerate the map, scan the tree, and apply the ratchet."""
450 try:
451 rows = hum_regmap.extract_from_pdf()
452 except hum_regmap.HumExtractionError as exc:
453 print(f"ERROR: {exc}", file=sys.stderr)
454 return 1
455
456 stale = freshness_error(rows)
457 if stale is not None:
458 print(f"ERROR: {stale}", file=sys.stderr)
459 return 1
460
461 rmap = hum_regmap.RegisterMap(rows)
462 result = scan_tree(rmap)
463 try:
464 assert_scan_not_vacuous(result)
465 except hum_regmap.HumExtractionError as exc:
466 print(f"ERROR: {exc}", file=sys.stderr)
467 return 1
468
469 actual = bucket(result.findings)
470 print(
471 f"HUM register map: {len(rows)} registers over {len(rmap.chapters)} chapters; "
472 f"scanned {result.files} files, {result.cite_claims} register citations, "
473 f"{result.window_claims} window symbols"
474 )
475 for rule in RULES:
476 total = sum(count for (_, name), count in actual.items() if name == rule)
477 print(f" {rule}: {total}")
478
479 if update:
480 baseline = load_baseline()
481 # Seeding is the one time the file is allowed to appear from nothing:
482 # before it exists there is no line to hold. Every later --update may
483 # only shrink, which is what makes this a ratchet rather than a
484 # rubber stamp.
485 seeding = not BASELINE_FILE.is_file()
486 grown = [] if seeding else [k for k, c in actual.items() if c > baseline.get(k, 0)]
487 if seeding:
488 print(f"seeding {BASELINE_FILE.relative_to(REPO_ROOT)} for the first time")
489 if grown:
490 print("ERROR: --update refuses to grow the baseline:", file=sys.stderr)
491 for path, rule in sorted(grown):
492 print(f" {path}: {rule}", file=sys.stderr)
493 return 1
494 write_baseline(actual)
495 print(f"re-baselined {BASELINE_FILE.relative_to(REPO_ROOT)}")
496 return 0
497
498 return _verdict(actual, result.findings)
499
500
501def _selftest_map() -> hum_regmap.RegisterMap:
502 """A tiny hand-built register map standing in for the manual."""
503 return hum_regmap.RegisterMap(
504 [
505 hum_regmap.HumRegister(32, "3.1.1", "EAMC", "0x0000", "0x0000", 1630, 1631, "Mode Cfg"),
506 hum_regmap.HumRegister(
507 32, "3.2.6", "EATMFSCq", "0x0040", "0x0040 + 0x4 * q", 1635, 1636, "Max Frame"
508 ),
509 ]
510 # Pad past the vacuity floor so the floor itself is not what is under
511 # test here; the floor gets its own assertions below.
512 + [
513 hum_regmap.HumRegister(1, "1", f"REG{n:04d}", "0x0000", "0x0000", 100, 100, "pad")
514 for n in range(hum_regmap.MIN_TOTAL_ROWS)
515 ]
516 + [
517 hum_regmap.HumRegister(ch, "1", f"CH{ch}REG", "0x0000", "0x0000", 100, 100, "pad")
518 for ch in range(2, hum_regmap.MIN_CHAPTERS_WITH_REGISTERS + 2)
519 ]
520 )
521
522
523def _selftest_cite_rules(failures: list[str]) -> None:
524 """Assert the two citation rules fire and stay quiet as documented."""
525 rmap = _selftest_map()
526
527 good = (
528 '/* HUM Ch 32.3.1.1 "EAMC : Mode Cfg" p 1630 */\n'
529 '/* HUM Ch 32.3.1.1 "EAMC : Mode Cfg" p 1631 */\n'
530 '/* HUM Ch 32.3.2.6 "EATMFSCq : Max Frame" p 1635 */\n'
531 )
532 findings = [
533 f for c in hum_regmap_scan.cite_claims("good.c", good) if (f := _check_cite(c, rmap))
534 ]
535 expect(not findings, "real symbol, real page, spilled page: no finding", failures)
536
537 invented = '/* HUM Ch 32.3 "EASCR : Security Configuration" p 1697 */\n'
538 findings = [
539 f for c in hum_regmap_scan.cite_claims("bad.c", invented) if (f := _check_cite(c, rmap))
540 ]
541 expect(
542 len(findings) == 1 and findings[0].rule == "unknown-cite-symbol",
543 "invented cited symbol fires unknown-cite-symbol",
544 failures,
545 )
546
547 misplaced = '/* HUM Ch 32.3.1.1 "EAMC : Mode Cfg" p 1690 */\n'
548 findings = [
549 f for c in hum_regmap_scan.cite_claims("bad.c", misplaced) if (f := _check_cite(c, rmap))
550 ]
551 expect(
552 len(findings) == 1 and findings[0].rule == "wrong-cite-page",
553 "real symbol on the wrong page fires wrong-cite-page",
554 failures,
555 )
556
557 prose = '/* HUM Ch 32.4 "Error Interrupt Sources" p 1685 */\n'
558 expect(
559 not hum_regmap_scan.cite_claims("p.c", prose),
560 "a citation naming no register yields no claim (no false positive)",
561 failures,
562 )
563
564
565def _window_findings(source: str, rmap: hum_regmap.RegisterMap) -> list[Finding]:
566 """Run the register-window rules over a snippet, for the selftest."""
567 return [
568 finding
569 for claim in hum_regmap_scan.window_claims("regs.h", source)
570 if (finding := _check_window(claim, {32}, rmap)) is not None
571 ]
572
573
574def _selftest_window_rules(failures: list[str]) -> None:
575 """Assert the two register-window rules fire and stay quiet as documented."""
576 rmap = _selftest_map()
577
578 window_ok = (
579 " volatile uint32_t EAMC;\n"
580 " volatile uint32_t reserved08;\n"
581 " volatile uint32_t EATMFSC[8];\n"
582 " k_ra8_etha_off_eamc = 0x0000U,\n"
583 )
584 claims = hum_regmap_scan.window_claims("regs.h", window_ok)
585 findings = _window_findings(window_ok, rmap)
586 expect(not findings, "real window symbols and offsets: no finding", failures)
587 expect(
588 all(claim.symbol != "RESERVED08" for claim in claims),
589 "reserved padding is not treated as a register",
590 failures,
591 )
592
593 window_bad = " volatile uint32_t EASCR;\n"
594 findings = _window_findings(window_bad, rmap)
595 expect(
596 len(findings) == 1 and findings[0].rule == "unknown-window-symbol",
597 "invented struct member fires unknown-window-symbol",
598 failures,
599 )
600
601
602def _selftest_array_rules(failures: list[str]) -> None:
603 """Assert indexed-register aliases resolve without weakening the rule."""
604 rmap = _selftest_map()
605
606 array_base = " k_ra8_etha_off_eatmfsc0 = 0x0040U,\n"
607 findings = _window_findings(array_base, rmap)
608 expect(
609 not findings,
610 "EATMFSC0 resolves to the manual's indexed EATMFSCq (no false positive)",
611 failures,
612 )
613
614 not_indexed = " volatile uint32_t EAMC9;\n"
615 findings = _window_findings(not_indexed, rmap)
616 expect(
617 len(findings) == 1 and findings[0].rule == "unknown-window-symbol",
618 "a trailing digit on a NON-indexed register is still reported (EAMC9)",
619 failures,
620 )
621
622 element = " k_ra8_etha_off_eatmfsc3 = 0x004CU,\n"
623 findings = _window_findings(element, rmap)
624 expect(
625 not findings,
626 "an explicitly-named array element uses base + index * stride (EATMFSC3)",
627 failures,
628 )
629
630 element_bad = " k_ra8_etha_off_eatmfsc3 = 0x0050U,\n"
631 findings = _window_findings(element_bad, rmap)
632 expect(
633 len(findings) == 1 and findings[0].rule == "wrong-window-offset",
634 "an array element at the wrong stride multiple still fires",
635 failures,
636 )
637
638 offset_bad = " k_ra8_etha_off_eamc = 0x0580U,\n"
639 findings = _window_findings(offset_bad, rmap)
640 expect(
641 len(findings) == 1 and findings[0].rule == "wrong-window-offset",
642 "offset disagreeing with the manual fires wrong-window-offset",
643 failures,
644 )
645
646
647def _selftest_guards(failures: list[str]) -> None:
648 """Assert the vacuity floors and the ratchet direction both hold."""
649 empty = []
650 try:
651 hum_regmap.assert_not_vacuous(empty)
652 fired = False
653 except hum_regmap.HumExtractionError:
654 fired = True
655 expect(fired, "an empty manual extraction is rejected as vacuous", failures)
656
657 try:
658 hum_regmap.assert_not_vacuous(_selftest_map().rows)
659 quiet = True
660 except hum_regmap.HumExtractionError:
661 quiet = False
662 expect(quiet, "a full manual extraction passes the vacuity floor", failures)
663
664 try:
665 assert_scan_not_vacuous(ScanResult([], 0, 0, 0))
666 fired = False
667 except hum_regmap.HumExtractionError:
668 fired = True
669 expect(fired, "a source scan that examined nothing is rejected as vacuous", failures)
670
671 base = Counter({("a.c", "unknown-cite-symbol"): 2})
672 expect(
673 not compare(Counter({("a.c", "unknown-cite-symbol"): 2}), base),
674 "a bucket at its baseline passes",
675 failures,
676 )
677 expect(
678 not compare(Counter({("a.c", "unknown-cite-symbol"): 1}), base),
679 "a bucket below its baseline passes",
680 failures,
681 )
682 expect(
683 len(compare(Counter({("a.c", "unknown-cite-symbol"): 3}), base)) == 1,
684 "a bucket above its baseline fails",
685 failures,
686 )
687 expect(
688 len(compare(Counter({("b.c", "unknown-cite-symbol"): 1}), base)) == 1,
689 "a file absent from the baseline fails on its first finding",
690 failures,
691 )
692
693 expect(
694 hum_regmap.canonical_symbol("EATMFSCq") == hum_regmap.canonical_symbol("EATMFSC"),
695 "array index letters normalise away (EATMFSCq == EATMFSC)",
696 failures,
697 )
698 expect(
699 hum_regmap.canonical_symbol("EAEIS0") != hum_regmap.canonical_symbol("EAEIS1"),
700 "trailing digits stay significant (EAEIS0 != EAEIS1)",
701 failures,
702 )
703
704
705def selftest() -> int:
706 """Prove the detector fires on broken input and stays quiet on good input."""
707 print("check_hum_register_map selftest:")
708 failures: list[str] = []
709 _selftest_cite_rules(failures)
710 _selftest_window_rules(failures)
711 _selftest_array_rules(failures)
712 _selftest_guards(failures)
713 return report(failures)
714
715
716def main(argv: list[str] | None = None) -> int:
717 """Entry point."""
718 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
719 parser.add_argument("--selftest", action="store_true", help="assert both directions and exit")
720 parser.add_argument("--update", action="store_true", help="re-baseline (shrink only)")
721 args = parser.parse_args(argv)
722 if args.selftest:
723 return selftest()
724 return run_gate(args.update)
725
726
727if __name__ == "__main__":
728 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298