Repository: UCSBarchlab/PyRTL

Issue:
Optimize is currently missing the elimination of "double not"
The pyrtl.optimize code is not eliminating "double not" conditions (reducing them to a single wire).   Right now it does some more complex optimizations including CSE but this very simple case gets missed. You can see very clearly from the code below.

```python
import pyrtl

a = pyrtl.Input(1,'a')
q = pyrtl.Output(1,'q')
q <<= ~(~(a))

pyrtl.synthesize()

with open('pre-optimize.svg','w') as f:
    pyrtl.output_to_svg(f)

pyrtl.optimize()

with open('post-optimize.svg','w') as f:
    pyrtl.output_to_svg(f)
```

Hints from issue comments:

Solution patch:
diff --git a/.gitignore b/.gitignore
index 75c349e8..1a33fac9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,6 @@ ipynb-examples/.ipynb_checkpoints
 
 # VS Code
 .vscode
+
+# Python venv
+pyvenv.cfg
\ No newline at end of file
diff --git a/codecov.yml b/codecov.yml
index 17dfa016..7aae2208 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -2,9 +2,9 @@ coverage:
   status:
     project:
       default:
-        threshold: 0.5%
+        informational: true
     patch:
       default:
-        threshold: 0.5%
+        informational: true
   precision: 1
   round: up
diff --git a/docs/helpers.rst b/docs/helpers.rst
index 1dba2319..894a2dcf 100644
--- a/docs/helpers.rst
+++ b/docs/helpers.rst
@@ -111,4 +111,5 @@ Encoders and Decoders
 ---------------------
 
 .. autofunction:: pyrtl.helperfuncs.one_hot_to_binary
+.. autofunction:: pyrtl.helperfuncs.binary_to_one_hot
 
diff --git a/pyrtl/__init__.py b/pyrtl/__init__.py
index 821b9941..e30e6b41 100644
--- a/pyrtl/__init__.py
+++ b/pyrtl/__init__.py
@@ -41,6 +41,7 @@
 from .helperfuncs import wire_struct
 from .helperfuncs import wire_matrix
 from .helperfuncs import one_hot_to_binary
+from .helperfuncs import binary_to_one_hot
 
 from .corecircuits import and_all_bits
 from .corecircuits import or_all_bits
diff --git a/pyrtl/helperfuncs.py b/pyrtl/helperfuncs.py
index 2c8fec8b..b55931c7 100644
--- a/pyrtl/helperfuncs.py
+++ b/pyrtl/helperfuncs.py
@@ -13,7 +13,15 @@
 from .core import working_block, _NameIndexer, _get_debug_mode, Block
 from .pyrtlexceptions import PyrtlError, PyrtlInternalError
 from .wire import WireVector, Input, Output, Const, Register, WrappedWireVector
-from .corecircuits import as_wires, rtl_all, rtl_any, concat, concat_list, select
+from .corecircuits import (
+    as_wires,
+    rtl_all,
+    rtl_any,
+    concat,
+    concat_list,
+    select,
+    shift_left_logical
+)
 
 # -----------------------------------------------------------------
 #        ___       __   ___  __   __
@@ -781,20 +789,29 @@ def _convert_verilog_str(val: str, bitwidth: int = None,
         raise PyrtlError('error, "signed" option with verilog-style string constants not supported')
 
     bases = {'b': 2, 'o': 8, 'd': 10, 'h': 16, 'x': 16}
-    passed_bitwidth = bitwidth
 
     neg = False
     if val.startswith('-'):
         neg = True
         val = val[1:]
+
     split_string = val.lower().split("'")
     if len(split_string) != 2:
         raise PyrtlError('error, string not in verilog style format')
     try:
-        bitwidth = int(split_string[0])
+        verilog_bitwidth = int(split_string[0])
+        bitwidth = bitwidth or verilog_bitwidth  # if bitwidth is None, use verilog_bitwidth
+        if verilog_bitwidth > bitwidth:
+            raise PyrtlError(
+                "bitwidth parameter passed (%d) cannot fit Verilog-style constant with bitwidth %d"
+                % (bitwidth, verilog_bitwidth)
+                + " (if bitwidth=None is used, PyRTL will determine the bitwidth from the "
+                "Verilog-style constant specification)"
+            )
+
         sval = split_string[1]
         if sval[0] == 's':
-            raise PyrtlError('error, signed integers are not supported in verilog-style constants')
+            raise PyrtlError('error, signed integers are not supported in Verilog-style constants')
         base = 10
         if sval[0] in bases:
             base = bases[sval[0]]
@@ -803,17 +820,12 @@ def _convert_verilog_str(val: str, bitwidth: int = None,
         num = int(sval, base)
     except (IndexError, ValueError):
         raise PyrtlError('error, string not in verilog style format')
+
     if neg and num:
         if (num >> bitwidth - 1):
             raise PyrtlError('error, insufficient bits for negative number')
         num = (1 << bitwidth) - num
 
-    if passed_bitwidth and passed_bitwidth != bitwidth:
-        raise PyrtlError('error, bitwidth parameter of constant does not match'
-                         ' the bitwidth infered from the verilog style specification'
-                         ' (if bitwidth=None is used, pyrtl will determine the bitwidth from the'
-                         ' verilog-style constant specification)')
-
     if num >> bitwidth != 0:
         raise PyrtlError('specified bitwidth %d for verilog constant insufficient to store value %d'
                          % (bitwidth, num))
@@ -1715,3 +1727,35 @@ def one_hot_to_binary(w) -> WireVector:
         already_found = already_found | w[i]
 
     return pos
+
+
+def binary_to_one_hot(bit_position, max_bitwidth: int = None) -> WireVector:
+    '''Takes an input representing a bit position and returns a WireVector
+    with that bit position set to 1 and the others to 0.
+
+    :param bit_position: WireVector, WireVector-like object, or something that can be converted
+        into a :py:class:`.Const` (in accordance with the :py:func:`.as_wires()`
+        required input). Example inputs: ``0b10``, ``0b1000``, ``4``.
+    :param max_bitwidth: Optional integer maximum bitwidth for the resulting one-hot WireVector.
+    :return: WireVector with the bit position given by the input set to 1 and all other bits
+        set to 0 (bit position 0 being the least significant bit).
+
+    If the max_bitwidth provided is not sufficient for the given bit_position to be set to 1,
+    a ``0`` WireVector of size max_bitwidth will be returned.
+
+    Examples::
+
+        binary_to_onehot(0)  # returns 0b01
+        binary_to_onehot(3)  # returns 0b1000
+        binary_to_onehot(0b100)  # returns 0b10000
+    '''
+
+    bit_position = as_wires(bit_position)
+
+    if max_bitwidth is not None:
+        bitwidth = max_bitwidth
+    else:
+        bitwidth = 2 ** len(bit_position)
+
+    # Need to dynamically set the appropriate bit position since bit_position may not be a Const
+    return shift_left_logical(Const(1, bitwidth=bitwidth), bit_position)
diff --git a/pyrtl/memory.py b/pyrtl/memory.py
index 451e3dcf..38dd0660 100644
--- a/pyrtl/memory.py
+++ b/pyrtl/memory.py
@@ -259,10 +259,10 @@ def _build_read_port(self, addr):
     def _assignment(self, item, val, is_conditional):
         from .conditional import _build
 
-        item = as_wires(item, bitwidth=self.addrwidth, truncating=False)
-        if len(item) > self.addrwidth:
-            raise PyrtlError('error, the wire indexing the memory bitwidth > addrwidth')
-        addr = item
+        # Even though as_wires is already called on item already in the __getitem__ method,
+        # we need to call it again here because __setitem__ passes the original item
+        # to _assignment.
+        addr = as_wires(item, bitwidth=self.addrwidth, truncating=False)
 
         if isinstance(val, MemBlock.EnabledWrite):
             data, enable = val.data, val.enable
diff --git a/pyrtl/passes.py b/pyrtl/passes.py
index 0b684303..6765ef47 100644
--- a/pyrtl/passes.py
+++ b/pyrtl/passes.py
@@ -52,11 +52,162 @@ def optimize(update_working_block=True, block=None, skip_sanity_check=False):
         constant_propagation(block, True)
         _remove_unlistened_nets(block)
         common_subexp_elimination(block)
+        _optimize_inverter_chains(block, skip_sanity_check)
         if (not skip_sanity_check) or _get_debug_mode():
             block.sanity_check()
     return block
 
 
+def _get_inverter_chains(wire_creator, wire_users):
+    """Returns all inverter chains in the block.
+
+    The function returns a list of inverter chains in the block.
+    Each inverter chain is represented as a list of the WireVectors
+    in the chain.
+
+    Consider the following circuit, for example:
+    A -~-> B -~-> C -w-> X
+    D -~-> E -w-> Y
+    If the function is called on this circuit, it will return
+    [[A, B, C], [D, E]].
+    """
+
+    # Build a list of inverter chains. Each inverter chain is a list of WireVectors,
+    # from source to destination.
+    inverter_chains = []
+    for current_dest, current_creator in wire_creator.items():
+        if current_creator.op != "~":
+            # Skip non-inverters.
+            continue
+
+        # The current inverter connects current_arg (a WireVector) to current_dest (also
+        # a WireVector).
+        current_arg = current_creator.args[0]
+        # current_users is the number of LogicNets that use current_dest.
+        current_users = len(wire_users[current_dest])
+
+        # Add the current inverter to the end of this inverter chain.
+        append_to = None
+        # Add the current inverter to the beginning of this inverter chain.
+        prepend_to = None
+        next_inverter_chains = []
+        for inverter_chain in inverter_chains:
+            chain_arg = inverter_chain[0]
+            chain_dest = inverter_chain[-1]
+            chain_users = len(wire_users[chain_dest])
+
+            if chain_dest is current_arg and chain_users == 1:
+                # This chain's only destination is the current inverter. Append the
+                # current inverter to the chain.
+                append_to = inverter_chain
+            elif chain_arg is current_dest and current_users == 1:
+                # This chain's only argument is the current inverter. Add the current
+                # inverter to the beginning of the chain.
+                prepend_to = inverter_chain
+            else:
+                # The current inverter is not connected to the inverter chain, so we
+                # pass the inverter chain through to next_inverter_chains
+                next_inverter_chains.append(inverter_chain)
+
+        if append_to and prepend_to:
+            # The current inverter joins two existing inverter chains.
+            next_inverter_chains.append(append_to + prepend_to)
+        elif append_to:
+            # Add the current inverter after 'append_to'.
+            next_inverter_chains.append(append_to + [current_dest])
+        elif prepend_to:
+            # Add the current inverter before 'prepend_to'.
+            next_inverter_chains.append([current_arg] + prepend_to)
+        else:
+            # The current inverter is not connected to any inverter chain, so
+            # we start a new inverter chain with it
+            next_inverter_chains.append([current_arg, current_dest])
+
+        inverter_chains = next_inverter_chains
+    return inverter_chains
+
+
+def _optimize_inverter_chains(block, skip_sanity_check=False):
+    """ Optimizes inverter chains in the block.
+
+    An inverter chain means two or more inverters directly connected
+    to each other. Inverter chains are redundant and can be removed.
+    For example, A -~-> B -~-> C -w-> X can be reduced to A -w-> X.
+
+    After optimization, a chain of an even number of inverters will
+    be reduced a direct connection, and a chain of an odd number of
+    inverters will be reduced to one inverter.
+
+    If an inverter chain has intermediate users it won't be removed.
+    For example, the inverter chain in the following circuit won't be removed:
+    A -~-> B -~-> C -w-> X
+    B -w-> Y
+    """
+
+    # wire_creator maps from WireVector to the LogicNet that defines its value.
+    # wire_users maps from WireVector to a list of LogicNets that use its value.
+    wire_creator, wire_users = block.net_connections()
+
+    new_logic = set()
+    net_removal_set = set()
+    wire_removal_set = set()
+
+    # This ProducerList maps the end wire of an inverter chain to its beginning wire.
+    # We need this because when removing an inverter chain its end wire gets removed,
+    # so we need to replace the source of LogicNets using the end wire of the inverter
+    # chain with the chain's beginning wire.
+    #
+    # We need a ProducerList, rather than a simple dict, because if an inverter chain
+    # of more than two inverters has intermediate users, we may have to query the dict
+    # multiple times to get the replacement for the inverter chain's last wire.
+    # Consider the following circuit, for example:
+    # A -~-> B -~-> C -w-> X
+    # C -~-> D -~-> E -w-> Y
+    # This is the optimized version of the circuit:
+    # A -w-> X
+    # A -w-> Y
+    # The inverter chains found will be A-B-C and C-D-E (two separate chains will be
+    # found instead of A-B-C-D-E because C has an intermediate user). In the dict,
+    # C will be mapped to A and E will be mapped to C. Hence, when finding the
+    # replacement of E, we have to first query the dict to get C, and then query
+    # the dict again on C to get A.
+    wire_src_dict = _ProducerList()
+
+    for inverter_chain in _get_inverter_chains(wire_creator, wire_users):
+        # If len(inverter_chain) = n, there are n-1 inverters in the chain.
+        # We only remove inverters if there are at least two inverters in a chain.
+        if len(inverter_chain) > 2:
+            if len(inverter_chain) % 2 == 1:  # There is an even number of inverters in a chain.
+                start_idx = 1
+            else:  # There is an odd number of inverters in a chain.
+                start_idx = 2
+            # Remove wires used in the inverter chain.
+            wires_to_remove = inverter_chain[start_idx:]
+            wire_removal_set.update(wires_to_remove)
+            # Remove inverters used in the chain.
+            inverters_to_remove = {wire_creator[wire] for wire in wires_to_remove}
+            net_removal_set.update(inverters_to_remove)
+            # Map the end wire of the inverter chain to the beginning wire.
+            wire_src_dict[inverter_chain[-1]] = inverter_chain[start_idx - 1]
+
+    # This loop recreates the block with inverter chains removed. It adds each
+    # LogicNet in the original block to the new block if it is not marked for
+    # removal, and replaces the source of the LogicNet if its source was the end wire
+    # of a removed inverter chain.
+    for net in block.logic:
+        if net not in net_removal_set:
+            new_logic.add(LogicNet(net.op, net.op_param,
+                                   args=tuple(wire_src_dict.find_producer(x) for x in net.args),
+                                   dests=net.dests))
+
+    block.logic = new_logic
+    for dead_wirevector in wire_removal_set:
+        block.remove_wirevector(dead_wirevector)
+
+    if (not skip_sanity_check) or _get_debug_mode():
+        block.sanity_check()
+
+
 class _ProducerList(object):
     """  Maps from wire to its immediate producer and finds ultimate producers. """
     def __init__(self):
@@ -69,10 +220,9 @@ def __setitem__(self, key, item):
         self.dict[key] = item
 
     def find_producer(self, item):
-        if item in self.dict:
-            return self.find_producer(self.dict[item])
-        else:
-            return item
+        while item in self.dict:
+            item = self.dict[item]
+        return item
 
 
 def _remove_wire_nets(block, skip_sanity_check=False):
diff --git a/pyrtl/wire.py b/pyrtl/wire.py
index b5e5e94c..a3fe3155 100644
--- a/pyrtl/wire.py
+++ b/pyrtl/wire.py
@@ -92,6 +92,92 @@ class WireVector(object):
     this case with a reference to WireVector ``b``. In contrast ``a <<= b``
     does not overwrite ``a``, but simply wires the two together.
 
+    -------------------
+    WireVector Equality
+    -------------------
+
+    WireVector's :meth:`.__eq__` generates logic that dynamically reports if two wires
+    carry the same values. WireVector's :meth:`.__eq__` returns a 1-bit WireVector, not
+    a ``bool``, and attempting to convert a WireVector to a ``bool`` throws a
+    ``PyrtlError``. This behavior is incompatible with `Python's data model
+    <https://docs.python.org/3/reference/expressions.html#value-comparisons>`_, which
+    can cause problems.
+
+    For example, you *can not* statically check if two WireVectors are equal with
+    ``==``. Statically checking for WireVector equality can be useful while constructing
+    or analyzing circuits::
+
+        >>> w1 = pyrtl.WireVector(name="w1", bitwidth=1)
+        >>> w2 = pyrtl.WireVector(name="w2", bitwidth=2)
+        >>> if w1 == w2:
+        ...     print('same')
+        ...
+        Traceback (most recent call last):
+        ...
+        pyrtl.pyrtlexceptions.PyrtlError: cannot convert WireVector to compile-time
+        boolean.  This error often happens when you attempt to use WireVectors with "=="
+        or something that calls "__eq__", such as when you test if a WireVector is "in"
+        something
+
+    The error about converting WireVector to ``bool`` results from Python attempting to
+    convert the 1-bit WireVector returned by :meth:`.__eq__` to ``True`` or ``False``
+    while evaluating the ``if`` statement's condition.
+
+    Instead, you *can* statically check if two WireVectors refer to the same object with
+    ``is``::
+
+        >>> if w1 is not w2:
+        ...     print('not the same')
+        ...
+        not the same
+        >>> temp = w1
+        >>> temp is w1
+        True
+        >>> temp is w2
+        False
+
+    Be careful when using Python features that depend on ``==`` with WireVectors. This
+    often comes up when checking if a WireVector is in a list with ``in``, which does
+    not work because ``in`` falls back on checking each item in the ``list`` for
+    equality with ``==``::
+
+        >>> l = [w1]
+        >>> w2 in l
+        Traceback (most recent call last):
+        ...
+        pyrtl.pyrtlexceptions.PyrtlError: cannot convert WireVector to compile-time
+        boolean.  This error often happens when you attempt to use WireVectors with "=="
+        or something that calls "__eq__", such as when you test if a WireVector is "in"
+        something
+
+    Most other ``list`` operations work, so you can store WireVectors in a ``list`` if
+    you avoid using the ``in`` operator::
+
+        >>> len(l)
+        1
+        >>> l[0] is w1
+        True
+        >>> [(w.name, w.bitwidth) for w in l]
+        [('w1', 1)]
+
+    WireVectors define a standard ``__hash__`` method, so if you need to check if a
+    WireVector is in a container, use a ``set`` or ``dict``. This works because these
+    containers use ``__hash__`` to skip unnecessary equality checks::
+
+        >>> s = {w1}
+        >>> w1 in s
+        True
+        >>> w2 in s
+        False
+
+        >>> d = {w1: 'hello'}
+        >>> w1 in d
+        True
+        >>> w2 in d
+        False
+        >>> d[w1]
+        'hello'
+
     """
 
     # "code" is a static variable used when output as string.
@@ -159,7 +245,10 @@ def __hash__(self):
 
     def __str__(self):
         """A string representation of the wire in 'name/bitwidth code' form."""
-        return ''.join([self.name, '/', str(self.bitwidth), self._code])
+        return f'{self.name}/{self.bitwidth}{self._code}'
+
+    def __repr__(self):
+        return str(self)
 
     def _validate_bitwidth(self, bitwidth):
         if bitwidth is not None:
@@ -852,11 +941,15 @@ def __init__(self, bitwidth: int, name: str = '', reset_value: int = None,
         super(Register, self).__init__(bitwidth=bitwidth, name=name, block=block)
         self.reg_in = None  # wire vector setting self.next
         if reset_value is not None:
-            reset_value, rst_bitwidth = infer_val_and_bitwidth(reset_value)
+            reset_value, rst_bitwidth = infer_val_and_bitwidth(
+                reset_value,
+                bitwidth=bitwidth,
+            )
             if rst_bitwidth > bitwidth:
                 raise PyrtlError(
                     'reset_value "%s" cannot fit in the specified %d bits for this register'
-                    % (str(reset_value), bitwidth))
+                    % (str(reset_value), bitwidth)
+                )
         self.reset_value = reset_value
 
     @property
@@ -930,6 +1023,9 @@ def __hash__(self):
     def __str__(self):
         return str(self.wire)
 
+    def __repr__(self):
+        return repr(self.wire)
+
     def __ilshift__(self, other):
         self.wire <<= other
         return self


Test patch:
diff --git a/tests/test_helperfuncs.py b/tests/test_helperfuncs.py
index 972fb502..a9798fee 100644
--- a/tests/test_helperfuncs.py
+++ b/tests/test_helperfuncs.py
@@ -1814,5 +1814,47 @@ def test_no_ones(self):
         self.assertEqual(sim.inspect('o'), 0)
 
 
+class TestBinaryToOneHot(unittest.TestCase):
+    def setUp(self):
+        pyrtl.reset_working_block()
+
+    def test_simple_binary_to_one_hot(self):
+        bit_position = pyrtl.Input(bitwidth=8, name='bit_position')
+        one_hot = pyrtl.Output(name='one_hot')
+        one_hot <<= pyrtl.binary_to_one_hot(bit_position)
+
+        self.assertEqual(one_hot.bitwidth, 256)
+
+        sim = pyrtl.Simulation()
+        sim.step({bit_position: 0})
+        self.assertEqual(sim.inspect('one_hot'), 0b01)
+        sim.step({bit_position: 2})
+        self.assertEqual(sim.inspect('one_hot'), 0b0100)
+        sim.step({bit_position: 5})
+        self.assertEqual(sim.inspect('one_hot'), 0b00100000)
+        sim.step({bit_position: 12})
+        self.assertEqual(sim.inspect('one_hot'), 0b0001000000000000)
+        sim.step({bit_position: 15})
+        self.assertEqual(sim.inspect('one_hot'), 0b1000000000000000)
+
+    # Tests with the max_bitwidth set
+    def test_with_max_bitwidth(self):
+        bit_position = pyrtl.Input(bitwidth=8, name='bit_position')
+        one_hot = pyrtl.Output(name='one_hot')
+        one_hot <<= pyrtl.binary_to_one_hot(bit_position, max_bitwidth=4)
+
+        self.assertEqual(one_hot.bitwidth, 4)
+
+        sim = pyrtl.Simulation()
+        sim.step({bit_position: 0})
+        self.assertEqual(sim.inspect('one_hot'), 0b0001)
+        sim.step({bit_position: 3})
+        self.assertEqual(sim.inspect('one_hot'), 0b1000)
+
+        # The max_bitwidth set is not enough for a bit position of 4
+        sim.step({bit_position: 4})
+        self.assertEqual(sim.inspect('one_hot'), 0b0000)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_memblock.py b/tests/test_memblock.py
index 351804af..a48bcf2b 100644
--- a/tests/test_memblock.py
+++ b/tests/test_memblock.py
@@ -102,6 +102,45 @@ def test_memblock_added_default_named(self):
         self.assertIs(pyrtl.working_block().get_memblock_by_name(mem.name), mem)
 
 
+class RTLMemBlockErrorTests(unittest.TestCase):
+    def setUp(self):
+        pyrtl.reset_working_block()
+
+    def test_negative_bitwidth(self):
+        with self.assertRaises(pyrtl.PyrtlError):
+            pyrtl.MemBlock(bitwidth=-1, addrwidth=1)
+
+    def test_negative_addrwidth(self):
+        with self.assertRaises(pyrtl.PyrtlError):
+            pyrtl.MemBlock(bitwidth=1, addrwidth=-1)
+
+    def test_memindex_bitwidth_greater_than_addrwidth(self):
+        mem = pyrtl.MemBlock(bitwidth=1, addrwidth=1)
+        mem_addr = pyrtl.Input(2, 'mem_addr')
+        mem_out = pyrtl.Output(1, 'mem_out')
+        with self.assertRaises(pyrtl.PyrtlError):
+            mem_out <<= mem[mem_addr]
+
+    def test_memblock_write_data_larger_than_memory_bitwidth(self):
+        mem = pyrtl.MemBlock(bitwidth=1, addrwidth=1)
+        mem_addr = pyrtl.Input(1, 'mem_addr')
+        mem_in = pyrtl.Input(2, 'mem_in')
+        with self.assertRaises(pyrtl.PyrtlError):
+            mem[mem_addr] <<= mem_in
+
+    def test_memblock_enable_signal_not_1_bit(self):
+        mem = pyrtl.MemBlock(bitwidth=1, addrwidth=1)
+        mem_addr = pyrtl.Input(1, 'mem_addr')
+        mem_in = pyrtl.Input(1, 'mem_in')
+        with self.assertRaises(pyrtl.PyrtlError):
+            mem[mem_addr] <<= pyrtl.MemBlock.EnabledWrite(mem_in, enable=pyrtl.Input(2))
+
+    def test_read_ports_exception(self):
+        mem = pyrtl.MemBlock(bitwidth=1, addrwidth=1)
+        with self.assertRaises(pyrtl.PyrtlError):
+            mem.read_ports()
+
+
 class MemIndexedTests(unittest.TestCase):
     def setUp(self):
         pyrtl.reset_working_block()
@@ -229,6 +268,51 @@ def test_write_memindexed_ior(self):
         self.assertEqual(self.mem1.num_read_ports, 1)
         self.assertEqual(self.mem2.num_write_ports, 1)
 
+    def test_memindexed_len(self):
+        self.mem = pyrtl.MemBlock(bitwidth=8, addrwidth=1)
+        self.assertEqual(len(self.mem[0]), 8)
+        self.mem_2 = pyrtl.MemBlock(bitwidth=16, addrwidth=1)
+        self.assertEqual(len(self.mem_2[0]), 16)
+
+    def test_memindexed_getitem(self):
+        mem = pyrtl.MemBlock(bitwidth=8, addrwidth=1, max_read_ports=None)
+        mem_addr = pyrtl.Input(1, 'mem_addr')
+        mem_out_array = [pyrtl.Output(8, 'mem_out_' + str(i)) for i in range(8)]
+        for i in range(8):
+            mem_out_array[i] <<= mem[mem_addr][i]
+        mem_value_map = {mem: {0: 7, 1: 5}}
+        sim = pyrtl.Simulation(memory_value_map=mem_value_map)
+        sim.step({mem_addr: 0})
+        binary = bin(mem_value_map[mem][0])[2:].zfill(8)
+        self.assertEqual([sim.inspect(mem_out_array[j]) for j in range(8)],
+                         [int(binary[7 - j]) for j in range(8)])
+
+    def test_memindexed_sign_extended(self):
+        mem = pyrtl.MemBlock(bitwidth=8, addrwidth=1)
+        mem_addr = pyrtl.Input(1, 'mem_addr')
+        mem_out = pyrtl.Output(16, 'mem_out')
+        mem_out <<= mem[mem_addr].sign_extended(16)
+        mem_value_map = {mem: {0: 0b00101101, 1: 0b10011011}}
+        mem_value_map_sign_extended = [0b0000000000101101, 0b1111111110011011]
+        sim = pyrtl.Simulation(memory_value_map=mem_value_map)
+        sim.step({mem_addr: 0})
+        self.assertEqual(sim.inspect(mem_out), mem_value_map_sign_extended[0])
+        sim.step({mem_addr: 1})
+        self.assertEqual(sim.inspect(mem_out), mem_value_map_sign_extended[1])
+
+    def test_memindexed_zero_extended(self):
+        mem = pyrtl.MemBlock(bitwidth=8, addrwidth=1)
+        mem_addr = pyrtl.Input(1, 'mem_addr')
+        mem_out = pyrtl.Output(16, 'mem_out')
+        mem_out <<= mem[mem_addr].zero_extended(16)
+        mem_value_map = {mem: {0: 0b00101101, 1: 0b10011011}}
+        mem_value_map_zero_extended = [0b0000000000101101, 0b0000000010011011]
+        sim = pyrtl.Simulation(memory_value_map=mem_value_map)
+        sim.step({mem_addr: 0})
+        self.assertEqual(sim.inspect(mem_out), mem_value_map_zero_extended[0])
+        sim.step({mem_addr: 1})
+        self.assertEqual(sim.inspect(mem_out), mem_value_map_zero_extended[1])
+
 
 class RTLRomBlockWiring(unittest.TestCase):
     data = list(range(2**5))
diff --git a/tests/test_passes.py b/tests/test_passes.py
index 332ad873..f5dcbcd5 100644
--- a/tests/test_passes.py
+++ b/tests/test_passes.py
@@ -316,6 +316,87 @@ def test_slice_net_removal_4(self):
         self.num_net_of_type('s', 1, block)
         self.num_net_of_type('w', 2, block)
 
+    def test_remove_double_inverts_1_invert(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire <<= ~inwire
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(2, block)
+        self.assert_num_wires(3, block)
+
+    def test_remove_double_inverts_3_inverts(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire <<= ~(~(~inwire))
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(2, block)
+        self.assert_num_wires(3, block)
+
+    def test_remove_double_inverts_5_inverts(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire <<= ~(~(~(~(~inwire))))
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(2, block)
+        self.assert_num_wires(3, block)
+
+    def test_remove_double_inverts_2_inverts(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire <<= ~(~inwire)
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(1, block)
+        self.assert_num_wires(2, block)
+
+    def test_remove_double_inverts_4_inverts(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire <<= ~(~(~(~inwire)))
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(1, block)
+        self.assert_num_wires(2, block)
+
+    def test_remove_double_inverts_6_inverts(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire <<= ~(~(~(~(~(~inwire)))))
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(1, block)
+        self.assert_num_wires(2, block)
+
+    def test_dont_remove_double_inverts_another_user(self):
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire2 = pyrtl.Output(bitwidth=1)
+        tempwire = pyrtl.WireVector()
+        tempwire <<= ~inwire
+        outwire <<= ~tempwire
+        outwire2 <<= tempwire
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(4, block)
+        self.assert_num_wires(5, block)
+
+    def test_multiple_double_invert_chains(self):
+        # _remove_double_inverts removes double inverts by chains,
+        # so it is useful to make sure it can remove
+        # double inverts from multiple chains
+        inwire = pyrtl.Input(bitwidth=1)
+        outwire = pyrtl.Output(bitwidth=1)
+        outwire2 = pyrtl.Output(bitwidth=1)
+        outwire <<= ~(~inwire)
+        outwire2 <<= ~(~(~(~(inwire))))
+        pyrtl.optimize()
+        block = pyrtl.working_block()
+        self.assert_num_net(2, block)
+        self.assert_num_wires(3, block)
+
 
 class TestConstFolding(NetWireNumTestCases):
     def setUp(self):
diff --git a/tests/test_wire.py b/tests/test_wire.py
index 070a4960..02095f0b 100644
--- a/tests/test_wire.py
+++ b/tests/test_wire.py
@@ -263,13 +263,38 @@ def test_reset_value_as_string(self):
         self.assertEqual(r.reset_value, 1)
 
     def test_invalid_reset_value_too_large(self):
-        with self.assertRaisesRegex(pyrtl.PyrtlError, "cannot fit in the specified"):
+        with self.assertRaises(pyrtl.PyrtlError):
             r = pyrtl.Register(4, reset_value=16)
 
     def test_invalid_reset_value_too_large_as_string(self):
-        with self.assertRaisesRegex(pyrtl.PyrtlError, "cannot fit in the specified"):
+        with self.assertRaises(pyrtl.PyrtlError):
             r = pyrtl.Register(4, reset_value="5'd16")
 
+    def test_negative_reset_value(self):
+        r = pyrtl.Register(4, reset_value=-4)
+        self.assertEqual(
+            pyrtl.helperfuncs.val_to_signed_integer(r.reset_value, r.bitwidth),
+            -4
+        )
+
+    def test_negative_reset_value_as_string(self):
+        r = pyrtl.Register(4, reset_value="-4'd1")
+        self.assertEqual(
+            pyrtl.helperfuncs.val_to_signed_integer(r.reset_value, r.bitwidth),
+            -1
+        )
+
+    def test_invalid_negative_reset_value_as_string(self):
+        with self.assertRaises(pyrtl.PyrtlError):
+            r = pyrtl.Register(2, reset_value="-4'd1")
+
+    def test_extending_negative_reset_value_as_string(self):
+        r = pyrtl.Register(4, reset_value="-3'd3")
+        self.assertEqual(
+            pyrtl.helperfuncs.val_to_signed_integer(r.reset_value, r.bitwidth),
+            -3
+        )
+
     def test_invalid_reset_value_not_an_integer(self):
         with self.assertRaises(pyrtl.PyrtlError):
             r = pyrtl.Register(4, reset_value='hello')
@@ -345,7 +370,6 @@ def test_bad_string(self):
         self.assert_bad_const("5'b111111'")
         self.assert_bad_const("'")
         self.assert_bad_const("'1")
-        self.assert_bad_const("2'b01", bitwidth=3)
         self.assert_bad_const("1'")
 
     def test_bool(self):
@@ -463,6 +487,7 @@ def test_ops(self):
 
                 self.assertEqual(hash(a), hash(wrapped_a))
                 self.assertEqual(str(a), str(wrapped_a))
+                self.assertEqual(repr(a), repr(wrapped_a))
                 self.assertEqual(len(a), len(wrapped_a))
 
                 a_and_b = pyrtl.wire.WireVector(name='a_and_b', bitwidth=4)