Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions graphify/extractors/verilog.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,21 @@ def add_edge(src: str, tgt: str, relation: str, line: int,
file_nid = _make_id(str(path))
add_node(file_nid, path.name, 1)

# Pre-scan module names so an instantiation resolves to a module defined
# later in the same file — Verilog allows instantiating a module declared
# further down the source (or in any order across the compilation unit).
local_modules: set[str] = set()

def _scan_modules(node) -> None:
if node.type == "module_declaration":
mn = _sv_first_identifier(_sv_child(node, "module_header"), source)
if mn:
local_modules.add(mn)
for child in node.children:
_scan_modules(child)

_scan_modules(root)

def walk(node, module_nid: str | None = None) -> None:
t = node.type

Expand Down Expand Up @@ -309,8 +324,28 @@ def walk(node, module_nid: str | None = None) -> None:
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)
if inst_type in local_modules:
# The instantiated module is defined in this file: point
# at its definition id (`_make_id(stem, name)`) instead of
# minting a second, bare-id node for the same module. A
# bare `_make_id(name)` never matches the scoped definition
# id, so the module was split into a real definition node
# and a phantom instantiation-target duplicate.
tgt_nid = _make_id(stem, inst_type)
else:
# Defined in another file: emit a SOURCELESS stub so the
# corpus-level rewire collapses it onto the real
# definition. A sourced stub bakes this file's path into
# the id and blocks the rewire — the #1402 phantom
# duplicate the other extractors avoid the same way.
tgt_nid = _make_id(inst_type)
if tgt_nid not in seen_ids:
seen_ids.add(tgt_nid)
nodes.append({
"id": tgt_nid, "label": inst_type,
"file_type": "code", "source_file": "",
"source_location": "", "confidence_score": 1.0,
})
add_edge(module_nid, tgt_nid, "instantiates", line)

for child in node.children:
Expand Down
34 changes: 34 additions & 0 deletions tests/test_languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3600,6 +3600,40 @@ def test_systemverilog_preserves_existing_module_extraction():
assert "instantiates" in _relations(r)


def test_systemverilog_instantiation_resolves_to_local_module():
"""`top` instantiating same-file `leaf` links to leaf's definition, not a
bare-id phantom duplicate. The instantiation target used an unscoped id
(`_make_id(name)`) while the definition used `_make_id(stem, name)`, so
they never matched: the module was split into a real definition node and a
sourced phantom that also blocked the corpus-level rewire (#1402 shape)."""
r = extract_verilog(FIXTURES / "sample.sv")
leaf_nodes = [n for n in r["nodes"] if n["label"] == "leaf"]
assert len(leaf_nodes) == 1, f"leaf split into duplicates: {leaf_nodes}"
leaf_id = leaf_nodes[0]["id"]
# The surviving node is the real, sourced definition.
assert leaf_nodes[0].get("source_file")
inst_targets = {e["target"] for e in r["edges"] if e["relation"] == "instantiates"}
assert leaf_id in inst_targets, (
f"instantiation did not link to the leaf definition {leaf_id}: {inst_targets}"
)


def test_systemverilog_cross_file_instantiation_is_sourceless_stub(tmp_path):
"""A module defined in another file is a SOURCELESS stub so the corpus-level
rewire can collapse it onto the real definition, rather than a sourced node
whose id bakes in this file's path and blocks the rewire."""
f = tmp_path / "wrapper.sv"
f.write_text("module wrapper;\n external_ip u_ip();\nendmodule\n")
r = extract_verilog(f)
stubs = [n for n in r["nodes"] if n["label"] == "external_ip"]
assert len(stubs) == 1, stubs
assert stubs[0].get("source_file") == "", (
f"cross-file instantiation target must be sourceless: {stubs[0]}"
)
inst_targets = {e["target"] for e in r["edges"] if e["relation"] == "instantiates"}
assert stubs[0]["id"] in inst_targets


def test_systemverilog_missing_file_returns_empty():
r = extract_verilog(Path("nonexistent.sv"))
assert r["nodes"] == []
Expand Down
Loading