mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
197 lines
7.6 KiB
Python
197 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the machine-readable RTL readiness and synthesis-intent record."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
STATUSES = {"prototype", "verified", "rtl-freeze", "tapeout-ready"}
|
|
INTENT_KEYS = {"registers", "reset", "clock_enable", "memory", "pipeline"}
|
|
EVIDENCE_KINDS = {
|
|
"cdc",
|
|
"equivalence",
|
|
"formal",
|
|
"format",
|
|
"lint",
|
|
"metrics",
|
|
"release",
|
|
"simulation",
|
|
"sta",
|
|
"synthesis",
|
|
"warning",
|
|
}
|
|
REQUIRED_EVIDENCE_BY_STATUS = {
|
|
"verified": {"format", "lint", "simulation", "synthesis"},
|
|
"rtl-freeze": {"equivalence"},
|
|
"tapeout-ready": {
|
|
"cdc",
|
|
"equivalence",
|
|
"format",
|
|
"lint",
|
|
"metrics",
|
|
"release",
|
|
"simulation",
|
|
"sta",
|
|
"synthesis",
|
|
"warning",
|
|
},
|
|
}
|
|
|
|
|
|
def issue(path: Path, message: str) -> str:
|
|
return f"{path}: {message}"
|
|
|
|
|
|
def nonempty_string(value: Any) -> bool:
|
|
return isinstance(value, str) and bool(value.strip())
|
|
|
|
|
|
def check_evidence(root: Path, record_path: Path, target: dict[str, Any]) -> list[str]:
|
|
problems: list[str] = []
|
|
evidence = target.get("required_evidence")
|
|
if not isinstance(evidence, list):
|
|
return [issue(record_path, f"target {target.get('name', '<unnamed>')} required_evidence must be a list")]
|
|
|
|
kinds: set[str] = set()
|
|
for index, item in enumerate(evidence):
|
|
if not isinstance(item, dict):
|
|
problems.append(issue(record_path, f"evidence {index} must be an object"))
|
|
continue
|
|
kind = item.get("kind")
|
|
evidence_path = item.get("path")
|
|
if kind not in EVIDENCE_KINDS:
|
|
problems.append(issue(record_path, f"evidence {index} has an unsupported kind"))
|
|
elif kind in kinds:
|
|
problems.append(issue(record_path, f"target {target.get('name')} duplicates evidence kind {kind}"))
|
|
else:
|
|
kinds.add(kind)
|
|
if not nonempty_string(evidence_path):
|
|
problems.append(issue(record_path, f"evidence {index} must contain a non-empty path"))
|
|
elif not (root / evidence_path).is_file():
|
|
problems.append(issue(record_path, f"evidence path does not exist: {evidence_path}"))
|
|
|
|
status = target.get("status")
|
|
required_kinds = set(REQUIRED_EVIDENCE_BY_STATUS.get(status, set()))
|
|
missing = sorted(required_kinds - kinds)
|
|
if missing:
|
|
problems.append(issue(record_path, f"target {target['name']} is missing evidence: {', '.join(missing)}"))
|
|
return problems
|
|
|
|
|
|
def check_target(root: Path, record_path: Path, target: Any) -> list[str]:
|
|
if not isinstance(target, dict):
|
|
return [issue(record_path, "each target must be an object")]
|
|
problems: list[str] = []
|
|
name = target.get("name")
|
|
if not nonempty_string(name):
|
|
problems.append(issue(record_path, "target name must be a non-empty string"))
|
|
status = target.get("status")
|
|
if status not in STATUSES:
|
|
problems.append(issue(record_path, f"target {name or '<unnamed>'} has invalid status"))
|
|
|
|
paths = target.get("paths")
|
|
if not isinstance(paths, list) or not paths or not all(nonempty_string(path) for path in paths):
|
|
problems.append(issue(record_path, f"target {name or '<unnamed>'} paths must be a non-empty string list"))
|
|
else:
|
|
for path in paths:
|
|
if not (root / path).exists():
|
|
problems.append(issue(record_path, f"target {name} path does not exist: {path}"))
|
|
|
|
profiles = target.get("configuration_profiles")
|
|
if not isinstance(profiles, list) or not profiles or not all(nonempty_string(profile) for profile in profiles):
|
|
problems.append(issue(record_path, f"target {name or '<unnamed>'} configuration_profiles must be a non-empty string list"))
|
|
|
|
intent = target.get("synthesis_intent")
|
|
if not isinstance(intent, dict):
|
|
problems.append(issue(record_path, f"target {name or '<unnamed>'} synthesis_intent must be an object"))
|
|
else:
|
|
for key in sorted(INTENT_KEYS):
|
|
if not nonempty_string(intent.get(key)):
|
|
problems.append(issue(record_path, f"target {name or '<unnamed>'} synthesis_intent.{key} is required"))
|
|
|
|
if status in {"rtl-freeze", "tapeout-ready"}:
|
|
if not nonempty_string(target.get("baseline_revision")):
|
|
problems.append(issue(record_path, f"target {name} requires baseline_revision at {status} status"))
|
|
if not nonempty_string(target.get("configuration_digest")):
|
|
problems.append(issue(record_path, f"target {name} requires configuration_digest at {status} status"))
|
|
|
|
problems.extend(check_evidence(root, record_path, target))
|
|
|
|
waivers = target.get("waivers", [])
|
|
if not isinstance(waivers, list):
|
|
problems.append(issue(record_path, f"target {name or '<unnamed>'} waivers must be a list"))
|
|
else:
|
|
for index, waiver in enumerate(waivers):
|
|
if not isinstance(waiver, dict):
|
|
problems.append(issue(record_path, f"waiver {index} must be an object"))
|
|
continue
|
|
for key in ("reason", "owner", "path", "issue", "expiry"):
|
|
if not nonempty_string(waiver.get(key)):
|
|
problems.append(issue(record_path, f"waiver {index} is missing {key}"))
|
|
return problems
|
|
|
|
|
|
def check(args: argparse.Namespace) -> int:
|
|
record_path = args.root.resolve() / args.record
|
|
try:
|
|
document = json.loads(record_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
print(issue(record_path, f"cannot read JSON: {error}"), file=sys.stderr)
|
|
return 1
|
|
|
|
problems: list[str] = []
|
|
if document.get("schema_version") != 1:
|
|
problems.append(issue(record_path, "schema_version must be 1"))
|
|
targets = document.get("targets")
|
|
if not isinstance(targets, list) or not targets:
|
|
problems.append(issue(record_path, "targets must be a non-empty list"))
|
|
else:
|
|
selected_targets = targets
|
|
if args.target is not None:
|
|
selected_targets = [
|
|
target for target in targets
|
|
if isinstance(target, dict) and target.get("name") == args.target
|
|
]
|
|
if not selected_targets:
|
|
problems.append(issue(record_path, f"target not found: {args.target}"))
|
|
else:
|
|
selected_targets = targets
|
|
names: set[str] = set()
|
|
for target in selected_targets:
|
|
if isinstance(target, dict) and target.get("name") in names:
|
|
problems.append(issue(record_path, f"duplicate target name: {target.get('name')}"))
|
|
if isinstance(target, dict) and nonempty_string(target.get("name")):
|
|
names.add(target["name"])
|
|
problems.extend(check_target(args.root.resolve(), record_path, target))
|
|
|
|
if problems:
|
|
print("RTL readiness violations:", file=sys.stderr)
|
|
print("\n".join(problems), file=sys.stderr)
|
|
return 1
|
|
print(f"rtl-readiness-check: passed ({len(targets) if isinstance(targets, list) else 0} targets)")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
parser.add_argument("--record", default="rtl/rtl_readiness.json")
|
|
selection = parser.add_mutually_exclusive_group()
|
|
selection.add_argument("--target", help="check one named readiness target")
|
|
selection.add_argument(
|
|
"--all",
|
|
action="store_true",
|
|
help="check all readiness targets (the default)",
|
|
)
|
|
args = parser.parse_args()
|
|
return check(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|