diff --git a/hdltools/apps/genstub.py b/hdltools/apps/genstub.py index a0d561c..b3df125 100644 --- a/hdltools/apps/genstub.py +++ b/hdltools/apps/genstub.py @@ -6,41 +6,11 @@ """This script generates a stub for the specified module.""" -import argparse -import logging -import sys +from hdltools.cli_parser import cli_parser +from hdltools.hdl_controller import HDLController -from hdltools.mod_parse import ModParse -from hdltools.gen_file import GenFile - -logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") - -parser = argparse.ArgumentParser() -parser.add_argument('--top') -parser.add_argument('--suffix', default='_stub') -parser.add_argument('--output') -parser.add_argument('file') -args = parser.parse_args() - -modules = ModParse(args.file) -names = modules.get_names() - -if not len(names) or (args.top and args.top not in names): - logging.error('module not found') - sys.exit(1) - -if not args.top: - args.top = names[0] - -module = modules.get_module(args.top) -module['name'] = args.top -module['suffix'] = args.suffix - -top = GenFile() -top.render('stub', module) - -if not args.output: - print(top) -else: - top.write(args.output) +args = cli_parser('stub') +ctrl = HDLController('stub') +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 2663893..a586e52 100644 --- a/hdltools/apps/genwrap.py +++ b/hdltools/apps/genwrap.py @@ -6,41 +6,11 @@ """This script generates a wrapper for the specified module.""" -import argparse -import logging -import sys +from hdltools.cli_parser import cli_parser +from hdltools.hdl_controller import HDLController -from hdltools.mod_parse import ModParse -from hdltools.gen_file import GenFile - -logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") - -parser = argparse.ArgumentParser() -parser.add_argument('--top') -parser.add_argument('--suffix', default='_wrap') -parser.add_argument('--output') -parser.add_argument('file') -args = parser.parse_args() - -modules = ModParse(args.file) -names = modules.get_names() - -if not len(names) or (args.top and args.top not in names): - logging.error('module not found') - sys.exit(1) - -if not args.top: - args.top = names[0] - -module = modules.get_module(args.top) -module['name'] = args.top -module['suffix'] = args.suffix - -top = GenFile() -top.render('wrap', module) - -if not args.output: - print(top) -else: - top.write(args.output) +args = cli_parser('wrap') +ctrl = HDLController('wrap') +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 9406a43..9035060 100644 --- a/hdltools/apps/modcomp.py +++ b/hdltools/apps/modcomp.py @@ -6,41 +6,22 @@ """This script compares two Verilog modules.""" -import argparse import difflib -import json -import logging import sys -from hdltools.mod_parse import ModParse +from hdltools.cli_parser import cli_parser +from hdltools.hdl_controller import HDLController -logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") +args = cli_parser('comp') +ctrl = HDLController('comp') +mod1 = ctrl.generate(args.files[0], args.top1) +mod2 = ctrl.generate(args.files[1], args.top2) -parser = argparse.ArgumentParser() -parser.add_argument('--top1') -parser.add_argument('--top2') -parser.add_argument('files', nargs=2) -args = parser.parse_args() - -modules = ModParse(args.files[0]) -module1 = modules.get_module(args.top1) -if not module1: - logging.error('first module not found') - sys.exit(1) - -modules = ModParse(args.files[1]) -module2 = modules.get_module(args.top2) -if not module2: - logging.error('second module not found') - sys.exit(1) - -if module1 == module2: - logging.info('modules are equal') +if mod1 == mod2: + print('INFO: modules are equal') else: - logging.error('modules have differences') - text1 = json.dumps(module1, indent=2, sort_keys=True).splitlines() - text2 = json.dumps(module2, indent=2, sort_keys=True).splitlines() - diff = difflib.unified_diff(text1, text2, lineterm='') - print("\n".join(diff)) + print('ERROR: modules are different\n') + diff = list(difflib.ndiff(mod1.splitlines(), mod2.splitlines())) + print('\n'.join(diff)) sys.exit(1) diff --git a/hdltools/cli_parser.py b/hdltools/cli_parser.py new file mode 100644 index 0000000..1fd53d2 --- /dev/null +++ b/hdltools/cli_parser.py @@ -0,0 +1,24 @@ +# +# Copyright (C) 2025 HDLtools Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +"""This file implements the command-line parser.""" + +import argparse + + +def cli_parser(app): + """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: + parser.add_argument('--top') + parser.add_argument('--suffix', default=f'_{app}') + parser.add_argument('--output') + parser.add_argument('file') + return parser.parse_args() diff --git a/hdltools/gen_file.py b/hdltools/gen_file.py deleted file mode 100644 index 0db5011..0000000 --- a/hdltools/gen_file.py +++ /dev/null @@ -1,34 +0,0 @@ -# -# Copyright (C) 2025 HDLtools Project -# -# SPDX-License-Identifier: GPL-3.0-or-later -# - -""" -Generates files based on templates. -""" - -from pathlib import Path -from jinja2 import Environment, FileSystemLoader - - -class GenFile: - """Generates files.""" - - def __init__(self): - tdir = Path(__file__).parent.joinpath('templates') - self.env = Environment(loader=FileSystemLoader(str(tdir))) - self.content = 'Nothing yet' - - def render(self, name, context): - """Render the specified template.""" - template = self.env.get_template(f'{name}.jinja') - self.content = template.render(context) - - def write(self, output): - """Write content into a file.""" - with open(output, 'w', encoding='utf-8') as fobj: - fobj.write(self.content) - - def __str__(self): - return self.content diff --git a/hdltools/hdl_controller.py b/hdltools/hdl_controller.py new file mode 100644 index 0000000..c7099b5 --- /dev/null +++ b/hdltools/hdl_controller.py @@ -0,0 +1,66 @@ +# +# Copyright (C) 2025 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. +""" + +import sys + +from hdltools.hdl_reader import HDLReader +from hdltools.mod_parser import ModParser +from hdltools.hdl_writer import HDLWriter + + +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 + + def generate(self, filepath, top=None, suffix=None): + """Generates HDL code from the input file.""" + + try: + self.reader.read_file(filepath) + except (OSError, UnicodeDecodeError): + self._error(f'file not found ({filepath})') + + raw_code = self.reader.get_code() + self.parser.set_code(raw_code) + self.parser.parse() + + modules = self.parser.get_names() + if not modules: + self._error('module not found') + if top and top not in modules: + self._error(f'top not found ({top})') + if not top: + top = modules[0] + + context = self.parser.get_module(top) + context['name'] = top + context['suffix'] = suffix + self.writer.render(self.template, context) + + return self.writer.get_code() + + def write(self, filepath): + """Writes the generated HDL code.""" + if not filepath: + print(self.writer.get_code()) + else: + self.writer.write_file(filepath) + + @staticmethod + def _error(message, ecode=1): + print(f'ERROR: {message}') + sys.exit(ecode) diff --git a/hdltools/hdl_detect.py b/hdltools/hdl_detect.py deleted file mode 100644 index 6fa7819..0000000 --- a/hdltools/hdl_detect.py +++ /dev/null @@ -1,72 +0,0 @@ -# -# Copyright (C) 2025 HDLtools Project -# -# SPDX-License-Identifier: GPL-3.0-or-later -# - -""" -Module for detecting hardware description languages (HDL). - -This module provides the `HdlDetect` class, which can detect whether a given -code is written in VHDL, Verilog, or SystemVerilog. It uses specific keywords -and patterns to identify the language based on the code provided. -""" - -import argparse -import re - - -class HdlDetect: - """ - Class to detect hardware description language (HDL) from a given code. - """ - - def __init__(self, code=''): - self.lang = None - self.detect(code) - - def detect(self, code): - """Detect the HDL from the provided code.""" - self.code = code - self.lang = None - - if 'entity' in self.code and 'module' not in self.code: - self.lang = 'vhdl' - elif 'entity' not in self.code and 'module' in self.code: - self.lang = 'vlog' - keywords = r'\blogic\b|\balways_ff\b|\balways_comb\b' - if re.search(keywords, self.code): - self.lang = 'slog' - - def is_vhdl(self): - """Returns True if the code is VHDL.""" - return self.lang == 'vhdl' - - def is_vlog(self): - """Returns True if the code is Verilog.""" - return self.lang == 'vlog' - - def is_slog(self): - """Returns True if the code is SystemVerilog.""" - return self.lang == 'slog' - - def is_xlog(self): - """Returns True if the code is Verilog or SystemVerilog.""" - return self.lang in ('vlog', 'slog') - - def get_lang(self): - """Returns the detected language.""" - return self.lang - - def __str__(self): - return self.lang - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('hdlfile') - args = parser.parse_args() - with open(args.hdlfile, 'r', encoding='utf-8') as fobj: - text = fobj.read() - obj = HdlDetect(text) - print(obj.get_lang()) diff --git a/hdltools/hdl_reader.py b/hdltools/hdl_reader.py new file mode 100644 index 0000000..700549e --- /dev/null +++ b/hdltools/hdl_reader.py @@ -0,0 +1,41 @@ +# +# 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/hdl_sanitize.py b/hdltools/hdl_sanitize.py deleted file mode 100644 index 88bb4e5..0000000 --- a/hdltools/hdl_sanitize.py +++ /dev/null @@ -1,45 +0,0 @@ -# -# Copyright (C) 2025 HDLtools Project -# -# SPDX-License-Identifier: GPL-3.0-or-later -# - -""" -Sanitizes HDL source code by removing comments and extra whitespaces. -""" - -import argparse -import re - - -class HdlSanitize: - """Removes comments and extra whitespace in HDL (VHDL/Verilog) files.""" - - def __init__(self, fpath, is_vhdl=False): - try: - with open(fpath, 'r', encoding='utf-8') as fobj: - ftext = fobj.read() - except (OSError, IOError) as error: - self.code = f'Error opening {fpath}: {error}' - return - if is_vhdl: - ftext = re.sub(r'--.*', '', ftext) - else: - ftext = re.sub(r'//.*', '', ftext) - ftext = re.sub(r'/\*.*?\*/', '', ftext, flags=re.DOTALL) - self.code = re.sub(r'\s+', ' ', ftext).strip() - - def get_code(self): - """Get the sanitized HDL code.""" - return self.code - - def __str__(self): - return self.code - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('hdlfile') - args = parser.parse_args() - code = HdlSanitize(args.hdlfile) - print(code) diff --git a/hdltools/hdl_writer.py b/hdltools/hdl_writer.py new file mode 100644 index 0000000..57e0427 --- /dev/null +++ b/hdltools/hdl_writer.py @@ -0,0 +1,35 @@ +# +# Copyright (C) 2025 HDLtools Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +""" +Generates and writes the output HDL code based on templates. +""" + +from pathlib import Path +from jinja2 import Environment, FileSystemLoader + + +class HDLWriter: + """Generates and writes the output HDL code.""" + + def __init__(self): + tdir = Path(__file__).parent.joinpath('templates') + self.env = Environment(loader=FileSystemLoader(str(tdir))) + self.code = '' + + def render(self, tempname, context): + """Render the specified template.""" + template = self.env.get_template(f'{tempname}.jinja') + self.code = template.render(context) + + def write_file(self, path): + """Writes the generated HDL code to the specified file.""" + with open(path, 'w', encoding='utf-8') as fobj: + fobj.write(self.code) + + def get_code(self): + """Returns the generated code.""" + return self.code diff --git a/hdltools/mod_parse.py b/hdltools/mod_parser.py similarity index 86% rename from hdltools/mod_parse.py rename to hdltools/mod_parser.py index 43d4e54..f9f984d 100644 --- a/hdltools/mod_parse.py +++ b/hdltools/mod_parser.py @@ -8,23 +8,23 @@ Parses SystemVerilog modules to extract information about parameters and ports. """ -import argparse -import json import re -from hdltools.hdl_sanitize import HdlSanitize - -class ModParse: +class ModParser: """Extract information about parameters and ports from modules.""" - def __init__(self, fpath): + def __init__(self, code=''): self.modules = {} - hdl = HdlSanitize(fpath) - self.code = hdl.get_code() - self._parse() + self.code = code + + def set_code(self, code): + """Sets the HDL code.""" + self.code = code - def _parse(self): + def parse(self): + """Parse the code.""" + self.modules = {} pattern = ( r'module\s+' r'(\w+)\s*' # name @@ -105,14 +105,3 @@ def get_module(self, module=None): if module in self.modules: return self.modules[module] return None - - def __str__(self): - return json.dumps(self.modules, indent=2) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('svfile') - args = parser.parse_args() - modules = ModParse(args.svfile) - print(modules) diff --git a/hdltools/templates/comp.jinja b/hdltools/templates/comp.jinja new file mode 100644 index 0000000..e28fc45 --- /dev/null +++ b/hdltools/templates/comp.jinja @@ -0,0 +1,25 @@ +{%- 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 %} +{%- endfor -%} +module {{ name }} {% if params -%}#( + {%- for name, props in params.items() -%} + {%- set type = props.type | default('') %} + {%- set packed = props.packed | default('') %} + parameter {{ type }} {{ packed }} {{ name }} = {{ props.value }}{% if not loop.last %},{% endif %} + {%- endfor %} +){% endif %}( +{%- for port in all_ports %} + {{ port }}{% if not loop.last %},{% endif %} +{%- endfor %} +); +endmodule diff --git a/tests/test_detect.py b/tests/test_detect.py deleted file mode 100644 index 5c75d7b..0000000 --- a/tests/test_detect.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest -from hdltools.hdl_detect import HdlDetect - - -code = { - 'vhdl': """ -library IEEE; -use IEEE.STD_LOGIC_1164.ALL; - -entity Example is - Port ( - data_i : in STD_LOGIC; - data_o : out STD_LOGIC - ); -end Example; - -architecture Behavioral of Example is -begin - data_o <= data_i; -end Behavioral; -""", - 'vlog': """ -module Example ( - input data_i, - output data_o -); - assign data_o = data_i; -endmodule -""", - 'slog': """ -module Example ( - input logic data_i, - output logic data_o -); - assign data_o = data_i; -endmodule -""" -} - - -@pytest.mark.parametrize("lang", ['vhdl', 'vlog', 'slog']) -def test_detect(lang): - obj = HdlDetect(code[lang]) - assert obj.get_lang() == lang diff --git a/tests/test_parse.py b/tests/test_parser.py similarity index 83% rename from tests/test_parse.py rename to tests/test_parser.py index 4c0cbcd..f4dad66 100644 --- a/tests/test_parse.py +++ b/tests/test_parser.py @@ -1,24 +1,31 @@ +import pytest + from pathlib import Path +from hdltools.hdl_reader import HDLReader +from hdltools.mod_parser import ModParser -from hdltools.mod_parse import ModParse -vfile = Path(__file__).parent.resolve() / 'hdl/modules.sv' +@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 = ModParse(vfile) +def test_modules(vobj): modules = vobj.get_modules() assert len(modules) == 3 -def test_empty(): - vobj = ModParse(vfile) +def test_empty(vobj): module = vobj.get_module('mod_empty') assert module == {} -def test_params(): - vobj = ModParse(vfile) +def test_params(vobj): module = vobj.get_module('mod_param') assert 'params' in module params = {} @@ -33,8 +40,7 @@ def test_params(): assert module["params"] == params -def test_ports(): - vobj = ModParse(vfile) +def test_ports(vobj): module = vobj.get_module('mod_param') assert 'ports' in module ports = { diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..299bf0d --- /dev/null +++ b/tests/test_reader.py @@ -0,0 +1,22 @@ +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 diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py deleted file mode 100644 index 2942951..0000000 --- a/tests/test_sanitize.py +++ /dev/null @@ -1,19 +0,0 @@ -from pathlib import Path - -from hdltools.hdl_sanitize import HdlSanitize - -vfile = Path(__file__).parent.resolve() / 'hdl/modules.sv' - - -def test_comments(): - vobj = HdlSanitize(vfile) - vcode = vobj.get_code() - comment_patterns = ['//', '/*', '*/', '--'] - assert not any(pattern in vcode for pattern in comment_patterns) - - -def test_spaces(): - vobj = HdlSanitize(vfile) - vcode = vobj.get_code() - assert '\n' not in vcode - assert ' ' not in vcode