Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,9 @@ is written):
| Solidity | `.sol` | Full support (contracts, libraries, interfaces, structs, enums, modifiers, events, errors, state variables, `import`/`using` directives, `emit`/`revert` calls) |
| Terraform / OpenTofu | `.tf`, `.tfvars`, `.tofu` | Full support (resources, data sources, modules, variables, outputs, providers incl. aliases, `locals`; `var.`/`local.`/`module.`/resource references with Terraform's per-directory scoping enforced; module calls bridged across the boundary — inputs to the child module's variables, `module.M.out` to the child's output, `source` to the module's files; cloudposse/atmos `remote-state` cross-component wiring when the component is statically named; `provider = aws.east` selections resolved up the module tree; `moved`/`import`/`removed`/`check` block references; `.tfvars` assignments linked to the variables they set) |
| Nix | `.nix` | Full support (functions with simple/destructured/curried params, `let`/attrset bindings, `inherit`, `import ./path` file edges — `./dir` resolving through `default.nix` — plus NixOS module `imports = [ ./x.nix ]` lists and `callPackage ./pkg.nix` file edges; call edges; module-system option wiring — a config write like `launchd.user.agents.x = { ... }` links to the module declaring `options.launchd.user.agents`, so option flows trace across modules) |
| SystemVerilog / Verilog | `.sv`, `.svh`, `.v` | Full support (modules, interfaces, packages, classes, programs, checkers, UDP declarations as class nodes; functions and tasks; `typedef`, `struct`/`union`, enum names; net/data/parameter/port declarations as fields; module instantiation edges — `u_foo counter(…)` resolves `counter` as a caller; package `import` and `` `include `` as import edges; system task calls `$display`/`$assert` and method calls; grammar: [tree-sitter/tree-sitter-verilog](https://github.com/tree-sitter/tree-sitter-verilog), MIT) |
| Tcl | `.tcl` | Full support (procedures and namespaces as functions/classes; `set` variable assignments; `source file.tcl` as import edges; all other commands as call edges; grammar: [tree-sitter-grammars/tree-sitter-tcl](https://github.com/tree-sitter-grammars/tree-sitter-tcl), MIT) |
| VHDL | `.vhd`, `.vhdl` | Full support (entities, architectures, packages, components as class nodes; functions and procedures; records, enumerations, subtypes; signal/variable/constant declarations; `use` clause imports; function and procedure call edges; grammar: [alemuller/tree-sitter-vhdl](https://github.com/alemuller/tree-sitter-vhdl), MIT — covers VHDL-93 through VHDL-2008) |

## Measured cross-file coverage

Expand Down
28 changes: 27 additions & 1 deletion src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
terraform: 'tree-sitter-terraform.wasm',
arkts: 'tree-sitter-arkts.wasm',
nix: 'tree-sitter-nix.wasm',
verilog: 'tree-sitter-verilog.wasm',
tcl: 'tree-sitter-tcl.wasm',
vhdl: 'tree-sitter-vhdl.wasm',
};

/**
Expand Down Expand Up @@ -141,6 +144,15 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.cu': 'cpp',
'.cuh': 'cpp',
'.nix': 'nix',
// HDL: SystemVerilog and Verilog
'.sv': 'verilog',
'.svh': 'verilog',
'.v': 'verilog',
// Tcl
'.tcl': 'tcl',
// VHDL
'.vhd': 'vhdl',
'.vhdl': 'vhdl',
Comment thread
3brahimi marked this conversation as resolved.
// XML: file-level tracking; the MyBatis extractor matches `<mapper namespace="...">`
// shape and emits SQL-statement nodes (other XML returns empty).
'.xml': 'xml',
Expand Down Expand Up @@ -271,6 +283,17 @@ export async function initGrammars(): Promise<void> {
* nix-community/tree-sitter-nix @ 3d0173d (MIT) with tree-sitter-cli 0.25.10
* (`generate` + `build --wasm`, ABI 15 — upstream's checked-in parser.c is
* still ABI 13; all 54 upstream corpus tests pass on the regenerated parser).
* HDL languages: tree-sitter-wasms doesn't ship SystemVerilog, Tcl, or VHDL;
* we vendor WASM files built from their upstream grammars:
* - tree-sitter/tree-sitter-verilog (MIT) — covers IEEE 1800 SystemVerilog
* and IEEE 1364 Verilog; built from the repo's checked-in parser.c with
* tree-sitter-cli `build --wasm`, ABI 14.
* - tree-sitter-grammars/tree-sitter-tcl (MIT) — Tcl/Tk 8.x command
* grammar; built from the repo's checked-in parser.c with
* tree-sitter-cli `build --wasm`, ABI 14.
* - alemuller/tree-sitter-vhdl (MIT) — VHDL-93 through VHDL-2008; built
* from the repo's checked-in parser.c with tree-sitter-cli `build --wasm`,
* ABI 14.
*
* TypeScript/TSX/JavaScript (+jsx, which shares the javascript grammar): the
* tree-sitter-wasms builds are 2023-era (^0.20.x); we vendor wasm built from
Expand All @@ -290,7 +313,7 @@ export async function initGrammars(): Promise<void> {
*/
const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery',
'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix',
'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix', 'verilog', 'tcl', 'vhdl',
'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go',
// R7a (C/C++ kernel port prep): tree-sitter-c v0.24.2 (b780e47) +
// tree-sitter-cpp v0.23.4 (f41e1a0), parser.c/scanner.c sha-matched against
Expand Down Expand Up @@ -655,6 +678,9 @@ export function getLanguageDisplayName(language: Language): string {
erlang: 'Erlang',
terraform: 'Terraform',
arkts: 'ArkTS',
verilog: 'SystemVerilog / Verilog',
tcl: 'Tcl',
vhdl: 'VHDL',
unknown: 'Unknown',
};
return names[language] || language;
Expand Down
6 changes: 6 additions & 0 deletions src/extraction/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import { solidityExtractor } from './solidity';
import { terraformExtractor } from './terraform';
import { arktsExtractor } from './arkts';
import { nixExtractor } from './nix';
import { verilogExtractor } from './verilog';
import { tclExtractor } from './tcl';
import { vhdlExtractor } from './vhdl';

export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
typescript: typescriptExtractor,
Expand Down Expand Up @@ -69,4 +72,7 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
terraform: terraformExtractor,
arkts: arktsExtractor,
nix: nixExtractor,
verilog: verilogExtractor,
tcl: tclExtractor,
vhdl: vhdlExtractor,
};
115 changes: 115 additions & 0 deletions src/extraction/languages/tcl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { Node as SyntaxNode } from 'web-tree-sitter';
import type { LanguageExtractor } from '../tree-sitter-types';

// Grammar: tree-sitter-tcl (vendored at src/extraction/wasm/tree-sitter-tcl.wasm,
// built from nicowillis/tree-sitter-tcl, MIT).
Comment thread
3brahimi marked this conversation as resolved.
//
// Node shapes:
// procedure: (procedure (simple_word<name>) (arguments ...) (braced_word<body>))
// namespace: (namespace (word_list (simple_word<"eval">) (simple_word<nsname>) ...))
// set: (set (id<varname>) ...)

export const tclExtractor: LanguageExtractor = {
classTypes: ['namespace'],
functionTypes: ['procedure'],
methodTypes: ['procedure'],
interfaceTypes: [],
structTypes: [],
enumTypes: [],
typeAliasTypes: [],
// command is ONLY in callTypes. The engine's else-if chain (importTypes → callTypes)
// means a type in importTypes never creates call edges — `source` detection is done
// inside extractImport, leaving call edges for every other command node.
importTypes: [],
callTypes: ['command'],
// 'set' is the grammar's variable assignment node (not 'variable_definition').
variableTypes: ['set'],
nameField: '', // resolveName handles all three shapes
bodyField: 'body', // procedure has field('body', ...)
paramsField: 'arguments', // procedure has field('arguments', ...)

resolveName(node: SyntaxNode, source: string): string | undefined {
if (node.type === 'procedure') {
const child = node.namedChild(0);
if (child && child.type === 'simple_word')
return source.substring(child.startIndex, child.endIndex);
}
if (node.type === 'namespace') {
const wl = node.namedChild(0);
if (wl && wl.type === 'word_list') {
const name = wl.namedChild(1);
if (name && name.type === 'simple_word')
return source.substring(name.startIndex, name.endIndex);
}
}
if (node.type === 'set') {
const child = node.namedChild(0);
if (child && child.type === 'id')
return source.substring(child.startIndex, child.endIndex);
}
return undefined;
},

getSignature(node: SyntaxNode, source: string): string | undefined {
const text = source.substring(node.startIndex, node.endIndex);
const firstLine = (text.split('\n')[0] ?? '').trim();
return firstLine.length > 120 ? firstLine.substring(0, 120) + '…' : firstLine;
},

visitNode(node: SyntaxNode, ctx: import('../tree-sitter-types').ExtractorContext): boolean {
if (node.type === 'command') {
const nameChild = node.namedChild(0);
if (!nameChild) return false;
const cmd = ctx.source.substring(nameChild.startIndex, nameChild.endIndex);
if (cmd !== 'source') return false;

const wl = node.namedChild(1);
const fileArg = wl?.namedChild(0);
if (!fileArg) return true;

const filename = ctx.source
.substring(fileArg.startIndex, fileArg.endIndex)
.replace(/^["'{]|['"}\]]+$/g, '');

ctx.createNode('import', filename, node, { signature: `source ${filename}` });

const parentId = ctx.nodeStack[ctx.nodeStack.length - 1];
if (parentId && filename) {
ctx.addUnresolvedReference({
fromNodeId: parentId,
referenceName: filename,
referenceKind: 'imports',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
}
return true;
}

if (node.type === 'set') {
const id = node.namedChild(0);
if (id && id.type === 'id') {
const name = ctx.source.substring(id.startIndex, id.endIndex);
ctx.createNode('variable', name, node, { signature: `set ${name}` });
}
// Preserve call extraction from `set` values (default variable extraction would skip children).
for (let i = 1; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) ctx.visitNode(child);
}
return true;
}

return false;
},
// `source filename` — word list is the second child.
const wl = node.namedChild(1);
if (!wl) return null;
const fileArg = wl.namedChild(0);
if (!fileArg) return null;
const filename = source
.substring(fileArg.startIndex, fileArg.endIndex)
.replace(/^["'{]|['"}\]]+$/g, '');
return { moduleName: filename, signature: `source ${filename}` };
},
};
186 changes: 186 additions & 0 deletions src/extraction/languages/verilog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';

// Grammar: tree-sitter-verilog (vendored at src/extraction/wasm/tree-sitter-verilog.wasm,
// built from nicowillis/tree-sitter-verilog, MIT). Covers SystemVerilog (IEEE 1800)
// and Verilog (IEEE 1364). The grammar node names follow the LRM naming convention
Comment thread
3brahimi marked this conversation as resolved.
// (module_declaration, interface_declaration, class_declaration, etc.).

// Hoisted: rebuilt on every resolveName call otherwise (38 alias sites in this grammar).
// module_or_interface_identifier is an alias of _simple_identifier (leaf node) — its
// text IS the module name.
const _nameNodeTypes = new Set([
'simple_identifier',
'escaped_identifier',
'module_or_interface_identifier',
]);

// Only descend through known wrapper nodes — never into body/attribute/statement nodes.
const _wrappers = new Set([
'package_identifier',
'class_identifier',
'interface_identifier',
'function_identifier',
'task_identifier',
'program_identifier',
'checker_identifier',
'enum_identifier',
'genvar_identifier',
'interface_ansi_header',
'interface_nonansi_header',
'program_ansi_header',
'program_nonansi_header',
'udp_ansi_declaration',
'udp_nonansi_declaration',
'list_of_genvar_identifiers',
// module_declaration → module_header → simple_identifier
'module_header',
// port name extraction: ansi_port_declaration → port_identifier → simple_identifier
'port_identifier',
// method call name extraction: method_call → method_call_body → method_identifier → simple_identifier
'method_call_body',
'method_identifier',
]);

export const verilogExtractor: LanguageExtractor = {
// module_declaration: name is module_declaration → module_header → simple_identifier.
// module_header is in _wrappers so resolveName descends through it.
classTypes: [
'module_declaration',
'package_declaration',
'interface_declaration',
'class_declaration',
'udp_declaration',
'program_declaration',
'checker_declaration',
],
// class_constructor_declaration/prototype represent `function new(...)`.
functionTypes: [
'function_body_declaration',
'task_body_declaration',
'class_constructor_declaration',
'class_constructor_prototype',
],
methodTypes: [
'function_body_declaration',
'task_body_declaration',
'class_constructor_declaration',
'class_constructor_prototype',
],
interfaceTypes: ['interface_declaration'],
structTypes: ['struct_union'],
Comment thread
3brahimi marked this conversation as resolved.
enumTypes: ['enum_name_declaration'],
typeAliasTypes: ['type_declaration'],
// include_compiler_directive covers `include "foo.sv" and `include <foo.sv>
importTypes: ['package_import_declaration', 'include_compiler_directive'],
// module_instantiation covers HDL module instantiation sites (creates caller edges).
// subroutine_call/function_subroutine_call wrap tf_call — keeping all three creates triple
// edges; keep tf_call (the leaf) and method_call (OOP), system_tf_call for $display/$assert.
callTypes: [
'module_instantiation',
'tf_call',
'method_call',
'system_tf_call',
'checker_instantiation',
'program_instantiation',
'interface_instantiation',
],
// fieldTypes / variableTypes: dual-register so signals inside a module → field kind,
// file-level declarations → variable kind.
fieldTypes: [
'net_declaration',
'data_declaration',
'parameter_declaration',
'local_parameter_declaration',
'genvar_declaration',
'ansi_port_declaration',
],
Comment thread
3brahimi marked this conversation as resolved.
variableTypes: [
'net_declaration',
'data_declaration',
'parameter_declaration',
'local_parameter_declaration',
'genvar_declaration',
'ansi_port_declaration',
],
// This grammar has zero field() calls — nameField/paramsField are dead config.
// Empty string stops them from being treated as real field names.
nameField: '',
bodyField: '',
paramsField: '',

resolveName(node: SyntaxNode, source: string): string | undefined {
// Class constructors: name is the keyword "new" (anonymous token, not in the AST).
if (
node.type === 'class_constructor_declaration' ||
node.type === 'class_constructor_prototype'
) return 'new';

// Recursive walk through _wrappers to find the name leaf (max depth 4).
function findName(n: SyntaxNode, depth: number): string | undefined {
if (depth > 4) return undefined;
for (let i = 0; i < n.namedChildCount; i++) {
const child = n.namedChild(i);
if (!child) continue;
if (_nameNodeTypes.has(child.type)) return getNodeText(child, source);
if (_wrappers.has(child.type)) {
const found = findName(child, depth + 1);
if (found) return found;
}
}
return undefined;
}
return findName(node, 0);
},

resolveBody(node: SyntaxNode): SyntaxNode | null {
return ['function_body_declaration', 'task_body_declaration'].includes(node.type)
? node
: null;
},

getSignature(node: SyntaxNode, source: string): string | undefined {
const text = source.substring(node.startIndex, node.endIndex);
const firstLine = (text.split('\n')[0] ?? '').trim();
return firstLine.length > 120 ? firstLine.substring(0, 120) + '…' : firstLine;
},

extractImport(node: SyntaxNode, source: string) {
// `include "foo.sv" or `include <foo.sv>
if (node.type === 'include_compiler_directive') {
const child = node.namedChild(0);
if (!child) return null;
const raw = getNodeText(child, source);
const filename = raw.replace(/^["<]|[">]$/g, '');
return { moduleName: filename, signature: `\`include ${raw}` };
}
if (node.type !== 'package_import_declaration') return null;
// "import foo_pkg::*;" or "import foo_pkg::bar;"
const sig = source.substring(node.startIndex, node.endIndex).trim();
for (let i = 0; i < node.namedChildCount; i++) {
const item = node.namedChild(i);
if (!item) continue;
for (let j = 0; j < item.namedChildCount; j++) {
const child = item.namedChild(j);
if (
child &&
(child.type === 'simple_identifier' ||
child.type === 'escaped_identifier' ||
child.type === 'package_identifier')
) {
if (child.type === 'package_identifier') {
const inner = child.namedChild(0);
if (inner) return { moduleName: getNodeText(inner, source), signature: sig };
}
return { moduleName: getNodeText(child, source), signature: sig };
}
}
}
// Regex fallback
const text = source.substring(node.startIndex, node.endIndex);
const match = text.match(/import\s+([\w$]+)\s*::/);
if (match?.[1]) return { moduleName: match[1], signature: text.trim() };
return null;
},
};
Loading