def src_many(comp, name, bits=None): if bits is None: bits = 16 conn0 = ic.wires[Connection(comp, name, 0)] src_conns = [(i, ic.wires.get(Connection(comp, name, i))) for i in range(bits)] if all((c.comp == conn0.comp and c.name == conn0.name and c.bit == bit) for (bit, c) in src_conns): conn = conn0 if isinstance(conn.comp, Const): return conn.comp.value if conn.comp == root: return f"self._{conn.name}" elif conn.comp.label == "Register": return f"_{all_comps.index(conn.comp)}_reg" elif inlinable(conn.comp): expr = component_expr(conn.comp) if expr: return f"({expr})" return f"_{all_comps.index(conn.comp)}_{conn.name}" # but it's always "out"? else: return "extend_sign(" + " | ".join( f"(bool({src_one(comp, name, i)}) << {i})" for i in range(bits)) + ")"
def Or(): ic = IC("Or", {"a": 1, "b": 1}, {"out": 1}) not_a = Not() not_b = Not() nand = Nand() ic.wire(Connection(root, "a", 0), Connection(not_a, "in_", 0)) ic.wire(Connection(root, "b", 0), Connection(not_b, "in_", 0)) ic.wire(Connection(not_a, "out", 0), Connection(nand, "a", 0)) ic.wire(Connection(not_b, "out", 0), Connection(nand, "b", 0)) ic.wire(Connection(nand, "out", 0), Connection(root, "out", 0)) return ic
def test_back_edges_goofy(): ic = IC("Nonsense", {"reset": 1}, {"out": 1}) nand1 = Nand() nand2 = Nand() ic.wire(Connection(root, "reset", 0), Connection(nand1, "a", 0)) ic.wire(Connection(nand2, "out", 0), Connection(nand1, "b", 0)) # back-edge here ic.wire(Connection(nand1, "out", 0), Connection(nand2, "a", 0)) ic.wire(Connection(nand1, "out", 0), Connection(nand2, "b", 0)) ic.wire(Connection(nand2, "out", 0), Connection(root, "out", 0)) nv, _ = synthesize(ic) assert nv.non_back_edge_mask == 0b011 # i.e. not nand2, yes nand1 and reset
def _constr(chip): """Construct an IC. If the Chip wraps a Component, a trivial IC is constructed around it so we can treat everything the same. """ comp = chip.constr() if isinstance(comp, IC): ic = comp else: ic = IC(comp.label, comp.inputs(), comp.outputs()) for name, bits in comp.inputs().items(): for i in range(bits): ic.wire(Connection(root, name, i), Connection(comp, name, i)) for name, bits in comp.outputs().items(): for i in range(bits): ic.wire(Connection(comp, name, i), Connection(root, name, i)) return ic
def test_back_edges_none(): ic = IC("Nonsense", {"reset": 1}, {"out": 1}) nand1 = Nand() dff = DFF() ic.wire(Connection(root, "reset", 0), Connection(nand1, "a", 0)) ic.wire(Connection(dff, "out", 0), Connection(nand1, "b", 0)) # not a back-edge, because it's a latched output ic.wire(Connection(nand1, "out", 0), Connection(dff, "in_", 0)) ic.wire(Connection(dff, "out", 0), Connection(root, "out", 0)) nv, _ = synthesize(ic) assert nv.non_back_edge_mask == 0b111 # i.e. every bit, which is one Nand, one DFF, and reset
def test_simplify_constant_zero(): ic = IC("Perverse1", {"in": 1}, {"out": 1}) nand1 = Nand() ic.wire(Connection(root, "in", 0), Connection(nand1, "a", 0)) ic.wire(Connection(Const(1, 0), "out", 0), Connection(nand1, "b", 0)) ic.wire(Connection(nand1, "out", 0), Connection(root, "out", 0)) simple = simplify(ic) assert simple.wires == { Connection(root, "out", 0): Connection(Const(1, 1), "out", 0) }
def Not(): ic = IC("Not", {"in_": 1}, {"out": 1}) nand = Nand() ic.wire(Connection(root, "in_", 0), Connection(nand, "a", 0)) ic.wire(Connection(root, "in_", 0), Connection(nand, "b", 0)) ic.wire(Connection(nand, "out", 0), Connection(root, "out", 0)) return ic
def src_one(comp, name, bit=0): conn = ic.wires[Connection(comp, name, bit)] # TODO: deal with lots of cases if isinstance(conn.comp, Const): value = conn.comp.value elif conn.comp == root: value = f"self._{conn.name}" elif inlinable(conn.comp): expr = component_expr(conn.comp) if expr: value = f"({expr})" elif conn.comp.label == "DFF": value = f"_{all_comps.index(conn.comp)}_dff" elif conn.comp.label == "Register": value = f"_{all_comps.index(conn.comp)}_reg" else: value = f"_{all_comps.index(conn.comp)}_{conn.name}" elif conn.comp.label == "DFF": value = f"_{all_comps.index(conn.comp)}_dff" elif conn.comp.label == "Register": value = f"_{all_comps.index(conn.comp)}_reg" elif conn.comp.label == "MemorySystem" and conn.name == "tty_ready": # Tricky: this seems pretty bogus. MemorySystem is the first primitive # which has more than one output, and it can be computed from the # special state. value = f"self._tty == 0" else: value = f"_{all_comps.index(conn.comp)}_{conn.name}" if conn.bit != 0 or any( c.comp == conn.comp and c.name == conn.name and c.bit != 0 for c in ic.wires.values()): # Note: assuming the value is used as a condition and not actually comparing with 0 # saves ~5%. But could be dangerous? return f"({value} & {hex(1 << conn.bit)})" elif conn.comp.label == "Register": raise Exception("TODO: unexpected wiring for 1-bit component") else: return value
def test_simple_synthesis(): ic = IC("JustNand", {"a": 1, "b": 1}, {"out": 1}) nand = Nand() ic.wire(Connection(root, "a", 0), Connection(nand, "a", 0)) ic.wire(Connection(root, "b", 0), Connection(nand, "b", 0)) ic.wire(Connection(nand, "out", 0), Connection(root, "out", 0)) nv, _ = synthesize(ic) assert nv.get(("out", 0)) == True nv.set(("a", 0), True) assert nv.get(("out", 0)) == True nv.set(("b", 0), True) assert nv.get(("out", 0)) == False nv.set(("a", 0), False) assert nv.get(("out", 0)) == True
def constr(): # Tricky: inputs/outputs aren't known yet, but need the IC to be initialized so we can refer # to it via an Instance ic = IC(comp_name, {}, {}) inst = Instance(ic, {}) input_coll = InputCollector(inst) output_coll = OutputCollector(inst) builder(input_coll, output_coll) # Now set up inputs and outputs, before doing any wiring: input_name_bit = [] for inst in instances([ref.inst for ref in output_coll.dict.values()]): for name, ref in inst.args.items(): if ref.inst != common and ref.inst._ic == ic: if ref.bit is not None: input_name_bit.append((ref.name, ref.bit)) else: input_name_bit.append( (ref.name, inst._ic.inputs()[name] - 1)) for (name, bit), ref in sorted(list(output_coll.dict.items())): if ref.inst != common and ref.inst._ic == ic: # FIXME: dup from above if ref.bit is not None: input_name_bit.append((ref.name, ref.bit)) else: # Note: we don't infer bit widths from outside, so just assume bit 0 when # an input is copied directly to an output. input_name_bit.append((ref.name, 0)) ic._inputs = {name: bit + 1 for (name, bit) in sorted(input_name_bit)} output_name_bit = [] for (name, bit), ref in output_coll.dict.items(): if bit is not None: max_bit = bit elif ref == clock: max_bit = 0 elif ref.inst._ic == ic: # Note: we don't infer bit widths from outside, so just assume bit 0 when # an input is copied directly to an output. max_bit = 0 elif ref.bit is not None: # referencing a particular bit makes this a single-bit max_bit = 0 else: max_bit = ref.inst._ic.outputs()[ref.name] - 1 output_name_bit.append((name, max_bit)) ic._outputs = { name: bit + 1 for (name, bit) in sorted(output_name_bit) } for (name, bit), ref in output_coll.dict.items(): if ref.inst == common: from_comp = common # HACK from_outputs = common.outputs() elif ref.inst._ic == ic: from_comp = root from_outputs = ic.inputs() else: from_comp = ref.inst._ic from_outputs = from_comp.outputs() if bit is None and ref.bit is None: for i in range(from_outputs[ref.name]): ic.wire(Connection(from_comp, ref.name, i), Connection(root, name, i)) else: ic.wire(Connection(from_comp, ref.name, ref.bit or 0), Connection(root, name, bit or 0)) for inst in instances([ref.inst for ref in output_coll.dict.values()]): for name, ref in inst.args.items(): if ref.inst == common: source_comp = common # HACK elif ref.inst._ic == ic: source_comp = root else: source_comp = ref.inst._ic if ref.bit is None: target_bits = inst._ic.inputs()[name] for i in range(target_bits): source = Connection(source_comp, ref.name, i) target = Connection(inst._ic, name, i) ic.wire(source, target) else: source = Connection(source_comp, ref.name, ref.bit) target = Connection(inst._ic, name, 0) ic.wire(source, target) # TODO: check for any un-wired or mis-wired inputs (and outputs?) return ic
def test_simplify_duplicate(): ic = IC("2Nands", {"in1": 1, "in2": 1}, {"out1": 1, "out2": 1}) nand1 = Nand() nand2 = Nand() ic.wire(Connection(root, "in1", 0), Connection(nand1, "a", 0)) ic.wire(Connection(root, "in2", 0), Connection(nand1, "b", 0)) ic.wire(Connection(root, "in2", 0), Connection(nand2, "a", 0)) ic.wire(Connection(root, "in1", 0), Connection(nand2, "b", 0)) ic.wire(Connection(nand1, "out", 0), Connection(root, "out1", 0)) ic.wire(Connection(nand2, "out", 0), Connection(root, "out2", 0)) simple = simplify(ic) simple_nand = simple.sorted_components()[0] # Note: in1 and in2 connected to a and b randomly assert simple.wires.keys() == set([ Connection(simple_nand, "a", 0), Connection(simple_nand, "b", 0), Connection(root, "out1", 0), Connection(root, "out2", 0), ]) assert simple.wires[Connection(root, "out1", 0)] == Connection(simple_nand, "out", 0) assert simple.wires[Connection(root, "out2", 0)] == Connection(simple_nand, "out", 0)
def test_simplify_constant_one(): """Nand(1, x) == Nand(x, 1) => Nand(x, x). """ ic = IC("WeirdNot", {"in": 1}, {"out": 1}) nand1 = Nand() ic.wire(Connection(root, "in", 0), Connection(nand1, "a", 0)) ic.wire(Connection(Const(1, 1), "out", 0), Connection(nand1, "b", 0)) ic.wire(Connection(nand1, "out", 0), Connection(root, "out", 0)) simple = simplify(ic) simple_nand = simple.sorted_components()[0] assert simple.wires == { Connection(simple_nand, "a", 0): Connection(root, "in", 0), Connection(simple_nand, "b", 0): Connection(root, "in", 0), Connection(root, "out", 0): Connection(simple_nand, "out", 0), }
def test_simplify_double_negative(): ic = IC("PerverseBuffer", {"in": 1}, {"out": 1}) nand1 = Nand() nand2 = Nand() ic.wire(Connection(root, "in", 0), Connection(nand1, "a", 0)) ic.wire(Connection(root, "in", 0), Connection(nand1, "b", 0)) ic.wire(Connection(nand1, "out", 0), Connection(nand2, "a", 0)) ic.wire(Connection(nand1, "out", 0), Connection(nand2, "b", 0)) ic.wire(Connection(nand2, "out", 0), Connection(root, "out", 0)) simple = simplify(ic) assert simple.wires == { Connection(root, "out", 0): Connection(root, "in", 0) }
def test_and3(): """A simple component that's definitely not handled as a primitive.""" ic = IC("And3", {"a": 1, "b": 1, "c": 1}, {"out": 1}) nand1 = Nand() ic.wire(Connection(root, "a", 0), Connection(nand1, "a", 0)) ic.wire(Connection(root, "b", 0), Connection(nand1, "b", 0)) nand2 = Nand() ic.wire(Connection(nand1, "out", 0), Connection(nand2, "a", 0)) ic.wire(Connection(nand1, "out", 0), Connection(nand2, "b", 0)) nand3 = Nand() ic.wire(Connection(nand2, "out", 0), Connection(nand3, "a", 0)) ic.wire(Connection(root, "c", 0), Connection(nand3, "b", 0)) nand4 = Nand() ic.wire(Connection(nand3, "out", 0), Connection(nand4, "a", 0)) ic.wire(Connection(nand3, "out", 0), Connection(nand4, "b", 0)) ic.wire(Connection(nand4, "out", 0), Connection(root, "out", 0)) and3 = run(ic) assert and3.out == False for i in range(8): a, b, c = [bool(i & (1 << j)) for j in range(3)] and3.a = a and3.b = b and3.c = c assert and3.out == (a and b and c)
def simplify(orig): """Construct a new chip which is logically identical to this one, but may be smaller and more efficient by the removal of certain recognized patterns. More effective after flatten(). When a constant is the input of a Nand, that gate is replaced with either a constant or the other input: Nand(a, 0) = 1; Nand(a, 1) = Not(a) When a series of two Nands negate and then re-negate the same single value, the second Nand is always removed, and the first may be as well if it's otherwise unused. When more than one Nand has the same two inputs, each such set is replaced with a single Nand. """ ic = orig.copy() def const_value(conn): if isinstance(conn.comp, Const): return conn.comp.value & (1 << conn.bit) != 0 else: return None def rewrite(old_conn, new_conn): for t, f in list(ic.wires.items()): if f == old_conn: ic.wires[t] = new_conn done = False while not done: done = True for comp in set([c.comp for c in ic.wires.keys()] + [c.comp for c in ic.wires.values()]): if isinstance(comp, Nand): a_src = ic.wires[Connection(comp, "a", 0)] b_src = ic.wires[Connection(comp, "b", 0)] a_val = const_value(a_src) b_val = const_value(b_src) if a_val == False or b_val == False: # Remove this Nand and rewrite its output as Const(1): del ic.wires[Connection(comp, "a", 0)] del ic.wires[Connection(comp, "b", 0)] old_conn = Connection(comp, "out", 0) new_conn = Connection(Const(1, 1), "out", 0) rewrite(old_conn, new_conn) done = False elif a_val == True and b_val == True: # Remove this Nand and rewrite its output as Const(0): del ic.wires[Connection(comp, "a", 0)] del ic.wires[Connection(comp, "b", 0)] old_conn = Connection(comp, "out", 0) new_conn = Connection(Const(1, 0), "out", 0) rewrite(old_conn, new_conn) done = False elif a_val == True: # Rewite to eliminate the Const: ic.wires[Connection(comp, "a", 0)] = b_src done = False elif b_val == True: # Rewite to eliminate the Const: ic.wires[Connection(comp, "b", 0)] = a_src done = False elif a_src == b_src: if isinstance(a_src.comp, Nand): src_a_src = ic.wires[Connection(a_src.comp, "a", 0)] src_b_src = ic.wires[Connection(a_src.comp, "b", 0)] if src_a_src == src_b_src: # Remove this Nand and rewrite its output as src's src: del ic.wires[Connection(comp, "a", 0)] del ic.wires[Connection(comp, "b", 0)] old_conn = Connection(comp, "out", 0) new_conn = src_a_src rewrite(old_conn, new_conn) # TODO: remove a_src.comp if not referenced? done = False # Construct the sort function once, since it has to search the graph: by_component = ic._connections_sort_key() # Find and collapse sets of Nands with the same inputs: nands_by_input_pair = {} for conn in list(ic.wires): comp = conn.comp if isinstance(comp, Nand): t = tuple( sorted([ ic.wires[Connection(comp, "a", 0)], ic.wires[Connection(comp, "b", 0)] ], key=by_component)) nands_by_input_pair.setdefault(t, set()).add(comp) for _, nands_set in nands_by_input_pair.items(): if len(nands_set) > 1: nands = list(nands_set) keep_nand = nands[0] for n in nands[1:]: del ic.wires[Connection(n, "a", 0)] del ic.wires[Connection(n, "b", 0)] rewrite(Connection(n, "out", 0), Connection(keep_nand, "out", 0)) done = False return ic.flatten() # HACK: a cheap way to remove dangling wires
def synthesize(ic): """Compile the chip down to traces and ops for evaluation. Returns a NandVector and a list of custom components (e.g. RAMs; anything other than Nand, DFF, and Const.) """ ic = ic.flatten() # TODO: check for missing wires? # TODO: check for unused components? any_clock_references = any( [True for conn in ic.wires.values() if conn == clock]) # Assign a bit for each output connection: all_bits = {} next_bit = 0 if any_clock_references: all_bits[clock] = next_bit next_bit += 1 for conn in sorted(set(ic.wires.values()), key=ic._connections_sort_key()): if conn != clock: all_bits[conn] = next_bit next_bit += 1 # Construct map of IC inputs, directly from all_bits: inputs = { (name, bit): 1 << all_bits[Connection(root, name, bit)] for name, bits in ic._inputs.items() for bit in range(bits) if Connection(root, name, bit) in all_bits # Not all input bits are necessarily connected. } if any_clock_references: inputs[("common.clock", 0)] = 1 << all_bits[clock] # Construct map of IC ouputs, mapped to all_bits via wires: outputs = {(name, bit): 1 << all_bits[ic.wires[Connection(root, name, bit)]] for name, bits in ic._outputs.items() for bit in range(bits)} internal = {} # TODO sorted_comps = ic.sorted_components() # For each component, construct a map of its traces' bit masks, and ask the component for its ops: initialize_ops = [] combine_ops = [] sequence_ops = [] stateful = [] for comp in sorted_comps: traces = {} for name, bits in comp.inputs().items(): traces[name] = [ 1 << all_bits[ic.wires[Connection(comp, name, bit)]] for bit in range(bits) ] for name, bits in comp.outputs().items(): traces[name] = [ 1 << all_bits[Connection(comp, name, bit)] for bit in range(bits) ] ops = component_ops(comp) init_ops = ops.initialize(**traces) comb_ops = ops.combine(**traces) seq_ops = ops.sequence(**traces) initialize_ops += init_ops combine_ops += comb_ops sequence_ops += seq_ops if not isinstance(ops, (NandOps, ConstOps, DFFOps)): stateful.append(ops) back_edge_from_components = set() for to_input, from_output in ic.wires.items(): if (not isinstance(from_output.comp, Const) and from_output.comp in sorted_comps and to_input.comp in sorted_comps and not isinstance(from_output.comp, DFF) and sorted_comps.index(from_output.comp) > sorted_comps.index( to_input.comp)): back_edge_from_components.add(from_output.comp) non_back_edge_mask = 0 for conn, bit in all_bits.items(): if conn.comp not in back_edge_from_components: non_back_edge_mask |= 1 << bit return (NandVector(inputs, outputs, internal, initialize_ops, combine_ops, sequence_ops, non_back_edge_mask), stateful)