diff --git a/README.md b/README.md index dccebd3e4..6a92e186f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index d4127631d..c138cd21e 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -50,6 +50,9 @@ const WASM_GRAMMAR_FILES: Record = { 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', }; /** @@ -141,6 +144,15 @@ export const EXTENSION_MAP: Record = { '.cu': 'cpp', '.cuh': 'cpp', '.nix': 'nix', + // HDL: SystemVerilog and Verilog + '.sv': 'verilog', + '.svh': 'verilog', + '.v': 'verilog', + // Tcl + '.tcl': 'tcl', + // VHDL + '.vhd': 'vhdl', + '.vhdl': 'vhdl', // XML: file-level tracking; the MyBatis extractor matches `` // shape and emits SQL-statement nodes (other XML returns empty). '.xml': 'xml', @@ -271,6 +283,17 @@ export async function initGrammars(): Promise { * 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 @@ -290,7 +313,7 @@ export async function initGrammars(): Promise { */ const VENDORED_WASM_LANGS: ReadonlySet = 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 @@ -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; diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 6b760b01d..565bbfbb9 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -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> = { typescript: typescriptExtractor, @@ -69,4 +72,7 @@ export const EXTRACTORS: Partial> = { terraform: terraformExtractor, arkts: arktsExtractor, nix: nixExtractor, + verilog: verilogExtractor, + tcl: tclExtractor, + vhdl: vhdlExtractor, }; diff --git a/src/extraction/languages/tcl.ts b/src/extraction/languages/tcl.ts new file mode 100644 index 000000000..379ae02bd --- /dev/null +++ b/src/extraction/languages/tcl.ts @@ -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) (arguments ...) (braced_word)) +// namespace: (namespace (word_list (simple_word<"eval">) (simple_word) ...)) +// set: (set (id) ...) + +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; + }, +}; diff --git a/src/extraction/languages/verilog.ts b/src/extraction/languages/verilog.ts new file mode 100644 index 000000000..eb8a5e803 --- /dev/null +++ b/src/extraction/languages/verilog.ts @@ -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 + 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', + ], + 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 + 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; + }, +}; diff --git a/src/extraction/languages/vhdl.ts b/src/extraction/languages/vhdl.ts new file mode 100644 index 000000000..bc5b5957d --- /dev/null +++ b/src/extraction/languages/vhdl.ts @@ -0,0 +1,78 @@ +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-vhdl (vendored at src/extraction/wasm/tree-sitter-vhdl.wasm, +// built from alemuller/tree-sitter-vhdl, MIT). Covers VHDL-93 through VHDL-2008. +// +// VHDL naming notes: +// - `_designator` is a private inlined rule: the field 'designator' is applied +// directly to identifier / extended_identifier / operator_symbol — no wrapper +// node appears in the AST. So entity/architecture/package/subtype/function/ +// procedure all share the same first-child identifier lookup. +// - signal/variable/constant names live in identifier_list → first identifier. +// - component_declaration has field('name', $.identifier) directly. + +export const vhdlExtractor: LanguageExtractor = { + // component_declaration added (alemuller grammar has field('name', $.identifier)). + classTypes: [ + 'entity_declaration', + 'architecture_body', + 'package_declaration', + 'component_declaration', + ], + // grammar defines concrete function_body / procedure_body, not abstract _subprogram_body. + functionTypes: ['function_body', 'procedure_body'], + methodTypes: ['function_body', 'procedure_body'], + interfaceTypes: [], + structTypes: ['record_type_definition'], + enumTypes: ['enumeration_type_definition'], + typeAliasTypes: ['subtype_declaration'], + importTypes: ['use_clause'], + callTypes: ['function_call', 'procedure_call_statement'], + // port_declaration is not a public node in this grammar (use + // signal_interface_declaration inside port_clause instead). + variableTypes: ['signal_declaration', 'variable_declaration', 'constant_declaration'], + nameField: '', + bodyField: '', + paramsField: '', + + resolveName(node: SyntaxNode, source: string): string | undefined { + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (!child) continue; + if ( + child.type === 'identifier' || + child.type === 'extended_identifier' || + child.type === 'operator_symbol' + ) { + return getNodeText(child, source); + } + if (child.type === 'identifier_list') { + const id = child.namedChild(0); + if (id) return getNodeText(id, source); + } + } + 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; + }, + + extractImport(node: SyntaxNode, source: string): { moduleName: string; signature: string } | null { + if (node.type !== 'use_clause') return null; + // "use ieee.std_logic_1164.all;" → moduleName = "ieee.std_logic_1164" + const text = source.substring(node.startIndex, node.endIndex); + const match = text.match(/use\s+([\w.]+)\.all\b|use\s+([\w.]+)\.([\w]+)/i); + if (match) { + // match[1]: "ieee.std_logic_1164" (stripped .all) + // match[2]: "work.my_pkg" (library.package, before .item) + const moduleName = match[1] || match[2] || ''; + return { moduleName, signature: text.trim() }; + } + return null; + }, +}; diff --git a/src/extraction/wasm/tree-sitter-tcl.wasm b/src/extraction/wasm/tree-sitter-tcl.wasm new file mode 100755 index 000000000..7931c4d14 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-tcl.wasm differ diff --git a/src/extraction/wasm/tree-sitter-verilog.wasm b/src/extraction/wasm/tree-sitter-verilog.wasm new file mode 100755 index 000000000..4187e7531 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-verilog.wasm differ diff --git a/src/extraction/wasm/tree-sitter-vhdl.wasm b/src/extraction/wasm/tree-sitter-vhdl.wasm new file mode 100755 index 000000000..7df34fe70 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-vhdl.wasm differ diff --git a/src/types.ts b/src/types.ts index a1861bba4..40be02a72 100644 --- a/src/types.ts +++ b/src/types.ts @@ -116,6 +116,9 @@ export const LANGUAGES = [ 'vbnet', 'erlang', 'terraform', + 'verilog', + 'tcl', + 'vhdl', 'unknown', ] as const;