regtool: add support to generate reg const as Rust (.rs)

Add -R and --rust option in regtool.py to generate Rust file.

Add regs-rust target in hw/Makefile to generate reg constant Rust
file for ALL ips. If a TOCK_ROOT variable is specified, e.g.
"TOCK_ROOT=~/ti50/third_party/tock/tock make -C hw regs-rust",
the Rust files will also be copied to
$(TOCK_ROOT)/chips/lowrisc/src/reg_constants folder.

BUG=none
TEST=regtool.py -R -o uart_regs.rs uart.hjson
     make -C hw regs-rust
     TOCK_ROOT=~/ti50/third_party/tock/tock make -C hw regs-rust

Signed-off-by: Chia-Chi Teng <ccteng@google.com>

Change-Id: I5f668d51e1f42e6541a41f3e565848230d6c07b6
diff --git a/util/reggen/gen_rust.py b/util/reggen/gen_rust.py
new file mode 100644
index 0000000..09d2f7f
--- /dev/null
+++ b/util/reggen/gen_rust.py
@@ -0,0 +1,364 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Generate Rust constants from validated register JSON tree
+"""
+
+import io
+import logging as log
+import sys
+import textwrap
+import warnings
+from typing import Optional, Set, TextIO
+
+
+from .field import Field
+from .ip_block import IpBlock
+from .params import LocalParam
+from .register import Register
+from .multi_register import MultiRegister
+from .signal import Signal
+from .window import Window
+
+
+def genout(outfile: TextIO, msg: str) -> None:
+    outfile.write(msg)
+
+
+def to_snake_case(s: str) -> str:
+    val = []
+    for i, ch in enumerate(s):
+        if i > 0 and ch.isupper():
+            val.append('_')
+        val.append(ch)
+    return ''.join(val)
+
+
+def as_define(s: str) -> str:
+    s = s.upper()
+    r = ''
+    for i in range(0, len(s)):
+        r += s[i] if s[i].isalnum() else '_'
+    return r
+
+
+def first_line(s: str) -> str:
+    """Returns the first line of a multi-line string"""
+    return s.splitlines()[0]
+
+
+def format_comment(s: str) -> str:
+    """Formats a string to comment wrapped to an 80 character line width
+
+    Returns wrapped string including newline and // comment characters.
+    """
+    comment = textwrap.wrap(s,
+                            width=77,
+                            initial_indent='// ',
+                            subsequent_indent='// ')
+    return '\n'.join(comment) + '\n'
+
+
+def data_type(name: str, val: int, as_hex: bool) -> str:
+    """ Returns proper data type for name-value pair. """
+
+    if name.endswith("_OFFSET") or name.endswith("_BASE_ADDR"):
+        return "usize"
+
+    if val.bit_length() > 32:
+        log.error(name + " value exceeds 32 bit " + str(val))
+        sys.exit(1)
+
+    if not as_hex and val < 0:
+        return "i32"
+
+    return "u32"
+
+
+def gen_const(outstr: TextIO,
+              name: str,
+              suffix: str,
+              val: int,
+              existing_defines: Set[str],
+              as_hex: bool = False) -> str:
+    r"""Produces a pub const string. Result includes newline.
+
+    Arguments:
+    name - Name of the constant
+    val - Value of the constant
+    existing_defines - set of already generated define names.
+        Error if `name` is in `existing_defines`.
+
+    Example result:
+    name = 'A_NAME'
+    val = '10'
+
+    pub const A_NAME: u32 = 10
+
+    """
+
+    suffix = '' if not suffix.strip() else '_' + suffix
+    name = name + suffix
+    if name in existing_defines:
+        log.error("Duplicate pub const for " + name)
+        sys.exit(1)
+
+    define_declare = 'pub const ' + name + ': ' + data_type(name, val, as_hex)
+
+    val_str = hex(val) if as_hex else str(val)
+    oneline_define = define_declare + ' = ' + val_str + ';'
+
+    existing_defines.add(name)
+
+    output = oneline_define + '\n'
+    genout(outstr, output)
+    return output
+
+
+def gen_const_register(outstr: TextIO,
+                       reg: Register,
+                       comp: str,
+                       width: int,
+                       rnames: Set[str],
+                       existing_defines: Set[str]) -> None:
+    rname = reg.name
+    offset = reg.offset
+
+    genout(outstr, format_comment(first_line(reg.desc)))
+    defname = as_define(comp + '_' + rname)
+    gen_const(outstr, defname, 'REG_OFFSET', offset, existing_defines, True)
+
+    for field in reg.fields:
+        dname = defname + '_' + as_define(field.name)
+        field_width = field.bits.width()
+
+        if field_width == 1:
+            # single bit
+            gen_const(outstr, dname, 'BIT', field.bits.lsb, existing_defines)
+        else:
+            # multiple bits (unless it is the whole register)
+            if field_width != width:
+                mask = field.bits.bitmask() >> field.bits.lsb
+                gen_const(outstr, dname, 'MASK', mask, existing_defines, True)
+                gen_const(outstr, dname, 'OFFSET', field.bits.lsb, existing_defines)
+
+            if field.enum is not None:
+                for enum in field.enum:
+                    ename = as_define(enum.name)
+                    gen_const(
+                        outstr,
+                        defname + '_' + as_define(field.name),
+                        'VALUE_' + ename,
+                        enum.value,
+                        existing_defines,
+                        True)
+
+    genout(outstr, '\n')
+    return
+
+
+def gen_const_window(outstr: TextIO,
+                     win: Window,
+                     comp: str,
+                     regwidth: int,
+                     rnames: Set[str],
+                     existing_defines: Set[str]) -> None:
+    offset = win.offset
+
+    genout(outstr, format_comment('Memory area: ' + first_line(win.desc)))
+    defname = as_define(comp + '_' + win.name)
+    gen_const(outstr, defname, 'REG_OFFSET', offset, existing_defines, True)
+    items = win.items
+    gen_const(outstr, defname, 'SIZE_WORDS', items, existing_defines)
+    items = items * (regwidth // 8)
+    gen_const(outstr, defname, 'SIZE_BYTES', items, existing_defines)
+
+    wid = win.validbits
+    if (wid != regwidth):
+        mask = (1 << wid) - 1
+        gen_const(outstr, defname, 'MASK', mask, existing_defines, True)
+
+
+def gen_rust_module_param(outstr: TextIO,
+                          param: LocalParam,
+                          module_name: str,
+                          existing_defines: Set[str]) -> None:
+    # Presently there is only one type (int), however if the new types are
+    # added, they potentially need to be handled differently.
+    known_types = ["int"]
+    if param.param_type not in known_types:
+        warnings.warn("Cannot generate a module define of type {}"
+                      .format(param.param_type))
+        return
+
+    if param.desc is not None:
+        genout(outstr, format_comment(first_line(param.desc)))
+    # Heuristic: if the name already has underscores, it's already snake_case,
+    # otherwise, assume StudlyCaps and covert it to snake_case.
+    param_name = param.name if '_' in param.name else to_snake_case(param.name)
+    define_name = as_define(module_name + '_PARAM_' + param_name)
+    if param.param_type == "int":
+        gen_const(outstr, define_name, '', int(param.value), existing_defines)
+
+    genout(outstr, '\n')
+
+
+def gen_const_module_params(outstr: TextIO,
+                            module_data: IpBlock,
+                            module_name: str,
+                            register_width: int,
+                            existing_defines: Set[str]) -> None:
+    for param in module_data.params.get_localparams():
+        gen_rust_module_param(outstr, param, module_name, existing_defines)
+
+    genout(outstr, format_comment(first_line("Register width")))
+    define_name = as_define(module_name + '_PARAM_REG_WIDTH')
+    gen_const(outstr, define_name, '', register_width, existing_defines)
+    genout(outstr, '\n')
+
+
+def gen_multireg_field_defines(outstr: TextIO,
+                               regname: str,
+                               field: Field,
+                               subreg_num: int,
+                               regwidth: int,
+                               existing_defines: Set[str]) -> None:
+    field_width = field.bits.width()
+    fields_per_reg = regwidth // field_width
+
+    suffix = as_define(field.name + "_FIELD_WIDTH")
+    gen_const(outstr, regname, suffix, field_width, existing_defines)
+
+    suffix = as_define(field.name + "_FIELDS_PER_REG")
+    gen_const(outstr, regname, suffix, fields_per_reg, existing_defines)
+
+    gen_const(outstr, regname, "MULTIREG_COUNT", subreg_num, existing_defines)
+
+    genout(outstr, '\n')
+
+
+def gen_const_multireg(outstr: TextIO,
+                       multireg: MultiRegister,
+                       component: str,
+                       regwidth: int,
+                       rnames: Set[str],
+                       existing_defines: Set[str]) -> None:
+    comment = multireg.reg.desc + " (common parameters)"
+    genout(outstr, format_comment(first_line(comment)))
+    if len(multireg.reg.fields) == 1:
+        regname = as_define(component + '_' + multireg.reg.name)
+        gen_multireg_field_defines(outstr, regname, multireg.reg.fields[0],
+                                   len(multireg.regs), regwidth, existing_defines)
+    else:
+        log.warn("Non-homogeneous multireg " + multireg.reg.name +
+                 " skip multireg specific data generation.")
+
+    for subreg in multireg.regs:
+        gen_const_register(outstr, subreg, component, regwidth, rnames,
+                           existing_defines)
+
+
+def gen_interrupt_field(outstr: TextIO,
+                        interrupt: Signal,
+                        component: str,
+                        regwidth: int,
+                        existing_defines: Set[str]) -> None:
+    fieldlsb = interrupt.bits.lsb
+    iname = interrupt.name
+    defname = as_define(component + '_INTR_COMMON_' + iname)
+
+    if interrupt.bits.width() == 1:
+        # single bit
+        gen_const(outstr, defname, 'BIT', fieldlsb, existing_defines)
+    else:
+        # multiple bits (unless it is the whole register)
+        if interrupt.bits.width() != regwidth:
+            mask = interrupt.bits.msb >> fieldlsb
+            gen_const(outstr, defname, 'MASK', mask, existing_defines, True)
+            gen_const(outstr, defname, 'OFFSET', fieldlsb, existing_defines)
+
+
+def gen_const_interrupts(outstr: TextIO,
+                         block: IpBlock,
+                         component: str,
+                         regwidth: int,
+                         existing_defines: Set[str]) -> None:
+    # If no_auto_intr is true, then we do not generate common defines,
+    # because the bit offsets for a particular interrupt may differ between
+    # the interrupt enable/state/test registers.
+    if block.no_auto_intr:
+        return
+
+    genout(outstr, format_comment(first_line("Common Interrupt Offsets")))
+    for intr in block.interrupts:
+        gen_interrupt_field(outstr, intr, component, regwidth, existing_defines)
+    genout(outstr, '\n')
+
+
+def gen_rust(block: IpBlock,
+             outfile: TextIO,
+             src_lic: Optional[str],
+             src_copy: str) -> int:
+    rnames = block.get_rnames()
+
+    outstr = io.StringIO()
+
+    # This tracks the defines that have been generated so far, so we
+    # can error if we attempt to duplicate a definition
+    existing_defines = set()  # type: Set[str]
+
+    gen_const_module_params(outstr, block, block.name, block.regwidth,
+                            existing_defines)
+
+    gen_const_interrupts(outstr, block, block.name, block.regwidth,
+                         existing_defines)
+
+    for rb in block.reg_blocks.values():
+        for x in rb.entries:
+            if isinstance(x, Register):
+                gen_const_register(outstr, x, block.name, block.regwidth, rnames,
+                                   existing_defines)
+                continue
+
+            if isinstance(x, MultiRegister):
+                gen_const_multireg(outstr, x, block.name, block.regwidth, rnames,
+                                   existing_defines)
+                continue
+
+            if isinstance(x, Window):
+                gen_const_window(outstr, x, block.name, block.regwidth,
+                                 rnames, existing_defines)
+                continue
+
+    generated = outstr.getvalue()
+    outstr.close()
+
+    genout(outfile, '// Generated register constants for ' + block.name + '\n\n')
+    if src_copy != '':
+        genout(outfile, '// Copyright information found in source file:\n')
+        genout(outfile, '// ' + src_copy + '\n\n')
+    if src_lic is not None:
+        genout(outfile, '// Licensing information found in source file:\n')
+        for line in src_lic.splitlines():
+            genout(outfile, '// ' + line + '\n')
+        genout(outfile, '\n')
+
+    genout(outfile, generated)
+
+    genout(outfile, '// End generated register constants for ' + block.name)
+
+    return 0
+
+
+def test_gen_const() -> None:
+    outstr = io.StringIO()
+
+    basic_oneline = 'pub const MACRO_NAME 10;\n'
+    assert (gen_const(outstr, 'MACRO', 'NAME', 10, set()) == basic_oneline)
+
+    long_macro_name = 'A_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_LONG_MACRO_NAME'
+    multiline = ('pub const ' + long_macro_name + ' \\\n' +
+                 '  1000000000;\n')
+
+    assert (gen_const(outstr, long_macro_name, '', 1000000000, set()) == multiline)
diff --git a/util/regtool.py b/util/regtool.py
index aa264c5..8b7ee7f 100755
--- a/util/regtool.py
+++ b/util/regtool.py
@@ -12,7 +12,7 @@
 from pathlib import PurePath
 
 from reggen import (gen_cheader, gen_dv, gen_fpv, gen_html,
-                    gen_json, gen_rtl, gen_selfdoc, version)
+                    gen_json, gen_rtl, gen_rust, gen_selfdoc, version)
 from reggen.ip_block import IpBlock
 
 DESC = """regtool, generate register info from Hjson source"""
@@ -46,6 +46,10 @@
                         '-D',
                         action='store_true',
                         help='Output C defines header')
+    parser.add_argument('--rust',
+                        '-R',
+                        action='store_true',
+                        help='Output Rust constants')
     parser.add_argument('--doc',
                         action='store_true',
                         help='Output source file documentation (gfm)')
@@ -117,7 +121,8 @@
     arg_to_format = [('j', ('json', None)), ('c', ('compact', None)),
                      ('d', ('html', None)), ('doc', ('doc', None)),
                      ('r', ('rtl', 'rtl')), ('s', ('dv', 'dv')),
-                     ('f', ('fpv', 'fpv/vip')), ('cdefines', ('cdh', None))]
+                     ('f', ('fpv', 'fpv/vip')), ('cdefines', ('cdh', None)),
+                     ('rust', ('rs', None))]
     format = None
     dirspec = None
     for arg_name, spec in arg_to_format:
@@ -225,6 +230,8 @@
                 return gen_html.gen_html(obj, outfile)
             elif format == 'cdh':
                 return gen_cheader.gen_cdefines(obj, outfile, src_lic, src_copy)
+            elif format == 'rs':
+                return gen_rust.gen_rust(obj, outfile, src_lic, src_copy)
             else:
                 return gen_json.gen_json(obj, outfile, format)