diff --git a/src/hammer-vlsi/hammer_utils/__init__.py b/src/hammer-vlsi/hammer_utils/__init__.py index c50f03311..699003761 100644 --- a/src/hammer-vlsi/hammer_utils/__init__.py +++ b/src/hammer-vlsi/hammer_utils/__init__.py @@ -16,6 +16,7 @@ from decimal import Decimal from .verilog_utils import * +from .spice_utils import * from .lef_utils import * diff --git a/src/hammer-vlsi/hammer_utils/spice_utils.py b/src/hammer-vlsi/hammer_utils/spice_utils.py new file mode 100644 index 000000000..a2a8ae09c --- /dev/null +++ b/src/hammer-vlsi/hammer_utils/spice_utils.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# spice_utils.py +# Misc SPICE utilities +# +# See LICENSE for licence details. + +import re +from typing import Tuple, Optional, List, Set, Dict +from enum import Enum + +class SpiceUtils: + + # Multilines in spice start with a + character + MultilinePattern = re.compile("(?:\s*\n\+)+\s*", flags=re.DOTALL) + ModuleDefPattern = re.compile("^(\.subckt\s+)(?P[^\s]+)(\s+.*)$", flags=re.IGNORECASE|re.MULTILINE) + ModuleInstPattern = re.compile("^(x.*?\s)(?P[^\s]+)(\s*)$", flags=re.IGNORECASE|re.MULTILINE) + EndModulePattern = re.compile("^\.ends.*$", flags=re.IGNORECASE|re.MULTILINE) + Tokens = [ + ('SUBCKT', ModuleDefPattern.pattern), + ('ENDS', EndModulePattern.pattern), + ('INST', ModuleInstPattern.pattern) + ] + TokenRegex = re.compile('|'.join('(?P<%s>%s)' % t for t in Tokens), flags=re.IGNORECASE|re.MULTILINE) + + @staticmethod + def uniquify_spice(sources: List[str]) -> List[str]: + """ + Uniquify the provided SPICE sources. If a module name exists in multiple files, each duplicate module will be + renamed. If the original name of a duplicated module is used in a file where it is not defined, an + exception is thrown as it is impossible to know which renamed module to use. For example: + + - file1.sp defines B and A, which instantiates B + - file2.sp defines B and C, which instantiates B + + B would be renamed to B_0 in file1.sp + B would be renamed to B_1 in file2.sp + + However, if file3.sp defines D, which instantiates B, an exception would be raised because we don't know if we + should chose B_0 or B_1. + + :param sources: A list of the contents of SPICE source files (not the filenames) + :return: A list of SPICE sources with unique modules + """ + sources_no_multiline = [SpiceUtils.remove_multilines(s) for s in sources] + module_trees = [SpiceUtils.parse_module_tree(s) for s in sources_no_multiline] + found_modules = set() # type: Set[str] + duplicates = set() # type: Set[str] + extmods = set() # type: Set[str] + for tree in module_trees: + extmods.update(set(item for sublist in tree.values() for item in sublist if item not in set(tree.keys()))) + for module in tree: + if module in found_modules: + duplicates.add(module) + found_modules.add(module) + + invalid_extmods = extmods.intersection(duplicates) + if len(invalid_extmods) != 0: + raise ValueError("Unable to resolve master for duplicate SPICE module name: {}".format(",".join(invalid_extmods))) + + def replace_source(source: str) -> str: + replacements = {} # type: Dict[str, str] + for old in duplicates: + i = 0 + new = old + while new in found_modules: + new = "{d}_{i}".format(d=old, i=i) + i = i + 1 + found_modules.add(new) + replacements[old] = new + + return SpiceUtils.replace_modules(source, replacements) + + return [replace_source(s) for s in sources_no_multiline] + + + @staticmethod + def parse_module_tree(s: str) -> Dict[str, Set[str]]: + """ + Parse a SPICE file and return a dictionary that contains all found modules pointing to lists of their submodules. + The SPICE file must not contain multiline statements. + + :param s: A SPICE file without any multiline statements + :return: A dictionary whose keys are all found modules and whose values are the list of submodules + """ + in_module = False + module_name = "" + tree = {} # type: Dict[str, Set[str]] + for m in SpiceUtils.TokenRegex.finditer(s): + kind = m.lastgroup + if kind == 'SUBCKT': + module_name = m.group("dname") + in_module = True + if module_name in tree: + raise ValueError("Multiple SPICE subckt definitions for \"{}\" in the same file".format(module_name)) + tree[module_name] = set() + elif kind == 'ENDS': + in_module = False + elif kind == 'INST': + if not in_module: + raise ValueError("Malformed SPICE source while parsing: \"{}\"".format(m.group())) + tree[module_name].add(m.group("mname")) + else: + assert False, "Should not get here" + + return tree + + @staticmethod + def replace_modules(source: str, mapping: Dict[str, str]) -> str: + """ + Replace module names in a provided SPICE file by the provided mapping. + The SPICE file must not contain multiline statements. + + + :param source: The input SPICE source without any multiline statements + :param mapping: A dictionary of old module names mapped to new module names + :return: SPICE source with the module names replaced + """ + # Not giving m a type because its type is different in different python3 versions :( + def repl_fn(m) -> str: + if m.group(2) in mapping: + return m.group(1) + mapping[m.group(2)] + m.group(3) + else: + return m.group(0) + + return SpiceUtils.ModuleInstPattern.sub(repl_fn, SpiceUtils.ModuleDefPattern.sub(repl_fn, source)) + + @staticmethod + def remove_multilines(s: str) -> str: + """ + Remove all multiline statements from the given SPICE source. + + :param s: The SPICE source + :return: SPICE source without multiline statements + """ + return SpiceUtils.MultilinePattern.sub(" ", s) + + + diff --git a/src/hammer-vlsi/hammer_utils/verilog_utils.py b/src/hammer-vlsi/hammer_utils/verilog_utils.py index 2167a23c5..17c6444a6 100644 --- a/src/hammer-vlsi/hammer_utils/verilog_utils.py +++ b/src/hammer-vlsi/hammer_utils/verilog_utils.py @@ -8,14 +8,11 @@ import re -__all__ = ['VerilogUtils'] - - class VerilogUtils: @staticmethod def remove_comments(v: str) -> str: """ - Remove comments from the given Verilog file. + Remove comments from the given Verilog source. :param v: Verilog source code :return: Source code without comments @@ -46,7 +43,7 @@ def contains_module(v: str, module: str) -> bool: @staticmethod def remove_module(v: str, module: str) -> str: """ - Remove the given module from the given Verilog source file, if it exists. + Remove the given module from the given Verilog source, if it exists. :param v: Verilog source code :param module: Module to remove diff --git a/src/hammer-vlsi/spice_utils_test.py b/src/hammer-vlsi/spice_utils_test.py new file mode 100644 index 000000000..284ac210b --- /dev/null +++ b/src/hammer-vlsi/spice_utils_test.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# SPICE-related tests for hammer-vlsi. +# +# See LICENSE for licence details. + +from inspect import cleandoc +from typing import Iterable, Tuple, Any, Dict, List, Set + +from hammer_utils import SpiceUtils + +import unittest + + +class SpiceUtilsTest(unittest.TestCase): + + multiline = cleandoc(""" + .SUBCKT foo a b c + r0 a b 10 + c0 a b 10.1 + r1 a c 200 + r2 b c 1 + xinst a b + + c bar + .ENDS + .subckt bar d e + + f + r0 d e 1 + r1 e + + f + + 100 + r2 d f + + + + 10 + .ends""") + + no_multiline = cleandoc(""" + .SUBCKT foo a b c + r0 a b 10 + c0 a b 10.1 + r1 a c 200 + r2 b c 1 + xinst a b c bar + .ENDS + .subckt bar d e f + r0 d e 1 + r1 e f 100 + r2 d f 10 + .ends""") + + def test_multiline(self) -> None: + self.assertEqual(SpiceUtils.remove_multilines(self.multiline), self.no_multiline) + + def test_replace_modules(self) -> None: + no_multiline_replaced = cleandoc(""" + .SUBCKT foo a b c + r0 a b 10 + c0 a b 10.1 + r1 a c 200 + r2 b c 1 + xinst a b c baz + .ENDS + .subckt baz d e f + r0 d e 1 + r1 e f 100 + r2 d f 10 + .ends""") + self.assertEqual(SpiceUtils.replace_modules(self.no_multiline, {"bar": "baz"}), no_multiline_replaced) + + hierarchy = [] # type: List[str] + hierarchy_uniq = [] # type: List[str] + + # 0 + hierarchy.append(cleandoc(""" + .subckt top clock in out vdd vss + x0 clock in mid vdd vss middle0 + x1 clock mid out vdd vss middle1 + .ends + .subckt middle0 clock in out vdd vss + m1 out mid vdd vdd pmos + m2 out mid vss vss nmos + xinst clock in mid vdd vss leaf0 + .ends + .subckt middle1 clock in out vdd vss + m1 out mid0 vdd vdd pmos + m2 out mid0 vss vss nmos + xinst clock mid1 mid0 vdd vss leaf1 + xinst clock in mid1 vdd vss leaf1 + .ends + .subckt leaf0 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 100 + .ends + .subckt leaf1 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 50 + .ends + """)) + + # 0 + hierarchy_uniq.append(cleandoc(""" + .subckt top clock in out vdd vss + x0 clock in mid vdd vss middle0_0 + x1 clock mid out vdd vss middle1_0 + .ends + .subckt middle0_0 clock in out vdd vss + m1 out mid vdd vdd pmos + m2 out mid vss vss nmos + xinst clock in mid vdd vss leaf0_0 + .ends + .subckt middle1_0 clock in out vdd vss + m1 out mid0 vdd vdd pmos + m2 out mid0 vss vss nmos + xinst clock mid1 mid0 vdd vss leaf1 + xinst clock in mid1 vdd vss leaf1 + .ends + .subckt leaf0_0 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 100 + .ends + .subckt leaf1 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 50 + .ends + """)) + + # 1 + hierarchy.append(cleandoc(""" + .subckt top1 clock in out vdd vss + x0 clock in mid vdd vss middle0 + x1 clock mid out vdd vss middle1 + .ends + .subckt middle0 clock in out vdd vss + m1 out mid vdd vdd pmos + m2 out mid vss vss nmos + xinst clock in mid vdd vss leaf0 + .ends + .subckt middle1 clock in out vdd vss + m1 out mid0 vdd vdd pmos + m2 out mid0 vss vss nmos + xinst clock mid1 mid0 vdd vss leaf1 + xinst clock in mid1 vdd vss leaf1 + .ends + .subckt leaf0 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 100 + .ends + """)) + + # 1 + hierarchy_uniq.append(cleandoc(""" + .subckt top1 clock in out vdd vss + x0 clock in mid vdd vss middle0_1 + x1 clock mid out vdd vss middle1_1 + .ends + .subckt middle0_1 clock in out vdd vss + m1 out mid vdd vdd pmos + m2 out mid vss vss nmos + xinst clock in mid vdd vss leaf0_1 + .ends + .subckt middle1_1 clock in out vdd vss + m1 out mid0 vdd vdd pmos + m2 out mid0 vss vss nmos + xinst clock mid1 mid0 vdd vss leaf1 + xinst clock in mid1 vdd vss leaf1 + .ends + .subckt leaf0_1 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 100 + .ends + """)) + + # 2 + hierarchy.append(cleandoc(""" + .subckt top3 clock in out vdd vss + x0 clock in mid vdd vss leaf0 + .ends + .subckt leaf0 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 50 + .ends""")) + + # 2 + hierarchy_uniq.append(cleandoc(""" + .subckt top3 clock in out vdd vss + x0 clock in mid vdd vss leaf0_2 + .ends + .subckt leaf0_2 clock in out vdd vss + m1 out in vdd vdd pmos + m2 out in vss vss nmos + r1 clock out 50 + .ends""")) + + # 3 + hierarchy.append(cleandoc(""" + .subckt top4 clock in out vdd vss + x0 clock in mid vdd vss leaf0 + .ends + """)) + + def test_module_tree(self) -> None: + tree = SpiceUtils.parse_module_tree(self.hierarchy[0]) + check = {} # type: Dict[str, Set[str]] + check['top'] = {'middle0', 'middle1'} + check['middle0'] = {'leaf0'} + check['middle1'] = {'leaf1'} + check['leaf0'] = set() + check['leaf1'] = set() + self.assertEqual(tree, check) + + def test_uniq(self) -> None: + self.assertEqual(SpiceUtils.uniquify_spice(self.hierarchy[0:3]), self.hierarchy_uniq) + + with self.assertRaises(ValueError): + # This should assert because it won't know how to handle leaf0 + SpiceUtils.uniquify_spice(self.hierarchy) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/unittests.sh b/src/test/unittests.sh index 7f2862b52..5f82db2de 100755 --- a/src/test/unittests.sh +++ b/src/test/unittests.sh @@ -12,6 +12,7 @@ python3 ../hammer-vlsi/cli_driver_test.py python3 ../hammer-vlsi/utils_test.py python3 ../hammer-vlsi/units_test.py python3 ../hammer-vlsi/verilog_utils_test.py +python3 ../hammer-vlsi/spice_utils_test.py python3 ../hammer-vlsi/lef_utils_test.py python3 ../hammer_config_test/test.py