[otbn] Add OTBN encoding information to insns.yml

This patch defines a way to write "encoding schemes" in the YAML. This
is slightly inspired by the LLVM approach [1] but massively
simplified. I initially had something even simpler, without allowing
any hierarchy in the encoding schemes, but it was quite ugly to fill
in the instruction encodings and I was worried that it would make it
very difficult to change the encodings if we needed to later.

Instructions now have encodings defined, and we have code in the
Python parser that resolves the (hierarchical) scheme used by an
instruction into a flat one, then matches up the named operands with
the fields in the encoding scheme.

We finally check that the encoding is not ambiguous: that no bit
pattern matches more than one instruction.

Once this is all done, the documentation generator now spits out an
encoding table next to each instruction.

Final encoding work by Stefan.

[1] See e.g. llvm/lib/Target/RISCV/RISCVInstrFormatsC.td in the LLVM
    source tree.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
Co-authored-by: Stefan Wallentowitz <stefan.wallentowitz@gi-de.com>
diff --git a/hw/ip/otbn/util/yaml_to_doc.py b/hw/ip/otbn/util/yaml_to_doc.py
index 3fe5898..b112025 100755
--- a/hw/ip/otbn/util/yaml_to_doc.py
+++ b/hw/ip/otbn/util/yaml_to_doc.py
@@ -8,7 +8,8 @@
 import argparse
 import sys
 
-from insn_yaml import Insn, InsnsFile, Operand, load_file
+from insn_yaml import (BoolLiteral, Encoding, Insn, InsnsFile, Operand,
+                       load_file)
 
 
 def render_operand_row(operand: Operand) -> str:
@@ -57,6 +58,82 @@
     return ''.join(parts)
 
 
+def render_encoding(mnemonic: str, encoding: Encoding) -> str:
+    '''Generate a table displaying an instruction encoding'''
+    parts = []
+    parts.append('<table style="font-size: 75%">')
+    parts.append('<tr>')
+    parts.append('<td></td>')
+    for bit in range(31, -1, -1):
+        parts.append('<td>{}</td>'.format(bit))
+    parts.append('</tr>')
+
+    # Build dictionary of bit ranges, keyed by the msb and with value a pair
+    # (width, desc) where width is the width of the range in bits and desc is a
+    # string describing what is stored in the range.
+    by_msb = {}
+
+    for field_name, field in encoding.fields.items():
+        scheme_field = field.scheme_field
+        # If this field is a literal value, explode it into single bits. To do
+        # so, we walk the ranges and match up with ranges in the value.
+        if isinstance(field.value, BoolLiteral):
+            assert field.value.width > 0
+            assert field.value.width == scheme_field.bits.width
+            bits_seen = 0
+            for msb, lsb in scheme_field.bits.ranges:
+                val_msb = scheme_field.bits.width - 1 - bits_seen
+                val_lsb = val_msb - msb + lsb
+                bits_seen += msb - lsb + 1
+
+                for idx in range(0, msb - lsb + 1):
+                    desc = field.value.char_for_bit(val_lsb + idx)
+                    by_msb[lsb + idx] = (1, '' if desc == 'x' else desc)
+            continue
+
+        # Otherwise this field's value is an operand name
+        assert isinstance(field.value, str)
+        operand_name = field.value
+
+        # If there is only one range (and no shifting), that's easy.
+        if len(scheme_field.bits.ranges) == 1 and scheme_field.shift == 0:
+            msb, lsb = scheme_field.bits.ranges[0]
+            by_msb[msb] = (msb - lsb + 1, operand_name)
+            continue
+
+        # Otherwise, we have to split up the operand into things like "foo[8:5]"
+        bits_seen = 0
+        for msb, lsb in scheme_field.bits.ranges:
+            val_msb = scheme_field.shift + scheme_field.bits.width - 1 - bits_seen
+            val_lsb = val_msb - msb + lsb
+            bits_seen += msb - lsb + 1
+            if msb == lsb:
+                desc = '{}[{}]'.format(operand_name, val_msb)
+            else:
+                desc = '{}[{}:{}]'.format(operand_name, val_msb, val_lsb)
+            by_msb[msb] = (msb - lsb + 1, desc)
+
+    parts.append('<tr>')
+    parts.append('<td>{}</td>'.format(mnemonic.upper()))
+
+    # Now run down the ranges in descending order of msb to get the table cells
+    next_bit = 31
+    for msb in sorted(by_msb.keys(), reverse=True):
+        # Sanity check to make sure we have a dense table
+        assert msb == next_bit
+
+        width, desc = by_msb[msb]
+        next_bit = msb - width
+
+        parts.append('<td colspan="{}">{}</td>'.format(width, desc))
+
+    assert next_bit == -1
+    parts.append('</tr>')
+
+    parts.append('</table>\n\n')
+    return ''.join(parts)
+
+
 def render_insn(insn: Insn, heading_level: int) -> str:
     '''Generate the documentation for an instruction
 
@@ -115,6 +192,10 @@
     if any(op.doc is not None for op in insn.operands):
         parts.append(render_operand_table(insn))
 
+    # Show encoding if we have one
+    if insn.encoding is not None:
+        parts.append(render_encoding(insn.mnemonic, insn.encoding))
+
     # Show decode pseudo-code if given
     if insn.decode is not None:
         parts.append('{} Decode\n\n'