#!/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")