Merge pull request #81 from mithro/libfixes

Fix a number of issues with the generated liberty output
diff --git a/scripts/python-skywater-pdk/skywater_pdk/liberty.py b/scripts/python-skywater-pdk/skywater_pdk/liberty.py
index c2c8de7..ab32f09 100755
--- a/scripts/python-skywater-pdk/skywater_pdk/liberty.py
+++ b/scripts/python-skywater-pdk/skywater_pdk/liberty.py
@@ -343,7 +343,8 @@
     if ocorner_type != TimingType.ccsnoise:
         remove_ccsnoise_from_library(common_data, "library")
 
-    output = liberty_dict("library", lib+"__"+corner, common_data)
+    attribute_types = {}
+    output = liberty_dict("library", lib+"__"+corner, common_data, attribute_types)
     assert output[-1] == '}', output
     top_write(output[:-1])
 
@@ -360,7 +361,13 @@
             remove_ccsnoise_from_cell(cell_data, cell_with_size)
 
         top_write([''])
-        top_write(liberty_dict("cell", "%s__%s" % (lib, cell_with_size), cell_data, [cell_with_size]))
+        top_write(liberty_dict(
+            "cell",
+            "%s__%s" % (lib, cell_with_size),
+            cell_data,
+            [cell_with_size],
+            attribute_types,
+        ))
 
     top_write([''])
     top_write(['}'])
@@ -602,6 +609,135 @@
     return k in ('variable', 'index', 'values')
 
 
+def liberty_guess(o):
+    """
+
+    >>> liberty_guess('hello')  # doctest: +ELLIPSIS
+    <function liberty_str at ...>
+    >>> liberty_guess(1.0)      # doctest: +ELLIPSIS
+    <function liberty_float at ...>
+    >>> liberty_guess(1)        # doctest: +ELLIPSIS
+    <function liberty_float at ...>
+    >>> liberty_guess(None)
+    Traceback (most recent call last):
+        ...
+    ValueError: None has unguessable type: <class 'NoneType'>
+
+    """
+    if isinstance(o, str):
+        return liberty_str
+    elif isinstance(o, (float,int)):
+        return liberty_float
+    else:
+        raise ValueError("%r has unguessable type: %s" % (o, type(o)))
+
+
+def liberty_bool(b):
+    """
+
+    >>> liberty_bool(True)
+    'true'
+    >>> liberty_bool(False)
+    'false'
+    >>> liberty_bool(1.0)
+    'true'
+    >>> liberty_bool(1.5)
+    Traceback (most recent call last):
+        ...
+    ValueError: 1.5 is not a bool
+
+    >>> liberty_bool(0.0)
+    'false'
+    >>> liberty_bool(0)
+    'false'
+    >>> liberty_bool(1)
+    'true'
+    >>> liberty_bool("error")
+    Traceback (most recent call last):
+        ...
+    ValueError: 'error' is not a bool
+
+    """
+    try:
+        b2 = bool(b)
+    except ValueError:
+        b2 = None
+
+    if b2 != b:
+        raise ValueError("%r is not a bool" % b)
+
+    return {True: 'true', False: 'false'}[b]
+
+
+def liberty_str(s):
+    """
+
+    >>> liberty_str("hello")
+    '"hello"'
+
+    >>> liberty_str('he"llo')
+    Traceback (most recent call last):
+        ...
+    ValueError: '"' is not allow in the string: 'he"llo'
+
+    >>> liberty_str(1.0)
+    '"1.0000000000"'
+
+    >>> liberty_str(1)
+    '"1.0000000000"'
+
+    >>> liberty_str([])
+    Traceback (most recent call last):
+        ...
+    ValueError: [] is not a string
+
+    >>> liberty_str(True)
+    Traceback (most recent call last):
+        ...
+    ValueError: True is not a string
+
+    """
+    try:
+        if isinstance(s, (int, float)):
+            s = liberty_float(s)
+    except ValueError:
+        pass
+
+    if not isinstance(s, str):
+        raise ValueError("%r is not a string" % s)
+
+    if '"' in s:
+        raise ValueError("'\"' is not allow in the string: %r" % s)
+
+    return '"'+s+'"'
+
+
+def liberty_int(f):
+    """
+
+    >>> liberty_int(1.0)
+    1
+    >>> liberty_int(1.5)
+    Traceback (most recent call last):
+        ...
+    ValueError: 1.5 is not an int
+
+    >>> liberty_int("error")
+    Traceback (most recent call last):
+        ...
+    ValueError: 'error' is not an int
+
+    """
+    try:
+        f2 = int(f)
+    except ValueError as e:
+        f2 = None
+
+    if f2 is None or f2 != f:
+        raise ValueError("%r is not an int" % f)
+    return int(f)
+
+
 def liberty_float(f):
     """
 
@@ -617,7 +753,42 @@
     >>> liberty_float(1)
     '1.0000000000'
 
+    >>> liberty_float(True)
+    Traceback (most recent call last):
+        ...
+    ValueError: True is not a float
+
+    >>> liberty_float(False)
+    Traceback (most recent call last):
+        ...
+    ValueError: False is not a float
+
+    >>> liberty_float(0)
+    '0.0000000000'
+
+    >>> liberty_float(None)
+    Traceback (most recent call last):
+        ...
+    ValueError: None is not a float
+
+    >>> liberty_float('hello')
+    Traceback (most recent call last):
+        ...
+    ValueError: 'hello' is not a float
+
+
     """
+    try:
+        f2 = float(f)
+    except (ValueError, TypeError):
+        f2 = None
+
+    if isinstance(f, bool):
+        f2 = None
+
+    if f is None or f2 != f:
+        raise ValueError("%r is not a float" % f)
+
     WIDTH = len(str(0.0083333333))
 
     s = json.dumps(f)
@@ -639,6 +810,14 @@
     return s
 
 
+LIBERTY_ATTRIBUTE_TYPES = {
+    'boolean':  liberty_bool,
+    'string':   liberty_str,
+    'int':      liberty_int,
+    'float':    liberty_float,
+}
+
+
 INDENT="    "
 
 
@@ -738,8 +917,42 @@
     return o
 
 
-def liberty_dict(dtype, dvalue, data, indent=tuple()):
+def liberty_dict(dtype, dvalue, data, indent=tuple(), attribute_types=None):
+
+    """
+
+    >>> def g(a, b, c):
+    ...     return {"group_name": a, "attribute_name":b, "attribute_type": c}
+    >>> d = {'float': 1.0, "str": "str"}
+    >>> print('\\n'.join(liberty_dict("library", "test", d)))
+    library ("test") {
+        float : 1.0000000000;
+        str : "str";
+    }
+    >>> d['define'] = [g("cell", "float", "string")]
+    >>> print('\\n'.join(liberty_dict("library", "test", d)))
+    library ("test") {
+        define(float,cell,string);
+        float : 1.0000000000;
+        str : "str";
+    }
+    >>> d['define'] = [g("library", "float", "string")]
+    >>> print('\\n'.join(liberty_dict("library", "test", d)))
+    library ("test") {
+        define(float,library,string);
+        float : "1.0000000000";
+        str : "str";
+    }
+
+    """
+
+
     assert isinstance(data, dict), (dtype, dvalue, data)
+
+    if attribute_types is None:
+        attribute_types = {}
+    assert isinstance(attribute_types, dict), (dtype, dvalue, attribute_types)
+
     o = []
 
     if dvalue:
@@ -773,28 +986,47 @@
     di = [attr_sort_key(i) for i in data.items()]
     di.sort()
     if debug:
+        print(" "*len(str(indent)), "s1   s2     ", "%-40s" % "ktype", '%-40r' % "kvalue", "value")
+        print("-"*len(str(indent)), "---- ----   ", "-"*40, "-"*40, "-"*44)
         for sk, kt, skv, kv, k, v in di:
-            print(str(indent), "%4.0f %4.0f -- " % sk, "%-40s" % kt, '%-40r' % kv, str(v)[:40], '...')
+            print(str(indent), "%4.0f %4.0f --" % sk, "%-40s" % kt, '%-40r' % kv, end=" ")
+            sv = str(v)
+            print(sv[:40], end=" ")
+            if len(sv) > 40:
+                print('...', end=" ")
+            print()
+
 
     # Output all the attributes
+    if dtype not in attribute_types:
+        dtype_attribute_types = {}
+        attribute_types[dtype] = dtype_attribute_types
+    dtype_attribute_types = attribute_types[dtype]
+
     for _, ktype, _, kvalue, k, v in di:
         indent_n = list(indent)+[k]
 
         if ktype == 'define':
             for d in sorted(data['define'], key=lambda d: d['group_name']+'.'+d['attribute_name']):
+
+                aname = d['attribute_name']
+                gname = d['group_name']
+                atype = d['attribute_type']
+
                 o.append('%sdefine(%s,%s,%s);' % (
-                    INDENT*len(indent_n),
-                    d['attribute_name'],
-                    d['group_name'],
-                    d['attribute_type']),
-                )
+                    INDENT*len(indent_n), aname, gname, atype))
+
+                assert atype in LIBERTY_ATTRIBUTE_TYPES, (atype, d)
+                if gname not in attribute_types:
+                    attribute_types[gname] = {}
+                attribute_types[gname][aname] = LIBERTY_ATTRIBUTE_TYPES[atype]
 
         elif ktype == "comp_attribute":
             o.extend(liberty_composite(kvalue, v, indent_n))
 
         elif isinstance(v, dict):
             assert isinstance(v, dict), (dtype, dvalue, k, v)
-            o.extend(liberty_dict(ktype, kvalue, v, indent_n))
+            o.extend(liberty_dict(ktype, kvalue, v, indent_n, attribute_types))
 
         elif isinstance(v, list):
             assert len(v) > 0, (dtype, dvalue, k, v)
@@ -803,24 +1035,25 @@
                     return o.items()
 
                 for l in sorted(v, key=sk):
-                    o.extend(liberty_dict(ktype, kvalue, l, indent_n))
+                    o.extend(liberty_dict(ktype, kvalue, l, indent_n, attribute_types))
 
             elif is_liberty_list(ktype):
                 o.extend(liberty_list(ktype, v, indent_n))
 
             elif "clk_width" == ktype:
                 for l in sorted(v):
-                    o.append("%s%s : %s;" % (INDENT*len(indent_n), k, l))
+                    o.append('%s%s : "%s";' % (INDENT*len(indent_n), k, l))
 
             else:
                 raise ValueError("Unknown %s: %r\n%s" % (k, v, indent_n))
 
         else:
-            if isinstance(v, str):
-                v = '"%s"' % v
-            elif isinstance(v, (float,int)):
-                v = liberty_float(v)
-            o.append("%s%s : %s;" % (INDENT*len(indent_n), k, v))
+            if ktype in dtype_attribute_types:
+                liberty_out = dtype_attribute_types[ktype]
+            else:
+                liberty_out = liberty_guess(v)
+            ov = liberty_out(v)
+            o.append("%s%s : %s;" % (INDENT*len(indent_n), k, ov))
 
     o.append("%s}" % (INDENT*len(indent)))
     return o