VCD to wavedrom JSON/SVG converter

Signed-off-by: Wojciech Gryncewicz <wgryncewicz@antmicro.com>
diff --git a/requirements.txt b/requirements.txt
index fe8a2c1..41b39cc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 flake8
+wavedrom
 
 # rst_include tool as GitHub doesn't support `.. include::` when rendering
 # previews.
diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py
new file mode 100755
index 0000000..69f3ef4
--- /dev/null
+++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/vcd2wavedrom.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 The SkyWater PDK Authors.
+#
+# Use of this source code is governed by the Apache 2.0
+# license that can be found in the LICENSE file or at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+
+''' VCD waveform to wawedrom script/SVG conversion script.
+'''
+
+from __future__ import print_function
+import os
+import sys
+import argparse
+import pathlib
+import wavedrom
+from contextlib import contextmanager
+
+
+wavedrom_template ="""\
+{{ signal: [
+{signals}
+]}}"""
+
+signal_template = "   {{ name: \"{name}\", {fill}wave: '{wave}' }}"
+
+def eprint(*args, **kwargs):
+    ''' Print to stderr '''
+    print(*args, file=sys.stderr, **kwargs)
+
+@contextmanager
+def file_or_stdout(file):
+    ''' Open file or stdout if file is None
+    '''
+    if file is None:
+        yield sys.stdout
+    else:
+        with file.open('w') as out_file:
+            yield out_file
+
+
+def readVCD (file):
+    ''' Parses VCD file. 
+
+    Args:
+        file - path to a VCD file [pathlib.Path]
+
+    Returns:
+        vcd  - dictionary containing vcd sections [dict]
+    '''
+    eprint()
+    eprint(file.name)
+    assert file.is_file(), file
+
+    vcd = {}
+    with file.open('r') as f:
+        currtag = 'body'
+        for line in f:
+            # regular line
+            if not line.startswith('$'):
+                vcd[currtag] = vcd.setdefault(currtag, '') + line
+                continue
+            # tag, other than end
+            if not line.startswith('$end'):
+                currtag = line.partition(' ')[0].lstrip('$').rstrip()
+                vcd[currtag] = vcd.setdefault(currtag, '') + line.partition(' ')[2].rpartition('$')[0]               
+            # line ends with end tag
+                if not vcd[currtag].endswith('\n'):
+                     vcd[currtag] += '\n'                          
+            if line.split()[-1]=='$end':
+                currtag = 'body'    
+                vcd[currtag] = ''
+
+    if 'var' not in vcd or 'dumpvars' not in vcd:
+      raise SyntaxError("Invalid VCD file format")
+
+    return vcd
+
+def parsetowavedrom (file, savetofile = False):
+    ''' Reads and simplifies VCD waveform
+        Generates wavedrom notation.
+
+    Args:
+        file - path to a VCD file [pathlib.Path]
+    
+    '''
+    varsubst = {} # var substitution
+    reg    = []   # list of signals
+    wire   = []   # list of signals (wire class)
+    wave   = {}   # waveform
+    event  = []   # event timings
+
+    vcd = readVCD (file)
+
+    # parse vars
+    for line in vcd['var'].split('\n'):
+        line = line.strip().split()
+        if len(line)<4:
+            if len(line):
+                print (f"Warning: malformed var definition {' '.join(line)}")
+            continue
+        if line[1]!='1':
+            print (f"Warning: bus in vars (unsupported)  {' '.join(line)}")
+        if line[0]=='reg':
+            reg.append(line[3])
+            varsubst[line[2]] = line[3]
+        if line[0]=='wire':
+            wire.append(line[3])
+            varsubst[line[2]] = line[3]
+
+    # set initial states
+    event.append(0)
+    #default 
+    for v in reg+wire:
+        wave[v] = ['x']
+    #defined
+    for line in vcd['dumpvars'].split('\n'): 
+        if len(line)>=2:
+            wave[ varsubst[line[1]] ] = [line[0]]
+
+    # parse wave body        
+    for line in vcd['body'].split('\n'):
+        #timestamp line
+        if line.startswith('#'):
+            line = line.strip().lstrip('#')
+            if not line.isnumeric():
+                raise SyntaxError("Invalid VCD timestamp")      
+            event.append(int(line))
+            for v in wave.keys():
+                wave[v].append('.')
+        # state change line
+        else :
+            if len(line)>=2:
+                wave [ varsubst[line[1]] ][-1] = line[0]
+            
+    # TODO: add "double interval support"
+
+    signals  = []
+    for v in wave.keys():     
+        fill    = ' ' * (max( [len(s) for s in wave.keys()] ) - len(v))
+        wavestr = ''.join(wave[v])
+        signals.append( signal_template.format( name = v, wave = wavestr, fill = fill ) )
+    signals = ',\n'.join(signals)
+
+    wavedrom = wavedrom_template.format ( signals = signals )
+
+    outfile = file.with_suffix(".wdr.json") if savetofile else None       
+    with file_or_stdout(outfile) as f:
+        f.write(wavedrom)
+    
+    return wavedrom
+
+def quoted_strings_wavedrom (wdr) :
+    ''' Convert wavedrom script to more restrictive
+        version of JSON with quoted keywords
+    '''
+    wdr = wdr.replace(' signal:',' "signal":')
+    wdr = wdr.replace(' name:',' "name":')
+    wdr = wdr.replace(' wave:',' "wave":')
+    wdr = wdr.replace("'",'"')
+    return wdr
+
+def main():
+    ''' Converts VCD waveform to wavedrom format'''
+    output_txt = 'output:\n  stdout or [vcdname].wdr.json file and/or [vcdname].svg file'
+    allcellpath = '../../../libraries/*/latest/cells/*/*.vcd'
+
+    parser = argparse.ArgumentParser(
+            description = main.__doc__,
+            epilog = output_txt,
+            formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument(
+            "--all_libs",
+            help="process all in "+allcellpath,
+            action="store_true")
+    parser.add_argument(
+            "-w",
+            "--wavedrom",
+            help="generate wavedrom .wdr.json file",
+            action="store_true")
+    parser.add_argument(
+            "-s",
+            "--savesvg",
+            help="generate .svg image",
+            action="store_true")
+    parser.add_argument(
+            "infile",
+            help="VCD waveform file",
+            type=pathlib.Path,
+            nargs="*")
+
+    args = parser.parse_args()
+
+    if args.all_libs:
+        path = pathlib.Path(allcellpath).expanduser()
+        parts = path.parts[1:] if path.is_absolute() else path.parts
+        paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts)))
+        args.infile = list(paths)
+
+    infile = [d.resolve() for d in args.infile if d.is_file()]
+
+    errors = 0
+    for f in infile:
+        try:
+            wdr = parsetowavedrom(f, args.wavedrom)
+            if args.savesvg:
+                svg = wavedrom.render( quoted_strings_wavedrom(wdr) )
+                outfile = f.with_suffix(".svg")  
+                svg.saveas(outfile)
+        except KeyboardInterrupt:
+            sys.exit(1)
+        except (AssertionError, FileNotFoundError, ChildProcessError) as ex:
+            eprint (f'Error: {type(ex).__name__}')
+            eprint (f'{ex.args}')
+            errors +=1
+    eprint (f'\n{len(infile)} files processed, {errors} errors.')
+    return 0 if errors else 1
+
+if __name__ == "__main__":
+    sys.exit(main())
+