-
Notifications
You must be signed in to change notification settings - Fork 4.1k
feat: add SystemVerilog/Verilog, Tcl, and VHDL language support #1508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
3brahimi
wants to merge
3
commits into
colbymchenry:main
Choose a base branch
from
3brahimi:feat/hdl-languages-verilog-tcl-vhdl
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| ], | ||
|
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; | ||
| }, | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.