diff --git a/graphify/extractors/verilog.py b/graphify/extractors/verilog.py index 2f5fea49b5..563f45fe72 100644 --- a/graphify/extractors/verilog.py +++ b/graphify/extractors/verilog.py @@ -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 @@ -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: diff --git a/tests/test_languages.py b/tests/test_languages.py index bfdc836f80..4508bbfdd4 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -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"] == []