From 4a72a41a86e435e09c204faa93a8931631df4776 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Sat, 10 Oct 2026 22:56:52 -0300 Subject: [PATCH 1/9] Rewrite ModParser using PySlang, deprecate HDLReader --- hdltools/hdl_controller.py | 17 +- hdltools/hdl_reader.py | 41 ---- hdltools/mod_parser.py | 423 ++++++++++++++++++++++++++++------ hdltools/templates/comp.jinja | 24 +- hdltools/templates/stub.jinja | 40 ++-- hdltools/templates/wrap.jinja | 28 ++- tests/hdl/modules.sv | 411 ++++++--------------------------- tests/test_reader.py | 22 -- 8 files changed, 471 insertions(+), 535 deletions(-) delete mode 100644 hdltools/hdl_reader.py delete mode 100644 tests/test_reader.py diff --git a/hdltools/hdl_controller.py b/hdltools/hdl_controller.py index c7099b5..2a07bac 100644 --- a/hdltools/hdl_controller.py +++ b/hdltools/hdl_controller.py @@ -1,18 +1,16 @@ # -# Copyright (C) 2025 HDLtools Project +# Copyright (C) 2025-2026 HDLtools Project # # SPDX-License-Identifier: GPL-3.0-or-later # """ This module defines the HDLController class that orchestrates the entire flow -from reading and sanitizing HDL code, parsing modules, and generating output -HDL code using Jinja2 templates. +from parsing modules, and generating output HDL code using Jinja2 templates. """ import sys -from hdltools.hdl_reader import HDLReader from hdltools.mod_parser import ModParser from hdltools.hdl_writer import HDLWriter @@ -21,7 +19,6 @@ class HDLController: """A central controller to orchestrate the HDL processing tasks.""" def __init__(self, template): - self.reader = HDLReader() self.parser = ModParser() self.writer = HDLWriter() self.template = template @@ -30,12 +27,14 @@ def generate(self, filepath, top=None, suffix=None): """Generates HDL code from the input file.""" try: - self.reader.read_file(filepath) + # Quick check to ensure the file exists and is readable + # before passing it to the slang driver. + with open(filepath, 'r', encoding='utf-8') as f: + f.read(1) except (OSError, UnicodeDecodeError): - self._error(f'file not found ({filepath})') + self._error(f'file not found or unreadable ({filepath})') - raw_code = self.reader.get_code() - self.parser.set_code(raw_code) + self.parser.set_file(filepath) self.parser.parse() modules = self.parser.get_names() diff --git a/hdltools/hdl_reader.py b/hdltools/hdl_reader.py deleted file mode 100644 index 700549e..0000000 --- a/hdltools/hdl_reader.py +++ /dev/null @@ -1,41 +0,0 @@ -# -# Copyright (C) 2025 HDLtools Project -# -# SPDX-License-Identifier: GPL-3.0-or-later -# - -""" -Reads and sanitizes the input HDL code by removing comments, extra whitespaces -and newlines. -""" - -import re - - -class HDLReader: - """Reads and sanitizes the input HDL code.""" - - def __init__(self, code=''): - self.code = code - - def read_file(self, path): - """Reads the HDL code from file.""" - with open(path, 'r', encoding='utf-8') as fobj: - self.code = fobj.read() - - def set_code(self, code): - """Directly sets the HDL code.""" - self.code = code - - def is_vhdl(self): - """Return True if the code seems to be VHDL.""" - return 'endmodule' not in self.code.lower() - - def get_code(self): - """Retrieves the sanitized HDL code.""" - if self.is_vhdl(): - text = re.sub(r'--[^\n]*', '', self.code) - else: - text = re.sub(r'//[^\n]*', '', self.code) - text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) - return re.sub(r'\s+', ' ', text).strip() diff --git a/hdltools/mod_parser.py b/hdltools/mod_parser.py index f9f984d..016b21b 100644 --- a/hdltools/mod_parser.py +++ b/hdltools/mod_parser.py @@ -1,94 +1,369 @@ # -# Copyright (C) 2025 HDLtools Project +# Copyright (C) 2025-2026 HDLtools Project # # SPDX-License-Identifier: GPL-3.0-or-later # """ -Parses SystemVerilog modules to extract information about parameters and ports. +Parses SystemVerilog modules to extract information about parameters +and ports using a robust AST-based approach (pyslang). """ -import re +from pyslang.driver import Driver class ModParser: - """Extract information about parameters and ports from modules.""" + """Extract params, localparams, and ports from SV modules.""" - def __init__(self, code=''): + def __init__(self, file_path='', defines=None): + """Initializes the parser with a target file and macros.""" self.modules = {} - self.code = code + self.file_path = file_path + self.defines = defines if defines else [] - def set_code(self, code): - """Sets the HDL code.""" - self.code = code + def set_file(self, file_path): + """Sets the SystemVerilog HDL file path.""" + self.file_path = file_path - def parse(self): - """Parse the code.""" - self.modules = {} - pattern = ( - r'module\s+' - r'(\w+)\s*' # name - r'(#\(.*?\))?\s*' # params - r'\((.*?)\)\s*;' # ports - r'.*?endmodule' - ) - matches = re.findall(pattern, self.code, re.DOTALL) - for name, params, ports in matches: - self.modules[name] = {} - if params: - self.modules[name]['params'] = self._extract_params(params) - if ports: - self.modules[name]['ports'] = self._extract_ports(ports) - - @staticmethod - def _extract_params(text): - text = re.sub(r'[#()]', '', text) - pattern = ( - r'parameter\s+' - r'(int|bit|logic|real|string)?\s*' # type - r'(\[[^\]]*\])?\s*' # packed - r'(\w+)\s*' # name - r'=\s*([^,;]+)' # value - ) - matches = re.findall(pattern, text) - params = {} - for ptype, packed, name, value in matches: - params[name] = {} - if ptype: - params[name]['type'] = ptype - if packed: - params[name]['packed'] = packed - if value: - params[name]['value'] = value.strip() - return params - - @staticmethod - def _extract_ports(text): - pattern = ( - r'(input|output|inout)\s+' # direction - r'(reg|wire|logic)?\s*' # type - r'(signed|unsigned)?\s*' # sign - r'((?:\[[^\]]*\])*)\s*' # packed - r'(\w+)\s*' # name - r'((?:\[[^\]]*\])*)\s*' # unpacked - r'(?:=\s*([^,;]+))?' # default + def set_defines(self, defines): + """Sets the macro definitions for the preprocessor.""" + self.defines = defines + + def _clean_token(self, token): + """Robustly extracts clean string value from a pyslang Token.""" + if not token: + return "" + + if hasattr(token, "valueText") and token.valueText: + return str(token.valueText) + + if hasattr(token, "value") and token.value: + val = str(token.value).strip() + if val: + return val + + parts = str(token).split() + return parts[-1] if parts else "" + + def _extract_dimensions(self, dimensions): + """Joins dimensions safely without whitespace or trivia.""" + if not dimensions: + return "" + + dim_str = "" + for d in dimensions: + clean_d = str(d).replace(" ", "").replace("\n", "") + clean_d = clean_d.replace("\r", "").replace("\t", "") + dim_str += clean_d + + return dim_str + + def _extract_type_info(self, type_node): + """Extracts base type, signing, and packed dimensions.""" + result = {} + if not type_node: + return result + + kw = "" + if hasattr(type_node, "keyword") and type_node.keyword: + kw = self._clean_token(type_node.keyword) + elif hasattr(type_node, "name") and type_node.name: + kw = self._clean_token(type_node.name) + + if kw: + result["type"] = kw + + if hasattr(type_node, "signing") and type_node.signing: + sign_str = self._clean_token(type_node.signing) + if sign_str: + result["sign"] = sign_str + + if hasattr(type_node, "dimensions") and type_node.dimensions: + packed_str = self._extract_dimensions(type_node.dimensions) + if packed_str: + result["packed"] = packed_str + + return result + + def _extract_header_attrs(self, header): + """Extracts direction, netType, varKeyword, and dataType.""" + attrs = {} + if not header: + return attrs + + dir_val = self._clean_token(getattr(header, "direction", None)) + net_val = self._clean_token(getattr(header, "netType", None)) + var_val = self._clean_token(getattr(header, "varKeyword", None)) + + type_node = getattr( + header, "dataType", getattr(header, "type", None) ) - matches = re.findall(pattern, text) - grouped_ports = {'inputs': {}, 'outputs': {}, 'inouts': {}} - for pdir, ptype, sign, packed, name, unpacked, default in matches: - data = {} - if ptype: - data['type'] = ptype - if sign: - data['sign'] = sign - if packed: - data['packed'] = packed + type_info = self._extract_type_info(type_node) if type_node else {} + + if dir_val or net_val or var_val or type_info: + if dir_val: + attrs["direction"] = dir_val + + combined = [] + if net_val: + combined.append(net_val) + if var_val: + combined.append(var_val) + + dt_str = type_info.pop("type", "") + if dt_str: + combined.append(dt_str) + + if combined: + attrs["type"] = " ".join(combined) + + attrs.update(type_info) + + return attrs + + def _extract_body_decl_attrs(self, node): + """Extracts type attributes from declarations in the body.""" + attrs = {} + combined = [] + + net_val = self._clean_token(getattr(node, "netType", None)) + if net_val: + combined.append(net_val) + + if hasattr(node, "modifiers") and node.modifiers: + for mod in node.modifiers: + mod_str = self._clean_token(mod) + if mod_str == "var": + combined.append("var") + + type_node = getattr(node, "dataType", getattr(node, "type", None)) + type_info = self._extract_type_info(type_node) if type_node else {} + + dt_str = type_info.pop("type", "") + if dt_str: + combined.append(dt_str) + + if combined: + attrs["type"] = " ".join(combined) + + attrs.update(type_info) + return attrs + + def _apply_declarator_attrs(self, decl, target_dict): + """Extracts unpacked dimensions and initializer from a declarator.""" + if hasattr(decl, "dimensions") and decl.dimensions: + unpacked = self._extract_dimensions(decl.dimensions) if unpacked: - data['unpacked'] = unpacked - if default: - data['default'] = default.strip() - grouped_ports[pdir + 's'][name] = data - return {k: v for k, v in grouped_ports.items() if v} + target_dict["unpacked"] = unpacked + + if hasattr(decl, "initializer") and decl.initializer: + init_str = str(decl.initializer) + init_str = init_str.replace("=", "", 1).strip() + if init_str: + target_dict["default"] = init_str + + def _process_parameter_list(self, param_decl, mod_dict): + """Processes a ParameterDeclaration node.""" + if not param_decl: + return + + keyword = self._clean_token(getattr(param_decl, "keyword", None)) + if keyword == "localparam": + target_group = "localparams" + else: + target_group = "parameters" + + target_dict = mod_dict[target_group] + type_node = getattr( + param_decl, "type", getattr(param_decl, "dataType", None) + ) + type_info = self._extract_type_info(type_node) + + if not hasattr(param_decl, "declarators"): + return + + for decl in param_decl.declarators: + if str(decl).strip() == ",": + continue + if not hasattr(decl, "name"): + continue + + param_name = self._clean_token(decl.name) + if not param_name: + continue + + param_dict = {} + param_dict.update(type_info) + self._apply_declarator_attrs(decl, param_dict) + target_dict[param_name] = param_dict + + def _handle_non_ansi_port(self, port, mod_dict): + """Registers a Non-ANSI port name in the dictionary.""" + if hasattr(port, "expr") and hasattr(port.expr, "name"): + port_name = self._clean_token(port.expr.name) + if port_name: + mod_dict["ports"][port_name] = {} + + def _handle_ansi_port(self, port, mod_dict, curr_h_attrs): + """Processes an ANSI port and returns current header attrs.""" + port_dict = {} + if hasattr(port, "header") and port.header: + h_attrs = self._extract_header_attrs(port.header) + if h_attrs: + curr_h_attrs = h_attrs + + port_dict.update(curr_h_attrs) + + if hasattr(port, "declarator") and port.declarator: + decl = port.declarator + p_name = self._clean_token(decl.name) + if p_name: + self._apply_declarator_attrs(decl, port_dict) + mod_dict["ports"][p_name] = port_dict + + return curr_h_attrs + + def _process_header_ports(self, port_list, mod_dict): + """Extracts port declarations from the module header.""" + is_non_ansi = "NonAnsi" in port_list.kind.name + curr_h_attrs = {} + + for port in port_list.ports: + if str(port).strip() == ",": + continue + + if is_non_ansi: + self._handle_non_ansi_port(port, mod_dict) + else: + curr_h_attrs = self._handle_ansi_port( + port, mod_dict, curr_h_attrs + ) + + def _process_body_port_decl(self, body_member, mod_dict): + """Processes a PortDeclaration in the module body.""" + h_node = getattr(body_member, "header", None) + h_attrs = self._extract_header_attrs(h_node) + + if not hasattr(body_member, "declarators"): + return + + for decl in body_member.declarators: + if str(decl).strip() == ",": + continue + if not hasattr(decl, "name"): + continue + + p_name = self._clean_token(decl.name) + p_dict = mod_dict["ports"] + if p_name and p_name in p_dict: + port_data = p_dict[p_name] + port_data.update(h_attrs) + self._apply_declarator_attrs(decl, port_data) + + def _process_body_data_decl(self, body_member, mod_dict): + """Processes a DataDeclaration or NetDeclaration in the body.""" + decl_attrs = self._extract_body_decl_attrs(body_member) + + if not hasattr(body_member, "declarators"): + return + + for decl in body_member.declarators: + if str(decl).strip() == ",": + continue + if not hasattr(decl, "name"): + continue + + p_name = self._clean_token(decl.name) + p_dict = mod_dict["ports"] + if p_name and p_name in p_dict: + port_data = p_dict[p_name] + port_data.update(decl_attrs) + self._apply_declarator_attrs(decl, port_data) + + def _process_body_members(self, members, mod_dict): + """Iterates and parses logic and declarations inside the module.""" + for body_member in members: + kind_name = body_member.kind.name + + if "ParameterDeclarationStatement" in kind_name: + param_decl = getattr(body_member, "parameter", None) + if param_decl: + self._process_parameter_list(param_decl, mod_dict) + + elif "PortDeclaration" in kind_name: + self._process_body_port_decl(body_member, mod_dict) + + elif ("DataDeclaration" in kind_name or + "NetDeclaration" in kind_name): + self._process_body_data_decl(body_member, mod_dict) + + def _run_slang(self): + """Configures and runs the pyslang compiler driver.""" + driver = Driver() + driver.addStandardArgs() + + args = ["slang"] + for macro in self.defines: + args.append(f"-D{macro}") + args.append(self.file_path) + + arg_str = " ".join(args) + + if not driver.parseCommandLine(arg_str): + print("Error: Slang failed to parse the arguments.") + return None + + if not driver.processOptions(): + print("Error: Slang failed to process the options.") + return None + + if not driver.parseAllSources(): + print("Error: Slang failed to parse the sources.") + return None + + return driver + + def parse(self): + """Main entry point to parse SV code using pyslang AST.""" + self.modules = {} + + if not self.file_path: + print("Error: No file path provided for ModParser.") + return + + driver = self._run_slang() + if not driver: + return + + for tree in driver.syntaxTrees: + root = tree.root + + for member in root.members: + if "Module" not in member.kind.name: + continue + + mod_name = self._clean_token(member.header.name) + self.modules[mod_name] = { + "parameters": {}, + "localparams": {}, + "ports": {} + } + mod_dict = self.modules[mod_name] + + # 1. Extract ANSI Parameters + param_list = member.header.parameters + if param_list and hasattr(param_list, "declarations"): + for param_decl in param_list.declarations: + self._process_parameter_list(param_decl, mod_dict) + + # 2. Extract Ports from header + port_list = member.header.ports + if port_list and hasattr(port_list, "ports"): + self._process_header_ports(port_list, mod_dict) + + # 3. Extract items from the Module Body + if hasattr(member, "members"): + self._process_body_members(member.members, mod_dict) def get_names(self): """Get a list of modules names.""" diff --git a/hdltools/templates/comp.jinja b/hdltools/templates/comp.jinja index e28fc45..9f447f6 100644 --- a/hdltools/templates/comp.jinja +++ b/hdltools/templates/comp.jinja @@ -1,21 +1,19 @@ {%- set all_ports = [] %} {%- set port_names = [] %} -{%- for direction, signals in ports.items() %} - {%- for name, props in signals.items() %} - {%- set dir = direction.rstrip('s') %} - {%- set type = props.type | default('') %} - {%- set packed = props.packed | default('') %} - {%- set unpacked = props.unpacked | default('') %} - {%- set port_str = dir ~ " " ~ type ~ " " ~ packed ~ " " ~ name ~ " " ~ unpacked %} - {%- set _ = all_ports.append(port_str) %} - {%- set _ = port_names.append(name) %} - {%- endfor %} +{%- for name, props in ports.items() %} + {%- set dir = props.direction | default('') %} + {%- set type = props.type | default('') %} + {%- set packed = props.packed | default('') %} + {%- set unpacked = props.unpacked | default('') %} + {%- set port_str = dir ~ " " ~ type ~ " " ~ packed ~ " " ~ name ~ " " ~ unpacked %} + {%- set _ = all_ports.append(port_str) %} + {%- set _ = port_names.append(name) %} {%- endfor -%} -module {{ name }} {% if params -%}#( - {%- for name, props in params.items() -%} +module {{ name }} {% if parameters -%}#( + {%- for name, props in parameters.items() -%} {%- set type = props.type | default('') %} {%- set packed = props.packed | default('') %} - parameter {{ type }} {{ packed }} {{ name }} = {{ props.value }}{% if not loop.last %},{% endif %} + parameter {{ type }} {{ packed }} {{ name }} = {{ props.default }}{% if not loop.last %},{% endif %} {%- endfor %} ){% endif %}( {%- for port in all_ports %} diff --git a/hdltools/templates/stub.jinja b/hdltools/templates/stub.jinja index 5e5cd53..72773fd 100644 --- a/hdltools/templates/stub.jinja +++ b/hdltools/templates/stub.jinja @@ -1,43 +1,41 @@ {%- set all_ports = [] %} {%- set port_names = [] %} -{%- for direction, signals in ports.items() %} - {%- for name, props in signals.items() %} - {%- set dir = direction.rstrip('s') %} - {%- set type = props.type | default('') %} - {%- set packed = props.packed | default('') %} - {%- set unpacked = props.unpacked | default('') %} - {%- set port_str = dir ~ " " ~ type ~ " " ~ packed ~ " " ~ name ~ " " ~ unpacked %} - {%- set _ = all_ports.append(port_str) %} - {%- set _ = port_names.append(name) %} - {%- endfor %} +{%- for name, props in ports.items() %} + {%- set dir = props.direction | default('') %} + {%- set type = props.type | default('') %} + {%- set packed = props.packed | default('') %} + {%- set unpacked = props.unpacked | default('') %} + {%- set port_str = dir ~ " " ~ type ~ " " ~ packed ~ " " ~ name ~ " " ~ unpacked %} + {%- set _ = all_ports.append(port_str) %} + {%- set _ = port_names.append(name) %} {%- endfor -%} -module {{ name }}{{ suffix }} {% if params -%}#( - {%- for name, props in params.items() -%} +module {{ name }}{{ suffix }} {% if parameters -%}#( + {%- for name, props in parameters.items() -%} {%- set type = props.type | default('') %} {%- set packed = props.packed | default('') %} - parameter {{ type }} {{ packed }} {{ name }} = {{ props.value }}{% if not loop.last %},{% endif %} + parameter {{ type }} {{ packed }} {{ name }} = {{ props.default }}{% if not loop.last %},{% endif %} {%- endfor %} ){% endif %}( {%- for port in all_ports %} {{ port }}{% if not loop.last %},{% endif %} {%- endfor %} ); -{% if ports.outputs %} - {%- for name, props in ports.outputs.items() %} +{%- for name, props in ports.items() %} + {%- if props.direction == 'output' %} {%- if props.type == "reg" %} always_comb {{ name }} = '0; {%- else %} assign {{ name }} = '0; {%- endif %} - {%- endfor %} -{% endif %} -{% if ports.inouts %} - {%- for name, props in ports.inouts.items() %} + {%- endif %} +{%- endfor %} +{%- for name, props in ports.items() %} + {%- if props.direction == 'inout' %} {%- if props.type == "reg" %} always_comb {{ name }} = 'Z; {%- else %} assign {{ name }} = 'Z; {%- endif %} - {%- endfor %} -{% endif %} + {%- endif %} +{%- endfor %} endmodule diff --git a/hdltools/templates/wrap.jinja b/hdltools/templates/wrap.jinja index 0dddd0e..4a58c3f 100644 --- a/hdltools/templates/wrap.jinja +++ b/hdltools/templates/wrap.jinja @@ -1,21 +1,19 @@ {%- set all_ports = [] %} {%- set port_names = [] %} -{%- for direction, signals in ports.items() %} - {%- for name, props in signals.items() %} - {%- set dir = direction.rstrip('s') %} - {%- set type = "wire" %} - {%- set packed = props.packed | default('') %} - {%- set unpacked = props.unpacked | default('') %} - {%- set port_str = dir ~ " " ~ type ~ " " ~ packed ~ " " ~ name ~ " " ~ unpacked %} - {%- set _ = all_ports.append(port_str) %} - {%- set _ = port_names.append(name) %} - {%- endfor %} +{%- for name, props in ports.items() %} + {%- set dir = props.direction | default('') %} + {%- set type = "wire" %} + {%- set packed = props.packed | default('') %} + {%- set unpacked = props.unpacked | default('') %} + {%- set port_str = dir ~ " " ~ type ~ " " ~ packed ~ " " ~ name ~ " " ~ unpacked %} + {%- set _ = all_ports.append(port_str) %} + {%- set _ = port_names.append(name) %} {%- endfor -%} -module {{ name }}{{ suffix }} {% if params -%}#( - {%- for name, props in params.items() -%} +module {{ name }}{{ suffix }} {% if parameters -%}#( + {%- for name, props in parameters.items() -%} {%- set type = props.type | default('') %} {%- set packed = props.packed | default('') %} - parameter {{ type }} {{ packed }} {{ name }} = {{ props.value }}{% if not loop.last %},{% endif %} + parameter {{ type }} {{ packed }} {{ name }} = {{ props.default }}{% if not loop.last %},{% endif %} {%- endfor %} ){% endif %}( {%- for port in all_ports %} @@ -23,8 +21,8 @@ module {{ name }}{{ suffix }} {% if params -%}#( {%- endfor %} ); - {{ name }} {% if params %}#( -{%- for name, props in params.items() %} + {{ name }} {% if parameters %}#( +{%- for name, props in parameters.items() %} .{{ name }} ({{ name }}){% if not loop.last %},{% endif %} {%- endfor %} ){% endif %} {{ name }} ( diff --git a/tests/hdl/modules.sv b/tests/hdl/modules.sv index 878392a..fcd359d 100644 --- a/tests/hdl/modules.sv +++ b/tests/hdl/modules.sv @@ -20,356 +20,87 @@ endmodule */ module mod_nopar ( - input in00, - input reg in01, - input wire in02, - input logic in03, - input signed in04, - input reg signed in05, - input wire signed in06, - input logic signed in07, - input [7:0] in08, - input reg [7:0] in09, - input wire [7:0][7:0] in10, - input logic [7:0][7:0] in11, - input signed [7:0] in12, - input reg signed [7:0] in13, - input wire signed [7:0][7:0] in14, - input logic signed [7:0][7:0] in15, - input in16 [1:8], - input reg in17 [1:8], - input wire in18 [1:8][1:8], - input logic in19 [1:8][1:8], - input signed in20 [1:8], - input reg signed in21 [1:8], - input wire signed in22 [1:8][1:8], - input logic signed in23 [1:8][1:8], - input [7:0] in24 [1:8], - input reg [7:0] in25 [1:8], - input wire [7:0][7:0] in26 [1:8][1:8], - input logic [7:0][7:0] in27 [1:8][1:8], - input signed [7:0] in28 [1:8], - input reg signed [7:0] in29 [1:8], - input wire signed [7:0][7:0] in30 [1:8][1:8], - input logic signed [7:0][7:0] in31 [1:8][1:8], - input in32 = 1'b0, - input reg in33 = '0, - input wire in34 = 1'b0, - input logic in35 = '0, - input signed in36 = 1'b0, - input reg signed in37 = '0, - input wire signed in38 = 1'b0, - input logic signed in39 = '0, - input [7:0] in40 = 8'h0, - input reg [7:0] in41 = '0, - input wire [7:0][7:0] in42 = 8'h0, - input logic [7:0][7:0] in43 = '0, - input signed [7:0] in44 = 8'h0, - input reg signed [7:0] in45 = '0, - input wire signed [7:0][7:0] in46 = 8'h0, - input logic signed [7:0][7:0] in47 = '0, - input in48 [1:8] = '{default:0}, - input reg in49 [1:8] = '{default:0}, - input wire in50 [1:8][1:8] = '{default:0}, - input logic in51 [1:8][1:8] = '{default:0}, - input signed in52 [1:8] = '{default:0}, - input reg signed in53 [1:8] = '{default:0}, - input wire signed in54 [1:8][1:8] = '{default:0}, - input logic signed in55 [1:8][1:8] = '{default:0}, - input [7:0] in56 [1:8] = '{default:0}, - input reg [7:0] in57 [1:8] = '{default:0}, - input wire [7:0][7:0] in58 [1:8][1:8] = '{default:0}, - input logic [7:0][7:0] in59 [1:8][1:8] = '{default:0}, - input signed [7:0] in60 [1:8] = '{default:0}, - input reg signed [7:0] in61 [1:8] = '{default:0}, - input wire signed [7:0][7:0] in62 [1:8][1:8] = '{default:0}, - input logic signed [7:0][7:0] in63 [1:8][1:8] = '{default:0}, - // - output out00, - output reg out01, - output wire out02, - output logic out03, - output signed out04, - output reg signed out05, - output wire signed out06, - output logic signed out07, - output [7:0] out08, - output reg [7:0] out09, - output wire [7:0][7:0] out10, - output logic [7:0][7:0] out11, - output signed [7:0] out12, - output reg signed [7:0] out13, - output wire signed [7:0][7:0] out14, - output logic signed [7:0][7:0] out15, - output out16 [1:8], - output reg out17 [1:8], - output wire out18 [1:8][1:8], - output logic out19 [1:8][1:8], - output signed out20 [1:8], - output reg signed out21 [1:8], - output wire signed out22 [1:8][1:8], - output logic signed out23 [1:8][1:8], - output [7:0] out24 [1:8], - output reg [7:0] out25 [1:8], - output wire [7:0][7:0] out26 [1:8][1:8], - output logic [7:0][7:0] out27 [1:8][1:8], - output signed [7:0] out28 [1:8], - output reg signed [7:0] out29 [1:8], - output wire signed [7:0][7:0] out30 [1:8][1:8], - output logic signed [7:0][7:0] out31 [1:8][1:8], - output out32 = 1'b0, - output reg out33 = '0, - output wire out34 = 1'b0, - output logic out35 = '0, - output signed out36 = 1'b0, - output reg signed out37 = '0, - output wire signed out38 = 1'b0, - output logic signed out39 = '0, - output [7:0] out40 = 8'h0, - output reg [7:0] out41 = '0, - output wire [7:0][7:0] out42 = 8'h0, - output logic [7:0][7:0] out43 = '0, - output signed [7:0] out44 = 8'h0, - output reg signed [7:0] out45 = '0, - output wire signed [7:0][7:0] out46 = 8'h0, - output logic signed [7:0][7:0] out47 = '0, - output out48 [1:8] = '{default:0}, - output reg out49 [1:8] = '{default:0}, - output wire out50 [1:8][1:8] = '{default:0}, - output logic out51 [1:8][1:8] = '{default:0}, - output signed out52 [1:8] = '{default:0}, - output reg signed out53 [1:8] = '{default:0}, - output wire signed out54 [1:8][1:8] = '{default:0}, - output logic signed out55 [1:8][1:8] = '{default:0}, - output [7:0] out56 [1:8] = '{default:0}, - output reg [7:0] out57 [1:8] = '{default:0}, - output wire [7:0][7:0] out58 [1:8][1:8] = '{default:0}, - output logic [7:0][7:0] out59 [1:8][1:8] = '{default:0}, - output signed [7:0] out60 [1:8] = '{default:0}, - output reg signed [7:0] out61 [1:8] = '{default:0}, - output wire signed [7:0][7:0] out62 [1:8][1:8] = '{default:0}, - output logic signed [7:0][7:0] out63 [1:8][1:8] = '{default:0}, - // - inout io00, - inout reg io01, - inout wire io02, - inout logic io03, - inout signed io04, - inout reg signed io05, - inout wire signed io06, - inout logic signed io07, - inout [7:0] io08, - inout reg [7:0] io09, - inout wire [7:0][7:0] io10, - inout logic [7:0][7:0] io11, - inout signed [7:0] io12, - inout reg signed [7:0] io13, - inout wire signed [7:0][7:0] io14, - inout logic signed [7:0][7:0] io15, - inout io16 [1:8], - inout reg io17 [1:8], - inout wire io18 [1:8][1:8], - inout logic io19 [1:8][1:8], - inout signed io20 [1:8], - inout reg signed io21 [1:8], - inout wire signed io22 [1:8][1:8], - inout logic signed io23 [1:8][1:8], - inout [7:0] io24 [1:8], - inout reg [7:0] io25 [1:8], - inout wire [7:0][7:0] io26 [1:8][1:8], - inout logic [7:0][7:0] io27 [1:8][1:8], - inout signed [7:0] io28 [1:8], - inout reg signed [7:0] io29 [1:8], - inout wire signed [7:0][7:0] io30 [1:8][1:8], - inout logic signed [7:0][7:0] io31 [1:8][1:8], - inout io32 = 1'b0, - inout reg io33 = '0, - inout wire io34 = 1'b0, - inout logic io35 = '0, - inout signed io36 = 1'b0, - inout reg signed io37 = '0, - inout wire signed io38 = 1'b0, - inout logic signed io39 = '0, - inout [7:0] io40 = 8'h0, - inout reg [7:0] io41 = '0, - inout wire [7:0][7:0] io42 = 8'h0, - inout logic [7:0][7:0] io43 = '0, - inout signed [7:0] io44 = 8'h0, - inout reg signed [7:0] io45 = '0, - inout wire signed [7:0][7:0] io46 = 8'h0, - inout logic signed [7:0][7:0] io47 = '0, - inout io48 [1:8] = '{default:0}, - inout reg io49 [1:8] = '{default:0}, - inout wire io50 [1:8][1:8] = '{default:0}, - inout logic io51 [1:8][1:8] = '{default:0}, - inout signed io52 [1:8] = '{default:0}, - inout reg signed io53 [1:8] = '{default:0}, - inout wire signed io54 [1:8][1:8] = '{default:0}, - inout logic signed io55 [1:8][1:8] = '{default:0}, - inout [7:0] io56 [1:8] = '{default:0}, - inout reg [7:0] io57 [1:8] = '{default:0}, - inout wire [7:0][7:0] io58 [1:8][1:8] = '{default:0}, - inout logic [7:0][7:0] io59 [1:8][1:8] = '{default:0}, - inout signed [7:0] io60 [1:8] = '{default:0}, - inout reg signed [7:0] io61 [1:8] = '{default:0}, - inout wire signed [7:0][7:0] io62 [1:8][1:8] = '{default:0}, - inout logic signed [7:0][7:0] io63 [1:8][1:8] = '{default:0} + input port0, + output reg port1, + inout wire port2, + input unsigned port3, port4 [8], port5 = '1, + output [7:0] port6, + inout port7 [1:8], + input port8 = 1'b0, + output logic signed [7:0][7:0] port9 [1:8][1:8] = '{default:0}, portA, portB ); - - always_comb begin - out00 = in00; - out01 = in01; - out02 = in02; - out03 = in03; - out04 = in04; - out05 = in05; - out06 = in06; - out07 = in07; - out08 = in08; - out09 = in09; - out10 = in10; - out11 = in11; - out12 = in12; - out13 = in13; - out14 = in14; - out15 = in15; - out16 = in16; - out17 = in17; - out18 = in18; - out19 = in19; - out20 = in20; - out21 = in21; - out22 = in22; - out23 = in23; - out24 = in24; - out25 = in25; - out26 = in26; - out27 = in27; - out28 = in28; - out29 = in29; - out30 = in30; - out31 = in31; - out32 = in32; - out33 = in33; - out34 = in34; - out35 = in35; - out36 = in36; - out37 = in37; - out38 = in38; - out39 = in39; - out40 = in40; - out41 = in41; - out42 = in42; - out43 = in43; - out44 = in44; - out45 = in45; - out46 = in46; - out47 = in47; - out48 = in48; - out49 = in49; - out50 = in50; - out51 = in51; - out52 = in52; - out53 = in53; - out54 = in54; - out55 = in55; - out56 = in56; - out57 = in57; - out58 = in58; - out59 = in59; - out60 = in60; - out61 = in61; - out62 = in62; - out63 = in63; - end - - always_comb begin - io00 = in00; - io01 = in01; - io02 = in02; - io03 = in03; - io04 = in04; - io05 = in05; - io06 = in06; - io07 = in07; - io08 = in08; - io09 = in09; - io10 = in10; - io11 = in11; - io12 = in12; - io13 = in13; - io14 = in14; - io15 = in15; - io16 = in16; - io17 = in17; - io18 = in18; - io19 = in19; - io20 = in20; - io21 = in21; - io22 = in22; - io23 = in23; - io24 = in24; - io25 = in25; - io26 = in26; - io27 = in27; - io28 = in28; - io29 = in29; - io30 = in30; - io31 = in31; - io32 = in32; - io33 = in33; - io34 = in34; - io35 = in35; - io36 = in36; - io37 = in37; - io38 = in38; - io39 = in39; - io40 = in40; - io41 = in41; - io42 = in42; - io43 = in43; - io44 = in44; - io45 = in45; - io46 = in46; - io47 = in47; - io48 = in48; - io49 = in49; - io50 = in50; - io51 = in51; - io52 = in52; - io53 = in53; - io54 = in54; - io55 = in55; - io56 = in56; - io57 = in57; - io58 = in58; - io59 = in59; - io60 = in60; - io61 = in61; - io62 = in62; - io63 = in63; - end - endmodule /* Module with parameters */ +// `define DEFINE1 80 + module mod_param #( - parameter INTPARAM1 = 8, - parameter int INTPARAM2 = 8, - parameter [3:0] VECPARAM1 = 4'b1010, - parameter logic [3:0] VECPARAM2 = '1, - parameter bit BITPARAM1 = 1'b0, - parameter bit [3:0] BITPARAM2 = 4'h3, - parameter string STRPARAM1 = "test", - parameter real REAPARAM1 = 2.5 + parameter PARAM0, + parameter int PARAM1, + parameter [3:0] PARAM2, + parameter PARAM3 = 1'b0, + parameter logic [3:0] PARAM4 = '1, PARAM5, PARAM6, + localparam LOCAL0 = PARAM3 + PARAM4, LOCAL1 = $clog2(PARAM0) )( - input [INTPARAM1-1:0] data_in, - output [INTPARAM1-1:0] data_out + input [$clog2(PARAM0)-1:0] port0, + output [`DEFINE1-1:0] port1, + inout [32] port2, + inout [`DEFINE2] port3, + inout [PARAM1] port4 ); +endmodule + +module mod_non_ansi (port0, port1, port2, port3, port4, port5, port6); + + // Parameters defined in the module body + parameter int PARAM0 = 32; + parameter logic [7:0] PARAM1 = 8'hAA, PARAM2= 8'hBB; + localparam int signed LOCAL0 = 16; + + // Port declarations in the module body + input port0; + wire port0 = 1'b1; + output reg port1; + inout port2; + input port3, port4; + logic signed [7:0] port3 = 8'h99, port4; + output [15:0] port5 [0:3]; + input var logic port6; + +endmodule + +// User-defined type outside the module +typedef logic [31:0] custom_data_t; + +module mod_edge_cases ( + port0, + port1, + port2 +); + + // 1. localparam in body + localparam int LOCAL0 = 16; + + // 2. Custom typedef and alternative net types + input custom_data_t port0; + output tri [1:0] port1; + + // 3. Legacy split declaration with interleaved RTL noise + output port2; + + wire internal_sig = 1'b0; + + always_comb begin + if (port0[0]) begin + // some logic + end + end + + reg port2; - assign data_out = data_in; + assign port1 = 2'bzz; endmodule diff --git a/tests/test_reader.py b/tests/test_reader.py deleted file mode 100644 index 299bf0d..0000000 --- a/tests/test_reader.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from pathlib import Path -from hdltools.hdl_reader import HDLReader - - -@pytest.fixture -def vcode(): - vfile = Path(__file__).parent.resolve() / 'hdl' / 'modules.sv' - vobj = HDLReader() - vobj.read_file(vfile) - return vobj.get_code() - - -def test_comments(vcode): - comment_patterns = ['//', '/*', '*/', '--'] - assert not any(pattern in vcode for pattern in comment_patterns) - - -def test_spaces(vcode): - assert '\n' not in vcode - assert ' ' not in vcode From 2f6b502b29a1777e194fdf481170e1f81378d81e Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Sat, 10 Oct 2026 22:58:17 -0300 Subject: [PATCH 2/9] Add app modjson --- hdltools/apps/modjson.py | 20 ++++++++++++++++++++ hdltools/cli_parser.py | 14 ++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 hdltools/apps/modjson.py diff --git a/hdltools/apps/modjson.py b/hdltools/apps/modjson.py new file mode 100644 index 0000000..209fc7a --- /dev/null +++ b/hdltools/apps/modjson.py @@ -0,0 +1,20 @@ +# +# Copyright (C) 2026 HDLtools Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +"""Print collected info as JSON.""" + +import json + +from hdltools.cli_parser import cli_parser +from hdltools.mod_parser import ModParser + + +args = cli_parser() +parser = ModParser(args.file) +# parser.set_defines(["DATA_WIDTH=32", "FEATURE_ENABLED"]) +parser.parse() +data = parser.get_modules() +print(json.dumps(data, indent=4)) diff --git a/hdltools/cli_parser.py b/hdltools/cli_parser.py index 1fd53d2..c96c9a2 100644 --- a/hdltools/cli_parser.py +++ b/hdltools/cli_parser.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 HDLtools Project +# Copyright (C) 2025-2026 HDLtools Project # # SPDX-License-Identifier: GPL-3.0-or-later # @@ -9,16 +9,22 @@ import argparse -def cli_parser(app): +def cli_parser(app=None): """Parse command-line arguments based on the selected application.""" + parser = argparse.ArgumentParser() + if app == 'comp': parser.add_argument('--top1') parser.add_argument('--top2') - parser.add_argument('files', nargs=2) - else: + elif app in ['stub', 'wrap']: parser.add_argument('--top') parser.add_argument('--suffix', default=f'_{app}') parser.add_argument('--output') + + if app == 'comp': + parser.add_argument('files', nargs=2) + else: parser.add_argument('file') + return parser.parse_args() From 37386827c96db37295b1422a8ba27647be8f961d Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Sat, 10 Oct 2026 22:58:42 -0300 Subject: [PATCH 3/9] Add script to prepare a venv --- source-me.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 source-me.sh diff --git a/source-me.sh b/source-me.sh new file mode 100644 index 0000000..d79c0b5 --- /dev/null +++ b/source-me.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "ERROR: This script must be sourced to activate the environment." + echo "Run it as: source ${0}" + exit 1 +fi + +if [ ! -d "venv" ]; then + python3 -m venv venv +fi + +source venv/bin/activate + +pip install --upgrade pip +pip install pyslang +pip install jinja2 + +# pip install -e . +SDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +export PYTHONPATH="${SDIR}:${PYTHONPATH}" From 354a5d56d4d4fb936cd085662ab82feef6478287 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Sat, 10 Oct 2026 22:59:00 -0300 Subject: [PATCH 4/9] Ignore venv --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 89a2017..b6a4970 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -ignore __pycache__ +ignore +venv From 2c18d85ba5f283b7571bb8ce37b3c256b4c2fdd5 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Mon, 12 Oct 2026 22:17:10 -0300 Subject: [PATCH 5/9] Remove test_paser.py and sanity.sh --- tests/hdl/modules.sv | 2 -- tests/hdl/sanity.sh | 16 --------- tests/test_parser.py | 85 -------------------------------------------- 3 files changed, 103 deletions(-) delete mode 100644 tests/hdl/sanity.sh delete mode 100644 tests/test_parser.py diff --git a/tests/hdl/modules.sv b/tests/hdl/modules.sv index fcd359d..a5e71d6 100644 --- a/tests/hdl/modules.sv +++ b/tests/hdl/modules.sv @@ -35,8 +35,6 @@ endmodule Module with parameters */ -// `define DEFINE1 80 - module mod_param #( parameter PARAM0, parameter int PARAM1, diff --git a/tests/hdl/sanity.sh b/tests/hdl/sanity.sh deleted file mode 100644 index 521e92f..0000000 --- a/tests/hdl/sanity.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DOCKER="docker run --rm -v $PWD:$PWD -w $PWD --user $(id -u):$(id -g)" - -IMAGE="ghcr.io/pyfpga/langutils" -for file in modules.sv memory1.sv memory2.sv; do - $DOCKER $IMAGE surelog -parse -nonote -noinfo -nowarning -sv "$file" && rm -fr slpp_all - $DOCKER $IMAGE verible-verilog-lint --rules=-module-filename,-explicit-parameter-storage-type "$file" -done - -IMAGE="ghcr.io/pyfpga/simulation" -for file in memory1.sv memory2.sv; do - $DOCKER $IMAGE verilator --lint-only "$file" -done diff --git a/tests/test_parser.py b/tests/test_parser.py deleted file mode 100644 index f4dad66..0000000 --- a/tests/test_parser.py +++ /dev/null @@ -1,85 +0,0 @@ -import pytest - -from pathlib import Path -from hdltools.hdl_reader import HDLReader -from hdltools.mod_parser import ModParser - - -@pytest.fixture -def vobj(): - filepath = Path(__file__).parent.resolve() / 'hdl' / 'modules.sv' - reader = HDLReader() - reader.read_file(filepath) - parser = ModParser(reader.get_code()) - parser.parse() - return parser - - -def test_modules(vobj): - modules = vobj.get_modules() - assert len(modules) == 3 - - -def test_empty(vobj): - module = vobj.get_module('mod_empty') - assert module == {} - - -def test_params(vobj): - module = vobj.get_module('mod_param') - assert 'params' in module - params = {} - params["INTPARAM1"] = {"value": "8"} - params["INTPARAM2"] = {"type": "int", "value": "8"} - params["VECPARAM1"] = {"packed": "[3:0]", "value": "4'b1010"} - params["VECPARAM2"] = {"type": "logic", "packed": "[3:0]", "value": "'1"} - params["BITPARAM1"] = {"type": "bit", "value": "1'b0"} - params["BITPARAM2"] = {"type": "bit", "packed": "[3:0]", "value": "4'h3"} - params["STRPARAM1"] = {"type": "string", "value": '"test"'} - params["REAPARAM1"] = {"type": "real", "value": "2.5"} - assert module["params"] == params - - -def test_ports(vobj): - module = vobj.get_module('mod_param') - assert 'ports' in module - ports = { - 'inputs': {'data_in': {'packed': '[INTPARAM1-1:0]'}}, - 'outputs': {'data_out': {'packed': '[INTPARAM1-1:0]'}} - } - assert module['ports'] == ports - module = vobj.get_module('mod_nopar') - assert 'ports' in module - assert module['ports']['inputs'] == gen_ports('in') - assert module['ports']['outputs'] == gen_ports('out') - assert module['ports']['inouts'] == gen_ports('io') - - -def gen_ports(prefix): - ports = {} - types = [None, 'reg', 'wire', 'logic'] - for i in range(64): - key = f"{prefix}{i:02d}" - entry = {} - if i % 4 != 0: - entry["type"] = types[i % 4] - if (i // 4) % 2 == 1: - entry["sign"] = "signed" - if (i // 8) % 2 == 1: - if (i // 2) % 2: - entry["packed"] = "[7:0][7:0]" - else: - entry["packed"] = "[7:0]" - if (i // 16) % 2 == 1: - if (i // 2) % 2: - entry["unpacked"] = "[1:8][1:8]" - else: - entry["unpacked"] = "[1:8]" - if (i // 32) % 2 == 1: - entry["default"] = "'0" if i % 2 else "1'b0" - if i >= 40: - entry["default"] = "'0" if i % 2 else "8'h0" - if i >= 48: - entry["default"] = "'{default:0}" - ports[key] = entry - return ports From 5769993981b937ee21c271eb4389cbca564498c6 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Mon, 12 Oct 2026 22:26:03 -0300 Subject: [PATCH 6/9] apps: add support for defines --- hdltools/apps/genstub.py | 2 +- hdltools/apps/genwrap.py | 2 +- hdltools/apps/modcomp.py | 2 +- hdltools/apps/modjson.py | 3 +-- hdltools/cli_parser.py | 8 ++++++++ hdltools/hdl_controller.py | 24 +++++++++++++----------- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/hdltools/apps/genstub.py b/hdltools/apps/genstub.py index b3df125..3ce165c 100644 --- a/hdltools/apps/genstub.py +++ b/hdltools/apps/genstub.py @@ -11,6 +11,6 @@ args = cli_parser('stub') -ctrl = HDLController('stub') +ctrl = HDLController('stub', defines=args.defines) ctrl.generate(args.file, args.top, args.suffix) ctrl.write(args.output) diff --git a/hdltools/apps/genwrap.py b/hdltools/apps/genwrap.py index a586e52..d2a77da 100644 --- a/hdltools/apps/genwrap.py +++ b/hdltools/apps/genwrap.py @@ -11,6 +11,6 @@ args = cli_parser('wrap') -ctrl = HDLController('wrap') +ctrl = HDLController('wrap', defines=args.defines) ctrl.generate(args.file, args.top, args.suffix) ctrl.write(args.output) diff --git a/hdltools/apps/modcomp.py b/hdltools/apps/modcomp.py index 9035060..3788455 100644 --- a/hdltools/apps/modcomp.py +++ b/hdltools/apps/modcomp.py @@ -14,7 +14,7 @@ args = cli_parser('comp') -ctrl = HDLController('comp') +ctrl = HDLController('comp', defines=args.defines) mod1 = ctrl.generate(args.files[0], args.top1) mod2 = ctrl.generate(args.files[1], args.top2) diff --git a/hdltools/apps/modjson.py b/hdltools/apps/modjson.py index 209fc7a..b0b5f6c 100644 --- a/hdltools/apps/modjson.py +++ b/hdltools/apps/modjson.py @@ -13,8 +13,7 @@ args = cli_parser() -parser = ModParser(args.file) -# parser.set_defines(["DATA_WIDTH=32", "FEATURE_ENABLED"]) +parser = ModParser(args.file, defines=args.defines) parser.parse() data = parser.get_modules() print(json.dumps(data, indent=4)) diff --git a/hdltools/cli_parser.py b/hdltools/cli_parser.py index c96c9a2..f9bbcb2 100644 --- a/hdltools/cli_parser.py +++ b/hdltools/cli_parser.py @@ -22,6 +22,14 @@ def cli_parser(app=None): parser.add_argument('--suffix', default=f'_{app}') parser.add_argument('--output') + parser.add_argument( + '-D', '--define', + dest='defines', + action='append', + default=[], + help='Define macros for the SV preprocessor' + ) + if app == 'comp': parser.add_argument('files', nargs=2) else: diff --git a/hdltools/hdl_controller.py b/hdltools/hdl_controller.py index 2a07bac..7aa0818 100644 --- a/hdltools/hdl_controller.py +++ b/hdltools/hdl_controller.py @@ -5,8 +5,8 @@ # """ -This module defines the HDLController class that orchestrates the entire flow -from parsing modules, and generating output HDL code using Jinja2 templates. +This module defines the HDLController class that orchestrates SystemVerilog +parsing (via pyslang) and output code generation (via Jinja2). """ import sys @@ -16,19 +16,20 @@ class HDLController: - """A central controller to orchestrate the HDL processing tasks.""" + """Orchestrate SV parsing and template-based code generation tasks.""" - def __init__(self, template): - self.parser = ModParser() + def __init__(self, template, defines=None): + """Initializes controller.""" + self.parser = ModParser(defines=defines) self.writer = HDLWriter() self.template = template def generate(self, filepath, top=None, suffix=None): - """Generates HDL code from the input file.""" + """Validates source, parses with slang, and renders output template.""" try: - # Quick check to ensure the file exists and is readable - # before passing it to the slang driver. + # Check to ensure the file is accessible + # before passing its path to the compiler driver. with open(filepath, 'r', encoding='utf-8') as f: f.read(1) except (OSError, UnicodeDecodeError): @@ -39,9 +40,9 @@ def generate(self, filepath, top=None, suffix=None): modules = self.parser.get_names() if not modules: - self._error('module not found') + self._error('no modules found or parsing failed') if top and top not in modules: - self._error(f'top not found ({top})') + self._error(f'top module not found ({top})') if not top: top = modules[0] @@ -53,7 +54,7 @@ def generate(self, filepath, top=None, suffix=None): return self.writer.get_code() def write(self, filepath): - """Writes the generated HDL code.""" + """Writes the generated code to a file, or stdout if empty.""" if not filepath: print(self.writer.get_code()) else: @@ -61,5 +62,6 @@ def write(self, filepath): @staticmethod def _error(message, ecode=1): + """Logs an error message and terminates execution.""" print(f'ERROR: {message}') sys.exit(ecode) From 29e01d1fc49394a21b5a6d9a0e3de29af0f181b8 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Mon, 12 Oct 2026 22:26:36 -0300 Subject: [PATCH 7/9] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c466636..812e50c 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Scripts for automating HDL-related tasks. -> **NOTE:** this project is still in development but already functional (March 2025). +> **NOTE:** this project is still in development but already functional (Oct 2026). From 5ce2645c9f55bcee5a9cb5c0665bdabbcd30f325 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Mon, 12 Oct 2026 22:28:21 -0300 Subject: [PATCH 8/9] ci: update lint and test workflows --- .github/workflows/lint.yml | 8 ++++---- .github/workflows/test.yml | 12 ++++++------ Makefile | 9 ++++++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c8676a1..dd8dd10 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ name: 'lint' on: push: paths: - - 'pyfpga/**' + - 'hdltools/**' - 'tests/**' jobs: @@ -11,8 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install dependencies - run: pip install pycodestyle pylint - - name: Run linting + run: pip install pycodestyle pylint pyslang + - name: Run lint run: make lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd2be6e..2732272 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,24 +3,24 @@ name: 'test' on: push: paths: - - 'pyfpga/**' + - 'hdltools/**' - 'tests/**' jobs: test: strategy: matrix: - pyver: ['3.8', '3.9', '3.10', '3.11', '3.12'] + pyver: ['3.11', '3.12', '3.13', '3.14', '3.15'] runs-on: ubuntu-latest name: ${{ matrix.pyver }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.pyver }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.pyver }} - name: Install dependencies - run: pip install pytest - - name: Run tests + run: pip install pyslang + - name: Run test run: make test diff --git a/Makefile b/Makefile index 0901a77..d694f72 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,15 @@ all: lint test lint: - pycodestyle hdltools tests - pylint -s n hdltools + PYTHONPATH=$(PWD) pycodestyle hdltools tests + PYTHONPATH=$(PWD) pylint -s n hdltools git diff --check --cached test: - PYTHONPATH=$(PWD) pytest -vv + PYTHONPATH=$(PWD) python3 hdltools/apps/genstub.py -h + PYTHONPATH=$(PWD) python3 hdltools/apps/genwrap.py -h + PYTHONPATH=$(PWD) python3 hdltools/apps/modcomp.py -h + PYTHONPATH=$(PWD) python3 hdltools/apps/modjson.py -h clean: py3clean . From ba7563465b08640142a051f8c749c9dd0e3e8224 Mon Sep 17 00:00:00 2001 From: Rodrigo Alejandro Melo Date: Tue, 13 Oct 2026 21:50:59 -0300 Subject: [PATCH 9/9] Makefile modified to assign PYTHONPATH once --- Makefile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d694f72..6e7e9f0 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,20 @@ #!/usr/bin/make +CDIR := $(shell pwd) +export PYTHONPATH := $(CDIR)/src:$(PYTHONPATH) + all: lint test lint: - PYTHONPATH=$(PWD) pycodestyle hdltools tests - PYTHONPATH=$(PWD) pylint -s n hdltools + pycodestyle hdltools + pylint -s n hdltools git diff --check --cached test: - PYTHONPATH=$(PWD) python3 hdltools/apps/genstub.py -h - PYTHONPATH=$(PWD) python3 hdltools/apps/genwrap.py -h - PYTHONPATH=$(PWD) python3 hdltools/apps/modcomp.py -h - PYTHONPATH=$(PWD) python3 hdltools/apps/modjson.py -h + python3 hdltools/apps/genstub.py -h + python3 hdltools/apps/genwrap.py -h + python3 hdltools/apps/modcomp.py -h + python3 hdltools/apps/modjson.py -h clean: py3clean .