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
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,
};
106 changes: 106 additions & 0 deletions src/extraction/languages/tcl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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).
//
// 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 — a type in importTypes never also gets call edges
// (else-if dispatch). `source` import detection is instead handled in visitNode
// below, which the engine always calls regardless of importTypes membership —
// extractImport would never fire here since 'command' isn't in importTypes.
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;
},
};
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
// (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'],
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