Skip to content

feat(verilog): index Verilog/SystemVerilog with module-instantiation edges - #402

Closed
FHYQ-Dong wants to merge 3 commits into
colbymchenry:mainfrom
FHYQ-Dong:feat/verilog-systemverilog-support
Closed

FHYQ-Dong wants to merge 3 commits into
colbymchenry:mainfrom
FHYQ-Dong:feat/verilog-systemverilog-support

Conversation

@FHYQ-Dong

Copy link
Copy Markdown

What

Adds Verilog / SystemVerilog (.v .vh .sv .svh) as a new indexed language, plus a small traversal change so the flagship tools can walk the HDL design hierarchy.

Extraction

  • Vendored tree-sitter-systemverilog@0.3.1 wasm (ABI 15, ships prebuilt — no Docker/emscripten build). One grammar/extractor covers both Verilog and SV.
  • New src/extraction/languages/verilog.ts (custom visitNode hook — SV nests names deep in *_ansi_header / *_body_declaration, so generic field-based name extraction can't reach them). Extracts:
    • modules / interfaces / packages → class / interface (containers)
    • functions / tasks → function; parameters / localparams → constant
    • typedefs → type_alias; package imports → import
    • module_instantiationinstantiates edge — the highest-value HDL relationship (the design hierarchy is the HDL equivalent of a call path)
  • Ports and internal signals are intentionally not extracted (they'd explode the node count without aiding structural queries).

Traversal (cross-cutting)

codegraph_trace / codegraph_callers / codegraph_callees now follow instantiates edges (previously calls/references/imports only). Without this the instantiation edges existed but no flagship tool walked them. instantiates is a precise tree-sitter edge, so it doesn't widen traces the way fuzzy matches would; getImpactRadius already traversed all edge kinds. In OO languages this also surfaces new X() construction dependencies.

Wiring

  • src/types.tsLANGUAGES
  • src/extraction/grammars.ts — wasm map, EXTENSION_MAP, vendored-path branch, display name
  • src/extraction/languages/verilog.ts (new) + languages/index.ts (EXTRACTORS)
  • src/extraction/wasm/tree-sitter-systemverilog.wasm (vendored; shipped by copy-assets; plain git, ~0.77 MB gzipped — consistent with existing wasms, no LFS)
  • src/graph/traversal.ts, src/mcp/tools.tsinstantiates traversal

Testing

  • 5 new Verilog cases in __tests__/extraction.test.ts (detection + module/function/task/typedef/import + the instantiates reference) — green.

  • graph / resolution / context suites: 61/61 green — no regression from the traversal change.

  • Deterministic extraction probes on 3 real repos:

    repo files verilog modules interfaces typedefs inst edges trace
    picorv32 (.v) 79 61 101 picorv32_axi → picorv32_pcpi_mul = 3 hops
    ibex (.sv) 1872 610 24 279 559 ibex_top → ibex_decoder = 4 hops
    cva6 (.sv) 799 658 22 541 625 cva6 → load_store_unit = 3 hops

Benchmark — ⚠️ provisional, NOT the canonical methodology

Note from the author: I don't currently have a way to use Claude, so I could only benchmark with China's DeepSeek (deepseek-v4-pro) through its Anthropic-compatible endpoint, not Claude Opus 4.7. Treat the numbers below as directional only — someone with Claude access should re-run them under the standard methodology before they go anywhere public.

The agent A/B below was run on DeepSeek deepseek-v4-pro, n=1 per arm — it is not the project's standard benchmark methodology (Claude Opus 4.7 × median of 4 runs/arm), so these numbers do not qualify for the README benchmark table or docs/benchmarks/codegraph-ab-matrix.md. Anyone publishing those must re-run with the standard methodology. Reproduce with: npm run build && ./scripts/local-install.sh then scripts/add-lang/bench.sh verilog <name> <url> "<question>" headless.

Directional results (with codegraph → without), all with-arms correct with 0 Read / 0 Grep:

repo tool calls Read+Grep duration
picorv32 2 → 12 0 → 7 16s → 64s
ibex 1 → 12 0 → 7 15s → 57s
cva6 1 → 13 0 → 8 15s → 76s

Full data + caveats: docs/design/dynamic-dispatch-coverage-playbook.md §6.

Docs

README (languages bullet + table row), CHANGELOG ([Unreleased]), corpus.json (picorv32/ibex/cva6), coverage-playbook §6 row, and the implementation plan at docs/plans/2026-05-25-verilog-support.md.

Notes

  • The 20 MB grammar wasm is lazy-loaded only when .v/.sv files are present.

…edges

Add Verilog/SystemVerilog (.v .vh .sv .svh) as a new indexed language, using
the vendored tree-sitter-systemverilog grammar (ABI 15, ships a prebuilt wasm --
no build needed). One grammar/extractor covers both V and SV.

Extracts modules/interfaces/packages (-> class/interface), functions/tasks,
parameters (-> constant), typedefs, and package imports. The high-value edge is
module instantiation: module_instantiation -> instantiates, giving the HDL
design hierarchy (top module -> leaf module, the HDL equivalent of a call path).
Because SystemVerilog nests declared names deep inside *_ansi_header /
*_body_declaration wrappers, all structural extraction is done in a custom
visitNode hook; only package imports use the generic importTypes + extractImport
path. Ports and internal signals are intentionally not extracted (they would
explode the node count without aiding structural queries).

Also make codegraph_trace / callers / callees follow instantiates edges
(previously calls/references/imports only) so the module hierarchy is queryable
by the flagship tools -- without this the edges existed but no tool walked them.
instantiates is a precise tree-sitter edge, so it does not widen traces the way
fuzzy matches would; getImpactRadius already traversed all edge kinds. In OO
languages this also surfaces new X() construction dependencies.

Wiring: src/types.ts (LANGUAGES), src/extraction/grammars.ts (wasm map,
EXTENSION_MAP, vendored-path branch, display name), new
src/extraction/languages/verilog.ts, languages/index.ts (EXTRACTORS), vendored
src/extraction/wasm/tree-sitter-systemverilog.wasm.

Tests: 5 new Verilog cases in extraction.test.ts (detection + module/function/
instantiation/typedef/import/call); graph/resolution/context suites 61/61 green
(no regression from the traversal change).

Docs: README (languages bullet + table row), CHANGELOG (Unreleased), corpus.json
(picorv32/ibex/cva6), coverage-playbook section 6 row, and the implementation
plan at docs/plans/2026-05-25-verilog-support.md.
Copilot AI review requested due to automatic review settings May 25, 2026 15:26
@FHYQ-Dong

Copy link
Copy Markdown
Author

related issue: #262

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds first-class Verilog/SystemVerilog support to CodeGraph, including extraction of module hierarchy edges and updating traversal/tools so that HDL “flow” can be traced through instantiations.

Changes:

  • Added verilog as a supported language with file extension mapping and a vendored tree-sitter grammar.
  • Implemented a custom SystemVerilog extractor that emits containers, functions/tasks, params/typedefs, imports, call edges, and instantiates edges.
  • Updated trace/callers/callees traversals to include instantiates edges; added tests + docs/CHANGELOG updates.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/types.ts Registers verilog in the language list.
src/mcp/tools.ts Makes codegraph_trace consider instantiates edges.
src/graph/traversal.ts Includes instantiates edges in recursive traversal for callers/callees.
src/extraction/languages/verilog.ts New Verilog/SystemVerilog extractor (custom visitNode).
src/extraction/languages/index.ts Wires verilogExtractor into the extractor registry.
src/extraction/grammars.ts Adds verilog wasm + extensions + display name + vendored wasm-path selection.
tests/extraction.test.ts Adds language detection + extractor behavior tests for verilog.
README.md Documents Verilog/SystemVerilog as supported.
CHANGELOG.md Notes new language support + traversal behavior change.
docs/plans/2026-05-25-verilog-support.md Historical plan/outcome writeup for the feature.
docs/design/dynamic-dispatch-coverage-playbook.md Adds Verilog/SV entry and validation notes.
.claude/skills/agent-eval/corpus.json Adds Verilog evaluation corpus entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/extraction/languages/verilog.ts Outdated
structTypes: [],
enumTypes: [],
typeAliasTypes: [],
importTypes: ['package_import_declaration'],
Comment on lines +238 to +247
extractImport: (node, source) => {
// package_import_declaration → package_import_item → simple_identifier (pkg)
const item = firstChildOfType(node, ['package_import_item']) ?? node;
const id = firstSimpleIdentifier(item);
if (!id) return null;
return {
moduleName: getNodeText(id, source),
signature: getNodeText(node, source).trim(),
};
},
Comment on lines +128 to +142
function handleInstantiation(node: SyntaxNode, ctx: ExtractorContext): boolean {
const typeNode = getChildByField(node, 'instance_type') ?? firstSimpleIdentifier(node);
if (typeNode && ctx.nodeStack.length > 0) {
const fromId = ctx.nodeStack[ctx.nodeStack.length - 1];
const moduleName = getNodeText(typeNode, ctx.source).trim();
if (fromId && moduleName) {
ctx.addUnresolvedReference({
fromNodeId: fromId,
referenceName: moduleName,
referenceKind: 'instantiates',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
}
}
Comment on lines +3967 to +3968
it('should extract package typedefs, functions, imports and call edges', () => {
const code = `
Comment on lines +3976 to +3982
module worker (input logic clk);
import math_pkg::*;
function automatic int caller(int y);
return add(y);
endfunction
endmodule
`;
@FHYQ-Dong
FHYQ-Dong force-pushed the feat/verilog-systemverilog-support branch from c89f4fb to a8821f8 Compare May 25, 2026 15:41
…ied instance types

Address PR review feedback:

- importTypes now targets package_import_item rather than the whole
  package_import_declaration, so "import a::*, b::*;" indexes every package
  instead of silently dropping all but the first -- one import node per item.
- handleInstantiation normalizes qualified instance types to the trailing
  segment (factored into a shared trailingSegment helper, also used by
  handleCall), so a "pkg::mod" instance still resolves to an unqualified "mod"
  declaration -- consistent with how call references are matched.
- Add a multi-package-import extraction test: both packages are indexed and
  call edges still resolve when several imports share one statement.
@FHYQ-Dong
FHYQ-Dong requested a review from Copilot May 25, 2026 15:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Comment thread src/mcp/tools.ts Outdated
Comment on lines 1044 to 1045
const edgeKinds: Edge['kind'][] = ['calls', 'instantiates'];
const MAX_HOPS = 7;
Comment on lines +140 to +159
function handleInstantiation(node: SyntaxNode, ctx: ExtractorContext): boolean {
const typeNode = getChildByField(node, 'instance_type') ?? firstSimpleIdentifier(node);
if (typeNode && ctx.nodeStack.length > 0) {
const fromId = ctx.nodeStack[ctx.nodeStack.length - 1];
// Normalize qualified types (`pkg::mod`) to the trailing segment, matching
// handleCall, so a `pkg::mod` instance still resolves to a `mod` declaration.
const moduleName = trailingSegment(getNodeText(typeNode, ctx.source).trim());
if (fromId && moduleName) {
ctx.addUnresolvedReference({
fromNodeId: fromId,
referenceName: moduleName,
referenceKind: 'instantiates',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
}
}
// Port-connection expressions rarely hold user calls worth tracking; skip them.
return true;
}
…trace hop guard, instantiation calls

Address GitHub Copilot review on the Verilog PR:

- Multi-package imports: importTypes targets package_import_item rather than the
  whole package_import_declaration, so "import a::*, b::*;" indexes every package
  instead of dropping all but the first -- one import node per item.
- Qualified-name consistency: factor a shared trailingSegment helper; both call
  references and module-instantiation types are normalized to the trailing
  segment, so a "pkg::mod" instance resolves to an unqualified "mod" declaration.
- Trace hop guard, per edge kind: cap the wander-prone CALL hops (dense call
  graphs produce spurious long shortest paths) but do not penalize module-
  instantiation depth (tree-like SoC hierarchies are legitimate), with a generous
  total-length sanity bound. Deep HDL hierarchies (>7 levels) no longer falsely
  report "no direct path".
- Instantiation port/param calls: handleInstantiation now walks its children, so
  function calls inside parameter overrides (".W(f(x))") and port connections
  (".a(helper(sig))") emit calls references instead of being silently dropped.

Tests: +3 Verilog cases (multi-package import; instantiation port/param calls;
plus the earlier instantiation case). graph/resolution/context suites stay
61/61 green.
@FHYQ-Dong
FHYQ-Dong requested a review from Copilot May 25, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Comment thread src/mcp/tools.ts
Comment on lines +1047 to +1051
const edgeKinds: Edge['kind'][] = ['calls', 'instantiates'];
const MAX_CALL_HOPS = 7;
const MAX_TOTAL_HOPS = 20;
const callHops = (p: Array<{ node: Node; edge: Edge | null }>) =>
p.filter((h) => h.edge?.kind === 'calls').length;
Comment thread src/mcp/tools.ts
Comment on lines +1060 to 1061
if (callHops(p) <= MAX_CALL_HOPS && p.length - 1 <= MAX_TOTAL_HOPS) { path = p; break; }
if (!overCap || p.length < overCap.length) overCap = p;
Comment thread src/graph/traversal.ts
Comment on lines +251 to +253
// 'instantiates' surfaces the module-instantiation hierarchy (HDLs) and
// `new X()` dependencies — "what instantiates this".
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls', 'references', 'imports', 'instantiates']);
Comment on lines +248 to +249
case 'tf_call':
return handleCall(node, ctx);
@danusha2345

Copy link
Copy Markdown
Contributor

Rebased this onto current main (b9ca4b7) and ran it on two real Gowin/Tang Nano designs, since it had gone CONFLICTING and I wanted to know whether it still holds up. It does.

Rebase: https://github.com/danusha2345/codegraph/tree/eval/402-verilog — two commits. The first is your three squashed, under your authorship: extractor, vendored grammar, design note and tests unchanged; the wiring re-applied to today's grammars.ts (VENDORED_WASM_LANGS, the extension map, the display name), languages/index.ts and the Language union. Your traversal.ts and codegraph_trace hunks are not carried — main's traverser already follows instantiates for callers/callees/impact, and codegraph_trace was removed. The second commit is mine and is what your trace hunk was for, moved to where the flow lives now: codegraph_explore's flow finder rides instantiates when the target is a Verilog node, and lets a Verilog module (indexed as class) sit on the chain. Gated to Verilog on both counts, so a TS/Java new X() stays a dependency, not a step. Test added.

On real designs (wasm arm; there is no kernel walker for this grammar):

design .v modules instantiations vs source
eMMC card reader (Tang Nano 9K) 33 32 34 every edge matches an instantiation in the source; top → pll / emmc_controller / uart_bridge / led_status, controller → cmd / dat / init / sector_buf, each testbench → its DUT
dual-UART multiplexer 11 11 11 same: top → 2× uart_rx, 2× fifo, uart_arbiter, uart_tx; testbenches → DUTs

423 / 179 calls edges are task and function calls inside testbenches, all same-file, all correct. Parameters come out as constants (177 / 79).

codegraph_explore, with the second commit:

top uart_rx                →  top → uart_bridge → uart_rx            (instantiates, instantiates)
emmc_controller emmc_crc16 →  emmc_controller → emmc_dat → emmc_crc16
tb_top uart_tx             →  tb_top → top → uart_tx

Without it, the same queries render blast radius and source but no Flow section, which for an HDL is the question being asked.

One thing to know, not a defect: the reader has pll.v and a simulation pll_stub.v both declaring module pll; top's instantiation resolves to pll.v (first by ranking), and the stub is a pll nobody instantiates. A -sim / _stub heuristic could pick the other way; I left it.

Suites on the rebase: extraction.test.ts 629 (your 7 included), explore/flow suites and kernel-grammar-parity green, 670 total across the six files I ran; tsc clean. Happy to open it as a fresh PR if you would rather not force-push here, or you can pull the branch — either way it is your code and your credit.

@danusha2345

Copy link
Copy Markdown
Contributor

Three more commits on the same branch, all yours to take or leave:

  • a87170a`include "x.vh" becomes an imports edge (the same file-path matcher a C #include uses) and `define a constant. On pulp-platform/axi (93 .sv/.svh) that is 158 include edges of 160 lines — the two misses name a file outside the repo.
  • 1b58cbf — when a synthesis-side file instantiates a module that exists both as src/pll.v and sim/pll_stub.v, the simulation twin is skipped as long as a non-simulation module remains; a testbench keeps every candidate. Gated to Verilog instantiates refs and to the conventional sim/ tb/ test/ dv/ paths and _stub/_tb/_sim names only.
  • 783582f — README row, the release-archive grammar gate in scripts/check-ui-build.mjs, the coverage docs.

Also ran it on a real SystemVerilog corpus, pulp-platform/axi: 177 modules and packages, 7 interfaces, 277 instantiations (185 → module, 92 → interface, twelve spot-checked against source, all right), 2 of 93 files with local parse errors (`ifdef inside a port list) that still index. No crash, no node explosion. 647 tests across the extraction / flow / resolution files, tsc clean.

danusha2345 added a commit to danusha2345/codegraph that referenced this pull request Sep 12, 2026
Порт extractor из colbymchenry#402 и eval/402-verilog; HDL flow ограничен Verilog. Проверены Dual UART, eMMC reader и pulp-platform/axi.

Co-authored-by: FHYQ-Dong <FHYQ-Dong@users.noreply.github.com>
@danusha2345

Copy link
Copy Markdown
Contributor

Follow-up: the rebased port of this extractor, plus instance/port-binding resolution, signal access queries, build profiles and slang-backed semantics, is now open against current main as #1875, with @FHYQ-Dong co-authored on the extractor commit.

@FHYQ-Dong

Copy link
Copy Markdown
Author

Hi @danusha2345 , thanks a lot for picking this up, and for the rebase and the real-design validation. I'm sorry, I no longer have the bandwidth to keep #402 moving, so I'm very happy for you to take it over in #1875. I'll close #402 in favor of this one.

I compared the extractors, and one thing from my local (unpushed) work isn't in #1875 yet: localparam dependency edges. Right now handleParam only creates the constant node. In my version, each identifier used in the value expression also got a references edge from the parameter:

module m;
  parameter  N = 8;
  localparam K = (N + 1) * 2;   // K --references--> N
  localparam M = K << 1;        // M --references--> K
endmodule

With these edges, impact on N follows the whole localparam chain (N → K → M). That's a common question in HDL ("what changes if I change DATA_W?"). The implementation was small: walk the param_assignment subtree, skip the first simple_identifier (that's the declared name itself), cap at ~32 identifiers per expression, and emit a references ref from the constant for each one. Feel free to adopt it or drop it, whatever fits your resolver design best.

Thanks again for carrying this forward!

@FHYQ-Dong FHYQ-Dong closed this Sep 18, 2026
danusha2345 added a commit to danusha2345/codegraph that referenced this pull request Sep 19, 2026
…puted from

`handleParam` only created the constant node, so `localparam K = (N + 1) * 2;`
had no edge to `N` and impact on a width parameter stopped at the parameter
itself. The value expression now goes through the same scoped signal walk the
processes use: identifiers become `hdl:signal:` references FROM the
parameter, callee names and package qualifiers are left out, and the resolver
binds them in the parameter's lexical scope only, so a same-named parameter
of another module is never the target.

Idea and shape by FHYQ-Dong (comment on colbymchenry#402). Measured on two real
designs, nodes unchanged: a Tang Nano 9k card reader +16 edges, pulp-platform
AXI +222 edges; every sampled edge stays inside its module.

Co-authored-by: FHYQ-Dong <FHYQ-Dong@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@danusha2345

Copy link
Copy Markdown
Contributor

Thank you — for the original extractor and for the comparison. Adopted: #1875 now has the localparam dependency edges (8f9d7dd, you are co-authored on the commit).

One difference from your sketch: instead of a separate identifier walk with a cap, the value expression goes through the same scoped signal walk the processes use. That already leaves out callee names ($clog2(N), width_of(K) contribute only their arguments) and package qualifiers (pkg::BASE is not evidence for a local BASE), and the resolver binds each identifier in the parameter's own lexical scope — so with a WIDTH in every module, b::Q = N * 3 binds to b::N, never a::N.

Measured on two real designs, node count unchanged: a Tang Nano 9k card reader +16 edges, pulp-platform AXI +222; sampled edges like axi_demux::SelectWidth -> axi_demux::NoMstPorts and uart_rx::DEFAULT_CPB -> uart_rx::CLK_FREQ all stay inside their module. Impact on N now reaches K and M.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants