mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
refactor: refactor RTL style checker and update test cases
- Enhanced the RTL style checker script to include new diagnostic rules for function declarations, primary design units, and synthesis exclusions. - Added functions to handle audit issues, ensuring that the audit records match the current inventory of RTL files. - Updated test cases to reflect changes in RTL file names and paths, ensuring consistency with the new naming conventions. - Introduced additional tests for style violations, including mismatched primary design units and synthesizable initial blocks. - Adjusted file paths in various test files to align with the new RTL structure and naming conventions.
This commit is contained in:
116 files changed
+3310
-2624
No files matched your search
+364
-19
@@ -16,6 +16,15 @@ INSTANCE_RE = re.compile(
|
||||
r"^\s*(?P<module>[A-Za-z_][A-Za-z0-9_$]*)\s*(?:#\s*\([^;]*\)\s*)?"
|
||||
r"u_[A-Za-z_][A-Za-z0-9_$]*\s*\($"
|
||||
)
|
||||
PRIMARY_DECL_RE = re.compile(
|
||||
r"^\s*(?:module|interface|package)\s+(?:automatic\s+)?"
|
||||
r"(?P<name>[A-Za-z_][A-Za-z0-9_$]*)\b",
|
||||
re.MULTILINE,
|
||||
)
|
||||
FUNCTION_HEADER_RE = re.compile(
|
||||
r"\bfunction\s+(?P<header>.*?)(?P<terminator>;|\n)",
|
||||
re.DOTALL,
|
||||
)
|
||||
LEGACY_ALWAYS_RE = re.compile(r"\balways\s*@")
|
||||
DECL_RE = re.compile(r"\b(?:wire|tri|wand|wor)\b")
|
||||
FORBIDDEN_TOKEN_RE = {
|
||||
@@ -53,6 +62,39 @@ PARAM_RE = re.compile(
|
||||
)
|
||||
UPPER_CAMEL_RE = re.compile(r"^[A-Z][A-Za-z0-9]*$")
|
||||
LOWER_SNAKE_RE = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$")
|
||||
EXPLICIT_FUNCTION_TYPE_RE = re.compile(
|
||||
r"^automatic\s+(?:bit|logic|reg|byte|shortint|int|longint|integer|time|"
|
||||
r"[A-Za-z_][A-Za-z0-9_$]*_(?:e|t))(?:\s+(?:signed|unsigned))?"
|
||||
r"(?:\s*\[[^\]]+\])?\s+[A-Za-z_][A-Za-z0-9_$]*\s*\("
|
||||
)
|
||||
REQUIRED_AUDIT_RULES = {
|
||||
"RTL-FILE",
|
||||
"RTL-SV",
|
||||
"RTL-NAME",
|
||||
"RTL-STRUCT",
|
||||
"RTL-COMB",
|
||||
"RTL-SEQ",
|
||||
"RTL-WIDTH",
|
||||
"RTL-CDC",
|
||||
"RTL-VERIFY",
|
||||
"RTL-FMT",
|
||||
}
|
||||
|
||||
|
||||
def diagnostic(rule: str, path: Path, message: str, line: int | None = None) -> str:
|
||||
location = f"{path}:{line}" if line is not None else str(path)
|
||||
return f"[{rule}] {location}: {message}"
|
||||
|
||||
|
||||
def strip_comments(source: str) -> str:
|
||||
"""Remove comments while retaining line numbers for diagnostics."""
|
||||
source = re.sub(
|
||||
r"/\*.*?\*/",
|
||||
lambda match: "\n" * match.group(0).count("\n"),
|
||||
source,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return re.sub(r"//[^\n]*", "", source)
|
||||
|
||||
|
||||
def tracked_files(root: Path) -> list[Path]:
|
||||
@@ -140,46 +182,214 @@ def instance_issues(path: Path, source: str) -> list[str]:
|
||||
and not text.startswith("//")
|
||||
and not text.startswith("`")
|
||||
):
|
||||
issues.append(f"{path}:{cursor + 1}: positional port connection")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-STRUCT-002",
|
||||
path,
|
||||
"positional port connection",
|
||||
cursor + 1,
|
||||
)
|
||||
)
|
||||
break
|
||||
cursor += 1
|
||||
return issues
|
||||
|
||||
|
||||
def verification_only_lines(source: str) -> set[int]:
|
||||
"""Return lines excluded from synthesis by an explicit SYNTHESIS guard."""
|
||||
excluded_lines: set[int] = set()
|
||||
stack: list[tuple[str, bool]] = []
|
||||
for number, line in enumerate(source.splitlines(), start=1):
|
||||
directive = line.strip()
|
||||
parent_excluded = stack[-1][1] if stack else False
|
||||
if directive.startswith("`ifndef SYNTHESIS"):
|
||||
stack.append(("ifndef-synthesis", True))
|
||||
elif directive.startswith("`ifdef SYNTHESIS"):
|
||||
stack.append(("ifdef-synthesis", parent_excluded))
|
||||
elif directive.startswith(("`ifdef ", "`ifndef ", "`if ")):
|
||||
stack.append(("other", parent_excluded))
|
||||
elif directive.startswith("`else") and stack:
|
||||
kind, excluded = stack[-1]
|
||||
if kind == "ifndef-synthesis":
|
||||
stack[-1] = (kind, False)
|
||||
elif kind == "ifdef-synthesis":
|
||||
stack[-1] = (kind, True)
|
||||
else:
|
||||
stack[-1] = (kind, excluded)
|
||||
elif directive.startswith("`endif") and stack:
|
||||
stack.pop()
|
||||
if stack and stack[-1][1]:
|
||||
excluded_lines.add(number)
|
||||
return excluded_lines
|
||||
|
||||
|
||||
def declaration_issues(path: Path, source: str) -> list[str]:
|
||||
parsed_source = strip_comments(source)
|
||||
declarations = list(PRIMARY_DECL_RE.finditer(parsed_source))
|
||||
modules = [match for match in declarations if match.group(0).lstrip().startswith("module")]
|
||||
if modules:
|
||||
declarations = modules
|
||||
if not declarations:
|
||||
if "`include" in parsed_source and "managed" in source.lower():
|
||||
return []
|
||||
return [diagnostic("RTL-FILE-001", path, "missing primary design unit")]
|
||||
primary = declarations[0]
|
||||
if primary.group("name") != path.stem:
|
||||
line = parsed_source.count("\n", 0, primary.start()) + 1
|
||||
return [
|
||||
diagnostic(
|
||||
"RTL-FILE-002",
|
||||
path,
|
||||
f"primary design unit '{primary.group('name')}' must match the file name",
|
||||
line,
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def function_issues(path: Path, source: str) -> list[str]:
|
||||
issues: list[str] = []
|
||||
parsed_source = strip_comments(source)
|
||||
for match in FUNCTION_HEADER_RE.finditer(parsed_source):
|
||||
header = re.sub(r"\s+", " ", match.group("header").strip())
|
||||
line = parsed_source.count("\n", 0, match.start()) + 1
|
||||
if not header.startswith("automatic "):
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-SV-008",
|
||||
path,
|
||||
"synthesizable function must be automatic",
|
||||
line,
|
||||
)
|
||||
)
|
||||
elif not EXPLICIT_FUNCTION_TYPE_RE.match(header):
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-SV-009",
|
||||
path,
|
||||
"function requires an explicit return type and typed arguments",
|
||||
line,
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def check_file(path: Path, source: str, profile: str) -> list[str]:
|
||||
issues: list[str] = []
|
||||
if profile == "owned":
|
||||
issues.extend(instance_issues(path, source))
|
||||
if path.suffix == ".sv":
|
||||
issues.extend(declaration_issues(path, source))
|
||||
issues.extend(function_issues(path, source))
|
||||
verification_lines = verification_only_lines(source)
|
||||
format_disabled = False
|
||||
format_disabled_line: int | None = None
|
||||
for number, line in enumerate(source.splitlines(), start=1):
|
||||
code = line.split("//", 1)[0]
|
||||
if "verilog_format: off" in line:
|
||||
if format_disabled:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-FMT-005",
|
||||
path,
|
||||
"nested verilog_format: off region",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if "--" not in line:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-FMT-007",
|
||||
path,
|
||||
"verilog_format: off requires an inline alignment rationale",
|
||||
number,
|
||||
)
|
||||
)
|
||||
format_disabled = True
|
||||
format_disabled_line = number
|
||||
format_exception = format_disabled
|
||||
if LEGACY_ALWAYS_RE.search(code):
|
||||
issues.append(f"{path}:{number}: use always_ff/always_comb in owned RTL")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-SEQ-001",
|
||||
path,
|
||||
"use always_ff/always_comb in owned RTL",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if re.search(r"\bwire\b", code) and not re.search(r"\binout\s+wire\b", code):
|
||||
issues.append(f"{path}:{number}: explicit wire in owned RTL; justify net semantics")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-STRUCT-005",
|
||||
path,
|
||||
"explicit wire in owned RTL; justify net semantics",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if LONG_LOCAL_RE.search(code):
|
||||
issues.append(f"{path}:{number}: prefer the local short-name abbreviation")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-NAME-009",
|
||||
path,
|
||||
"prefer the local short-name abbreviation",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if re.search(r"\binitial\s+begin\b", code) and number not in verification_lines:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-SV-007",
|
||||
path,
|
||||
"initial block must be isolated from synthesis",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if not format_exception:
|
||||
if any(character == "\t" for character in line):
|
||||
issues.append(f"{path}:{number}: tabs are not permitted in owned RTL")
|
||||
issues.append(
|
||||
diagnostic("RTL-FMT-002", path, "tabs are not permitted", number)
|
||||
)
|
||||
if line.rstrip() != line:
|
||||
issues.append(f"{path}:{number}: trailing whitespace")
|
||||
issues.append(diagnostic("RTL-FMT-003", path, "trailing whitespace", number))
|
||||
if any(ord(character) > 127 for character in line):
|
||||
issues.append(f"{path}:{number}: non-ASCII character")
|
||||
issues.append(diagnostic("RTL-FMT-001", path, "non-ASCII character", number))
|
||||
if len(line) > 100:
|
||||
issues.append(f"{path}:{number}: line exceeds 100 characters")
|
||||
issues.append(
|
||||
diagnostic("RTL-FMT-004", path, "line exceeds 100 characters", number)
|
||||
)
|
||||
for description, pattern in FORBIDDEN_TOKEN_RE.items():
|
||||
if pattern.search(code):
|
||||
issues.append(f"{path}:{number}: {description} is not permitted")
|
||||
issues.append(
|
||||
diagnostic("RTL-SV-001", path, f"{description} is not permitted", number)
|
||||
)
|
||||
if "verilog_format: on" in line:
|
||||
format_disabled = False
|
||||
if not format_disabled:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-FMT-005",
|
||||
path,
|
||||
"verilog_format: on has no matching off",
|
||||
number,
|
||||
)
|
||||
)
|
||||
else:
|
||||
format_disabled = False
|
||||
format_disabled_line = None
|
||||
if format_disabled:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-FMT-005",
|
||||
path,
|
||||
"verilog_format: off has no matching on",
|
||||
format_disabled_line,
|
||||
)
|
||||
)
|
||||
if source and not source.endswith("\n"):
|
||||
issues.append(f"{path}: missing final newline")
|
||||
issues.append(diagnostic("RTL-FMT-006", path, "missing final newline"))
|
||||
if UNNAMED_GENERATE_RE.search(source):
|
||||
issues.append(f"{path}: generated if/for block must have a label")
|
||||
issues.append(
|
||||
diagnostic("RTL-STRUCT-006", path, "generated if/for block must have a label")
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
@@ -196,7 +406,14 @@ def naming_issues(path: Path, source: str, manifest: dict[str, object]) -> list[
|
||||
code = line.split("//", 1)[0]
|
||||
module_match = MODULE_RE.match(code)
|
||||
if module_match and not LOWER_SNAKE_RE.fullmatch(module_match.group("name")):
|
||||
issues.append(f"{path}:{number}: module name must use lower_snake_case")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-NAME-001",
|
||||
path,
|
||||
"module name must use lower_snake_case",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if re.match(r"^\s*interface\b", code):
|
||||
in_interface = True
|
||||
if re.match(r"^\s*function\b", code):
|
||||
@@ -205,12 +422,26 @@ def naming_issues(path: Path, source: str, manifest: dict[str, object]) -> list[
|
||||
if port_match and not in_interface and not in_function:
|
||||
name = port_match.group("name")
|
||||
if not name.endswith(("_i", "_o", "_io")):
|
||||
issues.append(f"{path}:{number}: port '{name}' needs _i, _o, or _io suffix")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-NAME-002",
|
||||
path,
|
||||
f"port '{name}' needs _i, _o, or _io suffix",
|
||||
number,
|
||||
)
|
||||
)
|
||||
interface_match = INTERFACE_INSTANCE_RE.match(code)
|
||||
if interface_match and not interface_match.group("name").startswith("u_") and (
|
||||
interface_match.group("name") not in {"ribp", "ribl", "rib"}
|
||||
):
|
||||
issues.append(f"{path}:{number}: interface instance must start with u_")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-NAME-004",
|
||||
path,
|
||||
"interface instance must start with u_",
|
||||
number,
|
||||
)
|
||||
)
|
||||
macro_match = MACRO_RE.match(code)
|
||||
if macro_match:
|
||||
name = macro_match.group("name")
|
||||
@@ -220,9 +451,23 @@ def naming_issues(path: Path, source: str, manifest: dict[str, object]) -> list[
|
||||
if not name.startswith("RETROSOC_") and not any(
|
||||
name.startswith(str(prefix)) for prefix in exceptions
|
||||
) and not name.endswith(("_SV", "_SVH")):
|
||||
issues.append(f"{path}:{number}: macro '{name}' needs a reviewed namespace")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-NAME-008",
|
||||
path,
|
||||
f"macro '{name}' needs a reviewed namespace",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if re.search(r"\brst_ni\b", code):
|
||||
issues.append(f"{path}:{number}: use project reset spelling rst_n_i")
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-NAME-003",
|
||||
path,
|
||||
"use project reset spelling rst_n_i",
|
||||
number,
|
||||
)
|
||||
)
|
||||
if re.match(r"^\s*endfunction\b", code):
|
||||
in_function = False
|
||||
if re.match(r"^\s*endinterface\b", code):
|
||||
@@ -230,7 +475,9 @@ def naming_issues(path: Path, source: str, manifest: dict[str, object]) -> list[
|
||||
for match in ENUM_TYPE_RE.finditer(source):
|
||||
if not match.group("name").endswith("_e"):
|
||||
line = source.count("\n", 0, match.start()) + 1
|
||||
issues.append(f"{path}:{line}: enum typedef must end in _e")
|
||||
issues.append(
|
||||
diagnostic("RTL-NAME-006", path, "enum typedef must end in _e", line)
|
||||
)
|
||||
for number, line in enumerate(lines, start=1):
|
||||
code = line.split("//", 1)[0]
|
||||
if re.search(r"\bparameter\b", code) and not re.search(r"\blocalparam\b", code):
|
||||
@@ -239,7 +486,12 @@ def naming_issues(path: Path, source: str, manifest: dict[str, object]) -> list[
|
||||
if match.group("name") in {"DATA_WIDTH"}:
|
||||
continue
|
||||
issues.append(
|
||||
f"{path}:{number}: parameter '{match.group('name')}' must use UpperCamelCase"
|
||||
diagnostic(
|
||||
"RTL-NAME-007",
|
||||
path,
|
||||
f"parameter '{match.group('name')}' must use UpperCamelCase",
|
||||
number,
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
@@ -265,12 +517,102 @@ def verible_issues(tool: str, paths: list[Path], root: Path) -> list[str]:
|
||||
return [line for line in result.stdout.splitlines() if line]
|
||||
|
||||
|
||||
def audit_issues(root: Path, audit_path: Path, paths: list[Path]) -> list[str]:
|
||||
"""Validate that the committed audit covers the current owned source set."""
|
||||
try:
|
||||
audit = json.loads(audit_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
return [diagnostic("RTL-AUDIT-001", audit_path, f"invalid audit record: {error}")]
|
||||
if not isinstance(audit, dict):
|
||||
return [diagnostic("RTL-AUDIT-001", audit_path, "audit record must be an object")]
|
||||
|
||||
issues: list[str] = []
|
||||
if audit.get("schema_version") != 1:
|
||||
issues.append(diagnostic("RTL-AUDIT-001", audit_path, "unsupported schema_version"))
|
||||
if audit.get("profile") != "owned":
|
||||
issues.append(diagnostic("RTL-AUDIT-001", audit_path, "audit must cover the owned profile"))
|
||||
if audit.get("policy") != "docs/rtl-coding-style.md":
|
||||
issues.append(diagnostic("RTL-AUDIT-001", audit_path, "audit must identify the RTL policy"))
|
||||
boundary_record = audit.get("reviewed_boundary_record")
|
||||
required_boundary_fields = {"owner", "related_commit", "expiry", "removal_plan"}
|
||||
if not isinstance(boundary_record, dict) or not all(
|
||||
isinstance(boundary_record.get(field), str) and boundary_record[field]
|
||||
for field in required_boundary_fields
|
||||
):
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-AUDIT-001",
|
||||
audit_path,
|
||||
"reviewed boundaries require owner, related_commit, expiry, and removal_plan",
|
||||
)
|
||||
)
|
||||
|
||||
inventory = audit.get("inventory")
|
||||
audited_paths: set[Path] = set()
|
||||
if not isinstance(inventory, dict):
|
||||
issues.append(diagnostic("RTL-AUDIT-001", audit_path, "missing inventory object"))
|
||||
else:
|
||||
for directory, names in inventory.items():
|
||||
if not isinstance(directory, str) or not isinstance(names, list):
|
||||
issues.append(
|
||||
diagnostic("RTL-AUDIT-001", audit_path, "inventory entries must map paths to lists")
|
||||
)
|
||||
continue
|
||||
if not all(isinstance(name, str) for name in names):
|
||||
issues.append(
|
||||
diagnostic("RTL-AUDIT-001", audit_path, "inventory file names must be strings")
|
||||
)
|
||||
continue
|
||||
audited_paths.update(Path(directory) / name for name in names)
|
||||
|
||||
expected_paths = set(paths)
|
||||
missing = sorted(expected_paths - audited_paths)
|
||||
extra = sorted(audited_paths - expected_paths)
|
||||
if missing:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-AUDIT-002",
|
||||
audit_path,
|
||||
f"inventory is missing owned source '{missing[0]}'",
|
||||
)
|
||||
)
|
||||
if extra:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-AUDIT-003",
|
||||
audit_path,
|
||||
f"inventory names non-owned source '{extra[0]}'",
|
||||
)
|
||||
)
|
||||
|
||||
rules = audit.get("rules")
|
||||
if not isinstance(rules, list):
|
||||
issues.append(diagnostic("RTL-AUDIT-001", audit_path, "missing rules list"))
|
||||
else:
|
||||
rule_ids = {
|
||||
rule.get("id")
|
||||
for rule in rules
|
||||
if isinstance(rule, dict) and isinstance(rule.get("id"), str)
|
||||
}
|
||||
missing_rules = sorted(REQUIRED_AUDIT_RULES - rule_ids)
|
||||
if missing_rules:
|
||||
issues.append(
|
||||
diagnostic(
|
||||
"RTL-AUDIT-004",
|
||||
audit_path,
|
||||
f"missing rule matrix entry '{missing_rules[0]}'",
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--manifest", type=Path)
|
||||
parser.add_argument("--profile", default="owned", choices=("owned", "verification", "technology"))
|
||||
parser.add_argument("--baseline", type=Path)
|
||||
parser.add_argument("--audit", type=Path)
|
||||
parser.add_argument("--verible-verilog-lint", default="verible-verilog-lint")
|
||||
parser.add_argument(
|
||||
"--changed-only",
|
||||
@@ -306,6 +648,9 @@ def main() -> int:
|
||||
issues = [issue for group in issues for issue in (group if isinstance(group, list) else [group])]
|
||||
if args.profile == "owned":
|
||||
issues.extend(verible_issues(args.verible_verilog_lint, profile_files, root))
|
||||
if args.audit:
|
||||
audit_path = args.audit if args.audit.is_absolute() else root / args.audit
|
||||
issues.extend(audit_issues(root, audit_path, profile_files))
|
||||
baseline: set[str] = set()
|
||||
if args.baseline and args.baseline.is_file():
|
||||
baseline = set(json.loads(args.baseline.read_text(encoding="utf-8")))
|
||||
|
||||
Reference in new issue
Block a user