| #!/usr/bin/env python3 |
| |
| import argparse |
| import copy |
| import json |
| import pprint |
| import re |
| import sys |
| |
| from pathlib import Path |
| |
| debug = True |
| |
| |
| def get_lib_properties(fname): |
| data = json.load(open(str(fname), "r")) |
| assert len(data) == 1, list(data.keys()) |
| |
| libstr = list(data.keys())[0] |
| assert libstr.startswith('library,'), libstr |
| data = data[libstr] |
| |
| for c in list(data): |
| if c.startswith('cell,'): |
| del data[c] |
| if c.startswith('lu_table_template,') or c.startswith('power_lut_template,'): |
| del data[c] |
| if c.startswith('normalized_driver_waveform,'): |
| del data[c] |
| return libstr, data |
| |
| |
| |
| def filter_lib_properties(data): |
| for c in list(data): |
| if c.startswith('cell,'): |
| del data[c] |
| if c.startswith('lu_table_template,') or c.startswith('power_lut_template,'): |
| del data[c] |
| if c.startswith('normalized_driver_waveform,'): |
| del data[c] |
| |
| |
| RE_FLOAT = re.compile("^-?[0-9]+(\.[0-9]+)?([Ee][+-]?[0-9]+)?$") |
| RE_INT = re.compile("^-?[0-9]+$") |
| |
| |
| def rewrite_broken_quotes(s): |
| """ |
| >>> rewrite_broken_quotes('1, "pf"') |
| '"1","pf"' |
| |
| >>> rewrite_broken_quotes('"CLK GATE","M0"') |
| '"CLK GATE","M0"' |
| |
| >>> rewrite_broken_quotes('"CLK GATE,M0"') |
| '"CLK GATE","M0"' |
| |
| >>> rewrite_broken_quotes('CLK GATE",M0') |
| '"CLK GATE","M0"' |
| |
| >>> rewrite_broken_quotes('XXX') |
| '"XXX"' |
| |
| >>> rewrite_broken_quotes('statetable "CLK GATE SCE') |
| '"statetable","CLK GATE SCE"' |
| |
| """ |
| o = [] |
| |
| s = re.sub('([^,]) "', '\\1,"', s) |
| |
| for b in s.split(","): |
| b = b.strip() |
| if b and b[0] == '"': |
| b = b[1:] |
| if b and b[-1] == '"': |
| b = b[:-1] |
| |
| assert '"' not in b, (b, s) |
| |
| o.append('"%s"' % b) |
| return ",".join(o) |
| |
| |
| def fixup(s): |
| """ |
| >>> fixup('1, "pf"') |
| (1, 'pf') |
| |
| >>> fixup('"vpwr",1.760000') |
| ('vpwr', 1.76) |
| |
| >>> fixup('"VSS",0.000000') |
| ('VSS', 0.0) |
| |
| >>> fixup('Random') |
| 'Random' |
| |
| >>> fixup(0.0) |
| 0.0 |
| |
| >>> fixup('123') |
| 123 |
| |
| >>> fixup('123e6') |
| 123000000.0 |
| |
| >>> fixup('123E-10') |
| 1.23e-08 |
| |
| >>> fixup('ABC, 123H') |
| ('ABC', '123H') |
| |
| >>> fixup([{'v': "7.0", 'w': ""},]) |
| [{'v': 7.0, 'w': ''}] |
| |
| >>> fixup('CLK GATE","M0') |
| ('CLK GATE', 'M0') |
| |
| >>> fixup('CLK GATE", "M0') |
| ('CLK GATE', 'M0') |
| |
| >>> fixup('CLK GATE",M0') |
| ('CLK GATE', 'M0') |
| |
| >>> fixup(" hello") |
| 'hello' |
| |
| >>> fixup("statetable \\"CLK GATE SCE\\",m0") |
| ('statetable', 'CLK GATE SCE', 'm0') |
| |
| """ |
| |
| if isinstance(s, dict): |
| o = {} |
| for k, v in s.items(): |
| k = fixup(k) |
| v = fixup(v) |
| o[k] = v |
| return o |
| |
| if isinstance(s, list): |
| o = [] |
| for i in range(0, len(s)): |
| o.append( fixup(s[i]) ) |
| return o |
| |
| if isinstance(s, str): |
| |
| if ',' in s: |
| s = rewrite_broken_quotes(s) |
| |
| bits = s.split(',') |
| conv = False |
| for i in range(0, len(bits)): |
| v = bits[i].strip() |
| |
| vo = fixup(v) |
| |
| if not conv and vo != v: |
| conv = True |
| |
| bits[i] = vo |
| |
| if conv: |
| return tuple(bits) |
| |
| if s and s[0] == '"': |
| assert s[-1] == '"', s |
| s = s[1:-1] |
| |
| if RE_INT.match(s): |
| return int(s) |
| if RE_FLOAT.match(s): |
| return float(s) |
| |
| s = s.strip() |
| |
| return s |
| |
| |
| |
| class LibertyToJSONParser(): |
| |
| @classmethod |
| def join_duplicate_keys(cls, ordered_pairs) -> dict: |
| '''Converts multiple key-value entries in input sequence to one entry. |
| |
| The function takes all key duplicates, and form an entry with single |
| key and list of values. |
| |
| Parameters |
| ---------- |
| ordered_pairs: list |
| list of pairs key-value |
| |
| Returns |
| ------- |
| dict: dictionary, where values with same keys are grouped into list |
| ''' |
| d = {} |
| for k, v in ordered_pairs: |
| if k in d: |
| if type(d[k]) == list: |
| d[k].append(v) |
| else: |
| if d[k] != v: |
| d[k] = [d[k], v] |
| else: |
| d[k] = v |
| return d |
| |
| @classmethod |
| def load_timing_info_from_lib(cls, libfile: list) -> (dict): |
| '''Reads the LIB file and converts it to dictionary structure. |
| |
| Parameters |
| ---------- |
| libfile: list |
| The lines for input LIB file |
| |
| Returns |
| ------- |
| dict: a dictionary containing the whole structure of the file |
| ''' |
| |
| # regex for indent |
| inddef = r'^(?P<indent>\s*)' |
| |
| # regex for variables |
| vardef = r'([A-Za-z_][a-zA-Z_0-9\-]*)' |
| |
| # regex for floating-point numbers |
| numdef = r'[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?' |
| |
| # regex for all allowed characters in struct definition name |
| alloweddef = r'[^\n\"{{]+' |
| |
| # regex for arrays |
| arrdef = r'(\s*\"\s*(?P<arrvalues>({numdef}(,\s*)?)+)\s*\"\s*,?)'.format(numdef=numdef) # noqa: E501 |
| |
| # REGEX defining the dictionary name in LIB file, i.e. "pin ( QAI )", |
| # "pin(FBIO[22])" or "timing()" |
| structdecl = re.compile(r'{inddef}(?P<type>{vardef})\s*\(\s*(\"(?P<nameq>[^\n{{]+?)?\"|(?P<name>[^\n{{]+?)?)\s*\)'.format(vardef=vardef, inddef=inddef, alloweddef=alloweddef)) # noqa: E501 |
| |
| # REGEX defining global "attribute (entry);" statements |
| attdecl = re.compile(r'{inddef}(?P<attrname>{vardef})\s*\(\s*(\"(?P<attrvalq>[^\n\(\)]+?)\"|(?P<attrval>[^\n\(\)]+?))\s*\)\s*,$'.format(vardef=vardef, inddef=inddef)) # noqa: E501 |
| |
| # REGEX defining array for lu_table_template template breakpoints |
| arrdecl = re.compile(r'{inddef}(?P<arrname>{vardef})\s*\((?P<array>{arrdef}+)\)'.format(vardef=vardef, inddef=inddef, arrdef=arrdef)) # noqa: E501 |
| |
| # REGEX defining arrays |
| subarrdecl = re.compile(arrdef) |
| |
| singlearr = re.compile(r'{inddef}\"(?P<arrname>{vardef})\"\s*:{arrdef}'.format(vardef=vardef, inddef=inddef, arrdef=arrdef)) # noqa: E501 |
| |
| # REGEX defining Liberty `define` statements |
| defdecl = re.compile(r'{inddef}define\s*\(\s*(?P<attribute_name>{vardef})\s*,\s*(?P<group_name>{vardef})\s*,\s*(?P<attribute_type>{vardef})\s*\)\s*,'.format(vardef=vardef, inddef=inddef)) # noqa: E501 |
| |
| # REGEX defining lines with no ending colon |
| nocommadecl = re.compile(r'(?P<content>{inddef}{vardef}\s*:\s*(\"[^\n\"\(\)]+\"|[^\n\s\"\(\),]+))\s*$'.format(inddef=inddef, vardef=vardef)) # noqa: E501 |
| |
| # REGEX defining typical variable name, which is any variable starting |
| # with alphabetic character, followed by [A-Za-z_0-9] characters, and |
| # not within quotes |
| unwrappeddecl = re.compile(r'{inddef}(\"(?P<varnameq>{vardef})\"|(?P<varname>{vardef}))\s*:\s*(\"(?P<varvalueq>[^\n\"{{]*)\"|(?P<varvalue>[^\n\"{{]*))\s*,$'.format(inddef=inddef, vardef=vardef)) # noqa: E501 |
| # vardecl = re.compile(r'(?P<variable>(?<!\"){vardef}(\[[0-9]+\])?(?![^\:]*\"))'.format(vardef=vardef)) # noqa: E501 |
| |
| # join all lines into single string |
| fullfile = '\n'.join(libfile) |
| # remove comments (C/C++ style) |
| #fullfile = re.sub(r'(?:\/\*(.*?)\*\/)|(?:\/\/(.*?)\n)', '', |
| # fullfile, flags=re.DOTALL) |
| |
| fullfile = re.sub(r'(?:\/\*(.*?)\*\/)', '', |
| fullfile, flags=re.DOTALL) |
| |
| fullfile = re.sub(r'(?:\/\/(.*?)\n)', '\n', |
| fullfile, flags=re.DOTALL) |
| |
| # remove comments (Python style) |
| fullfile = re.sub(r'#[^\n]*\n', '', fullfile, flags=re.DOTALL) |
| |
| # remove line breaks |
| fullfile = re.sub(r'\\[\s\n\r\t]*', '', fullfile, flags=re.DOTALL) |
| |
| # replace all tabs with single space |
| fullfile = fullfile.replace('\t', ' ') |
| |
| # move non-whitespace content after } to new line |
| fullfile = re.sub(r'}\s*(?!\n)', '}\n', fullfile, flags=re.DOTALL) |
| |
| # split single string into lines |
| libfile = fullfile.split('\n') |
| fullfile = '' |
| |
| # replace semicolons with commas |
| libfile = [line.replace(';', ',') for line in libfile] |
| |
| # remove empty lines and trailing whitespaces |
| libfile = [line.rstrip() for line in libfile if line.strip()] |
| |
| for i in range(len(libfile)): |
| # add comma if not present |
| # TODO: not sure if this should be accepted or returned as error |
| libfile[i] = nocommadecl.sub(r'\g<content>,', libfile[i]) |
| |
| # parse `define` entries |
| libfile[i] = defdecl.sub( |
| r'\g<indent>"define" : {"attribute_name": ' |
| r'"\g<attribute_name>", "group_name": "\g<group_name>", ' |
| r'"attribute_type": "\g<attribute_type>"}', |
| libfile[i]) |
| |
| # parse array entries to make them JSON-compliant |
| arrmatch = arrdecl.match(libfile[i]) |
| if arrmatch: |
| arrays = '' |
| |
| first = True |
| matches = [match for match in |
| subarrdecl.finditer(arrmatch.group("array"))] |
| for match in matches: |
| if first: |
| if len(matches) == 1: |
| arrays = '{}'.format(match.group('arrvalues')) |
| else: |
| arrays = '[{}]'.format(match.group('arrvalues')) |
| first = False |
| else: |
| arrays += ', [{}]'.format(match.group('arrvalues')) |
| |
| libfile[i] = '{indent}{arrname} : [{arrays}],'.format( |
| indent=arrmatch.group('indent'), |
| arrname=arrmatch.group('arrname'), |
| arrays=arrays) |
| |
| # convert array-like attributes to arrays |
| # log_printer.log('INFO', libfile[i]) |
| libfile[i] = singlearr.sub( |
| r'\g<indent>"\g<arrname>" : [\g<arrvalues>],', |
| libfile[i]) |
| |
| # parse attribute entries |
| attmatch = attdecl.match(libfile[i]) |
| if attmatch: |
| libfile[i] = '{}"comp_attribute {}" : "{}",'.format( |
| attmatch.group("indent"), |
| attmatch.group("attrname"), |
| (attmatch.group("attrval") |
| if attmatch.group("attrval") else |
| attmatch.group("attrvalq")).replace('"', '\\"')) |
| |
| # remove parenthesis from struct names |
| structmatch = structdecl.match(libfile[i]) |
| if structmatch: |
| if structmatch.group("name") or structmatch.group("nameq"): |
| libfile[i] = '{}"{} {}" : {}'.format( |
| structmatch.group("indent"), |
| structmatch.group("type"), |
| (structmatch.group("name") if |
| structmatch.group("name") else |
| structmatch.group("nameq")).replace( |
| '"', '\\"'), |
| '{' if libfile[i].rstrip().endswith('{') else '') |
| else: |
| libfile[i] = structdecl.sub( |
| r'\g<indent>"\g<type> " :', |
| libfile[i]) |
| |
| # wrap all text in quotes |
| unwrappedmatch = unwrappeddecl.match(libfile[i]) |
| if unwrappedmatch: |
| singlearrdef = r'\[?\s*(\[\s*(?P<arrvalues>({numdef}(,\s*)?)+)\s*\])+\s*\]?'.format(numdef=numdef) # noqa: E501 |
| varval = (unwrappedmatch.group('varvalue') |
| if unwrappedmatch.group('varvalue') |
| else unwrappedmatch.group('varvalueq')) |
| varnam = (unwrappedmatch.group('varname') |
| if unwrappedmatch.group('varname') |
| else unwrappedmatch.group('varnameq')) |
| isarray = re.match( |
| singlearrdef, |
| varval) |
| if isarray: |
| libfile[i] = '{}"{}" : {},'.format( |
| unwrappedmatch.group('indent'), |
| varnam, |
| varval.strip()) |
| else: |
| libfile[i] = '{}"{}" : "{}",'.format( |
| unwrappedmatch.group('indent'), |
| varnam, |
| varval.strip()) |
| |
| # add colons after closing braces |
| libfile[i] = libfile[i].replace("}", "},") |
| |
| # remove colons before closing braces |
| fullfile = '\n'.join(libfile) |
| fullfile = re.sub( |
| r',(?P<tmp>\s*})', r'\g<tmp>', |
| fullfile, |
| flags=re.DOTALL) |
| libfile = fullfile.split('\n') |
| fullfile = '' |
| |
| # remove the colon in the end of file |
| libfile[-1] = re.sub(r',\s*', '', libfile[-1]) |
| |
| data = '\n'.join(libfile) |
| timingdict = json.loads( |
| data, object_pairs_hook=cls.join_duplicate_keys) |
| |
| timingdict = fixup(timingdict) |
| |
| return timingdict |
| |
| |
| from liberty_json_rewrite import TO_REMOVE |
| |
| |
| def strip_keys(data): |
| if isinstance(data, dict): |
| do = {} |
| for k, v in data.items(): |
| if k in TO_REMOVE: |
| continue |
| #k = fixup(k) |
| if isinstance(k, str) and ' ' in k: |
| k = k.split(' ', 1) |
| |
| if isinstance(k, (tuple, list)): |
| k = list(k) |
| if ' ' in k[0]: |
| k[0:1] = k[0].split(' ', 1) |
| k = ",".join(k) |
| assert isinstance(k, str), (k, v) |
| do[k] = strip_keys(v) |
| return do |
| |
| elif isinstance(data, list): |
| o = [] |
| for i in data: |
| o.append(strip_keys(i)) |
| return o |
| else: |
| return data |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "input", |
| help="LIB file containing timings", |
| type=Path) |
| parser.add_argument( |
| "--cache", |
| action="store_true", |
| help="Create lib.cache.json file", |
| default=False, |
| ) |
| parser.add_argument( |
| "output", |
| help="The output JSON file containing LIB timings", |
| type=Path, |
| default=None, |
| nargs='?') |
| |
| args = parser.parse_args() |
| if args.cache: |
| cache(args.input) |
| return |
| |
| assert str(args.input).endswith(".lib"), args.input |
| |
| if args.output is None: |
| args.output = Path(str(args.input)+'.json') |
| |
| with open(args.input, 'r') as infile: |
| libfile = infile.readlines() |
| |
| libfile = ['{\n'] + libfile + ['}'] |
| |
| timingdict = (LibertyToJSONParser.load_timing_info_from_lib(libfile)) |
| |
| timingdict = strip_keys(timingdict) |
| |
| with open(args.output, 'w') as out: |
| json.dump(timingdict, out, indent=4, sort_keys=True) |
| |
| |
| |
| def pprint_lib(f, data): |
| print("\n"*3) |
| print(f) |
| pdata = copy.deepcopy(data) |
| filter_lib_properties(pdata) |
| pprint.pprint(pdata) |
| |
| |
| def json_dump(d, f): |
| json.dump(d, f, sort_keys=True, indent=' ') |
| |
| |
| def cache(inputdir): |
| inputdir = str(inputdir) |
| if not inputdir.endswith('/'): |
| inputdir += '/' |
| |
| files = [] |
| for x in Path(inputdir).rglob('*.lib.json'): |
| if 'cds' in str(x): |
| continue |
| |
| f = str(x)[len(inputdir):] |
| files.append(f) |
| |
| files = list(set(files)) |
| files.sort() |
| |
| pprint.pprint(files) |
| |
| lib_data = {} |
| for f in files: |
| if debug: |
| print(f) |
| |
| p = inputdir+f |
| libstr, data = get_lib_properties(p) |
| lib_data[str(f)] = (libstr, data) |
| |
| if debug: |
| pprint_lib(f, data) |
| |
| with open('lib.cache.json', 'w') as f: |
| json_dump(lib_data, f) |
| |
| with open('lib.cache.json', 'w') as f: |
| json_dump(lib_data, f) |
| |
| return |
| |
| |
| if __name__ == '__main__': |
| import doctest |
| fails, _ = doctest.testmod() |
| if fails != 0: |
| sys.exit("Some test failed!") |
| |
| main() |