blob: 8ea68270a4c33000b5bd902a15519366b119561f [file] [log] [blame]
#!/usr/bin/env python3
# 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 sys
import yaml
from mako.template import Template
# Make vendored packages available in the search path.
sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor'))
try:
from yaml import CSafeDumper as YamlDumper
from yaml import CSafeLoader as YamlLoader
except ImportError:
from yaml import SafeDumper as YamlDumper
from yaml import SafeLoader as YamlLoader
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_verible(generic_impl_filepath, module_name):
""" Parse a SystemVerilog file to extract the 'module' header using Verible
Implementation of _parse_module_header() which uses verible-verilog-syntax
to do the parsing. This is the primary implementation and is used when
supported Verible version is available.
See _parse_module_header() for API details.
"""
from google_verible_verilog_syntax_py.verible_verilog_syntax import (
PreOrderTreeIterator, VeribleVerilogSyntax)
parser = VeribleVerilogSyntax()
data = parser.parse_file(generic_impl_filepath,
options={"skip_null": True})
if data.errors:
for err in data.errors:
print(
f'Verible: {err.phase} error in line {err.line} column {err.column}' +
(': {err.message}' if err.message else '.'))
# Intentionally not raising an exception here.
# There are chances that Verible recovered from errors.
if not data.tree:
raise ValueError(f"Unable to parse {generic_impl_filepath!r}.")
module = data.tree.find({"tag": "kModuleDeclaration"})
header = module.find({"tag": "kModuleHeader"})
if not header:
raise ValueError("Unable to extract module header from %s." %
(generic_impl_filepath, ))
name = header.find({"tag": ["SymbolIdentifier", "EscapedIdentifier"]},
iter_=PreOrderTreeIterator)
if not name:
raise ValueError("Unable to extract module name from %s." %
(generic_impl_filepath, ))
imports = header.find_all({"tag": "kPackageImportDeclaration"})
parameters_list = header.find({"tag": "kFormalParameterList"})
parameters = set()
if parameters_list:
for parameter in sorted(
parameters_list.iter_find_all({"tag": "kParamDeclaration"})):
if parameter.find({"tag": "parameter"}):
parameter_id = parameter.find(
{"tag": ["SymbolIdentifier", "EscapedIdentifier"]})
parameters.add(parameter_id.text)
ports = header.find({"tag": "kPortDeclarationList"})
return {
'module_header': header.text,
'package_import_declaration': '\n'.join([i.text for i in imports]),
'parameter_port_list': parameters_list.text if parameters_list else '',
'ports': ports.text if ports else '',
'parameters': parameters,
'parser': 'Verible'
}
def _parse_module_header_fallback(generic_impl_filepath, module_name):
""" Parse a SystemVerilog file to extract the 'module' header using RegExp
Legacy implementation of _parse_module_header() using regular expressions.
It is not as robust as Verible-backed implementation, but doesn't need
Verible to work.
See _parse_module_header() for API details.
"""
# 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
# package_import_declaration (optional, skipped)
r'\s*(?P<package_import_declaration>(?:import\s+[^;]+;)+)?'
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),
'parser':
'Fallback (regex)'
}
def test_parse_parameter_port_list():
assert _parse_parameter_port_list("parameter enum_t P") == {'P'}
assert _parse_parameter_port_list("parameter integer P") == {'P'}
assert _parse_parameter_port_list("parameter logic [W-1:0] P") == {'P'}
assert _parse_parameter_port_list("parameter logic [W-1:0] P = '0") == {
'P'
}
assert _parse_parameter_port_list("parameter logic [W-1:0] P = 'b0") == {
'P'
}
assert _parse_parameter_port_list("parameter logic [W-1:0] P = 2'd0") == {
'P'
}
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 list(sorted(parameters))
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).
- parser: parser used to extract the data.
"""
try:
return _parse_module_header_verible(generic_impl_filepath, module_name)
except Exception as e:
print(e)
print("Verible parser failed, using regex fallback instead.")
return _parse_module_header_fallback(generic_impl_filepath,
module_name)
def _check_gapi(gapi):
if 'cores' not in gapi:
print("Key 'cores' not found in GAPI structure. "
"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):
if not parameters:
s = " prim_{techlib}_{prim_name} u_impl_{techlib} (\n"
else:
s = " prim_{techlib}_{prim_name} #(\n"
s += ",\n".join(" .{p}({p})".format(p=p) for p in parameters)
s += "\n ) u_impl_{techlib} (\n"
s += " .*\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 """
# Sort list of technology libraries to produce a stable ordering in the
# generated wrapper.
techlibs_wo_generic = sorted(
[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 'generic' not 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'],
num_techlibs=len(techlibs),
# 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']),
parser_info=generic_hdr['parser'])
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", Dumper=YamlDumper)
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]
with open(gapi_filepath) as f:
gapi = yaml.load(f, 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()