330 lines
14 KiB
Python
330 lines
14 KiB
Python
"""Verilog extractor. Moved verbatim from graphify/extract.py."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from pathlib import Path
|
|
from graphify.extractors.base import _file_stem, _make_id, _read_text
|
|
|
|
|
|
def _sv_first_identifier(node, source: bytes) -> str | None:
|
|
"""First `simple_identifier` under node in pre-order, or None.
|
|
|
|
tree-sitter-verilog 1.0.3 nests declaration names a few levels deep instead
|
|
of exposing a `name` field. Scope the search to the right child node (e.g.
|
|
`function_identifier`) or this returns the return-type instead of the name.
|
|
"""
|
|
if node is None:
|
|
return None
|
|
for child in node.children:
|
|
if child.type == "simple_identifier":
|
|
return _read_text(child, source)
|
|
found = _sv_first_identifier(child, source)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
def _sv_child(node, type_name: str) -> object | None:
|
|
if node is None:
|
|
return None
|
|
for child in node.children:
|
|
if child.type == type_name:
|
|
return child
|
|
return None
|
|
|
|
_SV_BUILTIN_TYPES = frozenset({
|
|
"bit", "logic", "reg", "wire", "int", "integer", "shortint", "longint",
|
|
"byte", "time", "real", "shortreal", "void", "string", "type", "event",
|
|
"mailbox", "semaphore", "process", "chandle",
|
|
})
|
|
|
|
_SV_NON_TYPE_WORDS = frozenset({
|
|
"return", "if", "else", "for", "foreach", "while", "case", "begin", "end",
|
|
"function", "task", "class", "endclass", "endfunction", "endtask",
|
|
})
|
|
|
|
_SV_PARENS_INNER = r"(?:[^()]|\([^()]*\))*"
|
|
|
|
_SV_PARENS = r"\(" + _SV_PARENS_INNER + r"\)"
|
|
|
|
_SV_FUNC_RE = re.compile(
|
|
r"\bfunction\s+([A-Za-z_]\w*(?:\s*#\s*" + _SV_PARENS + r")?)\s+(\w+)\s*"
|
|
r"\((" + _SV_PARENS_INNER + r")\)\s*;",
|
|
re.MULTILINE,
|
|
)
|
|
|
|
_SV_PARAM_RE = re.compile(
|
|
r"\s*(?:input|output|inout|ref|const\s+ref)?\s*"
|
|
r"([A-Za-z_]\w*(?:\s*#\s*" + _SV_PARENS + r")?)\s+\w+"
|
|
)
|
|
|
|
def _sv_strip_comments(text: str) -> str:
|
|
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
|
return re.sub(r"//.*", "", text)
|
|
|
|
def _sv_split_type_list(text: str) -> list[str]:
|
|
parts: list[str] = []
|
|
depth = 0
|
|
start = 0
|
|
for idx, ch in enumerate(text):
|
|
if ch == "(":
|
|
depth += 1
|
|
elif ch == ")":
|
|
depth = max(0, depth - 1)
|
|
elif ch == "," and depth == 0:
|
|
item = text[start:idx].strip()
|
|
if item:
|
|
parts.append(item)
|
|
start = idx + 1
|
|
item = text[start:].strip()
|
|
if item:
|
|
parts.append(item)
|
|
return parts
|
|
|
|
def _sv_collect_type_refs(type_text: str, generic: bool = False,
|
|
skip: frozenset[str] = frozenset()) -> list[tuple[str, str]]:
|
|
refs: list[tuple[str, str]] = []
|
|
text = type_text.strip()
|
|
if not text:
|
|
return refs
|
|
head = re.match(r"([A-Za-z_]\w*)", text)
|
|
if head:
|
|
name = head.group(1)
|
|
# `skip` carries the enclosing class's `#(type T = ...)` parameters so
|
|
# they are not mistaken for referenced types.
|
|
if name not in _SV_BUILTIN_TYPES and name not in _SV_NON_TYPE_WORDS and name not in skip:
|
|
refs.append((name, "generic_arg" if generic else "type"))
|
|
params = re.search(r"#\s*\((" + _SV_PARENS_INNER + r")\)", text)
|
|
if params:
|
|
for arg in _sv_split_type_list(params.group(1)):
|
|
refs.extend(_sv_collect_type_refs(arg, generic=True, skip=skip))
|
|
return refs
|
|
|
|
def _augment_systemverilog_semantics(
|
|
raw: str,
|
|
stem: str,
|
|
str_path: str,
|
|
file_nid: str,
|
|
nodes: list[dict],
|
|
edges: list[dict],
|
|
seen_ids: set[str],
|
|
) -> None:
|
|
label_to_nid = {node["label"]: node["id"] for node in nodes}
|
|
|
|
def line_for(offset: int) -> int:
|
|
return raw.count("\n", 0, offset) + 1
|
|
|
|
def add_node(nid: str, label: str, line: int) -> None:
|
|
if nid not in seen_ids:
|
|
seen_ids.add(nid)
|
|
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
"source_file": str_path, "source_location": f"L{line}",
|
|
"confidence_score": 1.0})
|
|
label_to_nid[label] = nid
|
|
|
|
def ensure_type(label: str, line: int) -> str:
|
|
if label in label_to_nid:
|
|
return label_to_nid[label]
|
|
nid = _make_id(stem, label)
|
|
add_node(nid, label, line)
|
|
return nid
|
|
|
|
def add_edge(src: str, target_label: str, relation: str, line: int, context: str | None = None) -> None:
|
|
tgt = ensure_type(target_label, line)
|
|
edge = {"source": src, "target": tgt, "relation": relation,
|
|
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
"source_file": str_path, "source_location": f"L{line}", "weight": 1.0}
|
|
if context:
|
|
edge["context"] = context
|
|
edges.append(edge)
|
|
|
|
text = _sv_strip_comments(raw)
|
|
# Consuming `endclass` (rather than a lookahead) makes each match own its
|
|
# terminator, so back-to-back or malformed classes cannot bleed bodies.
|
|
class_re = re.compile(
|
|
r"\b(?:(interface)\s+)?class\s+(\w+)([^;{]*)\s*;(.*?)\bendclass\b",
|
|
re.DOTALL,
|
|
)
|
|
for match in class_re.finditer(text):
|
|
class_name = match.group(2)
|
|
header = match.group(3) or ""
|
|
body = match.group(4) or ""
|
|
line = line_for(match.start())
|
|
# `#(type T = Payload)` declares `T` as a class type parameter, not a
|
|
# referenced type — collect these to skip below.
|
|
type_params = frozenset(re.findall(r"\btype\s+(\w+)", header))
|
|
class_nid = _make_id(stem, class_name)
|
|
add_node(class_nid, class_name, line)
|
|
edges.append({"source": file_nid, "target": class_nid, "relation": "defines",
|
|
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
"source_file": str_path, "source_location": f"L{line}", "weight": 1.0})
|
|
|
|
ext = re.search(r"\bextends\s+(\w+)", header)
|
|
if ext:
|
|
add_edge(class_nid, ext.group(1), "inherits", line)
|
|
impl = re.search(r"\bimplements\s+([^;{]+)", header)
|
|
if impl:
|
|
for iface_name in _sv_split_type_list(impl.group(1)):
|
|
add_edge(class_nid, iface_name.split("#", 1)[0].strip(), "implements", line)
|
|
|
|
body_without_functions = re.sub(
|
|
r"\bfunction\b.*?\bendfunction\b",
|
|
lambda m: "\n" * m.group(0).count("\n"),
|
|
body,
|
|
flags=re.DOTALL,
|
|
)
|
|
# Optional leading class-property qualifiers (rand/local/protected/etc.)
|
|
# must be consumed: otherwise a qualified field like `rand Config x;`
|
|
# (three tokens) fails the `<type> <name>;` shape and its type reference
|
|
# is silently dropped.
|
|
for field in re.finditer(r"^\s*(?:(?:rand|randc|local|protected|static|const|automatic|var)\s+)*([A-Za-z_]\w*(?:\s*#\s*\([^;]+?\))?)\s+\w+\s*;", body_without_functions, re.MULTILINE):
|
|
# Count to the start of the type token (group 1), not the match
|
|
# start: `^\s*` consumes the leading newline(s), so field.start()
|
|
# would resolve to the class's line instead of the field's.
|
|
field_line = line + body_without_functions.count("\n", 0, field.start(1))
|
|
for ref_name, role in _sv_collect_type_refs(field.group(1), skip=type_params):
|
|
add_edge(class_nid, ref_name, "references", field_line, "generic_arg" if role == "generic_arg" else "field")
|
|
|
|
for fm in _SV_FUNC_RE.finditer(body):
|
|
return_type, func_name, params = fm.group(1), fm.group(2), fm.group(3)
|
|
func_line = line + body.count("\n", 0, fm.start())
|
|
func_nid = _make_id(class_nid, func_name)
|
|
add_node(func_nid, func_name, func_line)
|
|
edges.append({"source": class_nid, "target": func_nid, "relation": "method",
|
|
"confidence": "EXTRACTED", "confidence_score": 1.0,
|
|
"source_file": str_path, "source_location": f"L{func_line}", "weight": 1.0})
|
|
for ref_name, role in _sv_collect_type_refs(return_type, skip=type_params):
|
|
add_edge(func_nid, ref_name, "references", func_line, "generic_arg" if role == "generic_arg" else "return_type")
|
|
for param in _sv_split_type_list(params):
|
|
pm = _SV_PARAM_RE.match(param)
|
|
if not pm:
|
|
continue
|
|
for ref_name, role in _sv_collect_type_refs(pm.group(1), skip=type_params):
|
|
add_edge(func_nid, ref_name, "references", func_line, "generic_arg" if role == "generic_arg" else "parameter_type")
|
|
|
|
def extract_verilog(path: Path) -> dict:
|
|
"""Extract modules, functions, tasks, package imports, instantiations, and
|
|
SystemVerilog class semantics (inherits/implements edges, field/parameter/
|
|
return-type references) from .v/.sv files."""
|
|
try:
|
|
import tree_sitter_verilog as tsverilog
|
|
from tree_sitter import Language, Parser
|
|
except ImportError:
|
|
return {"nodes": [], "edges": [], "error": "tree_sitter_verilog not installed"}
|
|
|
|
try:
|
|
language = Language(tsverilog.language())
|
|
parser = Parser(language)
|
|
source = path.read_bytes()
|
|
tree = parser.parse(source)
|
|
root = tree.root_node
|
|
except Exception as e:
|
|
return {"nodes": [], "edges": [], "error": str(e)}
|
|
|
|
stem = _file_stem(path)
|
|
str_path = str(path)
|
|
nodes: list[dict] = []
|
|
edges: list[dict] = []
|
|
seen_ids: set[str] = set()
|
|
|
|
def add_node(nid: str, label: str, line: int) -> None:
|
|
if nid not in seen_ids:
|
|
seen_ids.add(nid)
|
|
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
"source_file": str_path, "source_location": f"L{line}",
|
|
"confidence_score": 1.0})
|
|
|
|
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
confidence: str = "EXTRACTED", score: float = 1.0) -> None:
|
|
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
"confidence": confidence, "confidence_score": score,
|
|
"source_file": str_path, "source_location": f"L{line}", "weight": 1.0})
|
|
|
|
file_nid = _make_id(str(path))
|
|
add_node(file_nid, path.name, 1)
|
|
|
|
def walk(node, module_nid: str | None = None) -> None:
|
|
t = node.type
|
|
|
|
# SystemVerilog class bodies are handled by _augment_systemverilog_semantics
|
|
# (regex over source text). Skip their subtrees so in-class methods are not
|
|
# double-emitted here — and with the wrong, return-type-derived name.
|
|
if t in ("class_declaration", "interface_class_declaration"):
|
|
return
|
|
|
|
if t == "module_declaration":
|
|
mod_name = _sv_first_identifier(_sv_child(node, "module_header"), source)
|
|
if mod_name:
|
|
line = node.start_point[0] + 1
|
|
nid = _make_id(stem, mod_name)
|
|
add_node(nid, mod_name, line)
|
|
add_edge(file_nid, nid, "defines", line)
|
|
for child in node.children:
|
|
walk(child, nid)
|
|
return
|
|
|
|
# `function_prototype` only appears inside class/interface-class bodies
|
|
# (skipped above) and nests its name differently; it is intentionally not
|
|
# handled here.
|
|
elif t == "function_declaration":
|
|
fn_body = _sv_child(node, "function_body_declaration")
|
|
func_name = _sv_first_identifier(_sv_child(fn_body, "function_identifier"), source)
|
|
if func_name:
|
|
line = node.start_point[0] + 1
|
|
parent = module_nid or file_nid
|
|
nid = _make_id(parent, func_name)
|
|
add_node(nid, f"{func_name}()", line)
|
|
add_edge(parent, nid, "contains", line)
|
|
|
|
elif t == "task_declaration":
|
|
tk_body = _sv_child(node, "task_body_declaration")
|
|
task_name = _sv_first_identifier(_sv_child(tk_body, "task_identifier"), source)
|
|
if task_name:
|
|
line = node.start_point[0] + 1
|
|
parent = module_nid or file_nid
|
|
nid = _make_id(parent, task_name)
|
|
add_node(nid, task_name, line)
|
|
add_edge(parent, nid, "contains", line)
|
|
|
|
elif t == "package_import_declaration":
|
|
for child in node.children:
|
|
if child.type == "package_import_item":
|
|
pkg_text = _read_text(child, source)
|
|
pkg_name = pkg_text.split("::")[0].strip()
|
|
if pkg_name:
|
|
line = node.start_point[0] + 1
|
|
tgt_nid = _make_id(pkg_name)
|
|
add_node(tgt_nid, pkg_name, line)
|
|
src_nid = module_nid or file_nid
|
|
add_edge(src_nid, tgt_nid, "imports_from", line)
|
|
|
|
elif t in ("module_instantiation", "checker_instantiation"):
|
|
# `leaf u_leaf();` parses as checker_instantiation in 1.0.3;
|
|
# module_instantiation (when it occurs) exposes a `module_type` field.
|
|
# Both reduce to the first identifier under the node — the instantiated
|
|
# type, not the instance name (which appears later).
|
|
if module_nid:
|
|
type_node = node.child_by_field_name("module_type")
|
|
inst_type = (_read_text(type_node, source).strip() if type_node
|
|
else _sv_first_identifier(node, source))
|
|
if inst_type:
|
|
line = node.start_point[0] + 1
|
|
tgt_nid = _make_id(inst_type)
|
|
add_node(tgt_nid, inst_type, line)
|
|
add_edge(module_nid, tgt_nid, "instantiates", line)
|
|
|
|
for child in node.children:
|
|
walk(child, module_nid)
|
|
|
|
walk(root)
|
|
_augment_systemverilog_semantics(
|
|
source.decode("utf-8", errors="replace"),
|
|
stem,
|
|
str_path,
|
|
file_nid,
|
|
nodes,
|
|
edges,
|
|
seen_ids,
|
|
)
|
|
return {"nodes": nodes, "edges": edges}
|