text: # Copyright lowRISC contributors.
  # Licensed under the Apache License, Version 2.0, see LICENSE for details.
  # SPDX-License-Identifier: Apache-2.0
  
  import os
  import re
  import shutil
  import subprocess
  import sys
  
  import yaml
  from mako.template import Template
  
  try:
      from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper
  except ImportError:
      from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
  
  
  def _split_vlnv(core_vlnv):
      (vendor, library, name, version) = core_vlnv.split(':', 4)
      return {
          'vendor': vendor,
          'library': library,
          'name': name,
          'version': version
      }
  
  
  def _prim_cores(cores, prim_name=None):
      """ Get all cores of primitives found by fusesoc
  
      If prim_name is given, only primitives with the given name are returned.
      Otherwise, all primitives are returned, independent of their name.
      """
      def _filter_primitives(core):
          """ Filter a list of cores to find the primitives we're interested in
  
          Matching cores follow the pattern
          "lowrisc:prim_<TECHLIB_NAME>:<PRIM_NAME>", where "<TECHLIB_NAME>" and
          "<PRIM_NAME>" are placeholders.
          """
  
          vlnv = _split_vlnv(core[0])
          if (vlnv['vendor'] == 'lowrisc' and
                  vlnv['library'].startswith('prim_') and
              (prim_name is None or vlnv['name'] == prim_name)):
              return core
          return None
  
      return dict(filter(_filter_primitives, cores.items()))
  
  
  def _techlibs(prim_cores):
      techlibs = set()
      for name, info in prim_cores.items():
          vlnv = _split_vlnv(name)
          techlibs.add(_library_to_techlib_name(vlnv['library']))
      return techlibs
  
  
  def _library_to_techlib_name(library):
      return library[len("prim_"):]
  
  
  def _core_info_for_techlib(prim_cores, techlib):
      for name, info in prim_cores.items():
          vlnv = _split_vlnv(name)
          if _library_to_techlib_name(vlnv['library']) == techlib:
              return (name, info)
  
  
  def _enum_name_for_techlib(techlib_name, qualified=True):
      name = "Impl" + techlib_name.capitalize()
      if qualified:
          name = "prim_pkg::" + name
      return name
  
  
  def _top_module_file(core_files, module_name):
      module_filename = module_name + '.sv'
      for file in core_files:
          if os.path.basename(file) == module_filename:
              return file
  
  
  def _parse_module_header(generic_impl_filepath, module_name):
      """ Parse a SystemVerilog file to extract the 'module' header
  
      Return a dict with the following entries:
      - module_header: the whole module header (including the 'module' keyword)
      - package_import_declaration: import declarations
      - parameter_port_list: parameter/localparam declarations in the header
      - ports: the list of ports. The portlist can be ANSI or non-ANSI style (with
        or without signal declarations; see the SV spec for details).
      """
  
      # TODO: Evaluate different Python SV parsers and use one instead of this
      # beautiful regex. A more complete parser would ignore comments, for
      # example, or properly handle matching parentheses ... Or write a minimal
      # parser that can parse the module header.
      #
      # Initial evaluations:
      # - https://github.com/PyHDI/Pyverilog:
      #   requires Icarus and Python 3.6
      # - https://kevinpt.github.io/hdlparse/:
      #   Didn't do anything in my tests.
      # - https://github.com/sepandhaghighi/verilogparser:
      #   Doesn't support SystemVerilog
  
      # Grammar fragments from the SV2017 spec:
      #
      # module_nonansi_header ::=
      #   { attribute_instance } module_keyword [ lifetime ] module_identifier
      #   { package_import_declaration } [ parameter_port_list ] list_of_ports ;
      # module_ansi_header ::=
      #   { attribute_instance } module_keyword [ lifetime ] module_identifier
      #   { package_import_declaration }1 [ parameter_port_list ] [ list_of_port_declarations ]
      # package_import_declaration ::=
      #   import package_import_item { , package_import_item } ;
      # package_import_item ::=
      #   package_identifier :: identifier
      #   | package_identifier :: *
  
      RE_MODULE_HEADER = (
          r'(?:\s|^)'
          r'(?P<module_header>'  # start: capture the whole module header
          r'module\s+'  # module_keyword
          r'(?:(?:static|automatic)\s+)?'  # lifetime (optional)
          + module_name +  # module_identifier
          r'\s*(?P<package_import_declaration>(?:import\s+[^;]+;)+)?'  # package_import_declaration (optional, skipped)
          r'\s*(?:#\s*\((?P<parameter_port_list>[^;]+)\))?'  # parameter_port_list (optional)
          r'\s*\(\s*(?P<ports>[^;]+)\s*\)'  # list_of_port_declarations or list_of_ports
          r'\s*;'  # trailing semicolon
          r')'  # end: capture the whole module header
      )
  
      data = ""
      with open(generic_impl_filepath, encoding="utf-8") as file:
          data = file.read()
      re_module_header = re.compile(RE_MODULE_HEADER, re.DOTALL)
      matches = re_module_header.search(data)
      if not matches:
          raise ValueError("Unable to extract module header from %s." %
                           (generic_impl_filepath, ))
  
      parameter_port_list = matches.group('parameter_port_list') or ''
      return {
          'module_header':
          matches.group('module_header').strip(),
          'package_import_declaration':
          matches.group('package_import_declaration') or '',
          'parameter_port_list':
          parameter_port_list,
          'ports':
          matches.group('ports').strip() or '',
          'parameters':
          _parse_parameter_port_list(parameter_port_list)
      }
  
  
  def _parse_parameter_port_list(parameter_port_list):
      """ Parse a list of ports in a module header into individual parameters """
  
      # Grammar (SV2017):
      #
      # parameter_port_list ::=
      #     # ( list_of_param_assignments { , parameter_port_declaration } )
      #   | # ( parameter_port_declaration { , parameter_port_declaration } )
      #   | #( )
      # parameter_port_declaration ::=
      #   parameter_declaration
      #   | local_parameter_declaration
      #   | data_type list_of_param_assignments
      #   | type list_of_type_assignments
  
      # XXX: Not covering the complete grammar, e.g. `parameter x, y`
      RE_PARAMS = (
          r'parameter\s+'
          r'(?:[a-zA-Z0-9\]\[:\s\$]+\s+)?'  # type
          r'(?P<name>\w+)'  # name
          r'(?:\s*=\s*[^,;]+)'  # initial value
      )
      re_params = re.compile(RE_PARAMS)
      parameters = set()
      for m in re_params.finditer(parameter_port_list):
          parameters.add(m.group('name'))
      return parameters
  
  
  def _check_gapi(gapi):
      if not 'cores' in gapi:
          print("Key 'cores' not found in GAPI structure. "
                "At least FuseSoC 1.11 is needed. "
                "Install a compatible version with "
                "'pip3 install --user -r python-requirements.txt'.")
          return False
      return True
  
  
  def _generate_prim_pkg(gapi):
      all_prim_cores = _prim_cores(gapi['cores'])
      techlibs = _techlibs(all_prim_cores)
  
      techlib_enums = []
  
      # Insert the required generic library first to ensure it gets enum value 0
      techlib_enums.append(_enum_name_for_techlib('generic', qualified=False))
  
      for techlib in techlibs:
          if techlib == 'generic':
              # The generic implementation is required and handled separately.
              continue
          techlib_enums.append(_enum_name_for_techlib(techlib, qualified=False))
  
      # Render prim_pkg.sv file
      print("Creating prim_pkg.sv")
      prim_pkg_sv_tpl_filepath = os.path.join(os.path.dirname(__file__),
                                              'primgen', 'prim_pkg.sv.tpl')
      prim_pkg_sv_tpl = Template(filename=prim_pkg_sv_tpl_filepath)
  
      prim_pkg_sv = prim_pkg_sv_tpl.render(encoding="utf-8",
                                           techlib_enums=techlib_enums)
      with open('prim_pkg.sv', 'w') as f:
          f.write(prim_pkg_sv)
  
      # Copy prim_pkg.core (no changes needed)
      prim_pkg_core_src = os.path.join(os.path.dirname(__file__), 'primgen',
                                       'prim_pkg.core.tpl')
      prim_pkg_core_dest = 'prim_pkg.core'
      shutil.copyfile(prim_pkg_core_src, prim_pkg_core_dest)
      print("Core file written to %s." % (prim_pkg_core_dest, ))
  
  
  def _instance_sv(prim_name, techlib, parameters):
      s = "prim_{techlib}_{prim_name} #(\n"
      s += ", ".join(".{p}({p})".format(p=p) for p in parameters)
      s += ") u_impl_{techlib} (\n" \
           "  .*\n" \
           ");\n"
      return s.format(prim_name=prim_name, techlib=techlib)
  
  
  def _create_instances(prim_name, techlibs, parameters):
      """ Build SystemVerilog code instantiating primitives from the techlib """
      techlibs_wo_generic = [
          techlib for techlib in techlibs if techlib != 'generic'
      ]
      techlibs_generic_last = techlibs_wo_generic + ['generic']
  
      if not techlibs_wo_generic:
          # Don't output the if/else blocks if there no alternatives exist.
          # We still want the generate block to keep hierarchical path names
          # stable, even if more than one techlib is found.
          s = " if (1) begin : gen_generic\n"
          s += _instance_sv(prim_name, "generic", parameters) + "\n"
          s += "end"
          return s
  
      nr_techlibs = len(techlibs_generic_last)
      out = ""
      for pos, techlib in enumerate(techlibs_generic_last):
          is_first = pos == 0
          is_last = pos == nr_techlibs - 1
  
          s = ""
          if not is_first:
              s += "else "
          if not is_last:
              s += "if (Impl == {techlib_enum}) "
  
          # TODO: wildcard port lists are against our style guide, but it's safer
          # to let the synthesis tool figure out the connectivity than us trying
          # to parse the port list into individual signals.
          s += "begin : gen_{techlib}\n" + _instance_sv(prim_name, techlib,
                                                        parameters) + "end"
  
          if not is_last:
              s += " "
  
          out += s.format(prim_name=prim_name,
                          techlib=techlib,
                          techlib_enum=_enum_name_for_techlib(techlib))
      return out
  
  
  def _generate_abstract_impl(gapi):
      prim_name = gapi['parameters']['prim_name']
      prim_cores = _prim_cores(gapi['cores'], prim_name)
  
      techlibs = _techlibs(prim_cores)
  
      if not 'generic' in techlibs:
          raise ValueError("Techlib generic is required, but not found for "
                           "primitive %s." % prim_name)
      print("Implementations for primitive %s: %s" %
            (prim_name, ', '.join(techlibs)))
  
      # Extract port list out of generic implementation
      generic_core = _core_info_for_techlib(prim_cores, 'generic')[1]
      generic_module_name = 'prim_generic_' + prim_name
      top_module_filename = _top_module_file(generic_core['files'],
                                             generic_module_name)
      top_module_file = os.path.join(generic_core['core_root'],
                                     top_module_filename)
  
      print("Inspecting generic module %s" % (top_module_file, ))
      generic_hdr = _parse_module_header(top_module_file, generic_module_name)
  
      # Render abstract primitive HDL from template
      print("Creating SystemVerilog module for abstract primitive")
      abstract_prim_sv_tpl_filepath = os.path.join(os.path.dirname(__file__),
                                                   'primgen',
                                                   'abstract_prim.sv.tpl')
      abstract_prim_sv_tpl = Template(filename=abstract_prim_sv_tpl_filepath)
  
      abstract_prim_sv = abstract_prim_sv_tpl.render(
          encoding="utf-8",
          prim_name=prim_name,
          module_header_imports=generic_hdr['package_import_declaration'],
          module_header_params=generic_hdr['parameter_port_list'],
          module_header_ports=generic_hdr['ports'],
          # Creating the code to instantiate the primitives in the Mako templating
          # language is tricky to do; do it in Python instead.
          instances=_create_instances(prim_name, techlibs,
                                      generic_hdr['parameters']))
      abstract_prim_sv_filepath = 'prim_%s.sv' % (prim_name)
      with open(abstract_prim_sv_filepath, 'w') as f:
          f.write(abstract_prim_sv)
      print("Abstract primitive written to %s" %
            (os.path.abspath(abstract_prim_sv_filepath), ))
  
      # Create core file depending on all primitive implementations we have in the
      # techlibs.
      print("Creating core file for primitive %s." % (prim_name, ))
      abstract_prim_core_filepath = os.path.abspath('prim_%s.core' % (prim_name))
      dependencies = []
      dependencies.append('lowrisc:prim:prim_pkg')
      dependencies += [
          _core_info_for_techlib(prim_cores, t)[0] for t in techlibs
      ]
      abstract_prim_core = {
          'name': "lowrisc:prim_abstract:%s" % (prim_name, ),
          'filesets': {
              'files_rtl': {
                  'depend': dependencies,
                  'files': [
                      abstract_prim_sv_filepath,
                  ],
                  'file_type': 'systemVerilogSource'
              },
          },
          'targets': {
              'default': {
                  'filesets': [
                      'files_rtl',
                  ],
              },
          },
      }
      with open(abstract_prim_core_filepath, 'w') as f:
          # FuseSoC requires this line to appear first in the YAML file.
          # Inserting this line through the YAML serializer requires ordered dicts
          # to be used everywhere, which is annoying syntax-wise on Python <3.7,
          # where native dicts are not sorted.
          f.write('CAPI=2:\n')
          yaml.dump(abstract_prim_core,
                    f,
                    encoding="utf-8",
                    default_flow_style=False,
                    sort_keys=False)
      print("Core file written to %s" % (abstract_prim_core_filepath, ))
  
  
  def _get_action_from_gapi(gapi, default_action):
      if 'parameters' in gapi and 'action' in gapi['parameters']:
          return gapi['parameters']['action']
      return default_action
  
  
  def main():
      gapi_filepath = sys.argv[1]
      gapi = yaml.load(open(gapi_filepath), Loader=YamlLoader)
  
      if not _check_gapi(gapi):
          sys.exit(1)
  
      action = _get_action_from_gapi(gapi, 'generate_abstract_impl')
  
      if action == 'generate_abstract_impl':
          return _generate_abstract_impl(gapi)
      elif action == 'generate_prim_pkg':
          return _generate_prim_pkg(gapi)
      else:
          raise ValueError("Invalid action: %s" % (action, ))
  
  
  if __name__ == '__main__':
      main()
  