Building on the boiler plate definition adding two operators
* `datapath.compress` - compressor tree circuit
* `datapath.partial_product` - partial product generation circuit
The key idea is to view datapath operators as generators of circuits that satisfy some contract, for example in the case of the `datapath.compress` summing it's results is equivalent to summing it's inputs. This allows us to defer implementing these critical circuits until later in the synthesis flow.
In a simple example, we can fold a*b+c using the datapath dialect to remove a carry-propagate adder:
```mlir
%0 = comb.mul %a, %b : i4
%1 = comb.add %0, %c : i4
```
Which is equivalent to:
```mlir
%0:4 = datapath.partial_product %a, %b : (i4, i4) -> (i4, i4, i4, i4)
%1:2 = datapath.compress %0#0, %0#1, %0#2, %0#3, %c : i4 [5 -> 2]
%2 = comb.add %1#0, %1#1 : i4
```