python
#!/usr/bin/env python3
"""Count output-port bits in a Verilog or SystemVerilog source file."""
from __future__ import annotations
import argparse
import ast
import operator
import re
import sys
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class OutputPort:
module: str
name: str
width: int
range_text: str
_BINARY_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.FloorDiv: operator.floordiv,
ast.Div: operator.floordiv,
ast.Mod: operator.mod,
ast.LShift: operator.lshift,
ast.RShift: operator.rshift,
ast.BitOr: operator.or_,
ast.BitAnd: operator.and_,
ast.BitXor: operator.xor,
}
_UNARY_OPERATORS = {
ast.UAdd: operator.pos,
ast.USub: operator.neg,
ast.Invert: operator.invert,
}
def strip_comments(source: str) -> str:
"""Remove Verilog comments while preserving strings and line boundaries."""
result: list[str] = []
index = 0
in_string = False
while index < len(source):
char = source[index]
next_char = source[index + 1] if index + 1 < len(source) else ""
if in_string:
result.append(char)
if char == "\\" and next_char:
result.append(next_char)
index += 2
continue
if char == '"':
in_string = False
index += 1
continue
if char == '"':
in_string = True
result.append(char)
index += 1
elif char == "/" and next_char == "/":
while index < len(source) and source[index] != "\n":
index += 1
elif char == "/" and next_char == "*":
index += 2
while index + 1 < len(source) and source[index : index + 2] != "*/":
result.append("\n" if source[index] == "\n" else " ")
index += 1
index += 2
else:
result.append(char)
index += 1
return "".join(result)
def normalize_verilog_number(expression: str) -> str:
"""Convert common Verilog integer literals into Python integer syntax."""
pattern = re.compile(r"(?i)(?:\d+)?'s?([bodh])([0-9a-f_xz?]+)")
def replace(match: re.Match[str]) -> str:
base_name = match.group(1).lower()
digits = match.group(2).replace("_", "")
if re.search(r"[xz?]", digits, re.I):
raise ValueError(f"unknown digit in literal {match.group(0)!r}")
base = {"b": 2, "o": 8, "d": 10, "h": 16}[base_name]
return str(int(digits, base))
return pattern.sub(replace, expression)
def evaluate_integer(expression: str, parameters: dict[str, int]) -> int:
"""Evaluate a restricted integer expression used by a packed range."""
normalized = normalize_verilog_number(expression)
tree = ast.parse(normalized, mode="eval")
def evaluate(node: ast.AST) -> int:
if isinstance(node, ast.Expression):
return evaluate(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, int):
return node.value
if isinstance(node, ast.Name) and node.id in parameters:
return parameters[node.id]
if isinstance(node, ast.BinOp) and type(node.op) in _BINARY_OPERATORS:
return _BINARY_OPERATORS[type(node.op)](evaluate(node.left), evaluate(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPERATORS:
return _UNARY_OPERATORS[type(node.op)](evaluate(node.operand))
raise ValueError(f"unsupported expression {expression!r}")
return int(evaluate(tree))
def parse_overrides(values: list[str]) -> dict[str, int]:
parameters: dict[str, int] = {}
for value in values:
if "=" not in value:
raise ValueError(f"parameter override must be NAME=VALUE: {value!r}")
name, expression = value.split("=", 1)
name = name.strip()
if not re.fullmatch(r"[A-Za-z_]\w*", name):
raise ValueError(f"invalid parameter name: {name!r}")
parameters[name] = evaluate_integer(expression.strip(), parameters)
return parameters
def parse_parameters(module_text: str, overrides: dict[str, int]) -> dict[str, int]:
parameters = dict(overrides)
declaration_pattern = re.compile(
r"\b(?:parameter|localparam)\b(?P<body>.*?);",
re.I | re.S,
)
assignment_pattern = re.compile(r"(?:\b\w+\b\s+)*\b([A-Za-z_]\w*)\s*=\s*([^,]+)")
for declaration in declaration_pattern.finditer(module_text):
for assignment in assignment_pattern.finditer(declaration.group("body")):
name, expression = assignment.groups()
if name in overrides:
continue
try:
parameters[name] = evaluate_integer(expression.strip(), parameters)
except (SyntaxError, ValueError, ZeroDivisionError):
continue
return parameters
def split_names(text: str) -> list[str]:
names: list[str] = []
for item in text.split(","):
item = re.sub(r"\s*=.*$", "", item).strip()
item = re.sub(r"\[[^\]]+\]\s*$", "", item).strip()
match = re.search(r"(?:\\\S+|[A-Za-z_$][\w$]*)\s*$", item)
if match:
names.append(match.group(0).strip())
return names
def range_width(range_text: str, parameters: dict[str, int]) -> int:
if not range_text:
return 1
bounds = range_text[1:-1].split(":", 1)
if len(bounds) != 2:
raise ValueError(f"unsupported output range {range_text!r}")
left = evaluate_integer(bounds[0].strip(), parameters)
right = evaluate_integer(bounds[1].strip(), parameters)
return abs(left - right) + 1
def extract_modules(source: str) -> list[tuple[str, str]]:
modules: list[tuple[str, str]] = []
module_pattern = re.compile(
r"\bmodule\s+(?:automatic\s+)?([A-Za-z_$][\w$]*)\b(.*?)\bendmodule\b",
re.I | re.S,
)
for match in module_pattern.finditer(source):
modules.append((match.group(1), match.group(2)))
return modules
def parse_outputs(source: str, overrides: dict[str, int]) -> tuple[list[OutputPort], list[str]]:
clean_source = strip_comments(source)
outputs: list[OutputPort] = []
errors: list[str] = []
output_pattern = re.compile(
r"\boutput\b\s*"
r"(?:wire|reg|logic|tri|tri0|tri1|wand|wor|uwire|signed|unsigned|var|supply0|supply1|\s)*"
r"(?P<range>\[[^\]]+\])?\s*"
r"(?P<names>[^;()]+?)\s*(?=;|,?\s*\b(?:input|output|inout)\b|\))",
re.I | re.S,
)
for module_name, module_text in extract_modules(clean_source):
parameters = parse_parameters(module_text, overrides)
seen: set[str] = set()
for declaration in output_pattern.finditer(module_text):
range_text = (declaration.group("range") or "").strip()
try:
width = range_width(range_text, parameters)
except (SyntaxError, ValueError, ZeroDivisionError) as error:
names = ", ".join(split_names(declaration.group("names"))) or "<unknown>"
errors.append(f"{module_name}: {names}: {error}")
continue
for name in split_names(declaration.group("names")):
if name in seen:
continue
seen.add(name)
outputs.append(OutputPort(module_name, name, width, range_text))
return outputs, errors
def get_options() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Count output-port bits in one Verilog/SystemVerilog file"
)
parser.add_argument("verilog_file", type=Path, help="input .v or .sv file")
parser.add_argument(
"-D",
"--define",
action="append",
default=[],
metavar="NAME=VALUE",
help="override a parameter used in an output range; may be repeated",
)
parser.add_argument(
"--total-only",
action="store_true",
help="print only the total output bit count",
)
return parser.parse_args()
def main() -> int:
options = get_options()
if not options.verilog_file.is_file():
print(f"Error: file not found: {options.verilog_file}", file=sys.stderr)
return 2
try:
overrides = parse_overrides(options.define)
source = options.verilog_file.read_text(encoding="utf-8", errors="replace")
outputs, errors = parse_outputs(source, overrides)
except ValueError as error:
print(f"Error: {error}", file=sys.stderr)
return 2
if not outputs and not errors:
print("Error: no output declarations found", file=sys.stderr)
return 1
total = sum(output.width for output in outputs)
if options.total_only:
print(total)
else:
current_module = ""
module_total = 0
for index, output in enumerate(outputs):
if output.module != current_module:
if current_module:
print(f" Module total: {module_total} bits\n")
current_module = output.module
module_total = 0
print(f"Module: {current_module}")
print(f" {'Output':<40} {'Range':<20} Bits")
print(f" {'-' * 40} {'-' * 20} ----")
print(f" {output.name:<40} {(output.range_text or '[0:0]'):<20} {output.width}")
module_total += output.width
if index == len(outputs) - 1:
print(f" Module total: {module_total} bits")
print(f"\nFile total: {total} output bits")
for error in errors:
print(f"Warning: unresolved output width: {error}", file=sys.stderr)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())
使用方法python count_verilog_outputs.py path/a.v 即可打印出path 目录下a.v的output的输出bit数