Skip to content
Open
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- CodeGraph's refusal to use a sensitive folder such as `~/.ssh`, `~/.config`, `/etc` or `C:\Windows` as a project can no longer be sidestepped: a link or junction that leads to one, a differently capitalized name on Windows and macOS, a `\\?\` spelling on Windows, and a path to a missing folder inside one are now refused like the folder itself. Before, any of these let an AI agent's query read the files inside. Thanks @mixxer for finding it.

### New Features

- **Verilog and SystemVerilog are indexed.** Modules, packages, interfaces and modports, named instances, ports and signals, always/assign blocks and generate scopes are searchable, and `codegraph_explore` surfaces the instantiation path from a top module down to a nested one; based on the extractor contributed in #402 by @FHYQ-Dong. Re-index projects that contain Verilog files.
- **HDL port bindings resolve to their formal ports.** Named, positional and wildcard (`.*`) connections link an instance's local signals to the instantiated module's ports, and bit or part selects keep their base signal.
- **Signal readers and writers on request.** `codegraph_explore` with `hdlAccess` (CLI: `codegraph explore --hdl-access`) lists where a signal is read, written, used as a control condition or as a clock/reset event, including the direction of arguments passed to known functions and tasks.
- **HDL build profiles.** An optional `hdl` section in `codegraph.json` selects source files or filelists, include directories and defines; conditional-compilation branches are indexed under the active profile, `codegraph status` reports the configured versus the indexed profile, and the file watcher follows filelists and included headers.
- **Computed parameters and port widths.** `codegraph hdl-semantic` runs an installed slang (or pyslang through `--python`) on the active profile and returns evaluated parameters, port widths and macro origins as a separate answer, without touching the indexed graph.
- **A derived parameter depends on the parameters it is computed from.** `localparam K = (N + 1) * 2;` links `K` to `N`, so impact on a width or depth parameter follows the whole chain of `localparam`s built on it, inside its own module. Suggested by @FHYQ-Dong.

### Fixes

- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ CodeGraph's parsing engine is a **native Rust kernel**: 20 languages — TypeScr
| **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi, Verilog/SystemVerilog |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks |
| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
Expand Down Expand Up @@ -835,6 +835,7 @@ is written):
| Astro | `.astro` | Full support (frontmatter + script extraction, template component/call references, `src/pages/` routes) |
| Liquid | `.liquid` | Full support |
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
| Verilog / SystemVerilog | `.v`, `.vh`, `.sv`, `.svh` | Modules, packages, interfaces/modports and named instances; ports and signals with named/positional/wildcard port bindings, bit/part selects and always/assign blocks; lexical and generate scopes; read/write/control/event signal-access queries; optional build profiles (filelists/includes/defines, see [docs/hdl-profiles.md](docs/hdl-profiles.md)). Source graph only — no elaboration or timing; `codegraph hdl-semantic` computes parameters and widths through an installed [slang](docs/hdl-semantics.md) |
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| R | `.R` `.r` | Full support (functions in every assignment form, S4/R5/R6 classes with methods, `library`/`require` imports, `source()` file references, call edges) |
| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) |
Expand Down
185 changes: 185 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14415,3 +14415,188 @@ describe('C++ COM interface declarations (#1519)', () => {
expect(Buffer.byteLength(expected)).toBe(Buffer.byteLength(source));
});
});

describe('Verilog / SystemVerilog Extraction', () => {
it('should report Verilog as supported', () => {
expect(isLanguageSupported('verilog')).toBe(true);
expect(getSupportedLanguages()).toContain('verilog');
});

it('should extract modules as containers with scoped functions and tasks', () => {
const code = `
module top (input logic clk, output logic [7:0] z);
function automatic int square(int n);
return n * n;
endfunction
task automatic do_reset();
z = '0;
endtask
endmodule
`;
const result = extractFromSource('top.sv', code);

const mod = result.nodes.find((n) => n.kind === 'class' && n.name === 'top');
expect(mod).toBeDefined();
expect(mod?.language).toBe('verilog');

const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'square');
expect(fn).toBeDefined();
expect(fn?.qualifiedName).toBe('top::square'); // scoped under the module

const task = result.nodes.find((n) => n.kind === 'function' && n.name === 'do_reset');
expect(task).toBeDefined();
});

it('should emit an instantiates reference from a module to its submodule type', () => {
const code = `
module alu #(parameter int WIDTH = 8) (input logic [WIDTH-1:0] a, output logic [WIDTH-1:0] y);
endmodule

module top (input logic [7:0] x, output logic [7:0] z);
alu #(.WIDTH(8)) u_alu (.a(x), .y(z));
endmodule
`;
const result = extractFromSource('soc.sv', code);

const top = result.nodes.find((n) => n.kind === 'class' && n.name === 'top');
expect(top).toBeDefined();

const inst = result.unresolvedReferences.find(
(r) => r.referenceKind === 'instantiates' && r.referenceName === 'alu'
);
expect(inst).toBeDefined();
expect(inst?.fromNodeId).toBe(top?.id); // the parent module is the source

// parameter is captured as a constant
const param = result.nodes.find((n) => n.kind === 'constant' && n.name === 'WIDTH');
expect(param).toBeDefined();
});

it('should extract package typedefs, functions, imports and call edges', () => {
const code = `
package math_pkg;
typedef enum logic [1:0] { IDLE, RUN, DONE } state_t;
function automatic int add(int a, int b);
return a + b;
endfunction
endpackage

module worker (input logic clk);
import math_pkg::*;
function automatic int caller(int y);
return add(y);
endfunction
endmodule
`;
const result = extractFromSource('pkg.sv', code);

const pkg = result.nodes.find((n) => n.kind === 'class' && n.name === 'math_pkg');
expect(pkg).toBeDefined();

const typedef = result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'state_t');
expect(typedef).toBeDefined();
expect(typedef?.qualifiedName).toBe('math_pkg::state_t');

const importNode = result.nodes.find((n) => n.kind === 'import' && n.name === 'math_pkg');
expect(importNode).toBeDefined();

const callRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'add'
);
expect(callRef).toBeDefined();
});

it('should extract every package in a multi-package import statement', () => {
const code = `
package a_pkg;
function automatic int fa(); return 1; endfunction
endpackage

package b_pkg;
function automatic int fb(); return 2; endfunction
endpackage

module worker (input logic clk);
import a_pkg::*, b_pkg::*;
function automatic int caller();
return fa() + fb();
endfunction
endmodule
`;
const result = extractFromSource('multi.sv', code);

const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
expect(imports).toContain('a_pkg');
expect(imports).toContain('b_pkg'); // the second import must not be dropped

// call edges still resolve when multiple imports share one statement
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
expect(calls).toContain('fa');
expect(calls).toContain('fb');
});

it('should capture function calls inside instantiation port/param expressions', () => {
const code = `
module sub #(parameter int W = 8) (input logic [W-1:0] a);
endmodule

module top (input logic [7:0] x);
function automatic int dbl(int v); return v * 2; endfunction
function automatic int wid(); return 8; endfunction
sub #(.W(wid())) u_sub (.a(dbl(x)));
endmodule
`;
const result = extractFromSource('inst_calls.sv', code);

const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
expect(calls).toContain('dbl'); // call inside a port connection
expect(calls).toContain('wid'); // call inside a parameter override

// the instantiation edge itself is still emitted
const inst = result.unresolvedReferences.find(
(r) => r.referenceKind === 'instantiates' && r.referenceName === 'sub'
);
expect(inst).toBeDefined();
});

it('should emit an import for each `include directive, named by the included path', () => {
const code = `
\`include "defs.vh"
\`include "axi/typedef.svh"
\`include <uvm_macros.svh>
module top (input logic clk);
endmodule
`;
const result = extractFromSource('top.sv', code);

const includes = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
expect(includes).toEqual(['defs.vh', 'axi/typedef.svh', 'uvm_macros.svh']);

// the path is what the import ref carries, so the file-path matcher can
// land it on the header by suffix
const refs = result.unresolvedReferences
.filter((r) => r.referenceKind === 'imports')
.map((r) => r.referenceName);
expect(refs).toEqual(['defs.vh', 'axi/typedef.svh', 'uvm_macros.svh']);
});

it('should extract `define macros and top-level parameters from a header', () => {
const code = `
\`ifndef DEFS_VH
\`define DEFS_VH
\`define DATA_W 16
\`define ADDR(x) (x + 1)
parameter CLK_HZ = 27_000_000;
localparam BAUD = 115200;
\`endif
`;
const result = extractFromSource('defs.vh', code);

const constants = result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name);
expect(constants).toEqual(['DEFS_VH', 'DATA_W', 'ADDR', 'CLK_HZ', 'BAUD']);
});
});
130 changes: 130 additions & 0 deletions __tests__/hdl-access-query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { CodeGraph } from '../src';
import { ToolHandler } from '../src/mcp/tools';
import { formatHdlAccess } from '../src/mcp/hdl-access';

let root: string, cg: CodeGraph, handler: ToolHandler;
const rtl = `module top(input clk, input enable, input d, output logic q);
always @(posedge clk) begin
if (enable) q <= q + d;
end
leaf u(.d(d));
endmodule
module leaf(input d); endmodule
module other(input d, output q); assign q=d; endmodule
module updates(output int q); initial q += 1; endmodule
`;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-access-query-'));
fs.writeFileSync(path.join(root, 'top.sv'), rtl);
// Integral seconds survive Date/fs.utimes round trips on all supported filesystems.
const fixtureTime = new Date('2024-01-01T00:00:00.000Z');
fs.utimesSync(path.join(root, 'top.sv'), fixtureTime, fixtureTime);
fs.writeFileSync(path.join(root, 'lib.sv'), 'package lib; task automatic fill(output int y); y=1; endtask endpackage');
fs.writeFileSync(path.join(root, 'call.sv'), 'module call_top; int y; initial lib::fill(y); endmodule');
fs.writeFileSync(path.join(root, 'plain.ts'), 'export function hello() { return 42; }\n');
cg = CodeGraph.initSync(root);
expect((await cg.indexAll()).success).toBe(true);
handler = new ToolHandler(cg);
});
afterAll(() => { cg?.close(); if (root) fs.rmSync(root, { recursive: true, force: true }); });
async function query(signal: string, hdlAccess?: string) {
const result = await handler.execute('codegraph_explore', { query: signal, ...(hdlAccess ? { hdlAccess } : {}) });
expect(result.isError).not.toBe(true);
return result.content.map(c => c.text).join('\n');
}

describe('HDL access MCP query', () => {
it('returns actual source sites and retains reads and writes on the same signal and line', async () => {
const readers = await query('top::q', 'read');
const writers = await query('top::q', 'write');
for (const result of [readers, writers]) {
expect(result).toContain('1 matching access site(s).');
expect(result).toContain('3\t if (enable) q <= q + d;');
expect(result).toContain('top.sv:3');
}
expect(readers).toContain('[read]');
expect(writers).toContain('[write]');
expect(await query('top.q', 'write')).toContain('**top::q**');
expect(await query('top::q', 'all')).toContain('2 matching access site(s).');
expect(await query('updates::q', 'readwrite')).toContain('[readwrite]');
});
it('explains writes proven by a known task output formal', async () => {
const result = await query('call_top::y', 'write');
expect(result).toContain('1 matching access site(s).');
expect(result).toContain('[write]');
expect(result).toContain('Formal direction: lib::fill(output y).');
expect(result).toContain('initial lib::fill(y);');
expect(result).toContain("known callable formal's argument direction");
});
it('includes control and event uses as readers and permits exact category filters', async () => {
expect(await query('top::clk', 'read')).toContain('[event]');
expect(await query('top::clk', 'event')).toContain('[event]');
expect(await query('top::enable', 'control')).toContain('[control]');
expect(await query('top::enable', 'read')).toContain('[control]');
expect(await query('top::d', 'write')).toContain('0 matching access site(s).');
});
it('keeps unclassified connections separate from access evidence and disambiguates exact targets', async () => {
expect(await query('top::d', 'all')).toContain('unclassified reference(s) excluded');
const all = await query('q', 'write');
expect(all).toContain('**top::q**');
expect(all).toContain('**other::q**');
expect(await query('top::q', 'write')).not.toContain('**other::q**');
expect(await query('where is q written', 'write')).toContain('No exact HDL signal matches');
});
it('does not change normal non-HDL exploration and validates the filter', async () => {
const normal = await query('hello');
expect(normal).toContain('hello');
expect(normal).toContain('return 42');
expect(normal).not.toContain('HDL access:');
const bad = await handler.execute('codegraph_explore', { query: 'q', hdlAccess: 'driver' });
expect(bad.isError).toBe(true);
expect(bad.content[0]!.text).toContain('hdlAccess must be one of');
});
it('rejects same-size edited source with restored mtime and labels the indexed definition', () => {
const file = path.join(root, 'top.sv');
const stat = fs.statSync(file);
try {
fs.writeFileSync(file, rtl.replace('q <= q + d', 'q <= q - d'));
fs.utimesSync(file, stat.atime, stat.mtime);
const changed = fs.statSync(file);
expect(changed.size).toBe(stat.size);
expect(Math.floor(changed.mtimeMs)).toBe(Math.floor(stat.mtimeMs));
const result = formatHdlAccess(cg, 'top::q', 'write');
expect(result).toContain('1 matching access site(s).');
expect(result).toContain('Definition location is from the index; current source is unverified');
expect(result).toContain('Source omitted: changed/unavailable since indexing');
expect(result).not.toContain('q <= q - d');
expect(result).not.toContain('q <= q + d');
} finally { fs.writeFileSync(file, rtl); fs.utimesSync(file, stat.atime, stat.mtime); }
});
it('does not attach stale indexed access positions to modified source', () => {
const file = path.join(root, 'top.sv');
const stat = fs.statSync(file);
try {
fs.writeFileSync(file, `// changed source\n${rtl}`);
const result = formatHdlAccess(cg, 'top::q', 'write');
expect(result).toContain('Source omitted: changed/unavailable since indexing');
expect(result).not.toContain('3\t if (enable)');
} finally { fs.writeFileSync(file, rtl); fs.utimesSync(file, stat.atime, stat.mtime); }
});
});

it('HDL access CLI forwards the existing explore filter and rejects invalid values', () => {
const bin = path.resolve(__dirname, '../dist/bin/codegraph.js');
const run = (filter: string) => spawnSync(process.execPath, [bin, 'explore', '--path', root, '--hdl-access', filter, 'top::q'], {
encoding: 'utf8', timeout: 30000,
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' },
});
const result = run('write');
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain('HDL access: top::q — write');
expect(result.stdout).toContain('q <= q + d;');
const invalid = run('driver');
expect(invalid.status).toBe(1);
expect(invalid.stdout + invalid.stderr).toContain('hdlAccess must be one of');
});
Loading