MC logo

Subassemblies

^  Ruby Example Code

<<Circuit Test III cgrp.rb One-Bit Adder Subassembly>>
require "csim"

# This represents a compound of gates.  It has an interface similar enough
# to Gate to be used in place of one, though it isn't derived from Gate.
# Ruby thinks this is just fine.

# To build compound circuits, you must create a Blueprint.  To do so:
#   1. Create the Blueprint object.
#   2. Create the objects, and connect them to each other.  Don't make
#      any external connections.
#   3. As needed specify gates you create as input or output devices for
#      the compund.
#   4. Call lock().  This completes the Bluprint and makes it ready to
#      generate compounds.
# The resulting Blueprint is also a Compound.  It can be connected and used
# like a Gate (though it is not derived from Gate).  The another method
# of Blueprint creates a Compound, which can be used in a circuit, but
# lacks the building infrastructure.  The Compound remembers its Blueprint,
# and its another method simply calls the one in Blueprint.
#
# You should not add the same component to multiple compounds.  

class Compound
protected
  # This is called only by Blueprint.  Clients don't get to create
  # Compounds themselves.
  def initialize(ingates, outgates, blueprint)
    @ingates = ingates          # Initially array of input gates, replaced
                                # on each connect with a connection to it.
    @conndex = -1               # Index for connecting to it.
    @outgates = outgates        # Array of output gates.
    @joindex = -1               # Index for joining.
    @blueprint = blueprint      # Our blueprint object.
  end

public
  # Connect to a specific input port.
  def portconn(port, v)
    c = @ingates[port]
    c.signal(v)
    return Gate::LinkHandle.new(self,port)
  end

  # Connect to the next input port.
  def connect(v)
    return portconn(@conndex += 1, v)
  end

  # Join an output to another device.  Outputs are joined in the order 
  # specified by outputs, or out(n) may be used to join to a specific output.
  def out(n)
    return @outgates[n]
  end
  def join(gate)
    @outgates[@joindex += 1].join(gate)
  end

  # Handle inbound signals.
  def signal(port, val)
    Gate.activate
    @ingates[port].signal(val)
    Gate.deactivate
  end

  # All the links on the output list, except for internal connections.
  def outlinks
    ret = [ ]
    for i in (0...@outgates.length)
      outs = @outgates[i].outlinks
      for j in (0...outs.length) 
        ret.push(outs[j]) if outs[j] && @blueprint.internmap[i][j] != 1
      end
    end
    return ret
  end

  # This creates another one of us.  This runs in the blueprint, which
  # has more information.
  def another
    return @blueprint.another
  end

  # Create several others of us in an array.
  def manyothers(n)
    ret = []
    n.times { ret.push(self.another) }
    return ret
  end

  # For prettier printing.
  def to_s
    ret = self.class.to_s
    st = @blueprint.subtype
    ret += " [" + st + "]" if st
    return ret + " " + self.object_id.to_s
  end

end

# Class Blueprint is used to describe a component made of other components.
class Blueprint < Compound
  # Note: Initialization is not really complete until the lock method is 
  # called.
  def initialize(subtype = nil)
    super([], [], self)
    @allgates = [ ]     # This is a list of connections to internal gates
                        #  which represent the device inputs.
    @allcons = [ ]      # List of [src, sink] in proper connect order.
    @internmap = [ ]    # For each output gate, a mask of connections which
                        #  are internal.
    @subtype = subtype
  end

  # Specify gates as inputs to the circuits.  You may specify any number of
  # Gates or Compounds, or arrays thereof.  Each input makes a connection to
  # the specified gate in the order given; gates may be repeated when the
  # they form more than one input.  If input order matters, be careful to
  # mix the specification of gates as inputs, and the joining of internal
  # connections, in the correct order.
  def inputs(*gates)
    for g in flatten(gates)
      @ingates.push(g.connect(false))
    end
  end

  # This specifies some gates which are outputs.  Gates, Compounds, or arrays
  # thereof may be specified.  Output connections are made to these gates
  # in the order given.  Gates may be repeated to supply multiple ouputs.
  # It is also possible to use a specific output connection number to
  # join multiple devices to the same port.
  def outputs(*gates)
    @outgates += flatten(gates)
  end

  # This closes the definition.  It finds all the objects in the collection,
  # and makes a list of pairs that can be used to reproduce all the connections
  # preserving relative order at each end.  This involves a topological sort.
  # Yecccch.
  def lock
    # Create the initial pending list of all input gates, plus all the
    # output gates (which should be redundant, but ...)
    gset = { }
    ((@ingates.map { |c| c.sinkg }) + @outgates).each { |g| gset[g] = true }
    pend = gset.keys

    # Process pending gates for outlinks.  Find all devices which are
    # connected downstream from an input or output.
    while g = pend.shift
      # Scan the receiving gates.  We take the list of outbound connections
      # and extract the sink gates therein, and run through that.
      for t in g.outlinks.map { |lnk| lnk.sinkg }
        if ! gset.has_key?(t) then
          # If not already in the set, add it to the set and the pending list.
          pend.push(t)
          gset[t] = true
        end
      end
    end

    # These are all the reachable gates; all the gates in the device.
    @allgates = gset.keys

    # Allocate graph nodes for each connection.  This allocates a node for
    # each connection between two contained gates.  Each node is an array
    # of the form
    #   [ srcnext, sinknext, predcnt, src, sink ]
    # Where the first two point to nodes that come later in the order at
    # the source or destination (respectively), predcnt is an integer number
    # of nodes which must come before this one, and src and sink are the
    # gates at the start and end of the connection.  This loop allocates
    # such nodes for each connection, adds links (only) for the source
    # ordering, and places them in a hash so we can find them by destination.
    nmap = { }
    n = 0
    for g in @allgates
      lnode = nil       # Previous node in source ordering.
      for c in g.outlinks
        # Create the node and store it in the hash.
        key = [ c.sinkg, c.sinkp ]
        node = [ nil, nil, (if lnode then 1 else 0 end), g, c.sinkg ]
        nmap[key] = node

        # Add a link here to the previous node.
        lnode[0] = node if lnode

        lnode = node
      end
      n += 1
    end

    # This adds more nodes to represent the array of input links.  These
    # nodes are like the above, except source position is nil.
    lnode = nil
    for c in @ingates do
        key = [ c.sinkg, c.sinkp ]
        node = [ nil, nil, (if lnode then 1 else 0 end), nil, c.sinkg ]
        nmap[key] = node
        lnode[0] = node if lnode
        lnode = node
    end

    # Now we go through and add links representing the ordering at the
    # destination.
    for k in nmap.keys          # Scan all node keys.
      gate, port = k            # Get the destination information
      targ = [gate, port+1]     # Get key of next node in destination order
      if nmap.has_key?(targ)    # See if such a node exists.
        tnode = nmap[targ]      # Get the sink successor from the hash.
        tnode[2] += 1           # Increment its pred. count for source node.
        nmap[k][1] = tnode      # Add a link from source to sink node.
      end
    end

    # Find all the roots (nodes without predecessors).  This is the initial
    # value of links which may be established, since all links which must
    # appear earlier have been created.
    ready = nmap.values.select { |n| n[2] == 0 }

    # Traverse the graph obeyong all the constratints and produce a list
    # of connections in an order which will preserve the order at each end.
    # That preserves the creation order of the links in case it matters.
    @allcons = [ ]
    while n = ready.shift
      # Extract the contents of the ready node and add the relevant information
      # to the output order list.
      srcnext, sinknext, count, source, sink, sinkp = n
      @allcons.push([source, sink])

      # Reduce the count of each successor, and add it each to the ready list 
      # if it has no more predecessors.
      if srcnext then
        srcnext[2] -= 1;
        ready.push(srcnext) if srcnext[2] == 0
      end
      if sinknext then
        sinknext[2] -= 1;
        ready.push(sinknext) if sinknext[2] == 0
      end
    end

    # This makes a record of all internal links outbound from output
    # devices.  These are not to be reported as connections out from the
    # compound.  They are recorded in an array parallel to @outgates.
    # For each output gate, they contain a bitmap showing ones for each
    # position occupied at this time.
    @internmap = @outgates.map do |g|
      ret = 0
      bit = 1
      g.outlinks.each { |c| if c then ret &= bit end; bit <<= 1; }
      ret
    end

    # Whew!
  end

  # Make a Compound like this one.
  def another
    # Make copies of all the objects, and keep a map from the original to the
    # copy, so we can copy the connections.
    copymap = { }
    @allgates.each { |g| copymap[g] = g.another }

    # Reproduce all the connections on the new gates.  Use the order computed
    # by close which preserves order of connections at each end.
    ingates = [ ]
    for c in @allcons
      if c[0] then
        copymap[c[0]].join(copymap[c[1]])
      else
        # nil source indicates an input connection
        ingates.push(copymap[c[1]].connect(false))
      end
    end

    # Construct the new compound using the new gates.
    return Compound.new(ingates, @outgates.map { |g| copymap[g] }, self)
  end

  attr_reader :internmap, :subtype

private
  # This flattens an list by opening component arrays.  
  def flatten(a)
    ret = []
    a.each { |x| if x.is_a?(Array) then ret += x else ret.push(x) end }
    return ret
  end
end

<<Circuit Test III One-Bit Adder Subassembly>>