109 lines
3.9 KiB
Python
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")
|