diff --git a/src/elements/Common.ts b/src/elements/Common.ts new file mode 100644 index 0000000..32255cc --- /dev/null +++ b/src/elements/Common.ts @@ -0,0 +1,43 @@ +import Parser from 'web-tree-sitter'; +import { IPosition } from './interfaces/Common'; + +/** + * Position of a node in the source code. + */ +export class Position implements IPosition { + /** + * Convert a tree-sitter node to a position object. + * @param node The tree-sitter node to convert. + * @returns A position object representing the node. + */ + static fromNode(node: Parser.SyntaxNode): Position { + return new Position( + node.startPosition.row, + node.endPosition.row, + node.startPosition.column, + node.endPosition.column, + ); + } + + /** + * Position of a node in the source code. + * @param startLine Start line number + * @param endLine End line number + * @param startColumn Start column number + * @param endColumn End column number + */ + constructor( + public startLine: number, + public endLine: number, + public startColumn: number, + public endColumn: number, + ) {} + + /** + * Convert the position to a string representation. + * @returns String representation of the position + */ + toString(): string { + return `(${this.startLine}:${this.startColumn}-${this.endLine}:${this.endColumn})`; + } +} diff --git a/src/elements/HDLElement.ts b/src/elements/HDLElement.ts new file mode 100644 index 0000000..53578dd --- /dev/null +++ b/src/elements/HDLElement.ts @@ -0,0 +1,53 @@ +import { IPosition } from './interfaces/Common'; +import { IHDLElement, IHDLElementInfo } from './interfaces/IHDLElement'; + +/** + * Base implementation for all HDL elements. + * @template T - Type of the HDL element info. + */ +export class HDLElement implements IHDLElement { + protected _info: T; + + /** + * Create a new HDL element. + * @param info Metadata about the HDL element + */ + constructor(info: T) { + this._info = info; + } + + /** + * Get the name of the HDL element. + */ + get name(): string { + return this._info.name ?? ''; + } + + /** + * Get the machine-generated description of the HDL element. + */ + get description(): string | undefined { + return this._info.description; + } + + /** + * Get the code comment associated with the HDL element. + */ + get comment(): string | undefined { + return this._info.comment; + } + + /** + * Get the position of the HDL element in the source code. + */ + get position(): IPosition { + return this._info.position; + } + + /** + * Get the full metadata object. + */ + get info(): T { + return this._info; + } +} diff --git a/src/elements/HDLFunction.ts b/src/elements/HDLFunction.ts new file mode 100644 index 0000000..06dca84 --- /dev/null +++ b/src/elements/HDLFunction.ts @@ -0,0 +1,24 @@ +import { HDLElement } from './HDLElement'; +import { IHDLFunctionInfo, IHDLFunction } from './interfaces/IHDLFunction'; + +/** + * Function element in VHDL. + */ +export class HDLFunction + extends HDLElement + implements IHDLFunction +{ + /** + * @inheritdoc + */ + get parameters(): string { + return this._info.parameters; + } + + /** + * @inheritdoc + */ + get returnType(): string { + return this._info.returnType; + } +} diff --git a/src/elements/HDLInstantiation.ts b/src/elements/HDLInstantiation.ts new file mode 100644 index 0000000..baa4e5a --- /dev/null +++ b/src/elements/HDLInstantiation.ts @@ -0,0 +1,20 @@ +import { HDLElement } from './HDLElement'; +import { + IHDLInstantiationInfo, + IHDLInstantiation, +} from './interfaces/IHDLInstantiation'; + +/** + * Instantiation element in VHDL. + */ +export class Instantiation + extends HDLElement + implements IHDLInstantiation +{ + /** + * @inheritdoc + */ + get instanceType(): string { + return this._info.instanceType; + } +} diff --git a/src/elements/HDLProcedure.ts b/src/elements/HDLProcedure.ts new file mode 100644 index 0000000..73d2ef0 --- /dev/null +++ b/src/elements/HDLProcedure.ts @@ -0,0 +1,17 @@ +import { HDLElement } from './HDLElement'; +import { IHDLProcedureInfo, IHDLProcedure } from './interfaces/IHDLProcedure'; + +/** + * Procedure element in VHDL. + */ +export class HDLProcedure + extends HDLElement + implements IHDLProcedure +{ + /** + * @inheritdoc + */ + get parameters(): string { + return this._info.parameters; + } +} diff --git a/src/elements/HDLProcess.ts b/src/elements/HDLProcess.ts new file mode 100644 index 0000000..5960743 --- /dev/null +++ b/src/elements/HDLProcess.ts @@ -0,0 +1,24 @@ +import { HDLElement } from './HDLElement'; +import { IHDLProcessInfo, IHDLProcess } from './interfaces/IHDLProcess'; + +/** + * Process element in VHDL. + */ +export class HDLProcess + extends HDLElement + implements IHDLProcess +{ + /** + * @inheritdoc + */ + get sensitivityList(): string[] | undefined { + return this._info.sensitivityList; + } + + /** + * @inheritdoc + */ + get processType(): string | undefined { + return this._info.processType; + } +} diff --git a/src/elements/HDLSignal.ts b/src/elements/HDLSignal.ts new file mode 100644 index 0000000..5c92f96 --- /dev/null +++ b/src/elements/HDLSignal.ts @@ -0,0 +1,24 @@ +import { HDLElement } from './HDLElement'; +import { IHDLSignalInfo, IHDLSignal } from './interfaces/IHDLSignal'; + +/** + * Signal element in VHDL. + */ +export class HDLSignal + extends HDLElement + implements IHDLSignal +{ + /** + * @inheritdoc + */ + get type(): string { + return this._info.type; + } + + /** + * @inheritdoc + */ + get defaultValue(): string | undefined { + return this._info.defaultValue; + } +} diff --git a/src/elements/interfaces/Common.ts b/src/elements/interfaces/Common.ts new file mode 100644 index 0000000..d51b0b1 --- /dev/null +++ b/src/elements/interfaces/Common.ts @@ -0,0 +1,52 @@ +import Parser from 'web-tree-sitter'; + +/** + * Static interface for position objects. + */ +export interface IPosition_ { + /** + * Convert a tree-sitter node to a position object. + * @param node The tree-sitter node to convert. + * @returns A position object representing the node. + */ + fromNode(node: Parser.SyntaxNode): IPosition; + + /** + * Create a new position object. + * @param startLine Start line number + * @param endLine End line number + * @param startColumn Start column number + * @param endColumn End column number + */ + new ( + startLine: number, + endLine: number, + startColumn: number, + endColumn: number, + ): IPosition; +} + +/** + * Position of a node in the source code. + */ +export interface IPosition { + /** + * The line number of the start of the node. + */ + startLine: number; + + /** + * The line number of the end of the node. + */ + endLine: number; + + /** + * The column number of the start of the node. + */ + startColumn: number; + + /** + * The column number of the end of the node. + */ + endColumn: number; +} diff --git a/src/elements/interfaces/HDLElementTagNameMap.ts b/src/elements/interfaces/HDLElementTagNameMap.ts new file mode 100644 index 0000000..0bf8115 --- /dev/null +++ b/src/elements/interfaces/HDLElementTagNameMap.ts @@ -0,0 +1,14 @@ +export interface HDLElementTagNameMap { + process: 'HDLProcess'; + type: 'HDLType'; + signal: 'HDLSignal'; + variable: 'HDLVariable'; + constant: 'HDLConstant'; + attribute: 'HDLAttribute'; + function: 'HDLFunction'; + procedure: 'HDLProcedure'; + instance: 'HDLInstance'; + generic: 'HDLGeneric'; + port: 'HDLPort'; + concurrent: 'HDLConcurrent'; +} diff --git a/src/elements/interfaces/IHDLElement.ts b/src/elements/interfaces/IHDLElement.ts new file mode 100644 index 0000000..1bd78ab --- /dev/null +++ b/src/elements/interfaces/IHDLElement.ts @@ -0,0 +1,66 @@ +import { IPosition } from './Common'; + +/** + * Shared metadata for any HDL element. + */ +export interface IHDLElementInfo { + /** + * Name of the HDL element. + * e.g. "my_signal", "my_function", "my_process". + */ + name?: string; + + /** + * Machine generated description of the element. + */ + description?: string; + + /** + * Code comment associated with the HDL element. + */ + comment?: string; + + /** + * Position of the HDL element in the source code. + */ + position: IPosition; +} + +/** + * Static interface for all HDL elements. + * @template T - Type of the HDL element info. + */ +export interface IHDLElement_ { + new (info: T): IHDLElement; +} + +/** + * Interface for all HDL elements. + * @template T - Type of the HDL element info. + */ +export interface IHDLElement { + /** + * Get the name of the HDL element. + */ + get name(): string; + + /** + * Get the machine generated description of the HDL element. + */ + get description(): string | undefined; + + /** + * Get the code comment associated with the HDL element. + */ + get comment(): string | undefined; + + /** + * Get the position of the HDL element in the source code. + */ + get position(): IPosition; + + /** + * Get the full metadata object. + */ + get info(): T; +} diff --git a/src/elements/interfaces/IHDLFunction.ts b/src/elements/interfaces/IHDLFunction.ts new file mode 100644 index 0000000..0ce7a2d --- /dev/null +++ b/src/elements/interfaces/IHDLFunction.ts @@ -0,0 +1,31 @@ +import { IHDLElement, IHDLElementInfo } from './IHDLElement'; + +/** + * Interface for function metadata. + */ +export interface IHDLFunctionInfo extends IHDLElementInfo { + /** + * Parameter list as string. + */ + parameters: string; + + /** + * Return type. + */ + returnType: string; +} + +/** + * Interface for function elements. + */ +export interface IHDLFunction extends IHDLElement { + /** + * Get the parameter list of the function. + */ + get parameters(): string; + + /** + * Get the return type of the function. + */ + get returnType(): string; +} diff --git a/src/elements/interfaces/IHDLInstantiation.ts b/src/elements/interfaces/IHDLInstantiation.ts new file mode 100644 index 0000000..6c173d5 --- /dev/null +++ b/src/elements/interfaces/IHDLInstantiation.ts @@ -0,0 +1,22 @@ +import { IHDLElement, IHDLElementInfo } from './IHDLElement'; + +/** + * Interface for component instantiation metadata. + */ +export interface IHDLInstantiationInfo extends IHDLElementInfo { + /** + * Type of the instantiated. + * e.g. "my_component", "my_package.my_component". + */ + instanceType: string; +} + +/** + * Interface for instantiation elements. + */ +export interface IHDLInstantiation extends IHDLElement { + /** + * Get the type of the instantiated element. + */ + get instanceType(): string; +} diff --git a/src/elements/interfaces/IHDLProcedure.ts b/src/elements/interfaces/IHDLProcedure.ts new file mode 100644 index 0000000..986b038 --- /dev/null +++ b/src/elements/interfaces/IHDLProcedure.ts @@ -0,0 +1,21 @@ +import { IHDLElement, IHDLElementInfo } from './IHDLElement'; + +/** + * Interface for procedure metadata. + */ +export interface IHDLProcedureInfo extends IHDLElementInfo { + /** + * Parameter list as string. + */ + parameters: string; +} + +/** + * Interface for procedure elements. + */ +export interface IHDLProcedure extends IHDLElement { + /** + * Get the parameter list of the procedure. + */ + get parameters(): string; +} diff --git a/src/elements/interfaces/IHDLProcess.ts b/src/elements/interfaces/IHDLProcess.ts new file mode 100644 index 0000000..4fe93f4 --- /dev/null +++ b/src/elements/interfaces/IHDLProcess.ts @@ -0,0 +1,33 @@ +import { IHDLElement, IHDLElementInfo } from './IHDLElement'; + +/** + * Interface for process metadata. + */ +export interface IHDLProcessInfo extends IHDLElementInfo { + /** + * Sensitivity list of the process. + */ + sensitivityList?: string[]; + + /** + * Type of process (optional: FSM, combinatorial, etc.). + */ + processType?: string; +} + +/** + * Interface for process elements. + */ +export interface IHDLProcess extends IHDLElement { + /** + * Get the sensitivity list of the process. + * e.g. "clk", "reset". + */ + get sensitivityList(): string[] | undefined; + + /** + * Get the type of the process. + * e.g. "FSM", "combinatorial", etc. + */ + get processType(): string | undefined; +} diff --git a/src/elements/interfaces/IHDLSignal.ts b/src/elements/interfaces/IHDLSignal.ts new file mode 100644 index 0000000..53b9eac --- /dev/null +++ b/src/elements/interfaces/IHDLSignal.ts @@ -0,0 +1,33 @@ +import { IHDLElement, IHDLElementInfo } from './IHDLElement'; + +/** + * Interface for signal metadata. + */ +export interface IHDLSignalInfo extends IHDLElementInfo { + /** + * Type of the signal. + * e.g. "std_logic", "std_logic_vector(7 downto 0)". + */ + type: string; + + /** + * Default value of the signal. + */ + defaultValue?: string; +} + +/** + * Interface for signal elements. + */ +export interface IHDLSignal extends IHDLElement { + /** + * Get the type of the signal. + * e.g. "std_logic", "std_logic_vector(7 downto 0)". + */ + get type(): string; + + /** + * Get the default value of the signal. + */ + get defaultValue(): string | undefined; +} diff --git a/src/extractors/HDLElementExtractor.ts b/src/extractors/HDLElementExtractor.ts new file mode 100644 index 0000000..ae264f8 --- /dev/null +++ b/src/extractors/HDLElementExtractor.ts @@ -0,0 +1,102 @@ +import Parser from 'web-tree-sitter'; +import { ICommentOptions } from './interfaces/ICommentOptions'; +import { + IHDLElement, + IHDLElementInfo, +} from '../elements/interfaces/IHDLElement'; + +/** + * Abstract base class for all HDL element extractors. + * Collects and provides access to leading comments and implements recursive AST traversal. + */ +export abstract class HDLElementExtractor< + T extends IHDLElement, +> { + protected readonly _comments: Map = new Map(); + + constructor( + protected readonly root: Parser.SyntaxNode, + protected readonly commentOptions: ICommentOptions = { + markerPrefix: '@', + stripPrefix: true, + }, + ) {} + + /** + * Collect all comments in the file and map them by line number. + */ + protected extractComments(): void { + const stack: Parser.SyntaxNode[] = [this.root]; + + while (stack.length > 0) { + const node = stack.pop(); + + if (!node) continue; + + if (node.type === 'comment') { + let comment = node.text.trim().replace(/^--\s*/, ''); + const prefix = this.commentOptions.markerPrefix; + + if (prefix && !comment.startsWith(prefix)) return; + + if (prefix && this.commentOptions.stripPrefix) { + comment = comment.substring(prefix.length); + + if (comment.startsWith(' ')) comment = comment.substring(1); + } + + this._comments.set(node.startPosition.row, comment); + } + + for (const child of node.namedChildren) { + stack.push(child); + } + } + } + + /** + * Try to find a contiguous block of comment lines directly above the given line. + * Stops at the first non-comment or skipped line. + * @param line The line number to check for comments. + * @returns The comment block as a string, or undefined if no comments are found. + */ + protected getLeadingComment(line: number): string | undefined { + const lines: string[] = []; + let currentLine = line - 1; + + while (this._comments.has(currentLine)) { + lines.unshift(this._comments.get(currentLine)!); + currentLine--; + } + + return lines.length > 0 ? lines.join('\n') : undefined; + } + + /** + * Recursively find all nodes of a given type in the syntax tree. + * @param node The starting node. + * @param type The node type to search for. + * @returns A flat array of matching nodes. + */ + protected findNodesByType( + node: Parser.SyntaxNode, + type: string, + ): Parser.SyntaxNode[] { + const result: Parser.SyntaxNode[] = []; + + if (node.type === type) { + result.push(node); + } + + for (const child of node.namedChildren) { + result.push(...this.findNodesByType(child, type)); + } + + return result; + } + + /** + * The main extraction method to be implemented by subclasses. + */ + abstract extract(): T[]; +} diff --git a/src/extractors/SignalExtractor.ts b/src/extractors/SignalExtractor.ts new file mode 100644 index 0000000..5e6a24b --- /dev/null +++ b/src/extractors/SignalExtractor.ts @@ -0,0 +1,51 @@ +import { HDLElementExtractor } from './HDLElementExtractor'; +import { Position } from '../elements/Common'; +import { HDLSignal } from '../elements/HDLSignal'; +import { IHDLSignalInfo } from '../elements/interfaces/IHDLSignal'; + +/** + * Extractor for VHDL signal declarations. + */ +export class SignalExtractor extends HDLElementExtractor { + /** + * Extract all signal declarations from the syntax tree. + */ + extract(): HDLSignal[] { + this.extractComments(); + + const result: HDLSignal[] = []; + + const signalNodes = this.findNodesByType( + this.root, + 'signal_declaration', + ); + + for (const node of signalNodes) { + const nameNode = node.descendantsOfType('identifier_list')[0]; + const typeNode = node.descendantsOfType('subtype_indication')[0]; + const defaultNode = node.descendantsOfType('default_expression')[0]; + + const names = nameNode.text.split(',').map((s) => s.trim()); + const type = typeNode?.text ?? ''; + const defaultValue = defaultNode?.text ?? undefined; + const pos = Position.fromNode(nameNode); + + for (const name of names) { + const comment = this.getLeadingComment(pos.startLine); + + const info: IHDLSignalInfo = { + name, + type, + defaultValue, + comment, + description: undefined, + position: pos, + }; + + result.push(new HDLSignal(info)); + } + } + + return result; + } +} diff --git a/src/extractors/interfaces/ICommentOptions.ts b/src/extractors/interfaces/ICommentOptions.ts new file mode 100644 index 0000000..e6c143e --- /dev/null +++ b/src/extractors/interfaces/ICommentOptions.ts @@ -0,0 +1,16 @@ +/** + * Options for how to treat HDL comments. + */ +export interface ICommentOptions { + /** + * Line must start with this prefix (after '--'). + * Example: '@' to accept '--@ comment'. + */ + markerPrefix?: string; + + /** + * If true, strip the marker prefix and one space (if present). + * Example: '--@ Hello' becomes 'Hello' + */ + stripPrefix?: boolean; +} diff --git a/src/parser/VHDLParser.ts b/src/parser/VHDLParser.ts new file mode 100644 index 0000000..d1dc661 --- /dev/null +++ b/src/parser/VHDLParser.ts @@ -0,0 +1,56 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import Parser from 'web-tree-sitter'; +import { IParser } from './interfaces/IParser'; + +/** + * VHDLParser is a class that provides methods to parse VHDL code + * using the tree-sitter library. + */ +export class VHDLParser implements IParser { + private _parser!: Parser; + + /** + * @inheritdoc + */ + async init(): Promise { + await Parser.init(); + this._parser = new Parser(); + + const lang = await Parser.Language.load( + join(__dirname, '../vendor/tree-sitter-vhdl.wasm'), + ); + this._parser.setLanguage(lang); + } + + /** + * @inheritdoc + */ + parse(code: string): Parser.Tree { + return this._parser.parse(code); + } + + /** + * @inheritdoc + */ + async parseFile(path: string): Promise { + const code = readFileSync(path, 'utf-8'); + + return this.parse(code); + } + + /** + * @inheritdoc + */ + async toJson(node: Parser.SyntaxNode): Promise { + return { + type: node.type, + text: node.text, + startPosition: node.startPosition, + endPosition: node.endPosition, + children: await Promise.all( + node.namedChildren.map((child) => this.toJson(child)), + ), + }; + } +} diff --git a/src/parser/interfaces/IParser.ts b/src/parser/interfaces/IParser.ts new file mode 100644 index 0000000..0b1bfeb --- /dev/null +++ b/src/parser/interfaces/IParser.ts @@ -0,0 +1,41 @@ +import Parser from 'web-tree-sitter'; + +/** + * Static parser interface + */ +export interface IParser_ { + new (): IParser; +} + +/** + * Parser interface + */ +export interface IParser { + /** + * Initialize the parser and load the language. + * @returns A promise that resolves when the parser is initialized. + */ + init(): Promise; + + /** + * Parse a string of code and return the parse tree. + * @param code The code to parse + * @returns The parsed tree. + */ + parse(code: string): Parser.Tree; + + /** + * Parse a file and return the parse tree. + * @param path The path to the file to parse + * @returns The parsed tree. + */ + parseFile(path: string): Promise; + + /** + * Convert a tree-sitter node to a JSON object. + * This is useful for debugging and visualization purposes. + * @param node The tree-sitter node to convert. + * @returns A JSON object representing the node. + */ + toJson(node: Parser.SyntaxNode): Promise; +}