Files
Raptor/docs/dialects/check_reference.py
2026-11-06 11:33:33 +01:00

109 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Check operation coverage, required content, and local reference links (stdlib only)."""
from collections import Counter
from html.parser import HTMLParser
from pathlib import Path
import re
import sys
from urllib.parse import unquote, urlsplit
ROOT = Path(__file__).resolve().parents[2]
REFERENCE = ROOT / "docs/dialects/index.html"
FIELDS = {"syntax", "inputs", "outputs", "attributes", "semantics", "constraints"}
class ReferenceParser(HTMLParser):
def __init__(self):
super().__init__()
self.ids = []
self.links = []
self.articles = []
self.current = None
self.field = None
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if "id" in attrs:
self.ids.append(attrs["id"])
if tag == "a" and "href" in attrs:
self.links.append(attrs["href"])
if tag == "article":
self.current = (attrs.get("id", ""), {})
self.articles.append(self.current)
if self.current is not None and "data-field" in attrs:
self.field = attrs["data-field"]
self.current[1][self.field] = ""
def handle_data(self, data):
if self.current is not None and self.field:
self.current[1][self.field] += data
def handle_endtag(self, tag):
if tag in {"pre", "dd"}:
self.field = None
if tag == "article":
self.current = None
def operation_names(sources):
"""Follow named operation base classes, including shared compute/plan bases."""
sources = [re.sub(r"/\*.*?\*/|//[^\n]*", "", source, flags=re.S) for source in sources]
classes = {"SpatOp": "spat", "PimOp": "pim"}
bases = re.findall(r"\bclass\s+(\w+)[^{;]*?:\s*(\w+)\s*<", "\n".join(sources))
for _ in range(len(bases)):
for name, base in bases:
if base in classes:
classes[name] = classes[base]
return {
classes[base] + "." + mnemonic
for source in sources
for base, mnemonic in re.findall(r'\bdef\s+\w+\s*:\s*(\w+)<\s*"([^"]+)"', source)
if base in classes
}
def check(document):
parser = ReferenceParser()
parser.feed(document)
expected = operation_names(
definition.read_text() for definition in sorted((ROOT / "src/PIM/Dialect").rglob("*.td"))
)
errors = []
if not expected:
errors.append("No operation definitions found; check the ODS discovery pattern")
actual = {name for name, _ in parser.articles}
for name in sorted(expected - actual):
errors.append(f"Missing operation: {name}")
for name in sorted(actual - expected):
errors.append(f"Unknown/stale operation: {name}")
for name, count in Counter(parser.ids).items():
if count > 1:
errors.append(f"Duplicate HTML id: {name}")
for name, fields in parser.articles:
for field in sorted(FIELDS):
if not fields.get(field, "").strip():
errors.append(f"{name}: missing/empty {field}")
if name not in fields.get("syntax", ""):
errors.append(f"{name}: syntax does not contain the exact operation name")
if "#" + name not in parser.links:
errors.append(f"{name}: missing index/permalink")
for href in parser.links:
link = urlsplit(href)
if link.scheme or link.netloc:
continue
if not link.path:
if unquote(link.fragment) not in parser.ids:
errors.append(f"Broken fragment: {href}")
elif not (REFERENCE.parent / unquote(link.path)).is_file():
errors.append(f"Broken local link: {href}")
return errors
if __name__ == "__main__":
failures = check(REFERENCE.read_text())
if failures:
print("\n".join(failures), file=sys.stderr)
sys.exit(1)
print("Dialect reference: operation coverage, content fields, and local links OK")